fix(agent): add stable debug conversation (#37744)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123 2026-06-22 17:21:09 +08:00 committed by GitHub
parent 7c20ffe6c4
commit 4065f63dce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 116 additions and 7 deletions

View File

@ -186,6 +186,7 @@ class AgentStatisticsQuery(BaseModel):
class AgentAppPartial(GenericAppPartial):
app_id: str | None = None
debug_conversation_id: str | None = None
role: str | None = None
active_config_is_published: bool = False
published_reference_count: int = 0
@ -194,6 +195,7 @@ class AgentAppPartial(GenericAppPartial):
class AgentAppDetailWithSite(GenericAppDetailWithSite):
app_id: str | None = None
debug_conversation_id: str | None = None
role: str | None = None
active_config_is_published: bool = False
@ -262,6 +264,7 @@ def _serialize_agent_app_detail(app_model) -> dict:
payload.pop("bound_agent_id", None)
payload["app_id"] = str(app_model.id)
payload["id"] = agent.id
payload["debug_conversation_id"] = agent.debug_conversation_id
payload["role"] = agent.role or ""
payload["active_config_is_published"] = roster_service.active_config_is_published(
tenant_id=app_model.tenant_id,
@ -301,6 +304,7 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
if agent:
item["app_id"] = app_id
item["id"] = agent.id
item["debug_conversation_id"] = agent.debug_conversation_id
item["role"] = agent.role or ""
item["active_config_is_published"] = active_config_is_published_by_agent_id.get(agent.id, False)
published_references = published_references_by_agent_id.get(agent.id, [])

View File

@ -0,0 +1,30 @@
"""add agent debug conversation id
Revision ID: c8f4a6b2d3e1
Revises: b2515f9d4c2a
Create Date: 2026-06-22 10:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
import models
# revision identifiers, used by Alembic.
revision = "c8f4a6b2d3e1"
down_revision = "b2515f9d4c2a"
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table("agents", schema=None) as batch_op:
batch_op.add_column(sa.Column("debug_conversation_id", models.types.StringUUID(), nullable=True))
batch_op.create_index("agent_debug_conversation_id_idx", ["debug_conversation_id"], unique=False)
def downgrade():
with op.batch_alter_table("agents", schema=None) as batch_op:
batch_op.drop_index("agent_debug_conversation_id_idx")
batch_op.drop_column("debug_conversation_id")

View File

@ -135,6 +135,7 @@ class Agent(DefaultFieldsMixin, Base):
Index("agent_tenant_workflow_id_idx", "tenant_id", "workflow_id"),
Index("agent_tenant_app_id_idx", "tenant_id", "app_id"),
Index("agent_active_config_snapshot_id_idx", "active_config_snapshot_id"),
Index("agent_debug_conversation_id_idx", "debug_conversation_id"),
Index(
"agent_tenant_invitable_idx",
"tenant_id",
@ -162,6 +163,7 @@ class Agent(DefaultFieldsMixin, Base):
scope: Mapped[AgentScope] = mapped_column(EnumText(AgentScope, length=32), nullable=False)
source: Mapped[AgentSource] = mapped_column(EnumText(AgentSource, length=32), nullable=False)
app_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
debug_conversation_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
workflow_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
workflow_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
active_config_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)

View File

@ -12066,6 +12066,7 @@ Default namespace
| bound_agent_id | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
| debug_conversation_id | string | | No |
| deleted_tools | [ [DeletedTool](#deletedtool) ] | | No |
| description | string | | No |
| enable_api | boolean | | Yes |
@ -12129,6 +12130,7 @@ default (the config form sends the full desired feature state on save).
| create_user_name | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
| debug_conversation_id | string | | No |
| description | string | | No |
| has_draft_trigger | boolean | | No |
| icon | string | | No |

View File

@ -3,6 +3,7 @@ from typing import Any, TypedDict
from sqlalchemy import and_, func, or_, select
from sqlalchemy.exc import IntegrityError
from core.app.entities.app_invoke_entities import InvokeFrom
from libs.datetime_utils import naive_utc_now
from libs.helper import to_timestamp
from models.agent import (
@ -18,8 +19,8 @@ from models.agent import (
WorkflowAgentNodeBinding,
)
from models.agent_config_entities import AgentSoulConfig
from models.enums import AppStatus
from models.model import App, AppMode, IconType
from models.enums import AppStatus, ConversationFromSource, ConversationStatus
from models.model import App, AppMode, Conversation, IconType
from models.workflow import Workflow
from services.agent.agent_soul_state import agent_soul_has_model
from services.agent.composer_validator import ComposerConfigValidator
@ -96,6 +97,7 @@ class AgentRosterService:
"scope": agent.scope.value,
"source": agent.source.value,
"app_id": agent.app_id,
"debug_conversation_id": getattr(agent, "debug_conversation_id", None),
"workflow_id": agent.workflow_id,
"workflow_node_id": agent.workflow_node_id,
"active_config_snapshot_id": agent.active_config_snapshot_id,
@ -391,9 +393,38 @@ class AgentRosterService:
self._session.add(revision)
agent.active_config_snapshot_id = version.id
agent.active_config_has_model = agent_soul_has_model(AgentSoulConfig())
agent.debug_conversation_id = self._create_agent_app_debug_conversation(
app_id=app_id,
account_id=account_id,
)
self._session.flush()
return agent
def _create_agent_app_debug_conversation(self, *, app_id: str, account_id: str) -> str:
"""Create the stable console conversation used by Agent App debug mode."""
conversation = Conversation(
app_id=app_id,
app_model_config_id=None,
model_provider=None,
model_id="",
override_model_configs=None,
mode=AppMode.AGENT,
name="Agent Debugging Conversation",
inputs={},
introduction="",
system_instruction="",
system_instruction_tokens=0,
status=ConversationStatus.NORMAL,
invoke_from=InvokeFrom.DEBUGGER,
from_source=ConversationFromSource.CONSOLE,
from_end_user_id=None,
from_account_id=account_id,
)
self._session.add(conversation)
self._session.flush()
return conversation.id
def load_app_backing_agents_by_app_id(self, *, tenant_id: str, app_ids: list[str]) -> dict[str, Agent]:
"""Return active app-backed Agents keyed by Agent App id."""
if not app_ids:

View File

@ -219,14 +219,22 @@ def test_agent_app_list_and_create_use_agent_route(
roster_controller.AgentRosterService,
"load_app_backing_agents_by_app_id",
lambda _self, **kwargs: {
"app-list": SimpleNamespace(id="agent-list", role="List role", active_config_snapshot_id=None)
"app-list": SimpleNamespace(
id="agent-list",
role="List role",
debug_conversation_id="debug-conversation-list",
active_config_snapshot_id=None,
)
},
)
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(
id="agent-created", role="Created role", active_config_snapshot_id=None
id="agent-created",
role="Created role",
debug_conversation_id="debug-conversation-created",
active_config_snapshot_id=None,
),
)
monkeypatch.setattr(
@ -263,6 +271,7 @@ def test_agent_app_list_and_create_use_agent_route(
assert listed["total"] == 1
assert listed["data"][0]["id"] == "agent-list"
assert listed["data"][0]["app_id"] == "app-list"
assert listed["data"][0]["debug_conversation_id"] == "debug-conversation-list"
assert listed["data"][0]["role"] == "List role"
assert listed["data"][0]["active_config_is_published"] is False
assert listed["data"][0]["published_reference_count"] == 1
@ -296,6 +305,7 @@ def test_agent_app_list_and_create_use_agent_route(
assert status == 201
assert created["id"] == "agent-created"
assert created["app_id"] == "app-created"
assert created["debug_conversation_id"] == "debug-conversation-created"
assert created["role"] == "Created role"
assert created["active_config_is_published"] is False
assert "bound_agent_id" not in created
@ -336,7 +346,12 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="Resolved role", active_config_snapshot_id=None),
lambda _self, **kwargs: SimpleNamespace(
id=agent_id,
role="Resolved role",
debug_conversation_id="debug-conversation-detail",
active_config_snapshot_id=None,
),
)
monkeypatch.setattr(
roster_controller.FeatureService,
@ -361,6 +376,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", agent_id)
assert detail["id"] == agent_id
assert detail["app_id"] == "app-1"
assert detail["debug_conversation_id"] == "debug-conversation-detail"
assert detail["role"] == "Resolved role"
assert detail["active_config_is_published"] is False
assert "bound_agent_id" not in detail
@ -374,6 +390,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
assert updated["name"] == "Renamed"
assert updated["id"] == agent_id
assert updated["app_id"] == "app-1"
assert updated["debug_conversation_id"] == "debug-conversation-detail"
assert updated["role"] == "Resolved role"
assert updated["active_config_is_published"] is False
assert "bound_agent_id" not in updated
@ -445,7 +462,12 @@ def test_agent_app_update_rejects_empty_role(app: Flask, monkeypatch: pytest.Mon
monkeypatch.setattr(
roster_controller.AgentRosterService,
"get_app_backing_agent",
lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="", active_config_snapshot_id=None),
lambda _self, **kwargs: SimpleNamespace(
id=agent_id,
role="",
debug_conversation_id="debug-conversation-detail",
active_config_snapshot_id=None,
),
)
monkeypatch.setattr(
roster_controller.FeatureService,

View File

@ -23,7 +23,8 @@ from models.agent_config_entities import (
DeclaredOutputType,
WorkflowNodeJobConfig,
)
from models.model import IconType
from models.enums import ConversationFromSource, ConversationStatus
from models.model import Conversation, 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
@ -1353,6 +1354,7 @@ class TestAgentAppBackingAgent:
assert agent.agent_kind == AgentKind.DIFY_AGENT
assert agent.name == "Iris"
assert agent.role == "research assistant"
assert agent.debug_conversation_id is not None
# A v1 snapshot + revision are seeded and wired as the active version.
snapshots = [a for a in session.added if isinstance(a, AgentConfigSnapshot)]
assert len(snapshots) == 1
@ -1362,6 +1364,14 @@ class TestAgentAppBackingAgent:
a for a in session.added if getattr(a, "operation", None) == AgentConfigRevisionOperation.CREATE_VERSION
]
assert len(revisions) == 1
conversations = [a for a in session.added if isinstance(a, Conversation)]
assert len(conversations) == 1
assert agent.debug_conversation_id == conversations[0].id
assert conversations[0].app_id == "app-1"
assert conversations[0].mode == "agent"
assert conversations[0].status == ConversationStatus.NORMAL
assert conversations[0].from_source == ConversationFromSource.CONSOLE
assert conversations[0].from_account_id == "account-1"
# Caller (AppService.create_app) owns the commit — helper must not commit.
assert session.commits == 0

View File

@ -29,6 +29,7 @@ export type AgentAppDetailWithSite = {
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
debug_conversation_id?: string | null
deleted_tools?: Array<DeletedTool>
description?: string | null
enable_api: boolean
@ -329,6 +330,7 @@ export type AgentAppPartial = {
create_user_name?: string | null
created_at?: number | null
created_by?: string | null
debug_conversation_id?: string | null
description?: string | null
has_draft_trigger?: boolean | null
icon?: string | null
@ -1501,6 +1503,7 @@ export type AgentAppDetailWithSiteWritable = {
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
debug_conversation_id?: string | null
deleted_tools?: Array<DeletedTool>
description?: string | null
enable_api: boolean
@ -1534,6 +1537,7 @@ export type AgentAppPartialWritable = {
create_user_name?: string | null
created_at?: number | null
created_by?: string | null
debug_conversation_id?: string | null
description?: string | null
has_draft_trigger?: boolean | null
icon?: string | null

View File

@ -684,6 +684,7 @@ export const zAgentAppPartial = z.object({
create_user_name: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
debug_conversation_id: z.string().nullish(),
description: z.string().nullish(),
has_draft_trigger: z.boolean().nullish(),
icon: z.string().nullish(),
@ -747,6 +748,7 @@ export const zAgentAppDetailWithSite = z.object({
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
debug_conversation_id: z.string().nullish(),
deleted_tools: z.array(zDeletedTool).optional(),
description: z.string().nullish(),
enable_api: z.boolean(),
@ -2086,6 +2088,7 @@ export const zAgentAppPartialWritable = z.object({
create_user_name: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
debug_conversation_id: z.string().nullish(),
description: z.string().nullish(),
has_draft_trigger: z.boolean().nullish(),
icon: z.string().nullish(),
@ -2150,6 +2153,7 @@ export const zAgentAppDetailWithSiteWritable = z.object({
bound_agent_id: z.string().nullish(),
created_at: z.int().nullish(),
created_by: z.string().nullish(),
debug_conversation_id: z.string().nullish(),
deleted_tools: z.array(zDeletedTool).optional(),
description: z.string().nullish(),
enable_api: z.boolean(),