Skip to content

Commit 680792f

Browse files
committed
Enhance AwsBedrockService and tests to support image-based medical information extraction, including validation for image types, improved error handling, and updated test cases for various image scenarios.
1 parent b606057 commit 680792f

File tree

3 files changed

+108
-134
lines changed

3 files changed

+108
-134
lines changed

backend/src/services/aws-bedrock.service.spec.ts

Lines changed: 83 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ vi.mock('@aws-sdk/client-bedrock-runtime', () => {
3333
// Mock validateFileSecurely to bypass file validation in tests
3434
vi.mock('../utils/security.utils', () => {
3535
return {
36-
validateFileSecurely: vi.fn(),
36+
validateFileSecurely: vi.fn().mockImplementation((buffer: Buffer, fileType: string) => {
37+
if (!['image/jpeg', 'image/png'].includes(fileType)) {
38+
throw new BadRequestException('Only JPEG and PNG images are allowed');
39+
}
40+
}),
3741
sanitizeMedicalData: vi.fn(data => data),
3842
RateLimiter: vi.fn().mockImplementation(() => ({
3943
tryRequest: vi.fn().mockReturnValue(true),
@@ -63,7 +67,7 @@ describe('AwsBedrockService', () => {
6367
'aws.region': 'us-east-1',
6468
'aws.aws.accessKeyId': 'test-access-key',
6569
'aws.aws.secretAccessKey': 'test-secret-key',
66-
'bedrock.model': 'anthropic.claude-v2',
70+
'bedrock.model': 'anthropic.claude-3-7-sonnet-20250219-v1:0',
6771
'bedrock.maxTokens': 2048,
6872
};
6973

@@ -99,30 +103,32 @@ describe('AwsBedrockService', () => {
99103
});
100104

101105
it('should initialize with test environment values', () => {
102-
expect(service['defaultModel']).toBe('anthropic.claude-v2');
106+
expect(service['defaultModel']).toBe('anthropic.claude-3-7-sonnet-20250219-v1:0');
103107
expect(service['defaultMaxTokens']).toBe(1000);
104108
});
105109
});
106110

107111
describe('extractMedicalInfo', () => {
108-
const mockFileBuffer = Buffer.from('test file content');
109-
const mockFileType = 'application/pdf';
112+
const mockImageBuffer = Buffer.from('test image content');
113+
const mockImageTypes = ['image/jpeg', 'image/png'];
110114
const mockMedicalInfo = {
111-
keyMedicalTerms: [{ term: 'Hypertension', definition: 'High blood pressure' }],
115+
keyMedicalTerms: [
116+
{ term: 'Hemoglobin', definition: 'Protein in red blood cells that carries oxygen' },
117+
],
112118
labValues: [
113119
{
114-
name: 'Blood Pressure',
115-
value: '140/90',
116-
unit: 'mmHg',
117-
normalRange: '120/80',
118-
isAbnormal: true,
120+
name: 'Hemoglobin',
121+
value: '14.5',
122+
unit: 'g/dL',
123+
normalRange: '12.0-15.5',
124+
isAbnormal: false,
119125
},
120126
],
121127
diagnoses: [
122128
{
123-
condition: 'Hypertension',
124-
details: 'Elevated blood pressure',
125-
recommendations: 'Lifestyle changes and monitoring',
129+
condition: 'Normal Blood Count',
130+
details: 'All values within normal range',
131+
recommendations: 'Continue routine monitoring',
126132
},
127133
],
128134
metadata: {
@@ -145,45 +151,45 @@ ${JSON.stringify(mockMedicalInfo, null, 2)}
145151
};
146152

147153
beforeEach(() => {
148-
// Mock the Bedrock client response
149154
mockBedrockClient.send.mockResolvedValue(mockResponse);
150155
});
151156

152-
it('should successfully extract medical information from a file', async () => {
153-
const result = await service.extractMedicalInfo(mockFileBuffer, mockFileType);
154-
155-
// Verify the result structure
156-
expect(result).toHaveProperty('keyMedicalTerms');
157-
expect(result).toHaveProperty('labValues');
158-
expect(result).toHaveProperty('diagnoses');
159-
expect(result).toHaveProperty('metadata');
160-
161-
// Verify the command was called with correct parameters
162-
expect(InvokeModelCommand).toHaveBeenCalledWith(
163-
expect.objectContaining({
164-
modelId: 'anthropic.claude-v2',
165-
contentType: 'application/json',
166-
accept: 'application/json',
167-
}),
168-
);
169-
170-
// Verify the content of the extracted information
171-
expect(result.keyMedicalTerms[0].term).toBe('Hypertension');
172-
expect(result.labValues[0].name).toBe('Blood Pressure');
173-
expect(result.diagnoses[0].condition).toBe('Hypertension');
174-
expect(result.metadata.isMedicalReport).toBe(true);
175-
expect(result.metadata.confidence).toBe(0.95);
176-
});
157+
it.each(mockImageTypes)(
158+
'should successfully extract medical information from %s',
159+
async imageType => {
160+
const result = await service.extractMedicalInfo(mockImageBuffer, imageType);
161+
162+
expect(result).toHaveProperty('keyMedicalTerms');
163+
expect(result).toHaveProperty('labValues');
164+
expect(result).toHaveProperty('diagnoses');
165+
expect(result).toHaveProperty('metadata');
166+
167+
expect(InvokeModelCommand).toHaveBeenCalledWith(
168+
expect.objectContaining({
169+
modelId: expect.any(String),
170+
contentType: 'application/json',
171+
accept: 'application/json',
172+
body: expect.stringContaining(imageType),
173+
}),
174+
);
175+
176+
expect(result.keyMedicalTerms[0].term).toBe('Hemoglobin');
177+
expect(result.labValues[0].name).toBe('Hemoglobin');
178+
expect(result.diagnoses[0].condition).toBe('Normal Blood Count');
179+
expect(result.metadata.isMedicalReport).toBe(true);
180+
expect(result.metadata.confidence).toBe(0.95);
181+
},
182+
);
177183

178-
it('should reject non-medical reports', async () => {
184+
it('should reject non-medical images', async () => {
179185
const nonMedicalInfo = {
180186
keyMedicalTerms: [],
181187
labValues: [],
182188
diagnoses: [],
183189
metadata: {
184190
isMedicalReport: false,
185191
confidence: 0.1,
186-
missingInformation: ['Not a medical document'],
192+
missingInformation: ['Not a medical image'],
187193
},
188194
};
189195

@@ -199,85 +205,93 @@ ${JSON.stringify(nonMedicalInfo, null, 2)}
199205
body: Buffer.from(JSON.stringify(nonMedicalResponse)) as any,
200206
});
201207

202-
await expect(service.extractMedicalInfo(mockFileBuffer, mockFileType)).rejects.toThrow(
208+
await expect(service.extractMedicalInfo(mockImageBuffer, 'image/jpeg')).rejects.toThrow(
203209
BadRequestException,
204210
);
205211

206-
await expect(service.extractMedicalInfo(mockFileBuffer, mockFileType)).rejects.toThrow(
207-
'The provided document does not appear to be a medical report.',
212+
await expect(service.extractMedicalInfo(mockImageBuffer, 'image/jpeg')).rejects.toThrow(
213+
'The provided image does not appear to be a medical document.',
208214
);
209215
});
210216

211-
it('should handle low confidence medical reports', async () => {
212-
const lowConfidenceInfo = {
217+
it('should handle low quality or unclear images', async () => {
218+
const lowQualityInfo = {
213219
keyMedicalTerms: [],
214220
labValues: [],
215221
diagnoses: [],
216222
metadata: {
217223
isMedicalReport: true,
218224
confidence: 0.5,
219-
missingInformation: ['Unclear handwriting', 'Missing sections'],
225+
missingInformation: ['Image too blurry', 'Text not readable'],
220226
},
221227
};
222228

223-
const lowConfidenceResponse = {
229+
const lowQualityResponse = {
224230
content: `Analysis results:
225231
\`\`\`json
226-
${JSON.stringify(lowConfidenceInfo, null, 2)}
232+
${JSON.stringify(lowQualityInfo, null, 2)}
227233
\`\`\``,
228234
};
229235

230236
mockBedrockClient.send.mockResolvedValue({
231237
$metadata: {},
232-
body: Buffer.from(JSON.stringify(lowConfidenceResponse)) as any,
238+
body: Buffer.from(JSON.stringify(lowQualityResponse)) as any,
233239
});
234240

235-
await expect(service.extractMedicalInfo(mockFileBuffer, mockFileType)).rejects.toThrow(
241+
await expect(service.extractMedicalInfo(mockImageBuffer, 'image/jpeg')).rejects.toThrow(
236242
BadRequestException,
237243
);
238244

239-
await expect(service.extractMedicalInfo(mockFileBuffer, mockFileType)).rejects.toThrow(
240-
'Low confidence in medical report analysis',
245+
await expect(service.extractMedicalInfo(mockImageBuffer, 'image/jpeg')).rejects.toThrow(
246+
'Low confidence in medical image analysis',
241247
);
242248
});
243249

244-
it('should handle missing information in medical reports', async () => {
245-
const missingInfoData = {
246-
keyMedicalTerms: [{ term: 'Hypertension', definition: 'High blood pressure' }],
250+
it('should handle partially visible information in images', async () => {
251+
const partialInfo = {
252+
keyMedicalTerms: [
253+
{ term: 'Hemoglobin', definition: 'Protein in red blood cells that carries oxygen' },
254+
],
247255
labValues: [],
248256
diagnoses: [],
249257
metadata: {
250258
isMedicalReport: true,
251259
confidence: 0.8,
252-
missingInformation: ['Lab values', 'Recommendations'],
260+
missingInformation: ['Bottom portion of image cut off', 'Some values not visible'],
253261
},
254262
};
255263

256-
const missingInfoResponse = {
264+
const partialResponse = {
257265
content: `Here's what I found:
258266
\`\`\`json
259-
${JSON.stringify(missingInfoData, null, 2)}
267+
${JSON.stringify(partialInfo, null, 2)}
260268
\`\`\``,
261269
};
262270

263271
mockBedrockClient.send.mockResolvedValue({
264272
$metadata: {},
265-
body: Buffer.from(JSON.stringify(missingInfoResponse)) as any,
273+
body: Buffer.from(JSON.stringify(partialResponse)) as any,
266274
});
267275

268-
const result = await service.extractMedicalInfo(mockFileBuffer, mockFileType);
276+
const result = await service.extractMedicalInfo(mockImageBuffer, 'image/jpeg');
269277

270-
expect(result.metadata.missingInformation).toContain('Lab values');
271-
expect(result.metadata.missingInformation).toContain('Recommendations');
278+
expect(result.metadata.missingInformation).toContain('Bottom portion of image cut off');
279+
expect(result.metadata.missingInformation).toContain('Some values not visible');
272280
expect(result.metadata.confidence).toBe(0.8);
273281
});
274282

275-
it('should handle errors when file processing fails', async () => {
276-
const error = new Error('Processing failed');
283+
it('should reject unsupported file types', async () => {
284+
await expect(service.extractMedicalInfo(mockImageBuffer, 'application/pdf')).rejects.toThrow(
285+
'Only JPEG and PNG images are allowed',
286+
);
287+
});
288+
289+
it('should handle errors when image processing fails', async () => {
290+
const error = new Error('Image processing failed');
277291
mockBedrockClient.send.mockRejectedValue(error);
278292

279-
await expect(service.extractMedicalInfo(mockFileBuffer, mockFileType)).rejects.toThrow(
280-
'Failed to extract medical information: Processing failed',
293+
await expect(service.extractMedicalInfo(mockImageBuffer, 'image/jpeg')).rejects.toThrow(
294+
'Failed to extract medical information from image: Image processing failed',
281295
);
282296
});
283297

@@ -288,21 +302,9 @@ ${JSON.stringify(missingInfoData, null, 2)}
288302
};
289303
mockBedrockClient.send.mockResolvedValue(invalidResponse);
290304

291-
await expect(service.extractMedicalInfo(mockFileBuffer, mockFileType)).rejects.toThrow(
305+
await expect(service.extractMedicalInfo(mockImageBuffer, 'image/jpeg')).rejects.toThrow(
292306
'Failed to extract JSON from response',
293307
);
294308
});
295-
296-
it('should handle different file types', async () => {
297-
const imageFileType = 'image/jpeg';
298-
await service.extractMedicalInfo(mockFileBuffer, imageFileType);
299-
300-
// Verify the command was called with the correct file type
301-
expect(InvokeModelCommand).toHaveBeenCalledWith(
302-
expect.objectContaining({
303-
body: expect.stringContaining(imageFileType),
304-
}),
305-
);
306-
});
307309
});
308310
});

backend/src/services/aws-bedrock.service.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class AwsBedrockService {
7878
}
7979

8080
/**
81-
* Extracts medical information from a file using AWS Bedrock
81+
* Extracts medical information from an image using AWS Bedrock
8282
*/
8383
async extractMedicalInfo(
8484
fileBuffer: Buffer,
@@ -91,10 +91,10 @@ export class AwsBedrockService {
9191
throw new BadRequestException('Too many requests. Please try again later.');
9292
}
9393

94-
// 2. Validate file securely
94+
// 2. Validate file securely (only images allowed)
9595
validateFileSecurely(fileBuffer, fileType);
9696

97-
// 3. Prepare the prompt for medical information extraction
97+
// 3. Prepare the prompt for medical information extraction from image
9898
const prompt = this.buildMedicalExtractionPrompt(fileBuffer.toString('base64'), fileType);
9999

100100
// 4. Call Bedrock with proper error handling
@@ -106,20 +106,20 @@ export class AwsBedrockService {
106106
// 6. Validate medical report status
107107
if (!extractedInfo.metadata.isMedicalReport) {
108108
throw new BadRequestException(
109-
'The provided document does not appear to be a medical report.',
109+
'The provided image does not appear to be a medical document.',
110110
);
111111
}
112112

113113
// 7. Check confidence level
114114
if (extractedInfo.metadata.confidence < 0.7) {
115-
throw new BadRequestException('Low confidence in medical report analysis');
115+
throw new BadRequestException('Low confidence in medical image analysis');
116116
}
117117

118118
// 8. Sanitize the extracted data
119119
return sanitizeMedicalData(extractedInfo);
120120
} catch (error: unknown) {
121121
// Log error securely without exposing sensitive details
122-
this.logger.error('Error processing medical document', {
122+
this.logger.error('Error processing medical image', {
123123
error: error instanceof Error ? error.message : 'Unknown error',
124124
fileType,
125125
timestamp: new Date().toISOString(),
@@ -131,23 +131,23 @@ export class AwsBedrockService {
131131
}
132132

133133
throw new BadRequestException(
134-
`Failed to extract medical information: ${error instanceof Error ? error.message : 'Unknown error'}`,
134+
`Failed to extract medical information from image: ${error instanceof Error ? error.message : 'Unknown error'}`,
135135
);
136136
}
137137
}
138138

139139
/**
140-
* Builds the prompt for medical information extraction
140+
* Builds the prompt for medical information extraction from images
141141
*/
142142
private buildMedicalExtractionPrompt(base64Content: string, fileType: string): string {
143143
return JSON.stringify({
144-
prompt: `\n\nHuman: Please analyze this medical document and extract key information. The document is provided as a base64-encoded ${fileType} file: ${base64Content}
144+
prompt: `\n\nHuman: Please analyze this medical image and extract key information. The image is provided as a base64-encoded ${fileType} file: ${base64Content}
145145
146-
Please extract and structure the following information:
147-
1. Key medical terms with their definitions
148-
2. Lab values with their normal ranges and any abnormalities
149-
3. Diagnoses with details and recommendations
150-
4. Analyze if this is a medical report and provide confidence level
146+
Please analyze the image carefully and extract the following information:
147+
1. Key medical terms visible in the image with their definitions
148+
2. Any visible lab values with their normal ranges and abnormalities
149+
3. Any diagnoses, findings, or medical observations with details and recommendations
150+
4. Analyze if this is a medical image (e.g., lab report, medical chart, prescription) and provide confidence level
151151
152152
Format the response as a JSON object with the following structure:
153153
{
@@ -161,9 +161,10 @@ Format the response as a JSON object with the following structure:
161161
}
162162
}
163163
164-
If information is missing, list the missing elements in the missingInformation array.
165-
Set confidence between 0 and 1 based on how confident you are about the medical nature and completeness of the document.
166-
Ensure all medical terms are explained in plain language. Mark lab values as abnormal if they fall outside the normal range.`,
164+
If any information is not visible or unclear in the image, list those items in the missingInformation array.
165+
Set confidence between 0 and 1 based on image clarity and how confident you are about the medical nature of the document.
166+
Ensure all visible medical terms are explained in plain language. Mark lab values as abnormal if they fall outside the normal range.
167+
If text in the image is not clear or partially visible, note this in the metadata.`,
167168
});
168169
}
169170

0 commit comments

Comments
 (0)