Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { RequestWithUser } from '../../auth/auth.middleware';
import { Readable } from 'stream';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';
import { ProcessingStatus } from '../../reports/models/report.model';

@Controller('document-processor')
export class DocumentProcessorController {
Expand All @@ -38,7 +39,7 @@ export class DocumentProcessorController {

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async processDocument(
async uploadAndProcessReport(
@UploadedFile() file: Express.Multer.File,
@Body('userId') userId: string,
@Body('debug') debug?: string,
Expand Down Expand Up @@ -114,12 +115,12 @@ export class DocumentProcessorController {
}

@Post('process-file')
async processFileFromPath(
@Body('filePath') filePath: string,
async processReport(
@Body('reportId') reportId: string,
@Req() request: RequestWithUser,
): Promise<ProcessedDocumentResult | any> {
if (!filePath) {
throw new BadRequestException('No filePath provided');
): Promise<any> {
if (!reportId) {
throw new BadRequestException('No reportId provided');
}

// Extract userId from the request (attached by auth middleware)
Expand All @@ -128,25 +129,74 @@ export class DocumentProcessorController {
throw new UnauthorizedException('User ID not found in request');
}

this.logger.log(`Processing document from file path: ${filePath}`);
this.logger.log(`Queueing document for processing, report ID: ${reportId}`);

try {
// Fetch the associated report record from DynamoDB
const report = await this.reportsService.findByFilePath(filePath, userId);
// Fetch the associated report record from DynamoDB using findOne method
const report = await this.reportsService.findOne(reportId, userId);
if (!report) {
throw new NotFoundException(`Report with filePath ${filePath} not found`);
throw new NotFoundException(`Report with ID ${reportId} not found`);
}

// Make sure we have a filePath to retrieve the file
if (!report.filePath) {
throw new BadRequestException(`Report with ID ${reportId} has no associated file`);
}

// Update report status to IN_PROGRESS before starting async processing
report.processingStatus = ProcessingStatus.IN_PROGRESS;
report.updatedAt = new Date().toISOString();
await this.reportsService.updateReport(report);

// Start async processing in background
this.processReportAsync(reportId, userId, report.filePath).catch(error => {
this.logger.error(`Async processing failed for report ${reportId}: ${error.message}`);
});

return {
success: true,
reportId: report.id,
status: ProcessingStatus.IN_PROGRESS,
message: 'Document processing started. Check the report status to know when it completes.',
};
} catch (error: unknown) {
this.logger.error(
`Error queueing document for report ID ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error;
}
}

/**
* Processes a report file asynchronously
* @param reportId - ID of the report to process
* @param userId - ID of the user who owns the report
* @param filePath - S3 path to the file
*/
private async processReportAsync(
reportId: string,
userId: string,
filePath: string,
): Promise<void> {
try {
this.logger.log(`Started async processing for report: ${reportId}`);

// Get the file from S3
const fileBuffer = await this.getFileFromS3(filePath);

// Process the document
const result = await this.documentProcessorService.processDocument(fileBuffer, userId);

// Fetch the report again to ensure we have the latest version
const report = await this.reportsService.findOne(reportId, userId);
if (!report) {
throw new Error(`Report ${reportId} not found during async processing`);
}

// Update the report with analysis results
report.title = result.analysis.title || 'Untitled Report';
report.category = result.analysis.category || 'general';
report.isProcessed = true;
report.processingStatus = ProcessingStatus.PROCESSED;

// Extract lab values
report.labValues = result.analysis.labValues || [];
Expand All @@ -162,13 +212,26 @@ export class DocumentProcessorController {
// Update the report in DynamoDB
await this.reportsService.updateReport(report);

return {
success: true,
reportId: report.id,
};
} catch (error: unknown) {
this.logger.log(`Completed async processing for report: ${reportId}`);
} catch (error) {
// If processing fails, update the report status to indicate failure
try {
const report = await this.reportsService.findOne(reportId, userId);
if (report) {
report.processingStatus = ProcessingStatus.UNPROCESSED; // Could add a FAILED status in the enum if needed
report.updatedAt = new Date().toISOString();
await this.reportsService.updateReport(report);
}
} catch (updateError: unknown) {
this.logger.error(
`Failed to update report status after processing error: ${
updateError instanceof Error ? updateError.message : 'Unknown error'
}`,
);
}

this.logger.error(
`Error processing document from path ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
`Error during async processing for report ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error;
}
Expand Down Expand Up @@ -606,4 +669,42 @@ export class DocumentProcessorController {

res.type('text/html').send(html);
}

@Get('report-status/:reportId')
async getReportStatus(
@Req() request: RequestWithUser,
@Body('reportId') idFromBody: string,
): Promise<any> {
// Get reportId from path parameter or body
const reportId = request.params.reportId || idFromBody;

if (!reportId) {
throw new BadRequestException('No reportId provided');
}

// Extract userId from the request (attached by auth middleware)
const userId = request.user?.sub;
if (!userId) {
throw new UnauthorizedException('User ID not found in request');
}

try {
// Fetch the associated report record from DynamoDB
const report = await this.reportsService.findOne(reportId, userId);
if (!report) {
throw new NotFoundException(`Report with ID ${reportId} not found`);
}

return {
reportId: report.id,
status: report.processingStatus,
isComplete: report.processingStatus === ProcessingStatus.PROCESSED,
};
} catch (error: unknown) {
this.logger.error(
`Error fetching report status for ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error;
}
}
}
1 change: 0 additions & 1 deletion backend/src/iac/backend-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,6 @@ export class BackendStack extends cdk.Stack {
const integrationOptions = {
connectionType: apigateway.ConnectionType.VPC_LINK,
vpcLink: vpcLink,
timeout: cdk.Duration.seconds(300), // Adding 5-minute timeout (300 seconds)
};

const getDocsIntegration = new apigateway.Integration({
Expand Down
14 changes: 12 additions & 2 deletions backend/src/reports/models/report.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export enum ReportStatus {
READ = 'READ',
}

export enum ProcessingStatus {
PROCESSED = 'processed',
UNPROCESSED = 'unprocessed',
IN_PROGRESS = 'in_progress',
}

export class Report {
@ApiProperty({ description: 'Unique identifier for the report' })
id: string;
Expand All @@ -21,8 +27,12 @@ export class Report {
@ApiProperty({ description: 'Category of the report' })
category: string;

@ApiProperty({ description: 'Whether the report has been processed' })
isProcessed: boolean;
@ApiProperty({
description: 'Processing status of the report',
enum: ProcessingStatus,
default: ProcessingStatus.UNPROCESSED,
})
processingStatus: ProcessingStatus;

@ApiProperty({ description: 'List of lab values' })
labValues: Array<{
Expand Down
4 changes: 2 additions & 2 deletions backend/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
QueryCommand,
} from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { Report, ReportStatus } from './models/report.model';
import { Report, ReportStatus, ProcessingStatus } from './models/report.model';
import { GetReportsQueryDto } from './dto/get-reports.dto';
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
import { v4 as uuidv4 } from 'uuid';
Expand Down Expand Up @@ -299,7 +299,7 @@ export class ReportsService {
title: 'New Report',
bookmarked: false,
category: '',
isProcessed: false,
processingStatus: ProcessingStatus.UNPROCESSED,
labValues: [],
summary: '',
status: ReportStatus.UNREAD,
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/common/api/reportService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios, { AxiosProgressEvent } from 'axios';
import { MedicalReport, ReportCategory, ReportStatus } from '../models/medicalReport';
import { MedicalReport, ReportCategory, ReportStatus, ProcessingStatus } from '../models/medicalReport';
import { fetchAuthSession } from '@aws-amplify/auth';
// Get the API URL from environment variables
const API_URL = import.meta.env.VITE_BASE_URL_API || '';
Expand All @@ -12,7 +12,7 @@ const mockReports: MedicalReport[] = [
title: 'Blood Test Report',
category: ReportCategory.GENERAL,
bookmarked: false,
isProcessed: true,
processingStatus: ProcessingStatus.PROCESSED,
labValues: [],
summary: 'Blood test results within normal range',
status: ReportStatus.UNREAD,
Expand All @@ -26,7 +26,7 @@ const mockReports: MedicalReport[] = [
title: 'Heart Checkup',
category: ReportCategory.HEART,
bookmarked: true,
isProcessed: true,
processingStatus: ProcessingStatus.PROCESSED,
labValues: [],
summary: 'Heart functioning normally',
status: ReportStatus.READ,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/common/components/Upload/UploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ const UploadModal = ({ isOpen, onClose, onUploadComplete }: UploadModalProps): J
if (onUploadComplete) {
onUploadComplete(result);
}
// Navigate to the processing tab with filePath in state
// Navigate to the processing tab with reportId in state
if (file) {
history.push('/tabs/processing', {
filePath: result.filePath,
reportId: result.id,
});
} else {
history.push('/tabs/processing');
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/common/models/medicalReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@ export enum ReportCategory {
* Status of a medical report.
*/
export enum ReportStatus {
UNREAD = 'UNREAD',
READ = 'READ',
UNREAD = 'UNREAD'
}

/**
* Processing status of a medical report.
*/
export enum ProcessingStatus {
PROCESSED = 'processed',
UNPROCESSED = 'unprocessed',
IN_PROGRESS = 'in_progress',
}

/**
Expand All @@ -42,7 +51,7 @@ export interface MedicalReport {
title: string;
category: ReportCategory | string;
bookmarked: boolean;
isProcessed: boolean;
processingStatus: ProcessingStatus;
labValues: LabValue[];
summary: string;
status: ReportStatus;
Expand Down
Loading