refactor(chat-api): 重构聊天接口请求模型:流式与非流式聊天统一使用 query + agent_config_id 请求体

This commit is contained in:
Wenjie Zhang 2026-03-26 23:13:44 +08:00
parent 7037d48d51
commit 9f51cfd96b
16 changed files with 516 additions and 262 deletions

View File

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

View File

@ -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}")

View File

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

View File

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

View File

@ -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 = (

View File

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

View File

@ -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}")

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ API Key 是一种用于身份验证的密钥字符串,外部系统可以通过
## 接口调用方式
外部系统通过 HTTP 请求调用 Yuxi 的对话接口,需要在请求头中携带 API Key。接口地址为 `POST /api/chat/agent/{agent_id}`,其中 `{agent_id}` 是目标智能体的标识符。请求头需要包含 `Authorization` 字段,值格式为 `Bearer <api_key>`,其中 `<api_key>` 是创建 API Key 时获取的完整密钥。请求体为 JSON 格式,包含 `query` 字段表示用户问题,以及可选的 `config``meta` 字段用于配置对话参数
外部系统通过 HTTP 请求调用 Yuxi 的对话接口,需要在请求头中携带 API Key。流式接口地址为 `POST /api/chat/agent`,非流式接口地址为 `POST /api/chat/agent/sync`(不支持 HITL。请求头需要包含 `Authorization` 字段,值格式为 `Bearer <api_key>`,其中 `<api_key>` 是创建 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系统会将同一线程的消息串联起来形成连贯的对话上下文。
## 认证方式

View File

@ -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` 合并成运行时输入

View File

@ -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 在依赖缓存和安装阶段直接失败

View File

@ -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 提示

View File

@ -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<Object>}
*/
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
}),

View File

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