Cuong2004 commited on
Commit
c6aaf95
·
1 Parent(s): c869f17

Reportation

Browse files
migration_add_tool_execution.sql ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Migration: Add tool execution tracking and report generation support
2
+ -- This extends the existing schema to capture full agent execution data
3
+
4
+ -- Create tool_executions table to store all tool calls and results
5
+ CREATE TABLE IF NOT EXISTS tool_executions (
6
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
7
+ session_id uuid REFERENCES conversation_sessions(id) ON DELETE CASCADE,
8
+ message_id uuid REFERENCES conversation_history(id) ON DELETE CASCADE,
9
+ tool_name text NOT NULL, -- 'derm_cv', 'rag_query', 'triage_rules', 'knowledge_base', 'maps'
10
+ tool_display_name text, -- Human-readable name
11
+ execution_order int NOT NULL, -- Order of execution in the workflow
12
+ input_data jsonb, -- Input parameters passed to tool
13
+ output_data jsonb, -- Full output from tool
14
+ execution_time_ms int, -- Duration in milliseconds
15
+ status text NOT NULL CHECK (status IN ('success', 'error', 'skipped')),
16
+ error_message text,
17
+ created_at timestamp with time zone DEFAULT now()
18
+ );
19
+
20
+ -- Create comprehensive_reports table for generated reports
21
+ CREATE TABLE IF NOT EXISTS comprehensive_reports (
22
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
23
+ session_id uuid REFERENCES conversation_sessions(id) ON DELETE CASCADE,
24
+ user_id text NOT NULL,
25
+ report_type text NOT NULL DEFAULT 'full', -- 'full', 'summary', 'tools_only'
26
+ report_content jsonb NOT NULL, -- Full report structure
27
+ report_markdown text, -- Human-readable markdown version
28
+ generated_at timestamp with time zone DEFAULT now(),
29
+ created_at timestamp with time zone DEFAULT now()
30
+ );
31
+
32
+ -- Create indexes for faster queries
33
+ CREATE INDEX IF NOT EXISTS idx_tool_executions_session_id ON tool_executions(session_id);
34
+ CREATE INDEX IF NOT EXISTS idx_tool_executions_message_id ON tool_executions(message_id);
35
+ CREATE INDEX IF NOT EXISTS idx_tool_executions_tool_name ON tool_executions(tool_name);
36
+ CREATE INDEX IF NOT EXISTS idx_tool_executions_created_at ON tool_executions(created_at DESC);
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_comprehensive_reports_session_id ON comprehensive_reports(session_id);
39
+ CREATE INDEX IF NOT EXISTS idx_comprehensive_reports_user_id ON comprehensive_reports(user_id);
40
+ CREATE INDEX IF NOT EXISTS idx_comprehensive_reports_generated_at ON comprehensive_reports(generated_at DESC);
41
+
42
+ -- Grant permissions
43
+ GRANT ALL ON TABLE tool_executions TO service_role;
44
+ GRANT ALL ON TABLE comprehensive_reports TO service_role;
45
+
46
+ GRANT SELECT, INSERT ON TABLE tool_executions TO anon;
47
+ GRANT SELECT, INSERT ON TABLE comprehensive_reports TO anon;
48
+
49
+ -- Enable RLS
50
+ ALTER TABLE tool_executions ENABLE ROW LEVEL SECURITY;
51
+ ALTER TABLE comprehensive_reports ENABLE ROW LEVEL SECURITY;
52
+
53
+ -- RLS Policies
54
+ DROP POLICY IF EXISTS "Service role can access all tool executions" ON tool_executions;
55
+ CREATE POLICY "Service role can access all tool executions"
56
+ ON tool_executions FOR ALL
57
+ TO service_role
58
+ USING (true)
59
+ WITH CHECK (true);
60
+
61
+ DROP POLICY IF EXISTS "Allow anonymous access to tool executions" ON tool_executions;
62
+ CREATE POLICY "Allow anonymous access to tool executions"
63
+ ON tool_executions FOR ALL
64
+ TO anon
65
+ USING (true)
66
+ WITH CHECK (true);
67
+
68
+ DROP POLICY IF EXISTS "Users can access own tool executions" ON tool_executions;
69
+ CREATE POLICY "Users can access own tool executions"
70
+ ON tool_executions FOR ALL
71
+ TO authenticated
72
+ USING (
73
+ EXISTS (
74
+ SELECT 1 FROM conversation_sessions cs
75
+ WHERE cs.id = tool_executions.session_id
76
+ AND cs.user_id = auth.uid()::text
77
+ )
78
+ );
79
+
80
+ DROP POLICY IF EXISTS "Service role can access all reports" ON comprehensive_reports;
81
+ CREATE POLICY "Service role can access all reports"
82
+ ON comprehensive_reports FOR ALL
83
+ TO service_role
84
+ USING (true)
85
+ WITH CHECK (true);
86
+
87
+ DROP POLICY IF EXISTS "Allow anonymous access to reports" ON comprehensive_reports;
88
+ CREATE POLICY "Allow anonymous access to reports"
89
+ ON comprehensive_reports FOR ALL
90
+ TO anon
91
+ USING (true)
92
+ WITH CHECK (true);
93
+
94
+ DROP POLICY IF EXISTS "Users can access own reports" ON comprehensive_reports;
95
+ CREATE POLICY "Users can access own reports"
96
+ ON comprehensive_reports FOR ALL
97
+ TO authenticated
98
+ USING (auth.uid()::text = user_id);
99
+
src/routes/index.ts CHANGED
@@ -8,9 +8,12 @@ import { mapsRoutes } from './maps.route.js';
8
  import { sessionsRoutes } from './sessions.route.js';
9
  import { conversationRoutes } from './conversation.route.js';
10
  import { websocketRoutes } from './websocket.route.js';
 
11
  import { MedagenAgent } from '../agent/agent-executor.js';
12
  import { SupabaseService } from '../services/supabase.service.js';
13
  import { MapsService } from '../services/maps.service.js';
 
 
14
 
15
  export async function registerRoutes(
16
  fastify: FastifyInstance,
@@ -24,6 +27,10 @@ export async function registerRoutes(
24
  // Register WebSocket routes
25
  await websocketRoutes(fastify);
26
 
 
 
 
 
27
  // Register triage routes
28
  await triageRoutes(fastify, agent, supabaseService, mapsService);
29
 
@@ -42,5 +49,8 @@ export async function registerRoutes(
42
  // Register sessions routes
43
  await sessionsRoutes(fastify, supabaseService);
44
  await conversationRoutes(fastify, supabaseService);
 
 
 
45
  }
46
 
 
8
  import { sessionsRoutes } from './sessions.route.js';
9
  import { conversationRoutes } from './conversation.route.js';
10
  import { websocketRoutes } from './websocket.route.js';
11
+ import { reportRoutes } from './report.route.js';
12
  import { MedagenAgent } from '../agent/agent-executor.js';
13
  import { SupabaseService } from '../services/supabase.service.js';
14
  import { MapsService } from '../services/maps.service.js';
15
+ import { ConversationHistoryService } from '../services/conversation-history.service.js';
16
+ import { ToolExecutionTrackerService } from '../services/tool-execution-tracker.service.js';
17
 
18
  export async function registerRoutes(
19
  fastify: FastifyInstance,
 
27
  // Register WebSocket routes
28
  await websocketRoutes(fastify);
29
 
30
+ // Initialize services
31
+ const conversationService = new ConversationHistoryService(supabaseService.getClient());
32
+ const toolTracker = new ToolExecutionTrackerService(supabaseService.getClient());
33
+
34
  // Register triage routes
35
  await triageRoutes(fastify, agent, supabaseService, mapsService);
36
 
 
49
  // Register sessions routes
50
  await sessionsRoutes(fastify, supabaseService);
51
  await conversationRoutes(fastify, supabaseService);
52
+
53
+ // Register report routes
54
+ await reportRoutes(fastify, supabaseService, conversationService, toolTracker);
55
  }
56
 
src/routes/report.route.ts ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
+ import { logger } from '../utils/logger.js';
3
+ import { ReportGenerationService } from '../services/report-generation.service.js';
4
+ import { ConversationHistoryService } from '../services/conversation-history.service.js';
5
+ import { ToolExecutionTrackerService } from '../services/tool-execution-tracker.service.js';
6
+ import { SupabaseService } from '../services/supabase.service.js';
7
+
8
+ export async function reportRoutes(
9
+ fastify: FastifyInstance,
10
+ supabaseService: SupabaseService,
11
+ conversationService: ConversationHistoryService,
12
+ toolTracker: ToolExecutionTrackerService
13
+ ) {
14
+ const reportService = new ReportGenerationService(
15
+ supabaseService.getClient(),
16
+ conversationService,
17
+ toolTracker
18
+ );
19
+
20
+ // Generate comprehensive report for a session
21
+ fastify.get('/api/reports/:session_id', async (
22
+ request: FastifyRequest<{
23
+ Params: { session_id: string };
24
+ Querystring: { type?: 'full' | 'summary' | 'tools_only' };
25
+ }>,
26
+ reply: FastifyReply
27
+ ) => {
28
+ try {
29
+ const { session_id } = request.params;
30
+ const reportType = request.query.type || 'full';
31
+
32
+ // Get session to verify ownership (optional - add auth later)
33
+ const { data: sessionData, error: sessionError } = await supabaseService.getClient()
34
+ .from('conversation_sessions')
35
+ .select('user_id')
36
+ .eq('id', session_id)
37
+ .single();
38
+
39
+ if (sessionError || !sessionData) {
40
+ return reply.status(404).send({
41
+ error: 'Session not found',
42
+ message: `Session ${session_id} does not exist`
43
+ });
44
+ }
45
+
46
+ // Check if report already exists
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
59
+ logger.info(`Generating ${reportType} report for session ${session_id}`);
60
+ const report = await reportService.generateReport(
61
+ session_id,
62
+ sessionData.user_id,
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({
75
+ error: 'Internal server error',
76
+ message: 'Failed to generate report'
77
+ });
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
+ }
164
+
src/routes/triage.route.ts CHANGED
@@ -4,6 +4,8 @@ import { MedagenAgent } from '../agent/agent-executor.js';
4
  import { SupabaseService } from '../services/supabase.service.js';
5
  import { MapsService } from '../services/maps.service.js';
6
  import { ConversationHistoryService } from '../services/conversation-history.service.js';
 
 
7
  import { logger } from '../utils/logger.js';
8
  import type { HealthCheckRequest, HealthCheckResponse } from '../types/index.js';
9
 
@@ -44,8 +46,9 @@ export async function triageRoutes(
44
  supabaseService: SupabaseService,
45
  mapsService: MapsService
46
  ) {
47
- // Initialize conversation history service
48
  const conversationService = new ConversationHistoryService(supabaseService.getClient());
 
49
  fastify.post('/api/health-check', {
50
  schema: {
51
  description: 'Endpoint chính để xử lý triage y tế. Sử dụng ReAct Agent với Gemini 2.5Flash để phân tích triệu chứng và đưa ra khuyến nghị. Hỗ trợ conversation history để xử lý multi-turn conversations.',
@@ -225,7 +228,11 @@ export async function triageRoutes(
225
  const conversationContext = await conversationService.getContextString(activeSessionId, 5);
226
 
227
  // Add user message to history
228
- await conversationService.addUserMessage(activeSessionId, user_id, normalizedText, normalizedImageUrl);
 
 
 
 
229
 
230
  // Process triage with agent (pass conversation context and location)
231
  const triageResult = await agent.processTriage(
@@ -236,21 +243,76 @@ export async function triageRoutes(
236
  location // Pass location for hospital finding
237
  );
238
 
 
 
239
  // Add assistant response to conversation history
 
240
  try {
241
  // Use markdown message if available, otherwise fallback to recommendation.action
242
  const assistantMessage = (triageResult as any).message || triageResult.recommendation.action;
243
- await conversationService.addAssistantMessage(
244
  activeSessionId,
245
  user_id,
246
  assistantMessage,
247
  triageResult
248
  );
 
249
  } catch (error) {
250
  logger.error({ error }, 'Failed to save conversation history');
251
  // Continue even if saving fails
252
  }
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  // Save session to database (for backward compatibility)
255
  try {
256
  await supabaseService.saveSession({
 
4
  import { SupabaseService } from '../services/supabase.service.js';
5
  import { MapsService } from '../services/maps.service.js';
6
  import { ConversationHistoryService } from '../services/conversation-history.service.js';
7
+ import { ToolExecutionTrackerService } from '../services/tool-execution-tracker.service.js';
8
+ import { ToolTrackingHelper } from '../utils/tool-tracking-helper.js';
9
  import { logger } from '../utils/logger.js';
10
  import type { HealthCheckRequest, HealthCheckResponse } from '../types/index.js';
11
 
 
46
  supabaseService: SupabaseService,
47
  mapsService: MapsService
48
  ) {
49
+ // Initialize conversation history service and tool tracker
50
  const conversationService = new ConversationHistoryService(supabaseService.getClient());
51
+ const toolTracker = new ToolExecutionTrackerService(supabaseService.getClient());
52
  fastify.post('/api/health-check', {
53
  schema: {
54
  description: 'Endpoint chính để xử lý triage y tế. Sử dụng ReAct Agent với Gemini 2.5Flash để phân tích triệu chứng và đưa ra khuyến nghị. Hỗ trợ conversation history để xử lý multi-turn conversations.',
 
228
  const conversationContext = await conversationService.getContextString(activeSessionId, 5);
229
 
230
  // Add user message to history
231
+ const userMessage = await conversationService.addUserMessage(activeSessionId, user_id, normalizedText, normalizedImageUrl);
232
+
233
+ // Start tracking tool executions for this message
234
+ toolTracker.startTracking(userMessage.id);
235
+ const startTime = Date.now();
236
 
237
  // Process triage with agent (pass conversation context and location)
238
  const triageResult = await agent.processTriage(
 
243
  location // Pass location for hospital finding
244
  );
245
 
246
+ const totalExecutionTime = Date.now() - startTime;
247
+
248
  // Add assistant response to conversation history
249
+ let assistantMessageId: string | undefined;
250
  try {
251
  // Use markdown message if available, otherwise fallback to recommendation.action
252
  const assistantMessage = (triageResult as any).message || triageResult.recommendation.action;
253
+ const assistantMessageObj = await conversationService.addAssistantMessage(
254
  activeSessionId,
255
  user_id,
256
  assistantMessage,
257
  triageResult
258
  );
259
+ assistantMessageId = assistantMessageObj.id;
260
  } catch (error) {
261
  logger.error({ error }, 'Failed to save conversation history');
262
  // Continue even if saving fails
263
  }
264
 
265
+ // Track tool executions (non-blocking)
266
+ try {
267
+ // Track CV execution if applicable
268
+ await ToolTrackingHelper.trackCVExecution(
269
+ toolTracker,
270
+ activeSessionId,
271
+ userMessage.id,
272
+ triageResult,
273
+ Math.floor(totalExecutionTime * 0.3) // Estimate 30% of time for CV
274
+ );
275
+
276
+ // Track Triage Rules execution
277
+ await ToolTrackingHelper.trackTriageRulesExecution(
278
+ toolTracker,
279
+ activeSessionId,
280
+ userMessage.id,
281
+ triageResult,
282
+ normalizedText || 'Image analysis',
283
+ Math.floor(totalExecutionTime * 0.2) // Estimate 20% of time
284
+ );
285
+
286
+ // Track RAG execution (estimate guidelines count from triage result)
287
+ const guidelinesCount = (triageResult as any).guidelines_count || 3; // Default estimate
288
+ await ToolTrackingHelper.trackRAGExecution(
289
+ toolTracker,
290
+ activeSessionId,
291
+ userMessage.id,
292
+ triageResult,
293
+ normalizedText || 'Image analysis',
294
+ Math.floor(totalExecutionTime * 0.3), // Estimate 30% of time
295
+ guidelinesCount
296
+ );
297
+
298
+ // Track Maps execution if hospital was found
299
+ const nearestClinic = (triageResult as any).nearest_clinic;
300
+ if (nearestClinic) {
301
+ const condition = triageResult.suspected_conditions?.[0]?.name;
302
+ await ToolTrackingHelper.trackMapsExecution(
303
+ toolTracker,
304
+ activeSessionId,
305
+ userMessage.id,
306
+ nearestClinic,
307
+ condition,
308
+ Math.floor(totalExecutionTime * 0.2) // Estimate 20% of time
309
+ );
310
+ }
311
+ } catch (error) {
312
+ logger.error({ error }, 'Failed to track tool executions');
313
+ // Continue even if tracking fails
314
+ }
315
+
316
  // Save session to database (for backward compatibility)
317
  try {
318
  await supabaseService.saveSession({
src/services/report-generation.service.ts ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { logger } from '../utils/logger.js';
3
+ import { ConversationHistoryService } from './conversation-history.service.js';
4
+ 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 {
60
+ private supabaseClient: SupabaseClient;
61
+ private conversationService: ConversationHistoryService;
62
+ private toolTracker: ToolExecutionTrackerService;
63
+ private llm: GeminiLLM;
64
+
65
+ constructor(
66
+ supabaseClient: SupabaseClient,
67
+ conversationService: ConversationHistoryService,
68
+ toolTracker: ToolExecutionTrackerService
69
+ ) {
70
+ this.supabaseClient = supabaseClient;
71
+ this.conversationService = conversationService;
72
+ this.toolTracker = toolTracker;
73
+ this.llm = new GeminiLLM();
74
+ }
75
+
76
+ /**
77
+ * Generate comprehensive report for a session
78
+ */
79
+ async generateReport(
80
+ sessionId: string,
81
+ userId: string,
82
+ reportType: 'full' | 'summary' | 'tools_only' = 'full'
83
+ ): Promise<ComprehensiveReport> {
84
+ try {
85
+ logger.info(`Generating ${reportType} report for session ${sessionId}`);
86
+
87
+ // Get session info
88
+ const { data: sessionData, error: sessionError } = await this.supabaseClient
89
+ .from('conversation_sessions')
90
+ .select('*')
91
+ .eq('id', sessionId)
92
+ .single();
93
+
94
+ if (sessionError || !sessionData) {
95
+ throw new Error(`Session not found: ${sessionId}`);
96
+ }
97
+
98
+ // Get conversation history
99
+ const conversationHistory = await this.conversationService.getHistory(sessionId, 1000);
100
+
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,
108
+ toolExecutions,
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
124
+ await this.saveReport(report);
125
+
126
+ logger.info(`Report generated successfully for session ${sessionId}`);
127
+ return report;
128
+ } catch (error) {
129
+ logger.error({ error }, 'Error generating report');
130
+ throw error;
131
+ }
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.
246
+
247
+ LOẠI BÁO CÁO: ${reportType === 'full' ? 'Báo cáo đầy đủ' : reportType === 'summary' ? 'Tóm tắt' : 'Chỉ công cụ'}
248
+
249
+ THÔNG TIN PHIÊN:
250
+ - Session ID: ${reportContent.session_info.session_id}
251
+ - Thời gian bắt đầu: ${reportContent.session_info.created_at}
252
+ - Thời gian cập nhật: ${reportContent.session_info.updated_at}
253
+ - Số lượng tin nhắn: ${reportContent.session_info.message_count}
254
+
255
+ LỊCH SỬ HỘI THOẠI:
256
+ ${reportContent.conversation_timeline.map((msg, idx) => `
257
+ ${idx + 1}. [${msg.role === 'user' ? 'Người dùng' : 'Hệ thống'}] (${msg.timestamp})
258
+ ${msg.role === 'user' ? 'Câu hỏi/Triệu chứng:' : 'Phản hồi:'}
259
+ ${msg.content}
260
+ ${msg.image_url ? '📷 [Có hình ảnh đính kèm]' : ''}
261
+ ${msg.triage_result ? `\n Mức độ khẩn cấp: ${msg.triage_result.triage_level}` : ''}
262
+ `).join('\n')}
263
+
264
+ THỰC THI CÔNG CỤ (TOOL EXECUTIONS):
265
+ ${reportContent.tool_executions.map((exec, idx) => `
266
+ ${idx + 1}. ${exec.tool_display_name || exec.tool_name}
267
+ - Thứ tự: ${exec.execution_order}
268
+ - Trạng thái: ${exec.status}
269
+ - Thời gian: ${exec.execution_time_ms}ms
270
+ - Input: ${JSON.stringify(exec.input_data, null, 2)}
271
+ - Output: ${JSON.stringify(exec.output_data, null, 2)}
272
+ `).join('\n')}
273
+
274
+ TÓM TẮT:
275
+ - Mối quan tâm chính: ${reportContent.summary.main_concerns.join(', ')}
276
+ - Các bệnh được đề xuất: ${reportContent.summary.top_conditions_suggested.map(c => `${c.name} (${c.confidence}, xuất hiện ${c.occurrences} lần)`).join(', ')}
277
+ - Mức độ khẩn cấp: ${reportContent.summary.triage_levels_identified.map(t => `${t.level} (${t.count} lần)`).join(', ')}
278
+ - Bệnh viện được đề xuất: ${reportContent.summary.hospitals_suggested.map(h => `${h.name} (${h.distance_km}km)`).join(', ') || 'Không có'}
279
+ - Số guideline đã truy xuất: ${reportContent.summary.key_guidelines_retrieved}
280
+
281
+ YÊU CẦU:
282
+ 1. Tạo báo cáo markdown CHUYÊN NGHIỆP, DỄ ĐỌC, CẤU TRÚC RÕ RÀNG
283
+ 2. Bao gồm TẤT CẢ thông tin quan trọng từ tools (CV top 3, RAG guidelines đầy đủ, triage reasoning)
284
+ 3. Sử dụng tiếng Việt hoàn toàn
285
+ 4. Format đẹp với markdown (tiêu đ���, danh sách, bảng nếu cần)
286
+ 5. Nhấn mạnh các thông tin quan trọng mà response message có thể đã bỏ qua
287
+ 6. Bao gồm disclaimer y tế phù hợp
288
+
289
+ CẤU TRÚC BÁO CÁO:
290
+ # BÁO CÁO TỔNG HỢP - PHIÊN TƯ VẤN Y TẾ
291
+
292
+ ## 1. THÔNG TIN PHIÊN
293
+ [Session info]
294
+
295
+ ## 2. TÓM TẮT CUỘC HỘI THOẠI
296
+ [Summary of main concerns, conditions, triage levels]
297
+
298
+ ## 3. CHI TIẾT HỘI THOẠI
299
+ [Full conversation timeline]
300
+
301
+ ## 4. PHÂN TÍCH CÔNG CỤ (TOOLS ANALYSIS)
302
+ [Detailed tool execution results - CV top 3, RAG guidelines, etc.]
303
+
304
+ ## 5. KẾT LUẬN VÀ KHUYẾN NGHỊ
305
+ [Final recommendations]
306
+
307
+ **Lưu ý:** Thông tin chỉ mang tính tham khảo, không thay thế bác sĩ.`;
308
+
309
+ try {
310
+ const generations = await this.llm._generate([prompt]);
311
+ return generations.generations[0][0].text.trim();
312
+ } catch (error) {
313
+ logger.error({ error }, 'Error generating markdown report');
314
+ // Fallback to simple markdown
315
+ return this.generateFallbackMarkdown(reportContent);
316
+ }
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
+
327
+ ## 1. THÔNG TIN PHIÊN
328
+ - **Session ID:** ${reportContent.session_info.session_id}
329
+ - **Thời gian bắt đầu:** ${reportContent.session_info.created_at}
330
+ - **Thời gian cập nhật:** ${reportContent.session_info.updated_at}
331
+ - **Số lượng tin nhắn:** ${reportContent.session_info.message_count}
332
+
333
+ ## 2. TÓM TẮT CUỘC HỘI THOẠI
334
+
335
+ ### Mối quan tâm chính:
336
+ ${reportContent.summary.main_concerns.map(c => `- ${c}`).join('\n')}
337
+
338
+ ### Các bệnh được đề xuất:
339
+ ${reportContent.summary.top_conditions_suggested.map(c =>
340
+ `- **${c.name}** (${c.confidence}, xuất hiện ${c.occurrences} lần, nguồn: ${c.source})`
341
+ ).join('\n')}
342
+
343
+ ### Mức độ khẩn cấp:
344
+ ${reportContent.summary.triage_levels_identified.map(t =>
345
+ `- **${t.level}**: ${t.count} lần`
346
+ ).join('\n')}
347
+
348
+ ### Bệnh viện được đề xuất:
349
+ ${reportContent.summary.hospitals_suggested.length > 0
350
+ ? reportContent.summary.hospitals_suggested.map(h =>
351
+ `- **${h.name}** (${h.distance_km}km) - ${h.address}`
352
+ ).join('\n')
353
+ : '- Không có bệnh viện nào được đề xuất'
354
+ }
355
+
356
+ ### Số guideline đã truy xuất:
357
+ - ${reportContent.summary.key_guidelines_retrieved} guideline snippets
358
+
359
+ ## 3. CHI TIẾT HỘI THOẠI
360
+
361
+ ${reportContent.conversation_timeline.map((msg, idx) => `
362
+ ### Tin nhắn ${idx + 1} - ${msg.role === 'user' ? 'Người dùng' : 'Hệ thống'}
363
+
364
+ **Thời gian:** ${msg.timestamp}
365
+
366
+ **Nội dung:**
367
+ ${msg.content}
368
+
369
+ ${msg.image_url ? '📷 *Có hình ảnh đính kèm*' : ''}
370
+
371
+ ${msg.triage_result ? `
372
+ **Kết quả phân tích:**
373
+ - Mức độ khẩn cấp: ${msg.triage_result.triage_level}
374
+ - Tóm tắt triệu chứng: ${msg.triage_result.symptom_summary}
375
+ ${msg.triage_result.suspected_conditions?.length > 0 ? `
376
+ - Bệnh nghi ngờ:
377
+ ${msg.triage_result.suspected_conditions.map((c: any) => ` - ${c.name} (${c.confidence}, nguồn: ${c.source})`).join('\n')}
378
+ ` : ''}
379
+ ` : ''}
380
+ `).join('\n')}
381
+
382
+ ## 4. PHÂN TÍCH CÔNG CỤ (TOOLS ANALYSIS)
383
+
384
+ ${reportContent.tool_executions.map((exec, idx) => `
385
+ ### ${idx + 1}. ${exec.tool_display_name || exec.tool_name}
386
+
387
+ **Thứ tự thực thi:** ${exec.execution_order}
388
+ **Trạng thái:** ${exec.status}
389
+ **Thời gian:** ${exec.execution_time_ms}ms
390
+
391
+ **Input:**
392
+ \`\`\`json
393
+ ${JSON.stringify(exec.input_data, null, 2)}
394
+ \`\`\`
395
+
396
+ **Output:**
397
+ \`\`\`json
398
+ ${JSON.stringify(exec.output_data, null, 2)}
399
+ \`\`\`
400
+ `).join('\n')}
401
+
402
+ ## 5. KẾT LUẬN VÀ KHUYẾN NGHỊ
403
+
404
+ Dựa trên phân tích toàn bộ cuộc hội thoại và kết quả từ các công cụ, đây là báo cáo tổng hợp đầy đủ về phiên tư vấn y tế.
405
+
406
+ **Lưu ý quan trọng:** Thông tin trong báo cáo này chỉ mang tính tham khảo giáo dục, không thay thế việc khám và chẩn đoán của bác sĩ. Nếu bạn có triệu chứng nghiêm trọng, hãy đến cơ sở y tế ngay lập tức.`;
407
+ }
408
+
409
+ /**
410
+ * Save report to database
411
+ */
412
+ private async saveReport(report: ComprehensiveReport): Promise<void> {
413
+ try {
414
+ const { error } = await this.supabaseClient
415
+ .from('comprehensive_reports')
416
+ .insert({
417
+ id: uuidv4(),
418
+ session_id: report.session_id,
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
+ });
426
+
427
+ if (error) {
428
+ logger.error({ error }, 'Failed to save report');
429
+ throw error;
430
+ }
431
+ } catch (error) {
432
+ logger.error({ error }, 'Error saving report');
433
+ throw error;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Get existing report for a session
439
+ */
440
+ async getReport(sessionId: string): Promise<ComprehensiveReport | null> {
441
+ try {
442
+ const { data, error } = await this.supabaseClient
443
+ .from('comprehensive_reports')
444
+ .select('*')
445
+ .eq('session_id', sessionId)
446
+ .order('generated_at', { ascending: false })
447
+ .limit(1)
448
+ .single();
449
+
450
+ if (error || !data) {
451
+ return null;
452
+ }
453
+
454
+ return {
455
+ session_id: data.session_id,
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');
463
+ return null;
464
+ }
465
+ }
466
+ }
467
+
src/services/tool-execution-tracker.service.ts ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { logger } from '../utils/logger.js';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ export interface ToolExecution {
6
+ tool_name: string;
7
+ tool_display_name: string;
8
+ execution_order: number;
9
+ input_data: any;
10
+ output_data: any;
11
+ execution_time_ms: number;
12
+ status: 'success' | 'error' | 'skipped';
13
+ error_message?: string;
14
+ }
15
+
16
+ export class ToolExecutionTrackerService {
17
+ private supabaseClient: SupabaseClient;
18
+ private currentMessageId: string | null = null;
19
+ private executionOrder: number = 0;
20
+
21
+ constructor(supabaseClient: SupabaseClient) {
22
+ this.supabaseClient = supabaseClient;
23
+ }
24
+
25
+ /**
26
+ * Start tracking for a new message
27
+ */
28
+ startTracking(messageId: string): void {
29
+ this.currentMessageId = messageId;
30
+ this.executionOrder = 0;
31
+ }
32
+
33
+ /**
34
+ * Track a tool execution
35
+ */
36
+ async trackToolExecution(
37
+ sessionId: string,
38
+ execution: ToolExecution
39
+ ): Promise<void> {
40
+ try {
41
+ if (!this.currentMessageId) {
42
+ logger.warn('No message ID set for tool execution tracking');
43
+ return;
44
+ }
45
+
46
+ const executionData = {
47
+ session_id: sessionId,
48
+ message_id: this.currentMessageId,
49
+ tool_name: execution.tool_name,
50
+ tool_display_name: execution.tool_display_name,
51
+ execution_order: execution.execution_order,
52
+ input_data: execution.input_data,
53
+ output_data: execution.output_data,
54
+ execution_time_ms: execution.execution_time_ms,
55
+ status: execution.status,
56
+ error_message: execution.error_message || null,
57
+ created_at: new Date().toISOString()
58
+ };
59
+
60
+ const { error } = await this.supabaseClient
61
+ .from('tool_executions')
62
+ .insert(executionData);
63
+
64
+ if (error) {
65
+ logger.error({ error }, 'Failed to track tool execution');
66
+ // Don't throw - tracking failure shouldn't break the workflow
67
+ } else {
68
+ logger.debug(`Tracked tool execution: ${execution.tool_name} (order: ${execution.execution_order})`);
69
+ }
70
+ } catch (error) {
71
+ logger.error({ error }, 'Error tracking tool execution');
72
+ // Don't throw - tracking is non-critical
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get next execution order number
78
+ */
79
+ getNextExecutionOrder(): number {
80
+ this.executionOrder++;
81
+ return this.executionOrder;
82
+ }
83
+
84
+ /**
85
+ * Get all tool executions for a message
86
+ */
87
+ async getToolExecutionsForMessage(messageId: string): Promise<ToolExecution[]> {
88
+ try {
89
+ const { data, error } = await this.supabaseClient
90
+ .from('tool_executions')
91
+ .select('*')
92
+ .eq('message_id', messageId)
93
+ .order('execution_order', { ascending: true });
94
+
95
+ if (error) {
96
+ logger.error({ error }, 'Failed to get tool executions');
97
+ return [];
98
+ }
99
+
100
+ return (data || []).map(row => ({
101
+ tool_name: row.tool_name,
102
+ tool_display_name: row.tool_display_name,
103
+ execution_order: row.execution_order,
104
+ input_data: row.input_data,
105
+ output_data: row.output_data,
106
+ execution_time_ms: row.execution_time_ms,
107
+ status: row.status,
108
+ error_message: row.error_message
109
+ }));
110
+ } catch (error) {
111
+ logger.error({ error }, 'Error getting tool executions');
112
+ return [];
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get all tool executions for a session
118
+ */
119
+ async getToolExecutionsForSession(sessionId: string): Promise<ToolExecution[]> {
120
+ try {
121
+ const { data, error } = await this.supabaseClient
122
+ .from('tool_executions')
123
+ .select('*')
124
+ .eq('session_id', sessionId)
125
+ .order('created_at', { ascending: true })
126
+ .order('execution_order', { ascending: true });
127
+
128
+ if (error) {
129
+ logger.error({ error }, 'Failed to get tool executions for session');
130
+ return [];
131
+ }
132
+
133
+ return (data || []).map(row => ({
134
+ tool_name: row.tool_name,
135
+ tool_display_name: row.tool_display_name,
136
+ execution_order: row.execution_order,
137
+ input_data: row.input_data,
138
+ output_data: row.output_data,
139
+ execution_time_ms: row.execution_time_ms,
140
+ status: row.status,
141
+ error_message: row.error_message
142
+ }));
143
+ } catch (error) {
144
+ logger.error({ error }, 'Error getting tool executions for session');
145
+ return [];
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Reset tracking state
151
+ */
152
+ reset(): void {
153
+ this.currentMessageId = null;
154
+ this.executionOrder = 0;
155
+ }
156
+ }
157
+
src/utils/tool-tracking-helper.ts ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ToolExecutionTrackerService, type ToolExecution } from '../services/tool-execution-tracker.service.js';
2
+ import { logger } from './logger.js';
3
+ import type { TriageResult } from '../types/index.js';
4
+
5
+ /**
6
+ * Helper to track tool executions from agent results
7
+ * This extracts tool execution data from TriageResult and tracks them
8
+ */
9
+ export class ToolTrackingHelper {
10
+ /**
11
+ * Track CV tool execution from triage result
12
+ */
13
+ static async trackCVExecution(
14
+ tracker: ToolExecutionTrackerService,
15
+ sessionId: string,
16
+ messageId: string,
17
+ triageResult: TriageResult,
18
+ executionTime: number
19
+ ): Promise<void> {
20
+ const cvFindings = triageResult.cv_findings;
21
+
22
+ if (cvFindings.model_used === 'none' || !cvFindings.raw_output?.top_predictions) {
23
+ return; // No CV execution
24
+ }
25
+
26
+ const toolName = cvFindings.model_used;
27
+ const toolDisplayName = this.getToolDisplayName(toolName);
28
+
29
+ const execution: ToolExecution = {
30
+ tool_name: toolName,
31
+ tool_display_name: toolDisplayName,
32
+ execution_order: tracker.getNextExecutionOrder(),
33
+ input_data: {
34
+ model: toolName,
35
+ has_image: true
36
+ },
37
+ output_data: {
38
+ top_conditions: cvFindings.raw_output.top_predictions.map((p: any) => ({
39
+ condition: p.condition,
40
+ probability: p.probability,
41
+ confidence: (p.probability * 100).toFixed(1) + '%'
42
+ })),
43
+ model_used: toolName
44
+ },
45
+ execution_time_ms: executionTime,
46
+ status: 'success'
47
+ };
48
+
49
+ await tracker.trackToolExecution(sessionId, execution);
50
+ }
51
+
52
+ /**
53
+ * Track RAG/Guideline retrieval execution
54
+ * This is inferred from triage result and suspected conditions
55
+ */
56
+ static async trackRAGExecution(
57
+ tracker: ToolExecutionTrackerService,
58
+ sessionId: string,
59
+ messageId: string,
60
+ triageResult: TriageResult,
61
+ userText: string,
62
+ executionTime: number,
63
+ guidelinesCount?: number
64
+ ): Promise<void> {
65
+ // RAG is always called in triage workflow
66
+ const execution: ToolExecution = {
67
+ tool_name: 'rag_query',
68
+ tool_display_name: 'Medical Guidelines Retrieval',
69
+ execution_order: tracker.getNextExecutionOrder(),
70
+ input_data: {
71
+ symptoms: userText,
72
+ suspected_conditions: triageResult.suspected_conditions.map(c => c.name),
73
+ triage_level: triageResult.triage_level
74
+ },
75
+ output_data: {
76
+ guidelines_retrieved: guidelinesCount || 0,
77
+ triage_level: triageResult.triage_level,
78
+ suspected_conditions: triageResult.suspected_conditions
79
+ },
80
+ execution_time_ms: executionTime,
81
+ status: 'success'
82
+ };
83
+
84
+ await tracker.trackToolExecution(sessionId, execution);
85
+ }
86
+
87
+ /**
88
+ * Track Triage Rules execution
89
+ */
90
+ static async trackTriageRulesExecution(
91
+ tracker: ToolExecutionTrackerService,
92
+ sessionId: string,
93
+ messageId: string,
94
+ triageResult: TriageResult,
95
+ userText: string,
96
+ executionTime: number
97
+ ): Promise<void> {
98
+ const execution: ToolExecution = {
99
+ tool_name: 'triage_rules',
100
+ tool_display_name: 'Triage Classification',
101
+ execution_order: tracker.getNextExecutionOrder(),
102
+ input_data: {
103
+ symptoms: userText,
104
+ has_cv_results: triageResult.cv_findings.model_used !== 'none'
105
+ },
106
+ output_data: {
107
+ triage_level: triageResult.triage_level,
108
+ red_flags: triageResult.red_flags,
109
+ reasoning: (triageResult as any).reasoning || 'Triage rules evaluation completed'
110
+ },
111
+ execution_time_ms: executionTime,
112
+ status: 'success'
113
+ };
114
+
115
+ await tracker.trackToolExecution(sessionId, execution);
116
+ }
117
+
118
+ /**
119
+ * Track Hospital/Maps execution
120
+ */
121
+ static async trackMapsExecution(
122
+ tracker: ToolExecutionTrackerService,
123
+ sessionId: string,
124
+ messageId: string,
125
+ nearestClinic: any,
126
+ condition: string | undefined,
127
+ executionTime: number
128
+ ): Promise<void> {
129
+ if (!nearestClinic) {
130
+ return; // No hospital search
131
+ }
132
+
133
+ const execution: ToolExecution = {
134
+ tool_name: 'maps',
135
+ tool_display_name: 'Hospital Search',
136
+ execution_order: tracker.getNextExecutionOrder(),
137
+ input_data: {
138
+ condition: condition,
139
+ location: 'user_location'
140
+ },
141
+ output_data: {
142
+ nearest_clinic: {
143
+ name: nearestClinic.name,
144
+ distance_km: nearestClinic.distance_km,
145
+ address: nearestClinic.address,
146
+ rating: nearestClinic.rating
147
+ }
148
+ },
149
+ execution_time_ms: executionTime,
150
+ status: 'success'
151
+ };
152
+
153
+ await tracker.trackToolExecution(sessionId, execution);
154
+ }
155
+
156
+ /**
157
+ * Get tool display name
158
+ */
159
+ private static getToolDisplayName(toolName: string): string {
160
+ const displayNames: Record<string, string> = {
161
+ 'derm_cv': 'Dermatology CV Analysis',
162
+ 'eye_cv': 'Eye Condition Analysis',
163
+ 'wound_cv': 'Wound Assessment',
164
+ 'triage_rules': 'Triage Classification',
165
+ 'rag_query': 'Medical Guidelines Retrieval',
166
+ 'guideline_retrieval': 'Medical Guidelines Retrieval',
167
+ 'knowledge_base': 'Structured Knowledge Search',
168
+ 'maps': 'Hospital Search'
169
+ };
170
+
171
+ return displayNames[toolName] || toolName;
172
+ }
173
+ }
174
+
test_api_usecases.js CHANGED
@@ -26,6 +26,59 @@ const useCases = [
26
  user_id: 'test_user_2'
27
  }
28
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  // {
30
  // id: 3,
31
  // name: 'MCP CSDL - Câu hỏi định nghĩa bệnh',
@@ -155,6 +208,8 @@ async function runTests() {
155
 
156
  const hasCVContent = response.data.cv_findings?.model_used &&
157
  response.data.cv_findings.model_used !== 'none';
 
 
158
 
159
  const result = {
160
  id: useCase.id,
@@ -171,7 +226,10 @@ async function runTests() {
171
  ragDetected: hasRAGContent,
172
  csdlDetected: hasCSDLContent,
173
  cvDetected: hasCVContent,
 
174
  cvModel: response.data.cv_findings?.model_used || 'none',
 
 
175
  responseLength: JSON.stringify(response.data).length
176
  };
177
 
@@ -200,6 +258,12 @@ async function runTests() {
200
  if (!result.ragDetected) validations.push('Expected RAG but not detected');
201
  }
202
 
 
 
 
 
 
 
203
  if (useCase.expectedMCP === 'RAG' && !result.ragDetected) {
204
  validations.push('Expected RAG but not detected in response');
205
  }
@@ -208,6 +272,10 @@ async function runTests() {
208
  validations.push('Expected CSDL but not detected in response');
209
  }
210
 
 
 
 
 
211
  if (useCase.expectedMCP === 'BOTH') {
212
  if (!result.ragDetected && !result.csdlDetected) {
213
  validations.push('Expected both RAG and CSDL but neither detected');
@@ -240,6 +308,10 @@ async function runTests() {
240
  console.log(` CV Detected: ${result.cvDetected ? '✅' : '❌'} (${result.cvModel})`);
241
  console.log(` RAG Detected: ${result.ragDetected ? '✅' : '❌'}`);
242
  console.log(` CSDL Detected: ${result.csdlDetected ? '✅' : '❌'}`);
 
 
 
 
243
  console.log(` Triage Level: ${result.triageLevel || 'N/A'}`);
244
  console.log(` Suspected Conditions: ${result.hasSuspectedConditions ? 'Yes' : 'No'}`);
245
  console.log(` Response Length: ${result.responseLength} chars`);
@@ -278,18 +350,26 @@ async function runTests() {
278
  console.log(`❌ Failed: ${failed}`);
279
  console.log(`📈 Success Rate: ${((passed / useCases.length) * 100).toFixed(1)}%`);
280
 
281
- console.log('\n--- Detailed Results ---');
282
  results.forEach(r => {
283
  const icon = r.status === 'PASS' ? '✅' : r.status === 'WARNING' ? '⚠️' : '❌';
 
 
 
 
 
 
284
  console.log(`${icon} UC${r.id}: ${r.name}`);
285
  if (r.status === 'PASS' || r.status === 'WARNING') {
286
- console.log(` Expected: ${r.expectedMCP} | RAG: ${r.ragDetected ? '' : ''} | CSDL: ${r.csdlDetected ? '✅' : '❌'}`);
287
- console.log(` Triage: ${r.triageLevel} | Duration: ${r.duration} | Length: ${r.responseLength} chars`);
288
- if (r.validations) {
289
- console.log(` Issues: ${r.validations.join(', ')}`);
 
 
290
  }
291
  } else {
292
- console.log(` Error: ${r.error}`);
293
  }
294
  });
295
 
@@ -308,25 +388,36 @@ async function runTests() {
308
  console.log('\n--- MCP Usage Statistics ---');
309
  let ragCount = 0;
310
  let csdlCount = 0;
 
 
311
  let bothCount = 0;
 
312
  let noneCount = 0;
313
 
314
  results.forEach(r => {
315
- if (r.ragDetected && r.csdlDetected) {
316
- bothCount++;
317
- } else if (r.ragDetected) {
318
- ragCount++;
319
- } else if (r.csdlDetected) {
320
- csdlCount++;
321
- } else {
322
- noneCount++;
 
 
 
 
 
323
  }
324
  });
325
 
326
- console.log(` RAG Only: ${ragCount}`);
327
- console.log(` CSDL Only: ${csdlCount}`);
328
- console.log(` Both RAG + CSDL: ${bothCount}`);
329
- console.log(` Neither: ${noneCount}`);
 
 
 
330
 
331
  console.log('\n--- Expected vs Actual MCP Usage ---');
332
  const mcpStats = {};
 
26
  user_id: 'test_user_2'
27
  }
28
  },
29
+ {
30
+ id: 11,
31
+ name: 'MCP Hospital - Emergency case với location',
32
+ expectedMCP: 'HOSPITAL',
33
+ payload: {
34
+ text: 'Tôi bị đau ngực dữ dội, khó thở, đổ mồ hôi lạnh',
35
+ user_id: 'test_user_11',
36
+ location: {
37
+ lat: 10.762622, // Ho Chi Minh City
38
+ lng: 106.660172
39
+ }
40
+ }
41
+ },
42
+ {
43
+ id: 12,
44
+ name: 'MCP Hospital - Urgent case với location',
45
+ expectedMCP: 'HOSPITAL',
46
+ payload: {
47
+ text: 'Tôi bị sốt cao 39 độ, đau đầu dữ dội, buồn nôn',
48
+ user_id: 'test_user_12',
49
+ location: {
50
+ lat: 21.028511, // Hanoi
51
+ lng: 105.804817
52
+ }
53
+ }
54
+ },
55
+ {
56
+ id: 13,
57
+ name: 'MCP Hospital - User yêu cầu tìm bệnh viện',
58
+ expectedMCP: 'HOSPITAL',
59
+ payload: {
60
+ text: 'Tôi cần tìm bệnh viện gần nhất để khám',
61
+ user_id: 'test_user_13',
62
+ location: {
63
+ lat: 10.762622,
64
+ lng: 106.660172
65
+ }
66
+ }
67
+ },
68
+ {
69
+ id: 14,
70
+ name: 'MCP CV + RAG + Hospital - Da liễu emergency với location',
71
+ expectedMCP: 'CV+RAG+HOSPITAL',
72
+ payload: {
73
+ text: 'Da tôi bị viêm nặng, đỏ rát, có mủ',
74
+ image_url: 'https://www.rosacea.org/sites/default/files/images/rosacea_subtype2.jpg',
75
+ user_id: 'test_user_14',
76
+ location: {
77
+ lat: 10.762622,
78
+ lng: 106.660172
79
+ }
80
+ }
81
+ },
82
  // {
83
  // id: 3,
84
  // name: 'MCP CSDL - Câu hỏi định nghĩa bệnh',
 
208
 
209
  const hasCVContent = response.data.cv_findings?.model_used &&
210
  response.data.cv_findings.model_used !== 'none';
211
+
212
+ const hasHospitalContent = !!response.data.nearest_clinic;
213
 
214
  const result = {
215
  id: useCase.id,
 
226
  ragDetected: hasRAGContent,
227
  csdlDetected: hasCSDLContent,
228
  cvDetected: hasCVContent,
229
+ hospitalDetected: hasHospitalContent,
230
  cvModel: response.data.cv_findings?.model_used || 'none',
231
+ hospitalName: response.data.nearest_clinic?.name || null,
232
+ hospitalDistance: response.data.nearest_clinic?.distance_km || null,
233
  responseLength: JSON.stringify(response.data).length
234
  };
235
 
 
258
  if (!result.ragDetected) validations.push('Expected RAG but not detected');
259
  }
260
 
261
+ if (useCase.expectedMCP === 'CV+RAG+HOSPITAL') {
262
+ if (!result.cvDetected) validations.push('Expected CV but not detected');
263
+ if (!result.ragDetected) validations.push('Expected RAG but not detected');
264
+ if (!result.hospitalDetected) validations.push('Expected Hospital but not detected');
265
+ }
266
+
267
  if (useCase.expectedMCP === 'RAG' && !result.ragDetected) {
268
  validations.push('Expected RAG but not detected in response');
269
  }
 
272
  validations.push('Expected CSDL but not detected in response');
273
  }
274
 
275
+ if (useCase.expectedMCP === 'HOSPITAL') {
276
+ if (!result.hospitalDetected) validations.push('Expected Hospital but not detected');
277
+ }
278
+
279
  if (useCase.expectedMCP === 'BOTH') {
280
  if (!result.ragDetected && !result.csdlDetected) {
281
  validations.push('Expected both RAG and CSDL but neither detected');
 
308
  console.log(` CV Detected: ${result.cvDetected ? '✅' : '❌'} (${result.cvModel})`);
309
  console.log(` RAG Detected: ${result.ragDetected ? '✅' : '❌'}`);
310
  console.log(` CSDL Detected: ${result.csdlDetected ? '✅' : '❌'}`);
311
+ console.log(` Hospital Detected: ${result.hospitalDetected ? '✅' : '❌'}`);
312
+ if (result.hospitalDetected) {
313
+ console.log(` Hospital: ${result.hospitalName || 'N/A'} (${result.hospitalDistance ? result.hospitalDistance + 'km' : 'N/A'})`);
314
+ }
315
  console.log(` Triage Level: ${result.triageLevel || 'N/A'}`);
316
  console.log(` Suspected Conditions: ${result.hasSuspectedConditions ? 'Yes' : 'No'}`);
317
  console.log(` Response Length: ${result.responseLength} chars`);
 
350
  console.log(`❌ Failed: ${failed}`);
351
  console.log(`📈 Success Rate: ${((passed / useCases.length) * 100).toFixed(1)}%`);
352
 
353
+ console.log('\n--- Detailed Results (Short) ---');
354
  results.forEach(r => {
355
  const icon = r.status === 'PASS' ? '✅' : r.status === 'WARNING' ? '⚠️' : '❌';
356
+ const mcpUsed = [];
357
+ if (r.cvDetected) mcpUsed.push('CV');
358
+ if (r.ragDetected) mcpUsed.push('RAG');
359
+ if (r.csdlDetected) mcpUsed.push('CSDL');
360
+ if (r.hospitalDetected) mcpUsed.push('HOSPITAL');
361
+
362
  console.log(`${icon} UC${r.id}: ${r.name}`);
363
  if (r.status === 'PASS' || r.status === 'WARNING') {
364
+ console.log(` Expected: ${r.expectedMCP} | Used: ${mcpUsed.join('+') || 'None'} | Triage: ${r.triageLevel}`);
365
+ if (r.hospitalDetected) {
366
+ console.log(` 🏥 Hospital: ${r.hospitalName} (${r.hospitalDistance}km)`);
367
+ }
368
+ if (r.validations && r.validations.length > 0) {
369
+ console.log(` ⚠️ ${r.validations.join(', ')}`);
370
  }
371
  } else {
372
+ console.log(` Error: ${r.error}`);
373
  }
374
  });
375
 
 
388
  console.log('\n--- MCP Usage Statistics ---');
389
  let ragCount = 0;
390
  let csdlCount = 0;
391
+ let cvCount = 0;
392
+ let hospitalCount = 0;
393
  let bothCount = 0;
394
+ let allThreeCount = 0;
395
  let noneCount = 0;
396
 
397
  results.forEach(r => {
398
+ if (r.status === 'PASS' || r.status === 'WARNING') {
399
+ if (r.ragDetected) ragCount++;
400
+ if (r.csdlDetected) csdlCount++;
401
+ if (r.cvDetected) cvCount++;
402
+ if (r.hospitalDetected) hospitalCount++;
403
+
404
+ if (r.ragDetected && r.csdlDetected && r.hospitalDetected) {
405
+ allThreeCount++;
406
+ } else if (r.ragDetected && r.csdlDetected) {
407
+ bothCount++;
408
+ } else if (!r.ragDetected && !r.csdlDetected && !r.cvDetected && !r.hospitalDetected) {
409
+ noneCount++;
410
+ }
411
  }
412
  });
413
 
414
+ console.log(` CV: ${cvCount}`);
415
+ console.log(` RAG: ${ragCount}`);
416
+ console.log(` CSDL: ${csdlCount}`);
417
+ console.log(` Hospital: ${hospitalCount}`);
418
+ console.log(` RAG + CSDL: ${bothCount}`);
419
+ console.log(` RAG + CSDL + Hospital: ${allThreeCount}`);
420
+ console.log(` None: ${noneCount}`);
421
 
422
  console.log('\n--- Expected vs Actual MCP Usage ---');
423
  const mcpStats = {};