Mirobody
The AI-native health data engine — Collect · Translate · Answer.
One lab prints A1c, the next HbA1c, a third hemoglobin A1c — one test,
three spellings, and that is before the units disagree. Mirobody turns lab
reports, wearables and genomics into one language AI can read: LOINC-coded,
UCUM-normalized, FHIR-ready. The resolver runs offline, and the engine powers
Theta Wellness, a live consumer health
product with 5,000+ registered users and 500+ daily active.
English · 中文
⚡ Try it in 60 seconds
No key, no config, no network — and with uvx, no install either:
uvx mirobody resolve "LDL cholesterol" 血红蛋白 ヘモグロビン "空腹血糖(GLU)" 血脂
pip install mirobody instead if you want it on your PATH. Either way the
resolver answers from a bundle that ships with the package.
血红蛋白 and ヘモグロビン land on the same code as hemoglobin, LOINC 718-7;
空腹血糖(GLU) on fasting glucose. 血脂 (lipids) is a category, not an
observation, so the resolver returns nothing rather than a plausible wrong code.
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("中性粒细胞百分比").loinc # '26511-6' Neutrophils/Leukocytes
resolve_reading("中性粒细胞", "62 %", None).loinc # '26511-6' a percentage...
resolve_reading("中性粒细胞", "4.2", "10*9/L").loinc # '26499-4' ...and a count are two codes
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.
resolve abstains rather than guessing: an empty code is a gap worth a second
look; method="refused" is a decision.
→ Engine reference · Indicators
Why Mirobody
- Two reports, same test, different spelling and units. That is the problem. Mirobody translates any reading, in any language, to LOINC + UCUM — and refuses to guess when it does not know the term.
- AI can only reason over what it can read. The agent reads the original documents, charts lab draws against sensor-derived series, and cites the page it read from.
- Self-hosted, standards-based, Apache-2.0. One
./deploy.shruns the whole stack on your own machine; what comes out is LOINC-coded, FHIR-ready records you can take anywhere, and the same tools are served over MCP to Claude Desktop, Cursor or your own agent.
Collect · Translate · Answer
The engine does three things, and the codebase, the docs and Contributing are organized around exactly these three stages:
| Stage | What it means | Where |
|---|---|---|
| ① Collect | Pull signals in: 3 device providers · 7 file formats · Apple Health (receive-only: a signed iOS client POSTs it in) | pulse/ |
| ② Translate (standardize) | One standard: resolve any reading to canonical codes (LOINC · SNOMED CT · RxNorm), normalize units to UCUM, land on FHIR-recognized code systems | indicator/ |
| ③ Answer (agent) | Reason: an agent reads the original documents through a virtual filesystem and answers with charts and citations | agent/ |
By the numbers
| Concept graph | 440,961 nodes · 22,044,110 cross-vocabulary edges · 595,746 source ids distilled into canonical concepts (LOINC · SNOMED CT · RxNorm bridges) |
| Aliases | 49,253 multilingual (中文 22,578 · 日本語 16,809 · de·es·fr·ko·ru); hemoglobin, 血红蛋白, 血紅素 and ヘモグロビン all land on 718-7 |
| Traditional Chinese | a shipped 3,336-character zh-Hant → zh-Hans fold table, plus curated Traditional rows — a curated row always beats a fold |
| Units | ~310 UCUM families with dimensional analysis and a molar-mass bridge keyed by LOINC code; 305 standard pulse indicators |
| Coverage | 213/213 on the panels an ordinary checkup prints, in English, Chinese (Simplified and Traditional) and Japanese (test_engine_coverage.py) |
| Bundle | LOINC 2.82: mirobody.BUNDLE_VERSION → loinc-2.82+2026.08.28-af2524b7a285 — release, cut date and a digest over the bundle's own members |
| Install | pip install mirobody is 2 packages, on numpy only — ~52 MB on macOS, ~100 MB on Linux, where numpy bundles its own BLAS |
Why 2.82 and not 2.83, what LOINC covers of the wearable world, and the opt-in semantic tier that cannot abstain: → Standardization in depth
📊 Benchmarks — open and independently reproducible
The most-downloaded health-AI benchmarks in their category on Hugging Face, 4,000+ downloads a month each: ESL-Bench (event-driven longitudinal health agents — 100 synthetic users, 10,000 queries, arXiv:2604.02834) · MedHall-Bench (medical hallucination) · MedHarm-Bench (harmful medical advice). 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 --depth 1 https://github.com/thetahealth/mirobody.git && cd mirobody
git lfs install && git lfs pull # the resolver's LOINC bundle; a fresh clone holds a pointer stub until you do
./deploy.sh # Postgres + pgvector, Redis, server, worker → http://localhost:18060
--depth 1 because the history is mostly superseded frontend builds and you
almost certainly do not want it: it takes the clone from ~125 MB to ~99 MB
(measured 2026-09-14 — most of what is left is the LFS bundle, which both
copies need).
Drop the flag if you plan to send a pull request. ./deploy.sh also fetches
the 22 MB concept graph behind semantic indicator search, which is a release
asset rather than a tracked file — see mirobody/res/EXTERNAL.tsv.
Sign in as caregiver@mirobody.ai, code 111111. No mail provider needed: the
sign-in page opens on password, and an account of your own is one request away:
curl -X POST localhost:18060/password/register -H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"at-least-8-chars"}'
Your care circle. The account is named for the role it plays: you sign in as the caregiver, and the record you read belongs to someone else. A circle is the unit of sharing — each member holds their own record and decides, on their own row, whether the others may view it.
SEED_DEMO_DATA is on by default, so the circle is already populated: you own a
thin record — a few weeks of self-tracked vitals and one unremarkable
checkup — and one synthetic person shares a thick one with you,
Demo (synthetic): 244 indicators, 14,273 readings across two years, five
documents the agent can read. Same question, two records: your HbA1c answers
with one boring-normal value from data you own; hers with a two-year story from
data you can only view. The recording walks both sides — your own indicators and
files, then the switch to her shared record and its two years of HbA1c:
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. A route that forgets to check answers 403 instead of
handing over a record. examples/06_care_circle_rules.py
prints the whole decision table offline. Set SEED_DEMO_DATA=false for a
deployment that will hold real data.
One key runs everything. Browsing the seeded record needs no key; the upload
and the questions below ride one. Put ONE key in the .env next to compose.yaml
and docker compose restart — the app re-reads /app/.env; a shell export does
not reach the containers. The .env is the only place for the key:
config.llm.yaml names the variable (api_key: OPENROUTER_API_KEY), never the
secret. An OpenRouter key as
OPENROUTER_API_KEY is the recommended one; a DashScope key when openrouter.ai is
unreachable from your network; a Google, OpenAI
(OPENAI_API_KEY), Anthropic
(ANTHROPIC_API_KEY) or DeepSeek key works alone as well. Every model decision —
which model chats, which reads report photos, which extracts the indicators, which
embeds — is a line in config.llm.yaml, where you can read and
change it (a self-hosted gateway is one <PREFIX>_BASE_URL in .env). The boot
log, and mirobody doctor, print what each surface selected and name the fix
where one has nothing.
① Collect + ② Translate. Drop demo/lab_report_2025-10-15.pdf
on the Data page and twelve analytes come out with values and units, each
linking back to the page it was read from:
③ Answer (agent). Ask about her HbA1c and the agent finds the data, charts the three lab draws against 104 sensor-derived estimates, and says plainly that the improvement did not hold. Ask again about the report you just uploaded and it reads that instead — the fourth scene of the four-minute walkthrough.
→ Docker deployment · Configuration · Local Python setup
🔌 Use it, extend it
| You want | Do this | Docs |
|---|---|---|
| Offline resolution and units in your code | pip install mirobody — 2 packages, no key, no network |
Engine |
| A document turned into readings | pip install 'mirobody[parse]' — PDF, image, Excel, Word, PowerPoint, text; only a scanned page reaches a vision model |
Engine |
| The agent harness as a library | pip install 'mirobody[agent]' — middleware, virtual-filesystem backends, checkpointer |
Bringing your own agent |
| These tools in Claude Desktop, Cursor or your own loop | Settings → MCP: every agent tool is also served at /mcp, gated per user |
MCP servers · examples/07_claude_agent_sdk.py |
| Your app talking to a deployment | HTTP API, or backbone mode: your agent, our data layer | API overview · Backbone |
| A new tool, skill or device provider | Drop a file into mirobody/agent/tools/, mirobody/agent/skills/ or mirobody/pulse/providers/ and restart — or pip install a package declaring a mirobody.providers / mirobody.tools / mirobody.agents entry point |
Adding tools · Skills · Providers |
| Your own agent harness | AGENT_DIRS → your directory replaces the shipped agent |
mirobody/agent/README.md |
| The LOINC axis table and alias sources at build time | mirobody.bundle — for generating a seed or a corpus |
mirobody/bundle.py |
🤝 Contributing
The highest-leverage contribution is a term the resolver gets wrong. Run
mirobody resolve "<term>"; if the answer is wrong or empty,
report it
or 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.md · Contributing guide · Repository layout · Roadmap · CHANGELOG · SECURITY
📚 Documentation
docs.mirobody.ai, in English and Chinese — start at the
Quickstart, or go straight to the
API reference. Long-form guides for
contributors live in docs/.
🙏 Acknowledgements
Projects that shaped the kernel's rules — none of their code is included here:
Open Wearables (the
data-standardization failure modes kernel/series and kernel/quality close),
Home Assistant (state_class),
Open mHealth / IEEE 1752 (field names),
wearipedia (synthetic payloads),
dlt / Airbyte / Singer (connector shape),
deepagents, LangChain and langchain-quickjs (the harness, the file-system projection, the eval REPL),
Regenstrief Institute (LOINC), UCUM, HL7 FHIR, OHDSI OMOP — see LICENSE-3RD-PARTY.
⭐ Star History
If it read a report for you, a star helps the next person find it. Releases land most weeks — Watch for them.
📚 Docs · ▶ Demo · 🔌 Platform · 🧪 Eval
Apache 2.0 · © 2026 Theta Health
Release files for mirobody 1.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mirobody-1.4.2.tar.gz | 26.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mirobody-1.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.3 MB
Release files / mirobody-1.4.2.tar.gz
| Download URL | mirobody-1.4.2.tar.gz |
|---|---|
| Size | 26.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3a3b1aa4f592beef606b993d55cd1a2ae2ef1f36dff7b2f0cf52bdc3622627bc
|
|
BLAKE2b-256 checksum How to use checksums |
ab76c6f1ef7a726b045ba1728fe3065b24dd2514866f9f22edcaae394e36ee23
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / mirobody-1.4.2-py3-none-any.whl
| Download URL | mirobody-1.4.2-py3-none-any.whl |
|---|---|
| Size | 26.7 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7a075c7708dbc3995132cb6a40bebcb09ff59c268c9a99ac8bd4ddcfe7993997
|
|
BLAKE2b-256 checksum How to use checksums |
ec0c7efc4f2c1f5fb5dd548d128b5cbef3e945d28f49fb2bde36b2a0b4e1efc0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|