From 4ce37f84e4f7f732636676aa74261b90f5d2a245 Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sun, 19 Nov 2023 22:42:44 +0800 Subject: [PATCH 01/35] feat: support Tongyi Qwen model of alibaba --- bot/bot_factory.py | 4 + bot/tongyi/tongyi_qwen_bot.py | 185 ++++++++++++++++++++++++++++++++++ common/const.py | 1 + config.py | 8 +- requirements-optional.txt | 3 + 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 bot/tongyi/tongyi_qwen_bot.py diff --git a/bot/bot_factory.py b/bot/bot_factory.py index da12f95..f618479 100644 --- a/bot/bot_factory.py +++ b/bot/bot_factory.py @@ -43,4 +43,8 @@ def create_bot(bot_type): elif bot_type == const.CLAUDEAI: from bot.claude.claude_ai_bot import ClaudeAIBot return ClaudeAIBot() + + elif bot_type == const.TONGYI: + from bot.tongyi.tongyi_qwen_bot import TongyiQwenBot + return TongyiQwenBot() raise RuntimeError diff --git a/bot/tongyi/tongyi_qwen_bot.py b/bot/tongyi/tongyi_qwen_bot.py new file mode 100644 index 0000000..d123dae --- /dev/null +++ b/bot/tongyi/tongyi_qwen_bot.py @@ -0,0 +1,185 @@ +# encoding:utf-8 + +import json +import time +from typing import List, Tuple + +import openai +import openai.error +import broadscope_bailian +from broadscope_bailian import ChatQaMessage + +from bot.bot import Bot +from bot.baidu.baidu_wenxin_session import BaiduWenxinSession +from bot.session_manager import SessionManager +from bridge.context import ContextType +from bridge.reply import Reply, ReplyType +from common.log import logger +from config import conf, load_config + +class TongyiQwenBot(Bot): + def __init__(self): + super().__init__() + self.access_key_id = conf().get("tongyi_access_key_id") + self.access_key_secret = conf().get("tongyi_access_key_secret") + self.agent_key = conf().get("tongyi_agent_key") + self.app_id = conf().get("tongyi_app_id") + self.node_id = conf().get("tongyi_node_id") + 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 "tongyi") + self.temperature = conf().get("temperature", 0.2) # 值在[0,1]之间,越大表示回复越具有不确定性 + self.top_p = conf().get("top_p", 1) + + def reply(self, query, context=None): + # acquire reply content + if context.type == ContextType.TEXT: + logger.info("[TONGYI] query={}".format(query)) + + session_id = context["session_id"] + reply = None + clear_memory_commands = conf().get("clear_memory_commands", ["#清除记忆"]) + if query in clear_memory_commands: + self.sessions.clear_session(session_id) + reply = Reply(ReplyType.INFO, "记忆已清除") + elif query == "#清除所有": + self.sessions.clear_all_session() + reply = Reply(ReplyType.INFO, "所有人记忆已清除") + elif query == "#更新配置": + load_config() + reply = Reply(ReplyType.INFO, "配置已更新") + if reply: + return reply + session = self.sessions.session_query(query, session_id) + logger.debug("[TONGYI] session query={}".format(session.messages)) + + reply_content = self.reply_text(session) + logger.debug( + "[TONGYI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format( + session.messages, + session_id, + reply_content["content"], + reply_content["completion_tokens"], + ) + ) + if reply_content["completion_tokens"] == 0 and len(reply_content["content"]) > 0: + reply = Reply(ReplyType.ERROR, reply_content["content"]) + elif reply_content["completion_tokens"] > 0: + self.sessions.session_reply(reply_content["content"], session_id, reply_content["total_tokens"]) + 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)) + return reply + + else: + reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type)) + return reply + + def reply_text(self, session: BaiduWenxinSession, retry_count=0) -> dict: + """ + call bailian's ChatCompletion to get the answer + :param session: a conversation session + :param retry_count: retry count + :return: {} + """ + 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) + 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, + "completion_tokens": completion_tokens, + "content": completion_content, + } + except Exception as e: + need_retry = retry_count < 2 + result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"} + if isinstance(e, openai.error.RateLimitError): + logger.warn("[TONGYI] RateLimitError: {}".format(e)) + result["content"] = "提问太快啦,请休息一下再问我吧" + if need_retry: + time.sleep(20) + elif isinstance(e, openai.error.Timeout): + logger.warn("[TONGYI] Timeout: {}".format(e)) + result["content"] = "我没有收到你的消息" + if need_retry: + time.sleep(5) + elif isinstance(e, openai.error.APIError): + logger.warn("[TONGYI] Bad Gateway: {}".format(e)) + result["content"] = "请再问我一次" + if need_retry: + time.sleep(10) + elif isinstance(e, openai.error.APIConnectionError): + logger.warn("[TONGYI] APIConnectionError: {}".format(e)) + need_retry = False + result["content"] = "我连接不到你的网络" + else: + logger.exception("[TONGYI] Exception: {}".format(e)) + need_retry = False + self.sessions.clear_session(session.session_id) + + if need_retry: + logger.warn("[TONGYI] 第{}次重试".format(retry_count + 1)) + return self.reply_text(session, retry_count + 1) + else: + return result + + def set_api_key(self): + 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() + + def convert_messages_format(self, messages) -> Tuple[str, List[ChatQaMessage]]: + history = [] + user_content = '' + assistant_content = '' + for message in messages: + role = message.get('role') + if role == 'user': + user_content += message.get('content') + elif role == 'assistant': + assistant_content = message.get('content') + history.append(ChatQaMessage(user_content, assistant_content)) + user_content = '' + assistant_content = '' + if user_content == '': + raise Exception('no user message') + return user_content, history + + def get_completion_content(self, response, node_id): + text = response['Data']['Text'] + if node_id == '': + return text + # TODO: 当使用流程编排创建大模型应用时,响应结构如下,最终结果在['finalResult'][node_id]['response']['text']中,暂时先这么写 + # { + # 'Success': True, + # 'Code': None, + # 'Message': None, + # 'Data': { + # 'ResponseId': '9822f38dbacf4c9b8daf5ca03a2daf15', + # 'SessionId': 'session_id', + # 'Text': '{"finalResult":{"LLM_T7islK":{"params":{"modelId":"qwen-plus-v1","prompt":"${systemVars.query}${bizVars.Text}"},"response":{"text":"作为一个AI语言模型,我没有年龄,因为我没有生日。\n我只是一个程序,没有生命和身体。"}}}}', + # 'Thoughts': [], + # 'Debug': {}, + # 'DocReferences': [] + # }, + # 'RequestId': '8e11d31551ce4c3f83f49e6e0dd998b0', + # 'Failed': None + # } + text_dict = json.loads(text) + completion_content = text_dict['finalResult'][node_id]['response']['text'] + return completion_content + + def calc_tokens(self, messages, completion_content): + completion_tokens = len(completion_content) + prompt_tokens = 0 + for message in messages: + prompt_tokens += len(message["content"]) + return completion_tokens, prompt_tokens + completion_tokens diff --git a/common/const.py b/common/const.py index a46765e..ef3b315 100644 --- a/common/const.py +++ b/common/const.py @@ -6,6 +6,7 @@ XUNFEI = "xunfei" CHATGPTONAZURE = "chatGPTOnAzure" LINKAI = "linkai" CLAUDEAI = "claude" +TONGYI = "tongyi" # model GPT35 = "gpt-3.5-turbo" diff --git a/config.py b/config.py index 8db08cd..95a2a31 100644 --- a/config.py +++ b/config.py @@ -16,7 +16,7 @@ available_setting = { "open_ai_api_base": "https://api.openai.com/v1", "proxy": "", # openai使用的代理 # chatgpt模型, 当use_azure_chatgpt为true时,其名称为Azure上model deployment名称 - "model": "gpt-3.5-turbo", # 还支持 gpt-4, gpt-4-turbo, wenxin, xunfei + "model": "gpt-3.5-turbo", # 还支持 gpt-4, gpt-4-turbo, wenxin, xunfei, tongyi "use_azure_chatgpt": False, # 是否使用azure的chatgpt "azure_deployment_id": "", # azure 模型部署名称 "azure_api_version": "", # azure api版本 @@ -65,6 +65,12 @@ available_setting = { # claude 配置 "claude_api_cookie": "", "claude_uuid": "", + # 通义千问API, 获取方式查看文档 https://help.aliyun.com/document_detail/2587494.html + "tongyi_access_key_id": "", + "tongyi_access_key_secret": "", + "tongyi_agent_key": "", + "tongyi_app_id": "", + "tongyi_node_id": "", # 流程编排模型用到的id,如果没有用到tongyi_node_id,请务必保持为空字符串 # wework的通用配置 "wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开 # 语音设置 diff --git a/requirements-optional.txt b/requirements-optional.txt index 5633274..c070f97 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -30,3 +30,6 @@ websocket-client==1.2.0 # claude bot curl_cffi + +# tongyi qwen +broadscope_bailian From 8d07ba6332f78ce276d8a0f1daa075a6484ada67 Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sun, 19 Nov 2023 23:00:18 +0800 Subject: [PATCH 02/35] fix: add tongyi type when init bridge --- bridge/bridge.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bridge/bridge.py b/bridge/bridge.py index ba6e568..ea8de13 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -35,6 +35,8 @@ class Bridge(object): self.btype["text_to_voice"] = const.LINKAI if model_type in ["claude"]: self.btype["chat"] = const.CLAUDEAI + if model_type in ["tongyi"]: + self.btype["chat"] = const.TONGYI self.bots = {} self.chat_bots = {} From c1022feab8fc6eb6c5f2ab3b0b42096aa1273f50 Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sat, 25 Nov 2023 10:06:10 +0800 Subject: [PATCH 03/35] fix: add tongyi model to model list --- common/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/const.py b/common/const.py index ef3b315..a2841f7 100644 --- a/common/const.py +++ b/common/const.py @@ -17,4 +17,4 @@ 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] +MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4", "xunfei", "claude", "gpt-4-turbo", GPT4_TURBO_PREVIEW, "tongyi"] From 0b2ce485860c9bbf53ef422466f3a4b4dfbc7552 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 27 Nov 2023 18:20:52 +0800 Subject: [PATCH 04/35] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f46c357..ff47ae3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Demo made by [Visionn](https://www.wangpc.cc/) # 更新日志 +>**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` 模型接入,完善图像理解&生成、语音识别&生成的多模态能力 >**2023.10.16:** 支持通过意图识别使用LinkAI联网搜索、数学计算、网页访问等插件,参考[插件文档](https://docs.link-ai.tech/platform/plugins) From 291f9360979ff8415e3a2154b4a5d4962ebc8c70 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 27 Nov 2023 20:24:42 +0800 Subject: [PATCH 05/35] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ff47ae3..30d2f52 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ 最新版本支持的功能如下: -- [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式 +- [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信、微信公众号和、业微信、飞书等部署方式 - [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, 文心一言, 讯飞星火 -- [x] **语音识别:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai(whisper/tts) 等多种语音模型 -- [x] **图片生成:** 支持图片生成 和 图生图(如照片修复),可选择 Dall-E, stable diffusion, replicate, midjourney模型 +- [x] **语音能力:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai(whisper/tts) 等多种语音模型 +- [x] **图像能力:** 支持图片生成、图片识别、图生图(如照片修复),可选择 Dall-E-3, stable diffusion, replicate, midjourney, vision模型 - [x] **丰富插件:** 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结、文档总结和对话等插件 - [X] **Tool工具:** 与操作系统和互联网交互,支持最新信息搜索、数学计算、天气和资讯查询、网页总结,基于 [chatgpt-tool-hub](https://github.com/goldfishh/chatgpt-tool-hub) 实现 - [x] **知识库:** 通过上传知识库文件自定义专属机器人,可作为数字分身、领域知识库、智能客服使用,基于 [LinkAI](https://link-ai.tech/console) 实现 @@ -54,6 +54,8 @@ Demo made by [Visionn](https://www.wangpc.cc/) # 快速开始 +快速开始文档:[项目搭建文档](https://docs.link-ai.tech/cow/quick-start) + ## 准备 ### 1. 账号注册 From 6e0d2f9437a4e37a6b3cefcef0f021030ecc12bb Mon Sep 17 00:00:00 2001 From: zhayujie Date: Tue, 28 Nov 2023 16:29:32 +0800 Subject: [PATCH 06/35] fix: remove unuse log and add plugin config in docker config --- bot/linkai/link_ai_bot.py | 2 +- plugins/config.json.template | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/linkai/link_ai_bot.py b/bot/linkai/link_ai_bot.py index 22f5172..6815eca 100644 --- a/bot/linkai/link_ai_bot.py +++ b/bot/linkai/link_ai_bot.py @@ -362,7 +362,7 @@ class LinkAISessionManager(SessionManager): try: max_tokens = conf().get("conversation_max_tokens", 2500) tokens_cnt = session.discard_exceeding(max_tokens, total_tokens) - logger.info(f"[LinkAI] chat history discard, before tokens={total_tokens}, now tokens={tokens_cnt}") + logger.debug(f"[LinkAI] chat history, before tokens={total_tokens}, now tokens={tokens_cnt}") except Exception as e: logger.warning("Exception when counting tokens precisely for session: {}".format(str(e))) return session diff --git a/plugins/config.json.template b/plugins/config.json.template index 3334a62..95a59bc 100644 --- a/plugins/config.json.template +++ b/plugins/config.json.template @@ -33,6 +33,12 @@ "max_tasks": 3, "max_tasks_per_user": 1, "use_image_create_prefix": true + }, + "summary": { + "enabled": true, + "group_enabled": true, + "max_file_size": 5000, + "type": ["FILE", "SHARING"] } } } From 865e4b534959bdbc5db91b96b1d5f8e6a57d2649 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 29 Nov 2023 17:41:14 +0800 Subject: [PATCH 07/35] feat: hello plugin support system prompt --- bot/linkai/link_ai_bot.py | 9 +++------ channel/chat_channel.py | 2 -- plugins/hello/hello.py | 4 +++- plugins/linkai/midjourney.py | 2 ++ 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bot/linkai/link_ai_bot.py b/bot/linkai/link_ai_bot.py index 6815eca..65cfe4a 100644 --- a/bot/linkai/link_ai_bot.py +++ b/bot/linkai/link_ai_bot.py @@ -370,12 +370,9 @@ class LinkAISessionManager(SessionManager): class LinkAISession(ChatGPTSession): def calc_tokens(self): - try: - cur_tokens = super().calc_tokens() - except Exception as e: - logger.debug("Exception when counting tokens precisely for query: {}".format(e)) - cur_tokens = len(str(self.messages)) - return cur_tokens + if not self.messages: + return 0 + return len(str(self.messages)) def discard_exceeding(self, max_tokens, cur_tokens=None): cur_tokens = self.calc_tokens() diff --git a/channel/chat_channel.py b/channel/chat_channel.py index c664435..ba017af 100644 --- a/channel/chat_channel.py +++ b/channel/chat_channel.py @@ -184,8 +184,6 @@ class ChatChannel(Channel): reply = e_context["reply"] if not e_context.is_pass(): logger.debug("[WX] ready to handle context: type={}, content={}".format(context.type, context.content)) - if e_context.is_break(): - context["generate_breaked_by"] = e_context["breaked_by"] if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: # 文字和图片消息 context["channel"] = e_context["channel"] reply = super().build_reply_content(context.content, context) diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py index dcc248f..007df6d 100644 --- a/plugins/hello/hello.py +++ b/plugins/hello/hello.py @@ -22,6 +22,7 @@ class Hello(Plugin): super().__init__() self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context logger.info("[Hello] inited") + self.config = super().load_config() def on_handle_context(self, e_context: EventContext): if e_context["context"].type not in [ @@ -30,7 +31,8 @@ class Hello(Plugin): ContextType.PATPAT, ]: return - + if not self.config or not self.config.get("use_character_desc"): + e_context["context"]["generate_breaked_by"] = EventAction.BREAK if e_context["context"].type == ContextType.JOIN_GROUP: if "group_welcome_msg" in conf(): reply = Reply() diff --git a/plugins/linkai/midjourney.py b/plugins/linkai/midjourney.py index 76395bd..9c6c57b 100644 --- a/plugins/linkai/midjourney.py +++ b/plugins/linkai/midjourney.py @@ -88,6 +88,8 @@ class MJBot: context = e_context['context'] if context.type == ContextType.TEXT: cmd_list = context.content.split(maxsplit=1) + if not cmd_list: + return None if cmd_list[0].lower() == f"{trigger_prefix}mj": return TaskType.GENERATE elif cmd_list[0].lower() == f"{trigger_prefix}mju": From 5f19f37dcb20c1649d7b64a532cf7043cbd80103 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 29 Nov 2023 23:15:31 +0800 Subject: [PATCH 08/35] feat: hello plugin support app code --- bot/linkai/link_ai_bot.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/bot/linkai/link_ai_bot.py b/bot/linkai/link_ai_bot.py index 65cfe4a..f308370 100644 --- a/bot/linkai/link_ai_bot.py +++ b/bot/linkai/link_ai_bot.py @@ -5,6 +5,7 @@ import time import requests +import config from bot.bot import Bot from bot.chatgpt.chat_gpt_session import ChatGPTSession from bot.session_manager import SessionManager @@ -60,7 +61,8 @@ class LinkAIBot(Bot): logger.info(f"[LINKAI] won't set appcode because a plugin ({context['generate_breaked_by']}) affected the context") app_code = None else: - app_code = context.kwargs.get("app_code") or conf().get("linkai_app_code") + plugin_app_code = self._find_group_mapping_code(context) + app_code = context.kwargs.get("app_code") or plugin_app_code or conf().get("linkai_app_code") linkai_api_key = conf().get("linkai_api_key") session_id = context["session_id"] @@ -164,6 +166,18 @@ class LinkAIBot(Bot): except Exception as e: logger.exception(e) + def _find_group_mapping_code(self, context): + try: + if context.kwargs.get("isgroup"): + group_name = context.kwargs.get("msg").from_user_nickname + if config.plugin_config and config.plugin_config.get("linkai"): + linkai_config = config.plugin_config.get("linkai") + group_mapping = linkai_config.get("group_app_map") + if group_mapping and group_name: + return group_mapping.get(group_name) + except Exception as e: + logger.exception(e) + return None def _build_vision_msg(self, query: str, path: str): try: From 88fb3dbf60d6c5c017fb9017ecefefed45a461bc Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 30 Nov 2023 11:51:04 +0800 Subject: [PATCH 09/35] fix: generate break by bug --- plugins/hello/hello.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py index 007df6d..a0004be 100644 --- a/plugins/hello/hello.py +++ b/plugins/hello/hello.py @@ -31,8 +31,6 @@ class Hello(Plugin): ContextType.PATPAT, ]: return - if not self.config or not self.config.get("use_character_desc"): - e_context["context"]["generate_breaked_by"] = EventAction.BREAK if e_context["context"].type == ContextType.JOIN_GROUP: if "group_welcome_msg" in conf(): reply = Reply() @@ -40,6 +38,8 @@ class Hello(Plugin): reply.content = conf().get("group_welcome_msg", "") e_context["reply"] = reply e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑 + if not self.config or not self.config.get("use_character_desc"): + e_context["context"]["generate_breaked_by"] = EventAction.BREAK_PASS return e_context["context"].type = ContextType.TEXT msg: ChatMessage = e_context["context"]["msg"] @@ -52,6 +52,8 @@ class Hello(Plugin): msg: ChatMessage = e_context["context"]["msg"] e_context["context"].content = f"请你随机使用一种风格介绍你自己,并告诉用户输入#help可以查看帮助信息。" e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 + if not self.config or not self.config.get("use_character_desc"): + e_context["context"]["generate_breaked_by"] = EventAction.BREAK return content = e_context["context"].content From 65424c7db94e7f3d2d5688ddb27299226f4f342d Mon Sep 17 00:00:00 2001 From: malsony Date: Fri, 1 Dec 2023 16:09:15 +0800 Subject: [PATCH 10/35] Update xunfei_spark_bot.py update API URL for v3.0 version of Xunfei Spark. --- bot/xunfei/xunfei_spark_bot.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bot/xunfei/xunfei_spark_bot.py b/bot/xunfei/xunfei_spark_bot.py index ed441bf..395d81e 100644 --- a/bot/xunfei/xunfei_spark_bot.py +++ b/bot/xunfei/xunfei_spark_bot.py @@ -40,10 +40,13 @@ class XunFeiBot(Bot): self.app_id = conf().get("xunfei_app_id") self.api_key = conf().get("xunfei_api_key") self.api_secret = conf().get("xunfei_api_secret") - # 默认使用v3.0版本,2.0版本可设置为generalv2, 1.5版本可设置为 general - self.domain = "generalv2" - # 默认使用v3.0版本,1.5版本可设置为 "ws://spark-api.xf-yun.com/v1.1/chat", - # 2.0版本可设置为 "ws://spark-api.xf-yun.com/v2.1/chat" + # 默认使用v2.0版本: "generalv2" + # v1.5版本为 "general" + # v3.0版本为: "generalv3" + self.domain = "generalv3" + # 默认使用v2.0版本: "ws://spark-api.xf-yun.com/v2.1/chat" + # v1.5版本为: "ws://spark-api.xf-yun.com/v1.1/chat" + # v3.0版本为: "ws://spark-api.xf-yun.com/v3.1/chat" self.spark_url = "ws://spark-api.xf-yun.com/v3.1/chat" self.host = urlparse(self.spark_url).netloc self.path = urlparse(self.spark_url).path From d89b0568860a87fa5c82efe43ee8c2f7db6e6671 Mon Sep 17 00:00:00 2001 From: chazzjimel <126439838+chazzjimel@users.noreply.github.com> Date: Sun, 3 Dec 2023 18:19:03 +0800 Subject: [PATCH 11/35] add ali voice output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加阿里云语音输出支持。 --- voice/ali/ali_api.py | 114 +++++++++++++++++++++++++++++++++ voice/ali/ali_voice.py | 74 +++++++++++++++++++++ voice/ali/config.json.template | 6 ++ voice/factory.py | 10 +-- 4 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 voice/ali/ali_api.py create mode 100644 voice/ali/ali_voice.py create mode 100644 voice/ali/config.json.template diff --git a/voice/ali/ali_api.py b/voice/ali/ali_api.py new file mode 100644 index 0000000..9d366a0 --- /dev/null +++ b/voice/ali/ali_api.py @@ -0,0 +1,114 @@ +# coding=utf-8 +""" +Author: chazzjimel +Email: chazzjimel@gmail.com +wechat:cheung-z-x + +Description: + +""" +import json +import time + +import requests +import datetime +import hashlib +import hmac +import base64 +import urllib.parse +import uuid + +from common.log import logger +from common.tmp_dir import TmpDir + + +def text_to_speech_aliyun(url, text, appkey, token): + # 请求的headers + headers = { + "Content-Type": "application/json", + } + + # 请求的payload + data = { + "text": text, + "appkey": appkey, + "token": token, + "format": "wav" + } + + # 发送POST请求 + response = requests.post(url, headers=headers, data=json.dumps(data)) + + # 检查响应状态码和内容类型 + if response.status_code == 200 and response.headers['Content-Type'] == 'audio/mpeg': + # 构造唯一的文件名 + output_file = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".wav" + + # 将响应内容写入文件 + with open(output_file, 'wb') as file: + file.write(response.content) + logger.debug(f"音频文件保存成功,文件名:{output_file}") + else: + # 打印错误信息 + logger.debug("响应状态码: {}".format(response.status_code)) + logger.debug("响应内容: {}".format(response.text)) + output_file = None + + return output_file + + +class AliyunTokenGenerator: + def __init__(self, access_key_id, access_key_secret): + self.access_key_id = access_key_id + self.access_key_secret = access_key_secret + + def sign_request(self, parameters): + # 将参数排序 + sorted_params = sorted(parameters.items()) + + # 构造待签名的字符串 + canonicalized_query_string = '' + for (k, v) in sorted_params: + canonicalized_query_string += '&' + self.percent_encode(k) + '=' + self.percent_encode(v) + + string_to_sign = 'GET&%2F&' + self.percent_encode(canonicalized_query_string[1:]) # 使用GET方法 + + # 计算签名 + h = hmac.new((self.access_key_secret + "&").encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1) + signature = base64.encodebytes(h.digest()).strip() + + return signature + + def percent_encode(self, encode_str): + encode_str = str(encode_str) + res = urllib.parse.quote(encode_str, '') + res = res.replace('+', '%20') + res = res.replace('*', '%2A') + res = res.replace('%7E', '~') + return res + + def get_token(self): + # 设置请求参数 + params = { + 'Format': 'JSON', + 'Version': '2019-02-28', + 'AccessKeyId': self.access_key_id, + 'SignatureMethod': 'HMAC-SHA1', + 'Timestamp': datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + 'SignatureVersion': '1.0', + 'SignatureNonce': str(uuid.uuid4()), # 使用uuid生成唯一的随机数 + 'Action': 'CreateToken', + 'RegionId': 'cn-shanghai' + } + + # 计算签名 + signature = self.sign_request(params) + params['Signature'] = signature + + # 构造请求URL + url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?' + urllib.parse.urlencode(params) + + # 发送请求 + response = requests.get(url) + + return response.text diff --git a/voice/ali/ali_voice.py b/voice/ali/ali_voice.py new file mode 100644 index 0000000..c9e90cc --- /dev/null +++ b/voice/ali/ali_voice.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" +Author: chazzjimel +Email: chazzjimel@gmail.com +wechat:cheung-z-x + +Description: +ali voice service + +""" +import json +import os +import re +import time + +from bridge.reply import Reply, ReplyType +from common.log import logger +from voice.voice import Voice +from voice.ali.ali_api import AliyunTokenGenerator +from voice.ali.ali_api import text_to_speech_aliyun + + +def textContainsEmoji(text): + # 此正则表达式匹配大多数表情符号和特殊字符 + pattern = re.compile( + '[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF\U00002702-\U000027B0\U00002600-\U000026FF]') + return bool(pattern.search(text)) + + +class AliVoice(Voice): + def __init__(self): + try: + curdir = os.path.dirname(__file__) + config_path = os.path.join(curdir, "config.json") + with open(config_path, "r") as fr: + config = json.load(fr) + self.token = None + self.token_expire_time = 0 + self.api_url = config.get("api_url") + self.appkey = config.get("appkey") + self.access_key_id = config.get("access_key_id") + self.access_key_secret = config.get("access_key_secret") + except Exception as e: + logger.warn("AliVoice init failed: %s, ignore " % e) + + # def voiceToText(self, voice_file): + # pass + + def textToVoice(self, text): + text = re.sub(r'[^\u4e00-\u9fa5\u3040-\u30FF\uAC00-\uD7AFa-zA-Z0-9' + r'äöüÄÖÜáéíóúÁÉÍÓÚàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛçÇñÑ,。!?,.]', '', text) + # 提取 token_id 值 + token_id = self.get_valid_token() + fileName = text_to_speech_aliyun(self.api_url, text, self.appkey, token_id) + if fileName: + logger.info("[Ali] textToVoice text={} voice file name={}".format(text, fileName)) + reply = Reply(ReplyType.VOICE, fileName) + else: + reply = Reply(ReplyType.ERROR, "抱歉,语音合成失败") + return reply + + def get_valid_token(self): + current_time = time.time() + if self.token is None or current_time >= self.token_expire_time: + get_token = AliyunTokenGenerator(self.access_key_id, self.access_key_secret) + token_str = get_token.get_token() + token_data = json.loads(token_str) + self.token = token_data["Token"]["Id"] + # 将过期时间减少一小段时间(例如5分钟),以避免在边界条件下的过期 + self.token_expire_time = token_data["Token"]["ExpireTime"] - 300 + logger.debug(f"新获取的阿里云token:{self.token}") + else: + logger.debug("使用缓存的token") + return self.token \ No newline at end of file diff --git a/voice/ali/config.json.template b/voice/ali/config.json.template new file mode 100644 index 0000000..7f8d0e6 --- /dev/null +++ b/voice/ali/config.json.template @@ -0,0 +1,6 @@ +{ + "api_url": "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/tts", + "appkey": "", + "access_key_id": "", + "access_key_secret": "" +} \ No newline at end of file diff --git a/voice/factory.py b/voice/factory.py index 8725e29..01229eb 100644 --- a/voice/factory.py +++ b/voice/factory.py @@ -29,12 +29,8 @@ def create_voice(voice_type): from voice.azure.azure_voice import AzureVoice return AzureVoice() - elif voice_type == "elevenlabs": - from voice.elevent.elevent_voice import ElevenLabsVoice + elif voice_type == "ali": + from voice.ali.ali_voice import AliVoice - return ElevenLabsVoice() - - elif voice_type == "linkai": - from voice.linkai.linkai_voice import LinkAIVoice - return LinkAIVoice() + return AliVoice() raise RuntimeError From f4f5be5b085ef9d03cdc9f74521fc22e2ceda258 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 4 Dec 2023 11:14:55 +0800 Subject: [PATCH 12/35] Create LICENSE --- lib/itchat/LICENSE | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/itchat/LICENSE diff --git a/lib/itchat/LICENSE b/lib/itchat/LICENSE new file mode 100644 index 0000000..ba1a0e2 --- /dev/null +++ b/lib/itchat/LICENSE @@ -0,0 +1,9 @@ +**The MIT License (MIT)** + +Copyright (c) 2017 LittleCoder ([littlecodersh@Github](https://github.com/littlecodersh)) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 36f9680eec870be244afe5c77ea5bf05c0ee3566 Mon Sep 17 00:00:00 2001 From: erayyym Date: Tue, 5 Dec 2023 03:58:42 -0500 Subject: [PATCH 13/35] =?UTF-8?q?adding=20features:=20=E9=80=80=E7=BE=A4?= =?UTF-8?q?=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后面还打算想办法加用户自己退出的提醒,目前版本是可以在群主(且群主/管理员自己是bot)踢人时候发出提醒 --- bridge/context.py | 2 ++ channel/wechat/wechat_channel.py | 2 +- channel/wechat/wechat_message.py | 14 +++++++++++--- plugins/hello/hello.py | 8 ++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/bridge/context.py b/bridge/context.py index 1e5958c..04d6320 100644 --- a/bridge/context.py +++ b/bridge/context.py @@ -16,6 +16,8 @@ class ContextType(Enum): JOIN_GROUP = 20 # 加入群聊 PATPAT = 21 # 拍了拍 FUNCTION = 22 # 函数调用 + EXIT_GROUP = 23 #退出 + def __str__(self): return self.name diff --git a/channel/wechat/wechat_channel.py b/channel/wechat/wechat_channel.py index 0989a85..db77d83 100644 --- a/channel/wechat/wechat_channel.py +++ b/channel/wechat/wechat_channel.py @@ -170,7 +170,7 @@ class WechatChannel(ChatChannel): 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)) - elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.ACCEPT_FRIEND]: + elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.ACCEPT_FRIEND, ContextType.EXIT_GROUP]: logger.debug("[WX]receive note msg: {}".format(cmsg.content)) elif cmsg.ctype == ContextType.TEXT: # logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg)) diff --git a/channel/wechat/wechat_message.py b/channel/wechat/wechat_message.py index e102018..b8b1d91 100644 --- a/channel/wechat/wechat_message.py +++ b/channel/wechat/wechat_message.py @@ -27,13 +27,21 @@ class WechatMessage(ChatMessage): self._prepare_fn = lambda: itchat_msg.download(self.content) elif itchat_msg["Type"] == NOTE and itchat_msg["MsgType"] == 10000: if is_group and ("加入群聊" in itchat_msg["Content"] or "加入了群聊" in itchat_msg["Content"]): - self.ctype = ContextType.JOIN_GROUP - self.content = itchat_msg["Content"] # 这里只能得到nickname, actual_user_id还是机器人的id if "加入了群聊" in itchat_msg["Content"]: + self.ctype = ContextType.JOIN_GROUP + self.content = itchat_msg["Content"] self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[-1] elif "加入群聊" in itchat_msg["Content"]: + self.ctype = ContextType.JOIN_GROUP + self.content = itchat_msg["Content"] self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0] + + elif is_group and ("移出了群聊" in itchat_msg["Content"]): + self.ctype = ContextType.EXIT_GROUP + self.content = itchat_msg["Content"] + self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0] + elif "你已添加了" in itchat_msg["Content"]: #通过好友请求 self.ctype = ContextType.ACCEPT_FRIEND self.content = itchat_msg["Content"] @@ -90,5 +98,5 @@ class WechatMessage(ChatMessage): if self.is_group: self.is_at = itchat_msg["IsAt"] self.actual_user_id = itchat_msg["ActualUserName"] - if self.ctype not in [ContextType.JOIN_GROUP, ContextType.PATPAT]: + if self.ctype not in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.EXIT_GROUP]: self.actual_user_nickname = itchat_msg["ActualNickName"] diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py index a0004be..80c7837 100644 --- a/plugins/hello/hello.py +++ b/plugins/hello/hello.py @@ -29,6 +29,7 @@ class Hello(Plugin): ContextType.TEXT, ContextType.JOIN_GROUP, ContextType.PATPAT, + ContextType.EXIT_GROUP ]: return if e_context["context"].type == ContextType.JOIN_GROUP: @@ -46,6 +47,13 @@ class Hello(Plugin): e_context["context"].content = f'请你随机使用一种风格说一句问候语来欢迎新用户"{msg.actual_user_nickname}"加入群聊。' e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 return + + if e_context["context"].type == ContextType.EXIT_GROUP: + e_context["context"].type = ContextType.TEXT + msg: ChatMessage = e_context["context"]["msg"] + e_context["context"].content = f'请你随机使用一种风格跟其他群用户说他违反规则"{msg.actual_user_nickname}"退出群聊。' + e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 + return if e_context["context"].type == ContextType.PATPAT: e_context["context"].type = ContextType.TEXT From e5ba26febe8a61d460f2a611d37167ad20a75b82 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 6 Dec 2023 00:31:31 +0800 Subject: [PATCH 14/35] fix: tts voice base url --- bot/linkai/link_ai_bot.py | 4 +++- voice/openai/openai_voice.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/linkai/link_ai_bot.py b/bot/linkai/link_ai_bot.py index f308370..cd743b4 100644 --- a/bot/linkai/link_ai_bot.py +++ b/bot/linkai/link_ai_bot.py @@ -94,7 +94,7 @@ class LinkAIBot(Bot): file_id = context.kwargs.get("file_id") if file_id: body["file_id"] = file_id - logger.info(f"[LINKAI] query={query}, app_code={app_code}, mode={body.get('model')}, file_id={file_id}") + logger.info(f"[LINKAI] query={query}, app_code={app_code}, model={body.get('model')}, file_id={file_id}") headers = {"Authorization": "Bearer " + linkai_api_key} # do http request @@ -120,6 +120,8 @@ class LinkAIBot(Bot): if response["choices"][0].get("img_urls"): thread = threading.Thread(target=self._send_image, args=(context.get("channel"), context, response["choices"][0].get("img_urls"))) thread.start() + if response["choices"][0].get("text_content"): + reply_content = response["choices"][0].get("text_content") return Reply(ReplyType.TEXT, reply_content) else: diff --git a/voice/openai/openai_voice.py b/voice/openai/openai_voice.py index 2dd3cbe..767353e 100644 --- a/voice/openai/openai_voice.py +++ b/voice/openai/openai_voice.py @@ -33,7 +33,8 @@ class OpenaiVoice(Voice): def textToVoice(self, text): try: - url = 'https://api.openai.com/v1/audio/speech' + api_base = conf().get("open_ai_api_base") or "https://api.openai.com/v1" + url = f'{api_base}/audio/speech' headers = { 'Authorization': 'Bearer ' + conf().get("open_ai_api_key"), 'Content-Type': 'application/json' From 293a03b7c8db3a75e420f0b4e353f84ab506208d Mon Sep 17 00:00:00 2001 From: chazzjimel <126439838+chazzjimel@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:43:19 +0800 Subject: [PATCH 15/35] add ali voice output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加阿里云语音输出接口 --- voice/ali/ali_api.py | 60 ++++++++++++++++++++++++++++++++++-------- voice/ali/ali_voice.py | 29 +++++++++++--------- voice/factory.py | 9 +++++++ 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/voice/ali/ali_api.py b/voice/ali/ali_api.py index 9d366a0..cac0c8c 100644 --- a/voice/ali/ali_api.py +++ b/voice/ali/ali_api.py @@ -7,9 +7,9 @@ wechat:cheung-z-x Description: """ + import json import time - import requests import datetime import hashlib @@ -23,12 +23,22 @@ from common.tmp_dir import TmpDir def text_to_speech_aliyun(url, text, appkey, token): - # 请求的headers + """ + 使用阿里云的文本转语音服务将文本转换为语音。 + + 参数: + - url (str): 阿里云文本转语音服务的端点URL。 + - text (str): 要转换为语音的文本。 + - appkey (str): 您的阿里云appkey。 + - token (str): 阿里云API的认证令牌。 + + 返回值: + - str: 成功时输出音频文件的路径,否则为None。 + """ headers = { "Content-Type": "application/json", } - # 请求的payload data = { "text": text, "appkey": appkey, @@ -36,20 +46,15 @@ def text_to_speech_aliyun(url, text, appkey, token): "format": "wav" } - # 发送POST请求 response = requests.post(url, headers=headers, data=json.dumps(data)) - # 检查响应状态码和内容类型 if response.status_code == 200 and response.headers['Content-Type'] == 'audio/mpeg': - # 构造唯一的文件名 output_file = TmpDir().path() + "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7FFFFFFF) + ".wav" - # 将响应内容写入文件 with open(output_file, 'wb') as file: file.write(response.content) logger.debug(f"音频文件保存成功,文件名:{output_file}") else: - # 打印错误信息 logger.debug("响应状态码: {}".format(response.status_code)) logger.debug("响应内容: {}".format(response.text)) output_file = None @@ -58,28 +63,55 @@ def text_to_speech_aliyun(url, text, appkey, token): class AliyunTokenGenerator: + """ + 用于生成阿里云服务认证令牌的类。 + + 属性: + - access_key_id (str): 您的阿里云访问密钥ID。 + - access_key_secret (str): 您的阿里云访问密钥秘密。 + """ + def __init__(self, access_key_id, access_key_secret): self.access_key_id = access_key_id self.access_key_secret = access_key_secret def sign_request(self, parameters): - # 将参数排序 + """ + 为阿里云服务签名请求。 + + 参数: + - parameters (dict): 请求的参数字典。 + + 返回值: + - str: 请求的签名签章。 + """ + # 将参数按照字典顺序排序 sorted_params = sorted(parameters.items()) - # 构造待签名的字符串 + # 构造待签名的查询字符串 canonicalized_query_string = '' for (k, v) in sorted_params: canonicalized_query_string += '&' + self.percent_encode(k) + '=' + self.percent_encode(v) + # 构造用于签名的字符串 string_to_sign = 'GET&%2F&' + self.percent_encode(canonicalized_query_string[1:]) # 使用GET方法 - # 计算签名 + # 使用HMAC算法计算签名 h = hmac.new((self.access_key_secret + "&").encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1) signature = base64.encodebytes(h.digest()).strip() return signature def percent_encode(self, encode_str): + """ + 对字符串进行百分比编码。 + + 参数: + - encode_str (str): 要编码的字符串。 + + 返回值: + - str: 编码后的字符串。 + """ encode_str = str(encode_str) res = urllib.parse.quote(encode_str, '') res = res.replace('+', '%20') @@ -88,6 +120,12 @@ class AliyunTokenGenerator: return res def get_token(self): + """ + 获取阿里云服务的令牌。 + + 返回值: + - str: 获取到的令牌。 + """ # 设置请求参数 params = { 'Format': 'JSON', diff --git a/voice/ali/ali_voice.py b/voice/ali/ali_voice.py index c9e90cc..3a57442 100644 --- a/voice/ali/ali_voice.py +++ b/voice/ali/ali_voice.py @@ -20,15 +20,11 @@ from voice.ali.ali_api import AliyunTokenGenerator from voice.ali.ali_api import text_to_speech_aliyun -def textContainsEmoji(text): - # 此正则表达式匹配大多数表情符号和特殊字符 - pattern = re.compile( - '[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF\U00002702-\U000027B0\U00002600-\U000026FF]') - return bool(pattern.search(text)) - - class AliVoice(Voice): def __init__(self): + """ + 初始化AliVoice类,从配置文件加载必要的配置。 + """ try: curdir = os.path.dirname(__file__) config_path = os.path.join(curdir, "config.json") @@ -43,13 +39,17 @@ class AliVoice(Voice): except Exception as e: logger.warn("AliVoice init failed: %s, ignore " % e) - # def voiceToText(self, voice_file): - # pass - def textToVoice(self, text): + """ + 将文本转换为语音文件。 + + :param text: 要转换的文本。 + :return: 返回一个Reply对象,其中包含转换得到的语音文件或错误信息。 + """ + # 清除文本中的非中文、非英文和非基本字符 text = re.sub(r'[^\u4e00-\u9fa5\u3040-\u30FF\uAC00-\uD7AFa-zA-Z0-9' r'äöüÄÖÜáéíóúÁÉÍÓÚàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛçÇñÑ,。!?,.]', '', text) - # 提取 token_id 值 + # 提取有效的token token_id = self.get_valid_token() fileName = text_to_speech_aliyun(self.api_url, text, self.appkey, token_id) if fileName: @@ -60,6 +60,11 @@ class AliVoice(Voice): return reply def get_valid_token(self): + """ + 获取有效的阿里云token。 + + :return: 返回有效的token字符串。 + """ current_time = time.time() if self.token is None or current_time >= self.token_expire_time: get_token = AliyunTokenGenerator(self.access_key_id, self.access_key_secret) @@ -71,4 +76,4 @@ class AliVoice(Voice): logger.debug(f"新获取的阿里云token:{self.token}") else: logger.debug("使用缓存的token") - return self.token \ No newline at end of file + return self.token diff --git a/voice/factory.py b/voice/factory.py index 01229eb..ed80758 100644 --- a/voice/factory.py +++ b/voice/factory.py @@ -29,6 +29,15 @@ def create_voice(voice_type): from voice.azure.azure_voice import AzureVoice return AzureVoice() + elif voice_type == "elevenlabs": + from voice.elevent.elevent_voice import ElevenLabsVoice + + return ElevenLabsVoice() + + elif voice_type == "linkai": + from voice.linkai.linkai_voice import LinkAIVoice + + return LinkAIVoice() elif voice_type == "ali": from voice.ali.ali_voice import AliVoice From 4d8790655941f7d057653e791ba451fbb348b0eb Mon Sep 17 00:00:00 2001 From: erayyym Date: Tue, 5 Dec 2023 13:18:42 -0500 Subject: [PATCH 16/35] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本地跑没有问题,用户打开这个功能需要在config.json加入 "group_chat_exit_group": true, (但是不确定写的对不对,刚开始学cs哈哈,之前没搞过这个) --- config.py | 1 + plugins/hello/hello.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/config.py b/config.py index 6cc5708..550c4f3 100644 --- a/config.py +++ b/config.py @@ -40,6 +40,7 @@ available_setting = { "image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀 "concurrency_in_session": 1, # 同一会话最多有多少条消息在处理中,大于1可能乱序 "image_create_size": "256x256", # 图片大小,可选有 256x256, 512x512, 1024x1024 (dall-e-3默认为1024x1024) + "group_chat_exit_group": False, # chatgpt会话参数 "expires_in_seconds": 3600, # 无操作会话的过期时间 # 人格描述 diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py index 80c7837..76772af 100644 --- a/plugins/hello/hello.py +++ b/plugins/hello/hello.py @@ -49,12 +49,15 @@ class Hello(Plugin): return if e_context["context"].type == ContextType.EXIT_GROUP: - e_context["context"].type = ContextType.TEXT - msg: ChatMessage = e_context["context"]["msg"] - e_context["context"].content = f'请你随机使用一种风格跟其他群用户说他违反规则"{msg.actual_user_nickname}"退出群聊。' - e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 + if conf().get("group_chat_exit_group", []) == True: + e_context["context"].type = ContextType.TEXT + msg: ChatMessage = e_context["context"]["msg"] + e_context["context"].content = f'请你随机使用一种风格跟其他群用户说他违反规则"{msg.actual_user_nickname}"退出群聊。' + e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 + return + e_context.action = EventAction.BREAK return - + if e_context["context"].type == ContextType.PATPAT: e_context["context"].type = ContextType.TEXT msg: ChatMessage = e_context["context"]["msg"] From 40fd545b2c4777004235388bfc3c68904e3de937 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 6 Dec 2023 10:51:47 +0800 Subject: [PATCH 17/35] fix: exit group optimize --- plugins/hello/hello.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py index 76772af..35c181f 100644 --- a/plugins/hello/hello.py +++ b/plugins/hello/hello.py @@ -49,7 +49,7 @@ class Hello(Plugin): return if e_context["context"].type == ContextType.EXIT_GROUP: - if conf().get("group_chat_exit_group", []) == True: + if conf().get("group_chat_exit_group"): e_context["context"].type = ContextType.TEXT msg: ChatMessage = e_context["context"]["msg"] e_context["context"].content = f'请你随机使用一种风格跟其他群用户说他违反规则"{msg.actual_user_nickname}"退出群聊。' From 55df19142fb660eb8f2ed5f67e49d00894ef0e55 Mon Sep 17 00:00:00 2001 From: You Xie Date: Wed, 6 Dec 2023 02:27:22 -0600 Subject: [PATCH 18/35] Update chat_gpt_bot.py retry APIConnectionError --- bot/chatgpt/chat_gpt_bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/chatgpt/chat_gpt_bot.py b/bot/chatgpt/chat_gpt_bot.py index 8c9a250..979ce4c 100644 --- a/bot/chatgpt/chat_gpt_bot.py +++ b/bot/chatgpt/chat_gpt_bot.py @@ -148,8 +148,9 @@ class ChatGPTBot(Bot, OpenAIImage): time.sleep(10) elif isinstance(e, openai.error.APIConnectionError): logger.warn("[CHATGPT] APIConnectionError: {}".format(e)) - need_retry = False result["content"] = "我连接不到你的网络" + if need_retry: + time.sleep(5) else: logger.exception("[CHATGPT] Exception: {}".format(e)) need_retry = False From 14ae2f169a7067fbcd782de2843e3860bd1132aa Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 7 Dec 2023 19:41:50 +0800 Subject: [PATCH 19/35] fix: hello plugin trigger app bug --- plugins/hello/hello.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/hello/hello.py b/plugins/hello/hello.py index 35c181f..e86c609 100644 --- a/plugins/hello/hello.py +++ b/plugins/hello/hello.py @@ -39,13 +39,13 @@ class Hello(Plugin): reply.content = conf().get("group_welcome_msg", "") e_context["reply"] = reply e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑 - if not self.config or not self.config.get("use_character_desc"): - e_context["context"]["generate_breaked_by"] = EventAction.BREAK_PASS return e_context["context"].type = ContextType.TEXT msg: ChatMessage = e_context["context"]["msg"] e_context["context"].content = f'请你随机使用一种风格说一句问候语来欢迎新用户"{msg.actual_user_nickname}"加入群聊。' e_context.action = EventAction.BREAK # 事件结束,进入默认处理逻辑 + if not self.config or not self.config.get("use_character_desc"): + e_context["context"]["generate_breaked_by"] = EventAction.BREAK return if e_context["context"].type == ContextType.EXIT_GROUP: From 9d4afeac313d496f041eaaf5be6c77074cd9349f Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 7 Dec 2023 22:44:43 +0800 Subject: [PATCH 20/35] feat: speech support app_code bind --- voice/linkai/linkai_voice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voice/linkai/linkai_voice.py b/voice/linkai/linkai_voice.py index c42b804..074c9fd 100644 --- a/voice/linkai/linkai_voice.py +++ b/voice/linkai/linkai_voice.py @@ -62,7 +62,8 @@ class LinkAIVoice(Voice): data = { "model": model, "input": text, - "voice": conf().get("tts_voice_id") + "voice": conf().get("tts_voice_id"), + "app_code": conf().get("linkai_app_code") } res = requests.post(url, headers=headers, json=data, timeout=(5, 120)) if res.status_code == 200: From 6eb3c90e1847c676d4455a81a5b8698bb311cc68 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 8 Dec 2023 14:12:21 +0800 Subject: [PATCH 21/35] feat: qwen model modify --- bot/bot_factory.py | 2 +- bot/tongyi/tongyi_qwen_bot.py | 12 ++++++------ bridge/bridge.py | 4 ++-- common/const.py | 4 ++-- config.py | 12 ++++++------ 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bot/bot_factory.py b/bot/bot_factory.py index f618479..a0edde1 100644 --- a/bot/bot_factory.py +++ b/bot/bot_factory.py @@ -44,7 +44,7 @@ def create_bot(bot_type): from bot.claude.claude_ai_bot import ClaudeAIBot return ClaudeAIBot() - elif bot_type == const.TONGYI: + elif bot_type == const.QWEN: from bot.tongyi.tongyi_qwen_bot import TongyiQwenBot return TongyiQwenBot() raise RuntimeError diff --git a/bot/tongyi/tongyi_qwen_bot.py b/bot/tongyi/tongyi_qwen_bot.py index d123dae..585cb47 100644 --- a/bot/tongyi/tongyi_qwen_bot.py +++ b/bot/tongyi/tongyi_qwen_bot.py @@ -20,14 +20,14 @@ from config import conf, load_config class TongyiQwenBot(Bot): def __init__(self): super().__init__() - self.access_key_id = conf().get("tongyi_access_key_id") - self.access_key_secret = conf().get("tongyi_access_key_secret") - self.agent_key = conf().get("tongyi_agent_key") - self.app_id = conf().get("tongyi_app_id") - self.node_id = conf().get("tongyi_node_id") + 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(BaiduWenxinSession, model=conf().get("model") or "tongyi") + self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or "qwen") self.temperature = conf().get("temperature", 0.2) # 值在[0,1]之间,越大表示回复越具有不确定性 self.top_p = conf().get("top_p", 1) diff --git a/bridge/bridge.py b/bridge/bridge.py index ea8de13..be951af 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -35,8 +35,8 @@ class Bridge(object): self.btype["text_to_voice"] = const.LINKAI if model_type in ["claude"]: self.btype["chat"] = const.CLAUDEAI - if model_type in ["tongyi"]: - self.btype["chat"] = const.TONGYI + if model_type in [const.QWEN]: + self.btype["chat"] = const.QWEN self.bots = {} self.chat_bots = {} diff --git a/common/const.py b/common/const.py index 9ecd1a2..fc74e64 100644 --- a/common/const.py +++ b/common/const.py @@ -6,7 +6,7 @@ XUNFEI = "xunfei" CHATGPTONAZURE = "chatGPTOnAzure" LINKAI = "linkai" CLAUDEAI = "claude" -TONGYI = "tongyi" +QWEN = "qwen" # model GPT35 = "gpt-3.5-turbo" @@ -17,7 +17,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, "tongyi"] +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] # channel FEISHU = "feishu" diff --git a/config.py b/config.py index 728f308..8300699 100644 --- a/config.py +++ b/config.py @@ -16,7 +16,7 @@ available_setting = { "open_ai_api_base": "https://api.openai.com/v1", "proxy": "", # openai使用的代理 # chatgpt模型, 当use_azure_chatgpt为true时,其名称为Azure上model deployment名称 - "model": "gpt-3.5-turbo", # 还支持 gpt-4, gpt-4-turbo, wenxin, xunfei, tongyi + "model": "gpt-3.5-turbo", # 还支持 gpt-4, gpt-4-turbo, wenxin, xunfei, qwen "use_azure_chatgpt": False, # 是否使用azure的chatgpt "azure_deployment_id": "", # azure 模型部署名称 "azure_api_version": "", # azure api版本 @@ -68,11 +68,11 @@ available_setting = { "claude_api_cookie": "", "claude_uuid": "", # 通义千问API, 获取方式查看文档 https://help.aliyun.com/document_detail/2587494.html - "tongyi_access_key_id": "", - "tongyi_access_key_secret": "", - "tongyi_agent_key": "", - "tongyi_app_id": "", - "tongyi_node_id": "", # 流程编排模型用到的id,如果没有用到tongyi_node_id,请务必保持为空字符串 + "qwen_access_key_id": "", + "qwen_access_key_secret": "", + "qwen_agent_key": "", + "qwen_app_id": "", + "qwen_node_id": "", # 流程编排模型用到的id,如果没有用到qwen_node_id,请务必保持为空字符串 # wework的通用配置 "wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开 # 语音设置 From ae4077ed6cc91612751ec4fa89dccbe667a27397 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Fri, 8 Dec 2023 14:29:14 +0800 Subject: [PATCH 22/35] fix: config adjust --- bridge/bridge.py | 4 ++-- voice/ali/ali_voice.py | 10 ++++++---- voice/ali/config.json.template | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bridge/bridge.py b/bridge/bridge.py index be951af..2b637c3 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -27,6 +27,8 @@ class Bridge(object): self.btype["chat"] = const.BAIDU if model_type in ["xunfei"]: self.btype["chat"] = const.XUNFEI + if model_type in [const.QWEN]: + self.btype["chat"] = const.QWEN 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"]: @@ -35,8 +37,6 @@ class Bridge(object): self.btype["text_to_voice"] = const.LINKAI if model_type in ["claude"]: self.btype["chat"] = const.CLAUDEAI - if model_type in [const.QWEN]: - self.btype["chat"] = const.QWEN self.bots = {} self.chat_bots = {} diff --git a/voice/ali/ali_voice.py b/voice/ali/ali_voice.py index 3a57442..79a9aaa 100644 --- a/voice/ali/ali_voice.py +++ b/voice/ali/ali_voice.py @@ -18,6 +18,7 @@ from common.log import logger from voice.voice import Voice from voice.ali.ali_api import AliyunTokenGenerator from voice.ali.ali_api import text_to_speech_aliyun +from config import conf class AliVoice(Voice): @@ -32,10 +33,11 @@ class AliVoice(Voice): config = json.load(fr) self.token = None self.token_expire_time = 0 + # 默认复用阿里云千问的 access_key 和 access_secret self.api_url = config.get("api_url") - self.appkey = config.get("appkey") - self.access_key_id = config.get("access_key_id") - self.access_key_secret = config.get("access_key_secret") + self.app_key = config.get("app_key") + self.access_key_id = conf().get("qwen_access_key_id") or config.get("access_key_id") + self.access_key_secret = conf().get("qwen_access_key_secret") or config.get("access_key_secret") except Exception as e: logger.warn("AliVoice init failed: %s, ignore " % e) @@ -51,7 +53,7 @@ class AliVoice(Voice): r'äöüÄÖÜáéíóúÁÉÍÓÚàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛçÇñÑ,。!?,.]', '', text) # 提取有效的token token_id = self.get_valid_token() - fileName = text_to_speech_aliyun(self.api_url, text, self.appkey, token_id) + fileName = text_to_speech_aliyun(self.api_url, text, self.app_key, token_id) if fileName: logger.info("[Ali] textToVoice text={} voice file name={}".format(text, fileName)) reply = Reply(ReplyType.VOICE, fileName) diff --git a/voice/ali/config.json.template b/voice/ali/config.json.template index 7f8d0e6..6a4aaa9 100644 --- a/voice/ali/config.json.template +++ b/voice/ali/config.json.template @@ -1,6 +1,6 @@ { "api_url": "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/tts", - "appkey": "", + "app_key": "", "access_key_id": "", "access_key_secret": "" } \ No newline at end of file From bfacdb9c3b0038c074895f56b53fa65d9ff460da Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sat, 9 Dec 2023 12:39:09 +0800 Subject: [PATCH 23/35] 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 be0bb591e71a54cc43488a4d0c9dca51646d229c Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sat, 9 Dec 2023 17:12:08 +0800 Subject: [PATCH 24/35] fix: do not draw when text_to_image is empty --- bot/linkai/link_ai_bot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/linkai/link_ai_bot.py b/bot/linkai/link_ai_bot.py index cd743b4..c9afa71 100644 --- a/bot/linkai/link_ai_bot.py +++ b/bot/linkai/link_ai_bot.py @@ -32,6 +32,9 @@ class LinkAIBot(Bot): if context.type == ContextType.TEXT: return self._chat(query, context) elif context.type == ContextType.IMAGE_CREATE: + if not conf().get("text_to_image"): + logger.warn("[LinkAI] text_to_image is not enabled, ignore the IMAGE_CREATE request") + return Reply(ReplyType.TEXT, "") ok, res = self.create_img(query, 0) if ok: reply = Reply(ReplyType.IMAGE_URL, res) From 207fa1d019fa9cae74fe356aaaae2c6c135969d5 Mon Sep 17 00:00:00 2001 From: Han Fangyuan Date: Sat, 9 Dec 2023 18:40:17 +0800 Subject: [PATCH 25/35] 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 26/35] 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 27/35] 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 28/35] 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 29/35] 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 30/35] =?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 31/35] 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 32/35] 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 33/35] 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 34/35] 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 35/35] 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` 模型接入,完善图像理解&生成、语音识别&生成的多模态能力