DEV Community

Cover image for My father wrote the papers — I built a RAG assistant so growers can query them safely
Kantemir Satibalov
Kantemir Satibalov

Posted on

My father wrote the papers — I built a RAG assistant so growers can query them safely

DEV Weekend Challenge: Passion Edition Submission

This is a submission for Weekend Challenge: Passion Edition

What I Built

Gardener's Assistant — a grounded RAG chat for horticulture.

My father spent decades as a plant breeder and researcher (Doctor of Agricultural Sciences, North Caucasus mountain horticulture institute). His articles — and his colleagues' — live in journal PDFs, not in anything a generic LLM can cite reliably. I built an assistant so growers and agronomists can ask questions and get answers grounded in those papers, with numbers verified before they reach the user.

The intended goal: make scientific horticulture queryable without hallucination. If retrieval cannot support an answer, the bot refuses. It does not invent rootstock codes, spray rates, or cultivar names.

What it does today:

  • Text chat over ~500 scientific articles (apple, pear, plum) via hybrid retrieval
  • Telegram Mini App + browser client (Docker, one compose up)
  • 68-question retrieval regression suite — the gate I run before trusting any LLM output
  • Numeric verifier in Go — dosages in the answer must appear in retrieved context

Demo

Gardener's Assistant chat: three horticulture questions in Russian receive grounded answers from scientific articles via RAG, with streaming response in a Telegram-style UI:

Admin panel: login, upload horticulture articles into the RAG corpus, trigger reindex, and review 👍/👎 answer ratings — filter by likes or dislikes, see totals, and inspect each rated Q&A with crop, timestamp, and session/message ID to trace who gave feedback:

GIF: see above — Russian UI and Russian source articles (working demo today). English corpus and UI copy are being rolled out next.

Run locally:

git clone https://github.com/kantik001/grounded_horticulture_en
cd grounded_horticulture_en
cp .env.example .env   # LLM_API_KEY required
docker compose up -d --build
Enter fullscreen mode Exit fullscreen mode

Chat: http://localhost/

Admin (article upload): http://localhost/admin.html

Three questions from the recording (Russian — matches the indexed corpus):

  1. Какие признаки парши на листьях яблони? — disease + glossary expansion
  2. Как густота посадки влияет на Айдаред на подвое СК 4? — exact identifier (BM25)
  3. Какие подвои подходят для интенсивного сада на склоне? — semantic + lexical blend

English equivalents (for the upcoming EN launch — same retrieval paths, translated articles):

  1. What are signs of apple scab on leaves?
  2. How does planting density affect Aidared on SK 4 rootstock?
  3. What rootstocks work for intensive orchards on slopes?

The public repo ships demo articles only (EN samples for quick start); the full journal corpus stays local for licensing. The pipeline, eval harness, and Docker stack are all there.


Code

GitHub logo kantik001 / grounded_horticulture_en

Grounded RAG horticulture assistant (English public portfolio, demo data only).

🍏 grounded-horticulture — horticulture assistant

Grounded RAG for horticulture: answers grounded in scientific articles with fact checking, not LLM hallucinations. Telegram Mini App and browser chat with API key.

License: Apache 2.0 Go Python Docker

Demo

Chat: question → RAG answer Admin: articles and 👍/👎
Chat demo Admin demo

▶ Full chat recording (MP4) · ▶ Full admin recording (MP4)


What it is

An assistant for gardeners and agronomists: text → hybrid search over articles → LLM answer with verification of numbers and dosages; photo → CV + recommendation (beta, no production weights in this repo).

Component Role
Go (server/) Auth, Postgres sessions, RAG+LLM orchestration, verify, rate limit, /metrics
Python (api/, rag/) Hybrid retrieval (Chroma + BM25 + reranker), CV /classify
Web (webapp/) Chat, article upload admin, nginx in Docker

Access: Telegram initData or browser X-API-Key (see .env.example).

Public repository: git contains demo data only (data/demo_hr/, data/apple/sample_*.txt). Full article…


Key paths:
Path What
rag/ + api/ Hybrid retrieval (Chroma, BM25, RRF, reranker)
server/ Go orchestration, SSE chat, numeric verifier
eval/*.jsonl 68 retrieval regression questions
scripts/run_rag_eval.py One-command eval runner
webapp/ Browser chat UI

How I Built It

Passion → engineering constraint

The personal motivation came first: my father's horticulture papers should be queryable, not buried in PDF archives. The engineering rule followed: I don't trust the LLM until retrieval is measurable. Before tuning models or UI, I wrote a 68-question JSONL eval suite — rootstock codes, diseases, out-of-scope refusals — and made it a regression gate.

Why hybrid retrieval (not "better embeddings")

Pure vector search understood topic but missed tokens that matter — cultivar names, rootstock codes like SK 4, OCR-noisy spellings.

Example eval question: "How does planting density affect Aidared on SK 4 rootstock?"

Vector-only returned paragraphs about rootstocks but not the Aidared token:

Fix: per-crop hybrid pipeline:

query → glossary expansion (domain synonyms)
     → Chroma (multilingual-e5-small) top-16
     → BM25 top-16
     → RRF merge
     → conditional cross-encoder rerank (rootstock / disease / variety only)
     → diversify (≤2 chunks per article) → top-8 to the LLM
Enter fullscreen mode Exit fullscreen mode

Result: 68/68 on the retrieval suite:

Re-verify anytime:

python scripts/run_rag_eval.py --suite all --in-process --fast
Enter fullscreen mode Exit fullscreen mode

Decisions worth calling out:

  • RRF over score normalization — BM25 and cosine similarities live on different scales; ranks merge cleanly.
  • Category-gated reranker — the cross-encoder helps dense technical questions but costs CPU; "when should I water?" skips it.
  • Eval retrieval separately from generation — no LLM tokens, ~20s locally, catches regressions before users do.

Go + Python split

  • Python (rag/, api/): embeddings, Chroma, BM25, reranker, POST /rag/context
  • Go (server/): auth (Telegram + API key), Postgres sessions, SSE streaming, numeric verifier (numbers in the answer must appear in retrieved context)

If retrieval is weak, Go short-circuits before paying for generation.

What broke (and what saved me)

Change Symptom Eval caught it?
Chunking split tables from headers Apple pass_rate −7 Yes — reverted in minutes
BM25 not rebuilt after corpus update Exact-code questions failed Yes
Glossary entry too aggressive MRR dropped, pass_rate unchanged Yes

Passion projects still need discipline. The interesting work wasn't the chat bubble — it was making scientific archives answerable without lying.


What I learned

  • Passion without measurement ships fairy tales. A 20-second eval run changed how I work more than any embedding upgrade.
  • Hybrid search (BM25 + vectors + RRF) beat "just use a better model" for scientific text with rare codes.
  • The hard problem was never the chat UI — it was making PDF archives answerable without lying.

Disclaimer: assistant output is informational; field decisions require local experts and compliant product labels. CV classification is beta.

Top comments (0)