Spaces:
Runtime error
Runtime error
| # 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 | | |
| """) |