wd666/app.py

714 lines
27 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 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("""
<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 "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'<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
# 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
}
)
st.toast("✅ 记录已保存到历史档案")
# 下载按钮
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'])
st.download_button(
"📥 下载此记录",
record['content'],
file_name=f"{record['type']}_{record['id']}.md"
)
# ==================== 底部信息 ====================
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
)