# File: src/verification_engine.py # Purpose: Verify booking outcomes before allowing the agent to generate any response # Note: On HF Spaces, booking logic is called in-process (no HTTP server needed) import uuid import random from dataclasses import dataclass from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from config import AVAILABLE_SLOTS, DEPARTMENTS, MAX_BOOKING_RETRIES # In-memory slot store (simulates database, lives for the session) _booked_slots: dict[str, list[str]] = {} @dataclass class VerificationResult: """ Carries the verified state of a booking attempt. The response generator ONLY uses data from this object — never from LLM memory. """ verified: bool appointment_id: str | None patient_name: str | None department: str | None date: str | None slot: str | None doctor: str | None failure_reason: str | None attempts: int def _call_booking_logic(patient_name: str, department: str, date: str, slot: str) -> dict: """ In-process booking logic — replaces HTTP call for HF Spaces deployment. Simulates real constraints: slot conflicts, 80% success rate. """ if department not in DEPARTMENTS: return {"success": False, "reason": f"Unknown department: {department}"} if slot not in AVAILABLE_SLOTS: return {"success": False, "reason": f"Invalid time slot: {slot}"} key = f"{department}::{date}" taken = _booked_slots.get(key, []) if slot in taken or random.random() < 0.20: return {"success": False, "reason": "Slot unavailable. Please choose another time."} appointment_id = f"APT-{uuid.uuid4().hex[:6].upper()}" doctor = f"Dr. Sample_{random.randint(1, 5)}" _booked_slots.setdefault(key, []).append(slot) return { "success": True, "appointment_id": appointment_id, "patient_name": patient_name, "department": department, "date": date, "slot": slot, "doctor": doctor, } def verify_booking(intent: dict) -> VerificationResult: """ Attempt to book and verify the result up to MAX_BOOKING_RETRIES times. Anti-hallucination gate: - verified=True ONLY when appointment_id is present in the result. - All data in VerificationResult comes from the booking logic, not the LLM. """ attempts = 0 last_reason = "Unknown error" for attempt in range(1, MAX_BOOKING_RETRIES + 1): attempts = attempt result = _call_booking_logic( patient_name=intent.get("patient_name", "Unknown"), department=intent.get("department", ""), date=intent.get("date", ""), slot=intent.get("slot", ""), ) print(f"[Verification] Attempt {attempt}: {result}") if result.get("success") and result.get("appointment_id"): return VerificationResult( verified=True, appointment_id=result["appointment_id"], patient_name=result.get("patient_name"), department=result.get("department"), date=result.get("date"), slot=result.get("slot"), doctor=result.get("doctor"), failure_reason=None, attempts=attempts, ) else: last_reason = result.get("reason", "Slot unavailable") return VerificationResult( verified=False, appointment_id=None, patient_name=intent.get("patient_name"), department=intent.get("department"), date=intent.get("date"), slot=intent.get("slot"), doctor=None, failure_reason=last_reason, attempts=attempts, ) def fetch_available_slots(department: str, date: str) -> list[str]: """Return open slots for a department on a given date.""" key = f"{department}::{date}" taken = _booked_slots.get(key, []) return [s for s in AVAILABLE_SLOTS if s not in taken]