/** * Test script for voice intent classification * * Run with: node scripts/test-voice-intent.mjs */ // Import intent types (inline for testing) const IntentType = { FEEDING: 'feeding', SLEEP: 'sleep', DIAPER: 'diaper', UNKNOWN: 'unknown', }; const FeedingType = { BOTTLE: 'bottle', BREAST_LEFT: 'breast_left', BREAST_RIGHT: 'breast_right', BREAST_BOTH: 'breast_both', SOLID: 'solid', }; const testCases = [ // ===== FEEDING TESTS ===== { name: 'Bottle feeding with amount in ml', input: 'Fed baby 120 ml', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.BOTTLE, expectedEntities: { amount: 120, unit: 'ml' }, }, { name: 'Bottle feeding with amount in oz', input: 'Gave him 4 oz', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.BOTTLE, expectedEntities: { amount: 4, unit: 'oz' }, }, { name: 'Bottle feeding simple', input: 'Bottle fed the baby', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.BOTTLE, }, { name: 'Breastfeeding left side', input: 'Nursed on left breast for 15 minutes', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.BREAST_LEFT, expectedEntities: { side: 'left', duration: 15 }, }, { name: 'Breastfeeding right side', input: 'Fed from right side', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.BREAST_RIGHT, expectedEntities: { side: 'right' }, }, { name: 'Breastfeeding both sides', input: 'Breastfed on both sides for 20 minutes', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.BREAST_BOTH, expectedEntities: { side: 'both', duration: 20 }, }, { name: 'Solid food', input: 'Baby ate solid food', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.SOLID, }, { name: 'Meal time', input: 'Had breakfast', expectedIntent: IntentType.FEEDING, expectedSubtype: FeedingType.SOLID, }, // ===== SLEEP TESTS ===== { name: 'Nap started', input: 'Baby fell asleep for a nap', expectedIntent: IntentType.SLEEP, expectedEntities: { type: 'nap' }, }, { name: 'Nap with duration', input: 'Napped for 45 minutes', expectedIntent: IntentType.SLEEP, expectedEntities: { duration: 45 }, }, { name: 'Bedtime', input: 'Put baby down for bedtime', expectedIntent: IntentType.SLEEP, expectedEntities: { type: 'night' }, }, { name: 'Night sleep', input: 'Baby is sleeping through the night', expectedIntent: IntentType.SLEEP, expectedEntities: { type: 'night' }, }, { name: 'Woke up', input: 'Baby woke up', expectedIntent: IntentType.SLEEP, }, { name: 'Simple sleep', input: 'Baby is sleeping', expectedIntent: IntentType.SLEEP, }, // ===== DIAPER TESTS ===== { name: 'Wet diaper', input: 'Changed wet diaper', expectedIntent: IntentType.DIAPER, expectedEntities: { type: 'wet' }, }, { name: 'Dirty diaper', input: 'Dirty diaper change', expectedIntent: IntentType.DIAPER, expectedEntities: { type: 'dirty' }, }, { name: 'Poopy diaper', input: 'Baby had a poopy diaper', expectedIntent: IntentType.DIAPER, expectedEntities: { type: 'dirty' }, }, { name: 'Both wet and dirty', input: 'Changed a wet and dirty diaper', expectedIntent: IntentType.DIAPER, expectedEntities: { type: 'both' }, }, { name: 'Poop and pee', input: 'Diaper had both poop and pee', expectedIntent: IntentType.DIAPER, expectedEntities: { type: 'both' }, }, { name: 'Simple diaper change', input: 'Changed diaper', expectedIntent: IntentType.DIAPER, }, { name: 'Bowel movement', input: 'Baby had a bowel movement', expectedIntent: IntentType.DIAPER, expectedEntities: { type: 'dirty' }, }, // ===== COMPLEX/EDGE CASES ===== { name: 'Feeding with relative time', input: 'Fed baby 100ml 30 minutes ago', expectedIntent: IntentType.FEEDING, expectedEntities: { amount: 100 }, }, { name: 'Natural language feeding', input: 'The baby drank 5 ounces from the bottle', expectedIntent: IntentType.FEEDING, expectedEntities: { amount: 5, unit: 'oz' }, }, { name: 'Conversational sleep', input: 'She just fell asleep for her afternoon nap', expectedIntent: IntentType.SLEEP, }, { name: 'Unclear command', input: 'Baby is crying', expectedIntent: IntentType.UNKNOWN, }, ]; // Simplified classification logic for testing function classifyIntent(text) { const lowerText = text.toLowerCase(); // Feeding patterns const feedingKeywords = ['fed', 'feed', 'bottle', 'breast', 'nurse', 'nursing', 'drank', 'ate', 'breakfast', 'lunch', 'dinner', 'solid', 'gave']; const hasFeedingKeyword = feedingKeywords.some(kw => lowerText.includes(kw)); // Sleep patterns const sleepKeywords = ['sleep', 'nap', 'asleep', 'woke', 'bedtime']; const hasSleepKeyword = sleepKeywords.some(kw => lowerText.includes(kw)); // Diaper patterns const diaperKeywords = ['diaper', 'nappy', 'wet', 'dirty', 'poop', 'pee', 'bowel', 'bm', 'soiled']; const hasDiaperKeyword = diaperKeywords.some(kw => lowerText.includes(kw)); let intent = IntentType.UNKNOWN; let subtype = null; const entities = {}; if (hasFeedingKeyword) { intent = IntentType.FEEDING; // Determine feeding subtype if (lowerText.includes('breast') || lowerText.includes('nurs')) { if (lowerText.includes('left')) { subtype = FeedingType.BREAST_LEFT; entities.side = 'left'; } else if (lowerText.includes('right')) { subtype = FeedingType.BREAST_RIGHT; entities.side = 'right'; } else if (lowerText.includes('both')) { subtype = FeedingType.BREAST_BOTH; entities.side = 'both'; } else { subtype = FeedingType.BREAST_BOTH; } } else if (lowerText.includes('solid') || lowerText.includes('ate') || lowerText.includes('breakfast') || lowerText.includes('lunch') || lowerText.includes('dinner')) { subtype = FeedingType.SOLID; } else if (lowerText.includes('from') && (lowerText.includes('left') || lowerText.includes('right'))) { // Handle "fed from right/left side" pattern if (lowerText.includes('left')) { subtype = FeedingType.BREAST_LEFT; entities.side = 'left'; } else { subtype = FeedingType.BREAST_RIGHT; entities.side = 'right'; } } else { subtype = FeedingType.BOTTLE; } // Extract amount const amountMatch = text.match(/(\d+(?:\.\d+)?)\s*(ml|oz|ounces?)/i); if (amountMatch) { entities.amount = parseFloat(amountMatch[1]); const unit = amountMatch[2].toLowerCase(); if (unit.startsWith('oz') || unit.startsWith('ounce')) { entities.unit = 'oz'; } else if (unit.startsWith('ml')) { entities.unit = 'ml'; } } // Extract duration const durationMatch = text.match(/(\d+)\s*minutes?/i); if (durationMatch) { entities.duration = parseInt(durationMatch[1]); } } else if (hasSleepKeyword) { intent = IntentType.SLEEP; // Extract sleep type if (lowerText.includes('nap')) { entities.type = 'nap'; } else if (lowerText.includes('night') || lowerText.includes('bedtime')) { entities.type = 'night'; } // Extract duration const durationMatch = text.match(/(\d+)\s*minutes?/i); if (durationMatch) { entities.duration = parseInt(durationMatch[1]); } } else if (hasDiaperKeyword) { intent = IntentType.DIAPER; const hasWet = /\b(wet|pee)\b/i.test(lowerText); const hasDirty = /\b(dirty|poop|poopy|soiled|bowel|bm)\b/i.test(lowerText); if (hasWet && hasDirty) { entities.type = 'both'; } else if (hasDirty) { entities.type = 'dirty'; } else if (hasWet) { entities.type = 'wet'; } } return { intent, subtype, entities }; } function runTests() { console.log('šŸŽ¤ Testing Voice Intent Classification\n'); console.log('='.repeat(60)); let passed = 0; let failed = 0; const failures = []; for (const testCase of testCases) { const result = classifyIntent(testCase.input); let testPassed = true; const errors = []; // Check intent if (result.intent !== testCase.expectedIntent) { testPassed = false; errors.push(`Intent: expected ${testCase.expectedIntent}, got ${result.intent}`); } // Check subtype if specified if (testCase.expectedSubtype && result.subtype !== testCase.expectedSubtype) { testPassed = false; errors.push(`Subtype: expected ${testCase.expectedSubtype}, got ${result.subtype}`); } // Check entities if specified if (testCase.expectedEntities) { for (const [key, value] of Object.entries(testCase.expectedEntities)) { if (result.entities[key] !== value) { testPassed = false; errors.push(`Entity ${key}: expected ${value}, got ${result.entities[key]}`); } } } if (testPassed) { passed++; console.log(`āœ… PASS: ${testCase.name}`); } else { failed++; console.log(`āŒ FAIL: ${testCase.name}`); failures.push({ testCase, errors }); } } console.log('='.repeat(60)); console.log(`\nšŸ“Š Results: ${passed} passed, ${failed} failed out of ${testCases.length} tests`); if (failures.length > 0) { console.log('\nāŒ Failed tests:'); for (const { testCase, errors } of failures) { console.log(`\n ${testCase.name}:`); console.log(` Input: "${testCase.input}"`); for (const error of errors) { console.log(` - ${error}`); } } } if (failed === 0) { console.log('šŸŽ‰ All tests passed!\n'); } else { console.log(`\nāš ļø ${failed} test(s) failed.\n`); process.exit(1); } } runTests();