chore: mini-openclaw 初始版本
Next.js 前端 + FastAPI/LangChain 后端透明 agent平台 - 含 .gitignore, 敏感文件(sessions数据/.env/venv等)已排除 - 待后续添加 trajectory 自进化机制
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""POST /api/chat — SSE streaming chat with Agent."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from graph.agent import agent_manager
|
||||
from graph.session_manager import session_manager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
session_id: str = "default"
|
||||
stream: bool = True
|
||||
|
||||
|
||||
async def _generate_title(session_id: str) -> str | None:
|
||||
"""Generate a title for a session using DeepSeek. Returns title or None."""
|
||||
try:
|
||||
messages = session_manager.load_session(session_id)
|
||||
first_user = ""
|
||||
first_assistant = ""
|
||||
for msg in messages:
|
||||
if msg["role"] == "user" and not first_user:
|
||||
first_user = msg["content"][:200]
|
||||
elif msg["role"] == "assistant" and not first_assistant:
|
||||
first_assistant = msg["content"][:200]
|
||||
if first_user and first_assistant:
|
||||
break
|
||||
|
||||
if not first_user:
|
||||
return None
|
||||
|
||||
from langchain_deepseek import ChatDeepSeek
|
||||
from langchain_core.messages import HumanMessage as HM
|
||||
|
||||
llm = ChatDeepSeek(
|
||||
model=os.getenv("DEEPSEEK_MODEL", "deepseek-chat"),
|
||||
api_key=os.getenv("DEEPSEEK_API_KEY"),
|
||||
api_base=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
||||
temperature=0.3,
|
||||
)
|
||||
|
||||
prompt = (
|
||||
f"根据以下对话内容,生成一个不超过10个字的中文标题,只输出标题文本,不要加引号或标点。\n\n"
|
||||
f"用户: {first_user}\n"
|
||||
f"助手: {first_assistant}"
|
||||
)
|
||||
|
||||
result = await llm.ainvoke([HM(content=prompt)])
|
||||
title = result.content.strip().strip('"\'""''')[:20]
|
||||
session_manager.update_title(session_id, title)
|
||||
return title
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
async def event_generator(message: str, session_id: str) -> AsyncGenerator[dict, None]:
|
||||
"""Generate SSE events from agent stream.
|
||||
|
||||
Tracks multiple response segments — each time the agent finishes
|
||||
tool calls and starts generating new text, a new_response event is
|
||||
emitted and a new segment begins. Each segment is saved as a
|
||||
separate assistant message in the session history.
|
||||
"""
|
||||
try:
|
||||
# Use merged history for agent context (combines consecutive assistant msgs)
|
||||
history = session_manager.load_session_for_agent(session_id)
|
||||
is_first_message = len(history) == 0
|
||||
|
||||
# Per-segment tracking
|
||||
segments: list[dict] = [] # finalized segments
|
||||
current_segment: dict = {"content": "", "tool_calls": []}
|
||||
|
||||
async for event in agent_manager.astream(message, history):
|
||||
event_type = event.get("type", "unknown")
|
||||
|
||||
if event_type == "retrieval":
|
||||
yield {
|
||||
"event": "retrieval",
|
||||
"data": json.dumps(
|
||||
{"query": event["query"], "results": event["results"]},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
|
||||
elif event_type == "token":
|
||||
current_segment["content"] += event["content"]
|
||||
yield {
|
||||
"event": "token",
|
||||
"data": json.dumps({"content": event["content"]}, ensure_ascii=False),
|
||||
}
|
||||
|
||||
elif event_type == "new_response":
|
||||
# Finalize current segment and start a new one
|
||||
segments.append(current_segment)
|
||||
current_segment = {"content": "", "tool_calls": []}
|
||||
yield {
|
||||
"event": "new_response",
|
||||
"data": json.dumps({}, ensure_ascii=False),
|
||||
}
|
||||
|
||||
elif event_type == "tool_start":
|
||||
current_segment["tool_calls"].append({
|
||||
"tool": event["tool"],
|
||||
"input": event.get("input", ""),
|
||||
})
|
||||
yield {
|
||||
"event": "tool_start",
|
||||
"data": json.dumps(
|
||||
{"tool": event["tool"], "input": event["input"]},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
|
||||
elif event_type == "tool_end":
|
||||
# Match with current segment's tool_calls to add output
|
||||
for tc in reversed(current_segment["tool_calls"]):
|
||||
if tc["tool"] == event["tool"] and "output" not in tc:
|
||||
tc["output"] = event["output"]
|
||||
break
|
||||
yield {
|
||||
"event": "tool_end",
|
||||
"data": json.dumps(
|
||||
{"tool": event["tool"], "output": event["output"]},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
|
||||
elif event_type == "done":
|
||||
# Finalize the last segment
|
||||
segments.append(current_segment)
|
||||
|
||||
# Save user message once
|
||||
session_manager.save_message(session_id, "user", message)
|
||||
|
||||
# Save each segment as a separate assistant message
|
||||
for seg in segments:
|
||||
tc = seg["tool_calls"] if seg["tool_calls"] else None
|
||||
session_manager.save_message(
|
||||
session_id, "assistant", seg["content"], tool_calls=tc
|
||||
)
|
||||
|
||||
yield {
|
||||
"event": "done",
|
||||
"data": json.dumps(
|
||||
{"content": event["content"], "session_id": session_id},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
|
||||
# Auto-generate title on first conversation
|
||||
if is_first_message:
|
||||
title = await _generate_title(session_id)
|
||||
if title:
|
||||
yield {
|
||||
"event": "title",
|
||||
"data": json.dumps(
|
||||
{"session_id": session_id, "title": title},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield {
|
||||
"event": "error",
|
||||
"data": json.dumps({"error": str(e)}, ensure_ascii=False),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/chat")
|
||||
async def chat(request: ChatRequest):
|
||||
if request.stream:
|
||||
return EventSourceResponse(
|
||||
event_generator(request.message, request.session_id)
|
||||
)
|
||||
# Non-streaming fallback
|
||||
result = await agent_manager.ainvoke(request.message, request.session_id)
|
||||
return {"reply": result}
|
||||
Reference in New Issue
Block a user