feat: 完成项目助手与监控演示并修复聊天及云部署配置

This commit is contained in:
NathanAlex7
2026-09-11 15:56:28 +08:00
parent c57d291f65
commit af1a1027d7
75 changed files with 5215 additions and 101 deletions
+60 -4
View File
@@ -1,6 +1,7 @@
"""POST /api/chat — SSE streaming chat with Agent."""
import json
import asyncio
import os
import traceback
from pathlib import Path
@@ -12,6 +13,7 @@ from sse_starlette.sse import EventSourceResponse
from graph.agent import agent_manager
from graph.session_manager import session_manager
from graph.chat_model_options import chat_model_options
router = APIRouter()
@@ -47,6 +49,7 @@ async def _generate_title(session_id: str) -> str | None:
api_key=os.getenv("DEEPSEEK_API_KEY"),
api_base=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
temperature=0.3,
**chat_model_options(),
)
prompt = (
@@ -64,6 +67,27 @@ async def _generate_title(session_id: str) -> str | None:
return None
class EmptyChatResponse(RuntimeError):
pass
async def bounded_events(iterator, idle_seconds=75, total_seconds=180):
"""Bound stalls and total execution; never replay possible tool side effects."""
deadline = asyncio.get_running_loop().time() + total_seconds
try:
while True:
remaining = deadline - asyncio.get_running_loop().time()
if remaining <= 0:
raise TimeoutError()
try:
item = await asyncio.wait_for(anext(iterator), timeout=min(idle_seconds, remaining))
except StopAsyncIteration:
break
yield item
finally:
await iterator.aclose()
async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict, None]:
"""Generate SSE events from agent stream.
@@ -72,6 +96,10 @@ async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict,
emitted and a new segment begins. Each segment is saved as a
separate assistant message in the session history.
"""
segments: list[dict] = []
current_segment: dict = {"content": "", "tool_calls": []}
saved = False
completed = False
try:
# Use merged history for agent context (combines consecutive assistant msgs)
history = session_manager.load_session_for_agent(session_id)
@@ -81,7 +109,7 @@ async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict,
segments: list[dict] = [] # finalized segments
current_segment: dict = {"content": "", "tool_calls": []}
async for event in agent_manager.astream(message, history, session_id):
async for event in bounded_events(agent_manager.astream(message, history, session_id)):
event_type = event.get("type", "unknown")
if event_type == "retrieval":
@@ -137,6 +165,12 @@ async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict,
}
elif event_type == "done":
if not str(event.get("content") or "").strip():
raise EmptyChatResponse()
# Some providers deliver final content without token events.
if not current_segment["content"] and not any(s["content"] for s in segments):
current_segment["content"] = event["content"]
yield {"event":"token", "data":json.dumps({"content":event["content"]}, ensure_ascii=False)}
# Finalize the last segment
segments.append(current_segment)
@@ -145,10 +179,14 @@ async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict,
# Save each segment as a separate assistant message
for seg in segments:
if not seg["content"].strip() and not seg["tool_calls"]:
continue
tc = seg["tool_calls"] if seg["tool_calls"] else None
session_manager.save_message(
session_id, "assistant", seg["content"], tool_calls=tc
)
saved = True
completed = True
yield {
"event": "done",
@@ -160,7 +198,10 @@ async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict,
# Auto-generate title on first conversation
if is_first_message:
title = await _generate_title(session_id)
try:
title = await asyncio.wait_for(_generate_title(session_id), timeout=8)
except TimeoutError:
title = None
if title:
yield {
"event": "title",
@@ -170,11 +211,26 @@ async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict,
),
}
if not completed:
raise ConnectionError("stream ended before done")
except Exception as e:
traceback.print_exc()
is_timeout = isinstance(e, TimeoutError) or type(e).__name__ in ("APITimeoutError", "ReadTimeout", "ConnectTimeout")
code = "empty_response" if isinstance(e, EmptyChatResponse) else "timeout" if is_timeout else "chat_failed"
message_text = {
"empty_response":"模型未返回回答正文,可能只返回了推理字段。请检查聊天模型兼容配置后重试。",
"timeout":"模型响应超时,已停止本次等待。请检查服务后重试;不会自动重复执行工具。",
"chat_failed":"聊天未完成,请检查当前后端、模型连接或访问凭据后重试。已接收的内容会保留。",
}[code]
print(f"Chat failed: {type(e).__name__}")
if not saved:
session_manager.save_message(session_id, "user", message)
for seg in segments:
if seg["content"] or seg["tool_calls"]:
session_manager.save_message(session_id, "assistant", seg["content"], tool_calls=seg["tool_calls"] or None)
session_manager.save_message(session_id, "assistant", current_segment["content"] + "\n\n【未完成】" + message_text, tool_calls=current_segment["tool_calls"] or None)
yield {
"event": "error",
"data": json.dumps({"error": str(e)}, ensure_ascii=False),
"data": json.dumps({"error": message_text, "code": code}, ensure_ascii=False),
}