File size: 17,197 Bytes
d4abe4b
 
1a8794d
 
 
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6aaf95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a8794d
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a8794d
d4abe4b
 
1a8794d
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6aaf95
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6aaf95
d4abe4b
c6aaf95
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6aaf95
 
 
 
 
 
d4abe4b
 
 
 
 
 
 
 
c6aaf95
 
 
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a8794d
 
d4abe4b
 
 
 
c6aaf95
 
 
 
1a8794d
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6aaf95
d4abe4b
 
c6aaf95
 
 
 
 
 
d4abe4b
 
c6aaf95
 
 
 
 
 
d4abe4b
 
c6aaf95
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6aaf95
 
d4abe4b
c6aaf95
d4abe4b
 
 
c6aaf95
 
 
 
 
 
 
 
 
 
 
 
 
d4abe4b
 
 
c6aaf95
 
 
 
 
 
 
d4abe4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a8794d
d4abe4b
1a8794d
 
d4abe4b
 
 
 
 
1a8794d
 
 
d4abe4b
 
1a8794d
 
 
d4abe4b
 
 
1a8794d
d4abe4b
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
import axios from 'axios';

// Support both local and HuggingFace Space
const BASE_URL = process.env.API_URL || 'https://medagen-backend.hf.space/api/health-check';
const HEALTH_CHECK_URL = process.env.HEALTH_URL || BASE_URL.replace('/api/health-check', '/health');

// npx tsx test_api_usecases.js
const useCases = [
  {
    id: 1,
    name: 'MCP CV - Da liễu với hình ảnh',
    expectedMCP: 'CV',
    payload: {
      text: 'Da tay nổi mẩn đỏ ngứa',
      image_url: 'https://www.rosacea.org/sites/default/files/images/rosacea_subtype2.jpg',
      user_id: 'test_user_1'
    }
  },
  {
    id: 2,
    name: 'MCP CV + RAG - Triệu chứng với hình ảnh',
    expectedMCP: 'CV+RAG',
    payload: {
      text: 'Mặt nổi nhiều mụn trứng cá, đỏ và sưng',
      image_url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/Atopic_dermatitis.jpg/800px-Atopic_dermatitis.jpg',
      user_id: 'test_user_2'
    }
  },
  {
    id: 11,
    name: 'MCP Hospital - Emergency case với location',
    expectedMCP: 'HOSPITAL',
    payload: {
      text: 'Tôi bị đau ngực dữ dội, khó thở, đổ mồ hôi lạnh',
      user_id: 'test_user_11',
      location: {
        lat: 10.762622, // Ho Chi Minh City
        lng: 106.660172
      }
    }
  },
  {
    id: 12,
    name: 'MCP Hospital - Urgent case với location',
    expectedMCP: 'HOSPITAL',
    payload: {
      text: 'Tôi bị sốt cao 39 độ, đau đầu dữ dội, buồn nôn',
      user_id: 'test_user_12',
      location: {
        lat: 21.028511, // Hanoi
        lng: 105.804817
      }
    }
  },
  {
    id: 13,
    name: 'MCP Hospital - User yêu cầu tìm bệnh viện',
    expectedMCP: 'HOSPITAL',
    payload: {
      text: 'Tôi cần tìm bệnh viện gần nhất để khám',
      user_id: 'test_user_13',
      location: {
        lat: 10.762622,
        lng: 106.660172
      }
    }
  },
  {
    id: 14,
    name: 'MCP CV + RAG + Hospital - Da liễu emergency với location',
    expectedMCP: 'CV+RAG+HOSPITAL',
    payload: {
      text: 'Da tôi bị viêm nặng, đỏ rát, có mủ',
      image_url: 'https://www.rosacea.org/sites/default/files/images/rosacea_subtype2.jpg',
      user_id: 'test_user_14',
      location: {
        lat: 10.762622,
        lng: 106.660172
      }
    }
  },
  // {
  //   id: 3,
  //   name: 'MCP CSDL - Câu hỏi định nghĩa bệnh',
  //   expectedMCP: 'CSDL',
  //   payload: {
  //     text: 'Trứng cá là gì?',
  //     user_id: 'test_user_3'
  //   }
  // },
  // {
  //   id: 4,
  //   name: 'MCP CSDL - Câu hỏi về nguyên nhân',
  //   expectedMCP: 'CSDL',
  //   payload: {
  //     text: 'Nguyên nhân gây ra trứng cá là gì?',
  //     user_id: 'test_user_4'
  //   }
  // },
  // {
  //   id: 5,
  //   name: 'MCP CSDL - Câu hỏi về điều trị',
  //   expectedMCP: 'CSDL',
  //   payload: {
  //     text: 'Cách điều trị mụn trứng cá như thế nào?',
  //     user_id: 'test_user_5'
  //   }
  // },
  // {
  //   id: 6,
  //   name: 'MCP CSDL - Câu hỏi về triệu chứng',
  //   expectedMCP: 'CSDL',
  //   payload: {
  //     text: 'Triệu chứng của trứng cá là gì?',
  //     user_id: 'test_user_6'
  //   }
  // },
  // {
  //   id: 7,
  //   name: 'MCP CSDL - Câu hỏi về phòng bệnh',
  //   expectedMCP: 'CSDL',
  //   payload: {
  //     text: 'Làm sao để phòng ngừa trứng cá?',
  //     user_id: 'test_user_7'
  //   }
  // },
  // {
  //   id: 8,
  //   name: 'MCP RAG + CSDL - Triệu chứng kèm câu hỏi giáo dục',
  //   expectedMCP: 'BOTH',
  //   payload: {
  //     text: 'Tôi bị mụn trứng cá, cho tôi biết về bệnh này và cách xử lý',
  //     user_id: 'test_user_8'
  //   }
  // },
  // {
  //   id: 9,
  //   name: 'MCP RAG - Triệu chứng da liễu không rõ ràng',
  //   expectedMCP: 'RAG',
  //   payload: {
  //     text: 'Da tay bị ngứa và nổi mẩn đỏ',
  //     user_id: 'test_user_9'
  //   }
  // },
  // {
  //   id: 10,
  //   name: 'MCP CSDL - Câu hỏi về biến chứng',
  //   expectedMCP: 'CSDL',
  //   payload: {
  //     text: 'Trứng cá có biến chứng gì không?',
  //     user_id: 'test_user_10'
  //   }
  // }
];

let lastSessionId = null;

async function runTests() {
  console.log('🚀 Starting MCP CV, RAG & CSDL API Tests...\n');
  console.log(`🌐 Testing endpoint: ${BASE_URL}`);
  console.log(`🏥 Health check: ${HEALTH_CHECK_URL}\n`);
  console.log('='.repeat(80));
  
  const results = [];
  let passed = 0;
  let failed = 0;
  let warnings = 0;

  for (const useCase of useCases) {
    console.log(`\n--- Use Case ${useCase.id}: ${useCase.name} ---`);
    
    // Update session_id for follow-up test
    if (useCase.id === 9 && lastSessionId) {
      useCase.payload.session_id = lastSessionId;
    }
    
    try {
      const startTime = Date.now();
      const response = await axios.post(BASE_URL, useCase.payload, {
        timeout: 60000, // Increased timeout for HuggingFace Space
        headers: {
          'Content-Type': 'application/json'
        },
        validateStatus: (status) => status < 500 // Accept 4xx as valid responses for testing
      });
      const duration = Date.now() - startTime;

      // Store session_id for next test
      if (response.data.session_id) {
        lastSessionId = response.data.session_id;
      }

      // Check if RAG, CSDL, or CV was used by examining response content
      const responseText = JSON.stringify(response.data).toLowerCase();
      const hasRAGContent = responseText.includes('guideline') || 
                           responseText.includes('hướng dẫn') ||
                           responseText.includes('phòng bệnh') ||
                           responseText.includes('điều trị') ||
                           (response.data.recommendation?.details && 
                            response.data.recommendation.details.length > 200);
      
      const hasCSDLContent = responseText.includes('định nghĩa') ||
                          responseText.includes('nguyên nhân') ||
                          responseText.includes('triệu chứng') ||
                          responseText.includes('biến chứng') ||
                          responseText.includes('tiên lượng') ||
                          response.data.suspected_conditions?.length > 0;
      
      const hasCVContent = response.data.cv_findings?.model_used && 
                          response.data.cv_findings.model_used !== 'none';
      
      const hasHospitalContent = !!response.data.nearest_clinic;

      const result = {
        id: useCase.id,
        name: useCase.name,
        expectedMCP: useCase.expectedMCP,
        status: 'PASS',
        statusCode: response.status,
        duration: `${duration}ms`,
        triageLevel: response.data.triage_level,
        hasRedFlags: response.data.red_flags?.length > 0,
        hasSuspectedConditions: response.data.suspected_conditions?.length > 0,
        hasRecommendation: !!response.data.recommendation?.action,
        sessionId: response.data.session_id || null,
        ragDetected: hasRAGContent,
        csdlDetected: hasCSDLContent,
        cvDetected: hasCVContent,
        hospitalDetected: hasHospitalContent,
        cvModel: response.data.cv_findings?.model_used || 'none',
        hospitalName: response.data.nearest_clinic?.name || null,
        hospitalDistance: response.data.nearest_clinic?.distance_km || null,
        responseLength: JSON.stringify(response.data).length
      };

      // Validation checks
      const validations = [];
      
      if (!response.data.triage_level) {
        validations.push('Missing triage_level');
      }
      
      if (!response.data.symptom_summary) {
        validations.push('Missing symptom_summary');
      }
      
      if (!response.data.recommendation) {
        validations.push('Missing recommendation');
      }
      
      // MCP-specific validations
      if (useCase.expectedMCP === 'CV' && !result.cvDetected) {
        validations.push('Expected CV but not detected in response');
      }
      
      if (useCase.expectedMCP === 'CV+RAG') {
        if (!result.cvDetected) validations.push('Expected CV but not detected');
        if (!result.ragDetected) validations.push('Expected RAG but not detected');
      }
      
      if (useCase.expectedMCP === 'CV+RAG+HOSPITAL') {
        if (!result.cvDetected) validations.push('Expected CV but not detected');
        if (!result.ragDetected) validations.push('Expected RAG but not detected');
        if (!result.hospitalDetected) validations.push('Expected Hospital but not detected');
      }
      
      if (useCase.expectedMCP === 'RAG' && !result.ragDetected) {
        validations.push('Expected RAG but not detected in response');
      }
      
      if (useCase.expectedMCP === 'CSDL' && !result.csdlDetected) {
        validations.push('Expected CSDL but not detected in response');
      }
      
      if (useCase.expectedMCP === 'HOSPITAL') {
        if (!result.hospitalDetected) validations.push('Expected Hospital but not detected');
      }
      
      if (useCase.expectedMCP === 'BOTH') {
        if (!result.ragDetected && !result.csdlDetected) {
          validations.push('Expected both RAG and CSDL but neither detected');
        } else if (!result.ragDetected) {
          validations.push('Expected RAG but not detected');
        } else if (!result.csdlDetected) {
          validations.push('Expected CSDL but not detected');
        }
      }
      
      // Check if response has meaningful content
      if (result.responseLength < 500) {
        validations.push('Response seems too short (may lack MCP content)');
      }

      if (validations.length > 0) {
        result.status = 'WARNING';
        result.validations = validations;
        warnings++;
      } else {
        passed++;
      }

      results.push(result);

      // Print result
      const statusIcon = response.status === 200 ? '✅' : response.status < 300 ? '⚠️' : '❌';
      console.log(`${statusIcon} Status: ${response.status} (${duration}ms)`);
      console.log(`   Expected MCP: ${result.expectedMCP}`);
      console.log(`   CV Detected: ${result.cvDetected ? '✅' : '❌'} (${result.cvModel})`);
      console.log(`   RAG Detected: ${result.ragDetected ? '✅' : '❌'}`);
      console.log(`   CSDL Detected: ${result.csdlDetected ? '✅' : '❌'}`);
      console.log(`   Hospital Detected: ${result.hospitalDetected ? '✅' : '❌'}`);
      if (result.hospitalDetected) {
        console.log(`   Hospital: ${result.hospitalName || 'N/A'} (${result.hospitalDistance ? result.hospitalDistance + 'km' : 'N/A'})`);
      }
      console.log(`   Triage Level: ${result.triageLevel || 'N/A'}`);
      console.log(`   Suspected Conditions: ${result.hasSuspectedConditions ? 'Yes' : 'No'}`);
      console.log(`   Response Length: ${result.responseLength} chars`);
      if (result.sessionId) {
        console.log(`   Session ID: ${result.sessionId.substring(0, 8)}...`);
      }
      if (validations.length > 0) {
        console.log(`   ⚠️  Warnings: ${validations.join(', ')}`);
      }

    } catch (error) {
      failed++;
      const result = {
        id: useCase.id,
        name: useCase.name,
        status: 'FAIL',
        error: error.response?.data?.message || error.message,
        statusCode: error.response?.status || 'N/A'
      };
      results.push(result);
      
      console.log(`❌ FAIL: ${result.error}`);
      if (error.response?.data) {
        console.log(`   Response: ${JSON.stringify(error.response.data).substring(0, 200)}...`);
      }
    }
  }

  // Summary Report
  console.log('\n' + '='.repeat(80));
  console.log('📊 TEST SUMMARY REPORT');
  console.log('='.repeat(80));
  console.log(`Total Use Cases: ${useCases.length}`);
  console.log(`✅ Passed: ${passed}`);
  console.log(`⚠️  Warnings: ${warnings}`);
  console.log(`❌ Failed: ${failed}`);
  console.log(`📈 Success Rate: ${((passed / useCases.length) * 100).toFixed(1)}%`);
  
  console.log('\n--- Detailed Results (Short) ---');
  results.forEach(r => {
    const icon = r.status === 'PASS' ? '✅' : r.status === 'WARNING' ? '⚠️' : '❌';
    const mcpUsed = [];
    if (r.cvDetected) mcpUsed.push('CV');
    if (r.ragDetected) mcpUsed.push('RAG');
    if (r.csdlDetected) mcpUsed.push('CSDL');
    if (r.hospitalDetected) mcpUsed.push('HOSPITAL');
    
    console.log(`${icon} UC${r.id}: ${r.name}`);
    if (r.status === 'PASS' || r.status === 'WARNING') {
      console.log(`   Expected: ${r.expectedMCP} | Used: ${mcpUsed.join('+') || 'None'} | Triage: ${r.triageLevel}`);
      if (r.hospitalDetected) {
        console.log(`   🏥 Hospital: ${r.hospitalName} (${r.hospitalDistance}km)`);
      }
      if (r.validations && r.validations.length > 0) {
        console.log(`   ⚠️  ${r.validations.join(', ')}`);
      }
    } else {
      console.log(`   ❌ Error: ${r.error}`);
    }
  });

  console.log('\n--- Performance Metrics ---');
  const durations = results
    .filter(r => r.duration)
    .map(r => parseInt(r.duration.replace('ms', '')));
  if (durations.length > 0) {
    const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
    const minDuration = Math.min(...durations);
    const maxDuration = Math.max(...durations);
    console.log(`   Average Response Time: ${avgDuration.toFixed(0)}ms`);
    console.log(`   Min: ${minDuration}ms | Max: ${maxDuration}ms`);
  }

  console.log('\n--- MCP Usage Statistics ---');
  let ragCount = 0;
  let csdlCount = 0;
  let cvCount = 0;
  let hospitalCount = 0;
  let bothCount = 0;
  let allThreeCount = 0;
  let noneCount = 0;
  
  results.forEach(r => {
    if (r.status === 'PASS' || r.status === 'WARNING') {
      if (r.ragDetected) ragCount++;
      if (r.csdlDetected) csdlCount++;
      if (r.cvDetected) cvCount++;
      if (r.hospitalDetected) hospitalCount++;
      
      if (r.ragDetected && r.csdlDetected && r.hospitalDetected) {
        allThreeCount++;
      } else if (r.ragDetected && r.csdlDetected) {
        bothCount++;
      } else if (!r.ragDetected && !r.csdlDetected && !r.cvDetected && !r.hospitalDetected) {
        noneCount++;
      }
    }
  });
  
  console.log(`   CV: ${cvCount}`);
  console.log(`   RAG: ${ragCount}`);
  console.log(`   CSDL: ${csdlCount}`);
  console.log(`   Hospital: ${hospitalCount}`);
  console.log(`   RAG + CSDL: ${bothCount}`);
  console.log(`   RAG + CSDL + Hospital: ${allThreeCount}`);
  console.log(`   None: ${noneCount}`);

  console.log('\n--- Expected vs Actual MCP Usage ---');
  const mcpStats = {};
  results.forEach(r => {
    if (r.status === 'PASS' || r.status === 'WARNING') {
      const key = r.expectedMCP;
      if (!mcpStats[key]) {
        mcpStats[key] = { total: 0, ragDetected: 0, csdlDetected: 0, both: 0, none: 0 };
      }
      mcpStats[key].total++;
      if (r.ragDetected && r.csdlDetected) {
        mcpStats[key].both++;
      } else if (r.ragDetected) {
        mcpStats[key].ragDetected++;
      } else if (r.csdlDetected) {
        mcpStats[key].csdlDetected++;
      } else {
        mcpStats[key].none++;
      }
    }
  });
  
  Object.entries(mcpStats).forEach(([mcp, stats]) => {
    console.log(`   ${mcp}:`);
    console.log(`      Total: ${stats.total} | RAG: ${stats.ragDetected} | CSDL: ${stats.csdlDetected} | Both: ${stats.both} | None: ${stats.none}`);
  });

  console.log('\n--- Triage Level Distribution ---');
  const triageCounts = {};
  results.forEach(r => {
    if (r.triageLevel) {
      triageCounts[r.triageLevel] = (triageCounts[r.triageLevel] || 0) + 1;
    }
  });
  Object.entries(triageCounts).forEach(([level, count]) => {
    console.log(`   ${level}: ${count}`);
  });

  console.log('\n' + '='.repeat(80));
  
  if (failed === 0 && warnings === 0) {
    console.log('🎉 All tests passed perfectly!');
    process.exit(0);
  } else if (failed === 0) {
    console.log('✅ All tests passed with some warnings');
    process.exit(0);
  } else {
    console.log('⚠️  Some tests failed. Please review the results above.');
    process.exit(1);
  }
}

// Check if server is running
async function checkServer() {
  try {
    await axios.get(HEALTH_CHECK_URL, { timeout: 10000 });
    return true;
  } catch (error) {
    console.error(`❌ Health check failed: ${error.message}`);
    return false;
  }
}

async function main() {
  console.log(`🌐 Testing against: ${BASE_URL}`);
  console.log(`🏥 Health check: ${HEALTH_CHECK_URL}\n`);
  
  const serverRunning = await checkServer();
  if (!serverRunning) {
    console.error(`❌ Server is not responding at ${HEALTH_CHECK_URL}`);
    console.error('Please check if the HuggingFace Space is running or set API_URL environment variable');
    console.error('Example: API_URL=https://your-space.hf.space/api/health-check npx tsx test_api_usecases.js');
    process.exit(1);
  }
  
  console.log('✅ Server is responding!\n');
  await runTests();
}

main().catch((error) => {
  console.error('Fatal error:', error);
  process.exit(1);
});