# app/agents/summarizer.py from __future__ import annotations import os, json, requests, re from typing import List, Dict, Any from app.schemas.report import CallReport from app.schemas.claim import Claim from app.schemas.evidence import Evidence from app.schemas.verdict import Verdict from app.core.config import WATSONX_BASE_URL, WATSONX_PROJECT, WATSONX_API_KEY from app.core.json_utils import extract_json_obj VERSION = os.getenv("IBM_API_VERSION", "2023-05-29") MODEL_ID = os.getenv("IBM_SUMMARY_MODEL_ID", "ibm/granite-3-8b-instruct") def _iam_token() -> str: r = requests.post( "https://iam.cloud.ibm.com/identity/token", data={"grant_type":"urn:ibm:params:oauth:grant-type:apikey","apikey":WATSONX_API_KEY}, headers={"Content-Type":"application/x-www-form-urlencoded"}, timeout=30 ) r.raise_for_status() return r.json()["access_token"] _JSON = re.compile(r"\{[\s\S]*\}\s*$") # Replace _safe_json with: def _safe_json(s: str) -> dict: return extract_json_obj(s) # ---- Prompt for structured output ---- PROMPT = """You are a precise meeting summarizer for sales/stakeholder calls. Given transcript segments, normalized claims, and their verification labels, produce a concise executive summary. Return STRICT JSON only with: { "call_summary": "string, <= 6 sentences, neutral, factual", "action_items": ["string", "..."] // 1-6 bullets, imperative } Guidelines: - Emphasize mismatches between stated claims and evidence. - Mention concrete metrics (%, $, dates) when present. - Avoid fluff and opinions; be concise and factual. Segments (JSON): {SEGMENTS_JSON} Claims (JSON): {CLAIMS_JSON} Verdicts (JSON): {VERDICTS_JSON} Now return JSON only: """ # ---- Public API ---- def make_report( segments: List[Dict[str, Any]], claims: List[Claim], evidence_flat: List[Evidence], verdicts: List[Verdict], ) -> CallReport: """ Build a CallReport: - call_summary + action_items from IBM LLM - claim_table from verdicts (+ best evidence id) """ # ---- Prepare claim_table from verdicts # Map claim_id -> claim text id2claim = {c.id: c.text for c in claims} claim_table: List[Dict[str, Any]] = [] for v in verdicts: claim_text = id2claim.get(v.claim_id, "") row = { "claim": claim_text, "status": v.label.capitalize(), "evidence_source": v.best_evidence_id or "" } claim_table.append(row) # ---- Try IBM Granite for summary/action items call_summary = "" action_items: List[str] = [] try: tok = _iam_token() url = f"{WATSONX_BASE_URL.rstrip('/')}/ml/v1/text/generation?version={VERSION}" headers = { "Authorization": f"Bearer {tok}", "Accept": "application/json", "Content-Type": "application/json", } body = { "input": PROMPT \ .replace("{SEGMENTS_JSON}", json.dumps(segments, ensure_ascii=False)) \ .replace("{CLAIMS_JSON}", json.dumps([{"id": c.id, "text": c.text} for c in claims], ensure_ascii=False)) \ .replace("{VERDICTS_JSON}", json.dumps([{ "claim_id": v.claim_id, "label": v.label, "confidence": v.confidence, "best_evidence_id": v.best_evidence_id, "rationale": v.rationale } for v in verdicts], ensure_ascii=False)), "model_id": MODEL_ID, "project_id": WATSONX_PROJECT, "parameters": { "decoding_method": "greedy", "temperature": 0.0, "max_new_tokens": 300, "min_new_tokens": 0, "repetition_penalty": 1.0 } } r = requests.post(url, headers=headers, json=body, timeout=120) r.raise_for_status() out = r.json() gen = (out.get("results") or [{}])[0].get("generated_text", "") parsed = _safe_json(gen) call_summary = (parsed.get("call_summary") or "").strip() action_items = parsed.get("action_items") or [] if not isinstance(action_items, list): action_items = [str(action_items)] except Exception as e: # Fallback: extract a terse summary from first few segments print(f"[summarizer] watsonx generation failed: {e}") texts = [s.get("text","") for s in segments if s.get("text")] joined = " ".join(texts)[:450].strip() call_summary = (joined + "…") if joined else "" if not action_items: action_items = ["Review claims vs. evidence and confirm metrics in source-of-truth."] # ---- Build CallReport dataclass (schema) report = CallReport( call_summary=call_summary, claim_table=claim_table, action_items=action_items, claims=claims, verdicts=verdicts, evidence=evidence_flat ) return report