bridge_chatgpt_vision.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. """
  2. 该文件中主要包含三个函数
  3. 不具备多线程能力的函数:
  4. 1. predict: 正常对话时使用,具备完备的交互功能,不可多线程
  5. 具备多线程调用能力的函数
  6. 2. predict_no_ui_long_connection:支持多线程
  7. """
  8. import json
  9. import time
  10. import logging
  11. import requests
  12. import base64
  13. import os
  14. import glob
  15. from toolbox import get_conf, update_ui, is_any_api_key, select_api_key, what_keys, clip_history, trimmed_format_exc, is_the_upload_folder, \
  16. update_ui_lastest_msg, get_max_token, encode_image, have_any_recent_upload_image_files
  17. proxies, TIMEOUT_SECONDS, MAX_RETRY, API_ORG, AZURE_CFG_ARRAY = \
  18. get_conf('proxies', 'TIMEOUT_SECONDS', 'MAX_RETRY', 'API_ORG', 'AZURE_CFG_ARRAY')
  19. timeout_bot_msg = '[Local Message] Request timeout. Network error. Please check proxy settings in config.py.' + \
  20. '网络错误,检查代理服务器是否可用,以及代理设置的格式是否正确,格式须是[协议]://[地址]:[端口],缺一不可。'
  21. def report_invalid_key(key):
  22. if get_conf("BLOCK_INVALID_APIKEY"):
  23. # 实验性功能,自动检测并屏蔽失效的KEY,请勿使用
  24. from request_llms.key_manager import ApiKeyManager
  25. api_key = ApiKeyManager().add_key_to_blacklist(key)
  26. def get_full_error(chunk, stream_response):
  27. """
  28. 获取完整的从Openai返回的报错
  29. """
  30. while True:
  31. try:
  32. chunk += next(stream_response)
  33. except:
  34. break
  35. return chunk
  36. def decode_chunk(chunk):
  37. # 提前读取一些信息 (用于判断异常)
  38. chunk_decoded = chunk.decode()
  39. chunkjson = None
  40. has_choices = False
  41. choice_valid = False
  42. has_content = False
  43. has_role = False
  44. try:
  45. chunkjson = json.loads(chunk_decoded[6:])
  46. has_choices = 'choices' in chunkjson
  47. if has_choices: choice_valid = (len(chunkjson['choices']) > 0)
  48. if has_choices and choice_valid: has_content = "content" in chunkjson['choices'][0]["delta"]
  49. if has_choices and choice_valid: has_role = "role" in chunkjson['choices'][0]["delta"]
  50. except:
  51. pass
  52. return chunk_decoded, chunkjson, has_choices, choice_valid, has_content, has_role
  53. from functools import lru_cache
  54. @lru_cache(maxsize=32)
  55. def verify_endpoint(endpoint):
  56. """
  57. 检查endpoint是否可用
  58. """
  59. return endpoint
  60. def predict_no_ui_long_connection(inputs, llm_kwargs, history=[], sys_prompt="", observe_window=None, console_slience=False):
  61. raise NotImplementedError
  62. def predict(inputs, llm_kwargs, plugin_kwargs, chatbot, history=[], system_prompt='', stream = True, additional_fn=None):
  63. have_recent_file, image_paths = have_any_recent_upload_image_files(chatbot)
  64. if is_any_api_key(inputs):
  65. chatbot._cookies['api_key'] = inputs
  66. chatbot.append(("输入已识别为openai的api_key", what_keys(inputs)))
  67. yield from update_ui(chatbot=chatbot, history=history, msg="api_key已导入") # 刷新界面
  68. return
  69. elif not is_any_api_key(chatbot._cookies['api_key']):
  70. chatbot.append((inputs, "缺少api_key。\n\n1. 临时解决方案:直接在输入区键入api_key,然后回车提交。\n\n2. 长效解决方案:在config.py中配置。"))
  71. yield from update_ui(chatbot=chatbot, history=history, msg="缺少api_key") # 刷新界面
  72. return
  73. if not have_recent_file:
  74. chatbot.append((inputs, "没有检测到任何近期上传的图像文件,请上传jpg格式的图片,此外,请注意拓展名需要小写"))
  75. yield from update_ui(chatbot=chatbot, history=history, msg="等待图片") # 刷新界面
  76. return
  77. if os.path.exists(inputs):
  78. chatbot.append((inputs, "已经接收到您上传的文件,您不需要再重复强调该文件的路径了,请直接输入您的问题。"))
  79. yield from update_ui(chatbot=chatbot, history=history, msg="等待指令") # 刷新界面
  80. return
  81. user_input = inputs
  82. if additional_fn is not None:
  83. from core_functional import handle_core_functionality
  84. inputs, history = handle_core_functionality(additional_fn, inputs, history, chatbot)
  85. raw_input = inputs
  86. logging.info(f'[raw_input] {raw_input}')
  87. def make_media_input(inputs, image_paths):
  88. for image_path in image_paths:
  89. inputs = inputs + f'<br/><br/><div align="center"><img src="file={os.path.abspath(image_path)}"></div>'
  90. return inputs
  91. chatbot.append((make_media_input(inputs, image_paths), ""))
  92. yield from update_ui(chatbot=chatbot, history=history, msg="等待响应") # 刷新界面
  93. # check mis-behavior
  94. if is_the_upload_folder(user_input):
  95. chatbot[-1] = (inputs, f"[Local Message] 检测到操作错误!当您上传文档之后,需点击“**函数插件区**”按钮进行处理,请勿点击“提交”按钮或者“基础功能区”按钮。")
  96. yield from update_ui(chatbot=chatbot, history=history, msg="正常") # 刷新界面
  97. time.sleep(2)
  98. try:
  99. headers, payload, api_key = generate_payload(inputs, llm_kwargs, history, system_prompt, image_paths)
  100. except RuntimeError as e:
  101. chatbot[-1] = (inputs, f"您提供的api-key不满足要求,不包含任何可用于{llm_kwargs['llm_model']}的api-key。您可能选择了错误的模型或请求源。")
  102. yield from update_ui(chatbot=chatbot, history=history, msg="api-key不满足要求") # 刷新界面
  103. return
  104. # 检查endpoint是否合法
  105. try:
  106. from .bridge_all import model_info
  107. endpoint = verify_endpoint(model_info[llm_kwargs['llm_model']]['endpoint'])
  108. except:
  109. tb_str = '```\n' + trimmed_format_exc() + '```'
  110. chatbot[-1] = (inputs, tb_str)
  111. yield from update_ui(chatbot=chatbot, history=history, msg="Endpoint不满足要求") # 刷新界面
  112. return
  113. history.append(make_media_input(inputs, image_paths))
  114. history.append("")
  115. retry = 0
  116. while True:
  117. try:
  118. # make a POST request to the API endpoint, stream=True
  119. response = requests.post(endpoint, headers=headers, proxies=proxies,
  120. json=payload, stream=True, timeout=TIMEOUT_SECONDS);break
  121. except:
  122. retry += 1
  123. chatbot[-1] = ((chatbot[-1][0], timeout_bot_msg))
  124. retry_msg = f",正在重试 ({retry}/{MAX_RETRY}) ……" if MAX_RETRY > 0 else ""
  125. yield from update_ui(chatbot=chatbot, history=history, msg="请求超时"+retry_msg) # 刷新界面
  126. if retry > MAX_RETRY: raise TimeoutError
  127. gpt_replying_buffer = ""
  128. is_head_of_the_stream = True
  129. if stream:
  130. stream_response = response.iter_lines()
  131. while True:
  132. try:
  133. chunk = next(stream_response)
  134. except StopIteration:
  135. # 非OpenAI官方接口的出现这样的报错,OpenAI和API2D不会走这里
  136. chunk_decoded = chunk.decode()
  137. error_msg = chunk_decoded
  138. # 首先排除一个one-api没有done数据包的第三方Bug情形
  139. if len(gpt_replying_buffer.strip()) > 0 and len(error_msg) == 0:
  140. yield from update_ui(chatbot=chatbot, history=history, msg="检测到有缺陷的非OpenAI官方接口,建议选择更稳定的接口。")
  141. break
  142. # 其他情况,直接返回报错
  143. chatbot, history = handle_error(inputs, llm_kwargs, chatbot, history, chunk_decoded, error_msg, api_key)
  144. yield from update_ui(chatbot=chatbot, history=history, msg="非OpenAI官方接口返回了错误:" + chunk.decode()) # 刷新界面
  145. return
  146. # 提前读取一些信息 (用于判断异常)
  147. chunk_decoded, chunkjson, has_choices, choice_valid, has_content, has_role = decode_chunk(chunk)
  148. if is_head_of_the_stream and (r'"object":"error"' not in chunk_decoded) and (r"content" not in chunk_decoded):
  149. # 数据流的第一帧不携带content
  150. is_head_of_the_stream = False; continue
  151. if chunk:
  152. try:
  153. if has_choices and not choice_valid:
  154. # 一些垃圾第三方接口的出现这样的错误
  155. continue
  156. # 前者是API2D的结束条件,后者是OPENAI的结束条件
  157. if ('data: [DONE]' in chunk_decoded) or (len(chunkjson['choices'][0]["delta"]) == 0):
  158. # 判定为数据流的结束,gpt_replying_buffer也写完了
  159. lastmsg = chatbot[-1][-1] + f"\n\n\n\n「{llm_kwargs['llm_model']}调用结束,该模型不具备上下文对话能力,如需追问,请及时切换模型。」"
  160. yield from update_ui_lastest_msg(lastmsg, chatbot, history, delay=1)
  161. logging.info(f'[response] {gpt_replying_buffer}')
  162. break
  163. # 处理数据流的主体
  164. status_text = f"finish_reason: {chunkjson['choices'][0].get('finish_reason', 'null')}"
  165. # 如果这里抛出异常,一般是文本过长,详情见get_full_error的输出
  166. if has_content:
  167. # 正常情况
  168. gpt_replying_buffer = gpt_replying_buffer + chunkjson['choices'][0]["delta"]["content"]
  169. elif has_role:
  170. # 一些第三方接口的出现这样的错误,兼容一下吧
  171. continue
  172. else:
  173. # 一些垃圾第三方接口的出现这样的错误
  174. gpt_replying_buffer = gpt_replying_buffer + chunkjson['choices'][0]["delta"]["content"]
  175. history[-1] = gpt_replying_buffer
  176. chatbot[-1] = (history[-2], history[-1])
  177. yield from update_ui(chatbot=chatbot, history=history, msg=status_text) # 刷新界面
  178. except Exception as e:
  179. yield from update_ui(chatbot=chatbot, history=history, msg="Json解析不合常规") # 刷新界面
  180. chunk = get_full_error(chunk, stream_response)
  181. chunk_decoded = chunk.decode()
  182. error_msg = chunk_decoded
  183. chatbot, history = handle_error(inputs, llm_kwargs, chatbot, history, chunk_decoded, error_msg, api_key)
  184. yield from update_ui(chatbot=chatbot, history=history, msg="Json异常" + error_msg) # 刷新界面
  185. print(error_msg)
  186. return
  187. def handle_error(inputs, llm_kwargs, chatbot, history, chunk_decoded, error_msg, api_key=""):
  188. from .bridge_all import model_info
  189. openai_website = ' 请登录OpenAI查看详情 https://platform.openai.com/signup'
  190. if "reduce the length" in error_msg:
  191. if len(history) >= 2: history[-1] = ""; history[-2] = "" # 清除当前溢出的输入:history[-2] 是本次输入, history[-1] 是本次输出
  192. history = clip_history(inputs=inputs, history=history, tokenizer=model_info[llm_kwargs['llm_model']]['tokenizer'],
  193. max_token_limit=(model_info[llm_kwargs['llm_model']]['max_token'])) # history至少释放二分之一
  194. chatbot[-1] = (chatbot[-1][0], "[Local Message] Reduce the length. 本次输入过长, 或历史数据过长. 历史缓存数据已部分释放, 您可以请再次尝试. (若再次失败则更可能是因为输入过长.)")
  195. elif "does not exist" in error_msg:
  196. chatbot[-1] = (chatbot[-1][0], f"[Local Message] Model {llm_kwargs['llm_model']} does not exist. 模型不存在, 或者您没有获得体验资格.")
  197. elif "Incorrect API key" in error_msg:
  198. chatbot[-1] = (chatbot[-1][0], "[Local Message] Incorrect API key. OpenAI以提供了不正确的API_KEY为由, 拒绝服务. " + openai_website); report_invalid_key(api_key)
  199. elif "exceeded your current quota" in error_msg:
  200. chatbot[-1] = (chatbot[-1][0], "[Local Message] You exceeded your current quota. OpenAI以账户额度不足为由, 拒绝服务." + openai_website); report_invalid_key(api_key)
  201. elif "account is not active" in error_msg:
  202. chatbot[-1] = (chatbot[-1][0], "[Local Message] Your account is not active. OpenAI以账户失效为由, 拒绝服务." + openai_website); report_invalid_key(api_key)
  203. elif "associated with a deactivated account" in error_msg:
  204. chatbot[-1] = (chatbot[-1][0], "[Local Message] You are associated with a deactivated account. OpenAI以账户失效为由, 拒绝服务." + openai_website); report_invalid_key(api_key)
  205. elif "API key has been deactivated" in error_msg:
  206. chatbot[-1] = (chatbot[-1][0], "[Local Message] API key has been deactivated. OpenAI以账户失效为由, 拒绝服务." + openai_website); report_invalid_key(api_key)
  207. elif "bad forward key" in error_msg:
  208. chatbot[-1] = (chatbot[-1][0], "[Local Message] Bad forward key. API2D账户额度不足.")
  209. elif "Not enough point" in error_msg:
  210. chatbot[-1] = (chatbot[-1][0], "[Local Message] Not enough point. API2D账户点数不足.")
  211. else:
  212. from toolbox import regular_txt_to_markdown
  213. tb_str = '```\n' + trimmed_format_exc() + '```'
  214. chatbot[-1] = (chatbot[-1][0], f"[Local Message] 异常 \n\n{tb_str} \n\n{regular_txt_to_markdown(chunk_decoded)}")
  215. return chatbot, history
  216. def generate_payload(inputs, llm_kwargs, history, system_prompt, image_paths):
  217. """
  218. 整合所有信息,选择LLM模型,生成http请求,为发送请求做准备
  219. """
  220. if not is_any_api_key(llm_kwargs['api_key']):
  221. raise AssertionError("你提供了错误的API_KEY。\n\n1. 临时解决方案:直接在输入区键入api_key,然后回车提交。\n\n2. 长效解决方案:在config.py中配置。")
  222. api_key = select_api_key(llm_kwargs['api_key'], llm_kwargs['llm_model'])
  223. headers = {
  224. "Content-Type": "application/json",
  225. "Authorization": f"Bearer {api_key}"
  226. }
  227. if API_ORG.startswith('org-'): headers.update({"OpenAI-Organization": API_ORG})
  228. if llm_kwargs['llm_model'].startswith('azure-'):
  229. headers.update({"api-key": api_key})
  230. if llm_kwargs['llm_model'] in AZURE_CFG_ARRAY.keys():
  231. azure_api_key_unshared = AZURE_CFG_ARRAY[llm_kwargs['llm_model']]["AZURE_API_KEY"]
  232. headers.update({"api-key": azure_api_key_unshared})
  233. base64_images = []
  234. for image_path in image_paths:
  235. base64_images.append(encode_image(image_path))
  236. messages = []
  237. what_i_ask_now = {}
  238. what_i_ask_now["role"] = "user"
  239. what_i_ask_now["content"] = []
  240. what_i_ask_now["content"].append({
  241. "type": "text",
  242. "text": inputs
  243. })
  244. for image_path, base64_image in zip(image_paths, base64_images):
  245. what_i_ask_now["content"].append({
  246. "type": "image_url",
  247. "image_url": {
  248. "url": f"data:image/jpeg;base64,{base64_image}"
  249. }
  250. })
  251. messages.append(what_i_ask_now)
  252. model = llm_kwargs['llm_model']
  253. if llm_kwargs['llm_model'].startswith('api2d-'):
  254. model = llm_kwargs['llm_model'][len('api2d-'):]
  255. payload = {
  256. "model": model,
  257. "messages": messages,
  258. "temperature": llm_kwargs['temperature'], # 1.0,
  259. "top_p": llm_kwargs['top_p'], # 1.0,
  260. "n": 1,
  261. "stream": True,
  262. "max_tokens": get_max_token(llm_kwargs),
  263. "presence_penalty": 0,
  264. "frequency_penalty": 0,
  265. }
  266. try:
  267. print(f" {llm_kwargs['llm_model']} : {inputs[:100]} ..........")
  268. except:
  269. print('输入中可能存在乱码。')
  270. return headers, payload, api_key