amplegest / agent /post_synthesis.py
Viney's picture
feat: multi-provider LLM support, prominent chat, design pass, and new analytics
7880373
Raw
History Blame Contribute Delete
14 kB
"""Deterministic evidence verification and conservative reliability scoring.
Runs after the LLM synthesis_node. Walks every SourcedFact-like dict in the brief,
recomputes its ``reliability`` via ``agent.reliability.reliability_for`` and
keeps absent or failed verification at LOW. Corroboration is explanatory only;
it never promotes an unverified claim.
The LLM picks reliability by hard-coded rule (10-K=HIGH, transcript=MEDIUM).
This module replaces that with a fail-closed score based on verification and
source class.
"""
from __future__ import annotations
import re
from datetime import datetime
from typing import Iterable, Optional
from agent.evidence import verify_brief_evidence as _verify_brief_evidence
from agent.reliability import reliability_for
# ---------------------------------------------------------------------------
# Heuristics
# ---------------------------------------------------------------------------
_RISK_KEYWORDS = re.compile(
r"\b(risk|risks|exposure|exposures|uncertainty|uncertainties|"
r"subject to|could adversely|may adversely|materially harm|"
r"litigation|cybersecurity|breach|tariff|sanction)\b",
re.IGNORECASE,
)
_STOPWORDS = {
"the", "and", "for", "with", "from", "that", "this", "have", "been",
"their", "into", "more", "than", "what", "when", "where", "which",
"while", "would", "could", "should", "about", "after", "before",
"these", "those", "such", "also", "then", "thus", "well", "very",
}
_MAX_AUTO_NOTES = 3
_JACCARD_THRESHOLD = 0.3
_MIN_TOKENS = 4
def _tokenize(snippet: str) -> set[str]:
"""Lower-case, alpha-only tokens longer than 3 chars, stopwords removed."""
if not snippet:
return set()
return {
w for w in re.findall(r"[a-zA-Z]+", snippet.lower())
if len(w) > 3 and w not in _STOPWORDS
}
def _jaccard(a: set[str], b: set[str]) -> float:
if not a or not b:
return 0.0
return len(a & b) / len(a | b)
def _is_risk_factors(fact: dict) -> bool:
"""True if the fact text or evidence_snippet looks like Risk Factors content."""
if fact.get("source") not in ("10-K", "10-Q"):
return False
haystack = (fact.get("evidence_snippet", "") or "") + " " + (fact.get("text", "") or "")
return bool(_RISK_KEYWORDS.search(haystack))
def _news_age_days(fact: dict, brief_filing_date: Optional[str]) -> Optional[int]:
"""Approximate age of a news fact in days using the brief's filing date as proxy."""
if fact.get("source") != "news" or not brief_filing_date:
return None
try:
ref = datetime.strptime(brief_filing_date[:10], "%Y-%m-%d")
return max(0, (datetime.now() - ref).days)
except ValueError:
return None
# ---------------------------------------------------------------------------
# Fact collection
# ---------------------------------------------------------------------------
def _collect_facts(brief: dict) -> list[tuple[str, dict]]:
"""Return (label, fact_dict) pairs for every reliability-bearing object in the brief.
Label is human-readable for evidence_notes ("Bull #2", "Risk: Regulatory", etc.).
"""
facts: list[tuple[str, dict]] = []
sn = brief.get("standout_number")
if isinstance(sn, dict):
facts.append(("Standout number", sn))
for i, f in enumerate(brief.get("what_changed", []) or [], 1):
if isinstance(f, dict):
facts.append((f"What changed #{i}", f))
for i, f in enumerate(brief.get("bull_points", []) or [], 1):
if isinstance(f, dict):
facts.append((f"Bull #{i}", f))
for i, f in enumerate(brief.get("bear_points", []) or [], 1):
if isinstance(f, dict):
facts.append((f"Bear #{i}", f))
for i, r in enumerate(brief.get("risks_categorized", []) or [], 1):
if isinstance(r, dict):
facts.append((f"Risk: {r.get('category', '?')}", r))
for i, t in enumerate(brief.get("management_commentary", []) or [], 1):
if isinstance(t, dict):
facts.append((f"Mgmt: {t.get('topic', '?')}", t))
for i, guidance in enumerate(brief.get("guidance_history", []) or [], 1):
if isinstance(guidance, dict):
facts.append((f"Guidance #{i}", guidance))
for i, tension in enumerate(brief.get("analytical_tensions", []) or [], 1):
if not isinstance(tension, dict):
continue
for side, key in (("bull", "bullish_evidence"), ("bear", "bearish_evidence")):
evidence = tension.get(key)
if isinstance(evidence, dict):
facts.append((f"Tension #{i} {side}", evidence))
for i, signal in enumerate(brief.get("earnings_quality_signals", []) or [], 1):
evidence = signal.get("evidence") if isinstance(signal, dict) else None
if isinstance(evidence, dict):
facts.append((f"Quality signal #{i}", evidence))
for i, subtext in enumerate(brief.get("between_the_lines", []) or [], 1):
evidence = subtext.get("evidence") if isinstance(subtext, dict) else None
if isinstance(evidence, dict):
facts.append((f"Between the lines #{i}", evidence))
mda = brief.get("mda_summary") or {}
if isinstance(mda, dict):
for i, f in enumerate(mda.get("drivers", []) or [], 1):
if isinstance(f, dict):
facts.append((f"MD&A driver #{i}", f))
for i, f in enumerate(mda.get("headwinds", []) or [], 1):
if isinstance(f, dict):
facts.append((f"MD&A headwind #{i}", f))
kq = mda.get("key_quote")
if isinstance(kq, dict):
facts.append(("MD&A key quote", kq))
return facts
# ---------------------------------------------------------------------------
# Corroboration
# ---------------------------------------------------------------------------
def _build_corroboration_index(facts: list[tuple[str, dict]]) -> dict[int, list[tuple[str, str]]]:
"""For each fact (by id), return list of (other_label, other_source) that corroborate it.
Two facts corroborate each other when they come from DIFFERENT sources AND their
evidence_snippets share a Jaccard score >= _JACCARD_THRESHOLD on tokens.
"""
tokens = [
(
label,
fact,
_tokenize(fact.get("evidence_snippet", ""))
if fact.get("verification_status") in (None, "VERIFIED") else set(),
)
for label, fact in facts
]
index: dict[int, list[tuple[str, str]]] = {}
for i, (label_i, fact_i, toks_i) in enumerate(tokens):
if len(toks_i) < _MIN_TOKENS:
index[id(fact_i)] = []
continue
matches: list[tuple[str, str]] = []
for j, (label_j, fact_j, toks_j) in enumerate(tokens):
if i == j:
continue
if fact_i.get("source") == fact_j.get("source"):
continue
if len(toks_j) < _MIN_TOKENS:
continue
if _jaccard(toks_i, toks_j) >= _JACCARD_THRESHOLD:
matches.append((label_j, str(fact_j.get("source", ""))))
index[id(fact_i)] = matches
return index
# ---------------------------------------------------------------------------
# Public entrypoint
# ---------------------------------------------------------------------------
def verify_evidence(brief: dict, evidence_payloads) -> dict:
"""Verify brief facts against retrieved evidence.v1 tool payloads."""
return _verify_brief_evidence(brief, evidence_payloads)
def apply_reliability(brief: dict, evidence_payloads=None) -> dict:
"""Recompute reliability for every fact-like object in the brief, in place.
Also appends up to _MAX_AUTO_NOTES auto-generated entries to brief['evidence_notes']
documenting cross-source corroborations and lone-source weaknesses.
Returns the same brief dict (mutated) for convenience.
"""
if not isinstance(brief, dict):
return brief
if evidence_payloads is not None:
_verify_brief_evidence(brief, evidence_payloads)
facts = _collect_facts(brief)
if not facts:
return brief
corro = _build_corroboration_index(facts)
filing_date = brief.get("filing_date")
auto_notes: list[str] = []
seen_note_keys: set[str] = set()
for label, fact in facts:
source = fact.get("source", "")
section = "Risk Factors" if label.startswith("Risk:") else None
age_days = _news_age_days(fact, filing_date)
corroborated = bool(corro.get(id(fact)))
new_reliability = reliability_for(
source=source,
section=section,
age_days=age_days,
corroborated=corroborated,
verification_status=fact.get("verification_status"),
)
old_reliability = fact.get("reliability")
fact["reliability"] = new_reliability
if len(auto_notes) >= _MAX_AUTO_NOTES:
continue
if fact.get("verification_status") in ("UNVERIFIED", "FAILED"):
key = f"verification:{label}"
if key not in seen_note_keys:
reason = fact.get("verification_reason") or "evidence not verified"
auto_notes.append(f"{label}: {reason} -> reliability held at LOW.")
seen_note_keys.add(key)
continue
# Note 1: meaningful uplift (transcript or stale-news boost)
if corroborated and source in ("transcript",) and new_reliability == "HIGH":
other_sources = sorted({src for _, src in corro[id(fact)] if src})
key = f"uplift:{label}"
if other_sources and key not in seen_note_keys:
auto_notes.append(
f"{label} ({source}) corroborated by {' + '.join(other_sources)} β†’ reliability uplift to HIGH."
)
seen_note_keys.add(key)
continue
# Note 2: lone news fact (not corroborated)
if source == "news" and not corroborated and new_reliability == "LOW":
key = f"lone_news:{label}"
if key not in seen_note_keys:
auto_notes.append(
f"{label} sourced only from news; no filing/transcript cross-confirmation β†’ LOW reliability."
)
seen_note_keys.add(key)
continue
# Note 3: Risk Factors downgrade (filing β†’ MEDIUM)
if section == "Risk Factors" and old_reliability == "HIGH" and new_reliability == "MEDIUM":
key = f"risk_factor:{label}"
if key not in seen_note_keys:
auto_notes.append(
f"{label} drawn from Risk Factors boilerplate β†’ downgraded to MEDIUM."
)
seen_note_keys.add(key)
if auto_notes:
existing = brief.get("evidence_notes") or []
if not isinstance(existing, list):
existing = []
# Keep all existing notes + appended auto notes, capped at 6 total
brief["evidence_notes"] = (existing + auto_notes)[:6]
_prune_tension_duplicates(brief)
return brief
def _prune_tension_duplicates(brief: dict) -> None:
"""Remove analytical_tensions that duplicate existing bull/bear points.
A tension is considered a duplicate when its bullish_evidence or bearish_evidence
snippet has Jaccard similarity >= _JACCARD_THRESHOLD with any bull or bear point
snippet. Forces the LLM to produce synthesis (interplay), not copy-paste.
"""
tensions = brief.get("analytical_tensions")
if not tensions or not isinstance(tensions, list):
return
reference_snippets: list[set[str]] = []
for lst_key in ("bull_points", "bear_points"):
for fact in (brief.get(lst_key) or []):
if isinstance(fact, dict):
toks = _tokenize(fact.get("evidence_snippet", ""))
if len(toks) >= _MIN_TOKENS:
reference_snippets.append(toks)
if not reference_snippets:
return
pruned: list[dict] = []
pruned_count = 0
for tension in tensions:
if not isinstance(tension, dict):
continue
bull_ev = tension.get("bullish_evidence") or {}
bear_ev = tension.get("bearish_evidence") or {}
bull_toks = _tokenize(bull_ev.get("evidence_snippet", "") if isinstance(bull_ev, dict) else "")
bear_toks = _tokenize(bear_ev.get("evidence_snippet", "") if isinstance(bear_ev, dict) else "")
is_dup = any(
_jaccard(toks, ref) >= _JACCARD_THRESHOLD
for toks in (bull_toks, bear_toks)
if len(toks) >= _MIN_TOKENS
for ref in reference_snippets
)
if is_dup:
pruned_count += 1
else:
pruned.append(tension)
brief["analytical_tensions"] = pruned
if pruned_count:
existing_notes = brief.get("evidence_notes") or []
if isinstance(existing_notes, list) and len(existing_notes) < 6:
brief["evidence_notes"] = existing_notes + [
f"Pruned {pruned_count} analytical tension(s) that duplicated bull/bear point evidence."
]
# ---------------------------------------------------------------------------
# Edge signal attach (authoritative computed data β€” never LLM-generated)
# ---------------------------------------------------------------------------
def attach_edge_signals(brief: dict, edge_signals: Optional[list[dict]]) -> dict:
"""Write deterministically-computed edge signals into the brief dict.
The LLM produces explanations via the synthesis prompt; this function
writes the authoritative computed numbers so they are never absent or
fabricated. Called in synthesis_node after apply_reliability().
"""
if not isinstance(brief, dict):
return brief
if not edge_signals:
brief.setdefault("quarter_deltas", [])
return brief
brief["quarter_deltas"] = edge_signals
return brief