feat: add agent debug conversation refresh API (#37784)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123 2026-06-23 12:34:13 +08:00 committed by GitHub
parent 7852c273e4
commit 26639e0923
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 277 additions and 16 deletions

View File

@ -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/<uuid:agent_id>/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/<uuid:agent_id>/copy")
class AgentAppCopyApi(Resource):
@console_ns.expect(console_ns.models[CopyAppPayload.__name__])

View File

@ -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)<br> |
| 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 |

View File

@ -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]:

View File

@ -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/<uuid:agent_id>/api-enable",
"/agent/<uuid:agent_id>/api-keys",
"/agent/<uuid:agent_id>/api-keys/<uuid:api_key_id>",
"/agent/<uuid:agent_id>/debug-conversation/refresh",
"/agent/<uuid:agent_id>/chat-messages",
"/agent/<uuid:agent_id>/chat-messages/<string:task_id>/stop",
"/agent/<uuid:agent_id>/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:

View File

@ -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",

View File

@ -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/<name>
*/
export const post8 = oc
export const post9 = oc
.route({
description: 'Commit an uploaded file into the Agent App drive under files/<name>',
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,
}

View File

@ -166,6 +166,10 @@ export type CopyAppPayload = {
name?: string | null
}
export type AgentDebugConversationRefreshResponse = {
debug_conversation_id: string
}
export type AgentDriveListResponse = {
items?: Array<AgentDriveItemResponse>
}
@ -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: {

View File

@ -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(),
})