diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index eb7cddf..45a71ff 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ 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 0000000..00dbe78 Binary files /dev/null and b/utils/__pycache__/storage.cpython-313.pyc differ 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