cavell-prism-client 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cavell_prism_client-0.2.0/.gitignore +16 -0
- cavell_prism_client-0.2.0/CHANGELOG.md +154 -0
- cavell_prism_client-0.2.0/CONTRIBUTING.md +59 -0
- cavell_prism_client-0.2.0/LICENSE +21 -0
- cavell_prism_client-0.2.0/PKG-INFO +133 -0
- cavell_prism_client-0.2.0/README.md +103 -0
- cavell_prism_client-0.2.0/SECURITY.md +25 -0
- cavell_prism_client-0.2.0/pyproject.toml +94 -0
- cavell_prism_client-0.2.0/src/cavell_client/__init__.py +49 -0
- cavell_prism_client-0.2.0/src/cavell_client/api.py +258 -0
- cavell_prism_client-0.2.0/src/cavell_client/client.py +243 -0
- cavell_prism_client-0.2.0/src/cavell_client/fhir.py +1080 -0
- cavell_prism_client-0.2.0/src/cavell_client/ingestion.py +1599 -0
- cavell_prism_client-0.2.0/src/cavell_client/models.py +154 -0
- cavell_prism_client-0.2.0/src/cavell_client/py.typed +0 -0
- cavell_prism_client-0.2.0/tests/__init__.py +0 -0
- cavell_prism_client-0.2.0/tests/conftest.py +141 -0
- cavell_prism_client-0.2.0/tests/helpers.py +60 -0
- cavell_prism_client-0.2.0/tests/test_api.py +531 -0
- cavell_prism_client-0.2.0/tests/test_client.py +395 -0
- cavell_prism_client-0.2.0/tests/test_fhir.py +2228 -0
- cavell_prism_client-0.2.0/tests/test_ingestion.py +3920 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-07-31
|
|
8
|
+
|
|
9
|
+
First public release, targeting the Prism API. The package is published as
|
|
10
|
+
**`cavell-prism-client`**; the import name stays `cavell_client`.
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- **Bearer auth replaces HTTP Basic.** `CavellAPI(base_url, api_key)` and
|
|
15
|
+
`CavellClient(api_url, api_key, fhir_base_url, ...)` take an LLM Gateway
|
|
16
|
+
key sent as `Authorization: Bearer <key>` on every request. Passing the
|
|
17
|
+
removed `username=`/`password=` keyword arguments raises a `TypeError`
|
|
18
|
+
with migration guidance. Positional callers of the old signature fail at
|
|
19
|
+
FHIR-client construction — switch to keyword arguments.
|
|
20
|
+
- **Base URLs moved** to `https://{qa,stg,prd}.prism.cavell.app/api`. A bare
|
|
21
|
+
host without a path gets `/api` appended automatically.
|
|
22
|
+
- **`CavellClient(...)` now validates both endpoints at construction**, so a
|
|
23
|
+
misconfiguration raises there instead of deep inside `seed()`/`extract()`,
|
|
24
|
+
and the exception type says which half is wrong: `GET /key/info`
|
|
25
|
+
pre-flights the LLM Gateway key without spending tokens
|
|
26
|
+
(`CavellAuthError` / `CavellGatewayUnavailableError` / `CavellAPIError`),
|
|
27
|
+
then `GET /metadata` checks the FHIR server (`FHIRAuthError` /
|
|
28
|
+
`FHIRConnectionError`). Calling `check_connection()` afterwards is no
|
|
29
|
+
longer necessary; constructing a client now requires network access to
|
|
30
|
+
both. `extract()` still pre-flights the key once per run.
|
|
31
|
+
- **`FHIRConnectionError`** — FHIR failures surface as a library exception
|
|
32
|
+
instead of a raw `httpx.HTTPStatusError`/`ConnectError` escaping from
|
|
33
|
+
`check_connection()`.
|
|
34
|
+
- **Run-global failures raise.** A 401 (`CavellAuthError`) or a 503 that
|
|
35
|
+
survives in-place retries (`CavellGatewayUnavailableError`) aborts
|
|
36
|
+
`IngestionPipeline.extract()` with an exception instead of producing
|
|
37
|
+
per-document failure outcomes. Re-running with `skip_processed=True`
|
|
38
|
+
(the default) resumes safely.
|
|
39
|
+
- **`extract(batch_size=...)` is now `extract(limit=...)`.** The old name
|
|
40
|
+
implied chunking it never did: it caps a single pass, and the documents
|
|
41
|
+
past the cap were left unprocessed. Passing `batch_size=` to `extract()`
|
|
42
|
+
raises `TypeError` with migration guidance rather than being silently
|
|
43
|
+
ignored — silently ignoring it would extract every document passed.
|
|
44
|
+
Use `extract_all()` to process a whole dataset in chunks. `limit` must
|
|
45
|
+
be `>= 1` if set.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `CavellAuthError` (401) and `CavellGatewayUnavailableError` (503), both
|
|
50
|
+
subclassing `CavellAPIError`.
|
|
51
|
+
- `Retry-After` is honored in the 429 retry loop (capped at 300s).
|
|
52
|
+
- **Chronology check**: documents older than the patient's newest
|
|
53
|
+
already-persisted document are **refused**. `extract()` and `extract_all()`
|
|
54
|
+
raise `OutOfOrderDocumentError` before anything in the call is extracted,
|
|
55
|
+
so no tokens are spent and nothing is persisted — including the in-order
|
|
56
|
+
documents in the same call. `OutOfOrderDocumentError.violations` lists every
|
|
57
|
+
offender as an `OutOfOrderDocument` (`patient_identifier`, `document_id`,
|
|
58
|
+
`document_index`, `date`, `watermark`). Equal dates are not violations
|
|
59
|
+
(dates are day-resolution), already-processed documents are filtered out
|
|
60
|
+
before the check, and the check fails open per patient if the watermark
|
|
61
|
+
query errors.
|
|
62
|
+
|
|
63
|
+
Extraction is context-aware: each note is read against the patient's
|
|
64
|
+
*current* resources with no date filtering, so an older note would be
|
|
65
|
+
interpreted against a clinical picture from its own future. Refusing is the
|
|
66
|
+
conservative position while that is true.
|
|
67
|
+
|
|
68
|
+
This supersedes an earlier design in which the older document was extracted
|
|
69
|
+
anyway behind an **update guard** that dropped updates to resources sourced
|
|
70
|
+
from newer documents. That code is retained but disabled
|
|
71
|
+
(`_apply_update_guard` and the commented-out block in
|
|
72
|
+
`_process_single_document`, plus one skipped test) in case the policy is
|
|
73
|
+
revisited.
|
|
74
|
+
- `mark_validated(resource_type, id)` removes the Prism `unvalidated` meta
|
|
75
|
+
tag via FHIR `$meta-delete`; `list_unvalidated_resources(patient, type)`
|
|
76
|
+
lists the clinician review queue.
|
|
77
|
+
- `list_processed_document_ids(patient_id=...)` for patient-scoped resume.
|
|
78
|
+
- **Active CarePlans are sent as extraction context**, activating the
|
|
79
|
+
server's plan versioning: continuing plans are updated/ended instead of
|
|
80
|
+
re-created on every note (previously ~10 duplicate plans per patient).
|
|
81
|
+
- Local dev tooling: `scripts/start_fhir.sh [--fresh]` and
|
|
82
|
+
`scripts/fhir_summary.py`; ruff security (S) rules; hardened pre-commit
|
|
83
|
+
(detect-secrets, uv-lock, lockfile-pinned ruff/ty).
|
|
84
|
+
- The `notebook` extra now declares version floors —
|
|
85
|
+
`pip install "cavell-prism-client[notebook]"` pulls `notebook>=7.6.1`,
|
|
86
|
+
`tqdm>=4.70.0`, and `ipywidgets>=8.0` (the demo notebooks need the
|
|
87
|
+
ipywidgets 8 progress-bar API).
|
|
88
|
+
- Python 3.14 is tested in CI and declared in the classifiers.
|
|
89
|
+
- **`IngestionPipeline.extract_all(documents, batch_size=N)`** — processes a
|
|
90
|
+
whole dataset, which `extract()` never did. Sorts every document by
|
|
91
|
+
ascending date across the **entire** dataset before splitting it into
|
|
92
|
+
batches, so each patient's documents are extracted oldest-first even when
|
|
93
|
+
they span batches. Batching an unsorted list splits it by input order,
|
|
94
|
+
which lets a later batch carry documents older than what an earlier batch
|
|
95
|
+
persisted; those trip the chronology guard and lose their updates.
|
|
96
|
+
Batches are cut by index, so the walk always terminates and works with
|
|
97
|
+
`skip_processed=False` and with documents that have no `document_id`. All
|
|
98
|
+
documents are validated before the first batch spends, which is also the
|
|
99
|
+
only way `document_id` uniqueness is enforced across batch boundaries. An
|
|
100
|
+
optional `on_batch` callback reports progress as each batch finishes.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- `skip_processed` queries `DocumentReference`s per patient instead of
|
|
105
|
+
scanning the whole server.
|
|
106
|
+
- Outbound extraction context is deflated: server bookkeeping (`meta`) and
|
|
107
|
+
generated narrative (`text`) are stripped from context resources.
|
|
108
|
+
- Version is single-sourced from `cavell_client.__version__` (hatch dynamic
|
|
109
|
+
versioning).
|
|
110
|
+
|
|
111
|
+
### Migration
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# before (0.1.x)
|
|
115
|
+
client = CavellClient(
|
|
116
|
+
api_url="https://<old-deployment-url>/api",
|
|
117
|
+
username="user",
|
|
118
|
+
password="pass",
|
|
119
|
+
fhir_base_url="http://localhost:8090",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# after (0.2.0)
|
|
123
|
+
client = CavellClient(
|
|
124
|
+
api_url="https://prd.prism.cavell.app/api",
|
|
125
|
+
api_key="<your LLM Gateway key>",
|
|
126
|
+
fhir_base_url="http://localhost:8090",
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Gateway keys are issued by the Cavell LLM Gateway, not Cavell accounts —
|
|
131
|
+
contact your Cavell representative if you don't have one.
|
|
132
|
+
|
|
133
|
+
Batched extraction moves to `extract_all()`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# before (0.1.x) — processed only the first 500 documents; the rest needed
|
|
137
|
+
# further extract() calls, and batch membership followed input order
|
|
138
|
+
for outcome in pipeline.extract(documents, batch_size=500):
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
# after (0.2.0) — processes every document, globally date-ordered
|
|
142
|
+
for outcome in pipeline.extract_all(documents, batch_size=500):
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
# to cap a single pass instead (e.g. a sanity check before a full run)
|
|
146
|
+
for outcome in pipeline.extract(documents, limit=10):
|
|
147
|
+
...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## [0.1.0]
|
|
151
|
+
|
|
152
|
+
Internal release: ingestion pipeline (seed → extract → persist), CSV
|
|
153
|
+
helpers, practitioner matching, observation deduplication, transient-failure
|
|
154
|
+
retries with a deferred pass.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
Requires [uv](https://docs.astral.sh/uv/) and Python ≥ 3.11.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/polaris-health/cavell-prism-client
|
|
9
|
+
cd cavell-prism-client
|
|
10
|
+
uv sync # installs the package + dev dependencies
|
|
11
|
+
uv run pytest # 250+ hermetic tests, sub-second
|
|
12
|
+
uv run ruff check && uv run ruff format --check
|
|
13
|
+
uv run ty check
|
|
14
|
+
uv run pre-commit install # ruff, ty, detect-secrets, commitlint on every commit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The test suite is fully hermetic (no network) and guarded by
|
|
18
|
+
`pytest --timeout=60` — a test that accidentally makes a real HTTP call
|
|
19
|
+
fails loudly.
|
|
20
|
+
|
|
21
|
+
For end-to-end experiments, `docker compose up -d` starts a local HAPI FHIR
|
|
22
|
+
server on `http://localhost:8090`.
|
|
23
|
+
|
|
24
|
+
## Commits and branches
|
|
25
|
+
|
|
26
|
+
- Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`, ...) — enforced
|
|
27
|
+
by commitlint via pre-commit.
|
|
28
|
+
- `develop` is the default branch; all PRs target it.
|
|
29
|
+
- `main` is release-only: promotion PRs `develop → main` merge with a merge
|
|
30
|
+
commit (enforced by ruleset + the `branch-guard` check).
|
|
31
|
+
- Every PR (develop and main) requires one **code-owner** approval
|
|
32
|
+
(`.github/CODEOWNERS`), and authors cannot approve their own PRs. Repo
|
|
33
|
+
admins can bypass the approval when merging a PR (`gh pr merge --admin`) —
|
|
34
|
+
the bypass is recorded on the PR; direct pushes and force pushes stay
|
|
35
|
+
impossible for everyone, and `v*` release tags can only be created by
|
|
36
|
+
admins.
|
|
37
|
+
|
|
38
|
+
## Releases
|
|
39
|
+
|
|
40
|
+
The version lives in exactly one place: `src/cavell_client/__init__.py`
|
|
41
|
+
(`__version__`). `pyproject.toml` reads it via hatch dynamic versioning.
|
|
42
|
+
|
|
43
|
+
- **Nightlies** publish automatically from `develop` to PyPI as
|
|
44
|
+
`<next-version>.devNNN` (skipped when develop hasn't changed). Install
|
|
45
|
+
with `pip install --pre cavell-prism-client`.
|
|
46
|
+
- **Stable release**:
|
|
47
|
+
1. Ensure `__version__` on develop is the version to release and
|
|
48
|
+
`CHANGELOG.md` has its section.
|
|
49
|
+
2. Open the promotion PR `develop → main`, merge it (merge commit).
|
|
50
|
+
3. Tag the merge commit: `git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z`.
|
|
51
|
+
The publish workflow verifies tag == version == on-main, builds, and
|
|
52
|
+
publishes to PyPI via trusted publishing, then creates the GitHub
|
|
53
|
+
release.
|
|
54
|
+
4. **Immediately after**: open a PR to develop bumping `__version__` to
|
|
55
|
+
the next minor — otherwise the next nightlies would sort below the
|
|
56
|
+
just-published stable.
|
|
57
|
+
|
|
58
|
+
Package name `cavell-prism-client`, import name `cavell_client` — this is
|
|
59
|
+
intentional; don't "fix" one to match the other.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cavell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cavell-prism-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python client for Cavell FHIR extraction API
|
|
5
|
+
Project-URL: Homepage, https://cavell.ai
|
|
6
|
+
Project-URL: Documentation, https://polaris-health.github.io/cavell-prism-client
|
|
7
|
+
Project-URL: Repository, https://github.com/polaris-health/cavell-prism-client
|
|
8
|
+
Project-URL: Changelog, https://github.com/polaris-health/cavell-prism-client/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Cavell <support@cavell.ai>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: clinical,ehr,extraction,fhir,healthcare,nlp
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: notebook
|
|
26
|
+
Requires-Dist: ipywidgets>=8.0; extra == 'notebook'
|
|
27
|
+
Requires-Dist: notebook>=7.6.1; extra == 'notebook'
|
|
28
|
+
Requires-Dist: tqdm>=4.70.0; extra == 'notebook'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Cavell Prism Client
|
|
32
|
+
|
|
33
|
+
[](https://github.com/polaris-health/cavell-prism-client/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/cavell-prism-client/)
|
|
35
|
+
[](https://pypi.org/project/cavell-prism-client/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
Python client for [Cavell Prism](https://cavell.ai) — extract structured
|
|
39
|
+
FHIR resources from clinical notes and persist them to your own FHIR server.
|
|
40
|
+
|
|
41
|
+
**Cavell never connects to your FHIR server.** Your system sends clinical
|
|
42
|
+
text to the Prism API, receives extracted resources back, and persists them
|
|
43
|
+
locally with your own credentials. Cavell has no access to your database and
|
|
44
|
+
stores no credentials: every request carries your own LLM Gateway key.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install cavell-prism-client
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add cavell-prism-client
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The import name is `cavell_client`.
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
You need a **Prism API URL** (`https://prd.prism.cavell.app/api`) and an
|
|
63
|
+
**LLM Gateway key** — contact your Cavell representative for a key. For a
|
|
64
|
+
local FHIR server, `docker compose up -d` in this repo starts HAPI on
|
|
65
|
+
`http://localhost:8090`.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from cavell_client import CavellClient, IngestionPipeline
|
|
69
|
+
from cavell_client import Organization, Patient, Document
|
|
70
|
+
|
|
71
|
+
with CavellClient(
|
|
72
|
+
api_url="https://prd.prism.cavell.app/api",
|
|
73
|
+
api_key="<your LLM Gateway key>",
|
|
74
|
+
fhir_base_url="http://localhost:8090",
|
|
75
|
+
) as client:
|
|
76
|
+
pipeline = IngestionPipeline(client, default_organization="CGH-001")
|
|
77
|
+
|
|
78
|
+
# 1. Seed reference data and patients
|
|
79
|
+
pipeline.seed(
|
|
80
|
+
organizations=[Organization(identifier="CGH-001", name="City General")],
|
|
81
|
+
patients=[Patient(identifier="MRN-1", managing_organization="CGH-001")],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# 2. Extract clinical notes (per patient, in date order) and persist
|
|
85
|
+
outcomes = pipeline.extract(
|
|
86
|
+
[
|
|
87
|
+
Document(
|
|
88
|
+
text="Patient diagnosed with type 2 diabetes...",
|
|
89
|
+
patient_identifier="MRN-1",
|
|
90
|
+
date="2024-01-15",
|
|
91
|
+
document_id="note-001",
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
for outcome in outcomes:
|
|
96
|
+
print(outcome)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Extraction is resume-safe (`skip_processed=True` by default), retries
|
|
100
|
+
transient failures, and aborts cleanly on auth/gateway outages. A document
|
|
101
|
+
older than the patient's newest already-extracted note is refused up front
|
|
102
|
+
with `OutOfOrderDocumentError` — extraction is context-aware and only moves
|
|
103
|
+
forward in time.
|
|
104
|
+
|
|
105
|
+
For a whole dataset, use `pipeline.extract_all(documents, batch_size=500)`: it
|
|
106
|
+
sorts every document by ascending date before splitting it into batches, so
|
|
107
|
+
each patient's notes are extracted oldest-first even when they span batches.
|
|
108
|
+
`extract()` makes a single pass and its `limit` caps that pass rather than
|
|
109
|
+
chunking it.
|
|
110
|
+
|
|
111
|
+
## Clinical validation
|
|
112
|
+
|
|
113
|
+
Every extracted resource carries an `unvalidated` meta tag. When a clinician
|
|
114
|
+
has reviewed a resource, remove the tag; any later update re-adds it:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
client.list_unvalidated_resources(patient_fhir_id, "Condition") # review queue
|
|
118
|
+
client.mark_validated("Condition", condition_id) # $meta-delete
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
Setup guides, the pipeline walkthrough, and demo notebooks (synthetic data):
|
|
124
|
+
[polaris-health.github.io/cavell-prism-client](https://polaris-health.github.io/cavell-prism-client)
|
|
125
|
+
|
|
126
|
+
## Contributing & security
|
|
127
|
+
|
|
128
|
+
See [CONTRIBUTING.md](https://github.com/polaris-health/cavell-prism-client/blob/main/CONTRIBUTING.md) for development setup and the release
|
|
129
|
+
process, and [SECURITY.md](https://github.com/polaris-health/cavell-prism-client/blob/main/SECURITY.md) for how to report vulnerabilities.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Cavell Prism Client
|
|
2
|
+
|
|
3
|
+
[](https://github.com/polaris-health/cavell-prism-client/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/cavell-prism-client/)
|
|
5
|
+
[](https://pypi.org/project/cavell-prism-client/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Python client for [Cavell Prism](https://cavell.ai) — extract structured
|
|
9
|
+
FHIR resources from clinical notes and persist them to your own FHIR server.
|
|
10
|
+
|
|
11
|
+
**Cavell never connects to your FHIR server.** Your system sends clinical
|
|
12
|
+
text to the Prism API, receives extracted resources back, and persists them
|
|
13
|
+
locally with your own credentials. Cavell has no access to your database and
|
|
14
|
+
stores no credentials: every request carries your own LLM Gateway key.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install cavell-prism-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv add cavell-prism-client
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The import name is `cavell_client`.
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
You need a **Prism API URL** (`https://prd.prism.cavell.app/api`) and an
|
|
33
|
+
**LLM Gateway key** — contact your Cavell representative for a key. For a
|
|
34
|
+
local FHIR server, `docker compose up -d` in this repo starts HAPI on
|
|
35
|
+
`http://localhost:8090`.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from cavell_client import CavellClient, IngestionPipeline
|
|
39
|
+
from cavell_client import Organization, Patient, Document
|
|
40
|
+
|
|
41
|
+
with CavellClient(
|
|
42
|
+
api_url="https://prd.prism.cavell.app/api",
|
|
43
|
+
api_key="<your LLM Gateway key>",
|
|
44
|
+
fhir_base_url="http://localhost:8090",
|
|
45
|
+
) as client:
|
|
46
|
+
pipeline = IngestionPipeline(client, default_organization="CGH-001")
|
|
47
|
+
|
|
48
|
+
# 1. Seed reference data and patients
|
|
49
|
+
pipeline.seed(
|
|
50
|
+
organizations=[Organization(identifier="CGH-001", name="City General")],
|
|
51
|
+
patients=[Patient(identifier="MRN-1", managing_organization="CGH-001")],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 2. Extract clinical notes (per patient, in date order) and persist
|
|
55
|
+
outcomes = pipeline.extract(
|
|
56
|
+
[
|
|
57
|
+
Document(
|
|
58
|
+
text="Patient diagnosed with type 2 diabetes...",
|
|
59
|
+
patient_identifier="MRN-1",
|
|
60
|
+
date="2024-01-15",
|
|
61
|
+
document_id="note-001",
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
for outcome in outcomes:
|
|
66
|
+
print(outcome)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Extraction is resume-safe (`skip_processed=True` by default), retries
|
|
70
|
+
transient failures, and aborts cleanly on auth/gateway outages. A document
|
|
71
|
+
older than the patient's newest already-extracted note is refused up front
|
|
72
|
+
with `OutOfOrderDocumentError` — extraction is context-aware and only moves
|
|
73
|
+
forward in time.
|
|
74
|
+
|
|
75
|
+
For a whole dataset, use `pipeline.extract_all(documents, batch_size=500)`: it
|
|
76
|
+
sorts every document by ascending date before splitting it into batches, so
|
|
77
|
+
each patient's notes are extracted oldest-first even when they span batches.
|
|
78
|
+
`extract()` makes a single pass and its `limit` caps that pass rather than
|
|
79
|
+
chunking it.
|
|
80
|
+
|
|
81
|
+
## Clinical validation
|
|
82
|
+
|
|
83
|
+
Every extracted resource carries an `unvalidated` meta tag. When a clinician
|
|
84
|
+
has reviewed a resource, remove the tag; any later update re-adds it:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client.list_unvalidated_resources(patient_fhir_id, "Condition") # review queue
|
|
88
|
+
client.mark_validated("Condition", condition_id) # $meta-delete
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
Setup guides, the pipeline walkthrough, and demo notebooks (synthetic data):
|
|
94
|
+
[polaris-health.github.io/cavell-prism-client](https://polaris-health.github.io/cavell-prism-client)
|
|
95
|
+
|
|
96
|
+
## Contributing & security
|
|
97
|
+
|
|
98
|
+
See [CONTRIBUTING.md](https://github.com/polaris-health/cavell-prism-client/blob/main/CONTRIBUTING.md) for development setup and the release
|
|
99
|
+
process, and [SECURITY.md](https://github.com/polaris-health/cavell-prism-client/blob/main/SECURITY.md) for how to report vulnerabilities.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|---------|-----------|
|
|
7
|
+
| latest 0.x release | ✅ |
|
|
8
|
+
| older releases | ❌ |
|
|
9
|
+
|
|
10
|
+
## Reporting a vulnerability
|
|
11
|
+
|
|
12
|
+
Please do **not** open a public issue for security problems.
|
|
13
|
+
|
|
14
|
+
Report vulnerabilities privately via
|
|
15
|
+
[GitHub Security Advisories](https://github.com/polaris-health/cavell-prism-client/security/advisories/new)
|
|
16
|
+
or email <info@cavell.ai>
|
|
17
|
+
|
|
18
|
+
We aim to acknowledge reports within 5 business days.
|
|
19
|
+
|
|
20
|
+
## Scope notes
|
|
21
|
+
|
|
22
|
+
- This SDK never stores credentials: the LLM Gateway key is held in memory
|
|
23
|
+
and sent per request; FHIR credentials go only to your own FHIR server.
|
|
24
|
+
- The demo datasets in `docs/notebooks/` are fully synthetic — reports about
|
|
25
|
+
"patient data" in them are appreciated but not vulnerabilities.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cavell-prism-client"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python client for Cavell FHIR extraction API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Cavell", email = "support@cavell.ai" }]
|
|
13
|
+
keywords = ["fhir", "healthcare", "clinical", "nlp", "extraction", "ehr"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Healthcare Industry",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
notebook = [
|
|
32
|
+
"notebook>=7.6.1",
|
|
33
|
+
"tqdm>=4.70.0",
|
|
34
|
+
"ipywidgets>=8.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://cavell.ai"
|
|
40
|
+
Documentation = "https://polaris-health.github.io/cavell-prism-client"
|
|
41
|
+
Repository = "https://github.com/polaris-health/cavell-prism-client"
|
|
42
|
+
Changelog = "https://github.com/polaris-health/cavell-prism-client/blob/main/CHANGELOG.md"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.version]
|
|
45
|
+
path = "src/cavell_client/__init__.py"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/cavell_client"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
# Demo data, docs, and CI never belong in the published artifact.
|
|
52
|
+
exclude = [
|
|
53
|
+
"docs/",
|
|
54
|
+
"scripts/",
|
|
55
|
+
".github/",
|
|
56
|
+
".claude/",
|
|
57
|
+
"docker-compose.yml",
|
|
58
|
+
"mkdocs.yml",
|
|
59
|
+
".pre-commit-config.yaml",
|
|
60
|
+
"commitlint.config.js",
|
|
61
|
+
"uv.lock",
|
|
62
|
+
".secrets.baseline",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 88
|
|
67
|
+
target-version = "py311"
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E", "W", "F", "I", "B", "UP", "S"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.per-file-ignores]
|
|
73
|
+
# pytest asserts and deliberately fake credentials
|
|
74
|
+
"tests/**" = ["S101", "S105", "S106"]
|
|
75
|
+
# demo notebooks and the study script use assert as a readable guard
|
|
76
|
+
"docs/notebooks/*.ipynb" = ["S101"]
|
|
77
|
+
"scripts/**" = ["S101"]
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
testpaths = ["tests"]
|
|
81
|
+
# Hermeticity guard: a test that accidentally makes a real network call fails
|
|
82
|
+
# loudly instead of sitting in retry backoff.
|
|
83
|
+
addopts = "--timeout=60"
|
|
84
|
+
|
|
85
|
+
[dependency-groups]
|
|
86
|
+
dev = [
|
|
87
|
+
"mkdocs-jupyter>=0.26.3",
|
|
88
|
+
"pre-commit>=4.0",
|
|
89
|
+
"mkdocs-material[imaging]>=9.7.7",
|
|
90
|
+
"pytest>=9.1.1",
|
|
91
|
+
"pytest-timeout>=2.3",
|
|
92
|
+
"ruff>=0.11",
|
|
93
|
+
"ty>=0.0.1",
|
|
94
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Python client for Cavell FHIR extraction API."""
|
|
2
|
+
|
|
3
|
+
from cavell_client.client import CavellClient
|
|
4
|
+
from cavell_client.ingestion import (
|
|
5
|
+
Document,
|
|
6
|
+
IngestionOutcome,
|
|
7
|
+
IngestionPipeline,
|
|
8
|
+
Organization,
|
|
9
|
+
Patient,
|
|
10
|
+
Practitioner,
|
|
11
|
+
)
|
|
12
|
+
from cavell_client.models import (
|
|
13
|
+
CavellAPIError,
|
|
14
|
+
CavellAuthError,
|
|
15
|
+
CavellError,
|
|
16
|
+
CavellGatewayUnavailableError,
|
|
17
|
+
ExtractResult,
|
|
18
|
+
FHIRAuthError,
|
|
19
|
+
FHIRConnectionError,
|
|
20
|
+
OutOfOrderDocument,
|
|
21
|
+
OutOfOrderDocumentError,
|
|
22
|
+
PatientNotFoundError,
|
|
23
|
+
PersistResult,
|
|
24
|
+
UsageStats,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"CavellClient",
|
|
29
|
+
"CavellError",
|
|
30
|
+
"CavellAPIError",
|
|
31
|
+
"CavellAuthError",
|
|
32
|
+
"CavellGatewayUnavailableError",
|
|
33
|
+
"Document",
|
|
34
|
+
"FHIRAuthError",
|
|
35
|
+
"FHIRConnectionError",
|
|
36
|
+
"IngestionOutcome",
|
|
37
|
+
"IngestionPipeline",
|
|
38
|
+
"Organization",
|
|
39
|
+
"OutOfOrderDocument",
|
|
40
|
+
"OutOfOrderDocumentError",
|
|
41
|
+
"Patient",
|
|
42
|
+
"Practitioner",
|
|
43
|
+
"PatientNotFoundError",
|
|
44
|
+
"ExtractResult",
|
|
45
|
+
"PersistResult",
|
|
46
|
+
"UsageStats",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
__version__ = "0.2.0"
|