intentHandler.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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. if conn.intent_type == "function_call":
  34. # 使用支持function calling的聊天方法,不再进行意图分析
  35. return False
  36. # 使用LLM进行意图分析
  37. intent_result = await analyze_intent_with_llm(conn, text)
  38. if not intent_result:
  39. return False
  40. # 会话开始时生成sentence_id
  41. conn.sentence_id = str(uuid.uuid4().hex)
  42. # 处理各种意图
  43. return await process_intent_result(conn, intent_result, text)
  44. async def check_direct_exit(conn: "ConnectionHandler", text):
  45. """检查是否有明确的退出命令"""
  46. _, text = remove_punctuation_and_length(text)
  47. cmd_exit = conn.cmd_exit
  48. for cmd in cmd_exit:
  49. if text == cmd:
  50. conn.logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
  51. await send_stt_message(conn, text)
  52. await conn.close()
  53. return True
  54. return False
  55. async def analyze_intent_with_llm(conn: "ConnectionHandler", text):
  56. """使用LLM分析用户意图"""
  57. if not hasattr(conn, "intent") or not conn.intent:
  58. conn.logger.bind(tag=TAG).warning("意图识别服务未初始化")
  59. return None
  60. # 对话历史记录
  61. dialogue = conn.dialogue
  62. try:
  63. intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
  64. return intent_result
  65. except Exception as e:
  66. conn.logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
  67. return None
  68. async def process_intent_result(
  69. conn: "ConnectionHandler", intent_result, original_text
  70. ):
  71. """处理意图识别结果"""
  72. try:
  73. # 尝试将结果解析为JSON
  74. intent_data = json.loads(intent_result)
  75. # 检查是否有function_call
  76. if "function_call" in intent_data:
  77. # 直接从意图识别获取了function_call
  78. conn.logger.bind(tag=TAG).debug(
  79. f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
  80. )
  81. function_name = intent_data["function_call"]["name"]
  82. if function_name == "continue_chat":
  83. return False
  84. if function_name == "result_for_context":
  85. await send_stt_message(conn, original_text)
  86. conn.client_abort = False
  87. def process_context_result():
  88. conn.dialogue.put(Message(role="user", content=original_text))
  89. from core.utils.current_time import get_current_time_info
  90. current_time, today_date, today_weekday, lunar_date = (
  91. get_current_time_info()
  92. )
  93. # 构建带上下文的基础提示
  94. context_prompt = f"""当前时间:{current_time}
  95. 今天日期:{today_date} ({today_weekday})
  96. 今天农历:{lunar_date}
  97. 请根据以上信息回答用户的问题:{original_text}"""
  98. response = conn.intent.replyResult(context_prompt, original_text)
  99. speak_txt(conn, response)
  100. conn.executor.submit(process_context_result)
  101. return True
  102. function_args = {}
  103. if "arguments" in intent_data["function_call"]:
  104. function_args = intent_data["function_call"]["arguments"]
  105. if function_args is None:
  106. function_args = {}
  107. # 确保参数是字符串格式的JSON
  108. if isinstance(function_args, dict):
  109. function_args = json.dumps(function_args)
  110. function_call_data = {
  111. "name": function_name,
  112. "id": str(uuid.uuid4().hex),
  113. "arguments": function_args,
  114. }
  115. await send_stt_message(conn, original_text)
  116. conn.client_abort = False
  117. # 准备工具调用参数
  118. tool_input = {}
  119. if function_args:
  120. if isinstance(function_args, str):
  121. tool_input = json.loads(function_args) if function_args else {}
  122. elif isinstance(function_args, dict):
  123. tool_input = function_args
  124. # 上报工具调用
  125. enqueue_tool_report(conn, function_name, tool_input)
  126. # 使用executor执行函数调用和结果处理
  127. def process_function_call():
  128. conn.dialogue.put(Message(role="user", content=original_text))
  129. # 工具调用超时时间
  130. tool_call_timeout = int(conn.config.get("tool_call_timeout", 30))
  131. # 使用统一工具处理器处理所有工具调用
  132. try:
  133. result = asyncio.run_coroutine_threadsafe(
  134. conn.func_handler.handle_llm_function_call(
  135. conn, function_call_data
  136. ),
  137. conn.loop,
  138. ).result(timeout=tool_call_timeout)
  139. except Exception as e:
  140. conn.logger.bind(tag=TAG).error(f"工具调用失败: {e}")
  141. result = ActionResponse(
  142. action=Action.ERROR, result="工具调用超时,请一会再试下哈", response="工具调用超时,请一会再试下哈"
  143. )
  144. # 上报工具调用结果
  145. if result:
  146. enqueue_tool_report(conn, function_name, tool_input, str(result.result) if result.result else None, report_tool_call=False)
  147. if result.action == Action.RESPONSE: # 直接回复前端
  148. text = result.response
  149. if text is not None:
  150. speak_txt(conn, text)
  151. elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
  152. text = result.result
  153. conn.dialogue.put(Message(role="tool", content=text))
  154. llm_result = conn.intent.replyResult(text, original_text)
  155. if llm_result is None:
  156. llm_result = text
  157. speak_txt(conn, llm_result)
  158. elif (
  159. result.action == Action.NOTFOUND
  160. or result.action == Action.ERROR
  161. ):
  162. text = result.response if result.response else result.result
  163. if text is not None:
  164. speak_txt(conn, text)
  165. elif function_name != "play_music":
  166. # For backward compatibility with original code
  167. # 获取当前最新的文本索引
  168. text = result.response
  169. if text is None:
  170. text = result.result
  171. if text is not None:
  172. speak_txt(conn, text)
  173. # 将函数执行放在线程池中
  174. conn.executor.submit(process_function_call)
  175. return True
  176. return False
  177. except json.JSONDecodeError as e:
  178. conn.logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
  179. return False
  180. def speak_txt(conn: "ConnectionHandler", text):
  181. # 记录文本到 sentence_id 映射
  182. conn.tts.store_tts_text(conn.sentence_id, text)
  183. conn.tts.tts_text_queue.put(
  184. TTSMessageDTO(
  185. sentence_id=conn.sentence_id,
  186. sentence_type=SentenceType.FIRST,
  187. content_type=ContentType.ACTION,
  188. )
  189. )
  190. conn.tts.tts_one_sentence(conn, ContentType.TEXT, content_detail=text)
  191. conn.tts.tts_text_queue.put(
  192. TTSMessageDTO(
  193. sentence_id=conn.sentence_id,
  194. sentence_type=SentenceType.LAST,
  195. content_type=ContentType.ACTION,
  196. )
  197. )
  198. conn.dialogue.put(Message(role="assistant", content=text))