AWS Agents for Humans Hackathon submission
Errand Concierge is a conversational AI agent that helps you manage errands, appointments, and small tasks through natural chat, parse a messy brain-dump into organized items, reschedule things by saying "push the dentist to Friday," mark things done, and sync everything to your Google Calendar automatically.
Built with the Strands Agents SDK, running on AWS Bedrock via a custom OpenAI-compatible endpoint (Bedrock Mantle), with a FastAPI backend and a hand-built vanilla HTML/CSS/JS frontend.
Live demo: https://errand-concierge.vercel.app Backend API: https://errand-concierge-production.up.railway.app
- Chat-based errand capture — dump a list of things you need to do in plain language, and the agent parses and adds each one individually.
- Natural rescheduling — "move rent to next week," "push the dentist to Friday."
- Status tracking — mark errands done, cancel them, or reopen them.
- Trash tab — deleted errands are soft-logged with a snapshot and timestamp before being hard-deleted from the active list, so you can see what you removed.
- Google Calendar sync — connecting your Google account (Calendar-only OAuth scope) automatically creates, updates, and deletes matching calendar events as you add, reschedule, or cancel errands through chat.
- Live-state awareness — since errands can also be managed directly through the sidebar UI (buttons hit the backend directly, bypassing the chat agent), the agent always re-fetches the current errand list before answering questions about what's pending, rather than trusting its own conversation memory.
| Layer | Technology |
|---|---|
| Agent framework | Strands Agents SDK |
| Model | openai.gpt-oss-120b via AWS Bedrock Mantle (bedrock-mantle.us-east-1.api.aws) , a non-standard, OpenAI-compatible Bedrock endpoint, used because the classic Bedrock runtime API was blocked on the AWS account this was built with |
| Backend | Python, FastAPI |
| Storage | Flat JSON files (tasks.json, tasks_trash.json) , no database, single-user demo scope |
| Calendar integration | Google OAuth 2.0 (Calendar-events scope only), Google Calendar API v3 |
| Frontend | Vanilla HTML/CSS/JavaScript, no framework or build step |
| Backend hosting | Railway |
| Frontend hosting | Vercel |
errand-concierge/
├── agent.py # Strands Agent definition, system prompt, model config
├── backend.py # FastAPI app: chat endpoint, errand CRUD, serves frontend
├── tools.py # @tool-decorated functions the agent can call
├── storage.py # JSON file read/write for tasks
├── calendar_tools.py # Google Calendar sync helpers (create/update/delete events)
├── google_auth.py # Google OAuth flow + token storage/refresh
├── integrations_routes.py # FastAPI routes for the Google OAuth connect/callback/status flow
├── main.py # CLI entry point for testing the agent directly
├── test_agent.py # Standalone script for a quick agent sanity check
├── app.py # Alternate Streamlit-based local UI (not used in production deploy)
├── requirements.txt # Python dependencies
├── .env.example # Documents required environment variables
├── .gitignore # Excludes secrets and local data files
└── frontend/
├── index.html # Main UI markup
├── style.css # All styling
├── app.js # Chat, errand grid, Google integration UI logic
├── vercel.json # Rewrites /static/* to match Vercel's flat file layout
└── assets/
├── logo-mark.png # Icon-only logo (sidebar + masthead)
└── logo-full.png # Full wordmark logo (unused currently)
Set up the Strands Agent with a system prompt defining the concierge persona and five tools: add_errand, list_errands, reschedule_errand, complete_errand, cancel_errand. Wired the model to AWS Bedrock Mantle's OpenAI-compatible endpoint since the account's classic Bedrock runtime access was blocked. Built a FastAPI backend (backend.py) exposing /api/chat and errand CRUD endpoints, and a custom HTML/CSS/JS frontend served directly by FastAPI via StaticFiles.
Date miscalculation bug. The agent was computing explicit calendar dates itself (e.g. confidently stating the wrong day of the week for a given date), because it was trying to do date arithmetic internally instead of trusting the underlying date parser. Fixed by rewriting the system prompt so the agent passes relative date phrases ("tuesday," "next friday") straight through to the tools untouched, and never rewrites them into an explicit date itself. The system prompt is also now built dynamically at runtime (build_system_prompt()) so it's always grounded in the real current date.
Stale-memory bug. The sidebar's delete/complete/cancel buttons call the backend's REST endpoints directly, bypassing the chat agent entirely. Since the Agent object is a long-lived singleton holding conversation history, it could confidently describe errands as still pending after they'd already been changed through the sidebar. Fixed with a system-prompt rule: whenever the user asks about current errands, or references one by name, the agent must call list_errands fresh rather than trusting anything said earlier in the conversation — except immediately after the agent itself just called a mutating tool in that same turn.
Duplicate-errand refusal bug. A related issue surfaced during testing: if an errand was added via chat, then deleted through the sidebar, and the user then asked the agent to add the exact same errand again, the agent would refuse, saying "you already have this," based purely on its own conversational memory of having added it earlier — without checking whether it still actually existed. Fixed with an explicit system-prompt rule: never refuse or flag a duplicate based on conversation memory; only call it out as an existing errand if list_errands was called in that same turn and it's confirmed still present.
Debug print statements. Removed leftover print() debugging statements from tools.py's add_errand function and, later, from all of calendar_tools.py (parsing, event creation, update, and token-header helper functions all had [calendar debug]-prefixed prints).
Reschedule producing zero-duration calendar events. Found and fixed a real bug while cleaning up calendar_tools.py: update_calendar_event was setting a rescheduled event's start and end time to the exact same timestamp, producing a zero-duration event, and was missing the timeZone field that create_calendar_event correctly included. Fixed to compute a one-hour end time and include the timezone, matching the create path.
- Trash tab: hard-deleting an errand still fully removes it from
tasks.json, but a snapshot with a deletion timestamp is now logged totasks_trash.jsonfirst. Added aGET /api/errands/trashendpoint and a 🗑️ Trash item in the sidebar, rendering deleted errands as read-only cards with a "Deleted" badge. - Status badge colors: color-coded badges for Pending (yellow), Done (green), Cancelled (red), and Trash (grey), with matching CSS custom properties for washes and solid colors.
- Branding: replaced a placeholder inline SVG mascot with a real supplied logo, cropped to an icon-only transparent version for the sidebar and masthead. Added the tagline "Your plans. Our priority."
- Visual polish pass: colored icon badges on the feature cards (Stay on Track / Reliable / Save Time / Peace of Mind), a pill-shaped date badge in the masthead, a handwritten-style "Less stress / More done" annotation using the Google Font Caveat, and a custom notepad-and-pen SVG illustration next to the welcome message.
Several rounds of cleanup were needed here, documented honestly because mistakes were made and caught along the way:
- Moved the Bedrock API key out of a hardcoded literal in
agent.pyinto an environment variable, loaded viapython-dotenv'sload_dotenv()at startup, so the real key only ever lives in a local.envfile (never committed) or in the hosting platform's environment variable settings. - Confirmed
google_auth.pyalready read its Google OAuth credentials from environment variables with only harmless placeholder fallbacks, no code change needed there. - Found a real hardcoded Bedrock API key in
test_agent.py(a separate quick-test script) that had been missed in the first pass. Fixed it the same way, and rotated the exposed key in the AWS Bedrock console as a precaution once it had been in a local commit. - Built a proper
.gitignoreexcluding.env,tokens.json,tasks.json,tasks_trash.json,__pycache__/, and virtual environment folders, after first catching thatgit statuswas being run from the home directory rather than the project folder, which would have tracked unrelated personal files (AWS credentials folder, shell history, unrelated scripts) alongside the project. - When first pushing to GitHub, GitHub's push protection correctly blocked the push because it detected a real AWS-format key inside a committed file (
test_agent.py). The key was rotated, the file fixed, and the entire local git history was reset (rm -rf .gitand a freshgit init) before re-pushing cleanly, since simply fixing the file in a new commit would have left the exposed key sitting in an earlier commit's history forever.
Backend — Railway (originally planned for Render, switched after Render started requiring card verification even on its free tier during setup; Railway's trial credit required no card):
- Created a new project from the GitHub repo.
- Set the start command to
python backend.py. - Added environment variables:
BEDROCK_API_KEY,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_REDIRECT_URI,ALLOWED_ORIGINS. - Generated a public Railway domain.
- Hit and fixed, in order: a missing
requirements.txt(dependencies were never actually committed, causingModuleNotFoundError: No module named 'fastapi'), a circular import betweentools.py→calendar_tools.py→google_auth.pycaused by a stray import that had been introduced while editing files locally, and finally a batch of environment variables that had been set to their own variable names instead of their real secret values (e.g.GOOGLE_CLIENT_ID's value was literally the textGOOGLE_CLIENT_ID), plus aGOOGLE_REDIRECT_URIthat had never been updated offlocalhost. Once all four were corrected to real values, both chat and Google Calendar connected successfully.
Frontend — Vercel:
- Imported the GitHub repo with root directory set to
frontend/, no build step (static site). - After deploying, the entire page rendered as unstyled plain HTML with broken image icons. Root cause:
index.htmlreferences all assets under/static/...paths (/static/style.css,/static/app.js,/static/assets/logo-mark.png), because locallybackend.pymounts the wholefrontendfolder under a/staticURL prefix viaStaticFiles. Vercel has no such mapping and serves the folder's real file structure directly, so every asset 404'd. Fixed with avercel.jsonrewrite rule redirecting any/static/*request to the matching real path. - Fixed a mobile layout bug where the masthead (logo, heading, and a flex-row date-pill-plus-handwritten-note block) had no responsive wrapping rule, causing the note and date pill to be squeezed off the right edge of the screen on narrow viewports. Added a mobile breakpoint that lets the masthead wrap and stack, and hides the purely decorative handwritten note entirely on very narrow phone screens.
See .env.example for the full list. In short:
| Variable | Purpose |
|---|---|
BEDROCK_API_KEY |
Auth key for the Bedrock Mantle OpenAI-compatible endpoint |
GOOGLE_CLIENT_ID |
Google OAuth 2.0 client ID |
GOOGLE_CLIENT_SECRET |
Google OAuth 2.0 client secret |
GOOGLE_REDIRECT_URI |
Must exactly match an Authorized redirect URI configured in Google Cloud Console |
ALLOWED_ORIGINS |
CORS allow-list for the frontend's domain |
PORT |
Set automatically by Railway; the app reads this instead of a hardcoded port |
git clone https://github.com/normid291/errand-concierge.git
cd errand-concierge
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtCreate a .env file in the project root (see .env.example) with your real BEDROCK_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REDIRECT_URI (use http://localhost:8000/api/integrations/google/callback for local testing).
python backend.pyVisit http://localhost:8000.
- In Google Cloud Console, enable the Google Calendar API for your project.
- Create an OAuth 2.0 Client ID (type: Web application).
- Add
http://localhost:8000/api/integrations/google/callbackas an authorized redirect URI (and your production callback URL, if deploying). - Add your own Google account as a Test user under the OAuth consent screen, this app stays in "Testing" publish status, which is fine for a demo and doesn't require Google's app verification review.
- Single-user by design. There's no login or account system, one shared
tokens.jsonfile represents "the" connected Google account for the whole app. Anyone who opens the deployed URL sees the same connected calendar. This was a deliberate scope decision for a hackathon demo, not an oversight. - Timezone is hardcoded to
Africa/Lagosincalendar_tools.py, matching the developer's own timezone for the demo. Would need to be made configurable for real multi-region use. - No persistent database — task storage is flat JSON files on disk. On Railway's free tier, the filesystem is not guaranteed to persist across redeploys, so errand data may reset when the service restarts or redeploys.
- Backend (Railway): watches the GitHub repo's
mainbranch and auto-redeploys on push. - Frontend (Vercel): watches
mainand auto-redeploys on push, root directory set tofrontend/. - If the Railway URL ever changes, update
frontend/app.js'sAPI_BASEconstant and push. - If the Vercel URL ever changes, update the
ALLOWED_ORIGINSenvironment variable on Railway to match, and update the Google Cloud Console authorized redirect URI to use the current Railway callback URL.