From 26639e0923da9e2f453791907b86073a00eafe1c Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Tue, 23 Jun 2026 12:34:13 +0800 Subject: [PATCH] feat: add agent debug conversation refresh API (#37784) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/agent/roster.py | 30 ++++++++ api/openapi/markdown/console-openapi.md | 20 ++++++ api/services/agent/roster_service.py | 46 +++++++++++++ .../console/agent/test_agent_controllers.py | 34 ++++++++++ .../services/agent/test_agent_services.py | 68 +++++++++++++++++++ .../generated/api/console/agent/orpc.gen.ts | 54 ++++++++++----- .../generated/api/console/agent/types.gen.ts | 24 +++++++ .../generated/api/console/agent/zod.gen.ts | 17 +++++ 8 files changed, 277 insertions(+), 16 deletions(-) diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 96bce6763f5..810dfda965a 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -228,6 +228,10 @@ class AgentAppDetailWithSite(GenericAppDetailWithSite): active_config_is_published: bool = False +class AgentDebugConversationRefreshResponse(BaseModel): + debug_conversation_id: str + + class AgentAppPagination(GenericAppPagination): data: list[AgentAppPartial] = Field( # type: ignore[assignment] # pyrefly: ignore[bad-override-mutable-attribute] validation_alias=AliasChoices("items", "data") @@ -254,6 +258,7 @@ register_response_schema_models( AgentAppPublishedReferenceResponse, AgentAppDetailWithSite, AgentAppPartial, + AgentDebugConversationRefreshResponse, AgentConfigSnapshotDetailResponse, AgentConfigSnapshotListResponse, AgentConfigSnapshotRestoreResponse, @@ -535,6 +540,31 @@ class AgentAppApi(Resource): return "", 204 +@console_ns.route("/agent//debug-conversation/refresh") +class AgentDebugConversationRefreshApi(Resource): + @console_ns.response( + 200, + "Agent debug conversation refreshed", + console_ns.models[AgentDebugConversationRefreshResponse.__name__], + ) + @console_ns.response(403, "Insufficient permissions") + @setup_required + @login_required + @account_initialization_required + @edit_permission_required + @with_current_user + @with_current_tenant_id + def post(self, tenant_id: str, current_user: Account, agent_id: UUID): + debug_conversation_id = _agent_roster_service().refresh_agent_app_debug_conversation_id( + tenant_id=tenant_id, + agent_id=str(agent_id), + account_id=current_user.id, + ) + return AgentDebugConversationRefreshResponse(debug_conversation_id=debug_conversation_id).model_dump( + mode="json" + ) + + @console_ns.route("/agent//copy") class AgentAppCopyApi(Resource): @console_ns.expect(console_ns.models[CopyAppPayload.__name__]) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index ef11a817662..a60e958f84b 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -602,6 +602,20 @@ Stop a running Agent App chat message generation | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | +### [POST] /agent/{agent_id}/debug-conversation/refresh +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Agent debug conversation refreshed | **application/json**: [AgentDebugConversationRefreshResponse](#agentdebugconversationrefreshresponse)
| +| 403 | Insufficient permissions | | + ### [GET] /agent/{agent_id}/drive/files List agent drive entries for an Agent App @@ -12562,6 +12576,12 @@ Audit operation recorded for Agent Soul version/revision changes. | date | string | | Yes | | message_count | integer | | Yes | +#### AgentDebugConversationRefreshResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| debug_conversation_id | string | | Yes | + #### AgentDriveDeleteFileByAgentQuery | Name | Type | Description | Required | diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index e78d49c65b7..b75fa0bb1ae 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -492,6 +492,52 @@ class AgentRosterService: self._session.commit() return conversation_id + def refresh_agent_app_debug_conversation_id( + self, *, tenant_id: str, agent_id: str, account_id: str, commit: bool = True + ) -> str: + """Start a new console debug conversation for the current Agent App editor.""" + + 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.status == AgentStatus.ACTIVE, + ) + ) + if agent is None or not agent.app_id: + raise AgentNotFoundError() + + conversation_id = self._create_agent_app_debug_conversation( + app_id=agent.app_id, + account_id=account_id, + ) + mapping = self._session.scalar( + select(AgentDebugConversation).where( + AgentDebugConversation.tenant_id == tenant_id, + AgentDebugConversation.agent_id == agent_id, + AgentDebugConversation.account_id == account_id, + ) + ) + if mapping is None: + self._session.add( + AgentDebugConversation( + tenant_id=tenant_id, + agent_id=agent_id, + app_id=agent.app_id, + account_id=account_id, + conversation_id=conversation_id, + ) + ) + else: + mapping.app_id = agent.app_id + mapping.conversation_id = conversation_id + self._session.flush() + if commit: + self._session.commit() + return conversation_id + def load_or_create_agent_app_debug_conversation_ids_by_agent_id( self, *, tenant_id: str, agents: list[Agent], account_id: str ) -> dict[str, str]: 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 02fd5da55ac..32a165ccd01 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 @@ -27,6 +27,7 @@ from controllers.console.agent.roster import ( AgentAppApi, AgentAppCopyApi, AgentAppListApi, + AgentDebugConversationRefreshApi, AgentInviteOptionsApi, AgentLogMessagesApi, AgentLogsApi, @@ -158,6 +159,7 @@ def test_agent_v2_console_routes_are_agent_id_first() -> None: "/agent//api-enable", "/agent//api-keys", "/agent//api-keys/", + "/agent//debug-conversation/refresh", "/agent//chat-messages", "/agent//chat-messages//stop", "/agent//feedbacks", @@ -483,6 +485,38 @@ def test_agent_app_copy_uses_agent_id_and_returns_agent_detail( } +def test_agent_debug_conversation_refresh_uses_current_user( + app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str +) -> None: + agent_id = "00000000-0000-0000-0000-000000000001" + captured: dict[str, object] = {} + + class FakeRosterService: + def refresh_agent_app_debug_conversation_id(self, **kwargs: object) -> str: + captured.update(kwargs) + return "new-debug-conversation-id" + + monkeypatch.setattr(roster_controller, "_agent_roster_service", lambda: FakeRosterService()) + + with app.test_request_context( + "/console/api/agent/00000000-0000-0000-0000-000000000001/debug-conversation/refresh", + method="POST", + ): + response = unwrap(AgentDebugConversationRefreshApi.post)( + AgentDebugConversationRefreshApi(), + "tenant-1", + SimpleNamespace(id=account_id), + agent_id, + ) + + assert response == {"debug_conversation_id": "new-debug-conversation-id"} + assert captured == { + "tenant_id": "tenant-1", + "agent_id": agent_id, + "account_id": account_id, + } + + def test_agent_api_access_uses_agent_id_and_returns_service_api_metadata( monkeypatch: pytest.MonkeyPatch, ) -> None: 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 52ff00c0855..846ce5a3e62 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -1649,6 +1649,74 @@ class TestAgentAppBackingAgent: with pytest.raises(roster_service.AgentNotFoundError): service.get_agent_app_model(tenant_id="tenant-1", agent_id="agent-x") + def test_refresh_agent_app_debug_conversation_creates_mapping(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", + ) + session = FakeSession(scalar=[agent, None]) + service = AgentRosterService(session) + + conversation_id = service.refresh_agent_app_debug_conversation_id( + tenant_id="tenant-1", + agent_id="agent-1", + account_id="account-1", + ) + + conversations = [a for a in session.added if isinstance(a, Conversation)] + assert len(conversations) == 1 + assert conversations[0].id == conversation_id + assert conversations[0].app_id == "app-1" + assert conversations[0].from_source == ConversationFromSource.CONSOLE + assert conversations[0].from_account_id == "account-1" + mappings = [a for a in session.added if isinstance(a, AgentDebugConversation)] + assert len(mappings) == 1 + assert mappings[0].tenant_id == "tenant-1" + assert mappings[0].agent_id == "agent-1" + assert mappings[0].app_id == "app-1" + assert mappings[0].account_id == "account-1" + assert mappings[0].conversation_id == conversation_id + assert session.deleted == [] + assert session.commits == 1 + + def test_refresh_agent_app_debug_conversation_replaces_existing_mapping(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", + ) + mapping = SimpleNamespace(app_id="old-app", conversation_id="old-conversation") + session = FakeSession(scalar=[agent, mapping]) + service = AgentRosterService(session) + + conversation_id = service.refresh_agent_app_debug_conversation_id( + tenant_id="tenant-1", + agent_id="agent-1", + account_id="account-1", + ) + + assert mapping.app_id == "app-1" + assert mapping.conversation_id == conversation_id + assert [a for a in session.added if isinstance(a, AgentDebugConversation)] == [] + conversations = [a for a in session.added if isinstance(a, Conversation)] + assert len(conversations) == 1 + assert conversations[0].id == conversation_id + assert session.deleted == [] + assert session.commits == 1 + def test_duplicate_agent_app_copies_app_config_and_active_soul(self, monkeypatch: pytest.MonkeyPatch): source_config = SimpleNamespace( opening_statement="hello", diff --git a/packages/contracts/generated/api/console/agent/orpc.gen.ts b/packages/contracts/generated/api/console/agent/orpc.gen.ts index 99f44301703..5e4a692f244 100644 --- a/packages/contracts/generated/api/console/agent/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agent/orpc.gen.ts @@ -84,6 +84,8 @@ import { zPostAgentByAgentIdCopyBody, zPostAgentByAgentIdCopyPath, zPostAgentByAgentIdCopyResponse, + zPostAgentByAgentIdDebugConversationRefreshPath, + zPostAgentByAgentIdDebugConversationRefreshResponse, zPostAgentByAgentIdFeaturesBody, zPostAgentByAgentIdFeaturesPath, zPostAgentByAgentIdFeaturesResponse, @@ -356,6 +358,25 @@ export const copy = { post: post5, } +export const post6 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postAgentByAgentIdDebugConversationRefresh', + path: '/agent/{agent_id}/debug-conversation/refresh', + tags: ['console'], + }) + .input(z.object({ params: zPostAgentByAgentIdDebugConversationRefreshPath })) + .output(zPostAgentByAgentIdDebugConversationRefreshResponse) + +export const refresh = { + post: post6, +} + +export const debugConversation = { + refresh, +} + /** * Time-limited external signed URL for one Agent App drive value */ @@ -481,7 +502,7 @@ export const drive = { /** * Update an Agent App's presentation features (opener, follow-up, citations, ...) */ -export const post6 = oc +export const post7 = oc .route({ description: 'Update an Agent App\'s presentation features (opener, follow-up, citations, ...)', inputStructure: 'detailed', @@ -496,13 +517,13 @@ export const post6 = oc .output(zPostAgentByAgentIdFeaturesResponse) export const features = { - post: post6, + post: post7, } /** * Create or update Agent App message feedback */ -export const post7 = oc +export const post8 = oc .route({ description: 'Create or update Agent App message feedback', inputStructure: 'detailed', @@ -517,7 +538,7 @@ export const post7 = oc .output(zPostAgentByAgentIdFeedbacksResponse) export const feedbacks = { - post: post7, + post: post8, } /** @@ -540,7 +561,7 @@ export const delete2 = oc /** * Commit an uploaded file into the Agent App drive under files/ */ -export const post8 = oc +export const post9 = oc .route({ description: 'Commit an uploaded file into the Agent App drive under files/', inputStructure: 'detailed', @@ -555,7 +576,7 @@ export const post8 = oc export const files2 = { delete: delete2, - post: post8, + post: post9, } export const get13 = oc @@ -684,7 +705,7 @@ export const read = { /** * Upload one Agent App sandbox file as a Dify ToolFile mapping */ -export const post9 = oc +export const post10 = oc .route({ description: 'Upload one Agent App sandbox file as a Dify ToolFile mapping', inputStructure: 'detailed', @@ -702,7 +723,7 @@ export const post9 = oc .output(zPostAgentByAgentIdSandboxFilesUploadResponse) export const upload = { - post: post9, + post: post10, } /** @@ -738,7 +759,7 @@ export const sandbox = { /** * Upload + standardize a Skill into an Agent App drive */ -export const post10 = oc +export const post11 = oc .route({ description: 'Upload + standardize a Skill into an Agent App drive', inputStructure: 'detailed', @@ -757,13 +778,13 @@ export const post10 = oc .output(zPostAgentByAgentIdSkillsUploadResponse) export const upload2 = { - post: post10, + post: post11, } /** * Infer CLI tool + ENV suggestions from a standardized Agent App skill */ -export const post11 = oc +export const post12 = oc .route({ description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill', inputStructure: 'detailed', @@ -776,7 +797,7 @@ export const post11 = oc .output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse) export const inferTools = { - post: post11, + post: post12, } /** @@ -828,7 +849,7 @@ export const statistics = { summary, } -export const post12 = oc +export const post13 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -840,7 +861,7 @@ export const post12 = oc .output(zPostAgentByAgentIdVersionsByVersionIdRestoreResponse) export const restore = { - post: post12, + post: post13, } export const get21 = oc @@ -919,6 +940,7 @@ export const byAgentId = { chatMessages, composer, copy, + debugConversation, drive, features, feedbacks, @@ -944,7 +966,7 @@ export const get24 = oc .input(z.object({ query: zGetAgentQuery.optional() })) .output(zGetAgentResponse) -export const post13 = oc +export const post14 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -958,7 +980,7 @@ export const post13 = oc export const agent = { get: get24, - post: post13, + post: post14, 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 837b267a636..988a8999c30 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -166,6 +166,10 @@ export type CopyAppPayload = { name?: string | null } +export type AgentDebugConversationRefreshResponse = { + debug_conversation_id: string +} + export type AgentDriveListResponse = { items?: Array } @@ -1968,6 +1972,26 @@ export type PostAgentByAgentIdCopyResponses = { export type PostAgentByAgentIdCopyResponse = PostAgentByAgentIdCopyResponses[keyof PostAgentByAgentIdCopyResponses] +export type PostAgentByAgentIdDebugConversationRefreshData = { + body?: never + path: { + agent_id: string + } + query?: never + url: '/agent/{agent_id}/debug-conversation/refresh' +} + +export type PostAgentByAgentIdDebugConversationRefreshErrors = { + 403: unknown +} + +export type PostAgentByAgentIdDebugConversationRefreshResponses = { + 200: AgentDebugConversationRefreshResponse +} + +export type PostAgentByAgentIdDebugConversationRefreshResponse + = PostAgentByAgentIdDebugConversationRefreshResponses[keyof PostAgentByAgentIdDebugConversationRefreshResponses] + 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 297055a155c..aeab80c9463 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -61,6 +61,13 @@ export const zSimpleResultResponse = z.object({ result: z.string(), }) +/** + * AgentDebugConversationRefreshResponse + */ +export const zAgentDebugConversationRefreshResponse = z.object({ + debug_conversation_id: z.string(), +}) + /** * AgentDriveDownloadResponse */ @@ -2437,6 +2444,16 @@ export const zPostAgentByAgentIdCopyPath = z.object({ */ export const zPostAgentByAgentIdCopyResponse = zAgentAppDetailWithSite +export const zPostAgentByAgentIdDebugConversationRefreshPath = z.object({ + agent_id: z.uuid(), +}) + +/** + * Agent debug conversation refreshed + */ +export const zPostAgentByAgentIdDebugConversationRefreshResponse + = zAgentDebugConversationRefreshResponse + export const zGetAgentByAgentIdDriveFilesPath = z.object({ agent_id: z.uuid(), })