wd666/app.py

1321 lines
53 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.

"""
Multi-Agent Decision Workshop - 主应用
多 Agent 决策工作坊:通过多角色辩论帮助用户做出更好的决策
"""
import streamlit as st
import os
import base64
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
from agents import get_all_agents, get_recommended_agents, AGENT_PROFILES
from orchestrator import DebateManager, DebateConfig
from orchestrator.research_manager import ResearchManager, ResearchConfig
from report import ReportGenerator
from report import ReportGenerator
from utils import LLMClient
from utils.storage import StorageManager
from utils.auto_agent_generator import generate_experts_for_topic
import config
# ==================== 页面配置 ====================
st.set_page_config(
page_title="🎭 多 Agent 决策工作坊",
page_icon="🎭",
layout="wide",
initial_sidebar_state="expanded"
)
# ==================== 样式 ====================
st.markdown("""
<style>
/* 蓝紫色渐变主题 - 模仿参考UI */
.stApp {
background: linear-gradient(180deg, #E8EEFF 0%, #F5F7FF 100%);
}
/* 标题渐变 - 蓝紫色 */
.stApp h1 {
background: linear-gradient(135deg, #4A5CDB 0%, #667eea 50%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
.stApp h2, .stApp h3 {
background: linear-gradient(90deg, #4A5CDB 0%, #667eea 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 600;
}
/* 正文保持深色可读性 */
.stApp .stMarkdown p, .stApp .stMarkdown li {
color: #333;
}
/* 主卡片样式 */
.main-card {
background: white;
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 4px 20px rgba(74, 92, 219, 0.1);
margin: 1rem 0;
border: 1px solid rgba(74, 92, 219, 0.1);
}
/* 场景卡片 */
.scenario-card {
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
margin: 0.5rem 0;
border-left: 4px solid #4A5CDB;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.scenario-card h4 {
color: #4A5CDB;
margin-bottom: 0.5rem;
font-weight: 600;
}
.scenario-card p {
color: #666;
font-size: 0.9rem;
}
/* 典型问题列表 */
.typical-questions {
background: #F8F9FF;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 0.5rem;
}
.typical-questions strong {
color: #4A5CDB;
}
/* 状态指示器 */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #E8FFE8;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #4CAF50;
}
.status-dot {
width: 10px;
height: 10px;
background: #4CAF50;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 原有样式保留 */
.agent-card {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
border-left: 4px solid #4A5CDB;
background-color: #F8F9FF;
}
.speech-bubble {
background-color: #F8F9FF;
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
.round-header {
background: linear-gradient(90deg, #4A5CDB 0%, #667eea 50%, #764ba2 100%);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
margin: 1rem 0;
}
.custom-agent-form {
background-color: #F8F9FF;
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
.research-step {
border-left: 3px solid #4A5CDB;
padding-left: 10px;
margin-bottom: 10px;
}
/* 按钮样式增强 */
.stButton > button {
border-radius: 0.5rem;
font-weight: 500;
}
/* 分隔线 */
hr {
border: none;
height: 1px;
background: linear-gradient(90deg, transparent, #4A5CDB, transparent);
margin: 1.5rem 0;
}
</style>
""", unsafe_allow_html=True)
# ==================== 常量定义 ====================
# 从环境变量读取 API Key隐藏在 .env 文件中)
DEFAULT_API_KEY = os.getenv("AIHUBMIX_API_KEY", "")
# 支持的模型列表
from config import AVAILABLE_MODELS, RESEARCH_MODEL_ROLES
# 决策类型
DECISION_TYPES = {
"product": "产品方案",
"business": "商业决策",
"tech": "技术选型",
"personal": "个人规划"
}
# ==================== 初始化 Session State ====================
if "storage" not in st.session_state:
st.session_state.storage = StorageManager()
# Load saved config
if "saved_config" not in st.session_state:
st.session_state.saved_config = st.session_state.storage.load_config()
# Helper to save config
def save_current_config():
cfg = {
"provider": st.session_state.get("selected_provider", "AIHubMix"),
# read from widget keys to persist what user sees
"api_key": st.session_state.get("api_key_input", st.session_state.get("api_key", "")),
"base_url": st.session_state.get("base_url_input", st.session_state.get("base_url", "")),
"language": st.session_state.get("output_language", "Chinese")
}
st.session_state.storage.save_config(cfg)
if "mode" not in st.session_state:
st.session_state.mode = "Deep Research"
# Debate State
if "debate_started" not in st.session_state:
st.session_state.debate_started = False
if "debate_finished" not in st.session_state:
st.session_state.debate_finished = False
if "speeches" not in st.session_state:
st.session_state.speeches = []
if "report" not in st.session_state:
st.session_state.report = ""
if "custom_agents" not in st.session_state:
st.session_state.custom_agents = {}
# Research State
if "research_plan" not in st.session_state:
st.session_state.research_plan = ""
if "research_started" not in st.session_state:
st.session_state.research_started = False
if "research_output" not in st.session_state:
st.session_state.research_output = "" # Final report
if "research_steps_output" not in st.session_state:
st.session_state.research_steps_output = [] # List of step results
if "generated_experts" not in st.session_state:
st.session_state.generated_experts = None # Auto-generated expert configs
# ==================== 侧边栏:配置 ====================
with st.sidebar:
st.header("⚙️ 设置")
# 全局 API Key & Provider 设置
with st.expander("🔑 API / Provider 设置", expanded=True):
# Saved preferences
saved = st.session_state.saved_config
# Provider Selection
provider_options = list(config.LLM_PROVIDERS.keys())
default_provider = saved.get("provider", "AIHubMix")
try:
prov_idx = provider_options.index(default_provider)
except ValueError:
prov_idx = 0
def on_provider_change():
# Update API key and base_url inputs when provider changes
sel = st.session_state.get("selected_provider")
if not sel:
return
prov_cfg = config.LLM_PROVIDERS.get(sel, {})
saved_cfg = st.session_state.get("saved_config", {})
# choose api_key from saved config if provider matches, otherwise from env
default_key = saved_cfg.get("api_key") if saved_cfg.get("provider") == sel else os.getenv(prov_cfg.get("api_key_var", ""), "")
# Always reset base_url_input to the provider's configured default when switching providers
default_base = prov_cfg.get("base_url", "")
# Set widget states
st.session_state["api_key_input"] = default_key
st.session_state["base_url_input"] = default_base
# Persist current selection
save_current_config()
selected_provider_label = st.selectbox(
"选择 API 提供商",
options=provider_options,
index=prov_idx,
key="selected_provider",
on_change=on_provider_change
)
# Recompute provider config from current selection (use session_state to be robust)
current_provider = st.session_state.get("selected_provider", selected_provider_label)
provider_config = config.LLM_PROVIDERS.get(current_provider, {})
provider_id = current_provider.lower()
# API Key Input
# If widget already has a value in session_state (from previous interactions), prefer it.
default_key = (
st.session_state.get("api_key_input")
if st.session_state.get("api_key_input") is not None and st.session_state.get("api_key_input") != ""
else (saved.get("api_key") if saved.get("provider") == current_provider else os.getenv(provider_config.get("api_key_var", ""), ""))
)
api_key = st.text_input(
f"{current_provider} API Key",
type="password",
value=default_key,
help=f"环境变量: {provider_config.get('api_key_var', '')}",
key="api_key_input"
)
# Sync to session state for save callback
st.session_state.api_key = api_key
# Base URL
# Special-case: ensure DeepSeek shows its correct official base URL
if current_provider == "DeepSeek":
default_url = provider_config.get("base_url", "")
else:
default_url = (
st.session_state.get("base_url_input")
if st.session_state.get("base_url_input") is not None and st.session_state.get("base_url_input") != ""
else (saved.get("base_url") if saved.get("provider") == current_provider else provider_config.get("base_url", ""))
)
base_url = st.text_input(
"API Base URL",
value=default_url,
key="base_url_input"
)
st.session_state.base_url = base_url
# Trigger save if values changed (manual check since text_input on_change is tricky with typing)
if api_key != saved.get("api_key") or base_url != saved.get("base_url"):
save_current_config()
if not api_key:
st.warning("⚠️ 请配置 API Key 以启用 AI 功能 (仍可查看历史档案)")
# Output Language Selection
lang_options = config.SUPPORTED_LANGUAGES
default_lang = saved.get("language", "Chinese")
try:
lang_idx = lang_options.index(default_lang)
except ValueError:
lang_idx = 0
output_language = st.sidebar.selectbox(
"🌐 输出语言",
options=lang_options,
index=lang_idx,
help="所有 AI Agent 将使用此语言进行回复",
key="output_language",
on_change=save_current_config
)
# 页面背景图片(全局)
bg_file = st.file_uploader("页面背景图片(可选)", type=['png', 'jpg', 'jpeg', 'gif'], key='page_bg_uploader')
if bg_file:
# 保存到 assets 并在 session_state 中保存 data url 用于注入样式
saved_path = st.session_state.storage.save_asset(bg_file)
st.session_state.bg_image_path = saved_path
try:
buf = bg_file.getbuffer()
except Exception:
buf = bg_file.read()
# detect mime
ext = (bg_file.name.split('.')[-1].lower() if hasattr(bg_file, 'name') and bg_file.name else 'png')
mime = 'image/png' if ext == 'png' else ('image/gif' if ext == 'gif' else 'image/jpeg')
data_url = f"data:{mime};base64,{base64.b64encode(buf).decode()}"
st.session_state.bg_image_data_url = data_url
st.success("页面背景已上传并保存")
st.divider()
# 模式选择
mode = st.radio(
"📊 选择模式",
["Council V4 (Deep Research)", "Debate Workshop", "📜 History Archives", "💬 用户反馈"],
index=0 if st.session_state.mode == "Deep Research" else (1 if st.session_state.mode == "Debate Workshop" else (2 if st.session_state.mode == "History Archives" else 3))
)
# Map selection back to internal mode string
if mode == "Council V4 (Deep Research)":
st.session_state.mode = "Deep Research"
elif mode == "Debate Workshop":
st.session_state.mode = "Debate Workshop"
elif mode == "📜 History Archives":
st.session_state.mode = "History Archives"
else:
st.session_state.mode = "Feedback"
st.divider()
if st.session_state.mode == "Debate Workshop": # Debate Workshop Settings
# 模型选择
model = st.selectbox(
"🤖 选择通用模型",
options=list(AVAILABLE_MODELS.keys()),
format_func=lambda x: AVAILABLE_MODELS[x],
index=0,
help="选择用于辩论的 AI 模型"
)
# 辩论配置
max_rounds = st.slider(
"🔄 辩论轮数",
min_value=1,
max_value=4,
value=2,
help="每轮所有 Agent 都会发言一次"
)
st.divider()
# ==================== 自定义角色 (Debate Only) ====================
st.subheader("✨ 自定义角色")
with st.expander(" 添加新角色", expanded=False):
new_agent_name = st.text_input("角色名称", placeholder="如:法务顾问", key="new_agent_name")
new_agent_emoji = st.text_input("角色 Emoji", value="🎯", max_chars=2, key="new_agent_emoji")
new_agent_perspective = st.text_input("视角定位", placeholder="如:法律合规视角", key="new_agent_perspective")
new_agent_focus = st.text_input("关注点(逗号分隔)", placeholder="如:合规风险, 法律条款", key="new_agent_focus")
new_agent_prompt = st.text_area("角色设定 Prompt", placeholder="描述这个角色的思考方式...", height=100, key="new_agent_prompt")
if st.button("✅ 添加角色", use_container_width=True):
if new_agent_name and new_agent_prompt:
agent_id = f"custom_{len(st.session_state.custom_agents)}"
st.session_state.custom_agents[agent_id] = {
"name": new_agent_name,
"emoji": new_agent_emoji,
"perspective": new_agent_perspective or "自定义视角",
"focus_areas": [f.strip() for f in new_agent_focus.split(",") if f.strip()],
"system_prompt": new_agent_prompt
}
st.success(f"已添加角色: {new_agent_emoji} {new_agent_name}")
st.rerun()
else:
st.warning("请至少填写角色名称和 Prompt")
# 显示已添加的自定义角色
if st.session_state.custom_agents:
st.markdown("**已添加的自定义角色:**")
for agent_id, agent_info in list(st.session_state.custom_agents.items()):
col1, col2 = st.columns([3, 1])
with col1:
st.markdown(f"{agent_info['emoji']} {agent_info['name']}")
with col2:
if st.button("🗑️", key=f"del_{agent_id}"):
del st.session_state.custom_agents[agent_id]
st.rerun()
# 注入全局页面背景样式(如果已上传)
if st.session_state.get("bg_image_data_url"):
st.markdown(
f"""<style>
.stApp {{
background-image: url('{st.session_state.get('bg_image_data_url')}');
background-size: cover;
background-position: center;
background-attachment: fixed;
}}
</style>""",
unsafe_allow_html=True
)
# ==================== 主界面逻辑 ====================
if st.session_state.mode == "Deep Research":
# ==================== 主标题区域 ====================
st.markdown("""
<div style="text-align: center; padding: 1rem 0;">
<h1 style="font-size: 2.5rem;">🍎 智能决策工作坊</h1>
<p style="color: #666; font-size: 1.1rem;">AI驱动的多智能体决策分析系统 - 基于多模型智囊团</p>
</div>
""", unsafe_allow_html=True)
# 状态指示器和语言选择
col_status, col_lang = st.columns([2, 1])
with col_status:
if api_key:
st.markdown("""
<div class="status-indicator">
<div class="status-dot"></div>
<span style="color: #4CAF50;">✓ 已连接到服务器</span>
</div>
""", unsafe_allow_html=True)
else:
st.warning("⚠️ 请在侧边栏配置 API Key")
with col_lang:
st.markdown(f"**语言/Language:** {output_language}")
st.divider()
# ==================== 开始决策按钮 ====================
st.markdown("""
<div class="main-card" style="text-align: center;">
<h3>🚀 开始决策</h3>
<p style="color: #666;">选择场景或自定义主题,开始多专家协作分析</p>
</div>
""", unsafe_allow_html=True)
st.divider()
# ==================== 支持的决策场景 ====================
st.markdown("""
<div class="main-card">
<h2>📋 支持的决策场景</h2>
<p style="color: #666; margin-bottom: 1.5rem;">系统支持以下决策场景每个场景都配置了专业的AI专家团队</p>
</div>
""", unsafe_allow_html=True)
# Decision scenario templates with typical questions
DECISION_SCENARIOS = {
"🚀 新产品发布评审": {
"topic": "新产品发布评审:评估产品功能完备性、市场准备度、发布时机和潜在风险",
"description": "评估新产品概念的可行性、市场潜力和实施计划",
"example": "我们计划在下个季度发布AI助手功能需要评估技术准备度、市场时机和竞争态势",
"questions": [
"这个产品的核心价值主张是什么?",
"目标用户群体是谁?需求是否真实存在?",
"技术实现难度如何?团队是否具备能力?",
"竞争对手有类似产品吗?我们的差异化在哪?"
]
},
"💰 投资审批决策": {
"topic": "投资审批决策:评估投资项目的财务回报、战略价值、风险因素和执行可行性",
"description": "分析投资项目的ROI、风险和战略价值",
"example": "公司考虑投资1000万用于数据中台建设需要评估ROI、技术风险和业务价值",
"questions": [
"预期投资回报率(ROI)是多少?",
"投资回收期需要多长时间?",
"主要风险因素有哪些?如何缓解?",
"是否有更优的替代方案?"
]
},
"🤝 合作伙伴评估": {
"topic": "合作伙伴评估:分析潜在合作方的能力、信誉、战略协同和合作风险",
"description": "评估潜在合作伙伴的匹配度和合作价值",
"example": "评估与XX公司建立战略合作的可行性包括技术互补性、市场协同和风险",
"questions": [
"合作方的核心能力是什么?",
"双方资源如何互补?",
"合作的战略协同效应有多大?",
"合作失败的风险和退出机制是什么?"
]
},
"📦 供应商评估": {
"topic": "供应商评估:评估供应商的质量、成本、交付能力、稳定性和合作风险",
"description": "对比分析供应商的综合能力",
"example": "评估更换核心零部件供应商的利弊,包括成本对比、质量风险和切换成本",
"questions": [
"供应商的质量控制体系如何?",
"价格竞争力与行业均值对比?",
"交付能力和响应速度如何?",
"供应商的财务稳定性如何?"
]
}
}
# Display scenario cards with typical questions
for scenario_name, scenario_data in DECISION_SCENARIOS.items():
st.markdown(f"""
<div class="scenario-card">
<h4>{scenario_name}</h4>
<p>{scenario_data['description']}</p>
<div class="typical-questions">
<strong>典型问题:</strong>
<ul style="margin: 0.5rem 0; padding-left: 1.5rem; color: #555;">
{''.join([f'<li>{q}</li>' for q in scenario_data['questions']])}
</ul>
</div>
</div>
""", unsafe_allow_html=True)
if st.button(f"使用此场景", key=f"use_{scenario_name}", use_container_width=True):
st.session_state.selected_scenario = scenario_data
st.session_state.prefill_topic = scenario_data['topic']
st.rerun()
st.divider()
# Get prefilled topic if available
prefill_topic = st.session_state.get("prefill_topic", "")
if st.session_state.get("selected_scenario"):
prefill_topic = prefill_topic or st.session_state.selected_scenario.get("topic", "")
col1, col2 = st.columns([3, 1])
with col1:
research_topic = st.text_area("研究/决策主题", value=prefill_topic, placeholder="请输入你想深入研究或决策的主题...", height=100)
with col2:
max_rounds = st.number_input("讨论轮数", min_value=1, max_value=5, value=2, help="专家们进行对话的轮数")
# Expert Configuration
st.subheader("👥 专家配置")
# Auto-generate experts row
col_num, col_auto = st.columns([2, 3])
with col_num:
num_experts = st.number_input("专家数量", min_value=2, max_value=5, value=3)
with col_auto:
st.write("") # Spacing
auto_gen_btn = st.button(
"🪄 根据主题自动生成专家",
disabled=(not research_topic or not api_key),
help="AI 将根据您的主题自动推荐合适的专家角色"
)
# Handle auto-generation
if auto_gen_btn and research_topic and api_key:
with st.spinner("🤖 AI 正在分析主题并生成专家配置..."):
try:
temp_client = LLMClient(
provider=provider_id,
api_key=api_key,
base_url=base_url,
model="gpt-4o-mini" # Use fast model for generation
)
generated = generate_experts_for_topic(
topic=research_topic,
num_experts=num_experts,
llm_client=temp_client,
language=output_language
)
st.session_state.generated_experts = generated
st.success(f"✅ 已生成 {len(generated)} 位专家配置!")
st.rerun()
except Exception as e:
st.error(f"生成失败: {e}")
experts_config = []
cols = st.columns(num_experts)
for i in range(num_experts):
with cols[i]:
default_model_key = list(AVAILABLE_MODELS.keys())[i % len(AVAILABLE_MODELS)]
# Use generated expert name if available
if st.session_state.generated_experts and i < len(st.session_state.generated_experts):
gen_expert = st.session_state.generated_experts[i]
default_name = gen_expert.get("name", f"Expert {i+1}")
perspective = gen_expert.get("perspective", "")
st.markdown(f"**{default_name}**")
if perspective:
st.caption(f"_{perspective}_")
else:
default_name = f"Expert {i+1}"
if i == num_experts - 1:
default_name = f"Expert {i+1} (Synthesizer)"
st.markdown(f"**Expert {i+1}**")
expert_name = st.text_input(f"名称 #{i+1}", value=default_name, key=f"expert_name_{i}")
expert_model = st.selectbox(f"模型 #{i+1}", options=list(AVAILABLE_MODELS.keys()), index=list(AVAILABLE_MODELS.keys()).index(default_model_key), key=f"expert_model_{i}")
experts_config.append({
"name": expert_name,
"model": expert_model
})
research_context = st.text_area("补充背景 (可选)", placeholder="任何额外的背景信息...", height=80)
start_research_btn = st.button("🚀 开始多模型协作", type="primary", disabled=(not research_topic or not api_key))
if not api_key:
st.info("💡 请先在侧边栏配置 API Key 才能开始任务")
# ==================== 恢复会话逻辑 (Resume Logic) ====================
# Try to load cached session
cached_session = st.session_state.storage.load_session_state("council_cache")
# If we have a cached session, and we are NOT currently running one (research_started is False)
if cached_session and not st.session_state.research_started:
st.info(f"🔍 检测到上次未完成的会话: {cached_session.get('topic', 'Unknown Topic')}")
col_res1, col_res2 = st.columns([1, 4])
with col_res1:
if st.button("🔄 恢复会话", type="primary"):
# Restore state
st.session_state.research_started = True
st.session_state.research_output = "" # Usually empty if unfinished
st.session_state.research_steps_output = cached_session.get("steps_output", [])
# Restore inputs if possible (tricky with widgets, but we can set defaults or just rely on cache for display)
# For simplicity, we restore the viewing state. Continuing generation is harder without rebuilding the exact generator state.
# Currently, "Resume" means "Restore View". To continue adding to it would require skipping done steps in manager.
st.rerun()
with col_res2:
if st.button("🗑️ 放弃", type="secondary"):
st.session_state.storage.clear_session_state("council_cache")
st.rerun()
# ==================== 历史渲染区域 (Always visible if started) ====================
if st.session_state.research_started and st.session_state.research_steps_output and not start_research_btn:
st.subheader("🗣️ 智囊团讨论历史")
for step in st.session_state.research_steps_output:
step_name = step.get('step', 'Unknown')
content = step.get('output', '')
role_type = "assistant"
with st.chat_message(role_type, avatar="🤖"):
st.markdown(f"**{step_name}**")
st.markdown(content)
st.divider()
# ==================== 执行区域 (Triggered by Button) ====================
if start_research_btn and research_topic:
st.session_state.research_started = True
st.session_state.research_output = ""
st.session_state.research_steps_output = []
# Clear any old cache when starting fresh
st.session_state.storage.clear_session_state("council_cache")
# 使用全局页面背景(若已上传)
research_bg_path = st.session_state.get("bg_image_path")
if st.session_state.get("bg_image_data_url"):
try:
st.markdown("**页面背景预览**")
st.image(st.session_state.get("bg_image_data_url"), use_column_width=True)
except Exception:
pass
manager = ResearchManager(
api_key=api_key,
base_url=base_url,
provider=provider_id
)
config_obj = ResearchConfig(
topic=research_topic,
context=research_context,
experts=experts_config,
language=output_language
)
manager.create_agents(config_obj)
st.subheader("🗣️ 智囊团讨论中...")
chat_container = st.container()
try:
for event in manager.collaborate(research_topic, research_context, max_rounds=max_rounds):
if event["type"] == "step_start":
current_step_name = event["step"]
current_agent = event["agent"]
current_model = event["model"]
# Create a chat message block
with chat_container:
with st.chat_message("assistant", avatar="🤖"):
st.markdown(f"**{current_step_name}**")
st.caption(f"({current_model})")
message_placeholder = st.empty()
current_content = ""
elif event["type"] == "content":
current_content += event["content"]
message_placeholder.markdown(current_content)
elif event["type"] == "step_end":
# Save step result for history
st.session_state.research_steps_output.append({
"step": current_step_name,
"output": event["output"]
})
# === AUTO-SAVE CACHE ===
# Save current progress to session cache
cache_data = {
"topic": research_topic,
"context": research_context,
"steps_output": st.session_state.research_steps_output,
"experts_config": experts_config,
"max_rounds": max_rounds
}
st.session_state.storage.save_session_state("council_cache", cache_data)
# =======================
# The last step output is the final plan
if st.session_state.research_steps_output:
final_plan = st.session_state.research_steps_output[-1]["output"]
st.session_state.research_output = final_plan
st.success("✅ 综合方案生成完毕")
# Auto-save history (附带背景图片路径如果存在)
metadata = {
"rounds": max_rounds,
"experts": [e["name"] for e in experts_config],
"language": output_language
}
if research_bg_path:
metadata["background_image"] = research_bg_path
st.session_state.storage.save_history(
session_type="council",
topic=research_topic,
content=final_plan,
metadata=metadata
)
# Clear session cache as we finished successfully
st.session_state.storage.clear_session_state("council_cache")
st.toast("✅ 记录已保存到历史档案")
except Exception as e:
st.error(f"发生错误: {str(e)}")
import traceback
st.code(traceback.format_exc())
# Show Final Report if available
if st.session_state.research_output:
st.divider()
st.subheader("📄 最终综合方案")
st.markdown(st.session_state.research_output)
st.download_button("📥 下载方案", st.session_state.research_output, "comprehensive_plan.md")
# Show breakdown history
with st.expander("查看完整思考过程"):
for step in st.session_state.research_steps_output:
st.markdown(f"### {step['step']}")
st.markdown(step['output'])
st.divider()
# 追问模式Deep Research
st.divider()
st.subheader("🔎 追问模式 — 深入提问")
followup_q = st.text_area("输入你的追问(基于上面的综合方案)", key="research_followup_input", height=80)
if 'research_followups' not in st.session_state:
st.session_state.research_followups = []
if st.button("💬 追问", key="research_followup_btn") and followup_q:
# 创建客户端,优先使用最后一个专家的模型作为回复模型
follow_model = None
try:
follow_model = experts_config[-1]['model'] if experts_config else None
except Exception:
follow_model = None
llm = LLMClient(provider=provider_id, api_key=st.session_state.get('api_key'), base_url=st.session_state.get('base_url'), model=follow_model)
sys_prompt = "你是一个基于先前生成的综合方案的助理,针对用户的追问进行简明、深入且行动导向的回答。"
user_prompt = f"已生成的综合方案:\n{st.session_state.research_output}\n\n用户追问:\n{followup_q}"
placeholder = st.empty()
reply = ""
try:
for chunk in llm.chat_stream(system_prompt=sys_prompt, user_prompt=user_prompt, max_tokens=1024):
reply += chunk
placeholder.markdown(reply)
except Exception as e:
placeholder.markdown(f"错误: {e}")
# 保存本次追问到 session仅会话级
st.session_state.research_followups.append({"q": followup_q, "a": reply})
st.success("追问已得到回复")
# 显示历史追问
if st.session_state.research_followups:
with st.expander("查看追问历史"):
for idx, qa in enumerate(st.session_state.research_followups[::-1]):
st.markdown(f"**Q{len(st.session_state.research_followups)-idx}:** {qa['q']}")
st.markdown(f"**A:** {qa['a']}")
st.divider()
elif st.session_state.mode == "Debate Workshop":
# ==================== 原始 Debate UI 逻辑 ====================
st.title("🎭 多 Agent 决策工作坊")
st.markdown("*让多个 AI 角色从不同视角辩论,帮助你做出更全面的决策*")
# ==================== 输入区域 ====================
col1, col2 = st.columns([2, 1])
with col1:
st.subheader("📝 决策议题")
# 决策类型选择
decision_type = st.selectbox(
"决策类型",
options=list(DECISION_TYPES.keys()),
format_func=lambda x: DECISION_TYPES[x],
index=0
)
# 议题输入
topic = st.text_area(
"请描述你的决策议题",
placeholder="例如:我们是否应该在 Q2 推出 AI 助手功能?\n\n或者:我应该接受这份新工作 offer 吗?",
height=120
)
# 背景信息(可选)
with st.expander(" 添加背景信息(可选)"):
context = st.text_area(
"背景信息",
placeholder="提供更多上下文信息,如:\n- 当前状况\n- 已有的资源和限制\n- 相关数据和事实",
height=100
)
# 页面背景请使用侧边栏的“页面背景图片”上传控件进行设置(全局)
context = context if 'context' in dir() else ""
with col2:
st.subheader("🎭 选择参与角色")
# 获取推荐的角色
recommended = get_recommended_agents(decision_type)
all_agents = get_all_agents()
# 预设角色选择
st.markdown("**预设角色:**")
selected_agents = []
for agent in all_agents:
is_recommended = agent["id"] in recommended
default_checked = is_recommended
if st.checkbox(
f"{agent['emoji']} {agent['name']}",
value=default_checked,
key=f"agent_{agent['id']}"
):
selected_agents.append(agent["id"])
# 自定义角色选择
if st.session_state.custom_agents:
st.markdown("**自定义角色:**")
for agent_id, agent_info in st.session_state.custom_agents.items():
if st.checkbox(
f"{agent_info['emoji']} {agent_info['name']}",
value=True,
key=f"agent_{agent_id}"
):
selected_agents.append(agent_id)
# 自定义模型配置 (Advanced)
agent_model_map = {}
with st.expander("🛠️ 为每个角色指定模型 (可选)"):
for agent_id in selected_agents:
# Find agent name
agent_name = next((a['name'] for a in all_agents if a['id'] == agent_id), agent_id)
if agent_id in st.session_state.custom_agents:
agent_name = st.session_state.custom_agents[agent_id]['name']
agent_model = st.selectbox(
f"{agent_name} 的模型",
options=list(AVAILABLE_MODELS.keys()),
index=list(AVAILABLE_MODELS.keys()).index(model) if model in AVAILABLE_MODELS else 0,
key=f"model_for_{agent_id}"
)
agent_model_map[agent_id] = agent_model
# 角色数量提示
if len(selected_agents) < 2:
st.warning("请至少选择 2 个角色")
elif len(selected_agents) > 6:
st.warning("建议不超过 6 个角色")
else:
st.info(f"已选择 {len(selected_agents)} 个角色")
# ==================== 辩论控制 ====================
st.divider()
col_btn1, col_btn2, col_btn3 = st.columns([1, 1, 2])
with col_btn1:
start_btn = st.button(
"🚀 开始辩论",
disabled=(not topic or len(selected_agents) < 2 or not api_key),
type="primary",
use_container_width=True
)
if not api_key:
st.caption("🔒 需配置 API Key")
with col_btn2:
reset_btn = st.button(
"🔄 重置",
use_container_width=True
)
if reset_btn:
st.session_state.debate_started = False
st.session_state.debate_finished = False
st.session_state.speeches = []
st.session_state.report = ""
st.rerun()
# ==================== 辩论展示区 ====================
if start_btn and topic and len(selected_agents) >= 2:
st.session_state.debate_started = True
st.session_state.speeches = []
st.divider()
st.subheader("🎬 辩论进行中...")
# 临时将自定义角色添加到 agent_profiles
from agents import agent_profiles
original_profiles = dict(agent_profiles.AGENT_PROFILES)
agent_profiles.AGENT_PROFILES.update(st.session_state.custom_agents)
try:
# 初始化默认客户端
llm_client = LLMClient(
provider=provider_id,
api_key=api_key,
base_url=base_url,
model=model
)
# 初始化特定角色的客户端
agent_clients = {}
for ag_id, ag_model in agent_model_map.items():
if ag_model != model: # Only create new client if different from default
agent_clients[ag_id] = LLMClient(
provider=provider_id,
api_key=api_key,
base_url=base_url,
model=ag_model
)
# 使用全局页面背景(若已上传)
debate_bg_path = st.session_state.get("bg_image_path")
if st.session_state.get("bg_image_data_url"):
try:
st.markdown("**页面背景预览**")
st.image(st.session_state.get("bg_image_data_url"), use_column_width=True)
except Exception:
pass
debate_manager = DebateManager(llm_client)
# 配置辩论
debate_config = DebateConfig(
topic=topic,
context=context,
agent_ids=selected_agents,
max_rounds=max_rounds,
agent_clients=agent_clients,
language=output_language
)
debate_manager.setup_debate(debate_config)
# 运行辩论(流式)
current_round = 0
speech_placeholder = None
for event in debate_manager.run_debate_stream():
if event["type"] == "round_start":
current_round = event["round"]
st.markdown(
f'<div class="round-header">📢 第 {current_round} 轮讨论</div>',
unsafe_allow_html=True
)
elif event["type"] == "speech_start":
# 显示模型名称
model_display = f" <span style='font-size:0.8em; color:gray'>({event.get('model_name', 'Unknown')})</span>"
st.markdown(f"**{event['emoji']} {event['agent_name']}**{model_display}", unsafe_allow_html=True)
speech_placeholder = st.empty()
current_content = ""
elif event["type"] == "speech_chunk":
current_content += event["chunk"]
speech_placeholder.markdown(current_content)
elif event["type"] == "speech_end":
st.session_state.speeches.append({
"agent_id": event["agent_id"],
"content": event["content"],
"round": current_round
})
st.divider()
# 生成报告
if st.session_state.debate_finished:
report_generator = ReportGenerator(llm_client)
speeches = debate_manager.get_all_speeches()
st.subheader("📊 决策报告")
report_placeholder = st.empty()
report_content = ""
for chunk in report_generator.generate_report_stream(
topic=topic,
speeches=speeches,
context=context
):
report_content += chunk
report_placeholder.markdown(report_content)
st.session_state.report = report_content
# Auto-save history
st.session_state.storage.save_history(
session_type="debate",
topic=topic,
content=report_content,
metadata={
"rounds": max_rounds,
"agents": selected_agents,
"language": output_language,
**({"background_image": st.session_state.get("bg_image_path")} if st.session_state.get("bg_image_path") else {})
}
)
st.toast("✅ 记录已保存到历史档案")
# 下载按钮
st.download_button(
label="📥 下载报告 (Markdown)",
data=report_content,
file_name="decision_report.md",
mime="text/markdown"
)
# 追问模式Debate Workshop
st.divider()
st.subheader("🔎 追问模式 — 基于报告的深入提问")
debate_followup_q = st.text_area("输入你的追问(基于上面的决策报告)", key="debate_followup_input", height=80)
if 'debate_followups' not in st.session_state:
st.session_state.debate_followups = []
if st.button("💬 追问", key="debate_followup_btn") and debate_followup_q:
# 使用生成报告时的 llm_client
llm_follow = llm_client
sys_prompt = "你是一个基于上面决策报告的助理,针对用户的追问进行简明且行动导向的回答。"
user_prompt = f"决策报告:\n{st.session_state.report}\n\n用户追问:\n{debate_followup_q}"
ph = st.empty()
reply = ""
try:
for chunk in llm_follow.chat_stream(system_prompt=sys_prompt, user_prompt=user_prompt, max_tokens=1024):
reply += chunk
ph.markdown(reply)
except Exception as e:
ph.markdown(f"错误: {e}")
st.session_state.debate_followups.append({"q": debate_followup_q, "a": reply})
st.success("追问已得到回复")
if st.session_state.debate_followups:
with st.expander("查看追问历史"):
for idx, qa in enumerate(st.session_state.debate_followups[::-1]):
st.markdown(f"**Q{len(st.session_state.debate_followups)-idx}:** {qa['q']}")
st.markdown(f"**A:** {qa['a']}")
st.divider()
except Exception as e:
st.error(f"发生错误: {str(e)}")
import traceback
st.code(traceback.format_exc())
st.info("请检查你的 API Key 和模型设置是否正确")
finally:
# 恢复原始角色配置
agent_profiles.AGENT_PROFILES = original_profiles
# ==================== 历史报告展示 ====================
elif st.session_state.report and not start_btn:
st.divider()
st.subheader("📊 上次的决策报告")
st.markdown(st.session_state.report)
st.download_button(
label="📥 下载报告 (Markdown)",
data=st.session_state.report,
file_name="decision_report.md",
mime="text/markdown"
)
# ==================== 历史档案浏览 ====================
elif st.session_state.mode == "History Archives":
st.title("📜 历史档案")
st.markdown("*查看过去的所有决策和研究记录*")
history_items = st.session_state.storage.list_history()
if not history_items:
st.info("暂无历史记录。开始一个新的 Council 或 Debate 来生成记录吧!")
else:
# Display as a table/list
for item in history_items:
with st.expander(f"{item['date']} | {item['type'].upper()} | {item['topic']}", expanded=False):
col1, col2 = st.columns([4, 1])
with col1:
st.caption(f"ID: {item['id']}")
with col2:
if st.button("查看详情", key=f"view_{item['id']}"):
st.session_state.view_history_id = item['filename']
st.rerun()
# View Detail Modal/Area
if "view_history_id" in st.session_state:
st.divider()
record = st.session_state.storage.load_history_item(st.session_state.view_history_id)
if record:
st.subheader(f"📄 记录详情: {record['topic']}")
st.markdown(f"**时间**: {record['date']} | **类型**: {record['type']}")
st.markdown("---")
st.markdown(record['content'])
# 如果历史记录里有背景图片,显示预览
try:
bg_path = record.get('metadata', {}).get('background_image')
if bg_path:
st.image(bg_path, caption="关联背景图片", use_column_width=True)
except Exception:
pass
st.download_button(
"📥 下载此记录",
record['content'],
file_name=f"{record['type']}_{record['id']}.md"
)
# ==================== 用户反馈页面 ====================
elif st.session_state.mode == "Feedback":
st.title("💬 用户反馈")
st.markdown("*您的反馈帮助我们不断改进产品*")
# Feedback form
st.subheader("📝 提交反馈")
feedback_type = st.selectbox(
"反馈类型",
["功能建议", "Bug 报告", "使用体验", "其他"],
help="选择您要反馈的类型"
)
# Rating
st.markdown("**整体满意度**")
rating = st.slider("", 1, 5, 4, format="%d")
rating_labels = {1: "😞 非常不满意", 2: "😕 不满意", 3: "😐 一般", 4: "😊 满意", 5: "🤩 非常满意"}
st.caption(rating_labels.get(rating, ""))
# Feedback content
feedback_content = st.text_area(
"详细描述",
placeholder="请描述您的反馈内容...\n\n例如:\n- 您遇到了什么问题?\n- 您希望增加什么功能?\n- 您对哪些方面有改进建议?",
height=200
)
# Feature requests for Council V4
st.subheader("🎯 功能需求调研")
st.markdown("您最希望看到哪些新功能?(可多选)")
feature_options = {
"more_scenarios": "📋 更多决策场景模板",
"export_pdf": "📄 导出 PDF 报告",
"voice_input": "🎤 语音输入支持",
"realtime_collab": "👥 多人实时协作",
"custom_prompts": "✏️ 自定义专家 Prompt",
"api_access": "🔌 API 接口支持",
"mobile_app": "📱 移动端应用"
}
selected_features = []
cols = st.columns(3)
for idx, (key, label) in enumerate(feature_options.items()):
with cols[idx % 3]:
if st.checkbox(label, key=f"feature_{key}"):
selected_features.append(key)
# Contact info (optional)
st.subheader("📧 联系方式(可选)")
contact_email = st.text_input("邮箱", placeholder="your@email.com")
# Submit button
st.divider()
if st.button("📤 提交反馈", type="primary", use_container_width=True):
if feedback_content.strip():
# Save feedback
feedback_data = {
"type": feedback_type,
"rating": rating,
"content": feedback_content,
"features": selected_features,
"email": contact_email,
"timestamp": st.session_state.storage._get_timestamp() if hasattr(st.session_state.storage, '_get_timestamp') else ""
}
# Save to storage
try:
import json
import os
feedback_dir = os.path.join(st.session_state.storage.base_dir, "feedback")
os.makedirs(feedback_dir, exist_ok=True)
from datetime import datetime
filename = f"feedback_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
filepath = os.path.join(feedback_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(feedback_data, f, ensure_ascii=False, indent=2)
st.success("🎉 感谢您的反馈!我们会认真阅读并持续改进产品。")
st.balloons()
except Exception as e:
st.error(f"保存反馈时出错: {e}")
else:
st.warning("请填写反馈内容")
# Show previous feedback summary
st.divider()
with st.expander("📊 我的反馈历史"):
try:
import os
import json
feedback_dir = os.path.join(st.session_state.storage.base_dir, "feedback")
if os.path.exists(feedback_dir):
files = sorted(os.listdir(feedback_dir), reverse=True)[:5]
if files:
for f in files:
filepath = os.path.join(feedback_dir, f)
with open(filepath, 'r', encoding='utf-8') as file:
data = json.load(file)
st.markdown(f"**{data.get('timestamp', 'Unknown')}** | {data.get('type', '')} | {'' * data.get('rating', 0)}")
st.caption(data.get('content', '')[:100] + "...")
st.divider()
else:
st.info("暂无反馈记录")
else:
st.info("暂无反馈记录")
except Exception:
st.info("暂无反馈记录")
# ==================== 底部信息 ====================
st.divider()
col_footer1, col_footer2, col_footer3 = st.columns(3)
with col_footer2:
st.markdown(
"<div style='text-align: center; color: #888;'>"
"🎭 Multi-Agent Decision Workshop<br>多 Agent 决策工作坊"
"</div>",
unsafe_allow_html=True
)