wd666/app.py

753 lines
29 KiB
Python
Raw Normal View History

2026-01-07 11:02:05 +08:00
"""
Multi-Agent Decision Workshop - 主应用
Agent 决策工作坊通过多角色辩论帮助用户做出更好的决策
"""
import streamlit as st
import os
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
2026-01-07 11:02:05 +08:00
from utils import LLMClient
from utils.storage import StorageManager
2026-01-07 11:02:05 +08:00
import config
# ==================== 页面配置 ====================
st.set_page_config(
page_title="🎭 多 Agent 决策工作坊",
page_icon="🎭",
layout="wide",
initial_sidebar_state="expanded"
)
# ==================== 样式 ====================
st.markdown("""
<style>
.agent-card {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
border-left: 4px solid #4A90A4;
background-color: #f8f9fa;
}
.speech-bubble {
background-color: #f0f2f6;
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
.round-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
margin: 1rem 0;
}
.custom-agent-form {
background-color: #e8f4f8;
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
.research-step {
border-left: 3px solid #FF4B4B;
padding-left: 10px;
margin-bottom: 10px;
}
</style>
""", unsafe_allow_html=True)
# ==================== 常量定义 ====================
# 从环境变量读取 API Key隐藏在 .env 文件中)
DEFAULT_API_KEY = os.getenv("AIHUBMIX_API_KEY", "")
# 支持的模型列表
from config import AVAILABLE_MODELS, RESEARCH_MODEL_ROLES
2026-01-07 11:02:05 +08:00
# 决策类型
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"),
"api_key": st.session_state.get("api_key", ""),
"base_url": st.session_state.get("base_url", ""),
"language": st.session_state.get("output_language", "Chinese")
}
st.session_state.storage.save_config(cfg)
2026-01-07 11:02:05 +08:00
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
# ==================== 侧边栏:配置 ====================
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
selected_provider_label = st.selectbox(
"选择 API 提供商",
options=provider_options,
index=prov_idx,
key="selected_provider",
on_change=save_current_config
)
provider_config = config.LLM_PROVIDERS[selected_provider_label]
provider_id = selected_provider_label.lower()
# API Key Input
# If saved key exists for this provider, use it. Otherwise env var.
default_key = saved.get("api_key") if saved.get("provider") == selected_provider_label else os.getenv(provider_config["api_key_var"], "")
api_key = st.text_input(
f"{selected_provider_label} API Key",
type="password",
value=default_key,
help=f"环境变量: {provider_config['api_key_var']}",
key="api_key_input"
)
# Sync to session state for save callback
st.session_state.api_key = api_key
# Base URL
default_url = saved.get("base_url") if saved.get("provider") == selected_provider_label else provider_config["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 以继续")
2026-01-07 11:02:05 +08:00
2026-01-07 14:04:52 +08:00
# 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
2026-01-07 14:04:52 +08:00
output_language = st.sidebar.selectbox(
"🌐 输出语言",
options=lang_options,
index=lang_idx,
help="所有 AI Agent 将使用此语言进行回复",
key="output_language",
on_change=save_current_config
2026-01-07 14:04:52 +08:00
)
2026-01-07 11:02:05 +08:00
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)
2026-01-07 11:02:05 +08:00
)
# 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"
else:
st.session_state.mode = "History Archives"
2026-01-07 11:02:05 +08:00
st.divider()
if st.session_state.mode == "Debate Workshop": # Debate Workshop Settings
2026-01-07 11:02:05 +08:00
# 模型选择
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 mode == "Deep Research":
st.title("🧪 Multi-Model Council V4")
st.markdown("*多模型智囊团:自定义 N 个专家进行多轮对话讨论,最后由最后一位专家决策*")
col1, col2 = st.columns([3, 1])
with col1:
research_topic = st.text_area("研究/决策主题", placeholder="请输入你想深入研究或决策的主题...", height=100)
with col2:
max_rounds = st.number_input("讨论轮数", min_value=1, max_value=5, value=2, help="专家们进行对话的轮数")
# Expert Configuration
st.subheader("👥 专家配置")
num_experts = st.number_input("专家数量", min_value=2, max_value=5, value=3)
2026-01-07 11:02:05 +08:00
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)]
st.markdown(f"**Expert {i+1}**")
# Default names
default_name = f"Expert {i+1}"
if i == num_experts - 1:
default_name = f"Expert {i+1} (Synthesizer)"
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
})
2026-01-07 11:02:05 +08:00
research_context = st.text_area("补充背景 (可选)", placeholder="任何额外的背景信息...", height=80)
2026-01-07 15:13:17 +08:00
research_image = st.file_uploader("上传背景图片(可选)", type=['png', 'jpg', 'jpeg', 'gif'], key='research_bg')
2026-01-07 11:02:05 +08:00
start_research_btn = st.button("🚀 开始多模型协作", type="primary", disabled=not research_topic)
2026-01-07 11:02:05 +08:00
if start_research_btn and research_topic:
st.session_state.research_started = True
2026-01-07 11:02:05 +08:00
st.session_state.research_output = ""
st.session_state.research_steps_output = []
2026-01-07 15:13:17 +08:00
# 如果上传了背景图片,保存到 assets
research_bg_path = None
if research_image:
research_bg_path = st.session_state.storage.save_asset(research_image)
if research_bg_path:
st.success("背景图片已上传并保存")
try:
st.image(research_image, caption="已上传背景图片预览", use_column_width=True)
except Exception:
pass
manager = ResearchManager(
api_key=api_key,
base_url=base_url,
provider=provider_id
)
2026-01-07 11:02:05 +08:00
config_obj = ResearchConfig(
topic=research_topic,
context=research_context,
2026-01-07 14:04:52 +08:00
experts=experts_config,
language=output_language
2026-01-07 11:02:05 +08:00
)
manager.create_agents(config_obj)
st.divider()
st.subheader("🗣️ 智囊团讨论中...")
2026-01-07 11:02:05 +08:00
chat_container = st.container()
2026-01-07 11:02:05 +08:00
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:
st.markdown(f"#### {current_step_name}")
st.caption(f"🤖 {current_agent} ({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"]
})
st.divider() # Separator between turns
2026-01-07 11:02:05 +08:00
# 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("✅ 综合方案生成完毕")
2026-01-07 11:02:05 +08:00
2026-01-07 15:13:17 +08:00
# 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,
2026-01-07 15:13:17 +08:00
metadata=metadata
)
st.toast("✅ 记录已保存到历史档案")
except Exception as e:
st.error(f"发生错误: {str(e)}")
import traceback
st.code(traceback.format_exc())
2026-01-07 11:02:05 +08:00
# Show Final Report if available
if st.session_state.research_output:
st.divider()
st.subheader("📄 最终综合方案")
2026-01-07 11:02:05 +08:00
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()
2026-01-07 11:02:05 +08:00
elif 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
)
2026-01-07 15:13:17 +08:00
context_image = st.file_uploader("上传背景图片(可选)", type=['png', 'jpg', 'jpeg', 'gif'], key='debate_bg')
2026-01-07 11:02:05 +08:00
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
2026-01-07 11:02:05 +08:00
# 角色数量提示
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
)
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:
# 初始化默认客户端
2026-01-07 11:02:05 +08:00
llm_client = LLMClient(
provider=provider_id,
2026-01-07 11:02:05 +08:00
api_key=api_key,
base_url=base_url,
2026-01-07 11:02:05 +08:00
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
)
2026-01-07 15:13:17 +08:00
# 如果在界面上传了背景图片,自动保存到 assets
debate_bg_path = None
try:
if 'context_image' in locals() and context_image:
debate_bg_path = st.session_state.storage.save_asset(context_image)
if debate_bg_path:
st.success("背景图片已上传并保存")
try:
st.image(context_image, caption="已上传背景图片预览", use_column_width=True)
except Exception:
pass
except Exception:
# ignore upload errors but continue
debate_bg_path = None
2026-01-07 11:02:05 +08:00
debate_manager = DebateManager(llm_client)
# 配置辩论
debate_config = DebateConfig(
topic=topic,
context=context,
agent_ids=selected_agents,
max_rounds=max_rounds,
2026-01-07 14:04:52 +08:00
agent_clients=agent_clients,
language=output_language
2026-01-07 11:02:05 +08:00
)
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)
2026-01-07 11:02:05 +08:00
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()
elif event["type"] == "debate_end":
st.session_state.debate_finished = True
st.success("✅ 辩论结束!正在生成决策报告...")
# 生成报告
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,
2026-01-07 15:13:17 +08:00
"language": output_language,
**({"background_image": debate_bg_path} if debate_bg_path else {})
}
)
st.toast("✅ 记录已保存到历史档案")
2026-01-07 11:02:05 +08:00
# 下载按钮
st.download_button(
label="📥 下载报告 (Markdown)",
data=report_content,
file_name="decision_report.md",
mime="text/markdown"
)
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'])
2026-01-07 15:13:17 +08:00
# 如果历史记录里有背景图片,显示预览
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"
)
2026-01-07 11:02:05 +08:00
# ==================== 底部信息 ====================
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
)