From d21bf291bbd05e9a62ae4990b9301ebbc44546f9 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 15 Jun 2026 13:21:38 +0800 Subject: [PATCH] fix: align agent app backing roster API (#37442) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/agent/roster.py | 51 +----- api/controllers/console/app/app.py | 4 +- api/models/agent.py | 2 + api/openapi/markdown/console-openapi.md | 71 +------- api/services/agent/roster_service.py | 2 +- api/services/app_service.py | 93 +++++++++- .../console/agent/test_agent_controllers.py | 56 +----- .../console/app/test_app_response_models.py | 35 ++++ api/tests/unit_tests/models/test_agent.py | 1 + .../services/agent/test_agent_services.py | 1 + .../unit_tests/services/test_app_service.py | 81 +++++++++ .../generated/api/console/agents/orpc.gen.ts | 45 ----- .../generated/api/console/agents/types.gen.ts | 167 ++++++------------ .../generated/api/console/agents/zod.gen.ts | 69 +------- .../generated/api/console/apps/types.gen.ts | 104 +++++------ .../generated/api/console/apps/zod.gen.ts | 108 +++++------ 16 files changed, 388 insertions(+), 502 deletions(-) diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 1066e51fea..735e5fe88b 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -8,10 +8,8 @@ from controllers.common.schema import query_params_from_model, register_response from controllers.console import console_ns from controllers.console.wraps import ( account_initialization_required, - edit_permission_required, setup_required, with_current_tenant_id, - with_current_user_id, ) from extensions.ext_database import db from fields.agent_fields import ( @@ -25,7 +23,7 @@ from fields.agent_fields import ( from libs.helper import dump_response from libs.login import login_required from services.agent.roster_service import AgentRosterService -from services.entities.agent_entities import RosterAgentCreatePayload, RosterAgentUpdatePayload, RosterListQuery +from services.entities.agent_entities import RosterListQuery class AgentInviteOptionsQuery(RosterListQuery): @@ -40,8 +38,6 @@ register_schema_models( console_ns, AgentInviteOptionsQuery, AgentIdPath, - RosterAgentCreatePayload, - RosterAgentUpdatePayload, RosterListQuery, ) register_response_schema_models( @@ -76,23 +72,6 @@ class AgentRosterListApi(Resource): ), ) - @console_ns.expect(console_ns.models[RosterAgentCreatePayload.__name__]) - @console_ns.response(201, "Agent created", console_ns.models[AgentRosterResponse.__name__]) - @setup_required - @login_required - @account_initialization_required - @edit_permission_required - @with_current_user_id - @with_current_tenant_id - def post(self, tenant_id: str, account_id: str): - payload = RosterAgentCreatePayload.model_validate(console_ns.payload or {}) - service = _agent_roster_service() - agent = service.create_roster_agent(tenant_id=tenant_id, account_id=account_id, payload=payload) - return dump_response( - AgentRosterResponse, - service.get_roster_agent_detail(tenant_id=tenant_id, agent_id=agent.id), - ), 201 - @console_ns.route("/agents/invite-options") class AgentInviteOptionsApi(Resource): @@ -129,34 +108,6 @@ class AgentRosterDetailApi(Resource): _agent_roster_service().get_roster_agent_detail(tenant_id=tenant_id, agent_id=str(agent_id)), ) - @console_ns.expect(console_ns.models[RosterAgentUpdatePayload.__name__]) - @console_ns.response(200, "Agent updated", console_ns.models[AgentRosterResponse.__name__]) - @setup_required - @login_required - @account_initialization_required - @edit_permission_required - @with_current_user_id - @with_current_tenant_id - def patch(self, tenant_id: str, account_id: str, agent_id: UUID): - payload = RosterAgentUpdatePayload.model_validate(console_ns.payload or {}) - return dump_response( - AgentRosterResponse, - _agent_roster_service().update_roster_agent( - tenant_id=tenant_id, agent_id=str(agent_id), account_id=account_id, payload=payload - ), - ) - - @console_ns.response(204, "Agent archived") - @setup_required - @login_required - @account_initialization_required - @edit_permission_required - @with_current_user_id - @with_current_tenant_id - def delete(self, tenant_id: str, account_id: str, agent_id: UUID): - _agent_roster_service().archive_roster_agent(tenant_id=tenant_id, agent_id=str(agent_id), account_id=account_id) - return "", 204 - @console_ns.route("/agents//versions") class AgentRosterVersionsApi(Resource): diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 71e685cb71..f6bba27d56 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -581,7 +581,7 @@ class AppListApi(Resource): @console_ns.doc("create_app") @console_ns.doc(description="Create a new application") @console_ns.expect(console_ns.models[CreateAppPayload.__name__]) - @console_ns.response(201, "App created successfully", console_ns.models[AppDetail.__name__]) + @console_ns.response(201, "App created successfully", console_ns.models[AppDetailWithSite.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "Invalid request parameters") @setup_required @@ -605,7 +605,7 @@ class AppListApi(Resource): app_service = AppService() app = app_service.create_app(current_tenant_id, params, current_user) - app_detail = AppDetail.model_validate(app, from_attributes=True) + app_detail = AppDetailWithSite.model_validate(app, from_attributes=True) return app_detail.model_dump(mode="json"), 201 diff --git a/api/models/agent.py b/api/models/agent.py index 669bcff677..e60e09de81 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -38,6 +38,8 @@ class AgentScope(StrEnum): class AgentSource(StrEnum): """Origin that created or imported the Agent.""" + # Created directly as a reusable Agent Roster asset. + ROSTER = "roster" # Created from an Agent App composer. AGENT_APP = "agent_app" # Created from a Workflow Agent Composer flow. diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 211bf30a6d..cf63b9de26 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -308,19 +308,6 @@ Check if activation token is valid | ---- | ----------- | ------ | | 200 | Agent roster list | **application/json**: [AgentRosterListResponse](#agentrosterlistresponse)
| -### [POST] /agents -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [RosterAgentCreatePayload](#rosteragentcreatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Agent created | **application/json**: [AgentRosterResponse](#agentrosterresponse)
| - ### [GET] /agents/invite-options #### Parameters @@ -337,19 +324,6 @@ Check if activation token is valid | ---- | ----------- | ------ | | 200 | Agent invite options | **application/json**: [AgentInviteOptionsResponse](#agentinviteoptionsresponse)
| -### [DELETE] /agents/{agent_id} -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Agent archived | - ### [GET] /agents/{agent_id} #### Parameters @@ -363,25 +337,6 @@ Check if activation token is valid | ---- | ----------- | ------ | | 200 | Agent detail | **application/json**: [AgentRosterResponse](#agentrosterresponse)
| -### [PATCH] /agents/{agent_id} -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [RosterAgentUpdatePayload](#rosteragentupdatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Agent updated | **application/json**: [AgentRosterResponse](#agentrosterresponse)
| - ### [GET] /agents/{agent_id}/versions #### Parameters @@ -599,7 +554,7 @@ Create a new application | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | App created successfully | **application/json**: [AppDetail](#appdetail)
| +| 201 | App created successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | @@ -16965,30 +16920,6 @@ Model class for provider quota configuration. | summary | string | | No | | word_count | integer | | No | -#### RosterAgentCreatePayload - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| agent_soul | [AgentSoulConfig](#agentsoulconfig) | | No | -| description | string | | No | -| icon | string | | No | -| icon_background | string | | No | -| icon_type | [AgentIconType](#agenticontype) | | No | -| name | string | | Yes | -| role | string | | No | -| version_note | string | | No | - -#### RosterAgentUpdatePayload - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| description | string | | No | -| icon | string | | No | -| icon_background | string | | No | -| icon_type | [AgentIconType](#agenticontype) | | No | -| name | string | | No | -| role | string | | No | - #### RosterListQuery | Name | Type | Description | Required | diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index e54a0e9128..56f5b9f60e 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -213,7 +213,7 @@ class AgentRosterService: tenant_id: str, account_id: str, payload: RosterAgentCreatePayload, - source: AgentSource = AgentSource.AGENT_APP, + source: AgentSource = AgentSource.ROSTER, ) -> Agent: ComposerConfigValidator.validate_agent_soul(payload.agent_soul) diff --git a/api/services/app_service.py b/api/services/app_service.py index e9741b8e23..837e1a1449 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -1,6 +1,7 @@ import json import logging from collections.abc import Sequence +from datetime import datetime from typing import Any, Literal, TypedDict, cast, override import sqlalchemy as sa @@ -23,7 +24,7 @@ from graphon.model_runtime.model_providers.base.large_language_model import Larg from libs.datetime_utils import naive_utc_now from libs.login import current_user from models import Account -from models.agent import AgentIconType +from models.agent import Agent, AgentIconType, AgentScope, AgentSource, AgentStatus from models.model import App, AppMode, AppModelConfig, IconType, Site from models.tools import ApiToolProvider from services.billing_service import BillingService @@ -377,6 +378,62 @@ class AppService: use_icon_as_answer_icon: bool max_active_requests: int + @staticmethod + def _get_backing_agent_for_update(app: App) -> Agent | None: + if app.mode != AppMode.AGENT: + return None + return db.session.scalar( + select(Agent).where( + Agent.tenant_id == app.tenant_id, + Agent.app_id == app.id, + Agent.scope == AgentScope.ROSTER, + Agent.source == AgentSource.AGENT_APP, + Agent.status == AgentStatus.ACTIVE, + ) + ) + + @staticmethod + def _to_agent_icon_type(icon_type: IconType | str | None) -> AgentIconType | None: + if icon_type is None: + return None + value = icon_type.value if isinstance(icon_type, IconType) else icon_type + return AgentIconType(value) + + def _sync_backing_agent_identity( + self, + app: App, + *, + name: str | None = None, + description: str | None = None, + icon_type: IconType | str | None = None, + icon: str | None = None, + icon_background: str | None = None, + account_id: str | None = None, + updated_at: datetime | None = None, + ) -> None: + """Keep the Roster identity aligned with its Agent App shell. + + Agent Soul remains versioned through Composer. This helper only mirrors + user-facing identity fields so Roster and Agent Console do not drift. + """ + agent = self._get_backing_agent_for_update(app) + if agent is None: + return + + if name is not None: + agent.name = name + if description is not None: + agent.description = description + if icon_type is not None: + agent.icon_type = self._to_agent_icon_type(icon_type) + if icon is not None: + agent.icon = icon + if icon_background is not None: + agent.icon_background = icon_background + agent.updated_by = account_id + if updated_at is not None: + agent.updated_at = updated_at + def update_app(self, app: App, args: ArgsDict) -> App: """ Update app @@ -400,6 +457,16 @@ class AppService: app.max_active_requests = args.get("max_active_requests") app.updated_by = current_user.id app.updated_at = naive_utc_now() + self._sync_backing_agent_identity( + app, + name=app.name, + description=app.description, + icon_type=app.icon_type, + icon=app.icon, + icon_background=app.icon_background, + account_id=current_user.id, + updated_at=app.updated_at, + ) db.session.commit() app_was_updated.send(app) @@ -417,6 +484,12 @@ class AppService: app.name = name app.updated_by = current_user.id app.updated_at = naive_utc_now() + self._sync_backing_agent_identity( + app, + name=app.name, + account_id=current_user.id, + updated_at=app.updated_at, + ) db.session.commit() app_was_updated.send(app) @@ -441,6 +514,14 @@ class AppService: app.icon_type = icon_type if isinstance(icon_type, IconType) else IconType(icon_type) app.updated_by = current_user.id app.updated_at = naive_utc_now() + self._sync_backing_agent_identity( + app, + icon_type=app.icon_type, + icon=app.icon, + icon_background=app.icon_background, + account_id=current_user.id, + updated_at=app.updated_at, + ) db.session.commit() app_was_updated.send(app) @@ -493,6 +574,16 @@ class AppService: """ app_was_deleted.send(app) + backing_agent = self._get_backing_agent_for_update(app) + if backing_agent is not None: + now = naive_utc_now() + account_id = getattr(current_user, "id", None) + backing_agent.status = AgentStatus.ARCHIVED + backing_agent.archived_by = account_id + backing_agent.archived_at = now + backing_agent.updated_by = account_id + backing_agent.updated_at = now + db.session.delete(app) db.session.commit() 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 d3a43f00e2..0007bddfa5 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 @@ -1,6 +1,6 @@ from inspect import unwrap from types import SimpleNamespace -from typing import Protocol, cast +from typing import cast import pytest from flask import Flask @@ -128,10 +128,6 @@ def _get_app_model_modes(view) -> list[AppMode]: return [] -class _PayloadWithDescription(Protocol): - description: object - - @pytest.fixture def account_id() -> str: return "account-1" @@ -153,27 +149,10 @@ def test_roster_list_get_parses_query_and_calls_service(app: Flask, monkeypatch: assert captured == {"tenant_id": "tenant-1", "page": 2, "limit": 5, "keyword": "analyst"} -def test_roster_list_post_creates_agent_and_returns_detail( - app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str -) -> None: - created_agent = SimpleNamespace(id="agent-1") - monkeypatch.setattr( - roster_controller.AgentRosterService, - "create_roster_agent", - lambda _self, **kwargs: created_agent, - ) - monkeypatch.setattr( - roster_controller.AgentRosterService, - "get_roster_agent_detail", - lambda _self, **kwargs: _agent_response(kwargs["agent_id"]), - ) - - with app.test_request_context(json={"name": "Analyst", "agent_soul": {"prompt": {"system_prompt": "x"}}}): - result, status = unwrap(AgentRosterListApi.post)(AgentRosterListApi(), "tenant-1", account_id) - - assert status == 201 - assert result["id"] == "agent-1" - assert result["agent_kind"] == "dify_agent" +def test_roster_direct_mutation_endpoints_are_not_exposed() -> None: + assert not hasattr(AgentRosterListApi, "post") + assert not hasattr(AgentRosterDetailApi, "patch") + assert not hasattr(AgentRosterDetailApi, "delete") def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -192,30 +171,14 @@ def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.Monkey assert captured == {"tenant_id": "tenant-1", "page": 1, "limit": 10, "keyword": None, "app_id": "app-1"} -def test_roster_detail_patch_delete_and_versions_call_services( - app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str -) -> None: +def test_roster_detail_and_versions_call_services(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: agent_id = "00000000-0000-0000-0000-000000000001" version_id = "00000000-0000-0000-0000-000000000002" - archived: dict[str, object] = {} monkeypatch.setattr( roster_controller.AgentRosterService, "get_roster_agent_detail", lambda _self, **kwargs: _agent_response(cast(str, kwargs["agent_id"])), ) - monkeypatch.setattr( - roster_controller.AgentRosterService, - "update_roster_agent", - lambda _self, **kwargs: { - **_agent_response(cast(str, kwargs["agent_id"])), - "description": cast(_PayloadWithDescription, kwargs["payload"]).description, - }, - ) - monkeypatch.setattr( - roster_controller.AgentRosterService, - "archive_roster_agent", - lambda _self, **kwargs: archived.update(kwargs), - ) monkeypatch.setattr( roster_controller.AgentRosterService, "list_agent_versions", @@ -245,13 +208,6 @@ def test_roster_detail_patch_delete_and_versions_call_services( ) assert unwrap(AgentRosterDetailApi.get)(AgentRosterDetailApi(), "tenant-1", agent_id)["id"] == agent_id - with app.test_request_context(json={"description": "updated"}): - assert ( - unwrap(AgentRosterDetailApi.patch)(AgentRosterDetailApi(), "tenant-1", account_id, agent_id)["description"] - == "updated" - ) - assert unwrap(AgentRosterDetailApi.delete)(AgentRosterDetailApi(), "tenant-1", account_id, agent_id) == ("", 204) - assert archived["account_id"] == "account-1" assert ( unwrap(AgentRosterVersionsApi.get)(AgentRosterVersionsApi(), "tenant-1", agent_id)["data"][0]["id"] == "version-1" diff --git a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py index 35eac429c0..47126d2b92 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py @@ -314,6 +314,39 @@ def test_app_list_query_rejects_flat_tag_ids(app_module): app_module.AppListQuery.model_validate(normalized) +def test_create_agent_app_response_includes_bound_agent_id(app_module, monkeypatch: pytest.MonkeyPatch): + 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.create_app.return_value = app_obj + monkeypatch.setattr(app_module, "AppService", lambda: app_service) + + app_module.console_ns.payload = payload + try: + response, status = _unwrap(app_module.AppListApi().post)("tenant-1", SimpleNamespace(id="account-1")) + finally: + app_module.console_ns.payload = None + + assert status == 201 + 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): AppPartial = app_models.AppPartial created_at = _ts() @@ -389,6 +422,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models): max_active_requests=5, deleted_tools=[{"type": "api", "tool_name": "search", "provider_id": "prov"}], site=site, + bound_agent_id="agent-1", ) serialized = AppDetailWithSite.model_validate(app_obj, from_attributes=True).model_dump(mode="json") @@ -398,6 +432,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models): assert serialized["deleted_tools"][0]["tool_name"] == "search" assert serialized["site"]["icon_url"] == "signed:site-icon" assert serialized["site"]["created_at"] == int(timestamp.timestamp()) + assert serialized["bound_agent_id"] == "agent-1" def test_app_pagination_aliases_per_page_and_has_next(app_models): diff --git a/api/tests/unit_tests/models/test_agent.py b/api/tests/unit_tests/models/test_agent.py index 2efc64ac86..aabbd4df30 100644 --- a/api/tests/unit_tests/models/test_agent.py +++ b/api/tests/unit_tests/models/test_agent.py @@ -27,6 +27,7 @@ def test_agent_enums_match_prd_boundaries(): assert AgentIconType.EMOJI.value == "emoji" assert AgentScope.ROSTER.value == "roster" assert AgentScope.WORKFLOW_ONLY.value == "workflow_only" + assert AgentSource.ROSTER.value == "roster" assert AgentSource.AGENT_APP.value == "agent_app" assert AgentSource.WORKFLOW.value == "workflow" assert AgentStatus.ACTIVE.value == "active" 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 77d5049fec..b64fc56170 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -787,6 +787,7 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch): assert service._load_versions_by_id([]) == {} assert created.name == "Analyst" + assert created.source == AgentSource.ROSTER assert created.active_config_snapshot_id is not None assert created.active_config_has_model is False assert backing_agent.active_config_snapshot_id is not None diff --git a/api/tests/unit_tests/services/test_app_service.py b/api/tests/unit_tests/services/test_app_service.py index b0edfeaf90..bb76411264 100644 --- a/api/tests/unit_tests/services/test_app_service.py +++ b/api/tests/unit_tests/services/test_app_service.py @@ -1,5 +1,6 @@ from __future__ import annotations +from types import SimpleNamespace from unittest.mock import MagicMock, patch from models.model import App @@ -155,3 +156,83 @@ class TestAgentAppType: app = App() app.mode = AppMode.CHAT assert app.bound_agent_id is None + + def test_update_agent_app_syncs_backing_agent_identity(self): + from models.agent import AgentIconType + from models.model import AppMode, IconType + from services.app_service import AppService + + app = SimpleNamespace( + id="app-1", + tenant_id="tenant-1", + mode=AppMode.AGENT, + name="Old", + description="old", + icon_type=IconType.EMOJI, + icon="robot", + icon_background="#fff", + use_icon_as_answer_icon=False, + max_active_requests=None, + created_by="account-1", + ) + backing_agent = SimpleNamespace( + name="Old", + description="old", + icon_type=AgentIconType.EMOJI, + icon="robot", + icon_background="#fff", + updated_by=None, + updated_at=None, + ) + + with ( + patch("services.app_service.db") as mock_db, + patch("services.app_service.current_user", SimpleNamespace(id="account-2")), + ): + mock_db.session.scalar.return_value = backing_agent + updated_app = AppService().update_app( + app, # type: ignore[arg-type] + { + "name": "Iris", + "description": "agent app", + "icon_type": "image", + "icon": "file-id", + "icon_background": "#123456", + "use_icon_as_answer_icon": False, + "max_active_requests": 0, + }, + ) + + assert updated_app.name == "Iris" + assert backing_agent.name == "Iris" + assert backing_agent.description == "agent app" + assert backing_agent.icon_type == AgentIconType.IMAGE + assert backing_agent.icon == "file-id" + assert backing_agent.icon_background == "#123456" + assert backing_agent.updated_by == "account-2" + assert backing_agent.updated_at == updated_app.updated_at + + def test_delete_agent_app_archives_backing_agent(self): + from models.agent import AgentStatus + from models.model import AppMode + from services.app_service import AppService + + app = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT) + backing_agent = SimpleNamespace(status=AgentStatus.ACTIVE, archived_by=None, archived_at=None) + + with ( + patch("services.app_service.db") as mock_db, + patch("services.app_service.current_user", SimpleNamespace(id="account-2")), + patch("services.app_service.BillingService"), + patch("services.app_service.EnterpriseService"), + patch("services.app_service.FeatureService"), + patch("services.app_service.dify_config"), + patch("services.app_service.remove_app_and_related_data_task"), + ): + mock_db.session.scalar.return_value = backing_agent + AppService().delete_app(app) # type: ignore[arg-type] + + assert backing_agent.status == AgentStatus.ARCHIVED + assert backing_agent.archived_by == "account-2" + assert backing_agent.archived_at is not None + mock_db.session.delete.assert_called_once_with(app) diff --git a/packages/contracts/generated/api/console/agents/orpc.gen.ts b/packages/contracts/generated/api/console/agents/orpc.gen.ts index 25c1ee7cc7..0a01a8f3c9 100644 --- a/packages/contracts/generated/api/console/agents/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agents/orpc.gen.ts @@ -4,8 +4,6 @@ import { oc } from '@orpc/contract' import * as z from 'zod' import { - zDeleteAgentsByAgentIdPath, - zDeleteAgentsByAgentIdResponse, zGetAgentsByAgentIdPath, zGetAgentsByAgentIdResponse, zGetAgentsByAgentIdVersionsByVersionIdPath, @@ -16,11 +14,6 @@ import { zGetAgentsInviteOptionsResponse, zGetAgentsQuery, zGetAgentsResponse, - zPatchAgentsByAgentIdBody, - zPatchAgentsByAgentIdPath, - zPatchAgentsByAgentIdResponse, - zPostAgentsBody, - zPostAgentsResponse, } from './zod.gen' export const get = oc @@ -69,18 +62,6 @@ export const versions = { byVersionId, } -export const delete_ = oc - .route({ - inputStructure: 'detailed', - method: 'DELETE', - operationId: 'deleteAgentsByAgentId', - path: '/agents/{agent_id}', - successStatus: 204, - tags: ['console'], - }) - .input(z.object({ params: zDeleteAgentsByAgentIdPath })) - .output(zDeleteAgentsByAgentIdResponse) - export const get4 = oc .route({ inputStructure: 'detailed', @@ -92,21 +73,8 @@ export const get4 = oc .input(z.object({ params: zGetAgentsByAgentIdPath })) .output(zGetAgentsByAgentIdResponse) -export const patch = oc - .route({ - inputStructure: 'detailed', - method: 'PATCH', - operationId: 'patchAgentsByAgentId', - path: '/agents/{agent_id}', - tags: ['console'], - }) - .input(z.object({ body: zPatchAgentsByAgentIdBody, params: zPatchAgentsByAgentIdPath })) - .output(zPatchAgentsByAgentIdResponse) - export const byAgentId = { - delete: delete_, get: get4, - patch, versions, } @@ -121,21 +89,8 @@ export const get5 = oc .input(z.object({ query: zGetAgentsQuery.optional() })) .output(zGetAgentsResponse) -export const post = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAgents', - path: '/agents', - successStatus: 201, - tags: ['console'], - }) - .input(z.object({ body: zPostAgentsBody })) - .output(zPostAgentsResponse) - export const agents = { get: get5, - post, inviteOptions, byAgentId, } diff --git a/packages/contracts/generated/api/console/agents/types.gen.ts b/packages/contracts/generated/api/console/agents/types.gen.ts index b40c31bb6a..eab4cbea18 100644 --- a/packages/contracts/generated/api/console/agents/types.gen.ts +++ b/packages/contracts/generated/api/console/agents/types.gen.ts @@ -12,15 +12,12 @@ export type AgentRosterListResponse = { total: number } -export type RosterAgentCreatePayload = { - agent_soul?: AgentSoulConfig - description?: string - icon?: string | null - icon_background?: string | null - icon_type?: AgentIconType | null - name: string - role?: string - version_note?: string | null +export type AgentInviteOptionsResponse = { + data: Array + has_more: boolean + limit: number + page: number + total: number } export type AgentRosterResponse = { @@ -51,23 +48,6 @@ export type AgentRosterResponse = { workflow_node_id?: string | null } -export type AgentInviteOptionsResponse = { - data: Array - has_more: boolean - limit: number - page: number - total: number -} - -export type RosterAgentUpdatePayload = { - description?: string | null - icon?: string | null - icon_background?: string | null - icon_type?: AgentIconType | null - name?: string | null - role?: string | null -} - export type AgentConfigSnapshotListResponse = { data: Array } @@ -84,51 +64,6 @@ export type AgentConfigSnapshotDetailResponse = { version_note?: string | null } -export type AgentSoulConfig = { - app_features?: AgentSoulAppFeaturesConfig - app_variables?: Array - 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 AgentIconType = 'emoji' | 'image' | 'link' - -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 AgentPublishedReferenceResponse = { - app_id: string - app_mode: string - app_name: string - node_ids?: Array - workflow_id: string - workflow_version: string -} - -export type AgentScope = 'roster' | 'workflow_only' - -export type AgentSource = 'agent_app' | 'imported' | 'system' | 'workflow' - -export type AgentStatus = 'active' | 'archived' - export type AgentInviteOptionResponse = { active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null active_config_snapshot_id?: string | null @@ -160,6 +95,51 @@ export type AgentInviteOptionResponse = { 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 + 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 + 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 @@ -538,19 +518,6 @@ export type GetAgentsResponses = { export type GetAgentsResponse = GetAgentsResponses[keyof GetAgentsResponses] -export type PostAgentsData = { - body: RosterAgentCreatePayload - path?: never - query?: never - url: '/agents' -} - -export type PostAgentsResponses = { - 201: AgentRosterResponse -} - -export type PostAgentsResponse = PostAgentsResponses[keyof PostAgentsResponses] - export type GetAgentsInviteOptionsData = { body?: never path?: never @@ -570,22 +537,6 @@ export type GetAgentsInviteOptionsResponses = { export type GetAgentsInviteOptionsResponse = GetAgentsInviteOptionsResponses[keyof GetAgentsInviteOptionsResponses] -export type DeleteAgentsByAgentIdData = { - body?: never - path: { - agent_id: string - } - query?: never - url: '/agents/{agent_id}' -} - -export type DeleteAgentsByAgentIdResponses = { - 204: void -} - -export type DeleteAgentsByAgentIdResponse - = DeleteAgentsByAgentIdResponses[keyof DeleteAgentsByAgentIdResponses] - export type GetAgentsByAgentIdData = { body?: never path: { @@ -602,22 +553,6 @@ export type GetAgentsByAgentIdResponses = { export type GetAgentsByAgentIdResponse = GetAgentsByAgentIdResponses[keyof GetAgentsByAgentIdResponses] -export type PatchAgentsByAgentIdData = { - body: RosterAgentUpdatePayload - path: { - agent_id: string - } - query?: never - url: '/agents/{agent_id}' -} - -export type PatchAgentsByAgentIdResponses = { - 200: AgentRosterResponse -} - -export type PatchAgentsByAgentIdResponse - = PatchAgentsByAgentIdResponses[keyof PatchAgentsByAgentIdResponses] - export type GetAgentsByAgentIdVersionsData = { body?: never path: { diff --git a/packages/contracts/generated/api/console/agents/zod.gen.ts b/packages/contracts/generated/api/console/agents/zod.gen.ts index d3fdf018c3..cee741afdb 100644 --- a/packages/contracts/generated/api/console/agents/zod.gen.ts +++ b/packages/contracts/generated/api/console/agents/zod.gen.ts @@ -2,25 +2,6 @@ import * as z from 'zod' -/** - * AgentIconType - * - * Supported icon storage formats for Agent roster entries. - */ -export const zAgentIconType = z.enum(['emoji', 'image', 'link']) - -/** - * RosterAgentUpdatePayload - */ -export const zRosterAgentUpdatePayload = z.object({ - description: z.string().nullish(), - icon: z.string().max(255).nullish(), - icon_background: z.string().max(255).nullish(), - icon_type: zAgentIconType.nullish(), - name: z.string().min(1).max(255).nullish(), - role: z.string().max(255).nullish(), -}) - /** * AgentConfigSnapshotSummaryResponse */ @@ -51,6 +32,13 @@ export const zAgentConfigSnapshotListResponse = z.object({ */ 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 */ @@ -75,7 +63,7 @@ export const zAgentScope = z.enum(['roster', 'workflow_only']) * * Origin that created or imported the Agent. */ -export const zAgentSource = z.enum(['agent_app', 'imported', 'system', 'workflow']) +export const zAgentSource = z.enum(['agent_app', 'imported', 'roster', 'system', 'workflow']) /** * AgentStatus @@ -688,20 +676,6 @@ export const zAgentSoulConfig = z.object({ tools: zAgentSoulToolsConfig.optional(), }) -/** - * RosterAgentCreatePayload - */ -export const zRosterAgentCreatePayload = z.object({ - agent_soul: zAgentSoulConfig.optional(), - description: z.string().optional().default(''), - icon: z.string().max(255).nullish(), - icon_background: z.string().max(255).nullish(), - icon_type: zAgentIconType.nullish(), - name: z.string().min(1).max(255), - role: z.string().max(255).optional().default(''), - version_note: z.string().nullish(), -}) - /** * AgentConfigSnapshotDetailResponse */ @@ -728,13 +702,6 @@ export const zGetAgentsQuery = z.object({ */ export const zGetAgentsResponse = zAgentRosterListResponse -export const zPostAgentsBody = zRosterAgentCreatePayload - -/** - * Agent created - */ -export const zPostAgentsResponse = zAgentRosterResponse - export const zGetAgentsInviteOptionsQuery = z.object({ app_id: z.string().optional(), keyword: z.string().optional(), @@ -747,15 +714,6 @@ export const zGetAgentsInviteOptionsQuery = z.object({ */ export const zGetAgentsInviteOptionsResponse = zAgentInviteOptionsResponse -export const zDeleteAgentsByAgentIdPath = z.object({ - agent_id: z.string(), -}) - -/** - * Agent archived - */ -export const zDeleteAgentsByAgentIdResponse = z.void() - export const zGetAgentsByAgentIdPath = z.object({ agent_id: z.string(), }) @@ -765,17 +723,6 @@ export const zGetAgentsByAgentIdPath = z.object({ */ export const zGetAgentsByAgentIdResponse = zAgentRosterResponse -export const zPatchAgentsByAgentIdBody = zRosterAgentUpdatePayload - -export const zPatchAgentsByAgentIdPath = z.object({ - agent_id: z.string(), -}) - -/** - * Agent updated - */ -export const zPatchAgentsByAgentIdResponse = zAgentRosterResponse - export const zGetAgentsByAgentIdVersionsPath = z.object({ agent_id: z.string(), }) diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index e5ab8f4a86..5ba8675ee3 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -21,19 +21,25 @@ export type CreateAppPayload = { name: string } -export type AppDetail = { +export type AppDetailWithSite = { access_mode?: string | null + api_base_url?: string | null app_model_config?: ModelConfig | null + bound_agent_id?: string | null created_at?: number | null created_by?: string | null + deleted_tools?: Array description?: string | null enable_api: boolean enable_site: boolean icon?: string | null icon_background?: string | null + icon_type?: string | null id: string + max_active_requests?: number | null mode_compatible_with_agent: string name: string + site?: Site | null tags?: Array tracing?: JsonValue | null updated_at?: number | null @@ -76,33 +82,6 @@ export type WorkflowOnlineUsersResponse = { data: Array } -export type AppDetailWithSite = { - access_mode?: string | null - api_base_url?: string | null - app_model_config?: ModelConfig | null - bound_agent_id?: string | null - created_at?: number | null - created_by?: string | null - deleted_tools?: Array - description?: string | null - enable_api: boolean - enable_site: boolean - icon?: string | null - icon_background?: string | null - icon_type?: string | null - id: string - max_active_requests?: number | null - mode_compatible_with_agent: string - name: string - site?: Site | null - tags?: Array - tracing?: JsonValue | null - updated_at?: number | null - updated_by?: string | null - use_icon_as_answer_icon?: boolean | null - workflow?: WorkflowPartial | null -} - export type UpdateAppPayload = { description?: string | null icon?: string | null @@ -404,6 +383,27 @@ export type AppApiStatusPayload = { enable_api: boolean } +export type AppDetail = { + access_mode?: string | null + app_model_config?: ModelConfig | null + created_at?: number | null + created_by?: string | null + description?: string | null + enable_api: boolean + enable_site: boolean + icon?: string | null + icon_background?: string | null + id: string + mode_compatible_with_agent: string + name: string + tags?: Array + tracing?: JsonValue | null + updated_at?: number | null + updated_by?: string | null + use_icon_as_answer_icon?: boolean | null + workflow?: WorkflowPartial | null +} + export type AudioTranscriptResponse = { text: string } @@ -1212,6 +1212,29 @@ export type ModelConfig = { provider: string } +export type DeletedTool = { + provider_id: string + tool_name: string + type: string +} + +export type Site = { + chat_color_theme?: string | null + chat_color_theme_inverted: boolean + copyright?: string | null + custom_disclaimer?: string | null + default_language: string + description?: string | null + icon?: string | null + icon_background?: string | null + icon_type?: string | null + readonly icon_url: string | null + privacy_policy?: string | null + show_workflow_steps: boolean + title: string + use_icon_as_answer_icon: boolean +} + export type Tag = { id: string name: string @@ -1250,29 +1273,6 @@ export type WorkflowOnlineUsersByApp = { users: Array } -export type DeletedTool = { - provider_id: string - tool_name: string - type: string -} - -export type Site = { - chat_color_theme?: string | null - chat_color_theme_inverted: boolean - copyright?: string | null - custom_disclaimer?: string | null - default_language: string - description?: string | null - icon?: string | null - icon_background?: string | null - icon_type?: string | null - readonly icon_url: string | null - privacy_policy?: string | null - show_workflow_steps: boolean - title: string - use_icon_as_answer_icon: boolean -} - export type AdvancedChatWorkflowRunForListResponse = { conversation_id?: string | null created_at?: number | null @@ -2707,7 +2707,7 @@ export type PostAppsErrors = { } export type PostAppsResponses = { - 201: AppDetail + 201: AppDetailWithSite } export type PostAppsResponse = PostAppsResponses[keyof PostAppsResponses] diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 4e559a6629..397fcd5cce 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -834,6 +834,35 @@ export const zAppIconPayload = z.object({ icon_type: zIconType.nullish(), }) +/** + * DeletedTool + */ +export const zDeletedTool = z.object({ + provider_id: z.string(), + tool_name: z.string(), + type: z.string(), +}) + +/** + * Site + */ +export const zSite = z.object({ + chat_color_theme: z.string().nullish(), + chat_color_theme_inverted: z.boolean(), + copyright: z.string().nullish(), + custom_disclaimer: z.string().nullish(), + default_language: z.string(), + description: z.string().nullish(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: z.string().nullish(), + icon_url: z.string().nullable(), + privacy_policy: z.string().nullish(), + show_workflow_steps: z.boolean(), + title: z.string(), + use_icon_as_answer_icon: z.boolean(), +}) + /** * Tag */ @@ -888,35 +917,6 @@ export const zImport = z.object({ status: zImportStatus, }) -/** - * DeletedTool - */ -export const zDeletedTool = z.object({ - provider_id: z.string(), - tool_name: z.string(), - type: z.string(), -}) - -/** - * Site - */ -export const zSite = z.object({ - chat_color_theme: z.string().nullish(), - chat_color_theme_inverted: z.boolean(), - copyright: z.string().nullish(), - custom_disclaimer: z.string().nullish(), - default_language: z.string(), - description: z.string().nullish(), - icon: z.string().nullish(), - icon_background: z.string().nullish(), - icon_type: z.string().nullish(), - icon_url: z.string().nullable(), - privacy_policy: z.string().nullish(), - show_workflow_steps: z.boolean(), - title: z.string(), - use_icon_as_answer_icon: z.boolean(), -}) - /** * AgentConfigSnapshotSummaryResponse */ @@ -2038,30 +2038,6 @@ export const zModelConfig = z.object({ provider: z.string(), }) -/** - * AppDetail - */ -export const zAppDetail = z.object({ - access_mode: z.string().nullish(), - app_model_config: zModelConfig.nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - description: z.string().nullish(), - enable_api: z.boolean(), - enable_site: z.boolean(), - icon: z.string().nullish(), - icon_background: z.string().nullish(), - id: z.string(), - mode_compatible_with_agent: z.string(), - name: z.string(), - tags: z.array(zTag).optional(), - tracing: zJsonValue.nullish(), - updated_at: z.int().nullish(), - updated_by: z.string().nullish(), - use_icon_as_answer_icon: z.boolean().nullish(), - workflow: zWorkflowPartial.nullish(), -}) - /** * AppDetailWithSite */ @@ -2092,6 +2068,30 @@ export const zAppDetailWithSite = z.object({ workflow: zWorkflowPartial.nullish(), }) +/** + * AppDetail + */ +export const zAppDetail = z.object({ + access_mode: z.string().nullish(), + app_model_config: zModelConfig.nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + description: z.string().nullish(), + enable_api: z.boolean(), + enable_site: z.boolean(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + id: z.string(), + mode_compatible_with_agent: z.string(), + name: z.string(), + tags: z.array(zTag).optional(), + tracing: zJsonValue.nullish(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + use_icon_as_answer_icon: z.boolean().nullish(), + workflow: zWorkflowPartial.nullish(), +}) + /** * ConversationDetail */ @@ -3638,7 +3638,7 @@ export const zPostAppsBody = zCreateAppPayload /** * App created successfully */ -export const zPostAppsResponse = zAppDetail +export const zPostAppsResponse = zAppDetailWithSite export const zPostAppsImportsBody = zAppImportPayload