feat: implement council v3 round-table mode
This commit is contained in:
parent
5913d2dc47
commit
02eea5bfb4
Binary file not shown.
145
app.py
145
app.py
@ -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)
|
||||||
|
|||||||
BIN
utils/__pycache__/storage.cpython-313.pyc
Normal file
BIN
utils/__pycache__/storage.cpython-313.pyc
Normal file
Binary file not shown.
112
utils/storage.py
Normal file
112
utils/storage.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user