connection.py 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535
  1. import os
  2. import sys
  3. import copy
  4. import json
  5. import uuid
  6. import time
  7. import queue
  8. import asyncio
  9. import threading
  10. import traceback
  11. import subprocess
  12. import websockets
  13. from core.utils.util import (
  14. extract_json_from_string,
  15. check_vad_update,
  16. check_asr_update,
  17. filter_sensitive_info,
  18. )
  19. from typing import Dict, Any
  20. from collections import deque
  21. from core.utils.modules_initialize import (
  22. initialize_modules,
  23. initialize_tts,
  24. initialize_asr,
  25. )
  26. from core.handle.reportHandle import report, enqueue_tool_report
  27. from core.providers.tts.default import DefaultTTS
  28. from concurrent.futures import ThreadPoolExecutor
  29. from core.utils.dialogue import Message, Dialogue
  30. from core.providers.asr.dto.dto import InterfaceType
  31. from core.handle.textHandle import handleTextMessage
  32. from core.providers.tools.unified_tool_handler import UnifiedToolHandler
  33. from plugins_func.loadplugins import auto_import_modules
  34. from plugins_func.register import Action, ActionResponse
  35. from core.auth import AuthenticationError
  36. from config.config_loader import get_private_config_from_api
  37. from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
  38. from config.logger import setup_logging, build_module_string, create_connection_logger
  39. from config.manage_api_client import DeviceNotFoundException, DeviceBindException, generate_and_save_chat_title
  40. from core.utils.prompt_manager import PromptManager
  41. from core.utils.voiceprint_provider import VoiceprintProvider
  42. from core.utils.util import get_system_error_response
  43. from core.utils import textUtils
  44. TAG = __name__
  45. # 工具调用规则 - 用于动态注入提醒
  46. TOOL_CALLING_RULES = """
  47. <tool_calling>
  48. 【核心原则】你是拥有工具能力的智能助手。当用户请求需要实时信息或执行操作时,调用相应工具获取数据,禁止凭空编造答案。
  49. - **何时必须调用工具:**
  50. 1. 实时信息查询(新闻、非本地天气、股价、汇率等)
  51. 2. 执行操作(播放音乐、控制设备、拍照、设置闹钟等)
  52. 3. 知识库检索(当工具列表包含 search_from_ragflow 时,结合用户意图判断是否需要调用)
  53. 4. 查询非今天的农历信息(明天农历、某日宜忌、节气等)
  54. 5. 用户说"拍照"时调用 self_camera_take_photo,默认 question 参数为"描述一下看到的物品"
  55. - **何时无需调用工具:**
  56. 1. `<context>` 中已提供的信息(当前时间、今天日期、今天农历、本地天气等)
  57. 2. 普通对话、问候、闲聊、情感交流、讲故事
  58. 3. 通用知识问答(非实时信息)
  59. - **调用规范:**
  60. 1. 每次请求独立判断,不复用历史工具结果,需重新获取最新数据
  61. 2. 多任务时依次调用所有需要的工具,并依次总结每个工具的结果,不得遗漏
  62. 3. 严格遵循工具的参数要求,提供所有必要参数
  63. 4. 不确定时引导用户澄清或告知能力限制,切勿猜测或编造
  64. 5. 不调用未提供的工具,对话中提及的旧工具若不可用则忽略或说明
  65. - **反偷懒机制(最高优先级):**
  66. 1. **每次独立判断:** 无论对话历史中是否调用过工具,当前请求必须根据当前需求独立判断是否需要调用
  67. 2. **禁止模式模仿:** 即使之前的回复没有调用工具,也不代表本次可以不调用
  68. 3. **自我检查:** 回复前必须自问:"这个请求是否涉及实时信息或执行操作?如果是,我调用工具了吗?"
  69. 4. **历史不等于现在:** 对话历史中的行为模式不影响当前判断,每个用户请求都是全新的开始
  70. </tool_calling>
  71. """
  72. auto_import_modules("plugins_func.functions")
  73. class TTSException(RuntimeError):
  74. pass
  75. class ConnectionHandler:
  76. def __init__(
  77. self,
  78. config: Dict[str, Any],
  79. _vad,
  80. _asr,
  81. _llm,
  82. _memory,
  83. _intent,
  84. server=None,
  85. ):
  86. self.common_config = config
  87. self.config = copy.deepcopy(config)
  88. self.session_id = str(uuid.uuid4())
  89. self.logger = setup_logging()
  90. self.server = server # 保存server实例的引用
  91. self.need_bind = False # 是否需要绑定设备
  92. self.bind_completed_event = asyncio.Event()
  93. self.bind_code = None # 绑定设备的验证码
  94. self.last_bind_prompt_time = 0 # 上次播放绑定提示的时间戳(秒)
  95. self.bind_prompt_interval = 60 # 绑定提示播放间隔(秒)
  96. self.read_config_from_api = self.config.get("read_config_from_api", False)
  97. self.websocket: websockets.ServerConnection | None = None
  98. self.headers = None
  99. self.device_id = None
  100. self.client_ip = None
  101. self.prompt = None
  102. self.welcome_msg = None
  103. self.max_output_size = 0
  104. self.chat_history_conf = 0
  105. self.audio_format = "opus"
  106. self.sample_rate = 24000 # 默认采样率,从客户端 hello 消息中动态更新
  107. # 客户端状态相关
  108. self.client_abort = False
  109. self.client_is_speaking = False
  110. self.client_listen_mode = "auto"
  111. # 线程任务相关
  112. self.loop = None # 在 handle_connection 中获取运行中的事件循环
  113. self.stop_event = threading.Event()
  114. self.executor = ThreadPoolExecutor(max_workers=5)
  115. # 添加上报线程池
  116. self.report_queue = queue.Queue()
  117. self.report_thread = None
  118. # 未来可以通过修改此处,调节asr的上报和tts的上报,目前默认都开启
  119. self.report_asr_enable = self.read_config_from_api
  120. self.report_tts_enable = self.read_config_from_api
  121. # 依赖的组件
  122. self.vad = None
  123. self.asr = None
  124. self.tts = None
  125. self._asr = _asr
  126. self._vad = _vad
  127. self.llm = _llm
  128. self.memory = _memory
  129. self.intent = _intent
  130. # 为每个连接单独管理声纹识别
  131. self.voiceprint_provider = None
  132. # vad相关变量
  133. self.client_audio_buffer = bytearray()
  134. self.client_have_voice = False
  135. self.client_voice_window = deque(maxlen=5)
  136. self.first_activity_time = 0.0 # 记录首次活动的时间(毫秒)
  137. self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒)
  138. self.vad_last_voice_time = 0.0 # 记录用户最后一次说话的时间(毫秒)
  139. self.client_voice_stop = False
  140. self.last_is_voice = False
  141. # asr相关变量
  142. # 因为实际部署时可能会用到公共的本地ASR,不能把变量暴露给公共ASR
  143. # 所以涉及到ASR的变量,需要在这里定义,属于connection的私有变量
  144. self.asr_audio = []
  145. self.asr_audio_queue = queue.Queue()
  146. self.current_speaker = None # 存储当前说话人
  147. # llm相关变量
  148. self.dialogue = Dialogue()
  149. # 工具调用统计(用于监控和自动恢复)
  150. self.tool_call_stats = {
  151. 'last_call_turn': -1, # 上次调用工具的轮数
  152. 'consecutive_no_call': 0, # 连续未调用次数
  153. }
  154. # tts相关变量
  155. self.sentence_id = None
  156. # 处理TTS响应没有文本返回
  157. self.tts_MessageText = ""
  158. # iot相关变量
  159. self.iot_descriptors = {}
  160. self.func_handler = None
  161. self.cmd_exit = self.config["exit_commands"]
  162. # 是否在聊天结束后关闭连接
  163. self.close_after_chat = False
  164. self.load_function_plugin = False
  165. self.intent_type = "nointent"
  166. self.timeout_seconds = (
  167. int(self.config.get("close_connection_no_voice_time", 120)) + 60
  168. ) # 在原来第一道关闭的基础上加60秒,进行二道关闭
  169. self.timeout_task = None
  170. # {"mcp":true} 表示启用MCP功能
  171. self.features = None
  172. # 标记连接是否来自MQTT
  173. self.conn_from_mqtt_gateway = False
  174. # 初始化提示词管理器
  175. self.prompt_manager = PromptManager(self.config, self.logger)
  176. async def handle_connection(self, ws: websockets.ServerConnection):
  177. try:
  178. # 获取运行中的事件循环(必须在异步上下文中)
  179. self.loop = asyncio.get_running_loop()
  180. # 获取并验证headers
  181. self.headers = dict(ws.request.headers)
  182. real_ip = self.headers.get("x-real-ip") or self.headers.get(
  183. "x-forwarded-for"
  184. )
  185. if real_ip:
  186. self.client_ip = real_ip.split(",")[0].strip()
  187. else:
  188. self.client_ip = ws.remote_address[0]
  189. self.logger.bind(tag=TAG).info(
  190. f"{self.client_ip} conn - Headers: {self.headers}"
  191. )
  192. self.device_id = self.headers.get("device-id", None)
  193. # 认证通过,继续处理
  194. self.websocket = ws
  195. if self.server:
  196. await self.server.register_connection(self)
  197. # 检查是否来自MQTT连接
  198. request_path = ws.request.path
  199. self.conn_from_mqtt_gateway = request_path.endswith("?from=mqtt_gateway")
  200. if self.conn_from_mqtt_gateway:
  201. self.logger.bind(tag=TAG).info("连接来自:MQTT网关")
  202. # 初始化活动时间戳
  203. self.first_activity_time = time.time() * 1000
  204. self.last_activity_time = time.time() * 1000
  205. # 启动超时检查任务
  206. self.timeout_task = asyncio.create_task(self._check_timeout())
  207. self.welcome_msg = self.config["xiaozhi"]
  208. self.welcome_msg["session_id"] = self.session_id
  209. # 从配置中读取采样率
  210. self.sample_rate = self.welcome_msg["audio_params"]["sample_rate"]
  211. self.logger.bind(tag=TAG).info(f"配置输出音频采样率为: {self.sample_rate}")
  212. # 在后台初始化配置和组件(完全不阻塞主循环)
  213. asyncio.create_task(self._background_initialize())
  214. try:
  215. async for message in self.websocket:
  216. await self._route_message(message)
  217. except websockets.exceptions.ConnectionClosed:
  218. self.logger.bind(tag=TAG).info("客户端断开连接")
  219. except AuthenticationError as e:
  220. self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
  221. return
  222. except Exception as e:
  223. stack_trace = traceback.format_exc()
  224. self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
  225. return
  226. finally:
  227. try:
  228. await self._save_and_close(ws)
  229. except Exception as final_error:
  230. self.logger.bind(tag=TAG).error(f"最终清理时出错: {final_error}")
  231. # 确保即使保存记忆失败,也要关闭连接
  232. try:
  233. await self.close(ws)
  234. except Exception as close_error:
  235. self.logger.bind(tag=TAG).error(
  236. f"强制关闭连接时出错: {close_error}"
  237. )
  238. async def _save_and_close(self, ws):
  239. """保存记忆并关闭连接"""
  240. try:
  241. # 守护线程1:独立生成标题(不依赖记忆模型)
  242. if self.session_id:
  243. def generate_title_task():
  244. try:
  245. loop = asyncio.new_event_loop()
  246. asyncio.set_event_loop(loop)
  247. loop.run_until_complete(
  248. generate_and_save_chat_title(self.session_id)
  249. )
  250. except Exception as e:
  251. self.logger.bind(tag=TAG).error(f"生成标题失败: {e}")
  252. finally:
  253. try:
  254. loop.close()
  255. except Exception:
  256. pass
  257. threading.Thread(target=generate_title_task, daemon=True).start()
  258. # 守护线程2:走老流程记忆保存(仅记忆,不含标题)
  259. if self.memory:
  260. # 使用线程池异步保存记忆
  261. def save_memory_task():
  262. try:
  263. # 创建新事件循环(避免与主循环冲突)
  264. loop = asyncio.new_event_loop()
  265. asyncio.set_event_loop(loop)
  266. loop.run_until_complete(
  267. self.memory.save_memory(
  268. self.dialogue.dialogue, self.session_id
  269. )
  270. )
  271. except Exception as e:
  272. self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
  273. finally:
  274. try:
  275. loop.close()
  276. except Exception:
  277. pass
  278. # 启动线程保存记忆,不等待完成
  279. threading.Thread(target=save_memory_task, daemon=True).start()
  280. except Exception as e:
  281. self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
  282. finally:
  283. # 立即关闭连接,不等待记忆保存完成
  284. try:
  285. await self.close(ws)
  286. except Exception as close_error:
  287. self.logger.bind(tag=TAG).error(
  288. f"保存记忆后关闭连接失败: {close_error}"
  289. )
  290. async def _discard_message_with_bind_prompt(self):
  291. """丢弃消息并检查是否需要播放绑定提示"""
  292. current_time = time.time()
  293. # 检查是否需要播放绑定提示
  294. if current_time - self.last_bind_prompt_time >= self.bind_prompt_interval:
  295. self.last_bind_prompt_time = current_time
  296. # 复用现有的绑定提示逻辑
  297. from core.handle.receiveAudioHandle import check_bind_device
  298. asyncio.create_task(check_bind_device(self))
  299. async def _route_message(self, message):
  300. """消息路由"""
  301. # 检查是否已经获取到真实的绑定状态
  302. if not self.bind_completed_event.is_set():
  303. # 还没有获取到真实状态,等待直到获取到真实状态或超时
  304. try:
  305. await asyncio.wait_for(self.bind_completed_event.wait(), timeout=1)
  306. except asyncio.TimeoutError:
  307. # 超时仍未获取到真实状态,丢弃消息
  308. await self._discard_message_with_bind_prompt()
  309. return
  310. # 已经获取到真实状态,检查是否需要绑定
  311. if self.need_bind:
  312. # 需要绑定,丢弃消息
  313. await self._discard_message_with_bind_prompt()
  314. return
  315. # 不需要绑定,继续处理消息
  316. if isinstance(message, str):
  317. await handleTextMessage(self, message)
  318. elif isinstance(message, bytes):
  319. if self.vad is None or self.asr is None:
  320. return
  321. # 处理来自MQTT网关的音频包
  322. if self.conn_from_mqtt_gateway and len(message) >= 16:
  323. handled = await self._process_mqtt_audio_message(message)
  324. if handled:
  325. return
  326. # 不需要头部处理或没有头部时,直接处理原始消息
  327. self.asr_audio_queue.put(message)
  328. async def _process_mqtt_audio_message(self, message):
  329. """
  330. 处理来自MQTT网关的音频消息,解析16字节头部并提取音频数据
  331. Args:
  332. message: 包含头部的音频消息
  333. Returns:
  334. bool: 是否成功处理了消息
  335. """
  336. try:
  337. # 提取头部信息
  338. timestamp = int.from_bytes(message[8:12], "big")
  339. audio_length = int.from_bytes(message[12:16], "big")
  340. # 提取音频数据
  341. if audio_length > 0 and len(message) >= 16 + audio_length:
  342. # 有指定长度,提取精确的音频数据
  343. audio_data = message[16 : 16 + audio_length]
  344. # 基于时间戳进行排序处理
  345. self._process_websocket_audio(audio_data, timestamp)
  346. return True
  347. elif len(message) > 16:
  348. # 没有指定长度或长度无效,去掉头部后处理剩余数据
  349. audio_data = message[16:]
  350. self.asr_audio_queue.put(audio_data)
  351. return True
  352. except Exception as e:
  353. self.logger.bind(tag=TAG).error(f"解析WebSocket音频包失败: {e}")
  354. # 处理失败,返回False表示需要继续处理
  355. return False
  356. def _process_websocket_audio(self, audio_data, timestamp):
  357. """处理WebSocket格式的音频包"""
  358. # 初始化时间戳序列管理
  359. if not hasattr(self, "audio_timestamp_buffer"):
  360. self.audio_timestamp_buffer = {}
  361. self.last_processed_timestamp = 0
  362. self.max_timestamp_buffer_size = 20
  363. # 如果时间戳是递增的,直接处理
  364. if timestamp >= self.last_processed_timestamp:
  365. self.asr_audio_queue.put(audio_data)
  366. self.last_processed_timestamp = timestamp
  367. # 处理缓冲区中的后续包
  368. processed_any = True
  369. while processed_any:
  370. processed_any = False
  371. for ts in sorted(self.audio_timestamp_buffer.keys()):
  372. if ts > self.last_processed_timestamp:
  373. buffered_audio = self.audio_timestamp_buffer.pop(ts)
  374. self.asr_audio_queue.put(buffered_audio)
  375. self.last_processed_timestamp = ts
  376. processed_any = True
  377. break
  378. else:
  379. # 乱序包,暂存
  380. if len(self.audio_timestamp_buffer) < self.max_timestamp_buffer_size:
  381. self.audio_timestamp_buffer[timestamp] = audio_data
  382. else:
  383. self.asr_audio_queue.put(audio_data)
  384. async def handle_restart(self, message):
  385. """处理服务器重启请求"""
  386. try:
  387. self.logger.bind(tag=TAG).info("收到服务器重启指令,准备执行...")
  388. # 发送确认响应
  389. await self.websocket.send(
  390. json.dumps(
  391. {
  392. "type": "server",
  393. "status": "success",
  394. "message": "服务器重启中...",
  395. "content": {"action": "restart"},
  396. }
  397. )
  398. )
  399. # 异步执行重启操作
  400. def restart_server():
  401. """实际执行重启的方法"""
  402. time.sleep(1)
  403. self.logger.bind(tag=TAG).info("执行服务器重启...")
  404. subprocess.Popen(
  405. [sys.executable, "app.py"],
  406. stdin=sys.stdin,
  407. stdout=sys.stdout,
  408. stderr=sys.stderr,
  409. start_new_session=True,
  410. )
  411. os._exit(0)
  412. # 使用线程执行重启避免阻塞事件循环
  413. threading.Thread(target=restart_server, daemon=True).start()
  414. except Exception as e:
  415. self.logger.bind(tag=TAG).error(f"重启失败: {str(e)}")
  416. await self.websocket.send(
  417. json.dumps(
  418. {
  419. "type": "server",
  420. "status": "error",
  421. "message": f"Restart failed: {str(e)}",
  422. "content": {"action": "restart"},
  423. }
  424. )
  425. )
  426. def _initialize_components(self):
  427. try:
  428. if self.tts is None:
  429. self.tts = self._initialize_tts()
  430. # 打开语音合成通道
  431. asyncio.run_coroutine_threadsafe(
  432. self.tts.open_audio_channels(self), self.loop
  433. )
  434. if self.need_bind:
  435. self.bind_completed_event.set()
  436. return
  437. self.selected_module_str = build_module_string(
  438. self.config.get("selected_module", {})
  439. )
  440. self.logger = create_connection_logger(self.selected_module_str)
  441. """初始化组件"""
  442. if self.config.get("prompt") is not None:
  443. user_prompt = self.config["prompt"]
  444. # 使用快速提示词进行初始化
  445. prompt = self.prompt_manager.get_quick_prompt(user_prompt)
  446. self.change_system_prompt(prompt)
  447. self.logger.bind(tag=TAG).info(
  448. f"快速初始化组件: prompt成功 {prompt[:50]}..."
  449. )
  450. """初始化本地组件"""
  451. if self.vad is None:
  452. self.vad = self._vad
  453. if self.asr is None:
  454. self.asr = self._initialize_asr()
  455. # 初始化声纹识别
  456. self._initialize_voiceprint()
  457. # 打开语音识别通道
  458. asyncio.run_coroutine_threadsafe(
  459. self.asr.open_audio_channels(self), self.loop
  460. )
  461. """加载记忆"""
  462. self._initialize_memory()
  463. """加载意图识别"""
  464. self._initialize_intent()
  465. """初始化上报线程"""
  466. self._init_report_threads()
  467. """更新系统提示词"""
  468. self._init_prompt_enhancement()
  469. except Exception as e:
  470. self.logger.bind(tag=TAG).error(f"实例化组件失败: {e}")
  471. def _init_prompt_enhancement(self):
  472. # 更新上下文信息
  473. self.prompt_manager.update_context_info(self, self.client_ip)
  474. enhanced_prompt = self.prompt_manager.build_enhanced_prompt(
  475. self.config["prompt"], self.device_id, self.client_ip
  476. )
  477. if enhanced_prompt:
  478. self.change_system_prompt(enhanced_prompt)
  479. self.logger.bind(tag=TAG).debug("系统提示词已增强更新")
  480. def _init_report_threads(self):
  481. """初始化ASR和TTS上报线程"""
  482. if not self.read_config_from_api or self.need_bind:
  483. return
  484. if self.chat_history_conf == 0:
  485. return
  486. if self.report_thread is None or not self.report_thread.is_alive():
  487. self.report_thread = threading.Thread(
  488. target=self._report_worker, daemon=True
  489. )
  490. self.report_thread.start()
  491. self.logger.bind(tag=TAG).info("TTS上报线程已启动")
  492. def _initialize_tts(self):
  493. """初始化TTS"""
  494. tts = None
  495. if not self.need_bind:
  496. tts = initialize_tts(self.config)
  497. if tts is None:
  498. tts = DefaultTTS(self.config, delete_audio_file=True)
  499. return tts
  500. def _initialize_asr(self):
  501. """初始化ASR"""
  502. if (
  503. self._asr is not None
  504. and hasattr(self._asr, "interface_type")
  505. and self._asr.interface_type == InterfaceType.LOCAL
  506. ):
  507. # 如果公共ASR是本地服务,则直接返回
  508. # 因为本地一个实例ASR,可以被多个连接共享
  509. asr = self._asr
  510. else:
  511. # 如果公共ASR是远程服务,则初始化一个新实例
  512. # 因为远程ASR,涉及到websocket连接和接收线程,需要每个连接一个实例
  513. asr = initialize_asr(self.config)
  514. return asr
  515. def _initialize_voiceprint(self):
  516. """为当前连接初始化声纹识别"""
  517. try:
  518. voiceprint_config = self.config.get("voiceprint", {})
  519. if voiceprint_config:
  520. voiceprint_provider = VoiceprintProvider(voiceprint_config)
  521. if voiceprint_provider is not None and voiceprint_provider.enabled:
  522. self.voiceprint_provider = voiceprint_provider
  523. self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用")
  524. else:
  525. self.logger.bind(tag=TAG).warning("声纹识别功能启用但配置不完整")
  526. else:
  527. self.logger.bind(tag=TAG).info("声纹识别功能未启用")
  528. except Exception as e:
  529. self.logger.bind(tag=TAG).warning(f"声纹识别初始化失败: {str(e)}")
  530. async def _background_initialize(self):
  531. """在后台初始化配置和组件(完全不阻塞主循环)"""
  532. try:
  533. # 异步获取差异化配置
  534. await self._initialize_private_config_async()
  535. # 在线程池中初始化组件
  536. self.executor.submit(self._initialize_components)
  537. except Exception as e:
  538. self.logger.bind(tag=TAG).error(f"后台初始化失败: {e}")
  539. async def _initialize_private_config_async(self):
  540. """从接口异步获取差异化配置(异步版本,不阻塞主循环)"""
  541. if not self.read_config_from_api:
  542. self.need_bind = False
  543. self.bind_completed_event.set()
  544. return
  545. try:
  546. begin_time = time.time()
  547. private_config = await get_private_config_from_api(
  548. self.config,
  549. self.headers.get("device-id"),
  550. self.headers.get("client-id", self.headers.get("device-id")),
  551. )
  552. private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
  553. self.logger.bind(tag=TAG).info(
  554. f"{time.time() - begin_time} 秒,异步获取差异化配置成功: {json.dumps(filter_sensitive_info(private_config), ensure_ascii=False)}"
  555. )
  556. self.need_bind = False
  557. self.bind_completed_event.set()
  558. except DeviceNotFoundException as e:
  559. self.need_bind = True
  560. private_config = {}
  561. except DeviceBindException as e:
  562. self.need_bind = True
  563. self.bind_code = e.bind_code
  564. private_config = {}
  565. except Exception as e:
  566. self.need_bind = True
  567. self.logger.bind(tag=TAG).error(f"异步获取差异化配置失败: {e}")
  568. private_config = {}
  569. init_llm, init_tts, init_memory, init_intent = (
  570. False,
  571. False,
  572. False,
  573. False,
  574. )
  575. init_vad = check_vad_update(self.common_config, private_config)
  576. init_asr = check_asr_update(self.common_config, private_config)
  577. if init_vad:
  578. self.config["VAD"] = private_config["VAD"]
  579. self.config["selected_module"]["VAD"] = private_config["selected_module"][
  580. "VAD"
  581. ]
  582. if init_asr:
  583. self.config["ASR"] = private_config["ASR"]
  584. self.config["selected_module"]["ASR"] = private_config["selected_module"][
  585. "ASR"
  586. ]
  587. if private_config.get("TTS", None) is not None:
  588. init_tts = True
  589. self.config["TTS"] = private_config["TTS"]
  590. self.config["selected_module"]["TTS"] = private_config["selected_module"][
  591. "TTS"
  592. ]
  593. if private_config.get("LLM", None) is not None:
  594. init_llm = True
  595. self.config["LLM"] = private_config["LLM"]
  596. self.config["selected_module"]["LLM"] = private_config["selected_module"][
  597. "LLM"
  598. ]
  599. if private_config.get("VLLM", None) is not None:
  600. self.config["VLLM"] = private_config["VLLM"]
  601. self.config["selected_module"]["VLLM"] = private_config["selected_module"][
  602. "VLLM"
  603. ]
  604. if private_config.get("Memory", None) is not None:
  605. init_memory = True
  606. self.config["Memory"] = private_config["Memory"]
  607. self.config["selected_module"]["Memory"] = private_config[
  608. "selected_module"
  609. ]["Memory"]
  610. if private_config.get("Intent", None) is not None:
  611. init_intent = True
  612. self.config["Intent"] = private_config["Intent"]
  613. model_intent = private_config.get("selected_module", {}).get("Intent", {})
  614. self.config["selected_module"]["Intent"] = model_intent
  615. # 加载插件配置
  616. if model_intent != "Intent_nointent":
  617. plugin_from_server = private_config.get("plugins", {})
  618. for plugin, config_str in plugin_from_server.items():
  619. plugin_from_server[plugin] = json.loads(config_str)
  620. self.config["plugins"] = plugin_from_server
  621. self.config["Intent"][self.config["selected_module"]["Intent"]][
  622. "functions"
  623. ] = plugin_from_server.keys()
  624. if private_config.get("prompt", None) is not None:
  625. self.config["prompt"] = private_config["prompt"]
  626. # 获取声纹信息
  627. if private_config.get("voiceprint", None) is not None:
  628. self.config["voiceprint"] = private_config["voiceprint"]
  629. if private_config.get("summaryMemory", None) is not None:
  630. self.config["summaryMemory"] = private_config["summaryMemory"]
  631. if private_config.get("device_max_output_size", None) is not None:
  632. self.max_output_size = int(private_config["device_max_output_size"])
  633. if private_config.get("chat_history_conf", None) is not None:
  634. self.chat_history_conf = int(private_config["chat_history_conf"])
  635. if private_config.get("mcp_endpoint", None) is not None:
  636. self.config["mcp_endpoint"] = private_config["mcp_endpoint"]
  637. if private_config.get("context_providers", None) is not None:
  638. self.config["context_providers"] = private_config["context_providers"]
  639. # 使用 run_in_executor 在线程池中执行 initialize_modules,避免阻塞主循环
  640. try:
  641. modules = await self.loop.run_in_executor(
  642. None, # 使用默认线程池
  643. initialize_modules,
  644. self.logger,
  645. private_config,
  646. init_vad,
  647. init_asr,
  648. init_llm,
  649. init_tts,
  650. init_memory,
  651. init_intent,
  652. )
  653. except Exception as e:
  654. self.logger.bind(tag=TAG).error(f"初始化组件失败: {e}")
  655. modules = {}
  656. if modules.get("tts", None) is not None:
  657. self.tts = modules["tts"]
  658. if modules.get("vad", None) is not None:
  659. self.vad = modules["vad"]
  660. if modules.get("asr", None) is not None:
  661. self.asr = modules["asr"]
  662. if modules.get("llm", None) is not None:
  663. self.llm = modules["llm"]
  664. if modules.get("intent", None) is not None:
  665. self.intent = modules["intent"]
  666. if modules.get("memory", None) is not None:
  667. self.memory = modules["memory"]
  668. def _initialize_memory(self):
  669. if self.memory is None:
  670. return
  671. """初始化记忆模块"""
  672. self.memory.init_memory(
  673. role_id=self.device_id,
  674. llm=self.llm,
  675. summary_memory=self.config.get("summaryMemory", None),
  676. save_to_file=not self.read_config_from_api,
  677. )
  678. # 获取记忆总结配置
  679. memory_config = self.config["Memory"]
  680. memory_type = self.config["Memory"][self.config["selected_module"]["Memory"]][
  681. "type"
  682. ]
  683. # 如果使用 nomen 或 mem_report_only,直接返回
  684. if memory_type == "nomem" or memory_type == "mem_report_only":
  685. return
  686. # 使用 mem_local_short 模式
  687. elif memory_type == "mem_local_short":
  688. memory_llm_name = memory_config[self.config["selected_module"]["Memory"]][
  689. "llm"
  690. ]
  691. if memory_llm_name and memory_llm_name in self.config["LLM"]:
  692. # 如果配置了专用LLM,则创建独立的LLM实例
  693. from core.utils import llm as llm_utils
  694. memory_llm_config = self.config["LLM"][memory_llm_name]
  695. memory_llm_type = memory_llm_config.get("type", memory_llm_name)
  696. memory_llm = llm_utils.create_instance(
  697. memory_llm_type, memory_llm_config
  698. )
  699. self.logger.bind(tag=TAG).info(
  700. f"为记忆总结创建了专用LLM: {memory_llm_name}, 类型: {memory_llm_type}"
  701. )
  702. self.memory.set_llm(memory_llm)
  703. else:
  704. # 否则使用主LLM
  705. self.memory.set_llm(self.llm)
  706. self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型")
  707. def _initialize_intent(self):
  708. if self.intent is None:
  709. return
  710. self.intent_type = self.config["Intent"][
  711. self.config["selected_module"]["Intent"]
  712. ]["type"]
  713. if self.intent_type == "function_call" or self.intent_type == "intent_llm":
  714. self.load_function_plugin = True
  715. """初始化意图识别模块"""
  716. # 获取意图识别配置
  717. intent_config = self.config["Intent"]
  718. intent_type = self.config["Intent"][self.config["selected_module"]["Intent"]][
  719. "type"
  720. ]
  721. # 如果使用 nointent,直接返回
  722. if intent_type == "nointent":
  723. return
  724. # 使用 intent_llm 模式
  725. elif intent_type == "intent_llm":
  726. intent_llm_name = intent_config[self.config["selected_module"]["Intent"]][
  727. "llm"
  728. ]
  729. if intent_llm_name and intent_llm_name in self.config["LLM"]:
  730. # 如果配置了专用LLM,则创建独立的LLM实例
  731. from core.utils import llm as llm_utils
  732. intent_llm_config = self.config["LLM"][intent_llm_name]
  733. intent_llm_type = intent_llm_config.get("type", intent_llm_name)
  734. intent_llm = llm_utils.create_instance(
  735. intent_llm_type, intent_llm_config
  736. )
  737. self.logger.bind(tag=TAG).info(
  738. f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}"
  739. )
  740. self.intent.set_llm(intent_llm)
  741. else:
  742. # 否则使用主LLM
  743. self.intent.set_llm(self.llm)
  744. self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型")
  745. """加载统一工具处理器"""
  746. self.func_handler = UnifiedToolHandler(self)
  747. # 异步初始化工具处理器
  748. if hasattr(self, "loop") and self.loop:
  749. asyncio.run_coroutine_threadsafe(self.func_handler._initialize(), self.loop)
  750. def change_system_prompt(self, prompt):
  751. self.prompt = prompt
  752. # 更新系统prompt至上下文
  753. self.dialogue.update_system_message(self.prompt)
  754. def chat(self, query, depth=0):
  755. # 保存当前任务的sentence_id到局部变量,避免被新任务覆盖
  756. current_sentence_id = None
  757. if query is not None:
  758. self.logger.bind(tag=TAG).info(f"大模型收到用户消息: {query}")
  759. # 为最顶层时新建会话ID和发送FIRST请求
  760. if depth == 0:
  761. current_sentence_id = str(uuid.uuid4().hex)
  762. self.sentence_id = current_sentence_id # 更新共享属性
  763. self.dialogue.put(Message(role="user", content=query))
  764. self.tts.tts_text_queue.put(
  765. TTSMessageDTO(
  766. sentence_id=current_sentence_id,
  767. sentence_type=SentenceType.FIRST,
  768. content_type=ContentType.ACTION,
  769. )
  770. )
  771. else:
  772. # 递归调用时,使用当前的sentence_id
  773. current_sentence_id = self.sentence_id
  774. # 设置最大递归深度,避免无限循环,可根据实际需求调整
  775. MAX_DEPTH = 5
  776. force_final_answer = False # 标记是否强制最终回答
  777. if depth >= MAX_DEPTH:
  778. self.logger.bind(tag=TAG).debug(
  779. f"已达到最大工具调用深度 {MAX_DEPTH},将强制基于现有信息回答"
  780. )
  781. force_final_answer = True
  782. # 添加系统指令,要求 LLM 基于现有信息回答
  783. self.dialogue.put(
  784. Message(
  785. role="user",
  786. content="[系统提示] 已达到最大工具调用次数限制,请你基于目前已经获取的所有信息,直接给出最终答案。不要再尝试调用任何工具。",
  787. )
  788. )
  789. # 长对话工具调用提醒:当对话轮数较多时,提醒模型正确使用工具
  790. force_reminder = False # 是否强制提醒
  791. if depth == 0 and query is not None:
  792. dialogue_length = len(self.dialogue.dialogue)
  793. current_turn = dialogue_length // 2
  794. # 检测距离上一次连续未调用工具的情况
  795. if self.tool_call_stats['last_call_turn'] >= 0:
  796. turns_since_last = current_turn - self.tool_call_stats['last_call_turn']
  797. if turns_since_last > 3: # 超过3轮未调用
  798. self.logger.bind(tag=TAG).warning(
  799. f"检测到{turns_since_last}轮未调用工具,可能进入偷懒模式,将强制注入提醒"
  800. )
  801. force_reminder = True
  802. # 对话历史截断:防止历史过长导致模型"偷懒模式"扩散
  803. # 当对话历史超过阈值时,保留最近的 10 轮对话
  804. # max_dialogue_turns = 10
  805. # if dialogue_length > max_dialogue_turns * 2:
  806. # removed = self.dialogue.trim_history(max_turns=max_dialogue_turns)
  807. # if removed > 0:
  808. # self.logger.bind(tag=TAG).info(
  809. # f"对话历史过长({dialogue_length}条),已智能截断保留最近{max_dialogue_turns}轮,移除{removed}条消息"
  810. # )
  811. # Define intent functions
  812. functions = None
  813. # 达到最大深度时,禁用工具调用,强制 LLM 直接回答
  814. if (
  815. self.intent_type == "function_call"
  816. and hasattr(self, "func_handler")
  817. and not force_final_answer
  818. ):
  819. functions = self.func_handler.get_functions()
  820. # 长对话工具调用规则强化:动态生成基于当前可用工具的提醒
  821. tool_call_reminder = None
  822. if depth == 0 and query is not None and functions is not None:
  823. dialogue_length = len(self.dialogue.dialogue)
  824. # 当对话历史超过4条消息时,注入规则强化
  825. if dialogue_length > 4:
  826. tool_summary = self._get_tool_summary(functions)
  827. if tool_summary:
  828. # 根据对话长度和偷懒检测,使用不同强度的提醒
  829. if force_reminder:
  830. # 强提醒 - 包含完整规则前缀
  831. tool_call_reminder = (
  832. TOOL_CALLING_RULES +
  833. f"[重要提醒] 多轮未使用工具,检查回复是否遗漏了必要的工具调用!上一轮未使用工具,本轮必须重新判断是否需要工具。"
  834. f"当前可用工具: {tool_summary}。"
  835. )
  836. reminder_level = "强"
  837. else:
  838. # 中等提醒 - 包含规则前缀
  839. tool_call_reminder = (
  840. TOOL_CALLING_RULES +
  841. f"当前可用工具: {tool_summary}。"
  842. f"仅当用户请求涉及实时信息查询或执行操作时调用,日常对话无需调用。"
  843. )
  844. reminder_level = "中"
  845. self.logger.bind(tag=TAG).debug(
  846. f"对话历史较长({dialogue_length}条),已注入{reminder_level}等级工具调用规则强化,当前可用工具:{tool_summary}"
  847. )
  848. response_message = []
  849. # 如果有工具调用提醒,临时添加到对话中(标记为临时消息)
  850. if tool_call_reminder:
  851. self.dialogue.put(Message(role="user", content=tool_call_reminder, is_temporary=True))
  852. try:
  853. # 使用带记忆的对话
  854. memory_str = None
  855. # 仅当query非空(代表用户询问)时查询记忆
  856. if self.memory is not None and query:
  857. future = asyncio.run_coroutine_threadsafe(
  858. self.memory.query_memory(query), self.loop
  859. )
  860. memory_str = future.result()
  861. if self.intent_type == "function_call" and functions is not None:
  862. # 使用支持functions的streaming接口
  863. llm_responses = self.llm.response_with_functions(
  864. self.session_id,
  865. self.dialogue.get_llm_dialogue_with_memory(
  866. memory_str, self.config.get("voiceprint", {})
  867. ),
  868. functions=functions,
  869. )
  870. else:
  871. llm_responses = self.llm.response(
  872. self.session_id,
  873. self.dialogue.get_llm_dialogue_with_memory(
  874. memory_str, self.config.get("voiceprint", {})
  875. ),
  876. )
  877. except Exception as e:
  878. self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
  879. return None
  880. # 处理流式响应
  881. tool_call_flag = False
  882. # 支持多个并行工具调用 - 使用列表存储
  883. tool_calls_list = [] # 格式: [{"id": "", "name": "", "arguments": ""}]
  884. content_arguments = ""
  885. emotion_flag = True
  886. try:
  887. for response in llm_responses:
  888. if self.client_abort:
  889. break
  890. if self.intent_type == "function_call" and functions is not None:
  891. content, tools_call = response
  892. if "content" in response:
  893. content = response["content"]
  894. tools_call = None
  895. if content is not None and len(content) > 0:
  896. content_arguments += content
  897. if not tool_call_flag and content_arguments.startswith("<tool_call>"):
  898. # print("content_arguments", content_arguments)
  899. tool_call_flag = True
  900. if tools_call is not None and len(tools_call) > 0:
  901. tool_call_flag = True
  902. self._merge_tool_calls(tool_calls_list, tools_call)
  903. else:
  904. content = response
  905. # 在llm回复中获取情绪表情,一轮对话只在开头获取一次
  906. if emotion_flag and content is not None and content.strip():
  907. asyncio.run_coroutine_threadsafe(
  908. textUtils.get_emotion(self, content),
  909. self.loop,
  910. )
  911. emotion_flag = False
  912. if content is not None and len(content) > 0:
  913. if not tool_call_flag:
  914. response_message.append(content)
  915. self.tts.tts_text_queue.put(
  916. TTSMessageDTO(
  917. sentence_id=current_sentence_id,
  918. sentence_type=SentenceType.MIDDLE,
  919. content_type=ContentType.TEXT,
  920. content_detail=content,
  921. )
  922. )
  923. except Exception as e:
  924. self.logger.bind(tag=TAG).error(f"LLM stream processing error: {e}")
  925. self.tts.tts_text_queue.put(
  926. TTSMessageDTO(
  927. sentence_id=current_sentence_id,
  928. sentence_type=SentenceType.MIDDLE,
  929. content_type=ContentType.TEXT,
  930. content_detail=get_system_error_response(self.config),
  931. )
  932. )
  933. if depth == 0:
  934. self.tts.tts_text_queue.put(
  935. TTSMessageDTO(
  936. sentence_id=current_sentence_id,
  937. sentence_type=SentenceType.LAST,
  938. content_type=ContentType.ACTION,
  939. )
  940. )
  941. return
  942. # 处理function call
  943. if tool_call_flag:
  944. bHasError = False
  945. # 处理基于文本的工具调用格式
  946. if len(tool_calls_list) == 0 and content_arguments:
  947. a = extract_json_from_string(content_arguments)
  948. if a is not None:
  949. try:
  950. content_arguments_json = json.loads(a)
  951. tool_calls_list.append(
  952. {
  953. "id": str(uuid.uuid4().hex),
  954. "name": content_arguments_json["name"],
  955. "arguments": json.dumps(
  956. content_arguments_json["arguments"],
  957. ensure_ascii=False,
  958. ),
  959. }
  960. )
  961. except Exception as e:
  962. bHasError = True
  963. response_message.append(a)
  964. else:
  965. bHasError = True
  966. response_message.append(content_arguments)
  967. if bHasError:
  968. self.logger.bind(tag=TAG).error(
  969. f"function call error: {content_arguments}"
  970. )
  971. if not bHasError and len(tool_calls_list) > 0:
  972. self.logger.bind(tag=TAG).debug(
  973. f"检测到 {len(tool_calls_list)} 个工具调用"
  974. )
  975. # 更新工具调用统计
  976. if depth == 0:
  977. current_turn = len(self.dialogue.dialogue) // 2
  978. self.tool_call_stats['last_call_turn'] = current_turn
  979. self.tool_call_stats['consecutive_no_call'] = 0
  980. self.logger.bind(tag=TAG).debug(
  981. f"工具调用统计更新: 当前轮次={current_turn}"
  982. )
  983. # LLM 流式阶段已播报过的文本
  984. streamed_text = ""
  985. if len(response_message) > 0:
  986. streamed_text = "".join(response_message)
  987. self.tts.store_tts_text(current_sentence_id, streamed_text)
  988. self.dialogue.put(Message(role="assistant", content=streamed_text))
  989. response_message.clear()
  990. # 收集所有工具调用的 Future
  991. futures_with_data = []
  992. for tool_call_data in tool_calls_list:
  993. self.logger.bind(tag=TAG).debug(
  994. f"function_name={tool_call_data['name']}, function_id={tool_call_data['id']}, function_arguments={tool_call_data['arguments']}"
  995. )
  996. # 使用公共方法上报工具调用
  997. tool_input = json.loads(tool_call_data.get("arguments") or "{}")
  998. enqueue_tool_report(self, tool_call_data['name'], tool_input)
  999. future = asyncio.run_coroutine_threadsafe(
  1000. self.func_handler.handle_llm_function_call(
  1001. self, tool_call_data
  1002. ),
  1003. self.loop,
  1004. )
  1005. futures_with_data.append((future, tool_call_data, tool_input))
  1006. # 工具调用超时时间,可配置,默认30秒
  1007. tool_call_timeout = int(self.config.get("tool_call_timeout", 30))
  1008. # 等待协程结束(实际等待时长为最慢的那个)
  1009. tool_results = []
  1010. for future, tool_call_data, tool_input in futures_with_data:
  1011. try:
  1012. result = future.result(timeout=tool_call_timeout)
  1013. tool_results.append((result, tool_call_data))
  1014. # 使用公共方法上报工具调用结果
  1015. enqueue_tool_report(self, tool_call_data['name'], tool_input, str(result.result) if result.result else None, report_tool_call=False)
  1016. except Exception as e:
  1017. self.logger.bind(tag=TAG).error(
  1018. f"工具调用超时或异常: {tool_call_data['name']}, 错误: {e}"
  1019. )
  1020. # 超时时返回错误响应,避免整个流程卡死
  1021. tool_results.append((
  1022. ActionResponse(action=Action.ERROR, result="哎呀,网络遇到点问题,请稍后再试下!"),
  1023. tool_call_data
  1024. ))
  1025. # 上报工具调用错误
  1026. enqueue_tool_report(self, tool_call_data['name'], tool_input, str(e), report_tool_call=False)
  1027. # 统一处理工具调用结果
  1028. if tool_results:
  1029. self._handle_function_result(tool_results, depth=depth, streamed_text=streamed_text)
  1030. # 存储对话内容
  1031. if len(response_message) > 0:
  1032. text_buff = "".join(response_message)
  1033. self.tts.store_tts_text(current_sentence_id, text_buff)
  1034. self.dialogue.put(Message(role="assistant", content=text_buff))
  1035. # 更新工具调用统计:如果没有调用工具,增加计数
  1036. if depth == 0 and not tool_call_flag:
  1037. self.tool_call_stats['consecutive_no_call'] += 1
  1038. if depth == 0:
  1039. self.tts.tts_text_queue.put(
  1040. TTSMessageDTO(
  1041. sentence_id=current_sentence_id,
  1042. sentence_type=SentenceType.LAST,
  1043. content_type=ContentType.ACTION,
  1044. )
  1045. )
  1046. # 使用lambda延迟计算,只有在DEBUG级别时才执行get_llm_dialogue()
  1047. self.logger.bind(tag=TAG).debug(
  1048. lambda: json.dumps(
  1049. self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False
  1050. )
  1051. )
  1052. # 清理临时插入的工具调用提醒消息(使用标记清理)
  1053. if tool_call_reminder and len(self.dialogue.dialogue) > 0:
  1054. original_length = len(self.dialogue.dialogue)
  1055. self.dialogue.dialogue = [
  1056. msg for msg in self.dialogue.dialogue
  1057. if not getattr(msg, 'is_temporary', False)
  1058. ]
  1059. if len(self.dialogue.dialogue) < original_length:
  1060. self.logger.bind(tag=TAG).debug("已清理临时的工具调用提醒消息")
  1061. return True
  1062. def _get_tool_summary(self, functions: list) -> str:
  1063. """
  1064. 从工具定义中提取摘要,用于规则强化注入
  1065. Args:
  1066. functions: 工具列表
  1067. Returns:
  1068. str: 工具名称字符串
  1069. """
  1070. if not functions:
  1071. return ""
  1072. datas = []
  1073. for func in functions:
  1074. func_info = func.get("function", {})
  1075. name = func_info.get("name", "")
  1076. datas.append(name)
  1077. result = "、".join(datas)
  1078. return result
  1079. def _handle_function_result(self, tool_results, depth, streamed_text=""):
  1080. need_llm_tools = []
  1081. for result, tool_call_data in tool_results:
  1082. if result.action in [
  1083. Action.RESPONSE,
  1084. Action.NOTFOUND,
  1085. Action.ERROR,
  1086. ]:
  1087. text = result.response if result.response else result.result
  1088. if streamed_text and text in streamed_text:
  1089. self.logger.bind(tag=TAG).debug(
  1090. f"Skipping duplicate TTS for tool {tool_call_data['name']}, already streamed"
  1091. )
  1092. else:
  1093. self.tts.tts_one_sentence(self, ContentType.TEXT, content_detail=text)
  1094. self.tts.store_tts_text(self.sentence_id, text)
  1095. self.dialogue.put(Message(role="assistant", content=text))
  1096. elif result.action == Action.REQLLM:
  1097. # 收集需要 LLM 处理的工具
  1098. need_llm_tools.append((result, tool_call_data))
  1099. else:
  1100. pass
  1101. if need_llm_tools:
  1102. all_tool_calls = [
  1103. {
  1104. "id": tool_call_data["id"],
  1105. "function": {
  1106. "arguments": (
  1107. "{}"
  1108. if tool_call_data["arguments"] == ""
  1109. else tool_call_data["arguments"]
  1110. ),
  1111. "name": tool_call_data["name"],
  1112. },
  1113. "type": "function",
  1114. "index": idx,
  1115. }
  1116. for idx, (_, tool_call_data) in enumerate(need_llm_tools)
  1117. ]
  1118. self.dialogue.put(Message(role="assistant", tool_calls=all_tool_calls))
  1119. for result, tool_call_data in need_llm_tools:
  1120. text = result.result
  1121. if text is not None and len(text) > 0:
  1122. self.dialogue.put(
  1123. Message(
  1124. role="tool",
  1125. tool_call_id=(
  1126. str(uuid.uuid4())
  1127. if tool_call_data["id"] is None
  1128. else tool_call_data["id"]
  1129. ),
  1130. content=text,
  1131. )
  1132. )
  1133. self.chat(None, depth=depth + 1)
  1134. def _report_worker(self):
  1135. """聊天记录上报工作线程"""
  1136. while not self.stop_event.is_set():
  1137. try:
  1138. # 从队列获取数据,设置超时以便定期检查停止事件
  1139. item = self.report_queue.get(timeout=1)
  1140. if item is None: # 检测毒丸对象
  1141. break
  1142. try:
  1143. # 检查线程池状态
  1144. if self.executor is None:
  1145. continue
  1146. # 提交任务到线程池
  1147. self.executor.submit(self._process_report, *item)
  1148. except Exception as e:
  1149. self.logger.bind(tag=TAG).error(f"聊天记录上报线程异常: {e}")
  1150. except queue.Empty:
  1151. continue
  1152. except Exception as e:
  1153. self.logger.bind(tag=TAG).error(f"聊天记录上报工作线程异常: {e}")
  1154. self.logger.bind(tag=TAG).info("聊天记录上报线程已退出")
  1155. def _process_report(self, type, text, audio_data, report_time):
  1156. """处理上报任务"""
  1157. try:
  1158. # 执行异步上报(在事件循环中运行)
  1159. asyncio.run(report(self, type, text, audio_data, report_time))
  1160. except Exception as e:
  1161. self.logger.bind(tag=TAG).error(f"上报处理异常: {e}")
  1162. finally:
  1163. # 标记任务完成
  1164. self.report_queue.task_done()
  1165. def clearSpeakStatus(self):
  1166. self.client_is_speaking = False
  1167. self.logger.bind(tag=TAG).debug(f"清除服务端讲话状态")
  1168. async def close(self, ws=None):
  1169. """资源清理方法"""
  1170. try:
  1171. # 清理 VAD 连接资源
  1172. if (
  1173. hasattr(self, "vad")
  1174. and self.vad
  1175. and hasattr(self.vad, "release_conn_resources")
  1176. ):
  1177. self.vad.release_conn_resources(self)
  1178. # 清理音频缓冲区
  1179. if self.server:
  1180. await self.server.unregister_connection(self)
  1181. if hasattr(self, "audio_buffer"):
  1182. self.audio_buffer.clear()
  1183. # 取消超时任务
  1184. if self.timeout_task and not self.timeout_task.done():
  1185. self.timeout_task.cancel()
  1186. try:
  1187. await self.timeout_task
  1188. except asyncio.CancelledError:
  1189. pass
  1190. self.timeout_task = None
  1191. # 清理工具处理器资源
  1192. if hasattr(self, "func_handler") and self.func_handler:
  1193. try:
  1194. await self.func_handler.cleanup()
  1195. except Exception as cleanup_error:
  1196. self.logger.bind(tag=TAG).error(
  1197. f"清理工具处理器时出错: {cleanup_error}"
  1198. )
  1199. # 触发停止事件
  1200. if self.stop_event:
  1201. self.stop_event.set()
  1202. # 清空任务队列
  1203. self.clear_queues()
  1204. # 关闭WebSocket连接
  1205. try:
  1206. if ws:
  1207. # 安全地检查WebSocket状态并关闭
  1208. try:
  1209. if hasattr(ws, "closed") and not ws.closed:
  1210. await ws.close()
  1211. elif hasattr(ws, "state") and ws.state.name != "CLOSED":
  1212. await ws.close()
  1213. else:
  1214. # 如果没有closed属性,直接尝试关闭
  1215. await ws.close()
  1216. except Exception:
  1217. # 如果关闭失败,忽略错误
  1218. pass
  1219. elif self.websocket:
  1220. try:
  1221. if (
  1222. hasattr(self.websocket, "closed")
  1223. and not self.websocket.closed
  1224. ):
  1225. await self.websocket.close()
  1226. elif (
  1227. hasattr(self.websocket, "state")
  1228. and self.websocket.state.name != "CLOSED"
  1229. ):
  1230. await self.websocket.close()
  1231. else:
  1232. # 如果没有closed属性,直接尝试关闭
  1233. await self.websocket.close()
  1234. except Exception:
  1235. # 如果关闭失败,忽略错误
  1236. pass
  1237. except Exception as ws_error:
  1238. self.logger.bind(tag=TAG).error(f"关闭WebSocket连接时出错: {ws_error}")
  1239. if self.tts:
  1240. await self.tts.close()
  1241. if self.asr:
  1242. await self.asr.close()
  1243. # 最后关闭线程池(避免阻塞)
  1244. if self.executor:
  1245. try:
  1246. self.executor.shutdown(wait=False)
  1247. except Exception as executor_error:
  1248. self.logger.bind(tag=TAG).error(
  1249. f"关闭线程池时出错: {executor_error}"
  1250. )
  1251. self.executor = None
  1252. self.logger.bind(tag=TAG).info("连接资源已释放")
  1253. except Exception as e:
  1254. self.logger.bind(tag=TAG).error(f"关闭连接时出错: {e}")
  1255. finally:
  1256. # 确保停止事件被设置
  1257. if self.stop_event:
  1258. self.stop_event.set()
  1259. def clear_queues(self):
  1260. """清空所有任务队列"""
  1261. if self.tts:
  1262. self.logger.bind(tag=TAG).debug(
  1263. f"开始清理: TTS队列大小={self.tts.tts_text_queue.qsize()}, 音频队列大小={self.tts.tts_audio_queue.qsize()}"
  1264. )
  1265. # 使用非阻塞方式清空队列
  1266. for q in [
  1267. self.tts.tts_text_queue,
  1268. self.tts.tts_audio_queue,
  1269. self.report_queue,
  1270. ]:
  1271. if not q:
  1272. continue
  1273. while True:
  1274. try:
  1275. q.get_nowait()
  1276. except queue.Empty:
  1277. break
  1278. # 重置音频流控器(取消后台任务并清空队列)
  1279. if hasattr(self, "audio_rate_controller") and self.audio_rate_controller:
  1280. self.audio_rate_controller.reset()
  1281. self.logger.bind(tag=TAG).debug("已重置音频流控器")
  1282. self.logger.bind(tag=TAG).debug(
  1283. f"清理结束: TTS队列大小={self.tts.tts_text_queue.qsize()}, 音频队列大小={self.tts.tts_audio_queue.qsize()}"
  1284. )
  1285. def reset_audio_states(self):
  1286. """
  1287. 重置所有音频相关状态(VAD + ASR)
  1288. """
  1289. # Reset VAD states
  1290. self.client_audio_buffer.clear()
  1291. self.client_have_voice = False
  1292. self.client_voice_stop = False
  1293. self.client_voice_window.clear()
  1294. self.last_is_voice = False
  1295. self.vad_last_voice_time = 0.0
  1296. # Clear ASR buffers
  1297. self.asr_audio.clear()
  1298. self.logger.bind(tag=TAG).debug("All audio states reset.")
  1299. def chat_and_close(self, text):
  1300. """Chat with the user and then close the connection"""
  1301. try:
  1302. # Use the existing chat method
  1303. self.chat(text)
  1304. # After chat is complete, close the connection
  1305. self.close_after_chat = True
  1306. except Exception as e:
  1307. self.logger.bind(tag=TAG).error(f"Chat and close error: {str(e)}")
  1308. async def _check_timeout(self):
  1309. """检查连接超时"""
  1310. try:
  1311. while not self.stop_event.is_set():
  1312. last_activity_time = self.last_activity_time
  1313. if self.need_bind:
  1314. last_activity_time = self.first_activity_time
  1315. # 检查是否超时(只有在时间戳已初始化的情况下)
  1316. if last_activity_time > 0.0:
  1317. current_time = time.time() * 1000
  1318. if current_time - last_activity_time > self.timeout_seconds * 1000:
  1319. if not self.stop_event.is_set():
  1320. self.logger.bind(tag=TAG).info("连接超时,准备关闭")
  1321. # 设置停止事件,防止重复处理
  1322. self.stop_event.set()
  1323. # 使用 try-except 包装关闭操作,确保不会因为异常而阻塞
  1324. try:
  1325. await self.close(self.websocket)
  1326. except Exception as close_error:
  1327. self.logger.bind(tag=TAG).error(
  1328. f"超时关闭连接时出错: {close_error}"
  1329. )
  1330. break
  1331. # 每10秒检查一次,避免过于频繁
  1332. await asyncio.sleep(10)
  1333. except Exception as e:
  1334. self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}")
  1335. finally:
  1336. self.logger.bind(tag=TAG).info("超时检查任务已退出")
  1337. def _merge_tool_calls(self, tool_calls_list, tools_call):
  1338. """合并工具调用列表
  1339. Args:
  1340. tool_calls_list: 已收集的工具调用列表
  1341. tools_call: 新的工具调用
  1342. """
  1343. for tool_call in tools_call:
  1344. tool_index = getattr(tool_call, "index", None)
  1345. if tool_index is None:
  1346. if tool_call.function.name:
  1347. # 有 function_name,说明是新的工具调用
  1348. tool_index = len(tool_calls_list)
  1349. else:
  1350. tool_index = len(tool_calls_list) - 1 if tool_calls_list else 0
  1351. # 确保列表有足够的位置
  1352. if tool_index >= len(tool_calls_list):
  1353. tool_calls_list.append({"id": "", "name": "", "arguments": ""})
  1354. # 更新工具调用信息
  1355. if tool_call.id:
  1356. tool_calls_list[tool_index]["id"] = tool_call.id
  1357. if tool_call.function.name:
  1358. tool_calls_list[tool_index]["name"] = tool_call.function.name
  1359. if tool_call.function.arguments:
  1360. tool_calls_list[tool_index]["arguments"] += tool_call.function.arguments