From 02eea5bfb462a3e4d703dcd8a721a0d6ce73835d Mon Sep 17 00:00:00 2001 From: xyz <123456@gmail.com> Date: Wed, 7 Jan 2026 14:42:29 +0800 Subject: [PATCH] feat: implement council v3 round-table mode --- __pycache__/app.cpython-313.pyc | Bin 23690 -> 29238 bytes app.py | 145 ++++++++++++++++++++-- utils/__pycache__/storage.cpython-313.pyc | Bin 0 -> 6406 bytes utils/storage.py | 112 +++++++++++++++++ 4 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 utils/__pycache__/storage.cpython-313.pyc create mode 100644 utils/storage.py diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index eb7cddf25d62ba805d2d3441b21ee5a18cd84acd..45a71fff965fe2ce692e0184c857012ade49a361 100644 GIT binary patch delta 13318 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 PEEt!|ofgpPpg{UxwnSK4|B$otrrn6Db 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 diff --git a/app.py b/app.py index 92073fd..6501236 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/utils/__pycache__/storage.cpython-313.pyc b/utils/__pycache__/storage.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00dbe789764fc0daf5f49e089e90099dcb2ea968 GIT binary patch literal 6406 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 Y$+z0D4MpoE-$OpdFwt)aO0)C-2h#sXWB>pF literal 0 HcmV?d00001 diff --git a/utils/storage.py b/utils/storage.py new file mode 100644 index 0000000..21d2b04 --- /dev/null +++ b/utils/storage.py @@ -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