grouphyx/app.py

351 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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