feat: 重构对话线程相关API,统一使用线程ID,移除不必要的智能体ID参数
This commit is contained in:
parent
9f51cfd96b
commit
efecc0692d
@ -54,7 +54,6 @@ class BaseAgent:
|
|||||||
"name": getattr(self, "name", "Unknown"),
|
"name": getattr(self, "name", "Unknown"),
|
||||||
"description": getattr(self, "description", "Unknown"),
|
"description": getattr(self, "description", "Unknown"),
|
||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
# "examples": metadata.get("examples", []),
|
|
||||||
"configurable_items": configurable_items,
|
"configurable_items": configurable_items,
|
||||||
"capabilities": getattr(self, "capabilities", []), # 智能体能力列表
|
"capabilities": getattr(self, "capabilities", []), # 智能体能力列表
|
||||||
}
|
}
|
||||||
|
|||||||
@ -866,16 +866,10 @@ async def stream_agent_resume(
|
|||||||
|
|
||||||
async def get_agent_state_view(
|
async def get_agent_state_view(
|
||||||
*,
|
*,
|
||||||
agent_id: str,
|
|
||||||
thread_id: str,
|
thread_id: str,
|
||||||
current_user_id: str,
|
current_user_id: str,
|
||||||
db,
|
db,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
if not agent_manager.get_agent(agent_id):
|
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_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":
|
||||||
@ -883,29 +877,10 @@ async def get_agent_state_view(
|
|||||||
|
|
||||||
raise HTTPException(status_code=404, detail="对话线程不存在")
|
raise HTTPException(status_code=404, detail="对话线程不存在")
|
||||||
|
|
||||||
agent = agent_manager.get_agent(agent_id)
|
agent = agent_manager.get_agent(conversation.agent_id)
|
||||||
graph = await agent.get_graph()
|
graph = await agent.get_graph()
|
||||||
langgraph_config = {"configurable": {"user_id": str(current_user_id), "thread_id": thread_id}}
|
langgraph_config = {"configurable": {"user_id": str(current_user_id), "thread_id": thread_id}}
|
||||||
state = await graph.aget_state(langgraph_config)
|
state = await graph.aget_state(langgraph_config)
|
||||||
agent_state = extract_agent_state(getattr(state, "values", {})) if state else {}
|
agent_state = extract_agent_state(getattr(state, "values", {})) if state else {}
|
||||||
|
|
||||||
# 如果 state 中没有 files,从附件构建
|
|
||||||
# 这确保了上传附件后立即可以在文件列表中看到文件
|
|
||||||
if not agent_state.get("files") or agent_state["files"] == {}:
|
|
||||||
try:
|
|
||||||
attachments = await conv_repo.get_attachments_by_thread_id(thread_id)
|
|
||||||
logger.info(f"[get_agent_state_view] found {len(attachments)} attachments in DB")
|
|
||||||
if attachments:
|
|
||||||
first_status = attachments[0].get("status")
|
|
||||||
first_has_markdown = bool(attachments[0].get("markdown"))
|
|
||||||
logger.info(
|
|
||||||
f"[get_agent_state_view] first attachment status: {first_status}, "
|
|
||||||
f"has markdown: {first_has_markdown}"
|
|
||||||
)
|
|
||||||
files = _build_state_files(attachments)
|
|
||||||
agent_state["files"] = files
|
|
||||||
logger.info(f"[get_agent_state_view] Built files from attachments: {len(files)} files")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to fetch attachments for thread {thread_id}: {e}")
|
|
||||||
|
|
||||||
return {"agent_state": agent_state}
|
return {"agent_state": agent_state}
|
||||||
|
|||||||
@ -478,3 +478,66 @@ async def delete_thread_attachment_view(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {"message": "附件已删除"}
|
return {"message": "附件已删除"}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_thread_history_view(
|
||||||
|
*,
|
||||||
|
thread_id: str,
|
||||||
|
current_user_id: str,
|
||||||
|
db: AsyncSession,
|
||||||
|
) -> dict:
|
||||||
|
"""获取对话历史消息,包含用户反馈状态"""
|
||||||
|
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="对话线程不存在")
|
||||||
|
|
||||||
|
messages = await conv_repo.get_messages_by_thread_id(thread_id)
|
||||||
|
|
||||||
|
history: list[dict] = []
|
||||||
|
role_type_map = {"user": "human", "assistant": "ai", "tool": "tool", "system": "system"}
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
user_feedback = None
|
||||||
|
if msg.feedbacks:
|
||||||
|
for feedback in msg.feedbacks:
|
||||||
|
if feedback.user_id == str(current_user_id):
|
||||||
|
user_feedback = {
|
||||||
|
"id": feedback.id,
|
||||||
|
"rating": feedback.rating,
|
||||||
|
"reason": feedback.reason,
|
||||||
|
"created_at": feedback.created_at.isoformat() if feedback.created_at else None,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
msg_dict = {
|
||||||
|
"id": msg.id,
|
||||||
|
"type": role_type_map.get(msg.role, msg.role),
|
||||||
|
"content": msg.content,
|
||||||
|
"created_at": msg.created_at.isoformat() if msg.created_at else None,
|
||||||
|
"error_type": msg.extra_metadata.get("error_type") if msg.extra_metadata else None,
|
||||||
|
"error_message": msg.extra_metadata.get("error_message") if msg.extra_metadata else None,
|
||||||
|
"extra_metadata": msg.extra_metadata,
|
||||||
|
"message_type": msg.message_type,
|
||||||
|
"image_content": msg.image_content,
|
||||||
|
"feedback": user_feedback,
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.tool_calls:
|
||||||
|
msg_dict["tool_calls"] = [
|
||||||
|
{
|
||||||
|
"id": str(tc.id),
|
||||||
|
"name": tc.tool_name,
|
||||||
|
"function": {"name": tc.tool_name},
|
||||||
|
"args": tc.tool_input or {},
|
||||||
|
"tool_call_result": {"content": (tc.tool_output or "")} if tc.status == "success" else None,
|
||||||
|
"status": tc.status,
|
||||||
|
"error_message": tc.error_message,
|
||||||
|
}
|
||||||
|
for tc in msg.tool_calls
|
||||||
|
]
|
||||||
|
|
||||||
|
history.append(msg_dict)
|
||||||
|
|
||||||
|
logger.info(f"Loaded {len(history)} messages with feedback for thread {thread_id}")
|
||||||
|
return {"history": history}
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
from fastapi import HTTPException
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from yuxi.agents.buildin import agent_manager
|
|
||||||
from yuxi.repositories.conversation_repository import ConversationRepository
|
|
||||||
from yuxi.utils.logging_config import logger
|
|
||||||
|
|
||||||
|
|
||||||
async def get_agent_history_view(
|
|
||||||
*,
|
|
||||||
agent_id: str,
|
|
||||||
thread_id: str,
|
|
||||||
current_user_id: str,
|
|
||||||
db: AsyncSession,
|
|
||||||
) -> dict:
|
|
||||||
if not agent_manager.get_agent(agent_id):
|
|
||||||
raise HTTPException(status_code=404, detail=f"智能体 {agent_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="对话线程不存在")
|
|
||||||
|
|
||||||
messages = await conv_repo.get_messages_by_thread_id(thread_id)
|
|
||||||
|
|
||||||
history: list[dict] = []
|
|
||||||
role_type_map = {"user": "human", "assistant": "ai", "tool": "tool", "system": "system"}
|
|
||||||
|
|
||||||
for msg in messages:
|
|
||||||
user_feedback = None
|
|
||||||
if msg.feedbacks:
|
|
||||||
for feedback in msg.feedbacks:
|
|
||||||
if feedback.user_id == str(current_user_id):
|
|
||||||
user_feedback = {
|
|
||||||
"id": feedback.id,
|
|
||||||
"rating": feedback.rating,
|
|
||||||
"reason": feedback.reason,
|
|
||||||
"created_at": feedback.created_at.isoformat() if feedback.created_at else None,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
msg_dict = {
|
|
||||||
"id": msg.id,
|
|
||||||
"type": role_type_map.get(msg.role, msg.role),
|
|
||||||
"content": msg.content,
|
|
||||||
"created_at": msg.created_at.isoformat() if msg.created_at else None,
|
|
||||||
"error_type": msg.extra_metadata.get("error_type") if msg.extra_metadata else None,
|
|
||||||
"error_message": msg.extra_metadata.get("error_message") if msg.extra_metadata else None,
|
|
||||||
"extra_metadata": msg.extra_metadata,
|
|
||||||
"message_type": msg.message_type,
|
|
||||||
"image_content": msg.image_content,
|
|
||||||
"feedback": user_feedback,
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.tool_calls:
|
|
||||||
msg_dict["tool_calls"] = [
|
|
||||||
{
|
|
||||||
"id": str(tc.id),
|
|
||||||
"name": tc.tool_name,
|
|
||||||
"function": {"name": tc.tool_name},
|
|
||||||
"args": tc.tool_input or {},
|
|
||||||
"tool_call_result": {"content": (tc.tool_output or "")} if tc.status == "success" else None,
|
|
||||||
"status": tc.status,
|
|
||||||
"error_message": tc.error_message,
|
|
||||||
}
|
|
||||||
for tc in msg.tool_calls
|
|
||||||
]
|
|
||||||
|
|
||||||
history.append(msg_dict)
|
|
||||||
|
|
||||||
logger.info(f"Loaded {len(history)} messages with feedback for thread {thread_id}")
|
|
||||||
return {"history": history}
|
|
||||||
@ -22,10 +22,12 @@ from yuxi.services.agent_run_service import (
|
|||||||
get_agent_run_view,
|
get_agent_run_view,
|
||||||
stream_agent_run_events,
|
stream_agent_run_events,
|
||||||
)
|
)
|
||||||
|
from yuxi.repositories.conversation_repository import ConversationRepository
|
||||||
from yuxi.services.conversation_service import (
|
from yuxi.services.conversation_service import (
|
||||||
create_thread_view,
|
create_thread_view,
|
||||||
delete_thread_attachment_view,
|
delete_thread_attachment_view,
|
||||||
delete_thread_view,
|
delete_thread_view,
|
||||||
|
get_thread_history_view,
|
||||||
list_thread_attachments_view,
|
list_thread_attachments_view,
|
||||||
list_threads_view,
|
list_threads_view,
|
||||||
update_thread_view,
|
update_thread_view,
|
||||||
@ -37,7 +39,6 @@ from yuxi.services.thread_files_service import (
|
|||||||
resolve_thread_artifact_view,
|
resolve_thread_artifact_view,
|
||||||
)
|
)
|
||||||
from yuxi.services.feedback_service import get_message_feedback_view, submit_message_feedback_view
|
from yuxi.services.feedback_service import get_message_feedback_view, submit_message_feedback_view
|
||||||
from yuxi.services.history_query_service import get_agent_history_view
|
|
||||||
from yuxi.repositories.agent_config_repository import AgentConfigRepository
|
from yuxi.repositories.agent_config_repository import AgentConfigRepository
|
||||||
from yuxi.utils.logging_config import logger
|
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
|
||||||
@ -170,20 +171,7 @@ async def call(query: str = Body(...), meta: dict = Body(None), current_user: Us
|
|||||||
async def get_agent(current_user: User = Depends(get_required_user)):
|
async def get_agent(current_user: User = Depends(get_required_user)):
|
||||||
"""获取所有可用智能体的基本信息(需要登录)"""
|
"""获取所有可用智能体的基本信息(需要登录)"""
|
||||||
agents_info = await agent_manager.get_agents_info(include_configurable_items=False)
|
agents_info = await agent_manager.get_agents_info(include_configurable_items=False)
|
||||||
|
return {"agents": agents_info}
|
||||||
# Return agents with basic information (without configurable_items for performance)
|
|
||||||
agents = [
|
|
||||||
{
|
|
||||||
"id": agent_info["id"],
|
|
||||||
"name": agent_info.get("name", "Unknown"),
|
|
||||||
"description": agent_info.get("description", ""),
|
|
||||||
"metadata": agent_info.get("metadata", {}),
|
|
||||||
"capabilities": agent_info.get("capabilities", []), # 智能体能力列表
|
|
||||||
}
|
|
||||||
for agent_info in agents_info
|
|
||||||
]
|
|
||||||
|
|
||||||
return {"agents": agents}
|
|
||||||
|
|
||||||
|
|
||||||
@chat.get("/agent/{agent_id}")
|
@chat.get("/agent/{agent_id}")
|
||||||
@ -197,14 +185,7 @@ async def get_single_agent(agent_id: str, current_user: User = Depends(get_requi
|
|||||||
# 获取智能体的完整信息(包含 configurable_items)
|
# 获取智能体的完整信息(包含 configurable_items)
|
||||||
agent_info = await agent.get_info()
|
agent_info = await agent.get_info()
|
||||||
|
|
||||||
return {
|
return agent_info
|
||||||
"id": agent_info["id"],
|
|
||||||
"name": agent_info.get("name", "Unknown"),
|
|
||||||
"description": agent_info.get("description", ""),
|
|
||||||
"metadata": agent_info.get("metadata", {}),
|
|
||||||
"configurable_items": agent_info.get("configurable_items", []),
|
|
||||||
"capabilities": agent_info.get("capabilities", []),
|
|
||||||
}
|
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@ -219,9 +200,6 @@ async def list_agent_configs(
|
|||||||
current_user: User = Depends(get_required_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
if not current_user.department_id:
|
|
||||||
raise HTTPException(status_code=400, detail="当前用户未绑定部门")
|
|
||||||
|
|
||||||
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} 不存在")
|
||||||
|
|
||||||
@ -256,9 +234,6 @@ async def get_agent_config_profile(
|
|||||||
current_user: User = Depends(get_required_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
if not current_user.department_id:
|
|
||||||
raise HTTPException(status_code=400, detail="当前用户未绑定部门")
|
|
||||||
|
|
||||||
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} 不存在")
|
||||||
|
|
||||||
@ -277,9 +252,6 @@ async def create_agent_config_profile(
|
|||||||
current_user: User = Depends(get_admin_user),
|
current_user: User = Depends(get_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
if not current_user.department_id:
|
|
||||||
raise HTTPException(status_code=400, detail="当前用户未绑定部门")
|
|
||||||
|
|
||||||
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} 不存在")
|
||||||
|
|
||||||
@ -309,9 +281,6 @@ async def update_agent_config_profile(
|
|||||||
current_user: User = Depends(get_admin_user),
|
current_user: User = Depends(get_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
if not current_user.department_id:
|
|
||||||
raise HTTPException(status_code=400, detail="当前用户未绑定部门")
|
|
||||||
|
|
||||||
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} 不存在")
|
||||||
|
|
||||||
@ -340,9 +309,6 @@ async def set_agent_config_default(
|
|||||||
current_user: User = Depends(get_admin_user),
|
current_user: User = Depends(get_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
if not current_user.department_id:
|
|
||||||
raise HTTPException(status_code=400, detail="当前用户未绑定部门")
|
|
||||||
|
|
||||||
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} 不存在")
|
||||||
|
|
||||||
@ -362,9 +328,6 @@ async def delete_agent_config_profile(
|
|||||||
current_user: User = Depends(get_admin_user),
|
current_user: User = Depends(get_admin_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
if not current_user.department_id:
|
|
||||||
raise HTTPException(status_code=400, detail="当前用户未绑定部门")
|
|
||||||
|
|
||||||
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} 不存在")
|
||||||
|
|
||||||
@ -488,17 +451,6 @@ async def stream_run_events(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@chat.get("/thread/{thread_id}/active_run")
|
|
||||||
async def get_thread_active_run(
|
|
||||||
thread_id: str,
|
|
||||||
current_user: User = Depends(get_required_user),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
):
|
|
||||||
"""获取当前会话活跃 run(需要登录)"""
|
|
||||||
return await get_active_run_by_thread(thread_id=thread_id, current_user_id=str(current_user.id), db=db)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# > === 模型管理分组 ===
|
# > === 模型管理分组 ===
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@ -520,10 +472,9 @@ async def update_chat_models(model_provider: str, model_names: list[str], curren
|
|||||||
return {"models": conf.model_names[model_provider].models}
|
return {"models": conf.model_names[model_provider].models}
|
||||||
|
|
||||||
|
|
||||||
@chat.post("/agent/{agent_id}/resume")
|
@chat.post("/thread/{thread_id}/resume")
|
||||||
async def resume_agent_chat(
|
async def resume_thread_chat(
|
||||||
agent_id: str,
|
thread_id: str,
|
||||||
thread_id: str = Body(...),
|
|
||||||
approved: bool | None = Body(None),
|
approved: bool | None = Body(None),
|
||||||
answer: dict | None = Body(None),
|
answer: dict | None = Body(None),
|
||||||
config: dict = Body({}),
|
config: dict = Body({}),
|
||||||
@ -532,6 +483,13 @@ async def resume_agent_chat(
|
|||||||
):
|
):
|
||||||
"""恢复被人工审批中断的对话(需要登录)"""
|
"""恢复被人工审批中断的对话(需要登录)"""
|
||||||
|
|
||||||
|
# 验证 thread 存在且属于当前用户
|
||||||
|
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="对话线程不存在")
|
||||||
|
agent_id = conversation.agent_id
|
||||||
|
|
||||||
def normalize_resume_input(raw_answer: Any, raw_approved: bool | None) -> Any:
|
def normalize_resume_input(raw_answer: Any, raw_approved: bool | None) -> Any:
|
||||||
def normalize_single_answer(value: Any) -> Any:
|
def normalize_single_answer(value: Any) -> Any:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
@ -602,6 +560,7 @@ 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,
|
||||||
@ -613,35 +572,42 @@ async def resume_agent_chat(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@chat.get("/agent/{agent_id}/history")
|
@chat.get("/thread/{thread_id}/active_run")
|
||||||
async def get_agent_history(
|
async def get_thread_active_run(
|
||||||
agent_id: str, thread_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
|
thread_id: str,
|
||||||
|
current_user: User = Depends(get_required_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取智能体历史消息(需要登录)- 包含用户反馈状态"""
|
"""获取当前会话活跃 run(需要登录)"""
|
||||||
|
return await get_active_run_by_thread(thread_id=thread_id, current_user_id=str(current_user.id), db=db)
|
||||||
|
|
||||||
|
|
||||||
|
@chat.get("/thread/{thread_id}/history")
|
||||||
|
async def get_thread_history(
|
||||||
|
thread_id: str, current_user: User = Depends(get_required_user), db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""获取对话历史消息(需要登录)- 包含用户反馈状态"""
|
||||||
try:
|
try:
|
||||||
return await get_agent_history_view(
|
return await get_thread_history_view(
|
||||||
agent_id=agent_id,
|
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
current_user_id=str(current_user.id),
|
current_user_id=str(current_user.id),
|
||||||
db=db,
|
db=db,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取智能体历史消息出错: {e}, {traceback.format_exc()}")
|
logger.error(f"获取对话历史消息出错: {e}, {traceback.format_exc()}")
|
||||||
raise HTTPException(status_code=500, detail=f"获取智能体历史消息出错: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"获取对话历史消息出错: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@chat.get("/agent/{agent_id}/state")
|
@chat.get("/thread/{thread_id}/state")
|
||||||
async def get_agent_state(
|
async def get_thread_state(
|
||||||
agent_id: str,
|
|
||||||
thread_id: str,
|
thread_id: str,
|
||||||
current_user: User = Depends(get_required_user),
|
current_user: User = Depends(get_required_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取智能体当前状态(需要登录)"""
|
"""获取对话当前状态(需要登录)"""
|
||||||
try:
|
try:
|
||||||
return await get_agent_state_view(
|
return await get_agent_state_view(
|
||||||
agent_id=agent_id,
|
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
current_user_id=str(current_user.id),
|
current_user_id=str(current_user.id),
|
||||||
db=db,
|
db=db,
|
||||||
@ -649,8 +615,8 @@ async def get_agent_state(
|
|||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取AgentState出错: {e}, {traceback.format_exc()}")
|
logger.error(f"获取对话状态出错: {e}, {traceback.format_exc()}")
|
||||||
raise HTTPException(status_code=500, detail=f"获取AgentState出错: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"获取对话状态出错: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
# ==================== 线程管理 API ====================
|
# ==================== 线程管理 API ====================
|
||||||
|
|||||||
@ -131,6 +131,11 @@ async def get_required_user(user: User | None = Depends(get_current_user)):
|
|||||||
detail="请登录后再访问",
|
detail="请登录后再访问",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
if not user.department_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="当前用户未绑定部门",
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,15 +4,52 @@ Integration tests for batch question resume payload validation.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||||||
|
|
||||||
|
|
||||||
async def test_resume_rejects_non_dict_answer(test_client, admin_headers):
|
async def _get_agent_id(test_client, headers):
|
||||||
|
"""Get an agent ID for testing."""
|
||||||
|
agents_response = await test_client.get("/api/chat/agent", headers=headers)
|
||||||
|
if agents_response.status_code != 200:
|
||||||
|
pytest.skip(f"Failed to get agents: {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.")
|
||||||
|
|
||||||
|
return agent_id
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_test_thread(test_client, admin_headers) -> str:
|
||||||
|
"""Helper to create a thread for resume testing using /thread endpoint."""
|
||||||
|
agent_id = await _get_agent_id(test_client, admin_headers)
|
||||||
|
thread_id = f"test-thread-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
# Create thread via /thread endpoint
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/api/chat/agent/dummy-agent/resume",
|
"/api/chat/thread",
|
||||||
json={"thread_id": "thread-test", "answer": "approve"},
|
json={"agent_id": agent_id, "title": "Test thread for resume"},
|
||||||
|
headers=admin_headers,
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
pytest.skip(f"Failed to create thread: {response.text}")
|
||||||
|
|
||||||
|
thread_data = response.json()
|
||||||
|
return thread_data.get("id") or thread_id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_resume_rejects_non_dict_answer(test_client, admin_headers):
|
||||||
|
"""Test that non-dict answer is rejected at the API level."""
|
||||||
|
response = await test_client.post(
|
||||||
|
"/api/chat/thread/fake-thread-id/resume",
|
||||||
|
json={"thread_id": "fake-thread-id", "answer": "approve"},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,9 +58,11 @@ async def test_resume_rejects_non_dict_answer(test_client, admin_headers):
|
|||||||
|
|
||||||
|
|
||||||
async def test_resume_rejects_empty_answer_map(test_client, admin_headers):
|
async def test_resume_rejects_empty_answer_map(test_client, admin_headers):
|
||||||
|
"""Test that empty answer map is rejected."""
|
||||||
|
thread_id = await _create_test_thread(test_client, admin_headers)
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/api/chat/agent/dummy-agent/resume",
|
f"/api/chat/thread/{thread_id}/resume",
|
||||||
json={"thread_id": "thread-test", "answer": {}},
|
json={"thread_id": thread_id, "answer": {}},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,9 +71,11 @@ async def test_resume_rejects_empty_answer_map(test_client, admin_headers):
|
|||||||
|
|
||||||
|
|
||||||
async def test_resume_rejects_empty_question_id(test_client, admin_headers):
|
async def test_resume_rejects_empty_question_id(test_client, admin_headers):
|
||||||
|
"""Test that empty question_id is rejected."""
|
||||||
|
thread_id = await _create_test_thread(test_client, admin_headers)
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/api/chat/agent/dummy-agent/resume",
|
f"/api/chat/thread/{thread_id}/resume",
|
||||||
json={"thread_id": "thread-test", "answer": {"": "选项A"}},
|
json={"thread_id": thread_id, "answer": {"": "选项A"}},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,9 +84,11 @@ async def test_resume_rejects_empty_question_id(test_client, admin_headers):
|
|||||||
|
|
||||||
|
|
||||||
async def test_resume_rejects_empty_answer_text(test_client, admin_headers):
|
async def test_resume_rejects_empty_answer_text(test_client, admin_headers):
|
||||||
|
"""Test that empty answer text is rejected."""
|
||||||
|
thread_id = await _create_test_thread(test_client, admin_headers)
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/api/chat/agent/dummy-agent/resume",
|
f"/api/chat/thread/{thread_id}/resume",
|
||||||
json={"thread_id": "thread-test", "answer": {"q1": " "}},
|
json={"thread_id": thread_id, "answer": {"q1": " "}},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,11 +97,14 @@ async def test_resume_rejects_empty_answer_text(test_client, admin_headers):
|
|||||||
|
|
||||||
|
|
||||||
async def test_resume_accepts_batch_answer_map(test_client, admin_headers):
|
async def test_resume_accepts_batch_answer_map(test_client, admin_headers):
|
||||||
|
"""Test that valid batch answer map is accepted."""
|
||||||
|
thread_id = await _create_test_thread(test_client, admin_headers)
|
||||||
response = await test_client.post(
|
response = await test_client.post(
|
||||||
"/api/chat/agent/dummy-agent/resume",
|
f"/api/chat/thread/{thread_id}/resume",
|
||||||
json={"thread_id": "thread-test", "answer": {"q1": "选项A", "q2": ["选项B", "选项C"]}},
|
json={"thread_id": thread_id, "answer": {"q1": "选项A", "q2": ["选项B", "选项C"]}},
|
||||||
headers=admin_headers,
|
headers=admin_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 通过参数校验后会进入流式逻辑,dummy-agent 不存在时也会以 200 返回 error chunk。
|
# Will return 200 if conversation exists and is in interrupt state,
|
||||||
assert response.status_code == 200
|
# or 404 if conversation not found
|
||||||
|
assert response.status_code in (200, 404)
|
||||||
|
|||||||
@ -90,8 +90,8 @@ export const agentApi = {
|
|||||||
* @param {string} threadId - 会话ID
|
* @param {string} threadId - 会话ID
|
||||||
* @returns {Promise} - 历史消息
|
* @returns {Promise} - 历史消息
|
||||||
*/
|
*/
|
||||||
getAgentHistory: (agentId, threadId) =>
|
getAgentHistory: (threadId) =>
|
||||||
apiGet(`/api/chat/agent/${agentId}/history?thread_id=${threadId}`),
|
apiGet(`/api/chat/thread/${threadId}/history`),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定会话的 AgentState
|
* 获取指定会话的 AgentState
|
||||||
@ -99,8 +99,8 @@ export const agentApi = {
|
|||||||
* @param {string} threadId - 会话ID
|
* @param {string} threadId - 会话ID
|
||||||
* @returns {Promise} - AgentState
|
* @returns {Promise} - AgentState
|
||||||
*/
|
*/
|
||||||
getAgentState: (agentId, threadId) =>
|
getAgentState: (threadId) =>
|
||||||
apiGet(`/api/chat/agent/${agentId}/state?thread_id=${threadId}`),
|
apiGet(`/api/chat/thread/${threadId}/state`),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit feedback for a message
|
* Submit feedback for a message
|
||||||
@ -168,14 +168,14 @@ export const agentApi = {
|
|||||||
* @param {Object} options - 可选参数(signal, headers等)
|
* @param {Object} options - 可选参数(signal, headers等)
|
||||||
* @returns {Promise} - 恢复响应流
|
* @returns {Promise} - 恢复响应流
|
||||||
*/
|
*/
|
||||||
resumeAgentChat: (agentId, data, options = {}) => {
|
resumeAgentChat: (threadId, 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}/resume`, {
|
return fetch(`/api/chat/thread/${threadId}/resume`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
@ -769,7 +769,7 @@ const fetchThreadMessages = async ({ agentId, threadId, delay = 0 }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await agentApi.getAgentHistory(agentId, threadId)
|
const response = await agentApi.getAgentHistory(threadId)
|
||||||
threadMessages.value[threadId] = response.history || []
|
threadMessages.value[threadId] = response.history || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleChatError(error, 'load')
|
handleChatError(error, 'load')
|
||||||
@ -808,9 +808,9 @@ const refreshThreadFilesAndAttachments = async (threadId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchAgentState = async (agentId, threadId) => {
|
const fetchAgentState = async (agentId, threadId) => {
|
||||||
if (!agentId || !threadId) return
|
if (!threadId) return
|
||||||
try {
|
try {
|
||||||
const res = await agentApi.getAgentState(agentId, threadId)
|
const res = await agentApi.getAgentState(threadId)
|
||||||
const targetChatId = currentChatId.value || threadId
|
const targetChatId = currentChatId.value || threadId
|
||||||
const ts = getThreadState(targetChatId)
|
const ts = getThreadState(targetChatId)
|
||||||
if (ts) {
|
if (ts) {
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await agentApi.resumeAgentChat(currentAgentId, requestBody, {
|
const response = await agentApi.resumeAgentChat(threadId, requestBody, {
|
||||||
signal: threadState.streamAbortController?.signal
|
signal: threadState.streamAbortController?.signal
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user