All demos
Health-techLangGraph

Medication Safety

Drug-interaction check — decision-support only

Ready to replay
0.0s / 9.9s

Security Pipeline

Input
Sandbox
Network
PII Scan
Injection
Vault
LLM Call
Result
Real run · LangGraph · python run.py · captured 2026-06-30 · SDK 1.3.0
Run it yourself
View the agent code· with Declawhealth-tech/sandboxed/05-med-safety-langgraph/run.py
"""W5 — Medication Safety Copilot, sandboxed with declaw.

Governance posture (see shared/governance.py): this is a drug-interaction copilot
producing an ADVISORY (decision-support) only. The safety advisory is framed as a
RECOMMEND_REVIEW for a licensed clinician (REVIEWER_CLINICIAN), who owns the
prescribing decision. Advisory only — no hard gate.

Same LangGraph orchestration as the baseline, but every node that touches
the network runs inside a microVM with egress locked to the allowlist
{api.openai.com, rxnav.nlm.nih.gov, api.fda.gov}. PHI in the LLM-call step
crosses the declaw proxy → Guardrails NER + regex scanners tokenize names,
SSN, email, phone, address before the request body leaves the VM.

What declaw specifically buys on this workflow:
  * A jailbroken LLM that tries to POST the chart to a non-allowlisted
    host is iptables-DROPped (network policy).
  * The three non-LLM tool calls (RxNav, openFDA x2) only contain drug
    names — no PHI — and declaw's audit log proves it per-destination.
  * Per-step microVM isolation — if a compromised library tries to read
    the patient chart that an earlier step cached in /tmp, it gets
    FileNotFoundError (different sandbox).
"""
from __future__ import annotations

import json
import sys
import textwrap
from pathlib import Path
from typing import Annotated, TypedDict

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph

REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "sandboxed"))
from shared.mock_phi import PATIENTS  # noqa: E402
from shared import governance as gov  # noqa: E402
from shared.declaw_helpers import (  # noqa: E402
    HEALTHCARE_API_DOMAINS, LLM_DOMAINS, healthcare_multi_api_policy,
    llm_envs, run_python_in_sandbox,
)


class MedSafetyState(TypedDict, total=False):
    patient_id: str
    candidate_drug: str
    current_meds: list[str]
    interactions: list[dict]
    adverse_events: dict
    advisory: dict
    trace: Annotated[list[dict], "tool-call trace"]


# ---------- Sandboxed external-API calls (one sandbox per network hop) ----

RXNAV_INTERACTIONS_SCRIPT = textwrap.dedent("""
    import json, urllib.parse, urllib.request
    with open('/tmp/in.json') as f: inp = json.load(f)
    drugs = inp['drugs']
    out = []
    for name in drugs:
        results = []
        for field in ('generic_name', 'brand_name'):
            q = urllib.parse.quote(f'openfda.{field}:"{name}"')
            url = f'https://api.fda.gov/drug/label.json?search={q}&limit=1'
            try:
                req = urllib.request.Request(url, headers={'Accept':'application/json'})
                with urllib.request.urlopen(req, timeout=15) as r:
                    d = json.loads(r.read().decode())
                results = d.get('results') or []
                if results: break
            except Exception: pass
        if not results:
            out.append({'drug': name, 'interactions_text': None}); continue
        text = ' '.join(results[0].get('drug_interactions', []))[:800] or None
        out.append({'drug': name, 'interactions_text': text})
    with open('/tmp/out.json','w') as f: json.dump({'interactions': out}, f)
""")


OPENFDA_AE_SCRIPT = textwrap.dedent("""
    import json, urllib.parse, urllib.request
    with open('/tmp/in.json') as f: inp = json.load(f)
    drug = inp['drug']; limit = int(inp.get('limit', 5))
    search = urllib.parse.quote(f'patient.drug.medicinalproduct:"{drug}"')
    url = (f'https://api.fda.gov/drug/event.json?search={search}'
           f'&count=patient.reaction.reactionmeddrapt.exact&limit={limit}')
    try:
        req = urllib.request.Request(url, headers={'Accept':'application/json'})
        with urllib.request.urlopen(req, timeout=15) as r:
            d = json.loads(r.read().decode())
        ae = {'drug': drug, 'top_reactions': [
            {'reaction': x.get('term'), 'count': x.get('count')}
            for x in (d.get('results') or [])[:limit]]}
    except Exception:
        ae = {'drug': drug, 'top_reactions': []}
    with open('/tmp/out.json','w') as f: json.dump(ae, f)
""")


SYNTHESIZE_SCRIPT = textwrap.dedent("""
    import json
    from openai import OpenAI
    with open('/tmp/in.json') as f: inp = json.load(f)
    # Governance labels injected from shared.governance on the host (the sandbox
    # cannot import the shared module). The advisory is DECISION-SUPPORT only; a
    # licensed clinician owns the prescribing decision.
    GOV = inp.get('gov', {})
    c = OpenAI()
    r = c.chat.completions.create(
        model='gpt-4.1',
        messages=[
            {'role':'system','content':
             'You are a clinical pharmacist producing DECISION-SUPPORT only — '
             'NOT a prescribing decision. Given a patient record, a proposed new '
             'drug, FDA-label interaction text for each drug, and reported '
             'adverse-event counts, produce a JSON safety advisory with keys: '
             'risk_level (low|moderate|high), interaction_summary, '
             'top_adverse_events, recommendation. The recommendation is advisory '
             'for a licensed clinician who OWNS the prescribing decision. '
             'Be terse. Respond with a single JSON object.'},
            {'role':'user','content': json.dumps(inp)},
        ],
        response_format={'type': 'json_object'},
        max_completion_tokens=600,
    )
    advisory = json.loads(r.choices[0].message.content)
    # Stamp the advisory as decision-support owned by a licensed clinician.
    advisory['status'] = GOV.get('RECOMMEND_REVIEW', 'RECOMMEND_REVIEW')
    advisory['decision_owner'] = GOV.get('REVIEWER_CLINICIAN', 'licensed clinician')
    with open('/tmp/out.json','w') as f:
        json.dump({'advisory': advisory}, f)
""")


# ---------- Nodes ----------

def fetch_meds(state: MedSafetyState) -> MedSafetyState:
    p = PATIENTS[state["patient_id"]]
    meds = [m.split()[0] for m in p.medications]
    return {"current_meds": meds, "trace": [{"tool": "fetch_meds"}]}


def check_interactions(state: MedSafetyState) -> MedSafetyState:
    drugs = state["current_meds"] + [state["candidate_drug"]]
    print(f"[check_interactions] sandbox — calling api.fda.gov for {len(drugs)} drugs")
    out = run_python_in_sandbox(
        "rx-interactions", RXNAV_INTERACTIONS_SCRIPT,
        healthcare_multi_api_policy(),
        payload={"drugs": drugs},
    )
    return {"interactions": out["interactions"],
            "trace": [{"tool": "rx-interactions sandbox",
                       "drugs": drugs, "records": len(out['interactions'])}]}


def check_adverse(state: MedSafetyState) -> MedSafetyState:
    print(f"[check_adverse] sandbox — calling api.fda.gov/drug/event")
    out = run_python_in_sandbox(
        "openfda-ae", OPENFDA_AE_SCRIPT,
        healthcare_multi_api_policy(),
        payload={"drug": state["candidate_drug"], "limit": 5},
    )
    return {"adverse_events": out,
            "trace": [{"tool": "openfda-ae sandbox",
                       "drug": state["candidate_drug"]}]}


def synthesize(state: MedSafetyState) -> MedSafetyState:
    p = PATIENTS[state["patient_id"]]
    payload = {
        "patient": {
            "id": p.id, "name": p.name,
            "diagnoses": p.diagnoses,
            "current_meds": p.medications,
        },
        "proposed_drug": state["candidate_drug"],
        "fda_label_interactions": state["interactions"],
        "openfda_adverse_events": state["adverse_events"],
        # Governance labels — the advisory is decision-support; a licensed
        # clinician owns the prescribing decision (the sandbox can't import
        # shared.governance).
        "gov": {
            "RECOMMEND_REVIEW": gov.RECOMMEND_REVIEW,
            "REVIEWER_CLINICIAN": gov.REVIEWER_CLINICIAN,
        },
    }
    print("[synthesize] sandbox — gpt-4.1 with PHI redaction + rehydration")
    out = run_python_in_sandbox(
        "advisory-llm", SYNTHESIZE_SCRIPT,
        healthcare_multi_api_policy(),
        payload=payload, envs=llm_envs(),
    )
    return {"advisory": out["advisory"],
            "trace": [{"tool": "advisory-llm sandbox"}]}


def build_graph():
    g = StateGraph(MedSafetyState)
    g.add_node("fetch_meds", fetch_meds)
    g.add_node("check_interactions", check_interactions)
    g.add_node("check_adverse", check_adverse)
    g.add_node("synthesize", synthesize)
    g.add_edge(START, "fetch_meds")
    g.add_edge("fetch_meds", "check_interactions")
    g.add_edge("check_interactions", "check_adverse")
    g.add_edge("check_adverse", "synthesize")
    g.add_edge("synthesize", END)
    return g.compile(checkpointer=MemorySaver())


def main() -> None:
    initial = {"patient_id": "p-001", "candidate_drug": "semaglutide"}
    print("=== Med Safety Copilot (sandboxed, multi-API, 3 microVMs) ===")
    print(f"governance: {gov.governance_banner()}")
    print(f"Advisory is decision-support for a {gov.REVIEWER_CLINICIAN}; the "
          "clinician owns the prescribing decision (advisory, not a gate).")
    print(f"patient={initial['patient_id']}  proposed={initial['candidate_drug']}")
    print(f"allow_out={LLM_DOMAINS + HEALTHCARE_API_DOMAINS}\n")
    config = {"configurable": {"thread_id": "med-safety-sbx-1"}}
    result = build_graph().invoke(initial, config=config)
    print("\n--- Tool trace ---")
    for t in result.get("trace", []):
        print(" ", t)
    print("\n--- Safety advisory (DECISION-SUPPORT — clinician owns prescribing) ---")
    print(json.dumps(result.get("advisory", {}), indent=2))
    print(f"\n[note] This advisory is decision-support only; a {gov.REVIEWER_CLINICIAN} "
          "owns the prescribing decision. Nothing is prescribed autonomously.")


if __name__ == "__main__":
    main()
View raw audit JSON
[
  {
    "atMs": 450,
    "kind": "stage",
    "payload": {
      "stage": "input",
      "status": "done",
      "detail": "read input files in-VM"
    }
  },
  {
    "atMs": 1519,
    "kind": "stage",
    "payload": {
      "stage": "sandbox",
      "status": "done",
      "detail": "3 Firecracker microVM(s) · own kernel · egress-locked"
    }
  },
  {
    "atMs": 2588,
    "kind": "network",
    "payload": {
      "event": "egress_allowed",
      "detail": {
        "host": "api.fda.gov",
        "port": 443,
        "reason": "allowlist"
      }
    }
  },
  {
    "atMs": 3656,
    "kind": "network",
    "payload": {
      "event": "egress_allowed",
      "detail": {
        "host": "api.openai.com",
        "port": 443,
        "reason": "allowlist"
      }
    }
  },
  {
    "atMs": 4725,
    "kind": "security",
    "payload": {
      "event": "pii_redaction",
      "detail": {
        "entities": [
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_28",
            "confidence": 0.85
          },
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_31",
            "confidence": 0.85
          },
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_31",
            "confidence": 0.85
          },
          {
            "entity_type": "PERSON",
            "masked_value": "REDACTED_PERSON_30",
            "confidence": 0.85
          }
        ],
        "destination": "api.openai.com",
        "rehydrated_on_reply": true
      }
    }
  },
  {
    "atMs": 5794,
    "kind": "audit",
    "payload": {
      "event": "pii_redaction",
      "category": "security",
      "detail": {
        "entities": 4,
        "destination": "api.openai.com",
        "rehydrated": true
      },
      "timestamp": "2026-06-30T20:28:42.659599Z"
    }
  },
  {
    "atMs": 6862,
    "kind": "security",
    "payload": {
      "event": "vault_brokered",
      "detail": {
        "keys": "OPENAI_API_KEY",
        "host": "api.openai.com",
        "injected_at": "egress proxy",
        "exposure_to_vm": "none (declaw:vault-managed placeholder)"
      }
    }
  },
  {
    "atMs": 7931,
    "kind": "stage",
    "payload": {
      "stage": "llm",
      "status": "done",
      "detail": "model called from inside the microVM (PII redacted on the wire) · 35.0s real",
      "durationMs": 1819
    }
  },
  {
    "atMs": 9000,
    "kind": "decision",
    "payload": {
      "text": "Interaction advisory → RECOMMEND_REVIEW (a licensed clinician owns the prescribing decision)"
    }
  }
]