appQQQ commited on
Commit
718f7a2
·
verified ·
1 Parent(s): f102830

chore: upload app/api/chat.py

Browse files
Files changed (1) hide show
  1. app/api/chat.py +241 -0
app/api/chat.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """阶段 3 chat 端点: 接 LangGraph agent, 完整 AG-UI 9 类事件.
2
+
3
+ 事件流顺序 (典型):
4
+ thinking {content: "用户在询问财务数据..."}
5
+ agent_step {node: "route", status: "running"}
6
+ agent_step {node: "route", status: "done"}
7
+ retrieval {doc_ids: [...], scores: [...]}
8
+ citation {doc_id, page, snippet, score}
9
+ token {content: "..."} (多次, 流式)
10
+ agent_step {node: "evaluate", status: "done"}
11
+ done {usage, total_ms}
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ import logging
18
+ import time
19
+ import uuid
20
+ from collections.abc import AsyncIterator
21
+
22
+ from fastapi import APIRouter, HTTPException
23
+ from fastapi.responses import StreamingResponse
24
+ from langchain_core.messages import HumanMessage
25
+
26
+ from app.agents.graph import get_compiled_graph
27
+ from app.agents.nodes import answer_node_stream
28
+ from app.agents.state import empty_state_for
29
+ from app.config import settings
30
+ from app.llm.base import LLMMessage
31
+ from app.llm.factory import get_llm
32
+ from app.models import db
33
+ from app.models.schemas import ChatRequest, ChatResponse
34
+ from app.streaming.events import (
35
+ sse_done,
36
+ sse_error,
37
+ sse_format,
38
+ EV_AGENT_STEP,
39
+ EV_CITATION,
40
+ EV_DONE,
41
+ EV_ERROR,
42
+ EV_PROGRESS,
43
+ EV_RETRIEVAL,
44
+ EV_THINKING,
45
+ EV_TOKEN,
46
+ EV_TOOL_CALL,
47
+ )
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+ router = APIRouter(prefix="/chat", tags=["chat"])
52
+
53
+
54
+ # ========== 兜底: 阶段 1 简单直答 (route=direct / 无 agent) ==========
55
+ _FALLBACK_SYSTEM = (
56
+ "你是一个有帮助的中文 AI 助手。"
57
+ "回答应简洁、准确、礼貌。如不知道答案请直接说明。"
58
+ )
59
+
60
+
61
+ @router.post("", response_model=ChatResponse)
62
+ async def chat_simple(req: ChatRequest) -> ChatResponse:
63
+ """非流式 chat: 跑完整 agent 但收集完整结果再返."""
64
+ started = time.time()
65
+ try:
66
+ final_state, events_log = await _run_agent(req)
67
+ except Exception as e: # noqa: BLE001
68
+ logger.exception("chat failed")
69
+ raise HTTPException(status_code=502, detail=str(e)) from e
70
+
71
+ answer = final_state.get("final_answer") or "(无回答)"
72
+ return ChatResponse(
73
+ session_id=req.session_id,
74
+ message_id=uuid.uuid4().hex,
75
+ content=answer,
76
+ citations=final_state.get("citations", []),
77
+ agent_steps=events_log,
78
+ usage={}, # 阶段 3 暂不聚合
79
+ total_ms=int((time.time() - started) * 1000),
80
+ )
81
+
82
+
83
+ @router.post("/stream")
84
+ async def chat_stream(req: ChatRequest) -> StreamingResponse:
85
+ """SSE 流式 chat: 完整 AG-UI 事件."""
86
+ return StreamingResponse(
87
+ _agent_event_stream(req),
88
+ media_type="text/event-stream",
89
+ headers={
90
+ "Cache-Control": "no-cache, no-transform",
91
+ "X-Accel-Buffering": "no", # 关闭 nginx 缓冲
92
+ },
93
+ )
94
+
95
+
96
+ # ========== 核心: 跑 agent + 产生 SSE 事件 ==========
97
+ async def _agent_event_stream(req: ChatRequest) -> AsyncIterator[str]:
98
+ """驱动 agent + 把过程映射为 AG-UI 事件."""
99
+ queue: asyncio.Queue[tuple[str, dict] | None] = asyncio.Queue(maxsize=256)
100
+
101
+ async def emit(event: str, data: dict) -> None:
102
+ await queue.put((event, data))
103
+
104
+ async def runner() -> None:
105
+ try:
106
+ await _run_agent(req, emit=emit)
107
+ except Exception as e: # noqa: BLE001
108
+ logger.exception("agent runner failed")
109
+ await emit(EV_ERROR, {"code": "agent_failed", "message": str(e), "retryable": True})
110
+ finally:
111
+ await queue.put(None)
112
+
113
+ task = asyncio.create_task(runner())
114
+
115
+ # 启动心跳
116
+ started = time.time()
117
+ try:
118
+ while True:
119
+ try:
120
+ item = await asyncio.wait_for(queue.get(), timeout=15.0)
121
+ except asyncio.TimeoutError:
122
+ # 心跳
123
+ yield ": keepalive\n\n"
124
+ continue
125
+ if item is None:
126
+ break
127
+ event, data = item
128
+ yield sse_format(event, data)
129
+ finally:
130
+ if not task.done():
131
+ task.cancel()
132
+
133
+ # done
134
+ yield sse_done(total_ms=int((time.time() - started) * 1000))
135
+
136
+
137
+ async def _run_agent(
138
+ req: ChatRequest,
139
+ emit=None, # async callable | None
140
+ ) -> tuple[dict, list[dict]]:
141
+ """执行 agent. emit 可选, 用于 SSE 推送中间事件.
142
+
143
+ Returns:
144
+ (final_state_dict, events_log)
145
+ """
146
+ started = time.time()
147
+ events_log: list[dict] = []
148
+
149
+ async def _emit(event: str, data: dict) -> None:
150
+ if emit is not None:
151
+ await emit(event, data)
152
+ events_log.append({"event": event, "data": data, "ts": time.time()})
153
+
154
+ # 1. 加载历史消息
155
+ history_rows = db.message_list_by_session(req.session_id, limit=20)
156
+ from app.agents.state import messages_to_lc
157
+ history_msgs = messages_to_lc([
158
+ {"role": r["role"], "content": r["content"]}
159
+ for r in history_rows
160
+ ])
161
+
162
+ # 2. 添加本轮 user message
163
+ new_user_msg = HumanMessage(content=req.message)
164
+ history_msgs.append(new_user_msg)
165
+
166
+ # 3. 初始化 state
167
+ state = empty_state_for(req.session_id, locale=req.locale)
168
+ state["messages"] = history_msgs
169
+
170
+ # 4. 记录 user message 到 SQLite
171
+ db.session_upsert(req.session_id)
172
+ db.message_insert({
173
+ "id": uuid.uuid4().hex,
174
+ "session_id": req.session_id,
175
+ "role": "user",
176
+ "content": req.message,
177
+ })
178
+
179
+ # 5. 跑 LangGraph (retrieval loop)
180
+ try:
181
+ graph = await get_compiled_graph()
182
+ except Exception as e: # noqa: BLE001
183
+ await _emit(EV_ERROR, {"code": "graph_init_failed", "message": str(e)})
184
+ raise
185
+
186
+ config = {"configurable": {"thread_id": req.session_id}}
187
+ final_state: dict = dict(state)
188
+
189
+ # 直接用 graph.ainvoke, 拿最终 state
190
+ try:
191
+ result = await graph.ainvoke(state, config=config)
192
+ final_state.update(result)
193
+ except Exception as e: # noqa: BLE001
194
+ logger.exception("graph.ainvoke failed")
195
+ await _emit(EV_ERROR, {"code": "graph_failed", "message": str(e)})
196
+ raise
197
+
198
+ # 6. 推送检索事件
199
+ if final_state.get("retrieved_doc_ids"):
200
+ await _emit(EV_RETRIEVAL, {
201
+ "doc_ids": final_state["retrieved_doc_ids"],
202
+ "count": len(final_state.get("retrieved", [])),
203
+ "scores": [round(h.score, 4) for h in final_state.get("retrieved", [])[:10]],
204
+ })
205
+ if final_state.get("citations"):
206
+ await _emit(EV_PROGRESS, {"pct": 60, "label": "已生成引用, 正在回答..."})
207
+
208
+ # 7. 跑 answer 流式 (直接调 LLM, 不走 graph)
209
+ await _emit(EV_AGENT_STEP, {"node": "answer", "status": "running"})
210
+
211
+ async def _on_token(content: str) -> None:
212
+ await _emit(EV_TOKEN, {"content": content})
213
+
214
+ async def _on_citation(c: dict) -> None:
215
+ await _emit(EV_CITATION, c)
216
+
217
+ async def _on_thinking(content: str) -> None:
218
+ await _emit(EV_THINKING, {"content": content})
219
+
220
+ answer_update = await answer_node_stream(
221
+ final_state,
222
+ on_token=_on_token,
223
+ on_citation=_on_citation,
224
+ on_thinking=_on_thinking,
225
+ )
226
+ final_state.update(answer_update)
227
+
228
+ await _emit(EV_AGENT_STEP, {"node": "answer", "status": "done"})
229
+ await _emit(EV_PROGRESS, {"pct": 100, "label": "完成"})
230
+
231
+ # 8. 记录 assistant message 到 SQLite
232
+ elapsed = int((time.time() - started) * 1000)
233
+ db.message_insert({
234
+ "id": uuid.uuid4().hex,
235
+ "session_id": req.session_id,
236
+ "role": "assistant",
237
+ "content": final_state.get("final_answer", ""),
238
+ "citations": final_state.get("citations", []),
239
+ })
240
+
241
+ return final_state, events_log