amplegest / ingestion /guidance_parser.py
Viney's picture
deploy: Primer app for HF Spaces — clean orphan history
35676b4
Raw
History Blame Contribute Delete
4.7 kB
"""Parse forward-looking guidance from MD&A and earnings call transcripts.
Uses Claude Haiku to extract structured low/high ranges per metric for the next
reporting period. Returns columns aligned with metrics_db._GUIDANCE_COLS.
"""
from __future__ import annotations
import json
import re
import sys
from typing import Optional
_EMPTY = {
"guidance_period": None,
"guidance_revenue_low": None, "guidance_revenue_high": None,
"guidance_eps_low": None, "guidance_eps_high": None,
"guidance_gross_margin_low": None, "guidance_gross_margin_high": None,
"guidance_operating_margin_low": None, "guidance_operating_margin_high": None,
}
MODEL = "claude-haiku-4-5-20251001"
MAX_CHARS = 8000
_GUIDANCE_HINT = re.compile(
r"(outlook|guidance|expect|anticipate|forecast|project|fiscal\s+(?:first|second|third|fourth|q[1-4]))",
re.IGNORECASE,
)
def _extract_relevant(text: str, max_chars: int) -> str:
"""Pull paragraphs that look like guidance to keep prompt small."""
if not text:
return ""
paras = re.split(r"\n{2,}|(?<=\.)\s{2,}", text)
keep = [p for p in paras if _GUIDANCE_HINT.search(p)]
joined = "\n\n".join(keep) if keep else text
return joined[:max_chars]
def _extract_json(raw: str) -> str:
m = re.search(r"```(?:json)?\s*(\{.*\})\s*```", raw, re.DOTALL)
if m:
return m.group(1)
s, e = raw.find("{"), raw.rfind("}")
return raw[s:e + 1] if (s != -1 and e != -1 and e > s) else raw
_PROMPT = """You extract forward-looking financial guidance from SEC filings and earnings calls.
Current reporting period just disclosed: {current_period}
Input text (MD&A excerpt + transcript excerpt):
---
{text}
---
Extract guidance issued by management for the NEXT reporting period (or full year if only annual is given).
Return STRICT JSON with this exact schema:
{{
"next_period": "Q12026" | "FY2026" | null,
"revenue": {{"low": <USD as number>, "high": <USD as number>}} | null,
"eps": {{"low": <number>, "high": <number>}} | null,
"gross_margin": {{"low": <decimal 0-1>, "high": <decimal 0-1>}} | null,
"operating_margin": {{"low": <decimal 0-1>, "high": <decimal 0-1>}} | null
}}
Rules:
- USD values in dollars (e.g. $94.5B → 94500000000).
- Margins as decimals (e.g. "75% gross margin" → 0.75). NEVER as percentages.
- If management gives a point estimate, set low == high.
- If a metric is not mentioned, use null.
- If no forward guidance exists at all, return all-null fields.
- Output JSON only, no commentary."""
def parse_guidance(
mda_text: str,
transcript_text: Optional[str],
current_period: str,
) -> dict:
"""Return a dict with keys matching metrics_db._GUIDANCE_COLS.
All-None on parse failure or absent guidance. Never raises.
"""
combined = "\n\n=== TRANSCRIPT ===\n\n".join(
t for t in (_extract_relevant(mda_text or "", MAX_CHARS // 2),
_extract_relevant(transcript_text or "", MAX_CHARS // 2)) if t
)
if not combined.strip():
return dict(_EMPTY)
try:
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
llm = ChatAnthropic(model=MODEL, temperature=0)
resp = llm.invoke([HumanMessage(
content=_PROMPT.format(current_period=current_period or "unknown", text=combined)
)])
raw = resp.content if isinstance(resp.content, str) else str(resp.content)
data = json.loads(_extract_json(raw))
except Exception as exc:
print(f"[guidance_parser] WARNING: parse failed: {exc}", file=sys.stderr)
return dict(_EMPTY)
def _pair(obj, key):
v = obj.get(key) if isinstance(obj, dict) else None
if not isinstance(v, dict):
return None, None
lo, hi = v.get("low"), v.get("high")
try:
lo = float(lo) if lo is not None else None
hi = float(hi) if hi is not None else None
except (TypeError, ValueError):
return None, None
return lo, hi
rev_lo, rev_hi = _pair(data, "revenue")
eps_lo, eps_hi = _pair(data, "eps")
gm_lo, gm_hi = _pair(data, "gross_margin")
om_lo, om_hi = _pair(data, "operating_margin")
return {
"guidance_period": data.get("next_period") if isinstance(data, dict) else None,
"guidance_revenue_low": rev_lo, "guidance_revenue_high": rev_hi,
"guidance_eps_low": eps_lo, "guidance_eps_high": eps_hi,
"guidance_gross_margin_low": gm_lo, "guidance_gross_margin_high": gm_hi,
"guidance_operating_margin_low": om_lo, "guidance_operating_margin_high": om_hi,
}