Building a GPT Powered Serverless Reminder App
Table of contents
- Challenge #1: Parsing Natural Language into Structured JSON
- Challenge #2: Generating EventBridge Expressions from JSON
- What is EventBridge and Why Use It?
- Challenge #3: Storing and Managing Reminders
- Challenge #4: Putting it all together (AWS Architecture)
- AWS Lambda: The Core Compute Layer:
- EventBridge: Scheduling Layer:
- DynamoDB: Storage Layer:
- Push Notifications (PNs) with FCM:
- AWS CDK: Efficient Deployment
- Some Development Notes and Insights:
- Conclusion
This project is inspired by Slack’s reminder feature, which lets users set reminders using natural language — a super handy tool for getting things done. But I wanted to push it further and build something that could handle even the most complex natural language reminders with ease.
At its core, yes, this might sound like just another to-do app. But it’s smarter — a lot smarter. It was also a great opportunity to work on and complete a hobby project after a long time, which was incredibly satisfying.
In this post, I’ll focus more on the backend side of things that powers this app, from natural language processing with GPT to scheduling reminders with a serverless event driven architecture. Let’s dive in!
I’ll approach this write-up by first listing the key challenges encountered while building the backend of this app. Once the challenges are outlined, I’ll dive deeper into each one to explain how they were tackled and the solutions implemented. Here’s a snapshot of the challenges we’ll explore:
Challenge #1: Parsing Natural Language into Structured JSON
Designing GPT prompts to interpret user inputs accurately and converting them into a standard JSON format for further processing.Challenge #2: Generating EventBridge Expressions from JSON
Translating parsed schedules into AWS EventBridge-compatible cron or rate expressions.Challenge #3: Storing and Managing Reminders
Implementing a scalable and efficient storage solution using AWS DynamoDB, integrated with the broader serverless architecture.Challenge #4: Putting it all together ( overall AWS architecture )
Designing a robust, event-driven architecture on AWS was key to ensuring scalability, reliability, and cost efficiency. By orchestrating Lambda, EventBridge, DynamoDB, and FCM, the system efficiently handled storing reminders, generating schedules, and delivering notifications across platforms.
Challenge #1: Parsing Natural Language into Structured JSON
One of the most critical steps in building the reminder app was parsing natural language input and converting it into a structured JSON format, adhering to a Pydantic-defined schema. I chose GPT 3.5 Turbo model for the reasonable speed, cost and accuracy.
1.1 Prompt Engineering for GPT:
While GPT-3.5 Turbo proved extremely valuable in understanding and processing natural language input, it has notable limitations, especially when handling specific dates and times. GPT’s primary challenge lies in its reliance on pattern recognition rather than an inherent understanding of calendars or date calculations. As a result, it often struggles with generating consistent and accurate absolute dates directly from user input.
For example, an input like “Remind me to visit doc on the 15th of next month” might result in GPT misinterpreting the date as “15th January” instead of the correct February date if today’s date is January 5th.
To address this, the prompt was carefully designed to generate relative terms (e.g., “next week,” “tomorrow”) that could be further processed using Python libraries like dateparser
and parsedatetime
. This approach leveraged GPT’s strength in understanding linguistic nuances while delegating the heavy lifting of date calculations to dedicated tools.
1.2 Date Conversion with Python Libraries:
dateparser
: This library processes relative date phrases (e.g., "next week") and converts them into specific dates. It works well for most cases but struggles with very ambiguous inputs.parsedatetime
: Used as a fallback for parsing complex or edge-case inputs whendateparser
fails. It provides additional flexibility in handling natural language dates.Example:
start_date_phrase = "next Monday"
start_date = dateparser.parse(
start_date_phrase,
settings={'PREFER_DATES_FROM': 'future', 'RELATIVE_BASE': datetime.now()}
)
1.3 Data Validation with Pydantic:
The Pydantic library ensured the parsed data conformed to a predefined schema, minimizing errors downstream. It allowed for robust validation and enforced the structure of fields like task
, start_date
, and repeat_frequency
.
Outcome
By combining a well-crafted prompt with intelligent parsing libraries, the system was able to handle a wide range of natural language inputs while minimizing ambiguities in date, time, and recurrence patterns.
class Reminder(BaseModel):
"""
This defines the output structure of gpt response.
"""
task: str = Field(description="The action or task to be reminded of.")
start_date_phrase: str = Field(description="The relative date phrase indicating when the reminder should start (e.g., today, tomorrow, next week).")
end_date: Optional[str] = Field(description="The end date of the reminder in dd-mm-yyyy format.")
time: Optional[str] = Field(default="11:00 AM", description="The specific time of day for the reminder.")
repeat_frequency: RepeatFrequency = Field(default_factory=RepeatFrequency, description="Repeat frequency settings.")
tags: List[str] = Field(default_factory=list, description="Tags associated with the reminder for easy categorization.")
def get_reminder_schedule_json(reminder_text: str):
""" Returns the reminder json with schedule details
Args:
reminder_text (str): Natural language text from which reminder details need to be extracted.
Returns:
dict: Details of the processed reminder which contains reminder frequency and other reminder details.
"""
# Set up the OpenAI model
model = ChatOpenAI(temperature=0, openai_api_key=os.getenv("OPENAI_API_KEY"), model="gpt-3.5-turbo")
# Set up the JSON output parser with the Reminder model
parser = JsonOutputParser(pydantic_object=Reminder)
# Define the prompt template with enhanced instructions
prompt = PromptTemplate(
template=(
"You are a highly intelligent assistant that parses reminder requests with dates, times, and repeat frequencies "
"in various formats. Follow these instructions carefully to extract details without introducing additional fields.\n\n"
"Instructions:\n"
"1. **Interpretation of Dates and Times**:\n"
" - Provide a relative date phrase for 'start_date_phrase' "
" that describes when the reminder should start (e.g., 'tomorrow,' 'next week').\n"
" - Understand whether the reminder is for a specific day/week or it repeats recurringly.\n"
" - Extract the time at which the reminder is needed; if no specific time is provided, default to 11:00 AM.\n"
" - For ambiguous or relative time-of-day phrases like:\n"
" - 'morning,' map to 8:00 AM.\n"
" - 'afternoon,' map to 2:00 PM.\n"
" - 'evening,' map to 6:00 PM.\n"
" - 'night,' map to 9:00 PM.\n"
" - Translate phrases like 'alternate days,' 'weekdays,' or 'weekends' into structured formats.\n\n"
"2. **Day of the Week Mapping**:\n"
" - Use the following mapping for days of the week when interpreting selected days:\n"
" {day_of_week_mapping_string}\n"
" - For example, 'Sunday' maps to 1, 'Wednesday' maps to 4, and so on.\n\n"
"3. Populate 'repeat_frequency' directly with integer values if provided, for fields like 'daily', 'weekly', etc.\n"
" - Do not introduce new keys such as 'interval'. Instead, set 'daily': 2 for 'every 2 days'.\n\n"
"4. For specific days, use 'selected_days_of_week' as a list of integers (e.g., [1, 4] for Sunday and Wednesday), "
" and 'selected_days_of_month' as a list of integers for days of the month (e.g., [1, 15]).\n\n"
"5. **Tags Extraction**:\n"
" - Identify single or double-word tags from the reminder that represent the main topics or categories.\n\n"
"Output the information in structured JSON with fields: task, start_date, end_date, time, repeat_frequency, and tags.\n\n"
"{format_instructions}\n\n"
"Reminder Text: {query}"
),
input_variables=["query"],
partial_variables={
"format_instructions": parser.get_format_instructions(),
"day_of_week_mapping_string": day_of_week_mapping_string,
},
)
# Create a chain that combines the prompt, model, and parser
chain = prompt | model | parser
# Pass the reminder text to the chain for processing
parsed_data = chain.invoke({"query": reminder_text})
# Extract the start date phrase and time from parsed data
start_date_phrase = parsed_data.get('start_date_phrase') or "today"
# Process start_date using dateparser
start_date = dateparser.parse(
start_date_phrase,
settings={'PREFER_DATES_FROM': 'future', 'RELATIVE_BASE': datetime.now()}
)
if not start_date:
# Fallback to parsedatetime if dateparser fails
cal = parsedatetime.Calendar()
time_struct, parse_status = cal.parse(start_date_phrase, datetime.now())
start_date = datetime(*time_struct[:6]) if parse_status == 1 else datetime.now().date()
parsed_data['start_date'] = start_date.strftime('%d-%m-%Y') if start_date else datetime.now().strftime('%d-%m-%Y')
# Filter out None values at the top level
parsed_data = {key: value for key, value in parsed_data.items() if value is not None}
# Remove None values specifically from within repeat_frequency
if 'repeat_frequency' in parsed_data:
parsed_data['repeat_frequency'] = {
k: v for k, v in parsed_data['repeat_frequency'].items() if v is not None
}
return parsed_data
Challenge #2: Generating EventBridge Expressions from JSON
Once the reminder JSON is generated, the next task is to schedule reminders based on this JSON. My goal for this project was to achieve a completely serverless architecture, which meant selecting a scheduling tool that could integrate seamlessly without me having to manage and scale instances. Since I was already using AWS for my backend, AWS EventBridge was the natural choice.
What is EventBridge and Why Use It?
AWS EventBridge is a serverless event bus that allows you to create rules to route and process events. Here’s why EventBridge was particularly useful:
Serverless and Fully Managed: No need to manage servers or scaling.
Native AWS Integration: Works seamlessly with other AWS services like Lambda, DynamoDB, and SQS.
Flexible Scheduling: Supports rule-based cron expressions, rate expressions as well as one time schedules.
When scheduling reminders in AWS EventBridge, three types of expressions can be used: Rate Expressions, Cron Expressions and One Time Schedules. Each serves different purposes, with varying levels of flexibility.
Rate Expressions:
Rate expressions are simple and ideal for recurring schedules at regular intervals. However, they lack the flexibility to specify exact times.
Example:
rate(2 days)
triggers every two days, but the time of day cannot be specified.Limitation: This makes rate expressions less suitable for reminders that need to fire at a specific time.
Cron Expressions:
Cron expressions offer far greater flexibility, allowing you to specify exact times and handle complex scheduling requirements.
- Example:
cron(0 9 * * ? *)
triggers every day at 9:00 AM.
cron(30 14 1/2 * ? *)
triggers every 2 days at 2:30 PM.
Since rate expressions cannot specify times, I primarily relied on cron expressions for my reminder app to ensure precise scheduling.
One-Time Schedules (Using at
):
For reminders that do not recur, EventBridge supports one-time schedules using the at
expression.
- Example:
at(2025-01-15T09:00:00)
triggers a one-time event on January 15, 2025, at 9:00 AM UTC.
This is especially useful for scheduling unique tasks or events.
def generate_eventbridge_expression(start_date, time_str, repeat_frequency, timezone="Asia/Kolkata"):
"""
Generates EventBridge schedule expression in UTC by converting input datetime to UTC.
Parameters:
- start_date (str): Date in the format "dd-mm-yyyy"
- time_str (str): Time in the format "hh:mm AM/PM"
- repeat_frequency (dict): Frequency of the schedule (e.g., daily, weekly, etc.)
- timezone (str): Timezone of the input datetime (default is Asia/Kolkata)
Returns:
- str: EventBridge schedule expression in UTC
"""
# Sanitize the time string
sanitized_time = sanitize_time_format(time_str)
# Convert start_date and sanitized_time to datetime format
local_timezone = pytz.timezone(timezone)
start_datetime_local = datetime.strptime(f"{start_date} {sanitized_time}", "%d-%m-%Y %I:%M %p")
start_datetime_local = local_timezone.localize(start_datetime_local)
# Convert to UTC
start_datetime_utc = start_datetime_local.astimezone(pytz.utc)
start_time = f"{start_datetime_utc.minute} {start_datetime_utc.hour}"
# Determine the EventBridge expression based on frequency
if not repeat_frequency:
# One-time expression for a specific date and time in UTC
expression = f"at({start_datetime_local.strftime('%Y-%m-%dT%H:%M:%S')})"
elif repeat_frequency.get("hourly"):
# Use rate expression for hourly schedules
hours = repeat_frequency["hourly"]
unit = "hour" if hours == 1 else "hours"
expression = f"rate({hours} {unit})"
elif repeat_frequency.get("daily"):
# Use cron for daily schedules with interval
interval = repeat_frequency["daily"]
if interval == 1:
expression = f"cron({start_time} * * ? *)" # Every day
else:
expression = f"cron({start_time} 1/{interval} * ? *)" # Every N days
elif repeat_frequency.get("selected_days_of_week"):
# Use cron for specific days of the week
selected_days_of_week = repeat_frequency["selected_days_of_week"]
day_map = {1: 'SUN', 2: 'MON', 3: 'TUE', 4: 'WED', 5: 'THU', 6: 'FRI', 7: 'SAT'}
days = [day_map[day] for day in selected_days_of_week]
day_str = ",".join(days)
expression = f"cron({start_time} ? * {day_str} *)"
elif repeat_frequency.get("weekly"):
# Convert weeks to days for the rate expression
weeks = repeat_frequency["weekly"]
days = weeks * 7 # Convert weeks to days
unit = "day" if days == 1 else "days"
expression = f"rate({days} {unit})"
elif repeat_frequency.get("selected_days_of_month"):
# Use cron for specific days of the month
selected_days_of_month = repeat_frequency["selected_days_of_month"]
if selected_days_of_month:
day_str = ",".join(map(str, selected_days_of_month))
expression = f"cron({start_time} {day_str} * ? *)"
else:
return None
elif repeat_frequency.get("monthly"):
# Use cron for monthly schedules
interval = repeat_frequency["monthly"]
if interval == 1:
expression = f"cron({start_time} {start_datetime_utc.day} * ? *)"
else:
expression = f"cron({start_time} {start_datetime_utc.day} 1/{interval} ? *)"
elif repeat_frequency.get("yearly"):
# Use cron for yearly schedules
expression = f"cron({start_time} {start_datetime_utc.day} {start_datetime_utc.month} ? *)"
else:
# One-time expression for a specific date and time
expression = f"at({start_datetime_utc.strftime('%Y-%m-%dT%H:%M:%S')})"
print(f"Generated EventBridge Expression (UTC): {expression}")
return expression
One important consideration when working with EventBridge is that all schedule expressions (both cron
and at
) are executed in UTC time, regardless of the local timezone of your application or users. This means that if you schedule a reminder at 9:00 AM in your local timezone (e.g., "Asia/Kolkata"), you need to explicitly convert the time to UTC before generating the EventBridge expression.
Here’s how this conversion is handled in the code:
Sanitize the Time Input:
Thesanitize_time_format
function ensures the time string is in a consistent format (e.g., "9:00 AM" or “9AM” → "09:00 AM").Convert Local Time to UTC:
Using thepytz
library, the local time is first localized to the user’s timezone and then converted to UTC.
Example:
local_timezone = pytz.timezone(timezone)
start_datetime_local = datetime.strptime(f"{start_date} {sanitized_time}", "%d-%m-%Y %I:%M %p")
start_datetime_local = local_timezone.localize(start_datetime_local)
start_datetime_utc = start_datetime_local.astimezone(pytz.utc)
3. Generate the Cron Expression in UTC:
After conversion, the UTC time is used to create the EventBridge cron or rate expression.
start_time = f"{start_datetime_utc.minute} {start_datetime_utc.hour}"
expression = f"cron({start_time} * * ? *)"
By explicitly converting times to UTC, this approach ensures that reminders trigger at the correct local times for the user, regardless of their timezone. This is a critical step to handle user expectations effectively when building global applications.
Challenge #3: Storing and Managing Reminders
To store and manage reminders effectively in this project, I utilized AWS DynamoDB, a highly scalable and serverless NoSQL database. The schema was carefully designed to accommodate reminders and customer device information.
Reminder Table Schema
The reminder table stores details for each reminder, including customer-specific metadata, schedule details, and execution states. It uses a composite primary key for efficient organization and retrieval:
Partition Key (
PK
):"CUSTOMER#{device_id}"
Groups all reminders belonging to a specific customer.Sort Key (
SK
):"REMINDER#{reminder_id}"
Ensures unique entries for each reminder under the customer.
Additional Fields:
task
: The reminder text or task (e.g., "exercise").eventbridge_expression
: The generated EventBridge cron or rate expression.start_date
: The start date of the reminder indd-mm-yyyy
format.time
: The time of day for the reminder.repeat_frequency
: Nested structure defining recurrence (e.g., daily, weekly).is_completed
: Boolean to track whether the reminder has been completed.created_at
&updated_at
: Timestamps for audit and tracking purposes.tags
: List of tags for categorization (e.g., health, finance).
Customer Devices Table Schema:
The customer devices table manages device-specific information for each customer. This helps synchronize reminders and manage push notifications. It uses the following keys:
Partition Key (
PK
):"CUSTOMER#{customer_id}"
Groups all devices belonging to a specific customer.Sort Key (
SK
):"DEVICE#{device_id}"
Ensures unique entries for each device.
Additional Fields:
device_id
: Unique identifier for the device.device_token_id
: FCM or other push notification token for sending reminders.platform
: OS of the device (e.g.,android
,ios
).os_version
: Operating system version.model
: Device model name (e.g., "Pixel 6").is_virtual
: Boolean indicating if the device is a virtual machine or real hardware.created_at
&updated_at
: Timestamps for audit and tracking purposes.
Important Considerations :
The
PK
andSK
design allows querying reminders or devices belonging to a specific customer efficiently.DynamoDB’s schema-less nature allows adding new fields if the system evolves.
The reminder table focuses on schedule management. The devices table handles device-specific information for notifications.
Challenge #4: Putting it all together (AWS Architecture)
Let’s now put all the pieces together to understand how I achieved a complete, serverless architecture for the reminder app. The architecture efficiently integrates AWS Lambda, EventBridge, DynamoDB, and Firebase Cloud Messaging (FCM) to handle everything from setting reminders to delivering push notifications (PNs)
AWS Lambda: The Core Compute Layer:
AWS Lambda forms the backbone of the system, with different functions handling specific tasks as microservices. These include
managing customer and device information,
processing natural language inputs to set reminders,
generating EventBridge rules,
fetching reminders from DynamoDB,
marking reminders as complete (including disabling EventBridge rules), and
processing triggered events to send push notifications via Firebase Cloud Messaging (FCM).
Each Lambda function plays a vital role in ensuring the seamless operation of the system.
EventBridge: Scheduling Layer:
EventBridge is used to manage all reminder scheduling. We already talked in detail about it above.
DynamoDB: Storage Layer:
The database is split into two tables for managing reminders and customer devices.
Push Notifications (PNs) with FCM:
Currently, push notifications are sent only to Android devices using Firebase Cloud Messaging (FCM). Here’s how it works:
- Firebase Registration:
Register on the Firebase Console and create a project.
Download the service account JSON file, which contains the credentials required to interact with FCM.
2. Sending PNs:
The “Process Events” Lambda fetches the
device_token_id
from the Customer Info Table.It uses Firebase SDK to send notifications via FCM to the appropriate device.
AWS CDK: Efficient Deployment
The entire infrastructure is deployed and managed using the AWS Cloud Development Kit (CDK), which simplifies resource provisioning and management. With CDK:
All resources (Lambdas, EventBridge, DynamoDB tables) are defined as code, ensuring repeatable deployments.
Updates and scaling are handled seamlessly by modifying the CDK stack configuration.
Some Development Notes and Insights:
Using Lambda Layers for Shared Dependencies
Managing multiple Lambda functions can lead to redundancy if each function bundles its own dependencies. To avoid this:
Why Use Layers?
Lambda Layers allow shared libraries and dependencies (e.g., Python packages) to be centrally managed.
Reduces deployment size and speeds up deployment by avoiding repeated uploads of identical dependencies across functions.
How to Use Layers?
Create a zip file with the dependencies (e.g., using
pip install -t python/ <packages>
).Deploy the layer and reference it in the Lambda function’s configuration.
layers=[
_lambda.LayerVersion.from_layer_version_arn(
self,
"DependenciesLayer",
os.getenv("LAMBDA_LAYER_ARN")
)
]
Managing IAM Permissions
Ensure each Lambda function has the necessary permissions for DynamoDB, SQS, and EventBridge.
EventBridge rules must have permissions to trigger Lambda functions.
process_events_lambda.add_permission(
"AllowEventBridgeInvoke",
principal=iam.ServicePrincipal("events.amazonaws.com"),
action="lambda:InvokeFunction",
source_arn=f"arn:aws:events:{self.region}:{self.account}:rule/*"
)
- For the AWS Scheduler (for one time schedules and rate expressions), ensure the role has permissions to invoke Lambda functions.
scheduler_role.add_to_policy(
iam.PolicyStatement(
actions=["lambda:InvokeFunction"],
resources=["*"]
)
)
Conclusion
This marks the end of this write-up. I hope it has inspired readers to see how easy and exciting it can be to build useful applications powered by GPT and serverless architectures.
The frontend of the app was developed using the Ionic Framework. While I’m not a frontend expert, I found Ionic to be beginner-friendly and easy to get started with, especially compared to some other frameworks.
Thanks a lot for reading!
Here’s the GitHub repository: RemindMe-Backend 🚀 Feel free to contribute and share suggestions.