From 88455604a2a32c6d666a240b73e2f996fc0e61e6 Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:22:34 +0800
Subject: [PATCH 01/11] Update config.yaml
---
main/xiaozhi-server/config.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index 2fe438cf..ffa2da33 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -86,7 +86,7 @@ selected_module:
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
- Intent: function_call
+ Intent: intent_llm
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
Intent:
@@ -534,4 +534,4 @@ wakeup_words:
- "小龙小龙"
- "喵喵同学"
- "小滨小滨"
- - "小冰小冰"
\ No newline at end of file
+ - "小冰小冰"
From 2199b8eca701c906b5472b87ddaec7b8042b9ce9 Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:24:05 +0800
Subject: [PATCH 02/11] Update config.yaml
---
main/xiaozhi-server/config.yaml | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index ffa2da33..3c770c88 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -88,6 +88,9 @@ selected_module:
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
Intent: intent_llm
+# 意图识别专用LLM配置,如果设置,则意图识别会使用这个模型而不是主LLM
+IntentLLM: XinferenceSmallLLM #ChatGLMLLM #XinferenceSmallLLM # 设置为空字符串""或不设置则使用主LLM
+
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
Intent:
# 不使用意图识别
@@ -181,6 +184,18 @@ VAD:
LLM:
# 所有openai类型均可以修改超参,以AliLLM为例
# 当前支持的type为openai、dify、ollama,可自行适配
+ XinferenceLLM:
+ # 定义LLM API类型
+ type: xinference
+ # Xinference服务地址和模型名称
+ model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型
+ base_url: http://localhost:9997 # Xinference服务地址
+ XinferenceSmallLLM:
+ # 定义轻量级LLM API类型,用于意图识别
+ type: xinference
+ # Xinference服务地址和模型名称
+ model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别
+ base_url: http://localhost:9997 # Xinference服务地址
AliLLM:
# 定义LLM API类型
type: openai
From 38191f976bd4f4eb36d5be880f887172a999a4f3 Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:25:31 +0800
Subject: [PATCH 03/11] Create xinference.py
---
.../providers/llm/xinference/xinference.py | 85 +++++++++++++++++++
1 file changed, 85 insertions(+)
create mode 100644 main/xiaozhi-server/core/providers/llm/xinference/xinference.py
diff --git a/main/xiaozhi-server/core/providers/llm/xinference/xinference.py b/main/xiaozhi-server/core/providers/llm/xinference/xinference.py
new file mode 100644
index 00000000..b90b0418
--- /dev/null
+++ b/main/xiaozhi-server/core/providers/llm/xinference/xinference.py
@@ -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 '' in content:
+ is_active = False
+ content = content.split('')[0]
+ if '' in content:
+ is_active = True
+ content = content.split('')[-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)}】"}
From 32d7400f7d92d08d75fb2f26eba2dac524e079d0 Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:26:46 +0800
Subject: [PATCH 04/11] Update intent_llm.py
---
.../providers/intent/intent_llm/intent_llm.py | 253 +++++++++++++++---
1 file changed, 213 insertions(+), 40 deletions(-)
diff --git a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
index 9d5d2507..5d0b0830 100644
--- a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
+++ b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
@@ -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 = (
- "你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类(使用和标志):\n"
+ "你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
""
f"{', '.join(intent_list)}"
"\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"
- "```"
- "你现在可以使用的音乐的名称如下(使用和标志):\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{music_file_names}\n"
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\": \"继续聊天\"}"
From 63a3c33df93d09b252fce6cf53e37781b14d23c4 Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:27:10 +0800
Subject: [PATCH 05/11] Update base.py
---
main/xiaozhi-server/core/providers/intent/base.py | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py
index a691d6cb..877dfced 100644
--- a/main/xiaozhi-server/core/providers/intent/base.py
+++ b/main/xiaozhi-server/core/providers/intent/base.py
@@ -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
From 3a3ddbc2bc691ce706e6a9034ca634cb6f8af13e Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:27:48 +0800
Subject: [PATCH 06/11] Update connection.py
---
main/xiaozhi-server/core/connection.py | 207 ++++++++++++++++++++++---
1 file changed, 185 insertions(+), 22 deletions(-)
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index c9a52b12..3391e126 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -196,7 +196,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)
@@ -310,7 +339,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)
From 9cbad1a4488ea8c62f6f168c5bb784d53924ad26 Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:28:32 +0800
Subject: [PATCH 07/11] Update get_news.py
---
.../plugins_func/functions/get_news.py | 29 ++++++++++++++-----
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/main/xiaozhi-server/plugins_func/functions/get_news.py b/main/xiaozhi-server/plugins_func/functions/get_news.py
index 43f8ea48..f7a6d342 100644
--- a/main/xiaozhi-server/plugins_func/functions/get_news.py
+++ b/main/xiaozhi-server/plugins_func/functions/get_news.py
@@ -128,13 +128,15 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
# 如果detail为True,获取上一条新闻的详细内容
if detail:
if not hasattr(conn, 'last_news_link') or not conn.last_news_link or 'link' not in conn.last_news_link:
- return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
+ direct_response = "抱歉,没有找到最近查询的新闻,请先获取一条新闻。"
+ return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", direct_response)
link = conn.last_news_link.get('link')
title = conn.last_news_link.get('title', '未知标题')
if link == '#':
- return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
+ direct_response = "抱歉,该新闻没有可用的链接获取详细内容。"
+ return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", direct_response)
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
@@ -142,7 +144,8 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
detail_content = fetch_news_detail(link)
if not detail_content or detail_content == "无法获取详细内容":
- return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
+ direct_response = f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。"
+ return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", direct_response)
# 构建详情报告
detail_report = (
@@ -153,7 +156,10 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
)
- return ActionResponse(Action.REQLLM, detail_report, None)
+ # 构造一个直接回复,简要概括新闻内容
+ direct_response = f"以下是《{title}》的详细内容:{detail_content[:200]}...(内容过长已省略)"
+
+ return ActionResponse(Action.REQLLM, detail_report, direct_response)
# 否则,获取新闻列表并随机选择一条
# 从配置中获取RSS URL
@@ -174,7 +180,8 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
news_items = fetch_news_from_rss(rss_url)
if not news_items:
- return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
+ direct_response = "抱歉,未能获取到新闻信息,请稍后再试。"
+ return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", direct_response)
# 随机选择一条新闻
selected_news = random.choice(news_items)
@@ -198,8 +205,16 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)"
)
- return ActionResponse(Action.REQLLM, news_report, None)
+ # 构造一个直接回复版本,简要播报新闻
+ direct_response = (
+ f"最新{mapped_category or ''}新闻:{selected_news['title']}。"
+ f"{selected_news['description'][:150]}..."
+ f"如果您想了解更多详情,可以说'请详细介绍这条新闻'。"
+ )
+
+ return ActionResponse(Action.REQLLM, news_report, direct_response)
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
- return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
\ No newline at end of file
+ direct_response = "抱歉,获取新闻时发生错误,请稍后再试。"
+ return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", direct_response)
From 44c3e73577afcc7fcb6163d0d2c8f2a290381e3d Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:28:59 +0800
Subject: [PATCH 08/11] Update intentHandler.py
---
.../core/handle/intentHandler.py | 284 +++++++++++++++---
1 file changed, 244 insertions(+), 40 deletions(-)
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index d8440fc6..900a25cf 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -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 ""
\ No newline at end of file
+ return ""
From fbd25353d5e36a1372c6398f8cddf20cf155f5dd Mon Sep 17 00:00:00 2001
From: koalalgx <82762701+koalalgx@users.noreply.github.com>
Date: Tue, 25 Mar 2025 14:29:28 +0800
Subject: [PATCH 09/11] Update get_weather.py
---
.../plugins_func/functions/get_weather.py | 198 ++++++++++++------
1 file changed, 136 insertions(+), 62 deletions(-)
diff --git a/main/xiaozhi-server/plugins_func/functions/get_weather.py b/main/xiaozhi-server/plugins_func/functions/get_weather.py
index 8cdd6ea1..58f63844 100644
--- a/main/xiaozhi-server/plugins_func/functions/get_weather.py
+++ b/main/xiaozhi-server/plugins_func/functions/get_weather.py
@@ -2,6 +2,7 @@ import requests
from bs4 import BeautifulSoup
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
+import json
TAG = __name__
logger = setup_logging()
@@ -39,85 +40,158 @@ HEADERS = {
)
}
-# 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons
-WEATHER_CODE_MAP = {
- "100": "晴", "101": "多云", "102": "少云", "103": "晴间多云", "104": "阴",
- "150": "晴", "151": "多云", "152": "少云", "153": "晴间多云",
- "300": "阵雨", "301": "强阵雨", "302": "雷阵雨", "303": "强雷阵雨", "304": "雷阵雨伴有冰雹",
- "305": "小雨", "306": "中雨", "307": "大雨", "308": "极端降雨", "309": "毛毛雨/细雨",
- "310": "暴雨", "311": "大暴雨", "312": "特大暴雨", "313": "冻雨", "314": "小到中雨",
- "315": "中到大雨", "316": "大到暴雨", "317": "暴雨到大暴雨", "318": "大暴雨到特大暴雨",
- "350": "阵雨", "351": "强阵雨", "399": "雨",
- "400": "小雪", "401": "中雪", "402": "大雪", "403": "暴雪", "404": "雨夹雪",
- "405": "雨雪天气", "406": "阵雨夹雪", "407": "阵雪", "408": "小到中雪", "409": "中到大雪", "410": "大到暴雪",
- "456": "阵雨夹雪", "457": "阵雪", "499": "雪",
- "500": "薄雾", "501": "雾", "502": "霾", "503": "扬沙", "504": "浮尘",
- "507": "沙尘暴", "508": "强沙尘暴",
- "509": "浓雾", "510": "强浓雾", "511": "中度霾", "512": "重度霾", "513": "严重霾", "514": "大雾", "515": "特强浓雾",
- "900": "热", "901": "冷", "999": "未知"
-}
def fetch_city_info(location, api_key):
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
- response = requests.get(url, headers=HEADERS).json()
- return response.get('location', [])[0] if response.get('location') else None
+ logger.bind(tag=TAG).info(f"正在请求城市信息API,URL: {url}")
+ try:
+ response = requests.get(url, headers=HEADERS)
+ logger.bind(tag=TAG).info(f"城市信息API响应状态码: {response.status_code}")
+ json_data = response.json()
+ logger.bind(tag=TAG).debug(f"城市信息API响应: {json.dumps(json_data, ensure_ascii=False)}")
+ return json_data.get('location', [])[0] if json_data.get('location') else None
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"获取城市信息失败: {str(e)}")
+ return None
def fetch_weather_page(url):
- response = requests.get(url, headers=HEADERS)
- return BeautifulSoup(response.text, "html.parser") if response.ok else None
+ logger.bind(tag=TAG).info(f"正在请求天气页面,URL: {url}")
+ try:
+ response = requests.get(url, headers=HEADERS)
+ logger.bind(tag=TAG).info(f"天气页面响应状态码: {response.status_code}")
+ if not response.ok:
+ logger.bind(tag=TAG).error(f"获取天气页面失败: {response.text[:200]}")
+ return BeautifulSoup(response.text, "html.parser") if response.ok else None
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"获取天气页面失败: {str(e)}")
+ return None
def parse_weather_info(soup):
- city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True)
+ try:
+ city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True)
+ logger.bind(tag=TAG).info(f"解析到城市名称: {city_name}")
- current_abstract = soup.select_one(".c-city-weather-current .current-abstract")
- current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知"
+ current_abstract = soup.select_one(".c-city-weather-current .current-abstract")
+ current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知"
+ logger.bind(tag=TAG).info(f"当前天气概况: {current_abstract}")
- current_basic = {}
- for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"):
- parts = item.get_text(strip=True, separator=" ").split(" ")
- if len(parts) == 2:
- key, value = parts[1], parts[0]
- current_basic[key] = value
+ current_basic = {}
+ for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"):
+ parts = item.get_text(strip=True, separator=" ").split(" ")
+ if len(parts) == 2:
+ key, value = parts[1], parts[0]
+ current_basic[key] = value
+ logger.bind(tag=TAG).info(f"当前天气详情: {json.dumps(current_basic, ensure_ascii=False)}")
- temps_list = []
- for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
- date = row.select_one(".date-bg .date").get_text(strip=True)
- weather_code = row.select_one(".date-bg .icon")["src"].split("/")[-1].split(".")[0]
- weather = WEATHER_CODE_MAP.get(weather_code, "未知")
- temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
- high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
- temps_list.append((date, weather, high_temp, low_temp))
+ temps_list = []
+ for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
+ date = row.select_one(".date-bg .date").get_text(strip=True)
+ temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
+ high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
+ temps_list.append((date, high_temp, low_temp))
+ logger.bind(tag=TAG).info(f"获取到未来7天温度: {temps_list}")
- return city_name, current_abstract, current_basic, temps_list
+ return city_name, current_abstract, current_basic, temps_list
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"解析天气信息失败: {str(e)}")
+ return "未知城市", "未知天气", {}, []
@register_function('get_weather', GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
- api_key = conn.config["plugins"]["get_weather"]["api_key"]
- default_location = conn.config["plugins"]["get_weather"]["default_location"]
- location = location or conn.client_ip_info.get("city") or default_location
- logger.bind(tag=TAG).debug(f"获取天气: {location}")
+ logger.bind(tag=TAG).info(f"===== 天气查询函数开始执行 =====")
+ logger.bind(tag=TAG).info(f"查询参数: location={location}, lang={lang}")
+
+ try:
+ api_key = conn.config["plugins"]["get_weather"]["api_key"]
+ default_location = conn.config["plugins"]["get_weather"]["default_location"]
+
+ # 位置参数处理逻辑优化
+ # 1. 如果传入了明确的位置参数且不为None/空字符串,则使用该参数
+ # 2. 否则,尝试使用客户端IP地址对应的城市信息
+ # 3. 如果上述都无效,则使用配置文件中的默认位置
+ if location and location.strip():
+ final_location = location.strip()
+ logger.bind(tag=TAG).info(f"使用用户明确指定的位置: {final_location}")
+ elif conn.client_ip_info and "city" in conn.client_ip_info and conn.client_ip_info["city"]:
+ final_location = conn.client_ip_info["city"]
+ logger.bind(tag=TAG).info(f"使用IP地址解析的位置: {final_location}")
+ else:
+ final_location = default_location
+ logger.bind(tag=TAG).info(f"使用配置文件中的默认位置: {final_location}")
+
+ logger.bind(tag=TAG).info(f"最终使用的查询地点: {final_location}, API Key: {api_key}")
- city_info = fetch_city_info(location, api_key)
- if not city_info:
- return ActionResponse(Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None)
+ city_info = fetch_city_info(final_location, api_key)
+ if not city_info:
+ error_msg = f"未找到相关的城市: {final_location},请确认地点是否正确"
+ logger.bind(tag=TAG).error(error_msg)
+ # 构造一个直接回复
+ direct_response = f"抱歉,我未能找到\"{final_location}\"的天气信息,请确认地点名称是否正确,或者尝试查询其他城市的天气。"
+ return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
- soup = fetch_weather_page(city_info['fxLink'])
- if not soup:
- return ActionResponse(Action.REQLLM, None, "请求失败")
+ logger.bind(tag=TAG).info(f"获取到城市信息: {json.dumps(city_info, ensure_ascii=False)}")
+ logger.bind(tag=TAG).info(f"天气查询链接: {city_info['fxLink']}")
+
+ soup = fetch_weather_page(city_info['fxLink'])
+ if not soup:
+ error_msg = "请求天气页面失败"
+ logger.bind(tag=TAG).error(error_msg)
+ direct_response = f"抱歉,在获取{final_location}的天气信息时遇到了问题,请稍后再试。"
+ return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
- city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
- weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
- for i, (date, weather, high, low) in enumerate(temps_list):
- if high and low:
- weather_report += f"{date}: {low}到{high}, {weather}\n"
- weather_report += (
- f"当前天气: {current_abstract}\n"
- f"当前天气参数: {current_basic}\n"
- f"(确保只报告指定单日的天气情况,除非未来会出现异常天气;或者用户明确要求想要了解多日天气,如果未指定,默认报告今天的天气。"
- "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
- )
-
- return ActionResponse(Action.REQLLM, weather_report, None)
+ city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
+
+ # 构建天气报告(给LLM使用的详细数据)
+ weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
+ for i, (date, high, low) in enumerate(temps_list):
+ if high and low:
+ weather_report += f"{date}: {low}到{high}\n"
+ weather_report += (
+ f"当前天气: {current_abstract}\n"
+ f"当前天气参数: {current_basic}\n"
+ f"(确保只报告指定单日的气温范围,除非用户明确要求想要了解多日天气,如果未指定,默认报告今天的温度范围。"
+ "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
+ )
+
+ # 同时构建一个直接可用的人性化天气回复
+ # 这用作备用,防止LLM处理失败时能够直接回复用户
+ today_temp = ""
+ for date, high, low in temps_list:
+ if "今天" in date or "今日" in date:
+ today_temp = f"{low}到{high}"
+ break
+ if not today_temp and temps_list:
+ today_temp = f"{temps_list[0][2]}到{temps_list[0][1]}"
+
+ feel_temp = current_basic.get("体感温度", "")
+ humidity = current_basic.get("相对湿度", "")
+
+ direct_response = f"{city_name}今天{current_abstract},温度{today_temp}。"
+ if feel_temp:
+ direct_response += f"体感温度{feel_temp}。"
+ if humidity:
+ direct_response += f"相对湿度{humidity}。"
+
+ # 添加简单的建议
+ if "雨" in current_abstract:
+ direct_response += "外出请记得带伞。"
+ elif temps_list and temps_list[0][1] and int(temps_list[0][1].replace("°", "")) > 30:
+ direct_response += "天气炎热,注意防暑。"
+ elif temps_list and temps_list[0][2] and int(temps_list[0][2].replace("°", "")) < 10:
+ direct_response += "天气较冷,注意保暖。"
+
+ logger.bind(tag=TAG).info(f"构建的天气报告: {weather_report}")
+ logger.bind(tag=TAG).info(f"备用直接回复: {direct_response}")
+ logger.bind(tag=TAG).info(f"===== 天气查询函数执行完毕,返回结果 =====")
+
+ # 返回详细天气报告,但添加direct_response作为备用
+ return ActionResponse(Action.REQLLM, weather_report, direct_response)
+ except Exception as e:
+ error_msg = f"查询天气时发生错误: {str(e)}"
+ logger.bind(tag=TAG).error(error_msg)
+ # 出错时也提供一个直接回复
+ direct_response = f"抱歉,在获取天气信息时遇到了技术问题,请稍后再试。"
+ return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
From 05356b6652fe8497282fa5a2d105d58ef9c90254 Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 28 Mar 2025 14:13:54 +0800
Subject: [PATCH 10/11] =?UTF-8?q?update:=E7=AE=80=E5=8C=96intent=5Fllm?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/config.yaml | 42 ++--
main/xiaozhi-server/config/logger.py | 2 +-
main/xiaozhi-server/core/connection.py | 4 +-
.../core/handle/intentHandler.py | 229 ++----------------
.../core/providers/intent/base.py | 10 +-
.../intent/function_call/function_call.py | 20 ++
.../providers/intent/intent_llm/intent_llm.py | 95 ++------
7 files changed, 90 insertions(+), 312 deletions(-)
create mode 100644 main/xiaozhi-server/core/providers/intent/function_call/function_call.py
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index 71e7d66e..e2c35e67 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -82,14 +82,12 @@ selected_module:
TTS: EdgeTTS
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
Memory: nomem
- # 意图识别模块,默认使用function_call。开启后,可以播放音乐、控制音量、识别退出指令
- # 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
- # 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
+ # 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。
+ # 不想开通意图识别,就设置成:nointent
+ # 意图识别可使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
+ # 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
- Intent: intent_llm
-
-# 意图识别专用LLM配置,如果设置,则意图识别会使用这个模型而不是主LLM
-IntentLLM: XinferenceSmallLLM #ChatGLMLLM #XinferenceSmallLLM # 设置为空字符串""或不设置则使用主LLM
+ Intent: function_call
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
Intent:
@@ -100,9 +98,13 @@ Intent:
intent_llm:
# 不需要动type
type: intent_llm
+ # 配备意图识别独立的思考模型
+ # 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
+ # 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
+ llm: ChatGLMLLM
function_call:
# 不需要动type
- type: nointent
+ type: function_call
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
# 下面是加载查天气、角色切换、加载查新闻的插件示例
@@ -184,18 +186,6 @@ VAD:
LLM:
# 所有openai类型均可以修改超参,以AliLLM为例
# 当前支持的type为openai、dify、ollama,可自行适配
- XinferenceLLM:
- # 定义LLM API类型
- type: xinference
- # Xinference服务地址和模型名称
- model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型
- base_url: http://localhost:9997 # Xinference服务地址
- XinferenceSmallLLM:
- # 定义轻量级LLM API类型,用于意图识别
- type: xinference
- # Xinference服务地址和模型名称
- model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别
- base_url: http://localhost:9997 # Xinference服务地址
AliLLM:
# 定义LLM API类型
type: openai
@@ -293,6 +283,18 @@ LLM:
variables:
k: "v"
k2: "v2"
+ XinferenceLLM:
+ # 定义LLM API类型
+ type: xinference
+ # Xinference服务地址和模型名称
+ model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型
+ base_url: http://localhost:9997 # Xinference服务地址
+ XinferenceSmallLLM:
+ # 定义轻量级LLM API类型,用于意图识别
+ type: xinference
+ # Xinference服务地址和模型名称
+ model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别
+ base_url: http://localhost:9997 # Xinference服务地址
TTS:
# 当前支持的type为edge、doubao,可自行适配
EdgeTTS:
diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py
index 2206e52c..89d991e3 100644
--- a/main/xiaozhi-server/config/logger.py
+++ b/main/xiaozhi-server/config/logger.py
@@ -13,7 +13,7 @@ def setup_logging():
log_format_file = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}")
selected_module = config.get("selected_module")
- selected_module_str = ''.join([key[0] + value[0] for key, value in selected_module.items()])
+ selected_module_str = ''.join([value[0] + value[1] for key, value in selected_module.items()])
log_format = log_format.replace("{version}", SERVER_VERSION)
log_format = log_format.replace("{selected_module}", selected_module_str)
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index 07c9c779..10ffc8f6 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -197,12 +197,12 @@ class ConnectionHandler:
"""为意图识别设置LLM,优先使用专用LLM"""
# 检查是否配置了专用的意图识别LLM
- intent_llm_name = self.config.get("IntentLLM", "")
+ intent_llm_name = self.config["Intent"]["intent_llm"]["llm"]
# 记录开始初始化意图识别LLM的时间
intent_llm_init_start = time.time()
- if intent_llm_name and intent_llm_name in self.config["LLM"]:
+ if not self.use_function_call_mode and 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]
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index 900a25cf..c63ca329 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -4,8 +4,7 @@ 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 core.utils.dialogue import Message
from loguru import logger
TAG = __name__
@@ -65,242 +64,46 @@ async def process_intent_result(conn, intent_result, original_text):
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"]
-
+ if function_name == "continue_chat":
+ return False
+ function_args = None
+ if "arguments" in intent_data["function_call"]:
+ 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}")
- # 获取歌曲名称
- 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():
- # 直接调用函数
+ def process_function_call():
+ conn.dialogue.put(Message(role="user", content=original_text))
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)
+ conn.executor.submit(process_function_call)
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:
+ return False
+ except json.JSONDecodeError as e:
logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
-
- # 默认返回False,表示继续常规聊天流程
- return False
+ return False
def extract_text_in_brackets(s):
diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py
index 877dfced..a17cde81 100644
--- a/main/xiaozhi-server/core/providers/intent/base.py
+++ b/main/xiaozhi-server/core/providers/intent/base.py
@@ -10,11 +10,13 @@ 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": "播放音乐",
- "get_weather": "查询天气",
- "get_news": "查询新闻"
})
def set_llm(self, llm):
diff --git a/main/xiaozhi-server/core/providers/intent/function_call/function_call.py b/main/xiaozhi-server/core/providers/intent/function_call/function_call.py
new file mode 100644
index 00000000..ff33e58f
--- /dev/null
+++ b/main/xiaozhi-server/core/providers/intent/function_call/function_call.py
@@ -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"]
diff --git a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
index 5d0b0830..1afd835e 100644
--- a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
+++ b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
@@ -20,19 +20,6 @@ class IntentProvider(IntentProviderBase):
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:
"""
@@ -42,60 +29,50 @@ class IntentProvider(IntentProviderBase):
"""
intent_list = []
- """
- "continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等",
- "end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
- "play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
- "get_weather": "4.查询天气, 用户希望查询某个地点的天气情况"
- "get_news": "5.查询新闻, 用户希望查询最新新闻或特定类型的新闻"
- """
- 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.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等")
- elif key == "get_weather":
- intent_list.append("4.查询天气, 用户希望查询某个地点的天气情况")
- elif key == "get_news":
- intent_list.append("5.查询新闻, 用户希望查询最新新闻或特定类型的新闻")
- else:
- intent_list.append(value)
-
prompt = (
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
""
f"{', '.join(intent_list)}"
"\n"
"处理步骤:"
- "1. 思考意图类型"
- "2. 继续聊天和结束聊天意图: 返回intent格式"
- "3. 播放音乐意图: 分析歌名,生成function_call格式"
- "4. 查询天气意图: 分析地点,生成function_call格式"
- "5. 查询新闻意图: 分析新闻类别,生成function_call格式"
+ "1. 思考意图类型,生成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"
+ "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"
- "返回: {\"intent\": \"继续聊天\"}\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"
- "返回: {\"intent\": \"结束聊天\"}\n"
+ "返回: {\"function_call\": {\"name\": \"handle_exit_intent\"}}\n"
"```\n"
"```\n"
"用户: 播放中秋月\n"
@@ -140,25 +117,6 @@ class IntentProvider(IntentProviderBase):
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")
@@ -170,13 +128,6 @@ class IntentProvider(IntentProviderBase):
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()
From 17126303fadc8be371ff289f53756b83c2da319c Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 28 Mar 2025 17:17:14 +0800
Subject: [PATCH 11/11] =?UTF-8?q?fix:=E5=B7=B2=E7=BB=8F=E4=BD=BF=E7=94=A8?=
=?UTF-8?q?=E4=BA=86intent=5Fllm=EF=BC=8C=E4=B8=8D=E5=BA=94=E8=AF=A5?=
=?UTF-8?q?=E5=86=8D=E5=9B=9E=E5=88=B0functioncall=E7=9A=84chat=E4=B8=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/core/connection.py | 190 +++--------------
.../core/handle/intentHandler.py | 15 +-
.../core/providers/intent/base.py | 7 +-
.../plugins_func/functions/get_news.py | 84 ++++----
.../plugins_func/functions/get_weather.py | 198 ++++++------------
5 files changed, 136 insertions(+), 358 deletions(-)
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index 10ffc8f6..2b6bda25 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -210,11 +210,6 @@ class ConnectionHandler:
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
@@ -337,7 +332,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, is_weather_query=False, is_news_query=False):
+ def chat_with_function_calling(self, query, tool_call=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():
@@ -355,7 +350,7 @@ class ConnectionHandler:
functions = self.func_handler.get_functions()
response_message = []
- processed_chars = 0
+ processed_chars = 0 # 跟踪已处理的字符位置
try:
start_time = time.time()
@@ -364,58 +359,14 @@ class ConnectionHandler:
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result()
- # 为天气查询添加特殊处理
- 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
- )
+ # 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
+ )
except Exception as e:
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
return None
@@ -431,6 +382,9 @@ class ConnectionHandler:
content_arguments = ""
for response in llm_responses:
content, tools_call = response
+ if "content" in response:
+ content = response["content"]
+ tools_call = None
if content is not None and len(content) > 0:
if len(response_message) <= 0 and (content == "```" or "" in content):
tool_call_flag = True
@@ -533,126 +487,36 @@ 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:
- 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}]))
-
- # 记录工具返回结果到对话历史
- 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))
-
- 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))
+ text = result.result
+ 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,
+ "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)
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)
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index c63ca329..5105a4d9 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -91,11 +91,18 @@ async def process_intent_result(conn, intent_result, original_text):
def process_function_call():
conn.dialogue.put(Message(role="user", content=original_text))
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
- if result:
+ if result and function_name != 'play_music':
# 获取当前最新的文本索引
- 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)
+ text = result.response
+ if text is None:
+ text = result.result
+ if text is not None:
+ text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
+ conn.recode_first_last_text(text, text_index)
+ future = conn.executor.submit(conn.speak_and_play, text, text_index)
+ conn.llm_finish_task = True
+ conn.tts_queue.put(future)
+ conn.dialogue.put(Message(role="assistant", content=text))
# 将函数执行放在线程池中
conn.executor.submit(process_function_call)
diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py
index a17cde81..2a39f1ae 100644
--- a/main/xiaozhi-server/core/providers/intent/base.py
+++ b/main/xiaozhi-server/core/providers/intent/base.py
@@ -23,13 +23,8 @@ class IntentProviderBase(ABC):
self.llm = llm
# 获取模型名称和类型信息
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}")
+ logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}")
@abstractmethod
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
diff --git a/main/xiaozhi-server/plugins_func/functions/get_news.py b/main/xiaozhi-server/plugins_func/functions/get_news.py
index f7a6d342..85b203dc 100644
--- a/main/xiaozhi-server/plugins_func/functions/get_news.py
+++ b/main/xiaozhi-server/plugins_func/functions/get_news.py
@@ -45,10 +45,10 @@ def fetch_news_from_rss(rss_url):
try:
response = requests.get(rss_url)
response.raise_for_status()
-
+
# 解析XML
root = ET.fromstring(response.content)
-
+
# 查找所有item元素(新闻条目)
news_items = []
for item in root.findall('.//item'):
@@ -56,14 +56,14 @@ def fetch_news_from_rss(rss_url):
link = item.find('link').text if item.find('link') is not None else "#"
description = item.find('description').text if item.find('description') is not None else "无描述"
pubDate = item.find('pubDate').text if item.find('pubDate') is not None else "未知时间"
-
+
news_items.append({
'title': title,
'link': link,
'description': description,
'pubDate': pubDate
})
-
+
return news_items
except Exception as e:
logger.bind(tag=TAG).error(f"获取RSS新闻失败: {e}")
@@ -75,9 +75,9 @@ def fetch_news_detail(url):
try:
response = requests.get(url)
response.raise_for_status()
-
+
soup = BeautifulSoup(response.content, 'html.parser')
-
+
# 尝试提取正文内容 (这里的选择器需要根据实际网站结构调整)
content_div = soup.select_one('.content_desc, .content, article, .article-content')
if content_div:
@@ -98,7 +98,7 @@ def map_category(category_text):
"""将用户输入的中文类别映射到配置文件中的类别键"""
if not category_text:
return None
-
+
# 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件
category_map = {
# 社会新闻
@@ -113,10 +113,10 @@ def map_category(category_text):
"金融": "finance",
"经济": "finance"
}
-
+
# 转换为小写并去除空格
normalized_category = category_text.lower().strip()
-
+
# 返回映射结果,如果没有匹配项则返回原始输入
return category_map.get(normalized_category, category_text)
@@ -128,25 +128,23 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
# 如果detail为True,获取上一条新闻的详细内容
if detail:
if not hasattr(conn, 'last_news_link') or not conn.last_news_link or 'link' not in conn.last_news_link:
- direct_response = "抱歉,没有找到最近查询的新闻,请先获取一条新闻。"
- return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", direct_response)
-
+ return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
+
link = conn.last_news_link.get('link')
title = conn.last_news_link.get('title', '未知标题')
-
+
if link == '#':
- direct_response = "抱歉,该新闻没有可用的链接获取详细内容。"
- return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", direct_response)
-
+ return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
+
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
-
+
# 获取新闻详情
detail_content = fetch_news_detail(link)
-
+
if not detail_content or detail_content == "无法获取详细内容":
- direct_response = f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。"
- return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", direct_response)
-
+ return ActionResponse(Action.REQLLM,
+ f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
+
# 构建详情报告
detail_report = (
f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n"
@@ -155,37 +153,33 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
)
-
- # 构造一个直接回复,简要概括新闻内容
- direct_response = f"以下是《{title}》的详细内容:{detail_content[:200]}...(内容过长已省略)"
-
- return ActionResponse(Action.REQLLM, detail_report, direct_response)
-
+
+ return ActionResponse(Action.REQLLM, detail_report, None)
+
# 否则,获取新闻列表并随机选择一条
# 从配置中获取RSS URL
rss_config = conn.config["plugins"]["get_news"]
default_rss_url = rss_config.get("default_rss_url", "https://www.chinanews.com.cn/rss/society.xml")
-
+
# 将用户输入的类别映射到配置中的类别键
mapped_category = map_category(category)
-
+
# 如果提供了类别,尝试从配置中获取对应的URL
rss_url = default_rss_url
if mapped_category and mapped_category in rss_config.get("category_urls", {}):
rss_url = rss_config["category_urls"][mapped_category]
-
+
logger.bind(tag=TAG).info(f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}")
-
+
# 获取新闻列表
news_items = fetch_news_from_rss(rss_url)
-
+
if not news_items:
- direct_response = "抱歉,未能获取到新闻信息,请稍后再试。"
- return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", direct_response)
-
+ return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
+
# 随机选择一条新闻
selected_news = random.choice(news_items)
-
+
# 保存当前新闻链接到连接对象,以便后续查询详情
if not hasattr(conn, 'last_news_link'):
conn.last_news_link = {}
@@ -193,7 +187,7 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
'link': selected_news.get('link', '#'),
'title': selected_news.get('title', '未知标题')
}
-
+
# 构建新闻报告
news_report = (
f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n"
@@ -204,17 +198,9 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
f"直接读出新闻即可,不需要额外多余的内容。"
f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)"
)
-
- # 构造一个直接回复版本,简要播报新闻
- direct_response = (
- f"最新{mapped_category or ''}新闻:{selected_news['title']}。"
- f"{selected_news['description'][:150]}..."
- f"如果您想了解更多详情,可以说'请详细介绍这条新闻'。"
- )
-
- return ActionResponse(Action.REQLLM, news_report, direct_response)
-
+
+ return ActionResponse(Action.REQLLM, news_report, None)
+
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
- direct_response = "抱歉,获取新闻时发生错误,请稍后再试。"
- return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", direct_response)
+ return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
\ No newline at end of file
diff --git a/main/xiaozhi-server/plugins_func/functions/get_weather.py b/main/xiaozhi-server/plugins_func/functions/get_weather.py
index 58f63844..060274d9 100644
--- a/main/xiaozhi-server/plugins_func/functions/get_weather.py
+++ b/main/xiaozhi-server/plugins_func/functions/get_weather.py
@@ -2,7 +2,6 @@ import requests
from bs4 import BeautifulSoup
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
-import json
TAG = __name__
logger = setup_logging()
@@ -40,158 +39,85 @@ HEADERS = {
)
}
+# 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons
+WEATHER_CODE_MAP = {
+ "100": "晴", "101": "多云", "102": "少云", "103": "晴间多云", "104": "阴",
+ "150": "晴", "151": "多云", "152": "少云", "153": "晴间多云",
+ "300": "阵雨", "301": "强阵雨", "302": "雷阵雨", "303": "强雷阵雨", "304": "雷阵雨伴有冰雹",
+ "305": "小雨", "306": "中雨", "307": "大雨", "308": "极端降雨", "309": "毛毛雨/细雨",
+ "310": "暴雨", "311": "大暴雨", "312": "特大暴雨", "313": "冻雨", "314": "小到中雨",
+ "315": "中到大雨", "316": "大到暴雨", "317": "暴雨到大暴雨", "318": "大暴雨到特大暴雨",
+ "350": "阵雨", "351": "强阵雨", "399": "雨",
+ "400": "小雪", "401": "中雪", "402": "大雪", "403": "暴雪", "404": "雨夹雪",
+ "405": "雨雪天气", "406": "阵雨夹雪", "407": "阵雪", "408": "小到中雪", "409": "中到大雪", "410": "大到暴雪",
+ "456": "阵雨夹雪", "457": "阵雪", "499": "雪",
+ "500": "薄雾", "501": "雾", "502": "霾", "503": "扬沙", "504": "浮尘",
+ "507": "沙尘暴", "508": "强沙尘暴",
+ "509": "浓雾", "510": "强浓雾", "511": "中度霾", "512": "重度霾", "513": "严重霾", "514": "大雾", "515": "特强浓雾",
+ "900": "热", "901": "冷", "999": "未知"
+}
def fetch_city_info(location, api_key):
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
- logger.bind(tag=TAG).info(f"正在请求城市信息API,URL: {url}")
- try:
- response = requests.get(url, headers=HEADERS)
- logger.bind(tag=TAG).info(f"城市信息API响应状态码: {response.status_code}")
- json_data = response.json()
- logger.bind(tag=TAG).debug(f"城市信息API响应: {json.dumps(json_data, ensure_ascii=False)}")
- return json_data.get('location', [])[0] if json_data.get('location') else None
- except Exception as e:
- logger.bind(tag=TAG).error(f"获取城市信息失败: {str(e)}")
- return None
+ response = requests.get(url, headers=HEADERS).json()
+ return response.get('location', [])[0] if response.get('location') else None
def fetch_weather_page(url):
- logger.bind(tag=TAG).info(f"正在请求天气页面,URL: {url}")
- try:
- response = requests.get(url, headers=HEADERS)
- logger.bind(tag=TAG).info(f"天气页面响应状态码: {response.status_code}")
- if not response.ok:
- logger.bind(tag=TAG).error(f"获取天气页面失败: {response.text[:200]}")
- return BeautifulSoup(response.text, "html.parser") if response.ok else None
- except Exception as e:
- logger.bind(tag=TAG).error(f"获取天气页面失败: {str(e)}")
- return None
+ response = requests.get(url, headers=HEADERS)
+ return BeautifulSoup(response.text, "html.parser") if response.ok else None
def parse_weather_info(soup):
- try:
- city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True)
- logger.bind(tag=TAG).info(f"解析到城市名称: {city_name}")
+ city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True)
- current_abstract = soup.select_one(".c-city-weather-current .current-abstract")
- current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知"
- logger.bind(tag=TAG).info(f"当前天气概况: {current_abstract}")
+ current_abstract = soup.select_one(".c-city-weather-current .current-abstract")
+ current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知"
- current_basic = {}
- for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"):
- parts = item.get_text(strip=True, separator=" ").split(" ")
- if len(parts) == 2:
- key, value = parts[1], parts[0]
- current_basic[key] = value
- logger.bind(tag=TAG).info(f"当前天气详情: {json.dumps(current_basic, ensure_ascii=False)}")
+ current_basic = {}
+ for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"):
+ parts = item.get_text(strip=True, separator=" ").split(" ")
+ if len(parts) == 2:
+ key, value = parts[1], parts[0]
+ current_basic[key] = value
- temps_list = []
- for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
- date = row.select_one(".date-bg .date").get_text(strip=True)
- temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
- high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
- temps_list.append((date, high_temp, low_temp))
- logger.bind(tag=TAG).info(f"获取到未来7天温度: {temps_list}")
+ temps_list = []
+ for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
+ date = row.select_one(".date-bg .date").get_text(strip=True)
+ weather_code = row.select_one(".date-bg .icon")["src"].split("/")[-1].split(".")[0]
+ weather = WEATHER_CODE_MAP.get(weather_code, "未知")
+ temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
+ high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
+ temps_list.append((date, weather, high_temp, low_temp))
- return city_name, current_abstract, current_basic, temps_list
- except Exception as e:
- logger.bind(tag=TAG).error(f"解析天气信息失败: {str(e)}")
- return "未知城市", "未知天气", {}, []
+ return city_name, current_abstract, current_basic, temps_list
@register_function('get_weather', GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
- logger.bind(tag=TAG).info(f"===== 天气查询函数开始执行 =====")
- logger.bind(tag=TAG).info(f"查询参数: location={location}, lang={lang}")
-
- try:
- api_key = conn.config["plugins"]["get_weather"]["api_key"]
- default_location = conn.config["plugins"]["get_weather"]["default_location"]
-
- # 位置参数处理逻辑优化
- # 1. 如果传入了明确的位置参数且不为None/空字符串,则使用该参数
- # 2. 否则,尝试使用客户端IP地址对应的城市信息
- # 3. 如果上述都无效,则使用配置文件中的默认位置
- if location and location.strip():
- final_location = location.strip()
- logger.bind(tag=TAG).info(f"使用用户明确指定的位置: {final_location}")
- elif conn.client_ip_info and "city" in conn.client_ip_info and conn.client_ip_info["city"]:
- final_location = conn.client_ip_info["city"]
- logger.bind(tag=TAG).info(f"使用IP地址解析的位置: {final_location}")
- else:
- final_location = default_location
- logger.bind(tag=TAG).info(f"使用配置文件中的默认位置: {final_location}")
-
- logger.bind(tag=TAG).info(f"最终使用的查询地点: {final_location}, API Key: {api_key}")
+ api_key = conn.config["plugins"]["get_weather"]["api_key"]
+ default_location = conn.config["plugins"]["get_weather"]["default_location"]
+ location = location or conn.client_ip_info.get("city") or default_location
+ logger.bind(tag=TAG).debug(f"获取天气: {location}")
- city_info = fetch_city_info(final_location, api_key)
- if not city_info:
- error_msg = f"未找到相关的城市: {final_location},请确认地点是否正确"
- logger.bind(tag=TAG).error(error_msg)
- # 构造一个直接回复
- direct_response = f"抱歉,我未能找到\"{final_location}\"的天气信息,请确认地点名称是否正确,或者尝试查询其他城市的天气。"
- return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
+ city_info = fetch_city_info(location, api_key)
+ if not city_info:
+ return ActionResponse(Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None)
- logger.bind(tag=TAG).info(f"获取到城市信息: {json.dumps(city_info, ensure_ascii=False)}")
- logger.bind(tag=TAG).info(f"天气查询链接: {city_info['fxLink']}")
-
- soup = fetch_weather_page(city_info['fxLink'])
- if not soup:
- error_msg = "请求天气页面失败"
- logger.bind(tag=TAG).error(error_msg)
- direct_response = f"抱歉,在获取{final_location}的天气信息时遇到了问题,请稍后再试。"
- return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
+ soup = fetch_weather_page(city_info['fxLink'])
+ if not soup:
+ return ActionResponse(Action.REQLLM, None, "请求失败")
- city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
-
- # 构建天气报告(给LLM使用的详细数据)
- weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
- for i, (date, high, low) in enumerate(temps_list):
- if high and low:
- weather_report += f"{date}: {low}到{high}\n"
- weather_report += (
- f"当前天气: {current_abstract}\n"
- f"当前天气参数: {current_basic}\n"
- f"(确保只报告指定单日的气温范围,除非用户明确要求想要了解多日天气,如果未指定,默认报告今天的温度范围。"
- "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
- )
-
- # 同时构建一个直接可用的人性化天气回复
- # 这用作备用,防止LLM处理失败时能够直接回复用户
- today_temp = ""
- for date, high, low in temps_list:
- if "今天" in date or "今日" in date:
- today_temp = f"{low}到{high}"
- break
- if not today_temp and temps_list:
- today_temp = f"{temps_list[0][2]}到{temps_list[0][1]}"
-
- feel_temp = current_basic.get("体感温度", "")
- humidity = current_basic.get("相对湿度", "")
-
- direct_response = f"{city_name}今天{current_abstract},温度{today_temp}。"
- if feel_temp:
- direct_response += f"体感温度{feel_temp}。"
- if humidity:
- direct_response += f"相对湿度{humidity}。"
-
- # 添加简单的建议
- if "雨" in current_abstract:
- direct_response += "外出请记得带伞。"
- elif temps_list and temps_list[0][1] and int(temps_list[0][1].replace("°", "")) > 30:
- direct_response += "天气炎热,注意防暑。"
- elif temps_list and temps_list[0][2] and int(temps_list[0][2].replace("°", "")) < 10:
- direct_response += "天气较冷,注意保暖。"
-
- logger.bind(tag=TAG).info(f"构建的天气报告: {weather_report}")
- logger.bind(tag=TAG).info(f"备用直接回复: {direct_response}")
- logger.bind(tag=TAG).info(f"===== 天气查询函数执行完毕,返回结果 =====")
-
- # 返回详细天气报告,但添加direct_response作为备用
- return ActionResponse(Action.REQLLM, weather_report, direct_response)
- except Exception as e:
- error_msg = f"查询天气时发生错误: {str(e)}"
- logger.bind(tag=TAG).error(error_msg)
- # 出错时也提供一个直接回复
- direct_response = f"抱歉,在获取天气信息时遇到了技术问题,请稍后再试。"
- return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
+ city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
+ weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
+ for i, (date, weather, high, low) in enumerate(temps_list):
+ if high and low:
+ weather_report += f"{date}: {low}到{high}, {weather}\n"
+ weather_report += (
+ f"当前天气: {current_abstract}\n"
+ f"当前天气参数: {current_basic}\n"
+ f"(确保只报告指定单日的天气情况,除非未来会出现异常天气;或者用户明确要求想要了解多日天气,如果未指定,默认报告今天的天气。"
+ "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
+ )
+
+ return ActionResponse(Action.REQLLM, weather_report, None)
\ No newline at end of file