fast_generators.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. # -*- coding: utf-8 -*-
  2. """
  3. Fast 模式 SSE 流式生成器 —— 跳过 Agent 与 Skills,直接查询数据 + LLM 生成报告
  4. """
  5. import json
  6. import time
  7. import asyncio
  8. import traceback
  9. from typing import AsyncGenerator
  10. from api.prompts import FAST_TUN_SYSTEM_PROMPT, FAST_DEVICE_SYSTEM_PROMPT
  11. from api.chat_model import get_chat_model
  12. from api.sse_core import _clean_response
  13. from tools.vent_tools import query_tun_data_by_id, query_device_data_by_id
  14. from db.chat_store import save_message
  15. async def fast_tun_sse_generator(
  16. tun_id: str,
  17. tun_name: str,
  18. thread_id: str,
  19. session_id: str,
  20. save_to_db: bool = True,
  21. ) -> AsyncGenerator[str, None]:
  22. """Fast 模式巷道解读 SSE 生成器。
  23. 跳过 Agent 与 Skills,直接调用 query_tun_data_by_id 获取数据,
  24. 将数据 + 报告模板发给 LLM,流式输出 SSE。
  25. Args:
  26. save_to_db: 是否将消息保存到数据库。点选解读等非对话场景应设为 False。
  27. """
  28. if save_to_db:
  29. save_message(session_id, "user", f"快速解读: {tun_name}")
  30. full_response = ""
  31. try:
  32. # 1. 立即通知前端,不阻塞(让用户第一时间看到反馈)
  33. yield f"data: {json.dumps({'type': 'thinking', 'source': 'fast', 'node': 'fetching_data'}, ensure_ascii=False)}\n\n"
  34. # 2. 获取缓存的模型实例(首次调用 ~1s,后续缓存命中 ~0s)
  35. model = get_chat_model()
  36. # 3. 直接调用数据查询工具
  37. data = await query_tun_data_by_id(tun_id)
  38. yield f"data: {json.dumps({'type': 'executing', 'source': 'fast', 'tools': ['query_tun_data_by_id']}, ensure_ascii=False)}\n\n"
  39. # 4. 构建消息并调用 LLM 流式生成报告
  40. messages = [
  41. {"role": "system", "content": FAST_TUN_SYSTEM_PROMPT},
  42. {"role": "user", "content": f"请根据以下巷道监测数据生成标准化解读报告,禁止给出任何建议:\n\n巷道名称:{tun_name}\n\n数据:\n{data}"},
  43. ]
  44. # 5. 通知前端 LLM 正在生成(消除"卡住"的感知)
  45. yield f"data: {json.dumps({'type': 'generating', 'source': 'fast'}, ensure_ascii=False)}\n\n"
  46. # 带心跳的超时迭代:避免 LLM TTFT 过长导致前端"卡住"
  47. first_token = False
  48. heartbeat_start = time.time()
  49. astream_iter = model.astream(messages)
  50. while True:
  51. try:
  52. if first_token:
  53. chunk = await astream_iter.__anext__()
  54. else:
  55. # 首 token 等待,3 秒超时发送心跳
  56. chunk = await asyncio.wait_for(astream_iter.__anext__(), timeout=3.0)
  57. if not first_token:
  58. first_token = True
  59. except asyncio.TimeoutError:
  60. elapsed = int(time.time() - heartbeat_start)
  61. yield f"data: {json.dumps({'type': 'progress', 'message': f'模型正在生成报告(已等待 {elapsed} 秒)...'}, ensure_ascii=False)}\n\n"
  62. continue
  63. except StopAsyncIteration:
  64. break
  65. # 处理推理内容(DeepSeek-R1 等推理模型会先输出 reasoning_content)
  66. reasoning = ""
  67. if hasattr(chunk, "additional_kwargs") and isinstance(chunk.additional_kwargs, dict):
  68. reasoning = chunk.additional_kwargs.get("reasoning_content", "")
  69. if reasoning:
  70. yield f"data: {json.dumps({'type': 'reasoning', 'content': reasoning}, ensure_ascii=False)}\n\n"
  71. continue # 推理内容不保存到 full_response
  72. content = chunk.content if hasattr(chunk, "content") else ""
  73. if content and isinstance(content, str):
  74. full_response += content
  75. yield f"data: {json.dumps({'type': 'token', 'source': 'fast', 'content': content}, ensure_ascii=False)}\n\n"
  76. # 6. 保存助手回复
  77. if save_to_db and full_response.strip():
  78. cleaned = _clean_response(full_response)
  79. save_message(session_id, "assistant", cleaned)
  80. yield f"data: {json.dumps({'type': 'done', 'thread_id': thread_id, 'session_id': session_id}, ensure_ascii=False)}\n\n"
  81. except Exception as e:
  82. traceback.print_exc()
  83. yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
  84. async def fast_device_sse_generator(
  85. device_id: str,
  86. device_name: str,
  87. device_type: str,
  88. thread_id: str,
  89. session_id: str,
  90. save_to_db: bool = True,
  91. ) -> AsyncGenerator[str, None]:
  92. """Fast 模式设备解读 SSE 生成器。
  93. 跳过 Agent 与 Skills,直接调用 query_device_data_by_id 获取数据,
  94. 将数据 + 报告模板发给 LLM,流式输出 SSE。
  95. Args:
  96. save_to_db: 是否将消息保存到数据库。点选解读等非对话场景应设为 False。
  97. """
  98. if save_to_db:
  99. save_message(session_id, "user", f"快速解读: {device_name}")
  100. full_response = ""
  101. try:
  102. # 1. 立即通知前端,不阻塞(让用户第一时间看到反馈)
  103. yield f"data: {json.dumps({'type': 'thinking', 'source': 'fast', 'node': 'fetching_data'}, ensure_ascii=False)}\n\n"
  104. # 2. 获取缓存的模型实例(首次调用 ~1s,后续缓存命中 ~0s)
  105. model = get_chat_model()
  106. # 3. 直接调用数据查询工具
  107. data = await query_device_data_by_id(device_id)
  108. yield f"data: {json.dumps({'type': 'executing', 'source': 'fast', 'tools': ['query_device_data_by_id']}, ensure_ascii=False)}\n\n"
  109. # 4. 构建消息并调用 LLM 流式生成报告
  110. messages = [
  111. {"role": "system", "content": FAST_DEVICE_SYSTEM_PROMPT},
  112. {"role": "user", "content": f"请根据以下设备监测数据生成标准化解读报告,禁止给出任何建议:\n\n设备名称:{device_name}\n设备类型:{device_type}\n\n数据:\n{data}"},
  113. ]
  114. # 5. 通知前端 LLM 正在生成(消除"卡住"的感知)
  115. yield f"data: {json.dumps({'type': 'generating', 'source': 'fast'}, ensure_ascii=False)}\n\n"
  116. # 带心跳的超时迭代:避免 LLM TTFT 过长导致前端"卡住"
  117. first_token = False
  118. heartbeat_start = time.time()
  119. astream_iter = model.astream(messages)
  120. while True:
  121. try:
  122. if first_token:
  123. chunk = await astream_iter.__anext__()
  124. else:
  125. # 首 token 等待,3 秒超时发送心跳
  126. chunk = await asyncio.wait_for(astream_iter.__anext__(), timeout=3.0)
  127. if not first_token:
  128. first_token = True
  129. except asyncio.TimeoutError:
  130. elapsed = int(time.time() - heartbeat_start)
  131. yield f"data: {json.dumps({'type': 'progress', 'message': f'模型正在生成报告(已等待 {elapsed} 秒)...'}, ensure_ascii=False)}\n\n"
  132. continue
  133. except StopAsyncIteration:
  134. break
  135. # 处理推理内容(DeepSeek-R1 等推理模型会先输出 reasoning_content)
  136. reasoning = ""
  137. if hasattr(chunk, "additional_kwargs") and isinstance(chunk.additional_kwargs, dict):
  138. reasoning = chunk.additional_kwargs.get("reasoning_content", "")
  139. if reasoning:
  140. yield f"data: {json.dumps({'type': 'reasoning', 'content': reasoning}, ensure_ascii=False)}\n\n"
  141. continue # 推理内容不保存到 full_response
  142. content = chunk.content if hasattr(chunk, "content") else ""
  143. if content and isinstance(content, str):
  144. full_response += content
  145. yield f"data: {json.dumps({'type': 'token', 'source': 'fast', 'content': content}, ensure_ascii=False)}\n\n"
  146. # 6. 保存助手回复
  147. if save_to_db and full_response.strip():
  148. cleaned = _clean_response(full_response)
  149. save_message(session_id, "assistant", cleaned)
  150. yield f"data: {json.dumps({'type': 'done', 'thread_id': thread_id, 'session_id': session_id}, ensure_ascii=False)}\n\n"
  151. except Exception as e:
  152. traceback.print_exc()
  153. yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"