diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 9d64b141d9..d7935552ac 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -12,6 +12,7 @@ from controllers.console.app.app import ( AppListQuery, AppPagination, AppPartial, + CopyAppPayload, UpdateAppPayload, _normalize_app_list_query_args, ) @@ -134,6 +135,7 @@ register_schema_models( console_ns, AgentAppCreatePayload, AgentAppUpdatePayload, + CopyAppPayload, AgentInviteOptionsQuery, AgentLogsQuery, AgentStatisticsQuery, @@ -348,6 +350,34 @@ class AgentAppApi(Resource): return "", 204 +@console_ns.route("/agent//copy") +class AgentAppCopyApi(Resource): + @console_ns.expect(console_ns.models[CopyAppPayload.__name__]) + @console_ns.response(201, "Agent app copied 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, tenant_id: str, current_user: Account, agent_id: UUID): + args = CopyAppPayload.model_validate(console_ns.payload or {}) + copied_app = _agent_roster_service().duplicate_agent_app( + tenant_id=tenant_id, + agent_id=str(agent_id), + account=current_user, + name=args.name, + description=args.description, + icon_type=args.icon_type, + icon=args.icon, + icon_background=args.icon_background, + ) + return _serialize_agent_app_detail(copied_app), 201 + + @console_ns.route("/agent/invite-options") class AgentInviteOptionsApi(Resource): @console_ns.doc(params=query_params_from_model(AgentInviteOptionsQuery)) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index b413b1c017..3e21ed4fe0 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -508,6 +508,27 @@ Stop a running Agent App chat message generation | ---- | ----------- | ------ | | 200 | Agent app composer validation result | **application/json**: [AgentComposerValidateResponse](#agentcomposervalidateresponse)
| +### [POST] /agent/{agent_id}/copy +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CopyAppPayload](#copyapppayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Agent app copied successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 400 | Invalid request parameters | | +| 403 | Insufficient permissions | | + ### [GET] /agent/{agent_id}/drive/files List agent drive entries for an Agent App diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index 85636a9609..8e68174b35 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -19,7 +19,7 @@ from models.agent import ( ) from models.agent_config_entities import AgentSoulConfig from models.enums import AppStatus -from models.model import App, AppMode +from models.model import App, AppMode, IconType from models.workflow import Workflow from services.agent.agent_soul_state import agent_soul_has_model from services.agent.composer_validator import ComposerConfigValidator @@ -29,7 +29,10 @@ from services.agent.errors import ( AgentNotFoundError, AgentVersionNotFoundError, ) +from services.app_service import AppService, CreateAppParams +from services.enterprise.enterprise_service import EnterpriseService from services.entities.agent_entities import RosterAgentCreatePayload, RosterAgentUpdatePayload +from services.feature_service import FeatureService class AgentReferencingWorkflow(TypedDict): @@ -48,6 +51,28 @@ class AgentReferencingWorkflow(TypedDict): class AgentRosterService: + _APP_MODEL_CONFIG_COPY_FIELDS = ( + "opening_statement", + "suggested_questions", + "suggested_questions_after_answer", + "speech_to_text", + "text_to_speech", + "more_like_this", + "model", + "user_input_form", + "dataset_query_variable", + "pre_prompt", + "agent_mode", + "sensitive_word_avoidance", + "retriever_resource", + "prompt_type", + "chat_prompt_config", + "completion_prompt_config", + "dataset_configs", + "external_data_tools", + "file_upload", + ) + def __init__(self, session: Any): self._session = session @@ -418,6 +443,142 @@ class AgentRosterService: raise AgentNotFoundError() return app + def duplicate_agent_app( + self, + *, + tenant_id: str, + agent_id: str, + account: Any, + name: str | None = None, + description: str | None = None, + icon_type: Any = None, + icon: str | None = None, + icon_background: str | None = None, + ) -> App: + source_app = self.get_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + source_agent = self.get_app_backing_agent(tenant_id=tenant_id, app_id=source_app.id) + if source_agent is None: + raise AgentNotFoundError() + + copied_name = name or self._next_duplicate_agent_name(tenant_id=tenant_id, base_name=source_app.name) + copied_description = description if description is not None else source_app.description + copied_icon_type = icon_type if icon_type is not None else source_app.icon_type + copied_icon = icon if icon is not None else source_app.icon + copied_icon_background = icon_background if icon_background is not None else source_app.icon_background + + target_app = AppService().create_app( + tenant_id, + CreateAppParams( + name=copied_name, + description=copied_description, + mode="agent", + agent_role=source_agent.role or "", + icon_type=self._normalize_app_icon_type(copied_icon_type), + icon=copied_icon, + icon_background=copied_icon_background, + api_rph=source_app.api_rph or 0, + api_rpm=source_app.api_rpm or 0, + max_active_requests=source_app.max_active_requests, + ), + account, + ) + + target_app.enable_site = source_app.enable_site + target_app.enable_api = source_app.enable_api + target_app.use_icon_as_answer_icon = source_app.use_icon_as_answer_icon + target_app.tracing = source_app.tracing + + self._copy_app_model_config(source_app=source_app, target_app=target_app, account_id=account.id) + self._copy_agent_active_snapshot( + tenant_id=tenant_id, + source_agent=source_agent, + target_app_id=target_app.id, + account_id=account.id, + ) + self._session.commit() + + if FeatureService.get_system_features().webapp_auth.enabled: + try: + original_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(source_app.id) + access_mode = original_settings.access_mode + except Exception: + access_mode = "public" + EnterpriseService.WebAppAuth.update_app_access_mode(target_app.id, access_mode) + + return target_app + + @staticmethod + def _normalize_app_icon_type(icon_type: IconType | str | None) -> str | None: + if icon_type is None: + return None + if isinstance(icon_type, IconType): + return icon_type.value + return icon_type + + def _copy_app_model_config(self, *, source_app: App, target_app: App, account_id: str) -> None: + source_config = source_app.app_model_config + target_config = target_app.app_model_config + if source_config is None or target_config is None: + return + + for field_name in self._APP_MODEL_CONFIG_COPY_FIELDS: + setattr(target_config, field_name, getattr(source_config, field_name)) + target_config.updated_by = account_id + + def _copy_agent_active_snapshot( + self, + *, + tenant_id: str, + source_agent: Agent, + target_app_id: str, + account_id: str, + ) -> None: + target_agent = self.get_app_backing_agent(tenant_id=tenant_id, app_id=target_app_id) + if target_agent is None: + raise AgentNotFoundError() + + source_version = self._get_version( + tenant_id=tenant_id, + agent_id=source_agent.id, + version_id=source_agent.active_config_snapshot_id, + ) + target_version = self._get_version( + tenant_id=tenant_id, + agent_id=target_agent.id, + version_id=target_agent.active_config_snapshot_id, + ) + + target_version.config_snapshot = AgentSoulConfig.model_validate(source_version.config_snapshot_dict) + target_version.summary = source_version.summary + target_version.version_note = source_version.version_note + target_version.created_by = account_id + target_agent.active_config_has_model = agent_soul_has_model(target_version.config_snapshot) + target_agent.updated_by = account_id + + def _next_duplicate_agent_name(self, *, tenant_id: str, base_name: str) -> str: + suffix = " copy" + max_base_len = 255 - len(suffix) + first_candidate = f"{base_name[:max_base_len]}{suffix}" + candidates = [first_candidate] + for index in range(2, 100): + numbered_suffix = f" copy {index}" + candidates.append(f"{base_name[: 255 - len(numbered_suffix)]}{numbered_suffix}") + + existing_names = set( + self._session.scalars( + select(Agent.name).where( + Agent.tenant_id == tenant_id, + Agent.scope == AgentScope.ROSTER, + Agent.status == AgentStatus.ACTIVE, + Agent.name.in_(candidates), + ) + ).all() + ) + for candidate in candidates: + if candidate not in existing_names: + return candidate + return f"{base_name[:245]} copy {int(naive_utc_now().timestamp())}" + 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. diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index a6ae9e6933..1679cc7d6a 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -21,6 +21,7 @@ from controllers.console.agent.composer import ( ) from controllers.console.agent.roster import ( AgentAppApi, + AgentAppCopyApi, AgentAppListApi, AgentInviteOptionsApi, AgentLogsApi, @@ -140,6 +141,7 @@ def test_agent_v2_console_routes_are_agent_id_first() -> None: "/agent//composer/validate", "/agent//composer/candidates", "/agent//features", + "/agent//copy", "/agent//referencing-workflows", "/agent//drive/files", "/agent//sandbox/files", @@ -347,6 +349,52 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( assert captured["delete"] is app_model +def test_agent_app_copy_uses_agent_id_and_returns_agent_detail( + app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str +) -> None: + agent_id = "00000000-0000-0000-0000-000000000001" + current_user = SimpleNamespace(id=account_id) + copied_app = _app_detail_obj(id="copied-app", bound_agent_id="copied-agent") + captured: dict[str, object] = {} + + class FakeRosterService: + def duplicate_agent_app(self, **kwargs: object) -> object: + captured.update(kwargs) + return copied_app + + monkeypatch.setattr(roster_controller, "_agent_roster_service", lambda: FakeRosterService()) + monkeypatch.setattr( + roster_controller, + "_serialize_agent_app_detail", + lambda app_model: {"id": "copied-agent", "app_id": app_model.id, "name": app_model.name}, + ) + + with app.test_request_context( + "/console/api/agent/00000000-0000-0000-0000-000000000001/copy", + json={ + "name": "Iris copy", + "description": "Copied", + "icon_type": "emoji", + "icon": "sparkles", + "icon_background": "#fff", + }, + ): + copied, status = unwrap(AgentAppCopyApi.post)(AgentAppCopyApi(), "tenant-1", current_user, agent_id) + + assert status == 201 + assert copied == {"id": "copied-agent", "app_id": "copied-app", "name": "Iris"} + assert captured == { + "tenant_id": "tenant-1", + "agent_id": agent_id, + "account": current_user, + "name": "Iris copy", + "description": "Copied", + "icon_type": "emoji", + "icon": "sparkles", + "icon_background": "#fff", + } + + def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict[str, object] = {} diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index fc85719883..fb0a316648 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -23,6 +23,7 @@ from models.agent_config_entities import ( DeclaredOutputType, WorkflowNodeJobConfig, ) +from models.model import IconType from models.workflow import Workflow from services.agent import composer_service, roster_service from services.agent.agent_soul_state import agent_soul_has_model @@ -1309,6 +1310,267 @@ class TestAgentAppBackingAgent: with pytest.raises(roster_service.AgentNotFoundError): service.get_agent_app_model(tenant_id="tenant-1", agent_id="agent-x") + def test_duplicate_agent_app_copies_app_config_and_active_soul(self, monkeypatch): + source_config = SimpleNamespace( + opening_statement="hello", + suggested_questions='["q1"]', + suggested_questions_after_answer='{"enabled": true}', + speech_to_text='{"enabled": false}', + text_to_speech='{"enabled": false}', + more_like_this='{"enabled": false}', + model=None, + user_input_form=None, + dataset_query_variable=None, + pre_prompt=None, + agent_mode=None, + sensitive_word_avoidance=None, + retriever_resource='{"enabled": true}', + prompt_type="simple", + chat_prompt_config=None, + completion_prompt_config=None, + dataset_configs=None, + external_data_tools=None, + file_upload='{"image": {"enabled": true}}', + ) + target_config = SimpleNamespace(**dict.fromkeys(AgentRosterService._APP_MODEL_CONFIG_COPY_FIELDS)) + source_app = SimpleNamespace( + id="source-app", + tenant_id="tenant-1", + name="Iris", + description="source desc", + icon_type="emoji", + icon="robot", + icon_background="#fff", + api_rph=1, + api_rpm=2, + max_active_requests=3, + enable_site=False, + enable_api=True, + use_icon_as_answer_icon=True, + tracing="{}", + app_model_config=source_config, + ) + target_app = SimpleNamespace( + id="target-app", + app_model_config=target_config, + enable_site=True, + enable_api=True, + use_icon_as_answer_icon=False, + tracing=None, + ) + source_agent = Agent( + id="source-agent", + tenant_id="tenant-1", + name="Iris", + description="source desc", + role="Analyst", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + app_id="source-app", + active_config_snapshot_id="source-version", + active_config_has_model=True, + ) + target_agent = Agent( + id="target-agent", + tenant_id="tenant-1", + name="Iris copy", + description="source desc", + role="Analyst", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + app_id="target-app", + active_config_snapshot_id="target-version", + ) + source_version = AgentConfigSnapshot( + id="source-version", + tenant_id="tenant-1", + agent_id="source-agent", + version=1, + config_snapshot=_agent_soul_with_model(), + summary="configured", + version_note="v1", + created_by="account-1", + ) + target_version = AgentConfigSnapshot( + id="target-version", + tenant_id="tenant-1", + agent_id="target-agent", + version=1, + config_snapshot=AgentSoulConfig(), + created_by="account-1", + ) + session = FakeSession( + scalar=[source_agent, source_app, source_agent, target_agent, source_version, target_version], + scalars=[[]], + ) + captured: dict[str, object] = {} + + class FakeAppService: + def create_app(self, tenant_id: str, params, account: object) -> object: + captured["tenant_id"] = tenant_id + captured["params"] = params + captured["account"] = account + return target_app + + monkeypatch.setattr(roster_service, "AppService", FakeAppService) + monkeypatch.setattr( + roster_service.FeatureService, + "get_system_features", + lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)), + ) + + account = SimpleNamespace(id="account-1") + duplicated = AgentRosterService(session).duplicate_agent_app( + tenant_id="tenant-1", + agent_id="source-agent", + account=account, + ) + + assert duplicated is target_app + params = captured["params"] + assert params.name == "Iris copy" + assert params.mode == "agent" + assert params.agent_role == "Analyst" + assert target_app.enable_site is False + assert target_app.enable_api is True + assert target_app.use_icon_as_answer_icon is True + assert target_app.tracing == "{}" + assert target_config.opening_statement == "hello" + assert target_config.file_upload == '{"image": {"enabled": true}}' + assert target_config.updated_by == "account-1" + assert target_version.config_snapshot.model.model == "gpt-4o" + assert target_version.summary == "configured" + assert target_version.version_note == "v1" + assert target_agent.active_config_has_model is True + assert target_agent.updated_by == "account-1" + assert session.commits == 1 + + def test_duplicate_agent_app_inherits_webapp_access_mode(self, monkeypatch): + source_app = SimpleNamespace( + id="source-app", + tenant_id="tenant-1", + name="Iris", + description="source desc", + icon_type=None, + icon="robot", + icon_background="#fff", + api_rph=1, + api_rpm=2, + max_active_requests=3, + enable_site=True, + enable_api=True, + use_icon_as_answer_icon=False, + tracing=None, + ) + source_agent = SimpleNamespace(id="source-agent", role="Analyst") + target_app = SimpleNamespace(id="target-app") + session = FakeSession() + service = AgentRosterService(session) + monkeypatch.setattr(service, "get_agent_app_model", lambda **_: source_app) + monkeypatch.setattr(service, "get_app_backing_agent", lambda **_: source_agent) + monkeypatch.setattr(service, "_copy_app_model_config", lambda **_: None) + monkeypatch.setattr(service, "_copy_agent_active_snapshot", lambda **_: None) + monkeypatch.setattr(service, "_next_duplicate_agent_name", lambda **_: "Iris copy") + + class FakeAppService: + def create_app(self, tenant_id: str, params, account: object) -> object: + return target_app + + access_mode_updates = [] + + class FakeWebAppAuth: + @classmethod + def get_app_access_mode_by_id(cls, app_id: str) -> object: + return SimpleNamespace(access_mode="private") + + @classmethod + def update_app_access_mode(cls, app_id: str, access_mode: str) -> None: + access_mode_updates.append((app_id, access_mode)) + + monkeypatch.setattr(roster_service, "AppService", FakeAppService) + monkeypatch.setattr( + roster_service.FeatureService, + "get_system_features", + lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=True)), + ) + monkeypatch.setattr(roster_service.EnterpriseService, "WebAppAuth", FakeWebAppAuth) + + duplicated = service.duplicate_agent_app( + tenant_id="tenant-1", + agent_id="source-agent", + account=SimpleNamespace(id="account-1"), + ) + + assert duplicated is target_app + assert access_mode_updates == [("target-app", "private")] + + def test_duplicate_agent_app_falls_back_to_public_access_mode(self, monkeypatch): + source_app = SimpleNamespace( + id="source-app", + tenant_id="tenant-1", + name="Iris", + description="source desc", + icon_type=IconType.EMOJI, + icon="robot", + icon_background="#fff", + api_rph=1, + api_rpm=2, + max_active_requests=3, + enable_site=True, + enable_api=True, + use_icon_as_answer_icon=False, + tracing=None, + ) + source_agent = SimpleNamespace(id="source-agent", role="Analyst") + target_app = SimpleNamespace(id="target-app") + session = FakeSession() + service = AgentRosterService(session) + monkeypatch.setattr(service, "get_agent_app_model", lambda **_: source_app) + monkeypatch.setattr(service, "get_app_backing_agent", lambda **_: source_agent) + monkeypatch.setattr(service, "_copy_app_model_config", lambda **_: None) + monkeypatch.setattr(service, "_copy_agent_active_snapshot", lambda **_: None) + monkeypatch.setattr(service, "_next_duplicate_agent_name", lambda **_: "Iris copy") + + class FakeAppService: + def create_app(self, tenant_id: str, params, account: object) -> object: + return target_app + + access_mode_updates = [] + + class FakeWebAppAuth: + @classmethod + def get_app_access_mode_by_id(cls, app_id: str) -> object: + raise ValueError("not found") + + @classmethod + def update_app_access_mode(cls, app_id: str, access_mode: str) -> None: + access_mode_updates.append((app_id, access_mode)) + + monkeypatch.setattr(roster_service, "AppService", FakeAppService) + monkeypatch.setattr( + roster_service.FeatureService, + "get_system_features", + lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=True)), + ) + monkeypatch.setattr(roster_service.EnterpriseService, "WebAppAuth", FakeWebAppAuth) + + service.duplicate_agent_app( + tenant_id="tenant-1", + agent_id="source-agent", + account=SimpleNamespace(id="account-1"), + ) + + assert access_mode_updates == [("target-app", "public")] + + def test_normalize_app_icon_type(self): + assert AgentRosterService._normalize_app_icon_type(None) is None + assert AgentRosterService._normalize_app_icon_type(IconType.EMOJI) == "emoji" + assert AgentRosterService._normalize_app_icon_type("image") == "image" + class TestListWorkflowsReferencingAppAgent: def test_groups_bindings_by_workflow_app_and_sorts_by_name(self): diff --git a/packages/contracts/generated/api/console/agent/orpc.gen.ts b/packages/contracts/generated/api/console/agent/orpc.gen.ts index ba01699e2b..13522677ab 100644 --- a/packages/contracts/generated/api/console/agent/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agent/orpc.gen.ts @@ -61,6 +61,9 @@ import { zPostAgentByAgentIdComposerValidateBody, zPostAgentByAgentIdComposerValidatePath, zPostAgentByAgentIdComposerValidateResponse, + zPostAgentByAgentIdCopyBody, + zPostAgentByAgentIdCopyPath, + zPostAgentByAgentIdCopyResponse, zPostAgentByAgentIdFeaturesBody, zPostAgentByAgentIdFeaturesPath, zPostAgentByAgentIdFeaturesResponse, @@ -239,6 +242,22 @@ export const composer = { validate, } +export const post3 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postAgentByAgentIdCopy', + path: '/agent/{agent_id}/copy', + successStatus: 201, + tags: ['console'], + }) + .input(z.object({ body: zPostAgentByAgentIdCopyBody, params: zPostAgentByAgentIdCopyPath })) + .output(zPostAgentByAgentIdCopyResponse) + +export const copy = { + post: post3, +} + /** * Time-limited external signed URL for one Agent App drive value */ @@ -320,7 +339,7 @@ export const drive = { /** * Update an Agent App's presentation features (opener, follow-up, citations, ...) */ -export const post3 = oc +export const post4 = oc .route({ description: 'Update an Agent App\'s presentation features (opener, follow-up, citations, ...)', inputStructure: 'detailed', @@ -335,13 +354,13 @@ export const post3 = oc .output(zPostAgentByAgentIdFeaturesResponse) export const features = { - post: post3, + post: post4, } /** * Create or update Agent App message feedback */ -export const post4 = oc +export const post5 = oc .route({ description: 'Create or update Agent App message feedback', inputStructure: 'detailed', @@ -356,7 +375,7 @@ export const post4 = oc .output(zPostAgentByAgentIdFeedbacksResponse) export const feedbacks = { - post: post4, + post: post5, } /** @@ -379,7 +398,7 @@ export const delete_ = oc /** * Commit an uploaded file into the Agent App drive under files/ */ -export const post5 = oc +export const post6 = oc .route({ description: 'Commit an uploaded file into the Agent App drive under files/', inputStructure: 'detailed', @@ -394,7 +413,7 @@ export const post5 = oc export const files2 = { delete: delete_, - post: post5, + post: post6, } export const get9 = oc @@ -483,7 +502,7 @@ export const read = { /** * Upload one Agent App sandbox file as a Dify ToolFile mapping */ -export const post6 = oc +export const post7 = oc .route({ description: 'Upload one Agent App sandbox file as a Dify ToolFile mapping', inputStructure: 'detailed', @@ -501,7 +520,7 @@ export const post6 = oc .output(zPostAgentByAgentIdSandboxFilesUploadResponse) export const upload = { - post: post6, + post: post7, } /** @@ -537,7 +556,7 @@ export const sandbox = { /** * Validate + standardize a Skill into an Agent App drive */ -export const post7 = oc +export const post8 = oc .route({ description: 'Validate + standardize a Skill into an Agent App drive', inputStructure: 'detailed', @@ -551,13 +570,13 @@ export const post7 = oc .output(zPostAgentByAgentIdSkillsStandardizeResponse) export const standardize = { - post: post7, + post: post8, } /** * Upload + validate a Skill package for an Agent App */ -export const post8 = oc +export const post9 = oc .route({ description: 'Upload + validate a Skill package for an Agent App', inputStructure: 'detailed', @@ -571,13 +590,13 @@ export const post8 = oc .output(zPostAgentByAgentIdSkillsUploadResponse) export const upload2 = { - post: post8, + post: post9, } /** * Infer CLI tool + ENV suggestions from a standardized Agent App skill */ -export const post9 = oc +export const post10 = oc .route({ description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill', inputStructure: 'detailed', @@ -590,7 +609,7 @@ export const post9 = oc .output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse) export const inferTools = { - post: post9, + post: post10, } /** @@ -714,6 +733,7 @@ export const byAgentId = { put: put2, chatMessages, composer, + copy, drive, features, feedbacks, @@ -738,7 +758,7 @@ export const get18 = oc .input(z.object({ query: zGetAgentQuery.optional() })) .output(zGetAgentResponse) -export const post10 = oc +export const post11 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -752,7 +772,7 @@ export const post10 = oc export const agent = { get: get18, - post: post10, + post: post11, inviteOptions, byAgentId, } diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 77b203fb95..2373233d06 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -122,6 +122,14 @@ export type AgentComposerValidateResponse = { warnings?: Array } +export type CopyAppPayload = { + description?: string | null + icon?: string | null + icon_background?: string | null + icon_type?: IconType | null + name?: string | null +} + export type AgentDriveListResponse = { items?: Array } @@ -1703,6 +1711,27 @@ export type PostAgentByAgentIdComposerValidateResponses = { export type PostAgentByAgentIdComposerValidateResponse = PostAgentByAgentIdComposerValidateResponses[keyof PostAgentByAgentIdComposerValidateResponses] +export type PostAgentByAgentIdCopyData = { + body: CopyAppPayload + path: { + agent_id: string + } + query?: never + url: '/agent/{agent_id}/copy' +} + +export type PostAgentByAgentIdCopyErrors = { + 400: unknown + 403: unknown +} + +export type PostAgentByAgentIdCopyResponses = { + 201: AppDetailWithSite +} + +export type PostAgentByAgentIdCopyResponse + = PostAgentByAgentIdCopyResponses[keyof PostAgentByAgentIdCopyResponses] + export type GetAgentByAgentIdDriveFilesData = { body?: never path: { diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 7ea5583818..dec6a25079 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -109,6 +109,17 @@ export const zAgentAppUpdatePayload = z.object({ use_icon_as_answer_icon: z.boolean().nullish(), }) +/** + * CopyAppPayload + */ +export const zCopyAppPayload = z.object({ + description: z.string().max(400).nullish(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: zIconType.nullish(), + name: z.string().nullish(), +}) + /** * DeletedTool */ @@ -2180,6 +2191,17 @@ export const zPostAgentByAgentIdComposerValidatePath = z.object({ */ export const zPostAgentByAgentIdComposerValidateResponse = zAgentComposerValidateResponse +export const zPostAgentByAgentIdCopyBody = zCopyAppPayload + +export const zPostAgentByAgentIdCopyPath = z.object({ + agent_id: z.string(), +}) + +/** + * Agent app copied successfully + */ +export const zPostAgentByAgentIdCopyResponse = zAppDetailWithSite + export const zGetAgentByAgentIdDriveFilesPath = z.object({ agent_id: z.string(), })