feat: implement council v3 round-table mode

This commit is contained in:
xyz 2026-01-07 14:42:29 +08:00
parent 5913d2dc47
commit 02eea5bfb4
4 changed files with 245 additions and 12 deletions

Binary file not shown.

145
app.py
View File

@ -13,7 +13,9 @@ from agents import get_all_agents, get_recommended_agents, AGENT_PROFILES
from orchestrator import DebateManager, DebateConfig from orchestrator import DebateManager, DebateConfig
from orchestrator.research_manager import ResearchManager, ResearchConfig from orchestrator.research_manager import ResearchManager, ResearchConfig
from report import ReportGenerator from report import ReportGenerator
from report import ReportGenerator
from utils import LLMClient from utils import LLMClient
from utils.storage import StorageManager
import config import config
# ==================== 页面配置 ==================== # ==================== 页面配置 ====================
@ -77,6 +79,23 @@ DECISION_TYPES = {
} }
# ==================== 初始化 Session State ==================== # ==================== 初始化 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: if "mode" not in st.session_state:
st.session_state.mode = "Deep Research" st.session_state.mode = "Deep Research"
@ -109,40 +128,73 @@ with st.sidebar:
# 全局 API Key & Provider 设置 # 全局 API Key & Provider 设置
with st.expander("🔑 API / Provider 设置", expanded=True): with st.expander("🔑 API / Provider 设置", expanded=True):
# Saved preferences
saved = st.session_state.saved_config
# Provider Selection # 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( selected_provider_label = st.selectbox(
"选择 API 提供商", "选择 API 提供商",
options=list(config.LLM_PROVIDERS.keys()), options=provider_options,
index=0 index=prov_idx,
key="selected_provider",
on_change=save_current_config
) )
provider_config = config.LLM_PROVIDERS[selected_provider_label] provider_config = config.LLM_PROVIDERS[selected_provider_label]
provider_id = selected_provider_label.lower() provider_id = selected_provider_label.lower()
# API Key Input # API Key Input
default_key = os.getenv(provider_config["api_key_var"], "") # 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( api_key = st.text_input(
f"{selected_provider_label} API Key", f"{selected_provider_label} API Key",
type="password", type="password",
value=default_key, value=default_key,
help=f"环境变量: {provider_config['api_key_var']}" 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 # 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( base_url = st.text_input(
"API Base URL", "API Base URL",
value=provider_config["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: if not api_key:
st.warning("请配置 API Key 以继续") st.warning("请配置 API Key 以继续")
# Output Language Selection # 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( output_language = st.sidebar.selectbox(
"🌐 输出语言", "🌐 输出语言",
options=config.SUPPORTED_LANGUAGES, options=lang_options,
index=0, index=lang_idx,
help="所有 AI Agent 将使用此语言进行回复" help="所有 AI Agent 将使用此语言进行回复",
key="output_language",
on_change=save_current_config
) )
st.divider() st.divider()
@ -150,15 +202,22 @@ with st.sidebar:
# 模式选择 # 模式选择
mode = st.radio( mode = st.radio(
"📊 选择模式", "📊 选择模式",
["Deep Research", "Debate Workshop"], ["Council V4 (Deep Research)", "Debate Workshop", "📜 History Archives"],
index=0 if st.session_state.mode == "Deep Research" else 1 index=0 if st.session_state.mode == "Deep Research" else (1 if st.session_state.mode == "Debate Workshop" else 2)
) )
st.session_state.mode = mode
# 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() st.divider()
if mode == "Debate Workshop": # Debate Workshop Settings if st.session_state.mode == "Debate Workshop": # Debate Workshop Settings
# 模型选择 # 模型选择
model = st.selectbox( model = st.selectbox(
"🤖 选择通用模型", "🤖 选择通用模型",
@ -311,6 +370,19 @@ if mode == "Deep Research":
st.session_state.research_output = final_plan st.session_state.research_output = final_plan
st.success("✅ 综合方案生成完毕") 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: except Exception as e:
st.error(f"发生错误: {str(e)}") st.error(f"发生错误: {str(e)}")
import traceback import traceback
@ -549,6 +621,19 @@ elif mode == "Debate Workshop":
st.session_state.report = 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( st.download_button(
label="📥 下载报告 (Markdown)", label="📥 下载报告 (Markdown)",
@ -580,6 +665,42 @@ elif mode == "Debate Workshop":
mime="text/markdown" 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() st.divider()
col_footer1, col_footer2, col_footer3 = st.columns(3) col_footer1, col_footer2, col_footer3 = st.columns(3)

Binary file not shown.

112
utils/storage.py Normal file
View File

@ -0,0 +1,112 @@
"""
Storage Manager - Handle local persistence of configuration and history/reports.
"""
import os
import json
import time
from typing import List, Dict, Any
from pathlib import Path
# Constants
STORAGE_DIR = ".storage"
CONFIG_FILE = "config.json"
HISTORY_DIR = "history"
class StorageManager:
def __init__(self):
self.root_dir = Path(STORAGE_DIR)
self.config_path = self.root_dir / CONFIG_FILE
self.history_dir = self.root_dir / HISTORY_DIR
# Ensure directories exist
self.root_dir.mkdir(exist_ok=True)
self.history_dir.mkdir(exist_ok=True)
def save_config(self, config_data: Dict[str, Any]):
"""Save UI configuration to file"""
try:
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(config_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving config: {e}")
def load_config(self) -> Dict[str, Any]:
"""Load UI configuration from file"""
if not self.config_path.exists():
return {}
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading config: {e}")
return {}
def save_history(self, session_type: str, topic: str, content: str, metadata: Dict[str, Any] = None):
"""
Save a session report/history
Args:
session_type: 'council' or 'debate'
topic: The main topic
content: The full markdown report or content
metadata: Additional info (model used, date, etc)
"""
timestamp = int(time.time())
date_str = time.strftime("%Y-%m-%d %H:%M:%S")
# Create a safe filename
safe_topic = "".join([c for c in topic[:20] if c.isalnum() or c in (' ', '_', '-')]).strip().replace(' ', '_')
filename = f"{timestamp}_{session_type}_{safe_topic}.json"
data = {
"id": str(timestamp),
"timestamp": timestamp,
"date": date_str,
"type": session_type,
"topic": topic,
"content": content,
"metadata": metadata or {}
}
try:
with open(self.history_dir / filename, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Error saving history: {e}")
return False
def list_history(self) -> List[Dict[str, Any]]:
"""List all history items (metadata only)"""
items = []
if not self.history_dir.exists():
return []
for file in self.history_dir.glob("*.json"):
try:
with open(file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Return summary info
items.append({
"id": data.get("id"),
"date": data.get("date"),
"type": data.get("type"),
"topic": data.get("topic"),
"filename": file.name
})
except Exception:
continue
# Sort by timestamp desc
return sorted(items, key=lambda x: x.get("date", ""), reverse=True)
def load_history_item(self, filename: str) -> Dict[str, Any]:
"""Load full content of a history item"""
path = self.history_dir / filename
if not path.exists():
return None
try:
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return None