🛠️添加小棉工具功能,移除MARSHOAI_ENABLE_TIME_PROMPT配置项

This commit is contained in:
Asankilp 2024-11-23 21:21:19 +08:00
parent 3a764f5ea9
commit aabd33f189
11 changed files with 220 additions and 21 deletions

View File

@ -100,6 +100,9 @@ _✨ 使用 OpenAI 标准格式 API 的聊天机器人插件 ✨_
当 nonebot 连接到支持的 OneBot v11 实现端时,可以接收头像双击戳一戳消息并进行响应。详见`MARSHOAI_POKE_SUFFIX`配置项。 当 nonebot 连接到支持的 OneBot v11 实现端时,可以接收头像双击戳一戳消息并进行响应。详见`MARSHOAI_POKE_SUFFIX`配置项。
## 🛠️ 小棉工具
小棉工具(MarshoTools)是`v0.5.0`版本的新增功能,支持加载外部函数库来为 Marsho 提供 Function Call 功能。[使用文档](./README_TOOLS.md)
## 👍 夸赞名单 ## 👍 夸赞名单
夸赞名单存储于插件数据目录下的`praises.json`里(该目录路径会在 Bot 启动时输出到日志),当配置项为`true` 夸赞名单存储于插件数据目录下的`praises.json`里(该目录路径会在 Bot 启动时输出到日志),当配置项为`true`
@ -139,7 +142,7 @@ _✨ 使用 OpenAI 标准格式 API 的聊天机器人插件 ✨_
| MARSHOAI_ENABLE_SUPPORT_IMAGE_TIP | 否 | `true` | 启用后用户发送带图请求时若模型不支持图片,则提示用户 | | MARSHOAI_ENABLE_SUPPORT_IMAGE_TIP | 否 | `true` | 启用后用户发送带图请求时若模型不支持图片,则提示用户 |
| MARSHOAI_ENABLE_NICKNAME_TIP | 否 | `true` | 启用后用户未设置昵称时提示用户设置 | | MARSHOAI_ENABLE_NICKNAME_TIP | 否 | `true` | 启用后用户未设置昵称时提示用户设置 |
| MARSHOAI_ENABLE_PRAISES | 否 | `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_AZURE_ENDPOINT | 否 | `https://models.inference.ai.azure.com` | OpenAI 标准格式 API 端点 |
| MARSHOAI_TEMPERATURE | 否 | 无 | 进行推理时的温度参数 | | MARSHOAI_TEMPERATURE | 否 | 无 | 进行推理时的温度参数 |
| MARSHOAI_TOP_P | 否 | 无 | 进行推理时的核采样参数 | | MARSHOAI_TOP_P | 否 | 无 | 进行推理时的核采样参数 |

View File

@ -1,9 +1,8 @@
from nonebot.plugin import require from nonebot.plugin import require
require("nonebot_plugin_alconna") require("nonebot_plugin_alconna")
require("nonebot_plugin_localstore") require("nonebot_plugin_localstore")
from .azure import * from .azure import *
from .hunyuan import * #from .hunyuan import *
from nonebot import get_driver, logger from nonebot import get_driver, logger
from .config import config from .config import config
from .metadata import metadata from .metadata import metadata

View File

@ -1,15 +1,18 @@
import contextlib import contextlib
import traceback import traceback
from typing import Optional from typing import Optional
from pathlib import Path
from arclet.alconna import Alconna, Args, AllParam from arclet.alconna import Alconna, Args, AllParam
from azure.ai.inference.models import ( from azure.ai.inference.models import (
UserMessage, UserMessage,
AssistantMessage, AssistantMessage,
ToolMessage,
TextContentItem, TextContentItem,
ImageContentItem, ImageContentItem,
ImageUrl, ImageUrl,
CompletionsFinishReason, CompletionsFinishReason,
ChatCompletionsToolCall,
) )
from azure.core.credentials import AzureKeyCredential from azure.core.credentials import AzureKeyCredential
from nonebot import on_command, logger from nonebot import on_command, logger
@ -18,11 +21,12 @@ from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from nonebot_plugin_alconna import on_alconna, MsgTarget from nonebot_plugin_alconna import on_alconna, MsgTarget
from nonebot_plugin_alconna.uniseg import UniMessage, UniMsg from nonebot_plugin_alconna.uniseg import UniMessage, UniMsg
import nonebot_plugin_localstore as store
from nonebot import get_driver from nonebot import get_driver
from .constants import * from .constants import *
from .metadata import metadata from .metadata import metadata
from .models import MarshoContext from .models import MarshoContext, MarshoTools
from .util import * from .util import *
driver = get_driver() driver = get_driver()
@ -53,11 +57,18 @@ refresh_data_cmd = on_command("refresh_data", permission=SUPERUSER)
model_name = config.marshoai_default_model model_name = config.marshoai_default_model
context = MarshoContext() context = MarshoContext()
tools = MarshoTools()
token = config.marshoai_token token = config.marshoai_token
endpoint = config.marshoai_azure_endpoint endpoint = config.marshoai_azure_endpoint
client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(token)) client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(token))
target_list = [] # 记录需保存历史上下文的列表 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() @add_usermsg_cmd.handle()
async def add_usermsg(target: MsgTarget, arg: Message = CommandArg()): 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() @praises_cmd.handle()
async def praises(): async def praises():
#await UniMessage(await tools.call("marshoai-weather.get_weather", {"location":"杭州"})).send()
await praises_cmd.finish(build_praises()) await praises_cmd.finish(build_praises())
@ -200,24 +212,45 @@ async def marsho(target: MsgTarget, event: Event, text: Optional[UniMsg] = None)
client=client, client=client,
model_name=model_name, model_name=model_name,
msg=context_msg + [UserMessage(content=usermsg)], msg=context_msg + [UserMessage(content=usermsg)],
tools=tools.get_tools_list()
) )
# await UniMessage(str(response)).send() # await UniMessage(str(response)).send()
choice = response.choices[0] choice = response.choices[0]
if ( if (choice["finish_reason"] == CompletionsFinishReason.STOPPED): # 当对话成功时将dict的上下文添加到上下文类中
choice["finish_reason"] == CompletionsFinishReason.STOPPED
): # 当对话成功时将dict的上下文添加到上下文类中
context.append( context.append(
UserMessage(content=usermsg).as_dict(), target.id, target.private UserMessage(content=usermsg).as_dict(), target.id, target.private
) )
context.append(choice.message.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: if [target.id, target.private] not in target_list:
target_list.append([target.id, target.private]) target_list.append([target.id, target.private])
await UniMessage(str(choice.message.content)).send(reply_to=True)
elif choice["finish_reason"] == CompletionsFinishReason.CONTENT_FILTERED: elif choice["finish_reason"] == CompletionsFinishReason.CONTENT_FILTERED:
await UniMessage("*已被内容过滤器过滤。请调整聊天内容后重试。").send( await UniMessage("*已被内容过滤器过滤。请调整聊天内容后重试。").send(reply_to=True)
reply_to=True
)
return 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: except Exception as e:
await UniMessage(str(e) + suggest_solution(str(e))).send() await UniMessage(str(e) + suggest_solution(str(e))).send()
traceback.print_exc() traceback.print_exc()

View File

@ -24,13 +24,15 @@ class ConfigModel(BaseModel):
marshoai_enable_support_image_tip: bool = True marshoai_enable_support_image_tip: bool = True
marshoai_enable_praises: bool = True marshoai_enable_praises: bool = True
marshoai_enable_time_prompt: bool = True marshoai_enable_time_prompt: bool = True
marshoai_enable_tools: bool = True
marshoai_azure_endpoint: str = "https://models.inference.ai.azure.com" marshoai_azure_endpoint: str = "https://models.inference.ai.azure.com"
marshoai_temperature: float | None = None marshoai_temperature: float | None = None
marshoai_max_tokens: int | None = None marshoai_max_tokens: int | None = None
marshoai_top_p: float | None = None marshoai_top_p: float | None = None
marshoai_additional_image_models: list = [] marshoai_additional_image_models: list = []
marshoai_tencent_secretid: str | None = None marshoai_tencent_secretid: str | None = None
marshoai_tencent_secretkey:str | None = None marshoai_tencent_secretkey: str | None = None
config: ConfigModel = get_plugin_config(ConfigModel) config: ConfigModel = get_plugin_config(ConfigModel)

View File

@ -1,5 +1,12 @@
from .util import * 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: class MarshoContext:
""" """
@ -47,3 +54,76 @@ class MarshoContext:
if target_id not in target_dict: if target_id not in target_dict:
target_dict[target_id] = [] target_dict[target_id] = []
return 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

View File

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

View File

@ -0,0 +1,11 @@
[
{
"type": "function",
"function": {
"name": "marshoai-basic__get_current_time",
"description": "获取现在的时间。",
"parameters": {
}
}
}
]

View File

@ -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": {
}
}
}
]

View File

@ -0,0 +1,2 @@
async def write_memory(memory: str):
return ""

View File

@ -0,0 +1,21 @@
[
{
"type": "function",
"function": {
"name": "marshoai-memory__write_memory",
"description": "当你想记住有关与你对话的人的一些信息的时候,调用此函数。",
"parameters": {
"type": "object",
"properties": {
"memory": {
"type": "string",
"description": "你想记住的内容,概括并保留关键内容。"
}
}
},
"required": [
"memory"
]
}
}
]

View File

@ -40,7 +40,7 @@ async def get_image_b64(url):
return None 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获取回复 """调用ai获取回复
参数: 参数:
@ -50,6 +50,7 @@ async def make_chat(client: ChatCompletionsClient, msg: list, model_name: str):
return await client.complete( return await client.complete(
messages=msg, messages=msg,
model=model_name, model=model_name,
tools=tools,
temperature=config.marshoai_temperature, temperature=config.marshoai_temperature,
max_tokens=config.marshoai_max_tokens, max_tokens=config.marshoai_max_tokens,
top_p=config.marshoai_top_p, top_p=config.marshoai_top_p,
@ -173,13 +174,6 @@ def get_prompt():
if config.marshoai_enable_praises: if config.marshoai_enable_praises:
praises_prompt = build_praises() praises_prompt = build_praises()
prompts += praises_prompt 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 marsho_prompt = config.marshoai_prompt
spell = SystemMessage(content=marsho_prompt + prompts).as_dict() spell = SystemMessage(content=marsho_prompt + prompts).as_dict()
return spell return spell