amplegest / agent /schemas.py
Viney's picture
fix: per-item drop of invalid brief facts; relax LLM-produced singleton fields
b6b4740
Raw
History Blame Contribute Delete
41.5 kB
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