From f0caea9026c413cbcc965740965fff9441526a60 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 21 Nov 2023 14:23:47 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E9=92=88=E5=AF=B9=20OneBot=20?= =?UTF-8?q?=E5=92=8C=20NoneBot=20=E7=9A=84=E6=B6=88=E6=81=AF=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E5=B1=82=E5=92=8C=E6=8F=92=E4=BB=B6=E7=9A=84=E5=88=9D?= =?UTF-8?q?=E6=AD=A5=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cores/qqbot/core.py | 113 +++++++-------- model/command/adapter/nonebot/command_arg.py | 2 + model/command/adapter/nonebot/common.py | 32 ++++ model/command/adapter/nonebot/driver.py | 11 ++ model/command/adapter/onebot/bot.py | 2 + model/command/adapter/onebot/message.py | 2 + model/command/adapter/onebot/message_event.py | 2 + .../command/adapter/onebot/message_segment.py | 2 + model/command/adapter/protocol_adapter.py | 137 ++++++++++++++++++ model/command/command.py | 14 +- model/command/openai_official.py | 8 +- model/command/rev_chatgpt.py | 4 +- model/platform/qqchan.py | 7 +- 13 files changed, 269 insertions(+), 67 deletions(-) create mode 100644 model/command/adapter/nonebot/command_arg.py create mode 100644 model/command/adapter/nonebot/common.py create mode 100644 model/command/adapter/nonebot/driver.py create mode 100644 model/command/adapter/onebot/bot.py create mode 100644 model/command/adapter/onebot/message.py create mode 100644 model/command/adapter/onebot/message_event.py create mode 100644 model/command/adapter/onebot/message_segment.py create mode 100644 model/command/adapter/protocol_adapter.py diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index 262483622..95000a6b6 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -207,6 +207,61 @@ def initBot(cfg, prov): if 'reply_prefix' in cfg: _global_object.reply_prefix = cfg['reply_prefix'] + + gu.log("--------加载机器人平台--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + thread_inst = None + admin_qq = cc.get('admin_qq', None) + admin_qqchan = cc.get('admin_qqchan', None) + if admin_qq == None: + gu.log("未设置管理者QQ号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) + admin_qq = input("请输入管理者QQ号(必须设置): ") + gu.log("管理者QQ号设置为: " + admin_qq, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + cc.put('admin_qq', admin_qq) + if admin_qqchan == None: + gu.log("未设置管理者QQ频道用户号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) + admin_qqchan = input("请输入管理者频道用户号(不是QQ号, 可以先回车跳过然后在频道发送指令!myid获取): ") + if admin_qqchan == "": + gu.log("跳过设置管理者频道用户号", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + else: + gu.log("管理者频道用户号设置为: " + admin_qqchan, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + cc.put('admin_qqchan', admin_qqchan) + + gu.log("管理者QQ: " + admin_qq, gu.LEVEL_INFO) + gu.log("管理者频道用户号: " + admin_qqchan, gu.LEVEL_INFO) + _global_object.admin_qq = admin_qq + _global_object.admin_qqchan = admin_qqchan + + # GOCQ + global gocq_bot + + if 'gocqbot' in cfg and cfg['gocqbot']['enable']: + gu.log("- 启用QQ机器人 -", gu.LEVEL_INFO) + + global gocq_app, gocq_loop + gocq_loop = asyncio.new_event_loop() + gocq_bot = QQ(True, cc, gocq_loop) + thread_inst = threading.Thread(target=run_gocq_bot, args=(gocq_loop, gocq_bot, gocq_app), daemon=False) + thread_inst.start() + else: + gocq_bot = QQ(False) + + _global_object.platform_qq = gocq_bot + + gu.log("机器人部署教程: https://github.com/Soulter/QQChannelChatGPT/wiki/", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + gu.log("如果有任何问题, 请在 https://github.com/Soulter/QQChannelChatGPT 上提交issue或加群322154837", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + gu.log("请给 https://github.com/Soulter/QQChannelChatGPT 点个star!", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + + # QQ频道 + if 'qqbot' in cfg and cfg['qqbot']['enable']: + gu.log("- 启用QQ频道机器人 -", gu.LEVEL_INFO) + global qqchannel_bot, qqchan_loop + qqchannel_bot = QQChan() + qqchan_loop = asyncio.new_event_loop() + _global_object.platform_qqchan = qqchannel_bot + thread_inst = threading.Thread(target=run_qqchan_bot, args=(cfg, qqchan_loop, qqchannel_bot), daemon=False) + thread_inst.start() + # thread.join() + # 语言模型提供商 gu.log("--------加载语言模型--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) @@ -312,8 +367,6 @@ def initBot(cfg, prov): nick_qq = tuple(nick_qq) _global_object.nick = nick_qq - thread_inst = None - gu.log("--------加载插件--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) # 加载插件 _command = Command(None, _global_object) @@ -327,59 +380,6 @@ def initBot(cfg, prov): llm_command_instance[NONE_LLM] = _command chosen_provider = NONE_LLM - gu.log("--------加载机器人平台--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - - admin_qq = cc.get('admin_qq', None) - admin_qqchan = cc.get('admin_qqchan', None) - if admin_qq == None: - gu.log("未设置管理者QQ号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) - admin_qq = input("请输入管理者QQ号(必须设置): ") - gu.log("管理者QQ号设置为: " + admin_qq, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - cc.put('admin_qq', admin_qq) - if admin_qqchan == None: - gu.log("未设置管理者QQ频道用户号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) - admin_qqchan = input("请输入管理者频道用户号(不是QQ号, 可以先回车跳过然后在频道发送指令!myid获取): ") - if admin_qqchan == "": - gu.log("跳过设置管理者频道用户号", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - else: - gu.log("管理者频道用户号设置为: " + admin_qqchan, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - cc.put('admin_qqchan', admin_qqchan) - - gu.log("管理者QQ: " + admin_qq, gu.LEVEL_INFO) - gu.log("管理者频道用户号: " + admin_qqchan, gu.LEVEL_INFO) - _global_object.admin_qq = admin_qq - _global_object.admin_qqchan = admin_qqchan - - # GOCQ - global gocq_bot - - if 'gocqbot' in cfg and cfg['gocqbot']['enable']: - gu.log("- 启用QQ机器人 -", gu.LEVEL_INFO) - - global gocq_app, gocq_loop - gocq_loop = asyncio.new_event_loop() - gocq_bot = QQ(True, cc, gocq_loop) - thread_inst = threading.Thread(target=run_gocq_bot, args=(gocq_loop, gocq_bot, gocq_app), daemon=False) - thread_inst.start() - else: - gocq_bot = QQ(False) - - _global_object.platform_qq = gocq_bot - - gu.log("机器人部署教程: https://github.com/Soulter/QQChannelChatGPT/wiki/", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - gu.log("如果有任何问题, 请在 https://github.com/Soulter/QQChannelChatGPT 上提交issue或加群322154837", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - gu.log("请给 https://github.com/Soulter/QQChannelChatGPT 点个star!", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - - # QQ频道 - if 'qqbot' in cfg and cfg['qqbot']['enable']: - gu.log("- 启用QQ频道机器人 -", gu.LEVEL_INFO) - global qqchannel_bot, qqchan_loop - qqchannel_bot = QQChan() - qqchan_loop = asyncio.new_event_loop() - _global_object.platform_qqchan = qqchannel_bot - thread_inst = threading.Thread(target=run_qqchan_bot, args=(cfg, qqchan_loop, qqchannel_bot), daemon=False) - thread_inst.start() - # thread.join() if thread_inst == None: input("[System-Error] 没有启用/成功启用任何机器人,程序退出") @@ -606,7 +606,7 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak if session_id in gocq_bot.waiting and gocq_bot.waiting[session_id] == '': gocq_bot.waiting[session_id] = qq_msg return - hit, command_result = llm_command_instance[chosen_provider].check_command( + hit, command_result = await llm_command_instance[chosen_provider].check_command( qq_msg, session_id, role, @@ -752,7 +752,6 @@ class botClient(botpy.Client): # 收到频道消息 async def on_at_message_create(self, message: Message): gu.log(str(message), gu.LEVEL_DEBUG, max_len=9999) - # 转换层 nakuru_guild_message = qqchannel_bot.gocq_compatible_receive(message) gu.log(f"转换后: {str(nakuru_guild_message)}", gu.LEVEL_DEBUG, max_len=9999) diff --git a/model/command/adapter/nonebot/command_arg.py b/model/command/adapter/nonebot/command_arg.py new file mode 100644 index 000000000..3f6e8a305 --- /dev/null +++ b/model/command/adapter/nonebot/command_arg.py @@ -0,0 +1,2 @@ +class CommandArg: + pass \ No newline at end of file diff --git a/model/command/adapter/nonebot/common.py b/model/command/adapter/nonebot/common.py new file mode 100644 index 000000000..ac7532768 --- /dev/null +++ b/model/command/adapter/nonebot/common.py @@ -0,0 +1,32 @@ +import sys +from types import ModuleType +import asyncio +from pyppeteer import launch + + +async def template_to_pic(template_path, template_name, templates, pages, wait, type, quality, device_scale_factor): + browser = await launch() + page = await browser.newPage() + await page.setViewport(pages["viewport"]) + await page.goto(pages["base_url"]) + await asyncio.sleep(wait) + await page.evaluate('''(templates) => { + // 在页面中执行 JavaScript 代码,将数据注入到模板中 + // 这里的示例代码仅供参考,具体需要根据实际情况修改 + document.getElementById('css').innerText = templates.css; + document.getElementById('data').innerText = JSON.stringify(templates.data); + document.getElementById('detail').innerText = templates.detail; + }''', templates) + screenshot = await page.screenshot({ + 'type': type, + 'quality': quality, + 'deviceScaleFactor': device_scale_factor + }) + await browser.close() + return screenshot + +def require(module_str: str): + module = ModuleType(module_str) + sys.modules[module_str] = module + if module_str == 'nonebot_plugin_htmlrender': + module.template_to_pic = template_to_pic diff --git a/model/command/adapter/nonebot/driver.py b/model/command/adapter/nonebot/driver.py new file mode 100644 index 000000000..a184fbcaf --- /dev/null +++ b/model/command/adapter/nonebot/driver.py @@ -0,0 +1,11 @@ +class Driver: + def __init__(self) -> None: + self.config = {} + + def on_startup(self, func): + pass + def on_bot_connect(self, func): + pass + +def get_driver(): + return Driver() \ No newline at end of file diff --git a/model/command/adapter/onebot/bot.py b/model/command/adapter/onebot/bot.py new file mode 100644 index 000000000..19be4bcb3 --- /dev/null +++ b/model/command/adapter/onebot/bot.py @@ -0,0 +1,2 @@ +class Bot: + pass \ No newline at end of file diff --git a/model/command/adapter/onebot/message.py b/model/command/adapter/onebot/message.py new file mode 100644 index 000000000..574b0ce16 --- /dev/null +++ b/model/command/adapter/onebot/message.py @@ -0,0 +1,2 @@ +class Message: + pass \ No newline at end of file diff --git a/model/command/adapter/onebot/message_event.py b/model/command/adapter/onebot/message_event.py new file mode 100644 index 000000000..11712e075 --- /dev/null +++ b/model/command/adapter/onebot/message_event.py @@ -0,0 +1,2 @@ +class MessageEvent: + pass \ No newline at end of file diff --git a/model/command/adapter/onebot/message_segment.py b/model/command/adapter/onebot/message_segment.py new file mode 100644 index 000000000..2905a1757 --- /dev/null +++ b/model/command/adapter/onebot/message_segment.py @@ -0,0 +1,2 @@ +class MessageSegment: + pass \ No newline at end of file diff --git a/model/command/adapter/protocol_adapter.py b/model/command/adapter/protocol_adapter.py new file mode 100644 index 000000000..1f0f6030a --- /dev/null +++ b/model/command/adapter/protocol_adapter.py @@ -0,0 +1,137 @@ +import sys +from types import ModuleType +import asyncio +from pyppeteer import launch + +from model.platform.qqchan import QQChan + +from .nonebot.driver import Driver, get_driver +from .onebot.message import Message +from .onebot.message_event import MessageEvent +from .onebot.message_segment import MessageSegment +from .nonebot.command_arg import CommandArg +from .onebot.bot import Bot + +from nakuru import ( + GuildMessage, + GroupMessage, + FriendMessage +) + +from typing import Union + +NONEBOT = "nonebot" + +class UnifiedBotCompatibleLayer(): + def __init__(self, platform_qq_sdk: QQChan) -> None: + # 初始化兼容层 + self.plugins: dict[str, CommandOper] = {} + self.platform_qq_sdk = platform_qq_sdk + self._nonebot() + self.load_plugins() + + async def check_commands(self, message: str, message_obj: Union[GroupMessage, FriendMessage, GuildMessage]): + for k in self.plugins: + if message.startswith(k): + if self.plugins[k].framework_name == NONEBOT: + await self._nonebot_plugins_oper(message, message_obj, k) + + async def _nonebot_plugins_oper(self, message: str, message_obj: Union[GroupMessage, FriendMessage, GuildMessage], plugin_name: str = None): + # bad implementation + # 高并发场景下,下面的代码是不安全的 + while self.plugins[plugin_name].message_obj is not None: + await asyncio.sleep(1) + self.plugins[plugin_name].message_obj = message_obj + bot, event, arg = self._nonebot_adapter(message_obj) + await self.plugins[plugin_name].exec(bot, event, arg) # wrapper + + def load_plugins(self): + import nonebot_plugin_gspanel.nonebot_plugin_gspanel + + def _nonebot(self): + # 模拟 nonebot 模块 + nonebot_module = ModuleType('nonebot') + sys.modules['nonebot'] = nonebot_module + + nonebot_log_module = ModuleType('nonebot.log') + sys.modules['nonebot.log'] = nonebot_log_module + + nonebot_adapter_module = ModuleType('nonebot.adapters') + sys.modules['nonebot.adapters'] = nonebot_adapter_module + + nonebot_params_module = ModuleType('nonebot.params') + sys.modules['nonebot.params'] = nonebot_params_module + + nonebot_drivers_module = ModuleType('nonebot.drivers') + sys.modules['nonebot.drivers'] = nonebot_drivers_module + + nonebot_plugin_module = ModuleType('nonebot.plugin') + sys.modules['nonebot.plugin'] = nonebot_plugin_module + + nonebot_adapter_onebot_v11_module = ModuleType('nonebot.adapters.onebot.v11') + sys.modules['nonebot.adapters.onebot.v11'] = nonebot_adapter_onebot_v11_module + + nonebot_adapter_onebot_v11_event_module = ModuleType('nonebot.adapters.onebot.v11.event') + sys.modules['nonebot.adapters.onebot.v11.event'] = nonebot_adapter_onebot_v11_event_module + + nonebot_adapter_onebot_v11_message_module = ModuleType('nonebot.adapters.onebot.v11.message') + sys.modules['nonebot.adapters.onebot.v11.message'] = nonebot_adapter_onebot_v11_message_module + + nonebot_log_module.logger = lambda: None + nonebot_adapter_module.Message = Message + nonebot_params_module.CommandArg = CommandArg + on_command = wrap_on_command(self) + nonebot_plugin_module.on_command = on_command + nonebot_adapter_onebot_v11_module.Bot = Bot + nonebot_adapter_onebot_v11_event_module.MessageEvent = MessageEvent + nonebot_adapter_onebot_v11_message_module.MessageSegment = MessageSegment + nonebot_module.get_driver = get_driver + nonebot_module.require = require + nonebot_drivers_module.Driver = Driver + + def _nonebot_adapter(self, message_obj): + bot = Bot() + event = MessageEvent() + arg = CommandArg() + # tododssss + return bot, event, arg + + +class BaseBot(): + def __init__(self, framework_name) -> None: + self.framework_name = framework_name + +class CommandOper(BaseBot): + ''' + CommandOper for NoneBot + ''' + def __init__(self, name, aliases=None, priority=1, block=False, _ubcl: UnifiedBotCompatibleLayer = None) -> None: + super().__init__("nonebot") + self.name = name + self.aliases = aliases + self.priority = priority + self.block = block + self.exec = None + self._ubcl = _ubcl + self.message_obj: Union[GroupMessage, FriendMessage, GuildMessage] = None + _ubcl.plugins[name] = self + + def handle(self): + def decorator(func): + async def wrapper(bot: Bot, event: MessageEvent, arg: Message = CommandArg(), *args, **kwargs): + # 你可以在这里添加自定义的处理逻辑 + print(f"Command {self.name} is executed.") + await func(bot, event, arg, *args, **kwargs) + self.exec = wrapper + return wrapper + return decorator + + async def finish(self, msg, at_sender = True): + if self.message_obj is not None: + self._ubcl.platform_qq_sdk.send(self.message_obj, msg) + self.message_obj = None + +def wrap_on_command(_ubcl: UnifiedBotCompatibleLayer): + def on_command(name, aliases=None, priority=1, block=False): + return CommandOper(name, aliases, priority, block, _ubcl = _ubcl) + return on_command diff --git a/model/command/command.py b/model/command/command.py index b23c4ec05..b25f774b1 100644 --- a/model/command/command.py +++ b/model/command/command.py @@ -26,21 +26,29 @@ from PIL import Image as PILImage from cores.qqbot.global_object import GlobalObject, AstrMessageEvent from pip._internal import main as pipmain +from .adapter.protocol_adapter import UnifiedBotCompatibleLayer +import asyncio + PLATFORM_QQCHAN = 'qqchan' PLATFORM_GOCQ = 'gocq' # 指令功能的基类,通用的(不区分语言模型)的指令就在这实现 class Command: - def __init__(self, provider: Provider, global_object: GlobalObject = None): + def __init__(self, provider: Provider, global_object: GlobalObject = None, unified_bot_compatible_layer: UnifiedBotCompatibleLayer = None): self.provider = provider self.global_object = global_object + self.unified_bot_compatible_layer = unified_bot_compatible_layer - def check_command(self, + + async def check_command(self, message, session_id: str, role, platform, message_obj): + # UBCL + await self.unified_bot_compatible_layer.check_commands(message, message_obj) + # 插件 cached_plugins = self.global_object.cached_plugins ame = AstrMessageEvent( @@ -70,10 +78,8 @@ class Command: if self.command_start_with(message, "nick"): return True, self.set_nick(message, platform, role) - if self.command_start_with(message, "plugin"): return True, self.plugin_oper(message, role, cached_plugins, platform) - if self.command_start_with(message, "myid") or self.command_start_with(message, "!myid"): return True, self.get_my_id(message_obj) if self.command_start_with(message, "nconf") or self.command_start_with(message, "newconf"): diff --git a/model/command/openai_official.py b/model/command/openai_official.py index aa5247e6f..5974f12e1 100644 --- a/model/command/openai_official.py +++ b/model/command/openai_official.py @@ -5,6 +5,7 @@ from cores.qqbot.personality import personalities from model.platform.qq import QQ from util import general_utils as gu from cores.qqbot.global_object import GlobalObject +from .adapter.protocol_adapter import UnifiedBotCompatibleLayer class CommandOpenAIOfficial(Command): def __init__(self, provider: ProviderOpenAIOfficial, global_object: GlobalObject): @@ -12,16 +13,17 @@ class CommandOpenAIOfficial(Command): self.cached_plugins = {} self.global_object = global_object self.personality_str = "" - super().__init__(provider, global_object) + self.unified_bot_compatible_layer = UnifiedBotCompatibleLayer(self.global_object.platform_qqchan) + super().__init__(provider, global_object, self.unified_bot_compatible_layer) - def check_command(self, + async def check_command(self, message: str, session_id: str, role: str, platform: str, message_obj): self.platform = platform - hit, res = super().check_command( + hit, res = await super().check_command( message, session_id, role, diff --git a/model/command/rev_chatgpt.py b/model/command/rev_chatgpt.py index 6780930c2..3f8399e8b 100644 --- a/model/command/rev_chatgpt.py +++ b/model/command/rev_chatgpt.py @@ -12,14 +12,14 @@ class CommandRevChatGPT(Command): self.personality_str = "" super().__init__(provider, global_object) - def check_command(self, + async def check_command(self, message: str, session_id: str, role: str, platform: str, message_obj): self.platform = platform - hit, res = super().check_command( + hit, res = await super().check_command( message, session_id, role, diff --git a/model/platform/qqchan.py b/model/platform/qqchan.py index 217598ae4..ad3974f82 100644 --- a/model/platform/qqchan.py +++ b/model/platform/qqchan.py @@ -188,4 +188,9 @@ class QQChan(): _n = NakuruGuildMessage() _n.channel_id = channel_id self.send_qq_msg(_n, message_chain) - \ No newline at end of file + + def send(self, message: NakuruGuildMessage, res: list): + ''' + 同 send_qq_msg。回复频道消息 + ''' + self.send_qq_msg(message, res) \ No newline at end of file From f4222e0923406d727918ea5ba227fca1ff851808 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 21 Nov 2023 22:37:35 +0800 Subject: [PATCH 2/5] bugfixes --- model/command/adapter/protocol_adapter.py | 1 + model/platform/qqchan.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/model/command/adapter/protocol_adapter.py b/model/command/adapter/protocol_adapter.py index 1f0f6030a..3e182d4d7 100644 --- a/model/command/adapter/protocol_adapter.py +++ b/model/command/adapter/protocol_adapter.py @@ -11,6 +11,7 @@ from .onebot.message_event import MessageEvent from .onebot.message_segment import MessageSegment from .nonebot.command_arg import CommandArg from .onebot.bot import Bot +from .nonebot.common import require from nakuru import ( GuildMessage, diff --git a/model/platform/qqchan.py b/model/platform/qqchan.py index ad3974f82..a9e46ad8a 100644 --- a/model/platform/qqchan.py +++ b/model/platform/qqchan.py @@ -76,7 +76,7 @@ class QQChan(): ngm.sub_type = "normal" ngm.message_id = message.id - ngm.guild_id = int(message.channel_id) + ngm.guild_id = int(message.guild_id) ngm.channel_id = int(message.channel_id) ngm.user_id = int(message.author.id) msg = [] From b5cb5eb9699b7ef1e08f82af19e35bd5002e5cf6 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 8 Sep 2024 19:41:00 +0800 Subject: [PATCH 3/5] feat: customized tool-use --- astrbot/message/handler.py | 77 ++++++++++++++++---- model/command/internal_handler.py | 21 ++++++ type/types.py | 13 +++- util/agent/func_call.py | 20 ++++-- util/agent/web_searcher.py | 112 ++++-------------------------- 5 files changed, 129 insertions(+), 114 deletions(-) diff --git a/astrbot/message/handler.py b/astrbot/message/handler.py index c3d26305e..89383a463 100644 --- a/astrbot/message/handler.py +++ b/astrbot/message/handler.py @@ -1,4 +1,4 @@ -import time +import time, json import re, os import asyncio import traceback @@ -16,6 +16,8 @@ from logging import Logger from nakuru.entities.components import Image from util.agent.func_call import FuncCall import util.agent.web_searcher as web_searcher +from openai._exceptions import * +from openai.types.chat.chat_completion_message_tool_call import Function logger: Logger = LogManager.GetLogger(log_name='astrbot') @@ -186,31 +188,82 @@ class MessageHandler(): image_url = comp.url if comp.url else comp.file break - web_search = self.context.web_search - if not web_search and msg_plain.startswith("ws"): - # leverage web search feature - web_search = True - msg_plain = msg_plain.removeprefix("ws").strip() - + # web_search = self.context.web_search + # if not web_search and msg_plain.startswith("ws"): + # # leverage web search feature + # web_search = True + # msg_plain = msg_plain.removeprefix("ws").strip() try: - if web_search: - llm_result = await web_searcher.web_search(msg_plain, provider, message.session_id, official_fc=True) + if not self.llm_tools.empty(): + # tools-use + tool_use_flag = True + llm_result = await provider.text_chat( + prompt=msg_plain, + session_id=message.session_id, + tools=self.llm_tools.get_func() + ) + + if isinstance(llm_result, Function): + logger.debug(f"function-calling: {llm_result}") + func_obj = None + for i in self.llm_tools.func_list: + if i["name"] == llm_result.name: + func_obj = i["func_obj"] + break + if not func_obj: + return MessageResult("AstrBot Function-calling 异常:未找到请求的函数调用。") + try: + args = json.loads(llm_result.arguments) + function_invoked_ret = await func_obj(**args) + has_func = True + except BaseException as e: + traceback.print_exc() + return MessageResult("AstrBot Function-calling 异常:" + str(e)) + else: + return MessageResult(llm_result) + else: + # normal chat + tool_use_flag = False llm_result = await provider.text_chat( prompt=msg_plain, session_id=message.session_id, image_url=image_url ) + except BadRequestError as e: + if tool_use_flag: + # seems like the model don't support function-calling + logger.error(f"error: {e}. Using local function-calling implementation") + + try: + # use local function-calling implementation + args = { + 'question': llm_result, + 'func_definition': self.llm_tools.func_dump(), + } + _, has_func = await self.llm_tools.func_call(**args) + + if not has_func: + # normal chat + llm_result = await provider.text_chat( + prompt=msg_plain, + session_id=message.session_id, + image_url=image_url + ) + except BaseException as e: + logger.error(traceback.format_exc()) + return CommandResult("AstrBot Function-calling 异常:" + str(e)) + except BaseException as e: logger.error(traceback.format_exc()) logger.error(f"LLM 调用失败。") return MessageResult("AstrBot 请求 LLM 资源失败:" + str(e)) - - # concatenate the reply prefix + + # concatenate reply prefix if self.reply_prefix: llm_result = self.reply_prefix + llm_result - # mask the unsafe content + # mask unsafe content llm_result = self.content_safety_helper.filter_content(llm_result) check = self.content_safety_helper.baidu_check(llm_result) if not check: diff --git a/model/command/internal_handler.py b/model/command/internal_handler.py index 422c226f8..9d16f8dca 100644 --- a/model/command/internal_handler.py +++ b/model/command/internal_handler.py @@ -9,6 +9,7 @@ from type.config import VERSION from SparkleLogging.utils.core import LogManager from logging import Logger from nakuru.entities.components import Image +from util.agent.web_searcher import search_from_bing, fetch_website_content logger: Logger = LogManager.GetLogger(log_name='astrbot') @@ -212,6 +213,23 @@ class InternalCommandHandler: ) elif l[1] == 'on': context.web_search = True + context.register_llm_tool("web_search", [{ + "type": "string", + "name": "keyword", + "description": "搜索关键词" + }], + "通过搜索引擎搜索。如果问题需要获取近期、实时的消息,在网页上搜索(如天气、新闻或任何需要通过网页获取信息的问题),则调用此函数;如果没有,不要调用此函数。", + search_from_bing + ) + context.register_llm_tool("fetch_website_content", [{ + "type": "string", + "name": "url", + "description": "要获取内容的网页链接" + }], + "获取网页的内容。如果问题带有合法的网页链接并且用户有需求了解网页内容(例如: `帮我总结一下 https://github.com 的内容`), 就调用此函数。如果没有,不要调用此函数。", + fetch_website_content + ) + return CommandResult( hit=True, success=True, @@ -219,6 +237,9 @@ class InternalCommandHandler: ) elif l[1] == 'off': context.web_search = False + context.unregister_llm_tool("web_search") + context.unregister_llm_tool("fetch_website_content") + return CommandResult( hit=True, success=True, diff --git a/type/types.py b/type/types.py index fd4d07d9c..542ab4fbf 100644 --- a/type/types.py +++ b/type/types.py @@ -110,6 +110,12 @@ class Context: ''' self.message_handler.llm_tools.add_func(tool_name, params, desc, func) + def unregister_llm_tool(self, tool_name: str): + ''' + 删除一个函数调用工具。 + ''' + self.message_handler.llm_tools.remove_func(tool_name) + def find_platform(self, platform_name: str) -> RegisteredPlatform: for platform in self.platforms: if platform_name == platform.platform_name: @@ -131,4 +137,9 @@ class Context: platform_name, message_type, id = l platform = self.find_platform(platform_name) await platform.platform_instance.send_msg_new(MessageType(message_type), id, message) - \ No newline at end of file + + def get_current_llm_provider(self) -> Provider: + ''' + 获取当前的 LLM Provider。 + ''' + return self.message_handler.provider \ No newline at end of file diff --git a/util/agent/func_call.py b/util/agent/func_call.py index e805f5bfc..5283ee4d6 100644 --- a/util/agent/func_call.py +++ b/util/agent/func_call.py @@ -23,6 +23,9 @@ class FuncCall(): def __init__(self, provider: Provider) -> None: self.func_list = [] self.provider = provider + + def empty(self) -> bool: + return len(self.func_list) == 0 def add_func(self, name: str, func_args: list, desc: str, func_obj: callable) -> None: ''' @@ -34,7 +37,7 @@ class FuncCall(): @param func_obj: 处理函数 ''' params = { - "type": "object", # hardcore here + "type": "object", # hard-coded here "properties": {} } for param in func_args: @@ -42,14 +45,23 @@ class FuncCall(): "type": param['type'], "description": param['description'] } - self._func = { + _func = { "name": name, "parameters": params, "description": desc, "func_obj": func_obj, } - self.func_list.append(self._func) - + self.func_list.append(_func) + + def remove_func(self, name: str) -> None: + ''' + 删除一个函数调用工具。 + ''' + for i, f in enumerate(self.func_list): + if f["name"] == name: + self.func_list.pop(i) + break + def func_dump(self) -> str: _l = [] for f in self.func_list: diff --git a/util/agent/web_searcher.py b/util/agent/web_searcher.py index c8634c2b7..e519ca697 100644 --- a/util/agent/web_searcher.py +++ b/util/agent/web_searcher.py @@ -16,6 +16,8 @@ from util.websearch.google import Google from model.provider.provider import Provider from SparkleLogging.utils.core import LogManager from logging import Logger +from type.types import Context +from type.message_event import AstrMessageEvent logger: Logger = LogManager.GetLogger(log_name='astrbot') @@ -31,24 +33,7 @@ def tidy_text(text: str) -> str: ''' return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ") -# def special_fetch_zhihu(link: str) -> str: -# ''' -# function-calling 函数, 用于获取知乎文章的内容 -# ''' -# response = requests.get(link, headers=HEADERS) -# response.encoding = "utf-8" -# soup = BeautifulSoup(response.text, "html.parser") - -# if "zhuanlan.zhihu.com" in link: -# r = soup.find(class_="Post-RichTextContainer") -# else: -# r = soup.find(class_="List-item").find(class_="RichContent-inner") -# if r is None: -# print("debug: zhihu none") -# raise Exception("zhihu none") -# return tidy_text(r.text) - -async def search_from_bing(keyword: str) -> str: +async def search_from_bing(context: Context, ame: AstrMessageEvent, keyword: str) -> str: ''' tools, 从 bing 搜索引擎搜索 ''' @@ -84,10 +69,11 @@ async def search_from_bing(keyword: str) -> str: site_result = site_result[:600] + "..." if len(site_result) > 600 else site_result ret += f"{idx}. {i.title} \n{i.snippet}\n{site_result}\n\n" idx += 1 - return ret + + return await summarize(context, ame, ret) -async def fetch_website_content(url): +async def fetch_website_content(context: Context, ame: AstrMessageEvent, url: str): header = HEADERS header.update({'User-Agent': random.choice(USER_AGENTS)}) async with aiohttp.ClientSession() as session: @@ -97,81 +83,13 @@ async def fetch_website_content(url): ret = doc.summary(html_partial=True) soup = BeautifulSoup(ret, 'html.parser') ret = tidy_text(soup.get_text()) - return ret - - -async def web_search(prompt: str, provider: Provider, session_id: str, official_fc: bool=False): - ''' - @param official_fc: 使用官方 function-calling - ''' - new_func_call = FuncCall(provider) - - new_func_call.add_func("web_search", [{ - "type": "string", - "name": "keyword", - "description": "搜索关键词" - }], - "通过搜索引擎搜索。如果问题需要获取近期、实时的消息,在网页上搜索(如天气、新闻或任何需要通过网页获取信息的问题),则调用此函数;如果没有,不要调用此函数。", - search_from_bing - ) - new_func_call.add_func("fetch_website_content", [{ - "type": "string", - "name": "url", - "description": "要获取内容的网页链接" - }], - "获取网页的内容。如果问题带有合法的网页链接并且用户有需求了解网页内容(例如: `帮我总结一下 https://github.com 的内容`), 就调用此函数。如果没有,不要调用此函数。", - fetch_website_content - ) + return await summarize(context, ame, ret) - has_func = False - function_invoked_ret = "" - if official_fc: - # we use official function-calling - try: - result = await provider.text_chat(prompt=prompt, session_id=session_id, tools=new_func_call.get_func()) - except BadRequestError as e: - # seems dont support function-calling - logger.error(f"error: {e}. Try to use local function-calling implementation") - return await web_search(prompt, provider, session_id, official_fc=False) - if isinstance(result, Function): - logger.debug(f"function-calling: {result}") - func_obj = None - for i in new_func_call.func_list: - if i["name"] == result.name: - func_obj = i["func_obj"] - break - if not func_obj: - return await provider.text_chat(prompt=prompt, session_id=session_id, ) + "\n(网页搜索失败, 此为默认回复)" - try: - args = json.loads(result.arguments) - function_invoked_ret = await func_obj(**args) - has_func = True - except BaseException as e: - traceback.print_exc() - return await provider.text_chat(prompt=prompt, session_id=session_id, ) + "\n(网页搜索失败, 此为默认回复)" - else: - return result - else: - # we use our own function-calling - try: - args = { - 'question': prompt, - 'func_definition': new_func_call.func_dump(), - } - function_invoked_ret, has_func = await new_func_call.func_call(**args) - - if not has_func: - return await provider.text_chat(prompt, session_id) - - except BaseException as e: - logger.error(traceback.format_exc()) - return await provider.text_chat(prompt, session_id) + "(网页搜索失败, 此为默认回复)" - - if has_func: - await provider.forget(session_id=session_id) - summary_prompt = f""" +async def summarize(context: Context, ame: AstrMessageEvent, text: str): + + summary_prompt = f""" 你是一个专业且高效的助手,你的任务是 -1. 根据下面的相关材料对用户的问题 `{prompt}` 进行总结; +1. 根据下面的相关材料对用户的问题 `{ame.message_str}` 进行总结; 2. 简单地发表你对这个问题的看法。 # 例子 @@ -183,7 +101,7 @@ async def web_search(prompt: str, provider: Provider, session_id: str, official_ 2. 请**直接输出总结**,不要输出多余的内容和提示语。 # 相关材料 -{function_invoked_ret}""" - ret = await provider.text_chat(prompt=summary_prompt, session_id=session_id) - return ret - return function_invoked_ret +{text}""" + + provider = context.get_current_llm_provider() + return await provider.text_chat(prompt=summary_prompt, session_id=ame.session_id) \ No newline at end of file From 98863ab90160822546c4e6f65b6030a6081d2160 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 8 Sep 2024 08:16:36 -0400 Subject: [PATCH 4/5] feat: customized tool-use --- astrbot/message/handler.py | 4 +++- util/agent/func_call.py | 1 - util/agent/web_searcher.py | 2 -- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/astrbot/message/handler.py b/astrbot/message/handler.py index 89383a463..a20726435 100644 --- a/astrbot/message/handler.py +++ b/astrbot/message/handler.py @@ -214,7 +214,9 @@ class MessageHandler(): return MessageResult("AstrBot Function-calling 异常:未找到请求的函数调用。") try: args = json.loads(llm_result.arguments) - function_invoked_ret = await func_obj(**args) + args['ame'] = message + args['context'] = self.context + llm_result = await func_obj(**args) has_func = True except BaseException as e: traceback.print_exc() diff --git a/util/agent/func_call.py b/util/agent/func_call.py index 5283ee4d6..830496cab 100644 --- a/util/agent/func_call.py +++ b/util/agent/func_call.py @@ -1,6 +1,5 @@ from model.provider.provider import Provider import json -import time import textwrap class FuncCallJsonFormatError(Exception): diff --git a/util/agent/web_searcher.py b/util/agent/web_searcher.py index e519ca697..d9b384314 100644 --- a/util/agent/web_searcher.py +++ b/util/agent/web_searcher.py @@ -1,6 +1,4 @@ -import traceback import random -import json import aiohttp import os From 5b72ebaad50a1f344706cf4842f663143915919c Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 8 Sep 2024 08:23:43 -0400 Subject: [PATCH 5/5] delete: remove deprecated files --- model/command/adapter/nonebot/command_arg.py | 2 - model/command/adapter/nonebot/common.py | 32 ---- model/command/adapter/nonebot/driver.py | 11 -- model/command/adapter/onebot/bot.py | 2 - model/command/adapter/onebot/message.py | 2 - model/command/adapter/onebot/message_event.py | 2 - .../command/adapter/onebot/message_segment.py | 2 - model/command/adapter/protocol_adapter.py | 138 ------------------ 8 files changed, 191 deletions(-) delete mode 100644 model/command/adapter/nonebot/command_arg.py delete mode 100644 model/command/adapter/nonebot/common.py delete mode 100644 model/command/adapter/nonebot/driver.py delete mode 100644 model/command/adapter/onebot/bot.py delete mode 100644 model/command/adapter/onebot/message.py delete mode 100644 model/command/adapter/onebot/message_event.py delete mode 100644 model/command/adapter/onebot/message_segment.py delete mode 100644 model/command/adapter/protocol_adapter.py diff --git a/model/command/adapter/nonebot/command_arg.py b/model/command/adapter/nonebot/command_arg.py deleted file mode 100644 index 3f6e8a305..000000000 --- a/model/command/adapter/nonebot/command_arg.py +++ /dev/null @@ -1,2 +0,0 @@ -class CommandArg: - pass \ No newline at end of file diff --git a/model/command/adapter/nonebot/common.py b/model/command/adapter/nonebot/common.py deleted file mode 100644 index ac7532768..000000000 --- a/model/command/adapter/nonebot/common.py +++ /dev/null @@ -1,32 +0,0 @@ -import sys -from types import ModuleType -import asyncio -from pyppeteer import launch - - -async def template_to_pic(template_path, template_name, templates, pages, wait, type, quality, device_scale_factor): - browser = await launch() - page = await browser.newPage() - await page.setViewport(pages["viewport"]) - await page.goto(pages["base_url"]) - await asyncio.sleep(wait) - await page.evaluate('''(templates) => { - // 在页面中执行 JavaScript 代码,将数据注入到模板中 - // 这里的示例代码仅供参考,具体需要根据实际情况修改 - document.getElementById('css').innerText = templates.css; - document.getElementById('data').innerText = JSON.stringify(templates.data); - document.getElementById('detail').innerText = templates.detail; - }''', templates) - screenshot = await page.screenshot({ - 'type': type, - 'quality': quality, - 'deviceScaleFactor': device_scale_factor - }) - await browser.close() - return screenshot - -def require(module_str: str): - module = ModuleType(module_str) - sys.modules[module_str] = module - if module_str == 'nonebot_plugin_htmlrender': - module.template_to_pic = template_to_pic diff --git a/model/command/adapter/nonebot/driver.py b/model/command/adapter/nonebot/driver.py deleted file mode 100644 index a184fbcaf..000000000 --- a/model/command/adapter/nonebot/driver.py +++ /dev/null @@ -1,11 +0,0 @@ -class Driver: - def __init__(self) -> None: - self.config = {} - - def on_startup(self, func): - pass - def on_bot_connect(self, func): - pass - -def get_driver(): - return Driver() \ No newline at end of file diff --git a/model/command/adapter/onebot/bot.py b/model/command/adapter/onebot/bot.py deleted file mode 100644 index 19be4bcb3..000000000 --- a/model/command/adapter/onebot/bot.py +++ /dev/null @@ -1,2 +0,0 @@ -class Bot: - pass \ No newline at end of file diff --git a/model/command/adapter/onebot/message.py b/model/command/adapter/onebot/message.py deleted file mode 100644 index 574b0ce16..000000000 --- a/model/command/adapter/onebot/message.py +++ /dev/null @@ -1,2 +0,0 @@ -class Message: - pass \ No newline at end of file diff --git a/model/command/adapter/onebot/message_event.py b/model/command/adapter/onebot/message_event.py deleted file mode 100644 index 11712e075..000000000 --- a/model/command/adapter/onebot/message_event.py +++ /dev/null @@ -1,2 +0,0 @@ -class MessageEvent: - pass \ No newline at end of file diff --git a/model/command/adapter/onebot/message_segment.py b/model/command/adapter/onebot/message_segment.py deleted file mode 100644 index 2905a1757..000000000 --- a/model/command/adapter/onebot/message_segment.py +++ /dev/null @@ -1,2 +0,0 @@ -class MessageSegment: - pass \ No newline at end of file diff --git a/model/command/adapter/protocol_adapter.py b/model/command/adapter/protocol_adapter.py deleted file mode 100644 index 3e182d4d7..000000000 --- a/model/command/adapter/protocol_adapter.py +++ /dev/null @@ -1,138 +0,0 @@ -import sys -from types import ModuleType -import asyncio -from pyppeteer import launch - -from model.platform.qqchan import QQChan - -from .nonebot.driver import Driver, get_driver -from .onebot.message import Message -from .onebot.message_event import MessageEvent -from .onebot.message_segment import MessageSegment -from .nonebot.command_arg import CommandArg -from .onebot.bot import Bot -from .nonebot.common import require - -from nakuru import ( - GuildMessage, - GroupMessage, - FriendMessage -) - -from typing import Union - -NONEBOT = "nonebot" - -class UnifiedBotCompatibleLayer(): - def __init__(self, platform_qq_sdk: QQChan) -> None: - # 初始化兼容层 - self.plugins: dict[str, CommandOper] = {} - self.platform_qq_sdk = platform_qq_sdk - self._nonebot() - self.load_plugins() - - async def check_commands(self, message: str, message_obj: Union[GroupMessage, FriendMessage, GuildMessage]): - for k in self.plugins: - if message.startswith(k): - if self.plugins[k].framework_name == NONEBOT: - await self._nonebot_plugins_oper(message, message_obj, k) - - async def _nonebot_plugins_oper(self, message: str, message_obj: Union[GroupMessage, FriendMessage, GuildMessage], plugin_name: str = None): - # bad implementation - # 高并发场景下,下面的代码是不安全的 - while self.plugins[plugin_name].message_obj is not None: - await asyncio.sleep(1) - self.plugins[plugin_name].message_obj = message_obj - bot, event, arg = self._nonebot_adapter(message_obj) - await self.plugins[plugin_name].exec(bot, event, arg) # wrapper - - def load_plugins(self): - import nonebot_plugin_gspanel.nonebot_plugin_gspanel - - def _nonebot(self): - # 模拟 nonebot 模块 - nonebot_module = ModuleType('nonebot') - sys.modules['nonebot'] = nonebot_module - - nonebot_log_module = ModuleType('nonebot.log') - sys.modules['nonebot.log'] = nonebot_log_module - - nonebot_adapter_module = ModuleType('nonebot.adapters') - sys.modules['nonebot.adapters'] = nonebot_adapter_module - - nonebot_params_module = ModuleType('nonebot.params') - sys.modules['nonebot.params'] = nonebot_params_module - - nonebot_drivers_module = ModuleType('nonebot.drivers') - sys.modules['nonebot.drivers'] = nonebot_drivers_module - - nonebot_plugin_module = ModuleType('nonebot.plugin') - sys.modules['nonebot.plugin'] = nonebot_plugin_module - - nonebot_adapter_onebot_v11_module = ModuleType('nonebot.adapters.onebot.v11') - sys.modules['nonebot.adapters.onebot.v11'] = nonebot_adapter_onebot_v11_module - - nonebot_adapter_onebot_v11_event_module = ModuleType('nonebot.adapters.onebot.v11.event') - sys.modules['nonebot.adapters.onebot.v11.event'] = nonebot_adapter_onebot_v11_event_module - - nonebot_adapter_onebot_v11_message_module = ModuleType('nonebot.adapters.onebot.v11.message') - sys.modules['nonebot.adapters.onebot.v11.message'] = nonebot_adapter_onebot_v11_message_module - - nonebot_log_module.logger = lambda: None - nonebot_adapter_module.Message = Message - nonebot_params_module.CommandArg = CommandArg - on_command = wrap_on_command(self) - nonebot_plugin_module.on_command = on_command - nonebot_adapter_onebot_v11_module.Bot = Bot - nonebot_adapter_onebot_v11_event_module.MessageEvent = MessageEvent - nonebot_adapter_onebot_v11_message_module.MessageSegment = MessageSegment - nonebot_module.get_driver = get_driver - nonebot_module.require = require - nonebot_drivers_module.Driver = Driver - - def _nonebot_adapter(self, message_obj): - bot = Bot() - event = MessageEvent() - arg = CommandArg() - # tododssss - return bot, event, arg - - -class BaseBot(): - def __init__(self, framework_name) -> None: - self.framework_name = framework_name - -class CommandOper(BaseBot): - ''' - CommandOper for NoneBot - ''' - def __init__(self, name, aliases=None, priority=1, block=False, _ubcl: UnifiedBotCompatibleLayer = None) -> None: - super().__init__("nonebot") - self.name = name - self.aliases = aliases - self.priority = priority - self.block = block - self.exec = None - self._ubcl = _ubcl - self.message_obj: Union[GroupMessage, FriendMessage, GuildMessage] = None - _ubcl.plugins[name] = self - - def handle(self): - def decorator(func): - async def wrapper(bot: Bot, event: MessageEvent, arg: Message = CommandArg(), *args, **kwargs): - # 你可以在这里添加自定义的处理逻辑 - print(f"Command {self.name} is executed.") - await func(bot, event, arg, *args, **kwargs) - self.exec = wrapper - return wrapper - return decorator - - async def finish(self, msg, at_sender = True): - if self.message_obj is not None: - self._ubcl.platform_qq_sdk.send(self.message_obj, msg) - self.message_obj = None - -def wrap_on_command(_ubcl: UnifiedBotCompatibleLayer): - def on_command(name, aliases=None, priority=1, block=False): - return CommandOper(name, aliases, priority, block, _ubcl = _ubcl) - return on_command