""" 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 from utils import LLMClient from utils.storage import StorageManager import config # ==================== 页面配置 ==================== st.set_page_config( page_title="🎭 多 Agent 决策工作坊", page_icon="🎭", layout="wide", initial_sidebar_state="expanded" ) # ==================== 样式 ==================== st.markdown(""" """, 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"), "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) 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 以继续") # 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 ) 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) ) # 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" 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 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) 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 }) research_context = st.text_area("补充背景 (可选)", placeholder="任何额外的背景信息...", height=80) start_research_btn = st.button("🚀 开始多模型协作", type="primary", disabled=not research_topic) if start_research_btn and research_topic: st.session_state.research_started = True st.session_state.research_output = "" st.session_state.research_steps_output = [] 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.divider() 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: 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 # 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 st.session_state.storage.save_history( session_type="council", topic=research_topic, content=final_plan, metadata={ "rounds": max_rounds, "experts": [e["name"] for e in experts_config], "language": output_language } ) 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() 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 ) 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 ) 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_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'