diff --git a/backend/package/yuxi/services/agent_run_service.py b/backend/package/yuxi/services/agent_run_service.py index 3b66bde8..88076999 100644 --- a/backend/package/yuxi/services/agent_run_service.py +++ b/backend/package/yuxi/services/agent_run_service.py @@ -12,6 +12,7 @@ from fastapi import HTTPException from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from yuxi.agents.buildin import agent_manager +from yuxi.repositories.agent_config_repository import AgentConfigRepository from yuxi.repositories.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository from yuxi.repositories.conversation_repository import ConversationRepository from yuxi.services.run_queue_service import ( @@ -51,9 +52,10 @@ def _format_sse(data: dict, event: str | None = None) -> str: async def create_agent_run_view( *, - agent_id: str, query: str, - config: dict, + agent_config_id: int, + thread_id: str, + meta: dict, image_content: str | None, current_user_id: str, db: AsyncSession, @@ -61,19 +63,28 @@ async def create_agent_run_view( if not query: raise HTTPException(status_code=422, detail="query 不能为空") + if not thread_id: + raise HTTPException(status_code=422, detail="thread_id 不能为空") + + config_repo = AgentConfigRepository(db) + config_item = await config_repo.get_by_id(config_id=int(agent_config_id)) + if config_item is None: + raise HTTPException(status_code=404, detail="配置不存在") + + agent_id = config_item.agent_id if not agent_manager.get_agent(agent_id): raise HTTPException(status_code=404, detail=f"智能体 {agent_id} 不存在") - thread_id = (config or {}).get("thread_id") - if not thread_id: - raise HTTPException(status_code=422, detail="config.thread_id 不能为空") - conv_repo = ConversationRepository(db) 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="对话线程不存在") - request_id = str((config or {}).get("request_id") or uuid.uuid4()) + request_id = str((meta or {}).get("request_id") or uuid.uuid4()) + config = { + "thread_id": thread_id, + "agent_config_id": int(agent_config_id), + } run_repo = AgentRunRepository(db) existing = await run_repo.get_run_by_request_id(request_id) if existing and existing.user_id == str(current_user_id): diff --git a/backend/package/yuxi/services/chat_service.py b/backend/package/yuxi/services/chat_service.py index fcc868fa..9e9b8675 100644 --- a/backend/package/yuxi/services/chat_service.py +++ b/backend/package/yuxi/services/chat_service.py @@ -298,6 +298,35 @@ def _ensure_full_msg(full_msg: AIMessage | None, accumulated_content: list[str]) return full_msg +def _extract_ai_message(messages: list[Any] | None) -> AIMessage | None: + """从消息列表中提取最后一条 AIMessage。""" + if not isinstance(messages, list): + return None + + for msg in reversed(messages): + if isinstance(msg, AIMessage): + return msg + + msg_dict = msg.model_dump() if hasattr(msg, "model_dump") else {} + if msg_dict.get("type") == "ai": + content = msg_dict.get("content", "") + return msg if hasattr(msg, "content") else AIMessage(content=content) + + return None + + +async def get_agent_config_by_id(db, user: User, agent_config_id: int): + """按配置 ID 解析 AgentConfig 记录。""" + department_id = user.department_id + + agent_config_repo = AgentConfigRepository(db) + config_item = await agent_config_repo.get_by_id(config_id=int(agent_config_id)) + if config_item is None or config_item.department_id != department_id: + raise ValueError("配置不存在") + + return config_item + + async def _resolve_agent_config(db, agent_id: str, user: User, agent_config_id): """解析 agent_config,返回 agent_config""" department_id = user.department_id @@ -305,8 +334,8 @@ async def _resolve_agent_config(db, agent_id: str, user: User, agent_config_id): agent_config_repo = AgentConfigRepository(db) config_item = None if agent_config_id is not None: - config_item = await agent_config_repo.get_by_id(config_id=int(agent_config_id)) - if config_item is not None and (config_item.department_id != department_id or config_item.agent_id != agent_id): + config_item = await get_agent_config_by_id(db, user, int(agent_config_id)) + if config_item.agent_id != agent_id: config_item = None if config_item is None: @@ -344,9 +373,9 @@ async def check_and_handle_interrupts( async def agent_chat( *, - agent_id: str, query: str, - config: dict, + agent_config_id: int, + thread_id: str | None, meta: dict, image_content: str | None, current_user, @@ -367,13 +396,6 @@ async def agent_chat( human_message = HumanMessage(content=query) message_type = "text" - init_msg = {"role": "user", "content": query, "type": "human"} - if image_content: - init_msg["message_type"] = "multimodal_image" - init_msg["image_content"] = image_content - else: - init_msg["message_type"] = "text" - if conf.enable_content_guard and await content_guard.check(query): return { "status": "error", @@ -382,6 +404,42 @@ async def agent_chat( "request_id": meta.get("request_id"), } + if not current_user.department_id: + return { + "status": "error", + "error_type": "invalid_config", + "error_message": "当前用户未绑定部门", + "request_id": meta.get("request_id"), + } + + user_id = str(current_user.id) + meta = dict(meta or {}) + if "request_id" not in meta or not meta.get("request_id"): + logger.warning("请求缺少 request_id,已自动生成一个新的 request_id") + meta["request_id"] = str(uuid.uuid4()) + + try: + config_item = await get_agent_config_by_id(db, current_user, agent_config_id) + except ValueError as e: + return { + "status": "error", + "error_type": "invalid_config", + "error_message": str(e), + "request_id": meta.get("request_id"), + } + + agent_id = config_item.agent_id + meta.update( + { + "query": query, + "agent_id": agent_id, + "server_model_name": agent_id, + "thread_id": thread_id, + "user_id": current_user.id, + "has_image": bool(image_content), + } + ) + try: agent = agent_manager.get_agent(agent_id) except Exception as e: @@ -394,20 +452,9 @@ async def agent_chat( } messages = [human_message] + agent_config = (config_item.config_json or {}).get("context", {}) - user_id = str(current_user.id) - agent_config_id = config.get("agent_config_id") - try: - agent_config = await _resolve_agent_config(db, agent_id, current_user, agent_config_id) - except ValueError as e: - return { - "status": "error", - "error_type": "invalid_config", - "error_message": str(e), - "request_id": meta.get("request_id"), - } - - if not (thread_id := config.get("thread_id")): + if not thread_id: thread_id = str(uuid.uuid4()) logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}") @@ -429,20 +476,18 @@ async def agent_chat( logger.error(f"Error saving user message: {e}") langgraph_config = {"configurable": {"thread_id": thread_id, "user_id": user_id}} + invoke_result = await agent.invoke_messages(messages, input_context=input_context) + full_msg = _extract_ai_message(invoke_result.get("messages") if isinstance(invoke_result, dict) else None) - full_msg = None - accumulated_content: list[str] = [] + if full_msg is None: + try: + graph = await agent.get_graph() + state = await graph.aget_state(langgraph_config) + full_msg = _extract_ai_message(getattr(state, "values", {}).get("messages", [])) if state else None + except Exception: + full_msg = None - async for msg, metadata in agent.stream_messages(messages, input_context=input_context): - if isinstance(msg, AIMessageChunk): - accumulated_content.append(msg.content) - else: - msg_dict = msg.model_dump() if hasattr(msg, "model_dump") else {} - if msg_dict.get("type") == "ai": - full_msg = msg - - full_msg = _ensure_full_msg(full_msg, accumulated_content) - full_content = "".join(accumulated_content) if accumulated_content else (full_msg.content if full_msg else "") + full_content = full_msg.content if full_msg else "" if conf.enable_content_guard and await content_guard.check(full_content): await save_partial_message(conv_repo, thread_id, full_msg, "content_guard_blocked") @@ -488,9 +533,9 @@ async def agent_chat( async def stream_agent_chat( *, - agent_id: str, query: str, - config: dict, + agent_config_id: int, + thread_id: str | None, meta: dict, image_content: str | None, current_user, @@ -533,6 +578,34 @@ async def stream_agent_chat( ) return + if not current_user.department_id: + yield make_chunk(status="error", error_type="invalid_config", error_message="当前用户未绑定部门", meta=meta) + return + + meta = dict(meta or {}) + if "request_id" not in meta or not meta.get("request_id"): + logger.warning("请求缺少 request_id,已自动生成一个新的 request_id") + meta["request_id"] = str(uuid.uuid4()) + + user_id = str(current_user.id) + try: + config_item = await get_agent_config_by_id(db, current_user, agent_config_id) + except ValueError as e: + yield make_chunk(status="error", error_type="invalid_config", error_message=str(e), meta=meta) + return + + agent_id = config_item.agent_id + meta.update( + { + "query": query, + "agent_id": agent_id, + "server_model_name": agent_id, + "thread_id": thread_id, + "user_id": current_user.id, + "has_image": bool(image_content), + } + ) + try: agent = agent_manager.get_agent(agent_id) except Exception as e: @@ -546,16 +619,9 @@ async def stream_agent_chat( return messages = [human_message] + agent_config = (config_item.config_json or {}).get("context", {}) - user_id = str(current_user.id) - agent_config_id = config.get("agent_config_id") - try: - agent_config = await _resolve_agent_config(db, agent_id, current_user, agent_config_id) - except ValueError as e: - yield make_chunk(status="error", error_type="invalid_config", error_message=str(e), meta=meta) - return - - if not (thread_id := config.get("thread_id")): + if not thread_id: thread_id = str(uuid.uuid4()) logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}") diff --git a/backend/package/yuxi/services/run_worker.py b/backend/package/yuxi/services/run_worker.py index 5a5fa05e..238a9347 100644 --- a/backend/package/yuxi/services/run_worker.py +++ b/backend/package/yuxi/services/run_worker.py @@ -235,9 +235,9 @@ async def process_agent_run(ctx, run_id: str): try: async with pg_manager.get_async_session_context() as db: stream = stream_agent_chat( - agent_id=agent_id, query=query, - config=config, + agent_config_id=config.get("agent_config_id"), + thread_id=config.get("thread_id"), meta=meta, image_content=image_content, current_user=user, diff --git a/backend/server/routers/chat_router.py b/backend/server/routers/chat_router.py index 213a9fe6..42d894f1 100644 --- a/backend/server/routers/chat_router.py +++ b/backend/server/routers/chat_router.py @@ -14,7 +14,7 @@ from server.utils.auth_middleware import get_db, get_required_user from yuxi import config as conf from yuxi.agents.buildin import agent_manager from yuxi.models import select_model -from yuxi.services.chat_service import get_agent_state_view, stream_agent_chat, stream_agent_resume, agent_chat +from yuxi.services.chat_service import agent_chat, get_agent_state_view, stream_agent_chat, stream_agent_resume from yuxi.services.agent_run_service import ( cancel_agent_run_view, create_agent_run_view, @@ -43,6 +43,8 @@ from yuxi.utils.logging_config import logger from yuxi.utils.image_processor import process_uploaded_image +# TODO:当前文件的功能过于庞杂,路由标签混乱 + # 图片上传响应模型 class ImageUploadResponse(BaseModel): success: bool @@ -76,9 +78,19 @@ class AgentConfigUpdate(BaseModel): class AgentRunCreate(BaseModel): - query: str - config: dict = Field(default_factory=dict) - image_content: str | None = None + query: str = Field(..., description="用户输入的问题") + agent_config_id: int = Field(..., description="智能体配置 ID,后端将据此解析 agent_id 和运行时 context") + thread_id: str = Field(..., description="会话线程 ID") + meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id") + image_content: str | None = Field(None, description="可选,base64 图片内容") + + +class AgentChatRequest(BaseModel): + query: str = Field(..., description="用户输入的问题") + agent_config_id: int = Field(..., description="智能体配置 ID,后端将据此解析 agent_id 和运行时 context") + thread_id: str | None = Field(None, description="可选,会话线程 ID;不传则自动创建") + meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id") + image_content: str | None = Field(None, description="可选,base64 图片内容") chat = APIRouter(prefix="/chat", tags=["chat"]) @@ -218,7 +230,6 @@ async def list_agent_configs( if not items: await repo.get_or_create_default( department_id=current_user.department_id, - agent_id=agent_id, created_by=str(current_user.id), ) items = await repo.list_by_department_agent(department_id=current_user.department_id, agent_id=agent_id) @@ -275,7 +286,6 @@ async def create_agent_config_profile( repo = AgentConfigRepository(db) item = await repo.create( department_id=current_user.department_id, - agent_id=agent_id, name=payload.name, description=payload.description, icon=payload.icon, @@ -367,44 +377,28 @@ async def delete_agent_config_profile( return {"success": True} -@chat.post("/agent/{agent_id}") +@chat.post("/agent") async def chat_agent( - agent_id: str, - query: str = Body(...), - config: dict = Body({}), - meta: dict = Body({}), - image_content: str | None = Body(None), + payload: AgentChatRequest, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): """使用特定智能体进行对话(需要登录)""" - logger.info(f"agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}") - logger.info(f"image_content present: {image_content is not None}") - if image_content: - logger.info(f"image_content length: {len(image_content)}") - logger.info(f"image_content preview: {image_content[:50]}...") + logger.info(f"query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.meta}") - # 确保 request_id 存在 - if "request_id" not in meta or not meta.get("request_id"): - meta["request_id"] = str(uuid.uuid4()) + # 查看图片内容 + logger.info(f"image_content present: {payload.image_content is not None}") + if payload.image_content: + logger.info(f"image_content length: {len(payload.image_content)}") + logger.info(f"image_content preview: {payload.image_content[:50]}...") - meta.update( - { - "query": query, - "agent_id": agent_id, - "server_model_name": config.get("model", agent_id), - "thread_id": config.get("thread_id"), - "user_id": current_user.id, - "has_image": bool(image_content), - } - ) return StreamingResponse( stream_agent_chat( - agent_id=agent_id, - query=query, - config=config, - meta=meta, - image_content=image_content, + query=payload.query, + agent_config_id=payload.agent_config_id, + thread_id=payload.thread_id, + meta=dict(payload.meta or {}), + image_content=payload.image_content, current_user=current_user, db=db, ), @@ -412,60 +406,41 @@ async def chat_agent( ) -@chat.post("/agent/{agent_id}/sync") +@chat.post("/agent/sync") async def chat_agent_sync( - agent_id: str, - query: str = Body(...), - config: dict = Body({}), - meta: dict = Body({}), - image_content: str | None = Body(None), + payload: AgentChatRequest, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): """使用特定智能体进行非流式对话(需要登录)""" - logger.info(f"[sync] agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}") - logger.info(f"[sync] image_content present: {image_content is not None}") - if image_content: - logger.info(f"[sync] image_content length: {len(image_content)}") - - # 确保 request_id 存在 - if "request_id" not in meta or not meta.get("request_id"): - meta["request_id"] = str(uuid.uuid4()) - - meta.update( - { - "query": query, - "agent_id": agent_id, - "server_model_name": config.get("model", agent_id), - "thread_id": config.get("thread_id"), - "user_id": current_user.id, - "has_image": bool(image_content), - } - ) + logger.info(f"[sync] query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.meta}") + logger.info(f"[sync] image_content present: {payload.image_content is not None}") + if payload.image_content: + logger.info(f"[sync] image_content length: {len(payload.image_content)}") return await agent_chat( - agent_id=agent_id, - query=query, - config=config, - meta=meta, - image_content=image_content, + query=payload.query, + agent_config_id=payload.agent_config_id, + thread_id=payload.thread_id, + meta=dict(payload.meta or {}), + image_content=payload.image_content, current_user=current_user, db=db, ) -@chat.post("/agent/{agent_id}/runs") +@chat.post("/runs") async def create_agent_run( - agent_id: str, payload: AgentRunCreate, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db), ): """创建异步 run 任务并入队(需要登录)""" return await create_agent_run_view( - agent_id=agent_id, query=payload.query, - config=payload.config or {}, + agent_config_id=payload.agent_config_id, + thread_id=payload.thread_id, + meta=dict(payload.meta or {}), image_content=payload.image_content, current_user_id=str(current_user.id), db=db, @@ -627,7 +602,6 @@ async def resume_agent_chat( meta["request_id"] = str(uuid.uuid4()) return StreamingResponse( stream_agent_resume( - agent_id=agent_id, thread_id=thread_id, resume_input=resume_input, meta=meta, diff --git a/backend/test/api/test_agent_bubble_sort_e2e.py b/backend/test/api/test_agent_bubble_sort_e2e.py index 37b150ed..8dfaf3a6 100644 --- a/backend/test/api/test_agent_bubble_sort_e2e.py +++ b/backend/test/api/test_agent_bubble_sort_e2e.py @@ -49,6 +49,7 @@ class AgentBubbleSortE2ETester: self.client = httpx.AsyncClient(base_url=API_BASE_URL, timeout=TIMEOUT, follow_redirects=True) self.headers: dict[str, str] | None = None self.agent_id: str | None = None + self.agent_config_id: int | None = None self.thread_id: str | None = None async def close(self): @@ -98,14 +99,29 @@ class AgentBubbleSortE2ETester: if not self.thread_id: raise RuntimeError(f"thread id missing: {payload}") + async def pick_agent_config(self) -> None: + assert self.headers and self.agent_id + response = await self.client.get(f"/api/chat/agent/{self.agent_id}/configs", headers=self.headers) + if response.status_code != 200: + raise RuntimeError(f"list configs failed: {response.status_code} {response.text}") + configs = response.json().get("configs") or [] + if not configs: + raise RuntimeError("no available agent configs") + config_id = configs[0].get("id") + if not config_id: + raise RuntimeError(f"config id missing: {response.text}") + self.agent_config_id = int(config_id) + async def create_run(self, query: str) -> str: - assert self.headers and self.agent_id and self.thread_id + assert self.headers and self.agent_config_id and self.thread_id request_id = f"agent-bubble-sort-{uuid.uuid4()}" response = await self.client.post( - f"/api/chat/agent/{self.agent_id}/runs", + "/api/chat/runs", json={ "query": query, - "config": {"thread_id": self.thread_id, "request_id": request_id}, + "agent_config_id": self.agent_config_id, + "thread_id": self.thread_id, + "meta": {"request_id": request_id}, }, headers=self.headers, ) @@ -166,6 +182,7 @@ class AgentBubbleSortE2ETester: async def run_case(self) -> None: await self.login() await self.pick_agent() + await self.pick_agent_config() await self.create_thread() query = ( diff --git a/backend/test/api/test_apikey_router.py b/backend/test/api/test_apikey_router.py index 41964a42..f7064a79 100644 --- a/backend/test/api/test_apikey_router.py +++ b/backend/test/api/test_apikey_router.py @@ -9,6 +9,26 @@ import pytest pytestmark = [pytest.mark.asyncio, pytest.mark.integration] +async def _get_default_agent_config_id(test_client, headers): + agent_response = await test_client.get("/api/chat/default_agent", headers=headers) + assert agent_response.status_code == 200 + agent_id = agent_response.json().get("default_agent_id") + if not agent_id: + pytest.skip("No default agent configured") + + configs_response = await test_client.get(f"/api/chat/agent/{agent_id}/configs", headers=headers) + assert configs_response.status_code == 200, configs_response.text + configs = configs_response.json().get("configs", []) + if not configs: + pytest.skip("No configs found for default agent") + + config_id = configs[0].get("id") + if not config_id: + pytest.skip("Agent config payload missing id field.") + + return agent_id, config_id + + async def test_list_api_keys_requires_auth(test_client): """List API keys should require authentication.""" response = await test_client.get("/api/apikey/") @@ -46,9 +66,7 @@ async def test_create_api_key(test_client, admin_headers): async def test_get_api_key(test_client, admin_headers): """Admin should be able to get a single API key.""" # First create a key - create_response = await test_client.post( - "/api/apikey/", json={"name": "Get Test"}, headers=admin_headers - ) + create_response = await test_client.post("/api/apikey/", json={"name": "Get Test"}, headers=admin_headers) assert create_response.status_code == 200 created = create_response.json()["api_key"] @@ -63,9 +81,7 @@ async def test_get_api_key(test_client, admin_headers): async def test_update_api_key(test_client, admin_headers): """Admin should be able to update an API key.""" # Create a key - create_response = await test_client.post( - "/api/apikey/", json={"name": "Update Test"}, headers=admin_headers - ) + create_response = await test_client.post("/api/apikey/", json={"name": "Update Test"}, headers=admin_headers) assert create_response.status_code == 200 created = create_response.json()["api_key"] @@ -84,9 +100,7 @@ async def test_update_api_key(test_client, admin_headers): async def test_delete_api_key(test_client, admin_headers): """Admin should be able to delete an API key.""" # Create a key - create_response = await test_client.post( - "/api/apikey/", json={"name": "Delete Test"}, headers=admin_headers - ) + create_response = await test_client.post("/api/apikey/", json={"name": "Delete Test"}, headers=admin_headers) assert create_response.status_code == 200 created = create_response.json()["api_key"] @@ -103,9 +117,7 @@ async def test_delete_api_key(test_client, admin_headers): async def test_regenerate_api_key(test_client, admin_headers): """Admin should be able to regenerate an API key.""" # Create a key - create_response = await test_client.post( - "/api/apikey/", json={"name": "Regenerate Test"}, headers=admin_headers - ) + create_response = await test_client.post("/api/apikey/", json={"name": "Regenerate Test"}, headers=admin_headers) assert create_response.status_code == 200 original_secret = create_response.json()["secret"] created = create_response.json()["api_key"] @@ -122,26 +134,19 @@ async def test_regenerate_api_key(test_client, admin_headers): async def test_api_key_auth_chat_endpoint(test_client, admin_headers): """Test that API Key can be used to authenticate to chat endpoint via Bearer token.""" # Create an API key - create_response = await test_client.post( - "/api/apikey/", json={"name": "Chat Auth Test"}, headers=admin_headers - ) + create_response = await test_client.post("/api/apikey/", json={"name": "Chat Auth Test"}, headers=admin_headers) assert create_response.status_code == 200 api_key_secret = create_response.json()["secret"] created = create_response.json()["api_key"] try: - # Get default agent - agent_response = await test_client.get("/api/chat/default_agent", headers=admin_headers) - assert agent_response.status_code == 200 - agent_id = agent_response.json().get("default_agent_id") - if not agent_id: - pytest.skip("No default agent configured") + _, agent_config_id = await _get_default_agent_config_id(test_client, admin_headers) # Call chat endpoint with API Key using Bearer format (streaming response) async with test_client.stream( "POST", - f"/api/chat/agent/{agent_id}", - json={"query": "Hello"}, + "/api/chat/agent", + json={"query": "Hello", "agent_config_id": agent_config_id}, headers={"Authorization": f"Bearer {api_key_secret}"}, ) as response: assert response.status_code == 200, response.text @@ -155,8 +160,8 @@ async def test_api_key_auth_requires_valid_key(test_client): """Test that invalid API Key is rejected.""" # Call chat endpoint with invalid API Key response = await test_client.post( - "/api/chat/agent/ChatbotAgent", - json={"query": "Hello"}, + "/api/chat/agent", + json={"query": "Hello", "agent_config_id": 1}, headers={"Authorization": "Bearer yxkey_invalid_key_that_does_not_exist"}, ) assert response.status_code == 401, response.text @@ -165,9 +170,7 @@ async def test_api_key_auth_requires_valid_key(test_client): async def test_api_key_auth_requires_bearer_prefix(test_client, admin_headers): """Test that API Key must be prefixed with 'Bearer '.""" # Create an API key - admin_response = await test_client.post( - "/api/apikey/", json={"name": "Prefix Test"}, headers=admin_headers - ) + admin_response = await test_client.post("/api/apikey/", json={"name": "Prefix Test"}, headers=admin_headers) assert admin_response.status_code == 200 api_key_secret = admin_response.json()["secret"] created = admin_response.json()["api_key"] @@ -175,8 +178,8 @@ async def test_api_key_auth_requires_bearer_prefix(test_client, admin_headers): try: # Call without Bearer prefix should fail response = await test_client.post( - "/api/chat/agent/ChatbotAgent", - json={"query": "Hello"}, + "/api/chat/agent", + json={"query": "Hello", "agent_config_id": 1}, headers={"Authorization": api_key_secret}, # Missing "Bearer " prefix ) assert response.status_code == 401, response.text @@ -187,18 +190,13 @@ async def test_api_key_auth_requires_bearer_prefix(test_client, admin_headers): async def test_jwt_still_works_after_apikey_auth(test_client, admin_headers): """Test that JWT Bearer tokens still work after API Key changes.""" - # Get default agent - agent_response = await test_client.get("/api/chat/default_agent", headers=admin_headers) - assert agent_response.status_code == 200 - agent_id = agent_response.json().get("default_agent_id") - if not agent_id: - pytest.skip("No default agent configured") + _, agent_config_id = await _get_default_agent_config_id(test_client, admin_headers) # Call chat with JWT Bearer token (admin_headers) - streaming response async with test_client.stream( "POST", - f"/api/chat/agent/{agent_id}", - json={"query": "Hello"}, + "/api/chat/agent", + json={"query": "Hello", "agent_config_id": agent_config_id}, headers=admin_headers, ) as response: assert response.status_code == 200, response.text @@ -208,9 +206,7 @@ async def test_jwt_still_works_after_apikey_auth(test_client, admin_headers): async def test_api_key_auto_binds_to_current_user(test_client, admin_headers): """Test that API Key created without user_id is auto-bound to creator.""" # Create API key as admin - create_response = await test_client.post( - "/api/apikey/", json={"name": "Auto Bind Test"}, headers=admin_headers - ) + create_response = await test_client.post("/api/apikey/", json={"name": "Auto Bind Test"}, headers=admin_headers) assert create_response.status_code == 200 created = create_response.json()["api_key"] @@ -220,15 +216,14 @@ async def test_api_key_auto_binds_to_current_user(test_client, admin_headers): # Verify the key can be used for auth api_key_secret = create_response.json()["secret"] + _, agent_config_id = await _get_default_agent_config_id(test_client, admin_headers) async with test_client.stream( "POST", - "/api/chat/agent/ChatbotAgent", - json={"query": "Hello"}, + "/api/chat/agent", + json={"query": "Hello", "agent_config_id": agent_config_id}, headers={"Authorization": f"Bearer {api_key_secret}"}, ) as response: assert response.status_code == 200, response.text finally: # Cleanup: delete the test API key await test_client.delete(f"/api/apikey/{created['id']}", headers=admin_headers) - - diff --git a/backend/test/api/test_attachment_and_agent_state.py b/backend/test/api/test_attachment_and_agent_state.py index 33169765..3af434e8 100644 --- a/backend/test/api/test_attachment_and_agent_state.py +++ b/backend/test/api/test_attachment_and_agent_state.py @@ -183,6 +183,25 @@ class APITester: print(f" ✗ 获取失败: {response.status_code} - {response.text}") return None + async def get_agent_config_id(self, agent_id: str) -> int | None: + """获取指定 Agent 的一个配置 ID""" + async with self._client() as client: + response = await client.get( + f"{self.base_url}/api/chat/agent/{agent_id}/configs", + headers=self.headers, + ) + + if response.status_code != 200: + print(f" ✗ 获取 Agent 配置失败: {response.status_code} - {response.text}") + return None + + configs = response.json().get("configs", []) + if not configs: + print(" ✗ 当前 Agent 没有可用配置") + return None + + return configs[0].get("id") + async def send_chat_message(self, agent_id: str, thread_id: str, query: str) -> bool: """发送聊天消息(流式)""" print(f"\n{'=' * 60}") @@ -191,11 +210,19 @@ class APITester: print(f" Query: {query}") print(f" Thread ID: {thread_id}") + agent_config_id = await self.get_agent_config_id(agent_id) + if not agent_config_id: + return False + async with self._client(timeout=120.0) as client: async with client.stream( "POST", - f"{self.base_url}/api/chat/agent/{agent_id}", - json={"query": query, "config": {"thread_id": thread_id}}, + f"{self.base_url}/api/chat/agent", + json={ + "query": query, + "agent_config_id": agent_config_id, + "thread_id": thread_id, + }, headers=self.headers, ) as response: print(f"\n 响应状态: {response.status_code}") diff --git a/backend/test/api/test_chat_agent_sync.py b/backend/test/api/test_chat_agent_sync.py index c0aa6c1a..83b77272 100644 --- a/backend/test/api/test_chat_agent_sync.py +++ b/backend/test/api/test_chat_agent_sync.py @@ -9,16 +9,8 @@ import pytest pytestmark = [pytest.mark.asyncio, pytest.mark.integration] -async def test_chat_agent_sync_requires_authentication(test_client): - """非流式端点需要认证""" - response = await test_client.post("/api/chat/agent/test_agent/sync", json={"query": "hello"}) - assert response.status_code == 401 - - -async def test_chat_agent_sync_basic_conversation(test_client, admin_headers): - """测试非流式对话基本功能""" - # 先获取可用智能体 - agents_response = await test_client.get("/api/chat/agent", headers=admin_headers) +async def _get_agent_and_config_id(test_client, headers): + agents_response = await test_client.get("/api/chat/agent", headers=headers) assert agents_response.status_code == 200, agents_response.text agents = agents_response.json().get("agents", []) @@ -29,10 +21,33 @@ async def test_chat_agent_sync_basic_conversation(test_client, admin_headers): if not agent_id: pytest.skip("Agent payload missing id field.") + configs_response = await test_client.get(f"/api/chat/agent/{agent_id}/configs", headers=headers) + assert configs_response.status_code == 200, configs_response.text + configs = configs_response.json().get("configs", []) + if not configs: + pytest.skip("No agent configs are available in the system.") + + config_id = configs[0].get("id") + if not config_id: + pytest.skip("Agent config payload missing id field.") + + return agent_id, config_id + + +async def test_chat_agent_sync_requires_authentication(test_client): + """非流式端点需要认证""" + response = await test_client.post("/api/chat/agent/sync", json={"query": "hello", "agent_config_id": 1}) + assert response.status_code == 401 + + +async def test_chat_agent_sync_basic_conversation(test_client, admin_headers): + """测试非流式对话基本功能""" + _, agent_config_id = await _get_agent_and_config_id(test_client, admin_headers) + # 调用非流式端点 response = await test_client.post( - f"/api/chat/agent/{agent_id}/sync", - json={"query": "Hello, say 'Hi' back to me"}, + "/api/chat/agent/sync", + json={"query": "Hello, say 'Hi' back to me", "agent_config_id": agent_config_id}, headers=admin_headers, ) @@ -58,26 +73,18 @@ async def test_chat_agent_sync_basic_conversation(test_client, admin_headers): async def test_chat_agent_sync_with_thread_id(test_client, admin_headers): """测试非流式对话指定 thread_id""" - # 获取可用智能体 - agents_response = await test_client.get("/api/chat/agent", headers=admin_headers) - assert agents_response.status_code == 200, agents_response.text - agents = agents_response.json().get("agents", []) - - if not agents: - pytest.skip("No agents are registered in the system.") - - agent_id = agents[0].get("id") - if not agent_id: - pytest.skip("Agent payload missing id field.") + _, agent_config_id = await _get_agent_and_config_id(test_client, admin_headers) import uuid + thread_id = str(uuid.uuid4()) response = await test_client.post( - f"/api/chat/agent/{agent_id}/sync", + "/api/chat/agent/sync", json={ "query": "Hello", - "config": {"thread_id": thread_id}, + "agent_config_id": agent_config_id, + "thread_id": thread_id, }, headers=admin_headers, ) @@ -87,29 +94,24 @@ async def test_chat_agent_sync_with_thread_id(test_client, admin_headers): # 验证 thread_id 是否保持一致 if payload["status"] == "finished": - assert payload.get("thread_id") == thread_id, f"thread_id mismatch: expected {thread_id}, got {payload.get('thread_id')}" + assert payload.get("thread_id") == thread_id, ( + f"thread_id mismatch: expected {thread_id}, got {payload.get('thread_id')}" + ) async def test_chat_agent_sync_with_meta(test_client, admin_headers): """测试非流式对话传递 meta 参数""" - agents_response = await test_client.get("/api/chat/agent", headers=admin_headers) - assert agents_response.status_code == 200, agents_response.text - agents = agents_response.json().get("agents", []) - - if not agents: - pytest.skip("No agents are registered in the system.") - - agent_id = agents[0].get("id") - if not agent_id: - pytest.skip("Agent payload missing id field.") + _, agent_config_id = await _get_agent_and_config_id(test_client, admin_headers) import uuid + request_id = str(uuid.uuid4()) response = await test_client.post( - f"/api/chat/agent/{agent_id}/sync", + "/api/chat/agent/sync", json={ "query": "Hello", + "agent_config_id": agent_config_id, "meta": {"request_id": request_id}, }, headers=admin_headers, @@ -119,35 +121,29 @@ async def test_chat_agent_sync_with_meta(test_client, admin_headers): payload = response.json() # 验证 request_id 是否保持一致 - assert payload.get("request_id") == request_id, f"request_id mismatch: expected {request_id}, got {payload.get('request_id')}" + assert payload.get("request_id") == request_id, ( + f"request_id mismatch: expected {request_id}, got {payload.get('request_id')}" + ) async def test_chat_agent_sync_vs_streaming_consistency(test_client, admin_headers): """对比测试:非流式与流式端点行为一致性""" - # 获取可用智能体 - agents_response = await test_client.get("/api/chat/agent", headers=admin_headers) - assert agents_response.status_code == 200, agents_response.text - agents = agents_response.json().get("agents", []) - - if not agents: - pytest.skip("No agents are registered in the system.") - - agent_id = agents[0].get("id") - if not agent_id: - pytest.skip("Agent payload missing id field.") + _, agent_config_id = await _get_agent_and_config_id(test_client, admin_headers) query = "What is 1+1?" # 调用流式端点 import uuid + thread_id = str(uuid.uuid4()) request_id = str(uuid.uuid4()) streaming_response = await test_client.post( - f"/api/chat/agent/{agent_id}", + "/api/chat/agent", json={ "query": query, - "config": {"thread_id": thread_id}, + "agent_config_id": agent_config_id, + "thread_id": thread_id, "meta": {"request_id": request_id}, }, headers=admin_headers, @@ -160,11 +156,12 @@ async def test_chat_agent_sync_vs_streaming_consistency(test_client, admin_heade async for line in streaming_response.aiter_lines(): if line: import json as json_lib + try: data = json_lib.loads(line) if data.get("response"): streaming_content.append(data["response"]) - except: + except Exception: pass # 调用非流式端点 @@ -172,10 +169,11 @@ async def test_chat_agent_sync_vs_streaming_consistency(test_client, admin_heade request_id2 = str(uuid.uuid4()) sync_response = await test_client.post( - f"/api/chat/agent/{agent_id}/sync", + "/api/chat/agent/sync", json={ "query": query, - "config": {"thread_id": thread_id2}, + "agent_config_id": agent_config_id, + "thread_id": thread_id2, "meta": {"request_id": request_id2}, }, headers=admin_headers, diff --git a/backend/test/test_agent_run_service.py b/backend/test/test_agent_run_service.py index 0939c4e2..419fc725 100644 --- a/backend/test/test_agent_run_service.py +++ b/backend/test/test_agent_run_service.py @@ -9,6 +9,14 @@ from sqlalchemy.exc import IntegrityError import yuxi.services.agent_run_service as agent_run_service +class FakeConfigRepo: + def __init__(self, db_session): + self.db = db_session + + async def get_by_id(self, config_id: int): + return SimpleNamespace(id=config_id, agent_id="ChatbotAgent", department_id=1) + + @pytest.mark.asyncio async def test_stream_agent_run_events_emits_error_and_close_on_db_error(monkeypatch: pytest.MonkeyPatch): @asynccontextmanager @@ -134,7 +142,7 @@ async def test_create_agent_run_commits_before_enqueue(monkeypatch: pytest.Monke async def get_conversation_by_thread_id(self, thread_id: str): del thread_id - return SimpleNamespace(user_id="1", status="active") + return SimpleNamespace(user_id="1", status="active", department_id=1) class Queue: async def enqueue_job(self, job_name: str, run_id: str, _job_id: str): @@ -148,14 +156,16 @@ async def test_create_agent_run_commits_before_enqueue(monkeypatch: pytest.Monke return Queue() monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda agent_id: object()) + monkeypatch.setattr(agent_run_service, "AgentConfigRepository", FakeConfigRepo) monkeypatch.setattr(agent_run_service, "ConversationRepository", ConvRepo) monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo) monkeypatch.setattr(agent_run_service, "get_arq_pool", fake_get_arq_pool) result = await agent_run_service.create_agent_run_view( - agent_id="ChatbotAgent", query="hello", - config={"thread_id": "thread-1", "request_id": "req-1"}, + agent_config_id=1, + thread_id="thread-1", + meta={"request_id": "req-1"}, image_content=None, current_user_id="1", db=db, @@ -217,20 +227,22 @@ async def test_create_agent_run_handles_integrity_error_with_same_user_existing( async def get_conversation_by_thread_id(self, thread_id: str): del thread_id - return SimpleNamespace(user_id="1", status="active") + return SimpleNamespace(user_id="1", status="active", department_id=1) async def fake_get_arq_pool(): raise AssertionError("should not enqueue on integrity fallback") monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda agent_id: object()) + monkeypatch.setattr(agent_run_service, "AgentConfigRepository", FakeConfigRepo) monkeypatch.setattr(agent_run_service, "ConversationRepository", ConvRepo) monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo) monkeypatch.setattr(agent_run_service, "get_arq_pool", fake_get_arq_pool) result = await agent_run_service.create_agent_run_view( - agent_id="ChatbotAgent", query="hello", - config={"thread_id": "thread-1", "request_id": "req-1"}, + agent_config_id=1, + thread_id="thread-1", + meta={"request_id": "req-1"}, image_content=None, current_user_id="1", db=db, @@ -288,17 +300,19 @@ async def test_create_agent_run_integrity_error_returns_409_for_other_user(monke async def get_conversation_by_thread_id(self, thread_id: str): del thread_id - return SimpleNamespace(user_id="1", status="active") + return SimpleNamespace(user_id="1", status="active", department_id=1) monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda agent_id: object()) + monkeypatch.setattr(agent_run_service, "AgentConfigRepository", FakeConfigRepo) monkeypatch.setattr(agent_run_service, "ConversationRepository", ConvRepo) monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo) with pytest.raises(agent_run_service.HTTPException) as exc: await agent_run_service.create_agent_run_view( - agent_id="ChatbotAgent", query="hello", - config={"thread_id": "thread-1", "request_id": "req-1"}, + agent_config_id=1, + thread_id="thread-1", + meta={"request_id": "req-1"}, image_content=None, current_user_id="1", db=db, diff --git a/backend/test/test_chat_service_sync.py b/backend/test/test_chat_service_sync.py new file mode 100644 index 00000000..92d3b6ac --- /dev/null +++ b/backend/test/test_chat_service_sync.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +from langchain.messages import AIMessage, HumanMessage + +from yuxi.services import chat_service as svc + + +class _FakeConvRepo: + def __init__(self, _db): + self.saved_messages: list[dict] = [] + + async def add_message_by_thread_id( + self, + *, + thread_id: str, + role: str, + content: str, + message_type: str = "text", + extra_metadata: dict | None = None, + image_content: str | None = None, + ): + self.saved_messages.append( + { + "thread_id": thread_id, + "role": role, + "content": content, + "message_type": message_type, + "extra_metadata": extra_metadata, + "image_content": image_content, + } + ) + return SimpleNamespace(id=1) + + +@pytest.mark.asyncio +async def test_agent_chat_uses_invoke_messages_and_persists_langgraph_state(monkeypatch: pytest.MonkeyPatch): + calls: dict[str, object] = {} + + class FakeGraph: + async def aget_state(self, config): + calls["state_config"] = config + return SimpleNamespace(values={"messages": [AIMessage(content="Hi from graph")], "todos": ["todo-1"]}) + + class FakeAgent: + async def invoke_messages(self, messages, input_context=None, **kwargs): + calls["invoke_messages"] = messages + calls["invoke_input_context"] = input_context + return {"messages": [messages[0], AIMessage(content="Hi from invoke")]} + + async def stream_messages(self, messages, input_context=None, **kwargs): + raise AssertionError("stream_messages should not be used by sync chat") + + async def get_graph(self): + return FakeGraph() + + async def fake_get_agent_config_by_id(db, user, agent_config_id): + assert user.id == "user-1" + assert agent_config_id == 123 + return SimpleNamespace(agent_id="test-agent", config_json={"context": {"temperature": 0.1}}) + + async def fake_save_messages_from_langgraph_state(*, agent_instance, thread_id, conv_repo, config_dict): + calls["saved_state"] = { + "agent_instance": agent_instance, + "thread_id": thread_id, + "conv_repo": conv_repo, + "config_dict": config_dict, + } + + async def fake_guard_check(_content): + return False + + monkeypatch.setattr(svc.agent_manager, "get_agent", lambda agent_id: FakeAgent()) + monkeypatch.setattr(svc, "get_agent_config_by_id", fake_get_agent_config_by_id) + monkeypatch.setattr(svc, "ConversationRepository", _FakeConvRepo) + monkeypatch.setattr(svc, "save_messages_from_langgraph_state", fake_save_messages_from_langgraph_state) + monkeypatch.setattr(svc.content_guard, "check", fake_guard_check) + + result = await svc.agent_chat( + query="hello", + agent_config_id=123, + thread_id="thread-1", + meta={"request_id": "req-1"}, + image_content=None, + current_user=SimpleNamespace(id="user-1", department_id="dept-1"), + db=object(), + ) + + assert result["status"] == "finished" + assert result["response"] == "Hi from invoke" + assert result["thread_id"] == "thread-1" + assert result["request_id"] == "req-1" + assert result["agent_state"] == {"todos": ["todo-1"], "files": {}} + + invoke_messages = calls["invoke_messages"] + assert isinstance(invoke_messages, list) + assert len(invoke_messages) == 1 + assert isinstance(invoke_messages[0], HumanMessage) + assert invoke_messages[0].content == "hello" + assert calls["invoke_input_context"] == {"temperature": 0.1, "user_id": "user-1", "thread_id": "thread-1"} + assert calls["saved_state"]["thread_id"] == "thread-1" + assert calls["saved_state"]["config_dict"] == {"configurable": {"thread_id": "thread-1", "user_id": "user-1"}} + + +@pytest.mark.asyncio +async def test_agent_chat_sync_returns_finished_even_when_state_has_interrupt(monkeypatch: pytest.MonkeyPatch): + class FakeGraph: + async def aget_state(self, config): + return SimpleNamespace( + values={ + "messages": [AIMessage(content="Need input later")], + "__interrupt__": [{"questions": [{"question": "继续吗?"}]}], + } + ) + + class FakeAgent: + async def invoke_messages(self, messages, input_context=None, **kwargs): + return {"messages": [messages[0], AIMessage(content="Need input later")]} + + async def stream_messages(self, messages, input_context=None, **kwargs): + raise AssertionError("stream_messages should not be used by sync chat") + + async def get_graph(self): + return FakeGraph() + + async def fake_get_agent_config_by_id(db, user, agent_config_id): + return SimpleNamespace(agent_id="test-agent", config_json={"context": {}}) + + async def fake_save_messages_from_langgraph_state(*, agent_instance, thread_id, conv_repo, config_dict): + return None + + async def fake_guard_check(_content): + return False + + monkeypatch.setattr(svc.agent_manager, "get_agent", lambda agent_id: FakeAgent()) + monkeypatch.setattr(svc, "get_agent_config_by_id", fake_get_agent_config_by_id) + monkeypatch.setattr(svc, "ConversationRepository", _FakeConvRepo) + monkeypatch.setattr(svc, "save_messages_from_langgraph_state", fake_save_messages_from_langgraph_state) + monkeypatch.setattr(svc.content_guard, "check", fake_guard_check) + + result = await svc.agent_chat( + query="hello", + agent_config_id=456, + thread_id="thread-2", + meta={"request_id": "req-2"}, + image_content=None, + current_user=SimpleNamespace(id="user-1", department_id="dept-1"), + db=object(), + ) + + assert result["status"] == "finished" + assert result["response"] == "Need input later" + assert result["thread_id"] == "thread-2" diff --git a/docs/advanced/api-key-integration.md b/docs/advanced/api-key-integration.md index 0611533d..66ac733c 100644 --- a/docs/advanced/api-key-integration.md +++ b/docs/advanced/api-key-integration.md @@ -14,7 +14,7 @@ API Key 是一种用于身份验证的密钥字符串,外部系统可以通过 ## 接口调用方式 -外部系统通过 HTTP 请求调用 Yuxi 的对话接口,需要在请求头中携带 API Key。接口地址为 `POST /api/chat/agent/{agent_id}`,其中 `{agent_id}` 是目标智能体的标识符。请求头需要包含 `Authorization` 字段,值格式为 `Bearer `,其中 `` 是创建 API Key 时获取的完整密钥。请求体为 JSON 格式,包含 `query` 字段表示用户问题,以及可选的 `config` 和 `meta` 字段用于配置对话参数。 +外部系统通过 HTTP 请求调用 Yuxi 的对话接口,需要在请求头中携带 API Key。流式接口地址为 `POST /api/chat/agent`,非流式接口地址为 `POST /api/chat/agent/sync`(不支持 HITL)。请求头需要包含 `Authorization` 字段,值格式为 `Bearer `,其中 `` 是创建 API Key 时获取的完整密钥。请求体为 JSON 格式,必填字段为 `query` 和 `agent_config_id`,可选字段为 `thread_id`、`image_content` 和 `meta`。 以下是一个典型的 Python 调用示例: @@ -22,14 +22,14 @@ API Key 是一种用于身份验证的密钥字符串,外部系统可以通过 import requests import json -url = "http://your-yuxi-server/api/chat/agent/agent_id" +url = "http://your-yuxi-server/api/chat/agent" headers = { "Authorization": "Bearer yxkey_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Content-Type": "application/json" } payload = { "query": "你好,请介绍一下你自己", - "config": {}, + "agent_config_id": 1, "meta": {} } @@ -47,7 +47,7 @@ for line in response.iter_lines(): `event: data` 表示数据事件,携带实际的对话内容。`event: error` 表示错误事件,当对话过程中发生错误时会收到此类事件。`event: done` 表示完成事件,标志对话结束。 -每次调用都会在响应中包含 `request_id`,这是本次对话的唯一标识符,可用于日志追踪和问题排查。如果需要在多轮对话中使用同一个会话,可以通过 `config.thread_id` 参数指定线程 ID,系统会将同一线程的消息串联起来形成连贯的对话上下文。 +每次调用都会在响应中包含 `request_id`,这是本次对话的唯一标识符,可用于日志追踪和问题排查。如果需要在多轮对话中使用同一个会话,可以通过 `thread_id` 参数指定线程 ID,系统会将同一线程的消息串联起来形成连贯的对话上下文。 ## 认证方式 diff --git a/docs/agents/agents-config.md b/docs/agents/agents-config.md index 571e3b5b..78db7171 100644 --- a/docs/agents/agents-config.md +++ b/docs/agents/agents-config.md @@ -177,7 +177,7 @@ Context 的价值不只在“配置页面”。它贯穿了从配置加载到实 当前主流程在 `chat_service.py` 中: 1. 通过 `agent_config_id` 查找配置 -2. 若未指定,则获取该部门下该 Agent 的默认配置 +2. 读取该配置绑定的 `agent_id` 3. 取出 `config_json.context` 4. 与 `user_id`、`thread_id` 合并成运行时输入 diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index fd9eaa81..65ad3b46 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -47,6 +47,7 @@ ### 修复 +- 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试 - 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试 - 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标 - 修复 GitHub Pages 文档部署工作流失败:移除 `actions/setup-node@v4` 对不存在 `docs/package-lock.json` 的缓存依赖,并将 `docs` 目录安装命令从 `npm ci` 调整为 `npm install`,避免因未提交锁文件导致 CI 在依赖缓存和安装阶段直接失败 diff --git a/docs/intro/quick-start.md b/docs/intro/quick-start.md index 4ac8a105..630c0c3a 100644 --- a/docs/intro/quick-start.md +++ b/docs/intro/quick-start.md @@ -3,7 +3,7 @@ 欢迎使用 Yuxi(语析),这是一个智能知识库和知识图谱 Agent 开发平台。 本指南将帮助你在几分钟内启动并运行系统,使你能够利用 LangGraph、RAG 技术和知识图谱构建 AI 驱动的知识应用。 -![系统架构图](https://private-user-images.githubusercontent.com/35524243/559928805-96742b56-eda7-4aae-a4df-6fe9d3c30fd1.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNjgxODMsIm5iZiI6MTc3NDM2Nzg4MywicGF0aCI6Ii8zNTUyNDI0My81NTk5Mjg4MDUtOTY3NDJiNTYtZWRhNy00YWFlLWE0ZGYtNmZlOWQzYzMwZmQxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjAzMjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwMzI0VDE1NTgwM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTQwODFlNzI0YjMwMTI4YWMzOWU2YjAxMjUyM2E4NTUzOWRiNzY5OWMzY2Y0OGE0MzJjZmZjNWI3ODMzZjYyMWMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.uw8QmYsqyXDoEP8wLPHvUYdhS2qbCo2engwiaprs-VM) +![系统架构图](https://xerrors.oss-cn-shanghai.aliyuncs.com/github/image-20260326130844668.png) ::: tip 提示 diff --git a/web/src/apis/agent_api.js b/web/src/apis/agent_api.js index 655cd77b..0ee2a01f 100644 --- a/web/src/apis/agent_api.js +++ b/web/src/apis/agent_api.js @@ -22,18 +22,17 @@ import { useUserStore } from '@/stores/user' export const agentApi = { /** * 发送聊天消息到指定智能体(流式响应) - * @param {string} agentId - 智能体ID * @param {Object} data - 聊天数据 * @returns {Promise} - 聊天响应流 */ - sendAgentMessage: (agentId, data, options = {}) => { + sendAgentMessage: (data, options = {}) => { const { signal, headers: extraHeaders, ...restOptions } = options || {} const baseHeaders = { 'Content-Type': 'application/json', ...useUserStore().getAuthHeaders() } - return fetch(`/api/chat/agent/${agentId}`, { + return fetch('/api/chat/agent', { method: 'POST', body: JSON.stringify(data), signal, @@ -190,14 +189,15 @@ export const agentApi = { /** * 创建异步运行任务(Run) - * @param {string} agentId - 智能体ID * @param {Object} data - run 请求体 * @returns {Promise} */ - createAgentRun: (agentId, data) => - apiPost(`/api/chat/agent/${agentId}/runs`, { + createAgentRun: (data) => + apiPost('/api/chat/runs', { query: data.query, - config: data.config || {}, + agent_config_id: data.agent_config_id, + thread_id: data.thread_id, + meta: data.meta || {}, image_content: data.image_content || null }), diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 2fbdc3b2..bfda818a 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -928,10 +928,8 @@ const sendMessage = async ({ const requestData = { query: text, - config: { - thread_id: threadId, - agent_config_id: selectedAgentConfigId.value - } + thread_id: threadId, + agent_config_id: selectedAgentConfigId.value } // 如果有图片,添加到请求中 @@ -940,7 +938,7 @@ const sendMessage = async ({ } try { - return await agentApi.sendAgentMessage(agentId, requestData, signal ? { signal } : undefined) + return await agentApi.sendAgentMessage(requestData, signal ? { signal } : undefined) } catch (error) { handleChatError(error, 'send') throw error @@ -1170,12 +1168,10 @@ const handleSendMessage = async ({ image } = {}) => { resetOnGoingConv(threadId) threadState.isStreaming = true try { - const runResp = await agentApi.createAgentRun(currentAgentId.value, { + const runResp = await agentApi.createAgentRun({ query: text, - config: { - thread_id: threadId, - agent_config_id: selectedAgentConfigId.value - }, + agent_config_id: selectedAgentConfigId.value, + thread_id: threadId, image_content: image?.imageContent }) const runId = runResp?.run_id