Metadata-Version: 2.4
Name: mirobody
Version: 1.2.1
Summary: The AI-native health data engine — collect, standardize, and reason over labs, wearables & genomics.
Author-email: Theta AI Team <developer@thetahealth.ai>
Project-URL: Homepage, https://github.com/thetahealth/mirobody
Project-URL: Bug Reports, https://github.com/thetahealth/mirobody/issues
Project-URL: Source, https://github.com/thetahealth/mirobody
Keywords: health,health-data,FHIR,LOINC,SNOMED,wearables,lab-reports,genomics,MCP,agent,AI,LLM
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE-3RD-PARTY
Requires-Dist: python-dotenv
Requires-Dist: ruamel.yaml>=0.17
Requires-Dist: cryptography
Requires-Dist: pandas
Requires-Dist: numpy
Requires-Dist: aiohttp
Requires-Dist: pydantic
Requires-Dist: pyjwt
Requires-Dist: pytz
Requires-Dist: pypdf>=5.1.0
Requires-Dist: pypdfium2>=4.30.0
Requires-Dist: pdfplumber>=0.11.7
Requires-Dist: Pillow>=11.1.0
Requires-Dist: tinytag>=1.6.0
Requires-Dist: mutagen>=1.47
Requires-Dist: rarfile>=4.0
Requires-Dist: py7zr>=0.22
Requires-Dist: openpyxl>=3.1
Requires-Dist: python-docx>=1.1
Requires-Dist: python-pptx>=1.0
Requires-Dist: azure-identity>=1.15.0
Requires-Dist: openai>=2.11.0
Requires-Dist: anthropic
Requires-Dist: google-genai>=2.0.0
Requires-Dist: jinja2
Requires-Dist: starlette
Requires-Dist: mcp>=2.0.0
Requires-Dist: tiktoken>=0.9.0
Provides-Extra: server
Requires-Dist: fastapi; extra == "server"
Requires-Dist: python-multipart; extra == "server"
Requires-Dist: uvicorn; extra == "server"
Requires-Dist: psycopg[binary]; extra == "server"
Requires-Dist: psycopg[pool]; extra == "server"
Requires-Dist: sqlalchemy; extra == "server"
Requires-Dist: greenlet; extra == "server"
Requires-Dist: redis; extra == "server"
Requires-Dist: aioboto3; extra == "server"
Requires-Dist: webauthn>=2.0.0; extra == "server"
Requires-Dist: mandrill; extra == "server"
Requires-Dist: requests-oauthlib; extra == "server"
Provides-Extra: agents
Requires-Dist: mirobody[server]; extra == "agents"
Requires-Dist: langchain>=1.3.15; extra == "agents"
Requires-Dist: langchain-openai>=1.4.3; extra == "agents"
Requires-Dist: langchain-anthropic>=1.5.6; extra == "agents"
Requires-Dist: langchain-google-genai>=4.2.4; extra == "agents"
Requires-Dist: langchain-google-vertexai; extra == "agents"
Requires-Dist: google-cloud-aiplatform>=1.157.0; extra == "agents"
Requires-Dist: langchain_community; extra == "agents"
Requires-Dist: deepagents>=0.7.5; extra == "agents"
Requires-Dist: langchain-quickjs>=0.1.3; extra == "agents"
Requires-Dist: langgraph-checkpoint-postgres>=3.1.2; extra == "agents"
Provides-Extra: cn
Requires-Dist: oss2; extra == "cn"
Requires-Dist: volcengine-python-sdk[ark]>=3.0.1; extra == "cn"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: pytest-sugar; extra == "test"
Requires-Dist: import-linter; extra == "test"
Provides-Extra: indicator-build
Requires-Dist: polars; extra == "indicator-build"
Requires-Dist: zhconv; extra == "indicator-build"
Requires-Dist: tqdm; extra == "indicator-build"
Requires-Dist: pgvector; extra == "indicator-build"
Dynamic: license-file

<div align="center">

# 🚀 Mirobody

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

[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-3776AB.svg?logo=python&logoColor=white)](pyproject.toml)
[![PyPI Downloads](https://img.shields.io/pepy/dt/mirobody?label=PyPI%20Downloads&color=orange)](https://pepy.tech/projects/mirobody)
[![Benchmarks](https://img.shields.io/badge/%F0%9F%A4%97_Benchmarks-4k%2B_downloads_each-FFD21E.svg)](https://huggingface.co/healthmemoryarena)
[![arXiv](https://img.shields.io/badge/arXiv-2604.02834-b31b1b.svg)](https://arxiv.org/abs/2604.02834)
[![Docs](https://img.shields.io/badge/Docs-docs.mirobody.ai-black)](https://docs.mirobody.ai/)

**[📚 Documentation](https://docs.mirobody.ai/)** · **[💬 Hosted chat — chat.mirobody.ai](https://chat.mirobody.ai/)** · **[🔌 API platform — platform.mirobody.ai](https://platform.mirobody.ai/)**

**English** · **[简体中文](README.zh-CN.md)** · **[繁體中文](README.zh-TW.md)** · **[日本語](README.ja.md)**

*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.*

<img src="docs/images/where-your-data-comes-from.svg" alt="From wearables to food photos — one standard format, ready for AI." width="920">

</div>

The engine does three things, and the codebase (and [Contributing](#-contributing)) is organized around exactly these three stages — the same **C · S · A** the [documentation](https://docs.mirobody.ai/en/api-reference/) uses:

| Stage                | What it means                                                                                                     | Where                                                   |
| -------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| **① Collect** | Pull signals in: 3 device providers + a SQL source · 7 file formats · Apple Health                                             | [`pulse/`](mirobody/pulse/) |
| **② Standardize**    | One standard: resolve any reading to canonical codes (LOINC · SNOMED CT · RxNorm), normalize units, land against FHIR-recognized code systems | [`indicator/`](mirobody/indicator/)                    |
| **③ Answers**  | Reason: agents read the*original documents* through a virtual filesystem and answer with charts & citations     | [`agent/`](mirobody/agent/)                  |

---

## ⚡ Try it in 60 seconds

Indicator resolution is the engine's front door and needs no key, no config and
no network:

```bash
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](#-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](CHANGELOG.md).

<p align="center">
  <img src="docs/images/resolve-demo.gif"
       alt="mirobody resolve: four languages landing on one LOINC code, fully offline" width="880">
</p>

> Real output, and the GIF is a build artifact — [`docs/demo/resolve.html`](docs/demo/resolve.html) rendered by [`scripts/make_demo_gifs.py`](scripts/make_demo_gifs.py), so it cannot drift away from the command it claims to show.

```python
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](https://docs.mirobody.ai/en/engine/) ·
[Indicators](https://docs.mirobody.ai/en/concepts/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`](mirobody/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](https://docs.mirobody.ai/en/concepts/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`](mirobody/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.

```bash
pytest mirobody/test_engine_coverage.py -s   # offline, about a second
```

→ [Standardization](https://docs.mirobody.ai/en/api-reference/standardization/) ·
[Architecture](https://docs.mirobody.ai/en/concepts/architecture/) ·
[Data flow](https://docs.mirobody.ai/en/concepts/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](https://huggingface.co/datasets/healthmemoryarena/ESL-Bench) | Event-driven longitudinal health agents — 100 synthetic users, 10,000 queries, programmatic ground truth ([arXiv:2604.02834](https://arxiv.org/abs/2604.02834)) | 4,800+ |
| [MedHall-Bench](https://huggingface.co/datasets/healthmemoryarena/MedHall-Bench) | Medical hallucination | 4,500+ |
| [MedHarm-Bench](https://huggingface.co/datasets/healthmemoryarena/MedHarm-Bench) | Harmful medical advice | 4,300+ |

Reproduce any of them with one command via
**[mirobody-eval](https://github.com/thetahealth/mirobody-eval)**, which also
seeds a deployment with synthetic (PHI-free) trajectories.

---

## 🚀 Run the whole thing

```bash
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:

```bash
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](https://openrouter.ai/keys)
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](https://dashscope.console.aliyun.com/apiKey) 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](https://docs.mirobody.ai/en/deployment/docker/) ·
[Configuration](https://docs.mirobody.ai/en/configuration/) ·
[Local Python setup](https://docs.mirobody.ai/en/development/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.

<p align="center">
  <img src="docs/images/care-circle-demo.gif"
       alt="Your own account's indicators and an uploaded report, then switching to Demo's shared record and opening two years of HbA1c" width="880">
</p>

<div align="center">
<img src="docs/images/your-care-circle.svg" alt="Your own thin record next to hers — the thick one you can only view." width="820">
</div>

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`](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.

<p align="center">
  <img src="docs/images/ask-circle-demo.gif"
       alt="Asking about the shared record's HbA1c; the agent queries, charts lab and sensor series together, and reads the trend" width="880">
</p>

```
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.

<p align="center">
  <img src="docs/images/upload-demo.gif"
       alt="Dropping a lab-report PDF on the Data page; twelve analytes extracted, each linked to its source file" width="880">
</p>

**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.

<p align="center">
  <img src="docs/images/ask-own-demo.gif"
       alt="Asking about your own just-uploaded panel; the agent reads the report and flags every result against its reference range" width="880">
</p>

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](https://github.com/thetahealth/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](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](https://docs.mirobody.ai/en/tools/adding-tools/) |
| An Agent Skill (SKILL.md) | `mirobody/agent/skills/` | [Skills](https://docs.mirobody.ai/en/tools/skills/) |
| A whole agent | `mirobody/agent/` | [Agents](https://docs.mirobody.ai/en/tools/agents/) |
| A device provider | `mirobody/pulse/providers/` | [Provider integration](https://docs.mirobody.ai/en/development/provider-integration/) |
| Someone else's MCP server | Settings → MCP | [MCP integration](https://docs.mirobody.ai/en/tools/mcp-integration/) |

Every tool the agent has is also served over MCP at `/mcp`, gated per user.
→ [Built-in tools](https://docs.mirobody.ai/en/tools/built-in/) ·
[MCP servers](https://docs.mirobody.ai/en/api-reference/mcp-servers/)

---

## 🔌 Use it from your own code

| Surface | For | Docs |
| --- | --- | --- |
| `pip install mirobody` | Resolution and file parsing, no server | [Engine](https://docs.mirobody.ai/en/engine/) |
| HTTP API | Your app talking to a deployment | [API overview](https://docs.mirobody.ai/en/api-reference/overview/) · [Data](https://docs.mirobody.ai/en/api-reference/data/) |
| MCP | Claude, Cursor, or any MCP client reading a user's record | [MCP servers](https://docs.mirobody.ai/en/api-reference/mcp-servers/) |
| Backbone mode | Your own agent, our data layer | [Backbone](https://docs.mirobody.ai/en/api-reference/backbone-mode/) |

Not sure which? → [Choose your API](https://docs.mirobody.ai/en/api-reference/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](https://docs.mirobody.ai/en/concepts/architecture/) ·
[CONTRIBUTING.md](CONTRIBUTING.md)

---

## 📚 Documentation

For full documentation, see **[docs.mirobody.ai](https://docs.mirobody.ai/)**
(English and Simplified Chinese).

| | |
| --- | --- |
| [Quickstart](https://docs.mirobody.ai/en/quickstart/) · [Installation](https://docs.mirobody.ai/en/installation/) · [Self-host](https://docs.mirobody.ai/en/self-host/) | Getting it running |
| [Indicators](https://docs.mirobody.ai/en/concepts/indicators/) · [Providers](https://docs.mirobody.ai/en/concepts/providers/) · [File processing](https://docs.mirobody.ai/en/concepts/file-processing/) | How the three stages work |
| [API reference](https://docs.mirobody.ai/en/api-reference/) · [Streaming](https://docs.mirobody.ai/en/api-reference/streaming/) · [Function calling](https://docs.mirobody.ai/en/api-reference/function-calling/) | Building against it |
| [Contributing](https://docs.mirobody.ai/en/development/contributing/) · [Setup](https://docs.mirobody.ai/en/development/setup/) | Working on it |

### In-repo, for contributors

Each package carries a `README.md` saying what it is; long-form guides live in
[`docs/`](docs/). All of it is English, whichever README you arrived from.

| | Where |
| --- | --- |
| Runnable examples | [`examples/`](examples/README.md) |
| ① Collect | [`pulse/`](mirobody/pulse/README.md) · [providers](mirobody/pulse/providers/README.md) · [aggregation](mirobody/pulse/aggregate/README.md) · [Apple Health](mirobody/pulse/apple/README.md) |
| ① guides | [connect a wearable](docs/provider-setup.md) · [write a provider](docs/provider-guide.md) · [file processing](docs/file-processing.md) · [Apple Health API](docs/apple-health.md) |
| ② Standardize | [`indicator/`](mirobody/indicator/README.md) · [indicators & units](mirobody/pulse/standardize/README.md) |
| ③ Answers | [`agent/`](mirobody/agent/README.md) · [tools](mirobody/agent/tools/README.md) · [ChatGPT widgets](mirobody/agent/resources/README.md) |
| Plumbing | [configuration](mirobody/utils/config/README.md) · [database schema](mirobody/schema/README.md) · [shipping the frontend](docs/frontend-shipping.md) |
| Working on it | [CONTRIBUTING.md](CONTRIBUTING.md) · [testing](docs/testing.md) · [aggregator script](docs/aggregation-tests.md) · [roadmap](docs/roadmap.md) · [CHANGELOG](CHANGELOG.md) · [SECURITY](SECURITY.md) |

---

## 🤝 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`](mirobody/res/resolver_overrides.tsv) plus a case to
[`test_engine_coverage.py`](mirobody/test_engine_coverage.py) — the coverage score
is the review.

```bash
pip install -e '.[test]' && pytest -q && lint-imports
```

→ [Contributing guide](https://docs.mirobody.ai/en/development/contributing/) ·
[CONTRIBUTING.md](CONTRIBUTING.md)

---

<div align="center">

**[📚 Docs](https://docs.mirobody.ai/)** · **[💬 Chat](https://chat.mirobody.ai/)** · **[🔌 Platform](https://platform.mirobody.ai/)** · **[🧪 Eval](https://github.com/thetahealth/mirobody-eval)**

Apache 2.0 · © 2026 [Theta Health](https://thetahealth.ai)

</div>
