Merge pull request #515 from koalalgx/main

意图识别intent_llm单独配置独立的LLM,意图识别intent_llm增加天气和新闻查询function
This commit is contained in:
hrz
2025-03-27 10:43:49 +08:00
committed by GitHub
8 changed files with 915 additions and 175 deletions
+185 -22
View File
@@ -194,7 +194,36 @@ class ConnectionHandler:
"""加载记忆"""
device_id = self.headers.get("device-id", None)
self.memory.init_memory(device_id, self.llm)
self.intent.set_llm(self.llm)
"""为意图识别设置LLM,优先使用专用LLM"""
# 检查是否配置了专用的意图识别LLM
intent_llm_name = self.config.get("IntentLLM", "")
# 记录开始初始化意图识别LLM的时间
intent_llm_init_start = time.time()
if intent_llm_name and intent_llm_name in self.config["LLM"]:
# 如果配置了专用LLM,则创建独立的LLM实例
from core.utils import llm as llm_utils
intent_llm_config = self.config["LLM"][intent_llm_name]
intent_llm_type = intent_llm_config.get("type", intent_llm_name)
intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config)
self.logger.bind(tag=TAG).info(f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}")
# 记录额外的模型信息
model_name = intent_llm_config.get("model_name", "未指定")
base_url = intent_llm_config.get("base_url", "未指定")
self.logger.bind(tag=TAG).info(f"意图识别LLM详细信息 - 模型名称: {model_name}, 服务地址: {base_url}")
self.intent.set_llm(intent_llm)
else:
# 否则使用主LLM
self.intent.set_llm(self.llm)
self.logger.bind(tag=TAG).info("意图识别使用主LLM")
# 记录意图识别LLM初始化耗时
intent_llm_init_time = time.time() - intent_llm_init_start
self.logger.bind(tag=TAG).info(f"意图识别LLM初始化完成,耗时: {intent_llm_init_time:.4f}")
"""加载位置信息"""
self.client_ip_info = get_ip_info(self.client_ip)
@@ -308,7 +337,7 @@ class ConnectionHandler:
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
return True
def chat_with_function_calling(self, query, tool_call=False):
def chat_with_function_calling(self, query, tool_call=False, is_weather_query=False, is_news_query=False):
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
"""Chat with function calling for intent detection using streaming"""
if self.isNeedAuth():
@@ -326,7 +355,7 @@ class ConnectionHandler:
functions = self.func_handler.get_functions()
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
processed_chars = 0
try:
start_time = time.time()
@@ -335,14 +364,58 @@ class ConnectionHandler:
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result()
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
# 使用支持functions的streaming接口
llm_responses = self.llm.response_with_functions(
self.session_id,
self.dialogue.get_llm_dialogue_with_memory(memory_str),
functions=functions
)
# 为天气查询添加特殊处理
if is_weather_query:
self.logger.bind(tag=TAG).info(f"检测到天气查询,添加特殊指令")
# 获取对话历史
dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str)
# 找到最后一条tool消息(可能是天气数据)
for i in range(len(dialogue_with_memory) - 1, -1, -1):
if dialogue_with_memory[i].get("role") == "tool" and "当前天气" in dialogue_with_memory[i].get("content", ""):
# 添加特殊指令
dialogue_with_memory.append({
"role": "system",
"content": "请根据上面的天气数据,以简洁友好的方式回答用户的天气查询。直接告诉用户当前天气状况、温度以及可能需要的建议,不要提及数据来源或解释你是如何获取这些信息的。"
})
self.logger.bind(tag=TAG).info(f"已添加天气查询特殊指令")
break
# 使用支持functions的streaming接口并传入修改后的对话历史
llm_responses = self.llm.response_with_functions(
self.session_id,
dialogue_with_memory,
functions=functions
)
# 为新闻查询添加特殊处理
elif is_news_query:
self.logger.bind(tag=TAG).info(f"检测到新闻查询,添加特殊指令")
# 获取对话历史
dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str)
# 找到最后一条tool消息(可能是新闻数据)
for i in range(len(dialogue_with_memory) - 1, -1, -1):
if dialogue_with_memory[i].get("role") == "tool" and "新闻" in dialogue_with_memory[i].get("content", ""):
# 添加特殊指令
dialogue_with_memory.append({
"role": "system",
"content": "请根据上面的新闻数据,以简洁友好的方式回答用户的新闻查询。直接告诉用户新闻内容,不要提及数据来源或解释你是如何获取这些信息的。保持新闻播报的语气和风格。"
})
self.logger.bind(tag=TAG).info(f"已添加新闻查询特殊指令")
break
# 使用支持functions的streaming接口并传入修改后的对话历史
llm_responses = self.llm.response_with_functions(
self.session_id,
dialogue_with_memory,
functions=functions
)
else:
llm_responses = self.llm.response_with_functions(
self.session_id,
self.dialogue.get_llm_dialogue_with_memory(memory_str),
functions=functions
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
return None
@@ -460,36 +533,126 @@ class ConnectionHandler:
return True
def _handle_function_result(self, result, function_call_data, text_index):
self.logger.bind(tag=TAG).info(f"处理函数调用结果,动作类型: {result.action.name if result.action else 'None'}")
# 检查是否有备用直接回复
direct_response = getattr(result, 'response', None)
if result.action == Action.RESPONSE: # 直接回复前端
text = result.response
self.logger.bind(tag=TAG).info(f"函数返回直接回复: {text[:100] if text else 'None'}...")
self.recode_first_last_text(text, text_index)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=text))
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
self.logger.bind(tag=TAG).info(f"处理REQLLM动作,需要进一步处理结果")
text = result.result
function_id = function_call_data["id"]
function_name = function_call_data["name"]
function_arguments = function_call_data["arguments"]
if text is not None and len(text) > 0:
function_id = function_call_data["id"]
function_name = function_call_data["name"]
function_arguments = function_call_data["arguments"]
self.dialogue.put(Message(role='assistant',
tool_calls=[{"id": function_id,
"function": {"arguments": function_arguments,
self.logger.bind(tag=TAG).info(f"函数返回结果长度: {len(text)}, 前100字符: {text[:100]}...")
# 特殊处理天气查询
if function_name == "get_weather":
self.logger.bind(tag=TAG).info(f"检测到天气查询结果,使用特殊处理")
# 记录工具调用到对话历史
self.dialogue.put(Message(role='assistant',
tool_calls=[{"id": function_id,
"function": {"arguments": function_arguments,
"name": function_name},
"type": 'function',
"index": 0}]))
"type": 'function',
"index": 0}]))
# 记录工具返回结果到对话历史
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
try:
# 使用天气数据生成回复
self.chat_with_function_calling(text, tool_call=True, is_weather_query=True)
except Exception as e:
self.logger.bind(tag=TAG).error(f"处理天气查询数据失败: {e}")
if direct_response:
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
self.recode_first_last_text(direct_response, text_index)
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=direct_response))
# 特殊处理新闻查询
elif function_name == "get_news":
self.logger.bind(tag=TAG).info(f"检测到新闻查询结果,使用特殊处理")
# 记录工具调用到对话历史
self.dialogue.put(Message(role='assistant',
tool_calls=[{"id": function_id,
"function": {"arguments": function_arguments,
"name": function_name},
"type": 'function',
"index": 0}]))
# 记录工具返回结果到对话历史
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
try:
# 使用新闻数据生成回复,设置is_news_query=True
self.chat_with_function_calling(text, tool_call=True, is_news_query=True)
except Exception as e:
self.logger.bind(tag=TAG).error(f"处理新闻查询数据失败: {e}")
if direct_response:
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
self.recode_first_last_text(direct_response, text_index)
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=direct_response))
else:
# 其他类型的函数调用
self.dialogue.put(Message(role='assistant',
tool_calls=[{"id": function_id,
"function": {"arguments": function_arguments,
"name": function_name},
"type": 'function',
"index": 0}]))
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
self.chat_with_function_calling(text, tool_call=True)
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
try:
self.chat_with_function_calling(text, tool_call=True)
except Exception as e:
self.logger.bind(tag=TAG).error(f"处理函数调用结果失败: {e}")
if direct_response:
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
self.recode_first_last_text(direct_response, text_index)
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=direct_response))
else:
self.logger.bind(tag=TAG).warning(f"函数返回结果为空")
if direct_response:
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
self.recode_first_last_text(direct_response, text_index)
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=direct_response))
else:
error_text = f"抱歉,我无法获取{function_name}的结果,请稍后再试。"
self.recode_first_last_text(error_text, text_index)
future = self.executor.submit(self.speak_and_play, error_text, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=error_text))
elif result.action == Action.NOTFOUND:
text = result.result
self.logger.bind(tag=TAG).info(f"未找到对应函数: {text}")
self.recode_first_last_text(text, text_index)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put(future)
self.dialogue.put(Message(role="assistant", content=text))
else:
text = result.result
self.logger.bind(tag=TAG).info(f"其他动作类型,直接返回结果: {text[:100] if text else None}...")
self.recode_first_last_text(text, text_index)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put(future)
+244 -40
View File
@@ -4,6 +4,9 @@ import uuid
from core.handle.sendAudioHandle import send_stt_message
from core.handle.helloHandle import checkWakeupWords
from core.utils.util import remove_punctuation_and_length
import re
import asyncio
from loguru import logger
TAG = __name__
logger = setup_logging()
@@ -21,11 +24,11 @@ async def handle_user_intent(conn, text):
# 使用支持function calling的聊天方法,不再进行意图分析
return False
# 使用LLM进行意图分析
intent = await analyze_intent_with_llm(conn, text)
if not intent:
intent_result = await analyze_intent_with_llm(conn, text)
if not intent_result:
return False
# 处理各种意图
return await process_intent_result(conn, intent, text)
return await process_intent_result(conn, intent_result, text)
async def check_direct_exit(conn, text):
@@ -40,7 +43,6 @@ async def check_direct_exit(conn, text):
return False
async def analyze_intent_with_llm(conn, text):
"""使用LLM分析用户意图"""
if not hasattr(conn, 'intent') or not conn.intent:
@@ -51,49 +53,251 @@ async def analyze_intent_with_llm(conn, text):
dialogue = conn.dialogue
try:
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
# 尝试解析JSON结果
try:
intent_data = json.loads(intent_result)
if "intent" in intent_data:
return intent_data["intent"]
except json.JSONDecodeError:
# 如果不是JSON格式,尝试直接获取意图文本
return intent_result.strip()
return intent_result
except Exception as e:
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
return None
async def process_intent_result(conn, intent, original_text):
async def process_intent_result(conn, intent_result, original_text):
"""处理意图识别结果"""
# 处理退出意图
if "结束聊天" in intent:
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
# 如果是明确的离别意图,发送告别语并关闭连接
await send_stt_message(conn, original_text)
conn.executor.submit(conn.chat_and_close, original_text)
return True
try:
# 尝试将结果解析为JSON
intent_data = json.loads(intent_result)
# 检查是否有function_call
if "function_call" in intent_data:
# 直接从意图识别获取了function_call
logger.bind(tag=TAG).info(f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}")
function_name = intent_data["function_call"]["name"]
function_args = intent_data["function_call"]["arguments"]
# 确保参数是字符串格式的JSON
if isinstance(function_args, dict):
function_args = json.dumps(function_args)
function_call_data = {
"name": function_name,
"id": str(uuid.uuid4().hex),
"arguments": function_args
}
# 处理特定类型的函数调用
if function_name == "get_weather":
logger.bind(tag=TAG).info(f"识别到天气查询意图")
# 先发送消息确认
await send_stt_message(conn, original_text)
# 使用executor执行函数调用和结果处理
def process_weather_query():
# 直接调用函数
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result:
# 获取当前最新的文本索引
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
# 处理函数调用结果
conn._handle_function_result(result, function_call_data, text_index)
# 将函数执行放在线程池中
conn.executor.submit(process_weather_query)
return True
elif function_name == "play_music":
logger.bind(tag=TAG).info(f"识别到音乐播放意图")
# 先发送消息确认
await send_stt_message(conn, original_text)
# 使用executor执行函数调用和结果处理
def process_music_query():
# 直接调用函数
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result:
# 获取当前最新的文本索引
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
# 处理函数调用结果
conn._handle_function_result(result, function_call_data, text_index)
# 将函数执行放在线程池中
conn.executor.submit(process_music_query)
return True
elif function_name == "get_news":
logger.bind(tag=TAG).info(f"识别到新闻查询意图")
# 先发送消息确认
await send_stt_message(conn, original_text)
# 使用executor执行函数调用和结果处理
def process_news_query():
# 直接调用函数
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result:
# 获取当前最新的文本索引
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
# 处理函数调用结果
conn._handle_function_result(result, function_call_data, text_index)
# 将函数执行放在线程池中
conn.executor.submit(process_news_query)
return True
else:
# 其他类型的函数调用,尝试直接执行
# 先发送消息确认
await send_stt_message(conn, original_text)
# 使用executor执行函数调用和结果处理
def process_function_call():
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result:
# 获取当前最新的文本索引
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
# 处理函数调用结果
conn._handle_function_result(result, function_call_data, text_index)
# 将函数执行放在线程池中
conn.executor.submit(process_function_call)
return True
# 处理传统意图格式
elif "intent" in intent_data:
intent = intent_data["intent"]
# 处理退出意图
if "结束聊天" in intent:
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
# 如果是明确的离别意图,发送告别语并关闭连接
await send_stt_message(conn, original_text)
conn.executor.submit(conn.chat_and_close, original_text)
return True
# 其他不需要特殊处理的意图,让常规聊天流程处理
return False
except json.JSONDecodeError:
# 如果不是有效的JSON,尝试兼容旧格式
intent = intent_result
# 处理退出意图
if "结束聊天" in intent:
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
# 如果是明确的离别意图,发送告别语并关闭连接
await send_stt_message(conn, original_text)
conn.executor.submit(conn.chat_and_close, original_text)
return True
# 处理播放音乐意图
if "播放音乐" in intent:
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
# 调用play_music函数来播放音乐
song_name = extract_text_in_brackets(intent)
function_id = str(uuid.uuid4().hex)
function_name = "play_music"
function_arguments = '{ "song_name": "' + song_name + '" }'
function_call_data = {
"name": function_name,
"id": function_id,
"arguments": function_arguments
}
conn.func_handler.handle_llm_function_call(conn, function_call_data)
return True
# 其他意图处理可以在这里扩展
# 处理播放音乐意图
if "播放音乐" in intent:
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
# 获取歌曲名称
song_name = extract_text_in_brackets(intent)
# 先发送消息确认
await send_stt_message(conn, original_text)
# 构造合适的音乐播放函数调用
function_id = str(uuid.uuid4().hex)
function_name = "play_music"
function_arguments = '{ "song_name": ' + (f'"{song_name}"' if song_name else '"random"') + ' }'
function_call_data = {
"name": function_name,
"id": function_id,
"arguments": function_arguments
}
# 使用executor执行函数调用和结果处理
def process_music_query():
# 直接调用函数
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result:
# 获取当前最新的文本索引
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
# 处理函数调用结果
conn._handle_function_result(result, function_call_data, text_index)
# 将函数执行放在线程池中
conn.executor.submit(process_music_query)
return True
# 处理查询天气意图
if "查询天气" in intent:
logger.bind(tag=TAG).info(f"识别到天气查询意图: {intent}")
# 获取地点
location = extract_text_in_brackets(intent)
# 先发送消息确认
await send_stt_message(conn, original_text)
# 构造合适的天气查询函数调用
function_id = str(uuid.uuid4().hex)
function_name = "get_weather"
function_arguments = '{ "location": ' + (f'"{location}"' if location and location != "当前位置" else 'null') + ', "lang": "zh_CN" }'
function_call_data = {
"name": function_name,
"id": function_id,
"arguments": function_arguments
}
# 使用executor执行函数调用和结果处理
def process_weather_query():
# 直接调用函数
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result:
# 获取当前最新的文本索引
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
# 处理函数调用结果
conn._handle_function_result(result, function_call_data, text_index)
# 将函数执行放在线程池中
conn.executor.submit(process_weather_query)
return True
# 处理查询新闻意图
if "查询新闻" in intent or "播报新闻" in intent or "看新闻" in intent:
logger.bind(tag=TAG).info(f"识别到新闻查询意图: {intent}")
# 获取新闻类别
category = extract_text_in_brackets(intent)
# 先发送消息确认
await send_stt_message(conn, original_text)
# 构造合适的新闻查询函数调用
function_id = str(uuid.uuid4().hex)
function_name = "get_news"
# 判断是否是查询详情
detail = "详情" in intent or "详细" in intent
# 构造参数JSON字符串
if detail:
function_arguments = '{ "detail": true, "lang": "zh_CN" }'
else:
function_arguments = '{ "category": ' + (f'"{category}"' if category else 'null') + ', "detail": false, "lang": "zh_CN" }'
function_call_data = {
"name": function_name,
"id": function_id,
"arguments": function_arguments
}
# 使用executor执行函数调用和结果处理
def process_news_query():
# 直接调用函数
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
if result:
# 获取当前最新的文本索引
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
# 处理函数调用结果
conn._handle_function_result(result, function_call_data, text_index)
# 将函数执行放在线程池中
conn.executor.submit(process_news_query)
return True
except Exception as e:
logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
# 默认返回False,表示继续常规聊天流程
return False
@@ -112,4 +316,4 @@ def extract_text_in_brackets(s):
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
return s[left_bracket_index + 1:right_bracket_index]
else:
return ""
return ""
@@ -12,12 +12,22 @@ class IntentProviderBase(ABC):
self.intent_options = config.get("intent_options", {
"continue_chat": "继续聊天",
"end_chat": "结束聊天",
"play_music": "播放音乐"
"play_music": "播放音乐",
"get_weather": "查询天气",
"get_news": "查询新闻"
})
def set_llm(self, llm):
self.llm = llm
logger.bind(tag=TAG).debug("Set LLM for intent provider")
# 获取模型名称和类型信息
model_name = getattr(llm, 'model_name', str(llm.__class__.__name__))
model_type = getattr(llm, 'type', 'unknown')
# 记录更详细的日志
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}, 类型: {model_type}")
# 尝试获取模型基础URL
base_url = getattr(llm, 'base_url', 'N/A')
if base_url != 'N/A':
logger.bind(tag=TAG).debug(f"意图识别LLM基础URL: {base_url}")
@abstractmethod
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
@@ -30,5 +40,6 @@ class IntentProviderBase(ABC):
- "继续聊天"
- "结束聊天"
- "播放音乐 歌名""随机播放音乐"
- "查询天气 地点名""查询天气 [当前位置]"
"""
pass
@@ -3,6 +3,9 @@ from ..base import IntentProviderBase
from plugins_func.functions.play_music import initialize_music_handler
from config.logger import setup_logging
import re
import json
import hashlib
import time
TAG = __name__
logger = setup_logging()
@@ -13,6 +16,23 @@ class IntentProvider(IntentProviderBase):
super().__init__(config)
self.llm = None
self.promot = self.get_intent_system_prompt()
# 添加缓存管理
self.intent_cache = {} # 缓存意图识别结果
self.cache_expiry = 600 # 缓存有效期10分钟
self.cache_max_size = 100 # 最多缓存100个意图
self.common_patterns = {
"天气": '{\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}',
"新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}',
"财经新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}',
"国际新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"国际\", \"detail\": false, \"lang\": \"zh_CN\"}}}',
"社会新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"社会\", \"detail\": false, \"lang\": \"zh_CN\"}}}',
"详细介绍": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}',
"详情": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}',
"再见": '{\"intent\": \"结束聊天\"}',
"结束": '{\"intent\": \"结束聊天\"}',
"拜拜": '{\"intent\": \"结束聊天\"}',
"播放音乐": '{\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"random\"}}}'
}
def get_intent_system_prompt(self) -> str:
"""
@@ -25,7 +45,9 @@ class IntentProvider(IntentProviderBase):
"""
"continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等",
"end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
"play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图"
"play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
"get_weather": "4.查询天气, 用户希望查询某个地点的天气情况"
"get_news": "5.查询新闻, 用户希望查询最新新闻或特定类型的新闻"
"""
for key, value in self.intent_options.items():
if key == "play_music":
@@ -34,50 +56,141 @@ class IntentProvider(IntentProviderBase):
intent_list.append("2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候")
elif key == "continue_chat":
intent_list.append("1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等")
elif key == "get_weather":
intent_list.append("4.查询天气, 用户希望查询某个地点的天气情况")
elif key == "get_news":
intent_list.append("5.查询新闻, 用户希望查询最新新闻或特定类型的新闻")
else:
intent_list.append(value)
# "如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'。\n"
# "如果听不出具体歌名,可以返回'随机播放音乐'。\n"
# "只需要返回意图结果的json,不要解释。"
# "返回格式如下:\n"
prompt = (
"你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类(使用<start>和<end>标志)\n"
"你是一个意图识别助手。分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
"<start>"
f"{', '.join(intent_list)}"
"<end>\n"
"你需要按照以下的步骤处理用户的对话"
"1. 思考出对话的意图是哪一类的"
"2. 属于1和2的意图, 直接返回,返回格式如下:\n"
"{intent: '用户意图'}\n"
"3. 属于3的意图,则继续分析用户希望播放的音乐\n"
"4. 如果无法识别出具体歌名,可以返回'随机播放音乐'\n"
"{intent: '播放音乐 [获取的音乐名字]'}\n"
"下面是几个处理的示例(思考的内容不返回, 只返回json部分, 无额外的内容)\n"
"```"
"处理步骤:"
"1. 思考意图类型"
"2. 继续聊天和结束聊天意图: 返回intent格式"
"3. 播放音乐意图: 分析歌名,生成function_call格式"
"4. 查询天气意图: 分析地点,生成function_call格式"
"5. 查询新闻意图: 分析新闻类别,生成function_call格式"
"\n\n"
"返回格式示例:\n"
"1. 继续聊天意图: {\"intent\": \"继续聊天\"}\n"
"2. 结束聊天意图: {\"intent\": \"结束聊天\"}\n"
"3. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n"
"4. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n"
"5. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
"\n"
"注意:\n"
"- 播放音乐:无歌名时,song_name设为\"random\"\n"
"- 查询天气:无地点时,location设为null\n"
"- 查询新闻:无类别时,category设为null;查询详情时,detail设为true\n"
"- 只返回纯JSON,不要任何其他内容\n"
"\n"
"示例分析:\n"
"```\n"
"用户: 你今天怎么样?\n"
"思考(不返回): 用户发来的数据是一个问候语,属于继续聊天的意图, 是种类1, 种类1的需求是直接返回\n"
"返回结果: {intent: '继续聊天'}\n"
"```"
"用户: 我今天有点累了, 我们明天再聊吧\n"
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
"返回结果: {intent: '结束聊天'}\n"
"```"
"用户: 我今天有点累了, 我们明天再聊吧\n"
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
"返回结果: {intent: '结束聊天'}\n"
"```"
"用户: 你可以播放一首中秋月给我听吗\n"
"思考(不返回): 用户表达了想听音乐的续签,属于播放音乐的意图, 是种类3, 种类3的需求需要继续判断播放的音乐, 这里用户希望的歌曲名明确给出是中秋月\n"
"返回结果: {intent: '播放音乐 [中秋月]'}\n"
"```"
"你现在可以使用的音乐的名称如下(使用<start>和<end>标志):\n"
"返回: {\"intent\": \"继续聊天\"}\n"
"```\n"
"```\n"
"用户: 我们明天再聊吧\n"
"返回: {\"intent\": \"结束聊天\"}\n"
"```\n"
"```\n"
"用户: 播放中秋月\n"
"返回: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"中秋月\"}}}\n"
"```\n"
"```\n"
"用户: 北京天气怎么样\n"
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"北京\", \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 今天天气怎么样\n"
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 播报财经新闻\n"
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 有什么最新新闻\n"
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"```\n"
"用户: 详细介绍一下这条新闻\n"
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}\n"
"```\n"
"可用的音乐名称:\n"
)
return prompt
def clean_cache(self):
"""清理过期缓存"""
now = time.time()
# 找出过期键
expired_keys = [k for k, v in self.intent_cache.items() if now - v['timestamp'] > self.cache_expiry]
for key in expired_keys:
del self.intent_cache[key]
# 如果缓存太大,移除最旧的条目
if len(self.intent_cache) > self.cache_max_size:
# 按时间戳排序并保留最新的条目
sorted_items = sorted(self.intent_cache.items(), key=lambda x: x[1]['timestamp'])
for key, _ in sorted_items[:len(sorted_items) - self.cache_max_size]:
del self.intent_cache[key]
def check_pattern_match(self, text):
"""检查文本是否匹配常见模式,并提取关键信息"""
# 城市+天气的特殊模式匹配
city_weather_pattern = re.search(r'([^\s,,。?!]+)天气', text)
if city_weather_pattern:
city = city_weather_pattern.group(1)
# 排除可能的误匹配,如"今天天气"、"明天天气"、"现在天气"等
if city not in ["今天", "今日", "明天", "现在", "当前", "未来", "明日", "这两天", "近期"]:
logger.bind(tag=TAG).info(f"提取到城市名: {city}")
# 返回包含城市名的function_call
return f'{{\"function_call\": {{\"name\": \"get_weather\", \"arguments\": {{\"location\": \"{city}\", \"lang\": \"zh_CN\"}}}}}}'
# 普通模式匹配
for pattern, intent in self.common_patterns.items():
if pattern in text:
return intent
return None
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
if not self.llm:
raise ValueError("LLM provider not set")
# 记录整体开始时间
total_start_time = time.time()
# 打印使用的模型信息
model_info = getattr(self.llm, 'model_name', str(self.llm.__class__.__name__))
logger.bind(tag=TAG).info(f"使用意图识别模型: {model_info}")
# 先尝试简单的模式匹配
pattern_match = self.check_pattern_match(text)
if pattern_match:
pattern_time = time.time() - total_start_time
logger.bind(tag=TAG).info(f"模式匹配成功: {text} -> {pattern_match}, 耗时: {pattern_time:.4f}")
return pattern_match
# 计算缓存键
cache_key = hashlib.md5(text.encode()).hexdigest()
# 检查缓存
if cache_key in self.intent_cache:
cache_entry = self.intent_cache[cache_key]
# 检查缓存是否过期
if time.time() - cache_entry['timestamp'] <= self.cache_expiry:
cache_time = time.time() - total_start_time
logger.bind(tag=TAG).info(f"使用缓存的意图: {cache_key} -> {cache_entry['intent']}, 耗时: {cache_time:.4f}")
return cache_entry['intent']
# 清理缓存
self.clean_cache()
# 构建用户最后一句话的提示
msgStr = ""
@@ -94,18 +207,78 @@ class IntentProvider(IntentProviderBase):
music_file_names = music_config["music_file_names"]
prompt_music = f"{self.promot}\n<start>{music_file_names}\n<end>"
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
# 记录预处理完成时间
preprocess_time = time.time() - total_start_time
logger.bind(tag=TAG).debug(f"意图识别预处理耗时: {preprocess_time:.4f}")
# 使用LLM进行意图识别
llm_start_time = time.time()
logger.bind(tag=TAG).info(f"开始LLM意图识别调用, 模型: {model_info}")
intent = self.llm.response_no_stream(
system_prompt=prompt_music,
user_prompt=user_prompt
)
# 使用正则表达式提取大括号中的内容
# 使用正则表达式提取 {} 中的内容
match = re.search(r'\{.*?\}', intent)
# 记录LLM调用完成时间
llm_time = time.time() - llm_start_time
logger.bind(tag=TAG).info(f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}")
# 记录后处理开始时间
postprocess_start_time = time.time()
# 清理和解析响应
intent = intent.strip()
# 尝试提取JSON部分
match = re.search(r'\{.*\}', intent, re.DOTALL)
if match:
result = match.group(0)
intent = result
else:
intent = "{intent: '继续聊天'}"
logger.bind(tag=TAG).info(f"Detected intent: {intent}")
return intent.strip()
intent = match.group(0)
# 记录总处理时间
total_time = time.time() - total_start_time
logger.bind(tag=TAG).info(f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'")
# 尝试解析为JSON
try:
intent_data = json.loads(intent)
# 如果包含function_call,则格式化为适合处理的格式
if "function_call" in intent_data:
function_data = intent_data["function_call"]
function_name = function_data.get("name")
function_args = function_data.get("arguments", {})
# 记录识别到的function call
logger.bind(tag=TAG).info(f"识别到function call: {function_name}, 参数: {function_args}")
# 添加到缓存
self.intent_cache[cache_key] = {
'intent': intent,
'timestamp': time.time()
}
# 后处理时间
postprocess_time = time.time() - postprocess_start_time
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}")
# 确保返回完全序列化的JSON字符串
return intent
else:
# 添加到缓存
self.intent_cache[cache_key] = {
'intent': intent,
'timestamp': time.time()
}
# 后处理时间
postprocess_time = time.time() - postprocess_start_time
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}")
# 返回普通意图
return intent
except json.JSONDecodeError:
# 后处理时间
postprocess_time = time.time() - postprocess_start_time
logger.bind(tag=TAG).error(f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}")
# 如果解析失败,默认返回继续聊天意图
return "{\"intent\": \"继续聊天\"}"
@@ -0,0 +1,85 @@
from config.logger import setup_logging
from openai import OpenAI
import json
from core.providers.llm.base import LLMProviderBase
TAG = __name__
logger = setup_logging()
class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.model_name = config.get("model_name")
self.base_url = config.get("base_url", "http://localhost:9997")
# Initialize OpenAI client with Xinference base URL
# 如果没有v1,增加v1
if not self.base_url.endswith("/v1"):
self.base_url = f"{self.base_url}/v1"
logger.bind(tag=TAG).info(f"Initializing Xinference LLM provider with model: {self.model_name}, base_url: {self.base_url}")
try:
self.client = OpenAI(
base_url=self.base_url,
api_key="xinference" # Xinference has a similar setup to Ollama where it doesn't need an actual key
)
logger.bind(tag=TAG).info("Xinference client initialized successfully")
except Exception as e:
logger.bind(tag=TAG).error(f"Error initializing Xinference client: {e}")
raise
def response(self, session_id, dialogue):
try:
logger.bind(tag=TAG).debug(f"Sending request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}")
responses = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
stream=True
)
is_active=True
for chunk in responses:
try:
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
content = delta.content if hasattr(delta, 'content') else ''
if content:
if '<think>' in content:
is_active = False
content = content.split('<think>')[0]
if '</think>' in content:
is_active = True
content = content.split('</think>')[-1]
if is_active:
yield content
except Exception as e:
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Xinference response generation: {e}")
yield "【Xinference服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None):
try:
logger.bind(tag=TAG).debug(f"Sending function call request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}")
if functions:
logger.bind(tag=TAG).debug(f"Function calls enabled with: {[f.get('function', {}).get('name') for f in functions]}")
stream = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
stream=True,
tools=functions,
)
for chunk in stream:
delta = chunk.choices[0].delta
content = delta.content
tool_calls = delta.tool_calls
if content:
yield content, tool_calls
elif tool_calls:
yield None, tool_calls
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Xinference function call: {e}")
yield {"type": "content", "content": f"【Xinference服务响应异常: {str(e)}"}