Meet Lakshmi. She is 72, and she doesn't exist. I built her for this project as a synthetic patient, but if you grew up in an Indian family you have probably met someone exactly like her.
Lakshmi sees a general physician for her blood pressure, sugar and thyroid. A cardiologist added a blood thinner and a statin last month. Her knee started hurting, so an orthopaedic surgeon prescribed a painkiller and an antacid. Then she had a fever on a Tuesday night, and her son Ravi picked up a strip from the chemist downstairs.
That is four prescribers, eight medicines, and every single one written by brand name:
| Doctor | What they wrote |
|---|---|
| Dr. Rao, general physician | Telma 40, Glycomet 500, Thyronorm 50 |
| Dr. Menon, cardiologist | Ecosprin 75, Atorva 10 |
| Dr. Shetty, orthopaedic | Aceclo Plus, Pan 40 |
| Chemist, for the fever | Dolo 650 |
Each doctor saw their own page. Nobody saw the whole box. So here is the question Ravi can't answer on a Sunday morning, holding the pill organiser:
Is anything in this box the same medicine twice? And does anything here fight with anything else?
I built DawaiGraph (dawai is Hindi for medicine) for Graph Hacks to answer that question. This post is the story of how a graph answers it, one hop at a time.
Why this is a graph problem, not a search problem
Look at "Aceclo Plus" and "Dolo 650". The two names share nothing: no word, no letters that matter. Text search won't connect them, and neither will embeddings of the names.
But Aceclo Plus is a combination pill: aceclofenac plus paracetamol. And Dolo 650 is paracetamol. The connection doesn't live in the names. It lives one level down, in the salts.
That is a pattern, and patterns are what a graph database is for:
MATCH (b1:Brand)-[c1:CONTAINS]->(s:Salt)<-[c2:CONTAINS]-(b2:Brand)
WHERE b1.id IN $brand_ids AND b2.id IN $brand_ids AND b1.id < b2.id
RETURN s.name AS salt, b1.name AS brand_a, c1.strength AS strength_a,
b2.name AS brand_b, c2.strength AS strength_b
Two brands, one shared salt, two hops. For Lakshmi, DawaiGraph returns:
Aceclo Plus and Dolo 650 both contain paracetamol (325 mg + 650 mg).
Path: Aceclo Plus → CONTAINS → Acetaminophen → CONTAINS → Dolo 650
Doctors to ask: Dr. Shetty, the chemist
That path is the explanation. Nobody has to trust a model's word for it; they can see the hops.
The graph model
Everything lives in one FalkorDB graph: what medicines are, and what this family takes.
-
Medicine knowledge:
(:Brand)-[:CONTAINS {strength}]->(:Salt),(:Salt)-[:INTERACTS {level}]->(:Salt), and(:Salt)-[:IN_CLASS]->(:DrugClass). -
Family memory:
(:Person)-[:TAKES]->(:Brand), prescriptions with the doctor who wrote them, and(:Finding)nodes for every problem raised, with(:Carer)nodes for the family members who follow up.
The data is all open: the A–Z Medicine Dataset of India from Kaggle (about 250,000 brand rows), DDInter for drug–drug interactions (160,235 pairs), and Wikidata for drug classes. I loaded the 70,000 best-known brands, which takes about 65 MB of FalkorDB Cloud's 100 MB free tier.
One small thing made a big difference: a full-text index with phonetic matching on brand names. When someone types, or a photo gets read as, "Telmaa 40", it still finds "Telma 40 Tablet".
Hop by hop
Here is everything DawaiGraph found in Lakshmi's box, ordered by how far the graph had to walk.
Three hops: interactions live between salts, but people hold brands.
Telma 40 is telmisartan, and Ecosprin 75 is aspirin. DDInter lists a moderate interaction between those two salts. So the path is Brand → Salt → Salt → Brand:
Ecosprin 75 → CONTAINS → Acetylsalicylic acid → INTERACTS (Moderate) → Telmisartan → CONTAINS → Telma 40
The same walk found three more moderate interactions in her box: Atorva with Pan 40, Glycomet with Thyronorm, and Pan 40 with Thyronorm. Three of the four cross a doctor boundary: Dr. Menon's Atorva meets Dr. Shetty's Pan 40, Dr. Shetty's Pan 40 meets Dr. Rao's Thyronorm, and Dr. Menon's Ecosprin meets Dr. Rao's Telma. The doctor who wrote one half never saw the other half.
Four hops: some links only exist through a third medicine.
I added a tool to the agent that answers "why are these two connected?" with a shortest-path search over just this person's medicines. Ask it about Thyronorm and Ecosprin, two medicines with no direct link, and it comes back with:
Thyronorm and Ecosprin are connected through PAN 40, another medicine Lakshmi takes:
Thyronorm → contains → Levothyroxine → Moderate interaction → Pantoprazole (in PAN 40) → Minor interaction → Acetylsalicylic acid → contained in → Ecosprin 75
It also adds a note that each link is a separate pairwise fact, not a combined effect. That honesty mattered to me more than the path itself.
The hop that doesn't exist: India's data gap
This is the part that surprised me.
Open interaction datasets are built mostly around drugs sold in the US and Europe. Many salts Indians take every day simply aren't in them. When I matched the Indian brand list against DDInter, the most common salt with no interaction data at all was aceclofenac: about 9,000 brands of it. Domperidone was right behind it.
Lakshmi's knee painkiller, Aceclo Plus, contains aceclofenac. A plain lookup returns nothing. And in a medicine app, "nothing found" quietly reads as "safe".
So I asked: what would a careful pharmacist do? They'd reason from the drug's family. Aceclofenac is an NSAID, in the same class as ibuprofen, naproxen and diclofenac, which do have data. In the graph, that is a walk to the drug's cousins:
MATCH (x)-[:IN_CLASS]->(c:DrugClass)<-[:IN_CLASS]-(sib:Salt)
WHERE sib.ddinter = true
OPTIONAL MATCH (sib)-[i:INTERACTS]-(y)
WHERE i.level IN ['Major', 'Moderate']
If at least 80% of at least 3 siblings interact with something in the regimen, DawaiGraph raises a class signal. For Lakshmi:
No interaction data exists for aceclofenac (Aceclo Plus). 10 of 10 drugs in its class have a Major or Moderate interaction with aspirin (Ecosprin 75). Unverified: a signal to ask about, not a known interaction.
It raised a second one with metformin (Glycomet), and for Suresh, 68, another person in the demo whom Ravi also looks after, the same aceclofenac (in a dentist's Zerodol-SP) against his warfarin.
I didn't pick 80% and 3 from thin air. I hid 272 drugs that do have data, pretended they didn't, and checked how often the class signal predicted their real interactions. At 80% of at least 3 siblings, 87.4% of the signals were real interactions (recall 43%). Lower thresholds caught more but were wrong more often. For something a family reads about a parent's medicines, I chose fewer, more reliable signals, and the label says "unverified" every time.
No hop: "no path" is not "safe"
Suresh also takes Augmentin (from the dentist) and Amlong (from his cardiologist). Ask why they're connected, and the agent says there is no link in the data, not even through his other medicines, and then adds the sentence I care about most:
Having no data in the database is not the same as proving it is safe.
Making the agent unable to make things up
A medicine agent that sounds confident and is wrong is worse than no agent. So I took a few decisions early:
- Fixed tools, no text-to-Cypher. The agent (Gemini, with function calling) can only call a small set of parameterised queries: resolve a brand, list the family, read a person's medicines, check them, explain a connection. It never writes its own Cypher.
- Resolve every brand first, and ask when unsure. When I gave it a photo of a synthetic prescription, it read the brands it could find in the graph and refused to guess one it couldn't read clearly.
- A name check after every answer. Code collects every medicine name in the reply and checks that it came from a tool result or the user's own words. In testing, the agent wrote "paracetamol" where the data says "Acetaminophen". It got caught and rewrote the answer.
- "How I got this" under every answer. Every tool it called, the Cypher file behind it and what came back. Each decision can be traced.
Memory that belongs to one family
Findings are nodes, not chat history. A finding's key is a hash of what it's about, so if the same problem shows up next week, it updates the existing node instead of raising the alarm again. When Divya, Lakshmi's daughter, marks it "asked the doctor", Ravi sees that too.
Each family is its own tenant in the graph, so one family never sees another's data. And before the next appointment, the app prints a doctor visit sheet: one checklist per doctor, with what to raise first.
The number
I wanted to know what the graph actually adds, so I measured it. I built 30 regimens of 5 to 9 real Indian brands each (half with a deliberately repeated salt), computed the ground truth straight from the source tables, and asked two systems for the duplicates and interactions:
| 30 regimens, 119 known interactions | DawaiGraph | Gemini 3.5 Flash alone |
|---|---|---|
| Interactions found | 119 / 119 | 48 / 119 (40%) |
| Stated interactions that DDInter lists | 100% | 46% |
| Stated interactions DDInter doesn't list | 0 | 52 |
| Duplicate salts found | 24 / 24 | 21 / 24, plus 7 that aren't duplicates |
To be fair: DawaiGraph scores 100% because the graph is built from the same data, so this mainly proves the queries are faithful. The interesting column is the right one. The same model that writes DawaiGraph's explanations, asked without the graph, missed 60% of the listed interactions and added 52 that the data doesn't list. That is the whole argument for giving an agent a graph instead of asking it to remember.
Limits, honestly
- The brand data's prices are from 2022, and it lists at most two salts per brand.
- About 77% of brands matched a salt with interaction data. The rest are exactly the gap the class signal tries to cover, and it covers part of it, not all.
- All the people here are synthetic.
- It is not medical advice. DawaiGraph never says "safe" and never tells anyone to stop a medicine. It tells you which doctor to ask.
What I learned
- Put the reasoning in the data, not the prompt. Every finding above is a path in the graph, which is why it can be shown, checked and remembered.
- The empty result is a feature. The most useful thing DawaiGraph does may be refusing to treat "no data" as "safe".
- Small free tiers teach you to be careful. One unbounded path query on the whole graph froze my free FalkorDB instance. Restricting path search to one person's medicines made it fast and safe.
Try it
- Live: https://dawaigraph-1037019152152.asia-south1.run.app (each browser gets its own copy of the synthetic families)
- Code, graph model and every Cypher query: https://github.com/Ankith-m1006/dawaigraph
- Demo video: https://youtu.be/cQ_jSRnSp58
- There's also a read-only MCP server, so any AI assistant can check a medicine list against the same graph.
Built for Graph Hacks by WeMakeDevs × FalkorDB. I used Claude (Anthropic) as a coding assistant to build DawaiGraph and to help draft this post; the decisions, testing and final wording are mine.




Top comments (0)