Spaces:
Running
Running
| """The Pydantic-enforced brief — where hallucinated numbers become impossible. | |
| The writer agent *assembles*; it never computes. That guarantee is enforced by | |
| the schema itself, not by asking nicely in a prompt: | |
| 1. **Every number is a claim.** Narrative prose may not contain a bare digit. A | |
| figure is written as a placeholder token ``{{c3}}`` that references a | |
| :class:`NumericClaim`. Pydantic rejects the brief outright if prose contains a | |
| digit outside a claim reference. | |
| 2. **Every claim is recomputed.** The deterministic verifier recalculates each | |
| claim's value from the raw price bars in state and compares to the cent. | |
| 3. **Every quotation is matched.** Fields holding third-party text (headlines) | |
| must appear verbatim in the news actually retrieved for that ticker. | |
| So the writer can only choose *which* verified figure to cite and *how to phrase* | |
| the surrounding sentence. It cannot type a number into the brief at all. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import Annotated, Literal | |
| from pydantic import BaseModel, ConfigDict, Field, field_validator | |
| from app.models.market import METRIC_UNITS, MetricUnit | |
| #: A reference to a NumericClaim inside narrative prose. | |
| CLAIM_REF_PATTERN = re.compile(r"\{\{(c\d+)\}\}") | |
| CLAIM_ID_PATTERN = re.compile(r"^c\d+$") | |
| ClaimId = Annotated[str, Field(pattern=r"^c\d+$")] | |
| def extract_claim_refs(text: str) -> list[str]: | |
| """All claim ids referenced by `text`, in order of appearance.""" | |
| return CLAIM_REF_PATTERN.findall(text or "") | |
| def strip_claim_refs(text: str) -> str: | |
| """`text` with every ``{{cN}}`` token removed.""" | |
| return CLAIM_REF_PATTERN.sub(" ", text or "") | |
| def validate_narrative(text: str, field_name: str) -> str: | |
| """Reject prose containing a numeral outside a claim reference.""" | |
| residue = strip_claim_refs(text) | |
| offenders = sorted({ch for ch in residue if ch.isdigit()}) | |
| if offenders: | |
| raise ValueError( | |
| f"{field_name} contains bare numerals {offenders!r}. Every figure must be a " | |
| f"verified claim reference such as {{{{c1}}}} — the writer performs no arithmetic." | |
| ) | |
| return text | |
| class NumericClaim(BaseModel): | |
| """One number in the brief, traceable to a tool-computed metric.""" | |
| model_config = ConfigDict(extra="forbid") | |
| claim_id: ClaimId | |
| ticker: str | |
| metric: str | |
| value: float | |
| unit: MetricUnit | |
| def _known_metric(cls, value: str) -> str: | |
| if value not in METRIC_UNITS: | |
| raise ValueError(f"unknown metric '{value}'; must be one of {sorted(METRIC_UNITS)}") | |
| return value | |
| def _upper(cls, value: str) -> str: | |
| return value.strip().upper() | |
| RowStatus = Literal["ok", "partial", "unavailable"] | |
| class SnapshotRow(BaseModel): | |
| """One row of the snapshot table. All cells are claim references.""" | |
| model_config = ConfigDict(extra="forbid") | |
| ticker: str | |
| company: str | None = None | |
| status: RowStatus = "ok" | |
| last_close: ClaimId | None = None | |
| change_1d: ClaimId | None = None | |
| return_30d: ClaimId | None = None | |
| volatility: ClaimId | None = None | |
| max_drawdown: ClaimId | None = None | |
| pe_ratio: ClaimId | None = None | |
| sentiment: ClaimId | None = None | |
| note: str | None = None | |
| def _note_narrative(cls, value: str | None) -> str | None: | |
| return validate_narrative(value, "SnapshotRow.note") if value else value | |
| def claim_ids(self) -> list[str]: | |
| return [ | |
| claim | |
| for claim in ( | |
| self.last_close, | |
| self.change_1d, | |
| self.return_30d, | |
| self.volatility, | |
| self.max_drawdown, | |
| self.pe_ratio, | |
| self.sentiment, | |
| ) | |
| if claim | |
| ] | |
| class KeyMove(BaseModel): | |
| """A notable movement, described in prose with claim references.""" | |
| model_config = ConfigDict(extra="forbid") | |
| ticker: str | |
| narrative: str | |
| direction: Literal["up", "down", "flat"] | |
| def _narrative(cls, value: str) -> str: | |
| return validate_narrative(value, "KeyMove.narrative") | |
| class NewsAndSentiment(BaseModel): | |
| """Per-ticker news read-through.""" | |
| model_config = ConfigDict(extra="forbid") | |
| ticker: str | |
| summary: str | |
| sentiment: ClaimId | None = None | |
| #: Must appear verbatim in the headlines retrieved for this ticker. | |
| top_headline: str | None = None | |
| headline_source: str | None = None | |
| def _summary(cls, value: str) -> str: | |
| return validate_narrative(value, "NewsAndSentiment.summary") | |
| class RiskFlag(BaseModel): | |
| """A flagged risk. `evidence` is a verbatim retrieved headline.""" | |
| model_config = ConfigDict(extra="forbid") | |
| ticker: str | |
| category: str | |
| #: Verbatim third-party text; matched against retrieved headlines. | |
| evidence: str | |
| assessment: str | |
| def _assessment(cls, value: str) -> str: | |
| return validate_narrative(value, "RiskFlag.assessment") | |
| class WatchItem(BaseModel): | |
| """Something to watch next session.""" | |
| model_config = ConfigDict(extra="forbid") | |
| ticker: str | None = None | |
| item: str | |
| def _item(cls, value: str) -> str: | |
| return validate_narrative(value, "WatchItem.item") | |
| class Brief(BaseModel): | |
| """The complete morning brief.""" | |
| model_config = ConfigDict(extra="forbid") | |
| generated_for: str = Field(description="Session date, ISO-8601 (YYYY-MM-DD).") | |
| watchlist: list[str] | |
| headline: str | |
| executive_summary: str | |
| snapshot: list[SnapshotRow] = Field(default_factory=list) | |
| key_moves: list[KeyMove] = Field(default_factory=list) | |
| news_and_sentiment: list[NewsAndSentiment] = Field(default_factory=list) | |
| risk_flags: list[RiskFlag] = Field(default_factory=list) | |
| watch_items: list[WatchItem] = Field(default_factory=list) | |
| data_gaps: list[str] = Field(default_factory=list) | |
| claims: list[NumericClaim] = Field(default_factory=list) | |
| #: True when one or more tickers failed — the brief is explicitly partial. | |
| partial: bool = False | |
| def _narrative_fields(cls, value: str) -> str: | |
| return validate_narrative(value, "Brief narrative") | |
| def _gaps(cls, value: list[str]) -> list[str]: | |
| # Data gaps quote provider error strings, which may legitimately contain | |
| # numerals ("need at least 2 bars"), so they are exempt from the digit | |
| # rule — but they are matched against recorded run errors by the verifier. | |
| return value | |
| def claims_by_id(self) -> dict[str, NumericClaim]: | |
| return {claim.claim_id: claim for claim in self.claims} | |
| def referenced_claim_ids(self) -> set[str]: | |
| """Every claim id referenced anywhere in the brief.""" | |
| referenced: set[str] = set() | |
| for row in self.snapshot: | |
| referenced.update(row.claim_ids()) | |
| for move in self.key_moves: | |
| referenced.update(extract_claim_refs(move.narrative)) | |
| for entry in self.news_and_sentiment: | |
| if entry.sentiment: | |
| referenced.add(entry.sentiment) | |
| referenced.update(extract_claim_refs(entry.summary)) | |
| for flag in self.risk_flags: | |
| referenced.update(extract_claim_refs(flag.assessment)) | |
| for item in self.watch_items: | |
| referenced.update(extract_claim_refs(item.item)) | |
| referenced.update(extract_claim_refs(self.headline)) | |
| referenced.update(extract_claim_refs(self.executive_summary)) | |
| for row in self.snapshot: | |
| if row.note: | |
| referenced.update(extract_claim_refs(row.note)) | |
| return referenced | |
| def quoted_texts(self) -> list[tuple[str, str, str]]: | |
| """(ticker, field, verbatim text) for every quoted third-party string.""" | |
| quotes: list[tuple[str, str, str]] = [] | |
| for entry in self.news_and_sentiment: | |
| if entry.top_headline: | |
| quotes.append((entry.ticker, "news_and_sentiment.top_headline", entry.top_headline)) | |
| for flag in self.risk_flags: | |
| quotes.append((flag.ticker, "risk_flags.evidence", flag.evidence)) | |
| return quotes | |