From aabd33f189967f0a2a8422fd1c1fe35fef45c1cc Mon Sep 17 00:00:00 2001 From: Asankilp Date: Sat, 23 Nov 2024 21:21:19 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=B0=8F=E6=A3=89=E5=B7=A5=E5=85=B7=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4MARSHOAI=5FENABLE=5FTIME=5FPROMPT=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- nonebot_plugin_marshoai/__init__.py | 3 +- nonebot_plugin_marshoai/azure.py | 49 +++++++++-- nonebot_plugin_marshoai/config.py | 4 +- nonebot_plugin_marshoai/models.py | 82 ++++++++++++++++++- .../tools/marshoai-basic/__init__.py | 15 ++++ .../tools/marshoai-basic/tools.json | 11 +++ .../tools/marshoai-basic/tools_test.json | 39 +++++++++ .../tools_wip/marshoai-memory/__init__.py | 2 + .../tools_wip/marshoai-memory/tools.json | 21 +++++ nonebot_plugin_marshoai/util.py | 10 +-- 11 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 nonebot_plugin_marshoai/tools/marshoai-basic/__init__.py create mode 100644 nonebot_plugin_marshoai/tools/marshoai-basic/tools.json create mode 100644 nonebot_plugin_marshoai/tools/marshoai-basic/tools_test.json create mode 100644 nonebot_plugin_marshoai/tools_wip/marshoai-memory/__init__.py create mode 100644 nonebot_plugin_marshoai/tools_wip/marshoai-memory/tools.json diff --git a/README.md b/README.md index 604ea10..659dcf8 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ _✨ 使用 OpenAI 标准格式 API 的聊天机器人插件 ✨_ 当 nonebot 连接到支持的 OneBot v11 实现端时,可以接收头像双击戳一戳消息并进行响应。详见`MARSHOAI_POKE_SUFFIX`配置项。 +## 🛠️ 小棉工具 +小棉工具(MarshoTools)是`v0.5.0`版本的新增功能,支持加载外部函数库来为 Marsho 提供 Function Call 功能。[使用文档](./README_TOOLS.md) + ## 👍 夸赞名单 夸赞名单存储于插件数据目录下的`praises.json`里(该目录路径会在 Bot 启动时输出到日志),当配置项为`true` @@ -139,7 +142,7 @@ _✨ 使用 OpenAI 标准格式 API 的聊天机器人插件 ✨_ | MARSHOAI_ENABLE_SUPPORT_IMAGE_TIP | 否 | `true` | 启用后用户发送带图请求时若模型不支持图片,则提示用户 | | MARSHOAI_ENABLE_NICKNAME_TIP | 否 | `true` | 启用后用户未设置昵称时提示用户设置 | | MARSHOAI_ENABLE_PRAISES | 否 | `true` | 是否启用夸赞名单功能 | -| MARSHOAI_ENABLE_TIME_PROMPT | 否 | `true` | 是否启用实时更新的日期与时间(精确到秒)与农历日期系统提示词 | +| MARSHOAI_ENABLE_TOOLS | 否 | `true` | 是否启用小棉工具(MarshoTools) | | MARSHOAI_AZURE_ENDPOINT | 否 | `https://models.inference.ai.azure.com` | OpenAI 标准格式 API 端点 | | MARSHOAI_TEMPERATURE | 否 | 无 | 进行推理时的温度参数 | | MARSHOAI_TOP_P | 否 | 无 | 进行推理时的核采样参数 | diff --git a/nonebot_plugin_marshoai/__init__.py b/nonebot_plugin_marshoai/__init__.py index d9728f7..5406a5f 100644 --- a/nonebot_plugin_marshoai/__init__.py +++ b/nonebot_plugin_marshoai/__init__.py @@ -1,9 +1,8 @@ from nonebot.plugin import require - require("nonebot_plugin_alconna") require("nonebot_plugin_localstore") from .azure import * -from .hunyuan import * +#from .hunyuan import * from nonebot import get_driver, logger from .config import config from .metadata import metadata diff --git a/nonebot_plugin_marshoai/azure.py b/nonebot_plugin_marshoai/azure.py index d0b1257..0e1c241 100644 --- a/nonebot_plugin_marshoai/azure.py +++ b/nonebot_plugin_marshoai/azure.py @@ -1,15 +1,18 @@ import contextlib import traceback from typing import Optional +from pathlib import Path from arclet.alconna import Alconna, Args, AllParam from azure.ai.inference.models import ( UserMessage, AssistantMessage, + ToolMessage, TextContentItem, ImageContentItem, ImageUrl, CompletionsFinishReason, + ChatCompletionsToolCall, ) from azure.core.credentials import AzureKeyCredential from nonebot import on_command, logger @@ -18,11 +21,12 @@ from nonebot.params import CommandArg from nonebot.permission import SUPERUSER from nonebot_plugin_alconna import on_alconna, MsgTarget from nonebot_plugin_alconna.uniseg import UniMessage, UniMsg +import nonebot_plugin_localstore as store from nonebot import get_driver from .constants import * from .metadata import metadata -from .models import MarshoContext +from .models import MarshoContext, MarshoTools from .util import * driver = get_driver() @@ -53,11 +57,18 @@ refresh_data_cmd = on_command("refresh_data", permission=SUPERUSER) model_name = config.marshoai_default_model context = MarshoContext() +tools = MarshoTools() token = config.marshoai_token endpoint = config.marshoai_azure_endpoint client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(token)) target_list = [] # 记录需保存历史上下文的列表 +@driver.on_startup +async def _preload_tools(): + tools_dir = store.get_plugin_data_dir() / "tools" + os.makedirs(tools_dir, exist_ok=True) + tools.load_tools(Path(__file__).parent / "tools") + tools.load_tools(store.get_plugin_data_dir() / "tools") @add_usermsg_cmd.handle() async def add_usermsg(target: MsgTarget, arg: Message = CommandArg()): @@ -77,6 +88,7 @@ async def add_assistantmsg(target: MsgTarget, arg: Message = CommandArg()): @praises_cmd.handle() async def praises(): + #await UniMessage(await tools.call("marshoai-weather.get_weather", {"location":"杭州"})).send() await praises_cmd.finish(build_praises()) @@ -200,24 +212,45 @@ async def marsho(target: MsgTarget, event: Event, text: Optional[UniMsg] = None) client=client, model_name=model_name, msg=context_msg + [UserMessage(content=usermsg)], + tools=tools.get_tools_list() ) # await UniMessage(str(response)).send() choice = response.choices[0] - if ( - choice["finish_reason"] == CompletionsFinishReason.STOPPED - ): # 当对话成功时,将dict的上下文添加到上下文类中 + if (choice["finish_reason"] == CompletionsFinishReason.STOPPED): # 当对话成功时,将dict的上下文添加到上下文类中 context.append( UserMessage(content=usermsg).as_dict(), target.id, target.private ) context.append(choice.message.as_dict(), target.id, target.private) if [target.id, target.private] not in target_list: target_list.append([target.id, target.private]) + await UniMessage(str(choice.message.content)).send(reply_to=True) elif choice["finish_reason"] == CompletionsFinishReason.CONTENT_FILTERED: - await UniMessage("*已被内容过滤器过滤。请调整聊天内容后重试。").send( - reply_to=True - ) + await UniMessage("*已被内容过滤器过滤。请调整聊天内容后重试。").send(reply_to=True) return - await UniMessage(str(choice.message.content)).send(reply_to=True) + elif choice["finish_reason"] == CompletionsFinishReason.TOOL_CALLS: + tool_msg = [] + while choice.message.tool_calls != None: + tool_msg.append(AssistantMessage(tool_calls=response.choices[0].message.tool_calls)) + for tool_call in choice.message.tool_calls: + if isinstance(tool_call, ChatCompletionsToolCall): + function_args = json.loads(tool_call.function.arguments.replace("'", '"')) + logger.info(f"调用函数 {tool_call.function.name} ,参数为 {function_args}") + await UniMessage(f"调用函数 {tool_call.function.name} ,参数为 {function_args}").send() + func_return = await tools.call(tool_call.function.name, function_args) + tool_msg.append(ToolMessage(tool_call_id=tool_call.id, content=func_return)) + response = await make_chat( + client=client, + model_name=model_name, + msg = context_msg + [UserMessage(content=usermsg)] + tool_msg, + tools=tools.get_tools_list() + ) + choice = response.choices[0] + context.append( + UserMessage(content=usermsg).as_dict(), target.id, target.private + ) + #context.append(tool_msg, target.id, target.private) + context.append(choice.message.as_dict(), target.id, target.private) + await UniMessage(str(choice.message.content)).send(reply_to=True) except Exception as e: await UniMessage(str(e) + suggest_solution(str(e))).send() traceback.print_exc() diff --git a/nonebot_plugin_marshoai/config.py b/nonebot_plugin_marshoai/config.py index 7194ca0..6f06aeb 100644 --- a/nonebot_plugin_marshoai/config.py +++ b/nonebot_plugin_marshoai/config.py @@ -24,13 +24,15 @@ class ConfigModel(BaseModel): marshoai_enable_support_image_tip: bool = True marshoai_enable_praises: bool = True marshoai_enable_time_prompt: bool = True + marshoai_enable_tools: bool = True marshoai_azure_endpoint: str = "https://models.inference.ai.azure.com" marshoai_temperature: float | None = None marshoai_max_tokens: int | None = None marshoai_top_p: float | None = None marshoai_additional_image_models: list = [] marshoai_tencent_secretid: str | None = None - marshoai_tencent_secretkey:str | None = None + marshoai_tencent_secretkey: str | None = None + config: ConfigModel = get_plugin_config(ConfigModel) diff --git a/nonebot_plugin_marshoai/models.py b/nonebot_plugin_marshoai/models.py index 9042567..46e30c1 100644 --- a/nonebot_plugin_marshoai/models.py +++ b/nonebot_plugin_marshoai/models.py @@ -1,5 +1,12 @@ from .util import * - +from .config import config +import os +import re +import json +import importlib +#import importlib.util +import traceback +from nonebot import logger class MarshoContext: """ @@ -47,3 +54,76 @@ class MarshoContext: if target_id not in target_dict: target_dict[target_id] = [] return target_dict[target_id] + + +class MarshoTools: + """ + Marsho 的工具类 + """ + def __init__(self): + self.tools_list = [] + self.imported_packages = {} + + def load_tools(self, tools_dir): + """ + 从指定路径加载工具包 + """ + if not os.path.exists(tools_dir): + logger.error(f"工具集目录 {tools_dir} 不存在。") + return + + for package_name in os.listdir(tools_dir): + package_path = os.path.join(tools_dir, package_name) + if os.path.isdir(package_path) and os.path.exists(os.path.join(package_path, '__init__.py')): + json_path = os.path.join(package_path, 'tools.json') + if os.path.exists(json_path): + try: + with open(json_path, 'r') as json_file: + data = json.load(json_file) + for i in data: + self.tools_list.append(i) + # 导入包 + spec = importlib.util.spec_from_file_location(package_name, os.path.join(package_path, "__init__.py")) + package = importlib.util.module_from_spec(spec) + spec.loader.exec_module(package) + self.imported_packages[package_name] = package + logger.info(f"成功加载工具包 {package_name}") + except json.JSONDecodeError as e: + logger.error(f"解码 JSON {json_path} 时发生错误: {e}") + except Exception as e: + logger.error(f"加载工具包时发生错误: {e}") + traceback.print_exc() + else: + logger.warning(f"在工具包 {package_path} 下找不到tools.json,跳过加载。") + else: + logger.warning(f"{package_path} 不是有效的工具包路径,跳过加载。") + + async def call(self, full_function_name: str, args: dict): + """ + 调用指定的函数 + """ + # 分割包名和函数名 + parts = full_function_name.split("__") + if len(parts) == 2: + package_name = parts[0] + function_name = parts[1] + else: + logger.error("函数名无效") + if package_name in self.imported_packages: + package = self.imported_packages[package_name] + try: + function = getattr(package, function_name) + return await function(**args) + except AttributeError: + logger.error(f"函数 '{function_name}' 在 '{package_name}' 中找不到。") + except TypeError as e: + logger.error(f"调用函数 '{function_name}' 时发生错误: {e}") + else: + logger.error(f"工具包 '{package_name}' 未导入") + + def get_tools_list(self): + if not self.tools_list or not config.marshoai_enable_tools: + return None + return self.tools_list + + diff --git a/nonebot_plugin_marshoai/tools/marshoai-basic/__init__.py b/nonebot_plugin_marshoai/tools/marshoai-basic/__init__.py new file mode 100644 index 0000000..bbf1af9 --- /dev/null +++ b/nonebot_plugin_marshoai/tools/marshoai-basic/__init__.py @@ -0,0 +1,15 @@ +import os +from datetime import datetime +from zhDateTime import DateTime +async def get_weather(location: str): + return f"{location}的温度是114514℃。" + +async def get_current_env(): + ver = os.popen("uname -a").read() + return str(ver) + +async def get_current_time(): + current_time = datetime.now().strftime("%Y.%m.%d %H:%M:%S") + current_lunar_date = (DateTime.now().to_lunar().date_hanzify()[5:]) + time_prompt = f"现在的时间是{current_time},农历{current_lunar_date}。" + return time_prompt \ No newline at end of file diff --git a/nonebot_plugin_marshoai/tools/marshoai-basic/tools.json b/nonebot_plugin_marshoai/tools/marshoai-basic/tools.json new file mode 100644 index 0000000..e872498 --- /dev/null +++ b/nonebot_plugin_marshoai/tools/marshoai-basic/tools.json @@ -0,0 +1,11 @@ +[ + { + "type": "function", + "function": { + "name": "marshoai-basic__get_current_time", + "description": "获取现在的时间。", + "parameters": { + } + } + } +] \ No newline at end of file diff --git a/nonebot_plugin_marshoai/tools/marshoai-basic/tools_test.json b/nonebot_plugin_marshoai/tools/marshoai-basic/tools_test.json new file mode 100644 index 0000000..57f3935 --- /dev/null +++ b/nonebot_plugin_marshoai/tools/marshoai-basic/tools_test.json @@ -0,0 +1,39 @@ +[ + { + "type": "function", + "function": { + "name": "marshoai-basic__get_weather", + "description": "当你想查询指定城市的天气时非常有用。", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "城市或县区,比如北京市、杭州市、余杭区等。" + } + } + }, + "required": [ + "location" + ] + } + }, + { + "type": "function", + "function": { + "name": "marshoai-basic__get_current_env", + "description": "获取当前的运行环境。", + "parameters": { + } + } + }, + { + "type": "function", + "function": { + "name": "marshoai-basic__get_current_time", + "description": "获取现在的时间。", + "parameters": { + } + } + } +] \ No newline at end of file diff --git a/nonebot_plugin_marshoai/tools_wip/marshoai-memory/__init__.py b/nonebot_plugin_marshoai/tools_wip/marshoai-memory/__init__.py new file mode 100644 index 0000000..b698cef --- /dev/null +++ b/nonebot_plugin_marshoai/tools_wip/marshoai-memory/__init__.py @@ -0,0 +1,2 @@ +async def write_memory(memory: str): + return "" \ No newline at end of file diff --git a/nonebot_plugin_marshoai/tools_wip/marshoai-memory/tools.json b/nonebot_plugin_marshoai/tools_wip/marshoai-memory/tools.json new file mode 100644 index 0000000..97e08af --- /dev/null +++ b/nonebot_plugin_marshoai/tools_wip/marshoai-memory/tools.json @@ -0,0 +1,21 @@ +[ + { + "type": "function", + "function": { + "name": "marshoai-memory__write_memory", + "description": "当你想记住有关与你对话的人的一些信息的时候,调用此函数。", + "parameters": { + "type": "object", + "properties": { + "memory": { + "type": "string", + "description": "你想记住的内容,概括并保留关键内容。" + } + } + }, + "required": [ + "memory" + ] + } + } +] \ No newline at end of file diff --git a/nonebot_plugin_marshoai/util.py b/nonebot_plugin_marshoai/util.py index 2743796..0a6e3e6 100644 --- a/nonebot_plugin_marshoai/util.py +++ b/nonebot_plugin_marshoai/util.py @@ -40,7 +40,7 @@ async def get_image_b64(url): return None -async def make_chat(client: ChatCompletionsClient, msg: list, model_name: str): +async def make_chat(client: ChatCompletionsClient, msg: list, model_name: str, tools: list = None): """调用ai获取回复 参数: @@ -50,6 +50,7 @@ async def make_chat(client: ChatCompletionsClient, msg: list, model_name: str): return await client.complete( messages=msg, model=model_name, + tools=tools, temperature=config.marshoai_temperature, max_tokens=config.marshoai_max_tokens, top_p=config.marshoai_top_p, @@ -173,13 +174,6 @@ def get_prompt(): if config.marshoai_enable_praises: praises_prompt = build_praises() prompts += praises_prompt - if config.marshoai_enable_time_prompt: - current_time = datetime.now().strftime("%Y.%m.%d %H:%M:%S") - current_lunar_date = ( - DateTime.now().to_lunar().date_hanzify()[5:] - ) # 库更新之前使用切片 - time_prompt = f"现在的时间是{current_time},农历{current_lunar_date}。" - prompts += time_prompt marsho_prompt = config.marshoai_prompt spell = SystemMessage(content=marsho_prompt + prompts).as_dict() return spell