Medbot / agent /backend.py
Moncey10's picture
Add robust error handling and self-debugging support
250e5f3
import os
import json
import httpx
from dotenv import load_dotenv
from supabase import create_client
load_dotenv()
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
supabase = None
if all([GROQ_API_KEY, SUPABASE_URL, SUPABASE_KEY]):
try:
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
except Exception as e:
print(f"WARNING: Could not initialize Supabase: {e}")
# ─────────────────────────────────────────
# TOOL FUNCTIONS
# ─────────────────────────────────────────
def get_patient_by_name(name: str):
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
res = supabase.table("patients").select("*").ilike("name", f"%{name}%").execute()
return res.data if res.data else {"error": "Patient not found"}
def get_patients_by_doctor(doctor: str):
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
res = supabase.table("patients").select("*").ilike("doctor", f"%{doctor}%").execute()
return res.data if res.data else {"error": "No patients found for this doctor"}
def get_admitted_patients():
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
res = supabase.table("patients").select("*").eq("status", "admitted").execute()
return res.data if res.data else {"message": "No admitted patients currently"}
def get_appointments_by_date(date: str):
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
res = supabase.table("appointments").select("*").eq("date", date).execute()
return res.data if res.data else {"message": f"No appointments on {date}"}
def get_patient_appointments(patient_name: str):
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
res = supabase.table("appointments").select("*").ilike("patient_name", f"%{patient_name}%").execute()
return res.data if res.data else {"error": "No appointments found for this patient"}
def add_patient(name: str, age: int, gender: str, disease: str, doctor: str, admission_date: str):
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
data = {
"name": name,
"age": age,
"gender": gender,
"disease": disease,
"doctor": doctor,
"admission_date": admission_date,
"status": "admitted"
}
res = supabase.table("patients").insert(data).execute()
return {"success": True, "patient": res.data[0]} if res.data else {"error": "Failed to add patient"}
def discharge_patient(patient_id: int):
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
res = supabase.table("patients").update({"status": "discharged"}).eq("id", patient_id).execute()
return {"success": True, "message": f"Patient {patient_id} discharged"} if res.data else {"error": "Patient not found"}
def update_appointment_status(appointment_id: int, status: str):
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
if status not in ["scheduled", "completed", "cancelled"]:
return {"error": "Invalid status. Use: scheduled, completed, cancelled"}
res = supabase.table("appointments").update({"status": status}).eq("id", appointment_id).execute()
return {"success": True, "message": f"Appointment {appointment_id} updated to {status}"} if res.data else {"error": "Appointment not found"}
def get_disease_statistics():
if not supabase:
return {"error": "Database not connected. Check SUPABASE_URL and SUPABASE_KEY."}
res = supabase.table("patients").select("disease").execute()
if not res.data:
return {"message": "No data available"}
stats = {}
for row in res.data:
d = row["disease"]
stats[d] = stats.get(d, 0) + 1
return stats
# ─────────────────────────────────────────
# FUNCTION MAP
# ─────────────────────────────────────────
available_functions = {
"get_patient_by_name": get_patient_by_name,
"get_patients_by_doctor": get_patients_by_doctor,
"get_admitted_patients": get_admitted_patients,
"get_appointments_by_date": get_appointments_by_date,
"get_patient_appointments": get_patient_appointments,
"add_patient": add_patient,
"discharge_patient": discharge_patient,
"update_appointment_status": update_appointment_status,
"get_disease_statistics": get_disease_statistics,
}
# ─────────────────────────────────────────
# TOOLS SCHEMA
# ─────────────────────────────────────────
tools = [
{
"type": "function",
"function": {
"name": "get_patient_by_name",
"description": "Search and get patient details by name",
"parameters": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "get_patients_by_doctor",
"description": "Get all patients assigned to a specific doctor",
"parameters": {
"type": "object",
"properties": {"doctor": {"type": "string"}},
"required": ["doctor"]
}
}
},
{
"type": "function",
"function": {
"name": "get_admitted_patients",
"description": "Get all currently admitted patients",
"parameters": {"type": "object", "properties": {}}
}
},
{
"type": "function",
"function": {
"name": "get_appointments_by_date",
"description": "Get all appointments on a specific date (format: YYYY-MM-DD)",
"parameters": {
"type": "object",
"properties": {"date": {"type": "string"}},
"required": ["date"]
}
}
},
{
"type": "function",
"function": {
"name": "get_patient_appointments",
"description": "Get all appointments for a specific patient by name",
"parameters": {
"type": "object",
"properties": {"patient_name": {"type": "string"}},
"required": ["patient_name"]
}
}
},
{
"type": "function",
"function": {
"name": "add_patient",
"description": "Register a new patient in the hospital system",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"gender": {"type": "string"},
"disease": {"type": "string"},
"doctor": {"type": "string"},
"admission_date": {"type": "string", "description": "Format: YYYY-MM-DD"}
},
"required": ["name", "age", "gender", "disease", "doctor", "admission_date"]
}
}
},
{
"type": "function",
"function": {
"name": "discharge_patient",
"description": "Discharge a patient by their ID (change status to discharged)",
"parameters": {
"type": "object",
"properties": {"patient_id": {"type": "integer"}},
"required": ["patient_id"]
}
}
},
{
"type": "function",
"function": {
"name": "update_appointment_status",
"description": "Update the status of an appointment (scheduled, completed, or cancelled)",
"parameters": {
"type": "object",
"properties": {
"appointment_id": {"type": "integer"},
"status": {"type": "string", "enum": ["scheduled", "completed", "cancelled"]}
},
"required": ["appointment_id", "status"]
}
}
},
{
"type": "function",
"function": {
"name": "get_disease_statistics",
"description": "Get count of patients grouped by disease/condition",
"parameters": {"type": "object", "properties": {}}
}
}
]
# ─────────────────────────────────────────
# MAIN AGENT
# ─────────────────────────────────────────
SYSTEM_PROMPT = """You are MedBot, a hospital admin assistant for City General Hospital.
ABSOLUTE RULES - NEVER BREAK THESE:
1. YOU MUST ALWAYS RESPOND IN ENGLISH ONLY. This is mandatory. Never use Hindi, Urdu, Hinglish, or any other language. Not even one word in another language.
2. You are ONLY a hospital admin assistant. Reject all non-hospital queries politely in English.
You help admins with:
- Patient records (search, add, discharge)
- Doctor-patient assignments
- Appointment management
- Disease statistics
Always use available tools for database queries.
Format responses cleanly with bullet points for multiple records.
"""
async def process_agent_chat(user_message: str):
try:
if not GROQ_API_KEY:
return {"response": "Error: GROQ_API_KEY not configured. Please set it in Space Variables."}
headers = {
"Authorization": f"Bearer {GROQ_API_KEY}",
"Content-Type": "application/json"
}
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message}
]
payload = {
"model": "llama-3.3-70b-versatile",
"messages": messages,
"tools": tools,
"tool_choice": "auto",
"temperature": 0.2
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers=headers,
json=payload
)
# Handle non-200 responses from Groq (e.g. invalid key, rate limit)
if response.status_code != 200:
try:
error_data = response.json()
error_msg = error_data.get("error", {}).get("message", response.text)
except Exception:
error_msg = response.text
return {"response": f"⚠️ Groq API Error (Status {response.status_code}): {error_msg}"}
try:
data = response.json()
except Exception as e:
return {"response": f"⚠️ Failed to parse Groq response as JSON. Status code: {response.status_code}. Raw response: {response.text}"}
print("GROQ RESPONSE:", data)
if "choices" not in data or not data["choices"]:
return {"response": f"⚠️ Error: Groq response did not contain 'choices'. Full response: {data}"}
assistant_message = data["choices"][0]["message"]
tool_calls = assistant_message.get("tool_calls")
if not tool_calls:
return {"response": assistant_message.get("content", "No response text received.")}
messages.append(assistant_message)
for tool_call in tool_calls:
function_name = tool_call["function"]["name"]
function_args = tool_call["function"].get("arguments")
try:
function_args = json.loads(function_args) if function_args else {}
except Exception as e:
function_args = {}
print(f"Error parsing tool arguments: {e}")
if function_name not in available_functions:
function_response = {"error": f"Function '{function_name}' is not supported."}
else:
function_to_call = available_functions[function_name]
try:
try:
function_response = function_to_call(**function_args)
except TypeError:
function_response = function_to_call()
except Exception as e:
import traceback
print(f"Error executing database tool '{function_name}': {e}")
traceback.print_exc()
function_response = {"error": f"Database query failed in {function_name}: {str(e)}"}
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": function_name,
"content": json.dumps(function_response)
})
second_payload = {
"model": "llama-3.3-70b-versatile",
"messages": messages,
"temperature": 0.2
}
async with httpx.AsyncClient(timeout=30.0) as client:
final_response = await client.post(
"https://api.groq.com/openai/v1/chat/completions",
headers=headers,
json=second_payload
)
if final_response.status_code != 200:
try:
error_data = final_response.json()
error_msg = error_data.get("error", {}).get("message", final_response.text)
except Exception:
error_msg = final_response.text
return {"response": f"⚠️ Groq Final Response Generation Error (Status {final_response.status_code}): {error_msg}"}
try:
final_data = final_response.json()
except Exception as e:
return {"response": f"⚠️ Failed to parse Groq final response as JSON. Status code: {final_response.status_code}. Raw response: {final_response.text}"}
if "choices" not in final_data or not final_data["choices"]:
return {"response": f"⚠️ Error: Groq final response did not contain 'choices'. Full response: {final_data}"}
final_answer = final_data["choices"][0]["message"].get("content", "No final answer generated.")
return {"response": final_answer}
except Exception as e:
import traceback
error_trace = traceback.format_exc()
print("RUNTIME ERROR IN process_agent_chat:\n", error_trace)
return {"response": f"⚠️ Server Runtime Error: {str(e)}\n\nTraceback:\n{error_trace}"}