diff --git a/README.md b/README.md index e31ce1c9..afacb187 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - [x] [GPT-3.0](https://github.com/zhayujie/bot-on-anything#2gpt-30) - [x] [New Bing](https://github.com/zhayujie/bot-on-anything#4newbing) - [x] [Google Bard](https://github.com/zhayujie/bot-on-anything#5bard) + - [x] [Azure OpenAI](#azure) **应用:** @@ -209,6 +210,44 @@ cookie示例: } ``` +### 6. Azure OpenAI + +这个选项主要是为了解决OpenAI的api免费额度到期后,无法使用国内信用卡继续使用的问题。 +众所周知微软和OpenAI的关系,微软在自家的Azure上也提供了OpenAI的服务,而Azure是可以绑定国内信用卡进行付费的。 + +#### (0) 申请开通 Azure OpenAI 服务 + +参考这个视频: +[![](https://res.cloudinary.com/marcomontalbano/image/upload/v1686642690/video_to_markdown/images/youtube---RI2pXNfOKQ-c05b58ac6eb4c4700831b2b3070cd403.jpg)](https://www.youtube.com/watch?v=-RI2pXNfOKQ "") + +Azure OpenAI 服务的快速入门文档:https://learn.microsoft.com/zh-cn/azure/cognitive-services/openai/chatgpt-quickstart?tabs=command-line&pivots=programming-language-python + +#### (1) 安装依赖 + +``` shell +pip install openai +``` + +#### (2) 配置说明 + +基本与ChatGPT的配置相同,从依赖项就能看出来,实际使用的还是openai的python库,只是调用时部分参数有所不同。 + +``` json +{ + "model": { + "type" : "azure_openai", + "azure_openai": { + "api_key": "你在Azure上创建的OpenAI服务实例的Key", + "api_base": "你在Azure上创建的OpenAI服务实例的Endpoint", + "api_version": "2023-05-15", # 目前是这个版本,如果有更新,可以在Azure上查看 + "engine": "你在OpenAI服务实例中部署模型时设置的名称", + } + } +} +``` + +以上4个配置项都是必须的,其中engine相当于ChatGPT配置中的model。ChatGPT中的其他配置亦可在此使用。 + ## 三、选择应用 ### 1.命令行终端 diff --git a/common/const.py b/common/const.py index 6d9d32b1..4bb1aeaf 100644 --- a/common/const.py +++ b/common/const.py @@ -15,7 +15,8 @@ # model OPEN_AI = "openai" +AZURE_OPEN_AI = "azure_openai" CHATGPT = "chatgpt" BAIDU = "baidu" BING = "bing" -BARD = "bard" \ No newline at end of file +BARD = "bard" diff --git a/model/model_factory.py b/model/model_factory.py index d0222037..4708329a 100644 --- a/model/model_factory.py +++ b/model/model_factory.py @@ -21,6 +21,10 @@ def create_bot(model_type): from model.openai.chatgpt_model import ChatGPTModel return ChatGPTModel() + elif model_type == const.AZURE_OPEN_AI: + from model.openai.azure_open_ai_model import AzureOpenAIModel + return AzureOpenAIModel() + elif model_type == const.BAIDU: from model.baidu.yiyan_model import YiyanModel return YiyanModel() diff --git a/model/openai/azure_open_ai_model.py b/model/openai/azure_open_ai_model.py new file mode 100644 index 00000000..428a5a0b --- /dev/null +++ b/model/openai/azure_open_ai_model.py @@ -0,0 +1,220 @@ +# encoding:utf-8 + +from model.model import Model +from config import model_conf, common_conf_val +from common import const +from common import log +import openai +import time + +user_session = dict() + +# OpenAI对话模型API (可用) +class AzureOpenAIModel(Model): + def __init__(self): + openai.api_type = 'azure' + openai.api_key = model_conf(const.AZURE_OPEN_AI).get('api_key') + api_base = model_conf(const.AZURE_OPEN_AI).get('api_base') + if api_base is None or api_base.strip() == '': + raise Exception('api_base is required, use your Azure OpenAI endpoint') + openai.api_base = api_base + openai.api_version = model_conf(const.AZURE_OPEN_AI).get('api_version') + engine = model_conf(const.AZURE_OPEN_AI).get('engine') + if engine is None or engine.strip() == '': + raise Exception('engine is required, it should be your Azure OpenAI deployment_name') + self.engine = engine + log.info(f"[AZURE_OPEN_AI] api_base={api_base} engine={engine}") + + def reply(self, query, context=None): + # acquire reply content + if not context or not context.get('type') or context.get('type') == 'TEXT': + log.info("[AZURE_OPEN_AI] query={}".format(query)) + from_user_id = context['from_user_id'] + clear_memory_commands = common_conf_val('clear_memory_commands', ['#清除记忆']) + if query in clear_memory_commands: + Session.clear_session(from_user_id) + return '记忆已清除' + + new_query = Session.build_session_query(query, from_user_id) + log.debug("[AZURE_OPEN_AI] session query={}".format(new_query)) + + # if context.get('stream'): + # # reply in stream + # return self.reply_text_stream(query, new_query, from_user_id) + + reply_content = self.reply_text(new_query, from_user_id, 0) + #log.debug("[AZURE_OPEN_AI] new_query={}, user={}, reply_cont={}".format(new_query, from_user_id, reply_content)) + return reply_content + + elif context.get('type', None) == 'IMAGE_CREATE': + return self.create_img(query, 0) + + def reply_text(self, query, user_id, retry_count=0): + try: + response = openai.ChatCompletion.create( + engine=self.engine, + messages=query, + temperature=model_conf(const.AZURE_OPEN_AI).get("temperature", 0.75), # 熵值,在[0,1]之间,越大表示选取的候选词越随机,回复越具有不确定性,建议和top_p参数二选一使用,创意性任务越大越好,精确性任务越小越好 + #max_tokens=4096, # 回复最大的字符数,为输入和输出的总数 + #top_p=model_conf(const.AZURE_OPEN_AI).get("top_p", 0.7),, #候选词列表。0.7 意味着只考虑前70%候选词的标记,建议和temperature参数二选一使用 + frequency_penalty=model_conf(const.AZURE_OPEN_AI).get("frequency_penalty", 0.0), # [-2,2]之间,该值越大则越降低模型一行中的重复用词,更倾向于产生不同的内容 + presence_penalty=model_conf(const.AZURE_OPEN_AI).get("presence_penalty", 1.0) # [-2,2]之间,该值越大则越不受输入限制,将鼓励模型生成输入中不存在的新词,更倾向于产生不同的内容 + ) + reply_content = response.choices[0]['message']['content'] + used_token = response['usage']['total_tokens'] + log.debug(response) + log.info("[AZURE_OPEN_AI] reply={}", reply_content) + if reply_content: + # save conversation + Session.save_session(query, reply_content, user_id, used_token) + return response.choices[0]['message']['content'] + except openai.error.RateLimitError as e: + # rate limit exception + log.warn(e) + if retry_count < 1: + time.sleep(5) + log.warn("[AZURE_OPEN_AI] RateLimit exceed, 第{}次重试".format(retry_count+1)) + return self.reply_text(query, user_id, retry_count+1) + else: + return "提问太快啦,请休息一下再问我吧" + except openai.error.APIConnectionError as e: + log.warn(e) + log.warn("[AZURE_OPEN_AI] APIConnection failed") + return "我连接不到网络,请稍后重试" + except openai.error.Timeout as e: + log.warn(e) + log.warn("[AZURE_OPEN_AI] Timeout") + return "我没有收到消息,请稍后重试" + except Exception as e: + # unknown exception + log.exception(e) + Session.clear_session(user_id) + return "请再问我一次吧" + + + async def reply_text_stream(self, query, context, retry_count=0): + try: + user_id=context['from_user_id'] + new_query = Session.build_session_query(query, user_id) + res = openai.ChatCompletion.create( + model= model_conf(const.AZURE_OPEN_AI).get("model") or "gpt-3.5-turbo", # 对话模型的名称 + messages=new_query, + temperature=model_conf(const.AZURE_OPEN_AI).get("temperature", 0.75), # 熵值,在[0,1]之间,越大表示选取的候选词越随机,回复越具有不确定性,建议和top_p参数二选一使用,创意性任务越大越好,精确性任务越小越好 + #max_tokens=4096, # 回复最大的字符数,为输入和输出的总数 + #top_p=model_conf(const.AZURE_OPEN_AI).get("top_p", 0.7),, #候选词列表。0.7 意味着只考虑前70%候选词的标记,建议和temperature参数二选一使用 + frequency_penalty=model_conf(const.AZURE_OPEN_AI).get("frequency_penalty", 0.0), # [-2,2]之间,该值越大则越降低模型一行中的重复用词,更倾向于产生不同的内容 + presence_penalty=model_conf(const.AZURE_OPEN_AI).get("presence_penalty", 1.0), # [-2,2]之间,该值越大则越不受输入限制,将鼓励模型生成输入中不存在的新词,更倾向于产生不同的内容 + stream=True + ) + full_response = "" + for chunk in res: + log.debug(chunk) + if (chunk["choices"][0]["finish_reason"]=="stop"): + break + chunk_message = chunk['choices'][0]['delta'].get("content") + if(chunk_message): + full_response+=chunk_message + yield False,full_response + Session.save_session(query, full_response, user_id) + log.info("[chatgpt]: reply={}", full_response) + yield True,full_response + + except openai.error.RateLimitError as e: + # rate limit exception + log.warn(e) + if retry_count < 1: + time.sleep(5) + log.warn("[AZURE_OPEN_AI] RateLimit exceed, 第{}次重试".format(retry_count+1)) + yield True, self.reply_text_stream(query, user_id, retry_count+1) + else: + yield True, "提问太快啦,请休息一下再问我吧" + except openai.error.APIConnectionError as e: + log.warn(e) + log.warn("[AZURE_OPEN_AI] APIConnection failed") + yield True, "我连接不到网络,请稍后重试" + except openai.error.Timeout as e: + log.warn(e) + log.warn("[AZURE_OPEN_AI] Timeout") + yield True, "我没有收到消息,请稍后重试" + except Exception as e: + # unknown exception + log.exception(e) + Session.clear_session(user_id) + yield True, "请再问我一次吧" + + def create_img(self, query, retry_count=0): + try: + log.info("[OPEN_AI] image_query={}".format(query)) + response = openai.Image.create( + prompt=query, #图片描述 + n=1, #每次生成图片的数量 + size="256x256" #图片大小,可选有 256x256, 512x512, 1024x1024 + ) + image_url = response['data'][0]['url'] + log.info("[OPEN_AI] image_url={}".format(image_url)) + return [image_url] + except openai.error.RateLimitError as e: + log.warn(e) + if retry_count < 1: + time.sleep(5) + log.warn("[OPEN_AI] ImgCreate RateLimit exceed, 第{}次重试".format(retry_count+1)) + return self.reply_text(query, retry_count+1) + else: + return "提问太快啦,请休息一下再问我吧" + except Exception as e: + log.exception(e) + return None + + +class Session(object): + @staticmethod + def build_session_query(query, user_id): + ''' + build query with conversation history + 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?"} + ] + :param query: query content + :param user_id: from user id + :return: query content with conversaction + ''' + session = user_session.get(user_id, []) + if len(session) == 0: + system_prompt = model_conf(const.AZURE_OPEN_AI).get("character_desc", "") + system_item = {'role': 'system', 'content': system_prompt} + session.append(system_item) + user_session[user_id] = session + user_item = {'role': 'user', 'content': query} + session.append(user_item) + return session + + @staticmethod + def save_session(query, answer, user_id, used_tokens=0): + max_tokens = model_conf(const.AZURE_OPEN_AI).get('conversation_max_tokens') + max_history_num = model_conf(const.AZURE_OPEN_AI).get('max_history_num', None) + if not max_tokens or max_tokens > 4000: + # default value + max_tokens = 1000 + session = user_session.get(user_id) + if session: + # append conversation + gpt_item = {'role': 'assistant', 'content': answer} + session.append(gpt_item) + + if used_tokens > max_tokens and len(session) >= 3: + # pop first conversation (TODO: more accurate calculation) + session.pop(1) + session.pop(1) + + if max_history_num is not None: + while len(session) > max_history_num * 2 + 1: + session.pop(1) + session.pop(1) + + @staticmethod + def clear_session(user_id): + user_session[user_id] = [] +