Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
137 changes: 30 additions & 107 deletions backend/src/document-processor/services/aws-bedrock.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,17 @@ export class AwsBedrockService {
private readonly inferenceProfileArn?: string;

// Medical document analysis prompt
private readonly medicalAnalysisPrompt = `Please analyze this medical document carefully, with specific attention to medical lab reports.

Look for and extract the following information:
1. Document title or main subject based on content
2. Document category based on organ system focus
3. Lab test values with their normal ranges and whether they are normal, high, or low (particularly important for blood work, metabolic panels, etc.)
4. Any diagnoses, findings, or medical observations with details and recommendations
5. Analyze if this is a medical document (lab report, test result, medical chart, prescription, etc.) and provide confidence level

This document may be a lab report showing blood work or other test results, so please pay special attention to tables, numeric values, reference ranges, and medical terminology.

IMPORTANT: Base your analysis on information from multiple trusted medical sources and authorities, including but not limited to:
- Mayo Clinic
- Cleveland Clinic
- CDC (Centers for Disease Control and Prevention)
- NIH (National Institutes of Health)
- WHO (World Health Organization)
- American Medical Association
- American Heart Association
- American Academy of Pediatrics
- UpToDate
- MedlinePlus

Format the response as a JSON object with the following structure:
private readonly medicalAnalysisPrompt = `Analyze this medical document with focus on lab reports. Extract:

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
5. Medical document verification with confidence level

Reference trusted sources: Mayo Clinic, Cleveland Clinic, CDC, NIH, WHO, AMA, etc.

Return ONLY a JSON object with this structure:
{
"title": string,
"category": string,
Expand All @@ -81,89 +68,25 @@ Format the response as a JSON object with the following structure:
}
}

For the title field, create a concise title that summarizes what the document is about (e.g., "Complete Blood Count Results", "Liver Function Test", "MRI Report").
For the category field, you MUST choose exactly one of these three values:
- "heart" - if the document focuses primarily on cardiac/cardiovascular issues or tests
- "brain" - if the document focuses primarily on neurological issues or brain-related tests
- "general" - for all other medical documents, or when the focus spans multiple systems

Set isMedicalReport to true if you see ANY medical content such as lab values, medical terminology, doctor's notes, or prescription information.
Set confidence between 0 and 1 based on document clarity and how confident you are about the medical nature of the document.


This is extremely important: If you see ANY lab values, numbers with units, or medical terminology, please consider this a medical document even if you're not 100% certain.

When extracting lab values:
1. Look for tables with numeric values and reference ranges
2. Include any values even if you're not sure of the meaning
3. For each lab value, use "status" field with values "normal", "high", or "low" based on whether the value falls within, above, or below the normal range
4. Set "isCritical" to true when the value indicates an urgent medical situation. Set it to false for values that are normal or only slightly abnormal.
5. Include a "conclusion" field that provides a brief interpretation of what this value indicates about the patient's health
6. Include a "suggestions" field that provides brief recommendations based on this value
7. IMPORTANT: If reference ranges are missing from the document, add "reference-ranges-missing" to the missingInformation array in metadata, and use standard reference ranges from trusted medical sources to determine the status.

EXTREMELY IMPORTANT FORMATTING INSTRUCTIONS:
1. ABSOLUTELY DO NOT START YOUR RESPONSE WITH ANY TEXT. Begin immediately with the JSON object.
2. Return ONLY the JSON object without any introduction, explanation, or text like "This appears to be a medical report..."
3. Do NOT include phrases like "Here is the information" or "formatted in the requested JSON structure"
4. Do NOT write any text before the opening brace { or after the closing brace }
5. Do NOT wrap the JSON in code blocks or add comments
6. Do NOT nest JSON inside other JSON fields
7. Start your response with the opening brace { and end with the closing brace }
8. CRITICAL: Do NOT place JSON data inside a definition field or any other field. Return only the direct JSON format requested.
9. Do NOT put explanatory text about how you structured the analysis inside the JSON.
10. Always provide empty arrays ([]) rather than null for empty fields.
11. YOU MUST NOT create a "term" called "Here is the information extracted" or similar phrases.
12. NEVER put actual data inside a "definition" field of a medical term.

YOU REPEATEDLY MAKE THESE MISTAKES:
- You create a "term" field with text like "Here is the information extracted"
- You start your response with "This appears to be a medical report..."
- You write "Here is the information extracted in the requested JSON format:" before the JSON
- THESE ARE WRONG and cause our system to fail

INCORRECT RESPONSE FORMATS (DO NOT DO THESE):

1) DO NOT DO THIS - Adding explanatory text before JSON:
"This appears to be a medical report. Here is the information extracted in the requested JSON format:

{
\"category\": \"heart\",
...
}"

2) DO NOT DO THIS - Nested JSON:
{
"labValues": [
{
"name": "Here is the information extracted",
"value": "{\"labValues\": [{\"name\": \"RBC\", \"value\": \"14.2\"}]}"
}
]
}

CORRECT FORMAT (DO THIS):
{
"title": "Complete Blood Count Results",
"category": "heart",
"labValues": [
{
"name": "Hemoglobin",
"value": "14.2",
"unit": "g/dL",
"normalRange": "13.5-17.5",
"status": "normal",
"isCritical": false,
"conclusion": "Normal hemoglobin levels indicate adequate oxygen-carrying capacity.",
"suggestions": "Continue regular health maintenance."
}
],
"diagnoses": [...],
"metadata": {...}
}

If any information is not visible or unclear in the document, list those items in the missingInformation array.
Ensure all visible medical terms are explained in plain language. Mark lab values as abnormal if they fall outside the normal range.
For lab values:
- Set "isCritical" to true for urgent medical situations
- Provide brief "conclusion" about what the value means for health
- Add brief "suggestions" based on the value
- If reference ranges are missing, add "reference-ranges-missing" to missingInformation and use standard ranges

CRITICAL FORMATTING RULES:
- Begin immediately with { and end with }
- No text before/after the JSON
- No introduction, explanations, code blocks, or comments
- No nested JSON or definition fields
- Empty arrays ([]) for null fields
- No "term" fields with phrases like "Here is the information extracted"

Common errors to avoid:
- Adding explanatory text before JSON
- Starting with "This appears to be a medical report..."
- Creating nested JSON structures
- Placing data inside definition fields

Document text:
`;
Expand Down
42 changes: 36 additions & 6 deletions backend/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,21 +373,51 @@ export class ReportsService {
}

try {
// First check if the report exists and belongs to the user
await this.findOne(report.id, report.userId);

// Set the updatedAt timestamp
report.updatedAt = new Date().toISOString();

// Update report in DynamoDB
const command = new PutItemCommand({
const command = new UpdateItemCommand({
TableName: this.tableName,
Item: marshall(report),
ConditionExpression: 'userId = :userId',
Key: marshall({
userId: report.userId, // Partition key
id: report.id, // Sort key
}),
UpdateExpression:
'SET title = :title, bookmarked = :bookmarked, category = :category, ' +
'processingStatus = :processingStatus, labValues = :labValues, summary = :summary, ' +
'confidence = :confidence, status = :status, updatedAt = :updatedAt',
ConditionExpression: 'userId = :userId', // Ensure the report belongs to the user
ExpressionAttributeValues: marshall({
':title': report.title,
':bookmarked': report.bookmarked,
':category': report.category,
':processingStatus': report.processingStatus,
':labValues': report.labValues,
':summary': report.summary,
':confidence': report.confidence,
':status': report.status,
':updatedAt': report.updatedAt,
':userId': report.userId,
}),
ReturnValues: 'ALL_NEW',
});

await this.dynamoClient.send(command);
this.logger.log(`Successfully updated report with ID ${report.id}`);
const response = await this.dynamoClient.send(command);

return report;
if (!response.Attributes) {
return report; // Return the updated report if no Attributes returned
}

return unmarshall(response.Attributes) as Report;
} catch (error: unknown) {
if (error instanceof NotFoundException) {
throw error;
}

this.logger.error(`Error updating report with ID ${report.id}:`);
this.logger.error(error);

Expand Down
130 changes: 115 additions & 15 deletions backend/src/services/perplexity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ export interface PerplexityMessage {
content: string;
}

export interface PerplexityToolFunction {
name: string;
description?: string;
parameters: Record<string, any>;
}

export interface PerplexityTool {
type: 'function';
function: PerplexityToolFunction;
}

export interface PerplexityResponseFormat {
type: 'text' | 'json_object';
}

export interface PerplexityRequest {
model: string;
messages: PerplexityMessage[];
Expand All @@ -18,6 +33,17 @@ export interface PerplexityRequest {
presence_penalty?: number;
frequency_penalty?: number;
stream?: boolean;
tools?: PerplexityTool[];
response_format?: PerplexityResponseFormat;
}

export interface PerplexityToolCall {
id: string;
type: string;
function: {
name: string;
arguments: string;
};
}

export interface PerplexityResponse {
Expand All @@ -32,6 +58,7 @@ export interface PerplexityResponse {
completion_tokens: number;
total_tokens: number;
};
tool_calls?: PerplexityToolCall[];
}

/**
Expand Down Expand Up @@ -104,6 +131,12 @@ export class PerplexityService {
model?: string;
maxTokens?: number;
temperature?: number;
topP?: number;
topK?: number;
presencePenalty?: number;
frequencyPenalty?: number;
tools?: PerplexityTool[];
responseFormat?: PerplexityResponseFormat;
},
): Promise<PerplexityResponse> {
try {
Expand All @@ -116,6 +149,16 @@ export class PerplexityService {
temperature: options?.temperature || 0.7,
};

// Add optional parameters if provided
if (options?.topP !== undefined) request.top_p = options.topP;
if (options?.topK !== undefined) request.top_k = options.topK;
if (options?.presencePenalty !== undefined)
request.presence_penalty = options.presencePenalty;
if (options?.frequencyPenalty !== undefined)
request.frequency_penalty = options.frequencyPenalty;
if (options?.tools) request.tools = options.tools;
if (options?.responseFormat) request.response_format = options.responseFormat;

const response = await axios.post<PerplexityResponse>(
`${this.baseUrl}/chat/completions`,
request,
Expand All @@ -140,6 +183,68 @@ export class PerplexityService {
}
}

/**
* Queries the Perplexity AI API with streaming responses
* @returns A readable stream of the response
*/
async createStreamingChatCompletion(
messages: PerplexityMessage[],
options?: {
model?: string;
maxTokens?: number;
temperature?: number;
topP?: number;
topK?: number;
presencePenalty?: number;
frequencyPenalty?: number;
tools?: PerplexityTool[];
responseFormat?: PerplexityResponseFormat;
},
): Promise<ReadableStream> {
try {
const apiKey = await this.getApiKey();

const request: PerplexityRequest = {
model: options?.model || this.defaultModel,
messages,
max_tokens: options?.maxTokens || this.defaultMaxTokens,
temperature: options?.temperature || 0.7,
stream: true,
};

// Add optional parameters if provided
if (options?.topP !== undefined) request.top_p = options.topP;
if (options?.topK !== undefined) request.top_k = options.topK;
if (options?.presencePenalty !== undefined)
request.presence_penalty = options.presencePenalty;
if (options?.frequencyPenalty !== undefined)
request.frequency_penalty = options.frequencyPenalty;
if (options?.tools) request.tools = options.tools;
if (options?.responseFormat) request.response_format = options.responseFormat;

const response = await axios.post(`${this.baseUrl}/chat/completions`, request, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
responseType: 'stream',
});

return response.data;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
this.logger.error(
`Perplexity API streaming error: ${error.response?.status} - ${error.message}`,
);
throw new Error(`Perplexity API streaming error: ${error.message}`);
}

const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Failed to create streaming chat completion: ${errorMessage}`);
throw new Error('Failed to create streaming chat completion');
}
}

/**
* Generates a simplified explanation of medical text
*
Expand Down Expand Up @@ -174,25 +279,19 @@ export class PerplexityService {
this.logger.log('Reviewing medical document analysis with Perplexity');

const systemPrompt =
'You are an AI assistant specializing in medical information verification.\n' +
'Your task is to review a medical document analysis and verify it against trusted medical sources.\n' +
'You must ensure all information is accurate, especially lab value reference ranges and interpretations.\n' +
'Use authoritative medical sources like Mayo Clinic, Cleveland Clinic, CDC, NIH, WHO, and medical journals.\n';
'Medical information verification specialist. Verify analysis against trusted sources (Mayo Clinic, Cleveland Clinic, CDC, NIH, WHO, medical journals). Ensure accuracy of lab ranges, interpretations, and recommendations. Return only corrected JSON.';

const analysisJson = JSON.stringify(analysis, null, 2);

const userPrompt =
`Please review the following medical document analysis for accuracy and completeness. ` +
`Check if the lab value reference ranges, interpretations, and recommendations align with trusted medical sources. ` +
`Focus on these key aspects:\n` +
`1. Verify lab value reference ranges\n` +
`2. Confirm interpretations of abnormal values\n` +
`3. Validate medical conclusions and recommendations\n` +
`4. Ensure all lab values are correctly categorized\n\n` +
`If you find any discrepancies, provide corrections in your response by returning the corrected JSON directly.\n\n` +
`Medical Document Analysis:\n${analysisJson}\n\n` +
`Original Medical Document Text:\n${originalText}\n\n` +
`Return the corrected JSON analysis with the same structure, no preamble or explanation needed.`;
`Review this medical analysis for accuracy. Verify:\n` +
`1. Lab value reference ranges\n` +
`2. Interpretations of abnormal values\n` +
`3. Medical conclusions and recommendations\n` +
`4. Lab value categorizations\n\n` +
`Analysis JSON:\n${analysisJson}\n\n` +
`Original Text:\n${originalText}\n\n` +
`Return ONLY corrected JSON with identical structure. No preamble, explanation, or text before/after JSON.`;

const messages: PerplexityMessage[] = [
{ role: 'system', content: systemPrompt },
Expand All @@ -203,6 +302,7 @@ export class PerplexityService {
const response = await this.createChatCompletion(messages, {
temperature: 0.3, // Lower temperature for more accurate/factual responses
maxTokens: 4000, // Ensure there's enough space for the full corrected analysis
responseFormat: { type: 'json_object' }, // Use JSON mode for reliable JSON response
});

// Parse the response to get the corrected analysis
Expand Down
Loading