Browse Source

feat: add dalle3 gpt-4-turbo model change

master
zhayujie 1 year ago
parent
commit
f001ac6903
16 changed files with 166 additions and 25 deletions
  1. +1
    -1
      README.md
  2. +2
    -2
      bot/chatgpt/chat_gpt_session.py
  3. +27
    -1
      bot/linkai/link_ai_bot.py
  4. +4
    -3
      bot/openai/open_ai_image.py
  5. +4
    -0
      bridge/bridge.py
  6. +1
    -0
      channel/chat_channel.py
  7. +6
    -3
      common/const.py
  8. +6
    -2
      config-template.json
  9. +6
    -3
      config.py
  10. +7
    -1
      plugins/godcmd/godcmd.py
  11. +2
    -1
      plugins/linkai/config.json.template
  12. +14
    -5
      plugins/linkai/linkai.py
  13. +3
    -2
      plugins/linkai/summary.py
  14. +4
    -0
      voice/factory.py
  15. +78
    -0
      voice/linkai/linkai_voice.py
  16. +1
    -1
      voice/openai/openai_voice.py

+ 1
- 1
README.md View File

@@ -7,7 +7,7 @@
- [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式 - [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式
- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, 文心一言, 讯飞星火 - [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, 文心一言, 讯飞星火
- [x] **语音识别:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai等多种语音模型 - [x] **语音识别:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai等多种语音模型
- [x] **图片生成:** 支持图片生成 和 图生图(如照片修复),可选择 Dell-E, stable diffusion, replicate, midjourney模型
- [x] **图片生成:** 支持图片生成 和 图生图(如照片修复),可选择 Dall-E, stable diffusion, replicate, midjourney模型
- [x] **丰富插件:** 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结、文档总结和对话等插件 - [x] **丰富插件:** 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结、文档总结和对话等插件
- [X] **Tool工具:** 与操作系统和互联网交互,支持最新信息搜索、数学计算、天气和资讯查询、网页总结,基于 [chatgpt-tool-hub](https://github.com/goldfishh/chatgpt-tool-hub) 实现 - [X] **Tool工具:** 与操作系统和互联网交互,支持最新信息搜索、数学计算、天气和资讯查询、网页总结,基于 [chatgpt-tool-hub](https://github.com/goldfishh/chatgpt-tool-hub) 实现
- [x] **知识库:** 通过上传知识库文件自定义专属机器人,可作为数字分身、领域知识库、智能客服使用,基于 [LinkAI](https://chat.link-ai.tech/console) 实现 - [x] **知识库:** 通过上传知识库文件自定义专属机器人,可作为数字分身、领域知识库、智能客服使用,基于 [LinkAI](https://chat.link-ai.tech/console) 实现


+ 2
- 2
bot/chatgpt/chat_gpt_session.py View File

@@ -62,10 +62,10 @@ def num_tokens_from_messages(messages, model):


import tiktoken import tiktoken


if model in ["gpt-3.5-turbo-0301", "gpt-35-turbo"]:
if model in ["gpt-3.5-turbo-0301", "gpt-35-turbo", "gpt-3.5-turbo-1106"]:
return num_tokens_from_messages(messages, model="gpt-3.5-turbo") return num_tokens_from_messages(messages, model="gpt-3.5-turbo")
elif model in ["gpt-4-0314", "gpt-4-0613", "gpt-4-32k", "gpt-4-32k-0613", "gpt-3.5-turbo-0613", elif model in ["gpt-4-0314", "gpt-4-0613", "gpt-4-32k", "gpt-4-32k-0613", "gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-35-turbo-16k", const.GPT4_PREVIEW, const.GPT4_VISION_PREVIEW]:
"gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-35-turbo-16k", const.GPT4_TURBO_PREVIEW, const.GPT4_VISION_PREVIEW]:
return num_tokens_from_messages(messages, model="gpt-4") return num_tokens_from_messages(messages, model="gpt-4")


try: try:


+ 27
- 1
bot/linkai/link_ai_bot.py View File

@@ -15,7 +15,7 @@ from common.log import logger
from config import conf, pconf from config import conf, pconf




class LinkAIBot(Bot, OpenAIImage):
class LinkAIBot(Bot):
# authentication failed # authentication failed
AUTH_FAILED_CODE = 401 AUTH_FAILED_CODE = 401
NO_QUOTA_CODE = 406 NO_QUOTA_CODE = 406
@@ -193,6 +193,32 @@ class LinkAIBot(Bot, OpenAIImage):
return self.reply_text(session, app_code, retry_count + 1) return self.reply_text(session, app_code, retry_count + 1)




def create_img(self, query, retry_count=0, api_key=None):
try:
logger.info("[LinkImage] image_query={}".format(query))
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {conf().get('linkai_api_key')}"
}
data = {
"prompt": query,
"n": 1,
"model": conf().get("text_to_image") or "dall-e-2",
"response_format": "url",
"img_proxy": conf().get("image_proxy")
}
url = conf().get("linkai_api_base", "https://api.link-ai.chat") + "/v1/images/generations"
res = requests.post(url, headers=headers, json=data, timeout=(5, 90))
t2 = time.time()
image_url = res.json()["data"][0]["url"]
logger.info("[OPEN_AI] image_url={}".format(image_url))
return True, image_url

except Exception as e:
logger.error(format(e))
return False, "画图出现问题,请休息一下再问我吧"


def _fetch_knowledge_search_suffix(self, response) -> str: def _fetch_knowledge_search_suffix(self, response) -> str:
try: try:
if response.get("knowledge_base"): if response.get("knowledge_base"):


+ 4
- 3
bot/openai/open_ai_image.py View File

@@ -24,7 +24,8 @@ class OpenAIImage(object):
api_key=api_key, api_key=api_key,
prompt=query, # 图片描述 prompt=query, # 图片描述
n=1, # 每次生成图片的数量 n=1, # 每次生成图片的数量
size=conf().get("image_create_size", "256x256"), # 图片大小,可选有 256x256, 512x512, 1024x1024
model=conf().get("text_to_image") or "dall-e-2",
# size=conf().get("image_create_size", "256x256"), # 图片大小,可选有 256x256, 512x512, 1024x1024
) )
image_url = response["data"][0]["url"] image_url = response["data"][0]["url"]
logger.info("[OPEN_AI] image_url={}".format(image_url)) logger.info("[OPEN_AI] image_url={}".format(image_url))
@@ -36,7 +37,7 @@ class OpenAIImage(object):
logger.warn("[OPEN_AI] ImgCreate RateLimit exceed, 第{}次重试".format(retry_count + 1)) logger.warn("[OPEN_AI] ImgCreate RateLimit exceed, 第{}次重试".format(retry_count + 1))
return self.create_img(query, retry_count + 1) return self.create_img(query, retry_count + 1)
else: else:
return False, "提问太快啦,请休息一下再问我吧"
return False, "画图出现问题,请休息一下再问我吧"
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
return False, str(e)
return False, "画图出现问题,请休息一下再问我吧"

+ 4
- 0
bridge/bridge.py View File

@@ -29,6 +29,10 @@ class Bridge(object):
self.btype["chat"] = const.XUNFEI self.btype["chat"] = const.XUNFEI
if conf().get("use_linkai") and conf().get("linkai_api_key"): if conf().get("use_linkai") and conf().get("linkai_api_key"):
self.btype["chat"] = const.LINKAI 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 [const.TTS_1, const.TTS_1_HD]:
self.btype["text_to_voice"] = const.LINKAI
if model_type in ["claude"]: if model_type in ["claude"]:
self.btype["chat"] = const.CLAUDEAI self.btype["chat"] = const.CLAUDEAI
self.bots = {} self.bots = {}


+ 1
- 0
channel/chat_channel.py View File

@@ -91,6 +91,7 @@ class ChatChannel(Channel):
# 消息内容匹配过程,并处理content # 消息内容匹配过程,并处理content
if ctype == ContextType.TEXT: if ctype == ContextType.TEXT:
if first_in and "」\n- - - - - - -" in content: # 初次匹配 过滤引用消息 if first_in and "」\n- - - - - - -" in content: # 初次匹配 过滤引用消息
logger.debug(content)
logger.debug("[WX]reference query skipped") logger.debug("[WX]reference query skipped")
return None return None




+ 6
- 3
common/const.py View File

@@ -7,9 +7,12 @@ CHATGPTONAZURE = "chatGPTOnAzure"
LINKAI = "linkai" LINKAI = "linkai"
CLAUDEAI = "claude" CLAUDEAI = "claude"


MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4", "xunfei", "claude"]

# model # model
GPT4 = "gpt-4" GPT4 = "gpt-4"
GPT4_PREVIEW = "gpt-4-1106-preview"
GPT4_TURBO_PREVIEW = "gpt-4-1106-preview"
GPT4_VISION_PREVIEW = "gpt-4-vision-preview" GPT4_VISION_PREVIEW = "gpt-4-vision-preview"
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]

+ 6
- 2
config-template.json View File

@@ -1,7 +1,10 @@
{ {
"channel_type": "wx",
"open_ai_api_key": "YOUR API KEY", "open_ai_api_key": "YOUR API KEY",
"model": "gpt-3.5-turbo", "model": "gpt-3.5-turbo",
"channel_type": "wx",
"text_to_image": "dall-e-2",
"voice_to_text": "openai",
"text_to_voice": "openai",
"proxy": "", "proxy": "",
"hot_reload": false, "hot_reload": false,
"single_chat_prefix": [ "single_chat_prefix": [
@@ -22,9 +25,10 @@
"image_create_prefix": [ "image_create_prefix": [
"画" "画"
], ],
"speech_recognition": false,
"speech_recognition": true,
"group_speech_recognition": false, "group_speech_recognition": false,
"voice_reply_voice": false, "voice_reply_voice": false,
"tts_voice_id": "alloy",
"conversation_max_tokens": 1000, "conversation_max_tokens": 1000,
"expires_in_seconds": 3600, "expires_in_seconds": 3600,
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", "character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。",


+ 6
- 3
config.py View File

@@ -34,9 +34,11 @@ available_setting = {
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称 "group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
"group_welcome_msg": "", # 配置新人进群固定欢迎语,不配置则使用随机风格欢迎 "group_welcome_msg": "", # 配置新人进群固定欢迎语,不配置则使用随机风格欢迎
"trigger_by_self": False, # 是否允许机器人触发 "trigger_by_self": False, # 是否允许机器人触发
"text_to_image": "dall-e-2", # 图片生成模型,可选 dall-e-2, dall-e-3
"image_proxy": True, # 是否需要图片代理,国内访问LinkAI时需要
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀 "image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
"concurrency_in_session": 1, # 同一会话最多有多少条消息在处理中,大于1可能乱序 "concurrency_in_session": 1, # 同一会话最多有多少条消息在处理中,大于1可能乱序
"image_create_size": "256x256", # 图片大小,可选有 256x256, 512x512, 1024x1024
"image_create_size": "256x256", # 图片大小,可选有 256x256, 512x512, 1024x1024 (dall-e-3默认为1024x1024)
# chatgpt会话参数 # chatgpt会话参数
"expires_in_seconds": 3600, # 无操作会话的过期时间 "expires_in_seconds": 3600, # 无操作会话的过期时间
# 人格描述 # 人格描述
@@ -66,12 +68,13 @@ available_setting = {
# wework的通用配置 # wework的通用配置
"wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开 "wework_smart": True, # 配置wework是否使用已登录的企业微信,False为多开
# 语音设置 # 语音设置
"speech_recognition": False, # 是否开启语音识别
"speech_recognition": True, # 是否开启语音识别
"group_speech_recognition": False, # 是否开启群组语音识别 "group_speech_recognition": False, # 是否开启群组语音识别
"voice_reply_voice": False, # 是否使用语音回复语音,需要设置对应语音合成引擎的api key "voice_reply_voice": False, # 是否使用语音回复语音,需要设置对应语音合成引擎的api key
"always_reply_voice": False, # 是否一直使用语音回复 "always_reply_voice": False, # 是否一直使用语音回复
"voice_to_text": "openai", # 语音识别引擎,支持openai,baidu,google,azure "voice_to_text": "openai", # 语音识别引擎,支持openai,baidu,google,azure
"text_to_voice": "baidu", # 语音合成引擎,支持baidu,google,pytts(offline),azure,elevenlabs
"text_to_voice": "tts-1", # 语音合成引擎,支持tts-1,tts-1-hd,baidu,google,pytts(offline),azure,elevenlabs
"tts_voice_id": "alloy",
# baidu 语音api配置, 使用百度语音识别和语音合成时需要 # baidu 语音api配置, 使用百度语音识别和语音合成时需要
"baidu_app_id": "", "baidu_app_id": "",
"baidu_api_key": "", "baidu_api_key": "",


+ 7
- 1
plugins/godcmd/godcmd.py View File

@@ -271,7 +271,7 @@ class Godcmd(Plugin):
if args[0] not in const.MODEL_LIST: if args[0] not in const.MODEL_LIST:
ok, result = False, "模型名称不存在" ok, result = False, "模型名称不存在"
else: else:
conf()["model"] = args[0]
conf()["model"] = self.model_mapping(args[0])
Bridge().reset_bot() Bridge().reset_bot()
ok, result = True, "模型设置为: " + str(conf().get("model")) ok, result = True, "模型设置为: " + str(conf().get("model"))
elif cmd == "id": elif cmd == "id":
@@ -467,3 +467,9 @@ class Godcmd(Plugin):
if context["isgroup"]: if context["isgroup"]:
return context.kwargs.get("msg").actual_user_id in global_config["admin_users"] return context.kwargs.get("msg").actual_user_id in global_config["admin_users"]
return False return False


def model_mapping(self, model) -> str:
if model == "gpt-4-turbo":
return const.GPT4_TURBO_PREVIEW
return model

+ 2
- 1
plugins/linkai/config.json.template View File

@@ -14,6 +14,7 @@
"summary": { "summary": {
"enabled": true, "enabled": true,
"group_enabled": true, "group_enabled": true,
"max_file_size": 5000
"max_file_size": 5000,
"type": ["FILE", "SHARING", "IMAGE"]
} }
} }

+ 14
- 5
plugins/linkai/linkai.py View File

@@ -46,19 +46,23 @@ class LinkAI(Plugin):
# filter content no need solve # filter content no need solve
return return


if context.type == ContextType.FILE and self._is_summary_open(context):
if context.type in [ContextType.FILE, ContextType.IMAGE] and self._is_summary_open(context):
# 文件处理 # 文件处理
context.get("msg").prepare() context.get("msg").prepare()
file_path = context.content file_path = context.content
if not LinkSummary().check_file(file_path, self.sum_config): if not LinkSummary().check_file(file_path, self.sum_config):
return return
_send_info(e_context, "正在为你加速生成摘要,请稍后")
if context.type != ContextType.IMAGE:
_send_info(e_context, "正在为你加速生成摘要,请稍后")
res = LinkSummary().summary_file(file_path) res = LinkSummary().summary_file(file_path)
if not res: if not res:
_set_reply_text("因为神秘力量无法获取文章内容,请稍后再试吧", e_context, level=ReplyType.TEXT)
_set_reply_text("因为神秘力量无法获取内容,请稍后再试吧", e_context, level=ReplyType.TEXT)
return return
USER_FILE_MAP[_find_user_id(context) + "-sum_id"] = res.get("summary_id")
_set_reply_text(res.get("summary") + "\n\n💬 发送 \"开启对话\" 可以开启与文件内容的对话", e_context, level=ReplyType.TEXT)
summary_text = res.get("summary")
if context.type != ContextType.IMAGE:
USER_FILE_MAP[_find_user_id(context) + "-sum_id"] = res.get("summary_id")
summary_text += "\n\n💬 发送 \"开启对话\" 可以开启与文件内容的对话"
_set_reply_text(summary_text, e_context, level=ReplyType.TEXT)
os.remove(file_path) os.remove(file_path)
return return


@@ -187,6 +191,11 @@ class LinkAI(Plugin):
return False return False
if context.kwargs.get("isgroup") and not self.sum_config.get("group_enabled"): if context.kwargs.get("isgroup") and not self.sum_config.get("group_enabled"):
return False return False
support_type = self.sum_config.get("type")
if not support_type:
return True
if context.type.name not in support_type:
return False
return True return True


# LinkAI 对话任务处理 # LinkAI 对话任务处理


+ 3
- 2
plugins/linkai/summary.py View File

@@ -13,7 +13,8 @@ class LinkSummary:
"file": open(file_path, "rb"), "file": open(file_path, "rb"),
"name": file_path.split("/")[-1], "name": file_path.split("/")[-1],
} }
res = requests.post(url=self.base_url() + "/v1/summary/file", headers=self.headers(), files=file_body, timeout=(5, 300))
url = self.base_url() + "/v1/summary/file"
res = requests.post(url, headers=self.headers(), files=file_body, timeout=(5, 300))
return self._parse_summary_res(res) return self._parse_summary_res(res)


def summary_url(self, url: str): def summary_url(self, url: str):
@@ -71,7 +72,7 @@ class LinkSummary:
return False return False


suffix = file_path.split(".")[-1] suffix = file_path.split(".")[-1]
support_list = ["txt", "csv", "docx", "pdf", "md"]
support_list = ["txt", "csv", "docx", "pdf", "md", "jpg", "jpeg", "png"]
if suffix not in support_list: if suffix not in support_list:
logger.warn(f"[LinkSum] unsupported file, suffix={suffix}, support_list={support_list}") logger.warn(f"[LinkSum] unsupported file, suffix={suffix}, support_list={support_list}")
return False return False


+ 4
- 0
voice/factory.py View File

@@ -33,4 +33,8 @@ def create_voice(voice_type):
from voice.elevent.elevent_voice import ElevenLabsVoice from voice.elevent.elevent_voice import ElevenLabsVoice


return ElevenLabsVoice() return ElevenLabsVoice()

elif voice_type == "linkai":
from voice.linkai.linkai_voice import LinkAIVoice
return LinkAIVoice()
raise RuntimeError raise RuntimeError

+ 78
- 0
voice/linkai/linkai_voice.py View File

@@ -0,0 +1,78 @@
"""
google voice service
"""
import json
import os
import random

import openai
import requests

from bridge.reply import Reply, ReplyType
from common.log import logger
from config import conf
from voice.voice import Voice
from common import const
import datetime

class LinkAIVoice(Voice):
def __init__(self):
pass

def voiceToText(self, voice_file):
logger.debug("[LinkVoice] voice file name={}".format(voice_file))
try:
url = conf().get("linkai_api_base", "https://api.link-ai.chat") + "/v1/audio/transcriptions"
headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
model = None
if not conf().get("text_to_voice") or conf().get("voice_to_text") == "openai":
model = const.WHISPER_1
file = open(voice_file, "rb")
file_body = {
"file": file
}
data = {
"model": model
}
res = requests.post(url, files=file_body, headers=headers, data=data, timeout=(5, 60))
if res.status_code == 200:
text = res.json().get("text")
else:
res_json = res.json()
logger.error(f"[LinkVoice] voiceToText error, status_code={res.status_code}, msg={res_json.get('message')}")
return None
reply = Reply(ReplyType.TEXT, text)
logger.info(f"[LinkVoice] voiceToText success, text={text}, file name={voice_file}")
except Exception as e:
logger.error(e)
reply = Reply(ReplyType.ERROR, "我暂时还无法听清您的语音,请稍后再试吧~")
return reply

def textToVoice(self, text):
try:
url = conf().get("linkai_api_base", "https://api.link-ai.chat") + "/v1/audio/speech"
headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
model = const.TTS_1
if not conf().get("text_to_voice") or conf().get("text_to_voice") in [const.TTS_1, const.TTS_1_HD]:
model = conf().get("text_to_voice") or const.TTS_1
data = {
"model": model,
"input": text,
"voice": conf().get("tts_voice_id")
}
res = requests.post(url, headers=headers, json=data, timeout=(5, 120))
if res.status_code == 200:
tmp_file_name = "tmp/" + datetime.datetime.now().strftime('%Y%m%d%H%M%S') + str(random.randint(0, 1000)) + ".mp3"
with open(tmp_file_name, 'wb') as f:
f.write(res.content)
reply = Reply(ReplyType.VOICE, tmp_file_name)
logger.info(f"[LinkVoice] textToVoice success, input={text}, model={model}, voice_id={data.get('voice')}")
return reply
else:
res_json = res.json()
logger.error(f"[LinkVoice] textToVoice error, status_code={res.status_code}, msg={res_json.get('message')}")
return None
except Exception as e:
logger.error(e)
reply = Reply(ReplyType.ERROR, "遇到了一点小问题,请稍后再问我吧")
return reply

+ 1
- 1
voice/openai/openai_voice.py View File

@@ -24,6 +24,6 @@ class OpenaiVoice(Voice):
reply = Reply(ReplyType.TEXT, text) reply = Reply(ReplyType.TEXT, text)
logger.info("[Openai] voiceToText text={} voice file name={}".format(text, voice_file)) logger.info("[Openai] voiceToText text={} voice file name={}".format(text, voice_file))
except Exception as e: except Exception as e:
reply = Reply(ReplyType.ERROR, str(e))
reply = Reply(ReplyType.ERROR, "我暂时还无法听清您的语音,请稍后再试吧~")
finally: finally:
return reply return reply

Loading…
Cancel
Save