Project Great Pumpkin

Follow along, one step at a time

Linus wants the most sincere pumpkin patch in NYC. You'll build the agent that finds it using real city data. Do the steps in order. Each one ends with a checkpoint so you know it worked, and a "Stuck?" box with the usual fixes.

  • Must do: Setup, 1 · Loop, 2 · Break it
  • If you have time: 3 · Score
  • Homework: 4 · MCP and the Skill bonus
Open the repo on GitHub

Setup

Get the code running

Why: Before any AI, make sure Python and the internet work. If this step passes, any later error is about your model settings.

No-install option: open the repo on GitHub, click Code → Codespaces → Create codespace. Everything installs for you and a .env file is created. Jump to step 4 below.

1. Download the code and go into the folder.

git clone https://github.com/3percentclub/project-great-pumpkin
cd project-great-pumpkin

2. Make a virtual environment (a private box for this project's packages) and turn it on.

Mac / Linux
python3 -m venv .venv
source .venv/bin/activate
Windows (PowerShell)
py -m venv .venv
.venv\Scripts\Activate.ps1

Your terminal line should now start with (.venv). Keep it on for every command below.

3. Install the packages.

pip install -r requirements.txt

4. Make your settings file.

Mac / Linux (Windows: copy .env.example .env)
cp .env.example .env

Open .env and fill in three lines: LLM_BASE_URL (who answers), LLM_API_KEY (your key) and LLM_MODEL (a model that supports tool calling). No key? Make a free account at openrouter.ai, create a key, and paste it into the OpenRouter block that's already filled in at the top.

5. Smoke test. This uses no AI, just the city data.

python src/patches.py

You should see

A short list of Queens gardens with addresses and zip codes, an empty list for 'Astoria', and a noise-complaint count. Names and numbers change with live data.

Stuck? Common errorsTap to see fixes
python3: command not found
Install Python 3.10+ from python.org, or use Codespaces.
Activate.ps1 cannot be loaded (Windows)
Run Set-ExecutionPolicy -Scope Process Bypass, then activate again.
No module named 'requests' or 'openai'
Your venv is off. Activate it (step 2), then run pip install again.

1 · Loop

Watch the agent loop

Why: The model never runs your code. It reads a tool card, asks for a call, and your loop runs the function and hands back the result.

python src/loop.py "Find a pumpkin patch in Queens"

Read what it prints. A 'tool call ->' line is the model asking. A 'tool result <-' line is your code answering.

You should see

[turn 1] tool call -> find_pumpkin_patches({"borough": "QUEENS"}) [turn 1] tool result <- [{"gardenname": "...", ...}] Answer: (a garden in Queens, with an address)

Now open src/loop.py and find run_agent. Look for the four STEP comments: SEND (messages + tool menu to the model), CHECK (no tool_calls means it's done), RUN (execute_tool does the work), REPORT (send the result back with the same tool_call_id, then loop).

Checkpoint: The final answer names a garden in Queens, with an address.

Stuck? Common errorsTap to see fixes
Missing LLM_BASE_URL (or KEY / MODEL)
Your .env is empty or misnamed. It must be called exactly .env, in the repo folder.
401 / invalid API key
The key is wrong or doesn't match the base URL. Re-copy it, no quotes or spaces.
404, 'model not found', or 'does not support tools'
Pick a different LLM_MODEL that supports tool calling. On OpenRouter, filter models by 'tools' and use a :free one.
429 / rate limited
Free models get busy. Wait a minute, or switch to another :free model.
Answers without any tool call
Small models skip tools. Use a bigger model.

2 · Break it

Break the spec, then fix it

Why: The model does exactly what the tool card allows. When it gets something wrong, fix the card, not the prompt.

Ask the way a real person would, using a neighborhood:

python src/loop.py "Find a pumpkin patch in Astoria"

You should see

tool call -> find_pumpkin_patches({"borough": "Astoria"}) tool result <- [] The answer is "I couldn't find any" or a confused guess. Astoria is a neighborhood, not a borough.

Fix 1: in src/loop.py, replace the borough line inside FIND_PUMPKIN_PATCHES with a list of allowed values (an enum) and a clearer description:

"borough": {
    "type": "string",
    "enum": ["BRONX", "BROOKLYN", "MANHATTAN", "QUEENS", "STATEN ISLAND"],
    "description": "One of the 5 NYC boroughs. Map neighborhoods to their "
                   "borough first (Astoria -> QUEENS, Bushwick -> BROOKLYN).",
},

Fix 2: in execute_tool, an empty list teaches the model nothing. Return an error with a hint instead, so it can retry on its own. Replace the find_pumpkin_patches branch with:

if name == "find_pumpkin_patches":
    gardens = get_gardens(args.get("borough", ""), args.get("limit", 5))
    if not gardens:
        return json.dumps({
            "error": f"No gardens found for borough {args.get('borough')!r}.",
            "hint": "Use one of: BRONX, BROOKLYN, MANHATTAN, QUEENS, STATEN ISLAND.",
        })
    return json.dumps(gardens)

Save and run the Astoria question again.

Checkpoint: The tool call shows "borough": "QUEENS" and the answer names a garden in Queens.

Stuck? Common errorsTap to see fixes
IndentationError or SyntaxError
Python cares about spaces. Line your pasted code up with the lines around it, and check for a missing comma after the closing }.
Still stuck?
Run the finished version: python solutions/milestone2.py "Find a pumpkin patch in Astoria"

3 · Score

Add a second tool: the sincerity score

Why: Linus wants the quietest patch. With two tools, the model chains them: find gardens, then check noise for each one's zip code.

1. At the top of src/loop.py, change the import line to:

from patches import get_gardens, count_noise_complaints, sincerity

2. Under FIND_PUMPKIN_PATCHES, add a second tool card and put both in TOOLS:

AUDIT_NOISE = {
    "type": "function",
    "function": {
        "name": "audit_noise",
        "description": "Count 311 noise complaints in one NYC zip code over the "
                       "last N days and return a sincerity score (0-100, higher "
                       "is quieter). Call once per garden zipcode.",
        "parameters": {
            "type": "object",
            "properties": {
                "zipcode": {"type": "string", "pattern": "^[0-9]{5}$",
                            "description": "5-digit zip code, e.g. 11102."},
                "days": {"type": "integer", "minimum": 1, "maximum": 30,
                         "description": "How many days back. Default 7."},
            },
            "required": ["zipcode"],
        },
    },
}
TOOLS = [FIND_PUMPKIN_PATCHES, AUDIT_NOISE]

3. In execute_tool, add this branch right before the 'Unknown tool' line:

if name == "audit_noise":
    count = count_noise_complaints(args["zipcode"], args.get("days", 7))
    return json.dumps({"zipcode": args["zipcode"], **sincerity(count)})

4. The model now makes one call per garden, so give it more turns. Change max_turns: int = 6 to max_turns: int = 12.

python src/loop.py "Which is the most sincere pumpkin patch in Astoria?"

Notice who does what: the model picks which zips to check. Plain code does the counting and the math. Never let the model do arithmetic your code can do.

Checkpoint: You see audit_noise calls for at least two different zips, with two different scores.

Stuck? Common errorsTap to see fixes
RuntimeError: No final answer after 6 turns
You missed step 4. Raise max_turns to 12.
NameError: count_noise_complaints
You missed step 1. Update the import line.
Still stuck?
Run python solutions/milestone3.py "Which is the most sincere pumpkin patch in Astoria?"

4 · MCP · optional

Stretch: plug the same tools into a real agent (MCP)

Why: Your loop only lives in loop.py. MCP wraps the tools once so any agent app (Claude Code, Cursor, VS Code, goose) can use them, and runs the loop for you.

Open src/server.py. It's the same two functions with type hints. FastMCP turns the hints and docstrings into tool cards automatically. Its tools are scout_patches and audit_noise.

python src/server.py

You should see

Nothing. It just waits. That's correct: it's listening for an agent app to connect. Press Ctrl+C to stop it.

Connect it to one app. Two rules: use full (absolute) paths, and use your venv's Python. In Codespaces, use the path from `which python` instead.

Claude Code (run from the repo folder)
claude mcp add great-pumpkin -- "$(pwd)/.venv/bin/python" "$(pwd)/src/server.py"
claude mcp list
goose (after goose configure)
goose session --with-extension "great-pumpkin:$(pwd)/.venv/bin/python $(pwd)/src/server.py"

Cursor or VS Code: copy the JSON from the README's Milestone 4 section. Then ask the agent in plain English: "Find community gardens in Bushwick."

Checkpoint: The app shows a scout_patches call with BROOKLYN. You wrote zero loop code: the app ran the loop.

Stuck? Common errorsTap to see fixes
Server shows as failed / not connected
Almost always a relative path or the wrong Python. Use the full .venv path.

Bonus · Skill · optional

Bonus: give the agent a playbook (Skill)

Why: A tool is something the agent can do. A Skill is a Markdown file that says how to do a whole job. The agent loads it only when your request matches its description line.

Read .agents/skills/sincerity-audit/SKILL.md: pick the borough → scout patches → audit noise per zip → rank → report a table. It's plain English, no code.

Claude Code reads Skills from .claude/skills/
mkdir -p .claude/skills && cp -r .agents/skills/sincerity-audit .claude/skills/

Restart Claude Code in this folder (with the MCP server from step 4 connected) and ask: "Audit the pumpkin patches in Astoria for Linus."

Checkpoint: You get a ranked Markdown table and one sentence naming the winner, without explaining the steps yourself.

Stuck on something not listed here? Post the exact error in the cohort Discord. See you at the Halloween Hackathon, Oct 23–25.