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.research_manager import ResearchManager, ResearchConfig
from report import ReportGenerator
from report import ReportGenerator
from utils import LLMClient
from utils.storage import StorageManager
import config
# ==================== 页面配置 ====================
@ -77,6 +79,23 @@ DECISION_TYPES = {
}
# ==================== 初始化 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"
@ -109,40 +128,73 @@ with st.sidebar:
# 全局 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=list(config.LLM_PROVIDERS.keys()),
index=0
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
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(
f"{selected_provider_label} API Key",
type="password",
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
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=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:
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=config.SUPPORTED_LANGUAGES,
index=0,
help="所有 AI Agent 将使用此语言进行回复"
options=lang_options,
index=lang_idx,
help="所有 AI Agent 将使用此语言进行回复",
key="output_language",
on_change=save_current_config
)
st.divider()
@ -150,15 +202,22 @@ with st.sidebar:
# 模式选择
mode = st.radio(
"📊 选择模式",
["Deep Research", "Debate Workshop"],
index=0 if st.session_state.mode == "Deep Research" else 1
["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)
)
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()
if mode == "Debate Workshop": # Debate Workshop Settings
if st.session_state.mode == "Debate Workshop": # Debate Workshop Settings
# 模型选择
model = st.selectbox(
"🤖 选择通用模型",
@ -311,6 +370,19 @@ if mode == "Deep Research":
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
@ -549,6 +621,19 @@ elif mode == "Debate Workshop":
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)",
@ -580,6 +665,42 @@ elif mode == "Debate Workshop":
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)

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