Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
46 changes: 10 additions & 36 deletions backend/PERPLEXITY_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This document describes the integration of the Perplexity API in the Medical Rep
## Overview

The integration enables the backend to leverage Perplexity's AI capabilities to:

1. Explain medical text in simpler terms
2. Support custom chat completions for more flexible AI interactions

Expand Down Expand Up @@ -47,25 +48,12 @@ The API key is securely managed using AWS Secrets Manager:
2. The application retrieves the key at runtime using the AWS SDK
3. The key is cached for 15 minutes to minimize API calls to Secrets Manager

### Service Functionality

The `PerplexityService` provides the following methods:

1. `createChatCompletion`: Sends a chat completion request to the Perplexity API
2. `explainMedicalText`: Specializes in explaining medical text in simple terms

### API Endpoints

The `PerplexityController` exposes the following endpoints:

1. `POST /api/perplexity/explain`: Explains medical text in simpler terms
2. `POST /api/perplexity/chat/completions`: Creates a custom chat completion

## Setup Instructions

### AWS Secrets Manager Setup

1. Create a secret in AWS Secrets Manager:

```
aws secretsmanager create-secret \
--name med-ai-perplexity-key \
Expand All @@ -80,9 +68,7 @@ The `PerplexityController` exposes the following endpoints:
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:region:account-id:secret:med-ai-perplexity-key-*"
}
]
Expand All @@ -93,12 +79,12 @@ The `PerplexityController` exposes the following endpoints:

Configure the following environment variables:

| Variable | Description | Default Value |
|----------|-------------|---------------|
| Variable | Description | Default Value |
| -------------------------------- | ----------------------------------------- | ----------------------- |
| `PERPLEXITY_API_KEY_SECRET_NAME` | Name of the secret in AWS Secrets Manager | `med-ai-perplexity-key` |
| `PERPLEXITY_MODEL` | Perplexity model to use | `mixtral-8x7b-instruct` |
| `PERPLEXITY_MAX_TOKENS` | Maximum tokens to generate | `2048` |
| `AWS_REGION` | AWS region for Secrets Manager | `us-east-1` |
| `PERPLEXITY_MODEL` | Perplexity model to use | `mixtral-8x7b-instruct` |
| `PERPLEXITY_MAX_TOKENS` | Maximum tokens to generate | `2048` |
| `AWS_REGION` | AWS region for Secrets Manager | `us-east-1` |

## Local Development

Expand All @@ -114,27 +100,15 @@ Then modify the `getApiKey` method in `PerplexityService` to check for this envi

The frontend can interact with the Perplexity API through the following endpoints:

### Explain Medical Text

```typescript
// Example frontend code
const explainMedicalText = async (text: string) => {
const response = await axios.post('/api/perplexity/explain', {
medicalText: text
});
return response.data.explanation;
};
```

### Custom Chat Completion

```typescript
// Example frontend code
const createChatCompletion = async (messages: any[]) => {
const response = await axios.post('/api/perplexity/chat/completions', {
messages: messages,
temperature: 0.7
temperature: 0.7,
});
return response.data.explanation;
};
```
```
1 change: 0 additions & 1 deletion backend/src/app.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ describe('AppModule', () => {
generateResponse: vi.fn().mockResolvedValue('test response'),
analyzeMedicalDocument: vi.fn().mockResolvedValue({
labValues: [],
diagnoses: [],
metadata: {
isMedicalReport: true,
confidence: 0.9,
Expand Down
23 changes: 1 addition & 22 deletions backend/src/controllers/perplexity/perplexity.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Body, Controller, Post, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { PerplexityService, PerplexityMessage } from '../../services/perplexity.service';
import { ExplainMedicalTextDto, ChatCompletionDto, PerplexityResponseDto } from './perplexity.dto';
import { ChatCompletionDto, PerplexityResponseDto } from './perplexity.dto';

/**
* Controller for Perplexity API endpoints
Expand All @@ -11,27 +11,6 @@ export class PerplexityController {

constructor(private readonly perplexityService: PerplexityService) {}

/**
* Explains medical text in simpler terms
*
* @param dto The DTO containing medical text to explain
* @returns The simplified explanation
*/
@Post('explain')
async explainMedicalText(@Body() dto: ExplainMedicalTextDto): Promise<PerplexityResponseDto> {
try {
const explanation = await this.perplexityService.explainMedicalText(dto.medicalText);
return { explanation };
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Failed to explain medical text: ${errorMessage}`);
throw new HttpException(
'Failed to process medical text explanation',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

/**
* Creates a custom chat completion
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ export class DocumentProcessorController {
report.missingInformation = result.analysis.metadata.missingInformation;
}

// Create summary from simplified explanation or diagnoses
report.summary = result.simplifiedExplanation!;
report.medicalComments = result.analysis.medicalComments!;

report.updatedAt = new Date().toISOString();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ vi.mock('@aws-sdk/client-bedrock-runtime', () => {
suggestions: 'Continue regular health maintenance.',
},
],
diagnoses: [],
metadata: {
isMedicalReport: true,
confidence: 0.95,
Expand Down Expand Up @@ -142,7 +141,7 @@ describe('AwsBedrockService', () => {
suggestions: 'Continue regular health maintenance.',
},
],
diagnoses: [],
medicalComments: 'Patient hemoglobin levels are within normal range.',
metadata: {
isMedicalReport: true,
confidence: 0.95,
Expand Down Expand Up @@ -293,10 +292,9 @@ describe('AwsBedrockService', () => {
const invalidResponses = [
null,
{},
{ labValues: [], diagnoses: [] }, // Missing metadata
{ labValues: [] }, // Missing metadata
{
labValues: [],
diagnoses: [],
metadata: { isMedicalReport: 'not a boolean', confidence: 0.5, missingInformation: [] },
},
];
Expand Down Expand Up @@ -325,7 +323,7 @@ describe('AwsBedrockService', () => {
suggestions: 'No action needed',
},
],
diagnoses: [],
medicalComments: '',
metadata: {
isMedicalReport: true,
confidence: 0.9,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface MedicalDocumentAnalysis {
conclusion: string;
suggestions: string;
}>;
diagnoses: Array<{ condition: string; details: string; recommendations: string }>;
medicalComments: string;
metadata: {
isMedicalReport: boolean;
confidence: number;
Expand All @@ -50,7 +50,7 @@ export class AwsBedrockService {
1. Title/subject from content
2. Category: "heart" (cardiac focus), "brain" (neurological focus), or "general" (all else)
3. Lab values with ranges and status (normal/high/low)
4. Diagnoses, findings, and recommendations
4. Medical comments, if there are any, if not, return empty string
5. Medical document verification with confidence level

Reference trusted sources: Mayo Clinic, Cleveland Clinic, CDC, NIH, WHO, AMA, etc.
Expand All @@ -60,7 +60,7 @@ Return ONLY a JSON object with this structure:
"title": string,
"category": string,
"labValues": [{"name": string, "value": string, "unit": string, "normalRange": string, "status": "normal" | "high" | "low", "isCritical": boolean, "conclusion": string, "suggestions": string}],
"diagnoses": [{"condition": string, "details": string, "recommendations": string}],
"medicalComments": string,
"metadata": {
"isMedicalReport": boolean,
"confidence": number,
Expand Down Expand Up @@ -365,7 +365,6 @@ Document text:
typeof response.title !== 'string' ||
typeof response.category !== 'string' ||
!Array.isArray(response.labValues) ||
!Array.isArray(response.diagnoses) ||
!response.metadata
) {
throw new BadRequestException('Invalid medical analysis response structure');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,23 @@ describe('DocumentProcessorService', () => {
title: 'Test Report',
category: 'general',
labValues: [],
diagnoses: [],
medicalComments: '',
metadata: {
isMedicalReport: true,
confidence: 0.9,
missingInformation: [],
},
};

const simplifiedExplanation = 'This is a simple explanation of the medical document.';

// Create a new test-specific instance with proper mocking
const testTextractService = { extractText: vi.fn() };
const testBedrockService = { analyzeMedicalDocument: vi.fn() };
const testPerplexityService = { explainMedicalText: vi.fn() };
const testPerplexityService = { reviewMedicalAnalysis: vi.fn() };

// Set up mocks
testTextractService.extractText.mockResolvedValue(extractedTextResult);
testBedrockService.analyzeMedicalDocument.mockResolvedValue(medicalAnalysis);
testPerplexityService.explainMedicalText.mockResolvedValue(simplifiedExplanation);
testPerplexityService.reviewMedicalAnalysis.mockResolvedValue(medicalAnalysis);

// Create a fresh service instance with our mocks
const testService = new DocumentProcessorService(
Expand All @@ -73,13 +71,10 @@ describe('DocumentProcessorService', () => {
extractedTextResult.rawText,
userId,
);
expect(testPerplexityService.explainMedicalText).toHaveBeenCalledWith(
extractedTextResult.rawText,
);
expect(testPerplexityService.reviewMedicalAnalysis).toHaveBeenCalled();
expect(result).toEqual({
extractedText: extractedTextResult,
analysis: medicalAnalysis,
simplifiedExplanation,
processingMetadata: expect.objectContaining({
fileSize: fileBuffer.length,
}),
Expand All @@ -94,7 +89,7 @@ describe('DocumentProcessorService', () => {
// Create test-specific service with proper mocking
const testTextractService = { extractText: vi.fn() };
const testBedrockService = { analyzeMedicalDocument: vi.fn() };
const testPerplexityService = { explainMedicalText: vi.fn() };
const testPerplexityService = { reviewMedicalAnalysis: vi.fn() };

// Make the mock reject with an error
testTextractService.extractText.mockRejectedValue(new Error('Failed to extract text'));
Expand Down Expand Up @@ -125,7 +120,7 @@ describe('DocumentProcessorService', () => {
// Create test-specific service with proper mocking
const testTextractService = { extractText: vi.fn() };
const testBedrockService = { analyzeMedicalDocument: vi.fn() };
const testPerplexityService = { explainMedicalText: vi.fn() };
const testPerplexityService = { reviewMedicalAnalysis: vi.fn() };

// Create a fresh service instance with our mocks
const testService = new DocumentProcessorService(
Expand All @@ -148,14 +143,13 @@ describe('DocumentProcessorService', () => {
title: 'Document 1 Report',
category: 'general',
labValues: [],
diagnoses: [],
medicalComments: '',
metadata: {
isMedicalReport: true,
confidence: 0.9,
missingInformation: [],
},
},
simplifiedExplanation: 'Simple explanation for document 1',
processingMetadata: {
processingTimeMs: 100,
fileSize: 4,
Expand All @@ -173,14 +167,13 @@ describe('DocumentProcessorService', () => {
title: 'Document 2 Report',
category: 'general',
labValues: [],
diagnoses: [],
medicalComments: '',
metadata: {
isMedicalReport: true,
confidence: 0.9,
missingInformation: [],
},
},
simplifiedExplanation: 'Simple explanation for document 2',
processingMetadata: {
processingTimeMs: 100,
fileSize: 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { PerplexityService } from '../../services/perplexity.service';
export interface ProcessedDocumentResult {
extractedText: ExtractedTextResult;
analysis: MedicalDocumentAnalysis;
simplifiedExplanation?: string;
processingMetadata: {
processingTimeMs: number;
fileSize: number;
Expand Down Expand Up @@ -62,6 +61,7 @@ export class DocumentProcessorService {

// Step 3: Review and verify analysis using Perplexity
this.logger.log('Reviewing medical analysis with Perplexity');

let analysis: MedicalDocumentAnalysis;

try {
Expand All @@ -79,39 +79,18 @@ export class DocumentProcessorService {
analysis = initialAnalysis;
}

// Step 4: Generate simplified explanation using Perplexity
let simplifiedExplanation: string | undefined;

try {
if (analysis.metadata.isMedicalReport && extractedText.rawText) {
this.logger.log('Generating simplified explanation');
simplifiedExplanation = await this.perplexityService.explainMedicalText(
extractedText.rawText,
);
this.logger.log('Simplified explanation generated successfully');
}
} catch (explanationError) {
this.logger.error('Error generating simplified explanation', {
error: explanationError instanceof Error ? explanationError.message : 'Unknown error',
});
// We don't want to fail the entire process if explanation fails
simplifiedExplanation = undefined;
}

const processingTime = Date.now() - startTime;

this.logger.log(`Document processing completed in ${processingTime}ms`, {
isMedicalReport: analysis.metadata.isMedicalReport,
confidence: analysis.metadata.confidence,
labValueCount: analysis.labValues.length,
hasExplanation: !!simplifiedExplanation,
});

// Return combined result
return {
extractedText,
analysis,
simplifiedExplanation,
processingMetadata: {
processingTimeMs: processingTime,
fileSize: fileBuffer.length,
Expand Down Expand Up @@ -175,14 +154,13 @@ export class DocumentProcessorService {
title: 'Failed Document',
category: 'general',
labValues: [],
diagnoses: [],
medicalComments: '',
metadata: {
isMedicalReport: false,
confidence: 0,
missingInformation: ['Document processing failed'],
},
},
simplifiedExplanation: undefined,
processingMetadata: {
processingTimeMs: 0,
fileSize: doc.buffer.length,
Expand Down
4 changes: 2 additions & 2 deletions backend/src/reports/models/report.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export class Report {
suggestions: string;
}>;

@ApiProperty({ description: 'Summary of the report' })
summary: string;
@ApiProperty({ description: 'Medical comments related to the report' })
medicalComments: string;

@ApiProperty({ description: 'Confidence score of the analysis (0-100)' })
confidence: number;
Expand Down
Loading