| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- # -*- 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"
|