This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
What I Built
touch-grass is the Anti-Router: an offline-first wanderlust agent that plans exploratory pedestrian dérives — scavenger-hunt walks optimized for discovery, not speed.
Instead of telling you "turn left in 100 feet," it generates navigational riddles: "Face north. Somewhere north-west of here waits a fountain — follow the quietest path you can find, and let me know when you arrive." You have to actually look at the world — buildings, trees, water, art — to orient yourself. The phone stays in your pocket.
It's for anyone who walks the same three blocks every day and wants their own neighborhood to feel foreign again.
Demo
$ uv run touch-grass derive "Carmel-by-the-Sea, California" --minutes 45
Dérive: 2 waypoints, ~2.70 mi (budget 2.25 mi).
- a tree @ (36.54186, -121.92063)
- a tree @ (36.54569, -121.92933)
$ uv run touch-grass track # feed "lat,lon" GPS fixes on stdin
You found a tree. Face north. Somewhere north-west of here waits a tree —
turn toward the north-west, follow the quietest path you can find...
On macOS, track also speaks each riddle aloud via say. A fully offline touch-grass demo simulates the whole loop — graph, routing, geofences, riddles — on a synthetic city.
Code
touch-grass — the Anti-Router
Offline-first wanderlust agent: plans exploratory pedestrian dérives (scavenger-hunt walks) from cached OpenStreetMap data, then narrates navigational riddles instead of turn-by-turn directions.
Setup
uv sync # core deps (osmnx only — stays light)
For LLM riddles, run Ollama separately and set
TOUCH_GRASS_MODEL (e.g. qwen3:4b). The daemon holds the weights; this
package only talks HTTP to localhost — no heavy deps, no model in RAM.
Usage
uv run touch-grass demo # offline sim, synthetic city
uv run touch-grass derive "Portland, Oregon" --minutes 45
derive downloads the walk graph + POIs once, caches them under
~/.cache/touch-grass/, then works fully offline.
How it works
| Module | Role |
|---|---|
spatial.py |
Cached OSMnx walk graph + POI tag extraction; radius from time budget |
router.py |
Anti-cost routing: penalizes arterials, rewards footways/alleys + jitter |
persona.py |
Turns bearings/POIs into riddles; template by default, GGUF LLM via TOUCH_GRASS_MODEL
|
state.py |
Geofenced waypoint state machine — |
-
spatial.py— caches a pedestrian-only OSMnx walk graph + POI tags (historic=*,natural=*,leisure=park...) locally before you go offline -
router.py— the Anti-Router: Dijkstra where arterials cost up to 50× and footways/alleys are discounted, plus seeded jitter so no two dérives repeat -
persona.py— turns bearings and POI metadata into riddles via local Ollama (stdliburllib, zero SDK), with a template fallback when no model is running -
state.py— a 15 m geofence state machine: the LLM only wakes when you physically arrive
How I Built It
Everything runs at the edge:
-
Open-weight LLM: any Ollama-pulled model (e.g.
qwen3:4b) served by a local Ollama daemon —TOUCH_GRASS_MODELselects it,keep_alive: 5mdrops weights between waypoints to save battery -
Open data: OpenStreetMap via OSMnx —
network_type="walk"graphs cached as GraphML, POIs pickled; after one fetch the app never touches the network - No agent framework: LangGraph/Smolagents were in the spec, but a 4-module pipeline didn't need them — plain functions won
Built with uv, testable headless (uv run pytest — no network, no model required).
Why Does Open Innovation Matter?
The entire premise — data sovereignty for where your feet go — only works because the stack is open. Google Maps can't power an app whose selling point is that no one logs your route. OSM gave me the graph, open-weight models gave me the voice, and Ollama gave me inference that works in airplane mode. A closed API would have made this project self-defeating: the agent's value is that it knows where you walked and tells no one.
My Agent Session
Built with Devin (SWE-2). Notable: the agent hit two real bugs on first live run — geopandas demanding pyarrow for a parquet cache (fixed by switching to stdlib pickle), and ox.nearest_nodes requiring scikit-learn on unprojected graphs (replaced with a 3-line equirectangular scan). Both fixes made the dependency tree lighter — staying honest to "offline-first" meant fewer deps, not more.
Prize Categories
- Best use of open-weight models / local inference
- Best offline-first / privacy-preserving project
Top comments (0)