Troubleshooting
The things that actually go wrong, in roughly the order people hit them.
RuntimeError: Missing environment variable: LLM_API_KEY
Working as designed — config fails loudly at startup rather than mysteriously at request time.
- Is
.envinapp/? - Any quotes around the value? Don’t quote them.
- Deployed? Environment changes need a redeploy to take effect.
- Following an older copy of this workshop? The variable was called
GEMINI_API_KEY— that name still works whenLLM_PROVIDERisgoogle.
ModuleNotFoundError for a local module like agent or tools
Run from inside app/, not the repo root — that’s the uv project root,
and uv run … resolves imports relative to it.
uv: command not found
The installer wrote to a shell profile your current terminal hasn’t read.
Open a new terminal, or source ~/.zshrc / source ~/.bashrc.
Tests fail with connection errors
They shouldn’t — the suite needs no network and no keys. If they’re hitting
the network, something in the code under test lost its mock. From app/, run
uv run --extra dev pytest -v and check which test.
The database
Section titled “The database”/health says database: false
In order of likelihood:
- You haven’t run
database/schema.sqlyet. SUPABASE_URLhas a trailing slash, or isn’t thehttps://xxx.supabase.coproject URL.- You used the publishable key instead of the secret one.
sb_publishable_…is blocked by the RLS you enabled — which is exactly its job. You wantsb_secret_…(or the legacyservice_rolekey on an older project). - You put the key in an
Authorization: Bearerheader as well asapikey. The new keys aren’t JWTs, so that header is at best redundant and at worst a rejected request. Sendapikeyonly. - The project is paused.
POST /runs 500s with column runs.thread_id does not exist
Your database was created before conversations existed. create table if not exists does not add columns to a table that’s already there, so re-running
an older copy of the schema changed nothing.
Fix: run database/schema.sql again from the current repo. The alter table … add column if not exists lines near the top of the runs section do the
upgrade, and the whole file is safe to re-run on a fresh database too.
The same applies to runs.messages, runs.provider and runs.model.
409 — “This conversation has reached 10 turns”
Working as intended. Every turn re-sends the whole history, so a thread costs
more per question the longer it runs and eventually stops fitting in the context
window. MAX_THREAD_TURNS turns that into an error you can act on.
Start a new conversation — the New conversation button in the client, or just
omit thread_id on the next POST /runs. Raise the limit only if you know your
model’s context window can take it.
The agent won’t answer anything about recent events
Check web_search on /health. If it’s false, no TAVILY_API_KEY is set and
the agent only has Wikipedia — which is written after the fact and genuinely
can’t help. It should say so rather than guess; if it’s guessing instead,
that’s worth knowing.
If it’s true and the agent still won’t search, look at the step log for
Searching the web. No such step means the model chose not to call the tool —
usually a sign the question doesn’t read as time-sensitive.
Everything worked yesterday, today nothing does
Free Supabase projects pause when they see too little database activity across a 7-day window. Open the dashboard and hit Resume project — you have 90 days before the backup is dropped.
This is the single most common way a workshop project dies. A paused database looks identical to a broken app. A few requests a day is enough to prevent it.
??? failure “extension \"vector\" is not available”
Supabase ships pgvector, so this means you’re on plain Postgres elsewhere.
Either install the extension or delete the memories block — nothing else
depends on it.
The model
Section titled “The model”404 from Gemini
Model names get retired and the error is unhelpfully generic. Check what’s
current at https://aistudio.google.com and set LLM_MODEL.
The default is gemini-flash-latest, an alias that shouldn’t 404. If you
pinned a specific version, that’s almost certainly the cause.
429 Too Many Requests
Free-tier limits are per key, per minute, per day, and per token. One agent run is 5–10 model calls.
app/llm.py already retries with backoff. If you’re still hitting it,
you’re sharing a key — everyone needs their own.
Empty response from model (finishReason=…)
Usually a safety filter, or the model spent its whole budget thinking. The
finishReason in the message tells you which.
The agent
Section titled “The agent”A run sits at running forever
Either it’s genuinely still working — check the steps table for the most
recent row — or the server process restarted mid-run and took the
BackgroundTasks job with it.
That’s the known limitation of BackgroundTasks; see
the upgrade path.
Worth adding: mark runs stuck at running for >10 minutes as failed, so the
UI stops spinning.
RuntimeError: Agent exceeded 12 steps
A guardrail doing its job. Either the question genuinely needs more research
(raise MAX_AGENT_STEPS) or the agent is looping — check steps to see
whether it’s repeating the same search.
The agent calls the same tool repeatedly
Your instructions or tool docstrings are too vague. Both are prompt.
Tightening the Args: descriptions is usually the whole fix.
The browser
Section titled “The browser”??? failure “"Failed to fetch"”
Almost always CORS.
- Set
ALLOWED_ORIGINSto your frontend’s origin, no trailing slash:https://yourname.github.io - Redeploy after changing it.
- Check you’re not calling an
http://backend from anhttps://page — browsers block mixed content. allow_origins=["*"]withallow_credentials=Trueis silently rejected by browsers. If you need credentials, list real origins.
It works locally, fails when deployed
Nine times out of ten it’s CORS or a missing environment variable. Check
/health on the deployed URL first — that isolates “is the service alive”
from “can the browser talk to it”.
Deploying the agent
Section titled “Deploying the agent”Build fails: uv: command not found
Render’s Python image doesn’t ship uv. The build command needs
pip install uv && uv sync --frozen.
Not an issue on FastAPI Cloud, which uses uv itself when it finds a
pyproject.toml and uv.lock.
Build fails: no pyproject.toml found
The uv project lives in app/, not the repo root, and both hosts default to
the repo root.
- Render — Root Directory →
app.deploy/render.yamlhandles this viarootDir: app, but a service created by hand needs the field set. - FastAPI Cloud — Root Directory →
appwhen you create the app, or App → Settings → Application Directory →app→ Update afterwards.
Build succeeds, service never starts
Check the start command binds 0.0.0.0, not 127.0.0.1. Inside a container
that’s the difference between reachable and invisible, and it shows up as a
port-scan timeout.
First request takes a minute
Free-tier cold start. Not a bug. Hit /health ten minutes before a demo.
Render spins down after ~15 minutes idle; FastAPI Cloud’s Hobby plan scales
to zero by default.
Pushed to GitHub, but FastAPI Cloud didn’t deploy
Only pushes to the default branch trigger a deploy — pushes to other
branches are ignored, and pull-request preview deployments aren’t supported.
Merge to your default branch, or deploy that commit with
uv run fastapi deploy.
A FastAPI Cloud variable should have been a secret, and wasn’t
Variables can only be marked Secret at creation. You can’t convert one afterwards in either direction — delete it and add it again with the toggle on.
And if a key was visible in a screenshot or a shared screen, rotate it. That’s cheaper than wondering.
Deploying the client
Section titled “Deploying the client”Vercel build fails: no package.json
Root Directory → client. The Vite project isn’t at the repo root, and this
is the single most commonly missed field in the whole deploy.
Changed VITE_API_URL, the site still calls the old backend
Vite inlines VITE_* variables into the bundle at build time, so there’s
no running process to pick up the new value. Redeploy.
If it’s still wrong after a redeploy, check the Backend URL field in the UI — it’s saved in your browser and overrides the built-in value. Clear it.
The deployed client 404s on refresh or a deep link
The static host is looking for a real file at that path. A single-page app
needs every path rewritten to /index.html — that’s what the rewrites block
in client/vercel.json does. On another host, find its equivalent.
The GitHub Pages copy loads blank, with 404s on the JS
A project page is served from /<repo>/, not the domain root, so the client
has to be built with a matching --base. website/scripts/sync-assets.mjs
passes it — if you built by hand, pass it too.
Keeping a free stack alive
Section titled “Keeping a free stack alive”The four things that will silently break a project you’re not touching daily:
| What | When | Fix |
|---|---|---|
| Supabase pauses | ~1 week idle | Ping weekly, or resume manually |
| The agent host sleeps | Render ~15 min idle; FastAPI Cloud scales to zero | Hit /health before demos |
| Gemini key rate-limits | per minute / day | Your own key; retry with backoff |
| Disk is wiped | every restart | Never write to disk; use Postgres |
A weekly keep-alive
Section titled “A weekly keep-alive”A scheduled GitHub Action is enough to stop Supabase pausing:
name: Keep the stack awakeon: schedule: - cron: "0 6 * * 1" # Mondays, 06:00 UTC workflow_dispatch:
jobs: ping: runs-on: ubuntu-latest steps: - run: curl -sS --fail --max-time 120 "${{ secrets.BACKEND_URL }}/health"/health queries the database, so one request keeps both the backend and
Supabase awake. Add BACKEND_URL to your repo secrets.
Before any demo
Section titled “Before any demo”- Hit
/health— confirmok: trueanddatabase: true - Run one real query end to end
- Open the live URL in an incognito window (catches “works because I’m logged in” and cache lies)
- Open it on a phone
- Have a recording as a fallback