From 58a6c0c7410c53435666d5825aabb686494fca47 Mon Sep 17 00:00:00 2001 From: RegimenArsenic Date: Fri, 7 Apr 2023 03:40:27 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD,=E6=96=B0=E5=A2=9E=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E6=8F=92=E4=BB=B6=E5=92=8C=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 3 + bridge/bridge.py | 27 ++++++- channel/http/http_channel.py | 46 +++++++----- channel/wechat/wechat_channel.py | 63 ++++++++-------- common/functions.py | 31 +++++++- plugins/README.md | 122 +++++++++++++++++++++++++++++++ plugins/__init__.py | 10 +++ plugins/createimg/createimg.py | 66 +++++++++++++++++ plugins/event.py | 59 +++++++++++++++ plugins/plugin.py | 7 ++ plugins/plugin_manager.py | 41 +++++++++++ plugins/plugin_registry.py | 51 +++++++++++++ plugins/selector/selector.json | 12 +++ plugins/selector/selector.py | 40 ++++++++++ 14 files changed, 520 insertions(+), 58 deletions(-) create mode 100644 plugins/README.md create mode 100644 plugins/__init__.py create mode 100644 plugins/createimg/createimg.py create mode 100644 plugins/event.py create mode 100644 plugins/plugin.py create mode 100644 plugins/plugin_manager.py create mode 100644 plugins/plugin_registry.py create mode 100644 plugins/selector/selector.json create mode 100644 plugins/selector/selector.py diff --git a/app.py b/app.py index 3502d86..c4119cb 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,8 @@ from channel import channel_factory from common import log, const from multiprocessing import Pool +from plugins.plugin_manager import PluginManager + # 启动通道 def start_process(channel_type, config_path): @@ -28,6 +30,7 @@ def main(): model_type = config.conf().get("model").get("type") channel_type = config.conf().get("channel").get("type") + PluginManager() # 1.单个字符串格式配置时,直接启动 if not isinstance(channel_type, list): start_process(channel_type, args.config) diff --git a/bridge/bridge.py b/bridge/bridge.py index f58198a..6b46d8c 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -1,14 +1,33 @@ from model import model_factory import config +from plugins.event import Event, EventContext +from plugins.plugin_manager import PluginManager + class Bridge(object): def __init__(self): pass def fetch_reply_content(self, query, context): - return model_factory.create_bot(config.conf().get("model").get("type")).reply(query, context) + econtext = PluginManager().emit_event(EventContext( + Event.ON_BRIDGE_HANDLE_CONTEXT, {'context': query, 'args': context})) + type = econtext['args'].get('model') or config.conf().get("model").get("type") + query = econtext.econtext.get("context", None) + reply = econtext.econtext.get("reply", "无回复") + if not econtext.is_pass() and query: + return model_factory.create_bot(type).reply(query, context) + else: + return reply async def fetch_reply_stream(self, query, context): - bot=model_factory.create_bot(config.conf().get("model").get("type")) - async for final,response in bot.reply_text_stream(query, context): - yield final,response + econtext = PluginManager().emit_event(EventContext( + Event.ON_BRIDGE_HANDLE_CONTEXT, {'context': query, 'args': context})) + type = econtext['args'].get('model') or config.conf().get("model").get("type") + query = econtext.get("context", None) + reply = econtext.get("reply", "无回复") + bot = model_factory.create_bot(type) + if not econtext.is_pass() and query: + async for final, response in bot.reply_text_stream(query, context): + yield final, response + else: + yield True, reply diff --git a/channel/http/http_channel.py b/channel/http/http_channel.py index a82fbbb..9ee35d3 100644 --- a/channel/http/http_channel.py +++ b/channel/http/http_channel.py @@ -12,6 +12,7 @@ from config import channel_conf_val from channel.channel import Channel from flask_socketio import SocketIO from common import log +from plugins.plugin_manager import * http_app = Flask(__name__,) socketio = SocketIO(http_app, close_timeout=5) @@ -26,13 +27,13 @@ http_app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1) async def return_stream(data): async for final, response in HttpChannel().handle_stream(data=data): try: - if(final): + if (final): socketio.server.emit( - 'disconnect', {'result': response, 'final': final}, request.sid, namespace="/chat") + 'disconnect', {'result': response, 'final': final}, request.sid, namespace="/chat") disconnect() else: socketio.server.emit( - 'message', {'result': response, 'final': final}, request.sid, namespace="/chat") + 'message', {'result': response, 'final': final}, request.sid, namespace="/chat") except Exception as e: disconnect() log.warn("[http]emit:{}", e) @@ -51,7 +52,8 @@ def stream(data): data["msg"], channel_conf_val(const.HTTP, 'image_create_prefix')) if img_match_prefix: reply_text = HttpChannel().handle(data=data) - socketio.emit('disconnect', {'result': reply_text}, namespace='/chat') + socketio.emit( + 'disconnect', {'result': reply_text}, namespace='/chat') disconnect() return asyncio.run(return_stream(data)) @@ -66,7 +68,7 @@ def connect(): @socketio.on('disconnect', namespace='/chat') def disconnect(): log.info('disconnect') - socketio.server.disconnect(request.sid,namespace="/chat") + socketio.server.disconnect(request.sid, namespace="/chat") @http_app.route("/chat", methods=['POST']) @@ -114,26 +116,30 @@ class HttpChannel(Channel): def handle(self, data): context = dict() - img_match_prefix = functions.check_prefix( - data["msg"], channel_conf_val(const.HTTP, 'image_create_prefix')) - if img_match_prefix: - data["msg"] = data["msg"].split(img_match_prefix, 1)[1].strip() - context['type'] = 'IMAGE_CREATE' + query = data["msg"] id = data["id"] context['from_user_id'] = str(id) - reply = super().build_reply_content(data["msg"], context) - if img_match_prefix: - if not isinstance(reply, list): - return reply - images = "" - for url in reply: - images += f"[!['IMAGE_CREATE']({url})]({url})\n" - reply = images + e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, { + 'channel': self, 'context': query, "args": context})) + reply = e_context['reply'] + if not e_context.is_pass(): + reply = super().build_reply_content(e_context["context"], e_context["args"]) + e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, { + 'channel': self, 'context': context, 'reply': reply, "args": context})) + reply = e_context['reply'] return reply async def handle_stream(self, data): context = dict() id = data["id"] context['from_user_id'] = str(id) - async for final, reply in super().build_reply_stream(data["msg"], context): - yield final, reply + context['stream'] = True + context['origin'] = data["msg"] + e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, { + 'channel': self, 'context': data["msg"], 'reply': data["msg"], "args": context})) + reply = e_context['reply'] + if not e_context.is_pass(): + async for final, reply in super().build_reply_stream(data["msg"], context): + yield final, reply + else: + yield True, reply diff --git a/channel/wechat/wechat_channel.py b/channel/wechat/wechat_channel.py index 4277aff..dcf6c99 100644 --- a/channel/wechat/wechat_channel.py +++ b/channel/wechat/wechat_channel.py @@ -14,7 +14,7 @@ from common.log import logger from common import const from config import channel_conf_val import requests - +from plugins.plugin_manager import * from common.sensitive_word import SensitiveWord import io @@ -81,25 +81,14 @@ class WechatChannel(Channel): str_list = content.split(match_prefix, 1) if len(str_list) == 2: content = str_list[1].strip() - - img_match_prefix = self.check_prefix(content, channel_conf_val(const.WECHAT, 'image_create_prefix')) - if img_match_prefix: - content = content.split(img_match_prefix, 1)[1].strip() - thread_pool.submit(self._do_send_img, content, from_user_id) - else: - thread_pool.submit(self._do_send, content, from_user_id) + thread_pool.submit(self._do_send, content, from_user_id) elif to_user_id == other_user_id and match_prefix: # 自己给好友发送消息 str_list = content.split(match_prefix, 1) if len(str_list) == 2: content = str_list[1].strip() - img_match_prefix = self.check_prefix(content, channel_conf_val(const.WECHAT, 'image_create_prefix')) - if img_match_prefix: - content = content.split(img_match_prefix, 1)[1].strip() - thread_pool.submit(self._do_send_img, content, to_user_id) - else: - thread_pool.submit(self._do_send, content, to_user_id) + thread_pool.submit(self._do_send, content, to_user_id) def handle_group(self, msg): @@ -137,13 +126,7 @@ class WechatChannel(Channel): group_white_list = channel_conf_val(const.WECHAT, 'group_name_white_list') if ('ALL_GROUP' in group_white_list or group_name in group_white_list or self.check_contain(group_name, channel_conf_val(const.WECHAT, 'group_name_keyword_white_list'))) and match_prefix: - - img_match_prefix = self.check_prefix(content, channel_conf_val(const.WECHAT, 'image_create_prefix')) - if img_match_prefix: - content = content.split(img_match_prefix, 1)[1].strip() - thread_pool.submit(self._do_send_img, content, group_id) - else: - thread_pool.submit(self._do_send_group, content, msg) + thread_pool.submit(self._do_send_group, content, msg) return None def send(self, msg, receiver): @@ -156,18 +139,25 @@ class WechatChannel(Channel): return context = dict() context['from_user_id'] = reply_user_id - reply_text = super().build_reply_content(query, context) - if reply_text: - self.send(channel_conf_val(const.WECHAT, "single_chat_reply_prefix") + reply_text, reply_user_id) + e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, { + 'channel': self, 'context': query, "args": context})) + + reply = e_context['reply'] + if not e_context.is_pass(): + reply = super().build_reply_content(e_context["context"], e_context["args"]) + e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, { + 'channel': self, 'context': context, 'reply': reply, "args": e_context["args"]})) + reply = e_context['reply'] + if reply: + self.send(channel_conf_val(const.WECHAT, "single_chat_reply_prefix") + reply, reply_user_id) except Exception as e: logger.exception(e) - def _do_send_img(self, query, reply_user_id): + def _do_send_img(self, query, context): try: if not query: return - context = dict() - context['type'] = 'IMAGE_CREATE' + reply_user_id=context['from_user_id'] img_urls = super().build_reply_content(query, context) if not img_urls: return @@ -192,12 +182,19 @@ class WechatChannel(Channel): if not query: return context = dict() - context['from_user_id'] = msg['ActualUserName'] - reply_text = super().build_reply_content(query, context) - if reply_text: - reply_text = '@' + msg['ActualNickName'] + ' ' + reply_text.strip() - self.send(channel_conf_val(const.WECHAT, "group_chat_reply_prefix", "") + reply_text, msg['User']['UserName']) - + context['from_user_id'] = msg['User']['UserName'] + e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, { + 'channel': self, 'context': query, "args": context})) + reply = e_context['reply'] + if not e_context.is_pass(): + context['from_user_id'] = msg['ActualUserName'] + reply = super().build_reply_content(e_context["context"], e_context["args"]) + e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, { + 'channel': self, 'context': context, 'reply': reply, "args": e_context["args"]})) + reply = e_context['reply'] + if reply: + reply = '@' + msg['ActualNickName'] + ' ' + reply.strip() + self.send(channel_conf_val(const.WECHAT, "group_chat_reply_prefix", "") + reply, msg['User']['UserName']) def check_prefix(self, content, prefix_list): for prefix in prefix_list: diff --git a/common/functions.py b/common/functions.py index 5560890..b1e1e99 100644 --- a/common/functions.py +++ b/common/functions.py @@ -1,4 +1,31 @@ +import json +import os import re +from common import log + +def singleton(cls): + instances = {} + + def get_instance(*args, **kwargs): + if cls not in instances: + instances[cls] = cls(*args, **kwargs) + return instances[cls] + + return get_instance + +def load_json_file(curdir: str, file: str = 'config.json'): + config_path = os.path.join(curdir, file) + try: + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + return config + except Exception as e: + if isinstance(e, FileNotFoundError): + log.warn( + f"[common]load json file failed, {config_path}\{file} not found") + else: + log.warn("[common]load json file failed") + raise e def contain_chinese(str): @@ -11,7 +38,9 @@ def contain_chinese(str): def check_prefix(content, prefix_list): + if(len(prefix_list)==0): + return True for prefix in prefix_list: if content.startswith(prefix): return prefix - return None \ No newline at end of file + return False diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..be632f4 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,122 @@ +# 简介 + +按 **[chatgpt-on-wechat](https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins)** 插件的思路对 **bot-on-anything** 进行插件化,期望能实现插件的共享使用,但是由于两个项目的架构存在较大差异,只能尽最大可能兼容 **[chatgpt-on-wechat](https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins)** 的插件,部分功能还需进行适配。 + + + +## **插件监听的事件:** + +事件顺序为 1、ON_HANDLE_CONTEXT --> 2、ON_BRIDGE_HANDLE_CONTEXT(ON_BRIDGE_HANDLE_STREAM_CONTEXT) --> 3、ON_DECORATE_REPLY + + +触发事件会产生事件的上下文EventContext,它可能包含了以下信息: + +EventContext(Event事件类型, {'channel' : 本次消息的context, 'context': 本次消息用户的提问, 'reply': 当前AI回复, "args": 其他上下文参数}) + +插件处理函数可通过修改EventContext中的context、reply、args或者调用channel中对应的方法来实现功能。 + + +``` +class Event(Enum): + + ON_HANDLE_CONTEXT = 2 # 对应通道处理消息前 + """ + e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 } + """ + + ON_DECORATE_REPLY = 3 # 得到回复后准备装饰 + """ + e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 , "args": 其他上下文参数 } + """ + + ON_SEND_REPLY = 4 # 发送回复前 + """ + bot-on-anything 不支持ON_SEND_REPLY事件,请使用ON_BRIDGE_HANDLE_CONTEXT或者ON_BRIDGE_HANDLE_STREAM_CONTEXT事件 + """ + + ON_BRIDGE_HANDLE_CONTEXT = 6 # 模型桥处理消息前 + """ + e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 , 模型桥会调用args.model指定的AI模型来进行回复 } + """ + + ON_BRIDGE_HANDLE_STREAM_CONTEXT = 7 # 模型桥处理流式消息前,流式对话的消息处理仅支持一次性返回,请直接返回结果 + """ + e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 , 模型桥会调用args.model指定的AI模型来进行回复 } + """ + +``` + +## 插件编写示例 + +以`plugins/selector`为例,其中编写了一个通过判断前缀调用不同模型的`Selector`插件。 + +### 1. 创建插件 + +在`plugins`目录下创建一个插件文件夹`selector`。然后,在该文件夹中创建同名``selector.py``文件。 + +``` +plugins/ +└── selector + └── selector.py +``` + +### 2. 编写功能 + +在`selector.py`文件中,创建插件类`Selector`,它继承自`Plugin`。 + +在类定义之前需要使用`@plugins.register`装饰器注册插件,并填写插件的相关信息,其中`desire_priority`表示插件默认的优先级,越大优先级越高。`Selector`插件加载时读取了同目录下的`selector.json`文件,从中取出对应的模型和触发前缀,`Selector`插件为事件`ON_HANDLE_CONTEXT`和`ON_BRIDGE_HANDLE_STREAM_CONTEXT`绑定了一个处理函数`select_model`,它表示在模型桥调用指定模型之前,都会先由`select_model`函数预处理。 + +```python +@plugins.register(name="Selector", desire_priority=99, hidden=True, desc="A model selector", version="0.1", author="RegimenArsenic") +class Selector(Plugin): + def __init__(self): + super().__init__() + curdir = os.path.dirname(__file__) + try: + self.config = functions.load_json_file(curdir, "selector.json") + except Exception as e: + log.warn("[Selector] init failed") + raise e + self.handlers[Event.ON_HANDLE_CONTEXT] = self.select_model + self.handlers[Event.ON_BRIDGE_HANDLE_STREAM_CONTEXT] = self.select_model + log.info("[Selector] inited") +``` + +### 3. 编写事件处理函数 + +#### 修改事件上下文 + +事件处理函数接收一个`EventContext`对象`e_context`作为参数。`e_context`包含了事件相关信息,利用`e_context['key']`来访问这些信息。 + +`EventContext(Event事件类型, {'channel' : 消息channel, 'context': Context, 'reply': Reply , "args": 其他上下文参数})` + +处理函数中通过修改`e_context`对象中的事件相关信息来实现所需功能,比如更改`e_context['reply']`中的内容可以修改回复,更改`e_context['context']`中的内容可以修改用户提问。 + +#### 决定是否交付给下个插件或默认逻辑 + +在处理函数结束时,还需要设置`e_context`对象的`action`属性,它决定如何继续处理事件。目前有以下三种处理方式: + +- `EventAction.CONTINUE`: 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑。 +- `EventAction.BREAK`: 事件结束,不再给下个插件处理,交付给默认的处理逻辑。 +- `EventAction.BREAK_PASS`: 事件结束,不再给下个插件处理,跳过默认的处理逻辑。 + +#### 示例处理函数 + +`Selector`插件通过判断前缀,如有`@bing`前缀,则修改调用模型为bing模型,若前缀为`@gpt`,则修改调用模型为chatgpt,否则就使用app里配置的原始模型插件,同时删去提问的前缀`@bing`或者`@gpt` + +```python + def select_model(self, e_context: EventContext): + model=e_context['args'].get('model') + for selector in self.config.get("selector", []): + prefix = selector.get('prefix', []) + check_prefix=functions.check_prefix(e_context["context"], prefix) + if (check_prefix): + model=selector.get('model') + if isinstance(check_prefix, str): + e_context["context"] = e_context["context"].split(check_prefix, 1)[1].strip() + break + log.debug(f"[Selector] select model {model}") + e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑 + e_context['args']['model']=model + return e_context +``` \ No newline at end of file diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..47c55ff --- /dev/null +++ b/plugins/__init__.py @@ -0,0 +1,10 @@ +# encoding:utf-8 +from .event import * +from .plugin import * +from plugins.plugin_registry import PluginRegistry + +instance = PluginRegistry() + +register = instance.register +# load_plugins = instance.load_plugins +# emit_event = instance.emit_event diff --git a/plugins/createimg/createimg.py b/plugins/createimg/createimg.py new file mode 100644 index 0000000..06269fc --- /dev/null +++ b/plugins/createimg/createimg.py @@ -0,0 +1,66 @@ +# encoding:utf-8 + +from channel.http.http_channel import HttpChannel +from channel.wechat.wechat_channel import WechatChannel +import plugins +from plugins import * +from common import functions +from config import channel_conf +from config import channel_conf_val +from common import const + + +@plugins.register(name="CreateImg", desire_priority=90, hidden=True, desc="A simple plugin that create images from model", version="0.1", author="RegimenArseic") +class Createimg(Plugin): + def __init__(self): + super().__init__() + self.handles = {HttpChannel: self.handle_http} + self.channel_types = {HttpChannel: const.HTTP, + WechatChannel: const.WECHAT} + self.handlers[Event.ON_HANDLE_CONTEXT] = self.handle_query + self.handlers[Event.ON_DECORATE_REPLY] = self.send_images + + def get_events(self): + return self.handlers + + def handle_query(self, e_context: EventContext): + channel = e_context['channel'] + channel_type = self.channel_types.get(type(channel), None) + if (channel_type): + query = e_context['context'] + if (query): + img_match_prefix = functions.check_prefix( + query, channel_conf_val(channel_type, 'image_create_prefix')) + if img_match_prefix: + if (channel_type == const.HTTP) and e_context['args'].get('stream', False): + e_context['reply'] = channel.handle( + {'msg': e_context['args']['origin'], 'id': e_context['args']['from_user_id']}) + e_context.action = EventAction.BREAK_PASS + else: + query = query.split(img_match_prefix, 1)[1].strip() + e_context['args']['type'] = 'IMAGE_CREATE' + if (channel_type == const.WECHAT): + channel._do_send_img( + query, e_context['args']) + e_context.action = EventAction.BREAK_PASS + else: + e_context.action = EventAction.CONTINUE + return e_context + + def handle_http(self, e_context: EventContext): + reply = e_context["reply"] + if e_context['args'].get('type', '') == 'IMAGE_CREATE': + if isinstance(reply, list): + images = "" + for url in reply: + images += f"[!['IMAGE_CREATE']({url})]({url})\n\n" + e_context["reply"] = images + return e_context + + def send_images(self, e_context: EventContext): + channel = e_context['channel'] + method = self.handles.get(type(channel), None) + if (method): + e_context = method(e_context) + e_context.action = EventAction.BREAK_PASS # 事件结束,不再给下个插件处理,不交付给默认的事件处理逻辑 + return e_context diff --git a/plugins/event.py b/plugins/event.py new file mode 100644 index 0000000..b1e3831 --- /dev/null +++ b/plugins/event.py @@ -0,0 +1,59 @@ +# encoding:utf-8 + +from enum import Enum + + +class Event(Enum): + # ON_RECEIVE_MESSAGE = 1 # 收到消息 + + ON_HANDLE_CONTEXT = 2 # 对应通道处理消息前 + """ + e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 } + """ + + ON_DECORATE_REPLY = 3 # 得到回复后准备装饰 + """ + e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 , "args": 其他上下文参数 } + """ + + ON_SEND_REPLY = 4 # 发送回复前 + """ + bot-on-anything 不支持ON_SEND_REPLY事件,请使用ON_BRIDGE_HANDLE_CONTEXT或者ON_BRIDGE_HANDLE_STREAM_CONTEXT事件 + """ + + # AFTER_SEND_REPLY = 5 # 发送回复后 + + ON_BRIDGE_HANDLE_CONTEXT = 6 # 模型桥处理消息前 + """ + e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 } + """ + + ON_BRIDGE_HANDLE_STREAM_CONTEXT = 7 # 模型桥处理流式消息前,流式对话的消息处理仅支持一次性返回,请直接返回结果 + """ + e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 } + """ + + +class EventAction(Enum): + CONTINUE = 1 # 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑 + BREAK = 2 # 事件结束,不再给下个插件处理,交付给默认的事件处理逻辑 + BREAK_PASS = 3 # 事件结束,不再给下个插件处理,不交付给默认的事件处理逻辑 + + +class EventContext: + def __init__(self, event, econtext=dict()): + self.event = event + self.econtext = econtext + self.action = EventAction.CONTINUE + + def __getitem__(self, key): + return self.econtext.get(key,"") + + def __setitem__(self, key, value): + self.econtext[key] = value + + def __delitem__(self, key): + del self.econtext[key] + + def is_pass(self): + return self.action == EventAction.BREAK_PASS diff --git a/plugins/plugin.py b/plugins/plugin.py new file mode 100644 index 0000000..551c9bb --- /dev/null +++ b/plugins/plugin.py @@ -0,0 +1,7 @@ +# encoding:utf-8 +class Plugin: + def __init__(self): + self.handlers = {} + + def get_help_text(self, **kwargs): + return "暂无帮助信息" \ No newline at end of file diff --git a/plugins/plugin_manager.py b/plugins/plugin_manager.py new file mode 100644 index 0000000..cb3bc61 --- /dev/null +++ b/plugins/plugin_manager.py @@ -0,0 +1,41 @@ +# encoding:utf-8 +import os +import importlib.util +from plugins.event import EventAction, EventContext,Event +from plugins.plugin_registry import PluginRegistry +from common import functions + +@functions.singleton +class PluginManager: + def __init__(self, plugins_dir="./plugins/"): + self.plugins_dir = plugins_dir + self.plugin_registry = PluginRegistry() + self.load_plugins() + + def load_plugins(self): + for plugin_name in self.find_plugin_names(): + if os.path.exists(f"./plugins/{plugin_name}/{plugin_name}.py"): + plugin_module = self.load_plugin_module(plugin_name) + self.plugin_registry.register_from_module(plugin_module) + + def find_plugin_names(self): + plugin_names = [] + for entry in os.scandir(self.plugins_dir): + if entry.is_dir(): + plugin_names.append(entry.name) + return plugin_names + + def load_plugin_module(self, plugin_name): + spec = importlib.util.spec_from_file_location( + plugin_name, os.path.join(self.plugins_dir, plugin_name, f"{plugin_name}.py") + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def emit_event(self, e_context: EventContext, *args, **kwargs): + for plugin in self.plugin_registry.list_plugins(): + if plugin.enabled and e_context.action == EventAction.CONTINUE: + if(e_context.event in plugin.handlers): + plugin.handlers[e_context.event](e_context, *args, **kwargs) + return e_context diff --git a/plugins/plugin_registry.py b/plugins/plugin_registry.py new file mode 100644 index 0000000..2340f28 --- /dev/null +++ b/plugins/plugin_registry.py @@ -0,0 +1,51 @@ +# encoding:utf-8 + +import inspect +from plugins.plugin import Plugin +from common.log import logger +from common import functions + +@functions.singleton +class PluginRegistry: + def __init__(self): + self.plugins = [] + + def register(self, name: str, desire_priority: int = 0, **kwargs): + def wrapper(plugin_cls): + plugin_cls.name = name + plugin_cls.priority = desire_priority + plugin_cls.desc = kwargs.get('desc') + plugin_cls.author = kwargs.get('author') + plugin_cls.version = kwargs.get('version') or "1.0" + plugin_cls.namecn = kwargs.get('namecn') or name + plugin_cls.hidden = kwargs.get('hidden') or False + plugin_cls.enabled = kwargs.get('enabled') or True + logger.info(f"Plugin {name}_v{plugin_cls.version} registered") + return plugin_cls + return wrapper + + def register_from_module(self, module): + plugins = [] + for name, obj in inspect.getmembers(module): + if inspect.isclass(obj) and issubclass(obj, Plugin) and obj != Plugin: + plugin_name = getattr(obj, "name", None) + if plugin_name: + plugin = obj() + plugin.name = plugin_name + plugin.priority = getattr(obj, "priority", 0) + plugin.desc = getattr(obj, "desc", None) + plugin.author = getattr(obj, "author", None) + plugin.version = getattr(obj, "version", "1.0") + plugin.namecn = getattr(obj, "namecn", plugin_name) + plugin.hidden = getattr(obj, "hidden", False) + plugin.enabled = getattr(obj, "enabled", True) + # Sort the list of plugins by priority + self.plugins.append(plugin) + self.plugins.sort(key=lambda x: x.priority, reverse=True) + + def get_plugin(self, name): + plugin = next((p for p in self.plugins if p.name.upper() == name.upper()), None) + return plugin + + def list_plugins(self): + return [plugin for plugin in self.plugins] \ No newline at end of file diff --git a/plugins/selector/selector.json b/plugins/selector/selector.json new file mode 100644 index 0000000..73d4a2e --- /dev/null +++ b/plugins/selector/selector.json @@ -0,0 +1,12 @@ +{ + "selector":[ + { + "model":"bing", + "prefix":["@bing"] + }, + { + "model":"chatgpt", + "prefix":["@gpt"] + } + ] +} \ No newline at end of file diff --git a/plugins/selector/selector.py b/plugins/selector/selector.py new file mode 100644 index 0000000..aa08f82 --- /dev/null +++ b/plugins/selector/selector.py @@ -0,0 +1,40 @@ +# encoding:utf-8 + +import os +import plugins +from plugins import * +from common import log +from common import functions + + +@plugins.register(name="Selector", desire_priority=99, hidden=True, desc="A model selector", version="0.1", author="RegimenArsenic") +class Selector(Plugin): + def __init__(self): + super().__init__() + curdir = os.path.dirname(__file__) + try: + self.config = functions.load_json_file(curdir, "selector.json") + except Exception as e: + log.warn("[Selector] init failed") + raise e + self.handlers[Event.ON_HANDLE_CONTEXT] = self.select_model + self.handlers[Event.ON_BRIDGE_HANDLE_STREAM_CONTEXT] = self.select_model + log.info("[Selector] inited") + + def get_events(self): + return self.handlers + + def select_model(self, e_context: EventContext): + model=e_context['args'].get('model') + for selector in self.config.get("selector", []): + prefix = selector.get('prefix', []) + check_prefix=functions.check_prefix(e_context["context"], prefix) + if (check_prefix): + model=selector.get('model') + if isinstance(check_prefix, str): + e_context["context"] = e_context["context"].split(check_prefix, 1)[1].strip() + break + log.debug(f"[Selector] select model {model}") + e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑 + e_context['args']['model']=model + return e_context From 655bfc69cb968a7ba592f79bd96f4e737cf1103a Mon Sep 17 00:00:00 2001 From: RegimenArsenic Date: Fri, 7 Apr 2023 03:59:51 +0800 Subject: [PATCH 2/4] bug fixed --- bridge/bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bridge/bridge.py b/bridge/bridge.py index 6b46d8c..46a2bc2 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -23,8 +23,8 @@ class Bridge(object): econtext = PluginManager().emit_event(EventContext( Event.ON_BRIDGE_HANDLE_CONTEXT, {'context': query, 'args': context})) type = econtext['args'].get('model') or config.conf().get("model").get("type") - query = econtext.get("context", None) - reply = econtext.get("reply", "无回复") + query = econtext.econtext.get("context", None) + reply = econtext.econtext.get("reply", "无回复") bot = model_factory.create_bot(type) if not econtext.is_pass() and query: async for final, response in bot.reply_text_stream(query, context): From 239b22387e026edb7117e43d1c5cdf9d3f41ef57 Mon Sep 17 00:00:00 2001 From: RegimenArsenic Date: Fri, 7 Apr 2023 15:03:58 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=89=8D=E7=BC=80,=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=9C=A8=E7=BE=A4=E4=B8=AD=E6=97=A0=E6=B3=95=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/selector/selector.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/selector/selector.json b/plugins/selector/selector.json index 73d4a2e..773fbf7 100644 --- a/plugins/selector/selector.json +++ b/plugins/selector/selector.json @@ -2,11 +2,11 @@ "selector":[ { "model":"bing", - "prefix":["@bing"] + "prefix":["#bing"] }, { "model":"chatgpt", - "prefix":["@gpt"] + "prefix":["#gpt"] } ] } \ No newline at end of file From cbeae8be19e2988e5bf80f70dd6a175986b4814f Mon Sep 17 00:00:00 2001 From: RegimenArsenic <37768579+RegimenArsenic@users.noreply.github.com> Date: Sat, 8 Apr 2023 01:28:22 +0800 Subject: [PATCH 4/4] [plugin]selector support bard --- plugins/selector/selector.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/selector/selector.json b/plugins/selector/selector.json index 773fbf7..c2bf3bd 100644 --- a/plugins/selector/selector.json +++ b/plugins/selector/selector.json @@ -7,6 +7,10 @@ { "model":"chatgpt", "prefix":["#gpt"] + }, + { + "model":"bard", + "prefix":["#google"] } ] -} \ No newline at end of file +}