from typing import Dict, List, Optional import httpx from tenacity import retry, stop_after_attempt, wait_exponential from app.config import settings from app.api.schemas import GUVIIntelligence from app.utils.logger import AgentLogger logger = AgentLogger("callback_client") GUVI_CALLBACK_URL = settings.GUVI_CALLBACK_URL def normalize_intelligence(intel: Dict) -> GUVIIntelligence: """ Map internal intelligence keys to GUVI expected format. CRITICAL: Ensures all fields are ARRAYS, never null. """ # Financial Accounts (Bank + Credit Cards) bank_accounts = list(intel.get("bank_accounts", []) or []) if "credit_cards" in intel and intel["credit_cards"]: bank_accounts.extend(intel["credit_cards"]) # Keywords (Keywords + OTPs + RATs + Emails) keywords = list(intel.get("keywords", []) or []) for key in ["otps", "rat_apps", "pan_cards", "aadhar_numbers", "emails"]: if key in intel and intel[key]: prefix = key.replace("_", " ").upper() for val in intel[key]: keywords.append(f"[{prefix}] {val}") # GUVI PAYLOAD VALIDATION: All fields must be arrays, never null return GUVIIntelligence( bankAccounts=bank_accounts if bank_accounts else [], upiIds=list(intel.get("upi_ids", []) or []), phishingLinks=list(intel.get("urls", []) or []), phoneNumbers=list(intel.get("phone_numbers", []) or []), suspiciousKeywords=keywords if keywords else [] ) class GUVIMandatoryCallback: """Handles mandatory reporting of intelligence to GUVI evaluation endpoint.""" @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=2, max=10), reraise=False) async def send_final_result( self, session_id: str, scam_detected: bool, total_messages: int, extracted_intelligence: Dict, agent_notes: str, # Allow extra args but ignore them for payload to prevent rejection scam_confidence: Optional[float] = None, risk_level: Optional[str] = None, timeline: Optional[List[str]] = None ) -> bool: """ Send final result payload to GUVI. Normalizes data to match exact Hackathon spec. """ # 1. Normalize Intelligence Keys (Critical Fix) intel = normalize_intelligence(extracted_intelligence) # 2. Strict Payload Construction (No extra fields) # CRITICAL: Append High-Value Intel to Notes for Human Judges (Failsafe) # High value intel for judges to see in notes (mapped or explicit) high_value_found = any("[OTP]" in k or "[CREDIT CARD]" in k or "[RAT APP]" in k for k in intel.suspiciousKeywords) if high_value_found: agent_notes += f"\n[CRITICAL INTEL] High-value target data (OTPs/Cards) detected and mapped to suspiciousKeywords." payload = { "sessionId": session_id, "scamDetected": scam_detected, "totalMessagesExchanged": total_messages, "extractedIntelligence": intel.dict(), "agentNotes": agent_notes } logger.info("Sending GUVI callback", payload=payload) try: # [FIX] Resilient Timeout (Risk #4) # GUVI server can be slow during bulk evaluation. async with httpx.AsyncClient(timeout=25.0) as client: response = await client.post( GUVI_CALLBACK_URL, json=payload, headers={ "Content-Type": "application/json", "x-api-key": settings.GUVI_API_KEY } ) if response.status_code in [200, 201]: logger.info("GUVI callback success", session_id=session_id) return True # Log detailed failure logger.error("GUVI callback failed", status=response.status_code, response=response.text) return False except Exception as e: logger.error("Network error during GUVI callback", error=str(e)) return False guvi_callback = GUVIMandatoryCallback()