Refactor report generation to focus solely on medical data. Introduce MedicalReport interface, streamlining report content by removing unnecessary technical details. Consolidate endpoints to return only JSON format for reports, enhancing clarity and usability. Update report generation logic to align with new structure, ensuring compatibility with existing ComprehensiveReport interface for backward compatibility.
Browse files- src/routes/report.route.ts +6 -92
- src/services/report-generation.service.ts +260 -127
src/routes/report.route.ts
CHANGED
|
@@ -47,12 +47,8 @@ export async function reportRoutes(
|
|
| 47 |
const existingReport = await reportService.getReport(session_id);
|
| 48 |
if (existingReport && existingReport.report_type === reportType) {
|
| 49 |
logger.info(`Returning existing ${reportType} report for session ${session_id}`);
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
report_type: existingReport.report_type,
|
| 53 |
-
generated_at: new Date().toISOString(),
|
| 54 |
-
report: existingReport
|
| 55 |
-
});
|
| 56 |
}
|
| 57 |
|
| 58 |
// Generate new report
|
|
@@ -63,12 +59,8 @@ export async function reportRoutes(
|
|
| 63 |
reportType
|
| 64 |
);
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
report_type: report.report_type,
|
| 69 |
-
generated_at: new Date().toISOString(),
|
| 70 |
-
report
|
| 71 |
-
});
|
| 72 |
} catch (error) {
|
| 73 |
logger.error({ error }, 'Error generating report');
|
| 74 |
return reply.status(500).send({
|
|
@@ -78,86 +70,8 @@ export async function reportRoutes(
|
|
| 78 |
}
|
| 79 |
});
|
| 80 |
|
| 81 |
-
//
|
| 82 |
-
|
| 83 |
-
request: FastifyRequest<{
|
| 84 |
-
Params: { session_id: string };
|
| 85 |
-
}>,
|
| 86 |
-
reply: FastifyReply
|
| 87 |
-
) => {
|
| 88 |
-
try {
|
| 89 |
-
const { session_id } = request.params;
|
| 90 |
-
|
| 91 |
-
const report = await reportService.getReport(session_id);
|
| 92 |
-
if (!report) {
|
| 93 |
-
// Generate if doesn't exist
|
| 94 |
-
const { data: sessionData } = await supabaseService.getClient()
|
| 95 |
-
.from('conversation_sessions')
|
| 96 |
-
.select('user_id')
|
| 97 |
-
.eq('id', session_id)
|
| 98 |
-
.single();
|
| 99 |
-
|
| 100 |
-
if (!sessionData) {
|
| 101 |
-
return reply.status(404).send({
|
| 102 |
-
error: 'Session not found'
|
| 103 |
-
});
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
const newReport = await reportService.generateReport(session_id, sessionData.user_id);
|
| 107 |
-
return reply.status(200).send({
|
| 108 |
-
markdown: newReport.report_markdown
|
| 109 |
-
});
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
return reply.status(200).send({
|
| 113 |
-
markdown: report.report_markdown
|
| 114 |
-
});
|
| 115 |
-
} catch (error) {
|
| 116 |
-
logger.error({ error }, 'Error getting report markdown');
|
| 117 |
-
return reply.status(500).send({
|
| 118 |
-
error: 'Internal server error',
|
| 119 |
-
message: 'Failed to get report markdown'
|
| 120 |
-
});
|
| 121 |
-
}
|
| 122 |
-
});
|
| 123 |
-
|
| 124 |
-
// Get report JSON only
|
| 125 |
-
fastify.get('/api/reports/:session_id/json', async (
|
| 126 |
-
request: FastifyRequest<{
|
| 127 |
-
Params: { session_id: string };
|
| 128 |
-
}>,
|
| 129 |
-
reply: FastifyReply
|
| 130 |
-
) => {
|
| 131 |
-
try {
|
| 132 |
-
const { session_id } = request.params;
|
| 133 |
-
|
| 134 |
-
const report = await reportService.getReport(session_id);
|
| 135 |
-
if (!report) {
|
| 136 |
-
const { data: sessionData } = await supabaseService.getClient()
|
| 137 |
-
.from('conversation_sessions')
|
| 138 |
-
.select('user_id')
|
| 139 |
-
.eq('id', session_id)
|
| 140 |
-
.single();
|
| 141 |
-
|
| 142 |
-
if (!sessionData) {
|
| 143 |
-
return reply.status(404).send({
|
| 144 |
-
error: 'Session not found'
|
| 145 |
-
});
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
const newReport = await reportService.generateReport(session_id, sessionData.user_id);
|
| 149 |
-
return reply.status(200).send(newReport.report_content);
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
return reply.status(200).send(report.report_content);
|
| 153 |
-
} catch (error) {
|
| 154 |
-
logger.error({ error }, 'Error getting report JSON');
|
| 155 |
-
return reply.status(500).send({
|
| 156 |
-
error: 'Internal server error',
|
| 157 |
-
message: 'Failed to get report JSON'
|
| 158 |
-
});
|
| 159 |
-
}
|
| 160 |
-
});
|
| 161 |
|
| 162 |
logger.info('Report routes registered');
|
| 163 |
}
|
|
|
|
| 47 |
const existingReport = await reportService.getReport(session_id);
|
| 48 |
if (existingReport && existingReport.report_type === reportType) {
|
| 49 |
logger.info(`Returning existing ${reportType} report for session ${session_id}`);
|
| 50 |
+
// Chỉ trả về JSON (report_content là MedicalReport)
|
| 51 |
+
return reply.status(200).send(existingReport.report_content);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
// Generate new report
|
|
|
|
| 59 |
reportType
|
| 60 |
);
|
| 61 |
|
| 62 |
+
// Chỉ trả về JSON (report_content là MedicalReport)
|
| 63 |
+
return reply.status(200).send(report.report_content);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
} catch (error) {
|
| 65 |
logger.error({ error }, 'Error generating report');
|
| 66 |
return reply.status(500).send({
|
|
|
|
| 70 |
}
|
| 71 |
});
|
| 72 |
|
| 73 |
+
// Chỉ có 1 endpoint duy nhất trả về JSON (MedicalReport)
|
| 74 |
+
// Đã được xử lý ở endpoint chính ở trên
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
logger.info('Report routes registered');
|
| 77 |
}
|
src/services/report-generation.service.ts
CHANGED
|
@@ -5,55 +5,86 @@ import { ToolExecutionTrackerService } from './tool-execution-tracker.service.js
|
|
| 5 |
import { GeminiLLM } from '../agent/gemini-llm.js';
|
| 6 |
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
session_id: string;
|
| 10 |
user_id: string;
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
| 26 |
}>;
|
| 27 |
-
|
| 28 |
-
tool_name: string;
|
| 29 |
-
tool_display_name: string;
|
| 30 |
-
execution_order: number;
|
| 31 |
-
input_data: any;
|
| 32 |
-
output_data: any;
|
| 33 |
-
execution_time_ms: number;
|
| 34 |
-
status: string;
|
| 35 |
-
}>;
|
| 36 |
-
summary: {
|
| 37 |
-
main_concerns: string[];
|
| 38 |
-
top_conditions_suggested: Array<{
|
| 39 |
-
name: string;
|
| 40 |
-
source: string;
|
| 41 |
-
confidence: string;
|
| 42 |
-
occurrences: number;
|
| 43 |
-
}>;
|
| 44 |
-
triage_levels_identified: Array<{
|
| 45 |
-
level: string;
|
| 46 |
-
count: number;
|
| 47 |
-
}>;
|
| 48 |
-
hospitals_suggested: Array<{
|
| 49 |
-
name: string;
|
| 50 |
-
distance_km: number;
|
| 51 |
-
address: string;
|
| 52 |
-
}>;
|
| 53 |
-
key_guidelines_retrieved: number;
|
| 54 |
-
};
|
| 55 |
};
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
export class ReportGenerationService {
|
|
@@ -101,7 +132,7 @@ export class ReportGenerationService {
|
|
| 101 |
// Get all tool executions for this session
|
| 102 |
const toolExecutions = await this.toolTracker.getToolExecutionsForSession(sessionId);
|
| 103 |
|
| 104 |
-
// Build report content
|
| 105 |
const reportContent = await this.buildReportContent(
|
| 106 |
sessionData,
|
| 107 |
conversationHistory,
|
|
@@ -109,15 +140,12 @@ export class ReportGenerationService {
|
|
| 109 |
reportType
|
| 110 |
);
|
| 111 |
|
| 112 |
-
// Generate markdown report using LLM
|
| 113 |
-
const reportMarkdown = await this.generateMarkdownReport(reportContent, reportType);
|
| 114 |
-
|
| 115 |
const report: ComprehensiveReport = {
|
| 116 |
session_id: sessionId,
|
| 117 |
user_id: userId,
|
| 118 |
report_type: reportType,
|
| 119 |
-
report_content: reportContent
|
| 120 |
-
report_markdown
|
| 121 |
};
|
| 122 |
|
| 123 |
// Save report to database
|
|
@@ -132,114 +160,219 @@ export class ReportGenerationService {
|
|
| 132 |
}
|
| 133 |
|
| 134 |
/**
|
| 135 |
-
* Build structured report content
|
| 136 |
*/
|
| 137 |
private async buildReportContent(
|
| 138 |
sessionData: any,
|
| 139 |
conversationHistory: any[],
|
| 140 |
toolExecutions: any[],
|
| 141 |
_reportType: string
|
| 142 |
-
): Promise<
|
| 143 |
-
// Extract
|
| 144 |
-
const
|
| 145 |
-
const
|
| 146 |
-
const
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
conversationHistory.forEach(msg => {
|
|
|
|
| 152 |
if (msg.role === 'user') {
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
-
|
|
|
|
| 156 |
if (msg.triage_result) {
|
| 157 |
-
const
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
// Extract suspected conditions
|
| 161 |
-
if (
|
| 162 |
-
|
| 163 |
const key = cond.name;
|
|
|
|
|
|
|
|
|
|
| 164 |
if (conditionsMap.has(key)) {
|
| 165 |
conditionsMap.get(key)!.count++;
|
| 166 |
} else {
|
| 167 |
conditionsMap.set(key, {
|
| 168 |
-
source
|
| 169 |
-
confidence
|
| 170 |
count: 1
|
| 171 |
});
|
| 172 |
}
|
| 173 |
});
|
| 174 |
}
|
| 175 |
-
|
| 176 |
// Extract hospital info
|
| 177 |
-
if (
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
}
|
| 185 |
});
|
| 186 |
-
|
| 187 |
-
//
|
|
|
|
|
|
|
|
|
|
| 188 |
toolExecutions.forEach(exec => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
if (exec.tool_name === 'rag_query' || exec.tool_name === 'guideline_retrieval') {
|
| 190 |
-
if (exec.output_data?.guidelines) {
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
}
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
source: data.source,
|
| 226 |
-
confidence: data.confidence,
|
| 227 |
-
occurrences: data.count
|
| 228 |
-
}))
|
| 229 |
-
.sort((a, b) => b.occurrences - a.occurrences),
|
| 230 |
-
triage_levels_identified: Array.from(triageLevelsMap.entries())
|
| 231 |
-
.map(([level, count]) => ({ level, count })),
|
| 232 |
-
hospitals_suggested: hospitals,
|
| 233 |
-
key_guidelines_retrieved: guidelinesCount
|
| 234 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
}
|
| 237 |
|
| 238 |
/**
|
| 239 |
-
* Generate markdown report using LLM
|
| 240 |
*/
|
| 241 |
private async generateMarkdownReport(
|
| 242 |
-
reportContent:
|
| 243 |
reportType: string
|
| 244 |
): Promise<string> {
|
| 245 |
const prompt = `Bạn là trợ lý y tế chuyên nghiệp. Hãy tạo một báo cáo tổng hợp đầy đủ về cuộc trò chuyện y tế dựa trên dữ liệu sau.
|
|
@@ -317,10 +450,10 @@ CẤU TRÚC BÁO CÁO:
|
|
| 317 |
}
|
| 318 |
|
| 319 |
/**
|
| 320 |
-
* Generate fallback markdown if LLM fails
|
| 321 |
*/
|
| 322 |
private generateFallbackMarkdown(
|
| 323 |
-
reportContent:
|
| 324 |
): string {
|
| 325 |
return `# BÁO CÁO TỔNG HỢP - PHIÊN TƯ VẤN Y TẾ
|
| 326 |
|
|
@@ -419,7 +552,7 @@ Dựa trên phân tích toàn bộ cuộc hội thoại và kết quả từ cá
|
|
| 419 |
user_id: report.user_id,
|
| 420 |
report_type: report.report_type,
|
| 421 |
report_content: report.report_content,
|
| 422 |
-
report_markdown: report.report_markdown,
|
| 423 |
generated_at: new Date().toISOString(),
|
| 424 |
created_at: new Date().toISOString()
|
| 425 |
});
|
|
@@ -456,7 +589,7 @@ Dựa trên phân tích toàn bộ cuộc hội thoại và kết quả từ cá
|
|
| 456 |
user_id: data.user_id,
|
| 457 |
report_type: data.report_type,
|
| 458 |
report_content: data.report_content,
|
| 459 |
-
report_markdown: data.report_markdown
|
| 460 |
};
|
| 461 |
} catch (error) {
|
| 462 |
logger.error({ error }, 'Error getting report');
|
|
|
|
| 5 |
import { GeminiLLM } from '../agent/gemini-llm.js';
|
| 6 |
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
|
| 8 |
+
/**
|
| 9 |
+
* Medical Report - Chỉ chứa dữ liệu có ý nghĩa về mặt y tế
|
| 10 |
+
* Loại bỏ tất cả thông tin kỹ thuật (tool_name, execution_order, execution_time_ms, status, etc.)
|
| 11 |
+
*/
|
| 12 |
+
export interface MedicalReport {
|
| 13 |
session_id: string;
|
| 14 |
user_id: string;
|
| 15 |
+
created_at: string;
|
| 16 |
+
updated_at: string;
|
| 17 |
+
|
| 18 |
+
// Thông tin triệu chứng và mối quan tâm
|
| 19 |
+
concerns: Array<{
|
| 20 |
+
description: string;
|
| 21 |
+
timestamp: string;
|
| 22 |
+
has_image: boolean;
|
| 23 |
+
}>;
|
| 24 |
+
|
| 25 |
+
// Kết quả phân tích hình ảnh (nếu có)
|
| 26 |
+
image_analysis?: {
|
| 27 |
+
top_conditions: Array<{
|
| 28 |
+
condition_name: string;
|
| 29 |
+
confidence_percent: string;
|
| 30 |
+
probability: number;
|
| 31 |
}>;
|
| 32 |
+
model_type: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
};
|
| 34 |
+
|
| 35 |
+
// Phân loại mức độ khẩn cấp
|
| 36 |
+
triage_assessment: Array<{
|
| 37 |
+
level: 'emergency' | 'urgent' | 'routine' | 'self-care';
|
| 38 |
+
timestamp: string;
|
| 39 |
+
red_flags: string[];
|
| 40 |
+
reasoning: string;
|
| 41 |
+
}>;
|
| 42 |
+
|
| 43 |
+
// Bệnh nghi ngờ
|
| 44 |
+
suspected_conditions: Array<{
|
| 45 |
+
condition_name: string;
|
| 46 |
+
source: 'cv_model' | 'guideline' | 'user_report' | 'reasoning';
|
| 47 |
+
confidence: 'high' | 'medium' | 'low';
|
| 48 |
+
occurrences: number;
|
| 49 |
+
}>;
|
| 50 |
+
|
| 51 |
+
// Hướng dẫn y tế đã truy xuất
|
| 52 |
+
medical_guidelines: Array<{
|
| 53 |
+
content: string;
|
| 54 |
+
relevance_score?: number;
|
| 55 |
+
source?: string;
|
| 56 |
+
}>;
|
| 57 |
+
|
| 58 |
+
// Khuyến nghị điều trị
|
| 59 |
+
recommendations: Array<{
|
| 60 |
+
action: string;
|
| 61 |
+
timeframe: string;
|
| 62 |
+
home_care_advice?: string;
|
| 63 |
+
warning_signs?: string;
|
| 64 |
+
timestamp: string;
|
| 65 |
+
}>;
|
| 66 |
+
|
| 67 |
+
// Bệnh viện được đề xuất
|
| 68 |
+
suggested_hospitals: Array<{
|
| 69 |
+
name: string;
|
| 70 |
+
distance_km: number;
|
| 71 |
+
address: string;
|
| 72 |
+
rating?: number;
|
| 73 |
+
specialty_match?: 'high' | 'medium' | 'low';
|
| 74 |
+
condition?: string;
|
| 75 |
+
}>;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* ComprehensiveReport - Giữ lại để backward compatibility
|
| 80 |
+
* Nhưng report_content sẽ là MedicalReport (chỉ JSON, không có markdown)
|
| 81 |
+
*/
|
| 82 |
+
export interface ComprehensiveReport {
|
| 83 |
+
session_id: string;
|
| 84 |
+
user_id: string;
|
| 85 |
+
report_type: 'full' | 'summary' | 'tools_only';
|
| 86 |
+
report_content: MedicalReport;
|
| 87 |
+
report_markdown?: string; // Optional, chỉ dùng cho display
|
| 88 |
}
|
| 89 |
|
| 90 |
export class ReportGenerationService {
|
|
|
|
| 132 |
// Get all tool executions for this session
|
| 133 |
const toolExecutions = await this.toolTracker.getToolExecutionsForSession(sessionId);
|
| 134 |
|
| 135 |
+
// Build report content (chỉ JSON, tập trung vào dữ liệu y tế)
|
| 136 |
const reportContent = await this.buildReportContent(
|
| 137 |
sessionData,
|
| 138 |
conversationHistory,
|
|
|
|
| 140 |
reportType
|
| 141 |
);
|
| 142 |
|
|
|
|
|
|
|
|
|
|
| 143 |
const report: ComprehensiveReport = {
|
| 144 |
session_id: sessionId,
|
| 145 |
user_id: userId,
|
| 146 |
report_type: reportType,
|
| 147 |
+
report_content: reportContent
|
| 148 |
+
// report_markdown không cần thiết nữa - chỉ dùng JSON
|
| 149 |
};
|
| 150 |
|
| 151 |
// Save report to database
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
/**
|
| 163 |
+
* Build structured report content - Chỉ chứa dữ liệu y tế có ý nghĩa
|
| 164 |
*/
|
| 165 |
private async buildReportContent(
|
| 166 |
sessionData: any,
|
| 167 |
conversationHistory: any[],
|
| 168 |
toolExecutions: any[],
|
| 169 |
_reportType: string
|
| 170 |
+
): Promise<MedicalReport> {
|
| 171 |
+
// Extract medical data từ conversation history
|
| 172 |
+
const concerns: MedicalReport['concerns'] = [];
|
| 173 |
+
const triageAssessments: MedicalReport['triage_assessment'] = [];
|
| 174 |
+
const recommendations: MedicalReport['recommendations'] = [];
|
| 175 |
+
|
| 176 |
+
// Extract conditions và hospitals từ conversation
|
| 177 |
+
const conditionsMap = new Map<string, {
|
| 178 |
+
source: 'cv_model' | 'guideline' | 'user_report' | 'reasoning';
|
| 179 |
+
confidence: 'high' | 'medium' | 'low';
|
| 180 |
+
count: number;
|
| 181 |
+
}>();
|
| 182 |
+
const hospitalsMap = new Map<string, MedicalReport['suggested_hospitals'][0]>();
|
| 183 |
+
|
| 184 |
+
// Process conversation history để extract dữ liệu y tế
|
| 185 |
conversationHistory.forEach(msg => {
|
| 186 |
+
// Extract concerns từ user messages
|
| 187 |
if (msg.role === 'user') {
|
| 188 |
+
concerns.push({
|
| 189 |
+
description: msg.content,
|
| 190 |
+
timestamp: msg.created_at,
|
| 191 |
+
has_image: !!msg.image_url
|
| 192 |
+
});
|
| 193 |
}
|
| 194 |
+
|
| 195 |
+
// Extract triage assessment và recommendations từ triage_result
|
| 196 |
if (msg.triage_result) {
|
| 197 |
+
const triageResult = msg.triage_result;
|
| 198 |
+
|
| 199 |
+
// Triage assessment
|
| 200 |
+
triageAssessments.push({
|
| 201 |
+
level: triageResult.triage_level,
|
| 202 |
+
timestamp: msg.created_at,
|
| 203 |
+
red_flags: triageResult.red_flags || [],
|
| 204 |
+
reasoning: triageResult.symptom_summary || 'Đánh giá dựa trên triệu chứng và phân tích hình ảnh'
|
| 205 |
+
});
|
| 206 |
+
|
| 207 |
+
// Recommendations
|
| 208 |
+
if (triageResult.recommendation) {
|
| 209 |
+
recommendations.push({
|
| 210 |
+
action: triageResult.recommendation.action || '',
|
| 211 |
+
timeframe: triageResult.recommendation.timeframe || '',
|
| 212 |
+
home_care_advice: triageResult.recommendation.home_care_advice,
|
| 213 |
+
warning_signs: triageResult.recommendation.warning_signs,
|
| 214 |
+
timestamp: msg.created_at
|
| 215 |
+
});
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
// Extract suspected conditions
|
| 219 |
+
if (triageResult.suspected_conditions) {
|
| 220 |
+
triageResult.suspected_conditions.forEach((cond: any) => {
|
| 221 |
const key = cond.name;
|
| 222 |
+
const confidence = this.mapConfidenceToLevel(cond.confidence);
|
| 223 |
+
const source = cond.source as 'cv_model' | 'guideline' | 'user_report' | 'reasoning';
|
| 224 |
+
|
| 225 |
if (conditionsMap.has(key)) {
|
| 226 |
conditionsMap.get(key)!.count++;
|
| 227 |
} else {
|
| 228 |
conditionsMap.set(key, {
|
| 229 |
+
source,
|
| 230 |
+
confidence,
|
| 231 |
count: 1
|
| 232 |
});
|
| 233 |
}
|
| 234 |
});
|
| 235 |
}
|
| 236 |
+
|
| 237 |
// Extract hospital info
|
| 238 |
+
if (triageResult.nearest_clinic) {
|
| 239 |
+
const clinic = triageResult.nearest_clinic;
|
| 240 |
+
const hospitalKey = clinic.name;
|
| 241 |
+
if (!hospitalsMap.has(hospitalKey)) {
|
| 242 |
+
hospitalsMap.set(hospitalKey, {
|
| 243 |
+
name: clinic.name,
|
| 244 |
+
distance_km: clinic.distance_km,
|
| 245 |
+
address: clinic.address,
|
| 246 |
+
rating: clinic.rating,
|
| 247 |
+
specialty_match: clinic.specialty_score
|
| 248 |
+
? (clinic.specialty_score > 0.5 ? 'high' : clinic.specialty_score > 0 ? 'medium' : 'low')
|
| 249 |
+
: undefined,
|
| 250 |
+
condition: triageResult.suspected_conditions?.[0]?.name
|
| 251 |
+
});
|
| 252 |
+
}
|
| 253 |
}
|
| 254 |
}
|
| 255 |
});
|
| 256 |
+
|
| 257 |
+
// Extract medical data từ tool executions
|
| 258 |
+
let imageAnalysis: MedicalReport['image_analysis'] | undefined;
|
| 259 |
+
const medicalGuidelines: MedicalReport['medical_guidelines'] = [];
|
| 260 |
+
|
| 261 |
toolExecutions.forEach(exec => {
|
| 262 |
+
// Extract CV/Image Analysis results
|
| 263 |
+
if (exec.tool_name === 'derm_cv' || exec.tool_name === 'eye_cv' || exec.tool_name === 'wound_cv') {
|
| 264 |
+
if (exec.output_data?.top_conditions && exec.output_data.top_conditions.length > 0) {
|
| 265 |
+
imageAnalysis = {
|
| 266 |
+
top_conditions: exec.output_data.top_conditions.map((cond: any) => ({
|
| 267 |
+
condition_name: cond.condition || cond.name || 'Unknown',
|
| 268 |
+
confidence_percent: cond.confidence || `${(cond.probability * 100).toFixed(1)}%`,
|
| 269 |
+
probability: cond.probability || parseFloat(cond.confidence?.replace('%', '')) / 100 || 0
|
| 270 |
+
})),
|
| 271 |
+
model_type: exec.tool_name
|
| 272 |
+
};
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// Extract RAG/Guidelines
|
| 277 |
if (exec.tool_name === 'rag_query' || exec.tool_name === 'guideline_retrieval') {
|
| 278 |
+
if (exec.output_data?.guidelines && Array.isArray(exec.output_data.guidelines)) {
|
| 279 |
+
exec.output_data.guidelines.forEach((guideline: any) => {
|
| 280 |
+
const content = typeof guideline === 'string'
|
| 281 |
+
? guideline
|
| 282 |
+
: (guideline.content || guideline.snippet || guideline.text || JSON.stringify(guideline));
|
| 283 |
+
|
| 284 |
+
if (content && content.trim()) {
|
| 285 |
+
medicalGuidelines.push({
|
| 286 |
+
content: content.trim(),
|
| 287 |
+
relevance_score: guideline.relevance_score || guideline.score,
|
| 288 |
+
source: guideline.source || 'Bộ Y Tế'
|
| 289 |
+
});
|
| 290 |
+
}
|
| 291 |
+
});
|
| 292 |
}
|
| 293 |
}
|
| 294 |
+
|
| 295 |
+
// Extract hospital từ maps tool
|
| 296 |
+
if (exec.tool_name === 'maps') {
|
| 297 |
+
if (exec.output_data?.hospital) {
|
| 298 |
+
const hospital = exec.output_data.hospital;
|
| 299 |
+
const hospitalKey = hospital.name;
|
| 300 |
+
if (!hospitalsMap.has(hospitalKey)) {
|
| 301 |
+
hospitalsMap.set(hospitalKey, {
|
| 302 |
+
name: hospital.name,
|
| 303 |
+
distance_km: hospital.distance_km,
|
| 304 |
+
address: hospital.address,
|
| 305 |
+
rating: hospital.rating,
|
| 306 |
+
specialty_match: hospital.specialty_match,
|
| 307 |
+
condition: exec.input_data?.condition
|
| 308 |
+
});
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
// Cũng extract từ top_hospitals nếu có
|
| 313 |
+
if (exec.output_data?.top_hospitals && Array.isArray(exec.output_data.top_hospitals)) {
|
| 314 |
+
exec.output_data.top_hospitals.forEach((hospital: any) => {
|
| 315 |
+
const hospitalKey = hospital.name;
|
| 316 |
+
if (!hospitalsMap.has(hospitalKey)) {
|
| 317 |
+
hospitalsMap.set(hospitalKey, {
|
| 318 |
+
name: hospital.name,
|
| 319 |
+
distance_km: hospital.distance_km,
|
| 320 |
+
address: hospital.address
|
| 321 |
+
});
|
| 322 |
+
}
|
| 323 |
+
});
|
| 324 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
}
|
| 326 |
+
});
|
| 327 |
+
|
| 328 |
+
// Build final medical report
|
| 329 |
+
const medicalReport: MedicalReport = {
|
| 330 |
+
session_id: sessionData.id,
|
| 331 |
+
user_id: sessionData.user_id || '',
|
| 332 |
+
created_at: sessionData.created_at,
|
| 333 |
+
updated_at: sessionData.updated_at,
|
| 334 |
+
concerns,
|
| 335 |
+
triage_assessment: triageAssessments,
|
| 336 |
+
suspected_conditions: Array.from(conditionsMap.entries())
|
| 337 |
+
.map(([condition_name, data]) => ({
|
| 338 |
+
condition_name,
|
| 339 |
+
source: data.source,
|
| 340 |
+
confidence: data.confidence,
|
| 341 |
+
occurrences: data.count
|
| 342 |
+
}))
|
| 343 |
+
.sort((a, b) => b.occurrences - a.occurrences),
|
| 344 |
+
medical_guidelines: medicalGuidelines,
|
| 345 |
+
recommendations,
|
| 346 |
+
suggested_hospitals: Array.from(hospitalsMap.values())
|
| 347 |
};
|
| 348 |
+
|
| 349 |
+
// Add image analysis nếu có
|
| 350 |
+
if (imageAnalysis) {
|
| 351 |
+
medicalReport.image_analysis = imageAnalysis;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
return medicalReport;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/**
|
| 358 |
+
* Map confidence string to level
|
| 359 |
+
*/
|
| 360 |
+
private mapConfidenceToLevel(confidence: string): 'high' | 'medium' | 'low' {
|
| 361 |
+
const confLower = confidence.toLowerCase();
|
| 362 |
+
if (confLower.includes('high') || confLower.includes('cao') || parseFloat(confLower) >= 70) {
|
| 363 |
+
return 'high';
|
| 364 |
+
}
|
| 365 |
+
if (confLower.includes('low') || confLower.includes('thấp') || parseFloat(confLower) < 40) {
|
| 366 |
+
return 'low';
|
| 367 |
+
}
|
| 368 |
+
return 'medium';
|
| 369 |
}
|
| 370 |
|
| 371 |
/**
|
| 372 |
+
* Generate markdown report using LLM (Optional - chỉ dùng cho display)
|
| 373 |
*/
|
| 374 |
private async generateMarkdownReport(
|
| 375 |
+
reportContent: MedicalReport,
|
| 376 |
reportType: string
|
| 377 |
): Promise<string> {
|
| 378 |
const prompt = `Bạn là trợ lý y tế chuyên nghiệp. Hãy tạo một báo cáo tổng hợp đầy đủ về cuộc trò chuyện y tế dựa trên dữ liệu sau.
|
|
|
|
| 450 |
}
|
| 451 |
|
| 452 |
/**
|
| 453 |
+
* Generate fallback markdown if LLM fails (Optional)
|
| 454 |
*/
|
| 455 |
private generateFallbackMarkdown(
|
| 456 |
+
reportContent: MedicalReport
|
| 457 |
): string {
|
| 458 |
return `# BÁO CÁO TỔNG HỢP - PHIÊN TƯ VẤN Y TẾ
|
| 459 |
|
|
|
|
| 552 |
user_id: report.user_id,
|
| 553 |
report_type: report.report_type,
|
| 554 |
report_content: report.report_content,
|
| 555 |
+
report_markdown: report.report_markdown || null,
|
| 556 |
generated_at: new Date().toISOString(),
|
| 557 |
created_at: new Date().toISOString()
|
| 558 |
});
|
|
|
|
| 589 |
user_id: data.user_id,
|
| 590 |
report_type: data.report_type,
|
| 591 |
report_content: data.report_content,
|
| 592 |
+
report_markdown: data.report_markdown || undefined
|
| 593 |
};
|
| 594 |
} catch (error) {
|
| 595 |
logger.error({ error }, 'Error getting report');
|