grouphyx/app.py

351 lines
12 KiB
Python
Raw Permalink Normal View History

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,
)