-
Notifications
You must be signed in to change notification settings - Fork 7
Kerbearasaurus/use-phenoml-ts-sdk #10
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,46 +1,44 @@ | ||
| // SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| import { BotEvent, MedplumClient } from '@medplum/core'; | ||
| import { DocumentReference, Resource, Questionnaire, QuestionnaireResponse } from '@medplum/fhirtypes'; | ||
| import { DocumentReference, Questionnaire, QuestionnaireResponse } from '@medplum/fhirtypes'; | ||
| import { Buffer } from 'buffer'; | ||
| import { PhenoMLClient, phenoml } from 'phenoml'; | ||
|
|
||
| /** | ||
| * A Medplum Bot that processes documents using the lang2fhir API. | ||
| * | ||
| * | ||
| * The bot will: | ||
| * 1. Download the document from the provided URL | ||
| * 2. Send the document to the lang2fhir API | ||
| * 3. Create a FHIR resource of the type specified in the input | ||
| * | ||
| * | ||
| * Required bot secrets: (You need to have an active PhenoML subscription to use this bot) | ||
| * - PHENOML_EMAIL: Your PhenoML API email | ||
| * - PHENOML_PASSWORD: Your PhenoML API password | ||
| */ | ||
|
|
||
| interface DocumentRequest { | ||
| version: string; | ||
| resource: string; | ||
| content: string; | ||
| fileType: string; | ||
| } | ||
|
|
||
| interface DocBotInput { | ||
| docref: DocumentReference; | ||
| resourceType: 'Questionnaire' | 'QuestionnaireResponse'; | ||
| } | ||
|
|
||
| const PHENOML_API_URL = "https://experiment.app.pheno.ml"; | ||
|
|
||
| // Maps content types to SDK file types | ||
| const FILE_TYPE_MAP: Record<string, phenoml.lang2Fhir.DocumentRequest.FileType> = { | ||
| 'application/pdf': 'application/pdf', | ||
| 'image/png': 'image/png', | ||
| 'image/jpeg': 'image/jpeg', | ||
| 'image/jpg': 'image/jpg', | ||
| }; | ||
|
|
||
| export async function handler( | ||
| medplum: MedplumClient, | ||
| medplum: MedplumClient, | ||
| event: BotEvent<DocBotInput> | ||
| ): Promise<Resource> { | ||
| ): Promise<Questionnaire | QuestionnaireResponse> { | ||
| try { | ||
|
|
||
| const inputDocRef = event.input.docref; | ||
| const inputResourceType = event.input.resourceType; | ||
|
|
||
| if (!inputDocRef) { | ||
| throw new Error('No media input provided to bot'); | ||
| } | ||
|
|
@@ -51,76 +49,37 @@ export async function handler( | |
| if (!['Questionnaire', 'QuestionnaireResponse'].includes(inputResourceType)) { | ||
| throw new Error(`Unsupported resource type: ${inputResourceType}`); | ||
| } | ||
|
|
||
| if (!inputDocRef.content?.[0].attachment?.url) { | ||
| throw new Error('DocumentReference resource must have content.url'); | ||
| } | ||
|
|
||
| const targetResourceType = inputResourceType.toLowerCase(); | ||
| const targetResourceType = inputResourceType.toLowerCase() as phenoml.lang2Fhir.DocumentRequest.Resource; | ||
|
|
||
| // Download the file content from the pre-signed URL | ||
| const blob = await medplum.download(inputDocRef.content?.[0].attachment?.url); | ||
| const arrayBuffer = await blob.arrayBuffer(); | ||
| const content = Buffer.from(arrayBuffer).toString('base64'); | ||
|
|
||
| const contentType = inputDocRef.content?.[0].attachment?.contentType || 'application/pdf'; | ||
| const fileType = FILE_TYPE_MAP[contentType] || 'application/pdf'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsupported file types silently misrepresented as PDFMedium Severity When a document has a content type not in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsupported file types silently misrepresented as PDFMedium Severity When a document has a content type not in |
||
|
|
||
| const email = event.secrets["PHENOML_EMAIL"].valueString as string; | ||
| const password = event.secrets["PHENOML_PASSWORD"].valueString as string; | ||
|
|
||
| // Create base64 encoded credentials for Basic Auth | ||
| const credentials = Buffer.from(`${email}:${password}`).toString('base64'); | ||
| // Get auth token using Basic Auth | ||
| const authResponse = await fetch(PHENOML_API_URL + '/auth/token', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Accept': 'application/json', | ||
| 'Authorization': `Basic ${credentials}` | ||
| }, | ||
| }).catch(error => { | ||
| throw new Error(`Failed to connect to PhenoML API: ${error.message}`); | ||
| }); | ||
|
|
||
| if (!authResponse.ok) { | ||
| throw new Error(`Authentication failed: ${authResponse.status} ${authResponse.statusText}`); | ||
| } | ||
| // Initialize PhenoML client with automatic auth handling | ||
| const phenomlClient = new PhenoMLClient({ username: email, password }); | ||
|
|
||
| const { token: bearerToken } = await authResponse.json() as { token: string }; | ||
| if (!bearerToken) { | ||
| throw new Error('No token received from auth response'); | ||
| } | ||
|
|
||
| // Prepare document request | ||
| const documentRequest: DocumentRequest = { | ||
| version: 'R4', // FHIR R4 | ||
| // Call lang2fhir document endpoint using SDK | ||
| const generatedResource = await phenomlClient.lang2Fhir.document({ | ||
| version: 'R4', | ||
| resource: targetResourceType, | ||
| content: content, | ||
| fileType: inputDocRef.content?.[0].attachment?.contentType || 'application/pdf' | ||
| }; | ||
|
|
||
| // Call lang2fhir/document endpoint | ||
| const documentResponse = await fetch(PHENOML_API_URL + '/lang2fhir/document', { | ||
| method: "POST", | ||
| body: JSON.stringify(documentRequest), | ||
| headers: { | ||
| 'Authorization': `Bearer ${bearerToken}`, | ||
| 'Content-Type': 'application/json', | ||
| 'Accept': 'application/json' | ||
| }, | ||
| }).catch(error => { | ||
| throw new Error(`Failed to connect to Lang2FHIR Document API: ${error.message}`); | ||
| fileType: fileType | ||
| }); | ||
|
|
||
| if (!documentResponse.ok) { | ||
| const errorText = await documentResponse.text().catch(() => 'No error details available'); | ||
| throw new Error(`Document processing failed: ${documentResponse.status} ${documentResponse.statusText} - ${errorText}`); | ||
| } | ||
|
|
||
| const generatedResource = await documentResponse.json().catch(error => { | ||
| throw new Error(`Failed to parse document response: ${error.message}`); | ||
| }); | ||
|
|
||
| return generatedResource as Questionnaire | QuestionnaireResponse; | ||
| return generatedResource as unknown as Questionnaire | QuestionnaireResponse; | ||
| } catch (error) { | ||
| throw new Error(`Bot execution failed: ${error instanceof Error ? error.message : String(error)}`); | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent baseUrl configuration in PhenoML client initialization
Medium Severity
The refactored bots (
lang2fhir-create.ts,lang2fhir-document.ts,phenoml-cohort.ts) initializePhenoMLClientwithout abaseUrl, while the new bots (phenoml-workflow.ts,phenoml-ips-summary.ts) explicitly setbaseUrl: 'https://experiment.app.pheno.ml'. The old code being replaced explicitly used this URL viaPHENOML_API_URL. If the SDK's default differs from this experiment URL, the refactored bots will fail or hit an incorrect endpoint while the new bots work correctly.Additional Locations (2)
src/bots/lang2fhir-document.ts#L70-L71src/bots/phenoml-cohort.ts#L57-L58