ModelScopeAgent / app.py
wenmeng zhou
remove cookie from user_id generation if cookies does not exists in header
c8cac1b
raw
history blame
31.8 kB
import hashlib
import importlib
import os
import random
import shutil
import traceback
import gradio as gr
import json
import yaml
from builder_core import beauty_output, init_builder_chatbot_agent
from config_utils import (DEFAULT_AGENT_DIR, Config, get_avatar_image,
get_ci_dir, get_user_cfg_file, get_user_dir,
is_valid_plugin_configuration, parse_configuration,
save_avatar_image, save_builder_configuration,
save_plugin_configuration)
from gradio_utils import ChatBot, format_cover_html, format_goto_publish_html
from i18n import I18n
from modelscope_agent.utils.logger import agent_logger as logger
from publish_util import (pop_user_info_from_config, prepare_agent_zip,
reload_agent_zip)
from user_core import init_user_chatbot_agent
def init_user(uuid_str, state):
try:
seed = state.get('session_seed', random.randint(0, 1000000000))
user_agent = init_user_chatbot_agent(uuid_str)
user_agent.seed = seed
state['user_agent'] = user_agent
except Exception as e:
logger.error(
uuid=uuid_str,
error=str(e),
content={'error_traceback': traceback.format_exc()})
return state
def init_builder(uuid_str, state):
try:
builder_agent = init_builder_chatbot_agent(uuid_str)
state['builder_agent'] = builder_agent
except Exception as e:
logger.error(
uuid=uuid_str,
error=str(e),
content={'error_traceback': traceback.format_exc()})
return state
def update_builder(uuid_str, state):
try:
builder_agent = init_builder_chatbot_agent(uuid_str)
state['builder_agent'] = builder_agent
except Exception as e:
logger.error(
uuid=uuid_str,
error=str(e),
content={'error_traceback': traceback.format_exc()})
return state
def check_uuid(uuid_str):
if not uuid_str or uuid_str == '':
if os.getenv('MODELSCOPE_ENVIRONMENT') == 'studio':
raise gr.Error('请登陆后使用! (Please login first)')
else:
uuid_str = 'local_user'
return uuid_str
def process_configuration(uuid_str, bot_avatar, name, description,
instructions, model, agent_language, suggestions,
knowledge_files, capabilities_checkboxes,
openapi_schema, openapi_auth, openapi_auth_apikey,
openapi_auth_apikey_type, openapi_privacy_policy,
state, request: gr.Request):
if request:
uuid_str = generate_user_id(request.headers)
logger.info(uuid=uuid_str, message='init_all', step='generate_uuid')
uuid_str = check_uuid(uuid_str)
tool_cfg = state['tool_cfg']
capabilities = state['capabilities']
bot_avatar, bot_avatar_path = save_avatar_image(bot_avatar, uuid_str)
suggestions_filtered = [row for row in suggestions if row[0]]
if len(suggestions_filtered) == 0:
suggestions_filtered == [['']]
user_dir = get_user_dir(uuid_str)
if knowledge_files is not None:
new_knowledge_files = [
os.path.join(user_dir, os.path.basename((f.name)))
for f in knowledge_files
]
for src_file, dst_file in zip(knowledge_files, new_knowledge_files):
if not os.path.exists(dst_file):
shutil.copy(src_file.name, dst_file)
else:
new_knowledge_files = []
builder_cfg = {
'name': name,
'avatar': bot_avatar,
'description': description,
'instruction': instructions,
'prompt_recommend': [row[0] for row in suggestions_filtered],
'knowledge': new_knowledge_files,
'tools': {
capability: dict(
name=tool_cfg[capability]['name'],
is_active=tool_cfg[capability]['is_active'],
use=True if capability in capabilities_checkboxes else False)
for capability in map(lambda item: item[1], capabilities)
},
'model': model,
'language': agent_language,
}
try:
try:
schema_dict = json.loads(openapi_schema)
except json.decoder.JSONDecodeError:
schema_dict = yaml.safe_load(openapi_schema)
except Exception as e:
raise gr.Error(
f'OpenAPI schema format error, should be one of json and yaml: {e}'
)
openapi_plugin_cfg = {
'schema': schema_dict,
'auth': {
'type': openapi_auth,
'apikey': openapi_auth_apikey,
'apikey_type': openapi_auth_apikey_type
},
'privacy_policy': openapi_privacy_policy
}
if is_valid_plugin_configuration(openapi_plugin_cfg):
save_plugin_configuration(openapi_plugin_cfg, uuid_str)
except Exception as e:
logger.error(
uuid=uuid_str,
error=str(e),
content={'error_traceback': traceback.format_exc()})
save_builder_configuration(builder_cfg, uuid_str)
update_builder(uuid_str, state)
init_user(uuid_str, state)
return [
gr.HTML.update(
visible=True,
value=format_cover_html(builder_cfg, bot_avatar_path)),
gr.Chatbot.update(
visible=False,
avatar_images=get_avatar_image(bot_avatar, uuid_str)),
gr.Dataset.update(samples=suggestions_filtered),
gr.DataFrame.update(value=suggestions_filtered)
]
# 创建 Gradio 界面
demo = gr.Blocks(css='assets/app.css')
with demo:
uuid_str = gr.Textbox(label='modelscope_uuid', visible=False)
draw_seed = random.randint(0, 1000000000)
state = gr.State({'session_seed': draw_seed})
#i18n = I18n('zh-cn')
i18n = I18n('en')
with gr.Row():
with gr.Column(scale=5):
header = gr.Markdown(i18n.get('header'))
with gr.Column(scale=1):
language = gr.Dropdown(
choices=[('English', 'en'), ('中文', 'zh-cn')],
show_label=False,
container=False,
value='en',
interactive=True)
with gr.Row():
with gr.Column():
with gr.Tabs() as tabs:
with gr.Tab(i18n.get_whole('create'), id=0) as create_tab:
with gr.Column():
# "Create" 标签页的 Chatbot 组件
start_text = i18n.get('start_text')
create_chatbot = gr.Chatbot(
show_label=False, value=[[None, start_text]])
create_chat_input = gr.Textbox(
label=i18n.get('message'),
placeholder=i18n.get('message_placeholder'))
create_send_button = gr.Button(
i18n.get('sendOnLoading'), interactive=False)
configure_tab = gr.Tab(i18n.get_whole('configure'), id=1)
with configure_tab:
with gr.Column():
# "Configure" 标签页的配置输入字段
with gr.Row():
bot_avatar_comp = gr.Image(
label=i18n.get('form_avatar'),
placeholder='Chatbot avatar image',
source='upload',
interactive=True,
type='filepath',
scale=1,
width=182,
height=182,
)
with gr.Column(scale=4):
name_input = gr.Textbox(
label=i18n.get('form_name'),
placeholder=i18n.get(
'form_name_placeholder'))
description_input = gr.Textbox(
label=i18n.get('form_description'),
placeholder=i18n.get(
'form_description_placeholder'))
instructions_input = gr.Textbox(
label=i18n.get('form_instructions'),
placeholder=i18n.get(
'form_instructions_placeholder'),
lines=3)
model_selector = gr.Dropdown(
label=i18n.get('form_model'))
agent_language_selector = gr.Dropdown(
label=i18n.get('form_agent_language'),
choices=['zh', 'en'],
value='zh')
suggestion_input = gr.Dataframe(
show_label=False,
value=[['']],
datatype=['str'],
headers=[i18n.get_whole('form_prompt_suggestion')],
type='array',
col_count=(1, 'fixed'),
interactive=True)
gr.Markdown(
i18n.get_whole('knowledge_text'))
knowledge_input = gr.File(
label=i18n.get('form_knowledge'),
file_count='multiple',
file_types=[
'text', '.json', '.csv', '.pdf', '.md'
])
capabilities_checkboxes = gr.CheckboxGroup(
label=i18n.get('form_capabilities'))
with gr.Accordion(
i18n.get('open_api_accordion'),
open=False) as open_api_accordion:
openapi_schema = gr.Textbox(
label='Schema',
placeholder=
'Enter your OpenAPI schema here, JSON or YAML format only'
)
with gr.Group():
openapi_auth_type = gr.Radio(
label='Authentication Type',
choices=['None', 'API Key'],
value='None')
openapi_auth_apikey = gr.Textbox(
label='API Key',
placeholder='Enter your API Key here')
openapi_auth_apikey_type = gr.Radio(
label='API Key type', choices=['Bearer'])
openapi_privacy_policy = gr.Textbox(
label='Privacy Policy',
placeholder='Enter privacy policy URL')
configure_button = gr.Button(
i18n.get('form_update_button'))
with gr.Accordion(
label=i18n.get('import_config'),
open=False) as update_accordion:
with gr.Column():
update_space = gr.Textbox(
label=i18n.get('space_addr'),
placeholder=i18n.get('input_space_addr'))
import_button = gr.Button(
i18n.get_whole('import_space'))
gr.Markdown(
f'#### {i18n.get_whole("import_hint")}')
with gr.Column():
# Preview
preview_header = gr.HTML(
f"""<div class="preview_header">{i18n.get('preview')}<div>""")
user_chat_bot_cover = gr.HTML(format_cover_html({}, None))
user_chatbot = ChatBot(
value=[[None, None]],
elem_id='user_chatbot',
elem_classes=['markdown-body'],
avatar_images=get_avatar_image('', uuid_str),
height=650,
latex_delimiters=[],
show_label=False,
visible=False)
preview_chat_input = gr.Textbox(
label=i18n.get('message'),
placeholder=i18n.get('message_placeholder'))
user_chat_bot_suggest = gr.Dataset(
label=i18n.get('prompt_suggestion'),
components=[preview_chat_input],
samples=[])
# preview_send_button = gr.Button('Send')
with gr.Row():
upload_button = gr.UploadButton(
i18n.get('upload_btn'),
file_types=['file', 'image', 'audio', 'video', 'text'],
file_count='multiple')
preview_send_button = gr.Button(
i18n.get('sendOnLoading'), interactive=False)
user_chat_bot_suggest.select(
lambda evt: evt[0],
inputs=[user_chat_bot_suggest],
outputs=[preview_chat_input])
with gr.Accordion(
label=i18n.get('publish'),
open=False) as publish_accordion:
publish_alert_md = gr.Markdown(f'{i18n.get("publish_alert")}')
with gr.Row():
with gr.Column():
publish_button = gr.Button(i18n.get_whole('build'))
build_hint_md = gr.Markdown(
f'#### 1.{i18n.get("build_hint")}')
with gr.Column():
publish_link = gr.HTML(
value=format_goto_publish_html(
i18n.get_whole('publish'), '', {}, True))
publish_hint_md = gr.Markdown(
f'#### 2.{i18n.get("publish_hint")}')
configure_updated_outputs = [
state,
# config form
bot_avatar_comp,
name_input,
description_input,
instructions_input,
model_selector,
agent_language_selector,
suggestion_input,
knowledge_input,
capabilities_checkboxes,
# bot
user_chat_bot_cover,
user_chat_bot_suggest,
preview_send_button,
create_send_button,
]
# 初始化表单
def init_ui_config(uuid_str, _state, builder_cfg, model_cfg, tool_cfg):
logger.info(
uuid=uuid_str,
message='builder_cfg',
content={'builder_cfg': str(builder_cfg)})
# available models
models = list(model_cfg.keys())
capabilities = [(tool_cfg[tool_key]['name'], tool_key)
for tool_key in tool_cfg.keys()
if tool_cfg[tool_key].get('is_active', False)]
_state['model_cfg'] = model_cfg
_state['tool_cfg'] = tool_cfg
_state['capabilities'] = capabilities
bot_avatar = get_avatar_image(builder_cfg.get('avatar', ''),
uuid_str)[1]
suggests = builder_cfg.get('prompt_recommend', [''])
return {
state:
_state,
bot_avatar_comp:
gr.Image.update(value=bot_avatar),
name_input:
builder_cfg.get('name', ''),
description_input:
builder_cfg.get('description'),
instructions_input:
builder_cfg.get('instruction'),
model_selector:
gr.Dropdown.update(
value=builder_cfg.get('model', models[0]), choices=models),
agent_language_selector:
builder_cfg.get('language') or 'zh',
suggestion_input:
[[str] for str in suggests] if len(suggests) > 0 else [['']],
knowledge_input:
builder_cfg.get('knowledge', [])
if len(builder_cfg['knowledge']) > 0 else None,
capabilities_checkboxes:
gr.CheckboxGroup.update(
value=[
tool for tool in builder_cfg.get('tools', {}).keys()
if builder_cfg.get('tools').get(tool).get('use', False)
],
choices=capabilities),
# bot
user_chat_bot_cover:
format_cover_html(builder_cfg, bot_avatar),
user_chat_bot_suggest:
gr.Dataset.update(samples=[[item] for item in suggests]),
}
# tab 切换的事件处理
def on_congifure_tab_select(_state, uuid_str, request: gr.Request):
if request:
uuid_str = generate_user_id(request.headers)
logger.info(uuid=uuid_str, message='init_all', step='generate_uuid')
uuid_str = check_uuid(uuid_str)
configure_updated = _state.get('configure_updated', False)
if configure_updated:
builder_cfg, model_cfg, tool_cfg, available_tool_list, _, _ = parse_configuration(
uuid_str)
_state['configure_updated'] = False
return init_ui_config(uuid_str, _state, builder_cfg, model_cfg,
tool_cfg)
else:
return {state: _state}
configure_tab.select(
on_congifure_tab_select,
inputs=[state, uuid_str],
outputs=configure_updated_outputs)
# 配置 "Create" 标签页的消息发送功能
def format_message_with_builder_cfg(_state, chatbot, builder_cfg,
uuid_str):
uuid_str = check_uuid(uuid_str)
bot_avatar = builder_cfg.get('avatar', '')
prompt_recommend = builder_cfg.get('prompt_recommend', [''])
suggestion = [[row] for row in prompt_recommend]
bot_avatar_path = get_avatar_image(bot_avatar, uuid_str)[1]
save_builder_configuration(builder_cfg, uuid_str)
_state['configure_updated'] = True
return {
create_chatbot:
chatbot,
user_chat_bot_cover:
gr.HTML.update(
visible=True,
value=format_cover_html(builder_cfg, bot_avatar_path)),
user_chatbot:
gr.Chatbot.update(
visible=False,
avatar_images=get_avatar_image(bot_avatar, uuid_str)),
user_chat_bot_suggest:
gr.Dataset.update(samples=suggestion)
}
def create_send_message(chatbot, input, _state, uuid_str, request: gr.Request):
if request:
uuid_str = generate_user_id(request.headers)
logger.info(uuid=uuid_str, message='init_all', step='generate_uuid')
uuid_str = check_uuid(uuid_str)
# 将发送的消息添加到聊天历史
builder_agent = _state['builder_agent']
chatbot.append((input, ''))
yield {
create_chatbot: chatbot,
create_chat_input: gr.Textbox.update(value=''),
}
response = ''
for frame in builder_agent.stream_run(
input, print_info=True, uuid_str=uuid_str):
llm_result = frame.get('llm_text', '')
exec_result = frame.get('exec_result', '')
step_result = frame.get('step', '')
logger.info(
uuid=uuid_str, message='frame', content={'frame': str(frame)})
if len(exec_result) != 0:
if isinstance(exec_result, dict):
exec_result = exec_result['result']
assert isinstance(exec_result, Config)
yield format_message_with_builder_cfg(
_state,
chatbot,
exec_result.to_dict(),
uuid_str=uuid_str)
else:
# llm result
if isinstance(llm_result, dict):
content = llm_result['content']
else:
content = llm_result
frame_text = content
response = beauty_output(f'{response}{frame_text}',
step_result)
chatbot[-1] = (input, response)
yield {
create_chatbot: chatbot,
}
create_send_button.click(
create_send_message,
inputs=[create_chatbot, create_chat_input, state, uuid_str],
outputs=[
create_chatbot, user_chat_bot_cover, user_chatbot,
user_chat_bot_suggest, create_chat_input
])
# 配置 "Configure" 标签页的提交按钮功能
configure_button.click(
process_configuration,
inputs=[
uuid_str, bot_avatar_comp, name_input, description_input,
instructions_input, model_selector, agent_language_selector,
suggestion_input, knowledge_input, capabilities_checkboxes,
openapi_schema, openapi_auth_type, openapi_auth_apikey,
openapi_auth_apikey_type, openapi_privacy_policy, state
],
outputs=[
user_chat_bot_cover, user_chatbot, user_chat_bot_suggest,
suggestion_input
])
# 配置 "Preview" 的消息发送功能
def preview_send_message(chatbot, input, _state, uuid_str, request: gr.Request):
if request:
uuid_str = generate_user_id(request.headers)
logger.info(uuid=uuid_str, message='init_all', step='generate_uuid')
# 将发送的消息添加到聊天历史
_uuid_str = check_uuid(uuid_str)
user_agent = _state['user_agent']
if 'new_file_paths' in _state:
new_file_paths = _state['new_file_paths']
else:
new_file_paths = []
_state['new_file_paths'] = []
chatbot.append((input, ''))
yield {
user_chatbot: gr.Chatbot.update(visible=True, value=chatbot),
user_chat_bot_cover: gr.HTML.update(visible=False),
preview_chat_input: gr.Textbox.update(value='')
}
response = ''
try:
for frame in user_agent.stream_run(
input,
print_info=True,
remote=False,
append_files=new_file_paths,
uuid=_uuid_str):
llm_result = frame.get('llm_text', '')
exec_result = frame.get('exec_result', '')
if len(exec_result) != 0:
# action_exec_result
if isinstance(exec_result, dict):
exec_result = str(exec_result['result'])
frame_text = f'<result>{exec_result}</result>'
else:
# llm result
frame_text = llm_result
# important! do not change this
response += frame_text
chatbot[-1] = (input, response)
yield {user_chatbot: chatbot}
except Exception as e:
if 'dashscope.common.error.AuthenticationError' in str(e):
msg = 'DASHSCOPE_API_KEY should be set via environment variable. You can acquire this in ' \
'https://help.aliyun.com/zh/dashscope/developer-reference/activate-dashscope-and-create-an-api-key'
elif 'rate limit' in str(e):
msg = 'Too many people are calling, please try again later.'
else:
msg = str(e)
chatbot[-1] = (input, msg)
yield {user_chatbot: chatbot}
preview_send_button.click(
preview_send_message,
inputs=[user_chatbot, preview_chat_input, state, uuid_str],
outputs=[user_chatbot, user_chat_bot_cover, preview_chat_input])
def upload_file(chatbot, upload_button, _state, uuid_str, request: gr.Request):
if request:
uuid_str = generate_user_id(request.headers)
logger.info(uuid=uuid_str, message='init_all', step='generate_uuid')
uuid_str = check_uuid(uuid_str)
new_file_paths = []
if 'file_paths' in _state:
file_paths = _state['file_paths']
else:
file_paths = []
for file in upload_button:
file_name = os.path.basename(file.name)
# covert xxx.json to xxx_uuid_str.json
file_name = file_name.replace('.', f'_{uuid_str}.')
file_path = os.path.join(get_ci_dir(), file_name)
if not os.path.exists(file_path):
# make sure file path's directory exists
os.makedirs(os.path.dirname(file_path), exist_ok=True)
shutil.copy(file.name, file_path)
file_paths.append(file_path)
new_file_paths.append(file_path)
if file_name.endswith(('.jpeg', '.png', '.jpg')):
chatbot += [((file_path, ), None)]
else:
chatbot.append((None, f'上传文件{file_name},成功'))
yield {
user_chatbot: gr.Chatbot.update(visible=True, value=chatbot),
user_chat_bot_cover: gr.HTML.update(visible=False),
preview_chat_input: gr.Textbox.update(value='')
}
_state['file_paths'] = file_paths
_state['new_file_paths'] = new_file_paths
upload_button.upload(
upload_file,
inputs=[user_chatbot, upload_button, state, uuid_str],
outputs=[user_chatbot, user_chat_bot_cover, preview_chat_input])
# configuration for publish
def publish_agent(name, uuid_str, state, request: gr.Request):
if request:
uuid_str = generate_user_id(request.headers)
logger.info(uuid=uuid_str, message='init_all', step='generate_uuid')
uuid_str = check_uuid(uuid_str)
env_params = {}
env_params.update(
pop_user_info_from_config(DEFAULT_AGENT_DIR, uuid_str))
output_url, envs_required = prepare_agent_zip(name, DEFAULT_AGENT_DIR,
uuid_str, state)
env_params.update(envs_required)
# output_url = "https://test.url"
return format_goto_publish_html(
i18n.get_whole('publish'), output_url, env_params)
publish_button.click(
publish_agent,
inputs=[name_input, uuid_str, state],
outputs=[publish_link],
)
def import_space(agent_url, uuid_str, state):
uuid_str = check_uuid(uuid_str)
_ = reload_agent_zip(agent_url, DEFAULT_AGENT_DIR, uuid_str, state)
# update config
builder_cfg, model_cfg, tool_cfg, available_tool_list, _, _ = parse_configuration(
uuid_str)
return init_ui_config(uuid_str, state, builder_cfg, model_cfg,
tool_cfg)
import_button.click(
import_space,
inputs=[update_space, uuid_str, state],
outputs=configure_updated_outputs,
)
def change_lang(language):
i18n = I18n(language)
return {
bot_avatar_comp:
gr.Image(label=i18n.get('form_avatar')),
name_input:
gr.Textbox(
label=i18n.get('form_name'),
placeholder=i18n.get('form_name_placeholder')),
description_input:
gr.Textbox(
label=i18n.get('form_description'),
placeholder=i18n.get('form_description_placeholder')),
instructions_input:
gr.Textbox(
label=i18n.get('form_instructions'),
placeholder=i18n.get('form_instructions_placeholder')),
model_selector:
gr.Dropdown(label=i18n.get('form_model')),
agent_language_selector:
gr.Dropdown(label=i18n.get('form_agent_language')),
knowledge_input:
gr.File(label=i18n.get('form_knowledge')),
capabilities_checkboxes:
gr.CheckboxGroup(label=i18n.get('form_capabilities')),
open_api_accordion:
gr.Accordion(label=i18n.get('open_api_accordion')),
configure_button:
gr.Button(i18n.get('form_update_button')),
preview_header:
gr.HTML(
f"""<div class="preview_header">{i18n.get('preview')}<div>"""),
preview_send_button:
gr.Button.update(value=i18n.get('send')),
create_chat_input:
gr.Textbox(
label=i18n.get('message'),
placeholder=i18n.get('message_placeholder')),
create_send_button:
gr.Button.update(value=i18n.get('send')),
user_chat_bot_suggest:
gr.Dataset(label=i18n.get('prompt_suggestion')),
preview_chat_input:
gr.Textbox(
label=i18n.get('message'),
placeholder=i18n.get('message_placeholder')),
publish_accordion:
gr.Accordion(label=i18n.get('publish')),
upload_button:
gr.UploadButton(i18n.get('upload_btn')),
header:
gr.Markdown(i18n.get('header')),
publish_alert_md:
gr.Markdown(f'{i18n.get("publish_alert")}'),
build_hint_md:
gr.Markdown(f'#### 1.{i18n.get("build_hint")}'),
publish_hint_md:
gr.Markdown(f'#### 2.{i18n.get("publish_hint")}'),
}
language.select(
change_lang,
inputs=[language],
outputs=configure_updated_outputs + [
configure_button, create_chat_input, open_api_accordion,
preview_header, preview_chat_input, publish_accordion,
upload_button, header, publish_alert_md, build_hint_md,
publish_hint_md
])
def generate_user_id(request_headers):
# 提取所需的信息
ip_address = request_headers['host']
user_agent = request_headers['user-agent']
accept_language = request_headers['accept-language']
if 'cookie' in request_headers:
cookie = request_headers['cookie']
else:
cookie = 'empty'
print(f'cookie not in request_headers: {request_headers}')
# 将信息组合成一个字符串
combined_info = f"{ip_address}-{user_agent}-{accept_language}-{cookie}"
# 使用哈希函数生成唯一标识符
user_id = hashlib.sha256(combined_info.encode()).hexdigest()
return user_id
def init_all(uuid_str, _state, request: gr.Request):
if request:
uuid_str = generate_user_id(request.headers)
logger.info(uuid=uuid_str, message='init_all', step='generate_uuid')
uuid_str = check_uuid(uuid_str)
builder_cfg, model_cfg, tool_cfg, available_tool_list, _, _ = parse_configuration(
uuid_str)
ret = init_ui_config(uuid_str, _state, builder_cfg, model_cfg,
tool_cfg)
yield ret
init_user(uuid_str, _state)
init_builder(uuid_str, _state)
yield {
state:
_state,
preview_send_button:
gr.Button.update(value=i18n.get('send'), interactive=True),
create_send_button:
gr.Button.update(value=i18n.get('send'), interactive=True),
}
demo.load(
init_all, inputs=[uuid_str, state,], outputs=configure_updated_outputs)
demo.queue(concurrency_count=10)
demo.launch(show_error=True)