Skip to content

Commit ccaf5a2

Browse files
committed
Merge branch 'main' of github.com:ModusCreateOrg/app-med-ai-gen into ADE-198
2 parents 429d8b0 + 4cce560 commit ccaf5a2

File tree

18 files changed

+270
-72
lines changed

18 files changed

+270
-72
lines changed

backend/PERPLEXITY_INTEGRATION.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default () => ({
2828
aws: {
2929
// ... existing aws config
3030
secretsManager: {
31-
perplexityApiKeySecret: process.env.PERPLEXITY_API_KEY_SECRET_NAME || 'medical-reports-explainer/perplexity-api-key',
31+
perplexityApiKeySecret: process.env.PERPLEXITY_API_KEY_SECRET_NAME || 'med-ai-perplexity-key',
3232
},
3333
},
3434
perplexity: {
@@ -68,7 +68,7 @@ The `PerplexityController` exposes the following endpoints:
6868
1. Create a secret in AWS Secrets Manager:
6969
```
7070
aws secretsmanager create-secret \
71-
--name medical-reports-explainer/perplexity-api-key \
71+
--name med-ai-perplexity-key \
7272
--description "Perplexity API Key for Medical Reports Explainer" \
7373
--secret-string "your-perplexity-api-key"
7474
```
@@ -83,7 +83,7 @@ The `PerplexityController` exposes the following endpoints:
8383
"Action": [
8484
"secretsmanager:GetSecretValue"
8585
],
86-
"Resource": "arn:aws:secretsmanager:region:account-id:secret:medical-reports-explainer/perplexity-api-key-*"
86+
"Resource": "arn:aws:secretsmanager:region:account-id:secret:med-ai-perplexity-key-*"
8787
}
8888
]
8989
}
@@ -95,7 +95,7 @@ Configure the following environment variables:
9595

9696
| Variable | Description | Default Value |
9797
|----------|-------------|---------------|
98-
| `PERPLEXITY_API_KEY_SECRET_NAME` | Name of the secret in AWS Secrets Manager | `medical-reports-explainer/perplexity-api-key` |
98+
| `PERPLEXITY_API_KEY_SECRET_NAME` | Name of the secret in AWS Secrets Manager | `med-ai-perplexity-key` |
9999
| `PERPLEXITY_MODEL` | Perplexity model to use | `mixtral-8x7b-instruct` |
100100
| `PERPLEXITY_MAX_TOKENS` | Maximum tokens to generate | `2048` |
101101
| `AWS_REGION` | AWS region for Secrets Manager | `us-east-1` |

backend/src/config/configuration.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ export default () => ({
1111
clientId: process.env.AWS_COGNITO_CLIENT_ID,
1212
},
1313
secretsManager: {
14-
perplexityApiKeySecret:
15-
process.env.PERPLEXITY_API_KEY_SECRET_NAME ||
16-
'medical-reports-explainer/perplexity-api-key',
14+
perplexityApiKeySecret: process.env.PERPLEXITY_API_KEY_SECRET_NAME || 'med-ai-perplexity-key',
1715
},
1816
aws: {
1917
accessKeyId: process.env.AWS_ACCESS_KEY_ID,

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

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -182,20 +182,39 @@ export class DocumentProcessorController {
182182
this.logger.log(`Started async processing for report: ${reportId}`);
183183

184184
// Get the file from S3
185-
const fileBuffer = await this.getFileFromS3(filePath);
185+
let fileBuffer;
186+
try {
187+
fileBuffer = await this.getFileFromS3(filePath);
188+
this.logger.log(`Successfully retrieved file from S3 for report: ${reportId}`);
189+
} catch (error) {
190+
const errorMessage = `Failed to retrieve file from S3 for report ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`;
191+
this.logger.error(errorMessage);
192+
await this.failReport(reportId, userId, errorMessage);
193+
return;
194+
}
186195

187196
// Process the document
188-
const result = await this.documentProcessorService.processDocument(fileBuffer, userId);
197+
let result;
198+
try {
199+
result = await this.documentProcessorService.processDocument(fileBuffer, userId);
200+
this.logger.log(`Successfully processed document for report: ${reportId}`);
201+
} catch (error) {
202+
const errorMessage = `Failed to process document for report ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`;
203+
this.logger.error(errorMessage);
204+
await this.failReport(reportId, userId, errorMessage);
205+
return;
206+
}
189207

190208
// Fetch the report again to ensure we have the latest version
191209
const report = await this.reportsService.findOne(reportId, userId);
192210
if (!report) {
193-
throw new Error(`Report ${reportId} not found during async processing`);
211+
this.logger.error(`Report ${reportId} not found during async processing`);
212+
return;
194213
}
195214

196215
// Update the report with analysis results
197-
report.title = result.analysis.title || 'Untitled Report';
198-
report.category = result.analysis.category || 'general';
216+
report.title = result.analysis.title;
217+
report.category = result.analysis.category;
199218
report.processingStatus = ProcessingStatus.PROCESSED;
200219

201220
// Extract lab values
@@ -214,25 +233,38 @@ export class DocumentProcessorController {
214233
this.logger.log(`Completed async processing for report: ${reportId}`);
215234
} catch (error) {
216235
// If processing fails, update the report status to indicate failure
217-
try {
218-
const report = await this.reportsService.findOne(reportId, userId);
219-
if (report) {
220-
report.processingStatus = ProcessingStatus.FAILED;
221-
report.updatedAt = new Date().toISOString();
222-
await this.reportsService.updateReport(report);
223-
}
224-
} catch (updateError: unknown) {
225-
this.logger.error(
226-
`Failed to update report status after processing error: ${
227-
updateError instanceof Error ? updateError.message : 'Unknown error'
228-
}`,
229-
);
230-
}
236+
const errorMessage = `Error during async processing for report ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`;
237+
this.logger.error(errorMessage);
238+
await this.failReport(reportId, userId, errorMessage);
239+
}
240+
}
231241

242+
/**
243+
* Updates a report's processing status to FAILED and logs a debug message
244+
* @param reportId - ID of the report to update
245+
* @param userId - ID of the user who owns the report
246+
* @param debugMessage - Optional debug message describing the failure
247+
*/
248+
private async failReport(
249+
reportId: string,
250+
userId: string,
251+
debugMessage: string | undefined = undefined,
252+
): Promise<void> {
253+
try {
254+
const report = await this.reportsService.findOne(reportId, userId);
255+
if (report) {
256+
report.processingStatus = ProcessingStatus.FAILED;
257+
report.updatedAt = new Date().toISOString();
258+
report.debugMessage = debugMessage;
259+
await this.reportsService.updateReport(report);
260+
this.logger.log(`Updated status of report ${reportId} to FAILED`);
261+
}
262+
} catch (updateError: unknown) {
232263
this.logger.error(
233-
`Error during async processing for report ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
264+
`Failed to update report status after processing error: ${
265+
updateError instanceof Error ? updateError.message : 'Unknown error'
266+
}`,
234267
);
235-
throw error;
236268
}
237269
}
238270

backend/src/iac/backend-stack.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export class BackendStack extends cdk.Stack {
255255
DYNAMODB_REPORTS_TABLE: reportsTable.tableName,
256256

257257
// Perplexity related
258-
PERPLEXITY_API_KEY_SECRET_NAME: `medical-reports-explainer/${props.environment}/perplexity-api-key`,
258+
PERPLEXITY_API_KEY_SECRET_NAME: `med-ai-perplexity-key`,
259259
PERPLEXITY_MODEL: 'sonar',
260260
PERPLEXITY_MAX_TOKENS: '2048',
261261

backend/src/reports/models/report.model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,7 @@ export class Report {
7474

7575
@ApiProperty({ description: 'Last update timestamp' })
7676
updatedAt: string;
77+
78+
@ApiProperty({ description: 'Optional debug message for the report' })
79+
debugMessage?: string;
7780
}

backend/src/reports/reports.controller.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Req,
1010
UnauthorizedException,
1111
Post,
12+
NotFoundException,
1213
} from '@nestjs/common';
1314
import {
1415
ApiTags,
@@ -20,7 +21,7 @@ import {
2021
ApiBody,
2122
} from '@nestjs/swagger';
2223
import { ReportsService } from './reports.service';
23-
import { Report } from './models/report.model';
24+
import { ProcessingStatus, Report } from './models/report.model';
2425
import { GetReportsQueryDto } from './dto/get-reports.dto';
2526
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
2627
import { RequestWithUser } from '../auth/auth.middleware';
@@ -80,7 +81,17 @@ export class ReportsController {
8081
@Get(':id')
8182
async getReport(@Param('id') id: string, @Req() request: RequestWithUser): Promise<Report> {
8283
const userId = this.extractUserId(request);
83-
return this.reportsService.findOne(id, userId);
84+
const report = await this.reportsService.findOne(id, userId);
85+
86+
if (!report) {
87+
throw new NotFoundException('Report not found');
88+
}
89+
90+
if (report.processingStatus === ProcessingStatus.FAILED) {
91+
throw new NotFoundException('Processing failed');
92+
}
93+
94+
return report;
8495
}
8596

8697
@ApiOperation({ summary: 'Update report status' })

backend/src/reports/reports.service.ts

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DynamoDBServiceException,
1414
PutItemCommand,
1515
QueryCommand,
16+
DeleteItemCommand,
1617
} from '@aws-sdk/client-dynamodb';
1718
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
1819
import { Report, ReportStatus, ProcessingStatus } from './models/report.model';
@@ -54,19 +55,24 @@ export class ReportsService {
5455
this.tableName = this.configService.get<string>('dynamodbReportsTable')!;
5556
}
5657

57-
async findAll(userId: string): Promise<Report[]> {
58+
async findAll(userId: string, withFailed = false): Promise<Report[]> {
5859
if (!userId) {
5960
throw new ForbiddenException('User ID is required');
6061
}
6162

6263
try {
63-
// Use QueryCommand instead of ScanCommand since userId is the partition key
64+
const expressionAttributeValues: any = { ':userId': userId };
65+
const processingStatusFilter = 'processingStatus <> :failedStatus';
66+
67+
if (!withFailed) {
68+
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
69+
}
70+
6471
const command = new QueryCommand({
6572
TableName: this.tableName,
6673
KeyConditionExpression: 'userId = :userId',
67-
ExpressionAttributeValues: marshall({
68-
':userId': userId,
69-
}),
74+
FilterExpression: !withFailed ? processingStatusFilter : undefined,
75+
ExpressionAttributeValues: marshall(expressionAttributeValues),
7076
});
7177

7278
const response = await this.dynamoClient.send(command);
@@ -91,7 +97,11 @@ export class ReportsService {
9197
}
9298
}
9399

94-
async findLatest(queryDto: GetReportsQueryDto, userId: string): Promise<Report[]> {
100+
async findLatest(
101+
queryDto: GetReportsQueryDto,
102+
userId: string,
103+
withFailed = false,
104+
): Promise<Report[]> {
95105
this.logger.log(
96106
`Running findLatest with params: ${JSON.stringify(queryDto)} for user ${userId}`,
97107
);
@@ -100,22 +110,26 @@ export class ReportsService {
100110
throw new ForbiddenException('User ID is required');
101111
}
102112

103-
// Convert limit to a number to avoid serialization errors
104113
const limit =
105114
typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10;
106115

116+
const expressionAttributeValues: any = { ':userId': userId };
117+
107118
try {
108-
// Use the GSI userIdCreatedAtIndex with QueryCommand for efficient retrieval
109-
// This is much more efficient than a ScanCommand
119+
const processingStatusFilter = 'processingStatus <> :failedStatus';
120+
121+
if (!withFailed) {
122+
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
123+
}
124+
110125
const command = new QueryCommand({
111126
TableName: this.tableName,
112-
IndexName: 'userIdCreatedAtIndex', // Use the GSI for efficient queries
127+
IndexName: 'userIdCreatedAtIndex',
113128
KeyConditionExpression: 'userId = :userId',
114-
ExpressionAttributeValues: marshall({
115-
':userId': userId,
116-
}),
117-
ScanIndexForward: false, // Get items in descending order (newest first)
118-
Limit: limit, // Only fetch the number of items we need
129+
FilterExpression: !withFailed ? processingStatusFilter : undefined,
130+
ExpressionAttributeValues: marshall(expressionAttributeValues),
131+
ScanIndexForward: false,
132+
Limit: limit,
119133
});
120134

121135
const response = await this.dynamoClient.send(command);
@@ -130,22 +144,17 @@ export class ReportsService {
130144
`Table "${this.tableName}" not found. Please check your database configuration.`,
131145
);
132146
} else if (error.name === 'ValidationException') {
133-
// This could happen if the GSI doesn't exist
134147
this.logger.warn('GSI validation error, falling back to standard query');
135148

136-
// Fallback to standard query and sort in memory if GSI has issues
137149
const fallbackCommand = new QueryCommand({
138150
TableName: this.tableName,
139151
KeyConditionExpression: 'userId = :userId',
140-
ExpressionAttributeValues: marshall({
141-
':userId': userId,
142-
}),
152+
ExpressionAttributeValues: marshall(expressionAttributeValues),
143153
});
144154

145155
const fallbackResponse = await this.dynamoClient.send(fallbackCommand);
146156
const reports = (fallbackResponse.Items || []).map(item => unmarshall(item) as Report);
147157

148-
// Sort by createdAt in descending order
149158
return reports
150159
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
151160
.slice(0, limit);
@@ -469,4 +478,54 @@ export class ReportsService {
469478
throw new InternalServerErrorException(`Failed to toggle bookmark for report ID ${id}`);
470479
}
471480
}
481+
482+
/**
483+
* Delete a report by ID
484+
* @param reportId Report ID
485+
* @param userId User ID
486+
* @returns A confirmation message
487+
*/
488+
async deleteReport(reportId: string, userId: string): Promise<string> {
489+
if (!reportId) {
490+
throw new NotFoundException('Report ID is required');
491+
}
492+
493+
if (!userId) {
494+
throw new ForbiddenException('User ID is required');
495+
}
496+
497+
try {
498+
const command = new DeleteItemCommand({
499+
TableName: this.tableName,
500+
Key: marshall({
501+
userId,
502+
id: reportId,
503+
}),
504+
ConditionExpression: 'userId = :userId',
505+
ExpressionAttributeValues: marshall({
506+
':userId': userId,
507+
}),
508+
});
509+
510+
await this.dynamoClient.send(command);
511+
this.logger.log(`Successfully deleted report with ID ${reportId} for user ${userId}`);
512+
513+
return `Report with ID ${reportId} successfully deleted`;
514+
} catch (error: unknown) {
515+
this.logger.error(`Error deleting report with ID ${reportId}:`);
516+
this.logger.error(error);
517+
518+
if (error instanceof DynamoDBServiceException) {
519+
if (error.name === 'ConditionalCheckFailedException') {
520+
throw new ForbiddenException('You do not have permission to delete this report');
521+
} else if (error.name === 'ResourceNotFoundException') {
522+
throw new InternalServerErrorException(
523+
`Table "${this.tableName}" not found. Please check your database configuration.`,
524+
);
525+
}
526+
}
527+
528+
throw new InternalServerErrorException(`Failed to delete report with ID ${reportId}`);
529+
}
530+
}
472531
}

backend/src/services/perplexity.service.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ export class PerplexityService {
7979
return this.apiKey;
8080
}
8181

82-
const secretName =
83-
this.configService.get<string>('aws.secretsManager.perplexityApiKeySecret') ||
84-
'medical-reports-explainer/perplexity-api-key';
82+
const secretName = this.configService.get<string>('aws.secretsManager.perplexityApiKeySecret');
8583

8684
if (!secretName) {
8785
throw new Error('Perplexity API key secret name is not configured');
@@ -158,7 +156,7 @@ export class PerplexityService {
158156
'Your goal is to help patients understand their medical reports by translating medical jargon into plain language.\n' +
159157
'You must be accurate, concise, comprehensive, and easy to understand. Use everyday analogies when helpful.\n';
160158

161-
const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 100 to 500 characters:\n\n${medicalText}`;
159+
const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 10 to 200 words, all in normal text NOT .md style, the more concise the better:\n\n${medicalText}`;
162160

163161
const messages: PerplexityMessage[] = [
164162
{ role: 'system', content: systemPrompt },
Lines changed: 12 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)