Spaces:
Sleeping
Sleeping
| """ | |
| Google Services — Calendar, Maps, and Gmail integrations. | |
| """ | |
| import os | |
| import base64 | |
| import json | |
| import math | |
| import uuid | |
| from datetime import datetime, timedelta | |
| from email.mime.text import MIMEText | |
| from email.mime.multipart import MIMEMultipart | |
| from email.mime.base import MIMEBase | |
| from email import encoders | |
| from google.oauth2 import service_account | |
| from google.oauth2.credentials import Credentials | |
| from googleapiclient.discovery import build | |
| import googlemaps | |
| import config | |
| from core import settings_manager as settings | |
| def get_secret(key: str) -> str: | |
| """Retrieve a secret from environment variables.""" | |
| return os.environ.get(key) | |
| # ============================================================ | |
| # Google Calendar | |
| # ============================================================ | |
| def get_calendar_service(): | |
| """Initialize and return a Google Calendar API service client. | |
| Uses GOOGLE_CALENDAR_CREDENTIALS from secrets (base64-encoded service account JSON). | |
| """ | |
| credentials_b64 = get_secret("GOOGLE_CALENDAR_CREDENTIALS") | |
| credentials_json = base64.b64decode(credentials_b64).decode("utf-8") | |
| credentials_info = json.loads(credentials_json) | |
| credentials = service_account.Credentials.from_service_account_info( | |
| credentials_info, | |
| scopes=["https://www.googleapis.com/auth/calendar"] | |
| ) | |
| service = build("calendar", "v3", credentials=credentials) | |
| return service | |
| def create_calendar_event(summary: str, description: str, location: str, start_datetime: str, end_datetime: str) -> str: | |
| """Create an event on the shared Google Calendar. | |
| Args: | |
| summary: Event title, e.g., "[Level 1] Robert Williams — John Moustoukas, MD" | |
| description: Event details (addresses, contact info) | |
| location: Visit address | |
| start_datetime: ISO format datetime string | |
| end_datetime: ISO format datetime string | |
| Returns: | |
| The created event's Google Calendar event ID | |
| """ | |
| service = get_calendar_service() | |
| calendar_id = get_secret("SHARED_CALENDAR_ID") | |
| event = { | |
| "summary": summary, | |
| "description": description, | |
| "location": location, | |
| "start": { | |
| "dateTime": start_datetime, | |
| "timeZone": settings.get_timezone(), | |
| }, | |
| "end": { | |
| "dateTime": end_datetime, | |
| "timeZone": settings.get_timezone(), | |
| }, | |
| } | |
| created_event = service.events().insert(calendarId=calendar_id, body=event).execute() | |
| return created_event["id"] | |
| def delete_calendar_event(event_id: str) -> bool: | |
| """Delete an event from the shared Google Calendar. | |
| Returns True if successful. | |
| """ | |
| try: | |
| service = get_calendar_service() | |
| calendar_id = get_secret("SHARED_CALENDAR_ID") | |
| service.events().delete(calendarId=calendar_id, eventId=event_id).execute() | |
| return True | |
| except Exception: | |
| return False | |
| def get_calendar_events(date: str) -> list[dict]: | |
| """Fetch all events from the shared calendar for a given date (YYYY-MM-DD). | |
| Returns list of event dicts with 'summary', 'start', 'end', 'id'. | |
| """ | |
| service = get_calendar_service() | |
| calendar_id = get_secret("SHARED_CALENDAR_ID") | |
| # Create time bounds for the given date | |
| date_obj = datetime.strptime(date, "%Y-%m-%d") | |
| time_min = date_obj.isoformat()+"Z"# + "T00:00:00Z" claude messed up here | |
| time_max = (date_obj + timedelta(days=1)).isoformat()+"Z"# + "T00:00:00Z" | |
| print(time_min, time_max) | |
| events_result = service.events().list( | |
| calendarId=calendar_id, | |
| timeMin=time_min, | |
| timeMax=time_max, | |
| singleEvents=True, | |
| orderBy="startTime" | |
| ).execute() | |
| events = events_result.get("items", []) | |
| return [ | |
| { | |
| "id": event["id"], | |
| "summary": event.get("summary", ""), | |
| "start": event["start"].get("dateTime", event["start"].get("date")), | |
| "end": event["end"].get("dateTime", event["end"].get("date")), | |
| } | |
| for event in events | |
| ] | |
| # ============================================================ | |
| # Google Maps | |
| # ============================================================ | |
| def get_maps_client(): | |
| """Initialize and return a Google Maps client. | |
| Uses GOOGLE_MAPS_API_KEY from secrets. | |
| """ | |
| return googlemaps.Client(key=get_secret("GOOGLE_MAPS_API_KEY")) | |
| def calculate_travel_time(origin_address: str, destination_address: str, arrival_by: datetime) -> dict: | |
| """Calculate driving travel time between two addresses using traffic-aware two-pass estimation. | |
| Pass 1: Get rough travel duration (no departure_time). | |
| Pass 2: Estimate departure = arrival_by - rough duration, then re-query with departure_time | |
| to get duration_in_traffic. | |
| Final recommended departure = arrival_by - traffic-aware duration. | |
| Args: | |
| origin_address: Full address string | |
| destination_address: Full address string | |
| arrival_by: When the provider must arrive (appointment start datetime) | |
| Returns: | |
| Dict with 'travel_minutes' (int) and 'recommended_departure' (datetime). | |
| Returns travel_minutes=0 if same address. | |
| Returns travel_minutes=-1 if the API call fails. | |
| """ | |
| if origin_address.strip().lower() == destination_address.strip().lower(): | |
| return {"travel_minutes": 0, "recommended_departure": arrival_by} | |
| try: | |
| client = get_maps_client() | |
| # Pass 1: rough estimate (no traffic) | |
| result = client.distance_matrix( | |
| origins=[origin_address], | |
| destinations=[destination_address], | |
| mode="driving" | |
| ) | |
| element = result["rows"][0]["elements"][0] | |
| if element["status"] != "OK": | |
| return {"travel_minutes": -1, "recommended_departure": arrival_by} | |
| rough_seconds = element["duration"]["value"] | |
| rough_minutes = math.ceil(rough_seconds / 60) | |
| rough_departure = arrival_by - timedelta(minutes=rough_minutes) | |
| # Pass 2: traffic-aware estimate using rough departure time | |
| result = client.distance_matrix( | |
| origins=[origin_address], | |
| destinations=[destination_address], | |
| mode="driving", | |
| departure_time=rough_departure | |
| ) | |
| element = result["rows"][0]["elements"][0] | |
| if element["status"] != "OK": | |
| return {"travel_minutes": rough_minutes, "recommended_departure": arrival_by - timedelta(minutes=rough_minutes)} | |
| traffic_seconds = element.get("duration_in_traffic", element["duration"])["value"] | |
| traffic_minutes = math.ceil(traffic_seconds / 60) | |
| recommended_departure = arrival_by - timedelta(minutes=traffic_minutes) | |
| return {"travel_minutes": traffic_minutes, "recommended_departure": recommended_departure} | |
| except Exception: | |
| return {"travel_minutes": -1, "recommended_departure": arrival_by} | |
| def geocode_address(address: str) -> tuple[float, float] | None: | |
| """Convert an address string to (latitude, longitude). | |
| Returns None if geocoding fails. | |
| """ | |
| try: | |
| client = get_maps_client() | |
| result = client.geocode(address) | |
| if not result: | |
| return None | |
| location = result[0]["geometry"]["location"] | |
| return (location["lat"], location["lng"]) | |
| except Exception: | |
| return None | |
| def calculate_travel_times_batch(origin: str, destinations: list[str], arrival_by: datetime) -> list[dict]: | |
| """Calculate travel times from one origin to multiple destinations using traffic-aware two-pass estimation. | |
| Pass 1: Get rough durations for all destinations (no departure_time). | |
| Pass 2: Use the max rough duration to compute a single departure_time for the batch, | |
| then re-query to get duration_in_traffic per destination. | |
| Returns a list of dicts with 'travel_minutes' and 'recommended_departure', in the same order as destinations. | |
| """ | |
| if not destinations: | |
| return [] | |
| try: | |
| client = get_maps_client() | |
| # Pass 1: rough estimates (no traffic) | |
| result = client.distance_matrix( | |
| origins=[origin], | |
| destinations=destinations, | |
| mode="driving" | |
| ) | |
| rough_seconds_list = [] | |
| for element in result["rows"][0]["elements"]: | |
| if element["status"] == "OK": | |
| rough_seconds_list.append(element["duration"]["value"]) | |
| else: | |
| rough_seconds_list.append(-1) | |
| # Use max rough duration to compute a single departure_time for the batch | |
| valid_seconds = [s for s in rough_seconds_list if s >= 0] | |
| if not valid_seconds: | |
| return [{"travel_minutes": -1, "recommended_departure": arrival_by} for _ in destinations] | |
| max_rough_minutes = math.ceil(max(valid_seconds) / 60) | |
| rough_departure = arrival_by - timedelta(minutes=max_rough_minutes) | |
| # Pass 2: traffic-aware estimates | |
| result = client.distance_matrix( | |
| origins=[origin], | |
| destinations=destinations, | |
| mode="driving", | |
| departure_time=rough_departure | |
| ) | |
| travel_results = [] | |
| for element in result["rows"][0]["elements"]: | |
| if element["status"] == "OK": | |
| traffic_seconds = element.get("duration_in_traffic", element["duration"])["value"] | |
| traffic_minutes = math.ceil(traffic_seconds / 60) | |
| travel_results.append({ | |
| "travel_minutes": traffic_minutes, | |
| "recommended_departure": arrival_by - timedelta(minutes=traffic_minutes), | |
| }) | |
| else: | |
| travel_results.append({"travel_minutes": -1, "recommended_departure": arrival_by}) | |
| return travel_results | |
| except Exception: | |
| return [{"travel_minutes": -1, "recommended_departure": arrival_by} for _ in destinations] | |
| # ============================================================ | |
| # Gmail | |
| # ============================================================ | |
| def _build_ics(appointment_details: dict, method: str = "REQUEST") -> str: | |
| """Build an .ics calendar string from appointment details. | |
| Args: | |
| appointment_details: Dict with date, start_time, end_time, patient_name, | |
| provider_name, address. | |
| method: VCALENDAR METHOD — "REQUEST" for new/updated, "CANCEL" for cancellations. | |
| """ | |
| appt_date = appointment_details.get("date", "") | |
| start_time = appointment_details.get("start_time", "00:00") | |
| end_time = appointment_details.get("end_time", "00:00") | |
| patient_name = appointment_details.get("patient_name", "") | |
| provider_name = appointment_details.get("provider_name", "") | |
| address = appointment_details.get("address", "") | |
| dt_start = datetime.strptime(f"{appt_date} {start_time}", "%Y-%m-%d %H:%M") | |
| dt_end = datetime.strptime(f"{appt_date} {end_time}", "%Y-%m-%d %H:%M") | |
| now = datetime.utcnow() | |
| uid = uuid.uuid5(uuid.NAMESPACE_URL, f"{appt_date}-{start_time}-{patient_name}-{provider_name}") | |
| return ( | |
| "BEGIN:VCALENDAR\r\n" | |
| "VERSION:2.0\r\n" | |
| "PRODID:-//SOS Care Now//Scheduling//EN\r\n" | |
| f"METHOD:{method}\r\n" | |
| "BEGIN:VEVENT\r\n" | |
| f"UID:{uid}\r\n" | |
| f"DTSTAMP:{now:%Y%m%dT%H%M%SZ}\r\n" | |
| f"DTSTART;TZID={settings.get_timezone()}:{dt_start:%Y%m%dT%H%M%S}\r\n" | |
| f"DTEND;TZID={settings.get_timezone()}:{dt_end:%Y%m%dT%H%M%S}\r\n" | |
| f"SUMMARY:{config.ORG_NAME} — {patient_name} with {provider_name}\r\n" | |
| f"LOCATION:{address}\r\n" | |
| f"STATUS:{'CANCELLED' if method == 'CANCEL' else 'CONFIRMED'}\r\n" | |
| "END:VEVENT\r\n" | |
| "END:VCALENDAR\r\n" | |
| ) | |
| def get_gmail_service(): | |
| """Initialize and return a Gmail API service client. | |
| Uses GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, and GMAIL_REFRESH_TOKEN from | |
| secrets. The refresh token is exchanged for a short-lived access token by | |
| the underlying google-auth library on each request. | |
| """ | |
| creds = Credentials.from_authorized_user_info( | |
| { | |
| "client_id": get_secret("GMAIL_CLIENT_ID"), | |
| "client_secret": get_secret("GMAIL_CLIENT_SECRET"), | |
| "refresh_token": get_secret("GMAIL_REFRESH_TOKEN"), | |
| }, | |
| scopes=["https://www.googleapis.com/auth/gmail.send"], | |
| ) | |
| return build("gmail", "v1", credentials=creds, cache_discovery=False) | |
| def send_email(to_address: str, subject: str, body: str, ics_content: str | None = None) -> bool: | |
| """Send an email via the Gmail API. | |
| Uses GMAIL_ADDRESS as the From header and the OAuth credentials in | |
| GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN. | |
| If ics_content is provided, attaches it as a .ics calendar invite. | |
| Returns True if sent successfully. | |
| """ | |
| try: | |
| gmail_address = get_secret("GMAIL_ADDRESS") | |
| message = MIMEMultipart() | |
| message["From"] = gmail_address | |
| message["To"] = to_address | |
| message["Subject"] = subject | |
| message.attach(MIMEText(body, "plain")) | |
| if ics_content: | |
| ics_part = MIMEBase("text", "calendar", method="REQUEST", name="invite.ics") | |
| ics_part.set_payload(ics_content.encode("utf-8")) | |
| encoders.encode_base64(ics_part) | |
| ics_part.add_header("Content-Disposition", "attachment", filename="invite.ics") | |
| message.attach(ics_part) | |
| raw = base64.urlsafe_b64encode(message.as_bytes()).decode("utf-8") | |
| service = get_gmail_service() | |
| service.users().messages().send(userId="me", body={"raw": raw}).execute() | |
| return True | |
| except Exception: | |
| return False | |
| def send_scheduling_notification(provider_email: str, patient_email: str, appointment_details: dict) -> dict: | |
| """Send scheduling confirmation emails to both the provider and patient. | |
| Args: | |
| provider_email: Provider's email | |
| patient_email: Patient's email | |
| appointment_details: Dict with keys: patient_name, provider_name, date, start_time, end_time, visit_level, address | |
| Returns: | |
| Dict with 'provider_sent': bool, 'patient_sent': bool | |
| """ | |
| patient_name = appointment_details.get("patient_name", "Patient") | |
| provider_name = appointment_details.get("provider_name", "Provider") | |
| date = appointment_details.get("date", "") | |
| start_time = appointment_details.get("start_time", "") | |
| end_time = appointment_details.get("end_time", "") | |
| visit_level = appointment_details.get("visit_level", "") | |
| address = appointment_details.get("address", "") | |
| recommended_departure = appointment_details.get("recommended_departure_time", "") | |
| # Email to provider | |
| provider_subject = f"{config.ORG_NAME} - New Appointment Scheduled" | |
| departure_line = f"\nRecommended Departure Time: {recommended_departure}" if recommended_departure else "" | |
| provider_body = f"""Hello {provider_name}, | |
| A new appointment has been scheduled: | |
| Patient: {patient_name} | |
| Date: {date} | |
| Time: {start_time} - {end_time} | |
| Visit Level: {visit_level} | |
| Address: {address}{departure_line} | |
| Please ensure you arrive on time. | |
| Best regards, | |
| {config.ORG_NAME} | |
| """ | |
| # Email to patient | |
| patient_subject = f"{config.ORG_NAME} - Appointment Confirmation" | |
| patient_body = f"""Hello {patient_name}, | |
| Your appointment has been confirmed: | |
| Provider: {provider_name} | |
| Date: {date} | |
| Time: {start_time} - {end_time} | |
| Your provider will visit you at: | |
| {address} | |
| If you need to reschedule or cancel, please contact us as soon as possible. | |
| Best regards, | |
| {config.ORG_NAME} | |
| """ | |
| ics = _build_ics(appointment_details) | |
| provider_sent = send_email(provider_email, provider_subject, provider_body, ics) if provider_email else False | |
| patient_sent = send_email(patient_email, patient_subject, patient_body, ics) if patient_email else False | |
| return {"provider_sent": provider_sent, "patient_sent": patient_sent} | |
| def send_cancellation_notification(provider_email: str, patient_email: str, appointment_details: dict) -> dict: | |
| """Send cancellation notification emails to both the provider and patient. | |
| Returns: | |
| Dict with 'provider_sent': bool, 'patient_sent': bool | |
| """ | |
| patient_name = appointment_details.get("patient_name", "Patient") | |
| provider_name = appointment_details.get("provider_name", "Provider") | |
| date = appointment_details.get("date", "") | |
| start_time = appointment_details.get("start_time", "") | |
| end_time = appointment_details.get("end_time", "") | |
| # Email to provider | |
| provider_subject = f"{config.ORG_NAME} - Appointment Cancelled" | |
| provider_body = f"""Hello {provider_name}, | |
| The following appointment has been cancelled: | |
| Patient: {patient_name} | |
| Date: {date} | |
| Time: {start_time} - {end_time} | |
| This time slot is now available for other appointments. | |
| Best regards, | |
| {config.ORG_NAME} | |
| """ | |
| # Email to patient | |
| patient_subject = f"{config.ORG_NAME} - Appointment Cancellation Confirmation" | |
| patient_body = f"""Hello {patient_name}, | |
| Your appointment has been cancelled: | |
| Provider: {provider_name} | |
| Date: {date} | |
| Time: {start_time} - {end_time} | |
| If you would like to reschedule, please contact us. | |
| Best regards, | |
| {config.ORG_NAME} | |
| """ | |
| ics = _build_ics(appointment_details, method="CANCEL") | |
| provider_sent = send_email(provider_email, provider_subject, provider_body, ics) if provider_email else False | |
| patient_sent = send_email(patient_email, patient_subject, patient_body, ics) if patient_email else False | |
| return {"provider_sent": provider_sent, "patient_sent": patient_sent} | |