Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOMED Hydrating, Filtering, Special MR Generation #1648

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,342 changes: 2,342 additions & 0 deletions packages/core/src/external/aws/lambda-logic/bundle-to-html-snomed.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{{>Resources/Medication.hbs medication=medAdm.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial ID=(generateUUID (toJsonString medAdm.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial))}},
{{>References/MedicationAdministration/medicationReference.hbs ID=(generateUUID (toJsonString medAdm.substanceAdministration)) REF=(concat 'Medication/' (generateUUID (toJsonString medAdm.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial)))}},

{{#with (evaluate 'Utils/GenerateOrganizationId.hbs' obj=edAdm.substanceAdministration.performer.assignedEntity.representedOrganization) as |orgId|}}
{{#with (evaluate 'Utils/GenerateOrganizationId.hbs' obj=medAdm.substanceAdministration.performer.assignedEntity.representedOrganization) as |orgId|}}
Copy link
Member Author

Choose a reason for hiding this comment

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

bug

Copy link
Member

Choose a reason for hiding this comment

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

Ouch, this is stuff that our tooling should catch for us. Isn't there a way we can parse/compile those? At least to check if the variable names are correct and not misspelled?

Also, is there a way we can have unit testing for this? We can def create a ticket to set it up.

Just because w/ unit tests we can also create new tests as we find bugs that would catch those if they were in place.

{{>Resources/Organization.hbs org=medAdm.substanceAdministration.performer.assignedEntity.representedOrganization ID=orgId.Id}},
{{>References/MedicationAdministration/performer.actor.hbs ID=(generateUUID (toJsonString medAdm.substanceAdministration)) REF=(concat 'Organization/' orgId.Id)}},
{{/with}}
Expand Down Expand Up @@ -71,7 +71,14 @@
{{/with}}
{{/if}}
{{/each}}

{{#if medAdm.substanceAdministration.precondition}}
{{#each (toArray medAdm.substanceAdministration.precondition) as |precondition|}}
{{#if precondition.criterion.value.code}}
{{>Resources/Condition.hbs conditionEntry=precondition.criterion ID=(generateUUID (toJsonString precondition.criterion))}},
{{>References/MedicationAdministration/reasonReference.hbs ID=(generateUUID (toJsonString medAdm.substanceAdministration)) REF=(concat 'Condition/' (generateUUID (toJsonString precondition.criterion)))}},
{{/if}}
{{/each}}
{{/if}}
{{/each}}
{{/each}}
{{/with}}
38 changes: 20 additions & 18 deletions packages/fhir-converter/src/templates/cda/Sections/Medication.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,44 @@
{{#if medEntry.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial}}
{{>Resources/Medication.hbs medication=medEntry.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial ID=(generateUUID (toJsonString medEntry.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial))}},
{{>References/MedicationStatement/medicationReference.hbs ID=(generateUUID (toJsonString medEntry.substanceAdministration)) REF=(concat 'Medication/' (generateUUID (toJsonString medEntry.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial)))}},
{{#each (toArray medEntry.substanceAdministration.entryRelationship) as |medReq|}}
{{#if medReq.supply}}
{{>Resources/MedicationRequest.hbs medicationRequest=medReq.supply ID=(generateUUID (toJsonString medReq.supply))}},
{{#each (toArray medEntry.substanceAdministration.entryRelationship) as |entryRelationship|}}
{{#if entryRelationship.supply}}
{{>Resources/MedicationRequest.hbs medicationRequest=entryRelationship.supply ID=(generateUUID (toJsonString entryRelationship.supply))}},
{{#with (evaluate 'Utils/GeneratePatientId.hbs' obj=@metriportPatientId) as |patientId|}}
{{>References/MedicationRequest/subject.hbs ID=(generateUUID (toJsonString medReq.supply)) REF=(concat 'Patient/' patientId.Id)}},
{{>References/MedicationRequest/subject.hbs ID=(generateUUID (toJsonString entryRelationship.supply)) REF=(concat 'Patient/' patientId.Id)}},
{{/with}}
{{>References/MedicationRequest/medicationReference.hbs ID=(generateUUID (toJsonString medReq.supply)) REF=(concat 'Medication/' (generateUUID (toJsonString medEntry.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial)))}},
{{>References/MedicationRequest/medicationReference.hbs ID=(generateUUID (toJsonString entryRelationship.supply)) REF=(concat 'Medication/' (generateUUID (toJsonString medEntry.substanceAdministration.consumable.manufacturedProduct.manufacturedMaterial)))}},
{{/if}}
{{!-- I am keeping what was originally here but I have yet to see an author placed here --}}
{{#if medReq.supply.author.assignedAuthor}}
{{#with (evaluate 'Utils/GeneratePractitionerId.hbs' obj=medReq.supply.author.assignedAuthor) as |practitionerId|}}
{{>Resources/Practitioner.hbs practitioner=medReq.supply.author.assignedAuthor ID=practitionerId.Id}},
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString medReq.supply)) REF=(concat 'Practitioner/' practitionerId.Id)}}
{{#if entryRelationship.supply.author.assignedAuthor}}
{{#with (evaluate 'Utils/GeneratePractitionerId.hbs' obj=entryRelationship.supply.author.assignedAuthor) as |practitionerId|}}
{{>Resources/Practitioner.hbs practitioner=entryRelationship.supply.author.assignedAuthor ID=practitionerId.Id}},
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString entryRelationship.supply)) REF=(concat 'Practitioner/' practitionerId.Id)}}
{{/with}}
{{#if medReq.supply.author.assignedAuthor.representedOrganization}}
{{#with (evaluate 'Utils/GenerateOrganizationId.hbs' obj=medReq.supply.author.assignedAuthor.representedOrganization) as |orgId|}}
{{>Resources/Organization.hbs org=medReq.supply.author.assignedAuthor.representedOrganization ID=orgId.Id}},
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString medReq.supply)) REF=(concat 'Organization/' orgId.Id)}}
{{#if entryRelationship.supply.author.assignedAuthor.representedOrganization}}
{{#with (evaluate 'Utils/GenerateOrganizationId.hbs' obj=entryRelationship.supply.author.assignedAuthor.representedOrganization) as |orgId|}}
{{>Resources/Organization.hbs org=entryRelationship.supply.author.assignedAuthor.representedOrganization ID=orgId.Id}},
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString entryRelationship.supply)) REF=(concat 'Organization/' orgId.Id)}}
{{/with}}
{{/if}}
{{else if (and medReq.supply medEntry.substanceAdministration.author.assignedAuthor)}}
{{else if (and entryRelationship.supply medEntry.substanceAdministration.author.assignedAuthor)}}
{{#with (evaluate 'Utils/GeneratePractitionerId.hbs' obj=medEntry.substanceAdministration.author.assignedAuthor) as |practitionerId|}}
{{>Resources/Practitioner.hbs practitioner=medEntry.substanceAdministration.author.assignedAuthor ID=practitionerId.Id}},
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString medReq.supply)) REF=(concat 'Practitioner/' practitionerId.Id)}}
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString entryRelationship.supply)) REF=(concat 'Practitioner/' practitionerId.Id)}}
{{/with}}
{{#if medEntry.substanceAdministration.author.assignedAuthor.representedOrganization}}
{{#with (evaluate 'Utils/GenerateOrganizationId.hbs' obj=medEntry.substanceAdministration.author.assignedAuthor.representedOrganization) as |orgId|}}
{{>Resources/Organization.hbs org=medEntry.substanceAdministration.author.assignedAuthor.representedOrganization ID=orgId.Id}},
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString medReq.supply)) REF=(concat 'Organization/' orgId.Id)}}
{{>References/MedicationRequest/requester.hbs ID=(generateUUID (toJsonString entryRelationship.supply)) REF=(concat 'Organization/' orgId.Id)}}
{{/with}}
{{/if}}
{{else if (contains (toJsonString entryRelationship.observation.templateId) '2.16.840.1.113883.10.20.22.4.19')}}
Copy link
Member

Choose a reason for hiding this comment

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

I can't tell what the OID is without looking up online. This is a yellow flag, as it increases dependency on the internet and external services. Maybe we can start moving those shared constants on a single file?

{{>Resources/Condition.hbs conditionEntry=entryRelationship.observation ID=(generateUUID (toJsonString entryRelationship.observation))}},
{{!-- add condition recorder here --}}
{{>References/MedicationStatement/reasonReference.hbs ID=(generateUUID (toJsonString medEntry.substanceAdministration)) REF=(concat 'Condition/' (generateUUID (toJsonString entryRelationship.observation)))}},
{{/if}}

{{/each}}
{{/if}}

{{#if medEntry.substanceAdministration.informant.assignedEntity.representedOrganization.name._}}
{{>Resources/Organization.hbs org=medEntry.substanceAdministration.informant.assignedEntity.representedOrganization ID=(generateUUID (toJsonString medEntry.substanceAdministration.informant.assignedEntity.representedOrganization))}},
{{/if}}
Expand Down
32 changes: 31 additions & 1 deletion packages/utils/src/customer-requests/convert-html-to-csv.ts
Copy link
Member

Choose a reason for hiding this comment

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

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DOMParser } from "xmldom";

import { readFileSync, writeFileSync } from "fs";
import * as path from "path";
// This is temporary function that will eventually be introduced
// as another way to render patient data. (Will need refactoring)
export function convertHtmlTablesToCsv(html: string) {
Expand Down Expand Up @@ -110,3 +111,32 @@ export function convertHtmlTablesToCsv(html: string) {

return convertedCsv;
}

function main() {
Copy link
Member Author

Choose a reason for hiding this comment

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

utility to call in the command line

const filePath = process.argv[2];

if (!filePath) {
console.error("Please provide an HTML file path.");
process.exit(1);
}

try {
const htmlContent = readFileSync(filePath, "utf8");
const csvContent = convertHtmlTablesToCsv(htmlContent);

const csvFilePath = path.join(
path.dirname(filePath),
path.basename(filePath, path.extname(filePath)) + ".csv"
);

writeFileSync(csvFilePath, csvContent);
console.log(`CSV file has been created at: ${csvFilePath}`);
// eslint-disable-next-line
} catch (error: any) {
console.error("Error processing the HTML file:", error.message);
}
}

if (require.main === module) {
main();
}
114 changes: 114 additions & 0 deletions packages/utils/src/customer-requests/full-snomed-pipeline.ts
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the main customer facing output of this PR. A script that for a given customer, gets all their patients consolidated data and performs the filtering and MR summary generation

Copy link
Member Author

Choose a reason for hiding this comment

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

Full script for running this whole process

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// This script is used to process all of a customers patients through a snomed filtering pipeline, and
// then generate MR summaries and CSV files from the resulting bundles
// The script makes consolidated queries, inserts a patient FHIR resource, and then calls the filtering
// and MR generation logic.
Comment on lines +1 to +4
Copy link
Member

Choose a reason for hiding this comment

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

nit: use multi-line comments

also, please indicate how to use it


import { bundleToHtml } from "@metriport/core/external/aws/lambda-logic/bundle-to-html-snomed";
import fs from "fs/promises";
import { convertHtmlTablesToCsv } from "./convert-html-to-csv";
import * as path from "path";
import { fullProcessing } from "../terminology-server/snomed-dedup-and-filter";
import { MetriportMedicalApi } from "@metriport/api-sdk";
import { getEnvVarOrFail } from "@metriport/core/util/env-var";
import * as dotenv from "dotenv";
import axios from "axios";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
dotenv.config();
dayjs.extend(duration);

const apiKey = getEnvVarOrFail("API_KEY");
const apiLoadBalancerURL = getEnvVarOrFail("API_URL");
const fhirUrl = getEnvVarOrFail("FHIR_URL");
const cxId = getEnvVarOrFail("CX_ID");
const metriportApi: MetriportMedicalApi = new MetriportMedicalApi(apiKey);

const timestamp = dayjs().toISOString();
const resultsDirectory = `./runs/consolidatedPatients/${timestamp}`;

async function fetchPatientIds(cxId: string): Promise<string[]> {
const url = `${apiLoadBalancerURL}/internal/patient/ids?cxId=${cxId}`;
try {
const response = await axios.get(url);
return response.data.patientIds;
} catch (error) {
console.error(`Error fetching patient IDs:`, error);
return [];
}
}

//eslint-disable-next-line
async function getFhirPatientData(patientId: string): Promise<any> {
const url = `${fhirUrl}/fhir/${cxId}/Patient/${patientId}/`;
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error(`Error fetching FHIR data for patient ${patientId}:`, error);
}
Comment on lines +46 to +48
Copy link
Member

Choose a reason for hiding this comment

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

This is failing silently and the code will keep executing. Is that expected?

}

export async function ensureDirectory(): Promise<void> {
await fs.mkdir(resultsDirectory, { recursive: true });
}

async function fetchAndSavePatientData(patientId: string): Promise<void> {
try {
const resources = ["Condition, Procedure, MedicationStatement, MedicationAdministration"];
const data = await metriportApi.getPatientConsolidated(patientId, resources);
const fhirPatient = await getFhirPatientData(patientId);
const resourceWrappedFhirPatient = { resource: fhirPatient };
if (!data.entry) {
data.entry = [];
}
data.entry.push(resourceWrappedFhirPatient);
const filePath = `${resultsDirectory}/${patientId}.json`;
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
console.log(`Data saved for patient ${patientId}`);
} catch (error) {
console.error(`Error fetching and saving data for patient ${patientId}:`, error);
}
}

async function processPatients(): Promise<void> {
console.log("Checking if directory exists...");
await ensureDirectory();
console.log("Fetching patient IDs...");
const patientIds = await fetchPatientIds(cxId);
for (const patientId of patientIds) {
await fetchAndSavePatientData(patientId);
}
console.log("All patient data processed.");
}

async function processFile(filePath: string) {
await fullProcessing(filePath);

const bundle = await fs.readFile(filePath, "utf8");
const bundleParsed = JSON.parse(bundle);

const html = bundleToHtml(bundleParsed);
const htmlOutputFilePath = filePath.replace(".json", ".html");
await fs.writeFile(htmlOutputFilePath, html);
console.log(`HTML file created at ${htmlOutputFilePath}`);

const csvContent = convertHtmlTablesToCsv(html);
const csvOutputFilePath = filePath.replace(".json", ".csv");
await fs.writeFile(csvOutputFilePath, csvContent);
console.log(`CSV file created at ${csvOutputFilePath}`);
}

async function main() {
await processPatients();

const files = await fs.readdir(resultsDirectory);
for (const file of files) {
const fullPath = path.join(resultsDirectory, file);
if (path.extname(fullPath) === ".json") {
console.log(`Processing file ${fullPath}`);
await processFile(fullPath);
}
}
}

main();
15 changes: 9 additions & 6 deletions packages/utils/src/customer-requests/medical-records-local.ts
Copy link
Member

Choose a reason for hiding this comment

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

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { bundleToHtml } from "@metriport/core/external/aws/lambda-logic/bundle-to-html";
import fs from "fs";

// get xml file from this folder and bundle to html
if (process.argv.length < 3) {
Copy link
Member Author

Choose a reason for hiding this comment

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

utility to call in command line

console.log("Usage: node medical-records-local.js <path-to-json-file>");
process.exit(1);
}

const bundle = fs.readFileSync("test-bundle.json", "utf8");
const filePath = process.argv[2];
const bundle = fs.readFileSync(filePath, "utf8");
const bundleParsed = JSON.parse(bundle);

// FHIR Bundle
const html = bundleToHtml(bundleParsed);

// Response from FHIR Converter
// const html = bundleToHtml(bundleParsed.fhirResource);
const outputFilePath = filePath.replace(".json", ".html");

fs.writeFileSync("test.html", html);
fs.writeFileSync(outputFilePath, html);
console.log(`HTML file created at ${outputFilePath}`);
4 changes: 2 additions & 2 deletions packages/utils/src/fhir-converter/e2e-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ dayjs.extend(duration);
* - fhirBaseUrl: the URL of the FHIR server;
*/

const cdaLocation = ``;
const converterBaseUrl = "http:https://localhost:8777";
const cdaLocation = `/Users/jonahkaye/Desktop/2024-01-23T08:02:29.892Z/test-patient-sample`;
Copy link
Member

Choose a reason for hiding this comment

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

left-over

const converterBaseUrl = "http:https://localhost:8080";
const fhirBaseUrl = "http:https://localhost:8889";
const parallelConversions = 10;
// Execute 1 batch at a time to avoid concurrency when upserting resources (resulting in 409/Conflict), which
Expand Down
82 changes: 82 additions & 0 deletions packages/utils/src/terminology-server/precondition-analysis.ts
Copy link
Member Author

Choose a reason for hiding this comment

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

Analysis done on preconditions on sample patient set for reference:

Total #of Conditions in the directory: 401308
Total #of Conditions generated from <Precondition> in the directory: 685
Total #of Conditions generated from <Precondition> in the directory of type disorder: 56

15802004 with count 1 is a disorder: Dystonia (disorder)
16932000 with count 20 is a disorder: Nausea and vomiting (disorder)
37796009 with count 3 is a disorder: Migraine (disorder)
38341003 with count 3 is a disorder: Hypertensive disorder, systemic arterial (disorder)
39579001 with count 2 is a disorder: Anaphylaxis (disorder)
70153002 with count 3 is a disorder: Hemorrhoids (disorder)
193462001 with count 6 is a disorder: Insomnia (disorder)
242253008 with count 2 is a disorder: Overdose of opiate (disorder)
302866003 with count 4 is a disorder: Hypoglycemia (disorder)
419076005 with count 1 is a disorder: Allergic reaction (disorder)
422400008 with count 10 is a disorder: Vomiting (disorder)
860914002 with count 1 is a disorder: Erectile dysfunction (disorder)

Copy link
Member

Choose a reason for hiding this comment

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

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import fs from "fs";
import path from "path";
import { getCodeDisplay } from "./term-server-api";

function processDirectory(
directoryPath: string,
hashTable: { [key: string]: number },
totalConditionCounts: { count: number }
) {
const filesAndDirectories = fs.readdirSync(directoryPath);

for (const name of filesAndDirectories) {
const currentPath = path.join(directoryPath, name);
const stat = fs.statSync(currentPath);

if (stat.isDirectory()) {
processDirectory(currentPath, hashTable, totalConditionCounts);
} else if (name.endsWith(".json")) {
processFile(currentPath, hashTable, totalConditionCounts);
}
}
}

function processFile(
filePath: string,
hashTable: { [key: string]: number },
totalConditionCounts: { count: number }
) {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (data.resourceType === "Bundle" && data.type === "batch") {
for (const entry of data.entry) {
if (
entry.resource?.resourceType === "Condition" &&
entry.resource.verificationStatus === true
) {
const codeInfo = entry.resource.code;
const code = codeInfo.coding[0].code;
if (hashTable[code]) {
hashTable[code] += 1;
} else {
hashTable[code] = 1;
}
}
totalConditionCounts.count += 1;
}
}
}

async function main() {
const directoryPath = process.argv[2];
if (!directoryPath) {
console.error("Please provide a directory path as an argument.");
process.exit(1);
}

const hashTable: { [key: string]: number } = {};
const totalConditionCount = { count: 0 };
processDirectory(directoryPath, hashTable, totalConditionCount);

let disorderCount = 0;
for (const code of Object.keys(hashTable)) {
const codeDetails = await getCodeDisplay(code, "SNOMEDCT_US");
if (codeDetails && codeDetails.category === "disorder") {
console.log(
`${code} with count ${hashTable[code]} is a disorder: ${codeDetails.display} (${codeDetails.category})`
);
disorderCount += hashTable[code];
}
}

const totalCount = Object.values(hashTable).reduce((acc, count) => acc + count, 0);

console.log(`Total #of Conditions in the directory: ${totalConditionCount.count}`);
console.log(`Total #of Conditions generated from <Precondition> in the directory: ${totalCount}`);
console.log(
`Total #of Conditions generated from <Precondition> in the directory of type disorder: ${disorderCount}`
);
}

if (require.main === module) {
main();
}
Loading
Loading