from __future__ import annotations import os from dataclasses import dataclass, field from functools import lru_cache from pathlib import Path from typing import Any, Dict, List import yaml from dotenv import load_dotenv def _split_csv(value: str) -> List[str]: return [item.strip() for item in (value or "").split(",") if item.strip()] @dataclass class ModelSettings: """ Centralized model configuration. Env overrides: - FIN_ASSISTANT_ROUTER_MODEL - FIN_ASSISTANT_AGENT_MODEL - FIN_ASSISTANT_FORMATTER_MODEL - FIN_ASSISTANT_EMBEDDING_MODEL """ router_model: str = "gpt-4o" agent_model: str = "gpt-4o" formatter_model: str = "gpt-4o" embedding_model: str = "text-embedding-3-large" @dataclass class UISettings: tabs: List[str] = field(default_factory=lambda: ["Chat"]) market_watchlist: List[str] = field( default_factory=lambda: ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"] ) portfolio_default_input: str = "10 AAPL, 5 MSFT, 2 VTI" portfolio_examples: List[str] = field( default_factory=lambda: [ "10 AAPL, 5 MSFT, 2 VTI", "100 NVDA, 40 SPY, 25 BND", "12 SCHD, 8 VEA, 6 VWO", ] ) @dataclass class CacheSettings: response_cache_ttl_days: int = 30 @dataclass class KBSettings: """ Retrieval tuning for the local knowledge base. Env overrides: - FIN_ASSISTANT_KB_MIN_SCORE (float, 0-1) """ min_score: float = 0.70 @dataclass class ProfileDefaults: risk: str = "moderate" experience: str = "beginner" @dataclass class AppSettings: title: str = "Finance Assistant" page_title: str = "Finance Assistant" models: ModelSettings = field(default_factory=ModelSettings) ui: UISettings = field(default_factory=UISettings) cache: CacheSettings = field(default_factory=CacheSettings) kb: KBSettings = field(default_factory=KBSettings) default_user_profile: ProfileDefaults = field(default_factory=ProfileDefaults) @classmethod def load(cls, path: str = "config.yaml") -> "AppSettings": load_dotenv() settings = cls() config_path = Path(path) if config_path.exists(): with config_path.open("r", encoding="utf-8") as f: raw = yaml.safe_load(f) or {} settings = cls.from_dict(raw) return settings.apply_env_overrides() @classmethod def from_dict(cls, raw: Dict[str, Any]) -> "AppSettings": models_raw = raw.get("models", {}) or {} ui_raw = raw.get("ui", {}) or {} cache_raw = raw.get("cache", {}) or {} kb_raw = raw.get("kb", {}) or {} profile_raw = raw.get("default_user_profile", {}) or {} return cls( title=str( raw.get("title", raw.get("app", {}).get("title", "Finance Assistant")) ), page_title=str( raw.get( "page_title", raw.get("app", {}).get("page_title", "Finance Assistant"), ) ), models=ModelSettings( router_model=str(models_raw.get("router_model", "gpt-4o")), agent_model=str(models_raw.get("agent_model", "gpt-4o")), formatter_model=str(models_raw.get("formatter_model", "gpt-4o")), embedding_model=str( models_raw.get("embedding_model", "text-embedding-3-large") ), ), ui=UISettings( tabs=list(ui_raw.get("tabs", ["Chat"])), market_watchlist=list( ui_raw.get( "market_watchlist", ["AAPL", "MSFT", "NVDA", "SPY", "QQQ"] ) ), portfolio_default_input=str( ui_raw.get("portfolio_default_input", "10 AAPL, 5 MSFT, 2 VTI") ), portfolio_examples=list( ui_raw.get( "portfolio_examples", [ "10 AAPL, 5 MSFT, 2 VTI", "100 NVDA, 40 SPY, 25 BND", "12 SCHD, 8 VEA, 6 VWO", ], ) ), ), cache=CacheSettings( response_cache_ttl_days=int( cache_raw.get("response_cache_ttl_days", 30) ) ), kb=KBSettings(min_score=float(kb_raw.get("min_score", 0.75))), default_user_profile=ProfileDefaults( risk=str(profile_raw.get("risk", "moderate")), experience=str(profile_raw.get("experience", "beginner")), ), ) def apply_env_overrides(self) -> "AppSettings": self.title = os.getenv("FIN_ASSISTANT_TITLE", self.title) self.page_title = os.getenv("FIN_ASSISTANT_PAGE_TITLE", self.page_title) self.models.router_model = os.getenv( "FIN_ASSISTANT_ROUTER_MODEL", self.models.router_model ) self.models.agent_model = os.getenv( "FIN_ASSISTANT_AGENT_MODEL", self.models.agent_model ) self.models.formatter_model = os.getenv( "FIN_ASSISTANT_FORMATTER_MODEL", self.models.formatter_model ) self.models.embedding_model = os.getenv( "FIN_ASSISTANT_EMBEDDING_MODEL", self.models.embedding_model ) tabs = os.getenv("FIN_ASSISTANT_TABS") if tabs: self.ui.tabs = _split_csv(tabs) watchlist = os.getenv("FIN_ASSISTANT_MARKET_WATCHLIST") if watchlist: self.ui.market_watchlist = _split_csv(watchlist) default_portfolio = os.getenv("FIN_ASSISTANT_PORTFOLIO_DEFAULT_INPUT") if default_portfolio: self.ui.portfolio_default_input = default_portfolio examples = os.getenv("FIN_ASSISTANT_PORTFOLIO_EXAMPLES") if examples: self.ui.portfolio_examples = [ item.strip() for item in examples.split(";") if item.strip() ] ttl_days = os.getenv("FIN_ASSISTANT_RESPONSE_CACHE_TTL_DAYS") if ttl_days: self.cache.response_cache_ttl_days = int(ttl_days) kb_min_score = os.getenv("FIN_ASSISTANT_KB_MIN_SCORE") if kb_min_score: self.kb.min_score = float(kb_min_score) self.default_user_profile.risk = os.getenv( "FIN_ASSISTANT_DEFAULT_RISK", self.default_user_profile.risk ) self.default_user_profile.experience = os.getenv( "FIN_ASSISTANT_DEFAULT_EXPERIENCE", self.default_user_profile.experience ) return self @lru_cache(maxsize=1) def get_settings() -> AppSettings: return AppSettings.load()