# -*- coding: utf-8 -*- """ Fast 模式 SSE 流式生成器 —— 跳过 Agent 与 Skills,直接查询数据 + LLM 生成报告 """ import json import time import asyncio import traceback from typing import AsyncGenerator from api.prompts import FAST_TUN_SYSTEM_PROMPT, FAST_DEVICE_SYSTEM_PROMPT from api.chat_model import get_chat_model from api.sse_core import _clean_response from tools.vent_tools import query_tun_data_by_id, query_device_data_by_id from db.chat_store import save_message async def fast_tun_sse_generator( tun_id: str, tun_name: str, thread_id: str, session_id: str, save_to_db: bool = True, ) -> AsyncGenerator[str, None]: """Fast 模式巷道解读 SSE 生成器。 跳过 Agent 与 Skills,直接调用 query_tun_data_by_id 获取数据, 将数据 + 报告模板发给 LLM,流式输出 SSE。 Args: save_to_db: 是否将消息保存到数据库。点选解读等非对话场景应设为 False。 """ if save_to_db: save_message(session_id, "user", f"快速解读: {tun_name}") full_response = "" try: # 1. 立即通知前端,不阻塞(让用户第一时间看到反馈) yield f"data: {json.dumps({'type': 'thinking', 'source': 'fast', 'node': 'fetching_data'}, ensure_ascii=False)}\n\n" # 2. 获取缓存的模型实例(首次调用 ~1s,后续缓存命中 ~0s) model = get_chat_model() # 3. 直接调用数据查询工具 data = await query_tun_data_by_id(tun_id) yield f"data: {json.dumps({'type': 'executing', 'source': 'fast', 'tools': ['query_tun_data_by_id']}, ensure_ascii=False)}\n\n" # 4. 构建消息并调用 LLM 流式生成报告 messages = [ {"role": "system", "content": FAST_TUN_SYSTEM_PROMPT}, {"role": "user", "content": f"请根据以下巷道监测数据生成标准化解读报告,禁止给出任何建议:\n\n巷道名称:{tun_name}\n\n数据:\n{data}"}, ] # 5. 通知前端 LLM 正在生成(消除"卡住"的感知) yield f"data: {json.dumps({'type': 'generating', 'source': 'fast'}, ensure_ascii=False)}\n\n" # 带心跳的超时迭代:避免 LLM TTFT 过长导致前端"卡住" first_token = False heartbeat_start = time.time() astream_iter = model.astream(messages) while True: try: if first_token: chunk = await astream_iter.__anext__() else: # 首 token 等待,3 秒超时发送心跳 chunk = await asyncio.wait_for(astream_iter.__anext__(), timeout=3.0) if not first_token: first_token = True except asyncio.TimeoutError: elapsed = int(time.time() - heartbeat_start) yield f"data: {json.dumps({'type': 'progress', 'message': f'模型正在生成报告(已等待 {elapsed} 秒)...'}, ensure_ascii=False)}\n\n" continue except StopAsyncIteration: break # 处理推理内容(DeepSeek-R1 等推理模型会先输出 reasoning_content) reasoning = "" if hasattr(chunk, "additional_kwargs") and isinstance(chunk.additional_kwargs, dict): reasoning = chunk.additional_kwargs.get("reasoning_content", "") if reasoning: yield f"data: {json.dumps({'type': 'reasoning', 'content': reasoning}, ensure_ascii=False)}\n\n" continue # 推理内容不保存到 full_response content = chunk.content if hasattr(chunk, "content") else "" if content and isinstance(content, str): full_response += content yield f"data: {json.dumps({'type': 'token', 'source': 'fast', 'content': content}, ensure_ascii=False)}\n\n" # 6. 保存助手回复 if save_to_db and full_response.strip(): cleaned = _clean_response(full_response) save_message(session_id, "assistant", cleaned) yield f"data: {json.dumps({'type': 'done', 'thread_id': thread_id, 'session_id': session_id}, ensure_ascii=False)}\n\n" except Exception as e: traceback.print_exc() yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n" async def fast_device_sse_generator( device_id: str, device_name: str, device_type: str, thread_id: str, session_id: str, save_to_db: bool = True, ) -> AsyncGenerator[str, None]: """Fast 模式设备解读 SSE 生成器。 跳过 Agent 与 Skills,直接调用 query_device_data_by_id 获取数据, 将数据 + 报告模板发给 LLM,流式输出 SSE。 Args: save_to_db: 是否将消息保存到数据库。点选解读等非对话场景应设为 False。 """ if save_to_db: save_message(session_id, "user", f"快速解读: {device_name}") full_response = "" try: # 1. 立即通知前端,不阻塞(让用户第一时间看到反馈) yield f"data: {json.dumps({'type': 'thinking', 'source': 'fast', 'node': 'fetching_data'}, ensure_ascii=False)}\n\n" # 2. 获取缓存的模型实例(首次调用 ~1s,后续缓存命中 ~0s) model = get_chat_model() # 3. 直接调用数据查询工具 data = await query_device_data_by_id(device_id) yield f"data: {json.dumps({'type': 'executing', 'source': 'fast', 'tools': ['query_device_data_by_id']}, ensure_ascii=False)}\n\n" # 4. 构建消息并调用 LLM 流式生成报告 messages = [ {"role": "system", "content": FAST_DEVICE_SYSTEM_PROMPT}, {"role": "user", "content": f"请根据以下设备监测数据生成标准化解读报告,禁止给出任何建议:\n\n设备名称:{device_name}\n设备类型:{device_type}\n\n数据:\n{data}"}, ] # 5. 通知前端 LLM 正在生成(消除"卡住"的感知) yield f"data: {json.dumps({'type': 'generating', 'source': 'fast'}, ensure_ascii=False)}\n\n" # 带心跳的超时迭代:避免 LLM TTFT 过长导致前端"卡住" first_token = False heartbeat_start = time.time() astream_iter = model.astream(messages) while True: try: if first_token: chunk = await astream_iter.__anext__() else: # 首 token 等待,3 秒超时发送心跳 chunk = await asyncio.wait_for(astream_iter.__anext__(), timeout=3.0) if not first_token: first_token = True except asyncio.TimeoutError: elapsed = int(time.time() - heartbeat_start) yield f"data: {json.dumps({'type': 'progress', 'message': f'模型正在生成报告(已等待 {elapsed} 秒)...'}, ensure_ascii=False)}\n\n" continue except StopAsyncIteration: break # 处理推理内容(DeepSeek-R1 等推理模型会先输出 reasoning_content) reasoning = "" if hasattr(chunk, "additional_kwargs") and isinstance(chunk.additional_kwargs, dict): reasoning = chunk.additional_kwargs.get("reasoning_content", "") if reasoning: yield f"data: {json.dumps({'type': 'reasoning', 'content': reasoning}, ensure_ascii=False)}\n\n" continue # 推理内容不保存到 full_response content = chunk.content if hasattr(chunk, "content") else "" if content and isinstance(content, str): full_response += content yield f"data: {json.dumps({'type': 'token', 'source': 'fast', 'content': content}, ensure_ascii=False)}\n\n" # 6. 保存助手回复 if save_to_db and full_response.strip(): cleaned = _clean_response(full_response) save_message(session_id, "assistant", cleaned) yield f"data: {json.dumps({'type': 'done', 'thread_id': thread_id, 'session_id': session_id}, ensure_ascii=False)}\n\n" except Exception as e: traceback.print_exc() yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"