Portfolio
Portfolio Assistant
Ask questions about the work, projects, or how to get in touch.
A Python-native LangChain agent backed by Gemini 2.5 Flash with three live tools — it reads a Google Sheet for dynamic pricing, checks Google Calendar for real-time availability, and books confirmed appointments directly to the calendar. All in a single conversational loop.
Project Overview
Business Problem
Handyman businesses waste time manually quoting and scheduling every job. This agent handles the full intake loop in conversation — pricing a job from live sheet data, finding open calendar slots, and booking confirmed appointments — so the business owner never has to touch a form.
One-Sentence Summary
A LangChain conversational agent with Gemini 2.5 Flash that quotes handyman jobs from Google Sheets, checks live calendar availability, and creates confirmed Google Calendar appointments — all through natural language.
My Role
Built every component from scratch: designed and implemented all three @tool functions, wrote the system prompt, configured the ReAct agent loop, handled Google OAuth, and wired the full conversation state management.
Biggest Challenge
Getting the agent to confirm before booking — LangChain agents are eager and will call tools immediately. The solution was a carefully engineered system prompt that teaches the agent to present slots as numbered options and wait for explicit user confirmation before invoking create_calendar_event_tool.
What I Learned
System prompt design is half the work with tool-calling agents. The tool definitions (docstrings) and the system prompt are the only things standing between a helpful agent and one that books appointments without asking. Precise language matters enormously.
Tools Used
Key Features
ReAct agent loop with Gemini 2.5 Flash as the reasoning backbone. Three live @tool functions: dynamic pricing from Google Sheets, real-time availability from Google Calendar, and confirmed booking creation via Calendar API. Full OAuth 2.0 credential flow. System prompt engineered to force slot-confirmation before any booking action. Conversation state maintained across the full session.
Architecture
Click any tool to see its implementation
Each @tool function has a docstring that tells the LangChain agent exactly when to use it. The agent decides which tool to call based on the user's message — no hardcoded routing.
Interactive Demo
Try the simulated agent conversation
This is a simulated demo — try: "how much for drywall repair", "what times are free Friday afternoon", or "book me for Tuesday at 10am"
Source Code
Key files from the project
from langchain.agents import create_agent from langchain_google_genai import ChatGoogleGenerativeAI from mypricing_tool import pricing_tool from myscheduling_tool import scheduling_tool from mycalendar_booking_tool import create_calendar_event_tool model = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0) tools = [pricing_tool, scheduling_tool, create_calendar_event_tool] agent = create_agent( model=model, tools=tools, system_prompt=""" You are a handyman service assistant. Use pricing_tool when the user asks for a quote or estimate. Use scheduling_tool when the user asks for appointment availability. Use create_calendar_event_tool ONLY after the user clearly confirms a slot. Present slots as numbered options. Do not book without explicit confirmation. """ ) def main(): conversation = [] while True: user_input = input('USER >> ') if user_input.lower() in ["quit", "exit"]: break conversation.append({"role": "user", "content": user_input}) response = agent.invoke({"messages": conversation}) conversation.extend(response["messages"])
@tool def pricing_tool(client_request: str): """ Review the customer's request and business pricing sheet, then determine the best quote. Use this when the user asks for a quote, estimate, or pricing. """ # Step 1: Read live pricing from Google Sheets sheets_service = get_sheets_service() result = sheets_service.spreadsheets().values().get( spreadsheetId=SPREADSHEET_ID, range="PricingSheet!A1:G100" ).execute() pricing_records = build_records(result) # Step 2: Ask Gemini to interpret and quote prompt = f""" You are a pricing analyst. Given this request and pricing sheet, return JSON with: service_type, urgency, complexity, estimated_price, explanation. Customer: {client_request} Pricing: {json.dumps(pricing_records)} """ response = model.invoke(prompt) return json.loads(response.content)
@tool def scheduling_tool(requested_day: str, time_window: str = "afternoon", duration_minutes: int = 60): """Find available Google Calendar slots for the requested day/time window.""" target_date = parse_next_weekday(requested_day) start_dt, end_dt = get_window(target_date, time_window) # Query Google Calendar freebusy API busy = get_busy_periods(calendar_service, start_dt, end_dt) open_slots = compute_open_slots(start_dt, end_dt, busy, duration_minutes) return { "available_slots": [ {"option_number": i+1, "display": slot.strftime("%A %I:%M %p"), "start_iso": slot.isoformat()} for i, slot in enumerate(open_slots[:5]) ] }
@tool def create_calendar_event_tool( customer_name: str, service_type: str, start_iso: str, duration_minutes: int = 60, location: str = "", notes: str = ""): """ Create a Google Calendar event for a CONFIRMED appointment. Only call this AFTER the user has explicitly confirmed a time slot. start_iso must be a full ISO datetime string. """ start_dt = datetime.fromisoformat(start_iso) end_dt = start_dt + timedelta(minutes=duration_minutes) event = { "summary": f"Handyman Appointment - {customer_name}", "start": {"dateTime": start_dt.isoformat(), "timeZone": "America/Chicago"}, "end": {"dateTime": end_dt.isoformat(), "timeZone": "America/Chicago"}, "description": f"Service: {service_type}\nNotes: {notes}" } created = calendar_service.events().insert(calendarId="primary", body=event).execute() return {"status": "success", "event_link": created["htmlLink"]}