intentHandler.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import json
  2. import uuid
  3. import asyncio
  4. from typing import TYPE_CHECKING
  5. if TYPE_CHECKING:
  6. from core.connection import ConnectionHandler
  7. from core.utils.dialogue import Message
  8. from core.providers.tts.dto.dto import ContentType
  9. from core.handle.helloHandle import checkWakeupWords
  10. from plugins_func.register import Action, ActionResponse
  11. from core.handle.sendAudioHandle import send_stt_message
  12. from core.handle.reportHandle import enqueue_tool_report
  13. from core.utils.util import remove_punctuation_and_length
  14. from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType
  15. TAG = __name__
  16. NAV_ALERT_ECHO_TTL_S = 12.0
  17. STRICT_HAZARD_MARK_PHRASES = {
  18. "标记急转弯": "sharp_turn",
  19. "标记起转弯": "sharp_turn",
  20. "标记急弯": "sharp_turn",
  21. "标记左转弯": "left_turn",
  22. "标记右转弯": "right_turn",
  23. "标记障碍": "obstacle",
  24. "标记颠簸": "bump",
  25. "标记大坑": "bump",
  26. "标记坑": "bump",
  27. "标记跳台": "jump",
  28. "标记陡坡": "slope",
  29. "标记涉水": "water",
  30. "标记水坑": "water",
  31. }
  32. NAV_ALERT_ECHO_PHRASES = {
  33. "前方左转弯",
  34. "前方右转弯",
  35. "准备急转弯",
  36. "前方颠簸",
  37. "前方水坑",
  38. "前方障碍",
  39. "前方跳台",
  40. "前方陡坡",
  41. "前方风险点",
  42. "注意左转弯",
  43. "注意右转弯",
  44. "注意急转弯",
  45. "注意颠簸",
  46. "注意水坑",
  47. "注意障碍",
  48. }
  49. async def handle_user_intent(conn: "ConnectionHandler", text):
  50. # 预处理输入文本,处理可能的JSON格式
  51. try:
  52. if text.strip().startswith("{") and text.strip().endswith("}"):
  53. parsed_data = json.loads(text)
  54. if isinstance(parsed_data, dict) and "content" in parsed_data:
  55. text = parsed_data["content"] # 提取content用于意图分析
  56. conn.current_speaker = parsed_data.get("speaker") # 保留说话人信息
  57. except (json.JSONDecodeError, TypeError):
  58. pass
  59. # 检查是否有明确的退出命令
  60. _, filtered_text = remove_punctuation_and_length(text)
  61. if await check_direct_exit(conn, filtered_text):
  62. return True
  63. # 检查是否是唤醒词
  64. if await checkWakeupWords(conn, filtered_text):
  65. return True
  66. if should_ignore_nav_alert_echo(conn, filtered_text):
  67. conn.logger.bind(tag=TAG).info(f"忽略导航播报回采文本: {filtered_text}")
  68. return True
  69. direct_function_call = match_direct_nav_command(filtered_text)
  70. if direct_function_call is not None:
  71. function_name, function_args = direct_function_call
  72. conn.logger.bind(tag=TAG).info(
  73. f"识别到直达导航命令: {filtered_text} -> {function_name}"
  74. )
  75. return await dispatch_function_call(
  76. conn,
  77. text,
  78. function_name,
  79. function_args,
  80. )
  81. if conn.intent_type == "function_call":
  82. # 使用支持function calling的聊天方法,不再进行意图分析
  83. return False
  84. # 使用LLM进行意图分析
  85. intent_result = await analyze_intent_with_llm(conn, text)
  86. if not intent_result:
  87. return False
  88. # 会话开始时生成sentence_id
  89. conn.sentence_id = str(uuid.uuid4().hex)
  90. # 处理各种意图
  91. return await process_intent_result(conn, intent_result, text)
  92. async def check_direct_exit(conn: "ConnectionHandler", text):
  93. """检查是否有明确的退出命令"""
  94. _, text = remove_punctuation_and_length(text)
  95. cmd_exit = conn.cmd_exit
  96. for cmd in cmd_exit:
  97. if text == cmd:
  98. conn.logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
  99. await send_stt_message(conn, text)
  100. await conn.close()
  101. return True
  102. return False
  103. async def analyze_intent_with_llm(conn: "ConnectionHandler", text):
  104. """使用LLM分析用户意图"""
  105. if not hasattr(conn, "intent") or not conn.intent:
  106. conn.logger.bind(tag=TAG).warning("意图识别服务未初始化")
  107. return None
  108. # 对话历史记录
  109. dialogue = conn.dialogue
  110. try:
  111. intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
  112. return intent_result
  113. except Exception as e:
  114. conn.logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
  115. return None
  116. def match_direct_nav_command(filtered_text: str):
  117. text = (filtered_text or "").strip()
  118. if not text:
  119. return None
  120. return None
  121. if text in {
  122. "开始录制路线",
  123. "开始记录路线",
  124. "录制路线",
  125. "开始记录",
  126. "开始记路线",
  127. }:
  128. return "nav_start_record", {}
  129. if text in {
  130. "结束录制",
  131. "停止录制",
  132. "结束记录",
  133. "停止记录",
  134. "录制结束",
  135. }:
  136. return "nav_stop_record", {}
  137. if text in {
  138. "开始预警",
  139. "开始播报",
  140. "开始领航",
  141. "开始导航",
  142. }:
  143. return "nav_start_run", {}
  144. if text in {
  145. "停止预警",
  146. "结束预警",
  147. "停止播报",
  148. "关闭预警",
  149. }:
  150. return "nav_stop_run", {}
  151. if text.startswith("标记"):
  152. if text in STRICT_HAZARD_MARK_PHRASES:
  153. return "nav_mark_hazard", {"hazard_type": STRICT_HAZARD_MARK_PHRASES[text]}
  154. return "nav_mark_hazard_reject", {}
  155. return None
  156. def should_ignore_nav_alert_echo(conn: "ConnectionHandler", filtered_text: str) -> bool:
  157. text = (filtered_text or "").strip()
  158. if not text:
  159. return False
  160. recent = getattr(conn, "_recent_nav_alert_notes", None)
  161. if not isinstance(recent, dict) or not recent:
  162. return False
  163. now = asyncio.get_running_loop().time()
  164. matched = False
  165. expired: list[str] = []
  166. for raw_text, spoken_at in recent.items():
  167. if now - float(spoken_at) > NAV_ALERT_ECHO_TTL_S:
  168. expired.append(raw_text)
  169. continue
  170. _, normalized = remove_punctuation_and_length(str(raw_text))
  171. if normalized == text:
  172. matched = True
  173. for raw_text in expired:
  174. recent.pop(raw_text, None)
  175. return matched
  176. async def dispatch_function_call(
  177. conn: "ConnectionHandler",
  178. original_text: str,
  179. function_name: str,
  180. function_args: dict | None = None,
  181. ):
  182. conn.sentence_id = str(uuid.uuid4().hex)
  183. function_args = function_args or {}
  184. function_args_json = json.dumps(function_args, ensure_ascii=False)
  185. function_call_data = {
  186. "name": function_name,
  187. "id": str(uuid.uuid4().hex),
  188. "arguments": function_args_json,
  189. }
  190. await send_stt_message(conn, original_text)
  191. conn.client_abort = False
  192. enqueue_tool_report(conn, function_name, function_args)
  193. def process_function_call():
  194. conn.dialogue.put(Message(role="user", content=original_text))
  195. tool_call_timeout = int(conn.config.get("tool_call_timeout", 30))
  196. try:
  197. result = asyncio.run_coroutine_threadsafe(
  198. conn.func_handler.handle_llm_function_call(conn, function_call_data),
  199. conn.loop,
  200. ).result(timeout=tool_call_timeout)
  201. except Exception as e:
  202. conn.logger.bind(tag=TAG).error(f"工具调用失败: {e}")
  203. result = ActionResponse(
  204. action=Action.ERROR,
  205. result="工具调用超时,请一会再试下哈",
  206. response="工具调用超时,请一会再试下哈",
  207. )
  208. if result:
  209. enqueue_tool_report(
  210. conn,
  211. function_name,
  212. function_args,
  213. str(result.result) if result.result else None,
  214. report_tool_call=False,
  215. )
  216. if result.action == Action.RESPONSE:
  217. text = result.response
  218. if text is not None:
  219. speak_txt(conn, text)
  220. elif result.action == Action.REQLLM:
  221. text = result.result
  222. conn.dialogue.put(Message(role="tool", content=text))
  223. llm_result = conn.intent.replyResult(text, original_text)
  224. if llm_result is None:
  225. llm_result = text
  226. speak_txt(conn, llm_result)
  227. elif result.action == Action.NOTFOUND or result.action == Action.ERROR:
  228. text = result.response if result.response else result.result
  229. if text is not None:
  230. speak_txt(conn, text)
  231. elif function_name != "play_music":
  232. text = result.response
  233. if text is None:
  234. text = result.result
  235. if text is not None:
  236. speak_txt(conn, text)
  237. conn.executor.submit(process_function_call)
  238. return True
  239. async def process_intent_result(
  240. conn: "ConnectionHandler", intent_result, original_text
  241. ):
  242. """处理意图识别结果"""
  243. try:
  244. # 尝试将结果解析为JSON
  245. intent_data = json.loads(intent_result)
  246. # 检查是否有function_call
  247. if "function_call" in intent_data:
  248. # 直接从意图识别获取了function_call
  249. conn.logger.bind(tag=TAG).debug(
  250. f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
  251. )
  252. function_name = intent_data["function_call"]["name"]
  253. if function_name == "continue_chat":
  254. return False
  255. if function_name == "result_for_context":
  256. await send_stt_message(conn, original_text)
  257. conn.client_abort = False
  258. def process_context_result():
  259. conn.dialogue.put(Message(role="user", content=original_text))
  260. from core.utils.current_time import get_current_time_info
  261. current_time, today_date, today_weekday, lunar_date = (
  262. get_current_time_info()
  263. )
  264. # 构建带上下文的基础提示
  265. context_prompt = f"""当前时间:{current_time}
  266. 今天日期:{today_date} ({today_weekday})
  267. 今天农历:{lunar_date}
  268. 请根据以上信息回答用户的问题:{original_text}"""
  269. response = conn.intent.replyResult(context_prompt, original_text)
  270. speak_txt(conn, response)
  271. conn.executor.submit(process_context_result)
  272. return True
  273. function_args = {}
  274. if "arguments" in intent_data["function_call"]:
  275. function_args = intent_data["function_call"]["arguments"]
  276. if function_args is None:
  277. function_args = {}
  278. if isinstance(function_args, str):
  279. function_args = json.loads(function_args) if function_args else {}
  280. return await dispatch_function_call(
  281. conn,
  282. original_text,
  283. function_name,
  284. function_args if isinstance(function_args, dict) else {},
  285. )
  286. return False
  287. except json.JSONDecodeError as e:
  288. conn.logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
  289. return False
  290. def speak_txt(conn: "ConnectionHandler", text):
  291. # 记录文本到 sentence_id 映射
  292. conn.tts.store_tts_text(conn.sentence_id, text)
  293. conn.tts.tts_text_queue.put(
  294. TTSMessageDTO(
  295. sentence_id=conn.sentence_id,
  296. sentence_type=SentenceType.FIRST,
  297. content_type=ContentType.ACTION,
  298. )
  299. )
  300. conn.tts.tts_one_sentence(conn, ContentType.TEXT, content_detail=text)
  301. conn.tts.tts_text_queue.put(
  302. TTSMessageDTO(
  303. sentence_id=conn.sentence_id,
  304. sentence_type=SentenceType.LAST,
  305. content_type=ContentType.ACTION,
  306. )
  307. )
  308. conn.dialogue.put(Message(role="assistant", content=text))