mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 15:13:55 +08:00
Merge pull request #1005 from JavaZeroo/add_news
feat: 添加多个新闻源,并修复大模型意图识别没有处理ActionResponse的问题
This commit is contained in:
@@ -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 .
|
||||
|
||||
@@ -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):
|
||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user