🚀 Mirobody
The AI-native health data engine — collect, standardize, and reason over labs, wearables & genomics.
📚 Documentation · 💬 Hosted chat — chat.mirobody.ai · 🔌 API platform — platform.mirobody.ai
Blood tests, wearables, genomics, imaging — all fragmented, all incompatible. Before AI can understand your health, someone has to unify these signals into a single standard AI can actually read. That is what this engine does.
The engine does three things, and the codebase (and Contributing) is organized around exactly these three stages — the same C · S · A the documentation uses:
| Stage | What it means | Where |
|---|---|---|
| ① Collect | Pull signals in: 3 device providers + a SQL source · 7 file formats · Apple Health | pulse/ |
| ② Standardize | One standard: resolve any reading to canonical codes (LOINC · SNOMED CT · RxNorm), normalize units, land against FHIR-recognized code systems | indicator/ |
| ③ Answers | Reason: agents read theoriginal documents through a virtual filesystem and answer with charts & citations | agent/ |
⚡ Try it in 60 seconds
Indicator resolution is the engine's front door and needs no key, no config and no network:
pip install mirobody
mirobody resolve "LDL cholesterol" 血红蛋白 ヘモグロビン "空腹血糖(GLU)" 血脂
Until 1.2.1 reaches PyPI, run this from a source checkout (
git clone+git lfs pull+pip install -e ., as in Run the whole thing): the published1.0.62wheel is an empty shell — no CLI, and the resolver data files are 133-byte Git-LFS pointer stubs, so nothing resolves. Details in the CHANGELOG.
Real output, and the GIF is a build artifact —
docs/demo/resolve.htmlrendered byscripts/make_demo_gifs.py, so it cannot drift away from the command it claims to show.
from mirobody.engine import resolve, resolve_reading
resolve("血红蛋白").loinc # '718-7' any language, one code
resolve("total cholesterol").loinc # '2093-3' [Mass/volume]
resolve_reading("total cholesterol", "5.0", "mmol/L") # '14647-2' [Moles/volume]
resolve_reading("total cholesterol", "193", "mg/dL") # '2093-3' the unit picks the code
resolve("血脂").resolved # False a category, not an observation
Pass the value and the unit when you have them. LOINC encodes the unit and
the result type into the identity, so the same name resolves to different codes —
filing a mmol/L result under a mg/dL code is how one series quietly ends up
holding two units. resolve abstains rather than guessing: "" is a gap worth a
second look, "refused" is the answer.
→ Engine reference ·
Indicators
What the standardization layer provides
Standardization here is not a lookup table but a complete terminology-normalization system:
- Concept graph: 440,961 nodes · 22,044,110 cross-vocabulary edges · 595,746 source ids distilled into canonical concepts (LOINC · SNOMED CT · RxNorm bridges).
- 49,253 multilingual aliases (中文 22,578 · 日本語 16,809 · +5:
de·es·fr·ko·ru).
hemoglobin,血红蛋白,血紅素andヘモグロビンall land on LOINC 718-7. - 繁體中文 is two problems, handled as two. Script folding is mechanical
(a shipped 3,336-character zh-Hant → zh-Hans table); vocabulary is not — Taiwan
usage picks different words, and folding
血紅素yields the HbA1c code. Those terms are curated under their Traditional spelling, and a curated row always beats a fold. - Units normalized to ~310 UCUM families, with dimensional analysis, a
molar-mass bridge keyed by LOINC code, and an explicit refusal for
%vs10*9/L. 300 standard pulse indicators. - A second tier exists, and stays opt-in. Everything above is lexical, so it
abstains on terms it does not know — an honest ceiling. Cosine recall
(
indicator/semantic.py) reaches past it but cannot abstain: for a term it has never seen it returns its nearest neighbour with the confidence of a correct answer, and no threshold separates the two. No matrix ships, soresolve()is unchanged until you pointMIROBODY_SEMANTIC_INDEXat one — then use it to suggest a code a human confirms, never to mint an identity. → Semantic recall — the benchmark, the two axis gates, and whymin_scoreis not a correctness threshold. - We measure the claim instead of asserting it.
test_engine_coverage.pyscores the offline resolver against the panels an ordinary checkup includes, written the way a report prints them, in English, 简体中文, 繁體中文 and 日本語 — plus the wearable vocabulary the platform API teaches. 211/211 today; it scored 32/94 the day it was written. It grades clinical correctness: answering血红蛋白with the HbA1c code is a failure, and血脂is required to resolve to nothing.
pytest mirobody/test_engine_coverage.py -s # offline, about a second
→ Standardization · Architecture · Data flow
📊 Benchmarks — open and independently reproducible
Our health-AI benchmarks are the most-downloaded in their category on Hugging Face (4,000+ each):
| Benchmark | What it measures | Downloads |
|---|---|---|
| ESL-Bench | Event-driven longitudinal health agents — 100 synthetic users, 10,000 queries, programmatic ground truth (arXiv:2604.02834) | 4,800+ |
| MedHall-Bench | Medical hallucination | 4,500+ |
| MedHarm-Bench | Harmful medical advice | 4,300+ |
Reproduce any of them with one command via mirobody-eval, which also seeds a deployment with synthetic (PHI-free) trajectories.
🚀 Run the whole thing
git clone https://github.com/thetahealth/mirobody.git && cd mirobody
git lfs pull # the engine's data bundles; `resolve` needs them
./deploy.sh # Postgres + pgvector, Redis, server, worker
Then open http://localhost:18060. The server prints the accounts it accepts
at startup — the shipped one is caregiver@mirobody.ai, code 111111, named for
the role it plays: you sign in as the caregiver and the record you read belongs to
someone else.
No mail provider? You do not need one. The sign-in page opens on password, with email-code as a third tab:
curl -X POST localhost:18060/password/register -H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"at-least-8-chars"}'
One key runs everything. Set an OpenRouter key
in OPENROUTER_API_KEY — for the Docker stack that means the .env file next
to compose.yaml, then docker compose restart (that alone suffices: the app
re-reads /app/.env; a shell export does not reach the containers) — and
conversation, vision file parsing and semantic
indicator search are all live — chat via Claude/GPT/DeepSeek, embeddings via
the open-weights Qwen3-Embedding-8B (self-hostable: serve the same model
behind any OpenAI-compatible /v1/embeddings and point
OPENROUTER_BASE_URL at it).
If openrouter.ai is unreachable from your network (the case in mainland
China), a DashScope key in
DASHSCOPE_API_KEY is a drop-in replacement — chat via Qwen (DeepSeek/Kimi
one uncomment away), vision via qwen3-vl, embeddings via text-embedding-v4.
No further configuration either way; direct provider keys (Google, OpenAI)
remain supported — see config.yaml.
→ Docker deployment ·
Configuration ·
Local Python setup
👨👩👧 The whole engine, in four minutes
SEED_DEMO_DATA defaults to on, so the ① → ② → ③ chain is walkable the moment
./deploy.sh finishes — signing in and browsing the seeded record need no
key; the upload extraction in part 2 and the questions after it ride the one
key configured above. Four parts, each recorded against the running stack.
1 · Arrive. You sign in owning a thin record — a few weeks of
self-tracked vitals and one unremarkable checkup, seeded as your own — and find
one synthetic person sharing a thick one with you: Demo (synthetic),
244 indicators and 14,273 readings across two years, five documents the agent
can read_file. Same question, two records: your HbA1c answers with one
boring-normal value from data you own; hers answers with a two-year story
from data you can only view. Isolation you can see, not just read about.
The switch in that diagram is a column, not a promise:
care_circle_members.health_access, NOT NULL DEFAULT 0, on your own row.
Being invited into a circle shares nothing — the member decides, and no other
person's action can raise it. The check that reads it raises rather than
returning a falsy value, so a route that forgets to look answers 403 instead of
handing over a record.
examples/06_care_circle_rules.py prints
the whole decision table offline.
2 · ③ Answers, on someone else's record. Ask about her HbA1c and the agent finds the data itself, cross-references the lab draws against the sensor-derived series, and charts both — then tells you the improvement did not hold.
lab-drawn HbA1c 7.2 % (2024-04) → 6.5 % (2024-10) → 6.6 % (2025-04)
only 3 lab draws in two years — the sensor eA1C has 104
3 · ① Collect + ② Standardize, on your own.
mirobody/demo/lab_report_2025-10-15.pdf is a panel deliberately held out of the
seed, so uploading it is not a no-op. Drop it on the Data page and twelve
analytes come out with their values and units in seconds, each linking back to
the page it was read from.
4 · ③ Answers, on what you just uploaded. Ask again, now about your own record. The agent reads the report through the virtual filesystem, flags all twelve results against their printed reference ranges — and says plainly that one date is not a trend.
That contrast is the demo's point: two years of history buys a trend, one panel buys an interpretation. Both answers cite what they read.
Every value is synthetic — generated for ESL-Bench by
mirobody-eval and vendored, so the
seed needs no network and no key. Set SEED_DEMO_DATA=false for a deployment that
will hold real data. What the extraction pass does not yet do with those twelve
readings is written down in docs/roadmap.md rather than glossed
over here.
🧩 Extend it
Five directory keys point at plugin roots; drop a file in and restart. Tools become both agent tools and MCP tools with no extra wiring.
| You want | Drop it in | Docs |
|---|---|---|
| A new tool | mirobody/agent/tools/ |
Adding tools |
| An Agent Skill (SKILL.md) | mirobody/agent/skills/ |
Skills |
| A whole agent | mirobody/agent/ |
Agents |
| A device provider | mirobody/pulse/providers/ |
Provider integration |
| Someone else's MCP server | Settings → MCP | MCP integration |
Every tool the agent has is also served over MCP at /mcp, gated per user.
→ Built-in tools ·
MCP servers
🔌 Use it from your own code
| Surface | For | Docs |
|---|---|---|
pip install mirobody |
Resolution and file parsing, no server | Engine |
| HTTP API | Your app talking to a deployment | API overview · Data |
| MCP | Claude, Cursor, or any MCP client reading a user's record | MCP servers |
| Backbone mode | Your own agent, our data layer | Backbone |
Not sure which? → Choose your API
🏗️ Repository layout
mirobody/
├── pulse/ ① Collect — providers, file parsing, aggregation
├── indicator/ ② Standardize — the resolver, units, concept graph (no DB, no network)
├── agent/ ③ Answers — DeepAgent, tools, skills, chat
├── mcp/ the MCP server
├── user/ identity and the care circle — who may read whose record
├── schema/ the DDL, replayed at boot in dev
└── demo/ care-circle demo data
One rule, machine-enforced: indicator/ never imports the agent layer, so
pip install mirobody is roughly 200 MB across ~90 packages, with no framework
in sight — adding [agents] roughly triples it, to ~600 MB (fresh-venv
measurements; exact figures vary by platform and installer). Two import-linter
contracts hold the line, and lint-imports fails the build.
→ Architecture · CONTRIBUTING.md
📚 Documentation
For full documentation, see docs.mirobody.ai (English and Simplified Chinese).
| Quickstart · Installation · Self-host | Getting it running |
| Indicators · Providers · File processing | How the three stages work |
| API reference · Streaming · Function calling | Building against it |
| Contributing · Setup | Working on it |
In-repo, for contributors
Each package carries a README.md saying what it is; long-form guides live in
docs/. All of it is English, whichever README you arrived from.
| Where | |
|---|---|
| Runnable examples | examples/ |
| ① Collect | pulse/ · providers · aggregation · Apple Health |
| ① guides | connect a wearable · write a provider · file processing · Apple Health API |
| ② Standardize | indicator/ · indicators & units |
| ③ Answers | agent/ · tools · ChatGPT widgets |
| Plumbing | configuration · database schema · shipping the frontend |
| Working on it | CONTRIBUTING.md · testing · aggregator script · roadmap · CHANGELOG · SECURITY |
🤝 Contributing
The highest-leverage contribution is a term the resolver gets wrong. Run
mirobody resolve "<term>", and if the answer is wrong or empty add a row to
resolver_overrides.tsv plus a case to
test_engine_coverage.py — the coverage score
is the review.
pip install -e '.[test]' && pytest -q && lint-imports
→ Contributing guide · CONTRIBUTING.md
📚 Docs · 💬 Chat · 🔌 Platform · 🧪 Eval
Apache 2.0 · © 2026 Theta Health
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 mirobody-1.2.1.tar.gz.
File metadata
- Download URL: mirobody-1.2.1.tar.gz
- Upload date:
- Size: 24.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f9c3d32a43bf1ce86b4b738a58c57c0095c127b698b2cd150710b7e2664665c
|
|
| MD5 |
f86660f90a5d135e58c4fabde8691624
|
|
| BLAKE2b-256 |
0743b719c878152be213a38a07f9c0d206b52e4899c789f564a5bcf5a80515dc
|
File details
Details for the file mirobody-1.2.1-py3-none-any.whl.
File metadata
- Download URL: mirobody-1.2.1-py3-none-any.whl
- Upload date:
- Size: 24.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83aff0a2a8fc876e6f17121822e67faca8899b8bdc8c3bc7a43fed46385925bf
|
|
| MD5 |
4ec0dbb6a4c3699d54c67f1c78b23827
|
|
| BLAKE2b-256 |
e50b44e0e636e145622ae27f73f47af95737dfc26db17fb7beb2a05059b45bfd
|