diff --git a/README.md b/README.md index b1943b4e..086daa0f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ First time here? Check out our [Docs](https://dotimplement.github.io/HealthChain ## HealthChainAPI -The HealthChainAPI provides a secure, asynchronous integration layer that coordinates multiple healthcare systems in a single application. +The HealthChainAPI provides a secure integration layer that coordinates multiple healthcare systems in a single application. ### Multi-Protocol Support @@ -217,9 +217,9 @@ response = adapter.format(doc) The InteropEngine is a template-based system that allows you to convert between FHIR, CDA, and HL7v2. ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType -engine = create_engine() +engine = create_interop() with open("tests/data/test_cda.xml", "r") as f: cda_data = f.read() diff --git a/dev-templates/README.md b/dev-templates/README.md new file mode 100644 index 00000000..adf7072c --- /dev/null +++ b/dev-templates/README.md @@ -0,0 +1,36 @@ +# Developer Templates + +This directory contains **work-in-progress templates** for HealthChain developers. + +## Purpose + +- ๐Ÿ”ง **Development workspace** for new templates +- โš ๏ธ **Experimental features** not ready for bundling +- ๐Ÿงช **Testing ground** before promoting to `healthchain/configs/` + +## Workflow + +1. **Develop here** - Create/fix templates in subdirectories +2. **Test thoroughly** - Use integration tests and real data +3. **Promote when stable** - Move to `healthchain/configs/` for bundling +4. **Update docs** - Move from "experimental" to "stable" in docs + +## Current Templates + +- `allergies/` - Allergy templates with known bugs (see README) + +## Guidelines + +- Each template type gets its own subdirectory +- Include README with known issues and usage +- Test with example CDAs in `resources/` +- Follow existing template patterns + +## Not for End Users + +End users should use: +```bash +healthchain init-configs my_configs # Gets stable bundled templates +``` + +This directory is for **HealthChain contributors only**. diff --git a/dev-templates/allergies/README.md b/dev-templates/allergies/README.md new file mode 100644 index 00000000..a369ba9b --- /dev/null +++ b/dev-templates/allergies/README.md @@ -0,0 +1,48 @@ +# Experimental Allergy Templates + +โš ๏ธ **Warning: These templates are experimental and contain known bugs.** + +This directory contains allergy-related configuration and templates that were removed from the default bundled configs due to reliability issues. + +๐Ÿ“– **For full documentation on experimental templates, see:** [docs/reference/interop/experimental.md](../docs/reference/interop/experimental.md) + +## Contents + +- `allergies.yaml` - Section configuration for allergy processing +- `allergy_entry.liquid` - Template for converting FHIR AllergyIntolerance to CDA +- `allergy_intolerance.liquid` - Template for converting CDA allergies to FHIR + +## Known Issues + +- Clinical status parsing has bugs (see integration test comments) +- Round-trip conversion may not preserve all data correctly +- Template logic is fragile and may fail with edge cases + +## Usage + +If you need allergy support despite the bugs: + +1. Copy these files to your custom config directory: + ```bash + # After running: healthchain init-configs my_configs + cp cookbook/experimental_templates/allergies/* my_configs/interop/cda/sections/ + cp cookbook/experimental_templates/allergies/allergy_*.liquid my_configs/templates/cda_fhir/ + cp cookbook/experimental_templates/allergies/allergy_*.liquid my_configs/templates/fhir_cda/ + ``` + +2. Add "allergies" to your CCD document config: + ```yaml + # my_configs/interop/cda/document/ccd.yaml + body: + include_sections: + - "allergies" # Add this + - "medications" + - "problems" + - "notes" + ``` + +3. Test thoroughly with your specific data before using in production. + +## Contributing + +If you fix the bugs in these templates, please consider contributing back to the main project! diff --git a/configs/interop/cda/sections/allergies.yaml b/dev-templates/allergies/allergies.yaml similarity index 100% rename from configs/interop/cda/sections/allergies.yaml rename to dev-templates/allergies/allergies.yaml diff --git a/configs/templates/fhir_cda/allergy_entry.liquid b/dev-templates/allergies/allergy_entry.liquid similarity index 100% rename from configs/templates/fhir_cda/allergy_entry.liquid rename to dev-templates/allergies/allergy_entry.liquid diff --git a/configs/templates/cda_fhir/allergy_intolerance.liquid b/dev-templates/allergies/allergy_intolerance.liquid similarity index 100% rename from configs/templates/cda_fhir/allergy_intolerance.liquid rename to dev-templates/allergies/allergy_intolerance.liquid diff --git a/docs/api/interop.md b/docs/api/interop.md index 1df7b425..71eab4e0 100644 --- a/docs/api/interop.md +++ b/docs/api/interop.md @@ -1,15 +1,9 @@ # Interoperability Engine ::: healthchain.interop.engine - ::: healthchain.interop.config_manager - ::: healthchain.interop.template_registry - ::: healthchain.interop.parsers.cda - ::: healthchain.interop.generators.cda - ::: healthchain.interop.generators.fhir - ::: healthchain.config.validators diff --git a/docs/cookbook/interop/basic_conversion.md b/docs/cookbook/interop/basic_conversion.md index bbee570c..25c8f3ac 100644 --- a/docs/cookbook/interop/basic_conversion.md +++ b/docs/cookbook/interop/basic_conversion.md @@ -13,12 +13,12 @@ This tutorial demonstrates how to use the HealthChain interoperability module to First, let's import the required modules and create an interoperability engine: ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType from pathlib import Path import json # Create an engine -engine = create_engine() +engine = create_interop() ``` ## Converting CDA to FHIR diff --git a/docs/index.md b/docs/index.md index 53d9e534..f5102fdd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Welcome to HealthChain ๐Ÿ’ซ ๐Ÿฅ -HealthChain is an open-source Python framework for building real-time AI applications in a healthcare context. +HealthChain is an open-source Python framework that makes it easier to connect your AI/ML pipelines to healthcare systems. [ :fontawesome-brands-discord: Join our Discord](https://discord.gg/UQC6uAepUz){ .md-button .md-button--primary }      diff --git a/docs/quickstart.md b/docs/quickstart.md index b586dade..d588b655 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -154,11 +154,15 @@ The HealthChain Interoperability module provides tools for converting between di [(Full Documentation on Interoperability Engine)](./reference/interop/interop.md) + +**Choose your setup based on your needs:** + +โœ… **Default configs** - For basic testing and prototyping only: ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType -# Create an interoperability engine -engine = create_engine() +# Uses bundled configs - basic CDA โ†” FHIR conversion +engine = create_interop() # Load a CDA document with open("tests/data/test_cda.xml", "r") as f: @@ -171,16 +175,31 @@ fhir_resources = engine.to_fhir(cda_xml, src_format=FormatType.CDA) cda_document = engine.from_fhir(fhir_resources, dest_format=FormatType.CDA) ``` -The interop module provides a flexible, template-based approach to healthcare format conversion: +> โš ๏ธ **Default configs are limited** - Only supports problems, medications, and notes. No allergies, custom mappings, or organization-specific templates. + +๐Ÿ› ๏ธ **Custom configs** - **Required for real-world use**: +```bash +# Create editable configuration templates +healthchain init-configs ./my_configs +``` -| Feature | Description | -|---------|-------------| -| Format conversion | Convert legacy formats (CDA, HL7v2) to FHIR resources and back | -| Template-based generation | Customize syntactic output using [Liquid](https://shopify.github.io/liquid/) templates | -| Configuration | Configure terminology mappings, validation rules, and environments | -| Extension | Register custom parsers, generators, and validators | +```python +# Use your customized configs +engine = create_interop(config_dir="./my_configs") + +# Now you can customize: +# โ€ข Add experimental features (allergies, procedures) +# โ€ข Modify terminology mappings (SNOMED, LOINC codes) +# โ€ข Customize templates for your organization's CDA format +# โ€ข Configure validation rules and environments +``` -For more details, see the [conversion examples](cookbook/interop/basic_conversion.md). +**When you need custom configs:** +- ๐Ÿฅ **Production healthcare applications** +- ๐Ÿ”ง **Organization-specific CDA templates** +- ๐Ÿงช **Experimental features** (allergies, procedures) +- ๐Ÿ—บ๏ธ **Custom terminology mappings** +- ๐Ÿ›ก๏ธ **Specific validation requirements** ## Utilities โš™๏ธ @@ -189,7 +208,7 @@ For more details, see the [conversion examples](cookbook/interop/basic_conversio Test your AI applications in realistic healthcare contexts with sandbox environments for CDS Hooks and clinical documentation workflows. -[(Full Documentation on Sandbox)](./reference/sandbox/sandbox.md) +[(Full Documentation on Sandbox)](./reference/utilities/sandbox.md) ```python import healthchain as hc diff --git a/docs/reference/index.md b/docs/reference/index.md index 3aa9afeb..a5776a0e 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -4,6 +4,6 @@ - [Gateway](gateway/gateway.md): Connect to multiple healthcare systems and services. - [Pipeline](pipeline/pipeline.md): Build and manage processing pipelines for healthcare NLP and ML tasks. -- [Sandbox](sandbox/sandbox.md): Test your pipelines in a simulated healthcare environment. +- [Sandbox](utilities/sandbox.md): Test your pipelines in a simulated healthcare environment. - [Interoperability](interop/interop.md): Convert between healthcare data formats like FHIR, CDA, and HL7v2. - [Utilities](utilities/fhir_helpers.md): Additional tools for development and testing. diff --git a/docs/reference/interop/configuration.md b/docs/reference/interop/configuration.md index 60d8519a..5eb66304 100644 --- a/docs/reference/interop/configuration.md +++ b/docs/reference/interop/configuration.md @@ -2,6 +2,44 @@ The interoperability module uses a configuration system to control its behavior. This includes mappings between different healthcare data formats, validation rules, and environment-specific settings. +## Configuration Overview + +HealthChain works out-of-the-box with default configurations, but you can customize them for your specific needs. + +### Default Usage + +```python +from healthchain.interop import create_interop + +# Uses bundled default configurations +engine = create_interop() +``` + +### Custom Configuration + +```python +# Use custom config directory +engine = create_interop(config_dir="/path/to/custom/configs") +``` + +### Creating Custom Configs + +To create editable configuration templates: + +```bash +# Create customizable config templates +healthchain init-configs ./my_configs + +# Then use them in your code +engine = create_interop(config_dir="./my_configs") +``` + +This gives you editable copies of: +- **Templates**: CDA โ†” FHIR conversion templates +- **Mappings**: Code system mappings (SNOMED, LOINC, etc.) +- **Validation**: Schema validation rules +- **Environment settings**: Development, testing, production configs + ## Configuration Components | Component | Description | @@ -188,12 +226,12 @@ defaults: ### Basic Configuration Access ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Create an engine -engine = create_engine() +engine = create_interop() # OR -engine = create_engine(config_dir="custom_configs/") +engine = create_interop(config_dir="custom_configs/") # Get all configurations engine.config.get_configs() @@ -202,10 +240,10 @@ engine.config.get_configs() id_prefix = engine.config.get_config_value("defaults.common.id_prefix") # Set the environment (this reloads configuration from the specified environment) -engine = create_engine(environment="production") +engine = create_interop(environment="production") # Validation level is set during initialization or using set_validation_level -engine = create_engine(validation_level="warn") +engine = create_interop(validation_level="warn") # OR engine.config.set_validation_level("strict") @@ -216,10 +254,10 @@ engine.config.set_config_value("cda.sections.problems.identifiers.code", "10160- ### Section Configuration ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Create an engine -engine = create_engine() +engine = create_interop() # Get all section configurations sections = engine.config.get_cda_section_configs() @@ -235,10 +273,10 @@ code = problems_config["identifiers"]["code"] ### Mapping Access ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Create an engine -engine = create_engine() +engine = create_interop() # Get all mappings mappings = engine.config.get_mappings() @@ -249,83 +287,20 @@ snomed = systems.get("http://snomed.info/sct", {}) snomed_oid = snomed.get("oid") # "2.16.840.1.113883.6.96" ``` -## Configuration Loading Order - -The configuration system follows a hierarchical loading order, where each layer can override values from previous layers: - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ 1. BASE CONFIGURATION โ”‚ -โ”‚ configs/defaults.yaml โ”‚ -โ”‚ โ”‚ -โ”‚ โ€ข Default values for all envs โ”‚ -โ”‚ โ€ข Complete set of all options โ”‚ -โ”‚ โ€ข Lowest precedence โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ 2. ENVIRONMENT CONFIGURATION โ”‚ -โ”‚ configs/environments/{env}.yamlโ”‚ -โ”‚ โ”‚ -โ”‚ โ€ข Environment-specific values โ”‚ -โ”‚ โ€ข Overrides matching defaults โ”‚ -โ”‚ โ€ข Medium precedence โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ 3. RUNTIME CONFIGURATION โ”‚ -โ”‚ Programmatic overrides โ”‚ -โ”‚ โ”‚ -โ”‚ โ€ข Set via API at runtime โ”‚ -โ”‚ โ€ข For temporary changes โ”‚ -โ”‚ โ€ข Highest precedence โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### Example of Configuration Override +## Configuration Precedence -Consider the following values set across different configuration layers: +Configuration values are loaded in order of precedence: -1. In `defaults.yaml`: -```yaml -defaults: - common: - id_prefix: "hc-" - subject: - reference: "Patient/example" -``` +1. **Base configuration** (defaults.yaml) - lowest precedence +2. **Environment configuration** (environments/{env}.yaml) - overrides defaults +3. **Runtime overrides** (via API) - highest precedence -2. In `environments/development.yaml`: -```yaml -defaults: - common: - id_prefix: "dev-" - subject: - reference: "Patient/Foo" -``` - -3. Runtime override in code: ```python -from healthchain.interop import create_engine -from healthchain.interop.types import FormatType - -# Create an engine -engine = create_engine() - -# Override a configuration value at runtime -engine.config.set_config_value("defaults.common.id_prefix", "test-") - -with open("tests/data/test_cda.xml", "r") as f: - cda = f.read() - -fhir_resources = engine.to_fhir(cda, FormatType.CDA) -print(fhir_resources[0].id) # Will use "test-" prefix instead of "dev-" or "hc-" +# Example: Override configuration at runtime +engine = create_interop() +engine.config.set_config_value("defaults.common.id_prefix", "custom-") ``` -In this example, the final value of `id_prefix` would be `"test-"` because the runtime override has the highest precedence, followed by the environment configuration, and finally the base configuration. - ## Validation Levels The configuration system supports different validation levels: @@ -337,10 +312,10 @@ The configuration system supports different validation levels: | `ignore` | Ignore configuration errors | ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Create an engine with a specific validation level -engine = create_engine(validation_level="warn") +engine = create_interop(validation_level="warn") # Change the validation level engine.config.set_validation_level("strict") diff --git a/docs/reference/interop/engine.md b/docs/reference/interop/engine.md index 2a77c310..903dabe5 100644 --- a/docs/reference/interop/engine.md +++ b/docs/reference/interop/engine.md @@ -5,10 +5,10 @@ The `InteropEngine` is the core component of the HealthChain interoperability mo ## Basic Usage ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType # Create an interoperability engine -engine = create_engine() +engine = create_interop() # Convert CDA XML to FHIR resources fhir_resources = engine.to_fhir(cda_xml, src_format=FormatType.CDA) @@ -22,23 +22,34 @@ fhir_resources = engine.to_fhir(hl7v2_message, src_format=FormatType.HL7V2) ## Creating an Engine -The `create_engine()` function is the recommended way to create an engine instance: +The `create_interop()` function is the recommended way to create an engine instance: ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Create with default configuration -engine = create_engine() +engine = create_interop() +``` + +### Custom Configuration -# Create with custom configuration directory -from pathlib import Path -config_dir = Path("/path/to/configs") -engine = create_engine(config_dir=config_dir) +```python +# Use custom config directory +engine = create_interop(config_dir="/path/to/custom/configs") -# Create with custom validation level -engine = create_engine(validation_level="warn") +# Create with custom validation level and environment +engine = create_interop(validation_level="warn", environment="production") ``` + +> **๐Ÿ’ก Tip:** +> To create editable configuration templates, run: +> +> ```bash +> healthchain init-configs ./my_configs +> ``` +> This will create a `my_configs` directory with editable default configuration templates. + ## Conversion Methods All conversions convert to and from FHIR. diff --git a/docs/reference/interop/experimental.md b/docs/reference/interop/experimental.md new file mode 100644 index 00000000..2e0a3b16 --- /dev/null +++ b/docs/reference/interop/experimental.md @@ -0,0 +1,77 @@ +# Experimental Templates + +This page tracks templates that are under development or have known issues. Use these at your own risk and please contribute fixes! + +## Template Status + +| Template Type | Status | Known Issues | Location | +|---------------|--------|--------------|----------| +| **Problems** | โœ… **Stable** | None | Bundled in default configs | +| **Medications** | โœ… **Stable** | None | Bundled in default configs | +| **Notes** | โœ… **Stable** | None | Bundled in default configs | +| **Allergies** | โš ๏ธ **Experimental** | Clinical status parsing bugs, round-trip issues | `dev-templates/allergies/` | + +## Using Experimental Templates + +### Allergies (AllergyIntolerance) + +**Status:** โš ๏ธ Experimental - Known bugs prevent inclusion in bundled configs + +**Known Issues:** +- Clinical status parsing has bugs (see integration test comments) +- Round-trip conversion may not preserve all data correctly +- Template logic is fragile and may fail with edge cases + +**Location:** `dev-templates/allergies/` + +**Usage:** +1. Copy experimental files to your custom config: + ```bash + # After running: healthchain init-configs my_configs + cp dev-templates/allergies/allergies.yaml my_configs/interop/cda/sections/ + cp dev-templates/allergies/allergy_*.liquid my_configs/templates/cda_fhir/ + cp dev-templates/allergies/allergy_*.liquid my_configs/templates/fhir_cda/ + ``` + +2. Enable in your CCD document config: + ```yaml + # my_configs/interop/cda/document/ccd.yaml + body: + include_sections: + - "allergies" # Add this line + - "medications" + - "problems" + - "notes" + ``` + +3. **Test thoroughly** with your specific data before production use. + +## Contributing Template Fixes + +We welcome contributions to improve experimental templates! + +### For Allergies: +- **Clinical status mapping** - The biggest issue is parsing clinical status from CDA observations +- **Round-trip fidelity** - Ensure CDA โ†’ FHIR โ†’ CDA preserves all important data +- **Edge case handling** - Make templates robust to various CDA structures + +### General Guidelines: +1. **Test with real data** - Use the example CDAs in `resources/` for testing +2. **Add comprehensive tests** - Include both unit and integration tests +3. **Document limitations** - Be clear about what your fix does/doesn't solve +4. **Follow template patterns** - Keep consistent with existing stable templates + +### Submitting Fixes: +1. Fix the templates in `dev-templates/` +2. Add/update tests to cover your changes +3. Move stable templates to bundled configs in your PR +4. Update this documentation + +## Roadmap + +**Next Priorities:** +1. ๐ŸŽฏ **Allergies stabilization** - Fix clinical status parsing and round-trip issues +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! diff --git a/docs/reference/interop/generators.md b/docs/reference/interop/generators.md index 5477c914..0c2c5979 100644 --- a/docs/reference/interop/generators.md +++ b/docs/reference/interop/generators.md @@ -20,11 +20,11 @@ The CDA Generator produces Clinical Document Architecture (CDA) XML documents fr ### Usage Examples ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType from healthchain.fhir import create_condition # Create an engine -engine = create_engine() +engine = create_interop() # Use the FHIR helper functions to create a condition resource condition = create_condition( @@ -50,10 +50,10 @@ The FHIR Generator transforms data from other formats into FHIR resources. It cu ### Usage Examples ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType # Create an engine -engine = create_engine() +engine = create_interop() # CDA section entries in dictionary format (@ is used to represent XML attributes) cda_section_entries = { @@ -203,7 +203,7 @@ The HL7v2 Generator produces HL7 version 2 messages from FHIR resources (Coming You can create a custom generator by implementing a class that inherits from `BaseGenerator` and registering it with the engine (this will replace the default generator for the format type): ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType from healthchain.interop.config_manager import InteropConfigManager from healthchain.interop.template_registry import TemplateRegistry from healthchain.interop.generators import BaseGenerator @@ -221,6 +221,6 @@ class CustomGenerator(BaseGenerator): return "Custom output format" # Register the custom generator with the engine -engine = create_engine() +engine = create_interop() engine.register_generator(FormatType.CDA, CustomGenerator(engine.config, engine.template_registry)) ``` diff --git a/docs/reference/interop/interop.md b/docs/reference/interop/interop.md index bd0084c6..03af0478 100644 --- a/docs/reference/interop/interop.md +++ b/docs/reference/interop/interop.md @@ -49,10 +49,10 @@ The main conversion methods are (hold on to your hats): - `.from_fhir()` - Convert FHIR resources to a destination format ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType # Create an interoperability engine -engine = create_engine() +engine = create_interop() # Convert CDA XML to FHIR resources with open('patient_ccd.xml', 'r') as f: @@ -98,7 +98,7 @@ You can customize the engine's behavior for different environments: ```python # Create an engine with specific environment settings -engine = create_engine( +engine = create_interop( config_dir=Path("/path/to/custom/configs"), validation_level="warn", # Options: strict, warn, ignore environment="production" # Options: development, testing, production diff --git a/docs/reference/interop/mappings.md b/docs/reference/interop/mappings.md index c5c756e5..df2c0ded 100644 --- a/docs/reference/interop/mappings.md +++ b/docs/reference/interop/mappings.md @@ -94,10 +94,10 @@ The `severity_codes.yaml` file maps severity designations between formats: The `InteropEngine` automatically loads and applies mappings during the conversion process. You can also access mappings directly through the configuration manager: ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Create an engine -engine = create_engine() +engine = create_interop() # Get all mappings mappings = engine.config.get_mappings() diff --git a/docs/reference/interop/parsers.md b/docs/reference/interop/parsers.md index cb325f6c..90001b32 100644 --- a/docs/reference/interop/parsers.md +++ b/docs/reference/interop/parsers.md @@ -24,10 +24,10 @@ The input data should be in the format `{}: {}`. ### Usage Examples ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType # Create an engine -engine = create_engine() +engine = create_interop() # Parse a CDA document directly to FHIR with open("tests/data/test_cda.xml", "r") as f: @@ -193,7 +193,7 @@ cda: You can create a custom parser by implementing a class that inherits from `BaseParser` and registering it with the engine (this will replace the default parser for the format type): ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType from healthchain.interop.config_manager import InteropConfigManager from healthchain.interop.parsers.base import BaseParser @@ -206,6 +206,6 @@ class CustomParser(BaseParser): return {"structured_data": "example"} # Register the custom parser with the engine -engine = create_engine() +engine = create_interop() engine.register_parser(FormatType.CDA, CustomParser(engine.config)) ``` diff --git a/docs/reference/interop/templates.md b/docs/reference/interop/templates.md index d3968f93..67f81826 100644 --- a/docs/reference/interop/templates.md +++ b/docs/reference/interop/templates.md @@ -36,13 +36,16 @@ Using full paths is recommended for clarity and to avoid confusion when template ## Default Templates -HealthChain provides default templates for the transformation of Problems, Medications, and Allergies sections in a Continuity of Care (CCD) CDA to FHIR and the reverse. They are configured to work out of the box with the default configuration. You are welcome to modify these templates at your own discretion or use them as a starting reference point for your writing your own templates. +HealthChain provides default templates for the transformation of Problems, Medications, and Notes sections in a Continuity of Care (CCD) CDA to FHIR and the reverse. They are configured to work out of the box with the default configuration and the example CDAs [here](https://github.com/dotimplement/HealthChain/tree/main/resources). + +You are welcome to modify these templates at your own discretion or use them as a starting reference point for your writing your own templates. **Always verify that templates work for your use case.** + +**Note:** Some templates are experimental and not included in the default configs. See [Experimental Templates](experimental.md) for details on templates under development. | CDA Section | FHIR Resource | |-------------|---------------| | **Problems** | [**Condition**](https://www.hl7.org/fhir/condition.html) | | **Medications** | [**MedicationStatement**](https://www.hl7.org/fhir/medicationstatement.html) | -| **Allergies** | [**AllergyIntolerance**](https://www.hl7.org/fhir/allergyintolerance.html) | | **Notes** | [**DocumentReference**](https://www.hl7.org/fhir/documentreference.html) | CDA to FHIR templates: @@ -51,7 +54,6 @@ CDA to FHIR templates: |-------------|------------------| | **Problems** | `fhir_cda/problem_entry.liquid` | | **Medications** | `fhir_cda/medication_entry.liquid` | -| **Allergies** | `fhir_cda/allergy_entry.liquid` | | **Notes** | `fhir_cda/note_entry.liquid` | FHIR to CDA templates: @@ -60,7 +62,6 @@ FHIR to CDA templates: |---------------|------------------| | **Condition** | `cda_fhir/condition.liquid` | | **MedicationStatement** | `cda_fhir/medication_statement.liquid` | -| **AllergyIntolerance** | `cda_fhir/allergy_intolerance.liquid` | | **DocumentReference** | `cda_fhir/document_reference.liquid` | ## Template Format @@ -97,11 +98,11 @@ Example template for a CDA to FHIR conversion: The Interoperability Engine API provides a high-level interface (`.to_fhir()` and `.from_fhir()`) for transforming healthcare data between different formats using templates. ```python -from healthchain.interop import create_engine, FormatType +from healthchain.interop import create_interop, FormatType from healthchain.fhir import create_condition # Create an engine -engine = create_engine() +engine = create_interop() # Create a FHIR resource condition = create_condition( @@ -124,10 +125,10 @@ fhir_resources = engine.to_fhir(cda_xml, src_format=FormatType.CDA) For advanced use cases, you can access the template system directly: ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Create an engine -engine = create_engine() +engine = create_interop() # Access the template registry registry = engine.template_registry @@ -198,7 +199,6 @@ The template system provides several custom filters for common healthcare docume | `extract_effective_period` | Extracts effective period data from CDA effectiveTime elements | | `extract_effective_timing` | Extracts timing data from effectiveTime elements | | `extract_clinical_status` | Extracts clinical status from an observation | -| `extract_reactions` | Extracts reactions from an observation | | `clean_empty` | Recursively removes empty values from dictionaries and lists | | `to_base64` | Encodes text to base64 | | `from_base64` | Decodes base64 to text | @@ -229,13 +229,13 @@ For more information on using filters, see Liquid's [official documentation](htt You can add custom filters to the template system: ```python -from healthchain.interop import create_engine +from healthchain.interop import create_interop def custom_filter(value): return f"CUSTOM:{value}" # Create an engine -engine = create_engine() +engine = create_interop() # Add a custom filter engine.template_registry.add_filter("custom", custom_filter) diff --git a/docs/reference/pipeline/adapters/adapters.md b/docs/reference/pipeline/adapters/adapters.md index d6ca84cc..f68f96f0 100644 --- a/docs/reference/pipeline/adapters/adapters.md +++ b/docs/reference/pipeline/adapters/adapters.md @@ -75,10 +75,10 @@ Both CDA and CDS adapters can be configured with custom interoperability engines ```python from healthchain.io import CdaAdapter -from healthchain.interop import create_engine +from healthchain.interop import create_interop # Custom engine with specific configuration -custom_engine = create_engine(config_dir="/path/to/custom/config") +custom_engine = create_interop(config_dir="/path/to/custom/config") adapter = CdaAdapter(engine=custom_engine) ``` For more information on the InteropEngine, see the [InteropEngine documentation](../../interop/interop.md). diff --git a/docs/reference/pipeline/adapters/cdaadapter.md b/docs/reference/pipeline/adapters/cdaadapter.md index 2da4301b..909b14c1 100644 --- a/docs/reference/pipeline/adapters/cdaadapter.md +++ b/docs/reference/pipeline/adapters/cdaadapter.md @@ -10,7 +10,7 @@ This adapter is particularly useful for clinical documentation improvement (CDI) | Input | Output | Document Access | |-------|--------|-----------------| -| [**CdaRequest**](../../../api/use_cases.md#healthchain.models.requests.cdarequest.CdaRequest) | [**CdaResponse**](../../../api/use_cases.md#healthchain.models.responses.cdaresponse.CdaResponse) | `Document.fhir.problem_list`, `Document.fhir.medication_list`, `Document.fhir.allergy_list`, `Document.text` | +| [**CdaRequest**](../../../api/use_cases.md#healthchain.models.requests.cdarequest.CdaRequest) | [**CdaResponse**](../../../api/use_cases.md#healthchain.models.responses.cdaresponse.CdaResponse) | `Document.fhir.problem_list`, `Document.fhir.medication_list`, `Document.text` | ## Document Data Access @@ -20,7 +20,6 @@ Data parsed from the CDA document is converted into FHIR resources and stored in |-------------|---------------|--------------------------| | Problem List | [Condition](https://www.hl7.org/fhir/condition.html) | `Document.fhir.problem_list` | | Medication List | [MedicationStatement](https://www.hl7.org/fhir/medicationstatement.html) | `Document.fhir.medication_list` | -| Allergy List | [AllergyIntolerance](https://www.hl7.org/fhir/allergyintolerance.html) | `Document.fhir.allergy_list` | | Clinical Notes | [DocumentReference](https://www.hl7.org/fhir/documentreference.html) | `Document.text` + `Document.fhir.bundle` | All FHIR resources are Pydantic models, so you can access them using the `model_dump()` method: diff --git a/docs/reference/pipeline/components/fhirproblemextractor.md b/docs/reference/pipeline/components/fhirproblemextractor.md index d38c7e48..add22c59 100644 --- a/docs/reference/pipeline/components/fhirproblemextractor.md +++ b/docs/reference/pipeline/components/fhirproblemextractor.md @@ -104,4 +104,4 @@ pipeline = MedicalCodingPipeline( - [FHIR Condition Resources](https://www.hl7.org/fhir/condition.html) - [Medical Coding Pipeline](../prebuilt_pipelines/medicalcoding.md) -- [Document Container](../../interop/containers.md) +- [Document Container](../data_container.md) diff --git a/docs/reference/pipeline/pipeline.md b/docs/reference/pipeline/pipeline.md index eefef0da..53626db1 100644 --- a/docs/reference/pipeline/pipeline.md +++ b/docs/reference/pipeline/pipeline.md @@ -12,8 +12,8 @@ HealthChain comes with a set of prebuilt pipelines that are out-of-the-box imple |----------|-----------|----------|-------------|---------------------| | [**MedicalCodingPipeline**](./prebuilt_pipelines/medicalcoding.md) | `Document` | Clinical Documentation | An NLP pipeline that processes free-text clinical notes into structured data | Automatically generating SNOMED CT codes from clinical notes | | [**SummarizationPipeline**](./prebuilt_pipelines/summarization.md) | `Document` | Clinical Decision Support | An NLP pipeline for summarizing clinical notes | Generating discharge summaries from patient history and notes | -| **QAPipeline** [TODO] | `Document` | Conversational AI | A Question Answering pipeline suitable for conversational AI applications | Developing a chatbot to answer patient queries about their medical records | -| **ClassificationPipeline** [TODO] | `Tabular` | Predictive Analytics | A pipeline for machine learning classification tasks | Predicting patient readmission risk based on historical health data | + Prebuilt pipelines are end-to-end workflows optimized for specific healthcare AI tasks. They can be used with adapters for seamless integration with EHR systems via [protocols](../gateway/gateway.md). diff --git a/docs/reference/utilities/sandbox.md b/docs/reference/utilities/sandbox.md index cc6c8cba..4bfe4c1f 100644 --- a/docs/reference/utilities/sandbox.md +++ b/docs/reference/utilities/sandbox.md @@ -35,7 +35,6 @@ class TestCDS(ClinicalDecisionSupport): The `@hc.ehr` decorator simulates EHR client behavior for testing. You must specify a **workflow** that determines how your data will be formatted. -Data should be wrapped in a [Prefetch](../../../api/data_models.md#healthchain.models.data.prefetch) object for CDS workflows, or return appropriate FHIR resources for document workflows. === "Clinical Decision Support" ```python diff --git a/healthchain/cli.py b/healthchain/cli.py index 72c2fbb7..292b55bf 100644 --- a/healthchain/cli.py +++ b/healthchain/cli.py @@ -10,6 +10,28 @@ def run_file(filename): print(f"An error occurred while trying to run the file: {e}") +def init_configs(target_dir: str): + """Initialize configuration templates for customization.""" + try: + from healthchain.interop import init_config_templates + + target_path = init_config_templates(target_dir) + print(f"\n๐ŸŽ‰ Success! Configuration templates created at: {target_path}") + print("\n๐Ÿ“– Next steps:") + print(" 1. Customize the configuration files in the created directory") + print(" 2. Use them in your code:") + print(" from healthchain.interop import create_interop") + print(f" engine = create_interop(config_dir='{target_dir}')") + print("\n๐Ÿ“š See documentation for configuration options") + + except FileExistsError as e: + print(f"โŒ Error: {str(e)}") + print("๐Ÿ’ก Tip: Choose a different directory name or remove the existing one") + except Exception as e: + print(f"โŒ Error initializing configs: {str(e)}") + print("๐Ÿ’ก Tip: Make sure HealthChain is properly installed") + + def main(): parser = argparse.ArgumentParser(description="HealthChain command-line interface") subparsers = parser.add_subparsers(dest="command", required=True) @@ -18,10 +40,25 @@ def main(): run_parser = subparsers.add_parser("run", help="Run a specified file") run_parser.add_argument("filename", type=str, help="The filename to run") + # Subparser for the 'init-configs' command + init_parser = subparsers.add_parser( + "init-configs", + help="Initialize configuration templates for interop customization", + ) + init_parser.add_argument( + "target_dir", + type=str, + nargs="?", + default="./healthchain_configs", + help="Directory to create configuration templates (default: ./healthchain_configs)", + ) + args = parser.parse_args() if args.command == "run": run_file(args.filename) + elif args.command == "init-configs": + init_configs(args.target_dir) if __name__ == "__main__": diff --git a/healthchain/config/validators.py b/healthchain/config/validators.py index 3940614d..4541d180 100644 --- a/healthchain/config/validators.py +++ b/healthchain/config/validators.py @@ -193,7 +193,7 @@ def validate_templates(cls, v): class CcdDocumentConfig(DocumentConfigBase): """Configuration model specific to CCD documents""" - allowed_sections: List[str] = ["problems", "medications", "allergies", "notes"] + allowed_sections: List[str] = ["problems", "medications", "notes"] class NoteSectionTemplateConfig(SectionTemplateConfigBase): @@ -218,7 +218,6 @@ def validate_note_section(cls, v): CDA_SECTION_CONFIG_REGISTRY = { "Condition": ProblemSectionTemplateConfig, "MedicationStatement": MedicationSectionTemplateConfig, - "AllergyIntolerance": AllergySectionTemplateConfig, "DocumentReference": NoteSectionTemplateConfig, } diff --git a/configs/defaults.yaml b/healthchain/configs/defaults.yaml similarity index 100% rename from configs/defaults.yaml rename to healthchain/configs/defaults.yaml diff --git a/configs/environments/development.yaml b/healthchain/configs/environments/development.yaml similarity index 100% rename from configs/environments/development.yaml rename to healthchain/configs/environments/development.yaml diff --git a/configs/environments/production.yaml b/healthchain/configs/environments/production.yaml similarity index 100% rename from configs/environments/production.yaml rename to healthchain/configs/environments/production.yaml diff --git a/configs/environments/testing.yaml b/healthchain/configs/environments/testing.yaml similarity index 100% rename from configs/environments/testing.yaml rename to healthchain/configs/environments/testing.yaml diff --git a/configs/interop/cda/document/ccd.yaml b/healthchain/configs/interop/cda/document/ccd.yaml similarity index 98% rename from configs/interop/cda/document/ccd.yaml rename to healthchain/configs/interop/cda/document/ccd.yaml index 2afe5eed..b6b7b928 100644 --- a/configs/interop/cda/document/ccd.yaml +++ b/healthchain/configs/interop/cda/document/ccd.yaml @@ -37,7 +37,6 @@ structure: structured_body: true non_xml_body: false include_sections: - - "allergies" - "medications" - "problems" - "notes" diff --git a/configs/interop/cda/sections/medications.yaml b/healthchain/configs/interop/cda/sections/medications.yaml similarity index 100% rename from configs/interop/cda/sections/medications.yaml rename to healthchain/configs/interop/cda/sections/medications.yaml diff --git a/configs/interop/cda/sections/notes.yaml b/healthchain/configs/interop/cda/sections/notes.yaml similarity index 100% rename from configs/interop/cda/sections/notes.yaml rename to healthchain/configs/interop/cda/sections/notes.yaml diff --git a/configs/interop/cda/sections/problems.yaml b/healthchain/configs/interop/cda/sections/problems.yaml similarity index 100% rename from configs/interop/cda/sections/problems.yaml rename to healthchain/configs/interop/cda/sections/problems.yaml diff --git a/configs/mappings/README.md b/healthchain/configs/mappings/README.md similarity index 100% rename from configs/mappings/README.md rename to healthchain/configs/mappings/README.md diff --git a/configs/mappings/cda_default/README.md b/healthchain/configs/mappings/cda_default/README.md similarity index 100% rename from configs/mappings/cda_default/README.md rename to healthchain/configs/mappings/cda_default/README.md diff --git a/configs/mappings/cda_default/severity_codes.yaml b/healthchain/configs/mappings/cda_default/severity_codes.yaml similarity index 100% rename from configs/mappings/cda_default/severity_codes.yaml rename to healthchain/configs/mappings/cda_default/severity_codes.yaml diff --git a/configs/mappings/cda_default/status_codes.yaml b/healthchain/configs/mappings/cda_default/status_codes.yaml similarity index 100% rename from configs/mappings/cda_default/status_codes.yaml rename to healthchain/configs/mappings/cda_default/status_codes.yaml diff --git a/configs/mappings/cda_default/systems.yaml b/healthchain/configs/mappings/cda_default/systems.yaml similarity index 100% rename from configs/mappings/cda_default/systems.yaml rename to healthchain/configs/mappings/cda_default/systems.yaml diff --git a/configs/templates/cda_fhir/condition.liquid b/healthchain/configs/templates/cda_fhir/condition.liquid similarity index 100% rename from configs/templates/cda_fhir/condition.liquid rename to healthchain/configs/templates/cda_fhir/condition.liquid diff --git a/configs/templates/cda_fhir/document_reference.liquid b/healthchain/configs/templates/cda_fhir/document_reference.liquid similarity index 100% rename from configs/templates/cda_fhir/document_reference.liquid rename to healthchain/configs/templates/cda_fhir/document_reference.liquid diff --git a/configs/templates/cda_fhir/medication_statement.liquid b/healthchain/configs/templates/cda_fhir/medication_statement.liquid similarity index 100% rename from configs/templates/cda_fhir/medication_statement.liquid rename to healthchain/configs/templates/cda_fhir/medication_statement.liquid diff --git a/configs/templates/fhir_cda/document.liquid b/healthchain/configs/templates/fhir_cda/document.liquid similarity index 100% rename from configs/templates/fhir_cda/document.liquid rename to healthchain/configs/templates/fhir_cda/document.liquid diff --git a/configs/templates/fhir_cda/medication_entry.liquid b/healthchain/configs/templates/fhir_cda/medication_entry.liquid similarity index 100% rename from configs/templates/fhir_cda/medication_entry.liquid rename to healthchain/configs/templates/fhir_cda/medication_entry.liquid diff --git a/configs/templates/fhir_cda/note_entry.liquid b/healthchain/configs/templates/fhir_cda/note_entry.liquid similarity index 100% rename from configs/templates/fhir_cda/note_entry.liquid rename to healthchain/configs/templates/fhir_cda/note_entry.liquid diff --git a/configs/templates/fhir_cda/problem_entry.liquid b/healthchain/configs/templates/fhir_cda/problem_entry.liquid similarity index 100% rename from configs/templates/fhir_cda/problem_entry.liquid rename to healthchain/configs/templates/fhir_cda/problem_entry.liquid diff --git a/configs/templates/fhir_cda/section.liquid b/healthchain/configs/templates/fhir_cda/section.liquid similarity index 100% rename from configs/templates/fhir_cda/section.liquid rename to healthchain/configs/templates/fhir_cda/section.liquid diff --git a/healthchain/interop/__init__.py b/healthchain/interop/__init__.py index 1ac2df07..7a7db681 100644 --- a/healthchain/interop/__init__.py +++ b/healthchain/interop/__init__.py @@ -15,20 +15,88 @@ import logging from pathlib import Path -from typing import Optional +from typing import Optional, Union +try: + from importlib import resources +except ImportError: + # Python < 3.9 fallback + try: + import importlib_resources as resources + except ImportError: + resources = None -def create_engine( - config_dir: Optional[Path] = None, + +def _get_bundled_configs() -> Path: + """Get path to bundled default configs. + + Returns: + Path to bundled configuration directory + """ + if resources: + try: + # Modern approach (Python 3.9+) + configs_ref = resources.files("healthchain") / "configs" + if hasattr(resources, "as_file"): + # For Python 3.9+ + with resources.as_file(configs_ref) as config_path: + return Path(config_path) + else: + # For older importlib_resources + return Path(str(configs_ref)) + except Exception: + pass + + # Fallback for development/editable installs + return Path(__file__).parent.parent / "configs" + + +def init_config_templates(target_dir: str = "./healthchain_configs") -> Path: + """Copy default configuration templates to a directory for customization. + + Creates a complete set of customizable configuration files that users can + modify for their specific interoperability needs. + + Args: + target_dir: Directory to create configuration templates in + + Returns: + Path to the created configuration directory + + Raises: + FileExistsError: If target directory already exists + OSError: If unable to copy configuration files + """ + import shutil + + source = _get_bundled_configs() + target = Path(target_dir) + + if target.exists(): + raise FileExistsError(f"Target directory already exists: {target}") + + try: + shutil.copytree(source, target) + print(f"โœ… Configuration templates copied to {target}") + print(f"๐Ÿ“ Customize them, then use: create_interop(config_dir='{target}')") + print("๐Ÿ“š See documentation for configuration options") + return target + except Exception as e: + raise OSError(f"Failed to copy configuration templates: {str(e)}") + + +def create_interop( + config_dir: Optional[Union[str, Path]] = None, validation_level: str = "strict", environment: str = "development", ) -> InteropEngine: """Create and initialize an InteropEngine instance Creates a configured InteropEngine for converting between healthcare data formats. + Automatically discovers configuration files from local directory or bundled defaults. Args: - config_dir: Base directory containing configuration files. If None, defaults to "configs" + config_dir: Base directory containing configuration files. If None, auto-discovers configs validation_level: Level of configuration validation ("strict", "warn", "ignore") environment: Configuration environment to use ("development", "testing", "production") @@ -39,11 +107,17 @@ def create_engine( ValueError: If config_dir doesn't exist or if validation_level/environment has invalid values """ logger = logging.getLogger(__name__) + if config_dir is None: - logger.warning("config_dir is not provided, looking for configs in /configs") - config_dir = Path("configs") - if not config_dir.exists(): - raise ValueError("config_dir does not exist") + # Use bundled configs as default + config_dir = _get_bundled_configs() + logger.info("Using bundled default configs") + else: + # Convert string to Path if needed + config_dir = Path(config_dir) + + if not config_dir.exists(): + raise ValueError(f"Config directory does not exist: {config_dir}") # TODO: Remove this once we have a proper environment system if environment not in ["development", "testing", "production"]: @@ -67,5 +141,7 @@ def create_engine( # Generators "CDAGenerator", "FHIRGenerator", - "create_engine", + # Factory functions + "create_interop", + "init_config_templates", ] diff --git a/healthchain/interop/config_manager.py b/healthchain/interop/config_manager.py index 8f0aba0a..7080330f 100644 --- a/healthchain/interop/config_manager.py +++ b/healthchain/interop/config_manager.py @@ -5,6 +5,7 @@ """ import logging +from pathlib import Path from typing import Dict, Optional, List, Type from pydantic import BaseModel @@ -43,7 +44,7 @@ class InteropConfigManager(ConfigManager): def __init__( self, - config_dir, + config_dir: Path, validation_level: str = ValidationLevel.STRICT, environment: Optional[str] = None, ): diff --git a/healthchain/interop/engine.py b/healthchain/interop/engine.py index ddf71b95..db7258d5 100644 --- a/healthchain/interop/engine.py +++ b/healthchain/interop/engine.py @@ -1,7 +1,7 @@ import logging from functools import cached_property -from typing import List, Union, Optional +from typing import List, Union, Optional, Any from pathlib import Path from fhir.resources.resource import Resource @@ -10,6 +10,8 @@ from healthchain.config.base import ValidationLevel from healthchain.interop.config_manager import InteropConfigManager +from healthchain.interop.generators.base import BaseGenerator +from healthchain.interop.parsers.base import BaseParser from healthchain.interop.types import FormatType, validate_format from healthchain.interop.parsers.cda import CDAParser @@ -191,7 +193,9 @@ def _get_generator(self, format_type: FormatType): return self._generators[format_type] - def register_parser(self, format_type: FormatType, parser_instance): + def register_parser( + self, format_type: FormatType, parser_instance: BaseParser + ) -> "InteropEngine": """Register a custom parser for a format type. This will replace the default parser for the format type. Args: @@ -207,7 +211,9 @@ def register_parser(self, format_type: FormatType, parser_instance): self._parsers[format_type] = parser_instance return self - def register_generator(self, format_type: FormatType, generator_instance): + def register_generator( + self, format_type: FormatType, generator_instance: BaseGenerator + ) -> "InteropEngine": """Register a custom generator for a format type. This will replace the default generator for the format type. Args: @@ -299,7 +305,7 @@ def from_fhir( self, resources: Union[List[Resource], Bundle], dest_format: Union[str, FormatType], - **kwargs, + **kwargs: Any, ) -> str: """Convert FHIR resources to a target format diff --git a/healthchain/interop/generators/cda.py b/healthchain/interop/generators/cda.py index e85a11d5..967f44ca 100644 --- a/healthchain/interop/generators/cda.py +++ b/healthchain/interop/generators/cda.py @@ -9,7 +9,7 @@ import xmltodict import uuid from datetime import datetime -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Any from fhir.resources.resource import Resource from healthchain.interop.models.cda import ClinicalDocument @@ -59,7 +59,7 @@ class CDAGenerator(BaseGenerator): ) """ - def transform(self, resources, **kwargs) -> str: + def transform(self, resources: List[Resource], **kwargs: Any) -> str: """Transform FHIR resources to CDA format. Args: diff --git a/healthchain/interop/generators/fhir.py b/healthchain/interop/generators/fhir.py index 570494f3..40e9926f 100644 --- a/healthchain/interop/generators/fhir.py +++ b/healthchain/interop/generators/fhir.py @@ -6,7 +6,7 @@ import uuid import logging -from typing import Dict, List, Optional, Type +from typing import Dict, List, Optional, Type, Any from fhir.resources.resource import Resource from liquid import Template @@ -42,7 +42,7 @@ class FHIRGenerator(BaseGenerator): ) """ - def transform(self, data, **kwargs) -> str: + def transform(self, data: List[Dict], **kwargs: Any) -> List[Resource]: """Transform input data to FHIR resources. Args: diff --git a/healthchain/interop/template_registry.py b/healthchain/interop/template_registry.py index a7442690..cda58802 100644 --- a/healthchain/interop/template_registry.py +++ b/healthchain/interop/template_registry.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Dict, Callable -from liquid import Environment, FileSystemLoader +from liquid import Environment, FileSystemLoader, Template log = logging.getLogger(__name__) @@ -141,7 +141,7 @@ def _load_templates(self) -> None: log.info(f"Loaded {len(self._templates)} templates") - def get_template(self, template_key: str): + def get_template(self, template_key: str) -> Template: """Get a template by key Args: diff --git a/healthchain/io/adapters/cdaadapter.py b/healthchain/io/adapters/cdaadapter.py index 4dd7b709..cb56e5af 100644 --- a/healthchain/io/adapters/cdaadapter.py +++ b/healthchain/io/adapters/cdaadapter.py @@ -3,7 +3,7 @@ from healthchain.io.containers import Document from healthchain.io.base import BaseAdapter -from healthchain.interop import create_engine, FormatType, InteropEngine +from healthchain.interop import create_interop, FormatType, InteropEngine from healthchain.models.requests.cdarequest import CdaRequest from healthchain.models.responses.cdaresponse import CdaResponse from healthchain.fhir import ( @@ -30,8 +30,7 @@ class CdaAdapter(BaseAdapter[CdaRequest, CdaResponse]): manipulation of the data within HealthChain pipelines. Attributes: - engine (InteropEngine): The interoperability engine for CDA conversions. - If not provided, the default engine is used. + engine (InteropEngine): The interoperability engine for CDA conversions. If not provided, the default engine is used. original_cda (str): The original CDA document for use in output. note_document_reference (DocumentReference): Reference to the note document extracted from the CDA. @@ -50,7 +49,7 @@ def __init__(self, engine: Optional[InteropEngine] = None): If None, creates a default engine. """ # Initialize engine with default if not provided - initialized_engine = engine or create_engine() + initialized_engine = engine or create_interop() super().__init__(engine=initialized_engine) self.engine = initialized_engine self.original_cda = None diff --git a/mkdocs.yml b/mkdocs.yml index 9f76e91f..90c13394 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,7 +53,7 @@ nav: - Working with xmltodict: reference/interop/xmltodict.md - Utilities: - FHIR Helpers: reference/utilities/fhir_helpers.md - - Sandbox: reference/sandbox/sandbox.md + - Sandbox: reference/utilities/sandbox.md - Data Generator: reference/utilities/data_generator.md - API Reference: - api/index.md diff --git a/pyproject.toml b/pyproject.toml index c2a679f4..ce33499b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "healthchain" version = "0.0.0" -description = "Remarkably simple testing and validation of AI/NLP applications in healthcare context." +description = "Python toolkit that makes it easier to connect your AI/ML pipelines to healthcare systems" authors = ["Jennifer Jiang-Kells ", "Adam Kells "] license = "Apache-2.0" readme = "README.md" @@ -16,7 +16,11 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering :: Artificial Intelligence", ] -include = ["healthchain/templates/*"] +include = [ + "healthchain/templates/*", + "healthchain/configs/**/*.yaml", + "healthchain/configs/**/*.liquid" +] [project.urls] "Homepage" = "https://dotimplement.github.io/HealthChain/" diff --git a/tests/integration_tests/test_interop_engine_integration.py b/tests/integration_tests/test_interop_engine_integration.py index 86fc7bc2..c67a6c91 100644 --- a/tests/integration_tests/test_interop_engine_integration.py +++ b/tests/integration_tests/test_interop_engine_integration.py @@ -4,7 +4,8 @@ from fhir.resources.condition import Condition from fhir.resources.medicationstatement import MedicationStatement -from fhir.resources.allergyintolerance import AllergyIntolerance + +# from fhir.resources.allergyintolerance import AllergyIntolerance # Removed from bundled configs from fhir.resources.documentreference import DocumentReference from healthchain.interop.engine import InteropEngine @@ -35,7 +36,7 @@ def test_cda_to_fhir_conversion(interop_engine, test_cda_xml): This test verifies that: - The engine successfully converts CDA to FHIR resources - - The expected resource types (Condition, MedicationStatement, AllergyIntolerance) are present + - The expected resource types (Condition, MedicationStatement) are present - The FHIR resources contain the correct data from the source CDA document """ # Convert CDA to FHIR @@ -48,12 +49,12 @@ def test_cda_to_fhir_conversion(interop_engine, test_cda_xml): # Check individual resources conditions = [r for r in resources if isinstance(r, Condition)] medications = [r for r in resources if isinstance(r, MedicationStatement)] - allergies = [r for r in resources if isinstance(r, AllergyIntolerance)] + # allergies = [r for r in resources if isinstance(r, AllergyIntolerance)] # Removed from bundled configs notes = [r for r in resources if isinstance(r, DocumentReference)] assert len(conditions) > 0 assert len(medications) > 0 - assert len(allergies) > 0 + # assert len(allergies) > 0 # Removed from bundled configs assert len(notes) > 0 # Verify specific data in the resources @@ -100,30 +101,31 @@ def test_cda_to_fhir_conversion(interop_engine, test_cda_xml): assert medication.dosage[0].doseAndRate[0].doseQuantity.unit == "mg" assert medication.effectivePeriod.end - # Allergy - allergy = allergies[0] - assert "dev-" in allergy.id - assert allergy.patient.reference == "Patient/Foo" - # TODO: fix this!! - # assert allergy.clinicalStatus.coding[0].code == "active" - assert ( - allergy.clinicalStatus.coding[0].system - == "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical" - ) - assert allergy.type.coding[0].code == "418471000" - assert allergy.type.coding[0].display == "Propensity to adverse reactions to food" - assert allergy.type.coding[0].system == "http://snomed.info/sct" - assert allergy.code.coding[0].code == "102263004" - assert allergy.code.coding[0].display == "EGGS" - assert allergy.code.coding[0].system == "http://snomed.info/sct" - assert allergy.reaction[0].manifestation[0].concept.coding[0].code == "65124004" - assert allergy.reaction[0].manifestation[0].concept.coding[0].display == "Swelling" - assert ( - allergy.reaction[0].manifestation[0].concept.coding[0].system - == "http://snomed.info/sct" - ) - assert allergy.reaction[0].severity == "severe" - assert allergy.onsetDateTime + # Allergy tests removed - allergies not in bundled configs due to known bugs + # See dev-templates/allergies/ for experimental allergy support + # allergy = allergies[0] + # assert "dev-" in allergy.id + # assert allergy.patient.reference == "Patient/Foo" + # # TODO: fix this!! + # # assert allergy.clinicalStatus.coding[0].code == "active" + # assert ( + # allergy.clinicalStatus.coding[0].system + # == "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical" + # ) + # assert allergy.type.coding[0].code == "418471000" + # assert allergy.type.coding[0].display == "Propensity to adverse reactions to food" + # assert allergy.type.coding[0].system == "http://snomed.info/sct" + # assert allergy.code.coding[0].code == "102263004" + # assert allergy.code.coding[0].display == "EGGS" + # assert allergy.code.coding[0].system == "http://snomed.info/sct" + # assert allergy.reaction[0].manifestation[0].concept.coding[0].code == "65124004" + # assert allergy.reaction[0].manifestation[0].concept.coding[0].display == "Swelling" + # assert ( + # allergy.reaction[0].manifestation[0].concept.coding[0].system + # == "http://snomed.info/sct" + # ) + # assert allergy.reaction[0].severity == "severe" + # assert allergy.onsetDateTime # Note note = notes[0] @@ -165,7 +167,7 @@ def test_fhir_to_cda_conversion(interop_engine, test_cda_xml): # Check for common elements that should be in the CDA assert "Problem List" in cda assert "Medications" in cda - assert "Allergies" in cda + # assert "Allergies" in cda # Removed from bundled configs assert "Progress Notes" in cda assert '' in cda @@ -196,26 +198,26 @@ def test_fhir_to_cda_conversion(interop_engine, test_cda_xml): '' in cda ) - assert ( - '' - in cda - ) - - assert ( - 'code="102263004" codeSystem="2.16.840.1.113883.6.96" displayName="EGGS"' in cda - ) - assert ( - 'code="65124004" codeSystem="2.16.840.1.113883.6.96" displayName="Swelling"' - in cda - ) - assert ( - '' - in cda - ) - assert ( - 'code="H" codeSystem="2.16.840.1.113883.5.1063" codeSystemName="SeverityObservation" displayName="H"' - in cda - ) + # assert ( + # '' + # in cda + # ) + + # assert ( + # 'code="102263004" codeSystem="2.16.840.1.113883.6.96" displayName="EGGS"' in cda + # ) + # assert ( + # 'code="65124004" codeSystem="2.16.840.1.113883.6.96" displayName="Swelling"' + # in cda + # ) + # assert ( + # '' + # in cda + # ) + # assert ( + # 'code="H" codeSystem="2.16.840.1.113883.5.1063" codeSystemName="SeverityObservation" displayName="H"' + # in cda + # ) assert "CDATA" in cda assert "test" in cda @@ -258,10 +260,8 @@ def test_round_trip_equivalence(interop_engine, test_cda_xml): == original_medications[0].medication.concept.coding[0].code ) - # TODO: allergy intolerance reverse parsing isn't quite right look into this - # print(resources_result[2].model_dump_json(indent=2)) - # print(resources[2].model_dump_json(indent=2)) - # assert resources_result[2].code.coding[0].code == resources[2].code.coding[0].code + # Note: Allergy tests removed - allergies not in bundled configs due to known bugs + # See dev-templates/allergies/ for experimental allergy support def test_cda_adapter_with_interop_engine( @@ -281,7 +281,7 @@ def test_cda_adapter_with_interop_engine( # Verify FHIR resources were extracted assert len(result.fhir.problem_list) == 1 assert len(result.fhir.medication_list) == 1 - assert len(result.fhir.allergy_list) == 1 + # assert len(result.fhir.allergy_list) == 1 # Removed from bundled configs # Verify types of extracted resources assert result.fhir.problem_list[0].code.coding[0].code == "38341003" @@ -289,7 +289,7 @@ def test_cda_adapter_with_interop_engine( result.fhir.problem_list[0].category[0].coding[0].code == "problem-list-item" ) # Should be set by the adapter assert result.fhir.medication_list[0].medication.concept.coding[0].code == "314076" - assert result.fhir.allergy_list[0].code.coding[0].code == "102263004" + # assert result.fhir.allergy_list[0].code.coding[0].code == "102263004" # Removed from bundled configs # Check document references assert result.data == "test" @@ -329,7 +329,7 @@ def test_cda_adapter_with_interop_engine( assert "Test Condition" in response.document # New problem assert "Medications" in response.document assert "314076" in response.document - assert "Allergies" in response.document - assert "102263004" in response.document + # assert "Allergies" in response.document # Removed from bundled configs + # assert "102263004" in response.document # Removed from bundled configs assert "Progress Notes" in response.document assert "test" in response.document diff --git a/tests/interop/test_init_functions.py b/tests/interop/test_init_functions.py new file mode 100644 index 00000000..5c772eb0 --- /dev/null +++ b/tests/interop/test_init_functions.py @@ -0,0 +1,101 @@ +"""Tests for interop initialization functions.""" + +import pytest +import tempfile +from pathlib import Path +from unittest.mock import Mock, patch + +from healthchain.interop import init_config_templates, create_interop +from healthchain.interop.engine import InteropEngine + + +def test_init_config_templates_prevents_overwriting_existing_configs(): + """init_config_templates prevents accidentally overwriting existing configuration.""" + with tempfile.TemporaryDirectory() as temp_dir: + target_dir = Path(temp_dir) / "existing" + target_dir.mkdir() # Create directory first + + with pytest.raises(FileExistsError, match="Target directory already exists"): + init_config_templates(str(target_dir)) + + +def test_init_config_templates_creates_customizable_config_structure(): + """init_config_templates creates complete configuration structure for user customization.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create minimal mock source structure + source_dir = Path(temp_dir) / "source" + source_dir.mkdir() + (source_dir / "defaults.yaml").write_text("version: 1.0") + + target_dir = Path(temp_dir) / "target" + + with patch("healthchain.interop._get_bundled_configs", return_value=source_dir): + result = init_config_templates(str(target_dir)) + + # Verify structure is created and files are copied + assert result == target_dir + assert target_dir.exists() + assert (target_dir / "defaults.yaml").exists() + + +def test_init_config_templates_handles_copy_failures_gracefully(): + """init_config_templates provides clear error message when copy operation fails.""" + with tempfile.TemporaryDirectory() as temp_dir: + nonexistent_source = Path(temp_dir) / "nonexistent" + target_dir = Path(temp_dir) / "target" + + with patch( + "healthchain.interop._get_bundled_configs", return_value=nonexistent_source + ): + with pytest.raises(OSError, match="Failed to copy configuration templates"): + init_config_templates(str(target_dir)) + + +@pytest.mark.parametrize("environment", ["invalid_env", "staging", "local"]) +def test_create_interop_validates_environment_parameter(environment): + """create_interop enforces valid environment values for configuration consistency.""" + with tempfile.TemporaryDirectory() as temp_dir: + with pytest.raises(ValueError, match="environment must be one of"): + create_interop(config_dir=temp_dir, environment=environment) + + +def test_create_interop_rejects_nonexistent_config_directory(): + """create_interop validates config directory exists before engine creation.""" + nonexistent_dir = Path("/nonexistent/path") + + with pytest.raises(ValueError, match="Config directory does not exist"): + create_interop(config_dir=nonexistent_dir) + + +@patch("healthchain.interop.InteropEngine") +def test_create_interop_supports_custom_validation_and_environment_settings( + mock_engine_class, +): + """create_interop passes validation and environment settings to engine for proper configuration.""" + with tempfile.TemporaryDirectory() as temp_dir: + mock_engine = Mock(spec=InteropEngine) + mock_engine_class.return_value = mock_engine + + result = create_interop( + config_dir=temp_dir, validation_level="warn", environment="testing" + ) + + # Verify configuration is passed correctly + mock_engine_class.assert_called_once_with(Path(temp_dir), "warn", "testing") + assert result == mock_engine + + +@patch("healthchain.interop.InteropEngine") +def test_create_interop_auto_discovers_configuration_when_none_specified( + mock_engine_class, +): + """create_interop automatically finds and uses available configuration when no config_dir provided.""" + mock_engine = Mock(spec=InteropEngine) + mock_engine_class.return_value = mock_engine + + # Should successfully create engine with auto-discovered configs + result = create_interop() + + # Verify engine was created (discovery mechanism is implementation detail) + mock_engine_class.assert_called_once() + assert result == mock_engine diff --git a/tests/pipeline/prebuilt/test_medicalcoding.py b/tests/pipeline/prebuilt/test_medicalcoding.py index 853db091..75fe2da0 100644 --- a/tests/pipeline/prebuilt/test_medicalcoding.py +++ b/tests/pipeline/prebuilt/test_medicalcoding.py @@ -96,4 +96,4 @@ def test_full_coding_pipeline_integration(mock_spacy_nlp, test_cda_request): assert "Aspirin" in cda_response.document assert "Hypertension" in cda_response.document - assert "Allergy to peanuts" in cda_response.document + # assert "Allergy to peanuts" in cda_response.document diff --git a/tests/pipeline/test_cdaadapter.py b/tests/pipeline/test_cdaadapter.py index 26085ab9..d9fdaf5d 100644 --- a/tests/pipeline/test_cdaadapter.py +++ b/tests/pipeline/test_cdaadapter.py @@ -13,7 +13,7 @@ def cda_adapter(): return CdaAdapter() -@patch("healthchain.io.adapters.cdaadapter.create_engine") +@patch("healthchain.io.adapters.cdaadapter.create_interop") @patch("healthchain.io.adapters.cdaadapter.create_document_reference") @patch("healthchain.io.adapters.cdaadapter.read_content_attachment") @patch("healthchain.io.adapters.cdaadapter.set_problem_list_item_category") @@ -23,7 +23,7 @@ def test_parse( mock_set_problem_category, mock_read_content, mock_create_doc_ref, - mock_create_engine, + mock_create_interop, cda_adapter, test_condition, test_medication, @@ -31,7 +31,7 @@ def test_parse( ): # Create mock engine mock_engine = Mock() - mock_create_engine.return_value = mock_engine + mock_create_interop.return_value = mock_engine # Mock document reference content extraction mock_read_content.return_value = [{"data": "Extracted note text"}] @@ -116,13 +116,13 @@ def test_parse( assert result is mock_doc -@patch("healthchain.io.adapters.cdaadapter.create_engine") +@patch("healthchain.io.adapters.cdaadapter.create_interop") def test_format( - mock_create_engine, cda_adapter, test_condition, test_medication, test_allergy + mock_create_interop, cda_adapter, test_condition, test_medication, test_allergy ): # Create mock engine mock_engine = Mock() - mock_create_engine.return_value = mock_engine + mock_create_interop.return_value = mock_engine # Configure mock engine to return CDA XML mock_engine.from_fhir.return_value = "Updated CDA" diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..8051012e --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,95 @@ +"""Tests for HealthChain CLI functionality.""" + +import pytest +import subprocess +from unittest.mock import patch + +from healthchain.cli import init_configs, run_file, main + + +@pytest.mark.parametrize( + "error,expected_messages", + [ + ( + FileExistsError("Directory already exists"), + [ + "โŒ Error: Directory already exists", + "๐Ÿ’ก Tip: Choose a different directory name or remove the existing one", + ], + ), + ( + Exception("Something went wrong"), + [ + "โŒ Error initializing configs: Something went wrong", + "๐Ÿ’ก Tip: Make sure HealthChain is properly installed", + ], + ), + ], +) +@patch("healthchain.interop.init_config_templates") +def test_init_configs_error_handling_provides_helpful_guidance( + mock_init_templates, error, expected_messages +): + """init_configs provides helpful error messages and guidance when template creation fails.""" + mock_init_templates.side_effect = error + + with patch("builtins.print") as mock_print: + init_configs("./test_configs") + + # Verify helpful error messages are displayed + for expected_msg in expected_messages: + assert any(expected_msg in str(call) for call in mock_print.call_args_list) + + +@patch("healthchain.interop.init_config_templates") +def test_init_configs_success_provides_usage_instructions(mock_init_templates): + """init_configs provides clear usage instructions when successful.""" + target_dir = "./test_configs" + mock_init_templates.return_value = target_dir + + with patch("builtins.print") as mock_print: + init_configs(target_dir) + + # Verify success message and usage instructions are provided + print_output = " ".join(str(call) for call in mock_print.call_args_list) + assert "๐ŸŽ‰ Success!" in print_output + assert "create_interop(config_dir=" in print_output + assert "๐Ÿ“– Next steps:" in print_output + + +@patch("subprocess.run") +def test_run_file_handles_execution_errors_gracefully(mock_run): + """run_file provides clear error message when script execution fails.""" + mock_run.side_effect = subprocess.CalledProcessError(1, "poetry") + + with patch("builtins.print") as mock_print: + run_file("failing_script.py") + + # Verify error message is informative + error_message = mock_print.call_args[0][0] + assert "An error occurred while trying to run the file:" in error_message + + +@pytest.mark.parametrize( + "args,expected_call", + [ + (["healthchain", "run", "test.py"], ("run_file", "test.py")), + (["healthchain", "init-configs", "my_configs"], ("init_configs", "my_configs")), + (["healthchain", "init-configs"], ("init_configs", "./healthchain_configs")), + ], +) +def test_main_routes_commands_correctly(args, expected_call): + """Main function correctly routes CLI commands to appropriate handlers.""" + function_name, expected_arg = expected_call + + with patch(f"healthchain.cli.{function_name}") as mock_function: + with patch("sys.argv", args): + main() + mock_function.assert_called_once_with(expected_arg) + + +def test_main_requires_command_argument(): + """Main function enforces required command argument.""" + with patch("sys.argv", ["healthchain"]): + with pytest.raises(SystemExit): + main()