').strip('
')) return history +def visualize_audio(chatbot, audio_shape): + if len(chatbot) == 0: chatbot.append(["[ 请讲话 ]", "[ 正在等您说完问题 ]"]) + chatbot[-1] = list(chatbot[-1]) + p1 = '「' + p2 = '」' + chatbot[-1][-1] = re.sub(p1+r'(.*)'+p2, '', chatbot[-1][-1]) + chatbot[-1][-1] += (p1+f"`{audio_shape}`"+p2) + class AsyncGptTask(): def __init__(self) -> None: self.observe_future = [] @@ -81,8 +94,9 @@ class InterviewAssistant(AliyunASR): self.capture_interval = 0.5 # second self.stop = False self.parsed_text = "" # 下个句子中已经说完的部分, 由 test_on_result_chg() 写入 - self.parsed_sentence = "" # 某段话的整个句子,由 test_on_sentence_end() 写入 + self.parsed_sentence = "" # 某段话的整个句子, 由 test_on_sentence_end() 写入 self.buffered_sentence = "" # + self.audio_shape = "" # 音频的可视化表现, 由 audio_convertion_thread() 写入 self.event_on_result_chg = threading.Event() self.event_on_entence_end = threading.Event() self.event_on_commit_question = threading.Event() @@ -117,7 +131,7 @@ class InterviewAssistant(AliyunASR): def begin(self, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt): # main plugin function self.init(chatbot) - chatbot.append(["[请讲话]", "[正在等您说完问题]"]) + chatbot.append(["[ 请讲话 ]", "[ 正在等您说完问题 ]"]) yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 self.plugin_wd.begin_watch() self.agt = AsyncGptTask() @@ -157,14 +171,18 @@ class InterviewAssistant(AliyunASR): self.commit_wd.begin_watch() chatbot[-1] = list(chatbot[-1]) - chatbot[-1] = [self.buffered_sentence, "[等待GPT响应]"] + chatbot[-1] = [self.buffered_sentence, "[ 等待GPT响应 ]"] yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 # add gpt task 创建子线程请求gpt,避免线程阻塞 history = chatbot2history(chatbot) self.agt.add_async_gpt_task(self.buffered_sentence, len(chatbot)-1, llm_kwargs, history, system_prompt) self.buffered_sentence = "" - chatbot.append(["[请讲话]", "[正在等您说完问题]"]) + chatbot.append(["[ 请讲话 ]", "[ 正在等您说完问题 ]"]) + yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 + + if not self.event_on_result_chg.is_set() and not self.event_on_entence_end.is_set() and not self.event_on_commit_question.is_set(): + visualize_audio(chatbot, self.audio_shape) yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 if len(self.stop_msg) != 0: @@ -183,7 +201,7 @@ def 语音助手(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt import nls from scipy import io except: - chatbot.append(["导入依赖失败", "使用该模块需要额外依赖, 安装方法:```pip install --upgrade aliyun-python-sdk-core==2.13.3 pyOpenSSL scipy git+https://github.com/aliyun/alibabacloud-nls-python-sdk.git```"]) + chatbot.append(["导入依赖失败", "使用该模块需要额外依赖, 安装方法:```pip install --upgrade aliyun-python-sdk-core==2.13.3 pyOpenSSL webrtcvad scipy git+https://github.com/aliyun/alibabacloud-nls-python-sdk.git```"]) yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 return diff --git a/docs/GithubAction+AllCapacity b/docs/GithubAction+AllCapacity index 5e50b40..bf9482d 100644 --- a/docs/GithubAction+AllCapacity +++ b/docs/GithubAction+AllCapacity @@ -14,7 +14,7 @@ RUN python3 -m pip install colorama Markdown pygments pymupdf RUN python3 -m pip install python-docx moviepy pdfminer RUN python3 -m pip install zh_langchain==0.2.1 pypinyin RUN python3 -m pip install rarfile py7zr -RUN python3 -m pip install aliyun-python-sdk-core==2.13.3 pyOpenSSL scipy git+https://github.com/aliyun/alibabacloud-nls-python-sdk.git +RUN python3 -m pip install aliyun-python-sdk-core==2.13.3 pyOpenSSL webrtcvad scipy git+https://github.com/aliyun/alibabacloud-nls-python-sdk.git # 下载分支 WORKDIR /gpt RUN git clone --depth=1 https://github.com/binary-husky/gpt_academic.git diff --git a/docs/GithubAction+NoLocal+Latex b/docs/GithubAction+NoLocal+Latex index d460513..cbed50f 100644 --- a/docs/GithubAction+NoLocal+Latex +++ b/docs/GithubAction+NoLocal+Latex @@ -5,6 +5,9 @@ FROM fuqingxu/python311_texlive_ctex:latest +# 删除文档文件以节约空间 +rm -rf /usr/local/texlive/2023/texmf-dist/doc + # 指定路径 WORKDIR /gpt diff --git a/main.py b/main.py index 0c13dcd..25b1f4a 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,7 @@ import os; os.environ['no_proxy'] = '*' # 避免代理网络产生意外污染 +import pickle +import codecs +import base64 def main(): import gradio as gr @@ -10,7 +13,7 @@ def main(): proxies, WEB_PORT, LLM_MODEL, CONCURRENT_COUNT, AUTHENTICATION = get_conf('proxies', 'WEB_PORT', 'LLM_MODEL', 'CONCURRENT_COUNT', 'AUTHENTICATION') CHATBOT_HEIGHT, LAYOUT, AVAIL_LLM_MODELS, AUTO_CLEAR_TXT = get_conf('CHATBOT_HEIGHT', 'LAYOUT', 'AVAIL_LLM_MODELS', 'AUTO_CLEAR_TXT') ENABLE_AUDIO, AUTO_CLEAR_TXT, PATH_LOGGING, AVAIL_THEMES, THEME = get_conf('ENABLE_AUDIO', 'AUTO_CLEAR_TXT', 'PATH_LOGGING', 'AVAIL_THEMES', 'THEME') - DARK_MODE, = get_conf('DARK_MODE') + DARK_MODE, NUM_CUSTOM_BASIC_BTN, SSL_KEYFILE, SSL_CERTFILE = get_conf('DARK_MODE', 'NUM_CUSTOM_BASIC_BTN', 'SSL_KEYFILE', 'SSL_CERTFILE') # 如果WEB_PORT是-1, 则随机选取WEB端口 PORT = find_free_port() if WEB_PORT <= 0 else WEB_PORT @@ -68,9 +71,11 @@ def main(): CHATBOT_HEIGHT /= 2 cancel_handles = [] + customize_btns = {} + predefined_btns = {} with gr.Blocks(title="GPT 学术优化", theme=set_theme, analytics_enabled=False, css=advanced_css) as demo: gr.HTML(title_html) - secret_css, dark_mode = gr.Textbox(visible=False), gr.Textbox(DARK_MODE, visible=False) + secret_css, dark_mode, persistent_cookie = gr.Textbox(visible=False), gr.Textbox(DARK_MODE, visible=False), gr.Textbox(visible=False) cookies = gr.State(load_chat_cookies()) with gr_L1(): with gr_L2(scale=2, elem_id="gpt-chat"): @@ -94,11 +99,16 @@ def main(): status = gr.Markdown(f"Tip: 按Enter提交, 按Shift+Enter换行。当前模型: {LLM_MODEL} \n {proxy_info}", elem_id="state-panel") with gr.Accordion("基础功能区", open=True, elem_id="basic-panel") as area_basic_fn: with gr.Row(): + for k in range(NUM_CUSTOM_BASIC_BTN): + customize_btn = gr.Button("自定义按钮" + str(k+1), visible=False, variant="secondary", info_str=f'基础功能区: 自定义按钮') + customize_btn.style(size="sm") + customize_btns.update({"自定义按钮" + str(k+1): customize_btn}) for k in functional: if ("Visible" in functional[k]) and (not functional[k]["Visible"]): continue variant = functional[k]["Color"] if "Color" in functional[k] else "secondary" functional[k]["Button"] = gr.Button(k, variant=variant, info_str=f'基础功能区: {k}') functional[k]["Button"].style(size="sm") + predefined_btns.update({k: functional[k]["Button"]}) with gr.Accordion("函数插件区", open=True, elem_id="plugin-panel") as area_crazy_fn: with gr.Row(): gr.Markdown("插件可读取“输入区”文本/路径作为参数(上传文件自动修正路径)") @@ -149,6 +159,8 @@ def main(): theme_dropdown = gr.Dropdown(AVAIL_THEMES, value=THEME, label="更换UI主题").style(container=False) checkboxes = gr.CheckboxGroup(["基础功能区", "函数插件区", "浮动输入区", "输入清除键", "插件参数区"], value=["基础功能区", "函数插件区"], label="显示/隐藏功能区", elem_id='cbs').style(container=False) + checkboxes_2 = gr.CheckboxGroup(["自定义菜单"], + value=[], label="显示/隐藏自定义菜单", elem_id='cbs').style(container=False) dark_mode_btn = gr.Button("切换界面明暗 ☀", variant="secondary").style(size="sm") dark_mode_btn.click(None, None, None, _js="""() => { if (document.querySelectorAll('.dark').length) { @@ -173,6 +185,77 @@ def main(): stopBtn2 = gr.Button("停止", variant="secondary"); stopBtn2.style(size="sm") clearBtn2 = gr.Button("清除", variant="secondary", visible=False); clearBtn2.style(size="sm") + def to_cookie_str(d): + # Pickle the dictionary and encode it as a string + pickled_dict = pickle.dumps(d) + cookie_value = base64.b64encode(pickled_dict).decode('utf-8') + return cookie_value + + def from_cookie_str(c): + # Decode the base64-encoded string and unpickle it into a dictionary + pickled_dict = base64.b64decode(c.encode('utf-8')) + return pickle.loads(pickled_dict) + + with gr.Floating(init_x="20%", init_y="50%", visible=False, width="40%", drag="top") as area_customize: + with gr.Accordion("自定义菜单", open=True, elem_id="edit-panel"): + with gr.Row() as row: + with gr.Column(scale=10): + AVAIL_BTN = [btn for btn in customize_btns.keys()] + [k for k in functional] + basic_btn_dropdown = gr.Dropdown(AVAIL_BTN, value="自定义按钮1", label="选择一个需要自定义基础功能区按钮").style(container=False) + basic_fn_title = gr.Textbox(show_label=False, placeholder="输入新按钮名称", lines=1).style(container=False) + basic_fn_prefix = gr.Textbox(show_label=False, placeholder="输入新提示前缀", lines=4).style(container=False) + basic_fn_suffix = gr.Textbox(show_label=False, placeholder="输入新提示后缀", lines=4).style(container=False) + with gr.Column(scale=1, min_width=70): + basic_fn_confirm = gr.Button("确认并保存", variant="primary"); basic_fn_confirm.style(size="sm") + basic_fn_load = gr.Button("加载已保存", variant="primary"); basic_fn_load.style(size="sm") + def assign_btn(persistent_cookie_, cookies_, basic_btn_dropdown_, basic_fn_title, basic_fn_prefix, basic_fn_suffix): + ret = {} + customize_fn_overwrite_ = cookies_['customize_fn_overwrite'] + customize_fn_overwrite_.update({ + basic_btn_dropdown_: + { + "Title":basic_fn_title, + "Prefix":basic_fn_prefix, + "Suffix":basic_fn_suffix, + } + } + ) + cookies_.update(customize_fn_overwrite_) + if basic_btn_dropdown_ in customize_btns: + ret.update({customize_btns[basic_btn_dropdown_]: gr.update(visible=True, value=basic_fn_title)}) + else: + ret.update({predefined_btns[basic_btn_dropdown_]: gr.update(visible=True, value=basic_fn_title)}) + ret.update({cookies: cookies_}) + try: persistent_cookie_ = from_cookie_str(persistent_cookie_) # persistent cookie to dict + except: persistent_cookie_ = {} + persistent_cookie_["custom_bnt"] = customize_fn_overwrite_ # dict update new value + persistent_cookie_ = to_cookie_str(persistent_cookie_) # persistent cookie to dict + ret.update({persistent_cookie: persistent_cookie_}) # write persistent cookie + return ret + + def reflesh_btn(persistent_cookie_, cookies_): + ret = {} + for k in customize_btns: + ret.update({customize_btns[k]: gr.update(visible=False, value="")}) + + try: persistent_cookie_ = from_cookie_str(persistent_cookie_) # persistent cookie to dict + except: return ret + + customize_fn_overwrite_ = persistent_cookie_.get("custom_bnt", {}) + cookies_['customize_fn_overwrite'] = customize_fn_overwrite_ + ret.update({cookies: cookies_}) + + for k,v in persistent_cookie_["custom_bnt"].items(): + if v['Title'] == "": continue + if k in customize_btns: ret.update({customize_btns[k]: gr.update(visible=True, value=v['Title'])}) + else: ret.update({predefined_btns[k]: gr.update(visible=True, value=v['Title'])}) + return ret + + basic_fn_load.click(reflesh_btn, [persistent_cookie, cookies],[cookies, *customize_btns.values(), *predefined_btns.values()]) + h = basic_fn_confirm.click(assign_btn, [persistent_cookie, cookies, basic_btn_dropdown, basic_fn_title, basic_fn_prefix, basic_fn_suffix], + [persistent_cookie, cookies, *customize_btns.values(), *predefined_btns.values()]) + h.then(None, [persistent_cookie], None, _js="""(persistent_cookie)=>{setCookie("persistent_cookie", persistent_cookie, 5);}""") # save persistent cookie + # 功能区显示开关与功能区的互动 def fn_area_visibility(a): ret = {} @@ -186,6 +269,14 @@ def main(): if "浮动输入区" in a: ret.update({txt: gr.update(value="")}) return ret checkboxes.select(fn_area_visibility, [checkboxes], [area_basic_fn, area_crazy_fn, area_input_primary, area_input_secondary, txt, txt2, clearBtn, clearBtn2, plugin_advanced_arg] ) + + # 功能区显示开关与功能区的互动 + def fn_area_visibility_2(a): + ret = {} + ret.update({area_customize: gr.update(visible=("自定义菜单" in a))}) + return ret + checkboxes_2.select(fn_area_visibility_2, [checkboxes_2], [area_customize] ) + # 整理反复出现的控件句柄组合 input_combo = [cookies, max_length_sl, md_dropdown, txt, txt2, top_p, temperature, chatbot, history, system_prompt, plugin_advanced_arg] output_combo = [cookies, chatbot, history, status] @@ -209,6 +300,9 @@ def main(): if ("Visible" in functional[k]) and (not functional[k]["Visible"]): continue click_handle = functional[k]["Button"].click(fn=ArgsGeneralWrapper(predict), inputs=[*input_combo, gr.State(True), gr.State(k)], outputs=output_combo) cancel_handles.append(click_handle) + for btn in customize_btns.values(): + click_handle = btn.click(fn=ArgsGeneralWrapper(predict), inputs=[*input_combo, gr.State(True), gr.State(btn.value)], outputs=output_combo) + cancel_handles.append(click_handle) # 文件上传区,接收文件后与chatbot的互动 file_upload.upload(on_file_uploaded, [file_upload, chatbot, txt, txt2, checkboxes, cookies], [chatbot, txt, txt2, cookies]) file_upload_2.upload(on_file_uploaded, [file_upload_2, chatbot, txt, txt2, checkboxes, cookies], [chatbot, txt, txt2, cookies]) @@ -307,6 +401,10 @@ def main(): } } }""" + load_cookie_js = """(persistent_cookie) => { + return getCookie("persistent_cookie"); + }""" + demo.load(None, inputs=None, outputs=[persistent_cookie], _js=load_cookie_js) demo.load(None, inputs=[dark_mode], outputs=None, _js=darkmode_js) # 配置暗色主题或亮色主题 demo.load(None, inputs=[gr.Textbox(LAYOUT, visible=False)], outputs=None, _js='(LAYOUT)=>{GptAcademicJavaScriptInit(LAYOUT);}') @@ -327,6 +425,9 @@ def main(): demo.queue(concurrency_count=CONCURRENT_COUNT).launch( quiet=True, server_name="0.0.0.0", + ssl_keyfile=None if SSL_KEYFILE == "" else SSL_KEYFILE, + ssl_certfile=None if SSL_CERTFILE == "" else SSL_CERTFILE, + ssl_verify=False, server_port=PORT, favicon_path="docs/logo.png", auth=AUTHENTICATION if len(AUTHENTICATION) != 0 else None, diff --git a/request_llm/bridge_all.py b/request_llm/bridge_all.py index 44e0ae4..0639951 100644 --- a/request_llm/bridge_all.py +++ b/request_llm/bridge_all.py @@ -134,6 +134,15 @@ model_info = { "tokenizer": tokenizer_gpt4, "token_cnt": get_token_num_gpt4, }, + + "gpt-3.5-random": { + "fn_with_ui": chatgpt_ui, + "fn_without_ui": chatgpt_noui, + "endpoint": openai_endpoint, + "max_token": 4096, + "tokenizer": tokenizer_gpt4, + "token_cnt": get_token_num_gpt4, + }, # azure openai "azure-gpt-3.5":{ diff --git a/request_llm/bridge_chatgpt.py b/request_llm/bridge_chatgpt.py index a1b6ba4..660300e 100644 --- a/request_llm/bridge_chatgpt.py +++ b/request_llm/bridge_chatgpt.py @@ -18,6 +18,7 @@ import logging import traceback import requests import importlib +import random # config_private.py放自己的秘密如API和代理网址 # 读取时首先看是否存在私密的config_private配置文件(不受git管控),如果有,则覆盖原config文件 @@ -39,6 +40,21 @@ def get_full_error(chunk, stream_response): break return chunk +def decode_chunk(chunk): + # 提前读取一些信息 (用于判断异常) + chunk_decoded = chunk.decode() + chunkjson = None + has_choices = False + has_content = False + has_role = False + try: + chunkjson = json.loads(chunk_decoded[6:]) + has_choices = 'choices' in chunkjson + if has_choices: has_content = "content" in chunkjson['choices'][0]["delta"] + if has_choices: has_role = "role" in chunkjson['choices'][0]["delta"] + except: + pass + return chunk_decoded, chunkjson, has_choices, has_content, has_role def predict_no_ui_long_connection(inputs, llm_kwargs, history=[], sys_prompt="", observe_window=None, console_slience=False): """ @@ -191,7 +207,9 @@ def predict(inputs, llm_kwargs, plugin_kwargs, chatbot, history=[], system_promp yield from update_ui(chatbot=chatbot, history=history, msg="非OpenAI官方接口返回了错误:" + chunk.decode()) # 刷新界面 return - chunk_decoded = chunk.decode() + # 提前读取一些信息 (用于判断异常) + chunk_decoded, chunkjson, has_choices, has_content, has_role = decode_chunk(chunk) + if is_head_of_the_stream and (r'"object":"error"' not in chunk_decoded) and (r"content" not in chunk_decoded): # 数据流的第一帧不携带content is_head_of_the_stream = False; continue @@ -199,15 +217,23 @@ def predict(inputs, llm_kwargs, plugin_kwargs, chatbot, history=[], system_promp if chunk: try: # 前者是API2D的结束条件,后者是OPENAI的结束条件 - if ('data: [DONE]' in chunk_decoded) or (len(json.loads(chunk_decoded[6:])['choices'][0]["delta"]) == 0): + if ('data: [DONE]' in chunk_decoded) or (len(chunkjson['choices'][0]["delta"]) == 0): # 判定为数据流的结束,gpt_replying_buffer也写完了 logging.info(f'[response] {gpt_replying_buffer}') break # 处理数据流的主体 - chunkjson = json.loads(chunk_decoded[6:]) status_text = f"finish_reason: {chunkjson['choices'][0].get('finish_reason', 'null')}" # 如果这里抛出异常,一般是文本过长,详情见get_full_error的输出 - gpt_replying_buffer = gpt_replying_buffer + chunkjson['choices'][0]["delta"]["content"] + if has_content: + # 正常情况 + gpt_replying_buffer = gpt_replying_buffer + chunkjson['choices'][0]["delta"]["content"] + elif has_role: + # 一些第三方接口的出现这样的错误,兼容一下吧 + continue + else: + # 一些垃圾第三方接口的出现这样的错误 + gpt_replying_buffer = gpt_replying_buffer + chunkjson['choices'][0]["delta"]["content"] + history[-1] = gpt_replying_buffer chatbot[-1] = (history[-2], history[-1]) yield from update_ui(chatbot=chatbot, history=history, msg=status_text) # 刷新界面 @@ -288,9 +314,19 @@ def generate_payload(inputs, llm_kwargs, history, system_prompt, stream): what_i_ask_now["role"] = "user" what_i_ask_now["content"] = inputs messages.append(what_i_ask_now) + model = llm_kwargs['llm_model'].strip('api2d-') + if model == "gpt-3.5-random": # 随机选择, 绕过openai访问频率限制 + model = random.choice([ + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k-0613", + "gpt-3.5-turbo-0301", + ]) + logging.info("Random select model:" + model) payload = { - "model": llm_kwargs['llm_model'].strip('api2d-'), + "model": model, "messages": messages, "temperature": llm_kwargs['temperature'], # 1.0, "top_p": llm_kwargs['top_p'], # 1.0, diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d9f78d6..5998bc4 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -9,7 +9,9 @@ validate_path() # 返回项目根路径 if __name__ == "__main__": from tests.test_utils import plugin_test - plugin_test(plugin='crazy_functions.函数动态生成->函数动态生成', main_input='交换图像的蓝色通道和红色通道', advanced_arg={"file_path_arg": "./build/ants.jpg"}) + # plugin_test(plugin='crazy_functions.函数动态生成->函数动态生成', main_input='交换图像的蓝色通道和红色通道', advanced_arg={"file_path_arg": "./build/ants.jpg"}) + + plugin_test(plugin='crazy_functions.Latex输出PDF结果->Latex翻译中文并重新编译PDF', main_input="2307.07522") # plugin_test(plugin='crazy_functions.虚空终端->虚空终端', main_input='修改api-key为sk-jhoejriotherjep') diff --git a/themes/common.js b/themes/common.js index 4e7a75e..849cb9a 100644 --- a/themes/common.js +++ b/themes/common.js @@ -10,9 +10,33 @@ function gradioApp() { return elem.shadowRoot ? elem.shadowRoot : elem; } +function setCookie(name, value, days) { + var expires = ""; + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toUTCString(); + } + + document.cookie = name + "=" + value + expires + "; path=/"; +} - - +function getCookie(name) { + var decodedCookie = decodeURIComponent(document.cookie); + var cookies = decodedCookie.split(';'); + + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i].trim(); + + if (cookie.indexOf(name + "=") === 0) { + return cookie.substring(name.length + 1, cookie.length); + } + } + + return null; + } + function addCopyButton(botElement) { // https://github.com/GaiZhenbiao/ChuanhuChatGPT/tree/main/web_assets/javascript // Copy bot button diff --git a/toolbox.py b/toolbox.py index 36acbd5..cd6cd1c 100644 --- a/toolbox.py +++ b/toolbox.py @@ -621,10 +621,20 @@ def on_report_generated(cookies, files, chatbot): def load_chat_cookies(): API_KEY, LLM_MODEL, AZURE_API_KEY = get_conf('API_KEY', 'LLM_MODEL', 'AZURE_API_KEY') + DARK_MODE, NUM_CUSTOM_BASIC_BTN = get_conf('DARK_MODE', 'NUM_CUSTOM_BASIC_BTN') if is_any_api_key(AZURE_API_KEY): if is_any_api_key(API_KEY): API_KEY = API_KEY + ',' + AZURE_API_KEY else: API_KEY = AZURE_API_KEY - return {'api_key': API_KEY, 'llm_model': LLM_MODEL} + customize_fn_overwrite_ = {} + for k in range(NUM_CUSTOM_BASIC_BTN): + customize_fn_overwrite_.update({ + "自定义按钮" + str(k+1):{ + "Title": r"", + "Prefix": r"请在自定义菜单中定义提示词前缀.", + "Suffix": r"请在自定义菜单中定义提示词后缀", + } + }) + return {'api_key': API_KEY, 'llm_model': LLM_MODEL, 'customize_fn_overwrite': customize_fn_overwrite_} def is_openai_api_key(key): CUSTOM_API_KEY_PATTERN, = get_conf('CUSTOM_API_KEY_PATTERN')