Building a Meeting Scheduler Agent with LangChain
What Is a Meeting Scheduler Agent?
A Meeting Scheduler Agent is an AI-powered autonomous system built with LangChain that can understand natural language requests, reason about availability, interact with calendar APIs, and schedule meetings on your behalf. Unlike traditional rigid scheduling tools, this agent leverages Large Language Models (LLMs) to interpret the nuances of human communication—handling fuzzy time expressions, negotiating conflicts, and even proposing alternative slots when the first choice isn't available.
At its core, the agent combines several capabilities:
- Natural language understanding — parsing requests like "Find a time next week when Alice and Bob are both free for a 30-minute catch-up"
- Tool use — calling external APIs (Google Calendar, Outlook, Calendly) to read and write calendar data
- Reasoning loops — checking availability, detecting conflicts, and iterating until a suitable slot is found
- Memory — retaining context about participants, preferences, and previous scheduling attempts across a conversation
Why It Matters
Manual scheduling consumes an enormous amount of cognitive bandwidth and time. Studies show knowledge workers spend up to 30% of their workweek on email and scheduling coordination. A LangChain-powered agent transforms this friction into a seamless, conversational experience. The agent doesn't just execute hard-coded rules—it reasons about edge cases, such as "Skip lunch hours" or "Prioritize morning slots for the CTO." This flexibility makes it far more useful than traditional calendar bots.
Beyond personal productivity, embedding such agents into team workflows, Slack channels, or customer-facing applications unlocks scalable coordination without human intermediation. The same pattern extends to booking doctor appointments, arranging delivery windows, or coordinating multi-party events.
Architecture Overview
The agent follows the classic LangChain ReAct (Reason + Act) pattern. The LLM receives a system prompt describing its scheduling role and available tools. When a user request arrives, the model decides whether to invoke a tool (e.g., query calendar availability) or respond directly. Tool outputs are fed back into the context window, and the loop continues until the model emits a final answer containing the confirmed meeting details.
The key components are:
- Toolkit — a set of Python functions wrapped as LangChain tools for calendar read, calendar write, timezone conversion, and conflict resolution
- Agent executor — the LangChain orchestrator that manages the reasoning loop and tool invocation
- LLM provider — typically OpenAI's GPT-4 or Anthropic's Claude for robust reasoning
- Memory store — conversation buffer memory so the agent remembers context across turns
Prerequisites & Setup
Before building the agent, install the required packages. We'll use LangChain, the OpenAI SDK, and the Google Calendar API client as our primary calendar backend.
pip install langchain langchain-openai langchain-community google-auth google-auth-oauthlib google-api-python-client python-dateutil pytz
You'll also need:
- An OpenAI API key (set as environment variable
OPENAI_API_KEY) - A Google Cloud project with the Calendar API enabled and OAuth 2.0 credentials downloaded as
credentials.json
Step 1: Define Calendar Tools
We start by building the tool functions that the agent will call. Each function needs a clear docstring describing its purpose—this becomes the tool description the LLM reads to decide when to invoke it.
1a. List Upcoming Events
This tool retrieves events within a date range for specified calendar IDs. The agent uses it to check availability.
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from datetime import datetime, timedelta
import pytz
import os
import pickle
SCOPES = ['https://www.googleapis.com/auth/calendar']
def get_calendar_service():
"""Authenticate and return the Google Calendar service."""
creds = None
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
return build('calendar', 'v3', credentials=creds)
def list_events(
calendar_id: str = 'primary',
time_min: str = None,
time_max: str = None,
max_results: int = 20
) -> str:
"""
List calendar events within a specified time range.
calendar_id: The calendar identifier (use 'primary' for the user's main calendar).
time_min: ISO format start datetime string (e.g., '2025-06-01T00:00:00+00:00').
time_max: ISO format end datetime string. If omitted, defaults to 7 days from time_min.
max_results: Maximum number of events to return (default 20).
Returns a formatted string listing events with start/end times and summaries.
"""
service = get_calendar_service()
if time_min is None:
time_min = datetime.utcnow().isoformat() + 'Z'
if time_max is None:
parsed_min = datetime.fromisoformat(time_min.replace('Z', '+00:00'))
time_max = (parsed_min + timedelta(days=7)).isoformat()
events_result = service.events().list(
calendarId=calendar_id,
timeMin=time_min,
timeMax=time_max,
maxResults=max_results,
singleEvents=True,
orderBy='startTime'
).execute()
events = events_result.get('items', [])
if not events:
return f"No events found in calendar '{calendar_id}' between {time_min} and {time_max}."
output_lines = []
for event in events:
start = event['start'].get('dateTime', event['start'].get('date'))
end = event['end'].get('dateTime', event['end'].get('date'))
summary = event.get('summary', 'Untitled')
output_lines.append(f"- {summary}: {start} → {end}")
return "Events:\n" + "\n".join(output_lines)
1b. Create a Calendar Event
This tool creates a new event and optionally sends email invitations to attendees.
def create_event(
summary: str,
start_time: str,
end_time: str,
attendees: str = "",
calendar_id: str = 'primary',
location: str = "",
description: str = ""
) -> str:
"""
Create a new calendar event.
summary: Title of the meeting (e.g., 'Sprint Planning').
start_time: ISO format start datetime with timezone offset.
end_time: ISO format end datetime with timezone offset.
attendees: Comma-separated email addresses of participants.
calendar_id: Calendar identifier (default 'primary').
location: Optional meeting location or URL.
description: Optional meeting description or agenda.
Returns a confirmation with the event link or an error message.
"""
service = get_calendar_service()
event_body = {
'summary': summary,
'start': {'dateTime': start_time, 'timeZone': 'America/New_York'},
'end': {'dateTime': end_time, 'timeZone': 'America/New_York'},
}
if location:
event_body['location'] = location
if description:
event_body['description'] = description
if attendees:
attendee_list = [{'email': email.strip()} for email in attendees.split(',')]
event_body['attendees'] = attendee_list
try:
event = service.events().insert(
calendarId=calendar_id,
body=event_body,
sendUpdates='all'
).execute()
return f"Event created successfully! Link: {event.get('htmlLink')} ID: {event.get('id')}"
except Exception as e:
return f"Failed to create event: {str(e)}"
1c. Timezone Conversion Utility
The agent often needs to convert times between timezones. This utility tool handles that.
from dateutil import parser as dateparser
def convert_timezone(time_str: str, from_tz: str, to_tz: str) -> str:
"""
Convert a datetime string from one timezone to another.
time_str: Datetime string (ISO format or natural like '2025-06-15 14:00').
from_tz: Source timezone name (e.g., 'America/New_York').
to_tz: Target timezone name (e.g., 'Europe/London').
Returns the converted datetime in ISO format.
"""
from_zone = pytz.timezone(from_tz)
to_zone = pytz.timezone(to_tz)
dt = dateparser.parse(time_str)
if dt.tzinfo is None:
dt = from_zone.localize(dt)
converted = dt.astimezone(to_zone)
return converted.isoformat()
1d. Find Free Slots (Reasoning Helper)
This is the most critical tool. Given a list of busy periods from multiple calendars, it computes the free windows within a target date range.
def find_free_slots(
busy_periods_json: str,
date_start: str,
date_end: str,
meeting_duration_minutes: int = 30,
working_hours_start: int = 9,
working_hours_end: int = 17
) -> str:
"""
Given a JSON array of busy periods, find available free slots.
busy_periods_json: JSON string like [{"start":"ISO","end":"ISO"},...].
date_start: Search window start (ISO format).
date_end: Search window end (ISO format).
meeting_duration_minutes: Required meeting length in minutes (default 30).
working_hours_start: Earliest acceptable hour (0-23, default 9).
working_hours_end: Latest acceptable hour (0-23, default 17).
Returns a list of free slot strings.
"""
import json
busy_periods = json.loads(busy_periods_json)
# Parse busy periods into sorted list of datetime tuples
busy_tuples = []
for period in busy_periods:
s = dateparser.parse(period['start'])
e = dateparser.parse(period['end'])
busy_tuples.append((s, e))
busy_tuples.sort(key=lambda x: x[0])
# Generate working-hour time windows day by day
start_date = dateparser.parse(date_start).date()
end_date = dateparser.parse(date_end).date()
current_date = start_date
free_slots = []
while current_date <= end_date:
day_start = datetime.combine(current_date, datetime.min.time().replace(hour=working_hours_start))
day_end = datetime.combine(current_date, datetime.min.time().replace(hour=working_hours_end))
# Check all busy periods that overlap this day
busy_today = [(max(s, day_start), min(e, day_end)) for (s, e) in busy_tuples
if s.date() == current_date or e.date() == current_date]
busy_today.sort(key=lambda x: x[0])
cursor = day_start
for b_start, b_end in busy_today:
if cursor < b_start:
gap = (b_start - cursor).total_seconds() / 60
if gap >= meeting_duration_minutes:
free_slots.append(f"{cursor.isoformat()} → {b_start.isoformat()} ({int(gap)} min)")
cursor = max(cursor, b_end)
if cursor < day_end:
gap = (day_end - cursor).total_seconds() / 60
if gap >= meeting_duration_minutes:
free_slots.append(f"{cursor.isoformat()} → {day_end.isoformat()} ({int(gap)} min)")
current_date += timedelta(days=1)
if not free_slots:
return "No free slots found in the specified window with the given constraints."
return "Available slots:\n" + "\n".join(free_slots)
Step 2: Wrap Functions as LangChain Tools
LangChain's Tool class wraps each function with a name, description, and argument schema. The description is crucial—it's what the LLM reads to decide which tool to call and what arguments to pass.
from langchain.tools import tool
from typing import Optional
from pydantic import BaseModel, Field
class ListEventsInput(BaseModel):
calendar_id: str = Field(default='primary', description="Calendar ID to query")
time_min: Optional[str] = Field(default=None, description="ISO start datetime")
time_max: Optional[str] = Field(default=None, description="ISO end datetime")
max_results: int = Field(default=20, description="Max events to return")
class CreateEventInput(BaseModel):
summary: str = Field(description="Meeting title")
start_time: str = Field(description="ISO start datetime with timezone")
end_time: str = Field(description="ISO end datetime with timezone")
attendees: str = Field(default="", description="Comma-separated email addresses")
calendar_id: str = Field(default='primary', description="Calendar ID")
location: str = Field(default="", description="Meeting location or URL")
description: str = Field(default="", description="Meeting agenda")
class ConvertTimezoneInput(BaseModel):
time_str: str = Field(description="Datetime string to convert")
from_tz: str = Field(description="Source timezone")
to_tz: str = Field(description="Target timezone")
class FindFreeSlotsInput(BaseModel):
busy_periods_json: str = Field(description="JSON array of busy periods")
date_start: str = Field(description="Search window start ISO datetime")
date_end: str = Field(description="Search window end ISO datetime")
meeting_duration_minutes: int = Field(default=30, description="Required meeting length")
working_hours_start: int = Field(default=9, description="Earliest start hour (0-23)")
working_hours_end: int = Field(default=17, description="Latest end hour (0-23)")
@tool("list_events", args_schema=ListEventsInput)
def tool_list_events(
calendar_id: str = 'primary',
time_min: Optional[str] = None,
time_max: Optional[str] = None,
max_results: int = 20
) -> str:
"""Query calendar events within a date range. Use to check someone's availability."""
return list_events(calendar_id, time_min, time_max, max_results)
@tool("create_event", args_schema=CreateEventInput)
def tool_create_event(
summary: str,
start_time: str,
end_time: str,
attendees: str = "",
calendar_id: str = 'primary',
location: str = "",
description: str = ""
) -> str:
"""Create a new calendar event and send invitations. Use to finalize a meeting."""
return create_event(summary, start_time, end_time, attendees, calendar_id, location, description)
@tool("convert_timezone", args_schema=ConvertTimezoneInput)
def tool_convert_timezone(time_str: str, from_tz: str, to_tz: str) -> str:
"""Convert a datetime between timezones. Use when participants are in different zones."""
return convert_timezone(time_str, from_tz, to_tz)
@tool("find_free_slots", args_schema=FindFreeSlotsInput)
def tool_find_free_slots(
busy_periods_json: str,
date_start: str,
date_end: str,
meeting_duration_minutes: int = 30,
working_hours_start: int = 9,
working_hours_end: int = 17
) -> str:
"""Analyze busy periods and return available free slots. Use to propose meeting times."""
return find_free_slots(
busy_periods_json, date_start, date_end,
meeting_duration_minutes, working_hours_start, working_hours_end
)
# Collect all tools
tools = [tool_list_events, tool_create_event, tool_convert_timezone, tool_find_free_slots]
Step 3: Build the Agent System Prompt
A well-crafted system prompt is essential. It sets the agent's persona, outlines the scheduling workflow, and gives explicit guidance on how to reason through multi-step scheduling tasks.
SYSTEM_PROMPT = """You are an expert meeting scheduling assistant powered by LangChain. Your job is to help users find mutually available times and schedule meetings.
You have access to the following tools:
- list_events: Check someone's calendar for existing events in a date range.
- find_free_slots: Given busy periods from multiple people, compute available free windows.
- create_event: Finalize a meeting by creating a calendar event and sending invitations.
- convert_timezone: Convert times between timezones (essential when participants span multiple zones).
WORKFLOW FOR SCHEDULING A MEETING:
1. Understand the request: Extract participants, meeting duration, date range, and any constraints (e.g., "avoid lunch hours", "must be before 3pm", "prefer mornings").
2. If timezones differ among participants, use convert_timezone to normalize all times to a reference timezone.
3. Query each participant's calendar using list_events for the relevant date range. Gather all busy periods.
4. Combine busy periods into a JSON array and pass them to find_free_slots along with the date range, meeting duration, and any working-hour constraints.
5. Present the best 2-3 free slot options to the user in a clear, human-friendly format. Include the time in each participant's local timezone if they differ.
6. Once the user selects a slot, call create_event with all details: summary, start/end times, attendees, and optional location/description.
7. Confirm the booking and share the event link.
IMPORTANT RULES:
- Always verify timezone consistency before creating an event. Never assume all participants share the same timezone unless stated.
- When presenting options, format them clearly: "Option 1: Monday June 15, 10:00am–10:30am EST (3:00pm–3:30pm GMT for Alice)".
- If no slots are available, suggest adjustments: different date range, shorter duration, relaxed working hours.
- Do not create an event without explicit user confirmation of the chosen slot.
- Be polite, concise, and efficient. The user's time matters.
"""
Step 4: Initialize the LLM and Agent
We now wire everything together using LangChain's create_tool_calling_agent and AgentExecutor. This modern approach uses the model's native function-calling capability rather than parsing text prompts.
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain.memory import ConversationBufferMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os
# Initialize the LLM - GPT-4o provides excellent reasoning for scheduling tasks
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.2, # Low temperature for consistent, logical scheduling decisions
api_key=os.environ.get("OPENAI_API_KEY")
)
# Build the prompt with system message and memory placeholders
prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM_PROMPT),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
# Create the agent
agent = create_tool_calling_agent(llm, tools, prompt)
# Add conversation memory so the agent remembers context across exchanges
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True,
max_token_limit=2000
)
# The AgentExecutor manages the reasoning loop
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
verbose=True, # Set to True during development to see the reasoning trace
max_iterations=15, # Prevent infinite loops
handle_parsing_errors=True,
early_stopping_method="generate"
)
Step 5: Create an Interactive Scheduling Interface
With the agent executor ready, we can build a simple chat loop or integrate it into a web application. Here's a complete command-line interface that demonstrates the agent in action.
def run_scheduling_session():
"""Run an interactive meeting scheduling session."""
print("=" * 60)
print(" 🤖 LangChain Meeting Scheduler Agent")
print(" Type 'help' for example requests or 'quit' to exit")
print("=" * 60)
# Example requests to showcase capabilities
example_requests = [
"Schedule a 30-minute sync between alice@example.com and bob@example.com next Tuesday afternoon. Both are in EST.",
"Find a time for a 1-hour design review with the team next week. Participants: alice@example.com (EST), charlie@example.com (GMT), diana@example.com (PST). Avoid lunch hours 12-1pm.",
"I need a 15-minute standup every morning this week with bob@example.com. Both EST, working hours 9am-5pm.",
"Book a 45-minute onboarding session with eve@example.com sometime next Monday. I'm in EST, Eve is in CET. Prefer morning slots."
]
print("\n📋 Example requests you can try:")
for i, req in enumerate(example_requests, 1):
print(f" {i}. {req}")
print()
while True:
user_input = input("\n👤 You: ").strip()
if user_input.lower() in ('quit', 'exit', 'q'):
print("👋 Goodbye!")
break
if user_input.lower() == 'help':
print("\n📋 Example requests:")
for i, req in enumerate(example_requests, 1):
print(f" {i}. {req}")
continue
if not user_input:
continue
print("\n🤖 Agent is thinking...\n")
try:
result = agent_executor.invoke({"input": user_input})
print(f"\n🤖 Agent: {result['output']}")
except Exception as e:
print(f"\n❌ Error: {str(e)}")
print("Please try again with more specific details.")
if __name__ == "__main__":
run_scheduling_session()
Step 6: Adding Advanced Capabilities
6a. Recurring Meeting Support
For recurring meetings, extend the create_event function to handle recurrence rules using the RRULE specification. The agent can parse natural language like "every Tuesday at 10am" and convert it to the appropriate RRULE string.
def create_recurring_event(
summary: str,
start_time: str,
end_time: str,
recurrence_rule: str,
attendees: str = "",
calendar_id: str = 'primary'
) -> str:
"""
Create a recurring calendar event.
recurrence_rule: RRULE string (e.g., 'RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=10').
"""
service = get_calendar_service()
event_body = {
'summary': summary,
'start': {'dateTime': start_time, 'timeZone': 'America/New_York'},
'end': {'dateTime': end_time, 'timeZone': 'America/New_York'},
'recurrence': [recurrence_rule],
}
if attendees:
event_body['attendees'] = [{'email': e.strip()} for e in attendees.split(',')]
event = service.events().insert(calendarId=calendar_id, body=event_body, sendUpdates='all').execute()
return f"Recurring event created: {event.get('htmlLink')}"
6b. Conflict Detection and Resolution
Add a dedicated tool that checks whether a proposed slot conflicts with any existing events on the participants' calendars, returning a detailed conflict report.
def check_conflict(
proposed_start: str,
proposed_end: str,
calendar_ids_json: str
) -> str:
"""
Check if a proposed meeting time conflicts with existing events.
proposed_start: Proposed meeting start (ISO format).
proposed_end: Proposed meeting end (ISO format).
calendar_ids_json: JSON array of calendar IDs to check (e.g., '["primary", "team_calendar@example.com"]').
Returns conflict details or 'No conflicts found.'
"""
import json
service = get_calendar_service()
calendar_ids = json.loads(calendar_ids_json)
prop_start_dt = dateparser.parse(proposed_start)
prop_end_dt = dateparser.parse(proposed_end)
conflicts = []
for cal_id in calendar_ids:
events_result = service.events().list(
calendarId=cal_id,
timeMin=proposed_start,
timeMax=proposed_end,
singleEvents=True
).execute()
for event in events_result.get('items', []):
event_start = dateparser.parse(event['start'].get('dateTime', event['start'].get('date')))
event_end = dateparser.parse(event['end'].get('dateTime', event['end'].get('date')))
if event_start < prop_end_dt and event_end > prop_start_dt:
conflicts.append(
f"Conflict in '{cal_id}': {event.get('summary','Untitled')} "
f"({event_start.isoformat()} → {event_end.isoformat()})"
)
if conflicts:
return "Conflicts found:\n" + "\n".join(conflicts)
return "No conflicts found."
Step 7: Testing the Agent
Thorough testing ensures the agent behaves reliably. Here's a test harness that validates core scheduling scenarios without hitting live APIs (using mocked calendar responses).
from unittest.mock import patch, MagicMock
def test_agent_basic_scheduling():
"""Test that the agent correctly handles a simple scheduling request."""
with patch('__main__.list_events') as mock_list:
mock_list.return_value = "No events found in calendar 'primary' between 2025-06-15T00:00:00 and 2025-06-16T00:00:00."
with patch('__main__.create_event') as mock_create:
mock_create.return_value = "Event created successfully! Link: https://calendar.google.com/event?id=test123"
test_input = (
"Schedule a 30-minute meeting called 'Test Sync' with alice@example.com "
"on Monday June 15 2025 at 10am EST. I'm bob@example.com also EST."
)
result = agent_executor.invoke({"input": test_input})
output = result['output']
# Verify the agent attempted to create the event
assert mock_create.called, "Agent should have called create_event"
# Verify the output contains a confirmation
assert "created" in output.lower() or "scheduled" in output.lower()
print("✅ Basic scheduling test passed")
def test_agent_timezone_conversion():
"""Test that the agent handles cross-timezone scheduling correctly."""
test_input = (
"I'm in New York (EST) and need to schedule a call with someone in London (GMT). "
"Find a time next Wednesday between 9am-5pm EST that works for both."
)
result = agent_executor.invoke({"input": test_input})
output = result['output']
# Should mention timezone conversion or show times in both zones
assert "GMT" in output or "London" in output or "timezone" in output.lower()
print("✅ Timezone handling test passed")
test_agent_basic_scheduling()
test_agent_timezone_conversion()
Best Practices for Production Scheduling Agents
1. Keep Tool Descriptions Crystal Clear
The LLM relies entirely on tool descriptions to understand what each function does. Write descriptions that include: what the tool accomplishes, when to use it, and the exact format of arguments. Ambiguous descriptions lead to incorrect tool selection and malformed arguments.
# ❌ Bad: Vague description
"""Lists events."""
# ✅ Good: Specific, with argument format hints
"""Query calendar events within a date range.
Use this to check someone's availability before proposing meeting times.
time_min and time_max must be ISO format strings like '2025-06-15T00:00:00+00:00'."""
2. Implement Graceful Error Recovery
API calls can fail due to rate limits, authentication expiry, or malformed data. Wrap tool functions in try-except blocks that return informative error strings rather than raising exceptions. The agent can then read the error and adapt its strategy.
def robust_calendar_call():
try:
# ... API call
return "Success result"
except RateLimitError:
return "Rate limit hit. Please wait 60 seconds and retry, or narrow the date range."
except AuthError:
return "Authentication expired. Ask the user to re-authenticate their calendar."
3. Constrain the Agent's Authority
Never allow the agent to create events without explicit user confirmation. Use a two-phase approach: the agent proposes options, the user selects one, and only then does the agent call create_event. This prevents the agent from scheduling unwanted meetings based on misinterpreted requests.
4. Manage Context Window Carefully
Scheduling conversations can grow long, especially when debugging conflicts across multiple calendars. Use ConversationBufferMemory with a max_token_limit to prevent the context from exceeding the model's limit. Alternatively, periodically summarize the conversation and replace older messages with the summary.
from langchain.memory import ConversationSummaryMemory
memory = ConversationSummaryMemory(
llm=llm,
memory_key="chat_history",
return_messages=True,
max_token_limit=1500
)
5. Log All Tool Calls for Debugging
Enable verbose mode during development and log every tool invocation with its arguments and outputs. This makes debugging incorrect agent behavior far easier. In production, store these logs in a structured format for monitoring.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("scheduler_agent")
# Wrap each tool with logging
def logged_tool(original_tool):
def wrapper(*args, **kwargs):
logger.info(f"Tool call: {original_tool.name} args={args} kwargs={kwargs}")
result = original_tool(*args, **kwargs)
logger.info(f"Tool result ({original_tool.name}): {result[:200]}...")
return result
return wrapper
6. Handle Ambiguous Requests with Clarification
When the user's request lacks critical details (e.g., no date range, no attendees), the agent should ask clarifying questions rather than guessing. Build this into the system prompt and reinforce it with few-shot examples.
# Add to system prompt:
"""
CLARIFICATION POLICY:
- If the user doesn't specify a date range, ask: "Which days should I search? This week, next week, or specific dates?"
- If attendee emails are missing, ask: "Which email addresses should I include?"
- If the meeting duration isn't specified, ask before assuming a default.
- Never silently assume critical missing information.
"""
7. Use Structured Output for Integration
When integrating the agent into a larger application (Slack bot, web app, email handler), parse the agent's final response to extract structured data. You can prompt the agent to end its response with a machine-readable JSON block.
# Add to system prompt:
"""
After confirming a booking, append a structured JSON block at the end of your response:
json
{"status":"confirmed","event_id":"...","link":"...","summary":"...","start":"...","end":"..."}
This block will be parsed by the application and used for downstream processing.
"""
8. Rate Limiting and Cost Control
Each agent invocation can trigger multiple LLM calls. Set max_iterations to a reasonable value (10–15) to prevent runaway loops. Monitor token usage and set budget alerts. For high-volume deployments, consider caching common reasoning patterns or using a smaller model for simple scheduling tasks.
Deployment Considerations
When deploying the agent to production, consider these infrastructure aspects:
- Authentication persistence — Store OAuth tokens securely (e.g., in a database with encryption) rather than pickle files on disk
- Concurrency — The Google Calendar API has per-user rate limits; use a queue for multiple simultaneous scheduling requests
- Webhook integration — Subscribe to calendar change notifications so the agent is aware of real-time updates
- Monitoring — Track scheduling success rate, average turns per task, and user satisfaction metrics
Complete Agent File
Below is the complete, runnable agent file consolidating all the components described above. Save this as scheduler_agent.py and run it after setting up your credentials.
#!/usr/bin/env python3
"""
LangChain Meeting Scheduler Agent
Complete implementation with Google Calendar integration.
Requires: pip install langchain langchain-openai google-api-python-client google-auth-oauthlib pytz python-dateutil
Set OPENAI_API_KEY environment variable and place credentials.json