mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 17:13:54 +08:00
Merge branch 'main' into vosk_asr
This commit is contained in:
@@ -10,14 +10,21 @@ class IntentProviderBase(ABC):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.intent_options = config.get("intent_options", {
|
||||
"handle_exit_intent": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
||||
"play_music": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
|
||||
"get_weather": "查询天气, 用户希望查询某个地点的天气情况",
|
||||
"get_news": "查询新闻, 用户希望查询最新新闻或特定类型的新闻",
|
||||
"get_lunar": "用于获取今天的阴历/农历和黄历信息",
|
||||
"get_time": "获取今天日期或者当前时间信息",
|
||||
"continue_chat": "继续聊天",
|
||||
"end_chat": "结束聊天",
|
||||
"play_music": "播放音乐"
|
||||
})
|
||||
|
||||
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__))
|
||||
# 记录更详细的日志
|
||||
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}")
|
||||
|
||||
@abstractmethod
|
||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||
@@ -30,5 +37,6 @@ class IntentProviderBase(ABC):
|
||||
- "继续聊天"
|
||||
- "结束聊天"
|
||||
- "播放音乐 歌名" 或 "随机播放音乐"
|
||||
- "查询天气 地点名" 或 "查询天气 [当前位置]"
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from ..base import IntentProviderBase
|
||||
from typing import List, Dict
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class IntentProvider(IntentProviderBase):
|
||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||
"""
|
||||
默认的意图识别实现,始终返回继续聊天
|
||||
Args:
|
||||
dialogue_history: 对话历史记录列表
|
||||
text: 本次对话记录
|
||||
Returns:
|
||||
固定返回"继续聊天"
|
||||
"""
|
||||
logger.bind(tag=TAG).debug("Using functionCallProvider, always returning continue chat")
|
||||
return self.intent_options["continue_chat"]
|
||||
@@ -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,10 @@ 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个意图
|
||||
|
||||
def get_intent_system_prompt(self) -> str:
|
||||
"""
|
||||
@@ -22,62 +29,119 @@ class IntentProvider(IntentProviderBase):
|
||||
"""
|
||||
intent_list = []
|
||||
|
||||
"""
|
||||
"continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等",
|
||||
"end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
||||
"play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图"
|
||||
"""
|
||||
for key, value in self.intent_options.items():
|
||||
if key == "play_music":
|
||||
intent_list.append("3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图")
|
||||
elif key == "end_chat":
|
||||
intent_list.append("2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候")
|
||||
elif key == "continue_chat":
|
||||
intent_list.append("1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等")
|
||||
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. 思考意图类型,生成function_call格式"
|
||||
"\n\n"
|
||||
"返回格式示例:\n"
|
||||
"1. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n"
|
||||
"2. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n"
|
||||
"3. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||
"4. 结束对话意图: {\"function_call\": {\"name\": \"handle_exit_intent\", \"arguments\": {\"say_goodbye\": \"goodbye\"}}}\n"
|
||||
"5. 获取当天日期时间: {\"function_call\": {\"name\": \"get_time\"}}\n"
|
||||
"6. 获取当前黄历意图: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
|
||||
"7. 继续聊天意图: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||
"\n"
|
||||
"注意:\n"
|
||||
"- 播放音乐:无歌名时,song_name设为\"random\"\n"
|
||||
"- 查询天气:无地点时,location设为null\n"
|
||||
"- 查询新闻:无类别时,category设为null;查询详情时,detail设为true\n"
|
||||
"- 如果没有明显的意图,应按照继续聊天意图处理\n"
|
||||
"- 只返回纯JSON,不要任何其他内容\n"
|
||||
"\n"
|
||||
"示例分析:\n"
|
||||
"```\n"
|
||||
"用户: 你好小智\n"
|
||||
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\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"
|
||||
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 现在是几号了?现在几点了?\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_time\"}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 今天农历是多少?\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 我们明天再聊吧\n"
|
||||
"返回: {\"function_call\": {\"name\": \"handle_exit_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]
|
||||
|
||||
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}")
|
||||
|
||||
# 计算缓存键
|
||||
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 +158,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)}】"}
|
||||
Reference in New Issue
Block a user