Skip to content

mijuny/pacs-agent

Repository files navigation

pacs-agent

License: CC BY-NC 4.0 Python 3.11+

Agent-first, PID-safe research image loader for hospital PACS.

CLI tool: rad-loader

Why This Exists

AI agents are becoming the primary interface for hospital IT tasks. A radiologist using Claude Code can query PACS, manage research data, and automate HL7 workflows faster than navigating GUIs or writing one-off scripts. Agent-driven workflows are not a future possibility — they are already more efficient than the alternatives for many tasks.

But there is a fundamental problem: the agent's context window is a PHI leak vector.

When an AI agent runs dcm4che or reads DICOM headers, patient names, IDs, and birth dates flow into the agent's context. That context is sent to cloud servers (Anthropic, OpenAI, etc.). This is a privacy violation under GDPR, HIPAA, and most hospital data protection policies — regardless of how the cloud provider handles the data.

Existing DICOM tools were not designed with this threat model in mind. They predate LLM agents and treat PHI as something the user manages. They output unstructured text that agents must parse. They have no concept of an agent's context window as an attack surface.

pacs-agent addresses both problems:

  1. PHI never enters the agent's context. The tool acts as a privacy gateway between the agent and hospital systems. Input is accession numbers (not patient identifiers). Output uses pseudonymized case IDs. Anonymization is mandatory and happens inside the tool — there is no code path that exposes PHI to the caller.

  2. The tool is designed for agent operation. Structured JSON output. Machine-readable errors. Idempotent operations that are safe to retry. Built-in verification so the agent can assess data quality programmatically instead of visually.

  Cloud                          Hospital network
┌─────────────┐               ┌─────────────────────────────────┐
│ AI Agent    │               │                                 │
│ (LLM)      │──accession──▶ │  pacs-agent     ──▶  PACS       │
│             │◀──case ID───  │  (PHI gateway)                  │
│             │               │                                 │
│ Never sees: │               │  Handles internally:            │
│ - names     │               │  - patient names                │
│ - IDs       │               │  - patient IDs                  │
│ - DOB       │               │  - birth dates                  │
│ - any PHI   │               │  - all PHI tags                 │
└─────────────┘               └─────────────────────────────────┘

Design principles for agent-safe hospital tools

pacs-agent is an implementation of a broader pattern. Any tool that lets an AI agent interact with hospital systems should follow these principles:

  1. No PHI in agent context — the tool handles PHI internally and returns only de-identified or coded data. The agent never needs to see a patient name to do its job.
  2. Structured, machine-readable output — JSON by default, not human text that agents have to parse with regex.
  3. Idempotent operations — agents retry on failure. Tools must handle duplicate requests gracefully (already-loaded studies are skipped, not duplicated).
  4. Built-in verification — agents cannot visually inspect results. The tool must provide programmatic quality checks (outlier detection, completeness verification).
  5. Audit trail — every action is logged with who, what, when, and outcome. Non-negotiable for clinical data.
  6. Accession-based addressing — accession numbers are the natural unit of work for research. They identify studies without revealing patient identity.

This pattern extends beyond PACS. HL7 message routing, RIS queries, report retrieval, DICOM routing — anywhere an agent touches hospital data, the same architecture applies: a local tool that acts as a PHI firewall, exposing only what the agent needs to see.

Architecture

Researcher / AI Agent
┌──────────────────┐
│ rad-loader       │
│ load project AC  │
└────────┬─────────┘
         │
         ▼
┌──────────────────────────────────────┐
│ 1. C-FIND (accession → study UID)   │
│        ↓                             │
│ 2. Start temporary SCP (port 9012)   │
│        ↓                             │
│ 3. C-MOVE (study UID → our SCP)     │
│        ↓                             │
│    PACS sends C-STORE ──┐            │
│        ↓                │            │
│ 4. Receive + anonymize  │ ◀── mandatory, no bypass
│        ↓                │            │
│ 5. Save .dcm files      │            │
│        ↓                             │
│ 6. Update key.csv + audit log        │
│        ↓                             │
│ 7. Shut down SCP                     │
└──────────────────┬───────────────────┘
                   │ DICOM
         ┌─────────▼──────────┐
         │ Hospital PACS      │
         │ (any vendor)       │
         └────────────────────┘

Key design choices:

  • Allowlist anonymization — only explicitly listed DICOM tags survive. Everything else is deleted. Safer than a blocklist, where new or unexpected tags could slip through.
  • No patient identifiers in I/O — input is accession numbers only, output uses case IDs (case0001, case0002, ...).
  • Idempotent — already-loaded studies are skipped automatically. Safe to re-run.
  • Audit trail — every load is recorded in a SQLite database.

Quick Start

Install

git clone https://github.com/mijuny/pacs-agent.git
cd pacs-agent
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Configure

cp config/example.yaml config/config.yaml
# Edit config/config.yaml with your PACS connection details

Your PACS administrator needs to:

  1. Register your AE title (e.g. MY-LOADER) on the PACS
  2. Allow your host to connect
  3. Open the SCP port (default 9012) in your firewall for incoming C-STORE from PACS

Basic usage

# Test PACS connection
rad-loader echo

# Query a study (no images downloaded)
rad-loader query VAR9946804

# Load a study (download, anonymize, save)
rad-loader load myproject VAR9946804

# Load from file (one accession per line)
rad-loader load myproject --file accessions.txt

# Dry run (query only)
rad-loader load myproject --file accessions.txt --dry-run

# Check project status (includes outlier detection)
rad-loader status myproject

# View audit log
rad-loader audit myproject

CLI Reference

rad-loader [--config CONFIG] [--human] [-v] {echo,query,load,status,audit}

Global flags (must come BEFORE the subcommand):

Flag Description
--config PATH YAML config file (default: config/config.yaml)
--human Human-readable output instead of JSON
-v Verbose logging (DICOM-level traffic)

Commands

echo — Test PACS connection (C-ECHO)

rad-loader echo

query — Look up a study by accession number (C-FIND, no download)

rad-loader query <ACCESSION>

load — Download, anonymize, and save studies

rad-loader load <PROJECT> <AC1> [AC2 ...]
rad-loader load <PROJECT> --file <ACCESSION_FILE>
rad-loader load <PROJECT> --file <ACCESSION_FILE> --dry-run

status — Project statistics with outlier detection

rad-loader status <PROJECT>

audit — View audit log

rad-loader audit <PROJECT> [--last N]
rad-loader audit --all [--last N]

Verification & Audit

Load verification

Every load command returns a verification field:

{
  "verification": {
    "ok": false,
    "total_requested": 10,
    "loaded": 7,
    "skipped": 1,
    "failed": 2,
    "not_found": 1,
    "warnings": ["AC001 (case0003): only 2 images (unusually low)"]
  }
}

Outlier detection

The status command compares cases within a project:

{
  "outliers": {
    "ok": false,
    "median_series": 5,
    "median_images": 450,
    "warnings": [
      "case0003: 1 series vs median 5 — possibly incomplete study",
      "case0009: modality CR differs from majority MR"
    ]
  }
}

Audit log

All loads are recorded in a SQLite database with: Unix user, accession numbers, project name, timestamp, duration, and result.

Anonymization

Approach

pacs-agent uses an allowlist approach: only DICOM tags explicitly listed in the allowlist survive anonymization. All other tags — including unknown, private, and vendor-specific tags — are deleted. This is safer than a blocklist approach, where new or unexpected tags could slip through.

What is removed (PHI)

Field DICOM Tag
PatientName (0010,0010)
PatientID (0010,0020)
PatientBirthDate (0010,0030)
OtherPatientIDs (0010,1000)
OtherPatientNames (0010,1001)
IssuerOfPatientID (0010,0021)
PatientAddress (0010,1040)
PatientTelephoneNumbers (0010,2154)
AdditionalPatientHistory (0010,21B0)
PatientComments (0010,4000)
ReferringPhysicianName (0008,0090)
PerformingPhysicianName (0008,1050)
OperatorsName (0008,1070)
RequestingPhysician (0032,1032)
InstitutionName (0008,0080)
InstitutionAddress (0008,0081)
RequestAttributesSequence (0040,0275)
All private tags Odd group numbers
All unknown tags Not on allowlist

What is preserved

Category Examples
Identifiers (non-PID) AccessionNumber, StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID
Study metadata StudyDate, Modality, StudyDescription, SeriesDescription
Demographics PatientSex, PatientAge, PatientSize, PatientWeight
MR parameters RepetitionTime, EchoTime, FlipAngle, MagneticFieldStrength, SliceThickness, DiffusionBValue, ...
CT parameters KVP, ExposureTime, XRayTubeCurrent, ConvolutionKernel, ...
General acquisition ProtocolName, BodyPartExamined, ContrastBolusAgent, PatientPosition
Pixel description Rows, Columns, PixelSpacing, BitsAllocated, WindowCenter/Width, RescaleSlope/Intercept
Spatial geometry ImagePositionPatient, ImageOrientationPatient, SliceLocation, FrameOfReferenceUID
Equipment Manufacturer, ManufacturerModelName, SoftwareVersions, DeviceSerialNumber
Pixel data PixelData (the actual image)

What is set after anonymization

Field Value
PatientName Case ID (e.g. case0001)
PatientID Case ID
PatientIdentityRemoved YES
DeidentificationMethod pacs-agent allowlist v1

Limitations

  • Burned-in annotations in pixel data are NOT removed. Review images manually.
  • Structured Reports (SR) are not anonymized — they are skipped.
  • Single-threaded: one study at a time via C-MOVE.
  • Temporary SCP: the receive port is open only during the load.

Agent Integration

pacs-agent is designed to be operated by AI agents (Claude Code, etc.). All commands output structured JSON by default.

How it works with Claude Code

  1. Add the CLAUDE.md template to your project — it teaches the agent all available commands, flag ordering, and the verification workflow.
  2. The agent uses accession numbers as input (provided by the researcher).
  3. The agent receives JSON responses with case IDs, image counts, and verification results — never patient data.
  4. The agent can run the full workflow: query, load, verify, report outliers — without any PHI ever entering its context.

Accession file format

Plain text, one accession number per line. Empty lines and # comments are allowed:

# Brain hemorrhage study, spring 2026
VAR9946804
VAR9946805

# Control patients
VAR9946810
VAR9946811

Configuration

See config/example.yaml:

pacs:
  host: "pacs.example.com"        # PACS host IP or hostname
  port: 104                       # PACS port (standard DICOM)
  ae_title: "YOUR_PACS"           # PACS AE title (called AE)

scp:
  ae_title: "MY-LOADER"           # Our AE title (calling AE / SCP)
  port: 9012                      # Port for incoming C-STORE

output:
  base_dir: "/data/research"      # Base output directory

Copy to config/config.yaml and fill in your values. All .yaml files except example.yaml are gitignored.

Output Structure

<output_base_dir>/
├── audit.db                    # Global audit database (SQLite)
├── <project>/
│   ├── key.csv                 # case_id,accession,study_date,modality,description,series_count,image_count
│   ├── load.json               # Machine-readable load summary
│   ├── case0001/
│   │   ├── series01/*.dcm
│   │   ├── series02/*.dcm
│   │   └── ...
│   └── case0002/
│       └── ...

Development

pip install -e ".[dev]"
pytest tests/ -v

Tests that require DICOM sample files are automatically skipped in CI. To run the full test suite locally, place DICOM WG-04 reference files in ~/projects/dicom-test-files/data/WG04/REF/ as .zst archives.

Finnish Guide

A researcher-oriented guide in Finnish is available at GUIDE_FI.md.

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

Citation

If you use pacs-agent in your research, please cite:

@software{pacs_agent,
  author = {Nyman, Mikko},
  title = {pacs-agent: Agent-first, PID-safe research image loader for hospital PACS},
  url = {https://github.com/mijuny/pacs-agent},
  year = {2026}
}

License

This work is licensed under CC BY-NC 4.0. You are free to use and adapt it for non-commercial purposes with attribution.

About

Agent-first, PID-safe research image loader for hospital PACS

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors