From 6bd1242d43a77f2afbde5468f4d78c0a3cb0c915 Mon Sep 17 00:00:00 2001 From: lanvent Date: Sun, 9 Apr 2023 16:16:54 +0800 Subject: [PATCH 1/7] chore: update requirements and config-template --- bridge/bridge.py | 2 +- channel/wechat/wechat_channel.py | 2 ++ config-template.json | 1 - requirements-optional.txt | 2 +- requirements.txt | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bridge/bridge.py b/bridge/bridge.py index 5c8448ee5..a19fc8379 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -19,7 +19,7 @@ def __init__(self): model_type = conf().get("model") if model_type in ["text-davinci-003"]: self.btype['chat'] = const.OPEN_AI - if conf().get("use_azure_chatgpt"): + if conf().get("use_azure_chatgpt", False): self.btype['chat'] = const.CHATGPTONAZURE self.bots={} diff --git a/channel/wechat/wechat_channel.py b/channel/wechat/wechat_channel.py index 36f6157a5..b11611d52 100644 --- a/channel/wechat/wechat_channel.py +++ b/channel/wechat/wechat_channel.py @@ -147,6 +147,8 @@ def handle_group(self, cmsg : ChatMessage): if conf().get('speech_recognition') != True: return logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content)) + elif cmsg.ctype == ContextType.IMAGE: + logger.debug("[WX]receive image for group msg: {}".format(cmsg.content)) else: # logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg)) pass diff --git a/config-template.json b/config-template.json index abb3486a5..528bd6010 100644 --- a/config-template.json +++ b/config-template.json @@ -2,7 +2,6 @@ "open_ai_api_key": "YOUR API KEY", "model": "gpt-3.5-turbo", "proxy": "", - "use_azure_chatgpt": false, "single_chat_prefix": ["bot", "@bot"], "single_chat_reply_prefix": "[bot] ", "group_chat_prefix": ["@bot"], diff --git a/requirements-optional.txt b/requirements-optional.txt index 55e3f7abf..949a1dcaa 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -21,4 +21,4 @@ web.py # chatgpt-tool-hub plugin --extra-index-url https://pypi.python.org/simple -chatgpt_tool_hub>=0.3.5 \ No newline at end of file +chatgpt_tool_hub>=0.3.7 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 75ff7fbef..61f979d99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -openai>=0.27.2 +openai==0.27.2 HTMLParser>=0.0.2 PyQRCode>=1.2.1 qrcode>=7.4.2 From bf4ae9a0515a75197111677921d12eaa802665a4 Mon Sep 17 00:00:00 2001 From: lanvent Date: Sun, 9 Apr 2023 17:37:19 +0800 Subject: [PATCH 2/7] fix: create tmpdir --- common/tmp_dir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/tmp_dir.py b/common/tmp_dir.py index 1738022ca..63f56ebc3 100644 --- a/common/tmp_dir.py +++ b/common/tmp_dir.py @@ -12,7 +12,7 @@ class TmpDir(object): def __init__(self): pathExists = os.path.exists(self.tmpFilePath) - if not pathExists and conf().get('speech_recognition') == True: + if not pathExists: os.makedirs(self.tmpFilePath) def path(self): From 89e8f385b4665aaf53e78f4038fe63265178ea30 Mon Sep 17 00:00:00 2001 From: yubai Date: Sun, 9 Apr 2023 15:36:03 +0800 Subject: [PATCH 3/7] bugfix for azure chatgpt adapting --- bot/chatgpt/chat_gpt_bot.py | 7 ++++--- bot/chatgpt/chat_gpt_session.py | 4 ++-- config-template.json | 3 ++- config.py | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/chat_gpt_bot.py index 158392d49..b7b86a6b1 100644 --- a/bot/chatgpt/chat_gpt_bot.py +++ b/bot/chatgpt/chat_gpt_bot.py @@ -151,6 +151,7 @@ def __init__(self): def compose_args(self): args = super().compose_args() - args["engine"] = args["model"] - del(args["model"]) - return args \ No newline at end of file + args["deployment_id"] = conf().get("azure_deployment_id") + #args["engine"] = args["model"] + #del(args["model"]) + return args diff --git a/bot/chatgpt/chat_gpt_session.py b/bot/chatgpt/chat_gpt_session.py index afbe6a5df..90fe064f9 100644 --- a/bot/chatgpt/chat_gpt_session.py +++ b/bot/chatgpt/chat_gpt_session.py @@ -55,7 +55,7 @@ def num_tokens_from_messages(messages, model): except KeyError: logger.debug("Warning: model not found. Using cl100k_base encoding.") encoding = tiktoken.get_encoding("cl100k_base") - if model == "gpt-3.5-turbo": + if model == "gpt-3.5-turbo" or model == "gpt-35-turbo": return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0301") elif model == "gpt-4": return num_tokens_from_messages(messages, model="gpt-4-0314") @@ -76,4 +76,4 @@ def num_tokens_from_messages(messages, model): if key == "name": num_tokens += tokens_per_name num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> - return num_tokens \ No newline at end of file + return num_tokens diff --git a/config-template.json b/config-template.json index 528bd6010..926017a46 100644 --- a/config-template.json +++ b/config-template.json @@ -1,5 +1,6 @@ { "open_ai_api_key": "YOUR API KEY", + "open_ai_api_base": "", "model": "gpt-3.5-turbo", "proxy": "", "single_chat_prefix": ["bot", "@bot"], @@ -14,4 +15,4 @@ "conversation_max_tokens": 1000, "expires_in_seconds": 3600, "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。" -} \ No newline at end of file +} diff --git a/config.py b/config.py index 56bff7e33..0520eb328 100644 --- a/config.py +++ b/config.py @@ -16,6 +16,7 @@ # chatgpt模型, 当use_azure_chatgpt为true时,其名称为Azure上model deployment名称 "model": "gpt-3.5-turbo", "use_azure_chatgpt": False, # 是否使用azure的chatgpt + "azure_deployment_id": "", #azure 模型部署名称 # Bot触发配置 "single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复 From f1e8344beb159241e281f966a541c7fd7d6225a9 Mon Sep 17 00:00:00 2001 From: lanvent Date: Sun, 9 Apr 2023 19:15:28 +0800 Subject: [PATCH 4/7] fix: no old signal handler --- app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 3e561aeb6..81dae855b 100644 --- a/app.py +++ b/app.py @@ -13,7 +13,8 @@ def sigterm_handler_wrap(_signo): def func(_signo, _stack_frame): logger.info("signal {} received, exiting...".format(_signo)) conf().save_user_datas() - return old_handler(_signo, _stack_frame) + if callable(old_handler): # check old_handler + return old_handler(_signo, _stack_frame) signal.signal(_signo, func) def run(): From fcfafb05f1d7cebf77eb67cfa9c1a95627c117c4 Mon Sep 17 00:00:00 2001 From: lanvent Date: Sun, 9 Apr 2023 19:57:43 +0800 Subject: [PATCH 5/7] fix: wechatmp's deadloop when reply is None from @JS00000 #789 --- channel/chat_channel.py | 5 +++++ channel/wechatmp/ServiceAccount.py | 8 ++++---- channel/wechatmp/SubscribeAccount.py | 29 +++++++++++++++++----------- channel/wechatmp/wechatmp_channel.py | 16 +++++++++------ 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/channel/chat_channel.py b/channel/chat_channel.py index 5000ae3de..eb64cfc4f 100644 --- a/channel/chat_channel.py +++ b/channel/chat_channel.py @@ -233,6 +233,9 @@ def _send(self, reply: Reply, context: Context, retry_cnt = 0): time.sleep(3+3*retry_cnt) self._send(reply, context, retry_cnt+1) + def _success_callback(self, session_id, **kwargs):# 线程正常结束时的回调函数 + logger.debug("Worker return success, session_id = {}".format(session_id)) + def _fail_callback(self, session_id, exception, **kwargs): # 线程异常结束时的回调函数 logger.exception("Worker return exception: {}".format(exception)) @@ -242,6 +245,8 @@ def func(worker:Future): worker_exception = worker.exception() if worker_exception: self._fail_callback(session_id, exception = worker_exception, **kwargs) + else: + self._success_callback(session_id, **kwargs) except CancelledError as e: logger.info("Worker cancelled, session_id = {}".format(session_id)) except Exception as e: diff --git a/channel/wechatmp/ServiceAccount.py b/channel/wechatmp/ServiceAccount.py index db9dff3e0..ae535ea0b 100644 --- a/channel/wechatmp/ServiceAccount.py +++ b/channel/wechatmp/ServiceAccount.py @@ -16,7 +16,7 @@ def GET(self): def POST(self): # Make sure to return the instance that first created, @singleton will do that. - channel_instance = WechatMPChannel() + channel = WechatMPChannel() try: webData = web.data() # logger.debug("[wechatmp] Receive request:\n" + webData.decode("utf-8")) @@ -27,14 +27,14 @@ def POST(self): message_id = wechatmp_msg.msg_id logger.info("[wechatmp] {}:{} Receive post query {} {}: {}".format(web.ctx.env.get('REMOTE_ADDR'), web.ctx.env.get('REMOTE_PORT'), from_user, message_id, message)) - context = channel_instance._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg) + context = channel._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg) if context: # set private openai_api_key # if from_user is not changed in itchat, this can be placed at chat_channel user_data = conf().get_user_data(from_user) context['openai_api_key'] = user_data.get('openai_api_key') # None or user openai_api_key - channel_instance.produce(context) - # The reply will be sent by channel_instance.send() in another thread + channel.produce(context) + # The reply will be sent by channel.send() in another thread return "success" elif wechatmp_msg.msg_type == 'event': diff --git a/channel/wechatmp/SubscribeAccount.py b/channel/wechatmp/SubscribeAccount.py index 745ef0e46..b1047c394 100644 --- a/channel/wechatmp/SubscribeAccount.py +++ b/channel/wechatmp/SubscribeAccount.py @@ -41,7 +41,8 @@ def POST(self): context = channel._compose_context(ContextType.TEXT, message, isgroup=False, msg=wechatmp_msg) logger.debug("[wechatmp] context: {} {}".format(context, wechatmp_msg)) if message_id in channel.received_msgs: # received and finished - return + # no return because of bandwords or other reasons + return "success" if supported and context: # set private openai_api_key # if from_user is not changed in itchat, this can be placed at chat_channel @@ -71,11 +72,12 @@ def POST(self): channel.query1[cache_key] = False channel.query2[cache_key] = False channel.query3[cache_key] = False - # Request again + # User request again, and the answer is not ready elif cache_key in channel.running and channel.query1.get(cache_key) == True and channel.query2.get(cache_key) == True and channel.query3.get(cache_key) == True: channel.query1[cache_key] = False #To improve waiting experience, this can be set to True. channel.query2[cache_key] = False #To improve waiting experience, this can be set to True. channel.query3[cache_key] = False + # User request again, and the answer is ready elif cache_key in channel.cache_dict: # Skip the waiting phase channel.query1[cache_key] = True @@ -89,7 +91,7 @@ def POST(self): logger.debug("[wechatmp] query1 {}".format(cache_key)) channel.query1[cache_key] = True cnt = 0 - while cache_key not in channel.cache_dict and cnt < 45: + while cache_key in channel.running and cnt < 45: cnt = cnt + 1 time.sleep(0.1) if cnt == 45: @@ -104,7 +106,7 @@ def POST(self): logger.debug("[wechatmp] query2 {}".format(cache_key)) channel.query2[cache_key] = True cnt = 0 - while cache_key not in channel.cache_dict and cnt < 45: + while cache_key in channel.running and cnt < 45: cnt = cnt + 1 time.sleep(0.1) if cnt == 45: @@ -119,7 +121,7 @@ def POST(self): logger.debug("[wechatmp] query3 {}".format(cache_key)) channel.query3[cache_key] = True cnt = 0 - while cache_key not in channel.cache_dict and cnt < 40: + while cache_key in channel.running and cnt < 40: cnt = cnt + 1 time.sleep(0.1) if cnt == 40: @@ -132,12 +134,17 @@ def POST(self): else: pass - if float(time.time()) - float(query_time) > 4.8: - reply_text = "【正在思考中,回复任意文字尝试获取回复】" - logger.info("[wechatmp] Timeout for {} {}, return".format(from_user, message_id)) - replyPost = reply.TextMsg(from_user, to_user, reply_text).send() - return replyPost - + + if cache_key not in channel.cache_dict and cache_key not in channel.running: + # no return because of bandwords or other reasons + return "success" + + # if float(time.time()) - float(query_time) > 4.8: + # reply_text = "【正在思考中,回复任意文字尝试获取回复】" + # logger.info("[wechatmp] Timeout for {} {}, return".format(from_user, message_id)) + # replyPost = reply.TextMsg(from_user, to_user, reply_text).send() + # return replyPost + if cache_key in channel.cache_dict: content = channel.cache_dict[cache_key] if len(content.encode('utf8'))<=MAX_UTF8_LEN: diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index 49f45e013..c56c1cb3b 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -97,8 +97,7 @@ def send(self, reply: Reply, context: Context): if self.passive_reply: receiver = context["receiver"] self.cache_dict[receiver] = reply.content - self.running.remove(receiver) - logger.debug("[send] reply to {} saved to cache: {}".format(receiver, reply)) + logger.info("[send] reply to {} saved to cache: {}".format(receiver, reply)) else: receiver = context["receiver"] reply_text = reply.content @@ -116,10 +115,15 @@ def send(self, reply: Reply, context: Context): return - def _fail_callback(self, session_id, exception, context, **kwargs): - logger.exception("[wechatmp] Fail to generation message to user, msgId={}, exception={}".format(context['msg'].msg_id, exception)) - assert session_id not in self.cache_dict - self.running.remove(session_id) + def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数 + logger.debug("[wechatmp] Success to generate reply, msgId={}".format(context['msg'].msg_id)) + if self.passive_reply: + self.running.remove(session_id) + def _fail_callback(self, session_id, exception, context, **kwargs): # 线程异常结束时的回调函数 + logger.exception("[wechatmp] Fail to generate reply to user, msgId={}, exception={}".format(context['msg'].msg_id, exception)) + if self.passive_reply: + assert session_id not in self.cache_dict + self.running.remove(session_id) From 48c08f4aad60c6e7c6e4deba1d0c13f43863986a Mon Sep 17 00:00:00 2001 From: lanvent Date: Mon, 10 Apr 2023 14:50:34 +0800 Subject: [PATCH 6/7] unset default timeout --- bot/chatgpt/chat_gpt_bot.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/chat_gpt_bot.py index b7b86a6b1..b3926592d 100644 --- a/bot/chatgpt/chat_gpt_bot.py +++ b/bot/chatgpt/chat_gpt_bot.py @@ -3,13 +3,12 @@ from bot.bot import Bot from bot.chatgpt.chat_gpt_session import ChatGPTSession from bot.openai.open_ai_image import OpenAIImage -from bot.session_manager import Session, SessionManager +from bot.session_manager import SessionManager from bridge.context import ContextType from bridge.reply import Reply, ReplyType from config import conf, load_config from common.log import logger from common.token_bucket import TokenBucket -from common.expired_dict import ExpiredDict import openai import openai.error import time @@ -91,8 +90,8 @@ def compose_args(self): "top_p":1, "frequency_penalty":conf().get('frequency_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容 "presence_penalty":conf().get('presence_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容 - "request_timeout": conf().get('request_timeout', 60), # 请求超时时间,openai接口默认设置为600,对于难问题一般需要较长时间 - "timeout": conf().get('request_timeout', 120), #重试超时时间,在这个时间内,将会自动重试 + "request_timeout": conf().get('request_timeout', None), # 请求超时时间,openai接口默认设置为600,对于难问题一般需要较长时间 + "timeout": conf().get('request_timeout', None), #重试超时时间,在这个时间内,将会自动重试 } def reply_text(self, session:ChatGPTSession, session_id, api_key, retry_count=0) -> dict: From ee91c86a29b03d1e8b956c55f121abd6b82f2dec Mon Sep 17 00:00:00 2001 From: lanvent Date: Mon, 10 Apr 2023 14:52:06 +0800 Subject: [PATCH 7/7] Update README.md --- plugins/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/README.md b/plugins/README.md index 7cb61bff7..1f07a2cbe 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1,7 +1,7 @@ **Table of Content** - [插件化初衷](#插件化初衷) -- [插件安装方法](#插件化安装方法) +- [插件安装方法](#插件安装方法) - [插件化实现](#插件化实现) - [插件编写示例](#插件编写示例) - [插件设计建议](#插件设计建议) @@ -52,6 +52,8 @@ 以下是它们的默认处理逻辑(太长不看,可跳到[插件编写示例](#插件编写示例)): +**注意以下包含的代码是`v1.1.0`中的片段,已过时,只可用于理解事件,最新的默认代码逻辑请参考[chat_channel](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/channel/chat_channel.py)** + #### 1. 收到消息 负责接收用户消息,根据用户的配置,判断本条消息是否触发机器人。如果触发,则会判断该消息的类型(声音、文本、画图命令等),将消息包装成如下的`Context`交付给下一个步骤。 @@ -91,9 +93,9 @@ if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: reply = super().build_reply_content(context.content, context) #文字跟画图交付给chatgpt elif context.type == ContextType.VOICE: # 声音先进行语音转文字后,修改Context类型为文字后,再交付给chatgpt - msg = context['msg'] - file_name = TmpDir().path() + context.content - msg.download(file_name) + cmsg = context['msg'] + cmsg.prepare() + file_name = context.content reply = super().build_voice_to_text(file_name) if reply.type != ReplyType.ERROR and reply.type != ReplyType.INFO: context.content = reply.content # 语音转文字后,将文字内容作为新的context