ranjithkumar111's picture
Upload 4 files
831e3f9 verified
Raw
History Blame Contribute Delete
11.3 kB
# 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 |
""")