mirror of
https://github.com/langgenius/dify.git
synced 2026-06-23 12:31:13 +08:00
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:
parent
4f61353dc2
commit
34c1bf1062
@ -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",
|
||||
]
|
||||
|
||||
@ -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")
|
||||
@ -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)
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
@ -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: {
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user