一、理论基础:智能体前端技术选型与 Streamlit 架构深度解析
1.1 智能体前端的核心需求与技术挑战
与传统 Web 应用不同,大模型智能体前端有其独特的核心需求:
- 流式输出支持:需要实时展示大模型逐 token 生成的内容
- 长会话管理:需要持久化保存用户的对话历史和系统状态
- 多模态展示:需要支持 Markdown、代码高亮、图表、文件等多种内容格式
- 实时状态反馈:需要展示智能体的思考过程、工具调用、执行进度
- 低延迟交互:需要保证用户输入和模型响应的流畅性
- 快速迭代能力:需要能够快速调整界面和功能,适应大模型的快速发展
传统前端技术栈的痛点:
- Vue/React 需要掌握 HTML、CSS、JavaScript、Node.js 等全套技术栈,学习成本高
- 需要单独开发后端 API 层,前后端分离增加了系统复杂度
- 流式输出需要自行实现 WebSocket 或 SSE,开发难度大
- 大模型相关的组件(如聊天界面、代码高亮)需要自行开发或引入第三方库
1.2 主流智能体前端技术栈深度对比
基于工业界 2026 年的最新实践,我们对四种主流智能体前端技术栈进行了全面对比:
| 技术栈 | 学习成本 | 开发速度 | 与 Python 后端集成 | 流式输出支持 | 多模态能力 | 企业级适用性 | 生产案例 |
|---|---|---|---|---|---|---|---|
| Streamlit | ⭐ 极低(纯 Python) | ⭐⭐⭐⭐⭐ 最快 | ⭐⭐⭐⭐⭐ 原生集成 | ⭐⭐⭐⭐⭐ 完美支持 | ⭐⭐⭐⭐ 优秀 | ⭐⭐⭐⭐ 适合内部工具、原型、MVP | 字节跳动、OpenAI、Anthropic 内部工具 |
| Chainlit | ⭐⭐ 低 | ⭐⭐⭐⭐ 快 | ⭐⭐⭐⭐ 专为 LangChain 设计 | ⭐⭐⭐⭐⭐ 完美支持 | ⭐⭐⭐⭐ 优秀 | ⭐⭐⭐⭐ 适合 LangChain 生态 | LangChain 官方推荐 |
| Gradio | ⭐ 极低 | ⭐⭐⭐⭐ 快 | ⭐⭐⭐⭐ 原生集成 | ⭐⭐⭐⭐ 良好 | ⭐⭐⭐⭐ 优秀 | ⭐⭐⭐ 适合 AI 演示、模型测试 | Hugging Face Spaces |
| Vue/React | ⭐⭐⭐⭐ 高 | ⭐⭐ 慢 | ⭐⭐ 需要 API 封装 | ⭐⭐⭐ 需自行实现 | ⭐⭐⭐⭐⭐ 最强 | ⭐⭐⭐⭐⭐ 适合面向 C 端的产品 | ChatGPT、Claude |
工业级结论 :对于我们的课程和绝大多数企业内部智能体项目,Streamlit 是无可争议的最佳选择:
- 零前端经验要求,所有 Python 开发者都能在 1 小时内上手
- 纯 Python 编写,与我们的后端代码无缝集成,不需要额外的 API 层
- 原生支持流式输出,完美匹配大模型的生成特性
- 丰富的组件生态,开箱即用的聊天、表单、图表、文件上传等功能
- 部署简单,一行命令即可启动服务
- 活跃的社区和官方支持,持续更新大模型相关的功能
1.3.1 Streamlit 运行机制
- 脚本重运行 :每当用户与界面交互(如点击按钮、输入文本),Streamlit 会从头到尾重新运行整个 Python 脚本
- 状态隔离 :每个浏览器标签页对应一个独立的会话,拥有自己的
st.session_state - 增量更新:Streamlit 会比较前后两次运行的输出差异,只更新变化的部分,保证界面流畅
- 组件状态自动管理 :所有输入组件(如
st.text_input、st.slider)会自动管理自己的状态,不需要手动编写事件处理函数
1.3.2 会话状态管理机制
st.session_state是 Streamlit 应用状态管理的核心,它是一个字典 - like 的对象,用于在脚本重运行之间保存数据。
核心特性:
- 会话隔离 :每个用户会话拥有独立的
st.session_state,互不干扰 - 跨页面共享 :在多页面应用中,
st.session_state在所有页面之间共享 - 自动序列化 :Streamlit 会自动序列化
st.session_state中的数据,支持大多数 Python 原生类型 - 组件双向绑定 :通过
key参数可以将组件与st.session_state中的变量进行双向绑定
工业级最佳实践:
- 使用
st.session_state.get('key', default_value)进行健壮的状态初始化 - 为所有输入组件设置唯一的
key参数 - 不要在
st.session_state中存储不可序列化的对象(如数据库连接、文件句柄) - 定期清理不需要的状态,避免内存泄漏
1.4 Streamlit 与 LangGraph 集成的核心优势
LangGraph 负责多智能体的逻辑编排,Streamlit 负责可视化和交互,两者结合是目前最快实现可观测多智能体应用的技术栈:
- 状态共享 :Streamlit 的
st.session_state可以直接存储 LangGraph 的状态和配置 - 流式输出:Streamlit 原生支持 LangGraph 的流式执行结果
- 实时状态展示:可以实时展示 LangGraph 的执行节点、工具调用、中间结果
- 人类介入:可以轻松实现 LangGraph 的人类介入功能,在关键节点等待用户审核
- 快速迭代:可以快速调整界面和智能体逻辑,不需要重新部署后端服务
二、核心实战:搭建企业级智能体工作台
2.1 第一步:环境搭建与项目结构设计
2.1.1 设计思想
- 模块化设计:将前端代码按功能划分为多个页面,便于维护和扩展
- 前后端一体化 :直接复用后端的
RAGService,不需要额外的 API 层 - 配置化:所有可配置项都放在统一的地方,便于调整
- 工业级标准:遵循 Python 编码规范,添加类型注解和异常处理
2.1.2 环境安装
在项目虚拟环境中执行以下命令,安装所有必需的依赖:
pip install streamlit==1.38.0 streamlit-markdown==1.0.3 streamlit-code-editor==0.1.16
2.1.3 项目结构设计
在项目根目录创建frontend文件夹,采用 Streamlit 官方推荐的多页面应用结构:
python
langchain-2026/
├── core/ # 原有后端核心代码(完全复用)
│ ├── rag_service.py
│ ├── llm_factory.py
│ ├── tools.py
│ ├── react_agent.py
│ ├── multi_agent.py
│ └── checkpoint.py
├── config/ # 原有配置(完全复用)
│ └── settings.py
├── data/ # 原有数据目录(完全复用)
├── frontend/ # 新增Streamlit前端
│ ├── pages/ # 多页面应用目录
│ │ ├── 1_chat.py # 智能聊天页面
│ │ ├── 2_report.py # 报告生成页面(第9天集成)
│ │ ├── 3_code.py # 代码生成页面(第10天集成)
│ │ └── 4_admin.py # 系统管理页面
│ └── app.py # 主应用入口
├── main8.py # 原有FastAPI后端(可选,前端可直接调用后端服务)
└── requirements.txt # 依赖清单
2.2 第二步:主应用入口与全局配置
2.2.1 设计思想
- 全局配置优先:所有全局配置都放在主应用入口,确保所有页面都能继承
- 样式统一:自定义全局 CSS 样式,打造企业级界面效果
- 服务单例化 :将
RAGService初始化为全局单例,避免重复初始化 - 异常处理:添加全局异常处理,保证应用的稳定性
2.2.2 核心代码实现
创建frontend/app.py:
python
import streamlit as st
import sys
import os
from typing import Optional
# 将项目根目录添加到Python路径,解决导入问题
# 这是Streamlit应用的标准做法,确保能够正确导入后端模块
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 导入统一日志配置(避免重复配置)
from utils.logger import logger
# ==========================
# 全局页面配置(必须放在最前面)
# ==========================
st.set_page_config(
page_title="企业级智能体平台",
layout="wide",
initial_sidebar_state="expanded",
menu_items={
"Get Help": "https://docs.streamlit.io",
"Report a bug": None,
"About": "# 企业级智能体平台\n基于LangChain和LangGraph构建的工业级智能体平台\n版本:v1.0.0"
}
)
# ==========================
# 自定义全局CSS样式
# ==========================
st.markdown("""
<style>
/* 主容器样式 */
.main .block-container {
padding-top: 2rem;
padding-bottom: 2rem;
max-width: 90%;
}
/* 消息气泡样式 */
.user-message {
background-color: #e3f2fd;
padding: 1rem 1.2rem;
border-radius: 1rem 1rem 0 1rem;
margin-bottom: 1rem;
text-align: right;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.assistant-message {
background-color: #f5f5f5;
padding: 1rem 1.2rem;
border-radius: 1rem 1rem 1rem 0;
margin-bottom: 1rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
/* 代码块样式优化 */
pre {
background-color: #272822 !important;
color: #f8f8f2 !important;
padding: 1rem !important;
border-radius: 0.5rem !important;
overflow-x: auto !important;
font-size: 0.9rem !important;
}
code {
background-color: #f0f0f0 !important;
padding: 0.1rem 0.3rem !important;
border-radius: 0.2rem !important;
font-size: 0.9rem !important;
}
/* 侧边栏样式 */
.css-1d391kg {
background-color: #f8f9fa;
border-right: 1px solid #e9ecef;
}
/* 按钮样式优化 */
.stButton>button {
width: 100%;
border-radius: 0.5rem;
height: 2.5rem;
font-weight: 500;
transition: all 0.2s ease;
}
.stButton>button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* 标题样式 */
h1, h2, h3, h4 {
color: #1976d2;
font-weight: 600;
}
/* 进度条样式 */
.stProgress > div > div > div {
background-color: #1976d2;
}
/* 隐藏侧边栏默认的 app 标题 */
[data-testid="stSidebarNav"] > div:first-child {
display: none !important;
}
/* 自定义侧边栏标题 */
[data-testid="stSidebarNav"]::before {
content: "企业级智能体平台";
display: block;
padding: 1rem;
font-size: 1.2rem;
font-weight: 600;
color: #1976d2;
border-bottom: 1px solid #e9ecef;
margin-bottom: 0.5rem;
}
</style>
""", unsafe_allow_html=True)
# 自定义浏览器标签页标题(覆盖Streamlit默认标题)
st.markdown("""
<script>
document.title = "企业级智能体平台";
</script>
""", unsafe_allow_html=True)
# ==========================
# 全局服务初始化(单例模式)
# ==========================
def init_services() -> None:
"""初始化全局服务,确保只初始化一次"""
if "rag_service" not in st.session_state:
with st.spinner("正在初始化智能体服务..."):
try:
from core.rag_service import RAGService
st.session_state.rag_service = RAGService()
logger.info("RAG服务初始化成功")
except Exception as e:
logger.error(f"RAG服务初始化失败: {str(e)}", exc_info=True)
st.error(f"智能体服务初始化失败: {str(e)}")
st.stop()
# 初始化其他全局状态
if "current_user" not in st.session_state:
st.session_state.current_user = "admin"
if "current_session_id" not in st.session_state:
st.session_state.current_session_id = None
if "messages" not in st.session_state:
st.session_state.messages = []
# 执行服务初始化
init_services()
# ==========================
# 应用主界面
# ==========================
st.title("企业级智能体平台")
st.divider()
# 侧边栏导航
st.sidebar.title("导航菜单")
page = st.sidebar.radio(
"选择功能",
["智能聊天", "报告生成", "代码生成", "系统管理"],
index=0,
help="选择要使用的智能体功能"
)
# 系统状态显示
st.sidebar.divider()
st.sidebar.subheader("系统状态")
if "rag_service" in st.session_state:
st.sidebar.success("智能体服务正常运行")
else:
st.sidebar.error("智能体服务异常")
st.sidebar.info(f"当前用户:{st.session_state.current_user}")
st.sidebar.info(f"当前版本:v1.0.0")
# 根据选择加载对应页面
if page == "智能聊天":
st.switch_page("pages/1_智能聊天.py")
elif page == "报告生成":
st.switch_page("pages/2_报告生成.py")
elif page == "代码生成":
st.switch_page("pages/3_代码生成.py")
elif page == "系统管理":
st.switch_page("pages/4_系统管理.py")
2.2.3 代码解释与注意事项
- 路径处理 :通过
sys.path.append将项目根目录添加到 Python 路径,确保能够正确导入后端模块 - 单例初始化 :通过检查
st.session_state确保RAGService只初始化一次,避免重复加载模型和向量数据库 - 全局样式:通过自定义 CSS 统一界面风格,打造企业级视觉效果
- 异常处理:添加服务初始化异常处理,保证应用能够优雅地处理错误
- 状态隔离:每个用户会话拥有独立的状态,互不干扰
2.3 第三步:核心聊天界面实现
2.3.1 设计思想
- 流式输出优先 :使用 Streamlit 原生的
st.write_stream实现流畅的打字机效果 - 消息历史管理 :使用
st.session_state保存消息历史,支持多轮对话 - 多模式支持:支持普通 RAG 和 ReAct Agent 两种模式,满足不同场景需求
- 用户体验优化:添加加载状态、错误提示、消息气泡等用户体验元素
2.3.2 核心代码实现
创建frontend/pages/1_chat.py:
python
import streamlit as st
import time
from langchain_core.messages import HumanMessage, AIMessage
from typing import Generator
# 页面配置
st.title("💬 智能聊天")
st.divider()
# ==========================
# 侧边栏配置
# ==========================
with st.sidebar:
st.subheader("聊天配置")
# 智能体模式选择
agent_mode = st.selectbox(
"智能体模式",
["普通RAG", "ReAct Agent"],
index=0,
help="普通RAG:仅使用知识库回答问题;ReAct Agent:可以使用工具解决复杂问题"
)
# 模型参数调整
temperature = st.slider(
"温度",
min_value=0.0,
max_value=1.0,
value=0.1,
step=0.1,
help="值越低,回答越准确、越保守;值越高,回答越有创造性、越多样化"
)
max_tokens = st.number_input(
"最大生成长度",
min_value=100,
max_value=4096,
value=2048,
step=100,
help="模型生成的最大token数量"
)
st.divider()
# 会话管理
st.subheader("会话管理")
if st.button("➕ 新建会话", type="primary", use_container_width=True):
st.session_state.current_session_id = f"session_{int(time.time())}"
st.session_state.messages = []
st.rerun()
# 历史会话列表(后续课程将实现持久化)
st.subheader("历史会话")
st.info("历史会话持久化功能将在后续课程中实现")
# ==========================
# 聊天消息显示区域
# ==========================
chat_container = st.container(height=600, border=True)
with chat_container:
# 显示历史消息
for message in st.session_state.messages:
if isinstance(message, HumanMessage):
with st.chat_message("user", avatar="👤"):
st.markdown(message.content)
elif isinstance(message, AIMessage):
with st.chat_message("assistant", avatar="🤖"):
st.markdown(message.content)
# ==========================
# 聊天输入与处理
# ==========================
if prompt := st.chat_input("输入你的问题,我会尽力为你解答..."):
# 添加用户消息到历史
st.session_state.messages.append(HumanMessage(content=prompt))
# 显示用户消息
with chat_container:
with st.chat_message("user", avatar="👤"):
st.markdown(prompt)
# 生成AI回答
with chat_container:
with st.chat_message("assistant", avatar="🤖"):
message_placeholder = st.empty()
full_response = ""
try:
# 调用后端服务,获取流式输出
use_agent = (agent_mode == "ReAct Agent")
# 生成流式响应
stream: Generator[str, None, None] = st.session_state.rag_service.stream_query(
question=prompt,
user_id=st.session_state.current_user,
use_agent=use_agent,
temperature=temperature,
max_tokens=max_tokens
)
# 使用st.write_stream展示流式输出(Streamlit 1.30+推荐方法)
full_response = st.write_stream(stream)
except Exception as e:
full_response = f"❌ 抱歉,发生了一个错误:{str(e)}"
message_placeholder.markdown(full_response)
st.error(f"生成回答失败: {str(e)}")
# 添加AI消息到历史
st.session_state.messages.append(AIMessage(content=full_response))
2.3.3 代码解释与注意事项
- 流式输出 :使用 Streamlit 1.30 + 新增的
st.write_stream方法,这是展示大模型流式输出的最佳实践,比手动循环更新更流畅、性能更好 - 消息类型 :使用 LangChain 的
HumanMessage和AIMessage类型保存消息历史,与后端保持一致 - 参数传递:将前端配置的模型参数(温度、最大生成长度)传递给后端服务,实现灵活的参数调整
- 异常处理:添加全局异常处理,保证应用在出错时能够给出友好的提示
- 用户体验:添加头像、消息气泡、加载状态等元素,提升用户体验
2.4 第四步:其他页面基础框架实现
2.4.1 报告生成页面
python
import streamlit as st
st.title("📊 智能报告生成")
st.divider()
# 侧边栏配置
with st.sidebar:
st.subheader("报告配置")
report_type = st.selectbox(
"报告类型",
["双智能体报告", "三智能体评审报告"],
index=0,
help="双智能体:研究员+写作家;三智能体:研究员+写作家+评审员"
)
auto_approve = st.checkbox(
"自动批准",
value=False,
help="自动批准研究结果,跳过人工审核环节"
)
max_reviews = st.slider(
"最大评审次数",
min_value=1,
max_value=5,
value=3,
help="最多允许修改的次数"
)
# 主界面
col1, col2 = st.columns([3, 1])
with col1:
report_topic = st.text_area(
"报告主题",
height=120,
placeholder="请输入报告主题,例如:生成一份关于公司2026年第一季度销售情况的分析报告"
)
if st.button("🚀 生成报告", type="primary", disabled=not report_topic, use_container_width=True):
st.info("🔧 报告生成功能将在第9天课程中实现")
st.info("当前使用模拟数据演示界面效果")
# 模拟生成过程
progress_bar = st.progress(0)
status_text = st.empty()
stages = ["正在分析需求...", "正在检索信息...", "正在整理研究要点...", "正在生成报告..."]
for i, stage in enumerate(stages):
status_text.text(stage)
for j in range(25):
time.sleep(0.02)
progress_bar.progress(i * 25 + j + 1)
status_text.text("✅ 报告生成完成!")
progress_bar.empty()
# 显示模拟报告
st.success("✅ 报告生成成功!")
st.markdown("""
# 2026年第一季度销售情况分析报告
## 一、摘要
本报告总结了公司2026年第一季度的销售情况,分析了市场趋势和竞品动态,并提出了第二季度的销售建议。第一季度总销售额达到1.2亿元,同比增长25%,超出预期目标。
## 二、核心发现
1. 华东地区表现最佳,贡献了40%的销售额
2. 新产品A系列销量超出预期,成为新的增长点
3. 线上渠道销售额占比首次超过线下渠道
## 三、结论与建议
1. 加大对华东地区的市场投入
2. 继续推广新产品A系列
3. 加强线上渠道建设
""")
with col2:
st.subheader("生成状态")
st.info("等待生成报告")
st.subheader("历史报告")
st.write("暂无历史报告")
2.4.2 代码生成页面
创建frontend/pages/3_code.py:
python
import streamlit as st
st.title("💻 智能代码生成")
st.divider()
# 侧边栏配置
with st.sidebar:
st.subheader("代码配置")
language = st.selectbox(
"编程语言",
["Python", "JavaScript", "Java", "Go"],
index=0
)
include_tests = st.checkbox(
"生成测试用例",
value=True
)
auto_test = st.checkbox(
"自动执行测试",
value=True
)
# 主界面
col1, col2 = st.columns([1, 1])
with col1:
st.subheader("需求描述")
requirement = st.text_area(
"请详细描述你需要的功能",
height=200,
placeholder="例如:实现一个计算器函数,支持加减乘除运算,并处理异常情况"
)
if st.button("🚀 生成代码", type="primary", disabled=not requirement, use_container_width=True):
st.info("🔧 代码生成功能将在第10天课程中实现")
st.info("当前使用模拟数据演示界面效果")
# 模拟生成过程
progress_bar = st.progress(0)
status_text = st.empty()
stages = ["正在分析需求...", "正在生成PRD...", "正在编写代码...", "正在执行测试..."]
for i, stage in enumerate(stages):
status_text.text(stage)
for j in range(25):
time.sleep(0.02)
progress_bar.progress(i * 25 + j + 1)
status_text.text("✅ 代码生成完成!")
progress_bar.empty()
with col2:
st.subheader("生成结果")
st.code("""
def calculator(expression: str) -> str:
\"\"\"
简单计算器函数,支持加减乘除运算
Args:
expression: 数学表达式字符串,例如 "2 + 3 * 4"
Returns:
计算结果字符串
\"\"\"
try:
# 使用eval计算表达式,注意:生产环境中需要添加安全限制
result = eval(expression)
return f"计算结果:{expression} = {result}"
except ZeroDivisionError:
return "错误:除数不能为零"
except SyntaxError:
return "错误:表达式语法错误"
except Exception as e:
return f"错误:{str(e)}"
# 测试用例
def test_calculator():
assert calculator("2 + 3") == "计算结果:2 + 3 = 5"
assert calculator("10 - 5") == "计算结果:10 - 5 = 5"
assert calculator("4 * 6") == "计算结果:4 * 6 = 24"
assert calculator("8 / 2") == "计算结果:8 / 2 = 4.0"
assert calculator("8 / 0") == "错误:除数不能为零"
print("所有测试通过!")
""", language="python")
2.4.3 系统管理页面
创建frontend/pages/4_admin.py:
python
import streamlit as st
import os
st.title("⚙️ 系统管理")
st.divider()
# 标签页
tab1, tab2, tab3 = st.tabs(["系统状态", "知识库管理", "日志查看"])
with tab1:
st.subheader("系统状态概览")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("总会话数", "128", "+12")
st.metric("今日活跃用户", "15", "+3")
with col2:
st.metric("知识库文档数", "32", "+5")
st.metric("总向量数", "1,245", "+120")
with col3:
st.metric("模型调用次数", "1,567", "+234")
st.metric("平均响应时间", "2.3s", "-0.5s")
with col4:
st.metric("任务成功率", "98.7%", "+0.3%")
st.metric("系统运行时间", "7天12小时")
st.divider()
st.subheader("服务状态")
st.success("✅ RAG服务正常运行")
st.success("✅ Agent服务正常运行")
st.success("✅ 向量数据库正常连接")
st.success("✅ LLM服务正常连接")
with tab2:
st.subheader("知识库管理")
# 文件上传
uploaded_files = st.file_uploader(
"上传文档到知识库",
type=["txt", "md", "pdf", "docx", "xlsx"],
accept_multiple_files=True,
help="支持上传txt、md、pdf、docx、xlsx格式的文档"
)
if uploaded_files:
if st.button("添加到知识库", type="primary"):
with st.spinner("正在处理文档..."):
# 这里将在后续课程中集成实际的文档处理功能
for file in uploaded_files:
st.success(f"✅ 已添加:{file.name}")
st.divider()
st.subheader("知识库文档列表")
st.dataframe(
[
{"文件名": "员工手册.pdf", "大小": "2.3MB", "上传时间": "2026-05-20", "状态": "已处理"},
{"文件名": "产品介绍.md", "大小": "156KB", "上传时间": "2026-05-22", "状态": "已处理"},
{"文件名": "销售数据.xlsx", "大小": "456KB", "上传时间": "2026-05-25", "状态": "已处理"}
],
use_container_width=True,
hide_index=True
)
with tab3:
st.subheader("系统日志")
log_level = st.selectbox(
"日志级别",
["INFO", "WARNING", "ERROR", "DEBUG"],
index=0
)
if st.button("刷新日志", use_container_width=True):
st.code("""
2026-05-27 10:23:45 - INFO - 系统启动成功
2026-05-27 10:23:46 - INFO - RAG服务初始化完成
2026-05-27 10:23:46 - INFO - Agent服务初始化完成
2026-05-27 10:24:12 - INFO - 用户admin发起聊天请求
2026-05-27 10:24:15 - INFO - 回答生成完成,耗时3.2秒
2026-05-27 10:25:32 - INFO - 用户admin发起Agent请求
2026-05-27 10:25:38 - INFO - Agent调用工具:calculator
2026-05-27 10:25:40 - INFO - 工具执行完成,返回结果
2026-05-27 10:25:42 - INFO - 回答生成完成,耗时10.1秒
""")
三、测试与验证
3.1 启动前端服务
在项目根目录执行以下命令启动 Streamlit 服务:
python
streamlit run frontend/app.py
