351 lines
12 KiB
Python
351 lines
12 KiB
Python
|
|
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(
|
|||
|
|
"""
|
|||
|
|
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #E2E8F0; margin-bottom: 24px;">
|
|||
|
|
<div style="font-size: 48px; margin-bottom: 8px;">🤖</div>
|
|||
|
|
<h2 style="margin: 0; color: #1E293B; font-size: 1.5rem;">多Agent决策工作坊</h2>
|
|||
|
|
<p style="margin: 8px 0 0 0; color: #64748B; font-size: 0.875rem;">智能辅助决策系统</p>
|
|||
|
|
</div>
|
|||
|
|
""",
|
|||
|
|
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"<span class='small-muted'>{role_info['description']}</span>", 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"""
|
|||
|
|
<div style=\"padding: 12px; background: #F1F5F9; border-radius: 8px; margin-top: 8px;\">
|
|||
|
|
<div style=\"font-size: 0.875rem; color: #64748B; line-height: 1.6;\">
|
|||
|
|
<strong>💡 提示:</strong>
|
|||
|
|
<br>• 每个角色各输出一次观点
|
|||
|
|
<br>• 优点:更快、更省调用、更适合课堂演示
|
|||
|
|
<br><br>
|
|||
|
|
<strong>⏱ 预计耗时:</strong>约 {est_low} ~ {est_high} 秒(与网络和模型负载有关)
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
""",
|
|||
|
|
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"""
|
|||
|
|
<div style="padding: 20px; background: #F0F9FF; border: 1px solid #BAE6FD; border-radius: 12px; border-left: 4px solid #3B82F6;">
|
|||
|
|
<div style="color: #1E293B; line-height: 1.75; white-space: pre-wrap;">{st.session_state.decision_points}</div>
|
|||
|
|
</div>
|
|||
|
|
""",
|
|||
|
|
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(
|
|||
|
|
"""
|
|||
|
|
<div style="padding: 16px; background: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 8px;">
|
|||
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|||
|
|
<span style="font-size: 20px;">💡</span>
|
|||
|
|
<strong style="color: #1E293B;">使用提示</strong>
|
|||
|
|
</div>
|
|||
|
|
<div style="color: #64748B; line-height: 1.6; font-size: 0.95rem;">
|
|||
|
|
• 您可以随时调整方案内容与参与角色,然后重新开始评审<br>
|
|||
|
|
• 建议使用示例方案进行第一次测试,以了解系统功能<br>
|
|||
|
|
• 评审结果仅供参考,最终决策请结合实际情况和专业判断
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
""",
|
|||
|
|
unsafe_allow_html=True,
|
|||
|
|
)
|