2026-01-07 11:02:05 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Multi-Agent Decision Workshop - 主应用
|
|
|
|
|
|
多 Agent 决策工作坊:通过多角色辩论帮助用户做出更好的决策
|
|
|
|
|
|
"""
|
|
|
|
|
|
import streamlit as st
|
|
|
|
|
|
import os
|
2026-01-07 15:29:01 +08:00
|
|
|
|
import base64
|
2026-01-07 11:02:05 +08:00
|
|
|
|
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
|
2026-01-07 14:42:29 +08:00
|
|
|
|
from report import ReportGenerator
|
2026-01-07 11:02:05 +08:00
|
|
|
|
from utils import LLMClient
|
2026-01-07 14:42:29 +08:00
|
|
|
|
from utils.storage import StorageManager
|
2026-01-09 09:25:02 +08:00
|
|
|
|
from utils.auto_agent_generator import generate_experts_for_topic
|
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>
|
2026-01-09 09:25:02 +08:00
|
|
|
|
/* 蓝紫色渐变主题 - 模仿参考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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 原有样式保留 */
|
2026-01-07 11:02:05 +08:00
|
|
|
|
.agent-card {
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
border-radius: 0.5rem;
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
2026-01-09 09:25:02 +08:00
|
|
|
|
border-left: 4px solid #4A5CDB;
|
|
|
|
|
|
background-color: #F8F9FF;
|
2026-01-07 11:02:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
.speech-bubble {
|
2026-01-09 09:25:02 +08:00
|
|
|
|
background-color: #F8F9FF;
|
2026-01-07 11:02:05 +08:00
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
border-radius: 0.5rem;
|
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.round-header {
|
2026-01-09 09:25:02 +08:00
|
|
|
|
background: linear-gradient(90deg, #4A5CDB 0%, #667eea 50%, #764ba2 100%);
|
2026-01-07 11:02:05 +08:00
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
|
border-radius: 0.5rem;
|
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.custom-agent-form {
|
2026-01-09 09:25:02 +08:00
|
|
|
|
background-color: #F8F9FF;
|
2026-01-07 11:02:05 +08:00
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
border-radius: 0.5rem;
|
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.research-step {
|
2026-01-09 09:25:02 +08:00
|
|
|
|
border-left: 3px solid #4A5CDB;
|
2026-01-07 11:02:05 +08:00
|
|
|
|
padding-left: 10px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
2026-01-09 09:25:02 +08:00
|
|
|
|
|
|
|
|
|
|
/* 按钮样式增强 */
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-01-07 11:02:05 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 常量定义 ====================
|
|
|
|
|
|
# 从环境变量读取 API Key(隐藏在 .env 文件中)
|
|
|
|
|
|
DEFAULT_API_KEY = os.getenv("AIHUBMIX_API_KEY", "")
|
|
|
|
|
|
|
|
|
|
|
|
# 支持的模型列表
|
2026-01-07 12:59:56 +08:00
|
|
|
|
from config import AVAILABLE_MODELS, RESEARCH_MODEL_ROLES
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
|
|
|
|
|
# 决策类型
|
|
|
|
|
|
DECISION_TYPES = {
|
|
|
|
|
|
"product": "产品方案",
|
|
|
|
|
|
"business": "商业决策",
|
|
|
|
|
|
"tech": "技术选型",
|
|
|
|
|
|
"personal": "个人规划"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 初始化 Session State ====================
|
2026-01-07 14:42:29 +08:00
|
|
|
|
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"),
|
2026-01-07 16:03:41 +08:00
|
|
|
|
# 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", "")),
|
2026-01-07 14:42:29 +08:00
|
|
|
|
"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
|
2026-01-09 09:25:02 +08:00
|
|
|
|
if "generated_experts" not in st.session_state:
|
|
|
|
|
|
st.session_state.generated_experts = None # Auto-generated expert configs
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 侧边栏:配置 ====================
|
|
|
|
|
|
with st.sidebar:
|
|
|
|
|
|
st.header("⚙️ 设置")
|
|
|
|
|
|
|
2026-01-07 13:56:37 +08:00
|
|
|
|
# 全局 API Key & Provider 设置
|
|
|
|
|
|
with st.expander("🔑 API / Provider 设置", expanded=True):
|
2026-01-07 14:42:29 +08:00
|
|
|
|
# Saved preferences
|
|
|
|
|
|
saved = st.session_state.saved_config
|
|
|
|
|
|
|
2026-01-07 13:56:37 +08:00
|
|
|
|
# Provider Selection
|
2026-01-07 14:42:29 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-01-07 16:03:41 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-01-07 13:56:37 +08:00
|
|
|
|
selected_provider_label = st.selectbox(
|
|
|
|
|
|
"选择 API 提供商",
|
2026-01-07 14:42:29 +08:00
|
|
|
|
options=provider_options,
|
|
|
|
|
|
index=prov_idx,
|
|
|
|
|
|
key="selected_provider",
|
2026-01-07 16:03:41 +08:00
|
|
|
|
on_change=on_provider_change
|
2026-01-07 13:56:37 +08:00
|
|
|
|
)
|
2026-01-07 16:03:41 +08:00
|
|
|
|
# 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()
|
|
|
|
|
|
|
2026-01-07 13:56:37 +08:00
|
|
|
|
# API Key Input
|
2026-01-07 16:03:41 +08:00
|
|
|
|
# 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", ""), ""))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-07 13:56:37 +08:00
|
|
|
|
api_key = st.text_input(
|
2026-01-07 16:03:41 +08:00
|
|
|
|
f"{current_provider} API Key",
|
|
|
|
|
|
type="password",
|
2026-01-07 13:56:37 +08:00
|
|
|
|
value=default_key,
|
2026-01-07 16:03:41 +08:00
|
|
|
|
help=f"环境变量: {provider_config.get('api_key_var', '')}",
|
2026-01-07 14:42:29 +08:00
|
|
|
|
key="api_key_input"
|
2026-01-07 13:56:37 +08:00
|
|
|
|
)
|
2026-01-07 14:42:29 +08:00
|
|
|
|
# Sync to session state for save callback
|
|
|
|
|
|
st.session_state.api_key = api_key
|
2026-01-07 13:56:37 +08:00
|
|
|
|
|
|
|
|
|
|
# Base URL
|
2026-01-07 16:03:41 +08:00
|
|
|
|
# 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", ""))
|
|
|
|
|
|
)
|
2026-01-07 13:56:37 +08:00
|
|
|
|
base_url = st.text_input(
|
2026-01-07 16:03:41 +08:00
|
|
|
|
"API Base URL",
|
2026-01-07 14:42:29 +08:00
|
|
|
|
value=default_url,
|
|
|
|
|
|
key="base_url_input"
|
2026-01-07 13:56:37 +08:00
|
|
|
|
)
|
2026-01-07 14:42:29 +08:00
|
|
|
|
st.session_state.base_url = base_url
|
2026-01-07 13:56:37 +08:00
|
|
|
|
|
2026-01-07 14:42:29 +08:00
|
|
|
|
# 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()
|
|
|
|
|
|
|
2026-01-07 13:56:37 +08:00
|
|
|
|
if not api_key:
|
2026-01-09 09:25:02 +08:00
|
|
|
|
st.warning("⚠️ 请配置 API Key 以启用 AI 功能 (仍可查看历史档案)")
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
2026-01-07 14:04:52 +08:00
|
|
|
|
# Output Language Selection
|
2026-01-07 14:42:29 +08:00
|
|
|
|
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(
|
|
|
|
|
|
"🌐 输出语言",
|
2026-01-07 14:42:29 +08:00
|
|
|
|
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 15:29:01 +08:00
|
|
|
|
# 页面背景图片(全局)
|
|
|
|
|
|
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("页面背景已上传并保存")
|
|
|
|
|
|
|
2026-01-07 11:02:05 +08:00
|
|
|
|
st.divider()
|
|
|
|
|
|
|
|
|
|
|
|
# 模式选择
|
|
|
|
|
|
mode = st.radio(
|
|
|
|
|
|
"📊 选择模式",
|
2026-01-09 09:25:02 +08:00
|
|
|
|
["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))
|
2026-01-07 11:02:05 +08:00
|
|
|
|
)
|
2026-01-07 14:42:29 +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"
|
2026-01-09 09:25:02 +08:00
|
|
|
|
elif mode == "📜 History Archives":
|
2026-01-07 14:42:29 +08:00
|
|
|
|
st.session_state.mode = "History Archives"
|
2026-01-09 09:25:02 +08:00
|
|
|
|
else:
|
|
|
|
|
|
st.session_state.mode = "Feedback"
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
|
|
|
|
|
st.divider()
|
|
|
|
|
|
|
2026-01-07 13:44:46 +08:00
|
|
|
|
|
2026-01-07 14:42:29 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-01-07 15:29:01 +08:00
|
|
|
|
# 注入全局页面背景样式(如果已上传)
|
|
|
|
|
|
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
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-07 11:02:05 +08:00
|
|
|
|
# ==================== 主界面逻辑 ====================
|
|
|
|
|
|
|
2026-01-07 15:29:01 +08:00
|
|
|
|
if st.session_state.mode == "Deep Research":
|
2026-01-09 09:25:02 +08:00
|
|
|
|
# ==================== 主标题区域 ====================
|
|
|
|
|
|
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", "")
|
2026-01-07 13:44:46 +08:00
|
|
|
|
|
|
|
|
|
|
col1, col2 = st.columns([3, 1])
|
|
|
|
|
|
with col1:
|
2026-01-09 09:25:02 +08:00
|
|
|
|
research_topic = st.text_area("研究/决策主题", value=prefill_topic, placeholder="请输入你想深入研究或决策的主题...", height=100)
|
2026-01-07 13:44:46 +08:00
|
|
|
|
with col2:
|
|
|
|
|
|
max_rounds = st.number_input("讨论轮数", min_value=1, max_value=5, value=2, help="专家们进行对话的轮数")
|
|
|
|
|
|
|
|
|
|
|
|
# Expert Configuration
|
|
|
|
|
|
st.subheader("👥 专家配置")
|
2026-01-09 09:25:02 +08:00
|
|
|
|
|
|
|
|
|
|
# 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}")
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
2026-01-07 13:44:46 +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)]
|
2026-01-09 09:25:02 +08:00
|
|
|
|
|
|
|
|
|
|
# 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}**")
|
2026-01-07 13:44:46 +08:00
|
|
|
|
|
|
|
|
|
|
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-09 09:25:02 +08:00
|
|
|
|
start_research_btn = st.button("🚀 开始多模型协作", type="primary", disabled=(not research_topic or not api_key))
|
|
|
|
|
|
if not api_key:
|
|
|
|
|
|
st.info("💡 请先在侧边栏配置 API Key 才能开始任务")
|
|
|
|
|
|
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
2026-01-09 09:25:02 +08:00
|
|
|
|
# ==================== 恢复会话逻辑 (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) ====================
|
2026-01-07 12:59:56 +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-09 09:25:02 +08:00
|
|
|
|
|
|
|
|
|
|
# Clear any old cache when starting fresh
|
|
|
|
|
|
st.session_state.storage.clear_session_state("council_cache")
|
|
|
|
|
|
|
2026-01-07 15:29:01 +08:00
|
|
|
|
# 使用全局页面背景(若已上传)
|
|
|
|
|
|
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
|
2026-01-07 15:13:17 +08:00
|
|
|
|
|
2026-01-07 13:44:46 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-01-07 13:44:46 +08:00
|
|
|
|
st.subheader("🗣️ 智囊团讨论中...")
|
|
|
|
|
|
chat_container = st.container()
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
2026-01-07 12:59:56 +08:00
|
|
|
|
try:
|
2026-01-07 13:44:46 +08:00
|
|
|
|
for event in manager.collaborate(research_topic, research_context, max_rounds=max_rounds):
|
2026-01-07 12:59:56 +08:00
|
|
|
|
if event["type"] == "step_start":
|
|
|
|
|
|
current_step_name = event["step"]
|
|
|
|
|
|
current_agent = event["agent"]
|
|
|
|
|
|
current_model = event["model"]
|
2026-01-07 13:44:46 +08:00
|
|
|
|
|
|
|
|
|
|
# Create a chat message block
|
|
|
|
|
|
with chat_container:
|
2026-01-09 09:25:02 +08:00
|
|
|
|
with st.chat_message("assistant", avatar="🤖"):
|
|
|
|
|
|
st.markdown(f"**{current_step_name}**")
|
|
|
|
|
|
st.caption(f"({current_model})")
|
|
|
|
|
|
message_placeholder = st.empty()
|
|
|
|
|
|
current_content = ""
|
2026-01-07 12:59:56 +08:00
|
|
|
|
|
|
|
|
|
|
elif event["type"] == "content":
|
2026-01-07 13:44:46 +08:00
|
|
|
|
current_content += event["content"]
|
|
|
|
|
|
message_placeholder.markdown(current_content)
|
2026-01-07 12:59:56 +08:00
|
|
|
|
|
|
|
|
|
|
elif event["type"] == "step_end":
|
2026-01-07 13:44:46 +08:00
|
|
|
|
# Save step result for history
|
2026-01-07 12:59:56 +08:00
|
|
|
|
st.session_state.research_steps_output.append({
|
|
|
|
|
|
"step": current_step_name,
|
|
|
|
|
|
"output": event["output"]
|
|
|
|
|
|
})
|
2026-01-09 09:25:02 +08:00
|
|
|
|
|
|
|
|
|
|
# === 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)
|
|
|
|
|
|
# =======================
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
2026-01-07 12:59:56 +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
|
|
|
|
|
|
|
2026-01-07 14:42:29 +08:00
|
|
|
|
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
|
2026-01-07 14:42:29 +08:00
|
|
|
|
)
|
2026-01-09 09:25:02 +08:00
|
|
|
|
|
|
|
|
|
|
# Clear session cache as we finished successfully
|
|
|
|
|
|
st.session_state.storage.clear_session_state("council_cache")
|
|
|
|
|
|
|
2026-01-07 14:42:29 +08:00
|
|
|
|
st.toast("✅ 记录已保存到历史档案")
|
|
|
|
|
|
|
2026-01-07 12:59:56 +08:00
|
|
|
|
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()
|
2026-01-07 12:59:56 +08:00
|
|
|
|
st.subheader("📄 最终综合方案")
|
2026-01-07 11:02:05 +08:00
|
|
|
|
st.markdown(st.session_state.research_output)
|
2026-01-07 12:59:56 +08:00
|
|
|
|
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
|
|
|
|
|
2026-01-07 16:03:41 +08:00
|
|
|
|
# 追问模式(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()
|
|
|
|
|
|
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
2026-01-07 15:29:01 +08:00
|
|
|
|
elif st.session_state.mode == "Debate Workshop":
|
2026-01-07 11:02:05 +08:00
|
|
|
|
# ==================== 原始 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:29:01 +08:00
|
|
|
|
# 页面背景请使用侧边栏的“页面背景图片”上传控件进行设置(全局)
|
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)
|
|
|
|
|
|
|
2026-01-07 12:59:56 +08:00
|
|
|
|
# 自定义模型配置 (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
|
|
|
|
|
|
)
|
2026-01-09 09:25:02 +08:00
|
|
|
|
if not api_key:
|
|
|
|
|
|
st.caption("🔒 需配置 API Key")
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
|
|
|
|
|
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 12:59:56 +08:00
|
|
|
|
# 初始化默认客户端
|
2026-01-07 11:02:05 +08:00
|
|
|
|
llm_client = LLMClient(
|
2026-01-07 13:44:46 +08:00
|
|
|
|
provider=provider_id,
|
2026-01-07 11:02:05 +08:00
|
|
|
|
api_key=api_key,
|
2026-01-07 13:44:46 +08:00
|
|
|
|
base_url=base_url,
|
2026-01-07 11:02:05 +08:00
|
|
|
|
model=model
|
|
|
|
|
|
)
|
2026-01-07 12:59:56 +08:00
|
|
|
|
|
|
|
|
|
|
# 初始化特定角色的客户端
|
|
|
|
|
|
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(
|
2026-01-07 13:44:46 +08:00
|
|
|
|
provider=provider_id,
|
2026-01-07 12:59:56 +08:00
|
|
|
|
api_key=api_key,
|
2026-01-07 13:44:46 +08:00
|
|
|
|
base_url=base_url,
|
2026-01-07 12:59:56 +08:00
|
|
|
|
model=ag_model
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-07 15:29:01 +08:00
|
|
|
|
# 使用全局页面背景(若已上传)
|
|
|
|
|
|
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
|
2026-01-07 15:13:17 +08:00
|
|
|
|
|
2026-01-07 11:02:05 +08:00
|
|
|
|
debate_manager = DebateManager(llm_client)
|
|
|
|
|
|
|
|
|
|
|
|
# 配置辩论
|
|
|
|
|
|
debate_config = DebateConfig(
|
|
|
|
|
|
topic=topic,
|
|
|
|
|
|
context=context,
|
|
|
|
|
|
agent_ids=selected_agents,
|
2026-01-07 12:59:56 +08:00
|
|
|
|
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":
|
2026-01-07 12:59:56 +08:00
|
|
|
|
# 显示模型名称
|
|
|
|
|
|
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()
|
2026-01-07 15:29:01 +08:00
|
|
|
|
|
2026-01-07 11:02:05 +08:00
|
|
|
|
# 生成报告
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-01-07 14:42:29 +08:00
|
|
|
|
# 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,
|
2026-01-07 15:29:01 +08:00
|
|
|
|
**({"background_image": st.session_state.get("bg_image_path")} if st.session_state.get("bg_image_path") else {})
|
2026-01-07 14:42:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
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"
|
|
|
|
|
|
)
|
2026-01-07 16:03:41 +08:00
|
|
|
|
|
|
|
|
|
|
# 追问模式(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()
|
2026-01-07 11:02:05 +08:00
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-07 14:42:29 +08:00
|
|
|
|
# ==================== 历史档案浏览 ====================
|
|
|
|
|
|
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
|
2026-01-07 14:42:29 +08:00
|
|
|
|
st.download_button(
|
|
|
|
|
|
"📥 下载此记录",
|
|
|
|
|
|
record['content'],
|
|
|
|
|
|
file_name=f"{record['type']}_{record['id']}.md"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-09 09:25:02 +08:00
|
|
|
|
# ==================== 用户反馈页面 ====================
|
|
|
|
|
|
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("暂无反馈记录")
|
|
|
|
|
|
|
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
|
|
|
|
|
|
)
|