|
|
import os
|
|
|
from datetime import datetime, timedelta
|
|
|
from typing import List, Optional, Dict, Any
|
|
|
from dotenv import load_dotenv
|
|
|
from supabase import create_client, Client
|
|
|
from cache import cache
|
|
|
import uuid
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
|
|
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
|
|
|
|
|
class Database:
|
|
|
def __init__(self):
|
|
|
self.client = None
|
|
|
if SUPABASE_URL and SUPABASE_KEY:
|
|
|
try:
|
|
|
self.client: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
|
except Exception as e:
|
|
|
print(f"Failed to initialize Supabase client: {e}")
|
|
|
|
|
|
|
|
|
self.mock_users = [
|
|
|
{"contact_number": "5550101", "name": "Alice Test", "created_at": datetime.now().isoformat()},
|
|
|
{"contact_number": "9730102", "name": "Naresh", "created_at": datetime.now().isoformat()}
|
|
|
]
|
|
|
self.mock_appointments = [
|
|
|
{
|
|
|
"id": "mock_apt_1",
|
|
|
"contact_number": "555-0101",
|
|
|
"appointment_time": "2026-01-22T10:00:00",
|
|
|
"status": "confirmed",
|
|
|
"purpose": "Checkup",
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
}
|
|
|
]
|
|
|
self.mock_summaries = []
|
|
|
self.mock_chat_messages = []
|
|
|
self.cache = cache
|
|
|
|
|
|
|
|
|
self.mock_slots = []
|
|
|
base_time = datetime.now().replace(minute=0, second=0, microsecond=0)
|
|
|
for d in range(1, 11):
|
|
|
day = base_time + timedelta(days=d)
|
|
|
for h in [9, 10, 14, 16]:
|
|
|
slot_time = day.replace(hour=h).isoformat()
|
|
|
self.mock_slots.append({"slot_time": slot_time, "is_booked": False})
|
|
|
|
|
|
def get_available_slots(self) -> List[str]:
|
|
|
"""Get list of available slot times."""
|
|
|
if self.client:
|
|
|
try:
|
|
|
response = self.client.table("appointment_slots")\
|
|
|
.select("slot_time")\
|
|
|
.eq("is_booked", False)\
|
|
|
.gt("slot_time", datetime.now().isoformat())\
|
|
|
.order("slot_time")\
|
|
|
.execute()
|
|
|
return [row["slot_time"] for row in response.data]
|
|
|
except Exception as e:
|
|
|
print(f"Error fetching slots from DB: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
now_str = datetime.now().isoformat()
|
|
|
return [s["slot_time"] for s in self.mock_slots if not s["is_booked"] and s["slot_time"] > now_str]
|
|
|
|
|
|
def get_user(self, contact_number: str) -> Optional[Dict[str, Any]]:
|
|
|
"""Check if a user exists by contact number (with caching)."""
|
|
|
|
|
|
contact_number = "".join(filter(str.isdigit, str(contact_number)))
|
|
|
|
|
|
|
|
|
cache_key = f"user:{contact_number}"
|
|
|
cached_user = self.cache.get(cache_key)
|
|
|
if cached_user:
|
|
|
return cached_user
|
|
|
|
|
|
if self.client:
|
|
|
try:
|
|
|
response = self.client.table("users").select("*").eq("contact_number", contact_number).execute()
|
|
|
if response.data:
|
|
|
user = response.data[0]
|
|
|
|
|
|
self.cache.set(cache_key, user, ttl=3600)
|
|
|
return user
|
|
|
except Exception as e:
|
|
|
print(f"Error fetching user from DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
for user in self.mock_users:
|
|
|
if user["contact_number"] == contact_number:
|
|
|
return user
|
|
|
return None
|
|
|
|
|
|
def create_user(self, contact_number: str, name: str = "Unknown") -> Optional[Dict[str, Any]]:
|
|
|
"""Create a new user."""
|
|
|
if self.client:
|
|
|
try:
|
|
|
data = {"contact_number": contact_number, "name": name}
|
|
|
|
|
|
response = self.client.table("users").upsert(data).execute()
|
|
|
if response.data:
|
|
|
return response.data[0]
|
|
|
except Exception as e:
|
|
|
print(f"Error creating user in DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
new_user = {"contact_number": contact_number, "name": name, "created_at": datetime.now().isoformat()}
|
|
|
self.mock_users.append(new_user)
|
|
|
return new_user
|
|
|
|
|
|
def get_user_appointments(self, contact_number: str) -> List[Dict[str, Any]]:
|
|
|
"""Fetch past and upcoming appointments for a user."""
|
|
|
if self.client:
|
|
|
try:
|
|
|
response = self.client.table("appointments")\
|
|
|
.select("*")\
|
|
|
.eq("contact_number", contact_number)\
|
|
|
.order("appointment_time", desc=True)\
|
|
|
.execute()
|
|
|
return response.data
|
|
|
except Exception as e:
|
|
|
print(f"Error fetching appointments from DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
return [
|
|
|
apt for apt in self.mock_appointments
|
|
|
if apt["contact_number"] == contact_number and apt["status"] != "cancelled"
|
|
|
]
|
|
|
|
|
|
def check_slot_availability(self, appointment_time: datetime) -> bool:
|
|
|
"""Check if a slot is valid and available."""
|
|
|
time_str = appointment_time.isoformat()
|
|
|
|
|
|
if self.client:
|
|
|
try:
|
|
|
|
|
|
response = self.client.table("appointment_slots")\
|
|
|
.select("*")\
|
|
|
.eq("slot_time", time_str)\
|
|
|
.eq("is_booked", False)\
|
|
|
.execute()
|
|
|
|
|
|
return len(response.data) > 0
|
|
|
except Exception as e:
|
|
|
print(f"Error checking availability in DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
for slot in self.mock_slots:
|
|
|
if slot["slot_time"] == time_str:
|
|
|
return not slot["is_booked"]
|
|
|
return False
|
|
|
|
|
|
def book_appointment(self, contact_number: str, appointment_time: str, purpose: str = "General") -> Optional[Dict[str, Any]]:
|
|
|
"""Book an appointment and mark slot as booked."""
|
|
|
if self.client:
|
|
|
try:
|
|
|
|
|
|
data = {
|
|
|
"contact_number": contact_number,
|
|
|
"appointment_time": appointment_time,
|
|
|
"status": "confirmed",
|
|
|
"purpose": purpose
|
|
|
}
|
|
|
response = self.client.table("appointments").insert(data).execute()
|
|
|
|
|
|
|
|
|
self.client.table("appointment_slots")\
|
|
|
.update({"is_booked": True})\
|
|
|
.eq("slot_time", appointment_time)\
|
|
|
.execute()
|
|
|
|
|
|
if response.data:
|
|
|
return response.data[0]
|
|
|
except Exception as e:
|
|
|
print(f"Error booking appointment in DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
import random
|
|
|
apt_id = f"APT-{random.randint(1000, 9999)}"
|
|
|
new_apt = {
|
|
|
"id": apt_id,
|
|
|
"contact_number": contact_number,
|
|
|
"appointment_time": appointment_time,
|
|
|
"status": "confirmed",
|
|
|
"purpose": purpose,
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
}
|
|
|
self.mock_appointments.append(new_apt)
|
|
|
|
|
|
|
|
|
for slot in self.mock_slots:
|
|
|
if slot["slot_time"] == appointment_time:
|
|
|
slot["is_booked"] = True
|
|
|
|
|
|
return new_apt
|
|
|
|
|
|
def cancel_appointment(self, appointment_id: str) -> bool:
|
|
|
"""Cancel an appointment."""
|
|
|
if self.client:
|
|
|
try:
|
|
|
response = self.client.table("appointments")\
|
|
|
.update({"status": "cancelled"})\
|
|
|
.eq("id", appointment_id)\
|
|
|
.execute()
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
print(f"Error cancelling appointment in DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
for apt in self.mock_appointments:
|
|
|
if apt["id"] == appointment_id:
|
|
|
apt["status"] = "cancelled"
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
def modify_appointment(self, appointment_id: str, new_time: str) -> bool:
|
|
|
"""Modify appointment time."""
|
|
|
if self.client:
|
|
|
try:
|
|
|
response = self.client.table("appointments")\
|
|
|
.update({"appointment_time": new_time})\
|
|
|
.eq("id", appointment_id)\
|
|
|
.execute()
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
print(f"Error modifying appointment in DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
for apt in self.mock_appointments:
|
|
|
if apt["id"] == appointment_id:
|
|
|
apt["appointment_time"] = new_time
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
def save_summary(self, contact_number: str, summary: str) -> bool:
|
|
|
"""Save the conversation summary."""
|
|
|
if self.client:
|
|
|
try:
|
|
|
data = {
|
|
|
"contact_number": contact_number,
|
|
|
"summary": summary,
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
}
|
|
|
|
|
|
self.client.table("conversations").insert(data).execute()
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
print(f"Error saving summary in DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
print(f"Mock saving summary for {contact_number}: {summary}")
|
|
|
self.mock_summaries.append({
|
|
|
"contact_number": contact_number,
|
|
|
"summary": summary,
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
})
|
|
|
return True
|
|
|
|
|
|
def save_chat_message(self, session_id: str, contact_number: str, role: str, content: str, tool_name: str = None, tool_args: dict = None) -> bool:
|
|
|
"""Save a single chat message to the database"""
|
|
|
if self.client:
|
|
|
try:
|
|
|
data = {
|
|
|
"session_id": session_id,
|
|
|
"contact_number": contact_number,
|
|
|
"role": role,
|
|
|
"content": content,
|
|
|
"tool_name": tool_name,
|
|
|
"tool_args": tool_args,
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
}
|
|
|
self.client.table("chat_messages").insert(data).execute()
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
print(f"Error saving chat message to DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
self.mock_chat_messages.append({
|
|
|
"session_id": session_id,
|
|
|
"contact_number": contact_number,
|
|
|
"role": role,
|
|
|
"content": content,
|
|
|
"tool_name": tool_name,
|
|
|
"tool_args": tool_args,
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
})
|
|
|
return True
|
|
|
|
|
|
def save_chat_transcript(self, session_id: str, contact_number: str, messages: list) -> bool:
|
|
|
"""Save entire chat transcript (batch insert)"""
|
|
|
if not messages:
|
|
|
return False
|
|
|
|
|
|
if self.client:
|
|
|
try:
|
|
|
|
|
|
data = []
|
|
|
for msg in messages:
|
|
|
data.append({
|
|
|
"session_id": session_id,
|
|
|
"contact_number": contact_number,
|
|
|
"role": msg.get("role"),
|
|
|
"content": msg.get("content"),
|
|
|
"tool_name": msg.get("tool_name"),
|
|
|
"tool_args": msg.get("tool_args"),
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
})
|
|
|
|
|
|
|
|
|
self.client.table("chat_messages").insert(data).execute()
|
|
|
print(f"✅ Saved {len(data)} chat messages to database")
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
print(f"Error saving chat transcript to DB (falling back to mock): {e}")
|
|
|
|
|
|
|
|
|
for msg in messages:
|
|
|
self.mock_chat_messages.append({
|
|
|
"session_id": session_id,
|
|
|
"contact_number": contact_number,
|
|
|
"role": msg.get("role"),
|
|
|
"content": msg.get("content"),
|
|
|
"tool_name": msg.get("tool_name"),
|
|
|
"tool_args": msg.get("tool_args"),
|
|
|
"created_at": datetime.now().isoformat()
|
|
|
})
|
|
|
print(f"Mock saved {len(messages)} chat messages")
|
|
|
return True
|
|
|
|
|
|
def get_chat_history(self, contact_number: str, limit: int = 100) -> list:
|
|
|
"""Get chat history for a user"""
|
|
|
if self.client:
|
|
|
try:
|
|
|
response = self.client.table("chat_messages")\
|
|
|
.select("*")\
|
|
|
.eq("contact_number", contact_number)\
|
|
|
.order("created_at", desc=True)\
|
|
|
.limit(limit)\
|
|
|
.execute()
|
|
|
return response.data if response.data else []
|
|
|
except Exception as e:
|
|
|
print(f"Error fetching chat history: {e}")
|
|
|
|
|
|
|
|
|
return [msg for msg in self.mock_chat_messages if msg["contact_number"] == contact_number][-limit:]
|
|
|
|
|
|
|
|
|
AVAILABLE_SLOTS = [
|
|
|
"2026-01-22T09:00:00",
|
|
|
"2026-01-22T10:00:00",
|
|
|
"2026-01-22T14:00:00",
|
|
|
"2026-01-23T11:00:00",
|
|
|
"2026-01-23T15:00:00"
|
|
|
]
|
|
|
|