Reportation
Browse files- migration_add_tool_execution.sql +99 -0
- src/routes/index.ts +10 -0
- src/routes/report.route.ts +164 -0
- src/routes/triage.route.ts +65 -3
- src/services/report-generation.service.ts +467 -0
- src/services/tool-execution-tracker.service.ts +157 -0
- src/utils/tool-tracking-helper.ts +174 -0
- test_api_usecases.js +109 -18
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} |
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
| 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.
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
}
|
| 324 |
});
|
| 325 |
|
| 326 |
-
console.log(`
|
| 327 |
-
console.log(`
|
| 328 |
-
console.log(`
|
| 329 |
-
console.log(`
|
|
|
|
|
|
|
|
|
|
| 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 = {};
|