🌱 Agronaut
An open-source AI agronomy agent that runs locally on your own computer or server, connects to messaging apps like Telegram and WhatsApp, and computes aquaponics system designs from a deterministic, source-cited engineering core instead of guessing them.
A self-hostable agent specialized for agriculture: a domain application in the spirit of Hermes / OpenClaw, rather than another agent framework. Its first deep domain is aquaponics. Describe your water, space and species in one sentence and it returns a buildable design, with a bill of materials, an operating envelope, a source for every number, and an explicit list of what it does not model. It will also search fish × crop mixes for the ratio that grows the most food from the least water.
Built by a hands-on aquaponics operator to cut the pain he lived: years of reading papers and losing fish to figure out what the math could have told him up front.
The sizing method behind it is a granted Taiwan utility model patent (TW M661364). The code is MIT, runs on open weights, and needs no proprietary API.
Quick start
1. A real design in two minutes, with no API key and no account
pip install agronaut
agronaut size --fish tilapia --crop lettuce --area 12 --temp 27 --water 3000
That prints a sized system: tank and system volume, fish count, feed rate, biofilter media, pump duty, a bill of materials, a source for every number, and an explicit list of what it does not model. Nothing in that command touches a network.
Try agronaut list for the species and crops it knows, or agronaut optimize --area 10 --temp 28 --water 5000 --objective food to search fish × crop ratios.
2. Add the conversation, still on your own machine
agronaut setup
It asks which model and which channel you want, checks each key against the live service as
you paste it, reads your Telegram id off a message you send your own bot, and writes
~/.config/agronaut/.env itself. Nothing to hand-write.
Prefer to configure it by hand?
Chat needs a language model. Ollama is the shortest path, and Agronaut already defaults to it:
ollama pull qwen2.5 # ~5 GB, once. Any tool-calling model works.
agronaut # chat in your terminal
Then just say what you have: "I have a 20 m² greenhouse in Bobo-Dioulasso, water sits around 28 °C, I want tilapia and lettuce."
Prefer a browser? agronaut web serves the Streamlit app on
localhost:8501.
Photos too, if you want them: ollama pull llama3.2-vision and set VLM_PROVIDER=ollama.
Config lives in ~/.config/agronaut/.env for an installed copy, or ./.env in a checkout.
No GPU?
qwen2.5:3bruns on an ordinary laptop CPU. Would rather not run a model at all? A free hosted key works instead:LLM_PROVIDER=nvidiawithNVIDIA_API_KEYfrom build.nvidia.com.
3. Put it on your phone (optional)
# .env in the project root
TELEGRAM_BOT_TOKEN=... # from @BotFather
AGRONAUT_ALLOWED_IDS=... # your Telegram user id, so it is yours alone
agronaut bot
Message your bot. /log ammonia 0.5 nitrate 40 temp 27 puts a reading into your live twin
and /forecast tells you what the week ahead does to it — both with no model in the path,
so they work even when the LLM is slow or unreachable.
What needs what
| You want | You need |
|---|---|
size, size-hydro, optimize, list, the web calculator |
Nothing. Pure aqua_model: deterministic, offline, cited. |
| Chat, the Telegram bot, photo understanding | A model — Ollama locally, or a hosted key |
/log, /forecast, /advise, /approve |
A model for setup, then nothing: the twin commands never call one |
Trouble, or want Docker, a hosted demo, or WhatsApp? See Install and run: all the options.
Why it's different from a chatbot
A chatbot retrieves what a paper said. Agronaut computes the answer for your specific system. The trustworthy part is a deterministic engineering model — the LLM only collects facts, routes to the right tool, and explains results in plain language.
YOU ──▶ agent layer (LLM: collect facts, route, explain)
│ proposes values
▼
validation gate ── rejects bad/uncertain input ──┐
│ typed, validated │
▼ │
aqua_model (TRUST ZONE — pure, tested, cited) │
coefficients ▸ mass balance ▸ sizing ▸ optimizer ◀───┘
│
▼
a sized system + bill of materials + operating envelope
+ cited coefficients + an explicit "what's NOT modeled" list
The math is verifiable on its own — you can audit every coefficient (with its source) without trusting the model. Calibration ≠ validation: the engine ships with seed defaults from published sources, meant to be calibrated against a real running system.
Features
Three modes in the app (sidebar Mode switch):
- Assistant (chat) — troubleshoot a running system (low DO, yellow leaves, pump sizing…).
- Design Calculator — fixed inputs → a fully sized system: tank/system volume, fish count, feed/day, pump turnover, biofilter, makeup water, bill of materials, operating envelope, maintenance checklist, and a downloadable funder-ready report.
- Optimize Ratio — search fish × crop-mix combinations for the best ratio under your binding constraint (e.g. a fixed water budget), maximizing food, protein, or water-use efficiency, and showing the gain over a naive even split.
The design and optimizer modes are fully deterministic and need no LLM at all.
Send it a photo
Photograph a yellowing leaf, a sick fish, or green water — on Telegram, WhatsApp, or the web chat — and you get a cited differential, not a guess:
- A vision model describes what it sees. It only observes.
- A deterministic guard strips any measurement or prescription out of that description, so a
fabricated
pH 6.4or an inventedadd 5 mL of saltcan never enter the conversation as though you had said it. A named condition is kept but flagged unverified. - A fixed, cited table (
aqua_model/triage.py) maps the visible symptoms to a ranked list of candidate causes — each naming the knowledge document it came from, and each with the checks that would tell it apart from its neighbours.
It will not hand you a single confident diagnosis, because a photograph cannot support one: iron deficiency and pH lockout look identical in an image, so you get both plus the check that separates them. Ordering follows the knowledge base's own rules — pH before iron, water quality before any fish pathogen. Nothing it says states a dose.
See the system, and then see it running
Every design renders as a self-contained 3D page — greenhouse, tanks, filtration, beds, graded plumbing with the flow animated in the direction the water actually goes. One HTML file, no server and no CDN, so it opens from a double-click on a laptop that has never been online.
With a twin bound to it, the same drawing stops being a picture:
# a design, plus the season it would have at a real site, on a slider
python scripts/render_3d.py --crop basil --site taichung_2025 --days 365 -o first_year.html
Drag the scrubber and the fish grow, the water turns amber and then red as ammonia and
nitrite cross the bands aqua_model/advisory.py acts on, and the crop is drawn as vigorously
as it is actually growing. You watch the nitrite spike of week two arrive instead of reading
about it afterwards. On Telegram or WhatsApp, show_my_system_3d does the same for the system
you run: your fish count, your water, advanced through the weather that actually happened
since you last spoke to the bot, then forward through the forecast.
The badge always says which of the three you are looking at (as designed, today, or a forecast), because confusing them would be the worst thing this view could do. "Today" is the same state the bot calls "Now", so the picture and the conversation never describe different water. The panel says in as many words that the geometry is a proposed arrangement, never a survey of your site.
Voice notes
Speak instead of typing, on Telegram or WhatsApp. The transcript runs through a normal turn, so memory, tools and cited knowledge all apply.
Consultative agent
Agronaut runs a consultation, not a one-shot Q&A. It identifies your goal (design a system, optimize a ratio, or troubleshoot a problem), asks for the few essentials that goal needs, then gives a first-cut recommendation tied to your system — and remembers it (a typed System Profile + episodic notes) across sessions.
You can also set the mode explicitly with /design, /optimize, or /troubleshoot —
the bot then jumps straight to gathering what that goal needs. All commands appear in
Telegram's / menu.
Agronaut also learns from outcomes: after suggesting a fix it can check back later ("did the water change fix the ammonia?"), and whatever worked is remembered and shapes its future advice.
Lessons can also become shared knowledge: a generalized, PII-stripped version of a verified
fix is nominated, the owner approves it in a local review CLI (python -m agronaut_agent.review),
and approved insights then help other operators — labeled as community experience, never as
verified science.
And it calibrates to reality: when you report real measured outcomes (harvest weight, FCR, crop yield), Agronaut tunes your future sizings toward your system — bounded to the published empirical ranges, so a measurement can only move a coefficient within what the literature allows, and every calibrated number is labeled.
The deterministic sizing model now covers five fish (tilapia, clarias, channel catfish, trout, common carp) and 30+ crops — leafy greens (lettuce, kale, chard, spinach, pak choi, arugula, watercress…), culinary herbs (basil, mint, cilantro, parsley, dill…), and fruiting crops (tomato, cucumber, pepper, strawberry, eggplant, zucchini…) — each with cited, calibratable seed coefficients placed within FAO 589's published feeding-rate band for its category.
Honesty by design
Every result lists the coefficients it used (value + range + source: FAO 589, UVI/Rakocy, literature) and an explicit list of what it does not model (pH/alkalinity, micronutrients, salinity, solids, pests, cohort logic, per-crop ET). A confidently-wrong design can't masquerade as complete.
The same rule governs the advice layer. Citation is enforced in code, not asked for in a prompt: every retrieved passage is labelled with its source before the model ever sees it. And retrieval is allowed to say no — a question the corpus cannot answer returns "no matching passages" rather than the three closest paragraphs wearing source labels. Ask Agronaut the capital of Canada and it will decline, not cite an aquaponics paper at you.
The engineering model (aquaponics core)
Parametric, not machine-learned — buildable today from published equations:
- Feeding-rate ratio (FRR) sizes the system: grams of feed per m² of plant area/day.
- Nitrogen balance is an independent consistency check (feed → fish-retained → excreted → plants + solids + water-exchange + denitrification), flagging disagreement with FRR rather than silently reconciling — this guards against over-sizing the grow beds.
- Water balance (evapotranspiration + evaporation + sludge − rainfall) drives the water-budget feasibility check.
- Optimizer is bounded enumeration over a small species×crop palette (no heavyweight solver), with the even-split baseline inside the search space so it can never do worse.
The advice layer (retrieval), and how it was tuned
Sizing is computed. Troubleshooting advice is retrieved, from a corpus of 22 hand-written operator guides plus openly licensed publications — currently 3941 chunks, led by Goddek et al. (2019) and FAO 589.
Retrieval is measured, not assumed. docs/dpg/retrieval_eval/golden_set.json holds queries in
real operator voice ("my tilapia are gasping at the surface", not "dissolved oxygen") plus
off-topic controls that must be refused:
python -m scripts.retrieval_eval # recall@k, precision@k, MRR, MAP@k + floor separation
python -m scripts.retrieval_sweep --all # re-pick floor / per-source cap / hybrid β
python -m scripts.corpus_report # what each declared source actually contributes
Nine techniques were implemented and measured. Four ship, four lose, one is available but
unused — and the losses are recorded in docs/dpg/retrieval_eval/techniques.json with the
conditions that would reverse them, which is how hybrid search went from rejected to shipped when
the corpus grew:
| ships | why | |
|---|---|---|
| Relevance floor | on (1.50) | refuses 8/10 off-topic queries, silences 0/33 real ones, keeps 0.117 headroom |
| Hybrid BM25 + RRF | on (β=0.90) | lost at 362 chunks, won at 1354, re-confirmed at 3941 |
| Per-source cap | on (1) | two books hold 97% of the corpus; at cap=2 they take 2 of 3 slots |
| PDF cleaning | on | drops contents pages; running header removed from 111 chunks → 4 |
| Metadata filtering | available, off | the third leg of hybrid search. Filters source_type, kb_tag, chapter, page, url_category on both pools before fusion. A capability, not a ranking change — no golden-set number moves, and none is claimed |
| Header chunking · context prefix · PDF chapter labels · cross-encoder rerank | off | each measured worse on this corpus |
Current: hit 0.879 · recall 0.833 · MAP 0.604 · 8/10 off-topic refused · 0/33 real silenced.
A decision expiring, caught in the act
techniques.json was written 2026-08-25. The next day, commit 70b2d00 added a second book and
took the corpus from 1354 to 3935 chunks. Nothing was re-measured, and every constant silently
became wrong for the corpus that actually shipped:
| at 1354 (recorded) | at 3941, old constants | at 3941, re-tuned | |
|---|---|---|---|
| hit_rate | 0.939 | 0.818 | 0.879 |
| recall@k | 0.894 | 0.727 | 0.833 |
| MAP@k | 0.697 | 0.548 | 0.604 |
| off-topic refused | 8/10 | 4/10 | 8/10 |
Two constants moved, one did not. The floor tightened 1.65 → 1.50, because the distance bands separated as the corpus grew (on-topic worst 1.383, closest off-topic 1.411, where at 1354 chunks they overlapped and no floor could work). The cap tightened 2 → 1, because cap=2 was calibrated against one oversized source and there are now two. β stayed at 0.90 — it describes the relationship between two ranking signals, which is a property of the query language, not of how much text sits behind it.
Worth being precise about which change did what, because they pull opposite ways. The floor costs retrieval quality: at cap=1, staying at 1.65 would score hit 0.909 and MAP 0.624 against 1.50's 0.879 and 0.604. That is bought deliberately, to double off-topic refusal from 4/10 to 8/10. The cap is what pays for it: at floor 1.50, cap=1 gives 0.879/0.833/0.604 against cap=2's 0.788/0.697/0.538.
The floor was not tightened to 1.40, though that refuses all 10 controls: it clears the worst real query by 0.017, and this project had already rejected a 0.032 margin as too thin. 33 golden queries say nothing about the 34th; headroom is the only thing that does.
python -m scripts.retrieval_sweep --all # re-pick all three, with the evidence table
That command exists because the drift was not carelessness. Re-measuring three constants was an afternoon of ad-hoc scripting, so it did not happen. It also earned its keep immediately: while this work was in review the corpus moved again (a 22nd knowledge file, 3935 → 3941 chunks) and re-running was one command rather than an afternoon. Run it after any corpus or embedding-model change.
Three of the four failures share one mechanism: they add topic words to chunks in a corpus where every document already shares a vocabulary domain, which dilutes rather than disambiguates. What worked was structural — refusing irrelevant passages, refusing error pages, refusing to let one source fill the whole answer.
Corpus licensing is mixed and deliberately explicit. The code is MIT; FAO 589 is
non-commercial-only. See docs/dpg/CORPUS.md — commercial users should drop
that entry from urls.txt and rebuild. Vet any source before adding it:
python -m scripts.corpus_report --candidate "<url>" --label "<expected topic>"
It checks four things, because a source can fail in four ways: unreachable, empty, wrong subject (a guessed publication ID once resolved to "Sharks for the Aquarium" — 28k characters that pass every check except being about aquaponics), or not openly licensed.
Observability: what a turn actually did
Retrieval quality is measured offline against a golden set. Production behaviour is a different question, and needs a different instrument.
Every turn is one trace. All the events a turn produces — the message, each model call, each tool call, the retrieval, the turn summary — carry the same random per-turn id, so the log reads as a path rather than as counters:
agronaut traces # recent turns: which tools ran, what retrieval returned, where the ms went
agronaut analytics # p50/p95/max latency for turn / model / retrieval, tokens, thumbs up-down
The trace holds shape, never content. No prompt, no reply, no passage text, no query is
recorded, and that is enforced by an allowlist that drops unknown fields rather than by callers
remembering not to pass them (agronaut_agent/tests/test_turn_tracing.py asserts it). The trace
id is minted fresh per turn and is never derived from the user, so it groups a turn without
following anyone between turns.
What gets measured, and why those things. Turn latency and model latency separately, because
the course is blunt that the transformer is the bottleneck and this project previously timed only
retrieval — the fast, cheap stage. Token counts in and out, omitted entirely rather than recorded
as 0 when a provider reports no usage, so a quiet provider cannot drag every cost aggregate
toward zero. And a failed turn is still written, because dropping the turns that broke is how a
p95 comes to look healthier than the service is.
Does the answer actually use what was retrieved?
retrieval_eval scores whether the right documents were found. It cannot score whether the reply
used them, and a system can hit recall 0.894 while inventing every number in its answer.
AGRONAUT_FAITHFULNESS_EVAL=1 python -m scripts.faithfulness_eval
Three metrics of three deliberately different kinds:
| judged by | what it catches | |
|---|---|---|
faithfulness |
an LLM, per atomic claim | claims the retrieved context does not support — the grounding measure |
response_relevancy |
an LLM + embeddings | an answer that is true but does not address the question |
citation_accuracy |
code, no model | [source: ...] labels that were never retrieved — a fabricated citation |
The judge is treated as a witness, not an oracle: rubrics are binary with named labels, an
unparseable verdict counts as unjudged rather than being folded into either side, and
n_unjudged is printed beside every score. It calls the network, so it is opt-in and never runs
in CI; the scoring arithmetic is pure and unit-tested without a model.
Human feedback
/good and /bad on Telegram record a bare rating, 1 or -1. There is no comment field on
purpose — it is the one place message content could enter the analytics log, and the allowlist
would drop it anyway. agronaut analytics prints the positive share.
Pluggable LLM backend (open models)
The chat layer is model-agnostic — pick a backend with one env var, no code change:
| Provider | LLM_PROVIDER |
Notes |
|---|---|---|
| Ollama (local) | ollama |
Offline, default (qwen2.5), drives the full tool-calling agent. The shortest path for a grower self-hosting with no API key: ollama pull qwen2.5 and go. Pick a tool-capable tag — older ones (llama3, mistral) bind tools and then never call any. Photos too: VLM_PROVIDER=ollama with ollama pull llama3.2-vision. |
| NVIDIA (hosted) | nvidia |
OpenAI-compatible open models; free tier. Needs NVIDIA_API_KEY. |
| Hugging Face | hf |
Default Qwen/Qwen2.5-7B-Instruct (Apache-2.0, strong at JSON). Needs HUGGINGFACEHUB_API_TOKEN. |
| Self-hosted (OpenAI-compatible) | openai_compat |
Zero proprietary API — point OPENAI_COMPAT_BASE_URL at your own vLLM / llama.cpp / LM Studio / TGI server. Drives the full tool-calling agent with an open-weights model you host. |
Self-hosted, no vendor (the open-weights path)
Agronaut is meant to be run by the grower, on their own machine, so the no-vendor path is the one that matters most. The shortest version is Ollama:
ollama pull qwen2.5 # the brain
ollama pull llama3.2-vision # optional: photo understanding
export LLM_PROVIDER=ollama # this is already the default
export VLM_PROVIDER=ollama # only needed if you pulled the vision model
python bot.py
That is the whole setup: no API key, no account, no connectivity after the pull. Every subsystem a grower touches runs on their own machine — the tool-calling agent, the deterministic twin, retrieval, and photo understanding.
A note on vision tags: a text-only model will accept an image, ignore it, and describe
something plausible that is not in your photograph. Agronaut asks Ollama whether the model
can see and refuses to start the vision path if it positively says no, but an old Ollama
that reports nothing cannot be checked — so pull a tag you know does vision
(llama3.2-vision, qwen2.5vl, llava, or moondream on a small machine).
For more control over serving (batching, quantisation, a shared box), use any OpenAI-compatible server instead:
# example: vLLM serving a tool-calling-capable open model
python -m vllm.entrypoints.openai.api_server --model Qwen/Qwen2.5-7B-Instruct
# then:
export LLM_PROVIDER=openai_compat
export OPENAI_COMPAT_BASE_URL=http://localhost:8000/v1
python bot.py
This runs the deterministic core and the tool-calling assistant with no proprietary dependency — the configuration Agronaut submits for Digital Public Good platform-independence.
Override the model with LLM_MODEL. Provider libraries are imported lazily — install only
the one you use. The design/optimizer modes run with no LLM dependency at all.
Install and run: all the options
The Quick start above is the short path. This section is the rest: working from a checkout, Docker, a hosted demo, every CLI command, and the full environment-variable reference.
From source
To change Agronaut rather than just use it:
git clone https://github.com/Rekin226/Agronaut.git && cd Agronaut
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
python3 -m pytest # 1235 tests, no model server needed
Docker (one command)
docker compose up web # Streamlit at http://localhost:8501
docker compose --profile bot up # web + the Telegram bot (needs .env)
The web app's Design Calculator and Optimizer work immediately. Chat and the bot need an
LLM provider configured in a local .env (see below). The SQLite memory DB persists in a
named volume, shared between web and bot.
From source
python3 -m venv .venv && source .venv/bin/activate
pip install -e . # installs the deps and the `agronaut` command
streamlit run app.py
Open the sidebar Mode switch. The Design Calculator and Optimize Ratio modes work immediately (no model server). For chat, run Ollama locally or set a hosted provider (see above).
(pip install -r requirement.txt still works if you only want the libraries — pip install -e .
installs the same list and adds the command below. For a deterministic-only install with no
chat stack, requirements.txt is a light manifest covering just the calculator and optimizer.)
Deploy a hosted demo (free, ~2 min)
The deterministic modes deploy to Streamlit Community Cloud with
zero config — the light requirements.txt keeps the build fast and key-free:
- Fork this repo (or use your own).
- Streamlit Community Cloud → New app → pick the repo, branch
main, main fileapp.py. - Deploy. Chat mode shows a friendly "needs the chat stack" note; the calculator and optimizer are fully live.
See docs/demo.md for the deployment details and for recording a README GIF.
The agronaut command
One front door over everything the project ships. It works from any directory once
installed; bare agronaut is the chat REPL, because that is what most people want.
agronaut # chat with the agent in the terminal
agronaut size --fish tilapia --crop lettuce --area 12 --temp 27 --water 3000
agronaut size-hydro --crop lettuce --area 10 --temp 22 --water 500
agronaut optimize --area 10 --temp 28 --water 5000 --objective food
agronaut list # supported species, crops, objectives
agronaut web # the Streamlit app (trailing flags go to streamlit,
# e.g. agronaut web --server.port=9000)
agronaut bot # the Telegram bot
agronaut review # approve/reject pending community insights
agronaut analytics # usage summary: latency p50/p95, tokens, feedback
agronaut traces # recent turns as pipeline traces (no message text)
Where it keeps things. In a checkout, the knowledge base, the reference tables, the
fetched-page cache and the SQLite memory DB all sit beside the source, as before. Installed
non-editably, the cited corpus and the reference tables (price book, growth calibration, the
twin's validation record) are read from <prefix>/share/agronaut and state goes to your XDG
directories rather than into site-packages — override any of it with AGRONAUT_CORPUS_DIR,
AGRONAUT_REFERENCE_DIR, AGRONAUT_CACHE_DIR, or AGRONAUT_DATA_DIR.
| Command | Needs an LLM? |
|---|---|
size / size-hydro / optimize / list |
No. Pure aqua_model — deterministic, offline, cited. Bad input exits non-zero at the trust gate rather than guessing. |
chat (the default) / bot |
Yes — a tool-calling provider (see above). |
web |
Only for the app's chat mode; the calculator and optimizer run without one. |
Run the tests
python3 -m pytest # the aqua_model core suite is pure (no model server needed)
Run the Telegram bot
The consultative agent is reachable over Telegram.
The two you actually need, in .env or the environment:
| Var | Purpose |
|---|---|
TELEGRAM_BOT_TOKEN |
from @BotFather |
AGRONAUT_ALLOWED_IDS |
comma-separated Telegram user IDs allowed to use the bot (empty = open to anyone, discouraged) |
Plus a model, which you have already if you followed the Quick start:
LLM_PROVIDER=ollama and a pulled model, or LLM_PROVIDER=nvidia with NVIDIA_API_KEY.
Everything else is optional — retrieval tuning, voice, vision, caching. Skip this until something needs changing.
| Var | Purpose |
|---|---|
AGRONAUT_RELEVANCE_MAX_DISTANCE |
how far a passage may be and still be used as context (default 1.50, off disables). Calibrated against the golden set; not portable — re-run python -m scripts.retrieval_sweep --all after any corpus or embedding-model change |
AGRONAUT_HYBRID / AGRONAUT_HYBRID_BETA |
keyword+semantic fusion, on by default at β=0.90 (β is the semantic weight) |
AGRONAUT_MAX_PER_SOURCE |
how many passages one source may contribute to a single answer (default 2; 1 favours breadth, 0 disables) |
AGRONAUT_INDEX_CACHE |
the built index is cached under data/.index_cache/, keyed by a corpus fingerprint; off rebuilds every time |
AGRONAUT_RERANK / AGRONAUT_MD_HEADERS / AGRONAUT_PDF_SECTIONS |
techniques that measured worse on this corpus and ship disabled — kept because the verdict is corpus-dependent (see docs/dpg/retrieval_eval/techniques.json) |
LLM_PROVIDER / NVIDIA_API_KEY |
the tool-calling brain. Defaults to ollama (local, no key); nvidia is free at build.nvidia.com |
LLM_MODEL |
optional. Local default qwen2.5. On NVIDIA, mistralai/mistral-nemotron measured ~20x faster than llama-3.3-70b with correct tool calls (docs/telegram_twin_testing.md) |
VLM_PROVIDER / VLM_MODEL |
optional photo understanding — send a picture of a sick fish or yellowing leaf and the bot describes it, then diagnoses through the same cited flow. Defaults to a hosted NVIDIA vision model (VLM_PROVIDER=nvidia, needs NVIDIA_API_KEY); set VLM_PROVIDER=ollama and ollama pull llama3.2-vision to run it locally with no key and no connectivity. AGRONAUT_VISION=off disables. The vision model only observes: a deterministic guard strips any reading or prescription out of its description, and the diagnosis itself comes from a fixed, cited triage table (aqua_model/triage.py) that returns a ranked differential — never a single verdict. Photos work on Telegram, WhatsApp, and the web chat. |
ASR_PROVIDER / ASR_MODEL |
optional voice notes — a spoken message is transcribed then answered in the same language. Defaults to a local faster-whisper model (works offline — best for low-connectivity field use; needs pip install faster-whisper). Set ASR_PROVIDER=nvidia for a hosted endpoint; AGRONAUT_VOICE=off disables. |
source .venv/bin/activate
agronaut bot # long-polls Telegram; Ctrl-C to stop (same as `python bot.py`)
Run on WhatsApp (Cloud API)
Agronaut also speaks WhatsApp — the channel most smallholder-facing programs reach farmers on. It uses Meta's WhatsApp Cloud API (webhook in, Graph API out) and needs a WhatsApp Business account. Set:
| Var | Purpose |
|---|---|
WHATSAPP_TOKEN |
permanent access token |
WHATSAPP_PHONE_NUMBER_ID |
the sender phone-number id |
WHATSAPP_VERIFY_TOKEN |
any string; also entered in Meta's webhook config |
WHATSAPP_APP_SECRET |
app secret, used to verify inbound request signatures |
agronaut whatsapp # serves the webhook + a follow-up poller (or: python whatsapp.py)
It refuses to start half-configured and tells you which variable is missing and where in the Meta dashboard to find it.
WhatsApp is webhook-based, not long-poll. Unlike the Telegram bot, Meta has to reach your machine over HTTPS, so a local run needs a tunnel:
cloudflared tunnel --url http://localhost:8080 # or: ngrok http 8080
Paste the https://… URL it prints into Meta's webhook config together with your
WHATSAPP_VERIFY_TOKEN, subscribe to the messages field, and message the number.
Two things worth knowing before you start:
- This is not your personal WhatsApp. The Cloud API is for WhatsApp Business. A number registered to it cannot be used in the normal WhatsApp app at the same time. Start with the free test number Meta gives you and message it from your personal phone.
- Meta's test number can only reply to recipients you have verified (up to five), and the token on the API Setup page expires in 24 hours. Both are fine for trying it, and both need replacing (a real number, a System User token) before anyone else uses it.
The same brain, memory, tools, and follow-ups as Telegram.
Keep it running (systemd)
For an always-on bot that survives crashes and reboots, run it as a systemd --user
service. Create ~/.config/systemd/user/agronaut-bot.service:
[Unit]
Description=Agronaut Telegram bot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/path/to/Agronaut
ExecStart=/path/to/Agronaut/.venv/bin/python bot.py
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
[Install]
WantedBy=default.target
loginctl enable-linger "$USER" # run even when you're not logged in
systemctl --user daemon-reload
systemctl --user enable --now agronaut-bot # start now + on boot
Manage it:
systemctl --user status agronaut-bot # is it up?
systemctl --user restart agronaut-bot # after pulling/changing code
journalctl --user -u agronaut-bot -f # live logs
Only one poller may run at a time — a manual
python bot.pyand the service will conflict on Telegram'sgetUpdates. When the service owns the bot, restart it after code changes (systemctl --user restart agronaut-bot) instead of running the script directly.
Project layout
aqua_model/ # TRUST ZONE — pure Python, no LLM, no network, no I/O, fully tested
coefficients.py # cited data layer (value + range + unit + source + safety factor)
species.py crops.py # seed databases, every field sourced (5 species, 30 crops)
massbalance.py # nitrogen consistency check, water balance, biofilter
sizing.py # size_system() — FRR anchors; build-artifact output
hydroponics.py # size_hydroponic_system() — plants only, no fish
optimizer.py # optimize() — best fish/crop ratio under a constraint
triage.py # visual symptoms -> a ranked, CITED differential (never a verdict)
calibration.py # bound coefficients toward an operator's own measurements
validate.py # the trust gate — the only door into the model
report.py pilot.py # funder-facing design report and pilot proposal
schematic.py # deterministic SVG/PNG system diagram
layout.py hydraulics.py # placement, grading, routed pipe runs and the real pump head
production.py mirror.py # the coupled season twin, and one operator's live state
advisory.py # proposals with derived confidence — a human approves, nothing acts
scene3d.py # the 3D scene, including the twin state the viewer renders
logging_schema.py # versioned install-logging standard (the dataset moat)
agent/ # LLM-facing layer (imports aqua_model, never the reverse)
llm.py # pluggable chat backend (ollama | nvidia | hf | openai_compat)
vision.py # pluggable VLM + the observation guard + EXIF stripping
observation_features.py # prose -> the categorical vocabulary triage.py accepts
classifier.py # pluggable image classifier as a FEATURE source (no backend yet)
transcribe.py # pluggable speech-to-text
calculator_ui.py optimizer_ui.py facts.py # Streamlit views + the UI/model seam
agronaut_agent/ # the channel-agnostic brain
cli.py # the `agronaut` command — one front door, routes to the real callables
core.py # handle_message / handle_image / handle_voice — the three seams
tools.py # the LLM-callable tools (thin wrappers over the trust zone)
store.py profile.py # SQLite memory: System Profile, notes, calibration, follow-ups
rag.py semantic.py # citation-enforced retrieval (floor, hybrid, per-source cap) + recall
channels/ # telegram_adapter.py, whatsapp_adapter.py, base.py
scripts/ # safety_eval.py (hermetic golden set, runs in CI), vision_eval.py
# faithfulness_eval.py (faithfulness / relevancy / citation accuracy)
# corpus_report.py (what each source contributes; --candidate vets one)
# retrieval_eval.py (recall/precision/MRR/MAP over the retrieval golden set)
# retrieval_sweep.py (re-calibrates floor / cap / beta on the live corpus)
skills/ # the deterministic core as a portable agentskills.io skill + CLI
knowledge/ urls.txt # the curated, cited knowledge base (urls.txt: CATEGORY|URL|LABEL|LICENCE)
docs/dpg/ # DPG compliance pack: privacy, AI transparency, safety eval
CORPUS.md # corpus provenance + the code(MIT)/content(mixed) licence split
retrieval_eval/ # golden set, baselines, and every technique's measured verdict
app.py # Streamlit app (chat | calculator | optimizer)
pyproject.toml # packaging + the `agronaut` console script (deps read from requirement.txt)
srcs/chatbot.py # legacy RAG/state-machine layer, slated for retirement (#25)
Roadmap
- M1 — design calculator ✅ deterministic sizing, cited coefficients, report, logging standard
- M2 — ratio optimizer ✅ fish/crop mix for max efficiency
- M3 — agent orchestrator — 🟡 the tool-calling agent is built and is what every channel now runs on. Retrieval is now measured end to end (golden set, recall/MRR/MAP, a relevance floor that refuses off-topic questions) and every technique's verdict is recorded; fully demoting RAG to a pure citation tool is still open (#25)
- Field senses ✅ photos and voice notes on Telegram, WhatsApp and the web, behind a code-enforced observation guard and a cited visual-triage table
- M4 — digital twin — time-series simulator calibrated on real installed systems (#26)
- M5 — reach — SMS/USSD for farmers without a smartphone (#73), offline-first (#79)
- Beyond aquaponics — does the architecture generalise to irrigated field crops via FAO-56? (#78)
Status, honestly. The design, optimizer, and triage core is built, tested, and enforced in CI (700+ tests; a hermetic advice-safety golden set that fails the build on a regression). What is not done is validation against reality: the coefficients are literature seeds meant to be calibrated, and the vision path has never been scored against a real photograph because the corpus is empty. Calibrated ≠ validated, and the model says so in every result it produces.
The advice layer has the same shape of honesty and the same gap. Retrieval is measured against a
33-query golden set, but that set was written by one person against the corpus it already had —
it cannot tell you about questions nobody thought to ask. And corpus breadth is the live
constraint: 22 hand-written files still answer most queries, because open-access aquaponics
literature is plentiful while open operator guidance barely exists. Widening it is
#77, and docs/dpg/CORPUS.md records which
sources were surveyed and why they were not added.
Use it from another agent (agentskills.io skill)
Agronaut's deterministic engine is also packaged as a portable
agentskills.io skill in
skills/aquaponics-engineer/, so agents like
Hermes, OpenClaw, or Claude Code can hand users a computed, cited design instead of a
guess:
python -m skills.aquaponics_engineer.cli size-aquaponics \
--fish tilapia --crop lettuce --area 12 --temp 27 --water 3000
Same trust zone, same citations, no LLM — a bad input is rejected at the gate.
Contributing
You don't need an API key, a GPU, or ML experience to contribute here. The Design
Calculator, the Optimizer, the whole engineering core, and the visual-triage table are
deterministic — pip install -r requirement.txt && pytest and you're developing.
The three contributions this project needs most:
| What | Why it matters | |
|---|---|---|
| 🌾 | Agronomy knowledge — a crop, a species, a symptom rule, a correction | Every number needs a published source. Finding one is the work. Practitioner corrections are especially welcome. |
| 📊 | Real system data — your FCR, harvest weights, yields | The coefficients are literature seeds meant to be calibrated against reality. Your data makes the model true rather than plausible. → #22 |
| 📷 | Photographs — deficient leaves, sick fish, algae, root disease | The vision path is currently verified against handwritten test strings, not real photos. Run python -m scripts.check_vision_corpus to see what's wanted. |
Then: good first issues · CONTRIBUTING.md (setup + the trust-zone rules) · Code of Conduct
One rule worth knowing before you write code: aqua_model/ is a trust zone — pure Python,
no LLM, no network, every number carrying a cited source, every output stating what it does
not model. CI enforces the first part by installing only pytest pandas Pillow and asserting
the core imports without any LLM library. Details in
CONTRIBUTING.md.
Citing Agronaut
If you use Agronaut in research or programme work, see CITATION.cff.
License
MIT — see LICENSE. The code is open by design (it's built on published science); the value is in calibrated, real-world data, not the equations. Contributions welcome.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agronaut-1.0.0.tar.gz.
File metadata
- Download URL: agronaut-1.0.0.tar.gz
- Upload date:
- Size: 427.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bac6fec167964a79593ec125ff05afc031c618b24fc10914a995cb3f450e63b2
|
|
| MD5 |
bbedaf80b0d12f6c4839bf86bf404736
|
|
| BLAKE2b-256 |
837d8ae87c78065739722d6ab8fcbe342e57541be6d98b78189f5f1d94c61ba6
|
Provenance
The following attestation bundles were made for agronaut-1.0.0.tar.gz:
Publisher:
publish.yml on Rekin226/Agronaut
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agronaut-1.0.0.tar.gz -
Subject digest:
bac6fec167964a79593ec125ff05afc031c618b24fc10914a995cb3f450e63b2 - Sigstore transparency entry: 2755676671
- Sigstore integration time:
-
Permalink:
Rekin226/Agronaut@15c99546938e0a1cd332fce52ce96afed2100aba -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Rekin226
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@15c99546938e0a1cd332fce52ce96afed2100aba -
Trigger Event:
release
-
Statement type:
File details
Details for the file agronaut-1.0.0-py3-none-any.whl.
File metadata
- Download URL: agronaut-1.0.0-py3-none-any.whl
- Upload date:
- Size: 452.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8123e8360dbae303e947a8125d908b2c9573ed3615e15253f497fc0f60f5facd
|
|
| MD5 |
91c7324f55c00a175bd60d67ae1f18a1
|
|
| BLAKE2b-256 |
77a8e0bf30d01af3660f1d5e4f6c9f0f7dbe820ca51e73874f0b34d71898035b
|
Provenance
The following attestation bundles were made for agronaut-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on Rekin226/Agronaut
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agronaut-1.0.0-py3-none-any.whl -
Subject digest:
8123e8360dbae303e947a8125d908b2c9573ed3615e15253f497fc0f60f5facd - Sigstore transparency entry: 2755676701
- Sigstore integration time:
-
Permalink:
Rekin226/Agronaut@15c99546938e0a1cd332fce52ce96afed2100aba -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Rekin226
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@15c99546938e0a1cd332fce52ce96afed2100aba -
Trigger Event:
release
-
Statement type: