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] 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