Spaces:
Sleeping
Sleeping
| # main.py | |
| from fastapi import FastAPI, Request, HTTPException, Body | |
| from fastapi.exceptions import RequestValidationError | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel, Field, ValidationError | |
| from googleapiclient.discovery import build | |
| from google.oauth2 import service_account | |
| from googleapiclient.errors import HttpError | |
| import os | |
| import uuid | |
| from datetime import datetime, timedelta | |
| from typing import List, Dict, Optional | |
| from dotenv import load_dotenv | |
| import logging | |
| import json | |
| # Load .env file (if it exists) | |
| load_dotenv() | |
| app = FastAPI(title="Vapi β Google Calendar & Meet Tool") | |
| # Add exception handler for validation errors to log what VAPI is actually sending | |
| async def validation_exception_handler(request: Request, exc: RequestValidationError): | |
| body = await request.body() | |
| body_str = body.decode('utf-8') if body else "No body" | |
| logging.error(f"Validation error on {request.url.path}: {exc.errors()}") | |
| logging.error(f"Request body: {body_str}") | |
| # Try to parse VAPI's format and convert it | |
| try: | |
| received_data = json.loads(body_str) if body_str else {} | |
| # Check if VAPI sent the message wrapper format | |
| if "message" in received_data and "toolCallList" in received_data["message"]: | |
| # Extract and convert VAPI format to our format | |
| vapi_tool_calls = received_data["message"]["toolCallList"] | |
| converted_tool_calls = [] | |
| for vapi_call in vapi_tool_calls: | |
| if "function" in vapi_call and isinstance(vapi_call["function"], dict): | |
| # VAPI format: {"id": "...", "function": {"name": "...", "arguments": {...}}} | |
| converted_call = { | |
| "id": vapi_call.get("id", "unknown"), | |
| "toolName": vapi_call["function"].get("name"), | |
| "arguments": vapi_call["function"].get("arguments", {}) | |
| } | |
| converted_tool_calls.append(converted_call) | |
| if converted_tool_calls: | |
| # Create a new request with converted format and process it | |
| converted_request = {"toolCallList": converted_tool_calls} | |
| # Re-parse with our model | |
| try: | |
| parsed_request = VapiRequest(**converted_request) | |
| # Process the request by calling the endpoint handler directly | |
| # We'll handle this in the endpoint itself instead | |
| logging.info(f"Successfully converted VAPI format: {json.dumps(converted_request, indent=2)}") | |
| except Exception as e: | |
| logging.error(f"Failed to parse converted request: {e}") | |
| return JSONResponse( | |
| status_code=422, | |
| content={ | |
| "detail": exc.errors(), | |
| "received_body": received_data, | |
| "expected_format": { | |
| "toolCallList": [ | |
| { | |
| "id": "call_xxx", | |
| "toolName": "check_availability or schedule_meeting", | |
| "arguments": { | |
| "date": "YYYY-MM-DD", | |
| "start_time": "HH:MM", | |
| "end_time": "HH:MM" | |
| } | |
| } | |
| ] | |
| } | |
| } | |
| ) | |
| except Exception as e: | |
| logging.error(f"Error in validation handler: {e}") | |
| return JSONResponse( | |
| status_code=422, | |
| content={ | |
| "detail": exc.errors(), | |
| "received_body": body_str[:500], | |
| "message": "Request validation failed. Expected format: {'toolCallList': [{'id': '...', 'toolName': '...', 'arguments': {...}}]}" | |
| } | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CONFIG & GOOGLE SERVICE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SCOPES = ["https://www.googleapis.com/auth/calendar"] | |
| def get_calendar_service(): | |
| creds_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS") | |
| if not creds_path: | |
| raise RuntimeError( | |
| "GOOGLE_APPLICATION_CREDENTIALS environment variable is not set.\n" | |
| "1) Add it to .env file (local dev)\n" | |
| "2) Set it in your hosting platform dashboard (production)" | |
| ) | |
| if not os.path.exists(creds_path): | |
| raise FileNotFoundError(f"Service account JSON file not found: {creds_path}") | |
| try: | |
| credentials = service_account.Credentials.from_service_account_file( | |
| creds_path, | |
| scopes=SCOPES | |
| ) | |
| service = build("calendar", "v3", credentials=credentials) | |
| return service | |
| except Exception as e: | |
| logging.error(f"Failed to load Google Calendar credentials: {e}") | |
| raise | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MODELS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ToolCall(BaseModel): | |
| id: str | |
| toolName: Optional[str] = None | |
| type: Optional[str] = None # VAPI sometimes sends "type": "function" | |
| function: Optional[Dict] = None # VAPI sends function details here | |
| arguments: Optional[Dict] = None # Make optional since VAPI puts it in function.arguments | |
| class VapiRequest(BaseModel): | |
| toolCallList: Optional[List[ToolCall]] = None | |
| message: Optional[Dict] = None # VAPI sometimes wraps in "message" object | |
| class Config: | |
| extra = "allow" # Allow extra fields like "message" wrapper | |
| # No default example - each endpoint defines its own example | |
| class ToolCallResult(BaseModel): | |
| toolCallId: str | |
| result: Dict | str | |
| class VapiResponse(BaseModel): | |
| results: List[ToolCallResult] | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HELPERS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def make_rfc3339(date: str, time_str: str, tz: str = "Europe/Oslo") -> str: | |
| """YYYY-MM-DD + HH:MM β RFC3339 string""" | |
| dt = datetime.strptime(f"{date} {time_str}", "%Y-%m-%d %H:%M") | |
| return dt.isoformat() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ENDPOINTS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def check_availability( | |
| request: Request, | |
| payload: Optional[VapiRequest] = Body( | |
| None, | |
| examples={ | |
| "check_availability": { | |
| "summary": "Check Availability Example", | |
| "description": "Example request for checking calendar availability. calendar_id is optional and defaults to tahasaif454@gmail.com", | |
| "value": { | |
| "toolCallList": [ | |
| { | |
| "id": "call_abc123", | |
| "toolName": "check_availability", | |
| "arguments": { | |
| "date": "2026-01-25", | |
| "start_time": "18:00", | |
| "end_time": "19:00" | |
| } | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| ) | |
| ): | |
| tool_call = None | |
| args = None | |
| tool_call_id = "unknown" | |
| try: | |
| # Get raw body to handle VAPI's format | |
| body = await request.body() | |
| body_str = body.decode('utf-8') if body else "" | |
| # Try to parse VAPI's format if payload is None or doesn't have toolCallList | |
| if payload is None or not payload.toolCallList: | |
| try: | |
| received_data = json.loads(body_str) if body_str else {} | |
| # Handle VAPI's message wrapper format | |
| if "message" in received_data: | |
| message_data = received_data["message"] | |
| if "toolCallList" in message_data: | |
| vapi_tool_calls = message_data["toolCallList"] | |
| if vapi_tool_calls and len(vapi_tool_calls) > 0: | |
| vapi_call = vapi_tool_calls[0] | |
| tool_call_id = vapi_call.get("id", "unknown") | |
| # Extract arguments from VAPI's function format | |
| if "function" in vapi_call and isinstance(vapi_call["function"], dict): | |
| args = vapi_call["function"].get("arguments", {}) | |
| elif "arguments" in vapi_call: | |
| args = vapi_call["arguments"] | |
| else: | |
| args = {} | |
| logging.info(f"Parsed VAPI format: id={tool_call_id}, args={args}") | |
| except json.JSONDecodeError as e: | |
| logging.error(f"Failed to parse JSON: {e}") | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId="unknown", result=f"Invalid JSON format: {str(e)}") | |
| ]) | |
| # If we still don't have args, try to get from payload | |
| if args is None: | |
| if payload is None: | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId="unknown", result="Request body is empty or invalid format.") | |
| ]) | |
| tool_calls = payload.toolCallList | |
| # If toolCallList is None, check if it's in the message wrapper | |
| if not tool_calls and payload.message: | |
| message_data = payload.message | |
| if isinstance(message_data, dict) and "toolCallList" in message_data: | |
| tool_calls = message_data.get("toolCallList", []) | |
| if not tool_calls: | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId="unknown", result="No tool calls in request") | |
| ]) | |
| tool_call = tool_calls[0] | |
| tool_call_id = tool_call.id | |
| # Handle VAPI's function format: extract arguments from function.arguments if needed | |
| if tool_call.function and isinstance(tool_call.function, dict): | |
| # VAPI sends: {"function": {"name": "...", "arguments": {...}}} | |
| args = tool_call.function.get("arguments", {}) | |
| elif tool_call.arguments: | |
| args = tool_call.arguments | |
| else: | |
| args = {} | |
| if not args: | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId=tool_call_id, result="No arguments found in request") | |
| ]) | |
| date = args.get("date") # "2025-02-10" | |
| start_time = args.get("start_time") # "14:30" | |
| end_time = args.get("end_time") # "15:00" | |
| # Default to the user's calendar if not specified in the arguments | |
| calendar_id = args.get("calendar_id", "robinsonjalutmotte@gmail.com") | |
| if not all([date, start_time, end_time]): | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result="Missing required fields: date, start_time, end_time" | |
| ) | |
| ]) | |
| # Validate date format and check if it's in the past | |
| try: | |
| parsed_date = datetime.strptime(date, "%Y-%m-%d").date() | |
| today = datetime.now().date() | |
| # Log the received date for debugging | |
| logging.info(f"check_availability - Received date: {date}, Parsed: {parsed_date}, Today: {today}") | |
| # Check if date is in the past (more than 1 day ago to allow for timezone differences) | |
| if parsed_date < today - timedelta(days=1): | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Error: The date {date} is in the past. Please provide a current or future date. Today is {today.strftime('%Y-%m-%d')}." | |
| ) | |
| ]) | |
| # Warn if date seems very old (like 2023 when it should be 2026) | |
| if parsed_date.year < 2024: | |
| logging.warning(f"Suspicious date received: {date} (year {parsed_date.year}). This might be a date parsing error.") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Error: The date {date} appears to be incorrect (year {parsed_date.year}). Please provide a current date. Today is {today.strftime('%Y-%m-%d')}. If you said 'tomorrow', please ensure the date is calculated correctly." | |
| ) | |
| ]) | |
| except ValueError as e: | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Invalid date format: {str(e)}. Use YYYY-MM-DD format (e.g., 2026-01-28). Received: {date}" | |
| ) | |
| ]) | |
| # Validate that end_time is after start_time | |
| try: | |
| start_dt = datetime.strptime(f"{date} {start_time}", "%Y-%m-%d %H:%M") | |
| end_dt = datetime.strptime(f"{date} {end_time}", "%Y-%m-%d %H:%M") | |
| if end_dt <= start_dt: | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Invalid time range: end_time ({end_time}) must be after start_time ({start_time}). Please provide a valid time range." | |
| ) | |
| ]) | |
| except ValueError as e: | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Invalid time format: {str(e)}. Use HH:MM format (24-hour time, e.g., 15:00 for 3 PM)." | |
| ) | |
| ]) | |
| # requested slot (CET is +01:00) | |
| start_rfc = make_rfc3339(date, start_time) + "+01:00" | |
| end_rfc = make_rfc3339(date, end_time) + "+01:00" | |
| # Full day range | |
| day_start = make_rfc3339(date, "00:00") + "+01:00" | |
| day_end = make_rfc3339(date, "23:59") + "+01:00" | |
| logging.info(f"Checking full day schedule on {calendar_id} for {date}") | |
| service = get_calendar_service() | |
| # Fetch ALL events for the day to provide guidance if slot is taken | |
| events_result = ( | |
| service.events() | |
| .list( | |
| calendarId=calendar_id, | |
| timeMin=day_start, | |
| timeMax=day_end, | |
| singleEvents=True, | |
| orderBy="startTime", | |
| ) | |
| .execute() | |
| ) | |
| all_day_events = events_result.get("items", []) | |
| # Check for conflicts in the specific slot | |
| conflicts = [] | |
| busy_slots = [] | |
| for event in all_day_events: | |
| e_start = event.get("start", {}).get("dateTime") or event.get("start", {}).get("date") | |
| e_end = event.get("end", {}).get("dateTime") or event.get("end", {}).get("date") | |
| # Simplified busy slot display for the bot | |
| # Convert ISO 8601 to HH:MM for easier reading | |
| try: | |
| display_start = datetime.fromisoformat(e_start).strftime("%H:%M") | |
| display_end = datetime.fromisoformat(e_end).strftime("%H:%M") | |
| busy_slots.append(f"{display_start} to {display_end}") | |
| except: | |
| busy_slots.append(f"{e_start} to {e_end}") | |
| # Overlap check: start < requested_end AND end > requested_start | |
| if e_start < end_rfc and e_end > start_rfc: | |
| conflicts.append(event.get("summary", "Untitled Appointment")) | |
| is_available = len(conflicts) == 0 | |
| if is_available: | |
| message = f"GREEN SIGNAL: The slot from {start_time} to {end_time} is available in Oslo time. You can proceed to schedule the meeting." | |
| else: | |
| busy_str = ", ".join(busy_slots) | |
| message = ( | |
| f"RED SIGNAL: The slot {start_time} to {end_time} is TAKEN. " | |
| f"Existing bookings on {date} (CET): {busy_str}. " | |
| "Please guide the user to a different time slot using this information." | |
| ) | |
| # VAPI requires result to be a string, not a dictionary | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId=tool_call_id, result=message) | |
| ]) | |
| except Exception as e: | |
| logging.exception("check_availability failed") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Error checking availability: {str(e)}" | |
| ) | |
| ]) | |
| async def schedule_meeting( | |
| request: Request, | |
| payload: Optional[VapiRequest] = Body( | |
| None, | |
| examples={ | |
| "schedule_meeting": { | |
| "summary": "Schedule Meeting Example", | |
| "description": "Example request for scheduling a meeting. calendar_id is optional and defaults to tahasaif454@gmail.com", | |
| "value": { | |
| "toolCallList": [ | |
| { | |
| "id": "call_xyz789", | |
| "toolName": "schedule_meeting", | |
| "arguments": { | |
| "date": "2026-01-25", | |
| "start_time": "18:00", | |
| "end_time": "19:00", | |
| "title": "Innoscribe-Demo", | |
| "description": "Demo session facilitated by Vapi Bot", | |
| "attendees": ["user@example.com"] | |
| } | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| ) | |
| ): | |
| tool_call = None | |
| args = None | |
| tool_call_id = "unknown" | |
| try: | |
| # Get raw body to handle VAPI's format | |
| body = await request.body() | |
| body_str = body.decode('utf-8') if body else "" | |
| # Try to parse VAPI's format if payload is None or doesn't have toolCallList | |
| if payload is None or not payload.toolCallList: | |
| try: | |
| received_data = json.loads(body_str) if body_str else {} | |
| # Handle VAPI's message wrapper format | |
| if "message" in received_data: | |
| message_data = received_data["message"] | |
| if "toolCallList" in message_data: | |
| vapi_tool_calls = message_data["toolCallList"] | |
| if vapi_tool_calls and len(vapi_tool_calls) > 0: | |
| vapi_call = vapi_tool_calls[0] | |
| tool_call_id = vapi_call.get("id", "unknown") | |
| # Extract arguments from VAPI's function format | |
| if "function" in vapi_call and isinstance(vapi_call["function"], dict): | |
| args = vapi_call["function"].get("arguments", {}) | |
| elif "arguments" in vapi_call: | |
| args = vapi_call["arguments"] | |
| else: | |
| args = {} | |
| logging.info(f"Parsed VAPI format: id={tool_call_id}, args={args}") | |
| except json.JSONDecodeError as e: | |
| logging.error(f"Failed to parse JSON: {e}") | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId="unknown", result=f"Invalid JSON format: {str(e)}") | |
| ]) | |
| # If we still don't have args, try to get from payload | |
| if args is None: | |
| if payload is None: | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId="unknown", result="Request body is empty or invalid format.") | |
| ]) | |
| tool_calls = payload.toolCallList | |
| # If toolCallList is None, check if it's in the message wrapper | |
| if not tool_calls and payload.message: | |
| message_data = payload.message | |
| if isinstance(message_data, dict) and "toolCallList" in message_data: | |
| tool_calls = message_data.get("toolCallList", []) | |
| if not tool_calls: | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId="unknown", result="No tool calls in request") | |
| ]) | |
| tool_call = tool_calls[0] | |
| tool_call_id = tool_call.id | |
| # Handle VAPI's function format: extract arguments from function.arguments if needed | |
| if tool_call.function and isinstance(tool_call.function, dict): | |
| # VAPI sends: {"function": {"name": "...", "arguments": {...}}} | |
| args = tool_call.function.get("arguments", {}) | |
| elif tool_call.arguments: | |
| args = tool_call.arguments | |
| else: | |
| args = {} | |
| if not args: | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId=tool_call_id, result="No arguments found in request") | |
| ]) | |
| date = args.get("date") | |
| start_time = args.get("start_time") | |
| end_time = args.get("end_time") | |
| title = args.get("title") or "Innoscribe-Demo" # Fallback title | |
| description = args.get("description", "") | |
| attendees = args.get("attendees", []) | |
| # Handle case where Vapi bot might send a single email string instead of a list | |
| if isinstance(attendees, str): | |
| attendees = [attendees] | |
| calendar_id = args.get("calendar_id", "tahasaif454@gmail.com") | |
| if not all([date, start_time, end_time]): | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result="Missing required fields: date, start_time, end_time" | |
| ) | |
| ]) | |
| # Validate date format and check if it's in the past | |
| try: | |
| parsed_date = datetime.strptime(date, "%Y-%m-%d").date() | |
| today = datetime.now().date() | |
| # Log the received date for debugging | |
| logging.info(f"schedule_meeting - Received date: {date}, Parsed: {parsed_date}, Today: {today}") | |
| # Check if date is in the past (more than 1 day ago to allow for timezone differences) | |
| if parsed_date < today - timedelta(days=1): | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Error: The date {date} is in the past. Please provide a current or future date. Today is {today.strftime('%Y-%m-%d')}." | |
| ) | |
| ]) | |
| # Warn if date seems very old (like 2023 when it should be 2026) | |
| if parsed_date.year < 2024: | |
| logging.warning(f"Suspicious date received: {date} (year {parsed_date.year}). This might be a date parsing error.") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Error: The date {date} appears to be incorrect (year {parsed_date.year}). Please provide a current date. Today is {today.strftime('%Y-%m-%d')}. If you said 'tomorrow', please ensure the date is calculated correctly." | |
| ) | |
| ]) | |
| except ValueError as e: | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Invalid date format: {str(e)}. Use YYYY-MM-DD format (e.g., 2026-01-28). Received: {date}" | |
| ) | |
| ]) | |
| # Validate that end_time is after start_time | |
| try: | |
| start_dt = datetime.strptime(f"{date} {start_time}", "%Y-%m-%d %H:%M") | |
| end_dt = datetime.strptime(f"{date} {end_time}", "%Y-%m-%d %H:%M") | |
| if end_dt <= start_dt: | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Invalid time range: end_time ({end_time}) must be after start_time ({start_time}). Please provide a valid time range." | |
| ) | |
| ]) | |
| except ValueError as e: | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Invalid time format: {str(e)}. Use HH:MM format (24-hour time, e.g., 15:00 for 3 PM)." | |
| ) | |
| ]) | |
| # Use Oslo offset (+01:00) | |
| start_rfc = make_rfc3339(date, start_time) + "+01:00" | |
| end_rfc = make_rfc3339(date, end_time) + "+01:00" | |
| # Build description with attendee information | |
| # Note: Service accounts cannot add attendees to events without Domain-Wide Delegation | |
| # So we include attendee info in the description instead | |
| full_description = description | |
| if attendees: | |
| attendee_list = ", ".join(attendees) | |
| if full_description: | |
| full_description += f"\n\nAttendees: {attendee_list}" | |
| else: | |
| full_description = f"Attendees: {attendee_list}" | |
| event = { | |
| "summary": title, | |
| "description": full_description, | |
| "start": { | |
| "dateTime": start_rfc, | |
| "timeZone": "Europe/Oslo", | |
| }, | |
| "end": { | |
| "dateTime": end_rfc, | |
| "timeZone": "Europe/Oslo", | |
| }, | |
| # Do NOT include attendees in event body - service accounts cannot use this field | |
| # Attendee information is included in the description instead | |
| } | |
| service = get_calendar_service() | |
| # Verify calendar access before creating event | |
| try: | |
| calendar_info = service.calendars().get(calendarId=calendar_id).execute() | |
| logging.info(f"Calendar access verified: {calendar_info.get('summary', calendar_id)}") | |
| except HttpError as e: | |
| error_content = e.content.decode(errors='ignore') if e.content else str(e) | |
| logging.error(f"Cannot access calendar {calendar_id}: {error_content}") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Error: Cannot access calendar {calendar_id}. Please ensure the service account has been granted access to this calendar. Error: {error_content}" | |
| ) | |
| ]) | |
| except Exception as e: | |
| logging.warning(f"Could not verify calendar access: {e}") | |
| # Create regular event without any Meet links | |
| # Note: Service accounts cannot add attendees to events without Domain-Wide Delegation | |
| # Attendee information is included in the event description instead | |
| logging.info(f"Creating event: {title} on {date} from {start_time} to {end_time} in calendar {calendar_id}") | |
| logging.info(f"Event data: {json.dumps(event, indent=2)}") | |
| # Initialize variables | |
| event_id = None | |
| html_link = "" | |
| try: | |
| created_event = ( | |
| service.events() | |
| .insert( | |
| calendarId=calendar_id, | |
| body=event, | |
| sendUpdates="none", | |
| ) | |
| .execute() | |
| ) | |
| # Verify event was created | |
| if not created_event: | |
| logging.error("Event creation returned None") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result="Error: Event creation failed. No event was returned from Google Calendar API." | |
| ) | |
| ]) | |
| event_id = created_event.get("id") | |
| html_link = created_event.get("htmlLink", "") | |
| if not event_id: | |
| logging.error("Event created but no ID returned") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result="Error: Event creation failed. No event ID was returned." | |
| ) | |
| ]) | |
| logging.info(f"Event created successfully: ID={event_id}, Link={html_link}") | |
| # Verify event exists by trying to get it | |
| try: | |
| verify_event = service.events().get(calendarId=calendar_id, eventId=event_id).execute() | |
| if verify_event: | |
| logging.info(f"Event verified: {verify_event.get('summary', 'No title')}") | |
| else: | |
| logging.warning(f"Event ID {event_id} was created but could not be retrieved") | |
| except Exception as verify_error: | |
| logging.warning(f"Could not verify event {event_id}: {verify_error}") | |
| except HttpError as e: | |
| error_content = e.content.decode(errors='ignore') if e.content else str(e) | |
| logging.error(f"Google Calendar API HttpError: {error_content}") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Google Calendar API error: {error_content}" | |
| ) | |
| ]) | |
| except Exception as e: | |
| logging.exception(f"Error creating event: {str(e)}") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Error creating event: {str(e)}" | |
| ) | |
| ]) | |
| # VAPI requires result to be a string, not a dictionary | |
| attendee_info = "" | |
| if attendees: | |
| attendee_list = ", ".join(attendees) | |
| attendee_info = f"\nAttendees: {attendee_list}\nNote: Attendee information has been added to the event description. Due to service account limitations, attendees were not added as calendar guests and will not receive automatic invitations." | |
| else: | |
| attendee_info = "\nNo attendees specified." | |
| if event_id: | |
| message = ( | |
| f"Meeting scheduled successfully: {title}\n" | |
| f"When: {date} {start_time} β {end_time}\n" | |
| f"Calendar: {calendar_id}\n" | |
| f"Event ID: {event_id}\n" | |
| f"Calendar Link: {html_link}\n" | |
| f"{attendee_info}" | |
| ) | |
| else: | |
| message = ( | |
| f"Warning: Meeting creation attempted but event ID not available.\n" | |
| f"When: {date} {start_time} β {end_time}\n" | |
| f"Calendar: {calendar_id}\n" | |
| f"{attendee_info}\n" | |
| f"Please check the calendar {calendar_id} manually." | |
| ) | |
| return VapiResponse(results=[ | |
| ToolCallResult(toolCallId=tool_call_id, result=message) | |
| ]) | |
| except HttpError as e: | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Google Calendar API error: {e.content.decode(errors='ignore')}" | |
| ) | |
| ]) | |
| except Exception as e: | |
| logging.exception("schedule_meeting failed") | |
| return VapiResponse(results=[ | |
| ToolCallResult( | |
| toolCallId=tool_call_id, | |
| result=f"Server error: {str(e)}" | |
| ) | |
| ]) | |