import sys from typing import Any, Literal, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from analysis.signals import QuarterDelta EvidenceSource = Literal["10-K", "10-Q", "transcript", "news", "metrics", "analyst"] VerificationStatus = Literal["VERIFIED", "UNVERIFIED", "FAILED"] class EvidenceRef(BaseModel): """LLM-copied locator whose canonical form is enforced only by ``verify_fact``.""" model_config = ConfigDict(extra="ignore") evidence_id: str = "" source: str = "" content_hash: str = "" document_id: str = "" chunk_id: Optional[str] = None source_url: Optional[str] = None as_of: Optional[str] = None @field_validator("source", mode="before") @classmethod def _coerce_source(cls, v: object) -> object: return _normalize_source(v) class EvidenceRecord(BaseModel): """Retrieved content plus its content-addressed reference.""" model_config = ConfigDict(extra="forbid") ref: EvidenceRef content: str = Field(min_length=1) metadata: dict[str, Any] = Field(default_factory=dict) _CANONICAL_CATEGORIES = { "Regulatory", "Operational", "Competitive", "Financial", "Macro", "Demand", "Geopolitical" } def _normalize_source(v: object) -> object: """Normalize case/whitespace only; compound sources must fail validation.""" if not isinstance(v, str): return v s = v.strip() canonical = { "10-k": "10-K", "10-q": "10-Q", "transcript": "transcript", "news": "news", "metrics": "metrics", "analyst": "analyst", } return canonical.get(s.lower(), s) _CATEGORY_ALIASES: dict[str, str] = { "Legal": "Regulatory", "Compliance": "Regulatory", "Cyber": "Operational", "Cybersecurity": "Operational", "Security": "Operational", "Supply Chain": "Operational", "Supply-Chain": "Operational", "Execution": "Operational", "Liquidity": "Financial", "Credit": "Financial", "Fx": "Financial", "Foreign Exchange": "Financial", "Currency": "Financial", "Economic": "Macro", "Recession": "Macro", "Inflation": "Macro", "Political": "Geopolitical", "Geo-Political": "Geopolitical", "Trade": "Geopolitical", "Tariff": "Geopolitical", "Tariffs": "Geopolitical", "Sanctions": "Geopolitical", "Customer": "Demand", "Consumer": "Demand", "Market": "Demand", } # --------------------------------------------------------------------------- # Earnings-quality helpers # --------------------------------------------------------------------------- _CANONICAL_ASSESSMENTS = {"positive", "neutral", "concerning"} _ASSESSMENT_ALIASES: dict[str, str] = { # → concerning "negative": "concerning", "bearish": "concerning", "concerned": "concerning", "caution": "concerning", "cautious": "concerning", "weak": "concerning", "poor": "concerning", "worrying": "concerning", "worrisome": "concerning", "alarming": "concerning", "deteriorating": "concerning", # → positive "bullish": "positive", "strong": "positive", "good": "positive", "favorable": "positive", "favourable": "positive", "encouraging": "positive", "improving": "positive", "healthy": "positive", "solid": "positive", # → neutral "mixed": "neutral", "balanced": "neutral", "stable": "neutral", "flat": "neutral", "unchanged": "neutral", "moderate": "neutral", } _CANONICAL_DIMENSIONS = { "consensus_beat_mix", "guidance_dynamics", "narrative_vs_numbers", "segment_mix", "capital_allocation", } _DIMENSION_ALIASES: dict[str, str] = { # → consensus_beat_mix "beat_mix": "consensus_beat_mix", "consensus": "consensus_beat_mix", "beat mix": "consensus_beat_mix", "earnings beat": "consensus_beat_mix", "beat_vs_consensus": "consensus_beat_mix", "surprise": "consensus_beat_mix", "eps_beat": "consensus_beat_mix", "eps beat": "consensus_beat_mix", # → consensus_beat_mix (below-the-line / beat-quality drivers the LLM names separately) "tax_rate": "consensus_beat_mix", "tax rate": "consensus_beat_mix", "effective_tax_rate": "consensus_beat_mix", "effective tax rate": "consensus_beat_mix", "tax_rate_and_effective_leverage": "consensus_beat_mix", "effective_leverage": "consensus_beat_mix", "below_the_line": "consensus_beat_mix", "below the line": "consensus_beat_mix", "earnings_quality": "consensus_beat_mix", "earnings quality": "consensus_beat_mix", # → guidance_dynamics "guidance": "guidance_dynamics", "guidance dynamics": "guidance_dynamics", "outlook": "guidance_dynamics", "forward_guidance": "guidance_dynamics", "guidance_quality": "guidance_dynamics", # → narrative_vs_numbers "narrative": "narrative_vs_numbers", "tone": "narrative_vs_numbers", "language": "narrative_vs_numbers", "narrative vs numbers": "narrative_vs_numbers", "narrative_tone": "narrative_vs_numbers", "management_tone": "narrative_vs_numbers", "management tone": "narrative_vs_numbers", # → segment_mix "segment": "segment_mix", "mix": "segment_mix", "revenue mix": "segment_mix", "revenue_mix": "segment_mix", "segment dynamics": "segment_mix", "product_mix": "segment_mix", "product mix": "segment_mix", # → capital_allocation "capital": "capital_allocation", "capex": "capital_allocation", "capital allocation": "capital_allocation", "buyback": "capital_allocation", "dividend": "capital_allocation", "cash_return": "capital_allocation", "cash return": "capital_allocation", "buybacks": "capital_allocation", } # --------------------------------------------------------------------------- # Between-the-lines signal type helpers # --------------------------------------------------------------------------- _CANONICAL_SIGNAL_TYPES = { "language_drift", "qa_evasion", "omission", "emphasis_shift", "accounting_quality" } _SIGNAL_TYPE_ALIASES: dict[str, str] = { # → language_drift "language drift": "language_drift", "tone_drift": "language_drift", "tone drift": "language_drift", "drift": "language_drift", "language_shift": "language_drift", "wording_shift": "language_drift", # → qa_evasion "evasion": "qa_evasion", "q&a evasion": "qa_evasion", "qa evasion": "qa_evasion", "pivot": "qa_evasion", "deflection": "qa_evasion", "non_answer": "qa_evasion", "management evasion": "qa_evasion", # → omission "silence": "omission", "missing topic": "omission", "absent": "omission", # → emphasis_shift "emphasis shift": "emphasis_shift", "kpi_dropped": "emphasis_shift", "kpi dropped": "emphasis_shift", "de_emphasis": "emphasis_shift", "deemphasis": "emphasis_shift", # → accounting_quality "accounting quality": "accounting_quality", "beat_quality": "accounting_quality", "beat quality": "accounting_quality", "earnings_quality": "accounting_quality", } def _normalize_assessment(v: object) -> object: """Coerce dirty LLM assessment strings to 'positive' | 'neutral' | 'concerning'. Falls back to 'neutral' for genuinely unknown values. """ if not isinstance(v, str): return v s = v.strip() if s in _CANONICAL_ASSESSMENTS: return s mapped = _ASSESSMENT_ALIASES.get(s.lower()) if mapped: print(f"[quality-assessment] coercing '{s}' to '{mapped}'", file=sys.stderr) return mapped print(f"[quality-assessment] unknown assessment '{s}', falling back to 'neutral'", file=sys.stderr) return "neutral" def _normalize_dimension(v: object) -> object: """Coerce dirty LLM dimension strings to a canonical snake_case value. Unknown strings pass through unchanged so the per-item Literal still rejects them — the brief-level sanitizer (_sanitize_quality_signals) will then drop the item before validation attempts to parse it. """ if not isinstance(v, str): return v s = v.strip() if s in _CANONICAL_DIMENSIONS: return s # Normalise spaces/hyphens to underscores, lowercase key = s.lower().replace("-", "_").replace(" ", "_") if key in _CANONICAL_DIMENSIONS: print(f"[quality-dimension] coercing '{s}' to '{key}'", file=sys.stderr) return key mapped = _DIMENSION_ALIASES.get(s.lower()) or _DIMENSION_ALIASES.get(key) if mapped: print(f"[quality-dimension] coercing '{s}' to '{mapped}'", file=sys.stderr) return mapped return v # let Literal validation reject it; brief-level validator drops the item # --------------------------------------------------------------------------- # Analytical tension helpers # --------------------------------------------------------------------------- _CANONICAL_WEIGHTS = {"material", "watch", "minor"} _WEIGHT_ALIASES: dict[str, str] = { # → material "high": "material", "critical": "material", "major": "material", "significant": "material", "important": "material", "serious": "material", "actionable": "material", # → watch "medium": "watch", "moderate": "watch", "monitor": "watch", "watchlist": "watch", "worth monitoring": "watch", "watchable": "watch", # → minor "low": "minor", "small": "minor", "trivial": "minor", "minimal": "minor", "negligible": "minor", "immaterial": "minor", } def _normalize_weight(v: object) -> object: """Coerce dirty LLM tension-weight strings to 'material' | 'watch' | 'minor'. Falls back to 'watch' for genuinely unknown values. """ if not isinstance(v, str): return v s = v.strip() if s in _CANONICAL_WEIGHTS: return s mapped = _WEIGHT_ALIASES.get(s.lower()) if mapped: print(f"[tension-weight] coercing '{s}' to '{mapped}'", file=sys.stderr) return mapped print(f"[tension-weight] unknown weight '{s}', falling back to 'watch'", file=sys.stderr) return "watch" # --------------------------------------------------------------------------- # Guidance verdict helpers # --------------------------------------------------------------------------- _CANONICAL_VERDICTS = {"beat", "in-line", "missed", "pending"} _VERDICT_ALIASES: dict[str, str] = { # → beat "beats": "beat", "above": "beat", "exceeded": "beat", "ahead": "beat", "outperformed": "beat", "strong beat": "beat", "topped": "beat", # → in-line "inline": "in-line", "in_line": "in-line", "met": "in-line", "on track": "in-line", "on_track": "in-line", "in line": "in-line", "on-track": "in-line", "as expected": "in-line", "in-line with": "in-line", # → missed "miss": "missed", "below": "missed", "shortfall": "missed", "fell short": "missed", "disappointed": "missed", "undershoot": "missed", # → pending "tbd": "pending", "n/a": "pending", "na": "pending", "not yet": "pending", "upcoming": "pending", "awaited": "pending", } def _normalize_verdict(v: object) -> object: """Coerce dirty LLM verdict strings to 'beat' | 'in-line' | 'missed' | 'pending'. Returns None for genuinely unknown values (field is Optional). """ if v is None: return v if not isinstance(v, str): return v s = v.strip() if s in _CANONICAL_VERDICTS: return s mapped = _VERDICT_ALIASES.get(s.lower()) if mapped: print(f"[guidance-verdict] coercing '{s}' to '{mapped}'", file=sys.stderr) return mapped print(f"[guidance-verdict] unknown verdict '{s}', setting to None", file=sys.stderr) return None # --------------------------------------------------------------------------- # Sentiment score / label helpers # --------------------------------------------------------------------------- _CANONICAL_SCORES = {-2, -1, 0, 1, 2} _CANONICAL_LABELS = { "Strongly Bearish", "Bearish", "Neutral", "Bullish", "Strongly Bullish", } _LABEL_ALIASES: dict[str, str] = { # → Strongly Bullish "strongly bullish": "Strongly Bullish", "very bullish": "Strongly Bullish", "strongly positive": "Strongly Bullish", "very positive": "Strongly Bullish", # → Bullish "bullish": "Bullish", "positive": "Bullish", "favorable": "Bullish", "optimistic": "Bullish", "constructive": "Bullish", # → Neutral "neutral": "Neutral", "mixed": "Neutral", "balanced": "Neutral", "flat": "Neutral", "unchanged": "Neutral", # → Bearish "bearish": "Bearish", "negative": "Bearish", "cautious": "Bearish", "concerned": "Bearish", "pessimistic": "Bearish", # → Strongly Bearish "strongly bearish": "Strongly Bearish", "very bearish": "Strongly Bearish", "strongly negative": "Strongly Bearish", "very negative": "Strongly Bearish", } def _coerce_score(v: object) -> object: """Coerce dirty LLM score to int in {-2,-1,0,1,2}. Accepts str like '+1', '-2', '1'; float like 1.0; int. Clamps out-of-range to nearest bound. Non-numeric → None (field is Optional). """ if v is None: return v if isinstance(v, int) and not isinstance(v, bool): clamped = max(-2, min(2, v)) if clamped != v: print(f"[sentiment-score] clamping {v} to {clamped}", file=sys.stderr) return clamped if isinstance(v, float): rounded = round(v) clamped = max(-2, min(2, rounded)) if clamped != v: print(f"[sentiment-score] coercing float {v} to {clamped}", file=sys.stderr) return clamped if isinstance(v, str): s = v.strip().lstrip("+") try: as_int = int(s) clamped = max(-2, min(2, as_int)) if clamped != as_int: print(f"[sentiment-score] clamping {as_int} to {clamped}", file=sys.stderr) else: print(f"[sentiment-score] coercing string '{v}' to {clamped}", file=sys.stderr) return clamped except ValueError: pass print(f"[sentiment-score] unrecognised score '{v}', setting to None", file=sys.stderr) return None def _coerce_sentiment_label(v: object) -> object: """Coerce dirty LLM sentiment label to a canonical value or None. Accepts case variants and synonyms; unknown → None (field is Optional). """ if v is None: return v if not isinstance(v, str): return v s = v.strip() if s in _CANONICAL_LABELS: return s mapped = _LABEL_ALIASES.get(s.lower()) if mapped: print(f"[sentiment-label] coercing '{s}' to '{mapped}'", file=sys.stderr) return mapped print(f"[sentiment-label] unknown label '{s}', setting to None", file=sys.stderr) return None def _normalize_signal_type(v: object) -> object: """Coerce dirty LLM signal_type strings to a canonical value. Falls back to 'language_drift' for genuinely unknown values. """ if not isinstance(v, str): return v s = v.strip().lower() # lowercase first — canonicals are all lowercase if s in _CANONICAL_SIGNAL_TYPES: return s mapped = _SIGNAL_TYPE_ALIASES.get(s) # s is already lowercased if mapped: print(f"[signal-type] coercing '{v}' to '{mapped}'", file=sys.stderr) return mapped print(f"[signal-type] unknown signal_type '{v}', falling back to 'language_drift'", file=sys.stderr) return "language_drift" def _drop_invalid_list_items( items: object, model_cls: type[BaseModel], field_name: str, ) -> object: """Validate LLM-produced list items independently and quarantine failures.""" if not isinstance(items, list): return items kept = [] for item in items: try: kept.append(model_cls.model_validate(item)) except Exception as exc: first_line = str(exc).splitlines()[0] if str(exc) else type(exc).__name__ print( f"[brief-sanitizer] dropping invalid {field_name} item: {first_line}", file=sys.stderr, ) return kept class SourcedFact(BaseModel): model_config = ConfigDict(extra="ignore") text: str = Field(description="The factual claim, one sentence.") source: EvidenceSource = Field( description="Document type the claim is drawn from." ) reliability: Literal["HIGH", "MEDIUM", "LOW"] = Field( description="HIGH for SEC filings, MEDIUM for transcripts, LOW for news." ) impact: Optional[Literal["HIGH", "MEDIUM", "LOW"]] = Field( default=None, description="Materiality for the investment thesis. HIGH = thesis-shifting (guidance ≥5%, beat/miss ≥10%, major M&A, regulatory action, strategic pivot); MEDIUM = material but confirmatory; LOW = context or supporting detail.", ) evidence_snippet: str = Field( description="A literal quote (≤30 words) from the cited source that directly supports the claim. Must appear verbatim in retrieved tool output." ) evidence_ref: Optional[EvidenceRef] = Field( default=None, description="Exact reference copied from the supporting evidence.v1 record.", ) verification_status: VerificationStatus = Field( default="UNVERIFIED", description="Set to VERIFIED only by deterministic post-synthesis verification.", ) verification_reason: Optional[str] = Field(default=None) @field_validator("source", mode="before") @classmethod def _coerce_source(cls, v: object) -> object: return _normalize_source(v) @field_validator('evidence_snippet') @classmethod def validate_snippet_length(cls, v: str) -> str: words = v.split() if len(words) > 30: return " ".join(words[:30]) return v class TrendPoint(BaseModel): model_config = ConfigDict(extra="ignore") period: str = Field(description="Reporting period label, e.g. 'Q3 2025' or 'FY 2024'.") revenue_bn: Optional[float] = Field(default=None, description="Revenue in billions USD.") revenue_yoy_pct: Optional[float] = Field(default=None, description="YoY revenue growth %.") operating_margin: Optional[float] = Field(default=None, description="Operating margin as decimal (0.25 = 25%).") eps: Optional[float] = Field(default=None, description="Diluted EPS.") class MDASection(BaseModel): model_config = ConfigDict(extra="ignore") drivers: list[SourcedFact] = Field( default_factory=list, description="2-4 key revenue or margin drivers cited in the MD&A or transcript." ) headwinds: list[SourcedFact] = Field( default_factory=list, description="1-3 headwinds or drags on performance cited in the MD&A or transcript." ) language_shift: str = Field( default="", description="1-2 sentences on how management language evolved vs prior periods: more confident, more cautious, more defensive? If cross-period data is unavailable, state so explicitly." ) key_quote: Optional[SourcedFact] = Field( default=None, description="The single most revealing management statement from the filing or transcript this period." ) @model_validator(mode="before") @classmethod def _drop_invalid_fact_items(cls, data: object) -> object: if not isinstance(data, dict): return data sanitized = dict(data) for field_name in ("drivers", "headwinds"): if field_name in sanitized: sanitized[field_name] = _drop_invalid_list_items( sanitized[field_name], SourcedFact, field_name ) return sanitized class CategorizedRisk(BaseModel): model_config = ConfigDict(extra="ignore") category: Literal["Regulatory", "Operational", "Competitive", "Financial", "Macro", "Demand", "Geopolitical"] = Field( description="Risk category." ) text: str = Field(description="The risk, 1-2 sentences grounded in filing language.") source: EvidenceSource = Field( description="Document type where this risk was cited." ) reliability: Literal["HIGH", "MEDIUM", "LOW"] = Field( description="HIGH for SEC filings, MEDIUM for transcripts, LOW for news." ) impact: Optional[Literal["HIGH", "MEDIUM", "LOW"]] = Field( default=None, description="Materiality for the investment thesis. HIGH = could materially impair earnings, revenue, or operations; MEDIUM = notable headwind; LOW = background/standard risk disclosure.", ) is_new_this_filing: bool = Field( description="True if this risk appears new or materially escalated vs prior filing." ) evidence_snippet: str = Field(default="", description="Verbatim excerpt supporting the risk.") evidence_ref: Optional[EvidenceRef] = None verification_status: VerificationStatus = "UNVERIFIED" verification_reason: Optional[str] = None @field_validator("source", mode="before") @classmethod def _coerce_source(cls, v: object) -> object: return _normalize_source(v) @field_validator("category", mode="before") @classmethod def _normalize_category(cls, v: object) -> str: normalized = str(v).strip().title() if normalized in _CANONICAL_CATEGORIES: return normalized if normalized in _CATEGORY_ALIASES: return _CATEGORY_ALIASES[normalized] print(f"[risk-category] coercing unknown category '{v}' to Operational", file=sys.stderr) return "Operational" class ManagementCommentaryTopic(BaseModel): model_config = ConfigDict(extra="ignore") topic: str = Field(description="Topic label, 2-5 words (e.g. 'iPhone demand', 'AI capex', 'margin guidance').") summary: str = Field(description="1-2 sentence summary of what management said about this topic.") source: Literal["10-K", "10-Q", "transcript"] = Field( description="Source document for this commentary item. Management commentary must come from filings or transcripts, not news." ) reliability: Literal["HIGH", "MEDIUM", "LOW"] = Field( description="HIGH for SEC filings, MEDIUM for transcripts." ) impact: Optional[Literal["HIGH", "MEDIUM", "LOW"]] = Field( default=None, description="Materiality for the investment thesis. HIGH = topic directly shapes earnings or valuation outlook; MEDIUM = important but secondary; LOW = routine commentary.", ) evidence_snippet: str = Field(description="Verbatim quote ≤30 words supporting this topic.") evidence_ref: Optional[EvidenceRef] = None verification_status: VerificationStatus = "UNVERIFIED" verification_reason: Optional[str] = None @field_validator("source", mode="before") @classmethod def _coerce_source(cls, v: object) -> object: return _normalize_source(v) @field_validator('evidence_snippet') @classmethod def _trim(cls, v: str) -> str: words = v.split() return " ".join(words[:30]) if len(words) > 30 else v class GuidancePoint(BaseModel): model_config = ConfigDict(extra="ignore") period: str = Field(description="The filing period when this guidance was given, e.g. 'Q1 2025'.") text: str = Field(description="The guidance statement, 1-2 sentences.") source: EvidenceSource = Field( description="Document type where this guidance appeared." ) @field_validator("source", mode="before") @classmethod def _coerce_source(cls, v: object) -> object: return _normalize_source(v) reliability: Optional[Literal["HIGH", "MEDIUM", "LOW"]] = Field( default=None, description="HIGH for SEC filings, MEDIUM for transcripts." ) evidence_snippet: str = Field(default="", description="Verbatim excerpt supporting the guidance.") evidence_ref: Optional[EvidenceRef] = None verification_status: VerificationStatus = "UNVERIFIED" verification_reason: Optional[str] = None impact: Optional[Literal["HIGH", "MEDIUM", "LOW"]] = Field( default=None, description="Materiality of this guidance for the thesis. HIGH = large guidance change (≥5% vs consensus or prior), new metric, policy shift; MEDIUM = in-line guidance update; LOW = reaffirmation of existing guidance.", ) metric_focus: Optional[str] = Field( default=None, description="Primary metric being guided on, e.g. 'Revenue', 'EPS', 'Operating margin', 'Capex'." ) actual_result: Optional[str] = Field( default=None, description="One-line summary of what was actually reported for the guided metric, e.g. 'Delivered $91.2B revenue, +2% vs guide midpoint'." ) verdict: Optional[Literal["beat", "in-line", "missed", "pending"]] = Field( default=None, description="Comparison outcome. Use 'pending' if actuals for the guided period are not yet available." ) @field_validator("verdict", mode="before") @classmethod def _coerce_verdict(cls, v: object) -> object: return _normalize_verdict(v) class SectionSentiment(BaseModel): model_config = ConfigDict(extra="ignore") score: Optional[Literal[-2, -1, 0, 1, 2]] = Field( default=None, description="Sentiment score: -2 strongly bearish, +2 strongly bullish." ) label: Optional[Literal[ "Strongly Bearish", "Bearish", "Neutral", "Bullish", "Strongly Bullish" ]] = None rationale: str = Field( max_length=400, description="One sentence (<=40 words) referencing evidence already cited in this brief." ) @field_validator("score", mode="before") @classmethod def _coerce_score_field(cls, v: object) -> object: return _coerce_score(v) @field_validator("label", mode="before") @classmethod def _coerce_label_field(cls, v: object) -> object: return _coerce_sentiment_label(v) class SentimentScores(BaseModel): model_config = ConfigDict(extra="ignore") metrics: Optional[SectionSentiment] = None mda: Optional[SectionSentiment] = None earnings_call: Optional[SectionSentiment] = None guidance: Optional[SectionSentiment] = None news: Optional[SectionSentiment] = None class MarketExpectations(BaseModel): model_config = ConfigDict(extra="ignore") consensus_eps_est: Optional[float] = Field( default=None, description="Analyst consensus EPS estimate for the current/next quarter." ) consensus_rev_est_bn: Optional[float] = Field( default=None, description="Analyst consensus revenue estimate in USD billions." ) revision_30d_pct: Optional[float] = Field( default=None, description="Percent change in average EPS estimate over the past 30 days. Positive = upward revisions (bullish momentum), negative = downward (bearish).", ) target_period: Optional[str] = None as_of: Optional[str] = None period_aligned: bool = False comparison_allowed: bool = False alignment_status: str = "UNVERIFIED" d1_price_reaction_pct: Optional[float] = Field( default=None, description="One-day percent price change after the most recent earnings release." ) d5_price_reaction_pct: Optional[float] = Field( default=None, description="Five-day percent price change after the most recent earnings release." ) since_release_price_reaction_pct: Optional[float] = Field( default=None, description="Percent price change from the earnings release close to the latest available close (as of report generation).", ) event_date: Optional[str] = None event_kind: str = "unknown" event_timing: str = "unknown" event_aligned: bool = False event_comparison_allowed: bool = False price_alignment_status: str = "UNVERIFIED" evidence_ref: Optional[EvidenceRef] = None rationale: str = Field( description="One sentence comparing reported results vs. expectations and market reception, e.g. 'Beat EPS by 4% and stock rallied 3.2% next day; estimates revised +1.8% over 30d.'" ) @model_validator(mode="after") def quarantine_unaligned_values(self): # Alignment is not self-attested by the model. A structured analyst # reference is required before the deterministic verifier can allow it. if self.evidence_ref is None: self.period_aligned = False self.comparison_allowed = False self.event_aligned = False self.event_comparison_allowed = False if not (self.period_aligned is True and self.comparison_allowed is True): self.consensus_eps_est = None self.consensus_rev_est_bn = None self.revision_30d_pct = None if not (self.event_aligned is True and self.event_comparison_allowed is True): self.d1_price_reaction_pct = None self.d5_price_reaction_pct = None self.since_release_price_reaction_pct = None return self class AnalyticalTension(BaseModel): model_config = ConfigDict(extra="ignore") headline: str = Field( description="One sentence naming the tension — what looks good on the surface vs what the deeper read reveals." ) bullish_reading: str = Field(description="The optimistic interpretation of the surface data.") bearish_reading: str = Field(description="What cross-referencing two data points suggests as a concern or caveat.") weight: Literal["material", "watch", "minor"] = Field( description="material = actionable tension, watch = worth monitoring, minor = low-conviction." ) bullish_evidence: SourcedFact = Field(description="The fact supporting the bullish reading.") bearish_evidence: SourcedFact = Field(description="The fact supporting the bearish reading.") @field_validator("weight", mode="before") @classmethod def _coerce_weight(cls, v: object) -> object: return _normalize_weight(v) class EarningsQualitySignal(BaseModel): model_config = ConfigDict(extra="ignore") dimension: Literal[ "consensus_beat_mix", "guidance_dynamics", "narrative_vs_numbers", "segment_mix", "capital_allocation", ] = Field(description="Which dimension of earnings quality this signal covers.") assessment: Literal["positive", "neutral", "concerning"] = Field( description="Direction of the signal." ) rationale: str = Field( description="One sentence explaining the assessment, grounded in retrieved evidence." ) evidence: SourcedFact = Field(description="The supporting SourcedFact.") @field_validator("dimension", mode="before") @classmethod def _coerce_dimension(cls, v: object) -> object: return _normalize_dimension(v) @field_validator("assessment", mode="before") @classmethod def _coerce_assessment(cls, v: object) -> object: return _normalize_assessment(v) class SubtextRead(BaseModel): """A 'reading between the lines' item: surface observation → expert subtext → implication.""" model_config = ConfigDict(extra="ignore") observation: str = Field( description="The surface signal — what is literally said, present, or notably absent." ) reading: str = Field( description="The expert interpretation: what this signals, what it conceals, or what it implies." ) signal_type: Literal[ "language_drift", "qa_evasion", "omission", "emphasis_shift", "accounting_quality" ] = Field( description=( "Type of between-the-lines signal: " "language_drift = wording/tone shift vs prior period; " "qa_evasion = management pivoted or refused to quantify in Q&A; " "omission = filing is silent on a known headwind; " "emphasis_shift = KPI or metric de-emphasized or dropped; " "accounting_quality = earnings quality concern (beat mix, accruals, etc.)." ) ) implication: str = Field( description="The concrete consequence or forward-looking thing to monitor." ) evidence: SourcedFact = Field( description="Verbatim evidence anchoring the observation." ) @field_validator("signal_type", mode="before") @classmethod def _coerce_signal_type(cls, v: object) -> object: return _normalize_signal_type(v) class BriefOutput(BaseModel): model_config = ConfigDict(extra="ignore") ticker: str = Field(description="Exchange ticker symbol, uppercase.") company_name: str = Field(description="Full legal company name.") filing_date: str = Field(description="Date of the most recent filing, YYYY-MM-DD.") status: Literal["COMPLETE", "PARTIAL"] = "COMPLETE" schema_version: str = "brief.v2" generated_at: Optional[str] = None data_as_of: Optional[str] = None verification_report: dict[str, Any] = Field(default_factory=dict) display_policy: dict[str, bool] = Field(default_factory=dict) company_profile: Optional[dict[str, Any]] = Field( default=None, description=( "Company Overview section (identity, business_lines, geographic_exposures, " "strategic_changes, attention_themes, watch_variables) produced in the same " "synthesis call. Validated separately against CompanyProfileSection." ), ) what_matters_most: str = Field( description="2-3 sentence AI synthesis of the single most important theme. This is the only interpretation field." ) non_obvious_takeaway: str = Field( default="", description="1-2 sentences: the single thing most readers will miss. Concrete, grounded in cross-referenced evidence. Never a restatement of bull/bear points." ) analytical_tensions: list[AnalyticalTension] = Field( default_factory=list, description="0-3 material tensions where surface reading and deep reading disagree. EMPTY LIST IS VALID — never manufacture tension." ) between_the_lines: list[SubtextRead] = Field( default_factory=list, description=( "0-3 expert 'between the lines' readings — surface observation + subtext + implication. " "EMPTY LIST IS VALID — never manufacture readings. Only emit items anchored to a " "precomputed edge signal or a verbatim cross-referenced evidence snippet." ) ) earnings_quality_signals: list[EarningsQualitySignal] = Field( default_factory=list, description="2-5 earnings quality signals across distinct dimensions. Use evidence already retrieved." ) standout_number: Optional[SourcedFact] = Field( default=None, description="The single most remarkable quantitative fact this quarter — the number a journalist would lead with." ) what_changed: list[SourcedFact] = Field( description="3-6 facts about material changes vs prior period. Facts only, no interpretation." ) bull_points: list[SourcedFact] = Field(description="3-5 facts supporting a positive view, grounded in evidence.") bear_points: list[SourcedFact] = Field(description="3-5 facts supporting a cautious view, grounded in evidence.") what_to_watch: list[str] = Field(description="3-5 upcoming catalysts, metrics, or events to monitor.") trends: list[TrendPoint] = Field( description="Last 4-6 quarters of headline metrics, chronologically oldest to newest." ) evidence_notes: list[str] = Field( default_factory=list, description="Cross-source conflicts or corroborations. Max 3." ) evidence_coverage: dict[str, Any] = Field( default_factory=dict, description="Deterministic verification coverage populated after synthesis.", ) mda_summary: MDASection = Field( default_factory=MDASection, description="Structured MD&A analysis from the 10-K/10-Q.", ) risks_categorized: list[CategorizedRisk] = Field(description="3-6 categorized risks from the filing.") management_commentary: list[ManagementCommentaryTopic] = Field( description="3-5 key management themes drawn from MD&A (preferred) or earnings call transcript." ) guidance_history: list[GuidancePoint] = Field( description="Forward guidance statements extracted from all available periods, most recent first." ) sentiment: Optional[SentimentScores] = Field( default=None, description="Per-section sentiment scores (-2..+2). Section is null if no evidence was retrieved for it." ) market_expectations: Optional[MarketExpectations] = Field( default=None, description="Analyst consensus, 30-day estimate revisions, and post-earnings price reaction. Set to null if no analyst data available." ) # Analyst Edge fields — populated deterministically by analysis/ modules and # attached by post_synthesis.attach_edge_signals after LLM synthesis. # Always present (empty list = no signals computed), never synthesized by LLM. quarter_deltas: list[QuarterDelta] = Field( default_factory=list, description="Verbatim text deltas computed deterministically across consecutive filing periods. Populated by code, not LLM.", ) @model_validator(mode="before") @classmethod def _sanitize_llm_fields(cls, data: object) -> object: if not isinstance(data, dict): return data sanitized = dict(data) list_fields: dict[str, type[BaseModel]] = { "what_changed": SourcedFact, "bull_points": SourcedFact, "bear_points": SourcedFact, "risks_categorized": CategorizedRisk, "guidance_history": GuidancePoint, "analytical_tensions": AnalyticalTension, "between_the_lines": SubtextRead, "trends": TrendPoint, "quarter_deltas": QuarterDelta, } for field_name, model_cls in list_fields.items(): if field_name in sanitized: sanitized[field_name] = _drop_invalid_list_items( sanitized[field_name], model_cls, field_name ) singleton_fields: tuple[tuple[str, type[BaseModel], object], ...] = ( ("standout_number", SourcedFact, None), ("sentiment", SentimentScores, None), ("market_expectations", MarketExpectations, None), ("mda_summary", MDASection, {}), ) for field_name, model_cls, fallback in singleton_fields: if field_name not in sanitized: continue value = sanitized[field_name] if value is None and fallback is None: continue try: sanitized[field_name] = model_cls.model_validate(value) except Exception as exc: first_line = str(exc).splitlines()[0] if str(exc) else type(exc).__name__ print( f"[brief-sanitizer] replacing invalid {field_name}: {first_line}", file=sys.stderr, ) sanitized[field_name] = fallback return sanitized @field_validator("status", mode="before") @classmethod def _normalize_status(cls, v: object) -> object: if isinstance(v, str) and v in {"COMPLETE", "PARTIAL"}: return v print( f"[brief-status] unknown status '{v}', falling back to 'PARTIAL'", file=sys.stderr, ) return "PARTIAL" @field_validator("earnings_quality_signals", mode="before") @classmethod def _sanitize_quality_signals(cls, v: object) -> object: """Drop EarningsQualitySignal items whose dimension cannot be coerced to a canonical value. Fires before item-level parsing; unknown dims that slip through _normalize_dimension as-is are caught here so the rest of the brief still validates. earnings_quality_signals has default_factory=list, so an empty result is valid. """ if not isinstance(v, list): return v kept = [] for item in v: if not isinstance(item, dict): kept.append(item) continue dim = _normalize_dimension(item.get("dimension")) if dim not in _CANONICAL_DIMENSIONS: print( f"[quality-dimension] dropping item with unknown dimension " f"'{item.get('dimension', '?')}'", file=sys.stderr, ) continue kept.append(item) return kept @field_validator("management_commentary", mode="before") @classmethod def _drop_news_commentary(cls, v: object) -> object: """Management commentary must be filing/transcript-sourced. The synthesis LLM occasionally mis-tags items as 'news'; drop those rather than failing the whole brief (the per-item Literal still rejects 'news' for anything that slips through). """ if not isinstance(v, list): return v kept = [] for item in v: if isinstance(item, dict) and _normalize_source(item.get("source")) == "news": print( f"[mgmt-commentary] dropping news-sourced item '{item.get('topic', '?')}'", file=sys.stderr, ) continue kept.append(item) return kept