Skip to main content

🚀 Mirobody

The AI-native health data engine — collect, standardize, and reason over labs, wearables & genomics.

License: Apache-2.0 Python 3.12+ PyPI Downloads Benchmarks arXiv Docs

📚 Documentation · 💬 Hosted chat — chat.mirobody.ai · 🔌 API platform — platform.mirobody.ai

English · 简体中文 · 繁體中文 · 日本語

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.

From wearables to food photos — one standard format, ready for AI.

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 published 1.0.62 wheel 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.

mirobody resolve: four languages landing on one LOINC code, fully offline

Real output, and the GIF is a build artifact — docs/demo/resolve.html rendered by scripts/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 % vs 10*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, so resolve() is unchanged until you point MIROBODY_SEMANTIC_INDEX at 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 why min_score is not a correctness threshold.
  • We measure the claim instead of asserting it. test_engine_coverage.py scores 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.

Your own account's indicators and an uploaded report, then switching to Demo's shared record and opening two years of HbA1c

Your own thin record next to hers — the thick one you can only view.

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.

Asking about the shared record's HbA1c; the agent queries, charts lab and sensor series together, and reads the trend

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.

Dropping a lab-report PDF on the Data page; twelve analytes extracted, each linked to its source file

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.

Asking about your own just-uploaded panel; the agent reads the report and flags every result against its reference range

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

mirobody-1.2.1.tar.gz (24.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mirobody-1.2.1-py3-none-any.whl (24.5 MB view details)

Uploaded Python 3

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

Hashes for mirobody-1.2.1.tar.gz
Algorithm Hash digest
SHA256 5f9c3d32a43bf1ce86b4b738a58c57c0095c127b698b2cd150710b7e2664665c
MD5 f86660f90a5d135e58c4fabde8691624
BLAKE2b-256 0743b719c878152be213a38a07f9c0d206b52e4899c789f564a5bcf5a80515dc

See more details on using hashes here.

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

Hashes for mirobody-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 83aff0a2a8fc876e6f17121822e67faca8899b8bdc8c3bc7a43fed46385925bf
MD5 4ec0dbb6a4c3699d54c67f1c78b23827
BLAKE2b-256 e50b44e0e636e145622ae27f73f47af95737dfc26db17fb7beb2a05059b45bfd

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.2.1 This release

2 files

1.0.62

2 files

1.0.51

2 files

1.0.50

2 files

1.0.4

2 files

1.0.3

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page