forked from medplum/medplum-demo-bots
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge from main repo: Release Version 2.1.2 (#2943)
- Loading branch information
1 parent
c57df02
commit 2b305a4
Showing
21 changed files
with
2,067 additions
and
262 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
> [email protected] build | ||
> tsc | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
{ | ||
"type": "module", | ||
"name": "medplum-demo-bots", | ||
"version": "2.0.29", | ||
"version": "2.1.2", | ||
"description": "Medplum Demo Bots", | ||
"scripts": { | ||
"build": "tsc", | ||
|
@@ -14,23 +13,23 @@ | |
"author": "Medplum <[email protected]>", | ||
"license": "Apache-2.0", | ||
"devDependencies": { | ||
"@medplum/cli": "2.0.29", | ||
"@medplum/core": "2.0.29", | ||
"@medplum/eslint-config": "2.0.29", | ||
"@medplum/fhirtypes": "2.0.29", | ||
"@medplum/mock": "2.0.29", | ||
"@types/node": "20.4.5", | ||
"@types/node-fetch": "2.6.4", | ||
"@medplum/cli": "2.1.2", | ||
"@medplum/core": "2.1.2", | ||
"@medplum/eslint-config": "2.1.2", | ||
"@medplum/fhirtypes": "2.1.2", | ||
"@medplum/mock": "2.1.2", | ||
"@types/node": "20.6.2", | ||
"@types/node-fetch": "2.6.5", | ||
"@types/ssh2-sftp-client": "9.0.0", | ||
"@vitest/coverage-v8": "0.33.0", | ||
"@vitest/ui": "0.33.0", | ||
"@vitest/coverage-v8": "0.34.4", | ||
"@vitest/ui": "0.34.4", | ||
"form-data": "4.0.0", | ||
"node-fetch": "2.6.12", | ||
"node-fetch": "2.7.0", | ||
"pdfmake": "0.2.7", | ||
"ssh2-sftp-client": "9.1.0", | ||
"stripe": "12.14.0", | ||
"typescript": "5.1.6", | ||
"vitest": "0.33.0" | ||
"stripe": "13.6.0", | ||
"typescript": "5.2.2", | ||
"vitest": "0.34.4" | ||
}, | ||
"eslintConfig": { | ||
"extends": [ | ||
|
@@ -39,9 +38,10 @@ | |
}, | ||
"prettier": { | ||
"printWidth": 120, | ||
"singleQuote": true | ||
"singleQuote": true, | ||
"trailingComma": "es5" | ||
}, | ||
"dependencies": { | ||
"esbuild": "0.18.17" | ||
"esbuild": "0.19.3" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# Patient Deduplication Demo | ||
|
||
This folder contains a reference implementation for a simple, human-in-the-loop patient deduplication pipeline, based on the guidance in [Patient Deduplication Architectures](https://www.medplum.com/docs/fhir-datastore/patient-deduplication#architecture-overview). | ||
|
||
The implementation consists of two bots: | ||
|
||
- [`find-matching-patients`](./find-matching-patients.ts) - Identifies duplicated Patients and performs a series of steps: | ||
- Check for the first name, last name, birth date, and zip code. | ||
- If it's a match, it will check a list to see if it's not on the doNotMatch list | ||
- Creates a `RiskAssessment`, with a numeric score, representing the candidate match. | ||
- Creates a `Task` for a human to review the match | ||
- [`merge-matching-patients`](./merge-matching-patients.ts) -The other that merges the two Patients into one | ||
- [Link the Patient records](https://www.medplum.com/docs/fhir-datastore/patient-deduplication#linking-patient-records-in-fhir) | ||
- [Merge the Contact info](https://www.medplum.com/docs/fhir-datastore/patient-deduplication#merge-rules) | ||
- [Rewrite Clinical Resource](https://www.medplum.com/docs/fhir-datastore/patient-deduplication#rewriting-references-from-clinical-data) | ||
- Delete the source record, if the user has requested it | ||
|
||
This implementation also consists of FHIR bundles with sample data, which can be uploaded using the [batch upload tool](https://www.medplum.com/docs/tutorials/importing-sample-data): | ||
|
||
- [patient-data.json](./patient-data.json) - Three sample `Patient` resources, two of whom are known duplicates, and one of which is a false positive | ||
- [merge-questionnaire.json](./merge-questionnaire.json) - An example `Questionnaire` resource that describes a form that a reviewer might use to evaluate a candidate match. | ||
|
||
## Setup | ||
|
||
To run and deploy your Bot do the following steps: | ||
|
||
Install: | ||
|
||
```bash | ||
npm i | ||
``` | ||
|
||
Build: | ||
|
||
```bash | ||
npm run build | ||
``` | ||
|
||
Test: | ||
|
||
```bash | ||
npm t | ||
``` | ||
|
||
[Create first Bot](https://www.medplum.com/docs/cli#bots) : | ||
|
||
```bash | ||
npx medplum bot create find-matching-patients <project id> "src/deduplication/find-matching-patients.ts" "dist/deduplication/find-matching-patients.js" | ||
``` | ||
|
||
[Deploy first Bot](https://www.medplum.com/docs/cli#bots) : | ||
|
||
```bash | ||
npx medplum bot deploy find-matching-patients | ||
``` | ||
|
||
```bash | ||
Update bot code..... | ||
Success! New bot version: <botID> | ||
Deploying bot... | ||
Deploy result: All OK | ||
``` | ||
|
||
Set up a `Subscription` following the instructions [here](https://www.medplum.com/docs/bots/bot-basics#executing-automatically-using-a-subscription) to trigger the Bot when a `Patient` record is updated. | ||
|
||
- Critera: `Patient?active=true` | ||
- Endpoint: `Bot/:find-matching-patients-bot-id` | ||
|
||
[Create second Bot](https://www.medplum.com/docs/cli#bots) : | ||
|
||
```bash | ||
npx medplum bot create merge-matching-patients <project id> "src/deduplication/merge-matching-patients.ts" "dist/deduplication/merge-matching-patients.js" | ||
``` | ||
|
||
[Deploy second Bot](https://www.medplum.com/docs/cli#bots) : | ||
|
||
```bash | ||
npx medplum bot deploy merge-matching-patients | ||
``` | ||
|
||
You will see the following in your command prompt if all goes well: | ||
|
||
```bash | ||
Update bot code..... | ||
Success! New bot version: <botID> | ||
Deploying bot... | ||
Deploy result: All OK | ||
``` | ||
|
||
Use the [batch upload tool](https://www.medplum.com/docs/tutorials/importing-sample-data) to import the [merge questionnaire](./merge-questionnaire.json) | ||
|
||
Set up a `Subscription` following the instructions [here](https://www.medplum.com/docs/bots/bot-basics#executing-automatically-using-a-subscription) to trigger the bot when the questionnaire is submitted. | ||
|
||
- Critera: `QuestionnaireResponse?questionnaire=Questionnaire/:merge-questionnaire-id` | ||
- Endpoint: `Bot/:merge-matching-patients-bot-id` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { | ||
ContentType, | ||
MedplumClient, | ||
createReference, | ||
getReferenceString, | ||
indexSearchParameterBundle, | ||
indexStructureDefinitionBundle, | ||
resolveId, | ||
} from '@medplum/core'; | ||
import { readJson } from '@medplum/definitions'; | ||
import { Bundle, List, Patient, SearchParameter } from '@medplum/fhirtypes'; | ||
import { MockClient } from '@medplum/mock'; | ||
import { handler } from './find-matching-patients'; | ||
import patientData from './patient-data.json'; | ||
|
||
interface TestContext { | ||
medplum: MedplumClient; | ||
} | ||
|
||
// npm t src/examples/patient-deduplication.test.ts | ||
// This test demostrates a automatically linking patients with three matching identifiers | ||
describe('Link Patient', async () => { | ||
// Load the FHIR definitions to enable search parameter indexing | ||
beforeAll(() => { | ||
indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle); | ||
indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle); | ||
indexSearchParameterBundle(readJson('fhir/r4/search-parameters.json') as Bundle<SearchParameter>); | ||
}); | ||
|
||
// Load the sample data from patient-data.json | ||
beforeEach<TestContext>(async (context) => { | ||
context.medplum = new MockClient(); | ||
await context.medplum.executeBatch(patientData as Bundle); | ||
}); | ||
|
||
test<TestContext>('Created RiskAssessment', async ({ medplum }) => { | ||
// Read the patient | ||
const patients = await medplum.searchResources('Patient', { given: 'Alex' }); | ||
|
||
await handler(medplum, { input: patients?.[0] as Patient, contentType: ContentType.FHIR_JSON, secrets: {} }); | ||
|
||
// We expect two risk assessments to be created for the two candidate matches | ||
const riskAssessments = await medplum.searchResources('RiskAssessment'); | ||
expect(riskAssessments.length).toBe(2); | ||
expect(riskAssessments.every((assessment) => resolveId(assessment.subject) === patients[0].id)); | ||
}); | ||
|
||
test<TestContext>('Does not create RiskAssessment due to doNotMatch List', async ({ medplum }) => { | ||
// Read two patients that should not be matched | ||
const alexSmith = await medplum.searchOne('Patient', { given: 'Alex', gender: 'male' }); | ||
const alexisSmith = await medplum.searchOne('Patient', { given: 'Alex', gender: 'female' }); | ||
|
||
if (!alexSmith || !alexisSmith) { | ||
throw new Error('Missing Input Patient'); | ||
} | ||
|
||
// Add each patient to the other's | ||
const doNotMatchAlex = (await medplum.searchOne('List', { subject: getReferenceString(alexSmith) })) as List; | ||
const doNotMatchAlexis = (await medplum.searchOne('List', { subject: getReferenceString(alexisSmith) })) as List; | ||
doNotMatchAlex.entry = [{ item: createReference(alexisSmith) }]; | ||
doNotMatchAlexis.entry = [{ item: createReference(alexSmith) }]; | ||
await medplum.updateResource(doNotMatchAlex); | ||
await medplum.updateResource(doNotMatchAlexis); | ||
|
||
await handler(medplum, { input: alexSmith, contentType: ContentType.FHIR_JSON, secrets: {} }); | ||
|
||
const riskAssessment = await medplum.searchResources('RiskAssessment'); | ||
expect(riskAssessment.length).toBe(1); | ||
expect(riskAssessment?.[0]?.subject?.reference).toBe(getReferenceString(alexSmith)); | ||
expect(riskAssessment?.[0]?.basis?.[0]).not.toBe(getReferenceString(alexisSmith)); | ||
}); | ||
}); |
Oops, something went wrong.