refactor(chat-api): 重构聊天接口请求模型:流式与非流式聊天统一使用 query + agent_config_id 请求体
This commit is contained in:
parent
7037d48d51
commit
9f51cfd96b
@ -12,6 +12,7 @@ from fastapi import HTTPException
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from yuxi.agents.buildin import agent_manager
|
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.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository
|
||||||
from yuxi.repositories.conversation_repository import ConversationRepository
|
from yuxi.repositories.conversation_repository import ConversationRepository
|
||||||
from yuxi.services.run_queue_service import (
|
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(
|
async def create_agent_run_view(
|
||||||
*,
|
*,
|
||||||
agent_id: str,
|
|
||||||
query: str,
|
query: str,
|
||||||
config: dict,
|
agent_config_id: int,
|
||||||
|
thread_id: str,
|
||||||
|
meta: dict,
|
||||||
image_content: str | None,
|
image_content: str | None,
|
||||||
current_user_id: str,
|
current_user_id: str,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
@ -61,19 +63,28 @@ async def create_agent_run_view(
|
|||||||
if not query:
|
if not query:
|
||||||
raise HTTPException(status_code=422, detail="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):
|
if not agent_manager.get_agent(agent_id):
|
||||||
raise HTTPException(status_code=404, detail=f"智能体 {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)
|
conv_repo = ConversationRepository(db)
|
||||||
conversation = await conv_repo.get_conversation_by_thread_id(thread_id)
|
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":
|
if not conversation or conversation.user_id != str(current_user_id) or conversation.status == "deleted":
|
||||||
raise HTTPException(status_code=404, detail="对话线程不存在")
|
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)
|
run_repo = AgentRunRepository(db)
|
||||||
existing = await run_repo.get_run_by_request_id(request_id)
|
existing = await run_repo.get_run_by_request_id(request_id)
|
||||||
if existing and existing.user_id == str(current_user_id):
|
if existing and existing.user_id == str(current_user_id):
|
||||||
|
|||||||
@ -298,6 +298,35 @@ def _ensure_full_msg(full_msg: AIMessage | None, accumulated_content: list[str])
|
|||||||
return full_msg
|
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):
|
async def _resolve_agent_config(db, agent_id: str, user: User, agent_config_id):
|
||||||
"""解析 agent_config,返回 agent_config"""
|
"""解析 agent_config,返回 agent_config"""
|
||||||
department_id = user.department_id
|
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)
|
agent_config_repo = AgentConfigRepository(db)
|
||||||
config_item = None
|
config_item = None
|
||||||
if agent_config_id is not None:
|
if agent_config_id is not None:
|
||||||
config_item = await agent_config_repo.get_by_id(config_id=int(agent_config_id))
|
config_item = await get_agent_config_by_id(db, user, int(agent_config_id))
|
||||||
if config_item is not None and (config_item.department_id != department_id or config_item.agent_id != agent_id):
|
if config_item.agent_id != agent_id:
|
||||||
config_item = None
|
config_item = None
|
||||||
|
|
||||||
if config_item is None:
|
if config_item is None:
|
||||||
@ -344,9 +373,9 @@ async def check_and_handle_interrupts(
|
|||||||
|
|
||||||
async def agent_chat(
|
async def agent_chat(
|
||||||
*,
|
*,
|
||||||
agent_id: str,
|
|
||||||
query: str,
|
query: str,
|
||||||
config: dict,
|
agent_config_id: int,
|
||||||
|
thread_id: str | None,
|
||||||
meta: dict,
|
meta: dict,
|
||||||
image_content: str | None,
|
image_content: str | None,
|
||||||
current_user,
|
current_user,
|
||||||
@ -367,13 +396,6 @@ async def agent_chat(
|
|||||||
human_message = HumanMessage(content=query)
|
human_message = HumanMessage(content=query)
|
||||||
message_type = "text"
|
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):
|
if conf.enable_content_guard and await content_guard.check(query):
|
||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
@ -382,6 +404,42 @@ async def agent_chat(
|
|||||||
"request_id": meta.get("request_id"),
|
"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:
|
try:
|
||||||
agent = agent_manager.get_agent(agent_id)
|
agent = agent_manager.get_agent(agent_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -394,20 +452,9 @@ async def agent_chat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
messages = [human_message]
|
messages = [human_message]
|
||||||
|
agent_config = (config_item.config_json or {}).get("context", {})
|
||||||
|
|
||||||
user_id = str(current_user.id)
|
if not thread_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")):
|
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}")
|
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}")
|
logger.error(f"Error saving user message: {e}")
|
||||||
|
|
||||||
langgraph_config = {"configurable": {"thread_id": thread_id, "user_id": user_id}}
|
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
|
if full_msg is None:
|
||||||
accumulated_content: list[str] = []
|
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):
|
full_content = full_msg.content if full_msg else ""
|
||||||
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 "")
|
|
||||||
|
|
||||||
if conf.enable_content_guard and await content_guard.check(full_content):
|
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")
|
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(
|
async def stream_agent_chat(
|
||||||
*,
|
*,
|
||||||
agent_id: str,
|
|
||||||
query: str,
|
query: str,
|
||||||
config: dict,
|
agent_config_id: int,
|
||||||
|
thread_id: str | None,
|
||||||
meta: dict,
|
meta: dict,
|
||||||
image_content: str | None,
|
image_content: str | None,
|
||||||
current_user,
|
current_user,
|
||||||
@ -533,6 +578,34 @@ async def stream_agent_chat(
|
|||||||
)
|
)
|
||||||
return
|
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:
|
try:
|
||||||
agent = agent_manager.get_agent(agent_id)
|
agent = agent_manager.get_agent(agent_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -546,16 +619,9 @@ async def stream_agent_chat(
|
|||||||
return
|
return
|
||||||
|
|
||||||
messages = [human_message]
|
messages = [human_message]
|
||||||
|
agent_config = (config_item.config_json or {}).get("context", {})
|
||||||
|
|
||||||
user_id = str(current_user.id)
|
if not thread_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")):
|
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}")
|
logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}")
|
||||||
|
|
||||||
|
|||||||
@ -235,9 +235,9 @@ async def process_agent_run(ctx, run_id: str):
|
|||||||
try:
|
try:
|
||||||
async with pg_manager.get_async_session_context() as db:
|
async with pg_manager.get_async_session_context() as db:
|
||||||
stream = stream_agent_chat(
|
stream = stream_agent_chat(
|
||||||
agent_id=agent_id,
|
|
||||||
query=query,
|
query=query,
|
||||||
config=config,
|
agent_config_id=config.get("agent_config_id"),
|
||||||
|
thread_id=config.get("thread_id"),
|
||||||
meta=meta,
|
meta=meta,
|
||||||
image_content=image_content,
|
image_content=image_content,
|
||||||
current_user=user,
|
current_user=user,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from server.utils.auth_middleware import get_db, get_required_user
|
|||||||
from yuxi import config as conf
|
from yuxi import config as conf
|
||||||
from yuxi.agents.buildin import agent_manager
|
from yuxi.agents.buildin import agent_manager
|
||||||
from yuxi.models import select_model
|
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 (
|
from yuxi.services.agent_run_service import (
|
||||||
cancel_agent_run_view,
|
cancel_agent_run_view,
|
||||||
create_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
|
from yuxi.utils.image_processor import process_uploaded_image
|
||||||
|
|
||||||
|
|
||||||
|
# TODO:当前文件的功能过于庞杂,路由标签混乱
|
||||||
|
|
||||||
# 图片上传响应模型
|
# 图片上传响应模型
|
||||||
class ImageUploadResponse(BaseModel):
|
class ImageUploadResponse(BaseModel):
|
||||||
success: bool
|
success: bool
|
||||||
@ -76,9 +78,19 @@ class AgentConfigUpdate(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class AgentRunCreate(BaseModel):
|
class AgentRunCreate(BaseModel):
|
||||||
query: str
|
query: str = Field(..., description="用户输入的问题")
|
||||||
config: dict = Field(default_factory=dict)
|
agent_config_id: int = Field(..., description="智能体配置 ID,后端将据此解析 agent_id 和运行时 context")
|
||||||
image_content: str | None = None
|
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"])
|
chat = APIRouter(prefix="/chat", tags=["chat"])
|
||||||
@ -218,7 +230,6 @@ async def list_agent_configs(
|
|||||||
if not items:
|
if not items:
|
||||||
await repo.get_or_create_default(
|
await repo.get_or_create_default(
|
||||||
department_id=current_user.department_id,
|
department_id=current_user.department_id,
|
||||||
agent_id=agent_id,
|
|
||||||
created_by=str(current_user.id),
|
created_by=str(current_user.id),
|
||||||
)
|
)
|
||||||
items = await repo.list_by_department_agent(department_id=current_user.department_id, agent_id=agent_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)
|
repo = AgentConfigRepository(db)
|
||||||
item = await repo.create(
|
item = await repo.create(
|
||||||
department_id=current_user.department_id,
|
department_id=current_user.department_id,
|
||||||
agent_id=agent_id,
|
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
description=payload.description,
|
description=payload.description,
|
||||||
icon=payload.icon,
|
icon=payload.icon,
|
||||||
@ -367,44 +377,28 @@ async def delete_agent_config_profile(
|
|||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
@chat.post("/agent/{agent_id}")
|
@chat.post("/agent")
|
||||||
async def chat_agent(
|
async def chat_agent(
|
||||||
agent_id: str,
|
payload: AgentChatRequest,
|
||||||
query: str = Body(...),
|
|
||||||
config: dict = Body({}),
|
|
||||||
meta: dict = Body({}),
|
|
||||||
image_content: str | None = Body(None),
|
|
||||||
current_user: User = Depends(get_required_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""使用特定智能体进行对话(需要登录)"""
|
"""使用特定智能体进行对话(需要登录)"""
|
||||||
logger.info(f"agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}")
|
logger.info(f"query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.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]}...")
|
|
||||||
|
|
||||||
# 确保 request_id 存在
|
# 查看图片内容
|
||||||
if "request_id" not in meta or not meta.get("request_id"):
|
logger.info(f"image_content present: {payload.image_content is not None}")
|
||||||
meta["request_id"] = str(uuid.uuid4())
|
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(
|
return StreamingResponse(
|
||||||
stream_agent_chat(
|
stream_agent_chat(
|
||||||
agent_id=agent_id,
|
query=payload.query,
|
||||||
query=query,
|
agent_config_id=payload.agent_config_id,
|
||||||
config=config,
|
thread_id=payload.thread_id,
|
||||||
meta=meta,
|
meta=dict(payload.meta or {}),
|
||||||
image_content=image_content,
|
image_content=payload.image_content,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
db=db,
|
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(
|
async def chat_agent_sync(
|
||||||
agent_id: str,
|
payload: AgentChatRequest,
|
||||||
query: str = Body(...),
|
|
||||||
config: dict = Body({}),
|
|
||||||
meta: dict = Body({}),
|
|
||||||
image_content: str | None = Body(None),
|
|
||||||
current_user: User = Depends(get_required_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""使用特定智能体进行非流式对话(需要登录)"""
|
"""使用特定智能体进行非流式对话(需要登录)"""
|
||||||
logger.info(f"[sync] agent_id: {agent_id}, query: {query}, config: {config}, meta: {meta}")
|
logger.info(f"[sync] query: {payload.query}, agent_config_id: {payload.agent_config_id}, meta: {payload.meta}")
|
||||||
logger.info(f"[sync] image_content present: {image_content is not None}")
|
logger.info(f"[sync] image_content present: {payload.image_content is not None}")
|
||||||
if image_content:
|
if payload.image_content:
|
||||||
logger.info(f"[sync] image_content length: {len(image_content)}")
|
logger.info(f"[sync] image_content length: {len(payload.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),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return await agent_chat(
|
return await agent_chat(
|
||||||
agent_id=agent_id,
|
query=payload.query,
|
||||||
query=query,
|
agent_config_id=payload.agent_config_id,
|
||||||
config=config,
|
thread_id=payload.thread_id,
|
||||||
meta=meta,
|
meta=dict(payload.meta or {}),
|
||||||
image_content=image_content,
|
image_content=payload.image_content,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
db=db,
|
db=db,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@chat.post("/agent/{agent_id}/runs")
|
@chat.post("/runs")
|
||||||
async def create_agent_run(
|
async def create_agent_run(
|
||||||
agent_id: str,
|
|
||||||
payload: AgentRunCreate,
|
payload: AgentRunCreate,
|
||||||
current_user: User = Depends(get_required_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""创建异步 run 任务并入队(需要登录)"""
|
"""创建异步 run 任务并入队(需要登录)"""
|
||||||
return await create_agent_run_view(
|
return await create_agent_run_view(
|
||||||
agent_id=agent_id,
|
|
||||||
query=payload.query,
|
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,
|
image_content=payload.image_content,
|
||||||
current_user_id=str(current_user.id),
|
current_user_id=str(current_user.id),
|
||||||
db=db,
|
db=db,
|
||||||
@ -627,7 +602,6 @@ async def resume_agent_chat(
|
|||||||
meta["request_id"] = str(uuid.uuid4())
|
meta["request_id"] = str(uuid.uuid4())
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
stream_agent_resume(
|
stream_agent_resume(
|
||||||
agent_id=agent_id,
|
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
resume_input=resume_input,
|
resume_input=resume_input,
|
||||||
meta=meta,
|
meta=meta,
|
||||||
|
|||||||
@ -49,6 +49,7 @@ class AgentBubbleSortE2ETester:
|
|||||||
self.client = httpx.AsyncClient(base_url=API_BASE_URL, timeout=TIMEOUT, follow_redirects=True)
|
self.client = httpx.AsyncClient(base_url=API_BASE_URL, timeout=TIMEOUT, follow_redirects=True)
|
||||||
self.headers: dict[str, str] | None = None
|
self.headers: dict[str, str] | None = None
|
||||||
self.agent_id: str | None = None
|
self.agent_id: str | None = None
|
||||||
|
self.agent_config_id: int | None = None
|
||||||
self.thread_id: str | None = None
|
self.thread_id: str | None = None
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
@ -98,14 +99,29 @@ class AgentBubbleSortE2ETester:
|
|||||||
if not self.thread_id:
|
if not self.thread_id:
|
||||||
raise RuntimeError(f"thread id missing: {payload}")
|
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:
|
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()}"
|
request_id = f"agent-bubble-sort-{uuid.uuid4()}"
|
||||||
response = await self.client.post(
|
response = await self.client.post(
|
||||||
f"/api/chat/agent/{self.agent_id}/runs",
|
"/api/chat/runs",
|
||||||
json={
|
json={
|
||||||
"query": query,
|
"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,
|
headers=self.headers,
|
||||||
)
|
)
|
||||||
@ -166,6 +182,7 @@ class AgentBubbleSortE2ETester:
|
|||||||
async def run_case(self) -> None:
|
async def run_case(self) -> None:
|
||||||
await self.login()
|
await self.login()
|
||||||
await self.pick_agent()
|
await self.pick_agent()
|
||||||
|
await self.pick_agent_config()
|
||||||
await self.create_thread()
|
await self.create_thread()
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
|
|||||||
@ -9,6 +9,26 @@ import pytest
|
|||||||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
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):
|
async def test_list_api_keys_requires_auth(test_client):
|
||||||
"""List API keys should require authentication."""
|
"""List API keys should require authentication."""
|
||||||
response = await test_client.get("/api/apikey/")
|
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):
|
async def test_get_api_key(test_client, admin_headers):
|
||||||
"""Admin should be able to get a single API key."""
|
"""Admin should be able to get a single API key."""
|
||||||
# First create a key
|
# First create a key
|
||||||
create_response = await test_client.post(
|
create_response = await test_client.post("/api/apikey/", json={"name": "Get Test"}, headers=admin_headers)
|
||||||
"/api/apikey/", json={"name": "Get Test"}, headers=admin_headers
|
|
||||||
)
|
|
||||||
assert create_response.status_code == 200
|
assert create_response.status_code == 200
|
||||||
created = create_response.json()["api_key"]
|
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):
|
async def test_update_api_key(test_client, admin_headers):
|
||||||
"""Admin should be able to update an API key."""
|
"""Admin should be able to update an API key."""
|
||||||
# Create a key
|
# Create a key
|
||||||
create_response = await test_client.post(
|
create_response = await test_client.post("/api/apikey/", json={"name": "Update Test"}, headers=admin_headers)
|
||||||
"/api/apikey/", json={"name": "Update Test"}, headers=admin_headers
|
|
||||||
)
|
|
||||||
assert create_response.status_code == 200
|
assert create_response.status_code == 200
|
||||||
created = create_response.json()["api_key"]
|
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):
|
async def test_delete_api_key(test_client, admin_headers):
|
||||||
"""Admin should be able to delete an API key."""
|
"""Admin should be able to delete an API key."""
|
||||||
# Create a key
|
# Create a key
|
||||||
create_response = await test_client.post(
|
create_response = await test_client.post("/api/apikey/", json={"name": "Delete Test"}, headers=admin_headers)
|
||||||
"/api/apikey/", json={"name": "Delete Test"}, headers=admin_headers
|
|
||||||
)
|
|
||||||
assert create_response.status_code == 200
|
assert create_response.status_code == 200
|
||||||
created = create_response.json()["api_key"]
|
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):
|
async def test_regenerate_api_key(test_client, admin_headers):
|
||||||
"""Admin should be able to regenerate an API key."""
|
"""Admin should be able to regenerate an API key."""
|
||||||
# Create a key
|
# Create a key
|
||||||
create_response = await test_client.post(
|
create_response = await test_client.post("/api/apikey/", json={"name": "Regenerate Test"}, headers=admin_headers)
|
||||||
"/api/apikey/", json={"name": "Regenerate Test"}, headers=admin_headers
|
|
||||||
)
|
|
||||||
assert create_response.status_code == 200
|
assert create_response.status_code == 200
|
||||||
original_secret = create_response.json()["secret"]
|
original_secret = create_response.json()["secret"]
|
||||||
created = create_response.json()["api_key"]
|
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):
|
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."""
|
"""Test that API Key can be used to authenticate to chat endpoint via Bearer token."""
|
||||||
# Create an API key
|
# Create an API key
|
||||||
create_response = await test_client.post(
|
create_response = await test_client.post("/api/apikey/", json={"name": "Chat Auth Test"}, headers=admin_headers)
|
||||||
"/api/apikey/", json={"name": "Chat Auth Test"}, headers=admin_headers
|
|
||||||
)
|
|
||||||
assert create_response.status_code == 200
|
assert create_response.status_code == 200
|
||||||
api_key_secret = create_response.json()["secret"]
|
api_key_secret = create_response.json()["secret"]
|
||||||
created = create_response.json()["api_key"]
|
created = create_response.json()["api_key"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get default agent
|
_, agent_config_id = await _get_default_agent_config_id(test_client, admin_headers)
|
||||||
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")
|
|
||||||
|
|
||||||
# Call chat endpoint with API Key using Bearer format (streaming response)
|
# Call chat endpoint with API Key using Bearer format (streaming response)
|
||||||
async with test_client.stream(
|
async with test_client.stream(
|
||||||
"POST",
|
"POST",
|
||||||
f"/api/chat/agent/{agent_id}",
|
"/api/chat/agent",
|
||||||
json={"query": "Hello"},
|
json={"query": "Hello", "agent_config_id": agent_config_id},
|
||||||
headers={"Authorization": f"Bearer {api_key_secret}"},
|
headers={"Authorization": f"Bearer {api_key_secret}"},
|
||||||
) as response:
|
) as response:
|
||||||
assert response.status_code == 200, response.text
|
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."""
|
"""Test that invalid API Key is rejected."""
|
||||||
# Call chat endpoint with invalid API Key
|
# Call chat endpoint with invalid API Key
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/api/chat/agent/ChatbotAgent",
|
"/api/chat/agent",
|
||||||
json={"query": "Hello"},
|
json={"query": "Hello", "agent_config_id": 1},
|
||||||
headers={"Authorization": "Bearer yxkey_invalid_key_that_does_not_exist"},
|
headers={"Authorization": "Bearer yxkey_invalid_key_that_does_not_exist"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 401, response.text
|
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):
|
async def test_api_key_auth_requires_bearer_prefix(test_client, admin_headers):
|
||||||
"""Test that API Key must be prefixed with 'Bearer '."""
|
"""Test that API Key must be prefixed with 'Bearer '."""
|
||||||
# Create an API key
|
# Create an API key
|
||||||
admin_response = await test_client.post(
|
admin_response = await test_client.post("/api/apikey/", json={"name": "Prefix Test"}, headers=admin_headers)
|
||||||
"/api/apikey/", json={"name": "Prefix Test"}, headers=admin_headers
|
|
||||||
)
|
|
||||||
assert admin_response.status_code == 200
|
assert admin_response.status_code == 200
|
||||||
api_key_secret = admin_response.json()["secret"]
|
api_key_secret = admin_response.json()["secret"]
|
||||||
created = admin_response.json()["api_key"]
|
created = admin_response.json()["api_key"]
|
||||||
@ -175,8 +178,8 @@ async def test_api_key_auth_requires_bearer_prefix(test_client, admin_headers):
|
|||||||
try:
|
try:
|
||||||
# Call without Bearer prefix should fail
|
# Call without Bearer prefix should fail
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/api/chat/agent/ChatbotAgent",
|
"/api/chat/agent",
|
||||||
json={"query": "Hello"},
|
json={"query": "Hello", "agent_config_id": 1},
|
||||||
headers={"Authorization": api_key_secret}, # Missing "Bearer " prefix
|
headers={"Authorization": api_key_secret}, # Missing "Bearer " prefix
|
||||||
)
|
)
|
||||||
assert response.status_code == 401, response.text
|
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):
|
async def test_jwt_still_works_after_apikey_auth(test_client, admin_headers):
|
||||||
"""Test that JWT Bearer tokens still work after API Key changes."""
|
"""Test that JWT Bearer tokens still work after API Key changes."""
|
||||||
# Get default agent
|
_, agent_config_id = await _get_default_agent_config_id(test_client, admin_headers)
|
||||||
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")
|
|
||||||
|
|
||||||
# Call chat with JWT Bearer token (admin_headers) - streaming response
|
# Call chat with JWT Bearer token (admin_headers) - streaming response
|
||||||
async with test_client.stream(
|
async with test_client.stream(
|
||||||
"POST",
|
"POST",
|
||||||
f"/api/chat/agent/{agent_id}",
|
"/api/chat/agent",
|
||||||
json={"query": "Hello"},
|
json={"query": "Hello", "agent_config_id": agent_config_id},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
) as response:
|
) as response:
|
||||||
assert response.status_code == 200, response.text
|
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):
|
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."""
|
"""Test that API Key created without user_id is auto-bound to creator."""
|
||||||
# Create API key as admin
|
# Create API key as admin
|
||||||
create_response = await test_client.post(
|
create_response = await test_client.post("/api/apikey/", json={"name": "Auto Bind Test"}, headers=admin_headers)
|
||||||
"/api/apikey/", json={"name": "Auto Bind Test"}, headers=admin_headers
|
|
||||||
)
|
|
||||||
assert create_response.status_code == 200
|
assert create_response.status_code == 200
|
||||||
created = create_response.json()["api_key"]
|
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
|
# Verify the key can be used for auth
|
||||||
api_key_secret = create_response.json()["secret"]
|
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(
|
async with test_client.stream(
|
||||||
"POST",
|
"POST",
|
||||||
"/api/chat/agent/ChatbotAgent",
|
"/api/chat/agent",
|
||||||
json={"query": "Hello"},
|
json={"query": "Hello", "agent_config_id": agent_config_id},
|
||||||
headers={"Authorization": f"Bearer {api_key_secret}"},
|
headers={"Authorization": f"Bearer {api_key_secret}"},
|
||||||
) as response:
|
) as response:
|
||||||
assert response.status_code == 200, response.text
|
assert response.status_code == 200, response.text
|
||||||
finally:
|
finally:
|
||||||
# Cleanup: delete the test API key
|
# Cleanup: delete the test API key
|
||||||
await test_client.delete(f"/api/apikey/{created['id']}", headers=admin_headers)
|
await test_client.delete(f"/api/apikey/{created['id']}", headers=admin_headers)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -183,6 +183,25 @@ class APITester:
|
|||||||
print(f" ✗ 获取失败: {response.status_code} - {response.text}")
|
print(f" ✗ 获取失败: {response.status_code} - {response.text}")
|
||||||
return None
|
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:
|
async def send_chat_message(self, agent_id: str, thread_id: str, query: str) -> bool:
|
||||||
"""发送聊天消息(流式)"""
|
"""发送聊天消息(流式)"""
|
||||||
print(f"\n{'=' * 60}")
|
print(f"\n{'=' * 60}")
|
||||||
@ -191,11 +210,19 @@ class APITester:
|
|||||||
print(f" Query: {query}")
|
print(f" Query: {query}")
|
||||||
print(f" Thread ID: {thread_id}")
|
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 self._client(timeout=120.0) as client:
|
||||||
async with client.stream(
|
async with client.stream(
|
||||||
"POST",
|
"POST",
|
||||||
f"{self.base_url}/api/chat/agent/{agent_id}",
|
f"{self.base_url}/api/chat/agent",
|
||||||
json={"query": query, "config": {"thread_id": thread_id}},
|
json={
|
||||||
|
"query": query,
|
||||||
|
"agent_config_id": agent_config_id,
|
||||||
|
"thread_id": thread_id,
|
||||||
|
},
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
) as response:
|
) as response:
|
||||||
print(f"\n 响应状态: {response.status_code}")
|
print(f"\n 响应状态: {response.status_code}")
|
||||||
|
|||||||
@ -9,16 +9,8 @@ import pytest
|
|||||||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||||||
|
|
||||||
|
|
||||||
async def test_chat_agent_sync_requires_authentication(test_client):
|
async def _get_agent_and_config_id(test_client, headers):
|
||||||
"""非流式端点需要认证"""
|
agents_response = await test_client.get("/api/chat/agent", headers=headers)
|
||||||
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)
|
|
||||||
assert agents_response.status_code == 200, agents_response.text
|
assert agents_response.status_code == 200, agents_response.text
|
||||||
agents = agents_response.json().get("agents", [])
|
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:
|
if not agent_id:
|
||||||
pytest.skip("Agent payload missing id field.")
|
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(
|
response = await test_client.post(
|
||||||
f"/api/chat/agent/{agent_id}/sync",
|
"/api/chat/agent/sync",
|
||||||
json={"query": "Hello, say 'Hi' back to me"},
|
json={"query": "Hello, say 'Hi' back to me", "agent_config_id": agent_config_id},
|
||||||
headers=admin_headers,
|
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):
|
async def test_chat_agent_sync_with_thread_id(test_client, admin_headers):
|
||||||
"""测试非流式对话指定 thread_id"""
|
"""测试非流式对话指定 thread_id"""
|
||||||
# 获取可用智能体
|
_, agent_config_id = await _get_agent_and_config_id(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.")
|
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
|
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
f"/api/chat/agent/{agent_id}/sync",
|
"/api/chat/agent/sync",
|
||||||
json={
|
json={
|
||||||
"query": "Hello",
|
"query": "Hello",
|
||||||
"config": {"thread_id": thread_id},
|
"agent_config_id": agent_config_id,
|
||||||
|
"thread_id": thread_id,
|
||||||
},
|
},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
)
|
)
|
||||||
@ -87,29 +94,24 @@ async def test_chat_agent_sync_with_thread_id(test_client, admin_headers):
|
|||||||
|
|
||||||
# 验证 thread_id 是否保持一致
|
# 验证 thread_id 是否保持一致
|
||||||
if payload["status"] == "finished":
|
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):
|
async def test_chat_agent_sync_with_meta(test_client, admin_headers):
|
||||||
"""测试非流式对话传递 meta 参数"""
|
"""测试非流式对话传递 meta 参数"""
|
||||||
agents_response = await test_client.get("/api/chat/agent", headers=admin_headers)
|
_, agent_config_id = await _get_agent_and_config_id(test_client, 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.")
|
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
request_id = str(uuid.uuid4())
|
request_id = str(uuid.uuid4())
|
||||||
|
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
f"/api/chat/agent/{agent_id}/sync",
|
"/api/chat/agent/sync",
|
||||||
json={
|
json={
|
||||||
"query": "Hello",
|
"query": "Hello",
|
||||||
|
"agent_config_id": agent_config_id,
|
||||||
"meta": {"request_id": request_id},
|
"meta": {"request_id": request_id},
|
||||||
},
|
},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
@ -119,35 +121,29 @@ async def test_chat_agent_sync_with_meta(test_client, admin_headers):
|
|||||||
payload = response.json()
|
payload = response.json()
|
||||||
|
|
||||||
# 验证 request_id 是否保持一致
|
# 验证 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):
|
async def test_chat_agent_sync_vs_streaming_consistency(test_client, admin_headers):
|
||||||
"""对比测试:非流式与流式端点行为一致性"""
|
"""对比测试:非流式与流式端点行为一致性"""
|
||||||
# 获取可用智能体
|
_, agent_config_id = await _get_agent_and_config_id(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.")
|
|
||||||
|
|
||||||
query = "What is 1+1?"
|
query = "What is 1+1?"
|
||||||
|
|
||||||
# 调用流式端点
|
# 调用流式端点
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
request_id = str(uuid.uuid4())
|
request_id = str(uuid.uuid4())
|
||||||
|
|
||||||
streaming_response = await test_client.post(
|
streaming_response = await test_client.post(
|
||||||
f"/api/chat/agent/{agent_id}",
|
"/api/chat/agent",
|
||||||
json={
|
json={
|
||||||
"query": query,
|
"query": query,
|
||||||
"config": {"thread_id": thread_id},
|
"agent_config_id": agent_config_id,
|
||||||
|
"thread_id": thread_id,
|
||||||
"meta": {"request_id": request_id},
|
"meta": {"request_id": request_id},
|
||||||
},
|
},
|
||||||
headers=admin_headers,
|
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():
|
async for line in streaming_response.aiter_lines():
|
||||||
if line:
|
if line:
|
||||||
import json as json_lib
|
import json as json_lib
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json_lib.loads(line)
|
data = json_lib.loads(line)
|
||||||
if data.get("response"):
|
if data.get("response"):
|
||||||
streaming_content.append(data["response"])
|
streaming_content.append(data["response"])
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 调用非流式端点
|
# 调用非流式端点
|
||||||
@ -172,10 +169,11 @@ async def test_chat_agent_sync_vs_streaming_consistency(test_client, admin_heade
|
|||||||
request_id2 = str(uuid.uuid4())
|
request_id2 = str(uuid.uuid4())
|
||||||
|
|
||||||
sync_response = await test_client.post(
|
sync_response = await test_client.post(
|
||||||
f"/api/chat/agent/{agent_id}/sync",
|
"/api/chat/agent/sync",
|
||||||
json={
|
json={
|
||||||
"query": query,
|
"query": query,
|
||||||
"config": {"thread_id": thread_id2},
|
"agent_config_id": agent_config_id,
|
||||||
|
"thread_id": thread_id2,
|
||||||
"meta": {"request_id": request_id2},
|
"meta": {"request_id": request_id2},
|
||||||
},
|
},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
|
|||||||
@ -9,6 +9,14 @@ from sqlalchemy.exc import IntegrityError
|
|||||||
import yuxi.services.agent_run_service as agent_run_service
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_stream_agent_run_events_emits_error_and_close_on_db_error(monkeypatch: pytest.MonkeyPatch):
|
async def test_stream_agent_run_events_emits_error_and_close_on_db_error(monkeypatch: pytest.MonkeyPatch):
|
||||||
@asynccontextmanager
|
@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):
|
async def get_conversation_by_thread_id(self, thread_id: str):
|
||||||
del thread_id
|
del thread_id
|
||||||
return SimpleNamespace(user_id="1", status="active")
|
return SimpleNamespace(user_id="1", status="active", department_id=1)
|
||||||
|
|
||||||
class Queue:
|
class Queue:
|
||||||
async def enqueue_job(self, job_name: str, run_id: str, _job_id: str):
|
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()
|
return Queue()
|
||||||
|
|
||||||
monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda agent_id: object())
|
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, "ConversationRepository", ConvRepo)
|
||||||
monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo)
|
monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo)
|
||||||
monkeypatch.setattr(agent_run_service, "get_arq_pool", fake_get_arq_pool)
|
monkeypatch.setattr(agent_run_service, "get_arq_pool", fake_get_arq_pool)
|
||||||
|
|
||||||
result = await agent_run_service.create_agent_run_view(
|
result = await agent_run_service.create_agent_run_view(
|
||||||
agent_id="ChatbotAgent",
|
|
||||||
query="hello",
|
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,
|
image_content=None,
|
||||||
current_user_id="1",
|
current_user_id="1",
|
||||||
db=db,
|
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):
|
async def get_conversation_by_thread_id(self, thread_id: str):
|
||||||
del thread_id
|
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():
|
async def fake_get_arq_pool():
|
||||||
raise AssertionError("should not enqueue on integrity fallback")
|
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.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, "ConversationRepository", ConvRepo)
|
||||||
monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo)
|
monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo)
|
||||||
monkeypatch.setattr(agent_run_service, "get_arq_pool", fake_get_arq_pool)
|
monkeypatch.setattr(agent_run_service, "get_arq_pool", fake_get_arq_pool)
|
||||||
|
|
||||||
result = await agent_run_service.create_agent_run_view(
|
result = await agent_run_service.create_agent_run_view(
|
||||||
agent_id="ChatbotAgent",
|
|
||||||
query="hello",
|
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,
|
image_content=None,
|
||||||
current_user_id="1",
|
current_user_id="1",
|
||||||
db=db,
|
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):
|
async def get_conversation_by_thread_id(self, thread_id: str):
|
||||||
del thread_id
|
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.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, "ConversationRepository", ConvRepo)
|
||||||
monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo)
|
monkeypatch.setattr(agent_run_service, "AgentRunRepository", Repo)
|
||||||
|
|
||||||
with pytest.raises(agent_run_service.HTTPException) as exc:
|
with pytest.raises(agent_run_service.HTTPException) as exc:
|
||||||
await agent_run_service.create_agent_run_view(
|
await agent_run_service.create_agent_run_view(
|
||||||
agent_id="ChatbotAgent",
|
|
||||||
query="hello",
|
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,
|
image_content=None,
|
||||||
current_user_id="1",
|
current_user_id="1",
|
||||||
db=db,
|
db=db,
|
||||||
|
|||||||
155
backend/test/test_chat_service_sync.py
Normal file
155
backend/test/test_chat_service_sync.py
Normal 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"
|
||||||
@ -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 调用示例:
|
以下是一个典型的 Python 调用示例:
|
||||||
|
|
||||||
@ -22,14 +22,14 @@ API Key 是一种用于身份验证的密钥字符串,外部系统可以通过
|
|||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
|
||||||
url = "http://your-yuxi-server/api/chat/agent/agent_id"
|
url = "http://your-yuxi-server/api/chat/agent"
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer yxkey_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
"Authorization": "Bearer yxkey_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
payload = {
|
payload = {
|
||||||
"query": "你好,请介绍一下你自己",
|
"query": "你好,请介绍一下你自己",
|
||||||
"config": {},
|
"agent_config_id": 1,
|
||||||
"meta": {}
|
"meta": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ for line in response.iter_lines():
|
|||||||
|
|
||||||
`event: data` 表示数据事件,携带实际的对话内容。`event: error` 表示错误事件,当对话过程中发生错误时会收到此类事件。`event: done` 表示完成事件,标志对话结束。
|
`event: data` 表示数据事件,携带实际的对话内容。`event: error` 表示错误事件,当对话过程中发生错误时会收到此类事件。`event: done` 表示完成事件,标志对话结束。
|
||||||
|
|
||||||
每次调用都会在响应中包含 `request_id`,这是本次对话的唯一标识符,可用于日志追踪和问题排查。如果需要在多轮对话中使用同一个会话,可以通过 `config.thread_id` 参数指定线程 ID,系统会将同一线程的消息串联起来形成连贯的对话上下文。
|
每次调用都会在响应中包含 `request_id`,这是本次对话的唯一标识符,可用于日志追踪和问题排查。如果需要在多轮对话中使用同一个会话,可以通过 `thread_id` 参数指定线程 ID,系统会将同一线程的消息串联起来形成连贯的对话上下文。
|
||||||
|
|
||||||
## 认证方式
|
## 认证方式
|
||||||
|
|
||||||
|
|||||||
@ -177,7 +177,7 @@ Context 的价值不只在“配置页面”。它贯穿了从配置加载到实
|
|||||||
当前主流程在 `chat_service.py` 中:
|
当前主流程在 `chat_service.py` 中:
|
||||||
|
|
||||||
1. 通过 `agent_config_id` 查找配置
|
1. 通过 `agent_config_id` 查找配置
|
||||||
2. 若未指定,则获取该部门下该 Agent 的默认配置
|
2. 读取该配置绑定的 `agent_id`
|
||||||
3. 取出 `config_json.context`
|
3. 取出 `config_json.context`
|
||||||
4. 与 `user_id`、`thread_id` 合并成运行时输入
|
4. 与 `user_id`、`thread_id` 合并成运行时输入
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
- 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试
|
||||||
- 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试
|
- 为沙盒与 viewer 文件系统补齐知识库只读映射:新增 `/home/gem/kbs` 命名空间,按“用户可访问知识库 ∩ 当前 Agent 已启用知识库”暴露原始文件与解析后的 Markdown,并补充对应后端与 viewer 路由测试
|
||||||
- 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标
|
- 修复前端工具图标与渲染匹配不准确的问题:工具管理列表与工具调用结果统一改为基于工具 `id` 的精确映射,避免模糊匹配导致的误渲染,未命中的工具不再显示默认扳手图标
|
||||||
- 修复 GitHub Pages 文档部署工作流失败:移除 `actions/setup-node@v4` 对不存在 `docs/package-lock.json` 的缓存依赖,并将 `docs` 目录安装命令从 `npm ci` 调整为 `npm install`,避免因未提交锁文件导致 CI 在依赖缓存和安装阶段直接失败
|
- 修复 GitHub Pages 文档部署工作流失败:移除 `actions/setup-node@v4` 对不存在 `docs/package-lock.json` 的缓存依赖,并将 `docs` 目录安装命令从 `npm ci` 调整为 `npm install`,避免因未提交锁文件导致 CI 在依赖缓存和安装阶段直接失败
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
欢迎使用 Yuxi(语析),这是一个智能知识库和知识图谱 Agent 开发平台。
|
欢迎使用 Yuxi(语析),这是一个智能知识库和知识图谱 Agent 开发平台。
|
||||||
本指南将帮助你在几分钟内启动并运行系统,使你能够利用 LangGraph、RAG 技术和知识图谱构建 AI 驱动的知识应用。
|
本指南将帮助你在几分钟内启动并运行系统,使你能够利用 LangGraph、RAG 技术和知识图谱构建 AI 驱动的知识应用。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
::: tip 提示
|
::: tip 提示
|
||||||
|
|||||||
@ -22,18 +22,17 @@ import { useUserStore } from '@/stores/user'
|
|||||||
export const agentApi = {
|
export const agentApi = {
|
||||||
/**
|
/**
|
||||||
* 发送聊天消息到指定智能体(流式响应)
|
* 发送聊天消息到指定智能体(流式响应)
|
||||||
* @param {string} agentId - 智能体ID
|
|
||||||
* @param {Object} data - 聊天数据
|
* @param {Object} data - 聊天数据
|
||||||
* @returns {Promise} - 聊天响应流
|
* @returns {Promise} - 聊天响应流
|
||||||
*/
|
*/
|
||||||
sendAgentMessage: (agentId, data, options = {}) => {
|
sendAgentMessage: (data, options = {}) => {
|
||||||
const { signal, headers: extraHeaders, ...restOptions } = options || {}
|
const { signal, headers: extraHeaders, ...restOptions } = options || {}
|
||||||
const baseHeaders = {
|
const baseHeaders = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...useUserStore().getAuthHeaders()
|
...useUserStore().getAuthHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(`/api/chat/agent/${agentId}`, {
|
return fetch('/api/chat/agent', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
signal,
|
signal,
|
||||||
@ -190,14 +189,15 @@ export const agentApi = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建异步运行任务(Run)
|
* 创建异步运行任务(Run)
|
||||||
* @param {string} agentId - 智能体ID
|
|
||||||
* @param {Object} data - run 请求体
|
* @param {Object} data - run 请求体
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
createAgentRun: (agentId, data) =>
|
createAgentRun: (data) =>
|
||||||
apiPost(`/api/chat/agent/${agentId}/runs`, {
|
apiPost('/api/chat/runs', {
|
||||||
query: data.query,
|
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
|
image_content: data.image_content || null
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@ -928,10 +928,8 @@ const sendMessage = async ({
|
|||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
query: text,
|
query: text,
|
||||||
config: {
|
thread_id: threadId,
|
||||||
thread_id: threadId,
|
agent_config_id: selectedAgentConfigId.value
|
||||||
agent_config_id: selectedAgentConfigId.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有图片,添加到请求中
|
// 如果有图片,添加到请求中
|
||||||
@ -940,7 +938,7 @@ const sendMessage = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await agentApi.sendAgentMessage(agentId, requestData, signal ? { signal } : undefined)
|
return await agentApi.sendAgentMessage(requestData, signal ? { signal } : undefined)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleChatError(error, 'send')
|
handleChatError(error, 'send')
|
||||||
throw error
|
throw error
|
||||||
@ -1170,12 +1168,10 @@ const handleSendMessage = async ({ image } = {}) => {
|
|||||||
resetOnGoingConv(threadId)
|
resetOnGoingConv(threadId)
|
||||||
threadState.isStreaming = true
|
threadState.isStreaming = true
|
||||||
try {
|
try {
|
||||||
const runResp = await agentApi.createAgentRun(currentAgentId.value, {
|
const runResp = await agentApi.createAgentRun({
|
||||||
query: text,
|
query: text,
|
||||||
config: {
|
agent_config_id: selectedAgentConfigId.value,
|
||||||
thread_id: threadId,
|
thread_id: threadId,
|
||||||
agent_config_id: selectedAgentConfigId.value
|
|
||||||
},
|
|
||||||
image_content: image?.imageContent
|
image_content: image?.imageContent
|
||||||
})
|
})
|
||||||
const runId = runResp?.run_id
|
const runId = runResp?.run_id
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user