Cuong2004 commited on
Commit
ea544af
·
1 Parent(s): 8a43827

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 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
- return reply.status(200).send({
51
- session_id,
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
- return reply.status(200).send({
67
- session_id,
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
- // Get report markdown only (for easy display)
82
- fastify.get('/api/reports/:session_id/markdown', async (
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ỉ 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
- export interface ComprehensiveReport {
 
 
 
 
9
  session_id: string;
10
  user_id: string;
11
- report_type: 'full' | 'summary' | 'tools_only';
12
- report_content: {
13
- session_info: {
14
- session_id: string;
15
- created_at: string;
16
- updated_at: string;
17
- message_count: number;
18
- };
19
- conversation_timeline: Array<{
20
- message_id: string;
21
- role: 'user' | 'assistant';
22
- content: string;
23
- image_url?: string;
24
- timestamp: string;
25
- triage_result?: any;
 
26
  }>;
27
- tool_executions: Array<{
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
- report_markdown: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: reportMarkdown
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<ComprehensiveReport['report_content']> {
143
- // Extract summary data
144
- const mainConcerns: string[] = [];
145
- const conditionsMap = new Map<string, { source: string; confidence: string; count: number }>();
146
- const triageLevelsMap = new Map<string, number>();
147
- const hospitals: Array<{ name: string; distance_km: number; address: string }> = [];
148
- let guidelinesCount = 0;
149
-
150
- // Process conversation history
 
 
 
 
 
 
151
  conversationHistory.forEach(msg => {
 
152
  if (msg.role === 'user') {
153
- mainConcerns.push(msg.content);
 
 
 
 
154
  }
155
-
 
156
  if (msg.triage_result) {
157
- const triageLevel = msg.triage_result.triage_level;
158
- triageLevelsMap.set(triageLevel, (triageLevelsMap.get(triageLevel) || 0) + 1);
159
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  // Extract suspected conditions
161
- if (msg.triage_result.suspected_conditions) {
162
- msg.triage_result.suspected_conditions.forEach((cond: any) => {
163
  const key = cond.name;
 
 
 
164
  if (conditionsMap.has(key)) {
165
  conditionsMap.get(key)!.count++;
166
  } else {
167
  conditionsMap.set(key, {
168
- source: cond.source,
169
- confidence: cond.confidence,
170
  count: 1
171
  });
172
  }
173
  });
174
  }
175
-
176
  // Extract hospital info
177
- if (msg.triage_result.nearest_clinic) {
178
- hospitals.push({
179
- name: msg.triage_result.nearest_clinic.name,
180
- distance_km: msg.triage_result.nearest_clinic.distance_km,
181
- address: msg.triage_result.nearest_clinic.address
182
- });
 
 
 
 
 
 
 
 
 
183
  }
184
  }
185
  });
186
-
187
- // Process tool executions
 
 
 
188
  toolExecutions.forEach(exec => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  if (exec.tool_name === 'rag_query' || exec.tool_name === 'guideline_retrieval') {
190
- if (exec.output_data?.guidelines) {
191
- guidelinesCount += exec.output_data.guidelines.length;
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
  }
194
- });
195
-
196
- return {
197
- session_info: {
198
- session_id: sessionData.id,
199
- created_at: sessionData.created_at,
200
- updated_at: sessionData.updated_at,
201
- message_count: conversationHistory.length
202
- },
203
- conversation_timeline: conversationHistory.map(msg => ({
204
- message_id: msg.id,
205
- role: msg.role,
206
- content: msg.content,
207
- image_url: msg.image_url,
208
- timestamp: msg.created_at,
209
- triage_result: msg.triage_result
210
- })),
211
- tool_executions: toolExecutions.map(exec => ({
212
- tool_name: exec.tool_name,
213
- tool_display_name: exec.tool_display_name,
214
- execution_order: exec.execution_order,
215
- input_data: exec.input_data,
216
- output_data: exec.output_data,
217
- execution_time_ms: exec.execution_time_ms,
218
- status: exec.status
219
- })),
220
- summary: {
221
- main_concerns: mainConcerns,
222
- top_conditions_suggested: Array.from(conditionsMap.entries())
223
- .map(([name, data]) => ({
224
- name,
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: ComprehensiveReport['report_content'],
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: ComprehensiveReport['report_content']
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');