Skip to content

Commit 6e51c34

Browse files
authored
Merge pull request #66 from ModusCreateOrg/ADE-152
[ADE-152] glue input and output of document-processor
2 parents 767cf99 + 7183764 commit 6e51c34

21 files changed

+1716
-798
lines changed

backend/package-lock.json

Lines changed: 1227 additions & 368 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@aws-sdk/client-bedrock": "^3.782.0",
3131
"@aws-sdk/client-bedrock-runtime": "^3.782.0",
3232
"@aws-sdk/client-dynamodb": "^3.758.0",
33+
"@aws-sdk/client-s3": "^3.787.0",
3334
"@aws-sdk/client-secrets-manager": "^3.758.0",
3435
"@aws-sdk/client-textract": "^3.782.0",
3536
"@aws-sdk/util-dynamodb": "^3.758.0",
@@ -74,8 +75,8 @@
7475
"@types/jwk-to-pem": "^2.0.2",
7576
"@types/multer": "^1.4.12",
7677
"@types/node": "^20.12.7",
77-
"@typescript-eslint/eslint-plugin": "^7.9.0",
78-
"@typescript-eslint/parser": "^7.9.0",
78+
"@typescript-eslint/eslint-plugin": "^8.30.1",
79+
"@typescript-eslint/parser": "^8.30.1",
7980
"@vitest/coverage-v8": "^3.1.1",
8081
"aws-cdk": "2.139.0",
8182
"aws-cdk-lib": "^2.185.0",

backend/src/app.module.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ describe('AppModule', () => {
4949
.useValue({
5050
generateResponse: vi.fn().mockResolvedValue('test response'),
5151
analyzeMedicalDocument: vi.fn().mockResolvedValue({
52-
keyMedicalTerms: [],
5352
labValues: [],
5453
diagnoses: [],
5554
metadata: {

backend/src/app.module.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ import { DocumentProcessorModule } from './document-processor/document-processor
2626
})
2727
export class AppModule implements NestModule {
2828
configure(consumer: MiddlewareConsumer) {
29-
consumer.apply(AuthMiddleware).exclude('document-processor/(.*)').forRoutes('*');
29+
consumer
30+
.apply(AuthMiddleware)
31+
.exclude(
32+
'document-processor/upload',
33+
'document-processor/test',
34+
'document-processor/test-form',
35+
)
36+
.forRoutes('*');
3037
}
3138
}

backend/src/config/configuration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ export default () => ({
33
environment: process.env.NODE_ENV || 'development',
44
aws: {
55
region: process.env.AWS_REGION || 'us-east-1',
6+
s3: {
7+
uploadBucket: process.env.S3_UPLOAD_BUCKET || '',
8+
},
69
cognito: {
710
userPoolId: process.env.AWS_COGNITO_USER_POOL_ID,
811
clientId: process.env.AWS_COGNITO_CLIENT_ID,
@@ -35,4 +38,6 @@ export default () => ({
3538
model: process.env.PERPLEXITY_MODEL || 'sonar',
3639
maxTokens: parseInt(process.env.PERPLEXITY_MAX_TOKENS || '2048', 10),
3740
},
41+
dynamodbReportsTable:
42+
process.env.DYNAMODB_REPORTS_TABLE || 'AIMedicalReportReportsTabledevelopment',
3843
});

backend/src/document-processor/controllers/document-processor.controller.ts

Lines changed: 156 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {
88
Logger,
99
Get,
1010
Res,
11+
Req,
12+
UnauthorizedException,
13+
NotFoundException,
14+
InternalServerErrorException,
1115
} from '@nestjs/common';
1216
import { FileInterceptor } from '@nestjs/platform-express';
1317
import {
@@ -16,12 +20,21 @@ import {
1620
} from '../services/document-processor.service';
1721
import { Express } from 'express';
1822
import { Response } from 'express';
23+
import { ReportsService } from '../../reports/reports.service';
24+
import { RequestWithUser } from '../../auth/auth.middleware';
25+
import { Readable } from 'stream';
26+
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
27+
import { ConfigService } from '@nestjs/config';
1928

2029
@Controller('document-processor')
2130
export class DocumentProcessorController {
2231
private readonly logger = new Logger(DocumentProcessorController.name);
2332

24-
constructor(private readonly documentProcessorService: DocumentProcessorService) {}
33+
constructor(
34+
private readonly documentProcessorService: DocumentProcessorService,
35+
private readonly reportsService: ReportsService,
36+
private readonly configService: ConfigService,
37+
) {}
2538

2639
@Post('upload')
2740
@UseInterceptors(FileInterceptor('file'))
@@ -35,11 +48,17 @@ export class DocumentProcessorController {
3548
}
3649

3750
// Validate file type
38-
const validMimeTypes = ['image/jpeg', 'image/png', 'image/tiff', 'application/pdf'];
51+
const validMimeTypes = [
52+
'image/jpeg',
53+
'image/png',
54+
'image/heic',
55+
'image/heif',
56+
'application/pdf',
57+
];
3958

4059
if (!validMimeTypes.includes(file.mimetype)) {
4160
throw new BadRequestException(
42-
`Invalid file type: ${file.mimetype}. Supported types: JPEG, PNG, TIFF, and PDF.`,
61+
`Invalid file type: ${file.mimetype}. Supported types: JPEG, PNG, HEIC, HEIF, and PDF.`,
4362
);
4463
}
4564

@@ -63,7 +82,6 @@ export class DocumentProcessorController {
6382
// Process the document
6483
const result = await this.documentProcessorService.processDocument(
6584
file.buffer,
66-
file.mimetype,
6785
effectiveUserId,
6886
);
6987

@@ -95,6 +113,138 @@ export class DocumentProcessorController {
95113
}
96114
}
97115

116+
@Post('process-file')
117+
async processFileFromPath(
118+
@Body('filePath') filePath: string,
119+
@Req() request: RequestWithUser,
120+
): Promise<ProcessedDocumentResult | any> {
121+
if (!filePath) {
122+
throw new BadRequestException('No filePath provided');
123+
}
124+
125+
// Extract userId from the request (attached by auth middleware)
126+
const userId = request.user?.sub;
127+
if (!userId) {
128+
throw new UnauthorizedException('User ID not found in request');
129+
}
130+
131+
this.logger.log(`Processing document from file path: ${filePath}`);
132+
133+
try {
134+
// Fetch the associated report record from DynamoDB
135+
const report = await this.reportsService.findByFilePath(filePath, userId);
136+
if (!report) {
137+
throw new NotFoundException(`Report with filePath ${filePath} not found`);
138+
}
139+
140+
// Get the file from S3
141+
const fileBuffer = await this.getFileFromS3(filePath);
142+
143+
// Process the document
144+
const result = await this.documentProcessorService.processDocument(fileBuffer, userId);
145+
146+
// Update the report with analysis results
147+
report.title = result.analysis.title || 'Untitled Report';
148+
report.category = result.analysis.category || 'general';
149+
report.isProcessed = true;
150+
151+
// Extract lab values
152+
report.labValues = result.analysis.labValues || [];
153+
154+
// Create summary from simplified explanation or diagnoses
155+
report.summary =
156+
result.simplifiedExplanation ||
157+
result.analysis.diagnoses.map(d => d.condition).join(', ') ||
158+
'No summary available';
159+
160+
report.updatedAt = new Date().toISOString();
161+
162+
// Update the report in DynamoDB
163+
await this.reportsService.updateReport(report);
164+
165+
return {
166+
success: true,
167+
reportId: report.id,
168+
analysis: result.analysis,
169+
};
170+
} catch (error: unknown) {
171+
this.logger.error(
172+
`Error processing document from path ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
173+
);
174+
throw error;
175+
}
176+
}
177+
178+
/**
179+
* Retrieves a file from S3 storage
180+
* @param filePath - The S3 key of the file
181+
* @returns Buffer containing the file data
182+
*/
183+
private async getFileFromS3(filePath: string): Promise<Buffer> {
184+
try {
185+
const bucketName = this.configService.get<string>('aws.s3.uploadBucket');
186+
if (!bucketName) {
187+
throw new InternalServerErrorException('S3 bucket name not configured');
188+
}
189+
190+
const region = this.configService.get<string>('aws.region') || 'us-east-1';
191+
192+
// Get optional AWS credentials if they exist
193+
const accessKeyId = this.configService.get<string>('aws.aws.accessKeyId');
194+
const secretAccessKey = this.configService.get<string>('aws.aws.secretAccessKey');
195+
const sessionToken = this.configService.get<string>('aws.aws.sessionToken');
196+
197+
// Create S3 client with credentials if they exist
198+
const s3ClientOptions: any = { region };
199+
200+
if (accessKeyId && secretAccessKey) {
201+
s3ClientOptions.credentials = {
202+
accessKeyId,
203+
secretAccessKey,
204+
...(sessionToken && { sessionToken }),
205+
};
206+
}
207+
208+
const s3Client = new S3Client(s3ClientOptions);
209+
210+
const command = new GetObjectCommand({
211+
Bucket: bucketName,
212+
Key: filePath,
213+
});
214+
215+
const response = await s3Client.send(command);
216+
217+
// Check if response.Body exists before converting
218+
if (!response.Body) {
219+
throw new InternalServerErrorException('Empty response from S3');
220+
}
221+
222+
// Convert the readable stream to a buffer
223+
return await this.streamToBuffer(response.Body as Readable);
224+
} catch (error) {
225+
this.logger.error(
226+
`Error retrieving file from S3: ${error instanceof Error ? error.message : 'Unknown error'}`,
227+
);
228+
throw new InternalServerErrorException(
229+
`Failed to retrieve file from S3: ${error instanceof Error ? error.message : 'Unknown error'}`,
230+
);
231+
}
232+
}
233+
234+
/**
235+
* Converts a readable stream to a buffer
236+
* @param stream - The readable stream from S3
237+
* @returns Buffer containing the stream data
238+
*/
239+
private async streamToBuffer(stream: Readable): Promise<Buffer> {
240+
return new Promise((resolve, reject) => {
241+
const chunks: Buffer[] = [];
242+
stream.on('data', chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
243+
stream.on('end', () => resolve(Buffer.concat(chunks)));
244+
stream.on('error', reject);
245+
});
246+
}
247+
98248
@Get('test')
99249
getTestStatus(): { status: string } {
100250
return { status: 'DocumentProcessorController is working' };
@@ -229,8 +379,8 @@ export class DocumentProcessorController {
229379
230380
<form id="uploadForm" enctype="multipart/form-data">
231381
<div class="form-group">
232-
<label for="file">Select File (PDF, JPEG, PNG, TIFF):</label>
233-
<input type="file" id="file" name="file" accept=".pdf,.jpg,.jpeg,.png,.tiff">
382+
<label for="file">Select File (PDF, JPEG, PNG, HEIC, HEIF):</label>
383+
<input type="file" id="file" name="file" accept=".pdf,.jpg,.jpeg,.png,.heic,.heif">
234384
</div>
235385
236386
<div id="filePreview"></div>

backend/src/document-processor/document-processor.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { AwsBedrockService } from './services/aws-bedrock.service';
66
import { DocumentProcessorController } from './controllers/document-processor.controller';
77
import { PerplexityService } from '../services/perplexity.service';
88
import { AwsSecretsService } from '../services/aws-secrets.service';
9+
import { ReportsModule } from '../reports/reports.module';
910

1011
@Module({
11-
imports: [ConfigModule],
12+
imports: [ConfigModule, ReportsModule],
1213
controllers: [DocumentProcessorController],
1314
providers: [
1415
DocumentProcessorService,

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

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,19 @@ vi.mock('@aws-sdk/client-bedrock-runtime', () => {
6565
{
6666
type: 'text',
6767
text: JSON.stringify({
68-
keyMedicalTerms: [
69-
{ term: 'RBC', definition: 'Red Blood Cells' },
70-
{ term: 'WBC', definition: 'White Blood Cells' },
71-
],
68+
title: 'Blood Test Results',
69+
category: 'general',
7270
labValues: [
7371
{
7472
name: 'Hemoglobin',
7573
value: '14.2',
7674
unit: 'g/dL',
7775
normalRange: '13.5-17.5',
78-
isAbnormal: false,
76+
status: 'normal',
77+
isCritical: false,
78+
conclusion:
79+
'Normal hemoglobin levels indicate adequate oxygen-carrying capacity.',
80+
suggestions: 'Continue regular health maintenance.',
7981
},
8082
],
8183
diagnoses: [],
@@ -126,17 +128,18 @@ describe('AwsBedrockService', () => {
126128
`;
127129

128130
const mockMedicalAnalysis: MedicalDocumentAnalysis = {
129-
keyMedicalTerms: [
130-
{ term: 'RBC', definition: 'Red Blood Cells' },
131-
{ term: 'WBC', definition: 'White Blood Cells' },
132-
],
131+
title: 'Blood Test Results',
132+
category: 'general',
133133
labValues: [
134134
{
135135
name: 'Hemoglobin',
136136
value: '14.2',
137137
unit: 'g/dL',
138138
normalRange: '13.5-17.5',
139-
isAbnormal: false,
139+
status: 'normal',
140+
isCritical: false,
141+
conclusion: 'Normal hemoglobin levels indicate adequate oxygen-carrying capacity.',
142+
suggestions: 'Continue regular health maintenance.',
140143
},
141144
],
142145
diagnoses: [],
@@ -290,10 +293,8 @@ describe('AwsBedrockService', () => {
290293
const invalidResponses = [
291294
null,
292295
{},
293-
{ keyMedicalTerms: 'not an array' },
294-
{ keyMedicalTerms: [], labValues: [], diagnoses: [] }, // Missing metadata
296+
{ labValues: [], diagnoses: [] }, // Missing metadata
295297
{
296-
keyMedicalTerms: [],
297298
labValues: [],
298299
diagnoses: [],
299300
metadata: { isMedicalReport: 'not a boolean', confidence: 0.5, missingInformation: [] },
@@ -309,8 +310,21 @@ describe('AwsBedrockService', () => {
309310

310311
// Test a valid response
311312
const validResponse: MedicalDocumentAnalysis = {
312-
keyMedicalTerms: [],
313-
labValues: [],
313+
title: 'Test Report',
314+
category: 'general',
315+
labValues: [
316+
// Adding an empty lab value with required properties
317+
{
318+
name: 'Sample Test',
319+
value: '0',
320+
unit: 'units',
321+
normalRange: '0-1',
322+
status: 'normal',
323+
isCritical: false,
324+
conclusion: 'Normal test result',
325+
suggestions: 'No action needed',
326+
},
327+
],
314328
diagnoses: [],
315329
metadata: {
316330
isMedicalReport: true,

0 commit comments

Comments
 (0)