"""Pydantic schemas used by agents that produce structured output. The framework's primary artifact is still prose: each agent's natural-language reasoning is what users read, what gets stored in the memory log, and what gets saved as markdown reports. Structured output is layered onto agents whose results have downstream machine-readable consumers (currently only the Portfolio Manager) so that: - The rating is type-safe and never has to be regex-extracted - Schema field descriptions become the model's output instructions - Each provider's native structured-output mode is used (json_schema for OpenAI/xAI, response_schema for Gemini, tool-use for Anthropic) - A render helper turns the parsed Pydantic instance back into the same markdown shape the rest of the system already consumes, so display, memory log, and saved reports keep working unchanged """ from __future__ import annotations from enum import Enum from typing import Optional from pydantic import BaseModel, Field class PortfolioRating(str, Enum): """5-tier portfolio rating used by the Research Manager and Portfolio Manager.""" BUY = "Buy" OVERWEIGHT = "Overweight" HOLD = "Hold" UNDERWEIGHT = "Underweight" SELL = "Sell" class PortfolioDecision(BaseModel): """Structured output produced by the Portfolio Manager. The model fills every field as part of its primary LLM call; no separate extraction pass is required. Field descriptions double as the model's output instructions, so the prompt body only needs to convey context and the rating-scale guidance. """ rating: PortfolioRating = Field( description=( "The final position rating. Exactly one of Buy / Overweight / Hold / " "Underweight / Sell, picked based on the analysts' debate." ), ) executive_summary: str = Field( description=( "A concise action plan covering entry strategy, position sizing, " "key risk levels, and time horizon. Two to four sentences." ), ) investment_thesis: str = Field( description=( "Detailed reasoning anchored in specific evidence from the analysts' " "debate. If prior lessons are referenced in the prompt context, " "incorporate them; otherwise rely solely on the current analysis." ), ) price_target: Optional[float] = Field( default=None, description="Optional target price in the instrument's quote currency.", ) time_horizon: Optional[str] = Field( default=None, description="Optional recommended holding period, e.g. '3-6 months'.", ) def render_pm_decision(decision: PortfolioDecision) -> str: """Render a PortfolioDecision back to the markdown shape the rest of the system expects. Memory log, CLI display, and saved report files all read this markdown, so the rendered output preserves the exact section headers (``**Rating**``, ``**Executive Summary**``, ``**Investment Thesis**``) that downstream parsers and the report writers already handle. """ parts = [ f"**Rating**: {decision.rating.value}", "", f"**Executive Summary**: {decision.executive_summary}", "", f"**Investment Thesis**: {decision.investment_thesis}", ] if decision.price_target is not None: parts.extend(["", f"**Price Target**: {decision.price_target}"]) if decision.time_horizon: parts.extend(["", f"**Time Horizon**: {decision.time_horizon}"]) return "\n".join(parts)