This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
Every pickup football group has the same thread. "Saturday?" "Maybe." "What time?" "After work." "Which Saturday?" Twenty messages later, nobody has committed and nobody has played.
TouchGrass FC is a Telegram group bot that replaces that thread with one question and one poll. Someone types /play, and the bot asks everyone to reply with when they can make it, in their own words and whichever language they type in (it's built for Ukrainian, English, Spanish, German, Polish, French, Russian). A local Gemma model turns each reply into structured availability. From there it's plain code: check the 7-day forecast, drop any slot that wouldn't finish before sunset, pick the best one, find the nearest pitch on OpenStreetMap, and post exactly one poll with a map pin. Then the bot goes quiet, except for a reminder three hours before kickoff and one question afterwards: did you actually play?
That's the whole design for this week's theme. Per player, the interaction is one message and one tap on the poll. Per match, it's one /played from whoever remembers. The only metric I care about is the one /stats reports: ⚽ Matches actually played via this bot: N. (The bot's reply to /played is "Legendary. Logged. Go shower." Good advice, honestly.)
Not yet, and I'd rather say so than invent a match. I've run the full loop in a live Telegram chat: messy replies in; weather score, pitch pin and poll out; reminder queued. But any number you see on /stats in my demo comes from me pressing the button to test it, not from a game. I'll update this post after the first real one.
The bot runs on my machine, so there's no live link. Here's a walkthrough in a real Telegram chat:
No bot token? python -m touchgrass simulate replays a scripted group chat in your terminal (more on that below).
A Telegram group bot that turns messy "when can you play?" chatter into ONE poll for a real-life pickup football match — then goes quiet so people actually go outside.
MIT licensed, and a brand-new repo started during the challenge window.
One rule shaped everything: the model only gets the job code can't do, which is reading human messages. Everything else is boring, testable code.
People don't write schemas. "можу в суботу після 18, якщо не дощ", "Sat morning I'm in" and "пятница после обеда" all have to land in the same structure. Ollama's format parameter accepts a JSON schema, so each request carries the Pydantic model's schema, at temperature 0 (abridged):
payload = {
"model": model,
"stream": False,
"options": {"temperature": 0},
"format": _SCHEMA, # Pydantic JSON schema
"messages": [{"role": "system", ...}, {"role": "user", "content": message}],
}
For the first message above, the output looks roughly like this (abridged):
{
"can_play": true,
"slots": [{"weekday": "saturday", "start": "18:00", "end": null}],
"conditions": ["no_rain"],
"language": "uk"
}
If validation fails, the error goes back to the model together with the original message, once. If that fails too, the bot asks the player to rephrase instead of guessing. One gotcha worth passing on: Ollama's format wants the complete JSON Schema object, not just the $defs block that Pydantic nests sub-models under.
score = player_count × weather_score. Weather score is a 60% rain penalty plus a 40% wind penalty, averaged over the slot window. Any slot ending after sunset (with a 15-minute buffer) is filtered out before scoring. No LLM involved: it's just numbers, which means it's unit-tested on recorded fixtures instead of vibes.
The repo has 40 labelled messages in Ukrainian, Russian and English: slang, negations, conditionals, vague ranges like "можу в будь-який день після 19". The eval harness runs them against each Gemma size:
| Model | Overall | can_play | language | slots | conditions | avg_lat | p95_lat |
|---|---|---|---|---|---|---|---|
gemma3:1b |
18% | 45% | 48% | 55% | 80% | 6.6s | 16.1s |
gemma3:4b |
28% | 88% | 50% | 72% | 88% | 23.2s | 36.1s |
The Overall column is the harshest number in the table, and I'm showing it anyway. Per field, gemma3:4b is reasonable at what the planner needs most: whether someone can play (88%), under what conditions (88%), and roughly when (72%). Language detection sits around 50% for both sizes; it mostly decides which language the invite is written in, and I haven't dug into why it's so weak yet.
Speed is the other cost. On my machine [TODO: one line on your hardware, e.g. "a laptop with no GPU"], gemma3:4b takes 23 seconds per message on average (p95: 36 s). gemma3:1b is about 3.5× faster but gets can_play wrong more than half the time. So 4b is the default, and when it returns garbage the bot asks instead of guessing. For a group that coordinates once a week, I can live with 23 seconds. For anything conversational, I couldn't.
python -m touchgrass simulate replays a scripted group conversation in your terminal, runs the real planner on synthetic weather, and prints a transcript. If Ollama is running, it uses the real model, so you'll wait as long as the numbers above suggest. If it isn't, it falls back to canned parses: you see the flow in seconds, but you're not seeing the model.
/setlocation./in. If you don't reply to the bot, as far as it knows you don't exist.
I didn't benchmark a hosted model, so I won't pretend to know by how much it would beat a 4B Gemma on accuracy or speed. My guess is easily. Here's what going open bought me instead:
OLLAMA_MODEL), and nothing changes underneath me between runs. nlu.py, and the 29 tests run without network access. Anyone can change the schema, rerun the suite and open a PR without asking me, or a vendor, for credentials.
Cost, honestly, isn't an argument at this scale: a few dozen inferences are cents on any hosted API. It only becomes one when a bot like this has real users.
I built this in Google's Antigravity from one written brief, split into three parallel workstreams: language understanding plus the eval fixtures; planner, weather and pitch lookup; and the Telegram layer plus the simulator. The agent wrote the code, the 29 tests and the first draft of this post. I ran the result in a live Telegram chat, then edited this write-up.
gemma3:1b and gemma3:4b) on the same 40 fixtures. The results are in the eval section above, including the parts that don't flatter it.
The bot's best feature is that it knows when to stop talking. This post is trying to learn from it.