Python SDK and CLI for PHI-safe observability and replay of healthcare AI agents
Project description
Auscult
Python SDK and CLI for PHI-safe observability and replay of healthcare AI agents.
Auscult records what an AI agent did (prompt, steps, outputs, errors), sanitizes all free text with PHI detection before anything touches the database, and makes the resulting synthetic traces inspectable for debugging, QA, and audit — without leaking patient data.
Install
With uv (recommended):
uv add auscult
uv run auscult setup # downloads en_core_web_lg (PHI NER)
export DATABASE_URL=sqlite:////tmp/auscult.sqlite # or Postgres
uv run auscult migrate
With pip:
pip install auscult
auscult setup
export DATABASE_URL=sqlite:////tmp/auscult.sqlite
auscult migrate
LangChain / LangGraph support is optional:
uv add 'auscult[langchain]'
# or: pip install 'auscult[langchain]'
Note: spaCy models are not on PyPI, so they are installed separately via
auscult setup(orpython -m spacy download en_core_web_lg). PyPI rejects packages that declare direct URL dependencies.
How it works
AuscultTracer(insrc/auscult/capture.py) records a run and its steps.- Every text field (
initial_prompt,llm_command,output,error_message) passes through the sanitizer before it is written to the database. There is no raw-storage mode. If sanitization raises, the step is not written (fail-closed). - The sanitizer (
src/auscult/sanitizer.py) uses Microsoft Presidio to detect PHI entities (names, phones, emails, dates/DOB, locations, street addresses, SSNs, MRNs, patient IDs, org-specific badge/case IDs) and Faker to substitute realistic synthetic values. - Replacements are consistent within a run: the same real value always maps to the same fake value, so traces stay coherent for replay and debugging.
- Each
Run/Stepstores aredaction_countandentity_counts(entity type → count, no raw spans) so you can monitor detection volume and mix. - JSON-shaped payloads are sanitized leaf-by-leaf so Faker replacements cannot corrupt object structure used later in replay comparisons.
- Faker is seeded from the run id, so re-capturing the same raw input with the
same
run_idyields identical synthetic replacements. - Optional
background=Truemoves sanitize + DB I/O onto a worker thread so the agent hot path stays passive; a full queue or worker error fails closed.
Caveat: sanitization is detection-based. Anything Presidio and the custom recognizers miss is stored as-is. Treat the database as sensitive until you have validated detection quality on your own traffic. "Sanitized" is not the same guarantee as "public."
Requirements
- Python 3.12+
- PostgreSQL (or SQLite for local experiments)
- A spaCy English model (
auscult setupinstallsen_core_web_lgby default)
Quickstart
uv add auscult
uv run auscult setup
export DATABASE_URL=sqlite:////tmp/auscult.sqlite
uv run auscult migrate
Plug into an existing agent (no loop rewrite)
Most agents don't need explicit record_step calls. Wrap the LLM client (or
pass a callback handler) once and every model call is captured automatically:
from openai import OpenAI
from auscult import observe_run
from auscult.integrations.openai import wrap_openai
client = wrap_openai(OpenAI()) # also: Azure / any OpenAI-compatible server
@observe_run(agent_type="triage-agent")
def run_triage(prompt: str) -> str:
# existing agent loop, unchanged — every chat.completions.create /
# responses.create call is sanitized and captured as a step
return my_existing_agent(client, prompt)
Anthropic works the same way via
auscult.integrations.anthropic.wrap_anthropic(client) (patches
messages.create).
For LangChain / LangGraph (install with auscult[langchain]), pass a
callback handler at invocation — the standard pluggable observability
mechanism for those frameworks:
from auscult.integrations.langchain import AuscultCallbackHandler
handler = AuscultCallbackHandler(agent_type="triage-agent")
result = graph.invoke(inputs, config={"callbacks": [handler]})
print(handler.last_run_id) # inspect with: auscult run <id>
Each LLM call and tool call becomes one sanitized step; the run finishes when
the root chain/graph ends (marked failed on error). The handler sets
raise_error, so sanitizer/DB failures propagate (fail-closed) instead of
being swallowed by LangChain.
If you prefer explicit context, start_run activates an ambient run that all
integrations record into (it propagates through asyncio, but not into
ThreadPoolExecutor workers — pass tracer= explicitly there):
from auscult import start_run, record_step
with start_run("triage-agent", initial_prompt=prompt):
run_existing_agent(client, prompt) # wrapped-client calls captured
record_step("manual_annotation()", output="anything else you want traced")
Instrument by hand
The explicit API is still there for custom loops:
from auscult.capture import AuscultTracer
tracer = AuscultTracer(agent_type="triage-agent", initial_prompt=prompt)
tracer.record_step(llm_command=command, output=output, error_message=error)
tracer.finish()
# Non-blocking capture for high-frequency agents (sanitize+DB on a worker):
with AuscultTracer(
agent_type="triage-agent",
initial_prompt=prompt,
background=True,
) as tracer:
tracer.record_step(llm_command=command, output=output)
Inspect from the CLI
auscult runs
auscult runs --agent-type triage-agent --status failed --since 2026-07-01 --limit 50
auscult run <id>
auscult steps <id>
auscult replay <id>
auscult compare <id> --handler mypkg.handlers:my_agent_handler
auscult stats
auscult export <id> --format jsonl -o run.jsonl
auscult purge --before 2026-01-01 --dry-run
auscult eval --verbose
auscult eval --dual-pass-model en_core_web_trf
auscult --json runs --limit 10 # machine-readable output for scripting
See ROADMAP.md for later ideas (audit UI, per-tenant lists, metrics).
Developing from source
git clone https://github.com/ruxir-ig/auscult.git
cd auscult
uv sync --group nlp # package + en_core_web_lg via uv sources
# or, for tests/CI (small model):
uv sync --group dev
export DATABASE_URL=sqlite:////tmp/auscult.sqlite
uv run auscult migrate
# equivalent: uv run migrate.py
Schema revisions (contributors):
uv run alembic revision --autogenerate -m "describe your change"
uv run auscult migrate
DATABASE_URL is read lazily for application code, and from the environment
when running migrations, so auscult --help works without it.
Sanitizer configuration
| Variable | Default | Meaning |
|---|---|---|
AUSCULT_SPACY_MODEL |
en_core_web_lg |
Primary spaCy model for NER. Use en_core_web_trf for best PERSON/LOCATION recall, or en_core_web_sm for faster local/test loads (auscult setup --model …, or uv sync --group dev / --group nlp-trf). |
AUSCULT_SCORE_THRESHOLD |
0.35 |
Minimum Presidio confidence to redact. Lower = more false positives redacted (safer for PHI; may over-redact clinical eponyms that slip past the allow-list). |
AUSCULT_DUAL_PASS_MODEL |
(unset) | Optional second spaCy model (e.g. en_core_web_trf). When set, PERSON/LOCATION candidates from this model are merged with the primary pass for better recall without running the heavy model on every entity type. |
Clinical disease eponyms (Parkinson, Addison, Crohn, …) are allow-listed so they
are not treated as PERSON. Organization-specific patterns (e.g. BADGE:…,
CASE#…) are deny-listed as CUSTOM_IDENTIFIER. Pass extra patterns via
Sanitizer(denylist_patterns=[...]).
Production guidance (defense in depth)
Even though the DB is meant to hold synthetic data:
- TLS to Postgres — use
sslmode=require(or verify-full) inDATABASE_URL. - At-rest encryption — enable volume encryption and/or column encryption for the Auscult database; sanitized ≠ public.
- Access control — restrict who can
SELECTfromruns/steps; treat traces as PHI-adjacent until audited. - Validate detection — run
auscult evalon the golden corpus after model / threshold changes; watchredaction_count/entity_countsdrift viaauscult stats.
Indexes
Migrations create composite indexes for filtered list/stats queries:
runs(agent_type, started_at),runs(status, started_at)- unique
(run_id, step_index)onsteps
Replay in Python
Replay a run in Python (for QA or regression checks):
from auscult.replay import RunReplayer
replayer = RunReplayer.from_run_id(run_id)
# Playback: walk recorded steps without calling an agent
for step in replayer.playback():
print(step.llm_command, "->", step.output or step.error_message)
# Compare: re-run your agent handler and diff against the recording
result = replayer.replay(my_agent_handler) # handler(cmd) -> (output, error)
assert result.all_matched
Tests
uv sync --group dev
AUSCULT_SPACY_MODEL=en_core_web_sm uv run pytest
uv run auscult eval --nlp-model en_core_web_sm
uv run ruff check src tests
uv run mypy
Run the end-to-end integration test (capture, migrations, sanitization, replay):
uv run pytest tests/test_integration.py
# or
uv run smoke_test.py
Tests cover PHI detection (names, phones, emails, dates/DOB formats, MRN variants, bare patient IDs, street addresses, clinical free text), clinical allow-list / deny-list behavior, JSON payload structure preservation, replacement consistency, fail-closed capture on sanitizer errors, the golden eval corpus, dual-pass merge behavior, and a guarantee that raw PHI never reaches the database.
Packaging note
spaCy models are not declared as package dependencies (PyPI forbids direct
URL requirements). After pip / uv install:
auscult setup # en_core_web_lg
auscult setup --model en_core_web_sm # smaller / faster
auscult setup --model en_core_web_trf # best PERSON recall
From a source checkout, uv can pull model wheels via [tool.uv.sources]:
uv sync --group nlp—en_core_web_lguv sync --group nlp-trf—en_core_web_trfuv sync --group dev—en_core_web_sm+ test tools +langchain-core
Optional PyPI extra:
auscult[langchain]—langchain-corefor the LangChain/LangGraph callback handler (OpenAI/Anthropic wrappers are duck-typed and need no extra)
Project details
Release history Release notifications | RSS feed
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 auscult-0.1.0.tar.gz.
File metadata
- Download URL: auscult-0.1.0.tar.gz
- Upload date:
- Size: 48.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb2b6205fa03e67bb64053ea10385e77dbcfc1920c3ff1f44f06a630319f273d
|
|
| MD5 |
cfc35a6cf0670220ab0eb479308e8166
|
|
| BLAKE2b-256 |
823eb3afb4416e6c8643e98d0e29895d46028cca6c50dcabd504b53e14292fe6
|
Provenance
The following attestation bundles were made for auscult-0.1.0.tar.gz:
Publisher:
publish.yml on ruxir-ig/auscult
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
auscult-0.1.0.tar.gz -
Subject digest:
fb2b6205fa03e67bb64053ea10385e77dbcfc1920c3ff1f44f06a630319f273d - Sigstore transparency entry: 2257045083
- Sigstore integration time:
-
Permalink:
ruxir-ig/auscult@41c059f60e83b58f96415394276e792b7b7898a0 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ruxir-ig
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@41c059f60e83b58f96415394276e792b7b7898a0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file auscult-0.1.0-py3-none-any.whl.
File metadata
- Download URL: auscult-0.1.0-py3-none-any.whl
- Upload date:
- Size: 45.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d5cc202488059bb4f13fc9dcf4ec0b36636bebe688d443a426b36d3aaf2ee1d
|
|
| MD5 |
dda305dd99a18027c2ebd404fb8cb7ac
|
|
| BLAKE2b-256 |
577e2aa70828e9b3787a6d01b9b2cc9278d11600344765c7deaf6dfa05e3fb3e
|
Provenance
The following attestation bundles were made for auscult-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ruxir-ig/auscult
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
auscult-0.1.0-py3-none-any.whl -
Subject digest:
9d5cc202488059bb4f13fc9dcf4ec0b36636bebe688d443a426b36d3aaf2ee1d - Sigstore transparency entry: 2257045096
- Sigstore integration time:
-
Permalink:
ruxir-ig/auscult@41c059f60e83b58f96415394276e792b7b7898a0 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ruxir-ig
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@41c059f60e83b58f96415394276e792b7b7898a0 -
Trigger Event:
release
-
Statement type: