From 1c823c4255cc9dd04d752a27bfd3bd67a8c035d2 Mon Sep 17 00:00:00 2001
From: JavaZeroo <2487163254@qq.com>
Date: Sat, 26 Apr 2025 00:43:40 +0800
Subject: [PATCH 01/11] feat: add news retrieval functionality and register new
function
---
Dockerfile-server | 8 +
.../core/handle/functionHandler.py | 1 +
.../core/handle/intentHandler.py | 75 ++++++--
.../providers/intent/intent_llm/intent_llm.py | 9 +
.../get_news_from_different_source.py | 179 ++++++++++++++++++
5 files changed, 255 insertions(+), 17 deletions(-)
create mode 100644 main/xiaozhi-server/plugins_func/functions/get_news_from_different_source.py
diff --git a/Dockerfile-server b/Dockerfile-server
index 7eb77114..86f4d666 100644
--- a/Dockerfile-server
+++ b/Dockerfile-server
@@ -3,11 +3,18 @@ FROM python:3.10-slim AS builder
WORKDIR /app
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends git && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
COPY main/xiaozhi-server/requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt
+RUN git clone https://github.com/microsoft/markitdown.git && cd markitdown && pip install -e 'packages/markitdown[all]'
+
# 第二阶段:生产镜像
FROM python:3.10-slim
@@ -21,6 +28,7 @@ RUN apt-get update && \
# 从构建阶段复制Python包和前端构建产物
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
+COPY --from=builder /app/markitdown/ /app/markitdown/
# 复制应用代码
COPY main/xiaozhi-server .
diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py
index b09a369c..10c2bf1a 100644
--- a/main/xiaozhi-server/core/handle/functionHandler.py
+++ b/main/xiaozhi-server/core/handle/functionHandler.py
@@ -55,6 +55,7 @@ class FunctionHandler:
self.function_registry.register_function("get_time")
self.function_registry.register_function("get_lunar")
self.function_registry.register_function("handle_device")
+ self.function_registry.register_function("get_news_from_different_source")
def register_config_functions(self):
"""注册配置中的函数,可以不同客户端使用不同的配置"""
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index 0174bc43..5b00f2ef 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -5,6 +5,8 @@ from core.handle.sendAudioHandle import send_stt_message
from core.handle.helloHandle import checkWakeupWords
from core.utils.util import remove_punctuation_and_length
from core.utils.dialogue import Message
+from plugins_func.register import Action
+from loguru import logger
TAG = __name__
@@ -100,24 +102,63 @@ async def process_intent_result(conn, intent_result, original_text):
result = conn.func_handler.handle_llm_function_call(
conn, function_call_data
)
- if result and function_name != "play_music":
- # 获取当前最新的文本索引
- text = result.response
- if text is None:
+ logger.bind(tag=TAG).debug(f"检测到Action : {result.action}")
+
+ if result:
+ if result.action == Action.RESPONSE: # 直接回复前端
+ text = result.response
+ 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))
+ elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
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, text_index))
- conn.dialogue.put(Message(role="assistant", content=text))
+ if text is not None and len(text) > 0:
+ conn.dialogue.put(Message(role="tool", content=text))
+ conn.executor.submit(conn.chat_with_function_calling, text, True)
+ elif result.action == Action.NOTFOUND or result.action == Action.ERROR:
+ 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, text_index))
+ conn.dialogue.put(Message(role="assistant", content=text))
+ elif function_name != "play_music":
+ # For backward compatibility with original code
+ # 获取当前最新的文本索引
+ 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/intent_llm/intent_llm.py b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
index 05d01c10..9915be33 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
@@ -41,6 +41,7 @@ class IntentProvider(IntentProviderBase):
'2. 结束对话意图: {"function_call": {"name": "handle_exit_intent", "arguments": {"say_goodbye": "goodbye"}}}\n'
'3. 获取当天日期时间: {"function_call": {"name": "get_time"}}\n'
'4. 继续聊天意图: {"function_call": {"name": "continue_chat"}}\n'
+ '5. 查询新闻意图: {"function_call": {"name": "get_news_from_different_source", "arguments":{"source": {"type": "string","description": "新闻源,可选项有{"thepaper": "澎湃新闻","wallstreetcn-quick": "华尔街见闻","ithome": "IT之家","zhihu": "知乎"}。可选参数,如果不提供则使用默认新闻源"},"detail": {"type": "boolean","description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"}}}\n'
"\n"
"注意:\n"
'- 播放音乐:无歌名时,song_name设为"random"\n'
@@ -53,6 +54,14 @@ class IntentProvider(IntentProviderBase):
'返回: {"function_call": {"name": "continue_chat"}}\n'
"```\n"
"```\n"
+ "用户: 最近有什么新闻吗\n"
+ '返回: {"function_call": {"name": "get_news_from_different_source", "arguments": {"source":"thepaper", "detail":"False"}}}\n'
+ "```\n"
+ "```\n"
+ "用户: 详细说说这个新闻\n"
+ '返回: {"function_call": {"name": "get_news_from_different_source", "arguments": {"source":"thepaper", "detail":"True"}}}\n'
+ "```\n"
+ "```\n"
"用户: 现在是几号了?现在几点了?\n"
'返回: {"function_call": {"name": "get_time"}}\n'
"```\n"
diff --git a/main/xiaozhi-server/plugins_func/functions/get_news_from_different_source.py b/main/xiaozhi-server/plugins_func/functions/get_news_from_different_source.py
new file mode 100644
index 00000000..c6440f8f
--- /dev/null
+++ b/main/xiaozhi-server/plugins_func/functions/get_news_from_different_source.py
@@ -0,0 +1,179 @@
+import random
+import requests
+import json
+from config.logger import setup_logging
+from plugins_func.register import register_function, ToolType, ActionResponse, Action
+from markitdown import MarkItDown
+
+TAG = __name__
+logger = setup_logging()
+
+# 新闻来源字典,包含名称和对应的API ID
+NEWS_SOURCES = {
+ "thepaper": "澎湃新闻",
+ "cls-depth": "财联社",
+}
+
+# 动态生成新闻源描述
+def generate_news_sources_description():
+ sources_desc = []
+ for source_id, source_name in NEWS_SOURCES.items():
+ sources_desc.append(f"{source_name}({source_id})")
+ return "、".join(sources_desc)
+
+GET_NEWS_FROM_DIFFERENT_SOURCE_FUNCTION_DESC = {
+ "type": "function",
+ "function": {
+ "name": "get_news_from_different_source",
+ "description": (
+ "获取最新新闻,随机选择一条新闻进行播报。"
+ f"用户可以选择不同的新闻源,如{generate_news_sources_description()}等。"
+ "如果没有指定,默认从澎湃新闻获取。"
+ "用户可以要求获取详细内容,此时会获取新闻的详细内容。"
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "type": "string",
+ "description": f"新闻源,例如{generate_news_sources_description()}等。可选参数,如果不提供则使用默认新闻源"
+ },
+ "detail": {
+ "type": "boolean",
+ "description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"
+ },
+ "lang": {
+ "type": "string",
+ "description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
+ }
+ },
+ "required": ["lang"]
+ }
+ }
+}
+
+
+def fetch_news_from_api(source="thepaper"):
+ """从API获取新闻列表"""
+ try:
+ api_url = f"https://newsnow.busiyi.world/api/s?id={source}"
+ response = requests.get(api_url, timeout=10)
+ response.raise_for_status()
+
+ data = response.json()
+
+ if "items" in data:
+ return data["items"]
+ else:
+ logger.bind(tag=TAG).error(f"获取新闻API响应格式错误: {data}")
+ return []
+
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"获取新闻API失败: {e}")
+ return []
+
+
+def fetch_news_detail(url):
+ """获取新闻详情页内容并使用MarkItDown清理HTML"""
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+
+ # 使用MarkItDown清理HTML内容
+ md = MarkItDown(enable_plugins=False)
+ result = md.convert(response)
+
+ # 获取清理后的文本内容
+ clean_text = result.text_content
+
+ # 如果清理后的内容为空,返回提示信息
+ if not clean_text or len(clean_text.strip()) == 0:
+ logger.bind(tag=TAG).warning(f"清理后的新闻内容为空: {url}")
+ return "无法解析新闻详情内容,可能是网站结构特殊或内容受限。"
+
+ return clean_text
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
+ return "无法获取详细内容"
+
+
+@register_function('get_news_from_different_source', GET_NEWS_FROM_DIFFERENT_SOURCE_FUNCTION_DESC, ToolType.SYSTEM_CTL)
+def get_news_from_different_source(conn, source: str = "thepaper", detail: bool = False, lang: str = "zh_CN"):
+ """获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
+ try:
+ # 如果detail为True,获取上一条新闻的详细内容
+ detail = str(detail).lower() == 'true'
+ if detail:
+ if not hasattr(conn, 'last_news_link_different') or not conn.last_news_link_different or 'url' not in conn.last_news_link_different:
+ return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
+
+ url = conn.last_news_link_different.get('url')
+ title = conn.last_news_link_different.get('title', '未知标题')
+ source_id = conn.last_news_link_different.get('source_id', 'thepaper')
+ source_name = NEWS_SOURCES.get(source_id, '未知来源')
+
+ if not url or url == '#':
+ return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
+
+ logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, 来源: {source_name}, URL={url}")
+
+ # 获取新闻详情
+ detail_content = fetch_news_detail(url)
+
+ if not detail_content or detail_content == "无法获取详细内容":
+ return ActionResponse(Action.REQLLM,
+ f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
+
+ # 构建详情报告
+ detail_report = (
+ f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n"
+ f"新闻标题: {title}\n"
+ # f"新闻来源: {source_name}\n"
+ f"详细内容: {detail_content}\n\n"
+ f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
+ f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
+ )
+
+ return ActionResponse(Action.REQLLM, detail_report, None)
+
+ # 否则,获取新闻列表并随机选择一条
+ # 验证新闻源是否有效,如果无效则使用默认源
+ if source not in NEWS_SOURCES:
+ logger.bind(tag=TAG).warning(f"无效的新闻源: {source},使用默认源thepaper")
+ source = "thepaper"
+
+ source_name = NEWS_SOURCES.get(source, "澎湃新闻")
+ logger.bind(tag=TAG).info(f"获取新闻: 新闻源={source}({source_name})")
+
+ # 获取新闻列表
+ news_items = fetch_news_from_api(source)
+
+ if not news_items:
+ return ActionResponse(Action.REQLLM, f"抱歉,未能从{source_name}获取到新闻信息,请稍后再试或尝试其他新闻源。", None)
+
+ # 随机选择一条新闻
+ selected_news = random.choice(news_items)
+
+ # 保存当前新闻链接到连接对象,以便后续查询详情
+ if not hasattr(conn, 'last_news_link_different'):
+ conn.last_news_link_different = {}
+ conn.last_news_link_different = {
+ 'url': selected_news.get('url', '#'),
+ 'title': selected_news.get('title', '未知标题'),
+ 'source_id': source
+ }
+
+ # 构建新闻报告
+ news_report = (
+ f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n"
+ f"新闻标题: {selected_news['title']}\n"
+ # f"新闻来源: {source_name}\n"
+ f"(请以自然、流畅的方式向用户播报这条新闻标题,"
+ f"提示用户可以要求获取详细内容,此时会获取新闻的详细内容。)"
+ )
+
+ return ActionResponse(Action.REQLLM, news_report, None)
+
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
+ return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
\ No newline at end of file
From 297c9e00857d5073559562065baeb91e6967a6a6 Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Thu, 8 May 2025 18:23:06 +0800
Subject: [PATCH 02/11] =?UTF-8?q?update:=E9=87=8D=E5=91=BD=E5=90=8D?=
=?UTF-8?q?=E6=96=B0=E9=97=BB=E6=8F=92=E4=BB=B6=EF=BC=8C=E4=BB=A5=E5=B9=B3?=
=?UTF-8?q?=E5=8F=B0=E6=9D=A5=E6=BA=90=E6=9D=A5=E5=91=BD=E5=90=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...get_news.py => get_news_from_chinanews.py} | 135 ++++++++++++------
...ent_source.py => get_news_from_newsnow.py} | 119 +++++++++------
2 files changed, 167 insertions(+), 87 deletions(-)
rename main/xiaozhi-server/plugins_func/functions/{get_news.py => get_news_from_chinanews.py} (62%)
rename main/xiaozhi-server/plugins_func/functions/{get_news_from_different_source.py => get_news_from_newsnow.py} (65%)
diff --git a/main/xiaozhi-server/plugins_func/functions/get_news.py b/main/xiaozhi-server/plugins_func/functions/get_news_from_chinanews.py
similarity index 62%
rename from main/xiaozhi-server/plugins_func/functions/get_news.py
rename to main/xiaozhi-server/plugins_func/functions/get_news_from_chinanews.py
index 85b203dc..64d499b1 100644
--- a/main/xiaozhi-server/plugins_func/functions/get_news.py
+++ b/main/xiaozhi-server/plugins_func/functions/get_news_from_chinanews.py
@@ -8,10 +8,10 @@ from plugins_func.register import register_function, ToolType, ActionResponse, A
TAG = __name__
logger = setup_logging()
-GET_NEWS_FUNCTION_DESC = {
+GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC = {
"type": "function",
"function": {
- "name": "get_news",
+ "name": "get_news_from_chinanews",
"description": (
"获取最新新闻,随机选择一条新闻进行播报。"
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
@@ -23,20 +23,20 @@ GET_NEWS_FUNCTION_DESC = {
"properties": {
"category": {
"type": "string",
- "description": "新闻类别,例如社会、科技、国际。可选参数,如果不提供则使用默认类别"
+ "description": "新闻类别,例如社会、科技、国际。可选参数,如果不提供则使用默认类别",
},
"detail": {
"type": "boolean",
- "description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"
+ "description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容",
},
"lang": {
"type": "string",
- "description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
- }
+ "description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN",
+ },
},
- "required": ["lang"]
- }
- }
+ "required": ["lang"],
+ },
+ },
}
@@ -51,18 +51,30 @@ def fetch_news_from_rss(rss_url):
# 查找所有item元素(新闻条目)
news_items = []
- for item in root.findall('.//item'):
- title = item.find('title').text if item.find('title') is not None else "无标题"
- 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 "未知时间"
+ for item in root.findall(".//item"):
+ title = (
+ item.find("title").text if item.find("title") is not None else "无标题"
+ )
+ 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
- })
+ news_items.append(
+ {
+ "title": title,
+ "link": link,
+ "description": description,
+ "pubDate": pubDate,
+ }
+ )
return news_items
except Exception as e:
@@ -76,18 +88,24 @@ def fetch_news_detail(url):
response = requests.get(url)
response.raise_for_status()
- soup = BeautifulSoup(response.content, 'html.parser')
+ soup = BeautifulSoup(response.content, "html.parser")
# 尝试提取正文内容 (这里的选择器需要根据实际网站结构调整)
- content_div = soup.select_one('.content_desc, .content, article, .article-content')
+ content_div = soup.select_one(
+ ".content_desc, .content, article, .article-content"
+ )
if content_div:
- paragraphs = content_div.find_all('p')
- content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
+ paragraphs = content_div.find_all("p")
+ content = "\n".join(
+ [p.get_text().strip() for p in paragraphs if p.get_text().strip()]
+ )
return content
else:
# 如果找不到特定的内容区域,尝试获取所有段落
- paragraphs = soup.find_all('p')
- content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
+ paragraphs = soup.find_all("p")
+ content = "\n".join(
+ [p.get_text().strip() for p in paragraphs if p.get_text().strip()]
+ )
return content[:2000] # 限制长度
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
@@ -111,7 +129,7 @@ def map_category(category_text):
"财经": "finance",
"财经新闻": "finance",
"金融": "finance",
- "经济": "finance"
+ "经济": "finance",
}
# 转换为小写并去除空格
@@ -121,20 +139,36 @@ def map_category(category_text):
return category_map.get(normalized_category, category_text)
-@register_function('get_news', GET_NEWS_FUNCTION_DESC, ToolType.SYSTEM_CTL)
-def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_CN"):
+@register_function(
+ "get_news_from_chinanews",
+ GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC,
+ ToolType.SYSTEM_CTL,
+)
+def get_news_from_chinanews(
+ conn, category: str = None, detail: bool = False, lang: str = "zh_CN"
+):
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
try:
# 如果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)
+ 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,
+ )
- link = conn.last_news_link.get('link')
- title = conn.last_news_link.get('title', '未知标题')
+ link = conn.last_news_link.get("link")
+ title = conn.last_news_link.get("title", "未知标题")
- if link == '#':
- return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
+ if link == "#":
+ return ActionResponse(
+ Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None
+ )
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
@@ -142,8 +176,11 @@ 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)
+ return ActionResponse(
+ Action.REQLLM,
+ f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。",
+ None,
+ )
# 构建详情报告
detail_report = (
@@ -158,8 +195,10 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
# 否则,获取新闻列表并随机选择一条
# 从配置中获取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")
+ rss_config = conn.config["plugins"]["get_news_from_chinanews"]
+ default_rss_url = rss_config.get(
+ "default_rss_url", "https://www.chinanews.com.cn/rss/society.xml"
+ )
# 将用户输入的类别映射到配置中的类别键
mapped_category = map_category(category)
@@ -169,23 +208,27 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
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}")
+ logger.bind(tag=TAG).info(
+ f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}"
+ )
# 获取新闻列表
news_items = fetch_news_from_rss(rss_url)
if not news_items:
- return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
+ return ActionResponse(
+ Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None
+ )
# 随机选择一条新闻
selected_news = random.choice(news_items)
# 保存当前新闻链接到连接对象,以便后续查询详情
- if not hasattr(conn, 'last_news_link'):
+ if not hasattr(conn, "last_news_link"):
conn.last_news_link = {}
conn.last_news_link = {
- 'link': selected_news.get('link', '#'),
- 'title': selected_news.get('title', '未知标题')
+ "link": selected_news.get("link", "#"),
+ "title": selected_news.get("title", "未知标题"),
}
# 构建新闻报告
@@ -203,4 +246,6 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
- return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
\ No newline at end of file
+ return ActionResponse(
+ Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None
+ )
diff --git a/main/xiaozhi-server/plugins_func/functions/get_news_from_different_source.py b/main/xiaozhi-server/plugins_func/functions/get_news_from_newsnow.py
similarity index 65%
rename from main/xiaozhi-server/plugins_func/functions/get_news_from_different_source.py
rename to main/xiaozhi-server/plugins_func/functions/get_news_from_newsnow.py
index c6440f8f..1b98539d 100644
--- a/main/xiaozhi-server/plugins_func/functions/get_news_from_different_source.py
+++ b/main/xiaozhi-server/plugins_func/functions/get_news_from_newsnow.py
@@ -11,9 +11,11 @@ logger = setup_logging()
# 新闻来源字典,包含名称和对应的API ID
NEWS_SOURCES = {
"thepaper": "澎湃新闻",
+ "baidu": "百度热搜",
"cls-depth": "财联社",
}
+
# 动态生成新闻源描述
def generate_news_sources_description():
sources_desc = []
@@ -21,10 +23,11 @@ def generate_news_sources_description():
sources_desc.append(f"{source_name}({source_id})")
return "、".join(sources_desc)
-GET_NEWS_FROM_DIFFERENT_SOURCE_FUNCTION_DESC = {
+
+GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC = {
"type": "function",
"function": {
- "name": "get_news_from_different_source",
+ "name": "get_news_from_newsnow",
"description": (
"获取最新新闻,随机选择一条新闻进行播报。"
f"用户可以选择不同的新闻源,如{generate_news_sources_description()}等。"
@@ -36,38 +39,43 @@ GET_NEWS_FROM_DIFFERENT_SOURCE_FUNCTION_DESC = {
"properties": {
"source": {
"type": "string",
- "description": f"新闻源,例如{generate_news_sources_description()}等。可选参数,如果不提供则使用默认新闻源"
+ "description": f"新闻源,例如{generate_news_sources_description()}等。可选参数,如果不提供则使用默认新闻源",
},
"detail": {
"type": "boolean",
- "description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"
+ "description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容",
},
"lang": {
"type": "string",
- "description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
- }
+ "description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN",
+ },
},
- "required": ["lang"]
- }
- }
+ "required": ["lang"],
+ },
+ },
}
-def fetch_news_from_api(source="thepaper"):
+def fetch_news_from_api(conn, source="thepaper"):
"""从API获取新闻列表"""
try:
api_url = f"https://newsnow.busiyi.world/api/s?id={source}"
+ if conn.config["plugins"].get("get_news_from_newsnow") and conn.config[
+ "plugins"
+ ]["get_news_from_newsnow"].get("url"):
+ api_url = conn.config["plugins"]["get_news_from_newsnow"]["url"] + source
+
response = requests.get(api_url, timeout=10)
response.raise_for_status()
-
+
data = response.json()
-
+
if "items" in data:
return data["items"]
else:
logger.bind(tag=TAG).error(f"获取新闻API响应格式错误: {data}")
return []
-
+
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻API失败: {e}")
return []
@@ -78,51 +86,72 @@ def fetch_news_detail(url):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
-
+
# 使用MarkItDown清理HTML内容
md = MarkItDown(enable_plugins=False)
result = md.convert(response)
-
+
# 获取清理后的文本内容
clean_text = result.text_content
-
+
# 如果清理后的内容为空,返回提示信息
if not clean_text or len(clean_text.strip()) == 0:
logger.bind(tag=TAG).warning(f"清理后的新闻内容为空: {url}")
return "无法解析新闻详情内容,可能是网站结构特殊或内容受限。"
-
+
return clean_text
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
return "无法获取详细内容"
-@register_function('get_news_from_different_source', GET_NEWS_FROM_DIFFERENT_SOURCE_FUNCTION_DESC, ToolType.SYSTEM_CTL)
-def get_news_from_different_source(conn, source: str = "thepaper", detail: bool = False, lang: str = "zh_CN"):
+@register_function(
+ "get_news_from_newsnow",
+ GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC,
+ ToolType.SYSTEM_CTL,
+)
+def get_news_from_newsnow(
+ conn, source: str = "thepaper", detail: bool = False, lang: str = "zh_CN"
+):
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
try:
# 如果detail为True,获取上一条新闻的详细内容
- detail = str(detail).lower() == 'true'
+ detail = str(detail).lower() == "true"
if detail:
- if not hasattr(conn, 'last_news_link_different') or not conn.last_news_link_different or 'url' not in conn.last_news_link_different:
- return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
+ if (
+ not hasattr(conn, "last_newsnow_link")
+ or not conn.last_newsnow_link
+ or "url" not in conn.last_newsnow_link
+ ):
+ return ActionResponse(
+ Action.REQLLM,
+ "抱歉,没有找到最近查询的新闻,请先获取一条新闻。",
+ None,
+ )
- url = conn.last_news_link_different.get('url')
- title = conn.last_news_link_different.get('title', '未知标题')
- source_id = conn.last_news_link_different.get('source_id', 'thepaper')
- source_name = NEWS_SOURCES.get(source_id, '未知来源')
+ url = conn.last_newsnow_link.get("url")
+ title = conn.last_newsnow_link.get("title", "未知标题")
+ source_id = conn.last_newsnow_link.get("source_id", "thepaper")
+ source_name = NEWS_SOURCES.get(source_id, "未知来源")
- if not url or url == '#':
- return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
+ if not url or url == "#":
+ return ActionResponse(
+ Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None
+ )
- logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, 来源: {source_name}, URL={url}")
+ logger.bind(tag=TAG).debug(
+ f"获取新闻详情: {title}, 来源: {source_name}, URL={url}"
+ )
# 获取新闻详情
detail_content = fetch_news_detail(url)
if not detail_content or detail_content == "无法获取详细内容":
- return ActionResponse(Action.REQLLM,
- f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
+ return ActionResponse(
+ Action.REQLLM,
+ f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。",
+ None,
+ )
# 构建详情报告
detail_report = (
@@ -141,26 +170,30 @@ def get_news_from_different_source(conn, source: str = "thepaper", detail: bool
if source not in NEWS_SOURCES:
logger.bind(tag=TAG).warning(f"无效的新闻源: {source},使用默认源thepaper")
source = "thepaper"
-
+
source_name = NEWS_SOURCES.get(source, "澎湃新闻")
logger.bind(tag=TAG).info(f"获取新闻: 新闻源={source}({source_name})")
# 获取新闻列表
- news_items = fetch_news_from_api(source)
+ news_items = fetch_news_from_api(conn, source)
if not news_items:
- return ActionResponse(Action.REQLLM, f"抱歉,未能从{source_name}获取到新闻信息,请稍后再试或尝试其他新闻源。", None)
+ return ActionResponse(
+ Action.REQLLM,
+ f"抱歉,未能从{source_name}获取到新闻信息,请稍后再试或尝试其他新闻源。",
+ None,
+ )
# 随机选择一条新闻
selected_news = random.choice(news_items)
-
+
# 保存当前新闻链接到连接对象,以便后续查询详情
- if not hasattr(conn, 'last_news_link_different'):
- conn.last_news_link_different = {}
- conn.last_news_link_different = {
- 'url': selected_news.get('url', '#'),
- 'title': selected_news.get('title', '未知标题'),
- 'source_id': source
+ if not hasattr(conn, "last_newsnow_link"):
+ conn.last_newsnow_link = {}
+ conn.last_newsnow_link = {
+ "url": selected_news.get("url", "#"),
+ "title": selected_news.get("title", "未知标题"),
+ "source_id": source,
}
# 构建新闻报告
@@ -176,4 +209,6 @@ def get_news_from_different_source(conn, source: str = "thepaper", detail: bool
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
- return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
\ No newline at end of file
+ return ActionResponse(
+ Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None
+ )
From be1ff832977fdade9f4123d209af2002b97056d6 Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Thu, 8 May 2025 18:23:37 +0800
Subject: [PATCH 03/11] =?UTF-8?q?update:=E9=80=9A=E8=BF=87pip=E6=9D=A5?=
=?UTF-8?q?=E5=AE=89=E8=A3=85markitdown=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Dockerfile-server | 8 --------
main/xiaozhi-server/requirements.txt | 3 ++-
2 files changed, 2 insertions(+), 9 deletions(-)
diff --git a/Dockerfile-server b/Dockerfile-server
index 86f4d666..7eb77114 100644
--- a/Dockerfile-server
+++ b/Dockerfile-server
@@ -3,18 +3,11 @@ FROM python:3.10-slim AS builder
WORKDIR /app
-RUN apt-get update && \
- apt-get install -y --no-install-recommends git && \
- apt-get clean && \
- rm -rf /var/lib/apt/lists/*
-
COPY main/xiaozhi-server/requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt
-RUN git clone https://github.com/microsoft/markitdown.git && cd markitdown && pip install -e 'packages/markitdown[all]'
-
# 第二阶段:生产镜像
FROM python:3.10-slim
@@ -28,7 +21,6 @@ RUN apt-get update && \
# 从构建阶段复制Python包和前端构建产物
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
-COPY --from=builder /app/markitdown/ /app/markitdown/
# 复制应用代码
COPY main/xiaozhi-server .
diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt
index 02fbfcdb..0a6b9b75 100755
--- a/main/xiaozhi-server/requirements.txt
+++ b/main/xiaozhi-server/requirements.txt
@@ -27,4 +27,5 @@ cnlunar==0.2.0
PySocks==1.7.1
dashscope==1.23.1
baidu-aip==4.16.13
-chardet==5.2.0
\ No newline at end of file
+chardet==5.2.0
+markitdown==0.1.1
\ No newline at end of file
From 47f409246db1a285543dcc06a2a9195ffb44c58c Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Thu, 8 May 2025 18:25:02 +0800
Subject: [PATCH 04/11] =?UTF-8?q?update:intent=5Fllm=E6=9A=82=E6=97=B6?=
=?UTF-8?q?=E4=B8=8D=E5=BC=95=E7=94=A8=E6=96=B0=E9=97=BB=E6=8F=92=E4=BB=B6?=
=?UTF-8?q?=EF=BC=8C=E4=B8=8B=E4=B8=80=E4=B8=AA=E7=89=88=E6=9C=AC=E6=94=B9?=
=?UTF-8?q?=E9=80=A0intent=5Fllm=EF=BC=8C=E8=AE=A9=E5=AE=83=E8=83=BD?=
=?UTF-8?q?=E5=8A=A8=E6=80=81=E8=8E=B7=E5=8F=96functioncall?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/config.yaml | 6 ++++--
main/xiaozhi-server/core/handle/functionHandler.py | 1 -
.../core/providers/intent/intent_llm/intent_llm.py | 9 ---------
3 files changed, 4 insertions(+), 12 deletions(-)
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index cef214ac..ad794965 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -104,12 +104,13 @@ plugins:
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
- get_news:
+ get_news_from_chinanews:
default_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
category_urls:
society: "https://www.chinanews.com.cn/rss/society.xml"
world: "https://www.chinanews.com.cn/rss/world.xml"
finance: "https://www.chinanews.com.cn/rss/finance.xml"
+ get_news_from_newsnow: {"url": "https://newsnow.busiyi.world/api/s?id="}
home_assistant:
devices:
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
@@ -183,7 +184,8 @@ Intent:
functions:
- change_role
- get_weather
- - get_news
+ # - get_news_from_chinanews
+ - get_news_from_newsnow
# play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放
# 如果用了hass_play_music,就不要开启play_music,两者只留一个
- play_music
diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py
index 10c2bf1a..b09a369c 100644
--- a/main/xiaozhi-server/core/handle/functionHandler.py
+++ b/main/xiaozhi-server/core/handle/functionHandler.py
@@ -55,7 +55,6 @@ class FunctionHandler:
self.function_registry.register_function("get_time")
self.function_registry.register_function("get_lunar")
self.function_registry.register_function("handle_device")
- self.function_registry.register_function("get_news_from_different_source")
def register_config_functions(self):
"""注册配置中的函数,可以不同客户端使用不同的配置"""
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 9915be33..05d01c10 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
@@ -41,7 +41,6 @@ class IntentProvider(IntentProviderBase):
'2. 结束对话意图: {"function_call": {"name": "handle_exit_intent", "arguments": {"say_goodbye": "goodbye"}}}\n'
'3. 获取当天日期时间: {"function_call": {"name": "get_time"}}\n'
'4. 继续聊天意图: {"function_call": {"name": "continue_chat"}}\n'
- '5. 查询新闻意图: {"function_call": {"name": "get_news_from_different_source", "arguments":{"source": {"type": "string","description": "新闻源,可选项有{"thepaper": "澎湃新闻","wallstreetcn-quick": "华尔街见闻","ithome": "IT之家","zhihu": "知乎"}。可选参数,如果不提供则使用默认新闻源"},"detail": {"type": "boolean","description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"}}}\n'
"\n"
"注意:\n"
'- 播放音乐:无歌名时,song_name设为"random"\n'
@@ -54,14 +53,6 @@ class IntentProvider(IntentProviderBase):
'返回: {"function_call": {"name": "continue_chat"}}\n'
"```\n"
"```\n"
- "用户: 最近有什么新闻吗\n"
- '返回: {"function_call": {"name": "get_news_from_different_source", "arguments": {"source":"thepaper", "detail":"False"}}}\n'
- "```\n"
- "```\n"
- "用户: 详细说说这个新闻\n"
- '返回: {"function_call": {"name": "get_news_from_different_source", "arguments": {"source":"thepaper", "detail":"True"}}}\n'
- "```\n"
- "```\n"
"用户: 现在是几号了?现在几点了?\n"
'返回: {"function_call": {"name": "get_time"}}\n'
"```\n"
From ff98f84f187513edda0bcc5f3ba962e0bf362c83 Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 9 May 2025 11:39:32 +0800
Subject: [PATCH 05/11] =?UTF-8?q?update:intent=5Fllm=E5=8A=A0=E8=BD=BD?=
=?UTF-8?q?=E5=8A=A8=E6=80=81=E6=8F=92=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/config.yaml | 7 ++
main/xiaozhi-server/core/connection.py | 26 +++--
.../core/handle/intentHandler.py | 17 ++-
main/xiaozhi-server/core/handle/iotHandle.py | 2 +-
.../core/handle/receiveAudioHandle.py | 2 +-
.../core/providers/intent/base.py | 12 --
.../providers/intent/intent_llm/intent_llm.py | 106 +++++++++++-------
main/xiaozhi-server/core/utils/dialogue.py | 8 +-
.../plugins_func/functions/hass_init.py | 4 +-
9 files changed, 113 insertions(+), 71 deletions(-)
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index ad794965..98c3d058 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -175,6 +175,13 @@ Intent:
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
# 如果你的不想使用selected_module.LLM意图识别,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
llm: ChatGLMLLM
+ # plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
+ # 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
+ # 下面是加载查天气、角色切换、加载查新闻的插件示例
+ functions:
+ - get_weather
+ - get_news_from_newsnow
+ - play_music
function_call:
# 不需要动type
type: function_call
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index b49917ba..0a5cd8e4 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -58,9 +58,9 @@ class ConnectionHandler:
self.config = copy.deepcopy(config)
self.session_id = str(uuid.uuid4())
self.logger = setup_logging()
- self.auth = AuthMiddleware(config)
self.server = server # 保存server实例的引用
+ self.auth = AuthMiddleware(config)
self.need_bind = False
self.bind_code = None
self.read_config_from_api = self.config.get("read_config_from_api", False)
@@ -128,8 +128,10 @@ class ConnectionHandler:
if len(cmd) > self.max_cmd_length:
self.max_cmd_length = len(cmd)
- self.close_after_chat = False # 是否在聊天结束后关闭连接
- self.use_function_call_mode = False
+ # 是否在聊天结束后关闭连接
+ self.close_after_chat = False
+ self.load_function_plugin = False
+ self.intent_type = "nointent"
self.timeout_task = None
self.timeout_seconds = (
@@ -365,11 +367,11 @@ class ConnectionHandler:
self.memory.init_memory(self.device_id, self.llm)
def _initialize_intent(self):
- if (
- self.config["Intent"][self.config["selected_module"]["Intent"]]["type"]
- == "function_call"
- ):
- self.use_function_call_mode = True
+ self.intent_type = self.config["Intent"][
+ self.config["selected_module"]["Intent"]
+ ]["type"]
+ if self.intent_type == "function_call" or self.intent_type == "intent_llm":
+ self.load_function_plugin = True
"""初始化意图识别模块"""
# 获取意图识别配置
intent_config = self.config["Intent"]
@@ -757,7 +759,13 @@ class ConnectionHandler:
)
self.dialogue.put(
- Message(role="tool", tool_call_id=function_id, content=text)
+ Message(
+ role="tool",
+ tool_call_id=(
+ str(uuid.uuid4()) if function_id is None else function_id
+ ),
+ content=text,
+ )
)
self.chat_with_function_calling(text, tool_call=True)
elif result.action == Action.NOTFOUND or result.action == Action.ERROR:
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index 5b00f2ef..383e4e65 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -19,7 +19,7 @@ async def handle_user_intent(conn, text):
if await checkWakeupWords(conn, text):
return True
- if conn.use_function_call_mode:
+ if conn.intent_type == "function_call":
# 使用支持function calling的聊天方法,不再进行意图分析
return False
# 使用LLM进行意图分析
@@ -103,7 +103,7 @@ async def process_intent_result(conn, intent_result, original_text):
conn, function_call_data
)
logger.bind(tag=TAG).debug(f"检测到Action : {result.action}")
-
+
if result:
if result.action == Action.RESPONSE: # 直接回复前端
text = result.response
@@ -118,14 +118,19 @@ async def process_intent_result(conn, intent_result, original_text):
conn.speak_and_play, text, text_index
)
conn.llm_finish_task = True
- conn.tts_queue.put(future)
+ conn.tts_queue.put((future, text_index))
conn.dialogue.put(Message(role="assistant", content=text))
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
text = result.result
if text is not None and len(text) > 0:
conn.dialogue.put(Message(role="tool", content=text))
- conn.executor.submit(conn.chat_with_function_calling, text, True)
- elif result.action == Action.NOTFOUND or result.action == Action.ERROR:
+ conn.executor.submit(
+ conn.chat_with_function_calling, text, True
+ )
+ elif (
+ result.action == Action.NOTFOUND
+ or result.action == Action.ERROR
+ ):
text = result.result
if text is not None:
text_index = (
@@ -157,7 +162,7 @@ async def process_intent_result(conn, intent_result, original_text):
conn.speak_and_play, text, text_index
)
conn.llm_finish_task = True
- conn.tts_queue.put(future)
+ conn.tts_queue.put((future, text_index))
conn.dialogue.put(Message(role="assistant", content=text))
# 将函数执行放在线程池中
diff --git a/main/xiaozhi-server/core/handle/iotHandle.py b/main/xiaozhi-server/core/handle/iotHandle.py
index 12432102..5b1bb9bb 100644
--- a/main/xiaozhi-server/core/handle/iotHandle.py
+++ b/main/xiaozhi-server/core/handle/iotHandle.py
@@ -317,7 +317,7 @@ async def handleIotDescriptors(conn, descriptors):
)
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
- if conn.use_function_call_mode:
+ if conn.load_function_plugin:
# 注册或获取设备类型
type_id = register_device_type(descriptor)
device_functions = device_type_registry.get_device_functions(type_id)
diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
index 383616c3..48ec24b2 100644
--- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
@@ -76,7 +76,7 @@ async def startToChat(conn, text):
# 意图未被处理,继续常规聊天流程
await send_stt_message(conn, text)
- if conn.use_function_call_mode:
+ if conn.intent_type == "function_call":
# 使用支持function calling的聊天方法
conn.executor.submit(conn.chat_with_function_calling, text)
else:
diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py
index 040dd025..f6df10dd 100644
--- a/main/xiaozhi-server/core/providers/intent/base.py
+++ b/main/xiaozhi-server/core/providers/intent/base.py
@@ -9,18 +9,6 @@ logger = setup_logging()
class IntentProviderBase(ABC):
def __init__(self, config):
self.config = config
- self.intent_options = [
- {
- "name": "handle_exit_intent",
- "desc": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
- },
- {
- "name": "play_music",
- "desc": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
- },
- {"name": "get_time", "desc": "获取今天日期或者当前时间信息"},
- {"name": "continue_chat", "desc": "继续聊天"},
- ]
def set_llm(self, llm):
self.llm = llm
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 05d01c10..d8b18916 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
@@ -15,57 +15,71 @@ class IntentProvider(IntentProviderBase):
def __init__(self, config):
super().__init__(config)
self.llm = None
- self.promot = self.get_intent_system_prompt()
+ self.promot = ""
# 添加缓存管理
self.intent_cache = {} # 缓存意图识别结果
self.cache_expiry = 600 # 缓存有效期10分钟
self.cache_max_size = 100 # 最多缓存100个意图
- def get_intent_system_prompt(self) -> str:
+ def get_intent_system_prompt(self, functions_list: str) -> str:
"""
- 根据配置的意图选项动态生成系统提示词
+ 根据配置的意图选项和可用函数动态生成系统提示词
+ Args:
+ functions: 可用的函数列表,JSON格式字符串
Returns:
格式化后的系统提示词
"""
+ # 构建函数说明部分
+ functions_desc = "可用的函数列表:\n"
+ for func in functions_list:
+ func_info = func.get("function", {})
+ name = func_info.get("name", "")
+ desc = func_info.get("description", "")
+ params = func_info.get("parameters", {})
+
+ functions_desc += f"\n函数名: {name}\n"
+ functions_desc += f"描述: {desc}\n"
+
+ if params:
+ functions_desc += "参数:\n"
+ for param_name, param_info in params.get("properties", {}).items():
+ param_desc = param_info.get("description", "")
+ param_type = param_info.get("type", "")
+ functions_desc += f"- {param_name} ({param_type}): {param_desc}\n"
+
+ functions_desc += "---\n"
+
prompt = (
- "你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
- "
-English +English · 常见问题 · 反馈问题 · 部署文档 diff --git a/README_en.md b/README_en.md new file mode 100644 index 00000000..9b4ed8ce --- /dev/null +++ b/README_en.md @@ -0,0 +1,273 @@ +[](https://github.com/xinnan-tech/xiaozhi-esp32-server) + +
+This project provides backend services for the open-source smart hardware project
+xiaozhi-esp32
+Implemented using Python, Java, and Vue according to the Xiaozhi Communication Protocol
+Helping you quickly set up your Xiaozhi server
+
+中文 +· FAQ +· Report Issues +· Deployment Guide +· Release Notes +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
| [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | This project was inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it |
+|
| [Tenclass](https://www.tenclass.com/) | Thanks to [Tenclass](https://www.tenclass.com/) for developing standard communication protocols, multi-device compatibility solutions, and high-concurrency scenario practices for the Xiaozhi ecosystem; providing comprehensive technical documentation support for this project |
+|
| [Xuanfeng Technology](https://github.com/Eric0308) | Thanks to [Xuanfeng Technology](https://github.com/Eric0308) for contributing function call framework, MCP communication protocol, and plugin call mechanism implementation code, significantly improving front-end device (IoT) interaction efficiency and functional extensibility through standardized instruction scheduling system and dynamic expansion capabilities |
+|
| [Huiyuan Design](http://ui.kwd988.net/) | Thanks to [Huiyuan Design](http://ui.kwd988.net/) for providing professional visual solutions for this project, empowering product user experience with their design experience serving over a thousand enterprises |
+|
| [Xi'an Qinren Information Technology](https://www.029app.com/) | Thanks to [Xi'an Qinren Information Technology](https://www.029app.com/) for deepening this project's visual system, ensuring consistency and extensibility of overall design style in multi-scenario applications |
+
+
+
+
+