From f6bee3aa58f0f67b22a33e568702a844e4f1e7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=83=A0=E6=96=87?= Date: Thu, 30 Nov 2023 10:41:34 +0800 Subject: [PATCH 01/16] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=92=89=E9=92=89?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=EF=BC=88Stream=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 2 +- channel/channel_factory.py | 3 + channel/dingtalk/dingtalk_channel.py | 151 +++++++++++++++++++++++++++ channel/dingtalk/dingtalk_message.py | 39 +++++++ common/const.py | 1 + config.py | 6 +- 6 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 channel/dingtalk/dingtalk_channel.py create mode 100644 channel/dingtalk/dingtalk_message.py diff --git a/app.py b/app.py index 19acdcd..2a14cba 100644 --- a/app.py +++ b/app.py @@ -43,7 +43,7 @@ def run(): # os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:9001' channel = channel_factory.create_channel(channel_name) - if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app", "wework", const.FEISHU]: + if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app", "wework", const.FEISHU,const.DINGTALK]: PluginManager().load_plugins() # startup channel diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 7044b9a..71f5f37 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -40,5 +40,8 @@ def create_channel(channel_type): elif channel_type == const.FEISHU: from channel.feishu.feishu_channel import FeiShuChanel return FeiShuChanel() + elif channel_type == const.DINGTALK: + from channel.dingtalk.dingtalk_channel import DingTalkChanel + return DingTalkChanel() raise RuntimeError diff --git a/channel/dingtalk/dingtalk_channel.py b/channel/dingtalk/dingtalk_channel.py new file mode 100644 index 0000000..9a9b170 --- /dev/null +++ b/channel/dingtalk/dingtalk_channel.py @@ -0,0 +1,151 @@ +""" +钉钉通道接入 + +@author huiwen +@Date 2023/11/28 +""" + +# -*- coding=utf-8 -*- +import uuid + +import requests +import web +from channel.dingtalk.dingtalk_message import DingTalkMessage +from bridge.context import Context +from bridge.reply import Reply, ReplyType +from common.log import logger +from common.singleton import singleton +from config import conf +from common.expired_dict import ExpiredDict +from bridge.context import ContextType +from channel.chat_channel import ChatChannel, check_prefix +from common import utils +import json +import os + + + +import argparse +import logging +from dingtalk_stream import AckMessage +import dingtalk_stream + +@singleton +class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): + dingtalk_app_id = conf().get('dingtalk_app_id') + dingtalk_app_secret = conf().get('dingtalk_app_secret') + + def setup_logger(self): + logger = logging.getLogger() + handler = logging.StreamHandler() + handler.setFormatter( + logging.Formatter('%(asctime)s %(name)-8s %(levelname)-8s %(message)s [%(filename)s:%(lineno)d]')) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + return logger + def __init__(self): + super().__init__() + super(dingtalk_stream.ChatbotHandler, self).__init__() + + self.logger = self.setup_logger() + # 历史消息id暂存,用于幂等控制 + self.receivedMsgs = ExpiredDict(60 * 60 * 7.1) + + logger.info("[dingtalk] app_id={}, app_secret={} ".format( + self.dingtalk_app_id, self.dingtalk_app_secret)) + # 无需群校验和前缀 + conf()["group_name_white_list"] = ["ALL_GROUP"] + conf()["single_chat_prefix"] = [] + + def startup(self): + + credential = dingtalk_stream.Credential( self.dingtalk_app_id, self.dingtalk_app_secret) + client = dingtalk_stream.DingTalkStreamClient(credential) + client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC,self) + client.start_forever() + + def handle_single(self, cmsg:DingTalkMessage): + # 处理单聊消息 + # + + if cmsg.ctype == ContextType.VOICE: + + logger.debug("[dingtalk]receive voice msg: {}".format(cmsg.content)) + elif cmsg.ctype == ContextType.IMAGE: + logger.debug("[dingtalk]receive image msg: {}".format(cmsg.content)) + elif cmsg.ctype == ContextType.PATPAT: + logger.debug("[dingtalk]receive patpat msg: {}".format(cmsg.content)) + elif cmsg.ctype == ContextType.TEXT: + expression = cmsg.my_msg + + + context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg) + if context: + self.produce(context) + + + + async def process(self, callback: dingtalk_stream.CallbackMessage): + + + + try: + + incoming_message = dingtalk_stream.ChatbotMessage.from_dict(callback.data) + dingtalk_msg = DingTalkMessage(incoming_message) + if incoming_message.conversation_type == '1': + self.handle_single(dingtalk_msg) + else: + self.handle_single(dingtalk_msg) + + except Exception as e: + logger.error(e) + return self.FAILED_MSG + + + def send(self, reply: Reply, context: Context): + + + incoming_message = context.kwargs['msg'].incoming_message + self.reply_text(reply.content, incoming_message) + + return AckMessage.STATUS_OK, 'OK' + +class dingtalkController: + # 类常量 + FAILED_MSG = '{"success": false}' + SUCCESS_MSG = '{"success": true}' + MESSAGE_RECEIVE_TYPE = "im.message.receive_v1" + + def GET(self): + return "dingtalk service start success!" + + + + def _compose_context(self, ctype: ContextType, content, **kwargs): + context = Context(ctype, content) + context.kwargs = kwargs + if "origin_ctype" not in context: + context["origin_ctype"] = ctype + + cmsg = context["msg"] + context["session_id"] = cmsg.from_user_id + context["receiver"] = cmsg.other_user_id + + if ctype == ContextType.TEXT: + # 1.文本请求 + # 图片生成处理 + img_match_prefix = check_prefix(content, conf().get("image_create_prefix")) + if img_match_prefix: + content = content.replace(img_match_prefix, "", 1) + context.type = ContextType.IMAGE_CREATE + else: + context.type = ContextType.TEXT + context.content = content.strip() + + elif context.type == ContextType.VOICE: + # 2.语音请求 + if "desire_rtype" not in context and conf().get("voice_reply_voice"): + context["desire_rtype"] = ReplyType.VOICE + + return context diff --git a/channel/dingtalk/dingtalk_message.py b/channel/dingtalk/dingtalk_message.py new file mode 100644 index 0000000..06dea41 --- /dev/null +++ b/channel/dingtalk/dingtalk_message.py @@ -0,0 +1,39 @@ +from bridge.context import ContextType +from channel.chat_message import ChatMessage +import json +import requests +from common.log import logger +from common.tmp_dir import TmpDir +from common import utils +from dingtalk_stream import ChatbotMessage + +class DingTalkMessage(ChatMessage): + def __init__(self, event: ChatbotMessage): + super().__init__(event) + + self.msg_id = event.message_id + msg_type = event.message_type + self.incoming_message =event + self.sender_staff_id = event.sender_staff_id + + self.create_time = event.create_at + if event.conversation_type=="1": + self.is_group = False + else: + self.is_group = True + + + if msg_type == "text": + self.ctype = ContextType.TEXT + + self.content = event.text.content.strip() + + self.from_user_id = event.sender_id + self.to_user_id = event.chatbot_user_id + + user_id = event.sender_id + nickname =event.sender_nick + + + + diff --git a/common/const.py b/common/const.py index b8d701a..6f3589c 100644 --- a/common/const.py +++ b/common/const.py @@ -20,3 +20,4 @@ MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4 # channel FEISHU = "feishu" +DINGTALK = "dingtalk" diff --git a/config.py b/config.py index 6cc5708..25deb04 100644 --- a/config.py +++ b/config.py @@ -123,7 +123,11 @@ available_setting = { "feishu_app_secret": "", # 飞书机器人APP secret "feishu_token": "", # 飞书 verification token "feishu_bot_name": "", # 飞书机器人的名字 - + + # 钉钉配置 + "dingtalk_app_id": "", # 钉钉机器人应用APP Id + "dingtalk_app_secret": "", # 钉钉机器人APP secret + # chatgpt指令自定义触发词 "clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头 # channel配置 From 32a8a847fc5f892779d03a220fbb43ca36c0c0b1 Mon Sep 17 00:00:00 2001 From: huiwen Date: Thu, 30 Nov 2023 12:09:03 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B0=8Fbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- channel/dingtalk/dingtalk_channel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/channel/dingtalk/dingtalk_channel.py b/channel/dingtalk/dingtalk_channel.py index 9a9b170..ad95883 100644 --- a/channel/dingtalk/dingtalk_channel.py +++ b/channel/dingtalk/dingtalk_channel.py @@ -97,7 +97,7 @@ class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): self.handle_single(dingtalk_msg) else: self.handle_single(dingtalk_msg) - + return AckMessage.STATUS_OK, 'OK' except Exception as e: logger.error(e) return self.FAILED_MSG @@ -109,7 +109,7 @@ class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): incoming_message = context.kwargs['msg'].incoming_message self.reply_text(reply.content, incoming_message) - return AckMessage.STATUS_OK, 'OK' + class dingtalkController: # 类常量 From bfacdb9c3b0038c074895f56b53fa65d9ff460da Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sat, 9 Dec 2023 12:39:09 +0800 Subject: [PATCH 03/16] feat: support character description of ali qwen model --- bot/tongyi/ali_qwen_session.py | 62 ++++++++++++++++++++++++++++++++++ bot/tongyi/tongyi_qwen_bot.py | 15 ++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 bot/tongyi/ali_qwen_session.py diff --git a/bot/tongyi/ali_qwen_session.py b/bot/tongyi/ali_qwen_session.py new file mode 100644 index 0000000..0eb1c4a --- /dev/null +++ b/bot/tongyi/ali_qwen_session.py @@ -0,0 +1,62 @@ +from bot.session_manager import Session +from common.log import logger + +""" + e.g. + [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Who won the world series in 2020?"}, + {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."}, + {"role": "user", "content": "Where was it played?"} + ] +""" + +class AliQwenSession(Session): + def __init__(self, session_id, system_prompt=None, model="qianwen"): + super().__init__(session_id, system_prompt) + self.model = model + self.reset() + + def discard_exceeding(self, max_tokens, cur_tokens=None): + precise = True + try: + cur_tokens = self.calc_tokens() + except Exception as e: + precise = False + if cur_tokens is None: + raise e + logger.debug("Exception when counting tokens precisely for query: {}".format(e)) + while cur_tokens > max_tokens: + if len(self.messages) > 2: + self.messages.pop(1) + elif len(self.messages) == 2 and self.messages[1]["role"] == "assistant": + self.messages.pop(1) + if precise: + cur_tokens = self.calc_tokens() + else: + cur_tokens = cur_tokens - max_tokens + break + elif len(self.messages) == 2 and self.messages[1]["role"] == "user": + logger.warn("user message exceed max_tokens. total_tokens={}".format(cur_tokens)) + break + else: + logger.debug("max_tokens={}, total_tokens={}, len(messages)={}".format(max_tokens, cur_tokens, len(self.messages))) + break + if precise: + cur_tokens = self.calc_tokens() + else: + cur_tokens = cur_tokens - max_tokens + return cur_tokens + + def calc_tokens(self): + return num_tokens_from_messages(self.messages, self.model) + +def num_tokens_from_messages(messages, model): + """Returns the number of tokens used by a list of messages.""" + # 官方token计算规则:"对于中文文本来说,1个token通常对应一个汉字;对于英文文本来说,1个token通常对应3至4个字母或1个单词" + # 详情请产看文档:https://help.aliyun.com/document_detail/2586397.html + # 目前根据字符串长度粗略估计token数,不影响正常使用 + tokens = 0 + for msg in messages: + tokens += len(msg["content"]) + return tokens diff --git a/bot/tongyi/tongyi_qwen_bot.py b/bot/tongyi/tongyi_qwen_bot.py index 585cb47..df99564 100644 --- a/bot/tongyi/tongyi_qwen_bot.py +++ b/bot/tongyi/tongyi_qwen_bot.py @@ -10,7 +10,7 @@ import broadscope_bailian from broadscope_bailian import ChatQaMessage from bot.bot import Bot -from bot.baidu.baidu_wenxin_session import BaiduWenxinSession +from bot.tongyi.ali_qwen_session import AliQwenSession from bot.session_manager import SessionManager from bridge.context import ContextType from bridge.reply import Reply, ReplyType @@ -27,7 +27,7 @@ class TongyiQwenBot(Bot): self.node_id = conf().get("qwen_node_id") or "" self.api_key_client = broadscope_bailian.AccessTokenClient(access_key_id=self.access_key_id, access_key_secret=self.access_key_secret) self.api_key_expired_time = self.set_api_key() - self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or "qwen") + self.sessions = SessionManager(AliQwenSession, model=conf().get("model") or "qwen") self.temperature = conf().get("temperature", 0.2) # 值在[0,1]之间,越大表示回复越具有不确定性 self.top_p = conf().get("top_p", 1) @@ -76,7 +76,7 @@ class TongyiQwenBot(Bot): reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type)) return reply - def reply_text(self, session: BaiduWenxinSession, retry_count=0) -> dict: + def reply_text(self, session: AliQwenSession, retry_count=0) -> dict: """ call bailian's ChatCompletion to get the answer :param session: a conversation session @@ -140,6 +140,7 @@ class TongyiQwenBot(Bot): history = [] user_content = '' assistant_content = '' + system_content = '' for message in messages: role = message.get('role') if role == 'user': @@ -149,8 +150,16 @@ class TongyiQwenBot(Bot): history.append(ChatQaMessage(user_content, assistant_content)) user_content = '' assistant_content = '' + elif role =='system': + system_content += message.get('content') if user_content == '': raise Exception('no user message') + if system_content != '': + # NOTE 模拟系统消息,测试发现人格描述以"你需要扮演ChatGPT"开头能够起作用,而以"你是ChatGPT"开头模型会直接否认 + system_qa = ChatQaMessage(system_content, '好的,我会严格按照你的设定回答问题') + history.insert(0, system_qa) + logger.debug("[TONGYI] converted qa messages: {}".format([item.to_dict() for item in history])) + logger.debug("[TONGYI] user content as prompt: {}".format(user_content)) return user_content, history def get_completion_content(self, response, node_id): From 207fa1d019fa9cae74fe356aaaae2c6c135969d5 Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sat, 9 Dec 2023 18:40:17 +0800 Subject: [PATCH 04/16] feat: hot reload conf of ali qwen model --- bot/tongyi/tongyi_qwen_bot.py | 42 +++++++++++++++++++++++++---------- plugins/godcmd/godcmd.py | 4 ++-- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/bot/tongyi/tongyi_qwen_bot.py b/bot/tongyi/tongyi_qwen_bot.py index df99564..21c50ca 100644 --- a/bot/tongyi/tongyi_qwen_bot.py +++ b/bot/tongyi/tongyi_qwen_bot.py @@ -15,21 +15,38 @@ from bot.session_manager import SessionManager from bridge.context import ContextType from bridge.reply import Reply, ReplyType from common.log import logger +from common import const from config import conf, load_config class TongyiQwenBot(Bot): def __init__(self): super().__init__() - self.access_key_id = conf().get("qwen_access_key_id") - self.access_key_secret = conf().get("qwen_access_key_secret") - self.agent_key = conf().get("qwen_agent_key") - self.app_id = conf().get("qwen_app_id") - self.node_id = conf().get("qwen_node_id") or "" - self.api_key_client = broadscope_bailian.AccessTokenClient(access_key_id=self.access_key_id, access_key_secret=self.access_key_secret) self.api_key_expired_time = self.set_api_key() - self.sessions = SessionManager(AliQwenSession, model=conf().get("model") or "qwen") - self.temperature = conf().get("temperature", 0.2) # 值在[0,1]之间,越大表示回复越具有不确定性 - self.top_p = conf().get("top_p", 1) + self.sessions = SessionManager(AliQwenSession, model=conf().get("model", const.QWEN)) + + def api_key_client(self): + return broadscope_bailian.AccessTokenClient(access_key_id=self.access_key_id(), access_key_secret=self.access_key_secret()) + + def access_key_id(self): + return conf().get("qwen_access_key_id") + + def access_key_secret(self): + return conf().get("qwen_access_key_secret") + + def agent_key(self): + return conf().get("qwen_agent_key") + + def app_id(self): + return conf().get("qwen_app_id") + + def node_id(self): + return conf().get("qwen_node_id", "") + + def temperature(self): + return conf().get("temperature", 0.2 ) + + def top_p(self): + return conf().get("top_p", 1) def reply(self, query, context=None): # acquire reply content @@ -87,8 +104,8 @@ class TongyiQwenBot(Bot): prompt, history = self.convert_messages_format(session.messages) self.update_api_key_if_expired() # NOTE 阿里百炼的call()函数参数比较奇怪, top_k参数表示top_p, top_p参数表示temperature, 可以参考文档 https://help.aliyun.com/document_detail/2587502.htm - response = broadscope_bailian.Completions().call(app_id=self.app_id, prompt=prompt, history=history, top_k=self.top_p, top_p=self.temperature) - completion_content = self.get_completion_content(response, self.node_id) + response = broadscope_bailian.Completions().call(app_id=self.app_id(), prompt=prompt, history=history, top_k=self.top_p(), top_p=self.temperature()) + completion_content = self.get_completion_content(response, self.node_id()) completion_tokens, total_tokens = self.calc_tokens(session.messages, completion_content) return { "total_tokens": total_tokens, @@ -129,9 +146,10 @@ class TongyiQwenBot(Bot): return result def set_api_key(self): - api_key, expired_time = self.api_key_client.create_token(agent_key=self.agent_key) + api_key, expired_time = self.api_key_client().create_token(agent_key=self.agent_key()) broadscope_bailian.api_key = api_key return expired_time + def update_api_key_if_expired(self): if time.time() > self.api_key_expired_time: self.api_key_expired_time = self.set_api_key() diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index 03a96bd..15c05a1 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -313,7 +313,7 @@ class Godcmd(Plugin): except Exception as e: ok, result = False, "你没有设置私有GPT模型" elif cmd == "reset": - if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI]: + if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI, const.QWEN]: bot.sessions.clear_session(session_id) if Bridge().chat_bots.get(bottype): Bridge().chat_bots.get(bottype).sessions.clear_session(session_id) @@ -339,7 +339,7 @@ class Godcmd(Plugin): ok, result = True, "配置已重载" elif cmd == "resetall": if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, - const.BAIDU, const.XUNFEI]: + const.BAIDU, const.XUNFEI, const.QWEN]: channel.cancel_all_session() bot.sessions.clear_all_session() ok, result = True, "重置所有会话成功" From c8910b8e148e1a87a74d270ded487bd21bb8d975 Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sat, 9 Dec 2023 19:26:11 +0800 Subject: [PATCH 05/16] fix: set correct top_p params of ali qwen model --- bot/tongyi/tongyi_qwen_bot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/tongyi/tongyi_qwen_bot.py b/bot/tongyi/tongyi_qwen_bot.py index 21c50ca..19bbc82 100644 --- a/bot/tongyi/tongyi_qwen_bot.py +++ b/bot/tongyi/tongyi_qwen_bot.py @@ -103,8 +103,8 @@ class TongyiQwenBot(Bot): try: prompt, history = self.convert_messages_format(session.messages) self.update_api_key_if_expired() - # NOTE 阿里百炼的call()函数参数比较奇怪, top_k参数表示top_p, top_p参数表示temperature, 可以参考文档 https://help.aliyun.com/document_detail/2587502.htm - response = broadscope_bailian.Completions().call(app_id=self.app_id(), prompt=prompt, history=history, top_k=self.top_p(), top_p=self.temperature()) + # NOTE 阿里百炼的call()函数未提供temperature参数,考虑到temperature和top_p参数作用相同,取两者较小的值作为top_p参数传入,详情见文档 https://help.aliyun.com/document_detail/2587502.htm + response = broadscope_bailian.Completions().call(app_id=self.app_id(), prompt=prompt, history=history, top_p=min(self.temperature(), self.top_p())) completion_content = self.get_completion_content(response, self.node_id()) completion_tokens, total_tokens = self.calc_tokens(session.messages, completion_content) return { @@ -181,6 +181,8 @@ class TongyiQwenBot(Bot): return user_content, history def get_completion_content(self, response, node_id): + if not response['Success']: + return f"[ERROR]\n{response['Code']}:{response['Message']}" text = response['Data']['Text'] if node_id == '': return text From 9838979bbdcfce9527fb216fc0cc4a1d8735a14c Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sat, 9 Dec 2023 19:40:07 +0800 Subject: [PATCH 06/16] refactor: update class name of qwen bot --- .../ali_qwen_bot.py} | 28 +++++++++---------- bot/{tongyi => ali}/ali_qwen_session.py | 0 bot/bot_factory.py | 4 +-- 3 files changed, 16 insertions(+), 16 deletions(-) rename bot/{tongyi/tongyi_qwen_bot.py => ali/ali_qwen_bot.py} (89%) rename bot/{tongyi => ali}/ali_qwen_session.py (100%) diff --git a/bot/tongyi/tongyi_qwen_bot.py b/bot/ali/ali_qwen_bot.py similarity index 89% rename from bot/tongyi/tongyi_qwen_bot.py rename to bot/ali/ali_qwen_bot.py index 19bbc82..ae9d767 100644 --- a/bot/tongyi/tongyi_qwen_bot.py +++ b/bot/ali/ali_qwen_bot.py @@ -10,7 +10,7 @@ import broadscope_bailian from broadscope_bailian import ChatQaMessage from bot.bot import Bot -from bot.tongyi.ali_qwen_session import AliQwenSession +from bot.ali.ali_qwen_session import AliQwenSession from bot.session_manager import SessionManager from bridge.context import ContextType from bridge.reply import Reply, ReplyType @@ -18,7 +18,7 @@ from common.log import logger from common import const from config import conf, load_config -class TongyiQwenBot(Bot): +class AliQwenBot(Bot): def __init__(self): super().__init__() self.api_key_expired_time = self.set_api_key() @@ -51,7 +51,7 @@ class TongyiQwenBot(Bot): def reply(self, query, context=None): # acquire reply content if context.type == ContextType.TEXT: - logger.info("[TONGYI] query={}".format(query)) + logger.info("[QWEN] query={}".format(query)) session_id = context["session_id"] reply = None @@ -68,11 +68,11 @@ class TongyiQwenBot(Bot): if reply: return reply session = self.sessions.session_query(query, session_id) - logger.debug("[TONGYI] session query={}".format(session.messages)) + logger.debug("[QWEN] session query={}".format(session.messages)) reply_content = self.reply_text(session) logger.debug( - "[TONGYI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format( + "[QWEN] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format( session.messages, session_id, reply_content["content"], @@ -86,7 +86,7 @@ class TongyiQwenBot(Bot): reply = Reply(ReplyType.TEXT, reply_content["content"]) else: reply = Reply(ReplyType.ERROR, reply_content["content"]) - logger.debug("[TONGYI] reply {} used 0 tokens.".format(reply_content)) + logger.debug("[QWEN] reply {} used 0 tokens.".format(reply_content)) return reply else: @@ -116,31 +116,31 @@ class TongyiQwenBot(Bot): need_retry = retry_count < 2 result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"} if isinstance(e, openai.error.RateLimitError): - logger.warn("[TONGYI] RateLimitError: {}".format(e)) + logger.warn("[QWEN] RateLimitError: {}".format(e)) result["content"] = "提问太快啦,请休息一下再问我吧" if need_retry: time.sleep(20) elif isinstance(e, openai.error.Timeout): - logger.warn("[TONGYI] Timeout: {}".format(e)) + logger.warn("[QWEN] Timeout: {}".format(e)) result["content"] = "我没有收到你的消息" if need_retry: time.sleep(5) elif isinstance(e, openai.error.APIError): - logger.warn("[TONGYI] Bad Gateway: {}".format(e)) + logger.warn("[QWEN] Bad Gateway: {}".format(e)) result["content"] = "请再问我一次" if need_retry: time.sleep(10) elif isinstance(e, openai.error.APIConnectionError): - logger.warn("[TONGYI] APIConnectionError: {}".format(e)) + logger.warn("[QWEN] APIConnectionError: {}".format(e)) need_retry = False result["content"] = "我连接不到你的网络" else: - logger.exception("[TONGYI] Exception: {}".format(e)) + logger.exception("[QWEN] Exception: {}".format(e)) need_retry = False self.sessions.clear_session(session.session_id) if need_retry: - logger.warn("[TONGYI] 第{}次重试".format(retry_count + 1)) + logger.warn("[QWEN] 第{}次重试".format(retry_count + 1)) return self.reply_text(session, retry_count + 1) else: return result @@ -176,8 +176,8 @@ class TongyiQwenBot(Bot): # NOTE 模拟系统消息,测试发现人格描述以"你需要扮演ChatGPT"开头能够起作用,而以"你是ChatGPT"开头模型会直接否认 system_qa = ChatQaMessage(system_content, '好的,我会严格按照你的设定回答问题') history.insert(0, system_qa) - logger.debug("[TONGYI] converted qa messages: {}".format([item.to_dict() for item in history])) - logger.debug("[TONGYI] user content as prompt: {}".format(user_content)) + logger.debug("[QWEN] converted qa messages: {}".format([item.to_dict() for item in history])) + logger.debug("[QWEN] user content as prompt: {}".format(user_content)) return user_content, history def get_completion_content(self, response, node_id): diff --git a/bot/tongyi/ali_qwen_session.py b/bot/ali/ali_qwen_session.py similarity index 100% rename from bot/tongyi/ali_qwen_session.py rename to bot/ali/ali_qwen_session.py diff --git a/bot/bot_factory.py b/bot/bot_factory.py index a0edde1..a54f706 100644 --- a/bot/bot_factory.py +++ b/bot/bot_factory.py @@ -45,6 +45,6 @@ def create_bot(bot_type): return ClaudeAIBot() elif bot_type == const.QWEN: - from bot.tongyi.tongyi_qwen_bot import TongyiQwenBot - return TongyiQwenBot() + from bot.ali.ali_qwen_bot import AliQwenBot + return AliQwenBot() raise RuntimeError From 95260e303c8407279db17ac401f4f3ac0bbb714f Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 11 Dec 2023 20:48:13 +0800 Subject: [PATCH 07/16] fix: process markdown url in knowledge base --- bot/linkai/link_ai_bot.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bot/linkai/link_ai_bot.py b/bot/linkai/link_ai_bot.py index c9afa71..ed47824 100644 --- a/bot/linkai/link_ai_bot.py +++ b/bot/linkai/link_ai_bot.py @@ -1,10 +1,9 @@ # access LinkAI knowledge base platform # docs: https://link-ai.tech/platform/link-app/wechat +import re import time - import requests - import config from bot.bot import Bot from bot.chatgpt.chat_gpt_session import ChatGPTSession @@ -125,6 +124,7 @@ class LinkAIBot(Bot): thread.start() if response["choices"][0].get("text_content"): reply_content = response["choices"][0].get("text_content") + reply_content = self._process_url(reply_content) return Reply(ReplyType.TEXT, reply_content) else: @@ -355,6 +355,14 @@ class LinkAIBot(Bot): except Exception as e: logger.exception(e) + def _process_url(self, text): + try: + url_pattern = re.compile(r'\[(.*?)\]\((http[s]?://.*?)\)') + def replace_markdown_url(match): + return f"{match.group(2)}" + return url_pattern.sub(replace_markdown_url, text) + except Exception as e: + logger.error(e) def _send_image(self, channel, context, image_urls): if not image_urls: From 3514c37e4c5680d74ba996f065e28a0a16e846f2 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 13 Dec 2023 20:57:04 +0800 Subject: [PATCH 08/16] fix: railway fork does not need action --- .github/workflows/deploy-image-arm.yml | 1 + .github/workflows/deploy-image.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/deploy-image-arm.yml b/.github/workflows/deploy-image-arm.yml index 163b7dc..9721add 100644 --- a/.github/workflows/deploy-image-arm.yml +++ b/.github/workflows/deploy-image-arm.yml @@ -19,6 +19,7 @@ env: jobs: build-and-push-image: + if: github.repository == 'zhayujie/chatgpt-on-wechat' runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/deploy-image.yml b/.github/workflows/deploy-image.yml index c3c8439..a30b77f 100644 --- a/.github/workflows/deploy-image.yml +++ b/.github/workflows/deploy-image.yml @@ -19,6 +19,7 @@ env: jobs: build-and-push-image: + if: github.repository == 'zhayujie/chatgpt-on-wechat' runs-on: ubuntu-latest permissions: contents: read From 413e09fb9ec26ec9aec82964ba2214d544b32692 Mon Sep 17 00:00:00 2001 From: 6vision Date: Thu, 14 Dec 2023 00:50:34 +0800 Subject: [PATCH 09/16] =?UTF-8?q?1=E3=80=81=E4=BC=81=E5=BE=AE=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E5=8F=B7=E6=94=AF=E6=8C=81=E6=96=87=E4=BB=B6=E5=92=8C?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E6=B6=88=E6=81=AF=202=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BC=81=E5=BE=AE=E4=B8=AA=E4=BA=BA=E5=8F=B7=E7=BE=A4?= =?UTF-8?q?=E5=90=8D=E8=8E=B7=E5=8F=96bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- channel/wework/wework_channel.py | 2 +- channel/wework/wework_message.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/channel/wework/wework_channel.py b/channel/wework/wework_channel.py index fb77843..1020261 100644 --- a/channel/wework/wework_channel.py +++ b/channel/wework/wework_channel.py @@ -120,7 +120,7 @@ def _check(func): @wework.msg_register( - [ntwork.MT_RECV_TEXT_MSG, ntwork.MT_RECV_IMAGE_MSG, 11072, ntwork.MT_RECV_VOICE_MSG]) + [ntwork.MT_RECV_TEXT_MSG, ntwork.MT_RECV_IMAGE_MSG, 11072, ntwork.MT_RECV_LINK_CARD_MSG,ntwork.MT_RECV_FILE_MSG, ntwork.MT_RECV_VOICE_MSG]) def all_msg_handler(wework_instance: ntwork.WeWork, message): logger.debug(f"收到消息: {message}") if 'data' in message: diff --git a/channel/wework/wework_message.py b/channel/wework/wework_message.py index e95dfb1..17e27f4 100644 --- a/channel/wework/wework_message.py +++ b/channel/wework/wework_message.py @@ -128,6 +128,18 @@ class WeworkMessage(ChatMessage): self.ctype = ContextType.IMAGE self.content = os.path.join(current_dir, "tmp", file_name) self._prepare_fn = lambda: cdn_download(wework, wework_msg, file_name) + elif wework_msg["type"] == 11045: # 文件消息 + print("文件消息") + print(wework_msg) + file_name = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + file_name = file_name + wework_msg['data']['cdn']['file_name'] + current_dir = os.getcwd() + self.ctype = ContextType.FILE + self.content = os.path.join(current_dir, "tmp", file_name) + self._prepare_fn = lambda: cdn_download(wework, wework_msg, file_name) + elif wework_msg["type"] == 11047: # 链接消息 + self.ctype = ContextType.SHARING + self.content = wework_msg['data']['url'] elif wework_msg["type"] == 11072: # 新成员入群通知 self.ctype = ContextType.JOIN_GROUP member_list = wework_msg['data']['member_list'] @@ -179,6 +191,7 @@ class WeworkMessage(ChatMessage): if conversation_id: room_info = get_room_info(wework=wework, conversation_id=conversation_id) self.other_user_nickname = room_info.get('nickname', None) if room_info else None + self.from_user_nickname = room_info.get('nickname', None) if room_info else None at_list = data.get('at_list', []) tmp_list = [] for at in at_list: From 23a237074ed1bcd1097fb3fcb370687896060b00 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 15 Dec 2023 10:19:48 +0800 Subject: [PATCH 10/16] feat: support gemini model --- bot/bot_factory.py | 5 +++ bot/chatgpt/chat_gpt_session.py | 2 +- bot/gemini/google_gemini_bot.py | 58 +++++++++++++++++++++++++++++++++ bridge/bridge.py | 4 +++ common/const.py | 3 +- config.py | 2 ++ plugins/godcmd/godcmd.py | 2 +- 7 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 bot/gemini/google_gemini_bot.py diff --git a/bot/bot_factory.py b/bot/bot_factory.py index a0edde1..a103209 100644 --- a/bot/bot_factory.py +++ b/bot/bot_factory.py @@ -47,4 +47,9 @@ def create_bot(bot_type): elif bot_type == const.QWEN: from bot.tongyi.tongyi_qwen_bot import TongyiQwenBot return TongyiQwenBot() + + elif bot_type == const.GEMINI: + from bot.gemini.google_gemini_bot import GoogleGeminiBot + return GoogleGeminiBot() + raise RuntimeError diff --git a/bot/chatgpt/chat_gpt_session.py b/bot/chatgpt/chat_gpt_session.py index e7dabec..74914f2 100644 --- a/bot/chatgpt/chat_gpt_session.py +++ b/bot/chatgpt/chat_gpt_session.py @@ -57,7 +57,7 @@ class ChatGPTSession(Session): def num_tokens_from_messages(messages, model): """Returns the number of tokens used by a list of messages.""" - if model in ["wenxin", "xunfei"]: + if model in ["wenxin", "xunfei", const.GEMINI]: return num_tokens_by_character(messages) import tiktoken diff --git a/bot/gemini/google_gemini_bot.py b/bot/gemini/google_gemini_bot.py new file mode 100644 index 0000000..4cc0dd3 --- /dev/null +++ b/bot/gemini/google_gemini_bot.py @@ -0,0 +1,58 @@ +""" +Google gemini bot + +@author zhayujie +@Date 2023/12/15 +""" +# encoding:utf-8 + +from bot.bot import Bot +import google.generativeai as genai +from bot.session_manager import SessionManager +from bridge.context import ContextType, Context +from bridge.reply import Reply, ReplyType +from common.log import logger +from config import conf +from bot.baidu.baidu_wenxin_session import BaiduWenxinSession + + +# OpenAI对话模型API (可用) +class GoogleGeminiBot(Bot): + + def __init__(self): + super().__init__() + self.api_key = conf().get("gemini_api_key") + # 复用文心的token计算方式 + self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or "gpt-3.5-turbo") + + def reply(self, query, context: Context = None) -> Reply: + if context.type != ContextType.TEXT: + logger.warn(f"[Gemini] Unsupported message type, type={context.type}") + return Reply(ReplyType.TEXT, None) + logger.info(f"[Gemini] query={query}") + session_id = context["session_id"] + session = self.sessions.session_query(query, session_id) + gemini_messages = self._convert_to_gemini_messages(session.messages) + genai.configure(api_key=self.api_key) + model = genai.GenerativeModel('gemini-pro') + response = model.generate_content(gemini_messages) + reply_text = response.text + self.sessions.session_reply(reply_text, session_id) + logger.info(f"[Gemini] reply={reply_text}") + return Reply(ReplyType.TEXT, reply_text) + + + def _convert_to_gemini_messages(self, messages: list): + res = [] + for msg in messages: + if msg.get("role") == "user": + role = "user" + elif msg.get("role") == "assistant": + role = "model" + else: + continue + res.append({ + "role": role, + "parts": [{"text": msg.get("content")}] + }) + return res diff --git a/bridge/bridge.py b/bridge/bridge.py index 2b637c3..53ee878 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -29,12 +29,16 @@ class Bridge(object): self.btype["chat"] = const.XUNFEI if model_type in [const.QWEN]: self.btype["chat"] = const.QWEN + if model_type in [const.GEMINI]: + self.btype["chat"] = const.GEMINI + if conf().get("use_linkai") and conf().get("linkai_api_key"): self.btype["chat"] = const.LINKAI if not conf().get("voice_to_text") or conf().get("voice_to_text") in ["openai"]: self.btype["voice_to_text"] = const.LINKAI if not conf().get("text_to_voice") or conf().get("text_to_voice") in ["openai", const.TTS_1, const.TTS_1_HD]: self.btype["text_to_voice"] = const.LINKAI + if model_type in ["claude"]: self.btype["chat"] = const.CLAUDEAI self.bots = {} diff --git a/common/const.py b/common/const.py index fc74e64..b2d0df6 100644 --- a/common/const.py +++ b/common/const.py @@ -7,6 +7,7 @@ CHATGPTONAZURE = "chatGPTOnAzure" LINKAI = "linkai" CLAUDEAI = "claude" QWEN = "qwen" +GEMINI = "gemini" # model GPT35 = "gpt-3.5-turbo" @@ -17,7 +18,7 @@ WHISPER_1 = "whisper-1" TTS_1 = "tts-1" TTS_1_HD = "tts-1-hd" -MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4", "xunfei", "claude", "gpt-4-turbo", GPT4_TURBO_PREVIEW, QWEN] +MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4", "xunfei", "claude", "gpt-4-turbo", GPT4_TURBO_PREVIEW, QWEN, GEMINI] # channel FEISHU = "feishu" diff --git a/config.py b/config.py index 8300699..bc4d9f7 100644 --- a/config.py +++ b/config.py @@ -73,6 +73,8 @@ available_setting = { "qwen_agent_key": "", "qwen_app_id": "", "qwen_node_id": "", # 流程编排模型用到的id,如果没有用到qwen_node_id,请务必保持为空字符串 + # Google Gemini Api Key + "gemini_api_key": "", # wework的通用配置 "wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开 # 语音设置 diff --git a/plugins/godcmd/godcmd.py b/plugins/godcmd/godcmd.py index 03a96bd..dd301e6 100644 --- a/plugins/godcmd/godcmd.py +++ b/plugins/godcmd/godcmd.py @@ -313,7 +313,7 @@ class Godcmd(Plugin): except Exception as e: ok, result = False, "你没有设置私有GPT模型" elif cmd == "reset": - if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI]: + if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI, const.GEMINI]: bot.sessions.clear_session(session_id) if Bridge().chat_bots.get(bottype): Bridge().chat_bots.get(bottype).sessions.clear_session(session_id) From eca1892e2aa61a63d581f3cc74a9c76f9fb6f51e Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 15 Dec 2023 14:23:36 +0800 Subject: [PATCH 11/16] fix: gemini no content bug --- bot/gemini/google_gemini_bot.py | 47 ++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/bot/gemini/google_gemini_bot.py b/bot/gemini/google_gemini_bot.py index 4cc0dd3..1a49d60 100644 --- a/bot/gemini/google_gemini_bot.py +++ b/bot/gemini/google_gemini_bot.py @@ -26,21 +26,24 @@ class GoogleGeminiBot(Bot): self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or "gpt-3.5-turbo") def reply(self, query, context: Context = None) -> Reply: - if context.type != ContextType.TEXT: - logger.warn(f"[Gemini] Unsupported message type, type={context.type}") - return Reply(ReplyType.TEXT, None) - logger.info(f"[Gemini] query={query}") - session_id = context["session_id"] - session = self.sessions.session_query(query, session_id) - gemini_messages = self._convert_to_gemini_messages(session.messages) - genai.configure(api_key=self.api_key) - model = genai.GenerativeModel('gemini-pro') - response = model.generate_content(gemini_messages) - reply_text = response.text - self.sessions.session_reply(reply_text, session_id) - logger.info(f"[Gemini] reply={reply_text}") - return Reply(ReplyType.TEXT, reply_text) - + try: + if context.type != ContextType.TEXT: + logger.warn(f"[Gemini] Unsupported message type, type={context.type}") + return Reply(ReplyType.TEXT, None) + logger.info(f"[Gemini] query={query}") + session_id = context["session_id"] + session = self.sessions.session_query(query, session_id) + gemini_messages = self._convert_to_gemini_messages(self._filter_messages(session.messages)) + genai.configure(api_key=self.api_key) + model = genai.GenerativeModel('gemini-pro') + response = model.generate_content(gemini_messages) + reply_text = response.text + self.sessions.session_reply(reply_text, session_id) + logger.info(f"[Gemini] reply={reply_text}") + return Reply(ReplyType.TEXT, reply_text) + except Exception as e: + logger.error("[Gemini] fetch reply error, may contain unsafe content") + logger.error(e) def _convert_to_gemini_messages(self, messages: list): res = [] @@ -56,3 +59,17 @@ class GoogleGeminiBot(Bot): "parts": [{"text": msg.get("content")}] }) return res + + def _filter_messages(self, messages: list): + res = [] + turn = "user" + for i in range(len(messages) - 1, -1, -1): + message = messages[i] + if message.get("role") != turn: + continue + res.insert(0, message) + if turn == "user": + turn = "assistant" + elif turn == "assistant": + turn = "user" + return res From c049a619dcea44025609a22369ffa2471da04789 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 15 Dec 2023 16:49:23 +0800 Subject: [PATCH 12/16] chore: remove useless code --- channel/feishu/feishu_message.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/channel/feishu/feishu_message.py b/channel/feishu/feishu_message.py index 73285f2..e2054c1 100644 --- a/channel/feishu/feishu_message.py +++ b/channel/feishu/feishu_message.py @@ -46,35 +46,6 @@ class FeishuMessage(ChatMessage): else: logger.info(f"[FeiShu] Failed to download file, key={file_key}, res={response.text}") self._prepare_fn = _download_file - - # elif msg.type == "voice": - # self.ctype = ContextType.VOICE - # self.content = TmpDir().path() + msg.media_id + "." + msg.format # content直接存临时目录路径 - # - # def download_voice(): - # # 如果响应状态码是200,则将响应内容写入本地文件 - # response = client.media.download(msg.media_id) - # if response.status_code == 200: - # with open(self.content, "wb") as f: - # f.write(response.content) - # else: - # logger.info(f"[wechatcom] Failed to download voice file, {response.content}") - # - # self._prepare_fn = download_voice - # elif msg.type == "image": - # self.ctype = ContextType.IMAGE - # self.content = TmpDir().path() + msg.media_id + ".png" # content直接存临时目录路径 - # - # def download_image(): - # # 如果响应状态码是200,则将响应内容写入本地文件 - # response = client.media.download(msg.media_id) - # if response.status_code == 200: - # with open(self.content, "wb") as f: - # f.write(response.content) - # else: - # logger.info(f"[wechatcom] Failed to download image file, {response.content}") - # - # self._prepare_fn = download_image else: raise NotImplementedError("Unsupported message type: Type:{} ".format(msg_type)) From 203d4d8bfb47883ab66867ab5f128f11cd40e6d8 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 15 Dec 2023 19:16:13 +0800 Subject: [PATCH 13/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30d2f52..6d38e6b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 最新版本支持的功能如下: - [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信、微信公众号和、业微信、飞书等部署方式 -- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, 文心一言, 讯飞星火 +- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, Gemini, 文心一言, 讯飞星火, 通义千问 - [x] **语音能力:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai(whisper/tts) 等多种语音模型 - [x] **图像能力:** 支持图片生成、图片识别、图生图(如照片修复),可选择 Dall-E-3, stable diffusion, replicate, midjourney, vision模型 - [x] **丰富插件:** 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结、文档总结和对话等插件 From 04943c0bfa0a9038d1f498dbea4a1920a8502f84 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sat, 16 Dec 2023 01:11:05 +0800 Subject: [PATCH 14/16] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6d38e6b..a818b4e 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Demo made by [Visionn](https://www.wangpc.cc/) # 更新日志 +>**2023.11.11:** [1.5.3版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.3) 和 [1.5.4版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.4),新增Google Gemini、通义千问模型 + >**2023.11.10:** [1.5.2版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.2),新增飞书通道、图像识别对话、黑名单配置 >**2023.11.10:** [1.5.0版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.0),新增 `gpt-4-turbo`, `dall-e-3`, `tts` 模型接入,完善图像理解&生成、语音识别&生成的多模态能力 From a21dd97786bcef834462cfd73a4742d8fba92831 Mon Sep 17 00:00:00 2001 From: huiwen Date: Sun, 17 Dec 2023 09:23:15 +0800 Subject: [PATCH 15/16] =?UTF-8?q?=E9=92=89=E9=92=89app=5Fid,=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E4=B8=BA=5Fclient=5Fid=EF=BC=8C=E5=92=8C=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- channel/dingtalk/dingtalk_channel.py | 95 ++++++++++++++++------------ channel/dingtalk/dingtalk_message.py | 3 +- config.py | 4 +- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/channel/dingtalk/dingtalk_channel.py b/channel/dingtalk/dingtalk_channel.py index ad95883..1f86ce7 100644 --- a/channel/dingtalk/dingtalk_channel.py +++ b/channel/dingtalk/dingtalk_channel.py @@ -32,8 +32,8 @@ import dingtalk_stream @singleton class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): - dingtalk_app_id = conf().get('dingtalk_app_id') - dingtalk_app_secret = conf().get('dingtalk_app_secret') + dingtalk_client_id = conf().get('dingtalk_client_id') + dingtalk_client_secret = conf().get('dingtalk_client_secret') def setup_logger(self): logger = logging.getLogger() @@ -51,15 +51,16 @@ class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): # 历史消息id暂存,用于幂等控制 self.receivedMsgs = ExpiredDict(60 * 60 * 7.1) - logger.info("[dingtalk] app_id={}, app_secret={} ".format( - self.dingtalk_app_id, self.dingtalk_app_secret)) + logger.info("[dingtalk] client_id={}, client_secret={} ".format( + self.dingtalk_client_id, self.dingtalk_client_secret)) # 无需群校验和前缀 conf()["group_name_white_list"] = ["ALL_GROUP"] - conf()["single_chat_prefix"] = [] + + def startup(self): - credential = dingtalk_stream.Credential( self.dingtalk_app_id, self.dingtalk_app_secret) + credential = dingtalk_stream.Credential( self.dingtalk_client_id, self.dingtalk_client_secret) client = dingtalk_stream.DingTalkStreamClient(credential) client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC,self) client.start_forever() @@ -78,11 +79,32 @@ class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): elif cmsg.ctype == ContextType.TEXT: expression = cmsg.my_msg - + cmsg.content = conf()["single_chat_prefix"][0] + cmsg.content + context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg) + if context: self.produce(context) + def handle_group(self, cmsg:DingTalkMessage): + # 处理群聊消息 + # + + if cmsg.ctype == ContextType.VOICE: + + logger.debug("[dingtalk]receive voice msg: {}".format(cmsg.content)) + elif cmsg.ctype == ContextType.IMAGE: + logger.debug("[dingtalk]receive image msg: {}".format(cmsg.content)) + elif cmsg.ctype == ContextType.PATPAT: + logger.debug("[dingtalk]receive patpat msg: {}".format(cmsg.content)) + elif cmsg.ctype == ContextType.TEXT: + expression = cmsg.my_msg + + cmsg.content = conf()["group_chat_prefix"][0] + cmsg.content + context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg) + context['no_need_at']=True + if context: + self.produce(context) async def process(self, callback: dingtalk_stream.CallbackMessage): @@ -96,7 +118,7 @@ class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): if incoming_message.conversation_type == '1': self.handle_single(dingtalk_msg) else: - self.handle_single(dingtalk_msg) + self.handle_group(dingtalk_msg) return AckMessage.STATUS_OK, 'OK' except Exception as e: logger.error(e) @@ -111,41 +133,32 @@ class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler): -class dingtalkController: - # 类常量 - FAILED_MSG = '{"success": false}' - SUCCESS_MSG = '{"success": true}' - MESSAGE_RECEIVE_TYPE = "im.message.receive_v1" - def GET(self): - return "dingtalk service start success!" - + # def _compose_context(self, ctype: ContextType, content, **kwargs): + # context = Context(ctype, content) + # context.kwargs = kwargs + # if "origin_ctype" not in context: + # context["origin_ctype"] = ctype - def _compose_context(self, ctype: ContextType, content, **kwargs): - context = Context(ctype, content) - context.kwargs = kwargs - if "origin_ctype" not in context: - context["origin_ctype"] = ctype - - cmsg = context["msg"] - context["session_id"] = cmsg.from_user_id - context["receiver"] = cmsg.other_user_id - - if ctype == ContextType.TEXT: - # 1.文本请求 - # 图片生成处理 - img_match_prefix = check_prefix(content, conf().get("image_create_prefix")) - if img_match_prefix: - content = content.replace(img_match_prefix, "", 1) - context.type = ContextType.IMAGE_CREATE - else: - context.type = ContextType.TEXT - context.content = content.strip() + # cmsg = context["msg"] + # context["session_id"] = cmsg.from_user_id + # context["receiver"] = cmsg.other_user_id + + # if ctype == ContextType.TEXT: + # # 1.文本请求 + # # 图片生成处理 + # img_match_prefix = check_prefix(content, conf().get("image_create_prefix")) + # if img_match_prefix: + # content = content.replace(img_match_prefix, "", 1) + # context.type = ContextType.IMAGE_CREATE + # else: + # context.type = ContextType.TEXT + # context.content = content.strip() - elif context.type == ContextType.VOICE: - # 2.语音请求 - if "desire_rtype" not in context and conf().get("voice_reply_voice"): - context["desire_rtype"] = ReplyType.VOICE + # elif context.type == ContextType.VOICE: + # # 2.语音请求 + # if "desire_rtype" not in context and conf().get("voice_reply_voice"): + # context["desire_rtype"] = ReplyType.VOICE - return context + # return context diff --git a/channel/dingtalk/dingtalk_message.py b/channel/dingtalk/dingtalk_message.py index 06dea41..57e1455 100644 --- a/channel/dingtalk/dingtalk_message.py +++ b/channel/dingtalk/dingtalk_message.py @@ -30,7 +30,8 @@ class DingTalkMessage(ChatMessage): self.from_user_id = event.sender_id self.to_user_id = event.chatbot_user_id - + self.other_user_nickname = event.conversation_title + user_id = event.sender_id nickname =event.sender_nick diff --git a/config.py b/config.py index 25deb04..03b5011 100644 --- a/config.py +++ b/config.py @@ -125,8 +125,8 @@ available_setting = { "feishu_bot_name": "", # 飞书机器人的名字 # 钉钉配置 - "dingtalk_app_id": "", # 钉钉机器人应用APP Id - "dingtalk_app_secret": "", # 钉钉机器人APP secret + "dingtalk_client_id": "", # 钉钉机器人Client ID + "dingtalk_client_secret": "", # 钉钉机器人Client Secret # chatgpt指令自定义触发词 "clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头 From 78d9be82b2b147f82e49d11ae4916b6c6cd6942c Mon Sep 17 00:00:00 2001 From: zhayujie Date: Tue, 19 Dec 2023 11:47:33 +0800 Subject: [PATCH 16/16] fix: add gemini dependency --- requirements-optional.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements-optional.txt b/requirements-optional.txt index c070f97..601f434 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -33,3 +33,6 @@ curl_cffi # tongyi qwen broadscope_bailian + +# google +google-generativeai