feat: Unify Agent v2 console routes (#37465)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123 2026-06-15 19:09:15 +08:00 committed by GitHub
parent a18635e566
commit 1e8329f02c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 6705 additions and 3735 deletions

View File

@ -0,0 +1,10 @@
from uuid import UUID
from extensions.ext_database import db
from models.model import App
from services.agent.roster_service import AgentRosterService
def resolve_agent_app_model(*, tenant_id: str, agent_id: UUID) -> App:
"""Resolve the hidden Agent App backing an Agent Console resource."""
return AgentRosterService(db.session).get_agent_app_model(tenant_id=tenant_id, agent_id=str(agent_id))

View File

@ -1,7 +1,10 @@
from uuid import UUID
from flask_restx import Resource from flask_restx import Resource
from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_app_model
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
account_initialization_required, account_initialization_required,
@ -35,6 +38,10 @@ register_response_schema_models(
) )
def _resolve_agent_app_id(*, tenant_id: str, agent_id: UUID) -> str:
return resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id).id
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/agent-composer") @console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/agent-composer")
class WorkflowAgentComposerApi(Resource): class WorkflowAgentComposerApi(Resource):
@console_ns.response( @console_ns.response(
@ -176,18 +183,18 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
) )
@console_ns.route("/apps/<uuid:app_id>/agent-composer") @console_ns.route("/agent/<uuid:agent_id>/composer")
class AgentAppComposerApi(Resource): class AgentComposerApi(Resource):
@console_ns.response(200, "Agent app composer state", console_ns.models[AgentAppComposerResponse.__name__]) @console_ns.response(200, "Agent app composer state", console_ns.models[AgentAppComposerResponse.__name__])
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, app_model: App): def get(self, tenant_id: str, agent_id: UUID):
app_id = _resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id)
return dump_response( return dump_response(
AgentAppComposerResponse, AgentAppComposerResponse,
AgentComposerService.load_agent_app_composer(tenant_id=tenant_id, app_id=app_model.id), AgentComposerService.load_agent_app_composer(tenant_id=tenant_id, app_id=app_id),
) )
@console_ns.expect(console_ns.models[ComposerSavePayload.__name__]) @console_ns.expect(console_ns.models[ComposerSavePayload.__name__])
@ -196,24 +203,24 @@ class AgentAppComposerApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@edit_permission_required @edit_permission_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
def put(self, tenant_id: str, account_id: str, app_model: App): def put(self, tenant_id: str, account_id: str, agent_id: UUID):
app_id = _resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id)
payload = ComposerSavePayload.model_validate(console_ns.payload or {}) payload = ComposerSavePayload.model_validate(console_ns.payload or {})
return dump_response( return dump_response(
AgentAppComposerResponse, AgentAppComposerResponse,
AgentComposerService.save_agent_app_composer( AgentComposerService.save_agent_app_composer(
tenant_id=tenant_id, tenant_id=tenant_id,
app_id=app_model.id, app_id=app_id,
account_id=account_id, account_id=account_id,
payload=payload, payload=payload,
), ),
) )
@console_ns.route("/apps/<uuid:app_id>/agent-composer/validate") @console_ns.route("/agent/<uuid:agent_id>/composer/validate")
class AgentAppComposerValidateApi(Resource): class AgentComposerValidateApi(Resource):
@console_ns.expect(console_ns.models[ComposerSavePayload.__name__]) @console_ns.expect(console_ns.models[ComposerSavePayload.__name__])
@console_ns.response( @console_ns.response(
200, "Agent app composer validation result", console_ns.models[AgentComposerValidateResponse.__name__] 200, "Agent app composer validation result", console_ns.models[AgentComposerValidateResponse.__name__]
@ -221,36 +228,36 @@ class AgentAppComposerValidateApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_tenant_id @with_current_tenant_id
def post(self, tenant_id: str, app_model: App): def post(self, tenant_id: str, agent_id: UUID):
_resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id)
payload = ComposerSavePayload.model_validate(console_ns.payload or {}) payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_save_payload(payload) ComposerConfigValidator.validate_save_payload(payload)
findings = AgentComposerService.collect_validation_findings( findings = AgentComposerService.collect_validation_findings(
tenant_id=tenant_id, tenant_id=tenant_id,
payload=payload, payload=payload,
agent_id=AgentComposerService.resolve_bound_agent_id(tenant_id=tenant_id, app_id=app_model.id), agent_id=str(agent_id),
) )
return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings}) return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings})
@console_ns.route("/apps/<uuid:app_id>/agent-composer/candidates") @console_ns.route("/agent/<uuid:agent_id>/composer/candidates")
class AgentAppComposerCandidatesApi(Resource): class AgentComposerCandidatesApi(Resource):
@console_ns.response( @console_ns.response(
200, "Agent app composer candidates", console_ns.models[AgentComposerCandidatesResponse.__name__] 200, "Agent app composer candidates", console_ns.models[AgentComposerCandidatesResponse.__name__]
) )
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_user_id @with_current_user_id
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, current_user_id: str, app_model: App): def get(self, tenant_id: str, current_user_id: str, agent_id: UUID):
app_id = _resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id)
return dump_response( return dump_response(
AgentComposerCandidatesResponse, AgentComposerCandidatesResponse,
AgentComposerService.get_agent_app_candidates( AgentComposerService.get_agent_app_candidates(
tenant_id=tenant_id, tenant_id=tenant_id,
app_id=app_model.id, app_id=app_id,
user_id=current_user_id, user_id=current_user_id,
), ),
) )

View File

@ -6,10 +6,21 @@ from pydantic import BaseModel, Field
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.app import (
AppDetailWithSite,
AppListQuery,
AppPagination,
UpdateAppPayload,
_normalize_app_list_query_args,
)
from controllers.console.wraps import ( from controllers.console.wraps import (
account_initialization_required, account_initialization_required,
cloud_edition_billing_resource_check,
edit_permission_required,
enterprise_license_required,
setup_required, setup_required,
with_current_tenant_id, with_current_tenant_id,
with_current_user,
) )
from extensions.ext_database import db from extensions.ext_database import db
from fields.agent_fields import ( from fields.agent_fields import (
@ -18,12 +29,17 @@ from fields.agent_fields import (
AgentInviteOptionsResponse, AgentInviteOptionsResponse,
AgentPublishedReferenceResponse, AgentPublishedReferenceResponse,
AgentRosterListResponse, AgentRosterListResponse,
AgentRosterResponse,
) )
from libs.helper import dump_response from libs.helper import dump_response
from libs.login import login_required from libs.login import login_required
from models import Account
from models.model import IconType
from services.agent.errors import AgentNotFoundError
from services.agent.roster_service import AgentRosterService from services.agent.roster_service import AgentRosterService
from services.app_service import AppListParams, AppService, CreateAppParams
from services.enterprise.enterprise_service import EnterpriseService
from services.entities.agent_entities import RosterListQuery from services.entities.agent_entities import RosterListQuery
from services.feature_service import FeatureService
class AgentInviteOptionsQuery(RosterListQuery): class AgentInviteOptionsQuery(RosterListQuery):
@ -34,20 +50,32 @@ class AgentIdPath(BaseModel):
agent_id: str agent_id: str
class AgentAppCreatePayload(BaseModel):
name: str = Field(..., min_length=1, description="Agent name")
description: str | None = Field(default=None, description="Agent description (max 400 chars)", max_length=400)
icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color")
register_schema_models( register_schema_models(
console_ns, console_ns,
AgentAppCreatePayload,
AgentInviteOptionsQuery, AgentInviteOptionsQuery,
AgentIdPath, AgentIdPath,
AppListQuery,
UpdateAppPayload,
RosterListQuery, RosterListQuery,
) )
register_response_schema_models( register_response_schema_models(
console_ns, console_ns,
AppDetailWithSite,
AppPagination,
AgentConfigSnapshotDetailResponse, AgentConfigSnapshotDetailResponse,
AgentConfigSnapshotListResponse, AgentConfigSnapshotListResponse,
AgentInviteOptionsResponse, AgentInviteOptionsResponse,
AgentPublishedReferenceResponse, AgentPublishedReferenceResponse,
AgentRosterListResponse, AgentRosterListResponse,
AgentRosterResponse,
) )
@ -55,25 +83,138 @@ def _agent_roster_service() -> AgentRosterService:
return AgentRosterService(db.session) return AgentRosterService(db.session)
@console_ns.route("/agents") def _serialize_agent_app_detail(app_model) -> dict:
class AgentRosterListApi(Resource): app_model = AppService().get_app(app_model)
@console_ns.doc(params=query_params_from_model(RosterListQuery)) if FeatureService.get_system_features().webapp_auth.enabled:
@console_ns.response(200, "Agent roster list", console_ns.models[AgentRosterListResponse.__name__]) app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json")
agent_id = payload.pop("bound_agent_id", None)
if not agent_id:
raise AgentNotFoundError()
payload["id"] = agent_id
return payload
def _serialize_agent_app_pagination(app_pagination) -> dict:
payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
for item in payload["data"]:
agent_id = item.pop("bound_agent_id", None)
if agent_id:
item["id"] = agent_id
return payload
def _resolve_agent_app_model(*, tenant_id: str, agent_id: UUID):
return _agent_roster_service().get_agent_app_model(tenant_id=tenant_id, agent_id=str(agent_id))
@console_ns.route("/agent")
class AgentAppListApi(Resource):
@console_ns.doc(params=query_params_from_model(AppListQuery))
@console_ns.response(200, "Agent app list", console_ns.models[AppPagination.__name__])
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@with_current_user
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str): def get(self, current_tenant_id: str, current_user: Account):
query = RosterListQuery.model_validate(request.args.to_dict(flat=True)) args = AppListQuery.model_validate(_normalize_app_list_query_args(request.args))
return dump_response( params = AppListParams(
AgentRosterListResponse, page=args.page,
_agent_roster_service().list_roster_agents( limit=args.limit,
tenant_id=tenant_id, page=query.page, limit=query.limit, keyword=query.keyword mode="agent",
), name=args.name,
tag_ids=args.tag_ids,
creator_ids=args.creator_ids,
is_created_by_me=args.is_created_by_me,
status="normal",
) )
app_pagination = AppService().get_paginate_apps(current_user.id, current_tenant_id, params)
if app_pagination is None:
empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
return empty.model_dump(mode="json")
@console_ns.route("/agents/invite-options") return _serialize_agent_app_pagination(app_pagination)
@console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__])
@console_ns.response(201, "Agent app created successfully", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "Invalid request parameters")
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("apps")
@edit_permission_required
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account):
args = AgentAppCreatePayload.model_validate(console_ns.payload)
params = CreateAppParams(
name=args.name,
description=args.description,
mode="agent",
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
)
app = AppService().create_app(current_tenant_id, params, current_user)
return _serialize_agent_app_detail(app), 201
@console_ns.route("/agent/<uuid:agent_id>")
class AgentAppApi(Resource):
@console_ns.response(200, "Agent app detail", console_ns.models[AppDetailWithSite.__name__])
@setup_required
@login_required
@account_initialization_required
@enterprise_license_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _serialize_agent_app_detail(app_model)
@console_ns.expect(console_ns.models[UpdateAppPayload.__name__])
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__])
@console_ns.response(403, "Insufficient permissions")
@console_ns.response(400, "Invalid request parameters")
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_tenant_id
def put(self, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
args = UpdateAppPayload.model_validate(console_ns.payload)
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
}
updated = AppService().update_app(app_model, args_dict)
return _serialize_agent_app_detail(updated)
@console_ns.response(204, "Agent app deleted successfully")
@console_ns.response(403, "Insufficient permissions")
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_tenant_id
def delete(self, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
AppService().delete_app(app_model)
return "", 204
@console_ns.route("/agent/invite-options")
class AgentInviteOptionsApi(Resource): class AgentInviteOptionsApi(Resource):
@console_ns.doc(params=query_params_from_model(AgentInviteOptionsQuery)) @console_ns.doc(params=query_params_from_model(AgentInviteOptionsQuery))
@console_ns.response(200, "Agent invite options", console_ns.models[AgentInviteOptionsResponse.__name__]) @console_ns.response(200, "Agent invite options", console_ns.models[AgentInviteOptionsResponse.__name__])
@ -95,21 +236,7 @@ class AgentInviteOptionsApi(Resource):
) )
@console_ns.route("/agents/<uuid:agent_id>") @console_ns.route("/agent/<uuid:agent_id>/versions")
class AgentRosterDetailApi(Resource):
@console_ns.response(200, "Agent detail", console_ns.models[AgentRosterResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID):
return dump_response(
AgentRosterResponse,
_agent_roster_service().get_roster_agent_detail(tenant_id=tenant_id, agent_id=str(agent_id)),
)
@console_ns.route("/agents/<uuid:agent_id>/versions")
class AgentRosterVersionsApi(Resource): class AgentRosterVersionsApi(Resource):
@console_ns.response(200, "Agent versions", console_ns.models[AgentConfigSnapshotListResponse.__name__]) @console_ns.response(200, "Agent versions", console_ns.models[AgentConfigSnapshotListResponse.__name__])
@setup_required @setup_required
@ -123,7 +250,7 @@ class AgentRosterVersionsApi(Resource):
) )
@console_ns.route("/agents/<uuid:agent_id>/versions/<uuid:version_id>") @console_ns.route("/agent/<uuid:agent_id>/versions/<uuid:version_id>")
class AgentRosterVersionDetailApi(Resource): class AgentRosterVersionDetailApi(Resource):
@console_ns.response(200, "Agent version detail", console_ns.models[AgentConfigSnapshotDetailResponse.__name__]) @console_ns.response(200, "Agent version detail", console_ns.models[AgentConfigSnapshotDetailResponse.__name__])
@setup_required @setup_required

View File

@ -1,5 +1,6 @@
import logging import logging
from typing import Any from typing import Any
from uuid import UUID
from flask import request from flask import request
from flask_restx import Resource from flask_restx import Resource
@ -13,8 +14,14 @@ from controllers.common.schema import (
register_schema_models, register_schema_models,
) )
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_app_model
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user from controllers.console.wraps import (
account_initialization_required,
setup_required,
with_current_tenant_id,
with_current_user,
)
from extensions.ext_database import db from extensions.ext_database import db
from fields.base import ResponseModel from fields.base import ResponseModel
from libs.helper import uuid_value from libs.helper import uuid_value
@ -42,7 +49,7 @@ from services.file_service import FileService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_AGENT_DRIVE_APP_MODES = [AppMode.AGENT, AppMode.WORKFLOW, AppMode.ADVANCED_CHAT] _WORKFLOW_AGENT_DRIVE_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]
class AgentLogQuery(BaseModel): class AgentLogQuery(BaseModel):
@ -72,6 +79,10 @@ class AgentDriveDeleteFileQuery(AgentDriveMutationQuery):
key: str = Field(min_length=1, description="Drive key, e.g. files/sample.pdf") key: str = Field(min_length=1, description="Drive key, e.g. files/sample.pdf")
class AgentDriveDeleteFileByAgentQuery(BaseModel):
key: str = Field(min_length=1, description="Drive key, e.g. files/sample.pdf")
class AgentLogMetaResponse(ResponseModel): class AgentLogMetaResponse(ResponseModel):
status: str status: str
executor: str executor: str
@ -138,7 +149,7 @@ class AgentDriveDeleteResponse(ResponseModel):
config_version_id: str | None = None config_version_id: str | None = None
register_schema_models(console_ns, AgentLogQuery, AgentDriveFilePayload) register_schema_models(console_ns, AgentLogQuery, AgentDriveFilePayload, AgentDriveDeleteFileByAgentQuery)
register_response_schema_models( register_response_schema_models(
console_ns, console_ns,
AgentDriveDeleteResponse, AgentDriveDeleteResponse,
@ -152,7 +163,7 @@ register_response_schema_models(
def _resolve_agent_id(app_model: App, node_id: str | None) -> str | None: def _resolve_agent_id(app_model: App, node_id: str | None) -> str | None:
if node_id: if node_id and app_model.mode != AppMode.AGENT:
return AgentComposerService.resolve_workflow_node_agent_id( return AgentComposerService.resolve_workflow_node_agent_id(
tenant_id=app_model.tenant_id, app_id=app_model.id, node_id=node_id tenant_id=app_model.tenant_id, app_id=app_model.id, node_id=node_id
) )
@ -163,6 +174,192 @@ def _agent_not_bound() -> tuple[dict[str, str], int]:
return {"code": "agent_not_bound", "message": "no agent is bound for this app/node"}, 400 return {"code": "agent_not_bound", "message": "no agent is bound for this app/node"}, 400
def _upload_skill_for_app(*, current_user: Account):
if "file" not in request.files:
return {"code": "no_file", "message": "no skill file uploaded"}, 400
if len(request.files) > 1:
return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400
upload = request.files["file"]
content = upload.stream.read()
try:
manifest = SkillPackageService().validate_and_extract(content=content, filename=upload.filename or "")
except SkillPackageError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
upload_file = FileService(db.engine).upload_file(
filename=upload.filename or "skill.zip",
content=content,
mimetype=upload.mimetype or "application/zip",
user=current_user,
)
skill_ref = manifest.to_skill_ref(file_id=upload_file.id)
return {"skill": skill_ref.model_dump(exclude_none=True), "manifest": manifest.model_dump()}, 201
def _standardize_skill_for_app(*, current_user: Account, app_model: App):
query = query_params_from_request(AgentDriveMutationQuery)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
if "file" not in request.files:
return {"code": "no_file", "message": "no skill file uploaded"}, 400
if len(request.files) > 1:
return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400
upload = request.files["file"]
content = upload.stream.read()
try:
result = SkillStandardizeService().standardize(
content=content,
filename=upload.filename or "",
tenant_id=app_model.tenant_id,
user_id=current_user.id,
agent_id=agent_id,
)
except (SkillPackageError, AgentDriveError) as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
return result, 201
def _commit_drive_file_for_app(*, current_user: Account, app_model: App, allow_node_id: bool = True):
query = query_params_from_request(AgentDriveMutationQuery)
node_id = query.node_id if allow_node_id else None
agent_id = _resolve_agent_id(app_model, node_id)
if not agent_id:
return _agent_not_bound()
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
upload_file = db.session.scalar(
select(UploadFile).where(
UploadFile.id == payload.upload_file_id,
UploadFile.tenant_id == app_model.tenant_id,
)
)
if upload_file is None:
return {"code": "upload_file_not_found", "message": "upload file not found in this workspace"}, 404
try:
key = normalize_drive_key(f"files/{upload_file.name}")
committed = AgentDriveService().commit(
tenant_id=app_model.tenant_id,
user_id=current_user.id,
agent_id=agent_id,
items=[
DriveCommitItem(
key=key,
file_ref=DriveFileRef(kind="upload_file", id=upload_file.id),
# ADD FILE uploads exist solely to live in the drive, so the
# drive owns (and physically cleans) the value on delete.
value_owned_by_drive=True,
)
],
)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
row = committed[0]
file_ref = AgentFileRefConfig.model_validate(
{
"id": row["key"],
"name": upload_file.name,
"file_id": upload_file.id,
"drive_key": row["key"],
"type": row.get("mime_type"),
"size": row.get("size"),
}
)
config_version_id = AgentComposerService.add_drive_file_ref(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
account_id=current_user.id,
file_ref=file_ref,
app_id=app_model.id,
node_id=node_id,
)
return {
"file": {
"name": upload_file.name,
"drive_key": row["key"],
"file_id": upload_file.id,
"size": row.get("size"),
"mime_type": row.get("mime_type"),
},
"config_version_id": config_version_id,
}, 201
def _delete_drive_file_for_app(*, current_user: Account, app_model: App, allow_node_id: bool = True):
query = query_params_from_request(AgentDriveDeleteFileQuery)
node_id = query.node_id if allow_node_id else None
agent_id = _resolve_agent_id(app_model, node_id)
if not agent_id:
return _agent_not_bound()
try:
key = normalize_drive_key(query.key)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
config_version_id = AgentComposerService.remove_drive_refs(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
account_id=current_user.id,
file_key=key,
app_id=app_model.id,
node_id=node_id,
)
removed_keys: list[str] = []
try:
removed_keys = AgentDriveService().delete(tenant_id=app_model.tenant_id, agent_id=agent_id, key=key)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
except Exception:
# Soul-first ordering: the ref is already gone; orphan KV rows are
# harmless and an idempotent DELETE retry cleans them.
logger.exception("agent drive delete failed for key %s (soul already updated)", key)
return {"result": "success", "removed_keys": removed_keys, "config_version_id": config_version_id}
def _delete_skill_for_app(*, current_user: Account, app_model: App, slug: str, allow_node_id: bool = True):
query = query_params_from_request(AgentDriveMutationQuery)
node_id = query.node_id if allow_node_id else None
agent_id = _resolve_agent_id(app_model, node_id)
if not agent_id:
return _agent_not_bound()
if "/" in slug or not slug.strip():
return {"code": "drive_key_invalid", "message": "skill slug must be a single path segment"}, 400
config_version_id = AgentComposerService.remove_drive_refs(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
account_id=current_user.id,
skill_slug=slug,
app_id=app_model.id,
node_id=node_id,
)
removed_keys: list[str] = []
try:
removed_keys = AgentDriveService().delete(tenant_id=app_model.tenant_id, agent_id=agent_id, prefix=f"{slug}/")
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
except Exception:
logger.exception("agent drive delete failed for skill %s (soul already updated)", slug)
return {"result": "success", "removed_keys": removed_keys, "config_version_id": config_version_id}
def _infer_skill_tools_for_app(*, app_model: App, slug: str):
query = query_params_from_request(AgentDriveMutationQuery)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
if "/" in slug or not slug.strip():
return {"code": "drive_key_invalid", "message": "skill slug must be a single path segment"}, 400
try:
return SkillToolInferenceService().infer(tenant_id=app_model.tenant_id, agent_id=agent_id, slug=slug)
except SkillToolInferenceError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
@console_ns.route("/apps/<uuid:app_id>/agent/logs") @console_ns.route("/apps/<uuid:app_id>/agent/logs")
class AgentLogApi(Resource): class AgentLogApi(Resource):
@console_ns.doc("get_agent_logs") @console_ns.doc("get_agent_logs")
@ -182,6 +379,23 @@ class AgentLogApi(Resource):
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id) return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id)
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")
class AgentSkillUploadByAgentApi(Resource):
@console_ns.doc("upload_agent_skill_by_agent")
@console_ns.doc(description="Upload + validate a Skill package for an Agent App")
@console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__])
@console_ns.response(400, "Invalid skill package")
@setup_required
@login_required
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _upload_skill_for_app(current_user=current_user)
@console_ns.route("/apps/<uuid:app_id>/agent/skills/upload") @console_ns.route("/apps/<uuid:app_id>/agent/skills/upload")
class AgentSkillUploadApi(Resource): class AgentSkillUploadApi(Resource):
@console_ns.doc("upload_agent_skill") @console_ns.doc("upload_agent_skill")
@ -192,7 +406,7 @@ class AgentSkillUploadApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_AGENT_DRIVE_APP_MODES) @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
@with_current_user @with_current_user
def post(self, current_user: Account, app_model: App): def post(self, current_user: Account, app_model: App):
"""Validate an uploaded Skill package and persist the archive. """Validate an uploaded Skill package and persist the archive.
@ -200,26 +414,28 @@ class AgentSkillUploadApi(Resource):
Returns a validated skill ref (to bind into the Agent soul config on save) Returns a validated skill ref (to bind into the Agent soul config on save)
plus its manifest. Standardizing into the agent drive is ENG-594. plus its manifest. Standardizing into the agent drive is ENG-594.
""" """
if "file" not in request.files: return _upload_skill_for_app(current_user=current_user)
return {"code": "no_file", "message": "no skill file uploaded"}, 400
if len(request.files) > 1:
return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400
upload = request.files["file"]
content = upload.stream.read()
try:
manifest = SkillPackageService().validate_and_extract(content=content, filename=upload.filename or "")
except SkillPackageError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
upload_file = FileService(db.engine).upload_file( @console_ns.route("/agent/<uuid:agent_id>/skills/standardize")
filename=upload.filename or "skill.zip", class AgentSkillStandardizeByAgentApi(Resource):
content=content, @console_ns.doc("standardize_agent_skill_by_agent")
mimetype=upload.mimetype or "application/zip", @console_ns.doc(description="Validate + standardize a Skill into an Agent App drive")
user=current_user, @console_ns.doc(params={"agent_id": "Agent ID"})
) @console_ns.response(
skill_ref = manifest.to_skill_ref(file_id=upload_file.id) 201,
return {"skill": skill_ref.model_dump(exclude_none=True), "manifest": manifest.model_dump()}, 201 "Skill standardized into drive",
console_ns.models[AgentSkillStandardizeResponse.__name__],
)
@console_ns.response(400, "Invalid skill package or no bound agent")
@setup_required
@login_required
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _standardize_skill_for_app(current_user=current_user, app_model=app_model)
@console_ns.route("/apps/<uuid:app_id>/agent/skills/standardize") @console_ns.route("/apps/<uuid:app_id>/agent/skills/standardize")
@ -236,32 +452,43 @@ class AgentSkillStandardizeApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_AGENT_DRIVE_APP_MODES) @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
@with_current_user @with_current_user
def post(self, current_user: Account, app_model: App): def post(self, current_user: Account, app_model: App):
"""Upload a Skill, validate it, and standardize it into the app agent's drive.""" """Upload a Skill, validate it, and standardize it into the app agent's drive."""
query = query_params_from_request(AgentDriveMutationQuery) return _standardize_skill_for_app(current_user=current_user, app_model=app_model)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
if "file" not in request.files:
return {"code": "no_file", "message": "no skill file uploaded"}, 400
if len(request.files) > 1:
return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400
upload = request.files["file"]
content = upload.stream.read() @console_ns.route("/agent/<uuid:agent_id>/files")
try: class AgentDriveFilesByAgentApi(Resource):
result = SkillStandardizeService().standardize( @console_ns.doc("commit_agent_drive_file_by_agent")
content=content, @console_ns.doc(description="Commit an uploaded file into the Agent App drive under files/<name>")
filename=upload.filename or "", @console_ns.doc(params={"agent_id": "Agent ID"})
tenant_id=app_model.tenant_id, @console_ns.expect(console_ns.models[AgentDriveFilePayload.__name__])
user_id=current_user.id, @console_ns.response(
agent_id=agent_id, 201, "File committed into the agent drive", console_ns.models[AgentDriveFileCommitResponse.__name__]
) )
except (SkillPackageError, AgentDriveError) as exc: @setup_required
return {"code": exc.code, "message": exc.message}, exc.status_code @login_required
return result, 201 @account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _commit_drive_file_for_app(current_user=current_user, app_model=app_model, allow_node_id=False)
@console_ns.doc("delete_agent_drive_file_by_agent")
@console_ns.doc(description="Delete one Agent App drive file by key")
@console_ns.doc(params={"agent_id": "Agent ID", **query_params_from_model(AgentDriveDeleteFileByAgentQuery)})
@console_ns.response(200, "File removed", console_ns.models[AgentDriveDeleteResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_user
@with_current_tenant_id
def delete(self, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return _delete_drive_file_for_app(current_user=current_user, app_model=app_model, allow_node_id=False)
@console_ns.route("/apps/<uuid:app_id>/agent/files") @console_ns.route("/apps/<uuid:app_id>/agent/files")
@ -276,73 +503,11 @@ class AgentDriveFilesApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_AGENT_DRIVE_APP_MODES) @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
@with_current_user @with_current_user
def post(self, current_user: Account, app_model: App): def post(self, current_user: Account, app_model: App):
"""ADD FILE: commit one uploaded file into the bound agent's drive.""" """ADD FILE: commit one uploaded file into the bound agent's drive."""
query = query_params_from_request(AgentDriveMutationQuery) return _commit_drive_file_for_app(current_user=current_user, app_model=app_model)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
upload_file = db.session.scalar(
select(UploadFile).where(
UploadFile.id == payload.upload_file_id,
UploadFile.tenant_id == app_model.tenant_id,
)
)
if upload_file is None:
return {"code": "upload_file_not_found", "message": "upload file not found in this workspace"}, 404
try:
key = normalize_drive_key(f"files/{upload_file.name}")
committed = AgentDriveService().commit(
tenant_id=app_model.tenant_id,
user_id=current_user.id,
agent_id=agent_id,
items=[
DriveCommitItem(
key=key,
file_ref=DriveFileRef(kind="upload_file", id=upload_file.id),
# ADD FILE uploads exist solely to live in the drive, so the
# drive owns (and physically cleans) the value on delete.
value_owned_by_drive=True,
)
],
)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
row = committed[0]
file_ref = AgentFileRefConfig.model_validate(
{
"id": row["key"],
"name": upload_file.name,
"file_id": upload_file.id,
"drive_key": row["key"],
"type": row.get("mime_type"),
"size": row.get("size"),
}
)
config_version_id = AgentComposerService.add_drive_file_ref(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
account_id=current_user.id,
file_ref=file_ref,
app_id=app_model.id,
node_id=query.node_id,
)
return {
"file": {
"name": upload_file.name,
"drive_key": row["key"],
"file_id": upload_file.id,
"size": row.get("size"),
"mime_type": row.get("mime_type"),
},
"config_version_id": config_version_id,
}, 201
@console_ns.doc("delete_agent_drive_file") @console_ns.doc("delete_agent_drive_file")
@console_ns.doc(description="Delete one drive file by key; soul ref first, then the KV row (ENG-625 D5)") @console_ns.doc(description="Delete one drive file by key; soul ref first, then the KV row (ENG-625 D5)")
@ -351,36 +516,26 @@ class AgentDriveFilesApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_AGENT_DRIVE_APP_MODES) @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
@with_current_user @with_current_user
def delete(self, current_user: Account, app_model: App): def delete(self, current_user: Account, app_model: App):
query = query_params_from_request(AgentDriveDeleteFileQuery) return _delete_drive_file_for_app(current_user=current_user, app_model=app_model)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
try:
key = normalize_drive_key(query.key)
except AgentDriveError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code
config_version_id = AgentComposerService.remove_drive_refs(
tenant_id=app_model.tenant_id, @console_ns.route("/agent/<uuid:agent_id>/skills/<string:slug>")
agent_id=agent_id, class AgentSkillByAgentApi(Resource):
account_id=current_user.id, @console_ns.doc("delete_agent_skill_by_agent")
file_key=key, @console_ns.doc(description="Delete a standardized skill from an Agent App drive")
app_id=app_model.id, @console_ns.doc(params={"agent_id": "Agent ID", "slug": "Skill slug (single path segment)"})
node_id=query.node_id, @console_ns.response(200, "Skill removed", console_ns.models[AgentDriveDeleteResponse.__name__])
) @setup_required
removed_keys: list[str] = [] @login_required
try: @account_initialization_required
removed_keys = AgentDriveService().delete(tenant_id=app_model.tenant_id, agent_id=agent_id, key=key) @with_current_user
except AgentDriveError as exc: @with_current_tenant_id
return {"code": exc.code, "message": exc.message}, exc.status_code def delete(self, tenant_id: str, current_user: Account, agent_id: UUID, slug: str):
except Exception: app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
# Soul-first ordering: the ref is already gone; orphan KV rows are return _delete_skill_for_app(current_user=current_user, app_model=app_model, slug=slug, allow_node_id=False)
# harmless and an idempotent DELETE retry cleans them.
logger.exception("agent drive delete failed for key %s (soul already updated)", key)
return {"result": "success", "removed_keys": removed_keys, "config_version_id": config_version_id}
@console_ns.route("/apps/<uuid:app_id>/agent/skills/<string:slug>") @console_ns.route("/apps/<uuid:app_id>/agent/skills/<string:slug>")
@ -400,34 +555,29 @@ class AgentSkillApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_AGENT_DRIVE_APP_MODES) @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
@with_current_user @with_current_user
def delete(self, current_user: Account, app_model: App, slug: str): def delete(self, current_user: Account, app_model: App, slug: str):
query = query_params_from_request(AgentDriveMutationQuery) return _delete_skill_for_app(current_user=current_user, app_model=app_model, slug=slug)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
if "/" in slug or not slug.strip():
return {"code": "drive_key_invalid", "message": "skill slug must be a single path segment"}, 400
config_version_id = AgentComposerService.remove_drive_refs(
tenant_id=app_model.tenant_id, @console_ns.route("/agent/<uuid:agent_id>/skills/<string:slug>/infer-tools")
agent_id=agent_id, class AgentSkillInferToolsByAgentApi(Resource):
account_id=current_user.id, @console_ns.doc("infer_agent_skill_tools_by_agent")
skill_slug=slug, @console_ns.doc(description="Infer CLI tool + ENV suggestions from a standardized Agent App skill")
app_id=app_model.id, @console_ns.doc(params={"agent_id": "Agent ID", "slug": "Skill slug (single path segment)"})
node_id=query.node_id, @console_ns.response(
) 200,
removed_keys: list[str] = [] "Inference result (draft suggestions, nothing persisted)",
try: console_ns.models[SkillToolInferenceResult.__name__],
removed_keys = AgentDriveService().delete( )
tenant_id=app_model.tenant_id, agent_id=agent_id, prefix=f"{slug}/" @setup_required
) @login_required
except AgentDriveError as exc: @account_initialization_required
return {"code": exc.code, "message": exc.message}, exc.status_code @with_current_tenant_id
except Exception: def post(self, tenant_id: str, agent_id: UUID, slug: str):
logger.exception("agent drive delete failed for skill %s (soul already updated)", slug) app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
return {"result": "success", "removed_keys": removed_keys, "config_version_id": config_version_id} return _infer_skill_tools_for_app(app_model=app_model, slug=slug)
@console_ns.route("/apps/<uuid:app_id>/agent/skills/<string:slug>/infer-tools") @console_ns.route("/apps/<uuid:app_id>/agent/skills/<string:slug>/infer-tools")
@ -451,16 +601,7 @@ class AgentSkillInferToolsApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_AGENT_DRIVE_APP_MODES) @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES)
def post(self, app_model: App, slug: str): def post(self, app_model: App, slug: str):
"""Suggest CLI tools/env for a skill. Saving still goes through composer validation.""" """Suggest CLI tools/env for a skill. Saving still goes through composer validation."""
query = query_params_from_request(AgentDriveMutationQuery) return _infer_skill_tools_for_app(app_model=app_model, slug=slug)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
if "/" in slug or not slug.strip():
return {"code": "drive_key_invalid", "message": "skill slug must be a single path segment"}, 400
try:
return SkillToolInferenceService().infer(tenant_id=app_model.tenant_id, agent_id=agent_id, slug=slug)
except SkillToolInferenceError as exc:
return {"code": exc.code, "message": exc.message}, exc.status_code

View File

@ -5,17 +5,18 @@ reference. This exposes the read-only "Workflow access" surface from the PRD:
which workflow apps use this Agent, without leaking the workflows' internals. which workflow apps use this Agent, without leaking the workflows' internals.
""" """
from uuid import UUID
from flask_restx import Resource from flask_restx import Resource
from pydantic import Field from pydantic import Field
from controllers.common.schema import register_response_schema_models from controllers.common.schema import register_response_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.agent.app_helpers import resolve_agent_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from extensions.ext_database import db from extensions.ext_database import db
from fields.base import ResponseModel from fields.base import ResponseModel
from libs.login import login_required from libs.login import login_required
from models.model import App, AppMode
from services.agent.roster_service import AgentRosterService from services.agent.roster_service import AgentRosterService
@ -34,23 +35,23 @@ class AgentReferencingWorkflowsResponse(ResponseModel):
register_response_schema_models(console_ns, AgentReferencingWorkflowsResponse) register_response_schema_models(console_ns, AgentReferencingWorkflowsResponse)
@console_ns.route("/apps/<uuid:app_id>/agent-referencing-workflows") @console_ns.route("/agent/<uuid:agent_id>/referencing-workflows")
class AgentAppReferencingWorkflowsResource(Resource): class AgentAppReferencingWorkflowsResource(Resource):
@console_ns.doc("list_agent_app_referencing_workflows") @console_ns.doc("list_agent_app_referencing_workflows")
@console_ns.doc(description="List workflow apps that reference this Agent App's bound Agent (read-only)") @console_ns.doc(description="List workflow apps that reference this Agent App's bound Agent (read-only)")
@console_ns.doc(params={"app_id": "Application ID"}) @console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.response( @console_ns.response(
200, 200,
"Referencing workflows listed successfully", "Referencing workflows listed successfully",
console_ns.models[AgentReferencingWorkflowsResponse.__name__], console_ns.models[AgentReferencingWorkflowsResponse.__name__],
) )
@console_ns.response(404, "App not found") @console_ns.response(404, "Agent not found")
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, app_model: App): def get(self, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
workflows = AgentRosterService(db.session).list_workflows_referencing_app_agent( workflows = AgentRosterService(db.session).list_workflows_referencing_app_agent(
tenant_id=tenant_id, app_id=app_model.id tenant_id=tenant_id, app_id=app_model.id
) )

View File

@ -9,17 +9,20 @@ persists them onto the app's ``app_model_config`` without touching anything the
Soul owns. Soul owns.
""" """
from uuid import UUID
from flask_restx import Resource from flask_restx import Resource
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from controllers.common.fields import SimpleResultResponse from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model from controllers.console.agent.app_helpers import resolve_agent_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
setup_required, setup_required,
with_current_tenant_id,
with_current_user, with_current_user,
) )
from events.app_event import app_model_config_was_updated from events.app_event import app_model_config_was_updated
@ -32,7 +35,6 @@ from models.agent_config_entities import (
AgentSuggestedQuestionsAfterAnswerFeatureConfig, AgentSuggestedQuestionsAfterAnswerFeatureConfig,
AgentTextToSpeechFeatureConfig, AgentTextToSpeechFeatureConfig,
) )
from models.model import App, AppMode
from services.agent_app_feature_service import AgentAppFeatureConfigService from services.agent_app_feature_service import AgentAppFeatureConfigService
@ -64,22 +66,23 @@ register_schema_models(console_ns, AgentAppFeaturesPayload)
register_response_schema_models(console_ns, SimpleResultResponse) register_response_schema_models(console_ns, SimpleResultResponse)
@console_ns.route("/apps/<uuid:app_id>/agent-features") @console_ns.route("/agent/<uuid:agent_id>/features")
class AgentAppFeatureConfigResource(Resource): class AgentAppFeatureConfigResource(Resource):
@console_ns.doc("update_agent_app_features") @console_ns.doc("update_agent_app_features")
@console_ns.doc(description="Update an Agent App's presentation features (opener, follow-up, citations, ...)") @console_ns.doc(description="Update an Agent App's presentation features (opener, follow-up, citations, ...)")
@console_ns.doc(params={"app_id": "Application ID"}) @console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.expect(console_ns.models[AgentAppFeaturesPayload.__name__]) @console_ns.expect(console_ns.models[AgentAppFeaturesPayload.__name__])
@console_ns.response(200, "Features updated successfully", console_ns.models[SimpleResultResponse.__name__]) @console_ns.response(200, "Features updated successfully", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(400, "Invalid configuration") @console_ns.response(400, "Invalid configuration")
@console_ns.response(404, "App not found") @console_ns.response(404, "Agent not found")
@setup_required @setup_required
@login_required @login_required
@edit_permission_required @edit_permission_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_user @with_current_user
def post(self, current_user: Account, app_model: App): @with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {}) args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {})
new_app_model_config = AgentAppFeatureConfigService.update_features( new_app_model_config = AgentAppFeatureConfigService.update_features(

View File

@ -22,6 +22,7 @@ from controllers.common.schema import (
register_schema_models, register_schema_models,
) )
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_app_model
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from fields.base import ResponseModel from fields.base import ResponseModel
@ -132,18 +133,18 @@ def _handle(exc: Exception) -> tuple[dict[str, object], int]:
raise exc raise exc
@console_ns.route("/apps/<uuid:app_id>/agent-sandbox/files") @console_ns.route("/agent/<uuid:agent_id>/sandbox/files")
class AgentAppSandboxListResource(Resource): class AgentAppSandboxListResource(Resource):
@console_ns.doc("list_agent_app_sandbox_files") @console_ns.doc("list_agent_app_sandbox_files")
@console_ns.doc(description="List a directory in an Agent App conversation sandbox") @console_ns.doc(description="List a directory in an Agent App conversation sandbox")
@console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentSandboxListQuery)}) @console_ns.doc(params={"agent_id": "Agent ID", **query_params_from_model(AgentSandboxListQuery)})
@console_ns.response(200, "Listing returned", console_ns.models[SandboxListResponse.__name__]) @console_ns.response(200, "Listing returned", console_ns.models[SandboxListResponse.__name__])
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, app_model: App): def get(self, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
query = query_params_from_request(AgentSandboxListQuery) query = query_params_from_request(AgentSandboxListQuery)
try: try:
result = AgentAppSandboxService().list_files( result = AgentAppSandboxService().list_files(
@ -157,18 +158,18 @@ class AgentAppSandboxListResource(Resource):
return result.model_dump() return result.model_dump()
@console_ns.route("/apps/<uuid:app_id>/agent-sandbox/files/read") @console_ns.route("/agent/<uuid:agent_id>/sandbox/files/read")
class AgentAppSandboxReadResource(Resource): class AgentAppSandboxReadResource(Resource):
@console_ns.doc("read_agent_app_sandbox_file") @console_ns.doc("read_agent_app_sandbox_file")
@console_ns.doc(description="Read a text/binary preview file in an Agent App conversation sandbox") @console_ns.doc(description="Read a text/binary preview file in an Agent App conversation sandbox")
@console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentSandboxFileQuery)}) @console_ns.doc(params={"agent_id": "Agent ID", **query_params_from_model(AgentSandboxFileQuery)})
@console_ns.response(200, "Preview returned", console_ns.models[SandboxReadResponse.__name__]) @console_ns.response(200, "Preview returned", console_ns.models[SandboxReadResponse.__name__])
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_tenant_id @with_current_tenant_id
def get(self, tenant_id: str, app_model: App): def get(self, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
query = query_params_from_request(AgentSandboxFileQuery) query = query_params_from_request(AgentSandboxFileQuery)
try: try:
result = AgentAppSandboxService().read_file( result = AgentAppSandboxService().read_file(
@ -182,7 +183,7 @@ class AgentAppSandboxReadResource(Resource):
return result.model_dump() return result.model_dump()
@console_ns.route("/apps/<uuid:app_id>/agent-sandbox/files/upload") @console_ns.route("/agent/<uuid:agent_id>/sandbox/files/upload")
class AgentAppSandboxUploadResource(Resource): class AgentAppSandboxUploadResource(Resource):
@console_ns.doc("upload_agent_app_sandbox_file") @console_ns.doc("upload_agent_app_sandbox_file")
@console_ns.doc(description="Upload one Agent App sandbox file as a Dify ToolFile mapping") @console_ns.doc(description="Upload one Agent App sandbox file as a Dify ToolFile mapping")
@ -191,9 +192,9 @@ class AgentAppSandboxUploadResource(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=[AppMode.AGENT])
@with_current_tenant_id @with_current_tenant_id
def post(self, tenant_id: str, app_model: App): def post(self, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
payload = AgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {}) payload = AgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
try: try:
result = AgentAppSandboxService().upload_file( result = AgentAppSandboxService().upload_file(

View File

@ -10,6 +10,8 @@ backend — drive data lives in the API's own DB/storage, served straight from
from __future__ import annotations from __future__ import annotations
from uuid import UUID
from flask_restx import Resource from flask_restx import Resource
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -19,8 +21,9 @@ from controllers.common.schema import (
register_response_schema_models, register_response_schema_models,
) )
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_app_model
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import account_initialization_required, setup_required from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from fields.base import ResponseModel from fields.base import ResponseModel
from libs.login import login_required from libs.login import login_required
from models.model import App, AppMode from models.model import App, AppMode
@ -33,11 +36,19 @@ class AgentDriveListQuery(BaseModel):
node_id: str | None = Field(default=None, description="Workflow node ID (workflow composer variant)") node_id: str | None = Field(default=None, description="Workflow node ID (workflow composer variant)")
class AgentDriveListByAgentQuery(BaseModel):
prefix: str = Field(default="", description="Key prefix filter: '<slug>/' for one skill, 'files/' for files")
class AgentDriveFileQuery(BaseModel): class AgentDriveFileQuery(BaseModel):
key: str = Field(min_length=1, description="Drive key, e.g. tender-analyzer/SKILL.md") key: str = Field(min_length=1, description="Drive key, e.g. tender-analyzer/SKILL.md")
node_id: str | None = Field(default=None, description="Workflow node ID (workflow composer variant)") node_id: str | None = Field(default=None, description="Workflow node ID (workflow composer variant)")
class AgentDriveFileByAgentQuery(BaseModel):
key: str = Field(min_length=1, description="Drive key, e.g. tender-analyzer/SKILL.md")
class AgentDriveItemResponse(ResponseModel): class AgentDriveItemResponse(ResponseModel):
key: str key: str
size: int | None = None size: int | None = None
@ -85,7 +96,66 @@ def _handle(exc: AgentDriveError) -> tuple[dict[str, object], int]:
return {"code": exc.code, "message": exc.message}, exc.status_code return {"code": exc.code, "message": exc.message}, exc.status_code
_APP_MODES = [AppMode.AGENT, AppMode.WORKFLOW, AppMode.ADVANCED_CHAT] _WORKFLOW_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]
@console_ns.route("/agent/<uuid:agent_id>/drive/files")
class AgentDriveListByAgentApi(Resource):
@console_ns.doc("list_agent_drive_files_by_agent")
@console_ns.doc(description="List agent drive entries for an Agent App")
@console_ns.doc(params={"agent_id": "Agent ID", **query_params_from_model(AgentDriveListByAgentQuery)})
@console_ns.response(200, "Drive entries", console_ns.models[AgentDriveListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID):
query = query_params_from_request(AgentDriveListByAgentQuery)
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
items = AgentDriveService().manifest(tenant_id=tenant_id, agent_id=str(agent_id), prefix=query.prefix)
except AgentDriveError as exc:
return _handle(exc)
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/files/preview")
class AgentDrivePreviewByAgentApi(Resource):
@console_ns.doc("preview_agent_drive_file_by_agent")
@console_ns.doc(description="Truncated text preview of one Agent App drive value")
@console_ns.doc(params={"agent_id": "Agent ID", **query_params_from_model(AgentDriveFileByAgentQuery)})
@console_ns.response(200, "Preview", console_ns.models[AgentDrivePreviewResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID):
query = query_params_from_request(AgentDriveFileByAgentQuery)
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
return AgentDriveService().preview(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
except AgentDriveError as exc:
return _handle(exc)
@console_ns.route("/agent/<uuid:agent_id>/drive/files/download")
class AgentDriveDownloadByAgentApi(Resource):
@console_ns.doc("download_agent_drive_file_by_agent")
@console_ns.doc(description="Time-limited external signed URL for one Agent App drive value")
@console_ns.doc(params={"agent_id": "Agent ID", **query_params_from_model(AgentDriveFileByAgentQuery)})
@console_ns.response(200, "Signed URL", console_ns.models[AgentDriveDownloadResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID):
query = query_params_from_request(AgentDriveFileByAgentQuery)
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
url = AgentDriveService().download_url(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key)
except AgentDriveError as exc:
return _handle(exc)
return {"url": url}
@console_ns.route("/apps/<uuid:app_id>/agent/drive/files") @console_ns.route("/apps/<uuid:app_id>/agent/drive/files")
@ -97,7 +167,7 @@ class AgentDriveListApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_APP_MODES) @get_app_model(mode=_WORKFLOW_APP_MODES)
def get(self, app_model: App): def get(self, app_model: App):
query = query_params_from_request(AgentDriveListQuery) query = query_params_from_request(AgentDriveListQuery)
agent_id = _resolve_agent_id(app_model, query.node_id) agent_id = _resolve_agent_id(app_model, query.node_id)
@ -121,7 +191,7 @@ class AgentDrivePreviewApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_APP_MODES) @get_app_model(mode=_WORKFLOW_APP_MODES)
def get(self, app_model: App): def get(self, app_model: App):
query = query_params_from_request(AgentDriveFileQuery) query = query_params_from_request(AgentDriveFileQuery)
agent_id = _resolve_agent_id(app_model, query.node_id) agent_id = _resolve_agent_id(app_model, query.node_id)
@ -142,7 +212,7 @@ class AgentDriveDownloadApi(Resource):
@setup_required @setup_required
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model(mode=_APP_MODES) @get_app_model(mode=_WORKFLOW_APP_MODES)
def get(self, app_model: App): def get(self, app_model: App):
query = query_params_from_request(AgentDriveFileQuery) query = query_params_from_request(AgentDriveFileQuery)
agent_id = _resolve_agent_id(app_model, query.node_id) agent_id = _resolve_agent_id(app_model, query.node_id)
@ -157,6 +227,9 @@ class AgentDriveDownloadApi(Resource):
__all__ = [ __all__ = [
"AgentDriveDownloadApi", "AgentDriveDownloadApi",
"AgentDriveDownloadByAgentApi",
"AgentDriveListApi", "AgentDriveListApi",
"AgentDriveListByAgentApi",
"AgentDrivePreviewApi", "AgentDrivePreviewApi",
"AgentDrivePreviewByAgentApi",
] ]

View File

@ -64,7 +64,7 @@ from services.entities.knowledge_entities.knowledge_entities import (
) )
from services.feature_service import FeatureService from services.feature_service import FeatureService
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "agent", "advanced-chat", "workflow", "completion"] ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
register_enum_models(console_ns, IconType) register_enum_models(console_ns, IconType)
@ -163,9 +163,7 @@ def _normalize_app_list_query_args(query_args: MultiDict[str, str]) -> dict[str,
class CreateAppPayload(BaseModel): class CreateAppPayload(BaseModel):
name: str = Field(..., min_length=1, description="App name") name: str = Field(..., min_length=1, description="App name")
description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400) description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400)
mode: Literal["chat", "agent-chat", "agent", "advanced-chat", "workflow", "completion"] = Field( mode: Literal["chat", "agent-chat", "advanced-chat", "workflow", "completion"] = Field(..., description="App mode")
..., description="App mode"
)
icon_type: IconType | None = Field(default=None, description="Icon type") icon_type: IconType | None = Field(default=None, description="Icon type")
icon: str | None = Field(default=None, description="Icon") icon: str | None = Field(default=None, description="Icon")
icon_background: str | None = Field(default=None, description="Icon background color") icon_background: str | None = Field(default=None, description="Icon background color")
@ -400,6 +398,8 @@ class AppPartial(ResponseModel):
create_user_name: str | None = None create_user_name: str | None = None
author_name: str | None = None author_name: str | None = None
has_draft_trigger: bool | None = None has_draft_trigger: bool | None = None
# For Agent App type: the roster Agent backing this app (None otherwise).
bound_agent_id: str | None = None
is_starred: bool = False is_starred: bool = False
@computed_field(return_type=str | None) # type: ignore @computed_field(return_type=str | None) # type: ignore

View File

@ -293,22 +293,42 @@ Check if activation token is valid
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Success | **application/json**: [ActivationCheckResponse](#activationcheckresponse)<br> | | 200 | Success | **application/json**: [ActivationCheckResponse](#activationcheckresponse)<br> |
### [GET] /agents ### [GET] /agent
#### Parameters #### Parameters
| Name | Located in | Description | Required | Schema | | Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ | | ---- | ---------- | ----------- | -------- | ------ |
| keyword | query | | No | string | | creator_ids | query | Filter by creator account IDs | No | [ string ] |
| limit | query | | No | integer, <br>**Default:** 20 | | is_created_by_me | query | Filter by creator | No | boolean |
| page | query | | No | integer, <br>**Default:** 1 | | limit | query | Page size (1-100) | No | integer, <br>**Default:** 20 |
| mode | query | App mode filter | No | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow", <br>**Default:** all |
| name | query | Filter by app name | No | string |
| page | query | Page number (1-99999) | No | integer, <br>**Default:** 1 |
| sort_by | query | Sort apps by last modified, recently created, or earliest created | No | string, <br>**Available values:** "earliest_created", "last_modified", "recently_created", <br>**Default:** last_modified |
| tag_ids | query | Filter by tag IDs | No | [ string ] |
#### Responses #### Responses
| Code | Description | Schema | | Code | Description | Schema |
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Agent roster list | **application/json**: [AgentRosterListResponse](#agentrosterlistresponse)<br> | | 200 | Agent app list | **application/json**: [AppPagination](#apppagination)<br> |
### [GET] /agents/invite-options ### [POST] /agent
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentAppCreatePayload](#agentappcreatepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Agent app created successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
| 400 | Invalid request parameters | |
| 403 | Insufficient permissions | |
### [GET] /agent/invite-options
#### Parameters #### Parameters
| Name | Located in | Description | Required | Schema | | Name | Located in | Description | Required | Schema |
@ -324,7 +344,21 @@ Check if activation token is valid
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Agent invite options | **application/json**: [AgentInviteOptionsResponse](#agentinviteoptionsresponse)<br> | | 200 | Agent invite options | **application/json**: [AgentInviteOptionsResponse](#agentinviteoptionsresponse)<br> |
### [GET] /agents/{agent_id} ### [DELETE] /agent/{agent_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Responses
| Code | Description |
| ---- | ----------- |
| 204 | Agent app deleted successfully |
| 403 | Insufficient permissions |
### [GET] /agent/{agent_id}
#### Parameters #### Parameters
| Name | Located in | Description | Required | Schema | | Name | Located in | Description | Required | Schema |
@ -335,9 +369,337 @@ Check if activation token is valid
| Code | Description | Schema | | Code | Description | Schema |
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Agent detail | **application/json**: [AgentRosterResponse](#agentrosterresponse)<br> | | 200 | Agent app detail | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
### [GET] /agents/{agent_id}/versions ### [PUT] /agent/{agent_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [UpdateAppPayload](#updateapppayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app updated successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)<br> |
| 400 | Invalid request parameters | |
| 403 | Insufficient permissions | |
### [GET] /agent/{agent_id}/composer
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer state | **application/json**: [AgentAppComposerResponse](#agentappcomposerresponse)<br> |
### [PUT] /agent/{agent_id}/composer
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer saved | **application/json**: [AgentAppComposerResponse](#agentappcomposerresponse)<br> |
### [GET] /agent/{agent_id}/composer/candidates
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer candidates | **application/json**: [AgentComposerCandidatesResponse](#agentcomposercandidatesresponse)<br> |
### [POST] /agent/{agent_id}/composer/validate
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer validation result | **application/json**: [AgentComposerValidateResponse](#agentcomposervalidateresponse)<br> |
### [GET] /agent/{agent_id}/drive/files
List agent drive entries for an Agent App
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| prefix | query | Key prefix filter: '<slug>/' for one skill, 'files/' for files | No | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Drive entries | **application/json**: [AgentDriveListResponse](#agentdrivelistresponse)<br> |
### [GET] /agent/{agent_id}/drive/files/download
Time-limited external signed URL for one Agent App drive value
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| key | query | Drive key, e.g. tender-analyzer/SKILL.md | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Signed URL | **application/json**: [AgentDriveDownloadResponse](#agentdrivedownloadresponse)<br> |
### [GET] /agent/{agent_id}/drive/files/preview
Truncated text preview of one Agent App drive value
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| key | query | Drive key, e.g. tender-analyzer/SKILL.md | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Preview | **application/json**: [AgentDrivePreviewResponse](#agentdrivepreviewresponse)<br> |
### [POST] /agent/{agent_id}/features
Update an Agent App's presentation features (opener, follow-up, citations, ...)
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentAppFeaturesPayload](#agentappfeaturespayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Features updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 400 | Invalid configuration | |
| 404 | Agent not found | |
### [DELETE] /agent/{agent_id}/files
Delete one Agent App drive file by key
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| key | query | Drive key, e.g. files/sample.pdf | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | File removed | **application/json**: [AgentDriveDeleteResponse](#agentdrivedeleteresponse)<br> |
### [POST] /agent/{agent_id}/files
Commit an uploaded file into the Agent App drive under files/<name>
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentDriveFilePayload](#agentdrivefilepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | File committed into the agent drive | **application/json**: [AgentDriveFileCommitResponse](#agentdrivefilecommitresponse)<br> |
### [GET] /agent/{agent_id}/referencing-workflows
List workflow apps that reference this Agent App's bound Agent (read-only)
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Referencing workflows listed successfully | **application/json**: [AgentReferencingWorkflowsResponse](#agentreferencingworkflowsresponse)<br> |
| 404 | Agent not found | |
### [GET] /agent/{agent_id}/sandbox/files
List a directory in an Agent App conversation sandbox
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| conversation_id | query | Agent App conversation ID | Yes | string |
| path | query | Directory path relative to the sandbox workspace | No | string, <br>**Default:** . |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Listing returned | **application/json**: [SandboxListResponse](#sandboxlistresponse)<br> |
### [GET] /agent/{agent_id}/sandbox/files/read
Read a text/binary preview file in an Agent App conversation sandbox
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| conversation_id | query | Agent App conversation ID | Yes | string |
| path | query | File path relative to the sandbox workspace | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
### [POST] /agent/{agent_id}/sandbox/files/upload
Upload one Agent App sandbox file as a Dify ToolFile mapping
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentSandboxUploadPayload](#agentsandboxuploadpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)<br> |
### [POST] /agent/{agent_id}/skills/standardize
Validate + standardize a Skill into an Agent App drive
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill standardized into drive | **application/json**: [AgentSkillStandardizeResponse](#agentskillstandardizeresponse)<br> |
| 400 | Invalid skill package or no bound agent | |
### [POST] /agent/{agent_id}/skills/upload
Upload + validate a Skill package for an Agent App
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill validated | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)<br> |
| 400 | Invalid skill package | |
### [DELETE] /agent/{agent_id}/skills/{slug}
Delete a standardized skill from an Agent App drive
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| slug | path | Skill slug (single path segment) | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill removed | **application/json**: [AgentDriveDeleteResponse](#agentdrivedeleteresponse)<br> |
### [POST] /agent/{agent_id}/skills/{slug}/infer-tools
Infer CLI tool + ENV suggestions from a standardized Agent App skill
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| slug | path | Skill slug (single path segment) | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Inference result (draft suggestions, nothing persisted) | **application/json**: [SkillToolInferenceResult](#skilltoolinferenceresult)<br> |
### [GET] /agent/{agent_id}/versions
#### Parameters #### Parameters
| Name | Located in | Description | Required | Schema | | Name | Located in | Description | Required | Schema |
@ -350,7 +712,7 @@ Check if activation token is valid
| ---- | ----------- | ------ | | ---- | ----------- | ------ |
| 200 | Agent versions | **application/json**: [AgentConfigSnapshotListResponse](#agentconfigsnapshotlistresponse)<br> | | 200 | Agent versions | **application/json**: [AgentConfigSnapshotListResponse](#agentconfigsnapshotlistresponse)<br> |
### [GET] /agents/{agent_id}/versions/{version_id} ### [GET] /agent/{agent_id}/versions/{version_id}
#### Parameters #### Parameters
| Name | Located in | Description | Required | Schema | | Name | Located in | Description | Required | Schema |
@ -862,164 +1224,6 @@ Run draft workflow for advanced chat application
| 400 | Invalid request parameters | | | 400 | Invalid request parameters | |
| 403 | Permission denied | | | 403 | Permission denied | |
### [GET] /apps/{app_id}/agent-composer
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer state | **application/json**: [AgentAppComposerResponse](#agentappcomposerresponse)<br> |
### [PUT] /apps/{app_id}/agent-composer
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer saved | **application/json**: [AgentAppComposerResponse](#agentappcomposerresponse)<br> |
### [GET] /apps/{app_id}/agent-composer/candidates
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer candidates | **application/json**: [AgentComposerCandidatesResponse](#agentcomposercandidatesresponse)<br> |
### [POST] /apps/{app_id}/agent-composer/validate
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent app composer validation result | **application/json**: [AgentComposerValidateResponse](#agentcomposervalidateresponse)<br> |
### [POST] /apps/{app_id}/agent-features
Update an Agent App's presentation features (opener, follow-up, citations, ...)
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentAppFeaturesPayload](#agentappfeaturespayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Features updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 400 | Invalid configuration | |
| 404 | App not found | |
### [GET] /apps/{app_id}/agent-referencing-workflows
List workflow apps that reference this Agent App's bound Agent (read-only)
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Referencing workflows listed successfully | **application/json**: [AgentReferencingWorkflowsResponse](#agentreferencingworkflowsresponse)<br> |
| 404 | App not found | |
### [GET] /apps/{app_id}/agent-sandbox/files
List a directory in an Agent App conversation sandbox
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string |
| conversation_id | query | Agent App conversation ID | Yes | string |
| path | query | Directory path relative to the sandbox workspace | No | string, <br>**Default:** . |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Listing returned | **application/json**: [SandboxListResponse](#sandboxlistresponse)<br> |
### [GET] /apps/{app_id}/agent-sandbox/files/read
Read a text/binary preview file in an Agent App conversation sandbox
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | Application ID | Yes | string |
| conversation_id | query | Agent App conversation ID | Yes | string |
| path | query | File path relative to the sandbox workspace | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
### [POST] /apps/{app_id}/agent-sandbox/files/upload
Upload one Agent App sandbox file as a Dify ToolFile mapping
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| app_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentSandboxUploadPayload](#agentsandboxuploadpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)<br> |
### [GET] /apps/{app_id}/agent/drive/files ### [GET] /apps/{app_id}/agent/drive/files
List agent drive entries (read-only inspector; one endpoint for both tabs) List agent drive entries (read-only inspector; one endpoint for both tabs)
@ -10982,6 +11186,16 @@ Default namespace
| validation | [ComposerValidationFindingsResponse](#composervalidationfindingsresponse) | | No | | validation | [ComposerValidationFindingsResponse](#composervalidationfindingsresponse) | | No |
| variant | string | | Yes | | variant | string | | Yes |
#### AgentAppCreatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | Agent description (max 400 chars) | No |
| icon | string | Icon | No |
| icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No |
| name | string | Agent name | Yes |
#### AgentAppFeaturesPayload #### AgentAppFeaturesPayload
Presentation features configurable on an Agent App. Presentation features configurable on an Agent App.
@ -11239,6 +11453,12 @@ Audit operation recorded for Agent Soul version/revision changes.
| version | integer | | Yes | | version | integer | | Yes |
| version_note | string | | No | | version_note | string | | No |
#### AgentDriveDeleteFileByAgentQuery
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| key | string | Drive key, e.g. files/sample.pdf | Yes |
#### AgentDriveDeleteResponse #### AgentDriveDeleteResponse
| Name | Type | Description | Required | | Name | Type | Description | Required |
@ -12182,7 +12402,6 @@ Enum class for api provider schema type.
| ---- | ---- | ----------- | -------- | | ---- | ---- | ----------- | -------- |
| access_mode | string | | No | | access_mode | string | | No |
| api_base_url | string | | No | | api_base_url | string | | No |
| app_model_config | [ModelConfig](#modelconfig) | | No |
| bound_agent_id | string | | No | | bound_agent_id | string | | No |
| created_at | integer | | No | | created_at | integer | | No |
| created_by | string | | No | | created_by | string | | No |
@ -12193,9 +12412,11 @@ Enum class for api provider schema type.
| icon | string | | No | | icon | string | | No |
| icon_background | string | | No | | icon_background | string | | No |
| icon_type | string | | No | | icon_type | string | | No |
| icon_url | string | | Yes |
| id | string | | Yes | | id | string | | Yes |
| max_active_requests | integer | | No | | max_active_requests | integer | | No |
| mode_compatible_with_agent | string | | Yes | | mode | string | | Yes |
| model_config | [ModelConfig](#modelconfig) | | No |
| name | string | | Yes | | name | string | | Yes |
| site | [Site](#site) | | No | | site | [Site](#site) | | No |
| tags | [ [Tag](#tag) ] | | No | | tags | [ [Tag](#tag) ] | | No |
@ -12290,10 +12511,10 @@ AppMCPServer Status Enum
| Name | Type | Description | Required | | Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- | | ---- | ---- | ----------- | -------- |
| has_next | boolean | | Yes | | data | [ [AppPartial](#apppartial) ] | | Yes |
| items | [ [AppPartial](#apppartial) ] | | Yes | | has_more | boolean | | Yes |
| limit | integer | | Yes |
| page | integer | | Yes | | page | integer | | Yes |
| per_page | integer | | Yes |
| total | integer | | Yes | | total | integer | | Yes |
#### AppPartial #### AppPartial
@ -12301,20 +12522,22 @@ AppMCPServer Status Enum
| Name | Type | Description | Required | | Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- | | ---- | ---- | ----------- | -------- |
| access_mode | string | | No | | access_mode | string | | No |
| app_model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
| author_name | string | | No | | author_name | string | | No |
| bound_agent_id | string | | No |
| create_user_name | string | | No | | create_user_name | string | | No |
| created_at | integer | | No | | created_at | integer | | No |
| created_by | string | | No | | created_by | string | | No |
| desc_or_prompt | string | | No | | description | string | | No |
| has_draft_trigger | boolean | | No | | has_draft_trigger | boolean | | No |
| icon | string | | No | | icon | string | | No |
| icon_background | string | | No | | icon_background | string | | No |
| icon_type | string | | No | | icon_type | string | | No |
| icon_url | string | | Yes |
| id | string | | Yes | | id | string | | Yes |
| is_starred | boolean | | No | | is_starred | boolean | | No |
| max_active_requests | integer | | No | | max_active_requests | integer | | No |
| mode_compatible_with_agent | string | | Yes | | mode | string | | Yes |
| model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
| name | string | | Yes | | name | string | | Yes |
| tags | [ [Tag](#tag) ] | | No | | tags | [ [Tag](#tag) ] | | No |
| updated_at | integer | | No | | updated_at | integer | | No |
@ -13109,7 +13332,7 @@ Enum class for configurate method of provider model.
| icon | string | Icon | No | | icon | string | Icon | No |
| icon_background | string | Icon background color | No | | icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No | | icon_type | [IconType](#icontype) | Icon type | No |
| mode | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "chat", "completion", "workflow" | App mode<br>*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"chat"`, `"completion"`, `"workflow"` | Yes | | mode | string, <br>**Available values:** "advanced-chat", "agent-chat", "chat", "completion", "workflow" | App mode<br>*Enum:* `"advanced-chat"`, `"agent-chat"`, `"chat"`, `"completion"`, `"workflow"` | Yes |
| name | string | App name | Yes | | name | string | App name | Yes |
#### CreateSnippetPayload #### CreateSnippetPayload
@ -15562,7 +15785,7 @@ Metadata operation data
| ---- | ---- | ----------- | -------- | | ---- | ---- | ----------- | -------- |
| created_at | integer | | No | | created_at | integer | | No |
| created_by | string | | No | | created_by | string | | No |
| model_dict | [JSONValue](#jsonvalue) | | No | | model | [JSONValue](#jsonvalue) | | No |
| pre_prompt | string | | No | | pre_prompt | string | | No |
| updated_at | integer | | No | | updated_at | integer | | No |
| updated_by | string | | No | | updated_by | string | | No |

View File

@ -19,7 +19,7 @@ from models.agent import (
) )
from models.agent_config_entities import AgentSoulConfig from models.agent_config_entities import AgentSoulConfig
from models.enums import AppStatus from models.enums import AppStatus
from models.model import App from models.model import App, AppMode
from models.workflow import Workflow from models.workflow import Workflow
from services.agent.agent_soul_state import agent_soul_has_model from services.agent.agent_soul_state import agent_soul_has_model
from services.agent.composer_validator import ComposerConfigValidator from services.agent.composer_validator import ComposerConfigValidator
@ -349,6 +349,43 @@ class AgentRosterService:
) )
) )
def get_agent_app_model(self, *, tenant_id: str, agent_id: str) -> App:
"""Resolve the Agent App hidden behind an app-backed Agent id.
The public /agent route uses Agent ids, while the runtime and legacy app
APIs still operate on App ids internally. Only app-backed roster Agents
are accepted here; workflow-only Agents and historical standalone roster
Agents are not Agent App resources.
"""
agent = self._session.scalar(
select(Agent)
.where(
Agent.tenant_id == tenant_id,
Agent.id == agent_id,
Agent.scope == AgentScope.ROSTER,
Agent.source == AgentSource.AGENT_APP,
Agent.app_id.is_not(None),
Agent.status == AgentStatus.ACTIVE,
)
.limit(1)
)
if agent is None or agent.app_id is None:
raise AgentNotFoundError()
app = self._session.scalar(
select(App)
.where(
App.tenant_id == tenant_id,
App.id == agent.app_id,
App.mode == AppMode.AGENT,
App.status == AppStatus.NORMAL,
)
.limit(1)
)
if app is None:
raise AgentNotFoundError()
return app
def list_workflows_referencing_app_agent(self, *, tenant_id: str, app_id: str) -> list[AgentReferencingWorkflow]: def list_workflows_referencing_app_agent(self, *, tenant_id: str, app_id: str) -> list[AgentReferencingWorkflow]:
"""List the workflow apps that reference this Agent App's bound Agent. """List the workflow apps that reference this Agent App's bound Agent.

View File

@ -1,16 +1,17 @@
from inspect import unwrap from inspect import unwrap
from types import SimpleNamespace from types import SimpleNamespace
from typing import cast from typing import Any, cast
import pytest import pytest
from flask import Flask from flask import Flask
from controllers.console import console_ns
from controllers.console.agent import composer as composer_controller from controllers.console.agent import composer as composer_controller
from controllers.console.agent import roster as roster_controller from controllers.console.agent import roster as roster_controller
from controllers.console.agent.composer import ( from controllers.console.agent.composer import (
AgentAppComposerApi, AgentComposerApi,
AgentAppComposerCandidatesApi, AgentComposerCandidatesApi,
AgentAppComposerValidateApi, AgentComposerValidateApi,
WorkflowAgentComposerApi, WorkflowAgentComposerApi,
WorkflowAgentComposerCandidatesApi, WorkflowAgentComposerCandidatesApi,
WorkflowAgentComposerImpactApi, WorkflowAgentComposerImpactApi,
@ -18,42 +19,15 @@ from controllers.console.agent.composer import (
WorkflowAgentComposerValidateApi, WorkflowAgentComposerValidateApi,
) )
from controllers.console.agent.roster import ( from controllers.console.agent.roster import (
AgentAppApi,
AgentAppListApi,
AgentInviteOptionsApi, AgentInviteOptionsApi,
AgentRosterDetailApi,
AgentRosterListApi,
AgentRosterVersionDetailApi, AgentRosterVersionDetailApi,
AgentRosterVersionsApi, AgentRosterVersionsApi,
) )
from models.model import AppMode
from services.entities.agent_entities import ComposerSaveStrategy, ComposerVariant from services.entities.agent_entities import ComposerSaveStrategy, ComposerVariant
def _agent_response(agent_id: str = "agent-1") -> dict:
return {
"id": agent_id,
"name": "Analyst",
"description": "",
"icon_type": None,
"icon": None,
"icon_background": None,
"agent_kind": "dify_agent",
"scope": "roster",
"source": "agent_app",
"app_id": None,
"workflow_id": None,
"workflow_node_id": None,
"active_config_snapshot_id": "version-1",
"active_config_snapshot": _version_response(),
"status": "active",
"created_by": "account-1",
"updated_by": "account-1",
"archived_by": None,
"archived_at": None,
"created_at": None,
"updated_at": None,
}
def _version_response(version_id: str = "version-1") -> dict: def _version_response(version_id: str = "version-1") -> dict:
return { return {
"id": version_id, "id": version_id,
@ -103,6 +77,37 @@ def _agent_app_composer_response() -> dict:
} }
def _app_detail_obj(**overrides):
data = {
"id": "app-1",
"name": "Iris",
"description": "Agent app",
"mode_compatible_with_agent": "agent",
"icon_type": "emoji",
"icon": "robot",
"icon_background": "#fff",
"enable_site": False,
"enable_api": False,
"app_model_config": None,
"workflow": None,
"tracing": None,
"use_icon_as_answer_icon": False,
"created_by": "account-1",
"created_at": None,
"updated_by": "account-1",
"updated_at": None,
"access_mode": None,
"tags": [],
"api_base_url": None,
"max_active_requests": 0,
"deleted_tools": [],
"site": None,
"bound_agent_id": "00000000-0000-0000-0000-000000000001",
}
data.update(overrides)
return SimpleNamespace(**data)
def _candidates_response(variant: str) -> dict: def _candidates_response(variant: str) -> dict:
return { return {
"variant": variant, "variant": variant,
@ -112,20 +117,38 @@ def _candidates_response(variant: str) -> dict:
} }
def _get_app_model_modes(view) -> list[AppMode]: def test_agent_v2_console_routes_are_agent_id_first() -> None:
current = view paths = {route for item in console_ns.resources for route in item.urls}
while current is not None:
closure = getattr(current, "__closure__", None) for route in (
if closure is not None: "/agent",
for cell in closure: "/agent/<uuid:agent_id>",
try: "/agent/<uuid:agent_id>/composer",
value = cell.cell_contents "/agent/<uuid:agent_id>/composer/validate",
except ValueError: "/agent/<uuid:agent_id>/composer/candidates",
continue "/agent/<uuid:agent_id>/features",
if isinstance(value, list) and all(isinstance(item, AppMode) for item in value): "/agent/<uuid:agent_id>/referencing-workflows",
return value "/agent/<uuid:agent_id>/drive/files",
current = getattr(current, "__wrapped__", None) "/agent/<uuid:agent_id>/sandbox/files",
return [] "/agent/<uuid:agent_id>/skills/upload",
"/agent/<uuid:agent_id>/files",
"/agent/invite-options",
):
assert route in paths
for route in (
"/agents",
"/agents/invite-options",
"/agents/<uuid:agent_id>",
"/agents/<uuid:agent_id>/versions",
"/apps/<uuid:app_id>/agent-composer",
"/apps/<uuid:app_id>/agent-composer/validate",
"/apps/<uuid:app_id>/agent-composer/candidates",
"/apps/<uuid:app_id>/agent-features",
"/apps/<uuid:app_id>/agent-referencing-workflows",
"/apps/<uuid:app_id>/agent-sandbox/files",
):
assert route not in paths
@pytest.fixture @pytest.fixture
@ -133,26 +156,114 @@ def account_id() -> str:
return "account-1" return "account-1"
def test_roster_list_get_parses_query_and_calls_service(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: def test_agent_app_list_and_create_use_agent_route(
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
) -> None:
captured: dict[str, object] = {} captured: dict[str, object] = {}
def list_roster_agents(_self: object, **kwargs: object) -> dict[str, object]: class FakeAppService:
captured.update(kwargs) def get_app(self, app_obj: object) -> object:
return {"data": [], "page": kwargs["page"], "limit": kwargs["limit"], "total": 0, "has_more": False} return app_obj
monkeypatch.setattr(roster_controller.AgentRosterService, "list_roster_agents", list_roster_agents) def get_paginate_apps(self, user_id: str, tenant_id: str, params) -> object:
captured["list"] = {"user_id": user_id, "tenant_id": tenant_id, "params": params}
return SimpleNamespace(
page=1,
per_page=10,
total=1,
has_next=False,
items=[_app_detail_obj(id="app-list", bound_agent_id="agent-list")],
)
with app.test_request_context("/console/api/agents?page=2&limit=5&keyword=analyst"): def create_app(self, tenant_id: str, params, current_user: object) -> object:
result = unwrap(AgentRosterListApi.get)(AgentRosterListApi(), "tenant-1") captured["create"] = {"tenant_id": tenant_id, "params": params, "current_user": current_user}
return _app_detail_obj(id="app-created", bound_agent_id="agent-created")
assert result["page"] == 2 monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
assert captured == {"tenant_id": "tenant-1", "page": 2, "limit": 5, "keyword": "analyst"} monkeypatch.setattr(
roster_controller.FeatureService,
"get_system_features",
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
)
with app.test_request_context("/console/api/agent?page=1&limit=10&mode=workflow"):
listed = unwrap(AgentAppListApi.get)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id))
assert listed["page"] == 1
assert listed["limit"] == 10
assert listed["total"] == 1
assert listed["data"][0]["id"] == "agent-list"
assert "bound_agent_id" not in listed["data"][0]
list_call = cast(dict[str, object], captured["list"])
list_params = cast(Any, list_call["params"])
assert list_params.mode == "agent"
assert list_params.status == "normal"
with app.test_request_context(
"/console/api/agent",
json={"name": "Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"},
):
created, status = unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id))
assert status == 201
assert created["id"] == "agent-created"
assert "bound_agent_id" not in created
create_call = cast(dict[str, object], captured["create"])
create_params = cast(Any, create_call["params"])
assert create_params.mode == "agent"
def test_roster_direct_mutation_endpoints_are_not_exposed() -> None: def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
assert not hasattr(AgentRosterListApi, "post") app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
assert not hasattr(AgentRosterDetailApi, "patch") ) -> None:
assert not hasattr(AgentRosterDetailApi, "delete") agent_id = "00000000-0000-0000-0000-000000000001"
app_model = _app_detail_obj(id="app-1", bound_agent_id=agent_id)
captured: dict[str, object] = {}
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_agent_app_model",
lambda _self, **kwargs: app_model,
)
monkeypatch.setattr(
roster_controller.FeatureService,
"get_system_features",
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
)
class FakeAppService:
def get_app(self, app_obj: object) -> object:
captured["get_app"] = app_obj
return app_obj
def update_app(self, app_obj: object, args: dict[str, object]) -> object:
captured["update"] = {"app": app_obj, "args": args}
return _app_detail_obj(id="app-1", name=args["name"], bound_agent_id=agent_id)
def delete_app(self, app_obj: object) -> None:
captured["delete"] = app_obj
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", agent_id)
assert detail["id"] == agent_id
assert "bound_agent_id" not in detail
with app.test_request_context(
"/console/api/agent/00000000-0000-0000-0000-000000000001",
json={"name": "Renamed", "description": "", "icon_type": "emoji", "icon": "R"},
):
updated = unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id)
assert updated["name"] == "Renamed"
assert updated["id"] == agent_id
assert "bound_agent_id" not in updated
update_call = cast(dict[str, object], captured["update"])
assert update_call["app"] is app_model
deleted, status = unwrap(AgentAppApi.delete)(AgentAppApi(), "tenant-1", agent_id)
assert (deleted, status) == ("", 204)
assert captured["delete"] is app_model
def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@ -164,21 +275,16 @@ def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.Monkey
monkeypatch.setattr(roster_controller.AgentRosterService, "list_invite_options", list_invite_options) monkeypatch.setattr(roster_controller.AgentRosterService, "list_invite_options", list_invite_options)
with app.test_request_context("/console/api/agents/invite-options?page=1&limit=10&app_id=app-1"): with app.test_request_context("/console/api/agent/invite-options?page=1&limit=10&app_id=app-1"):
result = unwrap(AgentInviteOptionsApi.get)(AgentInviteOptionsApi(), "tenant-1") result = unwrap(AgentInviteOptionsApi.get)(AgentInviteOptionsApi(), "tenant-1")
assert result == {"data": [], "page": 1, "limit": 10, "total": 0, "has_more": False} assert result == {"data": [], "page": 1, "limit": 10, "total": 0, "has_more": False}
assert captured == {"tenant_id": "tenant-1", "page": 1, "limit": 10, "keyword": None, "app_id": "app-1"} assert captured == {"tenant_id": "tenant-1", "page": 1, "limit": 10, "keyword": None, "app_id": "app-1"}
def test_roster_detail_and_versions_call_services(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: def test_agent_versions_call_services(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
agent_id = "00000000-0000-0000-0000-000000000001" agent_id = "00000000-0000-0000-0000-000000000001"
version_id = "00000000-0000-0000-0000-000000000002" version_id = "00000000-0000-0000-0000-000000000002"
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_roster_agent_detail",
lambda _self, **kwargs: _agent_response(cast(str, kwargs["agent_id"])),
)
monkeypatch.setattr( monkeypatch.setattr(
roster_controller.AgentRosterService, roster_controller.AgentRosterService,
"list_agent_versions", "list_agent_versions",
@ -207,7 +313,6 @@ def test_roster_detail_and_versions_call_services(app: Flask, monkeypatch: pytes
}, },
) )
assert unwrap(AgentRosterDetailApi.get)(AgentRosterDetailApi(), "tenant-1", agent_id)["id"] == agent_id
assert ( assert (
unwrap(AgentRosterVersionsApi.get)(AgentRosterVersionsApi(), "tenant-1", agent_id)["data"][0]["id"] unwrap(AgentRosterVersionsApi.get)(AgentRosterVersionsApi(), "tenant-1", agent_id)["data"][0]["id"]
== "version-1" == "version-1"
@ -294,59 +399,76 @@ def test_workflow_impact_returns_empty_without_version(app: Flask) -> None:
assert result == {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []} assert result == {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
def test_agent_app_composer_get_put_validate_and_candidates( def test_agent_composer_routes_resolve_app_from_agent_id(
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
) -> None: ) -> None:
app_model = SimpleNamespace(id="app-1") agent_id = "00000000-0000-0000-0000-000000000001"
captured: dict[str, object] = {}
payload = { payload = {
"variant": ComposerVariant.AGENT_APP.value, "variant": ComposerVariant.AGENT_APP.value,
"save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION.value, "save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION.value,
"agent_soul": {"prompt": {"system_prompt": "x"}}, "agent_soul": {"prompt": {"system_prompt": "x"}},
} }
monkeypatch.setattr(composer_controller, "resolve_agent_app_model", lambda **kwargs: SimpleNamespace(id="app-1"))
def load_agent_app_composer(**kwargs: object) -> dict:
captured["load"] = kwargs
return _agent_app_composer_response()
def save_agent_app_composer(**kwargs: object) -> dict:
captured["save"] = kwargs
return _agent_app_composer_response()
def collect_validation_findings(**kwargs: object) -> dict:
captured["validate"] = kwargs
return {"warnings": [], "knowledge_retrieval_placeholder": []}
def get_agent_app_candidates(**kwargs: object) -> dict:
captured["candidates"] = kwargs
return _candidates_response("agent_app")
monkeypatch.setattr( monkeypatch.setattr(
composer_controller.AgentComposerService, composer_controller.AgentComposerService,
"load_agent_app_composer", "load_agent_app_composer",
lambda **kwargs: _agent_app_composer_response(), load_agent_app_composer,
) )
monkeypatch.setattr( monkeypatch.setattr(
composer_controller.AgentComposerService, composer_controller.AgentComposerService,
"save_agent_app_composer", "save_agent_app_composer",
lambda **kwargs: _agent_app_composer_response(), save_agent_app_composer,
) )
monkeypatch.setattr(composer_controller.ComposerConfigValidator, "validate_save_payload", lambda payload: None) monkeypatch.setattr(composer_controller.ComposerConfigValidator, "validate_save_payload", lambda payload: None)
monkeypatch.setattr( monkeypatch.setattr(
composer_controller.AgentComposerService, "resolve_workflow_node_agent_id", lambda **kwargs: None composer_controller.AgentComposerService,
"collect_validation_findings",
collect_validation_findings,
) )
monkeypatch.setattr(composer_controller.AgentComposerService, "resolve_bound_agent_id", lambda **kwargs: None)
monkeypatch.setattr( monkeypatch.setattr(
composer_controller.AgentComposerService, composer_controller.AgentComposerService,
"get_agent_app_candidates", "get_agent_app_candidates",
lambda **kwargs: _candidates_response("agent_app"), get_agent_app_candidates,
) )
assert unwrap(AgentAppComposerApi.get)(AgentAppComposerApi(), "tenant-1", app_model)["variant"] == "agent_app" assert unwrap(AgentComposerApi.get)(AgentComposerApi(), "tenant-1", agent_id)["variant"] == "agent_app"
assert cast(dict[str, object], captured["load"])["app_id"] == "app-1"
with app.test_request_context(json=payload): with app.test_request_context(json=payload):
assert ( assert (
unwrap(AgentAppComposerApi.put)(AgentAppComposerApi(), "tenant-1", account_id, app_model)["variant"] unwrap(AgentComposerApi.put)(AgentComposerApi(), "tenant-1", account_id, agent_id)["variant"] == "agent_app"
== "agent_app"
) )
assert unwrap(AgentAppComposerValidateApi.post)(AgentAppComposerValidateApi(), "tenant-1", app_model) == { assert cast(dict[str, object], captured["save"])["app_id"] == "app-1"
assert unwrap(AgentComposerValidateApi.post)(AgentComposerValidateApi(), "tenant-1", agent_id) == {
"result": "success", "result": "success",
"errors": [], "errors": [],
"warnings": [], "warnings": [],
"knowledge_retrieval_placeholder": [], "knowledge_retrieval_placeholder": [],
} }
agent_app_candidates = unwrap(AgentAppComposerCandidatesApi.get)( assert cast(dict[str, object], captured["validate"])["agent_id"] == agent_id
AgentAppComposerCandidatesApi(), "tenant-1", account_id, app_model
)
assert agent_app_candidates["variant"] == "agent_app"
candidates = unwrap(AgentComposerCandidatesApi.get)(AgentComposerCandidatesApi(), "tenant-1", account_id, agent_id)
def test_agent_app_composer_routes_are_agent_mode_only() -> None: assert candidates["variant"] == "agent_app"
assert _get_app_model_modes(AgentAppComposerApi.get) == [AppMode.AGENT] assert cast(dict[str, object], captured["candidates"])["app_id"] == "app-1"
assert _get_app_model_modes(AgentAppComposerApi.put) == [AppMode.AGENT]
assert _get_app_model_modes(AgentAppComposerValidateApi.post) == [AppMode.AGENT]
assert _get_app_model_modes(AgentAppComposerCandidatesApi.get) == [AppMode.AGENT]
def test_dify_tool_candidate_response_keeps_granularity_fields(): def test_dify_tool_candidate_response_keeps_granularity_fields():

View File

@ -119,6 +119,7 @@ def test_handle_maps_sandbox_and_agent_backend_errors() -> None:
def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPatch) -> None: def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPatch) -> None:
service = _AgentAppService() service = _AgentAppService()
monkeypatch.setattr(module, "AgentAppSandboxService", lambda: service) monkeypatch.setattr(module, "AgentAppSandboxService", lambda: service)
monkeypatch.setattr(module, "resolve_agent_app_model", lambda *, tenant_id, agent_id: _app_model())
monkeypatch.setattr( monkeypatch.setattr(
module, module,
"query_params_from_request", "query_params_from_request",
@ -129,11 +130,10 @@ def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPat
"request", "request",
SimpleNamespace(get_json=lambda silent=True: {"conversation_id": "conv-1", "path": "report.txt"}), SimpleNamespace(get_json=lambda silent=True: {"conversation_id": "conv-1", "path": "report.txt"}),
) )
app_model = _app_model()
listing = unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", app_model) listing = unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", "agent-1")
preview = unwrap(module.AgentAppSandboxReadResource.get)(object(), "tenant-1", app_model) preview = unwrap(module.AgentAppSandboxReadResource.get)(object(), "tenant-1", "agent-1")
upload = unwrap(module.AgentAppSandboxUploadResource.post)(object(), "tenant-1", app_model) upload = unwrap(module.AgentAppSandboxUploadResource.post)(object(), "tenant-1", "agent-1")
assert listing["path"] == "sub/report.txt" assert listing["path"] == "sub/report.txt"
assert preview["text"] == "hello" assert preview["text"] == "hello"
@ -151,11 +151,12 @@ def test_agent_app_sandbox_resource_returns_normalized_errors(monkeypatch: pytes
raise AgentSandboxInspectorError("no_active_session", "no active session", status_code=404) raise AgentSandboxInspectorError("no_active_session", "no active session", status_code=404)
monkeypatch.setattr(module, "AgentAppSandboxService", FailingService) monkeypatch.setattr(module, "AgentAppSandboxService", FailingService)
monkeypatch.setattr(module, "resolve_agent_app_model", lambda *, tenant_id, agent_id: _app_model())
monkeypatch.setattr( monkeypatch.setattr(
module, "query_params_from_request", lambda model: SimpleNamespace(conversation_id="conv-1", path=".") module, "query_params_from_request", lambda model: SimpleNamespace(conversation_id="conv-1", path=".")
) )
assert unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", _app_model()) == ( assert unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", "agent-1") == (
{"code": "no_active_session", "message": "no active session"}, {"code": "no_active_session", "message": "no active session"},
404, 404,
) )

View File

@ -15,8 +15,11 @@ from flask import Flask
from controllers.console.app.agent_drive_inspector import ( from controllers.console.app.agent_drive_inspector import (
AgentDriveDownloadApi, AgentDriveDownloadApi,
AgentDriveDownloadByAgentApi,
AgentDriveListApi, AgentDriveListApi,
AgentDriveListByAgentApi,
AgentDrivePreviewApi, AgentDrivePreviewApi,
AgentDrivePreviewByAgentApi,
) )
from services.agent_drive_service import AgentDriveError from services.agent_drive_service import AgentDriveError
@ -53,6 +56,32 @@ def test_list_filters_value_pointers_out_of_console_payload():
assert drive.return_value.manifest.call_args.kwargs["prefix"] == "pdf-toolkit/" assert drive.return_value.manifest.call_args.kwargs["prefix"] == "pdf-toolkit/"
def test_list_by_agent_filters_value_pointers_out_of_console_payload():
raw = _raw(AgentDriveListByAgentApi.get)
with app.test_request_context("/?prefix=pdf-toolkit/"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.AgentDriveService") as drive,
):
drive.return_value.manifest.return_value = [
{
"key": "pdf-toolkit/SKILL.md",
"size": 5,
"hash": "h",
"mime_type": "text/markdown",
"file_kind": "tool_file",
"file_id": "tf-1",
"created_at": 1718000000,
}
]
body = raw(AgentDriveListByAgentApi(), "tenant-1", "agent-1")
assert body["items"][0]["key"] == "pdf-toolkit/SKILL.md"
assert "file_id" not in body["items"][0]
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
assert drive.return_value.manifest.call_args.kwargs["agent_id"] == "agent-1"
def test_list_resolves_workflow_node_binding_agent(): def test_list_resolves_workflow_node_binding_agent():
raw = _raw(AgentDriveListApi.get) raw = _raw(AgentDriveListApi.get)
with app.test_request_context("/?node_id=agent-node-1"): with app.test_request_context("/?node_id=agent-node-1"):
@ -101,6 +130,37 @@ def test_preview_passes_through_and_maps_errors():
assert body["code"] == "drive_key_not_found" assert body["code"] == "drive_key_not_found"
def test_preview_by_agent_passes_through_and_maps_errors():
raw = _raw(AgentDrivePreviewByAgentApi.get)
with app.test_request_context("/?key=pdf-toolkit/SKILL.md"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.AgentDriveService") as drive,
):
drive.return_value.preview.return_value = {
"key": "pdf-toolkit/SKILL.md",
"size": 5,
"truncated": False,
"binary": False,
"text": "# hi",
}
body = raw(AgentDrivePreviewByAgentApi(), "tenant-1", "agent-1")
assert body["text"] == "# hi"
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
with app.test_request_context("/?key=ghost/SKILL.md"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP),
patch(f"{_MOD}.AgentDriveService") as drive,
):
drive.return_value.preview.side_effect = AgentDriveError(
"drive_key_not_found", "no drive entry", status_code=404
)
body, status = raw(AgentDrivePreviewByAgentApi(), "tenant-1", "agent-1")
assert status == 404
assert body["code"] == "drive_key_not_found"
def test_download_returns_signed_url_json(): def test_download_returns_signed_url_json():
raw = _raw(AgentDriveDownloadApi.get) raw = _raw(AgentDriveDownloadApi.get)
with app.test_request_context("/?key=pdf-toolkit/.DIFY-SKILL-FULL.zip"): with app.test_request_context("/?key=pdf-toolkit/.DIFY-SKILL-FULL.zip"):
@ -108,3 +168,16 @@ def test_download_returns_signed_url_json():
drive.return_value.download_url.return_value = "https://signed.example/zip" drive.return_value.download_url.return_value = "https://signed.example/zip"
body = raw(AgentDriveDownloadApi(), _APP) body = raw(AgentDriveDownloadApi(), _APP)
assert body == {"url": "https://signed.example/zip"} assert body == {"url": "https://signed.example/zip"}
def test_download_by_agent_returns_signed_url_json():
raw = _raw(AgentDriveDownloadByAgentApi.get)
with app.test_request_context("/?key=pdf-toolkit/.DIFY-SKILL-FULL.zip"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.AgentDriveService") as drive,
):
drive.return_value.download_url.return_value = "https://signed.example/zip"
body = raw(AgentDriveDownloadByAgentApi(), "tenant-1", "agent-1")
assert body == {"url": "https://signed.example/zip"}
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")

View File

@ -14,7 +14,16 @@ from unittest.mock import MagicMock, patch
from flask import Flask from flask import Flask
from controllers.console.app.agent import AgentSkillStandardizeApi, AgentSkillUploadApi from controllers.console.app.agent import (
AgentDriveFilesByAgentApi,
AgentSkillByAgentApi,
AgentSkillInferToolsByAgentApi,
AgentSkillStandardizeApi,
AgentSkillStandardizeByAgentApi,
AgentSkillUploadApi,
AgentSkillUploadByAgentApi,
)
from models.model import AppMode
from services.agent.skill_package_service import SkillPackageError from services.agent.skill_package_service import SkillPackageError
from services.agent_drive_service import AgentDriveError from services.agent_drive_service import AgentDriveError
@ -32,7 +41,8 @@ def _file_ctx(*, files: dict[str, bytes] | None = None):
_USER = SimpleNamespace(id="user-1") _USER = SimpleNamespace(id="user-1")
_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id="agent-1") _APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id="agent-1")
_WORKFLOW_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW, bound_agent_id=None)
def test_upload_validates_and_returns_skill_ref(): def test_upload_validates_and_returns_skill_ref():
@ -56,6 +66,28 @@ def test_upload_validates_and_returns_skill_ref():
manifest.to_skill_ref.assert_called_once_with(file_id="uf-1") manifest.to_skill_ref.assert_called_once_with(file_id="uf-1")
def test_upload_by_agent_resolves_app_and_returns_skill_ref():
raw = _raw(AgentSkillUploadByAgentApi.post)
manifest = MagicMock()
manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"}
manifest.model_dump.return_value = {"name": "S"}
with _file_ctx(files={"file": b"zip-bytes"}):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.SkillPackageService") as pkg,
patch(f"{_MOD}.FileService") as fs,
patch(f"{_MOD}.db"),
):
pkg.return_value.validate_and_extract.return_value = manifest
fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1")
body, status = raw(AgentSkillUploadByAgentApi(), "tenant-1", _USER, "agent-1")
assert status == 201
assert body["skill"] == {"name": "S", "file_id": "uf-1"}
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
def test_upload_no_file_is_400(): def test_upload_no_file_is_400():
raw = _raw(AgentSkillUploadApi.post) raw = _raw(AgentSkillUploadApi.post)
with _file_ctx(files={}): with _file_ctx(files={}):
@ -87,9 +119,25 @@ def test_standardize_returns_result():
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
def test_standardize_by_agent_resolves_app():
raw = _raw(AgentSkillStandardizeByAgentApi.post)
with _file_ctx(files={"file": b"zip"}):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.SkillStandardizeService") as svc,
):
svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}}
body, status = raw(AgentSkillStandardizeByAgentApi(), "tenant-1", _USER, "agent-1")
assert status == 201
assert body["skill"] == {"path": "s"}
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
def test_standardize_no_bound_agent_is_400(): def test_standardize_no_bound_agent_is_400():
raw = _raw(AgentSkillStandardizeApi.post) raw = _raw(AgentSkillStandardizeApi.post)
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None) app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id=None)
with _file_ctx(files={"file": b"zip"}): with _file_ctx(files={"file": b"zip"}):
body, status = raw(AgentSkillStandardizeApi(), _USER, app_without_agent) body, status = raw(AgentSkillStandardizeApi(), _USER, app_without_agent)
assert status == 400 assert status == 400
@ -98,7 +146,6 @@ def test_standardize_no_bound_agent_is_400():
def test_standardize_resolves_workflow_node_agent(): def test_standardize_resolves_workflow_node_agent():
raw = _raw(AgentSkillStandardizeApi.post) raw = _raw(AgentSkillStandardizeApi.post)
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
with app.test_request_context( with app.test_request_context(
"/?node_id=agent-node-1", method="POST", data={"file": (io.BytesIO(b"zip"), "skill.zip")} "/?node_id=agent-node-1", method="POST", data={"file": (io.BytesIO(b"zip"), "skill.zip")}
): ):
@ -108,7 +155,7 @@ def test_standardize_resolves_workflow_node_agent():
): ):
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1" composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1"
svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}} svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}}
body, status = raw(AgentSkillStandardizeApi(), _USER, workflow_app) body, status = raw(AgentSkillStandardizeApi(), _USER, _WORKFLOW_APP)
assert status == 201 assert status == 201
assert body["skill"] == {"path": "s"} assert body["skill"] == {"path": "s"}
@ -165,6 +212,31 @@ def test_files_commit_validates_upload_and_returns_drive_ref():
assert composer.add_drive_file_ref.call_args.kwargs["app_id"] == "app-1" assert composer.add_drive_file_ref.call_args.kwargs["app_id"] == "app-1"
def test_files_by_agent_commit_uses_agent_route_and_ignores_node_id():
raw = _raw(AgentDriveFilesByAgentApi.post)
upload = SimpleNamespace(id="uf-1", name="sample.pdf")
with _json_ctx({"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}, query_string="node_id=ignored"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.console_ns") as ns,
patch(f"{_MOD}.db") as db_mock,
patch(f"{_MOD}.AgentDriveService") as drive,
patch(f"{_MOD}.AgentComposerService") as composer,
):
ns.payload = {"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}
db_mock.session.scalar.return_value = upload
drive.return_value.commit.return_value = [
{"key": "files/sample.pdf", "size": 5, "mime_type": "application/pdf"}
]
composer.add_drive_file_ref.return_value = "ver-2"
body, status = raw(AgentDriveFilesByAgentApi(), "tenant-1", _USER, "agent-1")
assert status == 201
assert body["config_version_id"] == "ver-2"
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
assert composer.add_drive_file_ref.call_args.kwargs["node_id"] is None
def test_files_commit_404_when_upload_not_in_tenant(): def test_files_commit_404_when_upload_not_in_tenant():
from controllers.console.app.agent import AgentDriveFilesApi from controllers.console.app.agent import AgentDriveFilesApi
@ -186,7 +258,6 @@ def test_files_commit_resolves_workflow_node_agent():
raw = _raw(AgentDriveFilesApi.post) raw = _raw(AgentDriveFilesApi.post)
upload = SimpleNamespace(id="uf-1", name="sample.pdf") upload = SimpleNamespace(id="uf-1", name="sample.pdf")
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
with _json_ctx({"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}, query_string="node_id=agent-node-1"): with _json_ctx({"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}, query_string="node_id=agent-node-1"):
with ( with (
patch(f"{_MOD}.console_ns") as ns, patch(f"{_MOD}.console_ns") as ns,
@ -201,7 +272,7 @@ def test_files_commit_resolves_workflow_node_agent():
{"key": "files/sample.pdf", "size": 5, "mime_type": "application/pdf"} {"key": "files/sample.pdf", "size": 5, "mime_type": "application/pdf"}
] ]
composer.add_drive_file_ref.return_value = "ver-2" composer.add_drive_file_ref.return_value = "ver-2"
body, status = raw(AgentDriveFilesApi(), _USER, workflow_app) body, status = raw(AgentDriveFilesApi(), _USER, _WORKFLOW_APP)
assert status == 201 assert status == 201
assert body["config_version_id"] == "ver-2" assert body["config_version_id"] == "ver-2"
@ -229,11 +300,27 @@ def test_files_delete_updates_soul_then_drive():
assert composer.remove_drive_refs.call_args.kwargs["app_id"] == "app-1" assert composer.remove_drive_refs.call_args.kwargs["app_id"] == "app-1"
def test_files_by_agent_delete_uses_agent_route_and_ignores_node_id():
raw = _raw(AgentDriveFilesByAgentApi.delete)
with _json_ctx(method="DELETE", query_string="key=files/sample.pdf&node_id=ignored"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.AgentComposerService") as composer,
patch(f"{_MOD}.AgentDriveService") as drive,
):
composer.remove_drive_refs.return_value = "ver-2"
drive.return_value.delete.return_value = ["files/sample.pdf"]
body = raw(AgentDriveFilesByAgentApi(), "tenant-1", _USER, "agent-1")
assert body["config_version_id"] == "ver-2"
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
assert composer.remove_drive_refs.call_args.kwargs["node_id"] is None
def test_files_delete_resolves_workflow_node_agent(): def test_files_delete_resolves_workflow_node_agent():
from controllers.console.app.agent import AgentDriveFilesApi from controllers.console.app.agent import AgentDriveFilesApi
raw = _raw(AgentDriveFilesApi.delete) raw = _raw(AgentDriveFilesApi.delete)
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
with _json_ctx(method="DELETE", query_string="key=files/sample.pdf&node_id=agent-node-1"): with _json_ctx(method="DELETE", query_string="key=files/sample.pdf&node_id=agent-node-1"):
with ( with (
patch(f"{_MOD}.AgentComposerService") as composer, patch(f"{_MOD}.AgentComposerService") as composer,
@ -242,7 +329,7 @@ def test_files_delete_resolves_workflow_node_agent():
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1" composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1"
composer.remove_drive_refs.return_value = "ver-2" composer.remove_drive_refs.return_value = "ver-2"
drive.return_value.delete.return_value = ["files/sample.pdf"] drive.return_value.delete.return_value = ["files/sample.pdf"]
body = raw(AgentDriveFilesApi(), _USER, workflow_app) body = raw(AgentDriveFilesApi(), _USER, _WORKFLOW_APP)
assert body["config_version_id"] == "ver-2" assert body["config_version_id"] == "ver-2"
assert drive.return_value.delete.call_args.kwargs["agent_id"] == "wf-agent-1" assert drive.return_value.delete.call_args.kwargs["agent_id"] == "wf-agent-1"
@ -284,6 +371,23 @@ def test_skill_delete_uses_slug_prefix_and_is_idempotent():
assert composer.remove_drive_refs.call_args.kwargs["app_id"] == "app-1" assert composer.remove_drive_refs.call_args.kwargs["app_id"] == "app-1"
def test_skill_delete_by_agent_uses_agent_route():
raw = _raw(AgentSkillByAgentApi.delete)
with _json_ctx(method="DELETE", query_string="node_id=ignored"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.AgentComposerService") as composer,
patch(f"{_MOD}.AgentDriveService") as drive,
):
composer.remove_drive_refs.return_value = "ver-2"
drive.return_value.delete.return_value = ["tender-analyzer/SKILL.md"]
body = raw(AgentSkillByAgentApi(), "tenant-1", _USER, "agent-1", "tender-analyzer")
assert body["config_version_id"] == "ver-2"
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
assert composer.remove_drive_refs.call_args.kwargs["node_id"] is None
def test_skill_delete_rejects_path_like_slug(): def test_skill_delete_rejects_path_like_slug():
from controllers.console.app.agent import AgentSkillApi from controllers.console.app.agent import AgentSkillApi
@ -314,11 +418,25 @@ def test_infer_tools_returns_draft_suggestions():
assert svc.return_value.infer.call_args.kwargs["slug"] == "audio-transcribe" assert svc.return_value.infer.call_args.kwargs["slug"] == "audio-transcribe"
def test_infer_tools_by_agent_uses_agent_route():
raw = _raw(AgentSkillInferToolsByAgentApi.post)
with _json_ctx(query_string="node_id=ignored"):
with (
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
patch(f"{_MOD}.SkillToolInferenceService") as svc,
):
svc.return_value.infer.return_value = {"inferable": True, "cli_tools": [], "reason": None}
body = raw(AgentSkillInferToolsByAgentApi(), "tenant-1", "agent-1", "audio-transcribe")
assert body["inferable"] is True
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
assert svc.return_value.infer.call_args.kwargs["agent_id"] == "agent-1"
def test_infer_tools_resolves_workflow_node_agent(): def test_infer_tools_resolves_workflow_node_agent():
from controllers.console.app.agent import AgentSkillInferToolsApi from controllers.console.app.agent import AgentSkillInferToolsApi
raw = _raw(AgentSkillInferToolsApi.post) raw = _raw(AgentSkillInferToolsApi.post)
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
with _json_ctx(query_string="node_id=agent-node-1"): with _json_ctx(query_string="node_id=agent-node-1"):
with ( with (
patch(f"{_MOD}.AgentComposerService") as composer, patch(f"{_MOD}.AgentComposerService") as composer,
@ -326,7 +444,7 @@ def test_infer_tools_resolves_workflow_node_agent():
): ):
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1" composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1"
svc.return_value.infer.return_value = {"inferable": False, "cli_tools": [], "reason": "none"} svc.return_value.infer.return_value = {"inferable": False, "cli_tools": [], "reason": "none"}
body = raw(AgentSkillInferToolsApi(), workflow_app, "audio-transcribe") body = raw(AgentSkillInferToolsApi(), _WORKFLOW_APP, "audio-transcribe")
assert body["inferable"] is False assert body["inferable"] is False
assert svc.return_value.infer.call_args.kwargs["agent_id"] == "wf-agent-1" assert svc.return_value.infer.call_args.kwargs["agent_id"] == "wf-agent-1"
@ -355,7 +473,7 @@ def test_infer_tools_rejects_path_like_slug_and_unbound_app():
body, status = raw(AgentSkillInferToolsApi(), _APP, "a/b") body, status = raw(AgentSkillInferToolsApi(), _APP, "a/b")
assert (status, body["code"]) == (400, "drive_key_invalid") assert (status, body["code"]) == (400, "drive_key_invalid")
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None) app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id=None)
with _json_ctx(): with _json_ctx():
body, status = raw(AgentSkillInferToolsApi(), app_without_agent, "x") body, status = raw(AgentSkillInferToolsApi(), app_without_agent, "x")
assert (status, body["code"]) == (400, "agent_not_bound") assert (status, body["code"]) == (400, "agent_not_bound")

View File

@ -314,37 +314,19 @@ def test_app_list_query_rejects_flat_tag_ids(app_module):
app_module.AppListQuery.model_validate(normalized) app_module.AppListQuery.model_validate(normalized)
def test_create_agent_app_response_includes_bound_agent_id(app_module, monkeypatch: pytest.MonkeyPatch): def test_create_app_endpoint_rejects_agent_mode(app_module, monkeypatch: pytest.MonkeyPatch):
payload = {"name": "Iris", "mode": "agent", "description": "Agent app"} payload = {"name": "Iris", "mode": "agent", "description": "Agent app"}
app_obj = SimpleNamespace(
id="app-1",
name="Iris",
description="Agent app",
mode_compatible_with_agent="agent",
icon_type="emoji",
icon="robot",
icon_background="#fff",
enable_site=False,
enable_api=False,
created_at=_ts(),
updated_at=_ts(),
bound_agent_id="agent-1",
)
app_service = MagicMock() app_service = MagicMock()
app_service.create_app.return_value = app_obj
monkeypatch.setattr(app_module, "AppService", lambda: app_service) monkeypatch.setattr(app_module, "AppService", lambda: app_service)
app_module.console_ns.payload = payload app_module.console_ns.payload = payload
try: try:
response, status = _unwrap(app_module.AppListApi().post)("tenant-1", SimpleNamespace(id="account-1")) with pytest.raises(ValidationError):
_unwrap(app_module.AppListApi().post)("tenant-1", SimpleNamespace(id="account-1"))
finally: finally:
app_module.console_ns.payload = None app_module.console_ns.payload = None
assert status == 201 app_service.create_app.assert_not_called()
assert response["id"] == "app-1"
assert response["bound_agent_id"] == "agent-1"
created_params = app_service.create_app.call_args.args[1]
assert created_params.mode == "agent"
def test_app_partial_serialization_uses_aliases(app_models): def test_app_partial_serialization_uses_aliases(app_models):

View File

@ -1,9 +1,4 @@
"""Regression tests for CreateAppPayload mode validation. """Regression tests for CreateAppPayload mode validation."""
The HTTP create-app payload must accept the new "agent" app mode; without it a
user cannot create an Agent App through POST /console/api/apps even though the
service layer (CreateAppParams) supports it.
"""
import pytest import pytest
from pydantic import ValidationError from pydantic import ValidationError
@ -14,12 +9,16 @@ from controllers.console.app.app import CreateAppPayload
class TestCreateAppPayloadMode: class TestCreateAppPayloadMode:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mode", "mode",
["chat", "agent-chat", "agent", "advanced-chat", "workflow", "completion"], ["chat", "agent-chat", "advanced-chat", "workflow", "completion"],
) )
def test_accepts_supported_modes(self, mode: str): def test_accepts_supported_modes(self, mode: str):
payload = CreateAppPayload.model_validate({"name": "X", "mode": mode}) payload = CreateAppPayload.model_validate({"name": "X", "mode": mode})
assert payload.mode == mode assert payload.mode == mode
def test_rejects_agent_mode(self):
with pytest.raises(ValidationError):
CreateAppPayload.model_validate({"name": "X", "mode": "agent"})
def test_rejects_unknown_mode(self): def test_rejects_unknown_mode(self):
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
CreateAppPayload.model_validate({"name": "X", "mode": "not-a-mode"}) CreateAppPayload.model_validate({"name": "X", "mode": "not-a-mode"})

View File

@ -1037,6 +1037,31 @@ class TestAgentAppBackingAgent:
assert service.get_app_backing_agent(tenant_id="tenant-1", app_id="app-x") is None assert service.get_app_backing_agent(tenant_id="tenant-1", app_id="app-x") is None
def test_get_agent_app_model_resolves_app_backing_agent(self):
agent = Agent(
id="agent-1",
tenant_id="tenant-1",
name="Iris",
description="",
agent_kind=AgentKind.DIFY_AGENT,
scope=AgentScope.ROSTER,
source=AgentSource.AGENT_APP,
status=AgentStatus.ACTIVE,
app_id="app-1",
)
app = SimpleNamespace(id="app-1", mode="agent", status="normal")
session = FakeSession(scalar=[agent, app])
service = AgentRosterService(session)
assert service.get_agent_app_model(tenant_id="tenant-1", agent_id="agent-1") is app
def test_get_agent_app_model_rejects_unbound_agent(self):
session = FakeSession()
service = AgentRosterService(session)
with pytest.raises(roster_service.AgentNotFoundError):
service.get_agent_app_model(tenant_id="tenant-1", agent_id="agent-x")
class TestListWorkflowsReferencingAppAgent: class TestListWorkflowsReferencingAppAgent:
def test_groups_bindings_by_workflow_app_and_sorts_by_name(self): def test_groups_bindings_by_workflow_app_and_sorts_by_name(self):

View File

@ -0,0 +1,582 @@
// This file is auto-generated by @hey-api/openapi-ts
import { oc } from '@orpc/contract'
import * as z from 'zod'
import {
zDeleteAgentByAgentIdFilesPath,
zDeleteAgentByAgentIdFilesQuery,
zDeleteAgentByAgentIdFilesResponse,
zDeleteAgentByAgentIdPath,
zDeleteAgentByAgentIdResponse,
zDeleteAgentByAgentIdSkillsBySlugPath,
zDeleteAgentByAgentIdSkillsBySlugResponse,
zGetAgentByAgentIdComposerCandidatesPath,
zGetAgentByAgentIdComposerCandidatesResponse,
zGetAgentByAgentIdComposerPath,
zGetAgentByAgentIdComposerResponse,
zGetAgentByAgentIdDriveFilesDownloadPath,
zGetAgentByAgentIdDriveFilesDownloadQuery,
zGetAgentByAgentIdDriveFilesDownloadResponse,
zGetAgentByAgentIdDriveFilesPath,
zGetAgentByAgentIdDriveFilesPreviewPath,
zGetAgentByAgentIdDriveFilesPreviewQuery,
zGetAgentByAgentIdDriveFilesPreviewResponse,
zGetAgentByAgentIdDriveFilesQuery,
zGetAgentByAgentIdDriveFilesResponse,
zGetAgentByAgentIdPath,
zGetAgentByAgentIdReferencingWorkflowsPath,
zGetAgentByAgentIdReferencingWorkflowsResponse,
zGetAgentByAgentIdResponse,
zGetAgentByAgentIdSandboxFilesPath,
zGetAgentByAgentIdSandboxFilesQuery,
zGetAgentByAgentIdSandboxFilesReadPath,
zGetAgentByAgentIdSandboxFilesReadQuery,
zGetAgentByAgentIdSandboxFilesReadResponse,
zGetAgentByAgentIdSandboxFilesResponse,
zGetAgentByAgentIdVersionsByVersionIdPath,
zGetAgentByAgentIdVersionsByVersionIdResponse,
zGetAgentByAgentIdVersionsPath,
zGetAgentByAgentIdVersionsResponse,
zGetAgentInviteOptionsQuery,
zGetAgentInviteOptionsResponse,
zGetAgentQuery,
zGetAgentResponse,
zPostAgentBody,
zPostAgentByAgentIdComposerValidateBody,
zPostAgentByAgentIdComposerValidatePath,
zPostAgentByAgentIdComposerValidateResponse,
zPostAgentByAgentIdFeaturesBody,
zPostAgentByAgentIdFeaturesPath,
zPostAgentByAgentIdFeaturesResponse,
zPostAgentByAgentIdFilesBody,
zPostAgentByAgentIdFilesPath,
zPostAgentByAgentIdFilesResponse,
zPostAgentByAgentIdSandboxFilesUploadBody,
zPostAgentByAgentIdSandboxFilesUploadPath,
zPostAgentByAgentIdSandboxFilesUploadResponse,
zPostAgentByAgentIdSkillsBySlugInferToolsPath,
zPostAgentByAgentIdSkillsBySlugInferToolsResponse,
zPostAgentByAgentIdSkillsStandardizePath,
zPostAgentByAgentIdSkillsStandardizeResponse,
zPostAgentByAgentIdSkillsUploadPath,
zPostAgentByAgentIdSkillsUploadResponse,
zPostAgentResponse,
zPutAgentByAgentIdBody,
zPutAgentByAgentIdComposerBody,
zPutAgentByAgentIdComposerPath,
zPutAgentByAgentIdComposerResponse,
zPutAgentByAgentIdPath,
zPutAgentByAgentIdResponse,
} from './zod.gen'
export const get = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentInviteOptions',
path: '/agent/invite-options',
tags: ['console'],
})
.input(z.object({ query: zGetAgentInviteOptionsQuery.optional() }))
.output(zGetAgentInviteOptionsResponse)
export const inviteOptions = {
get,
}
export const get2 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdComposerCandidates',
path: '/agent/{agent_id}/composer/candidates',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdComposerCandidatesPath }))
.output(zGetAgentByAgentIdComposerCandidatesResponse)
export const candidates = {
get: get2,
}
export const post = oc
.route({
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdComposerValidate',
path: '/agent/{agent_id}/composer/validate',
tags: ['console'],
})
.input(
z.object({
body: zPostAgentByAgentIdComposerValidateBody,
params: zPostAgentByAgentIdComposerValidatePath,
}),
)
.output(zPostAgentByAgentIdComposerValidateResponse)
export const validate = {
post,
}
export const get3 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdComposer',
path: '/agent/{agent_id}/composer',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdComposerPath }))
.output(zGetAgentByAgentIdComposerResponse)
export const put = oc
.route({
inputStructure: 'detailed',
method: 'PUT',
operationId: 'putAgentByAgentIdComposer',
path: '/agent/{agent_id}/composer',
tags: ['console'],
})
.input(z.object({ body: zPutAgentByAgentIdComposerBody, params: zPutAgentByAgentIdComposerPath }))
.output(zPutAgentByAgentIdComposerResponse)
export const composer = {
get: get3,
put,
candidates,
validate,
}
/**
* Time-limited external signed URL for one Agent App drive value
*/
export const get4 = oc
.route({
description: 'Time-limited external signed URL for one Agent App drive value',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdDriveFilesDownload',
path: '/agent/{agent_id}/drive/files/download',
tags: ['console'],
})
.input(
z.object({
params: zGetAgentByAgentIdDriveFilesDownloadPath,
query: zGetAgentByAgentIdDriveFilesDownloadQuery,
}),
)
.output(zGetAgentByAgentIdDriveFilesDownloadResponse)
export const download = {
get: get4,
}
/**
* Truncated text preview of one Agent App drive value
*/
export const get5 = oc
.route({
description: 'Truncated text preview of one Agent App drive value',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdDriveFilesPreview',
path: '/agent/{agent_id}/drive/files/preview',
tags: ['console'],
})
.input(
z.object({
params: zGetAgentByAgentIdDriveFilesPreviewPath,
query: zGetAgentByAgentIdDriveFilesPreviewQuery,
}),
)
.output(zGetAgentByAgentIdDriveFilesPreviewResponse)
export const preview = {
get: get5,
}
/**
* List agent drive entries for an Agent App
*/
export const get6 = oc
.route({
description: 'List agent drive entries for an Agent App',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdDriveFiles',
path: '/agent/{agent_id}/drive/files',
tags: ['console'],
})
.input(
z.object({
params: zGetAgentByAgentIdDriveFilesPath,
query: zGetAgentByAgentIdDriveFilesQuery.optional(),
}),
)
.output(zGetAgentByAgentIdDriveFilesResponse)
export const files = {
get: get6,
download,
preview,
}
export const drive = {
files,
}
/**
* Update an Agent App's presentation features (opener, follow-up, citations, ...)
*/
export const post2 = oc
.route({
description: 'Update an Agent App\'s presentation features (opener, follow-up, citations, ...)',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdFeatures',
path: '/agent/{agent_id}/features',
tags: ['console'],
})
.input(
z.object({ body: zPostAgentByAgentIdFeaturesBody, params: zPostAgentByAgentIdFeaturesPath }),
)
.output(zPostAgentByAgentIdFeaturesResponse)
export const features = {
post: post2,
}
/**
* Delete one Agent App drive file by key
*/
export const delete_ = oc
.route({
description: 'Delete one Agent App drive file by key',
inputStructure: 'detailed',
method: 'DELETE',
operationId: 'deleteAgentByAgentIdFiles',
path: '/agent/{agent_id}/files',
tags: ['console'],
})
.input(
z.object({ params: zDeleteAgentByAgentIdFilesPath, query: zDeleteAgentByAgentIdFilesQuery }),
)
.output(zDeleteAgentByAgentIdFilesResponse)
/**
* Commit an uploaded file into the Agent App drive under files/<name>
*/
export const post3 = oc
.route({
description: 'Commit an uploaded file into the Agent App drive under files/<name>',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdFiles',
path: '/agent/{agent_id}/files',
successStatus: 201,
tags: ['console'],
})
.input(z.object({ body: zPostAgentByAgentIdFilesBody, params: zPostAgentByAgentIdFilesPath }))
.output(zPostAgentByAgentIdFilesResponse)
export const files2 = {
delete: delete_,
post: post3,
}
/**
* List workflow apps that reference this Agent App's bound Agent (read-only)
*/
export const get7 = oc
.route({
description: 'List workflow apps that reference this Agent App\'s bound Agent (read-only)',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdReferencingWorkflows',
path: '/agent/{agent_id}/referencing-workflows',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdReferencingWorkflowsPath }))
.output(zGetAgentByAgentIdReferencingWorkflowsResponse)
export const referencingWorkflows = {
get: get7,
}
/**
* Read a text/binary preview file in an Agent App conversation sandbox
*/
export const get8 = oc
.route({
description: 'Read a text/binary preview file in an Agent App conversation sandbox',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdSandboxFilesRead',
path: '/agent/{agent_id}/sandbox/files/read',
tags: ['console'],
})
.input(
z.object({
params: zGetAgentByAgentIdSandboxFilesReadPath,
query: zGetAgentByAgentIdSandboxFilesReadQuery,
}),
)
.output(zGetAgentByAgentIdSandboxFilesReadResponse)
export const read = {
get: get8,
}
/**
* Upload one Agent App sandbox file as a Dify ToolFile mapping
*/
export const post4 = oc
.route({
description: 'Upload one Agent App sandbox file as a Dify ToolFile mapping',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdSandboxFilesUpload',
path: '/agent/{agent_id}/sandbox/files/upload',
tags: ['console'],
})
.input(
z.object({
body: zPostAgentByAgentIdSandboxFilesUploadBody,
params: zPostAgentByAgentIdSandboxFilesUploadPath,
}),
)
.output(zPostAgentByAgentIdSandboxFilesUploadResponse)
export const upload = {
post: post4,
}
/**
* List a directory in an Agent App conversation sandbox
*/
export const get9 = oc
.route({
description: 'List a directory in an Agent App conversation sandbox',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdSandboxFiles',
path: '/agent/{agent_id}/sandbox/files',
tags: ['console'],
})
.input(
z.object({
params: zGetAgentByAgentIdSandboxFilesPath,
query: zGetAgentByAgentIdSandboxFilesQuery,
}),
)
.output(zGetAgentByAgentIdSandboxFilesResponse)
export const files3 = {
get: get9,
read,
upload,
}
export const sandbox = {
files: files3,
}
/**
* Validate + standardize a Skill into an Agent App drive
*/
export const post5 = oc
.route({
description: 'Validate + standardize a Skill into an Agent App drive',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdSkillsStandardize',
path: '/agent/{agent_id}/skills/standardize',
successStatus: 201,
tags: ['console'],
})
.input(z.object({ params: zPostAgentByAgentIdSkillsStandardizePath }))
.output(zPostAgentByAgentIdSkillsStandardizeResponse)
export const standardize = {
post: post5,
}
/**
* Upload + validate a Skill package for an Agent App
*/
export const post6 = oc
.route({
description: 'Upload + validate a Skill package for an Agent App',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdSkillsUpload',
path: '/agent/{agent_id}/skills/upload',
successStatus: 201,
tags: ['console'],
})
.input(z.object({ params: zPostAgentByAgentIdSkillsUploadPath }))
.output(zPostAgentByAgentIdSkillsUploadResponse)
export const upload2 = {
post: post6,
}
/**
* Infer CLI tool + ENV suggestions from a standardized Agent App skill
*/
export const post7 = oc
.route({
description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdSkillsBySlugInferTools',
path: '/agent/{agent_id}/skills/{slug}/infer-tools',
tags: ['console'],
})
.input(z.object({ params: zPostAgentByAgentIdSkillsBySlugInferToolsPath }))
.output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse)
export const inferTools = {
post: post7,
}
/**
* Delete a standardized skill from an Agent App drive
*/
export const delete2 = oc
.route({
description: 'Delete a standardized skill from an Agent App drive',
inputStructure: 'detailed',
method: 'DELETE',
operationId: 'deleteAgentByAgentIdSkillsBySlug',
path: '/agent/{agent_id}/skills/{slug}',
tags: ['console'],
})
.input(z.object({ params: zDeleteAgentByAgentIdSkillsBySlugPath }))
.output(zDeleteAgentByAgentIdSkillsBySlugResponse)
export const bySlug = {
delete: delete2,
inferTools,
}
export const skills = {
standardize,
upload: upload2,
bySlug,
}
export const get10 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdVersionsByVersionId',
path: '/agent/{agent_id}/versions/{version_id}',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdVersionsByVersionIdPath }))
.output(zGetAgentByAgentIdVersionsByVersionIdResponse)
export const byVersionId = {
get: get10,
}
export const get11 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdVersions',
path: '/agent/{agent_id}/versions',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdVersionsPath }))
.output(zGetAgentByAgentIdVersionsResponse)
export const versions = {
get: get11,
byVersionId,
}
export const delete3 = oc
.route({
inputStructure: 'detailed',
method: 'DELETE',
operationId: 'deleteAgentByAgentId',
path: '/agent/{agent_id}',
successStatus: 204,
tags: ['console'],
})
.input(z.object({ params: zDeleteAgentByAgentIdPath }))
.output(zDeleteAgentByAgentIdResponse)
export const get12 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentId',
path: '/agent/{agent_id}',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdPath }))
.output(zGetAgentByAgentIdResponse)
export const put2 = oc
.route({
inputStructure: 'detailed',
method: 'PUT',
operationId: 'putAgentByAgentId',
path: '/agent/{agent_id}',
tags: ['console'],
})
.input(z.object({ body: zPutAgentByAgentIdBody, params: zPutAgentByAgentIdPath }))
.output(zPutAgentByAgentIdResponse)
export const byAgentId = {
delete: delete3,
get: get12,
put: put2,
composer,
drive,
features,
files: files2,
referencingWorkflows,
sandbox,
skills,
versions,
}
export const get13 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgent',
path: '/agent',
tags: ['console'],
})
.input(z.object({ query: zGetAgentQuery.optional() }))
.output(zGetAgentResponse)
export const post8 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgent',
path: '/agent',
successStatus: 201,
tags: ['console'],
})
.input(z.object({ body: zPostAgentBody }))
.output(zPostAgentResponse)
export const agent = {
get: get13,
post: post8,
inviteOptions,
byAgentId,
}
export const contract = {
agent,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { oc } from '@orpc/contract'
import * as z from 'zod'
import {
zGetAgentsByAgentIdPath,
zGetAgentsByAgentIdResponse,
zGetAgentsByAgentIdVersionsByVersionIdPath,
zGetAgentsByAgentIdVersionsByVersionIdResponse,
zGetAgentsByAgentIdVersionsPath,
zGetAgentsByAgentIdVersionsResponse,
zGetAgentsInviteOptionsQuery,
zGetAgentsInviteOptionsResponse,
zGetAgentsQuery,
zGetAgentsResponse,
} from './zod.gen'
export const get = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentsInviteOptions',
path: '/agents/invite-options',
tags: ['console'],
})
.input(z.object({ query: zGetAgentsInviteOptionsQuery.optional() }))
.output(zGetAgentsInviteOptionsResponse)
export const inviteOptions = {
get,
}
export const get2 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentsByAgentIdVersionsByVersionId',
path: '/agents/{agent_id}/versions/{version_id}',
tags: ['console'],
})
.input(z.object({ params: zGetAgentsByAgentIdVersionsByVersionIdPath }))
.output(zGetAgentsByAgentIdVersionsByVersionIdResponse)
export const byVersionId = {
get: get2,
}
export const get3 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentsByAgentIdVersions',
path: '/agents/{agent_id}/versions',
tags: ['console'],
})
.input(z.object({ params: zGetAgentsByAgentIdVersionsPath }))
.output(zGetAgentsByAgentIdVersionsResponse)
export const versions = {
get: get3,
byVersionId,
}
export const get4 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentsByAgentId',
path: '/agents/{agent_id}',
tags: ['console'],
})
.input(z.object({ params: zGetAgentsByAgentIdPath }))
.output(zGetAgentsByAgentIdResponse)
export const byAgentId = {
get: get4,
versions,
}
export const get5 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgents',
path: '/agents',
tags: ['console'],
})
.input(z.object({ query: zGetAgentsQuery.optional() }))
.output(zGetAgentsResponse)
export const agents = {
get: get5,
inviteOptions,
byAgentId,
}
export const contract = {
agents,
}

View File

@ -1,587 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseUrl: `${string}://${string}/console/api` | (string & {})
}
export type AgentRosterListResponse = {
data: Array<AgentRosterResponse>
has_more: boolean
limit: number
page: number
total: number
}
export type AgentInviteOptionsResponse = {
data: Array<AgentInviteOptionResponse>
has_more: boolean
limit: number
page: number
total: number
}
export type AgentRosterResponse = {
active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null
active_config_snapshot_id?: string | null
agent_kind: AgentKind
app_id?: string | null
archived_at?: number | null
archived_by?: string | null
created_at?: number | null
created_by?: string | null
description: string
icon?: string | null
icon_background?: string | null
icon_type?: AgentIconType | null
id: string
name: string
published_node_reference_count?: number
published_reference_count?: number
published_references?: Array<AgentPublishedReferenceResponse>
role?: string
scope: AgentScope
source: AgentSource
status: AgentStatus
updated_at?: number | null
updated_by?: string | null
workflow_id?: string | null
workflow_node_id?: string | null
}
export type AgentConfigSnapshotListResponse = {
data: Array<AgentConfigSnapshotSummaryResponse>
}
export type AgentConfigSnapshotDetailResponse = {
agent_id?: string | null
config_snapshot: AgentSoulConfig
created_at?: number | null
created_by?: string | null
id: string
revisions?: Array<AgentConfigRevisionResponse>
summary?: string | null
version: number
version_note?: string | null
}
export type AgentInviteOptionResponse = {
active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null
active_config_snapshot_id?: string | null
agent_kind: AgentKind
app_id?: string | null
archived_at?: number | null
archived_by?: string | null
created_at?: number | null
created_by?: string | null
description: string
existing_node_ids?: Array<string>
icon?: string | null
icon_background?: string | null
icon_type?: AgentIconType | null
id: string
in_current_workflow_count?: number
is_in_current_workflow?: boolean
name: string
published_node_reference_count?: number
published_reference_count?: number
published_references?: Array<AgentPublishedReferenceResponse>
role?: string
scope: AgentScope
source: AgentSource
status: AgentStatus
updated_at?: number | null
updated_by?: string | null
workflow_id?: string | null
workflow_node_id?: string | null
}
export type AgentConfigSnapshotSummaryResponse = {
agent_id?: string | null
created_at?: number | null
created_by?: string | null
id: string
summary?: string | null
version: number
version_note?: string | null
}
export type AgentKind = 'dify_agent'
export type AgentIconType = 'emoji' | 'image' | 'link'
export type AgentPublishedReferenceResponse = {
app_id: string
app_mode: string
app_name: string
node_ids?: Array<string>
workflow_id: string
workflow_version: string
}
export type AgentScope = 'roster' | 'workflow_only'
export type AgentSource = 'agent_app' | 'imported' | 'roster' | 'system' | 'workflow'
export type AgentStatus = 'active' | 'archived'
export type AgentSoulConfig = {
app_features?: AgentSoulAppFeaturesConfig
app_variables?: Array<AppVariableConfig>
env?: AgentSoulEnvConfig
human?: AgentSoulHumanConfig
knowledge?: AgentSoulKnowledgeConfig
memory?: AgentSoulMemoryConfig
misc_legacy?: AgentSoulAppFeaturesConfig
model?: AgentSoulModelConfig | null
prompt?: AgentSoulPromptConfig
sandbox?: AgentSoulSandboxConfig
schema_version?: number
skills_files?: AgentSoulSkillsFilesConfig
tools?: AgentSoulToolsConfig
}
export type AgentConfigRevisionResponse = {
created_at?: number | null
created_by?: string | null
current_snapshot_id: string
id: string
operation: AgentConfigRevisionOperation
previous_snapshot_id?: string | null
revision: number
summary?: string | null
version_note?: string | null
}
export type AgentSoulAppFeaturesConfig = {
opening_statement?: string | null
retriever_resource?: AgentFeatureToggleConfig | null
sensitive_word_avoidance?: AgentSensitiveWordAvoidanceFeatureConfig | null
speech_to_text?: AgentFeatureToggleConfig | null
suggested_questions?: Array<string> | null
suggested_questions_after_answer?: AgentSuggestedQuestionsAfterAnswerFeatureConfig | null
text_to_speech?: AgentTextToSpeechFeatureConfig | null
[key: string]: unknown
}
export type AppVariableConfig = {
default?: unknown
name: string
required?: boolean
type: string
}
export type AgentSoulEnvConfig = {
secret_refs?: Array<AgentSecretRefConfig>
variables?: Array<AgentEnvVariableConfig>
}
export type AgentSoulHumanConfig = {
contacts?: Array<AgentHumanContactConfig>
tools?: Array<AgentHumanToolConfig>
}
export type AgentSoulKnowledgeConfig = {
datasets?: Array<AgentKnowledgeDatasetConfig>
query_config?: AgentKnowledgeQueryConfig
query_mode?: AgentKnowledgeQueryMode | null
}
export type AgentSoulMemoryConfig = {
artifacts?: Array<AgentMemoryArtifactConfig>
budget?: string | null
scope?: string | null
}
export type AgentSoulModelConfig = {
credential_ref?: AgentSoulModelCredentialRef | null
model: string
model_provider: string
model_settings?: AgentSoulModelSettings
plugin_id: string
}
export type AgentSoulPromptConfig = {
system_prompt?: string
}
export type AgentSoulSandboxConfig = {
config?: AgentSandboxProviderConfig
provider?: string | null
}
export type AgentSoulSkillsFilesConfig = {
files?: Array<AgentFileRefConfig>
skills?: Array<AgentSkillRefConfig>
}
export type AgentSoulToolsConfig = {
cli_tools?: Array<AgentCliToolConfig>
dify_tools?: Array<AgentSoulDifyToolConfig>
}
export type AgentConfigRevisionOperation
= | 'create_version'
| 'save_current_version'
| 'save_new_agent'
| 'save_new_version'
| 'save_to_roster'
export type AgentFeatureToggleConfig = {
enabled?: boolean
[key: string]: unknown
}
export type AgentSensitiveWordAvoidanceFeatureConfig = {
config?: AgentModerationProviderConfig | null
enabled?: boolean
type?: string | null
[key: string]: unknown
}
export type AgentSuggestedQuestionsAfterAnswerFeatureConfig = {
enabled?: boolean
model?: AgentSoulModelConfig | null
prompt?: string | null
[key: string]: unknown
}
export type AgentTextToSpeechFeatureConfig = {
autoPlay?: string | null
enabled?: boolean
language?: string | null
voice?: string | null
[key: string]: unknown
}
export type AgentSecretRefConfig = {
credential_id?: string | null
env_name?: string | null
id?: string | null
key?: string | null
name?: string | null
permission?: AgentPermissionConfig | null
permission_status?: string | null
provider?: string | null
provider_credential_id?: string | null
ref?: string | null
type?: string | null
variable?: string | null
[key: string]: unknown
}
export type AgentEnvVariableConfig = {
default?:
| string
| number
| number
| boolean
| Array<string>
| Array<number>
| Array<number>
| Array<boolean>
| null
env_name?: string | null
key?: string | null
name?: string | null
required?: boolean
type?: string | null
value?:
| string
| number
| number
| boolean
| Array<string>
| Array<number>
| Array<number>
| Array<boolean>
| null
variable?: string | null
[key: string]: unknown
}
export type AgentHumanContactConfig = {
channel?: string | null
contact_id?: string | null
contact_method?: string | null
email?: string | null
human_id?: string | null
id?: string | null
method?: string | null
name?: string | null
tenant_id?: string | null
[key: string]: unknown
}
export type AgentHumanToolConfig = {
description?: string | null
enabled?: boolean
name?: string | null
[key: string]: unknown
}
export type AgentKnowledgeDatasetConfig = {
description?: string | null
id?: string | null
name?: string | null
[key: string]: unknown
}
export type AgentKnowledgeQueryConfig = {
query?: string | null
score_threshold?: number | null
score_threshold_enabled?: boolean | null
top_k?: number | null
[key: string]: unknown
}
export type AgentKnowledgeQueryMode = 'generated_query' | 'user_query'
export type AgentMemoryArtifactConfig = {
id?: string | null
name?: string | null
type?: string | null
url?: string | null
[key: string]: unknown
}
export type AgentSoulModelCredentialRef = {
id?: string | null
provider?: string | null
type: string
}
export type AgentSoulModelSettings = {
frequency_penalty?: number | null
max_tokens?: number | null
presence_penalty?: number | null
response_format?: AgentModelResponseFormatConfig | null
stop?: Array<string> | null
temperature?: number | null
top_p?: number | null
}
export type AgentSandboxProviderConfig = {
cpu?: number | null
env?: Array<AgentEnvVariableConfig>
image?: string | null
working_dir?: string | null
[key: string]: unknown
}
export type AgentFileRefConfig = {
drive_key?: string | null
file_id?: string | null
id?: string | null
name?: string | null
reference?: string | null
remote_url?: string | null
tenant_id?: string | null
transfer_method?: string | null
type?: string | null
upload_file_id?: string | null
url?: string | null
[key: string]: unknown
}
export type AgentSkillRefConfig = {
description?: string | null
file_id?: string | null
full_archive_file_id?: string | null
full_archive_key?: string | null
id?: string | null
manifest_files?: Array<string> | null
name?: string | null
path?: string | null
skill_md_file_id?: string | null
skill_md_key?: string | null
[key: string]: unknown
}
export type AgentCliToolConfig = {
approved?: boolean
authorization_status?: AgentCliToolAuthorizationStatus | null
command?: string | null
dangerous?: boolean
dangerous_accepted?: boolean
dangerous_acknowledged?: boolean
dangerous_command?: boolean
description?: string | null
enabled?: boolean
env?: AgentCliToolEnvConfig
id?: string | null
inferred_from?: string | null
install?: string | null
install_command?: string | null
install_commands?: Array<string>
invoke_metadata?: {
[key: string]: unknown
}
label?: string | null
name?: string | null
permission?: AgentPermissionConfig | null
pre_authorized?: boolean | null
requires_confirmation?: boolean
risk_accepted?: boolean
risk_level?: AgentCliToolRiskLevel | null
setup_command?: string | null
tool_name?: string | null
[key: string]: unknown
}
export type AgentSoulDifyToolConfig = {
credential_ref?: AgentSoulDifyToolCredentialRef | null
credential_type?: 'api-key' | 'oauth2' | 'unauthorized'
description?: string | null
enabled?: boolean
name?: string | null
plugin_id?: string | null
provider?: string | null
provider_id?: string | null
provider_type?: string
runtime_parameters?: {
[key: string]:
| string
| number
| number
| boolean
| Array<string>
| Array<number>
| Array<number>
| Array<boolean>
| null
}
tool_name?: string | null
}
export type AgentModerationProviderConfig = {
api_based_extension_id?: string | null
inputs_config?: AgentModerationIoConfig | null
keywords?: string | null
outputs_config?: AgentModerationIoConfig | null
[key: string]: unknown
}
export type AgentPermissionConfig = {
allowed?: boolean | null
state?: string | null
status?: string | null
}
export type AgentModelResponseFormatConfig = {
type?: string | null
[key: string]: unknown
}
export type AgentCliToolAuthorizationStatus
= | 'allowed'
| 'authorized'
| 'denied'
| 'forbidden'
| 'not_required'
| 'pending'
| 'pre_authorized'
| 'unauthorized'
export type AgentCliToolEnvConfig = {
secret_refs?: Array<AgentSecretRefConfig>
variables?: Array<AgentEnvVariableConfig>
}
export type AgentCliToolRiskLevel = 'dangerous' | 'safe' | 'unknown'
export type AgentSoulDifyToolCredentialRef = {
id?: string | null
provider?: string | null
type?: 'provider' | 'tool'
}
export type AgentModerationIoConfig = {
enabled?: boolean
preset_response?: string | null
[key: string]: unknown
}
export type GetAgentsData = {
body?: never
path?: never
query?: {
keyword?: string
limit?: number
page?: number
}
url: '/agents'
}
export type GetAgentsResponses = {
200: AgentRosterListResponse
}
export type GetAgentsResponse = GetAgentsResponses[keyof GetAgentsResponses]
export type GetAgentsInviteOptionsData = {
body?: never
path?: never
query?: {
app_id?: string
keyword?: string
limit?: number
page?: number
}
url: '/agents/invite-options'
}
export type GetAgentsInviteOptionsResponses = {
200: AgentInviteOptionsResponse
}
export type GetAgentsInviteOptionsResponse
= GetAgentsInviteOptionsResponses[keyof GetAgentsInviteOptionsResponses]
export type GetAgentsByAgentIdData = {
body?: never
path: {
agent_id: string
}
query?: never
url: '/agents/{agent_id}'
}
export type GetAgentsByAgentIdResponses = {
200: AgentRosterResponse
}
export type GetAgentsByAgentIdResponse
= GetAgentsByAgentIdResponses[keyof GetAgentsByAgentIdResponses]
export type GetAgentsByAgentIdVersionsData = {
body?: never
path: {
agent_id: string
}
query?: never
url: '/agents/{agent_id}/versions'
}
export type GetAgentsByAgentIdVersionsResponses = {
200: AgentConfigSnapshotListResponse
}
export type GetAgentsByAgentIdVersionsResponse
= GetAgentsByAgentIdVersionsResponses[keyof GetAgentsByAgentIdVersionsResponses]
export type GetAgentsByAgentIdVersionsByVersionIdData = {
body?: never
path: {
agent_id: string
version_id: string
}
query?: never
url: '/agents/{agent_id}/versions/{version_id}'
}
export type GetAgentsByAgentIdVersionsByVersionIdResponses = {
200: AgentConfigSnapshotDetailResponse
}
export type GetAgentsByAgentIdVersionsByVersionIdResponse
= GetAgentsByAgentIdVersionsByVersionIdResponses[keyof GetAgentsByAgentIdVersionsByVersionIdResponses]

View File

@ -1,743 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import * as z from 'zod'
/**
* AgentConfigSnapshotSummaryResponse
*/
export const zAgentConfigSnapshotSummaryResponse = z.object({
agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
id: z.string(),
summary: z.string().nullish(),
version: z.int(),
version_note: z.string().nullish(),
})
/**
* AgentConfigSnapshotListResponse
*/
export const zAgentConfigSnapshotListResponse = z.object({
data: z.array(zAgentConfigSnapshotSummaryResponse),
})
/**
* AgentKind
*
* Agent implementation family.
*
* This leaves room for future non-Dify agent implementations while keeping
* the current roster/workflow APIs scoped to Dify Agent.
*/
export const zAgentKind = z.enum(['dify_agent'])
/**
* AgentIconType
*
* Supported icon storage formats for Agent roster entries.
*/
export const zAgentIconType = z.enum(['emoji', 'image', 'link'])
/**
* AgentPublishedReferenceResponse
*/
export const zAgentPublishedReferenceResponse = z.object({
app_id: z.string(),
app_mode: z.string(),
app_name: z.string(),
node_ids: z.array(z.string()).optional(),
workflow_id: z.string(),
workflow_version: z.string(),
})
/**
* AgentScope
*
* Visibility and lifecycle scope of an Agent record.
*/
export const zAgentScope = z.enum(['roster', 'workflow_only'])
/**
* AgentSource
*
* Origin that created or imported the Agent.
*/
export const zAgentSource = z.enum(['agent_app', 'imported', 'roster', 'system', 'workflow'])
/**
* AgentStatus
*
* Soft lifecycle state for Agent records.
*/
export const zAgentStatus = z.enum(['active', 'archived'])
/**
* AgentRosterResponse
*/
export const zAgentRosterResponse = z.object({
active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(),
active_config_snapshot_id: z.string().nullish(),
agent_kind: zAgentKind,
app_id: z.string().nullish(),
archived_at: z.int().nullish(),
archived_by: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
description: z.string(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: zAgentIconType.nullish(),
id: z.string(),
name: z.string(),
published_node_reference_count: z.int().optional().default(0),
published_reference_count: z.int().optional().default(0),
published_references: z.array(zAgentPublishedReferenceResponse).optional(),
role: z.string().optional().default(''),
scope: zAgentScope,
source: zAgentSource,
status: zAgentStatus,
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
workflow_id: z.string().nullish(),
workflow_node_id: z.string().nullish(),
})
/**
* AgentRosterListResponse
*/
export const zAgentRosterListResponse = z.object({
data: z.array(zAgentRosterResponse),
has_more: z.boolean(),
limit: z.int(),
page: z.int(),
total: z.int(),
})
/**
* AgentInviteOptionResponse
*/
export const zAgentInviteOptionResponse = z.object({
active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(),
active_config_snapshot_id: z.string().nullish(),
agent_kind: zAgentKind,
app_id: z.string().nullish(),
archived_at: z.int().nullish(),
archived_by: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
description: z.string(),
existing_node_ids: z.array(z.string()).optional(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
icon_type: zAgentIconType.nullish(),
id: z.string(),
in_current_workflow_count: z.int().optional().default(0),
is_in_current_workflow: z.boolean().optional().default(false),
name: z.string(),
published_node_reference_count: z.int().optional().default(0),
published_reference_count: z.int().optional().default(0),
published_references: z.array(zAgentPublishedReferenceResponse).optional(),
role: z.string().optional().default(''),
scope: zAgentScope,
source: zAgentSource,
status: zAgentStatus,
updated_at: z.int().nullish(),
updated_by: z.string().nullish(),
workflow_id: z.string().nullish(),
workflow_node_id: z.string().nullish(),
})
/**
* AgentInviteOptionsResponse
*/
export const zAgentInviteOptionsResponse = z.object({
data: z.array(zAgentInviteOptionResponse),
has_more: z.boolean(),
limit: z.int(),
page: z.int(),
total: z.int(),
})
/**
* AppVariableConfig
*/
export const zAppVariableConfig = z.object({
default: z.unknown().optional(),
name: z.string().min(1).max(255),
required: z.boolean().optional().default(false),
type: z.string().min(1).max(64),
})
/**
* AgentSoulPromptConfig
*/
export const zAgentSoulPromptConfig = z.object({
system_prompt: z.string().optional().default(''),
})
/**
* AgentConfigRevisionOperation
*
* Audit operation recorded for Agent Soul version/revision changes.
*/
export const zAgentConfigRevisionOperation = z.enum([
'create_version',
'save_current_version',
'save_new_agent',
'save_new_version',
'save_to_roster',
])
/**
* AgentConfigRevisionResponse
*/
export const zAgentConfigRevisionResponse = z.object({
created_at: z.int().nullish(),
created_by: z.string().nullish(),
current_snapshot_id: z.string(),
id: z.string(),
operation: zAgentConfigRevisionOperation,
previous_snapshot_id: z.string().nullish(),
revision: z.int(),
summary: z.string().nullish(),
version_note: z.string().nullish(),
})
/**
* AgentFeatureToggleConfig
*/
export const zAgentFeatureToggleConfig = z.object({
enabled: z.boolean().optional().default(false),
})
/**
* AgentTextToSpeechFeatureConfig
*/
export const zAgentTextToSpeechFeatureConfig = z.object({
autoPlay: z.string().nullish(),
enabled: z.boolean().optional().default(false),
language: z.string().nullish(),
voice: z.string().nullish(),
})
/**
* AgentEnvVariableConfig
*/
export const zAgentEnvVariableConfig = z.object({
default: z
.union([
z.string(),
z.int(),
z.number(),
z.boolean(),
z.array(z.string()),
z.array(z.int()),
z.array(z.number()),
z.array(z.boolean()),
])
.nullish(),
env_name: z.string().max(255).nullish(),
key: z.string().max(255).nullish(),
name: z.string().max(255).nullish(),
required: z.boolean().optional().default(false),
type: z.string().max(64).nullish(),
value: z
.union([
z.string(),
z.int(),
z.number(),
z.boolean(),
z.array(z.string()),
z.array(z.int()),
z.array(z.number()),
z.array(z.boolean()),
])
.nullish(),
variable: z.string().max(255).nullish(),
})
/**
* AgentHumanContactConfig
*/
export const zAgentHumanContactConfig = z.object({
channel: z.string().max(64).nullish(),
contact_id: z.string().max(255).nullish(),
contact_method: z.string().max(64).nullish(),
email: z.string().max(255).nullish(),
human_id: z.string().max(255).nullish(),
id: z.string().max(255).nullish(),
method: z.string().max(64).nullish(),
name: z.string().max(255).nullish(),
tenant_id: z.string().max(255).nullish(),
})
/**
* AgentHumanToolConfig
*/
export const zAgentHumanToolConfig = z.object({
description: z.string().nullish(),
enabled: z.boolean().optional().default(true),
name: z.string().max(255).nullish(),
})
/**
* AgentSoulHumanConfig
*/
export const zAgentSoulHumanConfig = z.object({
contacts: z.array(zAgentHumanContactConfig).optional(),
tools: z.array(zAgentHumanToolConfig).optional(),
})
/**
* AgentKnowledgeDatasetConfig
*/
export const zAgentKnowledgeDatasetConfig = z.object({
description: z.string().nullish(),
id: z.string().max(255).nullish(),
name: z.string().max(255).nullish(),
})
/**
* AgentKnowledgeQueryConfig
*/
export const zAgentKnowledgeQueryConfig = z.object({
query: z.string().nullish(),
score_threshold: z.number().gte(0).lte(1).nullish(),
score_threshold_enabled: z.boolean().nullish(),
top_k: z.int().gte(1).nullish(),
})
/**
* AgentKnowledgeQueryMode
*/
export const zAgentKnowledgeQueryMode = z.enum(['generated_query', 'user_query'])
/**
* AgentSoulKnowledgeConfig
*/
export const zAgentSoulKnowledgeConfig = z.object({
datasets: z.array(zAgentKnowledgeDatasetConfig).optional(),
query_config: zAgentKnowledgeQueryConfig.optional(),
query_mode: zAgentKnowledgeQueryMode.nullish(),
})
/**
* AgentMemoryArtifactConfig
*/
export const zAgentMemoryArtifactConfig = z.object({
id: z.string().max(255).nullish(),
name: z.string().max(255).nullish(),
type: z.string().max(64).nullish(),
url: z.string().nullish(),
})
/**
* AgentSoulMemoryConfig
*/
export const zAgentSoulMemoryConfig = z.object({
artifacts: z.array(zAgentMemoryArtifactConfig).optional(),
budget: z.string().nullish(),
scope: z.string().nullish(),
})
/**
* AgentSoulModelCredentialRef
*
* Reference to model credentials resolved only at runtime.
*/
export const zAgentSoulModelCredentialRef = z.object({
id: z.string().max(255).nullish(),
provider: z.string().max(255).nullish(),
type: z.string().min(1).max(64),
})
/**
* AgentSandboxProviderConfig
*/
export const zAgentSandboxProviderConfig = z.object({
cpu: z.int().gte(1).nullish(),
env: z.array(zAgentEnvVariableConfig).optional(),
image: z.string().nullish(),
working_dir: z.string().nullish(),
})
/**
* AgentSoulSandboxConfig
*/
export const zAgentSoulSandboxConfig = z.object({
config: zAgentSandboxProviderConfig.optional(),
provider: z.string().nullish(),
})
/**
* AgentFileRefConfig
*/
export const zAgentFileRefConfig = z.object({
drive_key: z.string().max(512).nullish(),
file_id: z.string().max(255).nullish(),
id: z.string().max(255).nullish(),
name: z.string().max(255).nullish(),
reference: z.string().max(255).nullish(),
remote_url: z.string().nullish(),
tenant_id: z.string().max(255).nullish(),
transfer_method: z.string().max(64).nullish(),
type: z.string().max(64).nullish(),
upload_file_id: z.string().max(255).nullish(),
url: z.string().nullish(),
})
/**
* AgentSkillRefConfig
*/
export const zAgentSkillRefConfig = z.object({
description: z.string().nullish(),
file_id: z.string().max(255).nullish(),
full_archive_file_id: z.string().max(255).nullish(),
full_archive_key: z.string().max(512).nullish(),
id: z.string().max(255).nullish(),
manifest_files: z.array(z.string()).nullish(),
name: z.string().max(255).nullish(),
path: z.string().nullish(),
skill_md_file_id: z.string().max(255).nullish(),
skill_md_key: z.string().max(512).nullish(),
})
/**
* AgentSoulSkillsFilesConfig
*/
export const zAgentSoulSkillsFilesConfig = z.object({
files: z.array(zAgentFileRefConfig).optional(),
skills: z.array(zAgentSkillRefConfig).optional(),
})
/**
* AgentPermissionConfig
*/
export const zAgentPermissionConfig = z.object({
allowed: z.boolean().nullish(),
state: z.string().max(64).nullish(),
status: z.string().max(64).nullish(),
})
/**
* AgentSecretRefConfig
*/
export const zAgentSecretRefConfig = z.object({
credential_id: z.string().max(255).nullish(),
env_name: z.string().max(255).nullish(),
id: z.string().max(255).nullish(),
key: z.string().max(255).nullish(),
name: z.string().max(255).nullish(),
permission: zAgentPermissionConfig.nullish(),
permission_status: z.string().max(64).nullish(),
provider: z.string().max(255).nullish(),
provider_credential_id: z.string().max(255).nullish(),
ref: z.string().max(255).nullish(),
type: z.string().max(64).nullish(),
variable: z.string().max(255).nullish(),
})
/**
* AgentSoulEnvConfig
*/
export const zAgentSoulEnvConfig = z.object({
secret_refs: z.array(zAgentSecretRefConfig).optional(),
variables: z.array(zAgentEnvVariableConfig).optional(),
})
/**
* AgentModelResponseFormatConfig
*/
export const zAgentModelResponseFormatConfig = z.object({
type: z.string().max(64).nullish(),
})
/**
* AgentSoulModelSettings
*/
export const zAgentSoulModelSettings = z.object({
frequency_penalty: z.number().nullish(),
max_tokens: z.int().nullish(),
presence_penalty: z.number().nullish(),
response_format: zAgentModelResponseFormatConfig.nullish(),
stop: z.array(z.string()).nullish(),
temperature: z.number().nullish(),
top_p: z.number().nullish(),
})
/**
* AgentSoulModelConfig
*
* Stable model selection for Agent runtime without storing secret values.
*/
export const zAgentSoulModelConfig = z.object({
credential_ref: zAgentSoulModelCredentialRef.nullish(),
model: z.string().min(1).max(255),
model_provider: z.string().min(1).max(255),
model_settings: zAgentSoulModelSettings.optional(),
plugin_id: z.string().min(1).max(255),
})
/**
* AgentSuggestedQuestionsAfterAnswerFeatureConfig
*/
export const zAgentSuggestedQuestionsAfterAnswerFeatureConfig = z.object({
enabled: z.boolean().optional().default(false),
model: zAgentSoulModelConfig.nullish(),
prompt: z.string().nullish(),
})
/**
* AgentCliToolAuthorizationStatus
*
* Authorization state for Agent-scoped CLI tools.
*
* Missing status keeps backward compatibility with draft rows and CLI tools that
* do not need pre-authorization. Explicit denied-like states are blocked by the
* composer/publish validators and skipped by runtime request builders.
*/
export const zAgentCliToolAuthorizationStatus = z.enum([
'allowed',
'authorized',
'denied',
'forbidden',
'not_required',
'pending',
'pre_authorized',
'unauthorized',
])
/**
* AgentCliToolEnvConfig
*/
export const zAgentCliToolEnvConfig = z.object({
secret_refs: z.array(zAgentSecretRefConfig).optional(),
variables: z.array(zAgentEnvVariableConfig).optional(),
})
/**
* AgentCliToolRiskLevel
*
* Risk marker for CLI tool bootstrap commands.
*/
export const zAgentCliToolRiskLevel = z.enum(['dangerous', 'safe', 'unknown'])
/**
* AgentCliToolConfig
*/
export const zAgentCliToolConfig = z.object({
approved: z.boolean().optional().default(false),
authorization_status: zAgentCliToolAuthorizationStatus.nullish(),
command: z.string().nullish(),
dangerous: z.boolean().optional().default(false),
dangerous_accepted: z.boolean().optional().default(false),
dangerous_acknowledged: z.boolean().optional().default(false),
dangerous_command: z.boolean().optional().default(false),
description: z.string().nullish(),
enabled: z.boolean().optional().default(true),
env: zAgentCliToolEnvConfig.optional(),
id: z.string().max(255).nullish(),
inferred_from: z.string().max(255).nullish(),
install: z.string().nullish(),
install_command: z.string().nullish(),
install_commands: z.array(z.string()).optional(),
invoke_metadata: z.record(z.string(), z.unknown()).optional(),
label: z.string().max(255).nullish(),
name: z.string().max(255).nullish(),
permission: zAgentPermissionConfig.nullish(),
pre_authorized: z.boolean().nullish(),
requires_confirmation: z.boolean().optional().default(false),
risk_accepted: z.boolean().optional().default(false),
risk_level: zAgentCliToolRiskLevel.nullish(),
setup_command: z.string().nullish(),
tool_name: z.string().max(255).nullish(),
})
/**
* AgentSoulDifyToolCredentialRef
*
* Reference to a stored Dify Plugin Tool credential.
*
* Secret values are resolved only at runtime. The legacy ``credential_id``
* field is accepted by :class:`AgentSoulDifyToolConfig` and normalized here so
* old Agent tool payloads can be read while new payloads stay explicit.
*/
export const zAgentSoulDifyToolCredentialRef = z.object({
id: z.string().max(255).nullish(),
provider: z.string().max(255).nullish(),
type: z.enum(['provider', 'tool']).optional().default('tool'),
})
/**
* AgentSoulDifyToolConfig
*
* One Dify Plugin Tool configured on Agent Soul.
*
* The API backend prepares this persisted product shape into
* ``DifyPluginToolConfig`` before sending a run request to Agent backend.
* ``provider_id`` keeps compatibility with existing Agent tool config payloads;
* new callers should send ``plugin_id`` + ``provider`` when available.
*/
export const zAgentSoulDifyToolConfig = z.object({
credential_ref: zAgentSoulDifyToolCredentialRef.nullish(),
credential_type: z.enum(['api-key', 'oauth2', 'unauthorized']).optional().default('api-key'),
description: z.string().nullish(),
enabled: z.boolean().optional().default(true),
name: z.string().max(255).nullish(),
plugin_id: z.string().max(255).nullish(),
provider: z.string().max(255).nullish(),
provider_id: z.string().max(255).nullish(),
provider_type: z.string().optional().default('plugin'),
runtime_parameters: z
.record(
z.string(),
z
.union([
z.string(),
z.int(),
z.number(),
z.boolean(),
z.array(z.string()),
z.array(z.int()),
z.array(z.number()),
z.array(z.boolean()),
])
.nullable(),
)
.optional(),
tool_name: z.string().min(1).max(255).nullish(),
})
/**
* AgentSoulToolsConfig
*/
export const zAgentSoulToolsConfig = z.object({
cli_tools: z.array(zAgentCliToolConfig).optional(),
dify_tools: z.array(zAgentSoulDifyToolConfig).optional(),
})
/**
* AgentModerationIOConfig
*/
export const zAgentModerationIoConfig = z.object({
enabled: z.boolean().optional().default(false),
preset_response: z.string().nullish(),
})
/**
* AgentModerationProviderConfig
*/
export const zAgentModerationProviderConfig = z.object({
api_based_extension_id: z.string().nullish(),
inputs_config: zAgentModerationIoConfig.nullish(),
keywords: z.string().nullish(),
outputs_config: zAgentModerationIoConfig.nullish(),
})
/**
* AgentSensitiveWordAvoidanceFeatureConfig
*/
export const zAgentSensitiveWordAvoidanceFeatureConfig = z.object({
config: zAgentModerationProviderConfig.nullish(),
enabled: z.boolean().optional().default(false),
type: z.string().nullish(),
})
/**
* AgentSoulAppFeaturesConfig
*/
export const zAgentSoulAppFeaturesConfig = z.object({
opening_statement: z.string().nullish(),
retriever_resource: zAgentFeatureToggleConfig.nullish(),
sensitive_word_avoidance: zAgentSensitiveWordAvoidanceFeatureConfig.nullish(),
speech_to_text: zAgentFeatureToggleConfig.nullish(),
suggested_questions: z.array(z.string()).nullish(),
suggested_questions_after_answer: zAgentSuggestedQuestionsAfterAnswerFeatureConfig.nullish(),
text_to_speech: zAgentTextToSpeechFeatureConfig.nullish(),
})
/**
* AgentSoulConfig
*/
export const zAgentSoulConfig = z.object({
app_features: zAgentSoulAppFeaturesConfig.optional(),
app_variables: z.array(zAppVariableConfig).optional(),
env: zAgentSoulEnvConfig.optional(),
human: zAgentSoulHumanConfig.optional(),
knowledge: zAgentSoulKnowledgeConfig.optional(),
memory: zAgentSoulMemoryConfig.optional(),
misc_legacy: zAgentSoulAppFeaturesConfig.optional(),
model: zAgentSoulModelConfig.nullish(),
prompt: zAgentSoulPromptConfig.optional(),
sandbox: zAgentSoulSandboxConfig.optional(),
schema_version: z.int().optional().default(1),
skills_files: zAgentSoulSkillsFilesConfig.optional(),
tools: zAgentSoulToolsConfig.optional(),
})
/**
* AgentConfigSnapshotDetailResponse
*/
export const zAgentConfigSnapshotDetailResponse = z.object({
agent_id: z.string().nullish(),
config_snapshot: zAgentSoulConfig,
created_at: z.int().nullish(),
created_by: z.string().nullish(),
id: z.string(),
revisions: z.array(zAgentConfigRevisionResponse).optional(),
summary: z.string().nullish(),
version: z.int(),
version_note: z.string().nullish(),
})
export const zGetAgentsQuery = z.object({
keyword: z.string().optional(),
limit: z.int().gte(1).lte(100).optional().default(20),
page: z.int().gte(1).optional().default(1),
})
/**
* Agent roster list
*/
export const zGetAgentsResponse = zAgentRosterListResponse
export const zGetAgentsInviteOptionsQuery = z.object({
app_id: z.string().optional(),
keyword: z.string().optional(),
limit: z.int().gte(1).lte(100).optional().default(20),
page: z.int().gte(1).optional().default(1),
})
/**
* Agent invite options
*/
export const zGetAgentsInviteOptionsResponse = zAgentInviteOptionsResponse
export const zGetAgentsByAgentIdPath = z.object({
agent_id: z.string(),
})
/**
* Agent detail
*/
export const zGetAgentsByAgentIdResponse = zAgentRosterResponse
export const zGetAgentsByAgentIdVersionsPath = z.object({
agent_id: z.string(),
})
/**
* Agent versions
*/
export const zGetAgentsByAgentIdVersionsResponse = zAgentConfigSnapshotListResponse
export const zGetAgentsByAgentIdVersionsByVersionIdPath = z.object({
agent_id: z.string(),
version_id: z.string(),
})
/**
* Agent version detail
*/
export const zGetAgentsByAgentIdVersionsByVersionIdResponse = zAgentConfigSnapshotDetailResponse

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
import { account } from './account/orpc.gen' import { account } from './account/orpc.gen'
import { activate } from './activate/orpc.gen' import { activate } from './activate/orpc.gen'
import { agents } from './agents/orpc.gen' import { agent } from './agent/orpc.gen'
import { allWorkspaces } from './all-workspaces/orpc.gen' import { allWorkspaces } from './all-workspaces/orpc.gen'
import { apiBasedExtension } from './api-based-extension/orpc.gen' import { apiBasedExtension } from './api-based-extension/orpc.gen'
import { apiKeyAuth } from './api-key-auth/orpc.gen' import { apiKeyAuth } from './api-key-auth/orpc.gen'
@ -53,7 +53,7 @@ import { workspaces } from './workspaces/orpc.gen'
export const contract = { export const contract = {
account, account,
activate, activate,
agents, agent,
allWorkspaces, allWorkspaces,
apiBasedExtension, apiBasedExtension,
apiKeyAuth, apiKeyAuth,