diff --git a/docs/tutorials/interop/basic_conversion.md b/docs/cookbook/format_conversion.md similarity index 74% rename from docs/tutorials/interop/basic_conversion.md rename to docs/cookbook/format_conversion.md index 25c8f3ac..5dd3b364 100644 --- a/docs/tutorials/interop/basic_conversion.md +++ b/docs/cookbook/format_conversion.md @@ -1,37 +1,38 @@ -# Basic Format Conversion +# Convert Between Healthcare Data Formats -This tutorial demonstrates how to use the HealthChain interoperability module to convert between different healthcare data formats. +Convert between CDA, HL7v2, and FHIR formats using HealthChain's interoperability engine. This recipe covers the most common conversion scenarios for integrating legacy healthcare systems with modern FHIR-based applications. -## Prerequisites - -- HealthChain installed (`pip install healthchain`) -- Basic understanding of FHIR resources -- Sample CDA or HL7v2 files (or you can use the examples below) +The [InteropEngine](../reference/interop/engine.md) provides a unified interface for bidirectional format conversion, handling the complexity of mapping between different healthcare data standards. ## Setup -First, let's import the required modules and create an interoperability engine: +Install HealthChain: + +```bash +pip install healthchain +``` + +Create an interoperability engine: ```python from healthchain.interop import create_interop, FormatType from pathlib import Path import json -# Create an engine engine = create_interop() ``` ## Converting CDA to FHIR -Let's convert a CDA document to FHIR resources: +Parse a CDA document and extract FHIR resources: ```python -# Sample CDA document cda_xml = """ - + Example CDA Document @@ -54,7 +55,8 @@ cda_xml = """
- + Problems Hypertension @@ -62,10 +64,13 @@ cda_xml = """ - + - +
@@ -78,22 +83,21 @@ cda_xml = """ # Convert CDA to FHIR resources fhir_resources = engine.to_fhir(cda_xml, src_format=FormatType.CDA) -# Print the resulting FHIR resources +# Inspect the results for resource in fhir_resources: print(f"Resource Type: {resource.resource_type}") print(json.dumps(resource.dict(), indent=2)) - print("-" * 40) ``` ## Converting FHIR to CDA -You can also convert FHIR resources to a CDA document: +Generate a CDA document from FHIR resources: ```python from fhir.resources.condition import Condition from fhir.resources.patient import Patient -# Create some FHIR resources +# Create FHIR resources patient = Patient( resourceType="Patient", id="patient-1", @@ -135,20 +139,18 @@ condition = Condition( onsetDateTime="2018-01-01" ) -# Convert FHIR resources to CDA +# Convert to CDA resources = [patient, condition] cda_document = engine.from_fhir(resources, dest_format=FormatType.CDA) -# Print the resulting CDA document print(cda_document) ``` ## Converting HL7v2 to FHIR -Let's convert an HL7v2 message to FHIR resources: +Parse an HL7v2 message and extract FHIR resources: ```python -# Sample HL7v2 message hl7v2_message = """ MSH|^~\&|EPIC|EPICADT|SMS|SMSADT|199912271408|CHARRIS|ADT^A01|1817457|D|2.5| PID|1||PATID1234^5^M11^ADT1^MR^GOOD HEALTH HOSPITAL~123456789^^^USSSA^SS||EVERYMAN^ADAM^A^III||19610615|M||C|2222 HOME STREET^^GREENSBORO^NC^27401-1020|GL|(555) 555-2004|(555)555-2004||S||PATID12345001^2^M10^ADT1^AN^A|444333333|987654^NC| @@ -159,22 +161,19 @@ PV1|1|I|2000^2012^01||||004777^ATTEND^AARON^A|||SUR||||ADM|A0| # Convert HL7v2 to FHIR resources fhir_resources = engine.to_fhir(hl7v2_message, src_format=FormatType.HL7V2) -# Print the resulting FHIR resources for resource in fhir_resources: print(f"Resource Type: {resource.resource_type}") print(json.dumps(resource.dict(), indent=2)) - print("-" * 40) ``` ## Converting FHIR to HL7v2 -You can also convert FHIR resources to an HL7v2 message: +Generate an HL7v2 message from FHIR resources: ```python from fhir.resources.patient import Patient from fhir.resources.encounter import Encounter -# Create some FHIR resources patient = Patient( resourceType="Patient", id="patient-1", @@ -218,45 +217,38 @@ encounter = Encounter( } ) -# Convert FHIR resources to HL7v2 +# Convert to HL7v2 resources = [patient, encounter] hl7v2_message = engine.from_fhir(resources, dest_format=FormatType.HL7V2) -# Print the resulting HL7v2 message print(hl7v2_message) ``` -## Saving Conversion Results +## Saving Results -Let's save our conversion results to files: +Save converted data to files: ```python -# Save FHIR resources to JSON files output_dir = Path("./output") output_dir.mkdir(exist_ok=True) +# Save FHIR resources as JSON for resource in fhir_resources: filename = f"{resource.resource_type.lower()}_{resource.id}.json" with open(output_dir / filename, "w") as f: json.dump(resource.dict(), f, indent=2) -# Save CDA document to XML file +# Save CDA document as XML with open(output_dir / "document.xml", "w") as f: f.write(cda_document) -# Save HL7v2 message to file +# Save HL7v2 message with open(output_dir / "message.hl7", "w") as f: f.write(hl7v2_message) ``` -## Conclusion - -In this tutorial, you learned how to: - -- Convert CDA documents to FHIR resources -- Convert FHIR resources to CDA documents -- Convert HL7v2 messages to FHIR resources -- Convert FHIR resources to HL7v2 messages -- Save conversion results to files +## Next Steps -For more advanced interoperability features, see the [InteropEngine documentation](../../reference/interop/engine.md) and other cookbook examples. +- [InteropEngine documentation](../reference/interop/engine.md) - Advanced configuration and customization +- [Configuration guide](../reference/interop/configuration.md) - Custom templates and mappings +- [CDA Adapter](../reference/io/adapters/cdaadapter.md) - Higher-level CDA integration with Document containers diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 165624b6..0791585f 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -1,31 +1,109 @@ -# ๐Ÿณ Cookbook: Hands-On Examples +# Cookbook -Dive into real-world, production-ready examples to learn how to build interoperable healthcare AI apps with **HealthChain**. +Hands-on, production-ready examples for building healthcare AI applications with HealthChain. ---- +
+ Filter: + HealthTech + GenAI + ML Research + Gateway + Pipeline + Interop + FHIR + CDS Hooks + Sandbox + +
-## ๐Ÿšฆ Getting Started +
+
-- [**Working with FHIR Sandboxes**](./setup_fhir_sandboxes.md) - *Spin up and access free Epic, Medplum, and other FHIR sandboxes for safe experimentation. This is the recommended first step before doing the detailed tutorials below.* + +
๐Ÿšฆ
+
Working with FHIR Sandboxes
+
+ Spin up and access free Epic, Medplum, and other FHIR sandboxes for safe experimentation. Recommended first step before the other tutorials. +
+
+ FHIR + Sandbox +
+
---- + +
๐Ÿ”ฌ
+
Deploy ML Models: Real-Time Alerts & Batch Screening
+
+ Deploy the same ML model two ways: CDS Hooks for point-of-care sepsis alerts, and FHIR Gateway for population-level batch screening with RiskAssessment resources. +
+
+ ML Research + Gateway + CDS Hooks +
+
+ + +
๐Ÿ”—
+
Multi-Source Patient Data Aggregation
+
+ Merge patient data from multiple FHIR sources (Epic, Cerner, etc.), deduplicate conditions, prove provenance, and handle cross-vendor errors. Foundation for RAG and analytics workflows. +
+
+ GenAI + Gateway + FHIR +
+
-## ๐Ÿ“š How-To Guides + +
๐Ÿงพ
+
Automate Clinical Coding & FHIR Integration
+
+ Extract medical conditions from clinical documentation using AI, map to SNOMED CT codes, and sync as FHIR Condition resources for billing, analytics, and interoperability. +
+
+ HealthTech + Pipeline + Interop +
+
-- ๐Ÿ”ฌ **[Deploy ML Models: Real-Time Alerts & Batch Screening](./ml_model_deployment.md)** - *Deploy the same ML model two ways: CDS Hooks for point-of-care sepsis alerts, and FHIR Gateway for population-level batch screening with RiskAssessment resources.* + +
๐Ÿ“
+
Summarize Discharge Notes with CDS Hooks
+
+ Deploy a CDS Hooks-compliant service that listens for discharge events, auto-generates concise plain-language summaries, and delivers actionable clinical cards directly into the EHR workflow. +
+
+ HealthTech + Gateway + CDS Hooks +
+
-- ๐Ÿšฆ **[Multi-Source Patient Data Aggregation](./multi_ehr_aggregation.md)** - *Merge patient data from multiple FHIR sources (Epic, Cerner, etc.), deduplicate conditions, prove provenance, and robustly handle cross-vendor errors. Foundation for retrieval-augmented generation (RAG) and analytics workflows.* + +
๐Ÿ”„
+
Convert Between Healthcare Data Formats
+
+ Convert between CDA, HL7v2, and FHIR formats using the interoperability engine. Handle bidirectional conversion for integrating legacy systems with modern FHIR applications. +
+
+ HealthTech + Interop + FHIR +
+
-- ๐Ÿงพ **[Automate Clinical Coding & FHIR Integration](./clinical_coding.md)** - *Extract medical conditions from clinical documentation using AI, map to SNOMED CT codes, and sync as FHIR Condition resources to systems like Medplumโ€”enabling downstream billing, analytics, and interoperability.* + -- ๐Ÿ“ **[Summarize Discharge Notes with CDS Hooks](./discharge_summarizer.md)** - *Deploy a CDS Hooks-compliant service that listens for discharge events, auto-generates concise plain-language summaries, and delivers actionable clinical cards directly into the EHR workflow.* +
+
--- -!!! info "What next?" +!!! tip "What next?" See the source code for each recipe, experiment with the sandboxes, and adapt the patterns for your projects! diff --git a/docs/index.md b/docs/index.md index f3681305..da431181 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,11 @@ -# Welcome to HealthChain ๐Ÿ’ซ ๐Ÿฅ +--- +template: welcome.html +hide: + - navigation + - toc +--- + +# Welcome to HealthChain HealthChain is an open-source Python toolkit that streamlines productionizing healthcare AI. Built for AI/ML practitioners, it simplifies the complexity of real-time EHR integrations by providing seamless FHIR integration, unified data pipelines, and production-ready deployment. diff --git a/docs/javascripts/cookbook-filter.js b/docs/javascripts/cookbook-filter.js new file mode 100644 index 00000000..83c8911e --- /dev/null +++ b/docs/javascripts/cookbook-filter.js @@ -0,0 +1,113 @@ +/** + * Cookbook Filter - Tag-based filtering for cookbook cards + * Compatible with MkDocs Material's instant loading feature + */ +function initCookbookFilter() { + const filterTags = document.querySelectorAll('.tag-filter'); + const cards = document.querySelectorAll('.cookbook-card'); + const clearBtn = document.getElementById('clearFilters'); + const noResults = document.getElementById('noResults'); + + // Exit early if elements don't exist (not on cookbook page) + if (!filterTags.length || !cards.length) { + return; + } + + // Prevent duplicate initialization by checking for marker + if (filterTags[0].dataset.initialized === 'true') { + return; + } + filterTags[0].dataset.initialized = 'true'; + + const activeFilters = new Set(); + + function updateCards() { + let visibleCount = 0; + + cards.forEach(card => { + const cardTags = card.dataset.tags.split(' '); + const matches = activeFilters.size === 0 || + [...activeFilters].some(filter => cardTags.includes(filter)); + + if (matches) { + card.classList.remove('filtered-out'); + card.classList.add('filtered-in'); + visibleCount++; + } else { + card.classList.add('filtered-out'); + card.classList.remove('filtered-in'); + } + }); + + // Show/hide no results message + if (noResults) { + if (visibleCount === 0 && activeFilters.size > 0) { + noResults.classList.remove('hidden'); + } else { + noResults.classList.add('hidden'); + } + } + + // Show/hide clear button + if (clearBtn) { + if (activeFilters.size > 0) { + clearBtn.classList.remove('hidden'); + } else { + clearBtn.classList.add('hidden'); + } + } + } + + filterTags.forEach(tag => { + tag.addEventListener('click', () => { + const filterValue = tag.dataset.tag; + + if (activeFilters.has(filterValue)) { + activeFilters.delete(filterValue); + tag.classList.remove('active'); + } else { + activeFilters.add(filterValue); + tag.classList.add('active'); + } + + updateCards(); + }); + }); + + function clearAllFilters() { + activeFilters.clear(); + filterTags.forEach(tag => tag.classList.remove('active')); + updateCards(); + } + + if (clearBtn) { + clearBtn.addEventListener('click', clearAllFilters); + } + + // Expose globally for inline onclick handlers + window.clearAllFilters = clearAllFilters; +} + +// Run immediately since extra_javascript loads after DOM is ready +initCookbookFilter(); + +// Also handle MkDocs Material instant navigation if available +// This uses a polling approach to wait for document$ to be defined +(function waitForInstantNav() { + if (typeof document$ !== 'undefined') { + document$.subscribe(function() { + initCookbookFilter(); + }); + } else { + // Fallback: use location hash change as a proxy for navigation + window.addEventListener('hashchange', initCookbookFilter); + // Also try again after a short delay in case document$ loads later + setTimeout(function() { + if (typeof document$ !== 'undefined') { + document$.subscribe(function() { + initCookbookFilter(); + }); + } + }, 1000); + } +})(); diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 00000000..922c914f --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block footer %} +
+ +
+{% endblock %} diff --git a/docs/overrides/welcome.html b/docs/overrides/welcome.html new file mode 100644 index 00000000..d821ca52 --- /dev/null +++ b/docs/overrides/welcome.html @@ -0,0 +1,173 @@ + + +{% extends "main.html" %} +{% block tabs %} +{{ super() }} +{% endblock %} + +{% block content %} + +{% endblock %} + +{% block footer %} +
+ +
+{% endblock %} diff --git a/docs/quickstart.md b/docs/quickstart.md index 08f03972..30b3f2bd 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -86,7 +86,7 @@ print(doc.fhir.problem_list) # FHIR Condition #### 2. Build With Components and Adapters -[**Components**](./reference/) are reusable, stateful classes that encapsulate specific processing logic, model loading, or configuration for your pipeline. Use them to organize complex workflows, handle model state, or integrate third-party libraries with minimal setup. +[**Components**](./reference/pipeline/components/components.md) are reusable, stateful classes that encapsulate specific processing logic, model loading, or configuration for your pipeline. Use them to organize complex workflows, handle model state, or integrate third-party libraries with minimal setup. HealthChain provides a set of ready-to-use [**NLP Integrations**](./reference/pipeline/integrations/integrations.md) for common clinical NLP and ML tasks, and you can easily implement your own. @@ -131,7 +131,7 @@ output = adapter.format(processed_doc) Prebuilt pipelines are the fastest way to jump into healthcare AI with minimal setup: just load and run. Each pipeline bundles best-practice components and models for common clinical tasks (like coding or summarization) and handles all FHIR/CDA conversion for you. Easily customize or extend pipelines by adding/removing components, or swap models as needed. -[(Full Documentation on Pipelines)](./reference/pipeline/pipeline.md#prebuilt-) +[(Full Documentation on Pipelines)](./reference/pipeline/pipeline.md#prebuilt) ```python from healthchain.pipeline import MedicalCodingPipeline @@ -190,7 +190,7 @@ Workflows determine the request structure, required FHIR resources, and validati #### Available Dataset Loaders -[**Dataset Loaders**](./reference/utilities/sandbox.md#dataset-loaders) are shortcuts for loading common clinical test datasets from file. Currently available: +[**Dataset Loaders**](./reference/utilities/sandbox.md#dataset-registry) are shortcuts for loading common clinical test datasets from file. Currently available: | Dataset Key | Description | FHIR Version | Source | Download Link | |--------------------|---------------------------------------------|--------------|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md new file mode 100644 index 00000000..0dea45cb --- /dev/null +++ b/docs/reference/concepts.md @@ -0,0 +1,263 @@ +# Core Concepts + +HealthChain has three main components that work together to connect your AI applications to healthcare systems: + +- **Gateway:** Connect to multiple healthcare systems with a single API. +- **Pipelines:** Easily build data processing pipelines for both clinical text and [FHIR](https://www.hl7.org/fhir/) data. +- **InteropEngine:** Seamlessly convert between data formats like [FHIR](https://www.hl7.org/fhir/), [HL7 CDA](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=7), and [HL7v2](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=185). + + +## Gateway + +The [**HealthChainAPI**](./gateway/api.md) provides a unified interface for connecting your AI application and models to multiple healthcare systems through a single API. It automatically handles [FHIR API](https://www.hl7.org/fhir/http.html), [CDS Hooks](https://cds-hooks.org/), and [SOAP/CDA protocols](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=7) with [OAuth2 authentication](https://oauth.net/2/). + +[(Full Documentation on Gateway)](./gateway/gateway.md) + +```python +from healthchain.gateway import HealthChainAPI, FHIRGateway +from fhir.resources.patient import Patient + +# Create your healthcare application +app = HealthChainAPI(title="My Healthcare AI App") + +# Connect to multiple FHIR servers +fhir = FHIRGateway() +fhir.add_source("epic", "fhir://fhir.epic.com/r4?client_id=...") +fhir.add_source("medplum", "fhir://api.medplum.com/fhir/R4/?client_id=...") + +# Add AI transformations to FHIR data +@fhir.transform(Patient) +def enhance_patient(id: str, source: str = None) -> Patient: + patient = fhir.read(Patient, id, source) + # Your AI logic here + patient.active = True + fhir.update(patient, source) + return patient + +# Register and run +app.register_gateway(fhir) + +# Available at: GET /fhir/transform/Patient/123?source=epic +``` + +## Pipeline + +HealthChain [**Pipelines**](./pipeline/pipeline.md) provide a flexible way to build and manage processing pipelines for NLP and ML tasks that can easily integrate with electronic health record (EHR) systems. + +You can build pipelines with three different approaches: + +### 1. Quick Inline Functions + +For quick experiments, start by picking the right [**Container**](./io/containers/containers.md) when you initialize your pipeline (e.g. `Pipeline[Document]()` for clinical text). + +Containers make your pipeline FHIR-native by loading and transforming your data (free text, EHR resources, etc.) into structured FHIR-ready formats. Just add your processing functions with `@add_node`, compile with `.build()`, and your pipeline is ready to process FHIR data end-to-end. + +[(Full Documentation on Containers)](./io/containers/containers.md) + +```python +from healthchain.pipeline import Pipeline +from healthchain.io import Document +from healthchain.fhir import create_condition + +pipeline = Pipeline[Document]() + +@pipeline.add_node +def extract_diabetes(doc: Document) -> Document: + """Adds a FHIR Condition for diabetes if mentioned in the text.""" + if "diabetes" in doc.text.lower(): + condition = create_condition( + code="73211009", + display="Diabetes mellitus", + ) + doc.fhir.problem_list.append(condition) + + return doc + +pipe = pipeline.build() + +doc = Document("Patient has a history of diabetes.") +doc = pipe(doc) + +print(doc.fhir.problem_list) # FHIR Condition +``` + +### 2. Build With Components and Adapters + +[**Components**](./pipeline/components/components.md) are reusable, stateful classes that encapsulate specific processing logic, model loading, or configuration for your pipeline. Use them to organize complex workflows, handle model state, or integrate third-party libraries with minimal setup. + +HealthChain provides a set of ready-to-use [**NLP Integrations**](./pipeline/integrations/integrations.md) for common clinical NLP and ML tasks, and you can easily implement your own. + +[(Full Documentation on Components)](./pipeline/components/components.md) + +```python +from healthchain.pipeline import Pipeline +from healthchain.pipeline.components import TextPreProcessor, SpacyNLP, TextPostProcessor +from healthchain.io import Document + +pipeline = Pipeline[Document]() + +pipeline.add_node(TextPreProcessor()) +pipeline.add_node(SpacyNLP.from_model_id("en_core_sci_sm")) +pipeline.add_node(TextPostProcessor()) + +pipe = pipeline.build() + +doc = Document("Patient presents with hypertension.") +output = pipe(doc) +``` + +You can process legacy healthcare data formats too. [**Adapters**](./io/adapters/adapters.md) convert between healthcare formats like [CDA](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=7) and your pipeline โ€” just parse, process, and format without worrying about low-level data conversion. + +[(Full Documentation on Adapters)](./io/adapters/adapters.md) + +```python +from healthchain.io import CdaAdapter +from healthchain.models import CdaRequest + +# Use adapter for format conversion +adapter = CdaAdapter() +cda_request = CdaRequest(document="") + +# Parse, process, format +doc = adapter.parse(cda_request) +processed_doc = pipe(doc) +output = adapter.format(processed_doc) +``` + +### 3. Use Prebuilt Pipelines + +Prebuilt pipelines are the fastest way to jump into healthcare AI with minimal setup: just load and run. Each pipeline bundles best-practice components and models for common clinical tasks (like coding or summarization) and handles all FHIR/CDA conversion for you. Easily customize or extend pipelines by adding/removing components, or swap models as needed. + +[(Full Documentation on Pipelines)](./pipeline/pipeline.md#prebuilt) + +```python +from healthchain.pipeline import MedicalCodingPipeline +from healthchain.models import CdaRequest + +# Or load from local model +pipeline = MedicalCodingPipeline.from_local_model("./path/to/model", source="spacy") + +cda_request = CdaRequest(document="") +output = pipeline.process_request(cda_request) +``` + +## Interoperability + +The HealthChain Interoperability module provides tools for converting between different healthcare data formats, including FHIR, CDA, and HL7v2 messages. + +[(Full Documentation on Interoperability Engine)](./interop/interop.md) + +```python +from healthchain.interop import create_interop, FormatType + +# Uses bundled configs - basic CDA โ†” FHIR conversion +engine = create_interop() + +# Load a CDA document +with open("tests/data/test_cda.xml", "r") as f: + cda_xml = f.read() + +# Convert CDA XML to FHIR resources +fhir_resources = engine.to_fhir(cda_xml, src_format=FormatType.CDA) + +# Convert FHIR resources back to CDA +cda_document = engine.from_fhir(fhir_resources, dest_format=FormatType.CDA) +``` + + +## Utilities + +### Sandbox Client + +Use [**SandboxClient**](./utilities/sandbox.md) to quickly test your app against real-world EHR scenarios like CDS Hooks or Clinical Documentation Improvement (CDI) workflows. Load test datasets, send requests to your service, and validate responses in a few lines of code. + +[(Full Documentation on Sandbox)](./utilities/sandbox.md) + +#### Workflows + +A [**workflow**](./utilities/sandbox.md#workflow-protocol-compatibility) represents a specific event in an EHR system that triggers your service (e.g., `patient-view` when opening a patient chart, `encounter-discharge` when discharging a patient). + +Workflows determine the request structure, required FHIR resources, and validation rules. Different workflows are compatible with different protocols: + +| Workflow Type | Protocol | Example Workflows | +|-------------------------------------|------------|--------------------------------------------------------| +| **CDS Hooks** | REST | `patient-view`, `order-select`, `order-sign`, `encounter-discharge` | +| **Clinical Documentation** | SOAP | `sign-note-inpatient`, `sign-note-outpatient` | + + +#### Available Dataset Loaders + +[**Dataset Loaders**](./utilities/sandbox.md#dataset-registry) are shortcuts for loading common clinical test datasets from file. Currently available: + +| Dataset Key | Description | FHIR Version | Source | Download Link | +|--------------------|---------------------------------------------|--------------|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| `mimic-on-fhir` | **MIMIC-IV on FHIR Demo Dataset** | R4 | [PhysioNet Project](https://physionet.org/content/mimic-iv-fhir-demo/2.1.0/) | [Download ZIP](https://physionet.org/content/mimic-iv-fhir-demo/get-zip/2.1.0/) (49.5 MB) | +| `synthea-patient` | **Synthea FHIR Patient Records** | R4 | [Synthea Downloads](https://synthea.mitre.org/downloads) | [Download ZIP](https://arc.net/l/quote/hoquexhy) (100 Sample, 36 MB) | + + +```python +from healthchain.sandbox import list_available_datasets + +# See all registered datasets with descriptions +datasets = list_available_datasets() +print(datasets) +``` + +#### Basic Usage + +```python +from healthchain.sandbox import SandboxClient + +# Initialize client with your service URL and workflow +client = SandboxClient( + url="http://localhost:8000/cds/encounter-discharge", + workflow="encounter-discharge" +) + +# Load test data from a registered dataset +client.load_from_registry( + "synthea-patient", + data_dir="./data/synthea", + resource_types=["Condition", "DocumentReference"], + sample_size=3 +) + +# Optionally inspect before sending +client.preview_requests() # See what will be sent +client.get_status() # Check client state + +# Send requests to your service +responses = client.send_requests() +``` + +For clinical documentation workflows using SOAP/CDA: + +```python +# Use context manager for automatic result saving +with SandboxClient( + url="http://localhost:8000/notereader/ProcessDocument", + workflow="sign-note-inpatient", + protocol="soap" +) as client: + client.load_from_path("./cookbook/data/notereader_cda.xml") + responses = client.send_requests() + # Results automatically saved to ./output/ on success +``` + +### FHIR Helpers + +Use `healthchain.fhir` helpers to quickly create and manipulate FHIR resources (like `Condition`, `Observation`, etc.) in your code, ensuring they're standards-compliant with minimal boilerplate. + +[(Full Documentation on FHIR Helpers)](./utilities/fhir_helpers.md) + +```python +from healthchain.fhir import create_condition + +condition = create_condition( + code="38341003", + display="Hypertension", + system="http://snomed.info/sct", + subject="Patient/Foo", + clinical_status="active" +) +``` diff --git a/docs/reference/interop/experimental.md b/docs/reference/interop/experimental.md index baa15677..ba8793db 100644 --- a/docs/reference/interop/experimental.md +++ b/docs/reference/interop/experimental.md @@ -76,4 +76,4 @@ We welcome contributions to improve experimental templates! 2. ๐Ÿ”ฎ **Future sections** - Procedures, Vital Signs, Lab Results 3. ๐Ÿ”ง **Template tooling** - Better validation and testing framework -Want to help? Check our [contribution guidelines](../../community/contribution_guide.md) and pick up one of these challenges! +Want to help? Check our [contribution guidelines](https://github.com/dotimplement/HealthChain/blob/main/CONTRIBUTING.md) and pick up one of these challenges! diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 405a0d4f..d2d520bb 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,125 +1,345 @@ -@font-face { - font-family: 'Source Code Pro Custom', monospace; - src: url(https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap); - } - - :root > * { - --md-default-bg-color: #FFFFFF; - --md-code-bg-color: #2E3440; - --md-code-fg-color: #FFFFFF; - --md-text-font-family: "Roboto"; - --md-code-font: "Source Code Pro Custom"; - --md-default-fg-color--light: #D8DEE9; - --md-default-fg-color--lighter: #E5E9F0; - --md-default-fg-color--lightest: #ECEFF4; - } - - .index-pre-code { - max-width: 700px; - left: 50%; - } - - .index-pre-code pre>code { - text-align: left; - } - - .md-typeset pre>code { - border-radius: .2rem; - box-shadow: 10px 5px 5px #D8DEE9; - } - - .md-typeset p > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset strong > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-content p > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset td > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset li > code { - background: #ECEFF4; - color: #000000; - font-weight: 500; - } - - .md-typeset code { - font-weight: 500; - } - - .md-typeset pre { - margin-left: .5rem; - margin-right: .5rem; - margin-top: 2rem; - margin-bottom: 2rem; - } - - /* .language-python { - background: #FFFFFF ! important - } */ - - .language-bash { - background: #FFFFFF ! important - } - - h1.title { - color: #FFFFFF; - margin: 0px 0px 5px; - } - - h2.subtitle { - margin: 5px 0px 25px; - } - - .md-typeset { - line-height: 24px; - font-weight: 400; - } - - .md-typeset h1 { - font-weight: bold; - color: #000000; - } - - .md-typeset h2 { - font-weight: bold; - color: #000000; - } - - .md-typeset h3 { - font-weight: bold; - color: #000000; - } - - .md-nav__link--active { - background-color: #ECEFF4; - } - - code { - white-space : pre-wrap !important; - } - - .md-content a { - color: #ed4e80; /* Default link color */ - text-decoration: none; /* Remove underline by default */ - transition: all 0.2s ease-in-out; /* Smooth transition for hover effect */ - } - - .md-content a:hover { - color: #2e78d8; /* Darker shade for hover state */ - } +/* Import Source Code Pro from Google Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap'); + +/* ============================================ + Light Mode (Default) + ============================================ */ +:root { + --md-code-bg-color: #2E3440; + --md-code-fg-color: #FFFFFF; + --md-text-font-family: "Roboto"; + --md-code-font: "Source Code Pro", monospace; + --hc-heading-color: #1a1a1a; + --hc-inline-code-bg: #ECEFF4; + --hc-inline-code-color: #1a1a1a; + --hc-code-shadow: #D8DEE9; + --hc-nav-active-bg: #ECEFF4; + --hc-link-color: #ed4e80; + --hc-link-hover-color: #2e78d8; +} + +/* ============================================ + Dark Mode (Slate) + ============================================ */ +[data-md-color-scheme="slate"] { + --md-default-bg-color: #1a1a1a; + --md-default-fg-color: #ffffff; + --md-default-fg-color--light: #d1d5db; + --md-default-fg-color--lighter: #9ca3af; + --md-default-fg-color--lightest: #4b5563; + --hc-heading-color: #ffffff; + --hc-inline-code-bg: #374151; + --hc-inline-code-color: #e5e7eb; + --hc-code-shadow: transparent; + --hc-nav-active-bg: #374151; + --hc-link-color: #f472b6; + --hc-link-hover-color: #60a5fa; +} + +/* ============================================ + General Styles + ============================================ */ +.index-pre-code { + max-width: 700px; + left: 50%; +} + +.index-pre-code pre>code { + text-align: left; +} + +.md-typeset pre>code { + border-radius: .2rem; + box-shadow: 10px 5px 5px #D8DEE9; + background-color: #2E3440; + color: #FFFFFF; +} + +[data-md-color-scheme="slate"] .md-typeset pre>code { + box-shadow: none; +} + +.md-typeset p > code, +.md-typeset strong > code, +.md-typeset td > code, +.md-typeset li > code { + background: var(--hc-inline-code-bg); + color: var(--hc-inline-code-color); + font-weight: 500; +} + +.md-content p > code { + background: var(--hc-inline-code-bg); + color: var(--hc-inline-code-color); + font-weight: 500; +} + +.md-typeset code { + font-weight: 500; +} + +.md-typeset pre { + margin-left: .5rem; + margin-right: .5rem; + margin-top: 2rem; + margin-bottom: 2rem; +} + +h1.title { + color: #FFFFFF; + margin: 0px 0px 5px; +} + +h2.subtitle { + margin: 5px 0px 25px; +} + +.md-typeset { + line-height: 24px; + font-weight: 400; +} + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3 { + font-weight: bold; + color: var(--hc-heading-color); +} + +.md-nav__link--active { + background-color: var(--hc-nav-active-bg); +} + +code { + white-space: pre-wrap !important; +} + +.md-content a { + color: var(--hc-link-color); + text-decoration: none; + transition: all 0.2s ease-in-out; +} + +.md-content a:hover { + color: var(--hc-link-hover-color); +} + +/* ============================================ + Dark Mode Specific Overrides + ============================================ */ + +/* Ensure dark background applies to all main areas */ +[data-md-color-scheme="slate"], +[data-md-color-scheme="slate"] body, +[data-md-color-scheme="slate"] .md-main, +[data-md-color-scheme="slate"] .md-main__inner, +[data-md-color-scheme="slate"] .md-content, +[data-md-color-scheme="slate"] .md-content__inner, +[data-md-color-scheme="slate"] .md-sidebar, +[data-md-color-scheme="slate"] .md-sidebar__scrollwrap, +[data-md-color-scheme="slate"] .md-container { + background-color: #1a1a1a; +} + +/* Footer dark mode */ +[data-md-color-scheme="slate"] .md-footer { + background-color: #111827; +} + +[data-md-color-scheme="slate"] .md-footer-meta { + background-color: #111827; +} + +[data-md-color-scheme="slate"] .md-typeset { + color: var(--md-default-fg-color--light); +} + +[data-md-color-scheme="slate"] .md-typeset p, +[data-md-color-scheme="slate"] .md-typeset li, +[data-md-color-scheme="slate"] .md-typeset td { + color: #d1d5db; +} + +/* Dark mode navigation */ +[data-md-color-scheme="slate"] .md-nav__link { + color: #d1d5db; +} + +[data-md-color-scheme="slate"] .md-nav__link:hover { + color: #ffffff; +} + +[data-md-color-scheme="slate"] .md-nav__link--active { + color: #ffffff; +} + +/* ============================================ + Top Navigation Bar (Tabs) - Kedro-inspired + ============================================ */ + +/* Header styling */ +.md-header { + padding-left: 24px; + border-radius: 0px 0px 12px 12px; +} + +.md-header--shadow { + box-shadow: none; +} + +/* Tab link styling - larger like Kedro */ +.md-tabs__link { + font-size: 16px; + opacity: 0.85; + transition: opacity 0.2s ease; +} + +.md-tabs__link:hover { + opacity: 1; +} + +/* Active tab - underline effect like Kedro */ +.md-tabs__item--active { + font-weight: 600; +} + +.md-tabs__item--active .md-tabs__link { + opacity: 1; +} + +/* Underline on active tab */ +.md-tabs__item { + border-bottom: 2px solid transparent; + transition: border-color 0.2s ease; +} + +.md-tabs__item:hover { + border-bottom-color: currentColor; +} + +.md-tabs__item--active { + border-bottom: 2px solid currentColor; +} + +/* ============================================ + Light Mode Color Accents + ============================================ */ + +/* Accent color for headings */ +.md-typeset h1 { + color: #1a1a1a; + border-bottom: 3px solid #e59875; + padding-bottom: 0.4rem; + display: inline-block; +} + +/* H2 section styling - left accent border for visual anchoring */ +.md-typeset h2 { + border-left: 3px solid #e59875; + padding-left: 0.75rem; + margin-top: 2.5rem; +} + +/* Colored left border for admonitions */ +.md-typeset .admonition { + border-left: 4px solid #79a8a9; +} + +.md-typeset .admonition.note { + border-left-color: #79a8a9; +} + +.md-typeset .admonition.tip { + border-left-color: #e59875; +} + +.md-typeset .admonition.warning { + border-left-color: #f59e0b; +} + +/* Accent color for blockquotes */ +.md-typeset blockquote { + border-left: 4px solid #e59875; + background-color: #fef7f4; + padding: 1rem 1.5rem; + margin: 1.5rem 0; +} + +/* Table header accent */ +.md-typeset table:not([class]) th { + background-color: #f8f4f2; + border-bottom: 2px solid #e59875; +} + +/* Sidebar accent - active item */ +.md-nav__link--active { + color: #e59875 !important; + font-weight: 600; +} + +.md-nav__item--active > .md-nav__link { + color: #e59875; +} + +/* Search bar styling */ +.md-search__input { + border-radius: 8px; +} + +.md-search__input::placeholder { + color: rgba(255, 255, 255, 0.7); +} + +/* Button/CTA accent colors */ +.md-typeset .md-button { + background-color: #e59875; + border-color: #e59875; + color: #ffffff; +} + +.md-typeset .md-button:hover { + background-color: #d4845f; + border-color: #d4845f; +} + +.md-typeset .md-button--primary { + background-color: #e59875; + border-color: #e59875; +} + +/* Code block header accent */ +.md-typeset .highlight > .filename { + background-color: #f8f4f2; + border-bottom: 2px solid #e59875; +} + +/* ============================================ + Dark Mode Color Accent Overrides + ============================================ */ +[data-md-color-scheme="slate"] .md-typeset h1 { + color: #ffffff; + border-bottom-color: #e59875; +} + +[data-md-color-scheme="slate"] .md-typeset h2 { + border-left-color: #e59875; +} + +[data-md-color-scheme="slate"] .md-typeset blockquote { + background-color: #1f2937; + border-left-color: #e59875; +} + +[data-md-color-scheme="slate"] .md-typeset table:not([class]) th { + background-color: #1f2937; + border-bottom-color: #e59875; +} + +[data-md-color-scheme="slate"] .md-nav__link--active { + color: #e59875 !important; +} + +[data-md-color-scheme="slate"] .md-typeset .highlight > .filename { + background-color: #1f2937; + border-bottom-color: #e59875; +} diff --git a/docs/stylesheets/welcome.css b/docs/stylesheets/welcome.css new file mode 100644 index 00000000..b80579ce --- /dev/null +++ b/docs/stylesheets/welcome.css @@ -0,0 +1,789 @@ +/* HealthChain Welcome Page Styles + Adapted from Kedro's documentation pattern */ + +/* CSS Variables for theming */ +:root { + --welcome-page-bg-color: #ffffff; + --cards-section-bg-color: #f8f9fa; + --card-bg-color: #ffffff; + --card-border-color: #e0e0e0; + --card-featured-border-color: #e59875; + --card-featured-bg-color: #fef7f4; + --text-primary-color: #1a1a1a; + --text-secondary-color: #666666; + --accent-color: #e59875; + --accent-secondary: #79a8a9; + /* Persona accent colors */ + --persona-healthtech: #e59875; + --persona-healthtech-bg: #fef7f4; + --persona-healthtech-border: #e59875; + --persona-genai: #5c9ead; + --persona-genai-bg: #f0f7f9; + --persona-genai-border: #5c9ead; + --persona-ml: #7c6fb0; + --persona-ml-bg: #f5f3fa; + --persona-ml-border: #7c6fb0; +} + +[data-md-color-scheme="slate"] { + --welcome-page-bg-color: #1a1a1a; + --cards-section-bg-color: #1f2937; + --card-bg-color: #2d2d2d; + --card-border-color: #404040; + --card-featured-border-color: #e59875; + --card-featured-bg-color: #2d2d2d; + --text-primary-color: #ffffff; + --text-secondary-color: #d1d5db; + /* Dark mode persona colors - neutral backgrounds with colored left borders */ + --persona-healthtech-bg: #2d2d2d; + --persona-genai-bg: #2d2d2d; + --persona-ml-bg: #2d2d2d; +} + +/* Target welcome page container and parent elements */ +.md-content__inner:has(.welcome-page-container) { + padding: 0 !important; + margin: 0 !important; + max-width: none !important; + background-color: var(--welcome-page-bg-color); +} + +/* Ensure full-width background on welcome page */ +.md-main:has(.welcome-page-container) { + background-color: var(--welcome-page-bg-color); +} + +.md-main__inner:has(.welcome-page-container) { + max-width: none; +} + +.md-content:has(.welcome-page-container) { + max-width: none; + background-color: var(--welcome-page-bg-color); +} + +/* Main Container */ +.welcome-page-container { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--welcome-page-bg-color); +} + +/* Hero Section */ +.welcome-page-container header { + text-align: center; + padding: 0 16px; +} + +.welcome-title { + margin-top: 80px !important; + margin-bottom: 20px !important; + font-weight: 700 !important; + font-size: 3.25rem !important; + color: var(--text-primary-color); + line-height: 1.15; + letter-spacing: -0.02em; +} + +.welcome-subtitle { + font-size: 1.25rem; + color: var(--text-secondary-color); + max-width: 640px; + margin: 0 auto 32px auto; + line-height: 1.6; +} + +/* Explore/CTA Link */ +.explore-link { + text-align: center; + margin: 40px 0 48px 0; +} + +.explore-link a { + display: inline-flex; + align-items: center; + padding: 16px 32px; + background-color: var(--accent-color); + color: #ffffff !important; + text-decoration: none !important; + border-radius: 10px; + font-weight: 600; + font-size: 1.1rem; + transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s; + box-shadow: 0 4px 14px -4px rgba(229, 152, 117, 0.5); +} + +.explore-link a:hover { + background-color: #d4845f; + transform: translateY(-2px); + box-shadow: 0 6px 20px -4px rgba(229, 152, 117, 0.6); +} + +.explore-link .arrow { + margin-left: 12px; + font-size: 1.25rem; + transition: transform 0.2s; +} + +.explore-link a:hover .arrow { + transform: translateX(3px); +} + +/* Cards Section Wrapper */ +.cards-section-wrapper { + background-color: var(--cards-section-bg-color); + width: 100%; + padding: 0 0 72px 0; +} + +/* First section (Choose Your Path) has distinct background */ +.cards-section-wrapper section:first-child { + background-color: var(--welcome-page-bg-color); + padding-top: 40px; + padding-bottom: 56px; + margin-bottom: 0; +} + +.cards-section-wrapper section { + margin-bottom: 64px; + padding: 56px 24px 0 24px; +} + +.cards-section-wrapper section:last-child { + margin-bottom: 0; +} + +/* Section Titles */ +.section-title { + font-weight: 600; + font-size: 1.35rem; + text-align: center; + margin-bottom: 36px !important; + margin-top: 0; + color: var(--text-primary-color); + letter-spacing: -0.01em; +} + +/* Card Grid */ +.card-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 28px; + margin: 0 auto; + max-width: 1000px; +} + +.card-grid.two-col { + grid-template-columns: repeat(2, 1fr); + max-width: 650px; +} + +/* Card Styles */ +.card { + background-color: var(--card-bg-color); + border: 2px solid var(--card-border-color); + border-radius: 16px; + padding: 28px; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + text-decoration: none !important; + display: flex; + flex-direction: column; + min-height: fit-content; +} + +.card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.15); +} + +/* Persona cards get larger padding */ +.card.card-healthtech, +.card.card-genai, +.card.card-ml { + padding: 36px; +} + +/* Persona Cards with accent colors */ +.card.card-healthtech { + border-color: var(--persona-healthtech-border); + background-color: var(--persona-healthtech-bg); + position: relative; +} + +.card.card-healthtech .card-icon svg path { + fill: var(--persona-healthtech); +} + +.card.card-genai { + border-color: var(--persona-genai-border); + background-color: var(--persona-genai-bg); +} + +.card.card-genai .card-icon svg path { + fill: var(--persona-genai); +} + +.card.card-ml { + border-color: var(--persona-ml-border); + background-color: var(--persona-ml-bg); +} + +.card.card-ml .card-icon svg path { + fill: var(--persona-ml); +} + +/* Featured Card (Primary Persona) */ +.card.card-featured { + border-color: var(--card-featured-border-color); + background-color: var(--card-featured-bg-color); + position: relative; +} + +.card.card-featured::before { + content: "Recommended"; + position: absolute; + top: -14px; + left: 24px; + background-color: var(--accent-color); + color: #ffffff; + font-size: 0.85rem; + font-weight: 600; + padding: 6px 16px; + border-radius: 14px; + box-shadow: 0 3px 10px -2px rgba(229, 152, 117, 0.5); + letter-spacing: 0.01em; +} + +/* Card Icon */ +.card-icon { + margin-bottom: 18px; + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--cards-section-bg-color); + border-radius: 14px; +} + +/* Persona cards have white icon backgrounds */ +.card.card-healthtech .card-icon, +.card.card-genai .card-icon, +.card.card-ml .card-icon { + background-color: rgba(255, 255, 255, 0.7); +} + +[data-md-color-scheme="slate"] .card.card-healthtech .card-icon, +[data-md-color-scheme="slate"] .card.card-genai .card-icon, +[data-md-color-scheme="slate"] .card.card-ml .card-icon { + background-color: rgba(0, 0, 0, 0.2); +} + +/* Dark mode: Remove full colored borders, use subtle left accent borders */ +[data-md-color-scheme="slate"] .card.card-healthtech, +[data-md-color-scheme="slate"] .card.card-genai, +[data-md-color-scheme="slate"] .card.card-ml { + border: 1px solid #404040; + border-left: 3px solid; +} + +[data-md-color-scheme="slate"] .card.card-healthtech { + border-left-color: #e59875; +} + +[data-md-color-scheme="slate"] .card.card-genai { + border-left-color: #5c9ead; +} + +[data-md-color-scheme="slate"] .card.card-ml { + border-left-color: #7c6fb0; +} + +/* Dark mode featured card styling */ +[data-md-color-scheme="slate"] .card.card-featured { + border: 1px solid #404040; + border-left: 3px solid #e59875; +} + +/* Dark mode text contrast - WCAG AAA compliance */ +[data-md-color-scheme="slate"] .card-title, +[data-md-color-scheme="slate"] .card.card-healthtech .card-title, +[data-md-color-scheme="slate"] .card.card-genai .card-title, +[data-md-color-scheme="slate"] .card.card-ml .card-title { + color: #ffffff; +} + +[data-md-color-scheme="slate"] .card-description, +[data-md-color-scheme="slate"] .card.card-healthtech .card-description, +[data-md-color-scheme="slate"] .card.card-genai .card-description, +[data-md-color-scheme="slate"] .card.card-ml .card-description { + color: #d1d5db; +} + +/* Dark mode hero text - ensure high contrast */ +[data-md-color-scheme="slate"] .welcome-title { + color: #ffffff; +} + +[data-md-color-scheme="slate"] .welcome-subtitle { + color: #e5e7eb; +} + +[data-md-color-scheme="slate"] .section-title { + color: #ffffff; +} + +.card-icon svg { + width: 32px; + height: 32px; +} + +.card-icon svg path { + fill: var(--accent-color); +} + +/* Card Content */ +.card-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.card-title { + font-size: 1.15rem !important; + font-weight: 600 !important; + color: var(--text-primary-color); + margin-bottom: 12px !important; + margin-top: 0 !important; + line-height: 1.3; +} + +.card-description { + font-size: 1rem; + color: var(--text-secondary-color); + line-height: 1.6; + margin: 0; + flex: 1; +} + +/* Ensure persona card descriptions are fully visible */ +.card.card-healthtech .card-description, +.card.card-genai .card-description, +.card.card-ml .card-description { + font-size: 0.975rem; + line-height: 1.55; +} + +.card-cta { + margin-top: 24px; + font-size: 0.95rem; + font-weight: 600; + color: var(--accent-color); + display: inline-flex; + align-items: center; + transition: color 0.2s ease; +} + +/* Persona-specific CTA colors */ +.card.card-healthtech .card-cta { color: var(--persona-healthtech); } +.card.card-genai .card-cta { color: var(--persona-genai); } +.card.card-ml .card-cta { color: var(--persona-ml); } + +.card-cta .arrow { + margin-left: 8px; + transition: transform 0.2s ease; +} + +.card:hover .card-cta .arrow { + transform: translateX(4px); +} + +.card:hover .card-cta { + opacity: 0.85; +} + +/* Welcome Page Footer */ +.welcome-page-footer { + background-color: var(--welcome-page-bg-color); + padding: 32px 16px; + text-align: center; +} + +.welcome-page-footer .badges { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} + +.welcome-page-footer .badges img { + height: 20px; +} + +/* Mobile Responsive */ +@media screen and (max-width: 76.2344em) { + .card-grid, + .card-grid.two-col { + grid-template-columns: 1fr; + gap: 24px; + max-width: 100%; + padding: 0 8px; + } + + .cards-section-wrapper { + padding: 0 0 48px 0; + } + + .cards-section-wrapper section:first-child { + padding-top: 40px; + padding-bottom: 40px; + } + + .cards-section-wrapper section { + padding: 40px 16px 0 16px; + margin-bottom: 0; + } + + .card { + padding: 24px; + } + + .card.card-healthtech, + .card.card-genai, + .card.card-ml { + padding: 28px; + } + + .card-icon { + width: 48px; + height: 48px; + } + + .card-icon svg { + width: 28px; + height: 28px; + } + + .welcome-title { + margin-top: 48px !important; + font-size: 2rem !important; + } + + .welcome-subtitle { + font-size: 1.05rem; + } + + .explore-link { + margin: 28px 0 48px 0; + } + + .explore-link a { + padding: 14px 28px; + font-size: 1rem; + } + + .section-title { + margin-top: 0; + margin-bottom: 24px !important; + } + + .card.card-featured::before { + top: -12px; + font-size: 0.75rem; + padding: 5px 12px; + } + + .card-description { + font-size: 0.95rem; + } + + .card-title { + font-size: 1.1rem !important; + } +} + +/* Tablet breakpoint */ +@media screen and (min-width: 48em) and (max-width: 76.2344em) { + .card-grid { + grid-template-columns: repeat(2, 1fr); + } + + .card-grid.two-col { + grid-template-columns: repeat(2, 1fr); + } +} + +/* ============================================ + Cookbook Cards and Tags + ============================================ */ + +/* Cookbook wrapper with grey background */ +.cookbook-wrapper { + background-color: var(--cards-section-bg-color); + margin: 0 -0.8rem 1rem -0.8rem; + padding: 32px 24px; + border-radius: 12px; +} + +/* Cookbook card grid - 2 columns for better readability */ +.cookbook-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + margin: 0; +} + +@media screen and (max-width: 76.2344em) { + .cookbook-grid { + grid-template-columns: 1fr; + } +} + +/* Cookbook card */ +.cookbook-card { + background-color: var(--md-default-bg-color); + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 12px; + padding: 24px; + transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; + text-decoration: none !important; + display: flex; + flex-direction: column; + height: 100%; +} + +.cookbook-card:hover { + transform: translateY(-3px); + box-shadow: 0 6px 16px -4px rgba(0, 0, 0, 0.1); + border-color: var(--md-accent-fg-color); +} + +.cookbook-card-icon { + font-size: 2rem; + margin-bottom: 12px; +} + +.cookbook-card-title { + font-size: 1.1rem !important; + font-weight: 600 !important; + color: var(--md-default-fg-color); + margin: 0 0 8px 0 !important; + line-height: 1.3; +} + +.cookbook-card-description { + font-size: 0.9rem; + color: var(--md-default-fg-color--light); + line-height: 1.5; + margin: 0 0 16px 0; + flex: 1; +} + +/* Tag container */ +.cookbook-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: auto; +} + +/* Base tag style */ +.tag { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +/* Persona tags */ +.tag-healthtech { + background-color: #fef3e7; + color: #c76a15; +} + +.tag-genai { + background-color: #e8f4f8; + color: #1976a2; +} + +.tag-ml { + background-color: #f3e8f8; + color: #7b1fa2; +} + +/* Feature tags */ +.tag-gateway { + background-color: #e8f5e9; + color: #2e7d32; +} + +.tag-pipeline { + background-color: #fff3e0; + color: #e65100; +} + +.tag-interop { + background-color: #e3f2fd; + color: #1565c0; +} + +.tag-sandbox { + background-color: #fce4ec; + color: #c2185b; +} + +.tag-fhir { + background-color: #f1f8e9; + color: #558b2f; +} + +.tag-cdshooks { + background-color: #fff8e1; + color: #ff8f00; +} + +/* Dark mode tag adjustments */ +[data-md-color-scheme="slate"] .tag-healthtech { + background-color: #3d2d1f; + color: #ffb74d; +} + +[data-md-color-scheme="slate"] .tag-genai { + background-color: #1a3a4a; + color: #4fc3f7; +} + +[data-md-color-scheme="slate"] .tag-ml { + background-color: #2d1f3d; + color: #ce93d8; +} + +[data-md-color-scheme="slate"] .tag-gateway { + background-color: #1b3d1f; + color: #81c784; +} + +[data-md-color-scheme="slate"] .tag-pipeline { + background-color: #3d2a1a; + color: #ffb74d; +} + +[data-md-color-scheme="slate"] .tag-interop { + background-color: #1a2d3d; + color: #64b5f6; +} + +[data-md-color-scheme="slate"] .tag-sandbox { + background-color: #3d1a2a; + color: #f48fb1; +} + +[data-md-color-scheme="slate"] .tag-fhir { + background-color: #2a3d1a; + color: #aed581; +} + +[data-md-color-scheme="slate"] .tag-cdshooks { + background-color: #3d3a1a; + color: #ffd54f; +} + +[data-md-color-scheme="slate"] .cookbook-card { + background-color: var(--md-default-bg-color); + border-color: var(--md-default-fg-color--lightest); +} + +/* Tag legend / filter bar */ +.tag-legend { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 24px; + padding: 16px 20px; + background-color: var(--md-default-bg-color); + border-radius: 8px; + border: 1px solid var(--md-default-fg-color--lightest); +} + +.tag-legend-title { + font-size: 0.8rem; + font-weight: 600; + color: var(--md-default-fg-color--light); + margin-right: 8px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Clickable filter tags */ +.tag-filter { + cursor: pointer; + transition: transform 0.15s, opacity 0.15s, box-shadow 0.15s; + user-select: none; + opacity: 0.6; +} + +.tag-filter:hover { + transform: scale(1.05); + opacity: 0.85; +} + +.tag-filter.active { + opacity: 1; + box-shadow: 0 0 0 2px var(--md-default-bg-color), 0 0 0 4px currentColor; +} + +/* Clear filter button */ +.tag-clear { + cursor: pointer; + padding: 3px 10px; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 600; + background-color: var(--md-default-fg-color--lightest); + color: var(--md-default-fg-color--light); + transition: background-color 0.15s; + margin-left: auto; +} + +.tag-clear:hover { + background-color: var(--md-default-fg-color--lighter); +} + +.tag-clear.hidden { + display: none; +} + +/* Card filtering states */ +.cookbook-card.filtered-out { + display: none; +} + +.cookbook-card.filtered-in { + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* No results message */ +.no-results { + grid-column: 1 / -1; + text-align: center; + padding: 48px 24px; + color: var(--md-default-fg-color--light); + font-size: 1rem; +} + +.no-results.hidden { + display: none; +} diff --git a/docs/tutorials/clinicalflow/fhir-basics.md b/docs/tutorials/clinicalflow/fhir-basics.md new file mode 100644 index 00000000..8d25c052 --- /dev/null +++ b/docs/tutorials/clinicalflow/fhir-basics.md @@ -0,0 +1,221 @@ +# FHIR Basics + +Understand the healthcare data format you'll work with in HealthChain. + +## What is FHIR? + +**FHIR** (Fast Healthcare Interoperability Resources) is the modern standard for exchanging healthcare data. Think of it as "JSON for healthcare" - structured, standardized data that EHR systems like Epic and Cerner use. + +## Key Resources for CDS + +For our ClinicalFlow service, we'll work with three main FHIR resources: + +### Patient + +Identifies who the patient is: + +```json +{ + "resourceType": "Patient", + "id": "example-patient", + "name": [{"given": ["John"], "family": "Smith"}], + "birthDate": "1970-01-15", + "gender": "male" +} +``` + +### Condition + +Records diagnoses and health problems: + +```json +{ + "resourceType": "Condition", + "id": "example-condition", + "code": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "38341003", + "display": "Hypertension" + }] + }, + "subject": {"reference": "Patient/example-patient"}, + "clinicalStatus": { + "coding": [{"code": "active"}] + } +} +``` + +### MedicationStatement + +Tracks what medications a patient is taking: + +```json +{ + "resourceType": "MedicationStatement", + "id": "example-med", + "medicationCodeableConcept": { + "coding": [{ + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "197361", + "display": "Lisinopril 10 MG" + }] + }, + "subject": {"reference": "Patient/example-patient"}, + "status": "active" +} +``` + +## Working with FHIR in HealthChain + +HealthChain provides utilities to work with FHIR resources easily: + +```python +from healthchain.fhir import create_condition, create_patient +from fhir.resources.patient import Patient + +# Create a patient with basic demographics +# Note: create_patient generates an auto-prefixed ID (e.g., "hc-abc123") +patient = create_patient( + gender="male", + birth_date="1970-01-15", + identifier="MRN-12345" # Optional patient identifier (e.g., MRN) +) + +# Create a condition linked to the patient +# The 'subject' parameter is required and references the patient +condition = create_condition( + subject=f"Patient/{patient.id}", # Reference to the patient + code="38341003", + display="Hypertension", + system="http://snomed.info/sct" +) + +print(f"Created patient with ID: {patient.id}") +print(f"With condition: {condition.code.coding[0].display}") +``` + +??? info "FHIR Versions in HealthChain" + + HealthChain uses **FHIR R5** as the default version. However, **STU3** and **R4B** are also supported for compatibility with different EHR systems. + + You can specify the version when working with FHIR resources, and HealthChain provides utilities for converting between versions when needed. + + For more details on version handling, see the [FHIR utilities reference](../../reference/utilities/fhir_helpers.md). + +## FHIR Bundles + +When an EHR sends patient context, it often comes as a **Bundle** - a collection of related resources: + +```python +from fhir.resources.bundle import Bundle + +# A bundle might contain a patient, their conditions, and medications +bundle_data = { + "resourceType": "Bundle", + "type": "collection", + "entry": [ + {"resource": patient.model_dump()}, + {"resource": condition.model_dump()} + ] +} + +bundle = Bundle(**bundle_data) +print(f"Bundle contains {len(bundle.entry)} resources") +``` + +## The Document Container + +HealthChain's `Document` container bridges clinical text and FHIR data: + +```python +from healthchain.io import Document + +# Create a document with clinical text +doc = Document( + "Patient presents with chest pain and shortness of breath. " + "History of hypertension and diabetes." +) + +# The document can hold FHIR data extracted from text +print(f"Document text: {doc.text[:50]}...") + +# After NLP processing, the document will contain: +# - Extracted entities +# - Generated FHIR resources +# - Problem lists, medications, etc. +``` + +## How FHIR Flows into CDS Hooks + +When an EHR like Epic calls your CDS service, it sends a **CDS Hooks request** containing patient data. The FHIR resources you just learned about arrive in the `prefetch` field: + +```json +{ + "hookInstance": "abc-123-def-456", + "hook": "patient-view", + "context": { + "userId": "Practitioner/dr-smith", + "patientId": "patient-001" + }, + "prefetch": { + "patient": { + "resourceType": "Patient", + "id": "patient-001", + "name": [{"given": ["John"], "family": "Smith"}], + "birthDate": "1970-01-15", + "gender": "male" + }, + "conditions": { + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + { + "resource": { + "resourceType": "Condition", + "id": "condition-hypertension", + "code": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "38341003", + "display": "Hypertension" + }] + }, + "subject": {"reference": "Patient/patient-001"}, + "clinicalStatus": {"coding": [{"code": "active"}]} + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "condition-diabetes", + "code": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "73211009", + "display": "Diabetes mellitus" + }] + }, + "subject": {"reference": "Patient/patient-001"}, + "clinicalStatus": {"coding": [{"code": "active"}]} + } + } + ] + }, + "note": "Patient is a 65-year-old male presenting with chest pain and shortness of breath. History includes hypertension and diabetes, both well-controlled on current medications." + } +} +``` + +This is the complete picture: + +1. **`context`** - Tells you who triggered the hook (the practitioner) and which patient +2. **`prefetch.patient`** - The Patient resource with demographics +3. **`prefetch.conditions`** - A Bundle of the patient's active conditions +4. **`prefetch.note`** - Clinical text your NLP pipeline will process + +Your CDS service receives this request, processes the data, and returns clinical alert cards. We'll use this same sample data throughout the tutorial for testing. + +## What's Next + +Now that you understand FHIR basics and how data flows into CDS, let's [build a pipeline](pipeline.md) that processes clinical text and extracts structured data. diff --git a/docs/tutorials/clinicalflow/gateway.md b/docs/tutorials/clinicalflow/gateway.md new file mode 100644 index 00000000..80648615 --- /dev/null +++ b/docs/tutorials/clinicalflow/gateway.md @@ -0,0 +1,197 @@ +# Create Gateway + +Expose your pipeline as a CDS Hooks service that EHRs can call. + +## What is CDS Hooks? + +**CDS Hooks** is a standard for integrating clinical decision support with EHR systems. When a clinician performs an action (like opening a patient chart), the EHR calls your CDS service, and you return helpful information as "cards." + +The flow: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Clinician โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ EHR โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ Your CDS โ”‚ +โ”‚ opens โ”‚ โ”‚ (Epic, โ”‚ HTTP โ”‚ Service โ”‚ +โ”‚ chart โ”‚ โ”‚ Cerner) โ”‚ POST โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ + โ”‚ CDS Cards โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Display โ”‚ + โ”‚ alerts to โ”‚ + โ”‚ clinician โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Create the CDS Service + +Create a file called `app.py`. This imports the pipeline you created in the [previous step](pipeline.md): + +```python +from healthchain.gateway import HealthChainAPI, CDSHooksService +from healthchain.io import Document +from healthchain.models.requests.cdsrequest import CDSRequest +from healthchain.models.responses.cdsresponse import CDSResponse, Card +from pipeline import create_clinical_pipeline + +# Initialize the HealthChain API +app = HealthChainAPI(title="ClinicalFlow CDS Service") + +# Create your pipeline +nlp = create_clinical_pipeline() + +# Create a CDS Hooks service +cds_service = CDSHooksService() + +# Register a hook handler using the decorator +@cds_service.hook( + "patient-view", # Hook type: triggers when a clinician views a patient + id="patient-alerts", + title="Clinical Alert Service", + description="Analyzes patient data and returns relevant clinical alerts", +) +def patient_alerts(request: CDSRequest) -> CDSResponse: + """ + Process patient context and return CDS cards. + + Args: + request: CDSRequest containing context and prefetch data + """ + cards = [] + + # Get patient conditions from prefetch (if available) + prefetch = request.prefetch or {} + conditions = prefetch.get("conditions", []) + + # If we have clinical notes, process them + if clinical_note := prefetch.get("note"): + doc = Document(clinical_note) + result = nlp(doc) + + # Create cards for each extracted condition + for entity in result.nlp.get_entities(): + cards.append(Card( + summary=f"Condition detected: {entity['display']}", + detail=f"SNOMED code: {entity['code']}", + indicator="info", + source={"label": "ClinicalFlow", "url": "https://healthchain.dev"} + )) + + # Check for drug interaction alerts + if len(conditions) > 2: + cards.append(Card( + summary="Multiple active conditions", + detail=f"Patient has {len(conditions)} active conditions. Review for potential interactions.", + indicator="warning", + source={"label": "ClinicalFlow"} + )) + + return CDSResponse(cards=cards) + + +# Register the CDS service with the app +app.include_router(cds_service) + +# Run the server +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +## Understanding the Code + +### The `@cds_service.hook` Decorator + +This decorator registers your function as a CDS Hooks handler: + +- **First argument**: The hook type (e.g., `patient-view`, `order-select`) +- **`id`**: Unique identifier for this service endpoint +- **`title`**: Human-readable name +- **`description`**: What the service does + +### CDS Cards + +Cards are the responses you return to the EHR. Each card has: + +| Field | Description | +|-------|-------------| +| `summary` | Brief message shown to clinician | +| `detail` | Additional information (optional) | +| `indicator` | Urgency: `info`, `warning`, or `critical` | +| `source` | Attribution for the recommendation | + +## Run the Service + +Start your CDS service: + +=== "uv" + + ```bash + uv run python app.py + ``` + +=== "pip" + + ```bash + python app.py + ``` + +Your service is now running at `http://localhost:8000`. + +## Test the Endpoints + +### Discovery Endpoint + +CDS Hooks services must provide a discovery endpoint. Test it: + +```bash +curl http://localhost:8000/cds/cds-discovery +``` + +Response: + +```json +{ + "services": [ + { + "id": "patient-alerts", + "title": "Clinical Alert Service", + "description": "Analyzes patient data and returns relevant clinical alerts", + "hook": "patient-view" + } + ] +} +``` + +### Service Endpoint + +Test calling your service: + +```bash +curl -X POST http://localhost:8000/cds/cds-services/patient-alerts \ + -H "Content-Type: application/json" \ + -d '{ + "hookInstance": "test-123", + "hook": "patient-view", + "context": { + "patientId": "patient-001", + "userId": "doctor-001" + }, + "prefetch": { + "note": "Patient presents with chest pain and hypertension." + } + }' +``` + +## Interactive API Docs + +HealthChain generates OpenAPI documentation. Visit: + +- **Swagger UI**: `http://localhost:8000/docs` +- **ReDoc**: `http://localhost:8000/redoc` + +## What's Next + +Your CDS service is running! Now let's [test it properly](testing.md) with realistic patient data using HealthChain's sandbox. diff --git a/docs/tutorials/clinicalflow/index.md b/docs/tutorials/clinicalflow/index.md new file mode 100644 index 00000000..2c8dfc7d --- /dev/null +++ b/docs/tutorials/clinicalflow/index.md @@ -0,0 +1,63 @@ +# ClinicalFlow Tutorial + +Build your first Clinical Decision Support (CDS) service with HealthChain. + +## The Scenario + +You're a HealthTech engineer at a hospital. The clinical informatics team needs a service that: + +1. **Receives patient context** when a physician opens a chart +2. **Analyzes existing conditions and medications** +3. **Returns actionable alerts** for potential drug interactions or care gaps + +Doing this in practice has a number of pain points: + +- **Complex protocol requirements** - CDS Hooks and FHIR have strict specifications that take weeks to implement correctly +- **Fragmented EHR data** - Patient information comes in different formats across systems, requiring custom parsing logic +- **No easy testing path** - Validating your service against realistic clinical scenarios typically requires access to live EHR systems +- **Integration boilerplate** - Writing the HTTP endpoints, request validation, and response formatting is repetitive but error-prone + +HealthChain handles all of this for you, so you can focus on the clinical logic that matters. + +By the end of this tutorial, you'll have a working CDS Hooks service that integrates with EHR systems like Epic. + +## What You'll Build + +We'll build a Pipeline that processes clinical text from an EHR and a Service that wraps around it to return Clinical Alert Cards. + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ EHR System โ”‚โ”€โ”€โ”€โ”€โ”€>โ”‚ Your CDS โ”‚โ”€โ”€โ”€โ”€โ”€>โ”‚ Clinical โ”‚ +โ”‚ (Epic, etc.) โ”‚ โ”‚ Service โ”‚ โ”‚ Alert Cards โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”‚ โ”‚ + โ–ผ โ–ผ + Patient context NLP Pipeline + (FHIR resources) (HealthChain) +``` + +Throughout this tutorial, we'll use the same sample patient - **John Smith** with hypertension and diabetes - so you can see how data flows from FHIR resources through your pipeline to clinical alerts. + +## What You'll Learn + +| Step | What You'll Learn | +|------|-------------------| +| [Setup](setup.md) | Install dependencies, create project structure | +| [FHIR Basics](fhir-basics.md) | Understand FHIR resources and how they flow into CDS Hooks requests | +| [Build Pipeline](pipeline.md) | Create an NLP pipeline that extracts conditions from clinical text | +| [Create Gateway](gateway.md) | Expose your pipeline as a CDS Hooks service that EHRs can call | +| [Test with Sandbox](testing.md) | Validate with sample patient data (simulating what Epic would send) | +| [Next Steps](next-steps.md) | Production deployment and extending your service | + +## Prerequisites + +- **Python 3.10+** installed +- **Basic Python knowledge** (functions, classes, imports) +- **REST API familiarity** (HTTP methods, JSON) +- Healthcare knowledge is helpful but not required + + +## Ready? + +Let's start by [setting up your project](setup.md). diff --git a/docs/tutorials/clinicalflow/next-steps.md b/docs/tutorials/clinicalflow/next-steps.md new file mode 100644 index 00000000..3e71164e --- /dev/null +++ b/docs/tutorials/clinicalflow/next-steps.md @@ -0,0 +1,42 @@ +# Next Steps + +You've built a working CDS service. Here's how to take it further. + +## What You've Accomplished + +In this tutorial, you: + +- Set up a HealthChain development environment +- Learned FHIR basics (Patient, Condition, MedicationStatement) +- Built an NLP pipeline with Document containers +- Created a CDS Hooks gateway service +- Tested with the sandbox and synthetic data + + +## What to Do Next + +### Improve the NLP + +The NLP was hard coded in our example but HealthChain has simple pipeline integrations for spacy, huggingface and Langchain. Try to replace keyword matching with trained models. + + +### Add FHIR Output + +Convert extracted entities to FHIR resources: + +```python +from healthchain.pipeline.components import FHIRProblemListExtractor + +pipeline.add_node(FHIRProblemListExtractor()) + +# Now doc.fhir.problem_list contains FHIR Condition resources +``` + +## Learn More + +Explore HealthChain's [cookbook](../../../cookbook/index.md) documentation, we have a variety of cookbooks that will let you build upon the basics from this tutorial. + + +## Congratulations! + +You've completed the ClinicalFlow tutorial. You now have the foundation to build production-ready healthcare AI applications with HealthChain. diff --git a/docs/tutorials/clinicalflow/pipeline.md b/docs/tutorials/clinicalflow/pipeline.md new file mode 100644 index 00000000..2d7b737b --- /dev/null +++ b/docs/tutorials/clinicalflow/pipeline.md @@ -0,0 +1,138 @@ +# Build Pipeline + +Create an NLP pipeline to process clinical text and extract structured data. + +## What is a Pipeline? + +A **Pipeline** in HealthChain is a sequence of processing steps that transform input data. For clinical NLP, pipelines typically: + +1. Take clinical text as input +2. Process it through NLP models +3. Extract entities (conditions, medications, etc.) +4. Output structured FHIR resources + +## Create Your Pipeline + +Create a file called `pipeline.py`: + +```python +from healthchain.pipeline import Pipeline +from healthchain.io import Document + +def create_clinical_pipeline(): + """Create a pipeline for processing clinical notes.""" + + # Initialize pipeline with Document as the data container + pipeline = Pipeline[Document]() + + # Add a simple text preprocessing component + @pipeline.add_node + def preprocess(doc: Document) -> Document: + """Clean and normalize clinical text.""" + # Remove extra whitespace + doc.text = " ".join(doc.text.split()) + return doc + + # Add a clinical entity extraction component + @pipeline.add_node + def extract_conditions(doc: Document) -> Document: + """Extract condition mentions from text.""" + # Simple keyword-based extraction for this tutorial + # In production, you'd use a trained NLP model + condition_keywords = { + "hypertension": ("38341003", "Hypertension"), + "diabetes": ("73211009", "Diabetes mellitus"), + "chest pain": ("29857009", "Chest pain"), + "shortness of breath": ("267036007", "Dyspnea"), + } + + text_lower = doc.text.lower() + extracted = [] + + for keyword, (code, display) in condition_keywords.items(): + if keyword in text_lower: + extracted.append({ + "text": keyword, + "code": code, + "display": display, + "system": "http://snomed.info/sct" + }) + + # Store extracted conditions in document's NLP annotations + doc.nlp.set_entities(extracted) + return doc + + return pipeline.build() +``` + +## Using the Pipeline + +Create a file called `test_pipeline.py` to test your pipeline: + +```python +from pipeline import create_clinical_pipeline +from healthchain.io import Document + +# Create the pipeline +nlp = create_clinical_pipeline() + +# Process a clinical note +doc = Document( + "Patient is a 65-year-old male presenting with chest pain " + "and shortness of breath. History includes hypertension " + "and diabetes, both well-controlled on current medications." +) + +# Run the pipeline +result = nlp(doc) + +# Check extracted entities +print("Extracted conditions:") +for entity in result.nlp.get_entities(): + print(f" - {entity['display']} (SNOMED: {entity['code']})") +``` + +Run it: + +=== "uv" + + ```bash + uv run python test_pipeline.py + ``` + +=== "pip" + + ```bash + python test_pipeline.py + ``` + +Expected output: + +``` +Extracted conditions: + - Hypertension (SNOMED: 38341003) + - Diabetes mellitus (SNOMED: 73211009) + - Chest pain (SNOMED: 29857009) + - Dyspnea (SNOMED: 267036007) +``` + +## Pipeline Architecture + +Your pipeline now follows this flow: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Clinical โ”‚โ”€โ”€โ”€โ”€>โ”‚ Preprocess โ”‚โ”€โ”€โ”€โ”€>โ”‚ Extract โ”‚ +โ”‚ Text โ”‚ โ”‚ Text โ”‚ โ”‚ Conditions โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Document โ”‚ + โ”‚ + Entities โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## What's Next + +Now that you have a working pipeline, let's [create a Gateway](gateway.md) to expose it as a CDS Hooks service. Don't worry if these terms don't make sense, all will be explained on the next page! diff --git a/docs/tutorials/clinicalflow/setup.md b/docs/tutorials/clinicalflow/setup.md new file mode 100644 index 00000000..c6fea2f6 --- /dev/null +++ b/docs/tutorials/clinicalflow/setup.md @@ -0,0 +1,74 @@ +# Setup + +Get your development environment ready for building the ClinicalFlow service. + +## Install HealthChain + +Create a new project directory: + +```bash +mkdir clinicalflow +cd clinicalflow +``` + +### Option 1: Using uv (Recommended) + +[uv](https://docs.astral.sh/uv/) is a fast Python package manager. If you don't have it installed, you can install it by following the instructions [here](https://docs.astral.sh/uv/getting-started/installation/). + +Then initialize a project and install HealthChain: + +```bash +uv init +uv add healthchain +``` + + +### Option 2: Using pip with a virtual environment + +If you prefer using pip, create and activate a virtual environment first: + +```bash +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +pip install healthchain +``` + +All the code running examples in this tutorial will show both the uv and pip versions of the commands. These are typically the same up to a factor of adding `uv run` at the beginning of the command. + + +## Verify Installation + +Create a file called `check_install.py`: + +```python +import healthchain +from healthchain.io import Document + +# Test creating a simple document +doc = Document("Patient has a history of hypertension.") +print(f"Created document with {len(doc.text)} characters") +``` + +Run it: + +=== "uv" + + ```bash + uv run python check_install.py + ``` + +=== "pip" + + ```bash + python check_install.py + ``` + +You should see the following output: + +``` +Created document with 38 characters +``` + +## What's Next + +Now that your environment is set up, let's learn about [FHIR basics](fhir-basics.md) - the healthcare data format you'll be working with. diff --git a/docs/tutorials/clinicalflow/testing.md b/docs/tutorials/clinicalflow/testing.md new file mode 100644 index 00000000..1f24d4e2 --- /dev/null +++ b/docs/tutorials/clinicalflow/testing.md @@ -0,0 +1,230 @@ +# Test with Sandbox + +Validate your CDS service with realistic patient data using HealthChain's sandbox. + +## What is the Sandbox? + +The **Sandbox** simulates what an EHR like Epic does - it sends CDS Hooks requests to your service with patient data. Instead of connecting to a real EHR (which requires authentication, network access, and real patients), the sandbox lets you: + +- Load test data from files or generate synthetic patients +- Send CDS Hooks requests to your service (just like Epic would) +- Collect and analyze the responses + +Think of it as a "fake EHR" for development and testing. + +## Prepare Test Data + +First, create sample data that matches what an EHR would send. This is the same CDS Hooks request format we saw in [FHIR Basics](fhir-basics.md#how-fhir-flows-into-cds-hooks). + +Create a `data` directory: + +```bash +mkdir data +``` + +Create `data/test_request.json` - this is a complete CDS Hooks request: + +```json +{ + "hookInstance": "test-instance-001", + "hook": "patient-view", + "context": { + "userId": "Practitioner/dr-smith", + "patientId": "patient-001" + }, + "prefetch": { + "patient": { + "resourceType": "Patient", + "id": "patient-001", + "name": [{"given": ["John"], "family": "Smith"}], + "birthDate": "1970-01-15", + "gender": "male" + }, + "conditions": { + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + { + "resource": { + "resourceType": "Condition", + "id": "condition-hypertension", + "code": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "38341003", + "display": "Hypertension" + }] + }, + "subject": {"reference": "Patient/patient-001"}, + "clinicalStatus": {"coding": [{"code": "active"}]} + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "condition-diabetes", + "code": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "73211009", + "display": "Diabetes mellitus" + }] + }, + "subject": {"reference": "Patient/patient-001"}, + "clinicalStatus": {"coding": [{"code": "active"}]} + } + } + ] + }, + "note": "Patient is a 65-year-old male presenting with chest pain and shortness of breath. History includes hypertension and diabetes, both well-controlled on current medications." + } +} +``` + +This sample includes: + +- **Patient demographics** - John Smith, born 1970 +- **Two conditions** - Hypertension and Diabetes (matching what our pipeline recognizes) +- **Clinical note** - Free text that our NLP pipeline will process + +## Quick Test with curl + +The fastest way to test is with the sample data directly. With your service running, send the request: + +```bash +curl -X POST http://localhost:8000/cds/cds-services/patient-alerts \ + -H "Content-Type: application/json" \ + -d @data/test_request.json +``` + +You should see a response with cards for the detected conditions. + +## Create a Test Script + +For more control, create `test_service.py`: + +```python +import json +import requests + +# Load the test request +with open("./data/test_request.json") as f: + request_data = json.load(f) + +# Send to your CDS service +response = requests.post( + "http://localhost:8000/cds/cds-services/patient-alerts", + json=request_data +) + +# Analyze the response +result = response.json() +cards = result.get("cards", []) + +print(f"Cards returned: {len(cards)}") +for card in cards: + indicator = card.get("indicator", "info").upper() + summary = card.get("summary") + print(f" [{indicator}] {summary}") +``` + +## Run the Test + +Make sure your service is running in one terminal: + +=== "uv" + + ```bash + uv run python app.py + ``` + +=== "pip" + + ```bash + python app.py + ``` + +Then in another terminal, run the test: + +=== "uv" + + ```bash + uv run python test_service.py + ``` + +=== "pip" + + ```bash + python test_service.py + ``` + +Expected output: + +``` +Cards returned: 4 + [INFO] Condition detected: Hypertension + [INFO] Condition detected: Diabetes mellitus + [INFO] Condition detected: Chest pain + [INFO] Condition detected: Dyspnea + [WARNING] Multiple active conditions +``` + +The pipeline extracted four conditions from the clinical note text - exactly what we built it to do. + +## Advanced Usage +### Batch Testing with SandboxClient + +For testing with multiple patients or larger datasets, use the `SandboxClient`: + +```python +from healthchain.sandbox import SandboxClient + +# Create a client pointing to your service +client = SandboxClient( + url="http://localhost:8000/cds/cds-services/patient-alerts", + workflow="patient-view" +) + +# Load FHIR bundles - SandboxClient converts them to CDS requests +client.load_from_path("./data/bundles/", pattern="*.json") + +# Send all requests and collect responses +responses = client.send_requests() + +# Analyze results +print(f"Tested {len(responses)} patients") +for i, response in enumerate(responses): + cards = response.get("cards", []) + print(f" Patient {i + 1}: {len(cards)} cards") +``` + +### Save Test Results + +Save results for reporting or debugging: + +```python +# Save responses to files +client.save_results( + directory="./output/", + save_request=True, + save_response=True +) +``` + +### Preview Requests Before Sending + +Inspect what will be sent without actually calling the service: + +```python +# Preview queued requests +previews = client.preview_requests(limit=5) +for preview in previews: + print(f"Request {preview['index']}: {preview['hook']}") + +# Get full request data +request_data = client.get_request_data(format="dict") +``` + +## What's Next + +Your service is tested and working! Learn about [production deployment](next-steps.md) and extending your CDS service. diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 00000000..fc2ee897 --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,19 @@ +# Tutorials + +Learn HealthChain through hands-on, step-by-step tutorials. Each tutorial is designed to teach core concepts while building something practical. + +## Available Tutorials + +### ClinicalFlow Tutorial + +**Build a Clinical Decision Support Service** + +The ClinicalFlow tutorial teaches you how to build a CDS service that integrates with EHR systems. You'll learn HealthChain's core concepts by building a real application. Healthcare knowledge is helpful but not required - the tutorials explain all the concepts as you go. + + +- **Level**: Beginner to Intermediate +- **Prerequisites**: Python basics, familiarity with REST APIs + +[:octicons-arrow-right-24: Start the ClinicalFlow Tutorial](clinicalflow/index.md) + +--- diff --git a/mkdocs.yml b/mkdocs.yml index 7737e7c2..e84e4350 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,16 @@ nav: - Installation: installation.md - Quickstart: quickstart.md - Licence: distribution.md + - Tutorials: + - tutorials/index.md + - ClinicalFlow Tutorial: + - Introduction: tutorials/clinicalflow/index.md + - Setup: tutorials/clinicalflow/setup.md + - FHIR Basics: tutorials/clinicalflow/fhir-basics.md + - Build Pipeline: tutorials/clinicalflow/pipeline.md + - Create Gateway: tutorials/clinicalflow/gateway.md + - Test with Sandbox: tutorials/clinicalflow/testing.md + - Next Steps: tutorials/clinicalflow/next-steps.md - Cookbook: - cookbook/index.md - Setup FHIR Sandbox: cookbook/setup_fhir_sandboxes.md @@ -19,6 +29,7 @@ nav: - Automated Clinical Coding: cookbook/clinical_coding.md - Discharge Summarizer: cookbook/discharge_summarizer.md - ML Model Deployment: cookbook/ml_model_deployment.md + - Format Conversion: cookbook/format_conversion.md - Docs: - Welcome: reference/index.md - Gateway: @@ -82,6 +93,7 @@ nav: copyright: dotimplement theme: name: material + custom_dir: docs/overrides/ favicon: assets/images/healthchain_logo.png logo: assets/images/healthchain_logo.png icon: @@ -90,12 +102,27 @@ theme: - content.code.copy - navigation.expand - navigation.tabs + - navigation.tabs.sticky - navigation.sections + - navigation.footer + - navigation.instant - header.autohide - announce.dismiss + - search.suggest + - search.highlight palette: - primary: white - accent: blue + - scheme: default + primary: white + accent: blue + toggle: + icon: material/white-balance-sunny + name: Switch to dark mode + - scheme: slate + primary: custom + accent: blue + toggle: + icon: material/weather-night + name: Switch to light mode # font: # text: Roboto @@ -132,6 +159,10 @@ markdown_extensions: extra_css: - stylesheets/extra.css + - stylesheets/welcome.css + +extra_javascript: + - javascripts/cookbook-filter.js plugins: - blog