"""LLM model configurations and workflow enums for enhanced reporting.""" from dataclasses import dataclass, field from enum import Enum from typing import List, Optional from langchain_core.language_models import BaseChatModel from utils.llm.provider_factory import LLMProviderFactory # ============================================================================ # Investment Style and Phase Configuration (Feature 002) # ============================================================================ class InvestmentStyle(str, Enum): """Investment style profiles that configure analysis emphasis and defaults.""" LONG_TERM = "long_term" """Long-term investment focus (quarters to years). - Default timeframe: Weekly (1w) - Analysis priority: Fundamental > Technical - Review cycle: Quarterly - Suitable timeframes: 1w, 1mo, 3mo, 1y, 5y """ SWING_TRADING = "swing_trading" """Swing trading focus (weeks to months). - Default timeframe: Daily (1d) - Analysis priority: Technical + Fundamental (balanced) - Review cycle: Weekly - Suitable timeframes: 1d, 1w, 1mo """ class AnalysisPhase(str, Enum): """Workflow phases that can be selectively executed.""" TECHNICAL = "technical" """Technical Analysis Phase - Indicators, patterns, trends (~15-20s)""" FUNDAMENTAL = "fundamental" """Fundamental Analysis Phase - Financial metrics, valuation (~30-40s)""" SENTIMENT = "sentiment" """Sentiment Analysis Phase - Social & news sentiment (~20-30s)""" RESEARCH_SYNTHESIS = "research_synthesis" """Research Synthesis Phase - Integrated perspective (~15-20s)""" RISK = "risk" """Risk Assessment Phase - Risk scores, volatility (~15-20s)""" DECISION = "decision" """Portfolio Decision Phase - Final BUY/SELL/HOLD (~15-20s)""" @dataclass class PhaseConfiguration: """Configuration for phase-based workflow execution.""" investment_style: InvestmentStyle = InvestmentStyle.LONG_TERM """User's investment approach (long-term vs swing trading).""" enabled_phases: List[AnalysisPhase] = field( default_factory=lambda: [ AnalysisPhase.TECHNICAL, AnalysisPhase.FUNDAMENTAL, AnalysisPhase.SENTIMENT, AnalysisPhase.RESEARCH_SYNTHESIS, AnalysisPhase.RISK, AnalysisPhase.DECISION, ] ) """List of workflow phases to execute.""" timeframe: str = "1w" """Selected analysis timeframe (1d, 1w, 1mo, 3mo, 1y, 5y).""" chart_period_days: int = 180 """Number of days of historical data to fetch for charts.""" educational_mode: bool = True """Whether to include plain-English explanations and tooltips.""" def validate(self) -> List[str]: """Validate configuration and return list of errors. Returns: List of validation error messages (empty if valid) """ errors = [] # At least one phase must be enabled if not self.enabled_phases: errors.append("At least one analysis phase must be enabled") return errors phases_set = set(self.enabled_phases) analysis_phases = { AnalysisPhase.TECHNICAL, AnalysisPhase.FUNDAMENTAL, AnalysisPhase.SENTIMENT, } # RESEARCH_SYNTHESIS requires at least one analysis phase if AnalysisPhase.RESEARCH_SYNTHESIS in phases_set: if not (analysis_phases & phases_set): errors.append( "Research Synthesis phase requires at least one analysis phase " "(Technical, Fundamental, or Sentiment) to be enabled" ) # RISK requires at least one analysis phase if AnalysisPhase.RISK in phases_set: if not (analysis_phases & phases_set): errors.append( "Risk Assessment phase requires at least one analysis phase " "(Technical, Fundamental, or Sentiment) to be enabled" ) # DECISION requires RESEARCH_SYNTHESIS if AnalysisPhase.DECISION in phases_set: if AnalysisPhase.RESEARCH_SYNTHESIS not in phases_set: errors.append( "Portfolio Decision phase requires Research Synthesis phase to be enabled" ) return errors @classmethod def from_investment_style(cls, style: InvestmentStyle) -> "PhaseConfiguration": """Create configuration with defaults for investment style. Args: style: Investment style to configure for Returns: PhaseConfiguration with style-appropriate defaults """ if style == InvestmentStyle.LONG_TERM: return cls( investment_style=style, timeframe="1w", chart_period_days=365, # 1 year of data enabled_phases=[ AnalysisPhase.TECHNICAL, AnalysisPhase.FUNDAMENTAL, AnalysisPhase.SENTIMENT, AnalysisPhase.RESEARCH_SYNTHESIS, AnalysisPhase.RISK, AnalysisPhase.DECISION, ], ) else: # SWING_TRADING return cls( investment_style=style, timeframe="1d", chart_period_days=90, # 3 months of data enabled_phases=[ AnalysisPhase.TECHNICAL, AnalysisPhase.FUNDAMENTAL, AnalysisPhase.RESEARCH_SYNTHESIS, AnalysisPhase.DECISION, ], # Skip SENTIMENT and RISK for faster execution ) # ============================================================================ # LLM Model Configurations # ============================================================================ # Per-agent LLM model configurations # Format: {"provider": str, "model": str, "temperature": float} AGENT_MODELS = { # Technical Analysis Agents (User Story 1) "indicator_agent": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.1, }, "pattern_agent": { "provider": "openai", "model": "gpt-4o", # Needs vision for chart analysis "temperature": 0.1, }, "trend_agent": { "provider": "openai", "model": "gpt-4o", "temperature": 0.1, }, "decision_agent": { "provider": "openai", "model": "gpt-4o", "temperature": 0.1, }, # Fundamental Analysis Agents (User Story 2) "fundamentals_agent": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.1, }, "sentiment_agent": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.2, }, "news_agent": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.1, }, "technical_analyst": { "provider": "openai", "model": "gpt-4o", "temperature": 0.1, }, # Management Agents (User Story 2) "researcher_team": { "provider": "openai", "model": "gpt-4o", "temperature": 0.3, # Higher for debate/creativity }, "risk_manager": { "provider": "openai", "model": "gpt-4o", "temperature": 0.1, }, "portfolio_manager": { "provider": "openai", "model": "gpt-4o", "temperature": 0.1, }, } # Default models for each provider (used when provider is overridden but model is not) DEFAULT_MODELS_BY_PROVIDER = { "openai": "gpt-4o-mini", "anthropic": "claude-3-5-sonnet-20241022", "huggingface": "meta-llama/Llama-3.3-70B-Instruct", # Llama 3.3 available via HF Inference Providers "qwen": "qwen2.5-72b-instruct", } def get_model( agent_name: str, routing_policy: Optional[str] = None, runtime_config: Optional[dict] = None, ) -> BaseChatModel: """ Get configured LLM model for a specific agent. Args: agent_name: Name of the agent (e.g., "indicator_agent", "portfolio_manager") routing_policy: Optional routing policy override for HuggingFace (auto, fastest, cheapest, or provider name) runtime_config: Optional runtime configuration with llm_provider/llm_model overrides Returns: Configured LangChain chat model Raises: ValueError: If agent_name is not found in configuration """ if agent_name not in AGENT_MODELS: raise ValueError( f"Agent '{agent_name}' not found in model configuration. " f"Available agents: {', '.join(AGENT_MODELS.keys())}" ) config = AGENT_MODELS[agent_name] # Apply runtime overrides if provided if runtime_config: provider = runtime_config.get("llm_provider", config["provider"]) # If provider is overridden but model is not specified, use default model for that provider if "llm_provider" in runtime_config and "llm_model" not in runtime_config: model = DEFAULT_MODELS_BY_PROVIDER.get(provider, config["model"]) else: model = runtime_config.get("llm_model", config["model"]) else: provider = config["provider"] model = config["model"] # Use routing_policy from config if not overridden if routing_policy is None: routing_policy = config.get("routing_policy") return LLMProviderFactory.create( provider=provider, model=model, temperature=config["temperature"], routing_policy=routing_policy, ) # ============================================================================ # Valuation Dashboard Models (Feature 004) # ============================================================================ class ChartType(str, Enum): """Type of fundamental chart in the valuation dashboard.""" PE_RATIO = "PE_RATIO" """Price-to-Earnings ratio over time""" PB_RATIO = "PB_RATIO" """Price-to-Book ratio over time""" PS_RATIO = "PS_RATIO" """Price-to-Sales ratio over time""" EV_EBITDA = "EV_EBITDA" """EV/EBITDA ratio over time""" PROFIT_MARGINS = "PROFIT_MARGINS" """Multi-line chart (Gross, Operating, Net margins)""" ROE = "ROE" """Return on Equity over time""" REVENUE_EARNINGS_GROWTH = "REVENUE_EARNINGS_GROWTH" """YoY growth rates for revenue and earnings""" FREE_CASH_FLOW = "FREE_CASH_FLOW" """Free cash flow over time""" DEBT_TO_EQUITY = "DEBT_TO_EQUITY" """Debt-to-Equity ratio over time""" class DataAvailability(str, Enum): """Status of data completeness for a chart.""" COMPLETE = "complete" """All data available, chart rendered successfully""" PARTIAL = "partial" """Some data missing, chart rendered with gaps""" UNAVAILABLE = "unavailable" """No data available, placeholder chart shown""" ERROR = "error" """Error occurred during data fetch or calculation""" @dataclass class DataPoint: """Single (timestamp, value) pair in a time series.""" timestamp: str # ISO format datetime string value: Optional[float] # None if unavailable for this period @dataclass class DataSeries: """Time-series data for a single line on a chart.""" name: str # Series name (e.g., "Gross Margin") data_points: List[DataPoint] data_frequency: str # "quarterly", "annual", "monthly" color: Optional[str] = None # Hex color code (e.g., "#2962FF") line_style: Optional[str] = None # Line style ("-", "--", ":") @dataclass class ChartData: """Represents a single fundamental metric chart.""" chart_type: ChartType title: str data_series: List[DataSeries] x_label: str y_label: str unit: str # "ratio", "%", "USD millions" data_availability: DataAvailability file_path: Optional[str] = None # Path to generated chart PNG legend_enabled: bool = True error_message: Optional[str] = None rendering_hint: Optional[str] = None @dataclass class GridConfig: """Grid dimensions for a specific screen size.""" columns: int rows: int min_width: Optional[int] = None # Minimum column width in pixels @dataclass class DashboardLayout: """Configuration for responsive grid layout.""" desktop_config: GridConfig = field( default_factory=lambda: GridConfig(columns=2, rows=4, min_width=300) ) tablet_config: GridConfig = field( default_factory=lambda: GridConfig(columns=2, rows=4, min_width=300) ) mobile_config: GridConfig = field( default_factory=lambda: GridConfig(columns=1, rows=8) ) @dataclass class ValuationDashboard: """Container for the complete set of fundamental analysis charts.""" ticker: str start_date: str # ISO format datetime string end_date: str # ISO format datetime string charts: List[ChartData] layout_config: DashboardLayout generation_timestamp: str # ISO format datetime string data_source: str # "yfinance", "alpha_vantage", "hybrid" @dataclass class ValuationMetrics: """Valuation ratio data for charts and analysis.""" pe_ratio: Optional[float] = None # None if negative earnings pb_ratio: Optional[float] = None # None if negative equity ps_ratio: Optional[float] = None ev_ebitda: Optional[float] = None # None if negative EBITDA peg_ratio: Optional[float] = None market_cap: Optional[float] = None # USD millions @dataclass class ProfitabilityMetrics: """Profitability and efficiency data.""" gross_margin: Optional[float] = None # Percentage (0-100) operating_margin: Optional[float] = None # Percentage (0-100) net_margin: Optional[float] = None # Percentage (0-100) roe: Optional[float] = None # Percentage, None if equity <= 0 @dataclass class GrowthMetrics: """Year-over-year growth rates.""" revenue_growth_yoy: Optional[float] = None # Percentage earnings_growth_yoy: Optional[float] = None # Percentage @dataclass class CashFlowMetrics: """Cash flow data.""" free_cash_flow: Optional[float] = None # USD millions operating_cash_flow: Optional[float] = None # USD millions capex: Optional[float] = None # USD millions @dataclass class LeverageMetrics: """Financial leverage and debt data.""" debt_to_equity: Optional[float] = None # None if equity <= 0 total_debt: Optional[float] = None # USD millions total_equity: Optional[float] = None # USD millions @dataclass class BalanceSheet: """Balance sheet data for agent tables.""" total_assets: Optional[float] = None # USD millions total_liabilities: Optional[float] = None # USD millions total_equity: Optional[float] = None # USD millions working_capital: Optional[float] = None # USD millions cash_and_equivalents: Optional[float] = None # USD millions retained_earnings: Optional[float] = None # USD millions @dataclass class IncomeStatement: """Income statement data for agent tables.""" total_revenue: Optional[float] = None # USD millions gross_profit: Optional[float] = None # USD millions net_income: Optional[float] = None # USD millions ebitda: Optional[float] = None # USD millions basic_eps: Optional[float] = None # USD @dataclass class CashFlowStatement: """Cash flow statement data for agent tables.""" operating_cash_flow: Optional[float] = None # USD millions free_cash_flow: Optional[float] = None # USD millions capex: Optional[float] = None # USD millions @dataclass class FinancialStatements: """Complete financial statement data.""" balance_sheet: BalanceSheet income_statement: IncomeStatement cash_flow_statement: CashFlowStatement @dataclass class FundamentalMetrics: """Comprehensive collection of fundamental data.""" ticker: str as_of_date: str # ISO format datetime string valuation: ValuationMetrics profitability: ProfitabilityMetrics growth: GrowthMetrics cash_flow: CashFlowMetrics leverage: LeverageMetrics financial_statements: FinancialStatements data_sources: dict # Mapping of metric → data source used @dataclass class ContentSection: """Hierarchical section in agent response.""" heading: str # Markdown heading (e.g., "## Company Overview") level: int # Heading level (1-4) content: str # Markdown-formatted content subsections: List["ContentSection"] = field(default_factory=list) @dataclass class DataTable: """Formatted table presenting data.""" headers: List[str] rows: List[List[str]] # Each row is list of cell values title: Optional[str] = None format_hints: Optional[dict] = None # Column formatting hints @dataclass class ConclusionBlock: """Final recommendation or synthesis.""" heading: str # e.g., "Transaction Proposal" recommendation: str # Clear actionable recommendation rationale: str # Explanation for the recommendation supporting_data: Optional[DataTable] = None @dataclass class AgentResponseFormat: """Structured format for agent responses.""" sections: List[ContentSection] key_insights: List[str] # Bullet-pointed insights summary: List[str] # Numbered summary points conclusion: ConclusionBlock data_tables: List[DataTable] = field(default_factory=list)