feat: 完成项目助手与监控演示并修复聊天及云部署配置
This commit is contained in:
+60
-4
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user