Gita Guidance
Retrieval-only by design, so nothing can invent scripture
Stack
- Angular 22 standalone components
- TypeScript
- Devanagari, IAST, Hindi, English
- Speaker-separated results
- /lab transformer visualiser
- Per-head attention and causal mask
- Python, FastAPI, Pydantic v2
- BAAI/bge-m3 via sentence-transformers
- One vector per language, max-pooled
- 700-verse corpus, licence per field
- Canonical chapter:verse addressing
- Character-level GPT in PyTorch
- BM25 baseline evaluation harness
Problem
Someone typing “I am stressed, guide me” wants the verse that speaks to it, and two obvious approaches both fail. Keyword search cannot help: the word “stressed” appears nowhere in the 700 verses. A generative model can, and that is worse — the one failure a product like this must not have is inventing scripture and attributing it to a text people hold sacred.
What I built
- A 700-verse corpus in Sanskrit (Devanagari and IAST), Hindi and English, addressed by canonical chapter:verse, carrying licence metadata on every field.
- Multilingual dense retrieval with bge-m3: one vector per language per verse, max-pooled, so a Hindi question matches the Hindi rendering without the reader having to declare which language they are asking in.
- Speaker-aware ranking. Every verse is labelled by who speaks it, so the answer comes from Krishna and Arjuna’s lament is shown separately rather than competing with it.
- Exchange detection on top of those labels: where a Krishna verse directly follows an Arjuna one, both are rendered as a question and its answer — something the retriever cannot know, because it scores verses one at a time.
- A rule-based router that runs before embedding: 47 patterns across English, romanised Hindi and Devanagari, answering questions that express despair from a hand-chosen verse plan instead of from similarity search.
- An evaluation harness comparing dense retrieval against a BM25 baseline over in-corpus and out-of-corpus questions, and a routing test suite of 33 cases — 18 that must route, 15 that must not.
- A character-level transformer built from scratch in PyTorch, and a `/lab` page that draws it working: next-character probabilities, attention per head, and the causal mask as a picture.
Technical decisions & trade-offs
- Nothing on the answer path generates text. The shloka, its translations and its citation all come verbatim from the corpus, so the failure mode of a fabricated verse is structurally absent rather than prompt-mitigated.
- The router runs before the embedding call, not after retrieval. Similarity search matches despair beautifully to verses that are actively wrong to show someone in that state — 2:22 likens dying to changing clothes — so that query is never embedded at all.
- Routing replaces the ranking; it does not refuse the question. Refusing was a deliberate non-goal — the Gita exists to answer exactly this — so the curated path substitutes a hand-chosen plan for a ranked one rather than closing the door.
- The router’s tests assert both directions. Fifteen cases exist to prove ordinary distress does not over-route: a product that treats “my husband and I argue constantly” as a crisis is useless to the person asking.
- Exchange pairing only claims what the labels support. It stops at the first Krishna verse it crosses, so of 573 Krishna verses just 18 qualify — and 18:58, which sits 57 verses into an unbroken monologue, is never captioned as a reply to a question nobody asked.
- Hindi translations are copyrighted and therefore not in the repository. What is committed is the public-domain subset — the same 700 verses, enough for the app to run — so the licence boundary is enforced by what exists in git rather than by a note asking people to be careful.
- The transformer is kept in the repository and documented as unused. Its context window is 256 characters, which one verse plus a short question fills completely, leaving no room to answer in — a negative result worth writing down, since the alternative is a reader assuming a model sits on the answer path.
Challenges solved
- Embedding similarity matched questions to text that resembles a question — which in a dialogue is Arjuna asking, not Krishna answering. The fix was to label the corpus by speaker and rank on it, not to tune the query.
- There is no refusal path, and measurement says a score threshold will not supply one: the best out-of-corpus question scores 0.56 against a median answerable 0.52, so ask about cryptocurrency and the same openness returns five verses with a straight face.
- The evaluation set is 16 questions — 9 answerable, 7 not — against a target of 60, and the retrieval figures are over those 9, so one verse moves them 11 points. Indicative, and recorded that way rather than quoted as a result.
- A working prototype rather than an MVP: two local terminals and a 2.2 GB model download. Written down as the gap it is.
Outcome
A prototype that answers the question it was built for, and a finding I would not have got from a diagram: attention is retrieval — query against keys, weighted sum of values — and the RAG is that same operation with an external corpus and a top-k in place of the softmax.