voice-ai-workforce
Version:
Voice AI assistant for workforce management - hands-free task updates, time tracking, and team communication. Built by VenueBoost Inc.
591 lines (518 loc) • 17.9 kB
text/typescript
// packages/core/src/commandClassifier.ts
import {
VoiceCommand,
CommandDefinition,
CommandComplexity,
BusinessContext,
EntityType
} from '../../types/src/types';
export interface CommandClassification {
complexity: CommandComplexity;
canHandle: boolean;
shouldFallback: boolean;
fallbackReason?: string;
confidence: number;
businessRelevance: number; // 0-1 scale
requiredEntities: EntityType[];
detectedKeywords: string[];
}
export interface ClassificationConfig {
confidenceThreshold: number;
businessRelevanceThreshold: number;
enableSmartFallback: boolean;
strictMode: boolean; // If true, be more conservative about handling
}
/**
* Intelligent command classifier that determines what voice package can handle
* vs what needs business API integration
*/
export class CommandClassifier {
private businessKeywords: Record<string, number> = {
// High business relevance (0.8-1.0)
'clock': 0.9,
'timesheet': 0.95,
'overtime': 0.9,
'schedule': 0.8,
'shift': 0.9,
'break': 0.8,
// Task management (0.8-0.95)
'task': 0.9,
'project': 0.85,
'assign': 0.8,
'complete': 0.85,
'finish': 0.8,
'deadline': 0.8,
'priority': 0.8,
// Team and client operations (0.7-0.9)
'team': 0.8,
'client': 0.85,
'customer': 0.8,
'manager': 0.7,
'supervisor': 0.75,
'employee': 0.8,
'staff': 0.8,
// Reporting and analytics (0.8-0.95)
'report': 0.85,
'analytics': 0.8,
'metrics': 0.8,
'dashboard': 0.8,
'statistics': 0.8,
'performance': 0.85,
// Quality and compliance (0.8-0.9)
'quality': 0.8,
'inspection': 0.85,
'compliance': 0.8,
'safety': 0.85,
'audit': 0.8,
// Communication (0.7-0.85)
'message': 0.75,
'notify': 0.7,
'alert': 0.75,
'email': 0.7,
'communication': 0.75,
// Financial and invoicing (0.8-0.9)
'invoice': 0.85,
'payment': 0.8,
'billing': 0.8,
'expense': 0.8,
'budget': 0.8,
// Location and field operations (0.7-0.85)
'location': 0.75,
'site': 0.8,
'field': 0.8,
'route': 0.8,
'travel': 0.7,
// Equipment and supplies (0.8-0.9)
'equipment': 0.85,
'supplies': 0.8,
'inventory': 0.85,
'maintenance': 0.8,
'repair': 0.8
};
private simpleKeywords: Set<string> = new Set([
'help', 'hello', 'hi', 'hey', 'thanks', 'thank you', 'goodbye', 'bye',
'what', 'how', 'when', 'where', 'why', 'commands', 'tutorial', 'guide',
'settings', 'preferences', 'options', 'version', 'about', 'info'
]);
private fallbackPhrases: Record<string, string> = {
'data_required': 'Requires access to business data',
'complex_operation': 'Complex business operation requiring system integration',
'user_context': 'Needs user-specific information from business system',
'real_time_data': 'Requires real-time business data',
'authentication': 'Requires business authentication and permissions',
'database_operation': 'Database operation requiring business API',
'external_integration': 'Requires integration with external business systems',
'workflow_management': 'Complex workflow requiring business logic',
'compliance_check': 'Compliance validation requiring business rules',
'low_confidence': 'Low confidence in command understanding',
'unknown_command': 'Unknown command requiring business interpretation',
'entity_extraction_failed': 'Failed to extract required information',
'ambiguous_request': 'Ambiguous request requiring clarification'
};
constructor(
private businessContext: BusinessContext,
private config: ClassificationConfig = {
confidenceThreshold: 0.7,
businessRelevanceThreshold: 0.6,
enableSmartFallback: true,
strictMode: false
}
) {}
/**
* Classify a command to determine handling capability
*/
classifyCommand(
command: VoiceCommand,
commandDefinition?: CommandDefinition
): CommandClassification {
// Start with command definition if available
if (commandDefinition) {
return this.classifyDefinedCommand(command, commandDefinition);
}
// Analyze unknown command
return this.classifyUnknownCommand(command);
}
/**
* Classify a command with known definition
*/
private classifyDefinedCommand(
command: VoiceCommand,
commandDef: CommandDefinition
): CommandClassification {
const baseClassification: CommandClassification = {
complexity: commandDef.complexity,
canHandle: !commandDef.requiresBusinessData,
shouldFallback: commandDef.requiresBusinessData,
fallbackReason: commandDef.fallbackReason,
confidence: command.confidence,
businessRelevance: this.calculateBusinessRelevance(command.rawText),
requiredEntities: commandDef.entityRequirements || [],
detectedKeywords: this.detectKeywords(command.rawText)
};
// Apply confidence-based adjustments
if (this.config.enableSmartFallback) {
return this.applySmartFallback(baseClassification, command);
}
return baseClassification;
}
/**
* Classify an unknown command
*/
private classifyUnknownCommand(command: VoiceCommand): CommandClassification {
const text = command.rawText.toLowerCase();
const detectedKeywords = this.detectKeywords(text);
const businessRelevance = this.calculateBusinessRelevance(text);
// Simple commands that voice package can handle
if (this.isSimpleCommand(text)) {
return {
complexity: CommandComplexity.SIMPLE,
canHandle: true,
shouldFallback: false,
confidence: Math.max(command.confidence, 0.8),
businessRelevance,
requiredEntities: [],
detectedKeywords
};
}
// Business commands that need fallback
if (businessRelevance >= this.config.businessRelevanceThreshold) {
return {
complexity: CommandComplexity.BUSINESS,
canHandle: false,
shouldFallback: true,
fallbackReason: this.determineFallbackReason(text, businessRelevance, command.confidence),
confidence: command.confidence,
businessRelevance,
requiredEntities: this.detectRequiredEntities(text),
detectedKeywords
};
}
// Low confidence or ambiguous commands
return {
complexity: CommandComplexity.SIMPLE,
canHandle: false,
shouldFallback: true,
fallbackReason: command.confidence < this.config.confidenceThreshold
? this.fallbackPhrases.low_confidence
: this.fallbackPhrases.unknown_command,
confidence: command.confidence,
businessRelevance,
requiredEntities: [],
detectedKeywords
};
}
/**
* Apply smart fallback logic based on confidence and context
*/
private applySmartFallback(
classification: CommandClassification,
command: VoiceCommand
): CommandClassification {
// If confidence is too low, force fallback
if (command.confidence < this.config.confidenceThreshold) {
return {
...classification,
canHandle: false,
shouldFallback: true,
fallbackReason: this.fallbackPhrases.low_confidence
};
}
// If in strict mode, be more conservative
if (this.config.strictMode && classification.businessRelevance > 0.5) {
return {
...classification,
canHandle: false,
shouldFallback: true,
fallbackReason: this.fallbackPhrases.complex_operation
};
}
// Check for missing required entities
if (classification.requiredEntities.length > 0) {
const extractedEntities = Object.keys(command.entities);
const missingEntities = classification.requiredEntities.filter(
entity => !extractedEntities.includes(entity)
);
if (missingEntities.length > 0) {
return {
...classification,
canHandle: false,
shouldFallback: true,
fallbackReason: this.fallbackPhrases.entity_extraction_failed
};
}
}
return classification;
}
/**
* Calculate business relevance score (0-1)
*/
private calculateBusinessRelevance(text: string): number {
const words = text.toLowerCase().split(/\s+/);
let totalRelevance = 0;
let relevantWordCount = 0;
for (const word of words) {
if (this.businessKeywords[word]) {
totalRelevance += this.businessKeywords[word];
relevantWordCount++;
}
}
if (relevantWordCount === 0) return 0;
// Average relevance with bonus for multiple business words
const avgRelevance = totalRelevance / relevantWordCount;
const densityBonus = Math.min(relevantWordCount / words.length, 0.3);
return Math.min(avgRelevance + densityBonus, 1.0);
}
/**
* Detect keywords in text
*/
private detectKeywords(text: string): string[] {
const words = text.toLowerCase().split(/\s+/);
const detected: string[] = [];
for (const word of words) {
if (this.businessKeywords[word] || this.simpleKeywords.has(word)) {
detected.push(word);
}
}
return detected;
}
/**
* Check if command is simple and can be handled by voice package
*/
private isSimpleCommand(text: string): boolean {
const words = text.toLowerCase().split(/\s+/);
// Check for simple keyword patterns
const hasSimpleKeywords = words.some(word => this.simpleKeywords.has(word));
// Check for help patterns
const isHelpPattern = /\b(help|what.*do|show.*command|how.*work)\b/i.test(text);
// Check for greeting patterns
const isGreeting = /\b(hi|hello|hey|good\s+(morning|afternoon|evening))\b/i.test(text);
return hasSimpleKeywords || isHelpPattern || isGreeting;
}
/**
* Detect what entities are likely required for this command
*/
private detectRequiredEntities(text: string): EntityType[] {
const entities: EntityType[] = [];
const lowerText = text.toLowerCase();
// Task-related commands
if (/\b(task|complete|finish|assign)\b/.test(lowerText)) {
entities.push(EntityType.TASK_IDENTIFIER);
}
// Message/communication commands
if (/\b(message|tell|notify|send)\b/.test(lowerText)) {
entities.push(EntityType.RECIPIENT, EntityType.MESSAGE_CONTENT);
}
// Project commands
if (/\bproject\b/.test(lowerText)) {
entities.push(EntityType.PROJECT_NAME);
}
// Priority-related commands
if (/\b(priority|urgent|important)\b/.test(lowerText)) {
entities.push(EntityType.PRIORITY_LEVEL);
}
// Time-related commands
if (/\b(schedule|time|date|when)\b/.test(lowerText)) {
entities.push(EntityType.DATE_TIME);
}
return entities;
}
/**
* Determine specific fallback reason based on analysis
*/
private determineFallbackReason(
text: string,
businessRelevance: number,
confidence: number
): string {
// Low confidence
if (confidence < this.config.confidenceThreshold) {
return this.fallbackPhrases.low_confidence;
}
// High business relevance patterns
if (businessRelevance > 0.8) {
if (/\b(clock|timesheet|schedule)\b/i.test(text)) {
return this.fallbackPhrases.real_time_data;
}
if (/\b(assign|project|task)\b/i.test(text)) {
return this.fallbackPhrases.workflow_management;
}
if (/\b(report|analytics|metrics)\b/i.test(text)) {
return this.fallbackPhrases.database_operation;
}
if (/\b(user|employee|staff|team)\b/i.test(text)) {
return this.fallbackPhrases.user_context;
}
}
// Medium business relevance
if (businessRelevance > 0.6) {
return this.fallbackPhrases.complex_operation;
}
// Default fallback
return this.fallbackPhrases.data_required;
}
/**
* Update business keywords based on business context
*/
updateBusinessContext(context: BusinessContext): void {
this.businessContext = context;
// Add domain-specific keywords
this.addDomainKeywords(context.domain);
// Add capability-specific keywords
context.capabilities.forEach(capability => {
this.addCapabilityKeywords(capability);
});
}
/**
* Add domain-specific keywords
*/
private addDomainKeywords(domain: string): void {
const domainKeywords: Record<string, Record<string, number>> = {
construction: {
'foundation': 0.8,
'concrete': 0.8,
'blueprint': 0.8,
'contractor': 0.8,
'building': 0.8,
'permit': 0.8,
'materials': 0.8,
'crane': 0.8,
'scaffold': 0.8
},
retail: {
'customer': 0.9,
'sale': 0.9,
'inventory': 0.9,
'product': 0.8,
'checkout': 0.8,
'register': 0.8,
'discount': 0.8,
'refund': 0.8
},
healthcare: {
'patient': 0.9,
'appointment': 0.9,
'medical': 0.8,
'treatment': 0.8,
'diagnosis': 0.8,
'prescription': 0.8,
'clinic': 0.8
},
manufacturing: {
'production': 0.9,
'assembly': 0.8,
'quality': 0.9,
'machine': 0.8,
'operator': 0.8,
'shift': 0.9,
'output': 0.8
}
};
const keywords = domainKeywords[domain];
if (keywords) {
Object.assign(this.businessKeywords, keywords);
}
}
/**
* Add capability-specific keywords
*/
private addCapabilityKeywords(capability: string): void {
const capabilityKeywords: Record<string, Record<string, number>> = {
'time tracking': {
'clock': 0.95,
'time': 0.9,
'hours': 0.9,
'overtime': 0.9,
'break': 0.8,
'shift': 0.9
},
'task management': {
'task': 0.95,
'todo': 0.9,
'complete': 0.9,
'assign': 0.9,
'deadline': 0.8,
'priority': 0.8
},
'project management': {
'project': 0.95,
'milestone': 0.8,
'progress': 0.8,
'status': 0.8,
'timeline': 0.8
},
'team coordination': {
'team': 0.9,
'member': 0.8,
'leader': 0.8,
'meeting': 0.8,
'collaboration': 0.8
}
};
const keywords = capabilityKeywords[capability];
if (keywords) {
Object.assign(this.businessKeywords, keywords);
}
}
/**
* Get classification statistics for debugging
*/
getClassificationStats(commands: VoiceCommand[]): {
totalCommands: number;
canHandle: number;
shouldFallback: number;
avgConfidence: number;
avgBusinessRelevance: number;
complexityBreakdown: Record<CommandComplexity, number>;
topFallbackReasons: Array<{ reason: string; count: number }>;
} {
const classifications = commands.map(cmd => this.classifyCommand(cmd));
const stats = {
totalCommands: commands.length,
canHandle: classifications.filter(c => c.canHandle).length,
shouldFallback: classifications.filter(c => c.shouldFallback).length,
avgConfidence: classifications.reduce((sum, c) => sum + c.confidence, 0) / commands.length,
avgBusinessRelevance: classifications.reduce((sum, c) => sum + c.businessRelevance, 0) / commands.length,
complexityBreakdown: {
[CommandComplexity.SIMPLE]: 0,
[CommandComplexity.BUSINESS]: 0,
[CommandComplexity.HYBRID]: 0
},
topFallbackReasons: [] as Array<{ reason: string; count: number }>
};
// Count complexity breakdown
classifications.forEach(c => {
stats.complexityBreakdown[c.complexity]++;
});
// Count fallback reasons
const reasonCounts: Record<string, number> = {};
classifications
.filter(c => c.fallbackReason)
.forEach(c => {
const reason = c.fallbackReason!;
reasonCounts[reason] = (reasonCounts[reason] || 0) + 1;
});
stats.topFallbackReasons = Object.entries(reasonCounts)
.map(([reason, count]) => ({ reason, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 5);
return stats;
}
/**
* Update configuration
*/
updateConfig(newConfig: Partial<ClassificationConfig>): void {
this.config = { ...this.config, ...newConfig };
}
/**
* Add custom business keywords
*/
addCustomKeywords(keywords: Record<string, number>): void {
Object.assign(this.businessKeywords, keywords);
}
/**
* Get current business keywords
*/
getBusinessKeywords(): Record<string, number> {
return { ...this.businessKeywords };
}
}