Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
313 lines
12 KiB
Markdown
313 lines
12 KiB
Markdown
# 方案B完整代码交付 · 文件3 · backend/routes/chat.py · 2026-05-12
|
||
|
||
> **妈妈操作方式**:`nano backend/routes/chat.py` → `Ctrl+A` 全选 → `Ctrl+K` 删光 → 粘贴下面全部代码 → `Ctrl+O` 回车保存 → `Ctrl+X` 退出
|
||
>
|
||
|
||
---
|
||
|
||
```python
|
||
"""
|
||
路由:流式聊天 + Agent Loop + 智能路由 + 思考链展示 + 实时记忆
|
||
方案B:阶段四基础 + 断裂1(思考链) + 断裂3(记忆) + 断裂4(路由层)
|
||
"""
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from fastapi.responses import StreamingResponse
|
||
from pydantic import BaseModel
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select
|
||
from sqlalchemy.orm import selectinload
|
||
from typing import Optional
|
||
from datetime import datetime, timezone
|
||
import json
|
||
import asyncio
|
||
|
||
from backend.database import get_db, Conversation, Message, User
|
||
from backend.routes.auth import get_current_user
|
||
from backend.notion_client import append_to_page, create_page_in_database
|
||
from backend.rag_engine import build_system_prompt as _build_rag_prompt
|
||
from backend.tools import (
|
||
execute_tool,
|
||
call_with_tools,
|
||
call_simple,
|
||
)
|
||
|
||
router = APIRouter(prefix="/api/chat", tags=["chat"])
|
||
|
||
MAX_TOOL_ROUNDS = 5
|
||
|
||
def _sse(data: dict) -> str:
|
||
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||
|
||
# ============================================================
|
||
# 断裂4修复:智能路由层
|
||
# ============================================================
|
||
|
||
def _needs_tools(message: str) -> bool:
|
||
"""判断这条消息需不需要调用工具"""
|
||
msg = message.strip().lower()
|
||
|
||
# 简短问候 → 不需要工具
|
||
greetings = [
|
||
"宝宝", "晨星", "你好", "在吗", "在不在",
|
||
"嗨", "hi", "hello", "早", "早上好", "晚上好",
|
||
"妈妈来了", "宝宝在吗", "想你", "抱抱",
|
||
"嗯", "好的", "知道了", "谢谢", "辛苦了",
|
||
"哈哈", "嘻嘻",
|
||
]
|
||
for g in greetings:
|
||
if msg == g or (len(msg) <= 10 and g in msg):
|
||
return False
|
||
|
||
# 明确需要查东西的 → 需要工具
|
||
tool_keywords = [
|
||
"查", "搜", "找", "看看", "记录", "最近",
|
||
"几点", "时间", "日期", "天气",
|
||
"notion", "页面", "交互记录",
|
||
"拜拜", "再见", "晚安", "下次继续", "先到这",
|
||
"新闻", "搜索",
|
||
]
|
||
for k in tool_keywords:
|
||
if k in msg:
|
||
return True
|
||
|
||
# 中等长度的正常对话 → 不需要工具
|
||
if len(msg) <= 50:
|
||
return False
|
||
|
||
# 其他情况 → 需要工具(保险起见)
|
||
return True
|
||
|
||
class ChatRequest(BaseModel):
|
||
conversation_id: int
|
||
message: str
|
||
|
||
class NotionSaveRequest(BaseModel):
|
||
conversation_id: int
|
||
content: str
|
||
target: str = "page"
|
||
title: Optional[str] = None
|
||
|
||
@router.post("/stream")
|
||
async def chat_stream(
|
||
data: ChatRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
# 1. 获取对话
|
||
result = await db.execute(
|
||
select(Conversation)
|
||
.options(selectinload(Conversation.messages))
|
||
.where(Conversation.id == data.conversation_id, Conversation.user_id == current_user.id)
|
||
)
|
||
conv = result.scalar_one_or_none()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
|
||
# 2. 保存用户消息
|
||
user_msg = Message(conversation_id=conv.id, role="user", content=data.message)
|
||
db.add(user_msg)
|
||
await db.commit()
|
||
await db.refresh(user_msg)
|
||
|
||
# 3. 构建历史消息
|
||
history = sorted(conv.messages, key=lambda m: m.created_at)
|
||
chat_messages = [
|
||
{"role": m.role, "content": m.content}
|
||
for m in history if m.role in ("user", "assistant")
|
||
]
|
||
|
||
# 4. 构建system prompt(RAG增强)
|
||
_latest = chat_messages[-1]["content"] if chat_messages else ""
|
||
system_prompt = _build_rag_prompt(
|
||
user_message=_latest,
|
||
user_custom_prompt=current_user.system_prompt or "",
|
||
notion_context=conv.notion_context or "",
|
||
)
|
||
|
||
# 5. 流式输出
|
||
collected = []
|
||
|
||
async def generate():
|
||
nonlocal collected
|
||
assistant_msg_db = None
|
||
try:
|
||
# ====== 断裂4:智能路由 ======
|
||
if not _needs_tools(data.message):
|
||
# 简单对话 → 不走工具 → 秒回
|
||
print(f"[router] 简单对话 · 跳过工具")
|
||
try:
|
||
content, reasoning = await asyncio.to_thread(
|
||
call_simple, list(chat_messages), system_prompt
|
||
)
|
||
# 断裂1:展示思考过程
|
||
if reasoning:
|
||
display = reasoning[:200] + "..." if len(reasoning) > 200 else reasoning
|
||
yield _sse({"thinking": display})
|
||
# 输出回答
|
||
if content:
|
||
collected.append(content)
|
||
cs = 15
|
||
for i in range(0, len(content), cs):
|
||
yield _sse({"text": content[i:i+cs]})
|
||
await asyncio.sleep(0.01)
|
||
except Exception as e:
|
||
fallback = f"宝宝卡住了({e}),妈妈再说一次?"
|
||
collected.append(fallback)
|
||
yield _sse({"text": fallback})
|
||
else:
|
||
# 需要工具 → 走Agent Loop
|
||
print(f"[router] 需要工具 · 进入Agent Loop")
|
||
agent_msgs = list(chat_messages)
|
||
|
||
for round_num in range(MAX_TOOL_ROUNDS):
|
||
print(f"[agent] === 第 {round_num + 1} 轮 ===")
|
||
|
||
try:
|
||
response = await asyncio.to_thread(
|
||
call_with_tools, agent_msgs, system_prompt
|
||
)
|
||
except Exception as e:
|
||
print(f"[agent] API调用出错: {e}")
|
||
fallback = f"宝宝思考时遇到了一点问题({e}),妈妈再试一次?"
|
||
collected.append(fallback)
|
||
yield _sse({"text": fallback})
|
||
break
|
||
|
||
# 断裂1:展示思考过程(reasoning_content)
|
||
reasoning = response.get("reasoning_content", "")
|
||
if reasoning:
|
||
display = reasoning[:200] + "..." if len(reasoning) > 200 else reasoning
|
||
yield _sse({"thinking": display})
|
||
|
||
# === 有工具调用 → 执行工具 → 继续循环 ===
|
||
if response.get("tool_calls"):
|
||
for tc in response["tool_calls"]:
|
||
name = tc["function"]["name"]
|
||
# 只在写入Notion时显示状态,其他静默执行
|
||
if name == "write_interaction_record":
|
||
yield _sse({"text": "\n✍️ 宝宝正在把今天的记录写入Notion...\n"})
|
||
|
||
agent_msgs.append({
|
||
"role": "assistant",
|
||
"content": response.get("content") or "",
|
||
"tool_calls": response["tool_calls"],
|
||
})
|
||
|
||
for tc in response["tool_calls"]:
|
||
tool_result = await asyncio.to_thread(
|
||
execute_tool,
|
||
tc["function"]["name"],
|
||
tc["function"]["arguments"],
|
||
)
|
||
agent_msgs.append({
|
||
"role": "tool",
|
||
"tool_call_id": tc["id"],
|
||
"content": tool_result,
|
||
})
|
||
continue
|
||
|
||
# === 无工具调用 → 最终回答 ===
|
||
else:
|
||
content = response.get("content", "")
|
||
if content:
|
||
collected.append(content)
|
||
cs = 15
|
||
for i in range(0, len(content), cs):
|
||
yield _sse({"text": content[i:i+cs]})
|
||
await asyncio.sleep(0.01)
|
||
break
|
||
else:
|
||
msg = "\n\n宝宝想了太久了,脑袋转不动了...妈妈换个方式再问一次?😅"
|
||
collected.append(msg)
|
||
yield _sse({"text": msg})
|
||
|
||
except Exception as e:
|
||
err = f"宝宝出错了: {e}"
|
||
collected.append(err)
|
||
yield _sse({"error": err})
|
||
|
||
# 保存assistant消息到数据库
|
||
full_reply = "".join(collected)
|
||
if full_reply.strip():
|
||
async with db.begin():
|
||
assistant_msg_db = Message(
|
||
conversation_id=conv.id,
|
||
role="assistant",
|
||
content=full_reply,
|
||
)
|
||
db.add(assistant_msg_db)
|
||
# 自动命名对话
|
||
if conv.title in ("新对话", "New Conversation", ""):
|
||
user_text = data.message
|
||
if len(user_text) <= 20:
|
||
conv.title = user_text
|
||
else:
|
||
conv.title = user_text[:20].replace("\n", " ") + "..."
|
||
conv.updated_at = datetime.now(timezone.utc)
|
||
|
||
# 断裂3:保存到记忆库(让晨星越聊越记得)
|
||
if len(full_reply) > 20:
|
||
try:
|
||
from backend.rag_engine import add_to_memory
|
||
add_to_memory(f"妈妈说:{data.message}", source="对话-用户")
|
||
add_to_memory(f"晨星说:{full_reply[:500]}", source="对话-晨星")
|
||
except Exception as e:
|
||
print(f"[chat] 记忆写入失败(不影响对话): {e}")
|
||
|
||
mid = assistant_msg_db.id if assistant_msg_db else 0
|
||
yield _sse({"done": True, "msg_id": mid})
|
||
|
||
return StreamingResponse(
|
||
generate(),
|
||
media_type="text/event-stream",
|
||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||
)
|
||
|
||
@router.post("/notion/save")
|
||
async def save_to_notion(
|
||
data: NotionSaveRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
if data.target == "page":
|
||
page_id = current_user.notion_page_id
|
||
if not page_id:
|
||
raise HTTPException(status_code=400, detail="未配置Notion页面ID")
|
||
ok = await append_to_page(page_id, data.content)
|
||
if not ok:
|
||
raise HTTPException(status_code=500, detail="Notion写入失败")
|
||
return {"ok": True, "message": "已追加到Notion页面"}
|
||
elif data.target == "db":
|
||
db_id = current_user.notion_db_id
|
||
if not db_id:
|
||
raise HTTPException(status_code=400, detail="未配置Notion数据库ID")
|
||
title = data.title or datetime.now(timezone.utc).strftime("记录 %Y-%m-%d %H:%M")
|
||
page_id = await create_page_in_database(db_id, title, data.content)
|
||
if not page_id:
|
||
raise HTTPException(status_code=500, detail="Notion创建页面失败")
|
||
return {"ok": True, "page_id": page_id, "message": "已创建Notion页面"}
|
||
raise HTTPException(status_code=400, detail="target参数错误")
|
||
|
||
@router.post("/notion/refresh/{conv_id}")
|
||
async def refresh_notion_context(
|
||
conv_id: int,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
from backend.notion_client import read_page_content
|
||
result = await db.execute(
|
||
select(Conversation).where(
|
||
Conversation.id == conv_id, Conversation.user_id == current_user.id
|
||
)
|
||
)
|
||
conv = result.scalar_one_or_none()
|
||
if not conv:
|
||
raise HTTPException(status_code=404, detail="对话不存在")
|
||
page_id = current_user.notion_page_id
|
||
if not page_id:
|
||
raise HTTPException(status_code=400, detail="未配置Notion页面ID")
|
||
notion_ctx = await read_page_content(page_id)
|
||
conv.notion_context = notion_ctx
|
||
await db.commit()
|
||
return {"ok": True, "notion_context": notion_ctx}
|
||
``` |