import os
import streamlit as st
from dotenv import load_dotenv
from agent import DebateManager
from datetime import datetime
# 确保在任何机器/任何启动方式下都能加载 .env(符合课程要求)
load_dotenv()
# 启动即检查 API Key,避免老师电脑运行时“无提示失败”
_api_key = os.getenv("OPENAI_API_KEY") or os.getenv("DEEPSEEK_API_KEY")
if not _api_key:
st.error(
"未检测到 API Key。\n\n"
"请按课程要求在 multi_agent_workshop/ 目录下创建 .env 文件(可参考 .env.example),并设置:\n"
"- OPENAI_API_KEY=sk-xxxxxx(推荐)\n"
"或\n"
"- DEEPSEEK_API_KEY=sk-xxxxxx\n\n"
"然后重新运行:uv run streamlit run app.py"
)
st.stop()
from ui.rendering import render_decision_summary
# 从 ui.ui_components 模块导入所需的函数
from ui.ui_components import (
local_css,
show_welcome_guide,
show_role_descriptions,
show_example_topic,
validate_input,
show_progress_indicator,
show_empty_state,
show_debate_summary,
show_debate_timeline,
show_debate_comparison,
)
# 读取本地 CSS(原先大量内联 CSS 已经在 style.css,此处仅负责注入)
local_css("style.css")
st.set_page_config(
page_title="多Agent决策工作坊",
page_icon="🤖",
layout="wide",
initial_sidebar_state="expanded",
)
def save_debate_history(topic, selected_agents, debate_rounds, debate_history, decision_points):
"""保存辩论历史到session state"""
if "saved_debates" not in st.session_state:
st.session_state.saved_debates = []
debate_record = {
"id": len(st.session_state.saved_debates) + 1,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"topic": topic[:100] + "..." if len(topic) > 100 else topic,
"agents": selected_agents,
"rounds": debate_rounds,
"debate_history": debate_history,
"decision_points": decision_points,
}
st.session_state.saved_debates.append(debate_record)
return debate_record
def show_saved_debates(role_descriptions):
"""显示保存的辩论记录"""
if "saved_debates" not in st.session_state or not st.session_state.saved_debates:
return
st.markdown("---")
st.subheader("💾 历史记录")
for record in reversed(st.session_state.saved_debates[-5:]):
with st.expander(f"📅 {record['timestamp']} - {record['topic']}", expanded=False):
col1, col2, col3 = st.columns(3)
with col1:
st.markdown(f"**参与角色**:{len(record['agents'])} 个")
with col2:
st.markdown(f"**评审轮次**:{record['rounds']} 轮")
with col3:
st.markdown(
f"**观点数量**:{sum(len(r.get('opinions', {})) for r in record['debate_history'][1:])} 条"
)
if st.button(f"查看详情", key=f"view_{record['id']}", use_container_width=True):
st.session_state.debate_history = record["debate_history"]
st.session_state.decision_points = record["decision_points"]
st.rerun()
with st.sidebar:
st.markdown(
"""
""",
unsafe_allow_html=True,
)
st.markdown("---")
st.subheader("📋 方案描述")
with st.expander("🧩 一键插入结构化模板(推荐)", expanded=False):
st.markdown(
"""
如果你不知道怎么写方案,建议用下面模板,AI 输出会更稳定:
- 目标(要解决什么问题?)
- 目标用户(谁用?使用场景?)
- 核心功能(3-5 条)
- 约束条件(时间/预算/技术/合规等)
- 成功指标(如何衡量?)
- 你最担心的风险(可选)
"""
)
if st.button("📌 插入模板到输入框", use_container_width=True):
st.session_state.topic = (
"【目标】\n"
"\n"
"【目标用户/场景】\n"
"\n"
"【核心功能(3-5条)】\n"
"1. \n2. \n3. \n"
"\n"
"【约束条件】\n"
"- 时间:\n- 预算:\n- 技术:\n"
"\n"
"【成功指标】\n"
"\n"
"【已知风险/担忧(可选)】\n"
)
st.rerun()
topic = st.text_area(
"请输入需要评审的方案内容",
placeholder="请尽量结构化描述:目标 / 用户 / 核心功能 / 约束 / 指标...",
height=180,
help="建议提供至少50字的详细描述,以获得更准确的分析结果",
)
if st.button("📝 使用示例方案", key="use_example"):
st.session_state.topic = show_example_topic()
st.rerun()
if "topic" in st.session_state:
topic = st.session_state.topic
st.text_area("方案内容", value=topic, height=180, key="topic_display")
st.markdown("---")
st.subheader("👥 参与角色")
role_descriptions = show_role_descriptions()
# 快捷评审模式(更像产品,演示更丝滑)
mode = st.radio(
"评审模式",
options=["快速(2角色)", "标准(3角色)", "全面(5角色)"],
index=1,
help="用于快速配置参与角色数量",
)
if mode.startswith("快速"):
default_roles = ["product_manager", "tech_expert"]
elif mode.startswith("全面"):
default_roles = list(role_descriptions.keys())
else:
default_roles = ["product_manager", "tech_expert", "user_representative"]
with st.expander("查看角色说明(可选)", expanded=False):
for role_key, role_info in role_descriptions.items():
st.markdown(f"**{role_info['icon']} {role_info['name']}**:{role_info['focus']} ")
st.markdown(f"{role_info['description']}", unsafe_allow_html=True)
st.markdown("---")
selected_agents = st.multiselect(
"选择参与评审的角色",
options=list(role_descriptions.keys()),
format_func=lambda x: f"{role_descriptions[x]['icon']} {role_descriptions[x]['name']}",
default=default_roles,
help="建议至少选择3-4个角色以获得全面的评估",
)
if selected_agents:
st.info(f"已选择 {len(selected_agents)} 个角色")
st.markdown("---")
st.subheader("🔄 评审轮次")
# 单轮版本:固定 1 轮,减少运行时间与 API 调用次数
debate_rounds = 1
est_low = max(1, len(selected_agents)) * 2
est_high = max(1, len(selected_agents)) * 4
st.markdown(
f"""
💡 提示:
• 每个角色各输出一次观点
• 优点:更快、更省调用、更适合课堂演示
⏱ 预计耗时:约 {est_low} ~ {est_high} 秒(与网络和模型负载有关)
""",
unsafe_allow_html=True,
)
st.markdown("---")
errors, warnings = validate_input(topic, selected_agents)
if errors:
for error in errors:
st.error(error)
if warnings:
for warning in warnings:
st.warning(warning)
start_button = st.button(
"🚀 开始评审",
type="primary",
disabled=not topic or not selected_agents or len(errors) > 0,
use_container_width=True,
)
st.title("🤖 多Agent决策工作坊")
show_welcome_guide()
if "debate_manager" not in st.session_state:
st.session_state.debate_manager = DebateManager()
if "debate_history" not in st.session_state:
st.session_state.debate_history = []
if "decision_points" not in st.session_state:
st.session_state.decision_points = ""
if start_button:
st.session_state.debate_manager.reset()
show_progress_indicator(1, 4, "初始化评审环境")
with st.spinner("正在初始化评审环境..."):
for agent_name in selected_agents:
st.session_state.debate_manager.add_agent(agent_name)
show_progress_indicator(2, 4, "启动评审")
with st.spinner("正在进行评审..."):
debate_history = st.session_state.debate_manager.start_debate(topic, debate_rounds)
st.session_state.debate_history = debate_history
show_progress_indicator(3, 4, "生成决策要点")
with st.spinner("正在生成决策要点..."):
decision_points = st.session_state.debate_manager.generate_decision_points()
st.session_state.decision_points = decision_points
show_progress_indicator(4, 4, "完成")
save_debate_history(topic, selected_agents, debate_rounds, debate_history, decision_points)
st.success("✅ 评审完成!")
st.balloons()
if st.session_state.debate_history:
# 1) 先给“结论摘要”(首屏答案)
if st.session_state.decision_points:
st.subheader("✅ 一页结论摘要")
# 产品化渲染:优先用结构化展示;若解析失败,仍保留原 Markdown 兜底(不删除原功能)
try:
render_decision_summary(st.session_state.decision_points)
except Exception:
st.markdown(
f"""
{st.session_state.decision_points}
""",
unsafe_allow_html=True,
)
col1, col2 = st.columns(2)
with col1:
st.download_button(
label="📄 下载结论(Markdown)",
data=st.session_state.decision_points,
file_name="decision_summary.md",
mime="text/markdown",
use_container_width=True,
)
with col2:
if st.button("🔄 重新开始", use_container_width=True):
st.session_state.debate_history = []
st.session_state.decision_points = ""
st.rerun()
st.markdown("---")
# 2) 再给简要统计
show_debate_summary(st.session_state.debate_history, st.session_state.decision_points)
# 3) 过程作为依据:默认折叠展示
with st.expander("查看评审过程(依据)", expanded=False):
show_debate_timeline(st.session_state.debate_history, role_descriptions)
st.markdown("---")
st.subheader("📊 详细辩论结果")
show_debate_comparison(st.session_state.debate_history, role_descriptions)
show_saved_debates(role_descriptions)
else:
show_empty_state()
st.markdown("---")
st.markdown(
"""
💡
使用提示
• 您可以随时调整方案内容与参与角色,然后重新开始评审
• 建议使用示例方案进行第一次测试,以了解系统功能
• 评审结果仅供参考,最终决策请结合实际情况和专业判断
""",
unsafe_allow_html=True,
)