# File: app.py # Purpose: Streamlit demo dashboard with real Bland AI call trigger import streamlit as st import json import os import sys import requests from pathlib import Path from dotenv import load_dotenv load_dotenv(override=True) BASE_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(BASE_DIR)) from config import OUTPUTS_DIR BLAND_API_KEY = os.getenv("BLAND_API_KEY", "") DEFAULT_PHONE = os.getenv("PHONE_NUMBER", "") WEBHOOK_URL = os.getenv("WEBHOOK_URL", "") st.set_page_config( page_title="Hallucination-Safe AI Calling Agent", page_icon="đŸĨ", layout="wide", ) st.title("đŸĨ Hallucination-Safe AI Hospital Calling Agent") st.caption("Every response is verified by the backend before being communicated to the patient.") # Sidebar with st.sidebar: st.header("Pipeline Components") st.markdown(""" - **Voice**: Bland AI - **STT**: OpenAI Whisper - **Agent**: Llama 3.3 70B (Groq) - **Verification**: In-process booking engine - **Response**: Template-only (no LLM hallucination) """) st.divider() st.markdown("**System Status**") if BLAND_API_KEY: st.success("Bland AI key loaded") else: st.error("BLAND_API_KEY not set in .env") if os.getenv("GROQ_API_KEY"): st.success("Groq key loaded") else: st.error("GROQ_API_KEY not set in .env") if WEBHOOK_URL: st.success("Webhook ready") else: st.error("WEBHOOK_URL not set in .env") # Tabs tab1, tab2, tab3, tab4 = st.tabs(["📞 Call Me", "đŸ’Ŧ Text Demo", "📋 Booking Log", "â„šī¸ How It Works"]) # Tab 1: Real Call with tab1: st.subheader("Trigger a Real AI Call") phone_number = st.text_input( "Phone Number", value=DEFAULT_PHONE, placeholder="+919345521041", help="Enter your phone number with country code. e.g. +919345521041", ) st.info(f"Bland AI will call **{phone_number}** and book a hospital appointment through a live conversation.") st.markdown("#### The AI will ask you:") st.markdown(""" 1. Your name 2. Which department you need 3. Preferred date and time slot """) col1, col2 = st.columns([1, 2]) with col1: if st.button("📞 Call Me Now", type="primary", use_container_width=True): if not BLAND_API_KEY: st.error("BLAND_API_KEY not set in .env") st.stop() if not phone_number.strip(): st.error("Please enter a phone number.") st.stop() with st.spinner("Initiating call via Bland AI..."): try: payload = { "phone_number": phone_number.strip(), "task": ( "You are a hospital appointment booking assistant. " "Greet the patient warmly and collect: their full name, " "which department they need (Cardiology, Neurology, Orthopedics, " "Dermatology, General Medicine, Pediatrics, or Psychiatry), " "preferred date, and preferred time slot (9:00 AM, 10:00 AM, " "11:00 AM, 2:00 PM, 3:00 PM, or 4:00 PM). " "Once collected, confirm all details back to the patient and " "tell them the booking is being processed. Be polite and professional." ), "voice": "maya", "wait_for_greeting": True, "record": True, "webhook": WEBHOOK_URL, "max_duration": 5, "answered_by_enabled": True, } response = requests.post( "https://api.bland.ai/v1/calls", headers={ "authorization": BLAND_API_KEY, "Content-Type": "application/json", }, json=payload, timeout=15, ) response.raise_for_status() data = response.json() call_id = data.get("call_id", "unknown") st.success(f"Call initiated! Call ID: `{call_id}`") st.info(f"Your phone ({phone_number}) will ring shortly.") if "call_log" not in st.session_state: st.session_state["call_log"] = [] st.session_state["call_log"].append({ "call_id": call_id, "phone": phone_number, "status": "initiated", }) except requests.HTTPError as e: st.error(f"Bland AI error: {e.response.text}") except Exception as e: st.error(f"Error: {e}") with col2: st.markdown("#### What happens after the call:") st.code(""" Your phone rings (Bland AI) ↓ You speak your request ↓ Bland AI sends transcript to webhook ↓ Groq extracts booking intent ↓ Verification engine checks booking ↓ Confirmed appointment_id → Response spoken back """, language="text") st.divider() st.markdown("#### Recent Calls") call_log = st.session_state.get("call_log", []) if call_log: import pandas as pd st.dataframe(pd.DataFrame(call_log), use_container_width=True) else: st.info("No calls made yet in this session.") # Tab 2: Text Demo with tab2: st.subheader("Test Pipeline with Text Input") st.caption("Simulates what happens after Bland AI transcribes a call.") example = st.selectbox("Load an example transcript", [ "Custom input...", "I'd like to book a cardiology appointment tomorrow at 2 PM. My name is Ranjith Kumar.", "Book a neurology slot on 2025-06-10 at 10 AM for Priya Sharma.", "Umm I need to see a doctor, maybe some day soon...", "Please schedule an orthopedics appointment for 2025-06-15 at 4 PM. Patient is Kavya Nair.", ]) if example == "Custom input...": transcript = st.text_area("Enter patient transcript", height=100) else: transcript = st.text_area("Enter patient transcript", value=example, height=100) if st.button("Run Pipeline", type="primary"): if not transcript.strip(): st.warning("Please enter a transcript.") st.stop() with st.spinner("Running pipeline..."): try: from src.pipeline import run_pipeline result = run_pipeline(transcript) except Exception as e: st.error(f"Pipeline error: {e}") st.stop() intent = result.get("intent", {}) verification = result.get("verification") escalated = result.get("escalated", False) col1, col2, col3, col4 = st.columns(4) col1.metric("Confidence", f"{intent.get('confidence', 0):.0%}") col2.metric("Department", intent.get("department", "—")) if verification and verification.verified: col3.metric("Booking", "Confirmed") else: col3.metric("Booking", "Failed") col4.metric("Escalated", "Yes" if escalated else "No") col_left, col_right = st.columns(2) with col_left: st.subheader("Extracted Intent") st.json({ "patient_name": intent.get("patient_name"), "department": intent.get("department"), "date": intent.get("date"), "slot": intent.get("slot"), "confidence": intent.get("confidence"), "missing_info": intent.get("missing_info", []), }) with col_right: st.subheader("Verification Result") if verification: st.json({ "verified": verification.verified, "appointment_id": verification.appointment_id, "doctor": verification.doctor, "failure_reason": verification.failure_reason, "attempts": verification.attempts, }) else: st.info("Escalated before verification.") st.subheader("Patient-Facing Response") response = result["response"] if verification and verification.verified: st.success(f"🔊 {response}") st.balloons() elif escalated: st.warning(f"🔊 {response}") else: st.error(f"🔊 {response}") if "booking_log" not in st.session_state: st.session_state["booking_log"] = [] st.session_state["booking_log"].append({ "transcript": transcript[:80], "department": intent.get("department"), "slot": intent.get("slot"), "confidence": intent.get("confidence"), "verified": verification.verified if verification else None, "appointment_id": verification.appointment_id if verification else None, "escalated": escalated, "response": response[:100], }) # Tab 3: Booking Log with tab3: st.subheader("Session Booking Log") log = st.session_state.get("booking_log", []) if log: import pandas as pd df = pd.DataFrame(log) st.dataframe(df, use_container_width=True) st.download_button( "Download Log (JSON)", data=json.dumps(log, indent=2), file_name="booking_log.json", mime="application/json", ) else: st.info("No bookings yet.") # Tab 4: How It Works with tab4: st.subheader("Anti-Hallucination Architecture") col1, col2 = st.columns(2) with col1: st.markdown("#### Standard AI Caller (Hallucination Risk)") st.code(""" Patient Call ↓ LLM Agent ↓ Response <- LLM may invent confirmation """, language="text") with col2: st.markdown("#### This System (Verified)") st.code(""" Patient Call (Bland AI) ↓ Whisper STT ↓ LLM Agent (Groq) ↓ Booking Engine ↓ Verification Gate appointment_id exists? YES -> Confirmed NO -> Failure + alternatives """, language="text") st.divider() st.markdown(""" | File | Role | |---|---| | `src/agent.py` | Llama 3.3 70B via Groq - extracts intent as JSON | | `src/verification_engine.py` | Anti-hallucination gate - verified=True only with confirmed appointment_id | | `src/response_generator.py` | Template-only responses - zero LLM generation | | `src/pipeline.py` | Orchestrates all stages end-to-end | | `src/bland_webhook.py` | Receives Bland AI call transcript via webhook | """)