intentHandler.py 11 KB

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