21.0.1CDS Hooks
Trial

 

CDS Hooks are services called by CDS Clients (typically Electronic Health Record Systems (EHRs) or other health information systems). They implement a "hook"-based pattern for invoking decision support from within a clinician's workflow.

Smile CDR implements Version 1.1 of the CDS Hooks Specification.

The Smile CDR CDS Hooks Module simplifies the effort for creating CDS Hooks. All you need to do is create a method that accepts a CdsServiceRequestJson parameter and returns a CdsServiceResponseJson value and annotate this method with the @CdsService annotation. This annotation and the Json classes and all their subcomponents are available in the open-source project called cdr-api-public. Any FHIR resources in requests and responses are automatically serialized into hapi-fhir FHIR resource instances for you, so they are easy to work with within your code.

In addition to simplifying the effort to build CDS Hooks, the Smile CDR CDS Hooks module also provides the following:

  • All access is logged in the Smile CDR Audit Trail.
  • Authorization is controlled by the Smile CDR security framework.
  • Management and monitoring capabilities are provided by the Smile CDR platform.
  • CDS on FHIR implementation that auto-generates CDS Services from PlanDefinitions and executes via the $apply operation.

21.0.2Auto Prefetch
Trial

 

The Smile CDR CDS Hooks module provides a couple of powerful Auto-Prefetch features:

  1. If allowAutoFhirClientPrefetch is set to true in the @CdsService annotation on your CDS Service method, then before calling your method, Smile CDR will compare the prefetch elements declared by your service method in the @CdsService annotation to the prefetch elements included within the CdsServiceRequestJson REST request and if it detects any are missing, then Smile CDR will use the FHIR endpoint authorization details included within the fhirAuthorization element in the request to automatically add them to the prefetch before calling your method.
  2. Even simpler, if your Smile CDR server has a FHIR Storage module, you can optionally add a dependency from your CDS Hooks Module on your FHIR Storage module. If you do this, then when Smile CDR detects any required prefetch elements missing in a request, it will automatically fetch the missing data from your storage module before calling your CDS Hooks method. Note in this case, the same credentials used to call the CDS Hooks endpoint are used to authorize access to the FHIR Storage module.

21.0.2.1CDS Hooks Auto Prefetch Rules

  • If there are no missing prefetch elements, the CDS Hooks service method is called directly with the request. (Note that per the CDS Hooks specification, a value of null is not considered to be missing. CDS Hooks clients set a prefetch value to null to indicate that this prefetch data is known to not exist).
  • Otherwise, if a fhirServer is included in the request
    • If the @CdsService annotation on the service method has allowAutoFhirClientPrefetch = true, then Smile CDR will perform a FHIR REST call to that fhirServer endpoint to fetch the missing data.
    • otherwise, the CDS Hooks service method is expected to call the fhirServer endpoint itself to retrieve the missing data.
  • Otherwise, if the CDS Hooks Module declares a dependency on a FHIR Storage Module, then Smile CDR will fetch the missing data from that FHIR Storage Module.
  • Otherwise, the method will fail with HTTP 412 PRECONDITION FAILED (per the CDS Hooks specification).
  • The Auto-Prefetch rules can be overridden for individual elements by setting a source for the @CdsServicePrefetch. Smile CDR will attempt to use the source strategy for the query instead of following the order above.

21.0.3Architecture
Trial

 

The diagram below shows how CDS Hooks work. The box in grey contains customer code, which is code that you write.

CDS Hooks Architecture

A CDS Hooks implementation is packaged as a Java JAR file that contains several key components:

  • CDS Service classes, which implement CDS Hooks service and feedback methods.
  • A Spring Context Config class, which is a Spring Framework class used to instantiate and configure the CDS Hooks classes.

21.0.4CDS Hooks Classes
Trial

 

A CDS Hooks class contains annotated service and feedback methods. One CDS Hooks class can contain any number of these methods. A CDS Hooks service method is annotated with the @CdsService annotation and a CDS Hooks feedback method is annotated with the @CdsServiceFeedback annotation. The "value" of these annotations corresponds to the id of the CDS Hooks service. For example:

A method annotated with @CdsService(value="example-service") is accessed at a path like https://example.com:8888/cds-services/example-service

A method annotated with @CdsServiceFeedback(value="my-service") is accessed ata path like https://example.com:8888/cds-services/my-service/feedback.

A very basic example is shown below:

package ca.cdr.endpoint.cdshooks.module;

import ca.uhn.hapi.fhir.cdshooks.api.CdsService;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServiceFeedback;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServicePrefetch;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceAcceptedSuggestionJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceIndicatorEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardSourceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import org.hl7.fhir.r4.model.Patient;

import java.util.List;
import java.util.UUID;

public class ExampleCdsService {
   @CdsService(value = "example-service",
      hook = "patient-view",
      title = "Greet a patient",
      description = "This service says hello to a patient",
      prefetch = {
         @CdsServicePrefetch(value = "patient", query = "Patient/{{context.patientId}}")
      })
   public CdsServiceResponseJson exampleService(CdsServiceRequestJson theCdsRequest) {
      Patient patient = (Patient) theCdsRequest.getPrefetch("patient");
      CdsServiceResponseJson response = new CdsServiceResponseJson();
      CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
      card.setSummary("Hello " + patient.getNameFirstRep().getNameAsSingleString());
      card.setIndicator(CdsServiceIndicatorEnum.INFO);
      CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson();
      source.setLabel("Smile CDR");
      card.setSource(source);
      response.addCard(card);
      return response;
   }

   @CdsServiceFeedback("example-service")
   public CdsServiceFeedbackJson feedback(CdsServiceFeedbackJson theFeedback) {
      theFeedback.setAcceptedSuggestions(List.of(new CdsServiceAcceptedSuggestionJson().setId(UUID.randomUUID().toString())));
      return theFeedback;
   }
}

Both of these example methods accept a single json instance parameter (CdsServiceRequestJson and CdsServiceFeedbackJson respectively). Alternatively, these methods can accept a single String parameter in which case the CDS Hooks module will string-encode the instance before calling the method.

21.0.5The Spring Context Config Class
Trial

 

This mandatory class is a Spring Framework Annotation-based Application Context Config class. It is characterized by having the @Configuration annotation on the class itself, as well as having one or more non-static factory methods annotated with the @Bean method, which create instances of your providers (as well as creating any other utility classes you might need, such as database pools, HTTP clients, etc.).

This class must instantiate a bean named cdsServices:

  • The cdsServices bean method should return a List<Object> of classes that contain @CdsService and/or @CdsServiceFeedback annotated methods.

The following example shows a Spring Context Config class that registers the CDS Hooks example above.

@Configuration
public class TestServerAppCtx {

   /**
    * This bean is a list of CDS Hooks classes, each one
    * of which implements one or more CDS-Hook Services.
    */
   @Bean(name = "cdsServices")
   public List<Object> cdsServices(){
      List<Object> retVal = new ArrayList<>();
      retVal.add(new ExampleCdsService());
// add other CDS Hooks classes...
      return retVal;
   }
}

21.0.6CDS Hooks Extensions
Trial

 

The CDS Hooks specification introduces Extensions as a part of CDS hooks request and response. Also, a CDS service should also be able to expose which Extension it supports on the Discovery endpoint. In order to support extensions for your implementation, follow the steps:

  1. Create your custom class extending CdsHooksExtension. A sample is shown below:
public class MeasurementUnitExtension extends CdsHooksExtension {

   @JsonProperty("age-measurement-unit")
   private String myAgeMeasurementUnit;

   public String getAgeMeasurementUnit() {
      return myAgeMeasurementUnit;
   }

   public void setAgeMeasurementUnit(String theAgeMeasurementUnit) {
      myAgeMeasurementUnit = theAgeMeasurementUnit;
   }
}
  1. Define your CDS service, which specifies the Extension your CDS service supports and the custom class. The custom class is used for deserialization of the incoming CDS hook request Extension. A sample CDS service declaration is shown below alongside usage of CDS hooks request Extension to manipulate the CDS hooks response:
@CdsService(
      value = SERVICE_ID,
      hook = "patient-view",
      title = "Age",
      description = "Age calculated from patient birthday",
      prefetch = {@CdsServicePrefetch(value = "patient", query = "Patient/{{context.patientId}}")},
      allowAutoFhirClientPrefetch = true,
      extension = """
         {
            "age-measurement-unit": "years or months"
         }
         """,
      extensionClass = MeasurementUnitExtension.class)
public CdsServiceResponseJson calculateAge(CdsServiceRequestJson theCdsServiceRequestJson) {
   Patient patient = (Patient) theCdsServiceRequestJson.getPrefetch(PATIENT_KEY);

   if (patient == null) {
      throw new InvalidRequestException("'patient' prefetch resource missing from request.");
   }

   String ageMeasurementUnit = YEARS;
   // Get the extension passed in the request
   MeasurementUnitExtension extension = (MeasurementUnitExtension) theCdsServiceRequestJson.getExtension();
   if (!StringUtils.isEmpty(extension.getAgeMeasurementUnit())) {
      ageMeasurementUnit = (MONTHS).equals(extension.getAgeMeasurementUnit()) ? MONTHS : YEARS;
   }

   CdsServiceResponseJson response = new CdsServiceResponseJson();
   CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
   Integer ageInRequestedMeasurement = calculateAge(patient, ageMeasurementUnit);
   String name = getName(patient);

   if (ageInRequestedMeasurement == null) {
      card.setSummary(name + "'s age is unknown");
   } else {
      card.setSummary(name + " is " + ageInRequestedMeasurement + " " + ageMeasurementUnit + " old.");
   }
   card.setIndicator(CdsServiceIndicatorEnum.INFO);
   CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson();
   source.setLabel("ACME Age Calculator");
   card.setSource(source);
   response.addCard(card);
   return response;
}

private String getName(Patient thePatient) {
   if (thePatient == null) {
      return null;
   }
   List<HumanName> names = thePatient.getName();
   if (names == null || names.size() == 0 || names.get(0).isEmpty()) {
      return "This patient";
   }
   HumanName nameItem = names.get(0);
   return nameItem.getNameAsSingleString();
}

private Integer calculateAge(Patient thePatient, String theAgeMeasurementUnit) {
   if (thePatient == null) {
      return null;
   }
   Date birthDate = thePatient.getBirthDate();
   if (birthDate == null) {
      return null;
   }
   LocalDate localBirthDate =
         birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
   int ageDifferenceInYears = Period.between(
               localBirthDate,
               Instant.now().atZone(ZoneId.systemDefault()).toLocalDate())
         .getYears();
   return theAgeMeasurementUnit.equals(MONTHS) ? ageDifferenceInYears * 12 : ageDifferenceInYears;
}

21.0.7Building Your CDS Hooks
Trial

 

This section outlines considerations when building your CDS Hooks classes.

In order to build your Smile CDR CDS Hooks, you will need the annotation and json classes on your path. Currently, this is accomplished by adding the cdr-api-public-*.jar file to your build classpath. This jar file can be found in your smilecdr/lib folder. In the future, this jar will be published to a central repository and available as a maven dependency.

See Library Support for details concerning libraries available on the classpath where your CDS Hooks classes are loaded.

21.0.7.1Packaging Your Providers

The Spring Context Config class and your CDS Hooks classes must all be packaged up in a normal Java JAR file. No third party libraries should be packaged with your code.

If you are using Apache Maven as your build system, this just means you should use a normal project with a packaging of jar.

21.0.8Deploying Your CDS Hooks
Trial

 

Once you have created a JAR with your CDS Hooks in it, this JAR should be placed in the customerlib/ directory of the Smile CDR installation, and Smile CDR should be restarted. You will then want to create a new module configuration as follows:

  1. Log into the Web Admin Console.
  2. Navigate to the Module Config section ( Config -> Module Config ).
  3. Create a new module of type CDS Hooks REST Endpoint.
  4. Give the module a sensible ID.
  5. Under Listener Port, enter a port that the server will listen on.
  6. Under Spring Context Config Class, enter the fully qualified class name for your Spring Context Config Class.
  7. Under FHIR Version, enter the FHIR version that is used by your CDS Hooks clients. Any resources included in CDS Hooks requests (e.g. in context or prefetch) will be deserialized into hapi-fhir IBaseResource objects according to this FHIR Version.

21.0.9Accessing Security Attributes
Trial

 

CDS Hooks methods have access to the underlying authenticated user session if needed. See Security Attributes for more information.

21.0.10Exceptions
Trial

 

See Exceptions for details concerning exceptions and error management within CDS Hooks classes.

21.0.11Calling CDS Hooks
Trial

 

Per Version 1.1 of the CDS Hooks Specification, a list of all registered services is available at a path like https://example.com:8888/cds-services. As a convenience, swagger REST documentation is provided at the root of the endpoint: https://example.com:8888/.

21.0.12Example Project
Trial

 

A sample CDS Hooks project is available at the following links:

21.0.13CDS on FHIR
Trial

 

To create CDS Services from PlanDefinitions the dependencies for a FHIR Storage Module, FHIR Endpoint and CQL module must be set. This will create a listener on the storage module so that any changes to PlanDefinition resources will update the CDS Service cache.

Any PlanDefinition resource with an action that has a trigger of type named-event will have a CDS Service created using the PlanDefinition.id as the service id and the name of the trigger as the hook that the service is created for per the CDS on FHIR Specification.

CDS Services created this way will show up as registered services and can be called just as other services are called. The CDS Service request will be converted into parameters for the $apply operation, the results of which are then converted into a CDS Response per the CDS on FHIR Specification.

These CDS Services will take advantage of the Auto Prefetch feature. Prefetch data is included as a Bundle in the data parameter of the $apply call.

The $apply operation is running against the FHIR Storage Module, so it will also have access to any data stored there. Any CQL evaluation during the $apply operation that results in a retrieve will always pull from the Bundle and the FHIR Storage Module. This is done regardless of what data is passed into the prefetch of the service request.