wd666/app.py

565 lines
21 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
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 utils import LLMClient
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
# 决策类型
DECISION_TYPES = {
"product": "产品方案",
"business": "商业决策",
"tech": "技术选型",
"personal": "个人规划"
}
# ==================== 初始化 Session State ====================
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 设置
with st.expander("🔑 API Key 设置", expanded=True):
use_custom_key = st.checkbox("使用自定义 API Key")
if use_custom_key:
api_key = st.text_input(
"API Key",
type="password",
help="留空则使用环境变量中的 Key"
)
else:
api_key = DEFAULT_API_KEY
st.divider()
# 模式选择
mode = st.radio(
"📊 选择模式",
["Deep Research", "Debate Workshop"],
index=0 if st.session_state.mode == "Deep Research" else 1
)
st.session_state.mode = mode
st.divider()
if 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
)
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("✅ 综合方案生成完毕")
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
)
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()
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
# 下载按钮
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"
)
# ==================== 底部信息 ====================
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
)