From 5fe367b77bc43fe5c908a88af219246356addba8 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Fri, 27 Mar 2026 01:15:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E7=9A=84=20agent=5Fconfig=5Fid=20=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yuxi/agents/buildin/chatbot/prompt.py | 2 +- .../repositories/conversation_repository.py | 15 ++++++++ .../yuxi/services/agent_run_service.py | 4 +++ backend/package/yuxi/services/chat_service.py | 35 +++++++++++++++++++ .../yuxi/services/conversation_service.py | 3 ++ .../yuxi/storage/postgres/models_business.py | 3 +- backend/server/routers/chat_router.py | 1 + backend/test/api/test_chat_agent_sync.py | 10 ++++++ backend/test/test_conversation_repository.py | 16 +++++++++ web/src/components/AgentChatComponent.vue | 12 ++++--- 10 files changed, 95 insertions(+), 6 deletions(-) diff --git a/backend/package/yuxi/agents/buildin/chatbot/prompt.py b/backend/package/yuxi/agents/buildin/chatbot/prompt.py index 59a45cdd..45013f67 100644 --- a/backend/package/yuxi/agents/buildin/chatbot/prompt.py +++ b/backend/package/yuxi/agents/buildin/chatbot/prompt.py @@ -9,6 +9,6 @@ PROMPT = """ - /home/gem/user-data/uploads/:用于存放用户上传的文件 如果启用了知识库,除了使用知识库工具之外, -当需要精准获取信息的时候,还可以直接访问知识库文件系统 (路径为 /home/gem/kbs/)来获取信息。 +当需要精准获取信息的时候,或者 query_kb 中没有找到相关的内容,还可以直接访问知识库文件系统 (路径为 /home/gem/kbs/)来获取信息。 源文件可能无法解析,可以在 /home/gem/kbs//parsed/ 中找到解析后的 markdown 文件。 """ diff --git a/backend/package/yuxi/repositories/conversation_repository.py b/backend/package/yuxi/repositories/conversation_repository.py index 5c143fb3..81d3b42e 100644 --- a/backend/package/yuxi/repositories/conversation_repository.py +++ b/backend/package/yuxi/repositories/conversation_repository.py @@ -81,12 +81,27 @@ class ConversationRepository: metadata["attachments"] = list(metadata.get("attachments", [])) return metadata + def _normalize_agent_config_id(self, agent_config_id: int | None) -> int | None: + if agent_config_id is None: + return None + return int(agent_config_id) + async def _save_metadata(self, conversation: Conversation, metadata: dict) -> None: conversation.extra_metadata = metadata conversation.updated_at = utc_now_naive() await self.db.commit() await self.db.refresh(conversation) + async def bind_agent_config(self, thread_id: str, agent_config_id: int) -> Conversation | None: + conversation = await self.get_conversation_by_thread_id(thread_id) + if not conversation: + return None + + metadata = self._ensure_metadata(conversation) + metadata["agent_config_id"] = self._normalize_agent_config_id(agent_config_id) + await self._save_metadata(conversation, metadata) + return conversation + async def add_message( self, conversation_id: int, diff --git a/backend/package/yuxi/services/agent_run_service.py b/backend/package/yuxi/services/agent_run_service.py index 88076999..b5e0897b 100644 --- a/backend/package/yuxi/services/agent_run_service.py +++ b/backend/package/yuxi/services/agent_run_service.py @@ -79,6 +79,10 @@ async def create_agent_run_view( conversation = await conv_repo.get_conversation_by_thread_id(thread_id) if not conversation or conversation.user_id != str(current_user_id) or conversation.status == "deleted": raise HTTPException(status_code=404, detail="对话线程不存在") + if (conversation.extra_metadata or {}).get("agent_config_id") != int(agent_config_id): + conversation = await conv_repo.bind_agent_config(thread_id, agent_config_id) + if not conversation: + raise HTTPException(status_code=404, detail="对话线程不存在") request_id = str((meta or {}).get("request_id") or uuid.uuid4()) config = { diff --git a/backend/package/yuxi/services/chat_service.py b/backend/package/yuxi/services/chat_service.py index 2820bebe..72fadb55 100644 --- a/backend/package/yuxi/services/chat_service.py +++ b/backend/package/yuxi/services/chat_service.py @@ -371,6 +371,27 @@ async def check_and_handle_interrupts( logger.error(traceback.format_exc()) +async def _ensure_thread_bound_agent_config( + *, + conv_repo: ConversationRepository, + thread_id: str, + user_id: str, + agent_id: str, + agent_config_id: int, +) -> None: + conversation = await conv_repo.get_conversation_by_thread_id(thread_id) + if not conversation: + conversation = await conv_repo.create_conversation( + user_id=user_id, + agent_id=agent_id, + thread_id=thread_id, + ) + + current_agent_config_id = (conversation.extra_metadata or {}).get("agent_config_id") + if current_agent_config_id != int(agent_config_id): + await conv_repo.bind_agent_config(thread_id, agent_config_id) + + async def agent_chat( *, query: str, @@ -462,6 +483,13 @@ async def agent_chat( try: conv_repo = ConversationRepository(db) + await _ensure_thread_bound_agent_config( + conv_repo=conv_repo, + thread_id=thread_id, + user_id=user_id, + agent_id=agent_id, + agent_config_id=agent_config_id, + ) try: await conv_repo.add_message_by_thread_id( @@ -631,6 +659,13 @@ async def stream_agent_chat( try: conv_repo = ConversationRepository(db) + await _ensure_thread_bound_agent_config( + conv_repo=conv_repo, + thread_id=thread_id, + user_id=user_id, + agent_id=agent_id, + agent_config_id=agent_config_id, + ) try: await conv_repo.add_message_by_thread_id( diff --git a/backend/package/yuxi/services/conversation_service.py b/backend/package/yuxi/services/conversation_service.py index 70106591..2a6b1fee 100644 --- a/backend/package/yuxi/services/conversation_service.py +++ b/backend/package/yuxi/services/conversation_service.py @@ -287,6 +287,7 @@ async def create_thread_view( "title": conversation.title, "created_at": conversation.created_at.isoformat(), "updated_at": conversation.updated_at.isoformat(), + "metadata": conversation.extra_metadata or {}, } @@ -316,6 +317,7 @@ async def list_threads_view( "is_pinned": bool(conv.is_pinned), "created_at": conv.created_at.isoformat(), "updated_at": conv.updated_at.isoformat(), + "metadata": conv.extra_metadata or {}, } for conv in conversations ] @@ -356,6 +358,7 @@ async def update_thread_view( "is_pinned": bool(updated_conv.is_pinned), "created_at": updated_conv.created_at.isoformat(), "updated_at": updated_conv.updated_at.isoformat(), + "metadata": updated_conv.extra_metadata or {}, } diff --git a/backend/package/yuxi/storage/postgres/models_business.py b/backend/package/yuxi/storage/postgres/models_business.py index 6d625167..cb3ffdf9 100644 --- a/backend/package/yuxi/storage/postgres/models_business.py +++ b/backend/package/yuxi/storage/postgres/models_business.py @@ -231,6 +231,7 @@ class Conversation(Base): ) def to_dict(self) -> dict[str, Any]: + metadata = self.extra_metadata or {} return { "id": self.id, "thread_id": self.thread_id, @@ -241,7 +242,7 @@ class Conversation(Base): "is_pinned": bool(self.is_pinned), "created_at": format_utc_datetime(self.created_at), "updated_at": format_utc_datetime(self.updated_at), - "metadata": self.extra_metadata or {}, + "metadata": metadata, } diff --git a/backend/server/routers/chat_router.py b/backend/server/routers/chat_router.py index c239de8d..90f8c150 100644 --- a/backend/server/routers/chat_router.py +++ b/backend/server/routers/chat_router.py @@ -636,6 +636,7 @@ class ThreadResponse(BaseModel): is_pinned: bool = False created_at: str updated_at: str + metadata: dict[str, Any] = Field(default_factory=dict) class AttachmentResponse(BaseModel): diff --git a/backend/test/api/test_chat_agent_sync.py b/backend/test/api/test_chat_agent_sync.py index 83b77272..47b97094 100644 --- a/backend/test/api/test_chat_agent_sync.py +++ b/backend/test/api/test_chat_agent_sync.py @@ -98,6 +98,16 @@ async def test_chat_agent_sync_with_thread_id(test_client, admin_headers): f"thread_id mismatch: expected {thread_id}, got {payload.get('thread_id')}" ) + threads_response = await test_client.get("/api/chat/threads", headers=admin_headers) + assert threads_response.status_code == 200, threads_response.text + threads = threads_response.json() + target_thread = next((item for item in threads if item.get("id") == thread_id), None) + assert target_thread is not None, f"thread not found in thread list: {thread_id}" + assert (target_thread.get("metadata") or {}).get("agent_config_id") == agent_config_id, ( + "agent_config_id mismatch: " + f"expected {agent_config_id}, got {(target_thread.get('metadata') or {}).get('agent_config_id')}" + ) + async def test_chat_agent_sync_with_meta(test_client, admin_headers): """测试非流式对话传递 meta 参数""" diff --git a/backend/test/test_conversation_repository.py b/backend/test/test_conversation_repository.py index 172d1094..75de6dd4 100644 --- a/backend/test/test_conversation_repository.py +++ b/backend/test/test_conversation_repository.py @@ -20,3 +20,19 @@ def test_normalize_title_trims_spaces(): normalized = repo._normalize_title(" hello world ") assert normalized == "hello world" + + +def test_normalize_agent_config_id_casts_to_int(): + repo = ConversationRepository(None) # type: ignore[arg-type] + + normalized = repo._normalize_agent_config_id("12") # type: ignore[arg-type] + + assert normalized == 12 + + +def test_normalize_agent_config_id_allows_none(): + repo = ConversationRepository(None) # type: ignore[arg-type] + + normalized = repo._normalize_agent_config_id(None) + + assert normalized is None diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index b9c9812c..56b83e53 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -626,12 +626,16 @@ const setThreadAgentConfigId = (threadId, agentConfigId) => { if (!threadId) return const thread = threads.value.find((item) => item.id === threadId) if (thread) { - thread.agent_config_id = agentConfigId ?? null + thread.metadata = { + ...(thread.metadata || {}), + agent_config_id: agentConfigId ?? null + } } } const syncSelectedConfigForThread = async (thread) => { - if (!thread?.agent_config_id) return + const threadAgentConfigId = thread?.metadata?.agent_config_id + if (!threadAgentConfigId) return const targetAgentId = thread.agent_id || currentAgentId.value if (!targetAgentId) return @@ -641,8 +645,8 @@ const syncSelectedConfigForThread = async (thread) => { await agentStore.fetchAgentConfigs(targetAgentId) } - if (selectedAgentConfigId.value !== thread.agent_config_id) { - await agentStore.selectAgentConfig(thread.agent_config_id) + if (selectedAgentConfigId.value !== threadAgentConfigId) { + await agentStore.selectAgentConfig(threadAgentConfigId) } }