feat(agent): add skill inspect API (#37726)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123 2026-06-22 13:22:10 +08:00 committed by GitHub
parent 4f61353dc2
commit 34c1bf1062
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1579 additions and 186 deletions

View File

@ -10,8 +10,12 @@ backend — drive data lives in the API's own DB/storage, served straight from
from __future__ import annotations
import json
from collections.abc import Mapping
from typing import Any
from uuid import UUID
from flask import Response
from flask_restx import Resource
from pydantic import BaseModel, Field
@ -49,6 +53,10 @@ class AgentDriveFileByAgentQuery(BaseModel):
key: str = Field(min_length=1, description="Drive key, e.g. tender-analyzer/SKILL.md")
class AgentDriveSkillInspectQuery(BaseModel):
node_id: str | None = Field(default=None, description="Workflow node ID (workflow composer variant)")
class AgentDriveItemResponse(ResponseModel):
key: str
size: int | None = None
@ -56,12 +64,63 @@ class AgentDriveItemResponse(ResponseModel):
hash: str | None = None
file_kind: str
created_at: int | None = None
is_skill: bool | None = None
skill_metadata: str | None = None
class AgentDriveListResponse(ResponseModel):
items: list[AgentDriveItemResponse] = Field(default_factory=list)
class AgentDriveSkillItemResponse(ResponseModel):
path: str
skill_md_key: str
archive_key: str | None = None
name: str
description: str
size: int | None = None
mime_type: str | None = None
hash: str | None = None
created_at: int | None = None
class AgentDriveSkillListResponse(ResponseModel):
items: list[AgentDriveSkillItemResponse] = Field(default_factory=list)
class AgentDriveSkillFileResponse(ResponseModel):
path: str
name: str
type: str
drive_key: str | None = None
available_in_drive: bool
class AgentDriveSkillMarkdownResponse(ResponseModel):
key: str
size: int | None = None
truncated: bool
binary: bool
text: str | None = None
class AgentDriveSkillInspectResponse(ResponseModel):
path: str
skill_md_key: str
archive_key: str | None = None
name: str
description: str
size: int | None = None
mime_type: str | None = None
hash: str | None = None
created_at: int | None = None
source: str
files: list[AgentDriveSkillFileResponse] = Field(default_factory=list)
file_tree: list[dict[str, Any]] = Field(default_factory=list)
skill_md: AgentDriveSkillMarkdownResponse
warnings: list[str] = Field(default_factory=list)
class AgentDrivePreviewResponse(ResponseModel):
key: str
size: int | None = None
@ -75,7 +134,12 @@ class AgentDriveDownloadResponse(ResponseModel):
register_response_schema_models(
console_ns, AgentDriveListResponse, AgentDrivePreviewResponse, AgentDriveDownloadResponse
console_ns,
AgentDriveDownloadResponse,
AgentDriveListResponse,
AgentDrivePreviewResponse,
AgentDriveSkillInspectResponse,
AgentDriveSkillListResponse,
)
@ -96,6 +160,13 @@ def _handle(exc: AgentDriveError) -> tuple[dict[str, object], int]:
return {"code": exc.code, "message": exc.message}, exc.status_code
def _json_response(data: Mapping[str, Any]):
return Response(
response=json.dumps(data, ensure_ascii=False, separators=(",", ":")),
content_type="application/json; charset=utf-8",
)
_WORKFLOW_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]
@ -119,6 +190,49 @@ class AgentDriveListByAgentApi(Resource):
return {"items": [{k: v for k, v in item.items() if k != "file_id"} for item in items]}
@console_ns.route("/agent/<uuid:agent_id>/drive/skills")
class AgentDriveSkillListByAgentApi(Resource):
@console_ns.doc("list_agent_drive_skills_by_agent")
@console_ns.doc(description="List drive-backed skills for an Agent App")
@console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.response(200, "Drive skills", console_ns.models[AgentDriveSkillListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID):
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=str(agent_id))
except AgentDriveError as exc:
return _handle(exc)
return {"items": items}
@console_ns.route("/agent/<uuid:agent_id>/drive/skills/<path:skill_path>/inspect")
class AgentDriveSkillInspectByAgentApi(Resource):
@console_ns.doc("inspect_agent_drive_skill_by_agent")
@console_ns.doc(description="Inspect one drive-backed skill for slash-menu hover/detail UI")
@console_ns.doc(params={"agent_id": "Agent ID", "skill_path": "Skill path/slug, e.g. tender-analyzer"})
@console_ns.response(200, "Drive skill inspect view", console_ns.models[AgentDriveSkillInspectResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID, skill_path: str):
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
return _json_response(
AgentDriveService().inspect_skill(
tenant_id=tenant_id,
agent_id=str(agent_id),
skill_path=skill_path,
)
)
except AgentDriveError as exc:
return _handle(exc)
@console_ns.route("/agent/<uuid:agent_id>/drive/files/preview")
class AgentDrivePreviewByAgentApi(Resource):
@console_ns.doc("preview_agent_drive_file_by_agent")
@ -182,6 +296,61 @@ class AgentDriveListApi(Resource):
return {"items": [{k: v for k, v in item.items() if k != "file_id"} for item in items]}
@console_ns.route("/apps/<uuid:app_id>/agent/drive/skills")
class AgentDriveSkillListApi(Resource):
@console_ns.doc("list_agent_drive_skills")
@console_ns.doc(description="List drive-backed skills for the bound agent")
@console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentDriveListQuery)})
@console_ns.response(200, "Drive skills", console_ns.models[AgentDriveSkillListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=_WORKFLOW_APP_MODES)
def get(self, app_model: App):
query = query_params_from_request(AgentDriveListQuery)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
try:
items = AgentDriveService().list_skills(tenant_id=app_model.tenant_id, agent_id=agent_id)
except AgentDriveError as exc:
return _handle(exc)
return {"items": items}
@console_ns.route("/apps/<uuid:app_id>/agent/drive/skills/<path:skill_path>/inspect")
class AgentDriveSkillInspectApi(Resource):
@console_ns.doc("inspect_agent_drive_skill")
@console_ns.doc(description="Inspect one drive-backed skill for slash-menu hover/detail UI")
@console_ns.doc(
params={
"app_id": "Application ID",
"skill_path": "Skill path/slug, e.g. tender-analyzer",
**query_params_from_model(AgentDriveSkillInspectQuery),
}
)
@console_ns.response(200, "Drive skill inspect view", console_ns.models[AgentDriveSkillInspectResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=_WORKFLOW_APP_MODES)
def get(self, app_model: App, skill_path: str):
query = query_params_from_request(AgentDriveSkillInspectQuery)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
try:
return _json_response(
AgentDriveService().inspect_skill(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
skill_path=skill_path,
)
)
except AgentDriveError as exc:
return _handle(exc)
@console_ns.route("/apps/<uuid:app_id>/agent/drive/files/preview")
class AgentDrivePreviewApi(Resource):
@console_ns.doc("preview_agent_drive_file")
@ -232,4 +401,8 @@ __all__ = [
"AgentDriveListByAgentApi",
"AgentDrivePreviewApi",
"AgentDrivePreviewByAgentApi",
"AgentDriveSkillInspectApi",
"AgentDriveSkillInspectByAgentApi",
"AgentDriveSkillListApi",
"AgentDriveSkillListByAgentApi",
]

View File

@ -0,0 +1,39 @@
"""agent drive skill metadata refactor
Revision ID: b2515f9d4c2a
Revises: 4f7b2c8d9a10
Create Date: 2026-06-18 23:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = "b2515f9d4c2a"
down_revision = "4f7b2c8d9a10"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"agent_drive_files",
sa.Column("is_skill", sa.Boolean(), nullable=False, server_default=sa.text("false")),
)
op.add_column(
"agent_drive_files",
sa.Column("skill_metadata", sa.Text().with_variant(mysql.LONGTEXT(), "mysql"), nullable=True),
)
op.create_index(
"agent_drive_files_tenant_agent_is_skill_key_idx",
"agent_drive_files",
["tenant_id", "agent_id", "is_skill", "key"],
)
def downgrade() -> None:
op.drop_index("agent_drive_files_tenant_agent_is_skill_key_idx", table_name="agent_drive_files")
op.drop_column("agent_drive_files", "skill_metadata")
op.drop_column("agent_drive_files", "is_skill")

View File

@ -430,14 +430,17 @@ class AgentDriveFile(DefaultFieldsMixin, Base):
synced. ``value_owned_by_drive`` gates physical cleanup: only drive-owned values
(created by the agent runtime or Skill standardization, not shared with other
business records) have their storage object + record deleted when the KV entry is
overwritten or removed; otherwise only the KV row is dropped. Lifecycle never relies
on ``UploadFile.used/used_by`` (not a reliable refcount).
overwritten or removed; otherwise only the KV row is dropped. Skills are represented
by the canonical ``<path>/SKILL.md`` row with ``is_skill=True`` and a serialized
``skill_metadata`` string. Lifecycle never relies on ``UploadFile.used/used_by``
(not a reliable refcount).
"""
__tablename__ = "agent_drive_files"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_drive_file_pkey"),
UniqueConstraint("tenant_id", "agent_id", "key", name="agent_drive_file_scope_key_unique"),
Index("agent_drive_files_tenant_agent_is_skill_key_idx", "tenant_id", "agent_id", "is_skill", "key"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
@ -453,6 +456,8 @@ class AgentDriveFile(DefaultFieldsMixin, Base):
value_owned_by_drive: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, default=False, server_default=sa.text("false")
)
is_skill: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False, server_default=sa.text("false"))
skill_metadata: Mapped[str | None] = mapped_column(LongText, nullable=True)
size: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True)
hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
mime_type: Mapped[str | None] = mapped_column(String(255), nullable=True)

View File

@ -576,6 +576,37 @@ Truncated text preview of one Agent App drive value
| ---- | ----------- | ------ |
| 200 | Preview | **application/json**: [AgentDrivePreviewResponse](#agentdrivepreviewresponse)<br> |
### [GET] /agent/{agent_id}/drive/skills
List drive-backed skills for an Agent App
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string (uuid) |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Drive skills | **application/json**: [AgentDriveSkillListResponse](#agentdriveskilllistresponse)<br> |
### [GET] /agent/{agent_id}/drive/skills/{skill_path}/inspect
Inspect one drive-backed skill for slash-menu hover/detail UI
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string (uuid) |
| skill_path | path | Skill path/slug, e.g. tender-analyzer | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Drive skill inspect view | **application/json**: [AgentDriveSkillInspectResponse](#agentdriveskillinspectresponse)<br> |
### [POST] /agent/{agent_id}/features
Update an Agent App's presentation features (opener, follow-up, citations, ...)
@ -1454,6 +1485,40 @@ Truncated text preview of one drive value (binary-safe; SKILL.md is the main cas
| ---- | ----------- | ------ |
| 200 | Preview | **application/json**: [AgentDrivePreviewResponse](#agentdrivepreviewresponse)<br> |
### [GET] /apps/{app_id}/agent/drive/skills
List drive-backed skills for the bound agent
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string (uuid) |
| node_id | query | Workflow node ID (workflow composer variant) | No | string |
| prefix | query | Key prefix filter: '<slug>/' for one skill, 'files/' for files | No | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Drive skills | **application/json**: [AgentDriveSkillListResponse](#agentdriveskilllistresponse)<br> |
### [GET] /apps/{app_id}/agent/drive/skills/{skill_path}/inspect
Inspect one drive-backed skill for slash-menu hover/detail UI
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string (uuid) |
| skill_path | path | Skill path/slug, e.g. tender-analyzer | Yes | string |
| node_id | query | Workflow node ID (workflow composer variant) | No | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Drive skill inspect view | **application/json**: [AgentDriveSkillInspectResponse](#agentdriveskillinspectresponse)<br> |
### [DELETE] /apps/{app_id}/agent/files
Delete one drive file by key; soul ref first, then the KV row (ENG-625 D5)
@ -12425,9 +12490,11 @@ Audit operation recorded for Agent Soul version/revision changes.
| created_at | integer | | No |
| file_kind | string | | Yes |
| hash | string | | No |
| is_skill | boolean | | No |
| key | string | | Yes |
| mime_type | string | | No |
| size | integer | | No |
| skill_metadata | string | | No |
#### AgentDriveListResponse
@ -12445,6 +12512,65 @@ Audit operation recorded for Agent Soul version/revision changes.
| text | string | | No |
| truncated | boolean | | Yes |
#### AgentDriveSkillFileResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| available_in_drive | boolean | | Yes |
| drive_key | string | | No |
| name | string | | Yes |
| path | string | | Yes |
| type | string | | Yes |
#### AgentDriveSkillInspectResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| archive_key | string | | No |
| created_at | integer | | No |
| description | string | | Yes |
| file_tree | [ object ] | | No |
| files | [ [AgentDriveSkillFileResponse](#agentdriveskillfileresponse) ] | | No |
| hash | string | | No |
| mime_type | string | | No |
| name | string | | Yes |
| path | string | | Yes |
| size | integer | | No |
| skill_md | [AgentDriveSkillMarkdownResponse](#agentdriveskillmarkdownresponse) | | Yes |
| skill_md_key | string | | Yes |
| source | string | | Yes |
| warnings | [ string ] | | No |
#### AgentDriveSkillItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| archive_key | string | | No |
| created_at | integer | | No |
| description | string | | Yes |
| hash | string | | No |
| mime_type | string | | No |
| name | string | | Yes |
| path | string | | Yes |
| size | integer | | No |
| skill_md_key | string | | Yes |
#### AgentDriveSkillListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| items | [ [AgentDriveSkillItemResponse](#agentdriveskillitemresponse) ] | | No |
#### AgentDriveSkillMarkdownResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| binary | boolean | | Yes |
| key | string | | Yes |
| size | integer | | No |
| text | string | | No |
| truncated | boolean | | Yes |
#### AgentEnvVariableConfig
| Name | Type | Description | Required |

View File

@ -23,7 +23,7 @@ from typing import Any
from core.tools.tool_file_manager import ToolFileManager
from models.agent_config_entities import AgentSkillRefConfig
from services.agent.skill_package_service import SkillPackageService
from services.agent_drive_service import AgentDriveService, DriveCommitItem, DriveFileRef
from services.agent_drive_service import AgentDriveService, DriveCommitItem, DriveFileRef, DriveSkillMetadata
_FULL_ARCHIVE_NAME = ".DIFY-SKILL-FULL.zip"
_SKILL_MD_NAME = "SKILL.md"
@ -91,6 +91,12 @@ class SkillStandardizeService:
key=skill_md_key,
file_ref=DriveFileRef(kind="tool_file", id=md_tool_file.id),
value_owned_by_drive=True,
is_skill=True,
skill_metadata=DriveSkillMetadata(
name=manifest.name,
description=manifest.description,
manifest_files=manifest.files,
),
),
DriveCommitItem(
key=archive_key,

View File

@ -17,12 +17,14 @@ ToolFile records (see ``AgentDriveFile``). This service is the control plane:
from __future__ import annotations
import json
import logging
import re
import urllib.parse
from typing import Any, Literal
from typing import Any, Literal, TypedDict
from urllib.parse import unquote
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict, field_validator
from sqlalchemy import func, select
from sqlalchemy.exc import DataError, SQLAlchemyError
from sqlalchemy.orm import Session
@ -41,6 +43,8 @@ logger = logging.getLogger(__name__)
_MAX_KEY_LENGTH = 512
_DRIVE_REF_PREFIX = "agent-"
_SKILL_MD_SUFFIX = "/SKILL.md"
_SKILL_ARCHIVE_NAME = ".DIFY-SKILL-FULL.zip"
class AgentDriveError(Exception):
@ -58,16 +62,86 @@ class AgentDriveError(Exception):
class DriveFileRef(BaseModel):
model_config = ConfigDict(extra="forbid")
kind: Literal["upload_file", "tool_file"]
id: str
class DriveSkillMetadata(BaseModel):
"""Validated skill catalog metadata stored as a JSON string on the drive row."""
model_config = ConfigDict(extra="forbid")
name: str
description: str = ""
# Safe archive member paths captured during skill standardization. The drive
# stores only canonical SKILL.md + full archive, so the UI uses this manifest
# to show the original uploaded package contents.
manifest_files: list[str] | None = None
@field_validator("name")
@classmethod
def _validate_name(cls, value: str) -> str:
normalized = value.strip()
if not normalized:
raise ValueError("skill metadata name must not be blank")
return normalized
class DriveCommitItem(BaseModel):
model_config = ConfigDict(extra="forbid")
key: str
file_ref: DriveFileRef
# Drive-owned values may be physically cleaned on overwrite/removal; refs to
# files shared with other business records should set this False.
value_owned_by_drive: bool = True
is_skill: bool = False
skill_metadata: DriveSkillMetadata | None = None
class AgentDriveSkillInfo(TypedDict):
path: str
skill_md_key: str
archive_key: str | None
name: str
description: str
size: int | None
mime_type: str | None
hash: str | None
created_at: int | None
class AgentDriveSkillFileInfo(TypedDict):
path: str
name: str
type: str
drive_key: str | None
available_in_drive: bool
class AgentDriveSkillInspectInfo(TypedDict):
path: str
skill_md_key: str
archive_key: str | None
name: str
description: str
size: int | None
mime_type: str | None
hash: str | None
created_at: int | None
source: str
files: list[AgentDriveSkillFileInfo]
file_tree: list[dict[str, Any]]
skill_md: dict[str, Any]
warnings: list[str]
def decode_drive_mention_ref(ref_id: str) -> str:
"""Decode the prompt token's URL-encoded drive-key field."""
return unquote(ref_id or "")
def parse_agent_drive_ref(drive_ref: str) -> str:
@ -132,6 +206,8 @@ class AgentDriveService:
"mime_type": row.mime_type,
"file_kind": row.file_kind.value,
"file_id": row.file_id,
"is_skill": row.is_skill,
"skill_metadata": row.skill_metadata,
"created_at": int(row.created_at.timestamp()) if row.created_at else None,
}
if include_download_url:
@ -217,6 +293,87 @@ class AgentDriveService:
self._delete_storage(storage_key)
return removed_keys
def list_skills(self, *, tenant_id: str, agent_id: str) -> list[AgentDriveSkillInfo]:
"""Return the drive-backed skill catalog derived from canonical ``SKILL.md`` rows."""
with session_factory.create_session() as session:
self._assert_agent_belongs_to_tenant(session, tenant_id=tenant_id, agent_id=agent_id)
skill_rows = list(
session.scalars(
select(AgentDriveFile)
.where(
AgentDriveFile.tenant_id == tenant_id,
AgentDriveFile.agent_id == agent_id,
AgentDriveFile.is_skill.is_(True),
)
.order_by(AgentDriveFile.key)
)
)
archive_keys = set(
session.scalars(
select(AgentDriveFile.key).where(
AgentDriveFile.tenant_id == tenant_id,
AgentDriveFile.agent_id == agent_id,
AgentDriveFile.key.in_([self._skill_archive_key(row.key) for row in skill_rows]),
)
)
)
skills: list[AgentDriveSkillInfo] = []
for row in skill_rows:
metadata = self._parse_skill_metadata(row.key, row.skill_metadata)
archive_key = self._skill_archive_key(row.key)
skills.append(
{
"path": self._skill_path_from_key(row.key),
"skill_md_key": row.key,
"archive_key": archive_key if archive_key in archive_keys else None,
"name": metadata.name,
"description": metadata.description,
"size": row.size,
"mime_type": row.mime_type,
"hash": row.hash,
"created_at": int(row.created_at.timestamp()) if row.created_at else None,
}
)
return skills
def inspect_skill(self, *, tenant_id: str, agent_id: str, skill_path: str) -> AgentDriveSkillInspectInfo:
"""Return the UI-facing skill inspect view for slash-menu hover/detail."""
skill_path = normalize_drive_key(skill_path)
skill_md_key = skill_path if skill_path.endswith(_SKILL_MD_SUFFIX) else f"{skill_path}{_SKILL_MD_SUFFIX}"
skill_path = self._skill_path_from_key(skill_md_key)
catalog = next(
(item for item in self.list_skills(tenant_id=tenant_id, agent_id=agent_id) if item["path"] == skill_path),
None,
)
if catalog is None:
raise AgentDriveError("skill_not_found", "no drive-backed skill for this path", status_code=404)
manifest_files = self._manifest_files_from_skill_metadata(
tenant_id=tenant_id,
agent_id=agent_id,
skill_md_key=skill_md_key,
)
drive_items = self.manifest(tenant_id=tenant_id, agent_id=agent_id, prefix=f"{skill_path}/")
drive_keys = {item["key"] for item in drive_items}
preview = self.preview(tenant_id=tenant_id, agent_id=agent_id, key=skill_md_key)
files, warnings = self._skill_file_entries(
skill_path=skill_path,
skill_md_key=skill_md_key,
manifest_files=manifest_files,
drive_keys=drive_keys,
)
return {
**catalog,
"source": "skill_md",
"files": files,
"file_tree": self._build_file_tree(files),
"skill_md": preview,
"warnings": warnings,
}
def _commit_one(
self,
session: Session,
@ -228,9 +385,10 @@ class AgentDriveService:
pending_storage_deletes: list[str],
) -> dict[str, Any]:
key = normalize_drive_key(item.key)
skill_metadata = self._validate_skill_commit_fields(key=key, item=item)
file_kind = AgentDriveFileKind(item.file_ref.kind)
file_id = item.file_ref.id
size, mime_type = self._validate_source(
size, mime_type, file_hash = self._validate_source(
session, tenant_id=tenant_id, user_id=user_id, file_kind=file_kind, file_id=file_id
)
@ -245,6 +403,11 @@ class AgentDriveService:
# Idempotent re-commit of the same value: leave it (do not clean).
if existing.file_kind == file_kind and existing.file_id == file_id:
existing.value_owned_by_drive = item.value_owned_by_drive
existing.is_skill = item.is_skill
existing.skill_metadata = skill_metadata
existing.size = size
existing.mime_type = mime_type
existing.hash = file_hash
return self._row_dict(existing)
# Overwrite: clean the previous drive-owned value if no longer referenced.
if existing.value_owned_by_drive:
@ -259,7 +422,10 @@ class AgentDriveService:
existing.file_kind = file_kind
existing.file_id = file_id
existing.value_owned_by_drive = item.value_owned_by_drive
existing.is_skill = item.is_skill
existing.skill_metadata = skill_metadata
existing.size = size
existing.hash = file_hash
existing.mime_type = mime_type
return self._row_dict(existing)
@ -271,7 +437,10 @@ class AgentDriveService:
file_kind=file_kind,
file_id=file_id,
value_owned_by_drive=item.value_owned_by_drive,
is_skill=item.is_skill,
skill_metadata=skill_metadata,
size=size,
hash=file_hash,
mime_type=mime_type,
created_by=user_id,
)
@ -287,8 +456,187 @@ class AgentDriveService:
"size": row.size,
"mime_type": row.mime_type,
"value_owned_by_drive": row.value_owned_by_drive,
"is_skill": row.is_skill,
"skill_metadata": row.skill_metadata,
}
@staticmethod
def _skill_path_from_key(key: str) -> str:
if not key.endswith(_SKILL_MD_SUFFIX):
raise AgentDriveError(
"invalid_skill_key",
"skill rows must use the canonical '<path>/SKILL.md' key",
status_code=500,
)
path = key[: -len(_SKILL_MD_SUFFIX)]
if not path:
raise AgentDriveError(
"invalid_skill_key",
"skill rows must use the canonical '<path>/SKILL.md' key",
status_code=500,
)
return path
@classmethod
def _skill_archive_key(cls, key: str) -> str:
return f"{cls._skill_path_from_key(key)}/{_SKILL_ARCHIVE_NAME}"
@classmethod
def _validate_skill_commit_fields(cls, *, key: str, item: DriveCommitItem) -> str | None:
if not item.is_skill:
if item.skill_metadata is not None:
raise AgentDriveError(
"invalid_skill_metadata",
"skill metadata is only allowed for canonical skill rows",
status_code=400,
)
return None
cls._skill_path_from_key(key)
if item.skill_metadata is None:
raise AgentDriveError(
"invalid_skill_metadata",
"skill metadata is required for canonical skill rows",
status_code=400,
)
return json.dumps(
item.skill_metadata.model_dump(mode="json", exclude_none=True),
separators=(",", ":"),
sort_keys=True,
)
@staticmethod
def _parse_skill_metadata(key: str, raw_metadata: str | None) -> DriveSkillMetadata:
if raw_metadata is None:
raise AgentDriveError(
"invalid_skill_metadata",
f"skill row '{key}' is missing required metadata",
status_code=500,
)
try:
return DriveSkillMetadata.model_validate(json.loads(raw_metadata))
except (ValueError, TypeError) as exc:
raise AgentDriveError(
"invalid_skill_metadata",
f"skill row '{key}' has invalid stored metadata",
status_code=500,
) from exc
@staticmethod
def _manifest_files_from_skill_metadata(*, tenant_id: str, agent_id: str, skill_md_key: str) -> list[str] | None:
with session_factory.create_session() as session:
row = session.scalar(
select(AgentDriveFile).where(
AgentDriveFile.tenant_id == tenant_id,
AgentDriveFile.agent_id == agent_id,
AgentDriveFile.key == skill_md_key,
AgentDriveFile.is_skill.is_(True),
)
)
if row is None:
return None
try:
metadata = AgentDriveService._parse_skill_metadata(row.key, row.skill_metadata)
except Exception:
logger.warning("drive skill inspect: malformed skill metadata for %s", skill_md_key, exc_info=True)
return None
return [str(item) for item in (metadata.manifest_files or []) if str(item).strip()] or None
@classmethod
def _skill_file_entries(
cls,
*,
skill_path: str,
skill_md_key: str,
manifest_files: list[str] | None,
drive_keys: set[str],
) -> tuple[list[AgentDriveSkillFileInfo], list[str]]:
warnings: list[str] = []
if manifest_files:
paths = sorted({normalize_drive_key(path) for path in manifest_files})
else:
paths = sorted(
{
key.removeprefix(f"{skill_path}/")
for key in drive_keys
if not key.endswith(f"/{_SKILL_ARCHIVE_NAME}")
}
)
warnings.append("manifest_files_unavailable")
files: list[AgentDriveSkillFileInfo] = []
for path in paths:
if path == _SKILL_ARCHIVE_NAME:
continue
drive_key = f"{skill_path}/{path}"
files.append(
{
"path": path,
"name": path.rsplit("/", 1)[-1],
"type": "file",
"drive_key": drive_key if drive_key in drive_keys else None,
"available_in_drive": drive_key in drive_keys,
}
)
if "SKILL.md" not in {file["path"] for file in files}:
files.insert(
0,
{
"path": "SKILL.md",
"name": "SKILL.md",
"type": "file",
"drive_key": skill_md_key,
"available_in_drive": skill_md_key in drive_keys,
},
)
return files, warnings
@staticmethod
def _build_file_tree(files: list[AgentDriveSkillFileInfo]) -> list[dict[str, Any]]:
root: dict[str, Any] = {}
for file in files:
cursor = root
parts = [part for part in file["path"].split("/") if part]
path_parts: list[str] = []
for part in parts[:-1]:
path_parts.append(part)
directory = cursor.setdefault(
part,
{
"name": part,
"path": "/".join(path_parts),
"type": "directory",
"children": {},
},
)
cursor = directory["children"]
leaf_name = parts[-1] if parts else file["name"]
cursor[leaf_name] = {
"name": leaf_name,
"path": file["path"],
"type": file["type"],
"drive_key": file["drive_key"],
"available_in_drive": file["available_in_drive"],
}
def serialize(node: dict[str, Any]) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
for item in sorted(node.values(), key=lambda value: (value["type"] != "directory", value["name"])):
if item["type"] == "directory":
children = serialize(item["children"])
result.append(
{
"name": item["name"],
"path": item["path"],
"type": "directory",
"children": children,
}
)
else:
result.append(item)
return result
return serialize(root)
@staticmethod
def _assert_agent_belongs_to_tenant(session: Session, *, tenant_id: str, agent_id: str) -> None:
try:
@ -309,7 +657,7 @@ class AgentDriveService:
user_id: str,
file_kind: AgentDriveFileKind,
file_id: str,
) -> tuple[int | None, str | None]:
) -> tuple[int | None, str | None, str | None]:
"""Verify the source file exists for the tenant (and user, for ToolFile).
Malformed ids (e.g. a non-UUID hitting a UUID column) are treated as a
@ -328,7 +676,7 @@ class AgentDriveService:
raise AgentDriveError(
"source_not_found", "source ToolFile not found for this tenant/user", status_code=404
)
return tool_file.size, tool_file.mimetype
return tool_file.size, tool_file.mimetype, None
upload_file = session.scalar(
select(UploadFile).where(UploadFile.id == file_id, UploadFile.tenant_id == tenant_id)
)
@ -337,7 +685,7 @@ class AgentDriveService:
raise AgentDriveError("source_not_found", "source file ref is invalid", status_code=404) from exc
if upload_file is None:
raise AgentDriveError("source_not_found", "source UploadFile not found for this tenant", status_code=404)
return upload_file.size, upload_file.mime_type
return upload_file.size, upload_file.mime_type, upload_file.hash
def _cleanup_value(
self,
@ -509,6 +857,8 @@ __all__ = [
"AgentDriveService",
"DriveCommitItem",
"DriveFileRef",
"DriveSkillMetadata",
"decode_drive_mention_ref",
"normalize_drive_key",
"parse_agent_drive_ref",
]

View File

@ -20,6 +20,10 @@ from controllers.console.app.agent_drive_inspector import (
AgentDriveListByAgentApi,
AgentDrivePreviewApi,
AgentDrivePreviewByAgentApi,
AgentDriveSkillInspectApi,
AgentDriveSkillInspectByAgentApi,
AgentDriveSkillListApi,
AgentDriveSkillListByAgentApi,
)
from services.agent_drive_service import AgentDriveError
@ -97,6 +101,124 @@ def test_list_resolves_workflow_node_binding_agent():
assert composer.resolve_workflow_node_agent_id.call_args.kwargs["node_id"] == "agent-node-1"
def test_skill_list_by_agent_calls_service():
raw = _raw(AgentDriveSkillListByAgentApi.get)
with app.test_request_context("/"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.AgentDriveService") as drive,
):
drive.return_value.list_skills.return_value = [
{
"path": "pdf-toolkit",
"skill_md_key": "pdf-toolkit/SKILL.md",
"archive_key": "pdf-toolkit/.DIFY-SKILL-FULL.zip",
"name": "PDF Toolkit",
"description": "Work with PDFs.",
"size": 5,
"mime_type": "text/markdown",
"hash": None,
"created_at": 1718000000,
}
]
body = raw(AgentDriveSkillListByAgentApi(), "tenant-1", "agent-1")
assert body["items"][0]["path"] == "pdf-toolkit"
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
assert drive.return_value.list_skills.call_args.kwargs["agent_id"] == "agent-1"
def test_skill_list_resolves_workflow_node_binding_agent():
raw = _raw(AgentDriveSkillListApi.get)
with app.test_request_context("/?node_id=agent-node-1"):
with (
patch(f"{_MOD}.AgentComposerService") as composer,
patch(f"{_MOD}.AgentDriveService") as drive,
):
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-9"
drive.return_value.list_skills.return_value = []
body = raw(AgentDriveSkillListApi(), _APP)
assert body == {"items": []}
assert drive.return_value.list_skills.call_args.kwargs["agent_id"] == "wf-agent-9"
def test_skill_inspect_by_agent_returns_strict_json_response():
raw = _raw(AgentDriveSkillInspectByAgentApi.get)
payload = {
"path": "pdf-toolkit",
"skill_md_key": "pdf-toolkit/SKILL.md",
"archive_key": "pdf-toolkit/.DIFY-SKILL-FULL.zip",
"name": "PDF Toolkit",
"description": "Work with PDFs.",
"size": 5,
"mime_type": "text/markdown",
"hash": None,
"created_at": 1718000000,
"source": "skill_md",
"files": [
{
"path": "SKILL.md",
"name": "SKILL.md",
"type": "file",
"drive_key": "pdf-toolkit/SKILL.md",
"available_in_drive": True,
}
],
"file_tree": [],
"skill_md": {
"key": "pdf-toolkit/SKILL.md",
"size": 5,
"truncated": False,
"binary": False,
"text": "# PDF Toolkit\nUse it.\n",
},
"warnings": [],
}
with app.test_request_context("/"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP),
patch(f"{_MOD}.AgentDriveService") as drive,
):
drive.return_value.inspect_skill.return_value = payload
response = raw(AgentDriveSkillInspectByAgentApi(), "tenant-1", "agent-1", "pdf-toolkit")
assert response.status_code == 200
assert response.get_json()["skill_md"]["text"] == "# PDF Toolkit\nUse it.\n"
assert b"# PDF Toolkit\\nUse it.\\n" in response.get_data()
def test_skill_inspect_resolves_workflow_node_binding_agent():
raw = _raw(AgentDriveSkillInspectApi.get)
payload = {
"path": "pdf-toolkit",
"skill_md_key": "pdf-toolkit/SKILL.md",
"archive_key": None,
"name": "PDF Toolkit",
"description": "",
"size": 5,
"mime_type": "text/markdown",
"hash": None,
"created_at": None,
"source": "skill_md",
"files": [],
"file_tree": [],
"skill_md": {"key": "pdf-toolkit/SKILL.md", "size": 5, "truncated": False, "binary": False, "text": "# hi"},
"warnings": [],
}
with app.test_request_context("/?node_id=agent-node-1"):
with (
patch(f"{_MOD}.AgentComposerService") as composer,
patch(f"{_MOD}.AgentDriveService") as drive,
):
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-9"
drive.return_value.inspect_skill.return_value = payload
response = raw(AgentDriveSkillInspectApi(), _APP, "pdf-toolkit")
assert response.get_json()["path"] == "pdf-toolkit"
assert drive.return_value.inspect_skill.call_args.kwargs["agent_id"] == "wf-agent-9"
def test_list_400_when_no_agent_bound():
raw = _raw(AgentDriveListApi.get)
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)

View File

@ -67,6 +67,10 @@ def test_standardize_creates_two_drive_owned_toolfiles_and_commits():
assert [item.key for item in items] == ["pdf-toolkit/SKILL.md", "pdf-toolkit/.DIFY-SKILL-FULL.zip"]
assert all(item.value_owned_by_drive for item in items)
assert [item.file_ref.id for item in items] == ["md-tool-file", "zip-tool-file"]
assert items[0].is_skill is True
assert items[0].skill_metadata.name == "PDF Toolkit"
assert items[0].skill_metadata.manifest_files == ["SKILL.md", "scripts/run.py"]
assert items[1].is_skill is False
# The returned skill ref carries stable drive paths + file ids.
skill = result["skill"]

View File

@ -23,6 +23,7 @@ from services.agent_drive_service import (
AgentDriveError,
AgentDriveService,
DriveCommitItem,
DriveSkillMetadata,
normalize_drive_key,
parse_agent_drive_ref,
)
@ -515,3 +516,104 @@ def test_manifest_items_carry_created_at_for_inspector():
_commit("files/x.txt", tf)
items = AgentDriveService().manifest(tenant_id=TENANT, agent_id=AGENT)
assert items[0]["created_at"] is None or isinstance(items[0]["created_at"], int)
# ── DIFY-2517: skill catalog / inspect ───────────────────────────────────────
def _commit_skill(*, manifest_files: list[str] | None = None) -> None:
md = _seed_tool_file(name="SKILL.md")
zf = _seed_tool_file(name="full.zip")
AgentDriveService().commit(
tenant_id=TENANT,
user_id=USER,
agent_id=AGENT,
items=[
DriveCommitItem(
key="pdf-toolkit/SKILL.md",
file_ref={"kind": "tool_file", "id": md},
value_owned_by_drive=True,
is_skill=True,
skill_metadata=DriveSkillMetadata(
name="PDF Toolkit",
description="Work with PDFs.",
manifest_files=manifest_files,
),
),
DriveCommitItem(
key="pdf-toolkit/.DIFY-SKILL-FULL.zip",
file_ref={"kind": "tool_file", "id": zf},
value_owned_by_drive=True,
),
],
)
def test_list_skills_uses_canonical_skill_rows():
_commit_skill(manifest_files=["SKILL.md", "scripts/run.py"])
skills = AgentDriveService().list_skills(tenant_id=TENANT, agent_id=AGENT)
created_at = skills[0].pop("created_at")
assert skills == [
{
"path": "pdf-toolkit",
"skill_md_key": "pdf-toolkit/SKILL.md",
"archive_key": "pdf-toolkit/.DIFY-SKILL-FULL.zip",
"name": "PDF Toolkit",
"description": "Work with PDFs.",
"size": 5,
"mime_type": "text/plain",
"hash": None,
}
]
assert created_at is None or isinstance(created_at, int)
def test_inspect_skill_returns_manifest_files_and_file_tree():
_commit_skill(manifest_files=["SKILL.md", "references/guide.md", "scripts/run.py"])
with patch("services.agent_drive_service.storage") as storage_mock:
storage_mock.load_stream.return_value = iter([b"# PDF Toolkit\n"])
result = AgentDriveService().inspect_skill(tenant_id=TENANT, agent_id=AGENT, skill_path="pdf-toolkit")
assert result["source"] == "skill_md"
assert result["warnings"] == []
assert [file["path"] for file in result["files"]] == ["SKILL.md", "references/guide.md", "scripts/run.py"]
assert result["files"][0]["available_in_drive"] is True
assert result["files"][1]["available_in_drive"] is False
assert result["file_tree"][0]["name"] == "references"
assert result["file_tree"][1]["name"] == "scripts"
assert result["file_tree"][2]["name"] == "SKILL.md"
assert result["skill_md"]["text"] == "# PDF Toolkit\n"
def test_inspect_skill_falls_back_to_drive_keys_when_manifest_missing():
_commit_skill(manifest_files=None)
with patch("services.agent_drive_service.storage") as storage_mock:
storage_mock.load_stream.return_value = iter([b"# PDF Toolkit\n"])
result = AgentDriveService().inspect_skill(tenant_id=TENANT, agent_id=AGENT, skill_path="pdf-toolkit")
assert result["warnings"] == ["manifest_files_unavailable"]
assert [file["path"] for file in result["files"]] == ["SKILL.md"]
def test_skill_metadata_rejects_non_canonical_rows():
tf = _seed_tool_file(name="not-skill.md")
with pytest.raises(AgentDriveError) as exc_info:
AgentDriveService().commit(
tenant_id=TENANT,
user_id=USER,
agent_id=AGENT,
items=[
DriveCommitItem(
key="files/not-skill.md",
file_ref={"kind": "tool_file", "id": tf},
value_owned_by_drive=True,
is_skill=True,
skill_metadata=DriveSkillMetadata(name="Bad"),
)
],
)
assert exc_info.value.code == "invalid_skill_key"

View File

@ -29,6 +29,10 @@ import {
zGetAgentByAgentIdDriveFilesPreviewResponse,
zGetAgentByAgentIdDriveFilesQuery,
zGetAgentByAgentIdDriveFilesResponse,
zGetAgentByAgentIdDriveSkillsBySkillPathInspectPath,
zGetAgentByAgentIdDriveSkillsBySkillPathInspectResponse,
zGetAgentByAgentIdDriveSkillsPath,
zGetAgentByAgentIdDriveSkillsResponse,
zGetAgentByAgentIdLogsByConversationIdMessagesPath,
zGetAgentByAgentIdLogsByConversationIdMessagesQuery,
zGetAgentByAgentIdLogsByConversationIdMessagesResponse,
@ -336,8 +340,52 @@ export const files = {
preview,
}
/**
* Inspect one drive-backed skill for slash-menu hover/detail UI
*/
export const get9 = oc
.route({
description: 'Inspect one drive-backed skill for slash-menu hover/detail UI',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdDriveSkillsBySkillPathInspect',
path: '/agent/{agent_id}/drive/skills/{skill_path}/inspect',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdDriveSkillsBySkillPathInspectPath }))
.output(zGetAgentByAgentIdDriveSkillsBySkillPathInspectResponse)
export const inspect = {
get: get9,
}
export const bySkillPath = {
inspect,
}
/**
* List drive-backed skills for an Agent App
*/
export const get10 = oc
.route({
description: 'List drive-backed skills for an Agent App',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdDriveSkills',
path: '/agent/{agent_id}/drive/skills',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdDriveSkillsPath }))
.output(zGetAgentByAgentIdDriveSkillsResponse)
export const skills = {
get: get10,
bySkillPath,
}
export const drive = {
files,
skills,
}
/**
@ -420,7 +468,7 @@ export const files2 = {
post: post6,
}
export const get9 = oc
export const get11 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -432,10 +480,10 @@ export const get9 = oc
.output(zGetAgentByAgentIdLogSourcesResponse)
export const logSources = {
get: get9,
get: get11,
}
export const get10 = oc
export const get12 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -452,14 +500,14 @@ export const get10 = oc
.output(zGetAgentByAgentIdLogsByConversationIdMessagesResponse)
export const messages = {
get: get10,
get: get12,
}
export const byConversationId = {
messages,
}
export const get11 = oc
export const get13 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -473,14 +521,14 @@ export const get11 = oc
.output(zGetAgentByAgentIdLogsResponse)
export const logs = {
get: get11,
get: get13,
byConversationId,
}
/**
* Get Agent App message details by ID
*/
export const get12 = oc
export const get14 = oc
.route({
description: 'Get Agent App message details by ID',
inputStructure: 'detailed',
@ -493,7 +541,7 @@ export const get12 = oc
.output(zGetAgentByAgentIdMessagesByMessageIdResponse)
export const byMessageId2 = {
get: get12,
get: get14,
}
export const messages2 = {
@ -503,7 +551,7 @@ export const messages2 = {
/**
* List workflow apps that reference this Agent App's bound Agent (read-only)
*/
export const get13 = oc
export const get15 = oc
.route({
description: 'List workflow apps that reference this Agent App\'s bound Agent (read-only)',
inputStructure: 'detailed',
@ -516,13 +564,13 @@ export const get13 = oc
.output(zGetAgentByAgentIdReferencingWorkflowsResponse)
export const referencingWorkflows = {
get: get13,
get: get15,
}
/**
* Read a text/binary preview file in an Agent App conversation sandbox
*/
export const get14 = oc
export const get16 = oc
.route({
description: 'Read a text/binary preview file in an Agent App conversation sandbox',
inputStructure: 'detailed',
@ -540,7 +588,7 @@ export const get14 = oc
.output(zGetAgentByAgentIdSandboxFilesReadResponse)
export const read = {
get: get14,
get: get16,
}
/**
@ -570,7 +618,7 @@ export const upload = {
/**
* List a directory in an Agent App conversation sandbox
*/
export const get15 = oc
export const get17 = oc
.route({
description: 'List a directory in an Agent App conversation sandbox',
inputStructure: 'detailed',
@ -588,7 +636,7 @@ export const get15 = oc
.output(zGetAgentByAgentIdSandboxFilesResponse)
export const files3 = {
get: get15,
get: get17,
read,
upload,
}
@ -661,12 +709,12 @@ export const bySlug = {
inferTools,
}
export const skills = {
export const skills2 = {
upload: upload2,
bySlug,
}
export const get16 = oc
export const get18 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -683,14 +731,14 @@ export const get16 = oc
.output(zGetAgentByAgentIdStatisticsSummaryResponse)
export const summary = {
get: get16,
get: get18,
}
export const statistics = {
summary,
}
export const get17 = oc
export const get19 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -702,10 +750,10 @@ export const get17 = oc
.output(zGetAgentByAgentIdVersionsByVersionIdResponse)
export const byVersionId = {
get: get17,
get: get19,
}
export const get18 = oc
export const get20 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -717,7 +765,7 @@ export const get18 = oc
.output(zGetAgentByAgentIdVersionsResponse)
export const versions = {
get: get18,
get: get20,
byVersionId,
}
@ -733,7 +781,7 @@ export const delete3 = oc
.input(z.object({ params: zDeleteAgentByAgentIdPath }))
.output(zDeleteAgentByAgentIdResponse)
export const get19 = oc
export const get21 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -757,7 +805,7 @@ export const put2 = oc
export const byAgentId = {
delete: delete3,
get: get19,
get: get21,
put: put2,
chatMessages,
composer,
@ -771,12 +819,12 @@ export const byAgentId = {
messages: messages2,
referencingWorkflows,
sandbox,
skills,
skills: skills2,
statistics,
versions,
}
export const get20 = oc
export const get22 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -800,7 +848,7 @@ export const post10 = oc
.output(zPostAgentResponse)
export const agent = {
get: get20,
get: get22,
post: post10,
inviteOptions,
byAgentId,

View File

@ -148,6 +148,29 @@ export type AgentDrivePreviewResponse = {
truncated: boolean
}
export type AgentDriveSkillListResponse = {
items?: Array<AgentDriveSkillItemResponse>
}
export type AgentDriveSkillInspectResponse = {
archive_key?: string | null
created_at?: number | null
description: string
file_tree?: Array<{
[key: string]: unknown
}>
files?: Array<AgentDriveSkillFileResponse>
hash?: string | null
mime_type?: string | null
name: string
path: string
size?: number | null
skill_md: AgentDriveSkillMarkdownResponse
skill_md_key: string
source: string
warnings?: Array<string>
}
export type AgentAppFeaturesPayload = {
opening_statement?: string | null
retriever_resource?: AgentFeatureToggleConfig | null
@ -530,9 +553,39 @@ export type AgentDriveItemResponse = {
created_at?: number | null
file_kind: string
hash?: string | null
is_skill?: boolean | null
key: string
mime_type?: string | null
size?: number | null
skill_metadata?: string | null
}
export type AgentDriveSkillItemResponse = {
archive_key?: string | null
created_at?: number | null
description: string
hash?: string | null
mime_type?: string | null
name: string
path: string
size?: number | null
skill_md_key: string
}
export type AgentDriveSkillFileResponse = {
available_in_drive: boolean
drive_key?: string | null
name: string
path: string
type: string
}
export type AgentDriveSkillMarkdownResponse = {
binary: boolean
key: string
size?: number | null
text?: string | null
truncated: boolean
}
export type AgentFeatureToggleConfig = {
@ -1837,6 +1890,39 @@ export type GetAgentByAgentIdDriveFilesPreviewResponses = {
export type GetAgentByAgentIdDriveFilesPreviewResponse
= GetAgentByAgentIdDriveFilesPreviewResponses[keyof GetAgentByAgentIdDriveFilesPreviewResponses]
export type GetAgentByAgentIdDriveSkillsData = {
body?: never
path: {
agent_id: string
}
query?: never
url: '/agent/{agent_id}/drive/skills'
}
export type GetAgentByAgentIdDriveSkillsResponses = {
200: AgentDriveSkillListResponse
}
export type GetAgentByAgentIdDriveSkillsResponse
= GetAgentByAgentIdDriveSkillsResponses[keyof GetAgentByAgentIdDriveSkillsResponses]
export type GetAgentByAgentIdDriveSkillsBySkillPathInspectData = {
body?: never
path: {
agent_id: string
skill_path: string
}
query?: never
url: '/agent/{agent_id}/drive/skills/{skill_path}/inspect'
}
export type GetAgentByAgentIdDriveSkillsBySkillPathInspectResponses = {
200: AgentDriveSkillInspectResponse
}
export type GetAgentByAgentIdDriveSkillsBySkillPathInspectResponse
= GetAgentByAgentIdDriveSkillsBySkillPathInspectResponses[keyof GetAgentByAgentIdDriveSkillsBySkillPathInspectResponses]
export type PostAgentByAgentIdFeaturesData = {
body: AgentAppFeaturesPayload
path: {

View File

@ -286,9 +286,11 @@ export const zAgentDriveItemResponse = z.object({
created_at: z.int().nullish(),
file_kind: z.string(),
hash: z.string().nullish(),
is_skill: z.boolean().nullish(),
key: z.string(),
mime_type: z.string().nullish(),
size: z.int().nullish(),
skill_metadata: z.string().nullish(),
})
/**
@ -298,6 +300,70 @@ export const zAgentDriveListResponse = z.object({
items: z.array(zAgentDriveItemResponse).optional(),
})
/**
* AgentDriveSkillItemResponse
*/
export const zAgentDriveSkillItemResponse = z.object({
archive_key: z.string().nullish(),
created_at: z.int().nullish(),
description: z.string(),
hash: z.string().nullish(),
mime_type: z.string().nullish(),
name: z.string(),
path: z.string(),
size: z.int().nullish(),
skill_md_key: z.string(),
})
/**
* AgentDriveSkillListResponse
*/
export const zAgentDriveSkillListResponse = z.object({
items: z.array(zAgentDriveSkillItemResponse).optional(),
})
/**
* AgentDriveSkillFileResponse
*/
export const zAgentDriveSkillFileResponse = z.object({
available_in_drive: z.boolean(),
drive_key: z.string().nullish(),
name: z.string(),
path: z.string(),
type: z.string(),
})
/**
* AgentDriveSkillMarkdownResponse
*/
export const zAgentDriveSkillMarkdownResponse = z.object({
binary: z.boolean(),
key: z.string(),
size: z.int().nullish(),
text: z.string().nullish(),
truncated: z.boolean(),
})
/**
* AgentDriveSkillInspectResponse
*/
export const zAgentDriveSkillInspectResponse = z.object({
archive_key: z.string().nullish(),
created_at: z.int().nullish(),
description: z.string(),
file_tree: z.array(z.record(z.string(), z.unknown())).optional(),
files: z.array(zAgentDriveSkillFileResponse).optional(),
hash: z.string().nullish(),
mime_type: z.string().nullish(),
name: z.string(),
path: z.string(),
size: z.int().nullish(),
skill_md: zAgentDriveSkillMarkdownResponse,
skill_md_key: z.string(),
source: z.string(),
warnings: z.array(z.string()).optional(),
})
/**
* AgentFeatureToggleConfig
*/
@ -2304,6 +2370,26 @@ export const zGetAgentByAgentIdDriveFilesPreviewQuery = z.object({
*/
export const zGetAgentByAgentIdDriveFilesPreviewResponse = zAgentDrivePreviewResponse
export const zGetAgentByAgentIdDriveSkillsPath = z.object({
agent_id: z.uuid(),
})
/**
* Drive skills
*/
export const zGetAgentByAgentIdDriveSkillsResponse = zAgentDriveSkillListResponse
export const zGetAgentByAgentIdDriveSkillsBySkillPathInspectPath = z.object({
agent_id: z.uuid(),
skill_path: z.string(),
})
/**
* Drive skill inspect view
*/
export const zGetAgentByAgentIdDriveSkillsBySkillPathInspectResponse
= zAgentDriveSkillInspectResponse
export const zPostAgentByAgentIdFeaturesBody = zAgentAppFeaturesPayload
export const zPostAgentByAgentIdFeaturesPath = z.object({

File diff suppressed because it is too large Load Diff

View File

@ -193,6 +193,29 @@ export type AgentDrivePreviewResponse = {
truncated: boolean
}
export type AgentDriveSkillListResponse = {
items?: Array<AgentDriveSkillItemResponse>
}
export type AgentDriveSkillInspectResponse = {
archive_key?: string | null
created_at?: number | null
description: string
file_tree?: Array<{
[key: string]: unknown
}>
files?: Array<AgentDriveSkillFileResponse>
hash?: string | null
mime_type?: string | null
name: string
path: string
size?: number | null
skill_md: AgentDriveSkillMarkdownResponse
skill_md_key: string
source: string
warnings?: Array<string>
}
export type AgentDriveDeleteResponse = {
config_version_id?: string | null
removed_keys?: Array<string>
@ -1274,9 +1297,39 @@ export type AgentDriveItemResponse = {
created_at?: number | null
file_kind: string
hash?: string | null
is_skill?: boolean | null
key: string
mime_type?: string | null
size?: number | null
skill_metadata?: string | null
}
export type AgentDriveSkillItemResponse = {
archive_key?: string | null
created_at?: number | null
description: string
hash?: string | null
mime_type?: string | null
name: string
path: string
size?: number | null
skill_md_key: string
}
export type AgentDriveSkillFileResponse = {
available_in_drive: boolean
drive_key?: string | null
name: string
path: string
type: string
}
export type AgentDriveSkillMarkdownResponse = {
binary: boolean
key: string
size?: number | null
text?: string | null
truncated: boolean
}
export type AgentDriveFileResponse = {
@ -3125,6 +3178,44 @@ export type GetAppsByAppIdAgentDriveFilesPreviewResponses = {
export type GetAppsByAppIdAgentDriveFilesPreviewResponse
= GetAppsByAppIdAgentDriveFilesPreviewResponses[keyof GetAppsByAppIdAgentDriveFilesPreviewResponses]
export type GetAppsByAppIdAgentDriveSkillsData = {
body?: never
path: {
app_id: string
}
query?: {
node_id?: string
prefix?: string
}
url: '/apps/{app_id}/agent/drive/skills'
}
export type GetAppsByAppIdAgentDriveSkillsResponses = {
200: AgentDriveSkillListResponse
}
export type GetAppsByAppIdAgentDriveSkillsResponse
= GetAppsByAppIdAgentDriveSkillsResponses[keyof GetAppsByAppIdAgentDriveSkillsResponses]
export type GetAppsByAppIdAgentDriveSkillsBySkillPathInspectData = {
body?: never
path: {
app_id: string
skill_path: string
}
query?: {
node_id?: string
}
url: '/apps/{app_id}/agent/drive/skills/{skill_path}/inspect'
}
export type GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponses = {
200: AgentDriveSkillInspectResponse
}
export type GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponse
= GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponses[keyof GetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponses]
export type DeleteAppsByAppIdAgentFilesData = {
body?: never
path: {

View File

@ -918,9 +918,11 @@ export const zAgentDriveItemResponse = z.object({
created_at: z.int().nullish(),
file_kind: z.string(),
hash: z.string().nullish(),
is_skill: z.boolean().nullish(),
key: z.string(),
mime_type: z.string().nullish(),
size: z.int().nullish(),
skill_metadata: z.string().nullish(),
})
/**
@ -930,6 +932,70 @@ export const zAgentDriveListResponse = z.object({
items: z.array(zAgentDriveItemResponse).optional(),
})
/**
* AgentDriveSkillItemResponse
*/
export const zAgentDriveSkillItemResponse = z.object({
archive_key: z.string().nullish(),
created_at: z.int().nullish(),
description: z.string(),
hash: z.string().nullish(),
mime_type: z.string().nullish(),
name: z.string(),
path: z.string(),
size: z.int().nullish(),
skill_md_key: z.string(),
})
/**
* AgentDriveSkillListResponse
*/
export const zAgentDriveSkillListResponse = z.object({
items: z.array(zAgentDriveSkillItemResponse).optional(),
})
/**
* AgentDriveSkillFileResponse
*/
export const zAgentDriveSkillFileResponse = z.object({
available_in_drive: z.boolean(),
drive_key: z.string().nullish(),
name: z.string(),
path: z.string(),
type: z.string(),
})
/**
* AgentDriveSkillMarkdownResponse
*/
export const zAgentDriveSkillMarkdownResponse = z.object({
binary: z.boolean(),
key: z.string(),
size: z.int().nullish(),
text: z.string().nullish(),
truncated: z.boolean(),
})
/**
* AgentDriveSkillInspectResponse
*/
export const zAgentDriveSkillInspectResponse = z.object({
archive_key: z.string().nullish(),
created_at: z.int().nullish(),
description: z.string(),
file_tree: z.array(z.record(z.string(), z.unknown())).optional(),
files: z.array(zAgentDriveSkillFileResponse).optional(),
hash: z.string().nullish(),
mime_type: z.string().nullish(),
name: z.string(),
path: z.string(),
size: z.int().nullish(),
skill_md: zAgentDriveSkillMarkdownResponse,
skill_md_key: z.string(),
source: z.string(),
warnings: z.array(z.string()).optional(),
})
/**
* AgentDriveFileResponse
*/
@ -3916,6 +3982,35 @@ export const zGetAppsByAppIdAgentDriveFilesPreviewQuery = z.object({
*/
export const zGetAppsByAppIdAgentDriveFilesPreviewResponse = zAgentDrivePreviewResponse
export const zGetAppsByAppIdAgentDriveSkillsPath = z.object({
app_id: z.uuid(),
})
export const zGetAppsByAppIdAgentDriveSkillsQuery = z.object({
node_id: z.string().optional(),
prefix: z.string().optional().default(''),
})
/**
* Drive skills
*/
export const zGetAppsByAppIdAgentDriveSkillsResponse = zAgentDriveSkillListResponse
export const zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectPath = z.object({
app_id: z.uuid(),
skill_path: z.string(),
})
export const zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectQuery = z.object({
node_id: z.string().optional(),
})
/**
* Drive skill inspect view
*/
export const zGetAppsByAppIdAgentDriveSkillsBySkillPathInspectResponse
= zAgentDriveSkillInspectResponse
export const zDeleteAppsByAppIdAgentFilesPath = z.object({
app_id: z.uuid(),
})