fix(chat): 持久化会话与消息

This commit is contained in:
2026-09-05 10:12:09 +08:00
parent d15ceafbe0
commit feb8cc651f
15 changed files with 726 additions and 125 deletions
+123 -3
View File
@@ -1,4 +1,5 @@
import asyncio
import json
from collections.abc import AsyncIterator
from contextlib import aclosing
from datetime import datetime, timezone
@@ -15,6 +16,10 @@ from app.contracts import (
AgentRunListResponse,
AgentTraceResponse,
ChatRequest,
ChatMessageListResponse,
Conversation,
ConversationCreateRequest,
ConversationListResponse,
BenchmarkDatasetListResponse,
BenchmarkEventType,
BenchmarkKind,
@@ -321,6 +326,40 @@ async def clear_search_history() -> dict[str, list[str]]:
return {"queries": []}
@router.get("/chat/conversations", response_model=ConversationListResponse, tags=["Chat"])
async def list_chat_conversations(
limit: int = Query(default=50, ge=1, le=100), offset: int = Query(default=0, ge=0)
) -> ConversationListResponse:
from app.services import chat_history
items, total = chat_history.list_conversations(limit, offset)
return ConversationListResponse(items=items, page=PageMeta(total=total, limit=limit, offset=offset))
@router.post("/chat/conversations", response_model=Conversation, status_code=201, tags=["Chat"])
async def create_chat_conversation(request: ConversationCreateRequest) -> Conversation:
from app.services import chat_history
return chat_history.create(request.title, request.conversation_id)
@router.get("/chat/conversations/{conversation_id}/messages", response_model=ChatMessageListResponse, tags=["Chat"])
async def list_chat_messages(
conversation_id: str,
limit: int = Query(default=500, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
) -> ChatMessageListResponse:
from app.services import chat_history
items, total = chat_history.list_messages(conversation_id, limit, offset)
return ChatMessageListResponse(items=items, page=PageMeta(total=total, limit=limit, offset=offset))
@router.delete("/chat/conversations/{conversation_id}", response_model=OperationResponse, tags=["Chat"])
async def delete_chat_conversation(conversation_id: str) -> OperationResponse:
from app.services import chat_history
if not chat_history.delete(conversation_id):
raise ApiError(404, "CONVERSATION_NOT_FOUND", "conversation not found", {"conversation_id": conversation_id})
return OperationResponse(status="completed", resource_id=conversation_id, message="deleted")
@router.post(
"/chat",
response_class=StreamingResponse,
@@ -333,14 +372,38 @@ async def clear_search_history() -> dict[str, list[str]]:
tags=["Chat"],
)
async def chat(request: ChatRequest) -> StreamingResponse:
from app.services import chat_history
conversation_id = request.conversation_id
assistant_message_id = request.assistant_message_id or f"message_{uuid4().hex}"
if conversation_id:
user_message = next(
(message for message in reversed(request.messages) if message.role.value == "user" and message.content.strip()),
None,
)
if user_message is not None:
chat_history.append_message(
conversation_id,
message_id=request.user_message_id or f"message_{uuid4().hex}",
role="user",
content=user_message.content,
title=request.conversation_title or user_message.content[:30],
)
provider = provider_or_404(request.provider_id)
async def stream() -> AsyncIterator[str]:
sequence = 0
assistant_content = ""
assistant_thinking = ""
citations: list[dict] = []
tool_calls: list[dict] = []
argument_buffers: dict[str, str] = {}
usage: dict | None = None
try:
from app.services.chat_context import prepare
grounded_request, citations = await prepare(request)
for citation in citations:
grounded_request, grounded_citations = await prepare(request)
for citation in grounded_citations:
citations.append(citation)
event = ModelEvent(event=ModelEventType.citation, sequence=sequence,
data=citation, timestamp=utc_now())
sequence += 1
@@ -349,13 +412,58 @@ async def chat(request: ChatRequest) -> StreamingResponse:
async for event in events:
event = event.model_copy(update={"sequence": sequence})
sequence += 1
if event.event == ModelEventType.text_delta:
assistant_content += str(event.data.get("text", ""))
elif event.event == ModelEventType.thinking_delta:
assistant_thinking += str(event.data.get("text", ""))
elif event.event == ModelEventType.tool_call_start:
tool_calls.append({
"tool_call_id": str(event.data.get("tool_call_id", "")),
"name": str(event.data.get("name", "unknown")),
"parameters": event.data.get("arguments") if isinstance(event.data.get("arguments"), dict) else {},
"status": "running",
})
elif event.event == ModelEventType.tool_call_delta:
call_id = str(event.data.get("tool_call_id", ""))
call = next((item for item in tool_calls if item["tool_call_id"] == call_id), None)
if call is not None:
delta = event.data.get("arguments_delta")
if isinstance(delta, str):
argument_buffers[call_id] = argument_buffers.get(call_id, "") + delta
try:
parsed_arguments = json.loads(argument_buffers[call_id])
if isinstance(parsed_arguments, dict):
call["parameters"] = parsed_arguments
except ValueError:
pass
arguments = event.data.get("arguments")
if isinstance(arguments, dict):
call["parameters"].update(arguments)
elif event.event == ModelEventType.tool_call_end:
call_id = str(event.data.get("tool_call_id", ""))
call = next((item for item in tool_calls if item["tool_call_id"] == call_id), None)
if call is not None:
call["status"] = "completed"
elif event.event == ModelEventType.usage:
input_tokens = int(event.data.get("input_tokens", 0))
output_tokens = int(event.data.get("output_tokens", 0))
usage = {"input_tokens": input_tokens, "output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens}
elif event.event == ModelEventType.error:
if assistant_content:
assistant_content += "\n\n"
assistant_content += str(event.data.get("message", "Model generation failed."))
yield as_sse(event.event.value, event.model_dump_json())
except Exception as exc:
failure_message = exc.message if isinstance(exc, ApiError) else "知识库检索或模型生成失败,请检查服务状态。"
if assistant_content:
assistant_content += "\n\n"
assistant_content += failure_message
error = ModelEvent(
event=ModelEventType.error,
sequence=sequence,
data={"code": exc.code if isinstance(exc, ApiError) else "CHAT_FAILED",
"message": exc.message if isinstance(exc, ApiError) else "知识库检索或模型生成失败,请检查服务状态。"},
"message": failure_message},
timestamp=utc_now(),
)
done = ModelEvent(
@@ -364,6 +472,18 @@ async def chat(request: ChatRequest) -> StreamingResponse:
)
yield as_sse(error.event.value, error.model_dump_json())
yield as_sse(done.event.value, done.model_dump_json())
finally:
if conversation_id and (assistant_content or assistant_thinking or citations or tool_calls):
chat_history.append_message(
conversation_id,
message_id=assistant_message_id,
role="assistant",
content=assistant_content,
thinking=assistant_thinking or None,
citations=citations,
tool_calls=tool_calls,
usage=usage,
)
return StreamingResponse(stream(), media_type="text/event-stream")