How I Built an AI Ride Booking Agent with Airtable
Author(s): Isa Ismail
Originally published on Towards AI.
Most chatbots answer questions. This one books your ride, cancels it, and hands you a PDF receipt — all from a single sentence.
I built Velora, an AI ride booking agent that turns natural language into real database operations. No forms. No dropdowns. Just type “Book a sedan from Downtown to Airport at 6 PM” and watch the agent create a live reservation in Airtable, then generate a downloadable PDF receipt.
In this article, I’ll walk you through exactly how it works, why I chose the stack I did, and how you can build something similar.
Table of Contents
- Why Build an AI Ride Booking Agent?
- What Velora Can Do
- The Tech Stack
- How the Agent Works Under the Hood
- Building the Core Agent with Pydantic AI
- Connecting to a Live Airtable Database
- Generating PDF Receipts on Demand
- The Streamlit Frontend
- Deployment on Hugging Face Spaces
- Common Mistakes When Building AI Agents
- FAQ
- Final Thoughts
Why Build an AI Ride Booking Agent?
The gap between “AI demo” and “AI that actually does something” is massive. Most portfolio projects stop at generating text. Recruiters and clients want to see agents that interact with real systems — databases, APIs, and file generation.
A ride booking agent is the perfect showcase because it covers the full lifecycle:
- Create a reservation with structured data
- Read existing records from a live database
- Update reservation status (e.g., cancel a ride)
- Generate a tangible output (PDF receipt)
It proves you can build agents that don’t just talk — they act.
What Velora Can Do
Velora handles the complete ride booking workflow through natural language:
- Create bookings — passenger name, car type, pickup/dropoff addresses, times, and contact info
- Fetch reservations — retrieve any booking by reservation number
- Cancel rides — update the reservation status to “Cancelled” in the database
- Generate PDF receipts — instant downloadable receipts with all trip details
The agent is deployed live on Hugging Face Spaces and connected to a real Airtable base. Every operation you perform updates the database in real time.
Try it yourself:
- Live Demo: Hugging Face Spaces
- Live Database: Airtable Tables
- Source Code: GitHub Repository
The Tech Stack
| Layer | Technology | Purpose |
| ------------ | ------------------- | -------------------------------------------- |
| AI Framework | Pydantic AI | Agent orchestration and tool calling |
| LLM | OpenAI GPT-5-nano | Natural language understanding and reasoning |
| Validation | Pydantic | Structured input/output schemas |
| Database | Airtable | Live NoSQL database for reservations |
| PDF Engine | ReportLab | On-the-fly receipt generation |
| UI | Streamlit | Interactive chat interface |
| HTTP Client | HTTPX | Async API calls to Airtable |
| Deployment | Hugging Face Spaces | Free, public hosting with Docker |
| Container | Docker | Reproducible deployment |
This stack is intentionally lightweight. No heavy frameworks. No cloud bills. Just Python, a good agent library, and a live database.
How the Agent Works Under the Hood
The architecture is clean and linear:

User (Natural Language)
↓
Streamlit Chat UI
↓
Pydantic AI Agent (GPT-5-nano)
↓
Tool Layer (Pydantic-validated)
↓
Airtable API (Live Database)
↓
PDF Receipt Generator
When a user types a message, the agent:
- Understands intent — decides whether to create, fetch, or cancel
- Extracts parameters — pulls out names, addresses, times, reservation numbers
- Validates inputs — enforces types and required fields via Pydantic
- Executes the tool — makes async API calls to Airtable
- Returns results — responds in natural language with confirmation
- Triggers PDF generation — if a receipt is available, creates a download button
Building the Core Agent with Pydantic AI
Pydantic AI is the backbone of this project. It handles agent initialization, system prompts, and tool registration with minimal boilerplate.
Here’s how the agent is defined:
from pydantic_ai import Agent, RunContext
from pydantic import BaseModel, Field
agent = Agent(
"openai:gpt-5-nano",
system_prompt=(
"You are a helpful assistant that can access Data table to "
"query the results, generate the receipts, create and cancel the reservations. "
"Use the available tools to answer questions about "
"Receipts data. Be concise and accurate in your responses."
),
)
The system_prompt is critical. It tells the model exactly what it can do and keeps responses focused. Without this, the agent might hallucinate capabilities or give verbose, unhelpful answers.
Why Pydantic AI?
- Native Pydantic integration for type-safe tool inputs
- Built-in async support
- Clean decorator-based tool registration
- Works with multiple LLM providers
Connecting to a Live Airtable Database
The agent interacts with Airtable through three core tools: fetch records, create reservations, and cancel reservations. Each tool is a Pydantic-validated async function.
Fetching a Reservation
class AirtableFetchInput(BaseModel):
reservation: int = Field(..., description="Primary key (Reservation number) to search")
view: str = Field("Grid view", description="Airtable view name")
@agent.tool
async def fetch_records(ctx: RunContext, args: AirtableFetchInput) -> list[dict]:
url = f"https://api.airtable.com/v0/{BASE_ID}/{RECEIPT_TABLE_NAME}"
headers = {
"Authorization": f"Bearer {AIRTABLE_TOKEN}",
"Content-Type": "application/json"
}
formula = f"{{Reservation}}='{int(args.reservation)}'"
params = {
"filterByFormula": formula,
"maxRecords": 5,
"view": args.view
}
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json().get("records", [])
Notice the filterByFormula approach. Instead of fetching all records and filtering in Python, we let Airtable do the work. This keeps API calls fast and costs low.
Creating a Reservation
class AirtableCreateReservationInput(BaseModel):
passenger_name: str = Field(..., description="Name of the passenger")
car_type: str = Field(..., description="Type of car requested")
pickup_time: str = Field(..., description="Pickup date and time")
dropoff_time: str = Field(..., description="Drop-off date and time")
contact_number: str = Field(..., description="Customer contact number")
pickup_address: str = Field(..., description="Pickup location address")
dropoff_address: str = Field(..., description="Drop-off location address")
@agent.tool
async def create_reservation(ctx: RunContext, args: AirtableCreateReservationInput) -> dict:
url = f"https://api.airtable.com/v0/{BASE_ID}/{RESERVATION_TABLE_NAME}"
headers = {
"Authorization": f"Bearer {AIRTABLE_TOKEN}",
"Content-Type": "application/json"
}
record_data = {
"records": [{
"fields": {
"Name": args.passenger_name,
"Car_Type": args.car_type,
"Pickup_Time": args.pickup_time,
"Dropoff_Time": args.dropoff_time,
"Contact_Number": args.contact_number,
"Pickup_Address": args.pickup_address,
"Dropoff_Address": args.dropoff_address,
"Reservation_Type": "New_Reservation"
}
}]
}
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(url, headers=headers, json=record_data)
response.raise_for_status()
created_record = response.json().get("records", [])[0]
return {
"success": True,
"record_id": created_record.get("id"),
"message": f"Reservation created successfully for {args.passenger_name}"
}
The Pydantic models act as a contract. If the user forgets to mention a pickup address, the agent will either ask for it or infer it — but the tool will never execute with invalid data.
Cancelling a Reservation
Cancellation is a two-step process: find the record by reservation number, then patch its status field.
class AirtableCancelReservationInput(BaseModel):
reservation_number: int = Field(..., description="Reservation number (primary key) to cancel")
@agent.tool
async def cancel_reservation(ctx: RunContext, args: AirtableCancelReservationInput) -> dict:
# Step 1: Find the record
fetch_url = f"https://api.airtable.com/v0/{BASE_ID}/{RESERVATION_TABLE_NAME}"
headers = {
"Authorization": f"Bearer {AIRTABLE_TOKEN}",
"Content-Type": "application/json"
}
formula = f"{{Reservation_Number}}={int(args.reservation_number)}"
params = {"filterByFormula": formula, "maxRecords": 1}
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(fetch_url, headers=headers, params=params)
response.raise_for_status()
records = response.json().get("records", [])
if not records:
return {"success": False, "message": f"Reservation {args.reservation_number} not found"}
record_id = records[0]["id"]
# Step 2: Update status
update_url = f"{fetch_url}/{record_id}"
update_data = {"fields": {"Reservation_Type": "Cancelled_Reservation"}}
update_response = await client.patch(update_url, headers=headers, json=update_data)
update_response.raise_for_status()
return {
"success": True,
"reservation_number": args.reservation_number,
"message": f"Reservation {args.reservation_number} has been cancelled successfully"
}
This pattern — fetch then update — is common when working with Airtable. The reservation number is user-friendly, but Airtable’s API needs the internal record_id for updates.
Generating PDF Receipts on Demand
Once a reservation is fetched, the agent triggers PDF generation using ReportLab. The receipt is built in-memory and served as a downloadable buffer.
from reportlab.lib.pagesizes import LETTER
from reportlab.pdfgen import canvas
from io import BytesIO
def pdf_receipt_generator(data_input_api):
data = data_input_api.get("fields", {})
pdf_buffer = BytesIO()
c = canvas.Canvas(pdf_buffer, pagesize=LETTER)
width, height = LETTER
y_position = height - 50
c.setFont("Helvetica-Bold", 14)
c.drawString(50, y_position, "TRIP RECEIPT")
y_position -= 30
c.setFont("Helvetica", 12)
for key, value in data.items():
text_line = f"{key}: {value}"
c.drawString(50, y_position, text_line)
y_position -= 20
c.save()
pdf_buffer.seek(0)
return pdf_buffer
The PDF is generated server-side and streamed to the user as a download. No files are saved to disk, which keeps the deployment stateless and Hugging Face-friendly.
The Streamlit Frontend
The UI is a simple chat interface built with Streamlit. It handles message history, agent calls, and PDF downloads.
import streamlit as st
from main import ask_agent_sync
from pdf_generator import pdf_receipt_generator
st.set_page_config(page_title="Intelligent Booking Agent", page_icon=":robot:")
st.title("Velora AI Agent")
st.write("Chat with Velora about Bookings and Receipts")
if "messages" not in st.session_state:
st.session_state.messages = []
if "history" not in st.session_state:
st.session_state.history = None
if "tool_data" not in st.session_state:
st.session_state.tool_data = []
if "pdfs" not in st.session_state:
st.session_state.pdfs = []
# Display chat history
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# User input
if prompt := st.chat_input("Type your message..."):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("assistant"):
message_placeholder = st.empty()
message_placeholder.markdown("Typing...")
response = ask_agent_sync(prompt, st.session_state.history)
st.session_state.history = response["history"]
st.session_state.messages.append({"role": "assistant", "content": response["output"]})
message_placeholder.markdown(response["output"])
# Extract tool data for PDF generation
for entry in response["history"]:
if entry.__class__.__name__ == "ModelRequest":
for parts in entry.parts:
if parts.__class__.__name__ == "ToolReturnPart":
if parts.tool_name == "fetch_records":
st.session_state.tool_data.extend(parts.content)
if st.session_state.tool_data and not st.session_state.pdfs:
for item in st.session_state.tool_data:
pdf_buffer = pdf_receipt_generator(item)
st.session_state.pdfs.append(pdf_buffer)
# Render download buttons
if st.session_state.pdfs:
st.markdown("### 📄 Available Receipts")
for idx, item in enumerate(st.session_state.pdfs, start=1):
st.download_button(
label=f"📄 Download Receipt {idx}",
data=item,
file_name=f"trip_receipt_{idx}.pdf",
mime="application/pdf",
key=f"download_{idx}"
)
The key here is ask_agent_sync, a wrapper that bridges Streamlit's synchronous environment with the agent's async internals using nest_asyncio.
import asyncio
import nest_asyncio
from agent import agent
nest_asyncio.apply()
async def ask_agent(prompt: str, history=None):
result = await agent.run(prompt, message_history=history)
return {"output": result.output, "history": result.all_messages()}
def ask_agent_sync(prompt: str, history=None):
loop = asyncio.get_event_loop()
return loop.run_until_complete(ask_agent(prompt, history))
Without nest_asyncio, running async code inside Streamlit would throw event loop conflicts. This small utility makes everything work smoothly.
Deployment on Hugging Face Spaces
The project is containerized with Docker and deployed on Hugging Face Spaces.
FROM python:3.13.5-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
build-essential \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
COPY src/ ./src/
RUN pip3 install -r requirements.txt
EXPOSE 8501
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
ENTRYPOINT ["streamlit", "run", "src/app.py", "--server.port=8501", "--server.address=0.0.0.0"]
Why Hugging Face Spaces?
- Free hosting for demos
- Native Docker support
- Easy secret management for API keys
- Great for portfolio visibility
All sensitive credentials (Airtable token, base ID) are stored as Space Secrets. Nothing is hardcoded.
Common Mistakes When Building AI Agents
1. Skipping Input Validation
Without Pydantic schemas, your agent might pass malformed data to your database. Always validate tool inputs before execution.
2. Hardcoding API Keys
Never commit tokens to GitHub. Use environment variables or platform secrets. This project uses .env locally and Hugging Face Space Secrets in production.
3. Ignoring Async Patterns
Blocking API calls freeze your UI. This project uses httpx.AsyncClient for all Airtable operations and nest_asyncio to bridge Streamlit's sync runtime.
4. Over-Engineering the Stack
You don’t need LangChain, vector databases, or complex orchestration for every agent. Pydantic AI + a direct API client is often enough.
5. Forgetting Error Handling
Always handle “record not found” gracefully. The cancellation tool explicitly checks if a reservation exists before attempting an update.
FAQ
What is Pydantic AI?
Pydantic AI is a Python framework for building type-safe AI agents. It uses Pydantic models to validate tool inputs and outputs, making agent behavior predictable and robust.
Can I use a different LLM?
Yes. Pydantic AI supports multiple providers. You can swap openai:gpt-5-nano for anthropic:claude-3-sonnet, google:gemini-2.0, or any supported model with a one-line change.
Is the Airtable base really live?
Yes. The demo connects to a real Airtable base. You can view the live tables here and see your changes reflected instantly.
How does the PDF generation work?
ReportLab builds the PDF in-memory using a BytesIO buffer. The file is never saved to disk. Streamlit serves it directly as a download button.
Can I deploy this myself?
Absolutely. Clone the repo, add your Airtable credentials to .env, install dependencies with pip install -r requirements.txt, and run streamlit run app.py.
What’s next for this project?
Planned features include email receipt delivery, SMS confirmations, multi-database support, an admin dashboard, and voice-based booking.
Final Thoughts
This project proves that AI agents don’t need to be complex to be impressive. With under 300 lines of core logic, Velora creates reservations, queries a live database, cancels bookings, and generates PDF receipts — all from natural language.
If you’re building your AI portfolio, focus on real integrations. A chatbot that talks is nice. An agent that does something is memorable.
Check out the live demo, explore the database, and star the repo if you find it useful.
- Live Demo: Hugging Face Spaces
- Live Database: Airtable Tables
- GitHub: github.com/isaismail322/receipt_generator_ai_agent
If you enjoyed this breakdown, give it a clap and follow for more hands-on AI engineering content. Got questions? Drop them in the comments — I read every one.
Join thousands of data leaders on the AI newsletter. Join over 80,000 subscribers and keep up to date with the latest developments in AI. From research to projects and ideas. If you are building an AI startup, an AI-related product, or a service, we invite you to consider becoming a sponsor.
Published via Towards AI
Towards AI Academy
We Build Enterprise-Grade AI. We'll Teach You to Master It Too.
15 engineers. 100,000+ students. Towards AI Academy teaches what actually survives production.
Start free — no commitment:
→ 6-Day Agentic AI Engineering Email Guide — one practical lesson per day
→ Agents Architecture Cheatsheet — 3 years of architecture decisions in 6 pages
Our courses:
→ AI Engineering Certification — 90+ lessons from project selection to deployed product. The most comprehensive practical LLM course out there.
→ Agent Engineering Course — Hands on with production agent architectures, memory, routing, and eval frameworks — built from real enterprise engagements.
→ AI for Work — Understand, evaluate, and apply AI for complex work tasks.
Note: Article content contains the views of the contributing authors and not Towards AI.