mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 13:01:16 +08:00
fix: isolate agent debug conversations by account (#37766)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
ab11083c2d
commit
b67a04aa22
@ -271,7 +271,7 @@ def _agent_roster_service() -> AgentRosterService:
|
||||
return AgentRosterService(db.session)
|
||||
|
||||
|
||||
def _serialize_agent_app_detail(app_model) -> dict:
|
||||
def _serialize_agent_app_detail(app_model, *, current_user: Account) -> dict:
|
||||
"""Serialize an Agent App detail using roster-only DTOs.
|
||||
|
||||
`/agent` responses are roster-shaped rather than raw app-shaped: `id`
|
||||
@ -294,7 +294,11 @@ 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["debug_conversation_id"] = roster_service.get_or_create_agent_app_debug_conversation_id(
|
||||
tenant_id=app_model.tenant_id,
|
||||
agent_id=agent.id,
|
||||
account_id=current_user.id,
|
||||
)
|
||||
payload["role"] = agent.role or ""
|
||||
payload["active_config_is_published"] = roster_service.active_config_is_published(
|
||||
tenant_id=app_model.tenant_id,
|
||||
@ -303,7 +307,7 @@ def _serialize_agent_app_detail(app_model) -> dict:
|
||||
return payload
|
||||
|
||||
|
||||
def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
|
||||
def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str, current_user: Account) -> dict:
|
||||
"""Serialize Agent App lists with roster-shaped items.
|
||||
|
||||
Each item starts from the shared App list shape, then drops
|
||||
@ -326,6 +330,11 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict:
|
||||
tenant_id=tenant_id,
|
||||
agent_ids=[agent.id for agent in agents_by_app_id.values()],
|
||||
)
|
||||
debug_conversation_ids_by_agent_id = roster_service.load_or_create_agent_app_debug_conversation_ids_by_agent_id(
|
||||
tenant_id=tenant_id,
|
||||
agents=list(agents_by_app_id.values()),
|
||||
account_id=current_user.id,
|
||||
)
|
||||
payload = AgentAppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json")
|
||||
for item in payload["data"]:
|
||||
app_id = item["id"]
|
||||
@ -334,7 +343,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["debug_conversation_id"] = debug_conversation_ids_by_agent_id.get(agent.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, [])
|
||||
@ -442,7 +451,11 @@ class AgentAppListApi(Resource):
|
||||
empty = AgentAppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[])
|
||||
return empty.model_dump(mode="json")
|
||||
|
||||
return _serialize_agent_app_pagination(app_pagination, tenant_id=current_tenant_id)
|
||||
return _serialize_agent_app_pagination(
|
||||
app_pagination,
|
||||
tenant_id=current_tenant_id,
|
||||
current_user=current_user,
|
||||
)
|
||||
|
||||
@console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__])
|
||||
@console_ns.response(201, "Agent app created successfully", console_ns.models[AgentAppDetailWithSite.__name__])
|
||||
@ -467,7 +480,7 @@ class AgentAppListApi(Resource):
|
||||
)
|
||||
|
||||
app = AppService().create_app(current_tenant_id, params, current_user)
|
||||
return _serialize_agent_app_detail(app), 201
|
||||
return _serialize_agent_app_detail(app, current_user=current_user), 201
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>")
|
||||
@ -477,10 +490,11 @@ class AgentAppApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, agent_id: UUID):
|
||||
def get(self, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
|
||||
return _serialize_agent_app_detail(app_model)
|
||||
return _serialize_agent_app_detail(app_model, current_user=current_user)
|
||||
|
||||
@console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__])
|
||||
@console_ns.response(200, "Agent app updated successfully", console_ns.models[AgentAppDetailWithSite.__name__])
|
||||
@ -490,8 +504,9 @@ class AgentAppApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def put(self, tenant_id: str, agent_id: UUID):
|
||||
def put(self, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
|
||||
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
|
||||
args_dict: AppService.ArgsDict = {
|
||||
@ -505,7 +520,7 @@ class AgentAppApi(Resource):
|
||||
"role": args.role,
|
||||
}
|
||||
updated = AppService().update_app(app_model, args_dict)
|
||||
return _serialize_agent_app_detail(updated)
|
||||
return _serialize_agent_app_detail(updated, current_user=current_user)
|
||||
|
||||
@console_ns.response(204, "Agent app deleted successfully")
|
||||
@console_ns.response(403, "Insufficient permissions")
|
||||
@ -544,7 +559,7 @@ class AgentAppCopyApi(Resource):
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
)
|
||||
return _serialize_agent_app_detail(copied_app), 201
|
||||
return _serialize_agent_app_detail(copied_app, current_user=current_user), 201
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/api-access")
|
||||
|
||||
@ -40,12 +40,15 @@ from core.errors.error import (
|
||||
QuotaExceededError,
|
||||
)
|
||||
from core.helper.trace_id_helper import get_external_trace_id
|
||||
from extensions.ext_database import db
|
||||
from graphon.model_runtime.errors.invoke import InvokeError
|
||||
from libs import helper
|
||||
from libs.helper import uuid_value
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
from models.model import App, AppMode
|
||||
from services.agent.errors import AgentNotFoundError
|
||||
from services.agent.roster_service import AgentRosterService
|
||||
from services.app_generate_service import AppGenerateService
|
||||
from services.app_task_service import AppTaskService
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
@ -191,10 +194,11 @@ class ChatMessageApi(Resource):
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.AGENT])
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
return _create_chat_message(current_user=current_user, app_model=app_model)
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
return _create_chat_message(current_tenant_id=current_tenant_id, current_user=current_user, app_model=app_model)
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/chat-messages")
|
||||
@ -215,7 +219,12 @@ class AgentChatMessageApi(Resource):
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
|
||||
return _create_chat_message(current_user=current_user, app_model=app_model)
|
||||
return _create_chat_message(
|
||||
current_tenant_id=current_tenant_id,
|
||||
current_user=current_user,
|
||||
app_model=app_model,
|
||||
agent_id=str(agent_id),
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/chat-messages/<string:task_id>/stop")
|
||||
@ -249,11 +258,45 @@ class AgentChatMessageStopApi(Resource):
|
||||
return _stop_chat_message(current_user_id=current_user_id, app_model=app_model, task_id=task_id)
|
||||
|
||||
|
||||
def _create_chat_message(*, current_user: Account, app_model: App):
|
||||
def _resolve_current_user_agent_debug_conversation_id(
|
||||
*, current_tenant_id: str, current_user: Account, app_model: App, agent_id: str | None
|
||||
) -> str:
|
||||
roster_service = AgentRosterService(db.session)
|
||||
if agent_id:
|
||||
return roster_service.get_or_create_agent_app_debug_conversation_id(
|
||||
tenant_id=current_tenant_id,
|
||||
agent_id=agent_id,
|
||||
account_id=current_user.id,
|
||||
)
|
||||
|
||||
agent = roster_service.get_app_backing_agent(tenant_id=current_tenant_id, app_id=str(app_model.id))
|
||||
if agent is None:
|
||||
raise AgentNotFoundError()
|
||||
return roster_service.get_or_create_agent_app_debug_conversation_id(
|
||||
tenant_id=current_tenant_id,
|
||||
agent_id=agent.id,
|
||||
account_id=current_user.id,
|
||||
)
|
||||
|
||||
|
||||
def _create_chat_message(
|
||||
*, current_user: Account, app_model: App, current_tenant_id: str | None = None, agent_id: str | None = None
|
||||
):
|
||||
raw_payload = console_ns.payload or {}
|
||||
args_model = ChatMessagePayload.model_validate(raw_payload)
|
||||
args = args_model.model_dump(exclude_none=True, by_alias=True)
|
||||
|
||||
if AppMode.value_of(app_model.mode) == AppMode.AGENT:
|
||||
debug_conversation_id = _resolve_current_user_agent_debug_conversation_id(
|
||||
current_tenant_id=current_tenant_id or app_model.tenant_id,
|
||||
current_user=current_user,
|
||||
app_model=app_model,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
if args_model.conversation_id and args_model.conversation_id != debug_conversation_id:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
args["conversation_id"] = debug_conversation_id
|
||||
|
||||
streaming = _resolve_debugger_chat_streaming(
|
||||
app_mode=AppMode.value_of(app_model.mode),
|
||||
response_mode=args_model.response_mode,
|
||||
|
||||
@ -53,6 +53,7 @@ from libs.login import login_required
|
||||
from models.account import Account
|
||||
from models.enums import FeedbackFromSource, FeedbackRating
|
||||
from models.model import App, AppMode, Conversation, Message, MessageAnnotation, MessageFeedback
|
||||
from services.conversation_service import ConversationService
|
||||
from services.errors.conversation import ConversationNotExistsError
|
||||
from services.errors.message import MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError
|
||||
from services.message_service import MessageService, attach_message_extra_contents
|
||||
@ -186,10 +187,11 @@ class ChatMessageListApi(Resource):
|
||||
@account_initialization_required
|
||||
@setup_required
|
||||
@edit_permission_required
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, app_model: App):
|
||||
return _list_chat_messages(app_model=app_model)
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
return _list_chat_messages(app_model=app_model, current_user=current_user)
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/chat-messages")
|
||||
@ -205,10 +207,11 @@ class AgentChatMessageListApi(Resource):
|
||||
@setup_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, agent_id: UUID):
|
||||
def get(self, current_tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
|
||||
return _list_chat_messages(app_model=app_model)
|
||||
return _list_chat_messages(app_model=app_model, current_user=current_user)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/feedbacks")
|
||||
@ -390,14 +393,24 @@ class AgentMessageApi(Resource):
|
||||
return _get_message_detail(app_model=app_model, message_id=message_id)
|
||||
|
||||
|
||||
def _list_chat_messages(*, app_model: App):
|
||||
def _list_chat_messages(*, app_model: App, current_user: Account | None = None):
|
||||
args = ChatMessagesQuery.model_validate(request.args.to_dict())
|
||||
|
||||
conversation = db.session.scalar(
|
||||
select(Conversation)
|
||||
.where(Conversation.id == args.conversation_id, Conversation.app_id == app_model.id)
|
||||
.limit(1)
|
||||
)
|
||||
if AppMode.value_of(app_model.mode) == AppMode.AGENT and current_user is not None:
|
||||
try:
|
||||
conversation = ConversationService.get_conversation(
|
||||
app_model=app_model,
|
||||
conversation_id=args.conversation_id,
|
||||
user=current_user,
|
||||
)
|
||||
except ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
else:
|
||||
conversation = db.session.scalar(
|
||||
select(Conversation)
|
||||
.where(Conversation.id == args.conversation_id, Conversation.app_id == app_model.id)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""add agent debug conversation id
|
||||
"""add agent debug conversations
|
||||
|
||||
Revision ID: c8f4a6b2d3e1
|
||||
Revises: b2515f9d4c2a
|
||||
@ -18,13 +18,49 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _is_pg(conn) -> bool:
|
||||
return conn.dialect.name == "postgresql"
|
||||
|
||||
|
||||
def _uuid_column(name: str, *, nullable: bool = False, primary_key: bool = False) -> sa.Column:
|
||||
kwargs = {"nullable": nullable, "primary_key": primary_key}
|
||||
if primary_key and _is_pg(op.get_bind()):
|
||||
kwargs["server_default"] = sa.text("uuidv7()")
|
||||
return sa.Column(name, models.types.StringUUID(), **kwargs)
|
||||
|
||||
|
||||
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)
|
||||
op.create_table(
|
||||
"agent_debug_conversations",
|
||||
_uuid_column("id", primary_key=True),
|
||||
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("agent_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("app_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("account_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("conversation_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("agent_debug_conversation_pkey")),
|
||||
sa.UniqueConstraint(
|
||||
"tenant_id",
|
||||
"agent_id",
|
||||
"account_id",
|
||||
name=op.f("agent_debug_conversation_agent_account_unique"),
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_conversation_idx",
|
||||
"agent_debug_conversations",
|
||||
["conversation_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"agent_debug_conversation_account_idx",
|
||||
"agent_debug_conversations",
|
||||
["tenant_id", "account_id"],
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
op.drop_index("agent_debug_conversation_account_idx", table_name="agent_debug_conversations")
|
||||
op.drop_index("agent_debug_conversation_conversation_idx", table_name="agent_debug_conversations")
|
||||
op.drop_table("agent_debug_conversations")
|
||||
|
||||
@ -13,6 +13,7 @@ from .agent import (
|
||||
AgentConfigRevision,
|
||||
AgentConfigRevisionOperation,
|
||||
AgentConfigSnapshot,
|
||||
AgentDebugConversation,
|
||||
AgentDriveFile,
|
||||
AgentDriveFileKind,
|
||||
AgentIconType,
|
||||
@ -156,6 +157,7 @@ __all__ = [
|
||||
"AgentConfigRevision",
|
||||
"AgentConfigRevisionOperation",
|
||||
"AgentConfigSnapshot",
|
||||
"AgentDebugConversation",
|
||||
"AgentDriveFile",
|
||||
"AgentDriveFileKind",
|
||||
"AgentIconType",
|
||||
|
||||
@ -135,7 +135,6 @@ 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",
|
||||
@ -163,7 +162,6 @@ 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)
|
||||
@ -184,6 +182,34 @@ class Agent(DefaultFieldsMixin, Base):
|
||||
archived_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
|
||||
class AgentDebugConversation(DefaultFieldsMixin, Base):
|
||||
"""Per-account console debug conversation for an Agent App.
|
||||
|
||||
Agent App preview state must be isolated by editor account. The Agent row is
|
||||
shared by everyone in the workspace, so this table owns the user-specific
|
||||
conversation pointer used by console debug chat.
|
||||
"""
|
||||
|
||||
__tablename__ = "agent_debug_conversations"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="agent_debug_conversation_pkey"),
|
||||
UniqueConstraint(
|
||||
"tenant_id",
|
||||
"agent_id",
|
||||
"account_id",
|
||||
name="agent_debug_conversation_agent_account_unique",
|
||||
),
|
||||
Index("agent_debug_conversation_conversation_idx", "conversation_id"),
|
||||
Index("agent_debug_conversation_account_idx", "tenant_id", "account_id"),
|
||||
)
|
||||
|
||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
conversation_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
|
||||
|
||||
class AgentConfigSnapshot(DefaultFieldsMixin, Base):
|
||||
"""Immutable Agent Soul snapshot.
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ from models.agent import (
|
||||
AgentConfigRevision,
|
||||
AgentConfigRevisionOperation,
|
||||
AgentConfigSnapshot,
|
||||
AgentDebugConversation,
|
||||
AgentKind,
|
||||
AgentScope,
|
||||
AgentSource,
|
||||
@ -97,7 +98,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),
|
||||
"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,
|
||||
@ -393,15 +394,12 @@ 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()
|
||||
self._get_or_create_agent_app_debug_conversation(agent=agent, account_id=account_id)
|
||||
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."""
|
||||
"""Create one console debug conversation for an Agent App editor."""
|
||||
|
||||
conversation = Conversation(
|
||||
app_id=app_id,
|
||||
@ -425,6 +423,98 @@ class AgentRosterService:
|
||||
self._session.flush()
|
||||
return conversation.id
|
||||
|
||||
def _get_or_create_agent_app_debug_conversation(self, *, agent: Agent, account_id: str) -> str:
|
||||
if not agent.app_id:
|
||||
raise AgentNotFoundError()
|
||||
|
||||
mapping = self._session.scalar(
|
||||
select(AgentDebugConversation).where(
|
||||
AgentDebugConversation.tenant_id == agent.tenant_id,
|
||||
AgentDebugConversation.agent_id == agent.id,
|
||||
AgentDebugConversation.account_id == account_id,
|
||||
)
|
||||
)
|
||||
if mapping is not None:
|
||||
conversation_id = self._session.scalar(
|
||||
select(Conversation.id).where(
|
||||
Conversation.id == mapping.conversation_id,
|
||||
Conversation.app_id == agent.app_id,
|
||||
Conversation.from_source == ConversationFromSource.CONSOLE,
|
||||
Conversation.from_account_id == account_id,
|
||||
Conversation.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
if conversation_id:
|
||||
return conversation_id
|
||||
|
||||
mapping.conversation_id = self._create_agent_app_debug_conversation(
|
||||
app_id=agent.app_id,
|
||||
account_id=account_id,
|
||||
)
|
||||
self._session.flush()
|
||||
return mapping.conversation_id
|
||||
|
||||
conversation_id = self._create_agent_app_debug_conversation(
|
||||
app_id=agent.app_id,
|
||||
account_id=account_id,
|
||||
)
|
||||
self._session.add(
|
||||
AgentDebugConversation(
|
||||
tenant_id=agent.tenant_id,
|
||||
agent_id=agent.id,
|
||||
app_id=agent.app_id,
|
||||
account_id=account_id,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
)
|
||||
self._session.flush()
|
||||
return conversation_id
|
||||
|
||||
def get_or_create_agent_app_debug_conversation_id(
|
||||
self, *, tenant_id: str, agent_id: str, account_id: str, commit: bool = True
|
||||
) -> str:
|
||||
"""Return the current editor's debug conversation for an Agent App."""
|
||||
|
||||
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:
|
||||
raise AgentNotFoundError()
|
||||
|
||||
conversation_id = self._get_or_create_agent_app_debug_conversation(agent=agent, account_id=account_id)
|
||||
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]:
|
||||
"""Return per-account debug conversations for a page of Agent Apps."""
|
||||
|
||||
conversation_ids_by_agent_id: dict[str, str] = {}
|
||||
changed = False
|
||||
for agent in agents:
|
||||
if (
|
||||
agent.tenant_id != tenant_id
|
||||
or agent.scope != AgentScope.ROSTER
|
||||
or agent.source != AgentSource.AGENT_APP
|
||||
):
|
||||
continue
|
||||
conversation_ids_by_agent_id[agent.id] = self._get_or_create_agent_app_debug_conversation(
|
||||
agent=agent,
|
||||
account_id=account_id,
|
||||
)
|
||||
changed = True
|
||||
if changed:
|
||||
self._session.commit()
|
||||
return conversation_ids_by_agent_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:
|
||||
|
||||
@ -246,6 +246,16 @@ def test_agent_app_list_and_create_use_agent_route(
|
||||
active_config_snapshot_id=None,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_or_create_agent_app_debug_conversation_id",
|
||||
lambda _self, **kwargs: "debug-conversation-detail",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_or_create_agent_app_debug_conversation_id",
|
||||
lambda _self, **kwargs: "debug-conversation-detail",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"load_published_references_by_agent_id",
|
||||
@ -266,6 +276,16 @@ def test_agent_app_list_and_create_use_agent_route(
|
||||
]
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"load_or_create_agent_app_debug_conversation_ids_by_agent_id",
|
||||
lambda _self, **kwargs: {"agent-list": "debug-conversation-list"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_or_create_agent_app_debug_conversation_id",
|
||||
lambda _self, **kwargs: "debug-conversation-created",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.FeatureService,
|
||||
"get_system_features",
|
||||
@ -362,6 +382,11 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
active_config_snapshot_id=None,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_or_create_agent_app_debug_conversation_id",
|
||||
lambda _self, **kwargs: "debug-conversation-detail",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.FeatureService,
|
||||
"get_system_features",
|
||||
@ -382,7 +407,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
|
||||
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
|
||||
|
||||
detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", agent_id)
|
||||
detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", SimpleNamespace(id=account_id), agent_id)
|
||||
assert detail["id"] == agent_id
|
||||
assert detail["app_id"] == "app-1"
|
||||
assert detail["debug_conversation_id"] == "debug-conversation-detail"
|
||||
@ -394,7 +419,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
"/console/api/agent/00000000-0000-0000-0000-000000000001",
|
||||
json={"name": "Renamed", "description": "", "role": "Reviewer", "icon_type": "emoji", "icon": "R"},
|
||||
):
|
||||
updated = unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id)
|
||||
updated = unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", SimpleNamespace(id=account_id), agent_id)
|
||||
|
||||
assert updated["name"] == "Renamed"
|
||||
assert updated["id"] == agent_id
|
||||
@ -429,7 +454,7 @@ def test_agent_app_copy_uses_agent_id_and_returns_agent_detail(
|
||||
monkeypatch.setattr(
|
||||
roster_controller,
|
||||
"_serialize_agent_app_detail",
|
||||
lambda app_model: {"id": "copied-agent", "app_id": app_model.id, "name": app_model.name},
|
||||
lambda app_model, **_kwargs: {"id": "copied-agent", "app_id": app_model.id, "name": app_model.name},
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
@ -620,7 +645,7 @@ def test_agent_app_update_rejects_empty_role(app: Flask, monkeypatch: pytest.Mon
|
||||
json={"name": "Renamed", "description": "", "role": "", "icon_type": "emoji", "icon": "R"},
|
||||
):
|
||||
with pytest.raises(ValueError, match="String should have at least 1 character"):
|
||||
unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id)
|
||||
unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", SimpleNamespace(id="account-1"), agent_id)
|
||||
|
||||
|
||||
def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@ -1043,7 +1068,7 @@ def test_agent_chat_generate_and_stop_routes_resolve_app_from_agent_id(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
|
||||
) -> None:
|
||||
agent_id = "00000000-0000-0000-0000-000000000001"
|
||||
app_model = SimpleNamespace(id="app-1", mode="agent")
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode="agent")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def resolve_agent_app_model(**kwargs: object) -> object:
|
||||
@ -1082,7 +1107,7 @@ def test_agent_chat_generate_and_stop_routes_resolve_app_from_agent_id(
|
||||
def test_agent_chat_helper_forces_agent_streaming_and_external_trace(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
|
||||
) -> None:
|
||||
app_model = SimpleNamespace(id="app-1", mode="agent")
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode="agent")
|
||||
current_user = SimpleNamespace(id=account_id)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
@ -1091,6 +1116,11 @@ def test_agent_chat_helper_forces_agent_streaming_and_external_trace(
|
||||
return {"answer": "ok"}
|
||||
|
||||
monkeypatch.setattr(completion_controller.AppGenerateService, "generate", generate)
|
||||
monkeypatch.setattr(
|
||||
completion_controller,
|
||||
"_resolve_current_user_agent_debug_conversation_id",
|
||||
lambda **kwargs: "debug-conversation-1",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
completion_controller.helper,
|
||||
"compact_generate_response",
|
||||
@ -1109,10 +1139,83 @@ def test_agent_chat_helper_forces_agent_streaming_and_external_trace(
|
||||
assert captured["streaming"] is True
|
||||
args = cast(dict[str, object], captured["args"])
|
||||
assert args["response_mode"] == "streaming"
|
||||
assert args["conversation_id"] == "debug-conversation-1"
|
||||
assert args["auto_generate_name"] is False
|
||||
assert args["external_trace_id"] == "trace-1"
|
||||
|
||||
|
||||
def test_agent_chat_helper_rejects_foreign_debug_conversation(
|
||||
app: Flask,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
account_id: str,
|
||||
) -> None:
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode="agent")
|
||||
|
||||
monkeypatch.setattr(
|
||||
completion_controller,
|
||||
"_resolve_current_user_agent_debug_conversation_id",
|
||||
lambda **kwargs: "owned-conversation",
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
json={
|
||||
"inputs": {},
|
||||
"query": "hello",
|
||||
"response_mode": "streaming",
|
||||
"conversation_id": "00000000-0000-0000-0000-000000000001",
|
||||
}
|
||||
):
|
||||
with pytest.raises(NotFound):
|
||||
completion_controller._create_chat_message(
|
||||
current_tenant_id="tenant-1",
|
||||
current_user=SimpleNamespace(id=account_id),
|
||||
app_model=app_model,
|
||||
agent_id="agent-1",
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_current_user_agent_debug_conversation_uses_agent_or_backing_app(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
calls: list[dict[str, object]] = []
|
||||
|
||||
class FakeRosterService:
|
||||
def __init__(self, session: object) -> None:
|
||||
calls.append({"session": session})
|
||||
|
||||
def get_or_create_agent_app_debug_conversation_id(self, **kwargs: object) -> str:
|
||||
calls.append({"get_or_create": kwargs})
|
||||
return f"debug-{kwargs['agent_id']}"
|
||||
|
||||
def get_app_backing_agent(self, **kwargs: object) -> object:
|
||||
calls.append({"get_app_backing_agent": kwargs})
|
||||
return SimpleNamespace(id="backing-agent")
|
||||
|
||||
monkeypatch.setattr(completion_controller, "AgentRosterService", FakeRosterService)
|
||||
monkeypatch.setattr(completion_controller, "db", SimpleNamespace(session="session-1"))
|
||||
|
||||
explicit_id = completion_controller._resolve_current_user_agent_debug_conversation_id(
|
||||
current_tenant_id="tenant-1",
|
||||
current_user=SimpleNamespace(id="account-1"),
|
||||
app_model=SimpleNamespace(id="app-1"),
|
||||
agent_id="agent-1",
|
||||
)
|
||||
fallback_id = completion_controller._resolve_current_user_agent_debug_conversation_id(
|
||||
current_tenant_id="tenant-1",
|
||||
current_user=SimpleNamespace(id="account-1"),
|
||||
app_model=SimpleNamespace(id="app-1"),
|
||||
agent_id=None,
|
||||
)
|
||||
|
||||
assert explicit_id == "debug-agent-1"
|
||||
assert fallback_id == "debug-backing-agent"
|
||||
assert calls[1] == {"get_or_create": {"tenant_id": "tenant-1", "agent_id": "agent-1", "account_id": "account-1"}}
|
||||
assert calls[3] == {"get_app_backing_agent": {"tenant_id": "tenant-1", "app_id": "app-1"}}
|
||||
assert calls[4] == {
|
||||
"get_or_create": {"tenant_id": "tenant-1", "agent_id": "backing-agent", "account_id": "account-1"}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error", "expected"),
|
||||
[
|
||||
@ -1159,7 +1262,7 @@ def test_agent_chat_helper_maps_generation_errors(
|
||||
def test_agent_chat_message_routes_resolve_app_from_agent_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
agent_id = "00000000-0000-0000-0000-000000000001"
|
||||
message_id = "00000000-0000-0000-0000-000000000002"
|
||||
app_model = SimpleNamespace(id="app-1")
|
||||
app_model = SimpleNamespace(id="app-1", mode="agent")
|
||||
current_user = SimpleNamespace(id="account-1")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
@ -1189,7 +1292,9 @@ def test_agent_chat_message_routes_resolve_app_from_agent_id(app: Flask, monkeyp
|
||||
monkeypatch.setattr(message_controller, "_get_message_suggested_questions", get_message_suggested_questions)
|
||||
monkeypatch.setattr(message_controller, "_get_message_detail", get_message_detail)
|
||||
|
||||
assert unwrap(AgentChatMessageListApi.get)(AgentChatMessageListApi(), "tenant-1", agent_id) == {"data": []}
|
||||
assert unwrap(AgentChatMessageListApi.get)(AgentChatMessageListApi(), "tenant-1", current_user, agent_id) == {
|
||||
"data": []
|
||||
}
|
||||
assert cast(dict[str, object], captured["list"])["app_model"] is app_model
|
||||
|
||||
with app.test_request_context(json={"message_id": message_id, "rating": "like"}):
|
||||
@ -1246,11 +1351,73 @@ def test_list_chat_messages_supports_first_id_pagination(app: Flask, monkeypatch
|
||||
"/console/api/agent/agent-1/chat-messages"
|
||||
f"?conversation_id={conversation_id}&first_id={first_message_id}&limit=1"
|
||||
):
|
||||
result = message_controller._list_chat_messages(app_model=SimpleNamespace(id="app-1"))
|
||||
result = message_controller._list_chat_messages(app_model=SimpleNamespace(id="app-1", mode="chat"))
|
||||
|
||||
assert result == {"data": [older_message_id], "limit": 1, "has_more": True}
|
||||
|
||||
|
||||
def test_list_agent_chat_messages_uses_current_user_conversation(
|
||||
app: Flask,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
conversation_id = "00000000-0000-0000-0000-000000000010"
|
||||
message_id = "00000000-0000-0000-0000-000000000011"
|
||||
conversation = SimpleNamespace(id=conversation_id)
|
||||
message = SimpleNamespace(id=message_id, created_at=1)
|
||||
current_user = SimpleNamespace(id="account-1")
|
||||
app_model = SimpleNamespace(id="app-1", mode="agent")
|
||||
captured: dict[str, object] = {}
|
||||
session = SimpleNamespace(
|
||||
scalar=lambda _stmt: False,
|
||||
scalars=lambda _stmt: SimpleNamespace(all=lambda: [message]),
|
||||
)
|
||||
|
||||
class FakeMessagePaginationResponse:
|
||||
@classmethod
|
||||
def model_validate(cls, pagination: object, from_attributes: bool = False) -> object:
|
||||
return SimpleNamespace(
|
||||
model_dump=lambda mode: {
|
||||
"data": [item.id for item in pagination.data],
|
||||
"limit": pagination.limit,
|
||||
"has_more": pagination.has_more,
|
||||
}
|
||||
)
|
||||
|
||||
def get_conversation(**kwargs: object) -> object:
|
||||
captured.update(kwargs)
|
||||
return conversation
|
||||
|
||||
monkeypatch.setattr(message_controller.ConversationService, "get_conversation", get_conversation)
|
||||
monkeypatch.setattr(message_controller, "db", SimpleNamespace(session=session))
|
||||
monkeypatch.setattr(message_controller, "attach_message_extra_contents", lambda messages: None)
|
||||
monkeypatch.setattr(message_controller, "MessageInfiniteScrollPaginationResponse", FakeMessagePaginationResponse)
|
||||
|
||||
with app.test_request_context(f"/console/api/agent/agent-1/chat-messages?conversation_id={conversation_id}"):
|
||||
result = message_controller._list_chat_messages(app_model=app_model, current_user=current_user)
|
||||
|
||||
assert result == {"data": [message_id], "limit": 20, "has_more": False}
|
||||
assert captured == {"app_model": app_model, "conversation_id": conversation_id, "user": current_user}
|
||||
|
||||
|
||||
def test_list_agent_chat_messages_rejects_foreign_conversation(
|
||||
app: Flask,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
conversation_id = "00000000-0000-0000-0000-000000000010"
|
||||
monkeypatch.setattr(
|
||||
message_controller.ConversationService,
|
||||
"get_conversation",
|
||||
lambda **kwargs: (_ for _ in ()).throw(message_controller.ConversationNotExistsError()),
|
||||
)
|
||||
|
||||
with app.test_request_context(f"/console/api/agent/agent-1/chat-messages?conversation_id={conversation_id}"):
|
||||
with pytest.raises(NotFound):
|
||||
message_controller._list_chat_messages(
|
||||
app_model=SimpleNamespace(id="app-1", mode="agent"),
|
||||
current_user=SimpleNamespace(id="account-1"),
|
||||
)
|
||||
|
||||
|
||||
def test_update_message_feedback_rejects_empty_rating_without_existing_feedback(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@ -8,6 +8,7 @@ from models.agent import (
|
||||
Agent,
|
||||
AgentConfigRevisionOperation,
|
||||
AgentConfigSnapshot,
|
||||
AgentDebugConversation,
|
||||
AgentKind,
|
||||
AgentScope,
|
||||
AgentSource,
|
||||
@ -1093,6 +1094,11 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch: pytest.MonkeyPatch
|
||||
scalars=[[AgentConfigSnapshot(id="version-1", agent_id="agent-1", version=1)]],
|
||||
)
|
||||
service = AgentRosterService(fake_session)
|
||||
monkeypatch.setattr(
|
||||
AgentRosterService,
|
||||
"_get_or_create_agent_app_debug_conversation",
|
||||
lambda self, *, agent, account_id: "debug-conversation-1",
|
||||
)
|
||||
payload = roster_service.RosterAgentCreatePayload(
|
||||
name="Analyst",
|
||||
description="desc",
|
||||
@ -1134,6 +1140,135 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch: pytest.MonkeyPatch
|
||||
assert loaded_versions["version-1"].agent_id == "agent-1"
|
||||
|
||||
|
||||
def test_agent_app_debug_conversation_create_reuse_and_recreate():
|
||||
agent = Agent(
|
||||
id="agent-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
name="Analyst",
|
||||
description="",
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
)
|
||||
|
||||
create_session = FakeSession(scalar=[agent, None])
|
||||
created_id = AgentRosterService(create_session).get_or_create_agent_app_debug_conversation_id(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="agent-1",
|
||||
account_id="account-1",
|
||||
)
|
||||
created_conversation = next(value for value in create_session.added if isinstance(value, Conversation))
|
||||
created_mapping = next(value for value in create_session.added if isinstance(value, AgentDebugConversation))
|
||||
assert created_id == created_mapping.conversation_id
|
||||
assert created_conversation.app_id == "app-1"
|
||||
assert created_conversation.from_account_id == "account-1"
|
||||
assert created_mapping.tenant_id == "tenant-1"
|
||||
assert created_mapping.agent_id == "agent-1"
|
||||
assert created_mapping.account_id == "account-1"
|
||||
assert create_session.commits == 1
|
||||
|
||||
existing_mapping = AgentDebugConversation(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="agent-1",
|
||||
app_id="app-1",
|
||||
account_id="account-1",
|
||||
conversation_id="existing-conversation",
|
||||
)
|
||||
reuse_session = FakeSession(scalar=[agent, existing_mapping, "existing-conversation"])
|
||||
reused_id = AgentRosterService(reuse_session).get_or_create_agent_app_debug_conversation_id(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="agent-1",
|
||||
account_id="account-1",
|
||||
)
|
||||
assert reused_id == "existing-conversation"
|
||||
assert reuse_session.added == []
|
||||
assert reuse_session.commits == 1
|
||||
|
||||
stale_mapping = AgentDebugConversation(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="agent-1",
|
||||
app_id="app-1",
|
||||
account_id="account-1",
|
||||
conversation_id="deleted-conversation",
|
||||
)
|
||||
recreate_session = FakeSession(scalar=[agent, stale_mapping, None])
|
||||
recreated_id = AgentRosterService(recreate_session).get_or_create_agent_app_debug_conversation_id(
|
||||
tenant_id="tenant-1",
|
||||
agent_id="agent-1",
|
||||
account_id="account-1",
|
||||
)
|
||||
assert recreated_id == stale_mapping.conversation_id
|
||||
assert recreated_id != "deleted-conversation"
|
||||
assert any(isinstance(value, Conversation) for value in recreate_session.added)
|
||||
assert recreate_session.commits == 1
|
||||
|
||||
|
||||
def test_agent_app_debug_conversation_requires_app_binding():
|
||||
agent = Agent(
|
||||
id="agent-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id=None,
|
||||
name="Analyst",
|
||||
description="",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
)
|
||||
|
||||
with pytest.raises(roster_service.AgentNotFoundError):
|
||||
AgentRosterService(FakeSession())._get_or_create_agent_app_debug_conversation(
|
||||
agent=agent,
|
||||
account_id="account-1",
|
||||
)
|
||||
|
||||
|
||||
def test_load_or_create_agent_app_debug_conversations_filters_agent_apps():
|
||||
valid_agent = Agent(
|
||||
id="agent-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
name="Analyst",
|
||||
description="",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
)
|
||||
wrong_tenant_agent = Agent(
|
||||
id="agent-2",
|
||||
tenant_id="tenant-2",
|
||||
app_id="app-2",
|
||||
name="Other tenant",
|
||||
description="",
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
)
|
||||
workflow_agent = Agent(
|
||||
id="agent-3",
|
||||
tenant_id="tenant-1",
|
||||
app_id=None,
|
||||
name="Workflow only",
|
||||
description="",
|
||||
scope=AgentScope.WORKFLOW_ONLY,
|
||||
source=AgentSource.WORKFLOW,
|
||||
status=AgentStatus.ACTIVE,
|
||||
)
|
||||
|
||||
fake_session = FakeSession(scalar=[None])
|
||||
result = AgentRosterService(fake_session).load_or_create_agent_app_debug_conversation_ids_by_agent_id(
|
||||
tenant_id="tenant-1",
|
||||
agents=[valid_agent, wrong_tenant_agent, workflow_agent],
|
||||
account_id="account-1",
|
||||
)
|
||||
|
||||
assert list(result) == ["agent-1"]
|
||||
assert result["agent-1"]
|
||||
assert fake_session.commits == 1
|
||||
assert len([value for value in fake_session.added if isinstance(value, AgentDebugConversation)]) == 1
|
||||
|
||||
|
||||
def test_agent_app_visible_versions_exclude_draft_saves():
|
||||
agent_app = Agent(source=AgentSource.AGENT_APP)
|
||||
roster_agent = Agent(source=AgentSource.ROSTER)
|
||||
@ -1448,7 +1583,6 @@ 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
|
||||
@ -1460,12 +1594,18 @@ class TestAgentAppBackingAgent:
|
||||
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"
|
||||
debug_mappings = [a for a in session.added if isinstance(a, AgentDebugConversation)]
|
||||
assert len(debug_mappings) == 1
|
||||
assert debug_mappings[0].tenant_id == "tenant-1"
|
||||
assert debug_mappings[0].agent_id == agent.id
|
||||
assert debug_mappings[0].app_id == "app-1"
|
||||
assert debug_mappings[0].account_id == "account-1"
|
||||
assert debug_mappings[0].conversation_id == conversations[0].id
|
||||
# Caller (AppService.create_app) owns the commit — helper must not commit.
|
||||
assert session.commits == 0
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user