diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 165624b6..fe5a84e8 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -1,31 +1,164 @@ -# 🍳 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 +
+
+ + +
🧾
+
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 +
+
+ + +
πŸ“
+
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 +
+
+ + + +
+
+ + --- -!!! 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/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..0c3f522f --- /dev/null +++ b/docs/overrides/welcome.html @@ -0,0 +1,173 @@ + + +{% extends "main.html" %} +{% block tabs %} +{{ super() }} +{% endblock %} + +{% block content %} +
+
+

Production-Ready Healthcare AI

+

+ Built-in FHIR support, real-time EHR connectivity, and deployment tooling for healthcare AI/ML systems. Skip months of custom integration work. +

+ + +
+ +
+ +
+

Choose Your Path

+ + +
+ + +
+

Get Started

+ + +
+ + +
+

Join the Community

+ + +
+
+
+{% endblock %} + +{% block footer %} + +{% endblock %} diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md new file mode 100644 index 00000000..6e1cf9e4 --- /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-loaders) 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/stylesheets/extra.css b/docs/stylesheets/extra.css index 405a0d4f..c26ff5ba 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,125 +1,371 @@ @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 */ - } + 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); +} + +/* ============================================ + 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 Custom"; + --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"] { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] body { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-main { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-main__inner { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-content { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-content__inner { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-sidebar { + background-color: #1a1a1a; +} + +[data-md-color-scheme="slate"] .md-sidebar__scrollwrap { + background-color: #1a1a1a; +} + +[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..30f93446 --- /dev/null +++ b/docs/tutorials/clinicalflow/fhir-basics.md @@ -0,0 +1,143 @@ +# 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.bundle import Bundle + +# Create a patient +patient = create_patient( + id="patient-001", + given_name="John", + family_name="Smith", + birth_date="1970-01-15" +) + +# Create a condition +condition = create_condition( + id="condition-001", + code="38341003", + display="Hypertension", + system="http://snomed.info/sct", + patient_reference="Patient/patient-001" +) + +print(f"Created patient: {patient.name[0].given[0]} {patient.name[0].family}") +print(f"With condition: {condition.code.coding[0].display}") +``` + +## 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.dict()}, + {"resource": condition.dict()} + ] +} + +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. +``` + +## What's Next + +Now that you understand FHIR basics, 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..1469ff8f --- /dev/null +++ b/docs/tutorials/clinicalflow/gateway.md @@ -0,0 +1,183 @@ +# 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`: + +```python +from healthchain.gateway import HealthChainAPI, CDSHooksService +from healthchain.io import Document +from pipeline import create_clinical_pipeline + +# Initialize the HealthChain API +app = HealthChainAPI(title="ClinicalFlow CDS Service") + +# Create your pipeline +nlp = create_clinical_pipeline() + +# Define the CDS Hooks service +@app.cds_hooks( + id="patient-alerts", + title="Clinical Alert Service", + description="Analyzes patient data and returns relevant clinical alerts", + hook="patient-view" # Triggers when a clinician views a patient +) +def patient_alerts(context, prefetch): + """ + Process patient context and return CDS cards. + + Args: + context: CDS Hooks context (patient ID, user, etc.) + prefetch: Pre-fetched FHIR resources + """ + cards = [] + + # Get patient conditions from prefetch (if available) + 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.entities: + cards.append({ + "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({ + "summary": "Multiple active conditions", + "detail": f"Patient has {len(conditions)} active conditions. Review for potential interactions.", + "indicator": "warning", + "source": {"label": "ClinicalFlow"} + }) + + return cards + + +# Run the server +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +## Understanding the Code + +### The `@app.cds_hooks` Decorator + +This decorator registers your function as a CDS Hooks endpoint: + +- **`id`**: Unique identifier for this service +- **`title`**: Human-readable name +- **`hook`**: When to trigger (e.g., `patient-view`, `order-select`) + +### 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: + +```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-services +``` + +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-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..e4a3da54 --- /dev/null +++ b/docs/tutorials/clinicalflow/index.md @@ -0,0 +1,53 @@ +# ClinicalFlow Tutorial + +Build your first Clinical Decision Support (CDS) service with HealthChain. + +## The Scenario + +You're a HealthTech engineer at a hospital system. The clinical informatics team needs a CDS 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 + +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 + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ EHR System │─────>β”‚ Your CDS │─────>β”‚ Clinical β”‚ +β”‚ (Epic, etc.) β”‚ β”‚ Service β”‚ β”‚ Alert Cards β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚ β”‚ + β–Ό β–Ό + Patient context NLP Pipeline + (FHIR resources) (HealthChain) +``` + +## What You'll Learn + +| Step | What You'll Learn | +|------|-------------------| +| [Setup](setup.md) | Install dependencies, create project structure | +| [FHIR Basics](fhir-basics.md) | Understand Patient, Condition, and Medication resources | +| [Build Pipeline](pipeline.md) | Create an NLP pipeline with Document containers | +| [Create Gateway](gateway.md) | Expose your pipeline as a CDS Hooks service | +| [Test with Sandbox](testing.md) | Validate with synthetic patient data | +| [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 + +## Time Required + +This tutorial takes approximately **45 minutes** to complete. + +## 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..53616e05 --- /dev/null +++ b/docs/tutorials/clinicalflow/next-steps.md @@ -0,0 +1,156 @@ +# 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 + +## Production Considerations + +### Authentication + +Real EHR integrations require OAuth2 authentication: + +```python +from healthchain.gateway import HealthChainAPI + +app = HealthChainAPI( + title="ClinicalFlow CDS Service", + auth_config={ + "type": "oauth2", + "token_url": "https://your-auth-server/token", + "scopes": ["patient/*.read", "user/*.read"] + } +) +``` + +### HTTPS + +Always use HTTPS in production. With uvicorn: + +```bash +uvicorn app:app --host 0.0.0.0 --port 443 --ssl-keyfile key.pem --ssl-certfile cert.pem +``` + +### Logging and Monitoring + +Add structured logging: + +```python +import logging + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger("clinicalflow") + +@app.cds_hooks(id="patient-alerts", ...) +def patient_alerts(context, prefetch): + logger.info(f"Processing request for patient {context.get('patientId')}") + # ... your logic +``` + +## Connect to Real EHR Sandboxes + +### Epic Sandbox + +1. Register at [Epic's Developer Portal](https://fhir.epic.com/) +2. Create an application +3. Configure your service URL +4. Test against Epic's sandbox environment + +### Cerner Sandbox + +1. Register at [Cerner's Developer Portal](https://code.cerner.com/) +2. Follow their CDS Hooks integration guide +3. Test with their Millennium sandbox + +## Extend Your Service + +### Add More Hooks + +Support multiple trigger points: + +```python +@app.cds_hooks( + id="order-check", + title="Medication Order Check", + hook="order-select" +) +def check_medication_orders(context, prefetch): + """Check for drug interactions when orders are selected.""" + # ... drug interaction logic + pass + + +@app.cds_hooks( + id="discharge-summary", + title="Discharge Summary Generator", + hook="encounter-discharge" +) +def generate_discharge_summary(context, prefetch): + """Generate discharge summary at end of encounter.""" + # ... summarization logic + pass +``` + +### Improve NLP + +Replace keyword matching with trained models: + +```python +from healthchain.pipeline.components.integrations import SpacyNLP + +# Use a clinical NLP model +pipeline.add_node(SpacyNLP.from_model_id("en_core_sci_lg")) + +# Or integrate with external services +from healthchain.pipeline.components import LLMComponent + +pipeline.add_node(LLMComponent( + provider="openai", + model="gpt-4", + prompt_template="Extract clinical conditions from: {text}" +)) +``` + +### 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 documentation: + +| Topic | Description | +|-------|-------------| +| [Gateway Reference](../../reference/gateway/gateway.md) | Deep dive into gateway patterns | +| [Pipeline Reference](../../reference/pipeline/pipeline.md) | Advanced pipeline configuration | +| [CDS Hooks Cookbook](../../cookbook/discharge_summarizer.md) | Complete CDS Hooks example | +| [Multi-EHR Integration](../../cookbook/multi_ehr_aggregation.md) | Connect to multiple EHRs | + +## Get Help + +- **Discord**: [Join our community](https://discord.gg/UQC6uAepUz) +- **GitHub**: [Report issues](https://github.com/dotimplement/healthchain/issues) +- **Office Hours**: Thursdays 4:30-5:30pm GMT + +## 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..9e06255c --- /dev/null +++ b/docs/tutorials/clinicalflow/pipeline.md @@ -0,0 +1,144 @@ +# 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 + doc.entities = extracted + return doc + + return pipeline.build() +``` + +## Using the Pipeline + +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.entities: + print(f" - {entity['display']} (SNOMED: {entity['code']})") +``` + +Expected output: + +``` +Extracted conditions: + - Hypertension (SNOMED: 38341003) + - Diabetes mellitus (SNOMED: 73211009) + - Chest pain (SNOMED: 29857009) + - Dyspnea (SNOMED: 267036007) +``` + +## Adding SpaCy Integration (Optional) + +For more sophisticated NLP, integrate spaCy: + +```python +from healthchain.pipeline import Pipeline +from healthchain.pipeline.components.integrations import SpacyNLP +from healthchain.io import Document + +def create_spacy_pipeline(): + """Create a pipeline with spaCy NLP.""" + + pipeline = Pipeline[Document]() + + # Add spaCy for tokenization and NER + pipeline.add_node(SpacyNLP.from_model_id("en_core_web_sm")) + + return pipeline.build() +``` + +## 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. diff --git a/docs/tutorials/clinicalflow/setup.md b/docs/tutorials/clinicalflow/setup.md new file mode 100644 index 00000000..92b1f560 --- /dev/null +++ b/docs/tutorials/clinicalflow/setup.md @@ -0,0 +1,72 @@ +# Setup + +Get your development environment ready for building the ClinicalFlow service. + +## Install HealthChain + +Create a new project directory and install HealthChain: + +```bash +mkdir clinicalflow +cd clinicalflow +pip install healthchain +``` + +For NLP capabilities, install with the optional spaCy integration: + +```bash +pip install healthchain[nlp] +``` + +## Verify Installation + +Create a file called `check_install.py`: + +```python +import healthchain +from healthchain.io import Document + +print(f"HealthChain version: {healthchain.__version__}") + +# Test creating a simple document +doc = Document("Patient has a history of hypertension.") +print(f"Created document with {len(doc.text)} characters") +``` + +Run it: + +```bash +python check_install.py +``` + +You should see output like: + +``` +HealthChain version: 0.x.x +Created document with 40 characters +``` + +## Project Structure + +Create the following project structure: + +``` +clinicalflow/ +β”œβ”€β”€ app.py # Main CDS Hooks service +β”œβ”€β”€ pipeline.py # NLP processing pipeline +└── test_service.py # Testing script +``` + +## Download Sample Data (Optional) + +For testing, you can use Synthea-generated patient data. HealthChain's sandbox can load this automatically, but if you want local data: + +```bash +mkdir data +# Download a sample Synthea bundle (optional) +# We'll use HealthChain's built-in data loaders in the testing step +``` + +## 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..1f0c2eab --- /dev/null +++ b/docs/tutorials/clinicalflow/testing.md @@ -0,0 +1,182 @@ +# Test with Sandbox + +Validate your CDS service with realistic patient data using HealthChain's sandbox. + +## What is the Sandbox? + +The **Sandbox** provides tools for testing CDS services without connecting to a real EHR. It can: + +- Generate realistic patient data +- Send CDS Hooks requests to your service +- Validate responses against the specification +- Save results for analysis + +## Create a Test Script + +Create a file called `test_service.py`: + +```python +from healthchain.sandbox import SandboxClient + +# Create a sandbox client pointing to your service +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) + +# Generate synthetic test data +client.generate_data( + num_patients=3, + conditions_per_patient=2 +) + +# Send requests and collect responses +responses = client.send_requests() + +# Analyze results +print(f"Sent {len(responses)} requests") +for i, response in enumerate(responses): + print(f"\nPatient {i + 1}:") + print(f" Status: {response.status_code}") + if response.ok: + cards = response.json().get("cards", []) + print(f" Cards returned: {len(cards)}") + for card in cards: + print(f" - {card.get('indicator', 'info').upper()}: {card.get('summary')}") +``` + +## Run the Test + +Make sure your service is running, then: + +```bash +python test_service.py +``` + +Expected output: + +``` +Sent 3 requests + +Patient 1: + Status: 200 + Cards returned: 2 + - INFO: Condition detected: Hypertension + - WARNING: Multiple active conditions + +Patient 2: + Status: 200 + Cards returned: 1 + - INFO: Condition detected: Diabetes mellitus + +Patient 3: + Status: 200 + Cards returned: 3 + - INFO: Condition detected: Chest pain + - INFO: Condition detected: Hypertension + - WARNING: Multiple active conditions +``` + +## Using Real Test Datasets + +Load data from Synthea (a synthetic patient generator): + +```python +from healthchain.sandbox import SandboxClient + +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) + +# Load from Synthea data directory +client.load_from_registry( + "synthea-patient", + data_dir="./data/synthea", + resource_types=["Patient", "Condition", "MedicationStatement"], + sample_size=5 +) + +responses = client.send_requests() +``` + +## Save Test Results + +Save results for reporting or debugging: + +```python +# Save responses to files +client.save_results("./output/test_results/") + +# Results are saved as JSON: +# - output/test_results/request_1.json +# - output/test_results/response_1.json +# - output/test_results/summary.json +``` + +## Validate CDS Hooks Compliance + +The sandbox validates that responses meet the CDS Hooks specification: + +```python +from healthchain.sandbox import SandboxClient + +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) + +# Enable strict validation +client.validate_responses = True + +responses = client.send_requests() + +# Check for validation errors +for response in responses: + if response.validation_errors: + print(f"Validation errors: {response.validation_errors}") +``` + +## Testing Different Hooks + +Test different CDS Hooks workflows: + +```python +# Test order-select hook +order_client = SandboxClient( + url="http://localhost:8000/cds-services/drug-interactions", + workflow="order-select" +) + +# Test order-sign hook +sign_client = SandboxClient( + url="http://localhost:8000/cds-services/order-review", + workflow="order-sign" +) +``` + +## Debugging Tips + +### Enable Verbose Logging + +```python +import logging +logging.basicConfig(level=logging.DEBUG) + +client = SandboxClient( + url="http://localhost:8000/cds-services/patient-alerts", + workflow="patient-view" +) +``` + +### Inspect Request/Response + +```python +response = client.send_single_request(patient_data) +print("Request sent:", response.request_body) +print("Response received:", response.json()) +``` + +## 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..8e3f0f76 --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,36 @@ +# 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: + +- **Time**: ~45 minutes +- **Level**: Beginner to Intermediate +- **Prerequisites**: Python basics, familiarity with REST APIs + +[:octicons-arrow-right-24: Start the ClinicalFlow Tutorial](clinicalflow/index.md) + +--- + +## What You'll Learn + +| Tutorial | Core Concepts | +|----------|---------------| +| **ClinicalFlow** | FHIR resources, Document containers, Pipeline components, CDS Hooks gateway, Sandbox testing | + +## Prerequisites + +Before starting any tutorial, make sure you have: + +- Python 3.10 or higher installed +- HealthChain installed (`pip install healthchain`) +- A code editor of your choice +- Basic understanding of Python and REST APIs + +Healthcare knowledge is helpful but not required - the tutorials explain concepts as you go. diff --git a/mkdocs.yml b/mkdocs.yml index 7737e7c2..1239ee8e 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 @@ -82,6 +92,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 +101,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 +158,7 @@ markdown_extensions: extra_css: - stylesheets/extra.css + - stylesheets/welcome.css plugins: - blog