| 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", |
| } |
|
|
| |
| |
| |
|
|
| _CANONICAL_ASSESSMENTS = {"positive", "neutral", "concerning"} |
|
|
| _ASSESSMENT_ALIASES: dict[str, str] = { |
| |
| "negative": "concerning", "bearish": "concerning", "concerned": "concerning", |
| "caution": "concerning", "cautious": "concerning", "weak": "concerning", |
| "poor": "concerning", "worrying": "concerning", "worrisome": "concerning", |
| "alarming": "concerning", "deteriorating": "concerning", |
| |
| "bullish": "positive", "strong": "positive", "good": "positive", |
| "favorable": "positive", "favourable": "positive", "encouraging": "positive", |
| "improving": "positive", "healthy": "positive", "solid": "positive", |
| |
| "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] = { |
| |
| "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", |
| |
| "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": "guidance_dynamics", "guidance dynamics": "guidance_dynamics", |
| "outlook": "guidance_dynamics", "forward_guidance": "guidance_dynamics", |
| "guidance_quality": "guidance_dynamics", |
| |
| "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": "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": "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", |
| } |
|
|
| |
| |
| |
|
|
| _CANONICAL_SIGNAL_TYPES = { |
| "language_drift", "qa_evasion", "omission", |
| "emphasis_shift", "accounting_quality" |
| } |
|
|
| _SIGNAL_TYPE_ALIASES: dict[str, str] = { |
| |
| "language drift": "language_drift", |
| "tone_drift": "language_drift", |
| "tone drift": "language_drift", |
| "drift": "language_drift", |
| "language_shift": "language_drift", |
| "wording_shift": "language_drift", |
| |
| "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", |
| |
| "silence": "omission", |
| "missing topic": "omission", |
| "absent": "omission", |
| |
| "emphasis shift": "emphasis_shift", |
| "kpi_dropped": "emphasis_shift", |
| "kpi dropped": "emphasis_shift", |
| "de_emphasis": "emphasis_shift", |
| "deemphasis": "emphasis_shift", |
| |
| "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 |
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| _CANONICAL_WEIGHTS = {"material", "watch", "minor"} |
|
|
| _WEIGHT_ALIASES: dict[str, str] = { |
| |
| "high": "material", "critical": "material", "major": "material", |
| "significant": "material", "important": "material", "serious": "material", |
| "actionable": "material", |
| |
| "medium": "watch", "moderate": "watch", "monitor": "watch", |
| "watchlist": "watch", "worth monitoring": "watch", "watchable": "watch", |
| |
| "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" |
|
|
|
|
| |
| |
| |
|
|
| _CANONICAL_VERDICTS = {"beat", "in-line", "missed", "pending"} |
|
|
| _VERDICT_ALIASES: dict[str, str] = { |
| |
| "beats": "beat", "above": "beat", "exceeded": "beat", "ahead": "beat", |
| "outperformed": "beat", "strong beat": "beat", "topped": "beat", |
| |
| "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", |
| |
| "miss": "missed", "below": "missed", "shortfall": "missed", |
| "fell short": "missed", "disappointed": "missed", "undershoot": "missed", |
| |
| "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 |
|
|
|
|
| |
| |
| |
|
|
| _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", "very bullish": "Strongly Bullish", |
| "strongly positive": "Strongly Bullish", "very positive": "Strongly Bullish", |
| |
| "bullish": "Bullish", "positive": "Bullish", "favorable": "Bullish", |
| "optimistic": "Bullish", "constructive": "Bullish", |
| |
| "neutral": "Neutral", "mixed": "Neutral", "balanced": "Neutral", |
| "flat": "Neutral", "unchanged": "Neutral", |
| |
| "bearish": "Bearish", "negative": "Bearish", "cautious": "Bearish", |
| "concerned": "Bearish", "pessimistic": "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() |
| if s in _CANONICAL_SIGNAL_TYPES: |
| return s |
| mapped = _SIGNAL_TYPE_ALIASES.get(s) |
| 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): |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| 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 |
|
|