Skip to content
Open
1 change: 1 addition & 0 deletions docs/api/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ We implement the following models for supporting multiple healthcare predictive
:maxdepth: 3

models/pyhealth.models.BaseModel
models/pyhealth.models.BHCToAVS
models/pyhealth.models.LogisticRegression
models/pyhealth.models.MLP
models/pyhealth.models.CNN
Expand Down
11 changes: 11 additions & 0 deletions docs/api/models/pyhealth.models.BHCToAVS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pyhealth.models.BHCToAVS
========================

BHCToAVS
------------------------------

.. autoclass:: pyhealth.models.bhc_to_avs.BHCToAVS
:members:
:inherited-members:
:show-inheritance:
:undoc-members:
21 changes: 21 additions & 0 deletions examples/bhc_to_avs_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pyhealth.models.bhc_to_avs import BHCToAVS

# Initialize the model
model = BHCToAVS()

# Example Brief Hospital Course (BHC) text with common clinical abbreviations generated synthetically via ChatGPT 5.1
bhc = (
"Pt admitted with acute onset severe epigastric pain and hypotension. "
"Labs notable for elevated lactate, WBC 18K, mild AST/ALT elevation, and Cr 1.4 (baseline 0.9). "
"CT A/P w/ contrast demonstrated peripancreatic fat stranding c/w acute pancreatitis; "
"no necrosis or peripancreatic fluid collection. "
"Pt received aggressive IVFs, electrolyte repletion, IV analgesia, and NPO status initially. "
"Serial abd exams remained benign with no rebound or guarding. "
"BP stabilized, lactate downtrended, and pt tolerated ADAT to low-fat diet without recurrence of sx. "
"Discharged in stable condition w/ instructions for GI f/u and outpatient CMP in 1 week."
)

# Generate a patient-friendly After-Visit Summary
print(model.predict(bhc))

# Expected output: A simplified, patient-friendly summary explaining the hospital stay without medical jargon.
3 changes: 2 additions & 1 deletion pyhealth/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .adacare import AdaCare, AdaCareLayer
from .agent import Agent, AgentLayer
from .base_model import BaseModel
from .bhc_to_avs import BHCToAVS
from .cnn import CNN, CNNLayer
from .concare import ConCare, ConCareLayer
from .contrawr import ContraWR, ResBlock2D
Expand All @@ -26,4 +27,4 @@
from .transformer import Transformer, TransformerLayer
from .transformers_model import TransformersModel
from .vae import VAE
from .sdoh import SdohClassifier
from .sdoh import SdohClassifier
155 changes: 155 additions & 0 deletions pyhealth/models/bhc_to_avs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
BHC to AVS Model

Generates patient-friendly After Visit Summaries (AVS) from Brief Hospital Course (BHC)
notes using a fine-tuned Mistral 7B model with a LoRA adapter.

This model requires access to a gated Hugging Face repository. Provide credentials
using one of the following methods:

1. Set an environment variable:
export HF_TOKEN="hf_..."

2. Pass the token explicitly when creating the model:
model = BHCToAVS(hf_token="hf_...")

If no token is provided and the repository is gated, a RuntimeError will be raised.
"""

# Author: Charan Williams
# NetID: charanw2


from dataclasses import dataclass, field
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
from peft import PeftModelForCausalLM
from pyhealth.models.base_model import BaseModel

# System prompt used during inference
_SYSTEM_PROMPT = (
"You are a clinical summarization model. Produce accurate, patient-friendly summaries "
"using only information from the doctor's note. Do not add new details.\n\n"
)

# Prompt used during fine-tuning
_PROMPT = (
"Summarize for the patient what happened during the hospital stay based on this doctor's note:\n"
"{bhc}\n\n"
"Summary for the patient:\n"
)
Comment on lines +36 to +41
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 36 states "Prompt used during fine-tuning" but this prompt is actually used during inference (as seen on line 146). If this prompt was indeed used during fine-tuning and is also being reused during inference, the comment should clarify this. If it's only used during inference, the comment is misleading and should be corrected to "Prompt template used during inference" or similar.

Copilot uses AI. Check for mistakes.


@dataclass
class BHCToAVS(BaseModel):
"""
BHCToAVS is a model class designed to generate After-Visit Summaries (AVS) from
Brief Hospital Course (BHC) notes using a pre-trained base model and a LoRA adapter.

Attributes:
base_model_id (str): The HuggingFace repository identifier for the base
Mistral 7B model.
adapter_model_id (str): The HuggingFace repository identifier for the LoRA
adapter weights.
hf_token (str | None): HuggingFace access token for gated repositories.

Methods:
_get_pipeline(): Creates and caches a HuggingFace text-generation pipeline
using the base model and LoRA adapter.
predict(bhc_text: str) -> str: Generates a patient-friendly After-Visit
Summary (AVS) from a given Brief Hospital Course (BHC) note.
"""

base_model_id: str = field(default="mistralai/Mistral-7B-Instruct-v0.3")
adapter_model_id: str = field(default="williach31/mistral-7b-bhc-to-avs-lora")
hf_token: str | None = None
Comment on lines +64 to +66
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BHCToAVS model deviates from the standard PyHealth model API pattern. Most models in the codebase (e.g., LogisticRegression, AdaCare, GAMENet) accept a dataset parameter in their initialization to query information like tokens and schemas. This model does not accept or use a dataset parameter, which is inconsistent with the BaseModel interface it inherits from. Consider whether this model should follow the standard pattern or if the deviation is intentional for this use case. If intentional, this should be clearly documented.

Copilot uses AI. Check for mistakes.

def __post_init__(self):
# Ensure nn.Module (via BaseModel) is initialized
super().__init__()

Comment on lines +68 to +71
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BHCToAVS class uses a dataclass decorator but also inherits from BaseModel (which is an nn.Module). The post_init method calls super().init(), but this pattern is inconsistent with how BaseModel is typically used in the codebase. Looking at SdohClassifier (another dataclass-based model in the same codebase), it does not override post_init. Additionally, BaseModel's init expects an optional dataset parameter, but this implementation calls it without any arguments. This could lead to incorrect initialization. Consider removing post_init entirely or ensuring it properly initializes the BaseModel with appropriate parameters.

Suggested change
def __post_init__(self):
# Ensure nn.Module (via BaseModel) is initialized
super().__init__()

Copilot uses AI. Check for mistakes.
def _resolve_token(self):
return self.hf_token or os.getenv("HF_TOKEN")

def _get_pipeline(self):
"""Create and cache the text-generation pipeline."""
if not hasattr(self, "_pipeline"):
# Resolve HuggingFace token
token = self._resolve_token()

# Throw RuntimeError if token is not found
if token is None:
raise RuntimeError(
"Hugging Face token not found. This model requires access to a gated repository.\n\n"
"Set the HF_TOKEN environment variable or pass hf_token=... when initializing BHCToAVS.\n\n"
"Example:\n"
" export HF_TOKEN='hf_...'\n"
" model = BHCToAVS()\n"
)
Comment on lines +83 to +89
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message spans lines 87-92 and provides good guidance, but the message could be more specific about where to obtain a HuggingFace token. Consider adding a link to the HuggingFace token generation page (https://huggingface.co/settings/tokens) to help users quickly resolve this issue.

Copilot uses AI. Check for mistakes.

# Load base model
base = AutoModelForCausalLM.from_pretrained(
self.base_model_id,
torch_dtype=torch.bfloat16,
device_map="auto",
token=token,
)

# Load LoRA adapter
model = PeftModelForCausalLM.from_pretrained(
base,
self.adapter_model_id,
torch_dtype=torch.bfloat16,
token=token,
)

tokenizer = AutoTokenizer.from_pretrained(self.base_model_id, token=token)

# Create HF pipeline
self._pipeline = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
model_kwargs={"torch_dtype": torch.bfloat16},
)

return self._pipeline
Comment on lines +75 to +117
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _get_pipeline method loads a large 7B parameter model without any explicit guidance on resource requirements or expected load time. Consider adding documentation (either in the class docstring or method docstring) about: (1) expected memory requirements (GPU/CPU), (2) approximate model loading time, and (3) recommended hardware specifications. This would help users understand the resource implications before attempting to use the model.

Copilot uses AI. Check for mistakes.

def predict(self, bhc_text: str) -> str:
"""
Generate an After-Visit Summary (AVS) from a Brief Hospital Course (BHC) note.

Parameters
----------
bhc_text : str
Raw BHC text.

Returns
-------
str
Patient-friendly summary.
"""

# Validate input to provide clear error messages and avoid unexpected failures.
if not isinstance(bhc_text, str):
raise TypeError(
f"bhc_text must be a string, got {type(bhc_text).__name__}."
)
if not bhc_text.strip():
raise ValueError("bhc_text must be a non-empty string.")
prompt = _SYSTEM_PROMPT + _PROMPT.format(bhc=bhc_text)

pipe = self._get_pipeline()
eos_id = pipe.tokenizer.eos_token_id
outputs = pipe(
prompt,
max_new_tokens=512,
temperature=0.0,
eos_token_id=eos_id,
pad_token_id=eos_id,
return_full_text=False,
)

# Output is a single text string
return outputs[0]["generated_text"].strip()
96 changes: 96 additions & 0 deletions tests/core/test_bhc_to_avs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
Unit tests for the BHCToAVS model.

These tests validate both the unit-level behavior of the predict method
(using a mocked pipeline) and an optional integration path that runs
against the real Hugging Face model when credentials are provided.
"""

import os
import unittest
from unittest.mock import patch

from tests.base import BaseTestCase
from pyhealth.models.bhc_to_avs import BHCToAVS


class _DummyPipeline:
"""
Lightweight mock pipeline used to simulate Hugging Face text generation.

This avoids downloading models or requiring authentication during unit tests.
"""

def __call__(self, prompt, **kwargs):
"""Return a fixed, deterministic generated response."""
return [
{
"generated_text": "Your pain improved with supportive care and you were discharged in good condition."
}
]


class TestBHCToAVS(BaseTestCase):
"""Unit and integration tests for the BHCToAVS model."""

def setUp(self):
"""Set a deterministic random seed before each test."""
self.set_random_seed()

def test_predict_unit(self):
"""
Test the predict method using a mocked pipeline.

This test verifies that:
- The model returns a string output
- The output is non-empty
- The output differs from the input text
"""

bhc_text = (
"Patient admitted with abdominal pain. Imaging showed no acute findings. "
"Pain improved with supportive care and the patient was discharged in stable condition."
)

with patch.object(BHCToAVS, "_get_pipeline", return_value=_DummyPipeline()):
model = BHCToAVS()
summary = model.predict(bhc_text)

# Output must be type str
self.assertIsInstance(summary, str)

# Output should not be empty
self.assertGreater(len(summary.strip()), 0)

# Output should be different from input
self.assertNotIn(bhc_text[:40], summary)
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assertion on line 66 uses a weak check that only verifies the first 40 characters of the input are not present in the output. This could pass even if the model is simply copying most of the input text or behaving incorrectly. Consider adding more robust assertions that verify the output actually represents a simplified, patient-friendly summary (e.g., checking for absence of medical jargon, checking for specific expected transformations, or verifying the output differs meaningfully from the input).

Copilot uses AI. Check for mistakes.

@unittest.skipUnless(
os.getenv("RUN_BHC_TO_AVS_INTEGRATION") == "1" and os.getenv("HF_TOKEN"),
"Integration test disabled. Set RUN_BHC_TO_AVS_INTEGRATION=1 and HF_TOKEN to enable.",
)
def test_predict_integration(self):
"""
Integration test for the BHCToAVS model.

This test runs the full inference pipeline using the real Hugging Face model.
It requires the HF_TOKEN environment variable to be set and is skipped by default.
"""

# For Mistral weights, you will need HF_TOKEN set in the environment.
bhc_text = (
"Patient admitted with abdominal pain. Imaging showed no acute findings. "
"Pain improved with supportive care and the patient was discharged in stable condition."
)

model = BHCToAVS()
summary = model.predict(bhc_text)

# Output must be type str
self.assertIsInstance(summary, str)

# Output should not be empty
self.assertGreater(len(summary.strip()), 0)

# Output should be different from input
self.assertNotIn(bhc_text[:40], summary)
Loading