Skip to content

Commit e332a5c

Browse files
committed
Enhance AwsBedrockService with medical document analysis capabilities
- Introduced MedicalDocumentAnalysis interface in backend/src/services/aws-bedrock.service.ts to define the structure of medical analysis results. - Implemented analyzeMedicalDocument method to analyze medical documents and return structured data, including key medical terms, lab values, and diagnoses. - Added comprehensive mock responses for various scenarios in backend/src/services/aws-bedrock.service.spec.ts to improve unit test coverage. - Included validation for response structure and error handling for invalid or empty responses, ensuring robustness in medical document processing.
1 parent b701e74 commit e332a5c

File tree

2 files changed

+402
-1
lines changed

2 files changed

+402
-1
lines changed

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

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ConfigService } from '@nestjs/config';
2-
import { AwsBedrockService } from './aws-bedrock.service';
2+
import { AwsBedrockService, MedicalDocumentAnalysis } from './aws-bedrock.service';
33
import { describe, it, expect, beforeEach, vi, beforeAll, afterAll } from 'vitest';
4+
import { BadRequestException } from '@nestjs/common';
45

56
// Mock the Logger
67
vi.mock('@nestjs/common', async () => {
@@ -17,6 +18,97 @@ vi.mock('@nestjs/common', async () => {
1718
};
1819
});
1920

21+
// Mock AWS SDK Bedrock client
22+
vi.mock('@aws-sdk/client-bedrock-runtime', () => {
23+
return {
24+
BedrockRuntimeClient: vi.fn().mockImplementation(() => ({
25+
send: vi.fn().mockImplementation(command => {
26+
// Get the prompt from the command body
27+
const body = JSON.parse(command.input.body);
28+
const prompt = body.messages[0].content[0].text;
29+
30+
// Basic mock response structure with required properties
31+
const createMockResponse = (bodyContent: any) => ({
32+
body: new TextEncoder().encode(JSON.stringify(bodyContent)),
33+
contentType: 'application/json',
34+
$metadata: {
35+
httpStatusCode: 200,
36+
requestId: 'mock-request-id',
37+
attempts: 1,
38+
totalRetryDelay: 0,
39+
},
40+
});
41+
42+
// Return different mock responses based on the prompt content
43+
if (prompt.includes('invalid document')) {
44+
return Promise.resolve(
45+
createMockResponse({
46+
content: [
47+
{
48+
type: 'text',
49+
text: 'This is not valid JSON',
50+
},
51+
],
52+
}),
53+
);
54+
} else if (prompt.includes('empty response')) {
55+
return Promise.resolve(
56+
createMockResponse({
57+
content: [],
58+
}),
59+
);
60+
} else if (prompt.includes('BLOOD TEST RESULTS')) {
61+
// Only return success response for actual medical document text
62+
return Promise.resolve(
63+
createMockResponse({
64+
content: [
65+
{
66+
type: 'text',
67+
text: JSON.stringify({
68+
keyMedicalTerms: [
69+
{ term: 'RBC', definition: 'Red Blood Cells' },
70+
{ term: 'WBC', definition: 'White Blood Cells' },
71+
],
72+
labValues: [
73+
{
74+
name: 'Hemoglobin',
75+
value: '14.2',
76+
unit: 'g/dL',
77+
normalRange: '13.5-17.5',
78+
isAbnormal: false,
79+
},
80+
],
81+
diagnoses: [],
82+
metadata: {
83+
isMedicalReport: true,
84+
confidence: 0.95,
85+
missingInformation: [],
86+
},
87+
}),
88+
},
89+
],
90+
}),
91+
);
92+
} else {
93+
return Promise.resolve(
94+
createMockResponse({
95+
content: [
96+
{
97+
type: 'text',
98+
text: 'Default response',
99+
},
100+
],
101+
}),
102+
);
103+
}
104+
}),
105+
})),
106+
InvokeModelCommand: vi.fn().mockImplementation(params => ({
107+
input: params,
108+
})),
109+
};
110+
});
111+
20112
describe('AwsBedrockService', () => {
21113
let service: AwsBedrockService;
22114
let mockConfigService: ConfigService;
@@ -62,4 +154,137 @@ describe('AwsBedrockService', () => {
62154
expect(service['defaultMaxTokens']).toBe(1000);
63155
});
64156
});
157+
158+
describe('analyzeMedicalDocument', () => {
159+
it('should successfully analyze a valid medical document', async () => {
160+
// Create a sample medical document text
161+
const sampleText = `
162+
BLOOD TEST RESULTS
163+
Patient: John Doe
164+
Date: 2023-01-15
165+
166+
Red Blood Cells (RBC): 5.1 x10^6/µL (Normal: 4.5-5.9)
167+
White Blood Cells (WBC): 7.2 x10^3/µL (Normal: 4.5-11.0)
168+
Hemoglobin: 14.2 g/dL (Normal: 13.5-17.5)
169+
`;
170+
171+
// Call the method
172+
const result = await service.analyzeMedicalDocument(sampleText);
173+
174+
// Assert the result
175+
expect(result).toBeDefined();
176+
expect(result.keyMedicalTerms).toHaveLength(2);
177+
expect(result.keyMedicalTerms[0].term).toBe('RBC');
178+
expect(result.keyMedicalTerms[0].definition).toBe('Red Blood Cells');
179+
180+
expect(result.labValues).toHaveLength(1);
181+
expect(result.labValues[0].name).toBe('Hemoglobin');
182+
expect(result.labValues[0].value).toBe('14.2');
183+
expect(result.labValues[0].unit).toBe('g/dL');
184+
expect(result.labValues[0].isAbnormal).toBe(false);
185+
186+
expect(result.metadata.isMedicalReport).toBe(true);
187+
expect(result.metadata.confidence).toBeGreaterThan(0.9);
188+
});
189+
190+
it('should correctly format the prompt for medical document analysis', async () => {
191+
// Spy on the invokeBedrock method
192+
const invokeBedrockSpy = vi.spyOn(service as any, 'invokeBedrock');
193+
194+
// Sample document text
195+
const sampleText = 'Sample medical document';
196+
197+
try {
198+
await service.analyzeMedicalDocument(sampleText);
199+
} catch (error) {
200+
// We don't care about the result, just the prompt format
201+
}
202+
203+
// Verify invokeBedrock was called
204+
expect(invokeBedrockSpy).toHaveBeenCalled();
205+
206+
// Verify the prompt format
207+
const prompt = invokeBedrockSpy.mock.calls[0][0] as string;
208+
209+
// Check key elements of the prompt
210+
expect(prompt).toContain('Please analyze this medical document carefully');
211+
expect(prompt).toContain('Format the response as a JSON object');
212+
expect(prompt).toContain('keyMedicalTerms');
213+
expect(prompt).toContain('labValues');
214+
expect(prompt).toContain('diagnoses');
215+
expect(prompt).toContain('metadata');
216+
expect(prompt).toContain('Sample medical document'); // Document text is appended
217+
});
218+
219+
it('should throw BadRequestException for invalid JSON response', async () => {
220+
// Create a sample invalid document text
221+
const invalidDocument = 'This is an invalid document that will cause an invalid response';
222+
223+
// Expect the method to throw BadRequestException
224+
await expect(service.analyzeMedicalDocument(invalidDocument)).rejects.toThrow(
225+
BadRequestException,
226+
);
227+
});
228+
229+
it('should throw BadRequestException for empty response', async () => {
230+
// Create a sample text that will trigger an empty response
231+
const emptyResponseText = 'This will trigger an empty response';
232+
233+
// Expect the method to throw BadRequestException
234+
await expect(service.analyzeMedicalDocument(emptyResponseText)).rejects.toThrow(
235+
BadRequestException,
236+
);
237+
});
238+
239+
it('should handle rate limiting correctly', async () => {
240+
// Mock the rate limiter to reject requests
241+
service['rateLimiter'].tryRequest = vi.fn().mockReturnValue(false);
242+
243+
// Create a sample medical document text
244+
const sampleText = 'Sample medical document text';
245+
246+
// Expect the method to throw BadRequestException due to rate limiting
247+
await expect(service.analyzeMedicalDocument(sampleText)).rejects.toThrow(
248+
'Rate limit exceeded',
249+
);
250+
});
251+
252+
it('should validate response structure correctly', () => {
253+
// Create invalid response objects to test validation
254+
const invalidResponses = [
255+
null,
256+
{},
257+
{ keyMedicalTerms: 'not an array' },
258+
{ keyMedicalTerms: [], labValues: [], diagnoses: [] }, // Missing metadata
259+
{
260+
keyMedicalTerms: [],
261+
labValues: [],
262+
diagnoses: [],
263+
metadata: { isMedicalReport: 'not a boolean', confidence: 0.5, missingInformation: [] },
264+
},
265+
];
266+
267+
// Test each invalid response
268+
invalidResponses.forEach(response => {
269+
expect(() => service['validateMedicalAnalysisResponse'](response)).toThrow(
270+
BadRequestException,
271+
);
272+
});
273+
274+
// Test a valid response
275+
const validResponse: MedicalDocumentAnalysis = {
276+
keyMedicalTerms: [],
277+
labValues: [],
278+
diagnoses: [],
279+
metadata: {
280+
isMedicalReport: true,
281+
confidence: 0.9,
282+
missingInformation: [],
283+
},
284+
};
285+
286+
// Should not throw for valid response
287+
expect(() => service['validateMedicalAnalysisResponse'](validResponse)).not.toThrow();
288+
});
289+
});
65290
});

0 commit comments

Comments
 (0)