innoscribe-vapi / app /main.py
Tahasaif3's picture
'code'
14f2717
# 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
@app.exception_handler(RequestValidationError)
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
# ────────────────────────────────────────────────
@app.post(
"/check_availability",
response_model=VapiResponse,
summary="Check Availability",
description="Checks if a specific time slot is available in a Google Calendar. Returns availability status and any conflicting events. calendar_id is optional and defaults to tahasaif454@gmail.com if not provided.",
openapi_extra={
"requestBody": {
"content": {
"application/json": {
"example": {
"toolCallList": [
{
"id": "call_abc123",
"toolName": "check_availability",
"arguments": {
"date": "2026-01-25",
"start_time": "18:00",
"end_time": "19:00"
}
}
]
}
}
}
}
},
responses={
200: {
"description": "Successful Response",
"content": {
"application/json": {
"example": {
"results": [
{
"toolCallId": "call_abc123",
"result": "GREEN SIGNAL: The slot from 18:00 to 19:00 is available in Oslo time. You can proceed to schedule the meeting."
}
]
}
}
}
},
422: {
"description": "Validation Error"
}
}
)
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)}"
)
])
@app.post(
"/schedule_meeting",
response_model=VapiResponse,
summary="Schedule Meeting",
description="Creates a new meeting/event in Google Calendar with specified date, time, title, description, and attendees. Sends calendar invitations to attendees. calendar_id is optional and defaults to tahasaif454@gmail.com if not provided.",
openapi_extra={
"requestBody": {
"content": {
"application/json": {
"example": {
"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"]
}
}
]
}
}
}
}
},
responses={
200: {
"description": "Successful Response",
"content": {
"application/json": {
"example": {
"results": [
{
"toolCallId": "call_xyz789",
"result": "Meeting scheduled: Innoscribe-Demo\nWhen: 2026-01-25 18:00 – 19:00\nAttendee: user@example.com\n"
}
]
}
}
}
},
422: {
"description": "Validation Error"
}
}
)
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)}"
)
])