@multiface.js/context
Version:
Context awareness and memory management for multimodal interactions
552 lines (465 loc) • 17.4 kB
text/typescript
import { create } from 'zustand';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
ContextState,
ContextConfig,
ContextCallbacks,
ContextualData,
UserProfile,
ConversationMemory,
ContextualIntent,
ContextEvent
} from './types';
interface ContextStore extends ContextState {
updateContext: (context: Partial<ContextualData>) => void;
addConversationMemory: (memory: ConversationMemory) => void;
updateUserProfile: (profile: Partial<UserProfile>) => void;
detectIntent: (input: string, context: ContextualData) => ContextualIntent;
getRelevantMemories: (query: string, limit?: number) => ConversationMemory[];
clearOldMemories: () => void;
}
export class ContextManager {
private config: ContextConfig;
private callbacks: ContextCallbacks;
private store: any;
private contextUpdateTimer: NodeJS.Timeout | null = null;
private storageKey = '@multiface_context';
constructor(config: ContextConfig, callbacks: ContextCallbacks = {}) {
// Set default values and merge with user config
const defaultConfig: ContextConfig = {
enableLearning: true,
maxConversationHistory: 100,
contextUpdateInterval: 30000, // 30 seconds
privacyMode: 'balanced',
biometricAuth: false,
locationTracking: true,
conversationSummary: true
};
this.config = Object.assign({}, defaultConfig, config);
this.callbacks = callbacks;
this.initializeStore();
}
private initializeStore() {
this.store = create<ContextStore>((set, get) => ({
currentContext: this.getDefaultContext(),
userProfile: this.getDefaultUserProfile(),
conversationHistory: [],
activeMemories: [],
contextualIntents: [],
isLearning: this.config.enableLearning,
privacySettings: {
storeConversations: this.config.privacyMode !== 'strict',
shareBiometrics: this.config.biometricAuth,
shareLocation: this.config.locationTracking,
dataRetentionDays: this.config.privacyMode === 'strict' ? 7 :
this.config.privacyMode === 'balanced' ? 30 : 90
},
updateContext: (contextUpdate: Partial<ContextualData>) => {
set((state) => {
const newContext = { ...state.currentContext, ...contextUpdate };
this.callbacks.onContextUpdate?.(newContext);
// Emit context update event
this.emitEvent({
type: 'context_update',
data: newContext,
timestamp: Date.now()
});
return { currentContext: newContext };
});
},
addConversationMemory: (memory: ConversationMemory) => {
set((state) => {
const newHistory = [memory, ...state.conversationHistory]
.slice(0, this.config.maxConversationHistory);
// Update active memories (last 5 conversations)
const activeMemories = newHistory.slice(0, 5);
this.callbacks.onMemoryCreated?.(memory);
// Emit memory created event
this.emitEvent({
type: 'memory_created',
data: memory,
timestamp: Date.now()
});
return {
conversationHistory: newHistory,
activeMemories
};
});
},
updateUserProfile: (profileUpdate: Partial<UserProfile>) => {
set((state) => {
const newProfile = { ...state.userProfile, ...profileUpdate };
this.callbacks.onUserProfileUpdated?.(newProfile);
// Emit profile update event
this.emitEvent({
type: 'profile_updated',
data: newProfile,
timestamp: Date.now()
});
return { userProfile: newProfile };
});
},
detectIntent: (input: string, context: ContextualData): ContextualIntent => {
const intent = this.analyzeIntent(input, context);
set((state) => ({
contextualIntents: [intent, ...state.contextualIntents.slice(0, 9)]
}));
this.callbacks.onIntentDetected?.(intent);
// Emit intent detected event
this.emitEvent({
type: 'intent_detected',
data: intent,
timestamp: Date.now()
});
return intent;
},
getRelevantMemories: (query: string, limit: number = 5): ConversationMemory[] => {
const state = get();
return this.searchMemories(state.conversationHistory, query, limit);
},
clearOldMemories: () => {
set((state) => {
const cutoffDate = Date.now() - (state.privacySettings.dataRetentionDays * 24 * 60 * 60 * 1000);
const filteredHistory = state.conversationHistory.filter(
memory => memory.timestamp > cutoffDate
);
return {
conversationHistory: filteredHistory,
activeMemories: filteredHistory.slice(0, 5)
};
});
}
}));
}
async initialize(): Promise<void> {
try {
await this.loadPersistedData();
this.startContextUpdates();
} catch (error) {
console.error('Failed to initialize context manager:', error);
}
}
private async loadPersistedData(): Promise<void> {
try {
const storedData = await AsyncStorage.getItem(this.storageKey);
if (storedData) {
const parsedData = JSON.parse(storedData);
// Update store with persisted data
this.store.getState().updateUserProfile(parsedData.userProfile || {});
if (parsedData.conversationHistory && this.store.getState().privacySettings.storeConversations) {
parsedData.conversationHistory.forEach((memory: ConversationMemory) => {
this.store.getState().addConversationMemory(memory);
});
}
}
} catch (error) {
console.error('Failed to load persisted context data:', error);
}
}
private async persistData(): Promise<void> {
try {
const state = this.store.getState();
const dataToStore = {
userProfile: state.userProfile,
conversationHistory: state.privacySettings.storeConversations ?
state.conversationHistory : [],
timestamp: Date.now()
};
await AsyncStorage.setItem(this.storageKey, JSON.stringify(dataToStore));
} catch (error) {
console.error('Failed to persist context data:', error);
}
}
private startContextUpdates(): void {
if (this.contextUpdateTimer) {
clearInterval(this.contextUpdateTimer);
}
this.contextUpdateTimer = setInterval(() => {
this.updateTemporalContext();
this.persistData();
}, this.config.contextUpdateInterval);
}
private updateTemporalContext(): void {
const now = new Date();
const hour = now.getHours();
let timeOfDay: 'morning' | 'afternoon' | 'evening' | 'night';
if (hour < 6) timeOfDay = 'night';
else if (hour < 12) timeOfDay = 'morning';
else if (hour < 18) timeOfDay = 'afternoon';
else timeOfDay = 'evening';
const temporalUpdate = {
temporal: {
timestamp: Date.now(),
timeOfDay,
dayOfWeek: now.toLocaleDateString('en-US', { weekday: 'long' }),
isWeekend: now.getDay() === 0 || now.getDay() === 6,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
};
this.store.getState().updateContext(temporalUpdate);
}
private analyzeIntent(input: string, context: ContextualData): ContextualIntent {
const lowerInput = input.toLowerCase();
// Simple intent detection (in a real implementation, this would use NLP/ML)
const intentPatterns = {
'get_weather': ['weather', 'temperature', 'rain', 'sunny', 'cloudy'],
'set_reminder': ['remind', 'reminder', 'schedule', 'appointment'],
'play_music': ['play', 'music', 'song', 'artist', 'album'],
'navigation': ['navigate', 'directions', 'route', 'go to', 'find'],
'call_contact': ['call', 'phone', 'dial', 'contact'],
'send_message': ['message', 'text', 'send', 'sms'],
'search': ['search', 'find', 'look up', 'google'],
'control_device': ['turn on', 'turn off', 'dim', 'brighten', 'volume']
};
let detectedIntent = 'unknown';
let confidence = 0.3;
for (const [intent, patterns] of Object.entries(intentPatterns)) {
const matches = patterns.filter(pattern => lowerInput.includes(pattern));
if (matches.length > 0) {
detectedIntent = intent;
confidence = Math.min(0.9, 0.5 + (matches.length * 0.2));
break;
}
}
// Extract entities (simplified)
const entities = this.extractEntities(input);
// Generate contextual suggestions
const suggestions = this.generateSuggestions(detectedIntent, context);
return {
intent: detectedIntent,
confidence,
entities,
context: {
temporal: context.temporal.timeOfDay !== undefined,
spatial: context.spatial.location !== undefined,
personal: true,
conversational: this.store.getState().activeMemories.length > 0
},
suggestions
};
}
private extractEntities(input: string): Array<{ type: string; value: string; confidence: number }> {
const entities: Array<{ type: string; value: string; confidence: number }> = [];
// Time entities
const timePatterns = [
{ pattern: /(\d{1,2}:\d{2})/g, type: 'time' },
{ pattern: /(tomorrow|today|yesterday)/gi, type: 'date' },
{ pattern: /(morning|afternoon|evening|night)/gi, type: 'time_period' }
];
// Location entities
const locationPatterns = [
{ pattern: /(home|work|office|school)/gi, type: 'location' },
{ pattern: /(\d+\s+\w+\s+(street|road|avenue|blvd))/gi, type: 'address' }
];
// Contact entities
const contactPattern = /(?:call|text|message)\s+(\w+)/gi;
[...timePatterns, ...locationPatterns].forEach(({ pattern, type }) => {
const matches = input.match(pattern);
if (matches) {
matches.forEach(match => {
entities.push({
type,
value: match.trim(),
confidence: 0.8
});
});
}
});
return entities;
}
private generateSuggestions(intent: string, context: ContextualData): string[] {
const suggestions: string[] = [];
switch (intent) {
case 'get_weather':
suggestions.push('What\'s the weather like today?');
if (context.spatial.location) {
suggestions.push('Weather forecast for this week');
}
break;
case 'set_reminder':
suggestions.push('Set reminder for tomorrow');
if (context.temporal.timeOfDay === 'evening') {
suggestions.push('Remind me tomorrow morning');
}
break;
case 'play_music':
suggestions.push('Play my favorite playlist');
if (context.user.activityLevel === 'running') {
suggestions.push('Play workout music');
}
break;
case 'navigation':
suggestions.push('Navigate to home');
suggestions.push('Find nearby restaurants');
break;
default:
suggestions.push('What can I help you with?');
suggestions.push('Try asking about weather, music, or reminders');
}
return suggestions.slice(0, 3);
}
private searchMemories(memories: ConversationMemory[], query: string, limit: number): ConversationMemory[] {
const lowerQuery = query.toLowerCase();
return memories
.filter(memory =>
memory.summary?.toLowerCase().includes(lowerQuery) ||
memory.topics.some(topic => topic.toLowerCase().includes(lowerQuery)) ||
memory.messages.some(msg => msg.content.toLowerCase().includes(lowerQuery))
)
.sort((a, b) => b.importance - a.importance)
.slice(0, limit);
}
private getDefaultContext(): ContextualData {
const now = new Date();
const hour = now.getHours();
let timeOfDay: 'morning' | 'afternoon' | 'evening' | 'night';
if (hour < 6) timeOfDay = 'night';
else if (hour < 12) timeOfDay = 'morning';
else if (hour < 18) timeOfDay = 'afternoon';
else timeOfDay = 'evening';
return {
temporal: {
timestamp: Date.now(),
timeOfDay,
dayOfWeek: now.toLocaleDateString('en-US', { weekday: 'long' }),
isWeekend: now.getDay() === 0 || now.getDay() === 6,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
},
spatial: {
environment: 'unknown',
ambientLight: 'unknown',
noiseLevel: 'unknown'
},
device: {
networkType: 'wifi',
orientation: 'portrait'
},
app: {
currentScreen: 'main',
sessionDuration: 0
},
user: {
activityLevel: 'stationary',
attentionLevel: 'focused'
}
};
}
private getDefaultUserProfile(): UserProfile {
return {
id: `user_${Date.now()}`,
preferences: {
language: 'en-US',
voiceSettings: {
speechRate: 1.0,
volume: 0.8
},
interactionModes: ['voice', 'text', 'gesture'],
personalizations: {}
},
usage: {
totalInteractions: 0,
preferredInputTypes: {},
commonCommands: [],
lastActiveTime: Date.now()
}
};
}
private emitEvent(event: ContextEvent): void {
// In a real implementation, this could emit to an event bus
console.log('Context Event:', event);
}
// Public API methods
getContext(): ContextualData {
return this.store.getState().currentContext;
}
getUserProfile(): UserProfile {
return this.store.getState().userProfile;
}
addMessage(content: string, type: 'text' | 'voice' | 'gesture' | 'image' = 'text'): void {
const conversationId = `conv_${Date.now()}`;
const message = {
id: `msg_${Date.now()}`,
sender: 'user',
content,
type,
timestamp: Date.now()
};
const memory: ConversationMemory = {
id: conversationId,
timestamp: Date.now(),
participants: ['user', 'assistant'],
messages: [message],
context: this.getContext(),
topics: this.extractTopics(content),
sentiment: this.analyzeSentiment(content),
importance: this.calculateImportance(content, this.getContext())
};
this.store.getState().addConversationMemory(memory);
}
private extractTopics(content: string): string[] {
// Simple topic extraction (in production, use NLP)
const words = content.toLowerCase().split(/\s+/);
const stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'];
return words.filter(word => word.length > 3 && !stopWords.includes(word)).slice(0, 5);
}
private analyzeSentiment(content: string): 'positive' | 'neutral' | 'negative' {
// Simple sentiment analysis
const positiveWords = ['good', 'great', 'excellent', 'happy', 'love', 'like', 'amazing'];
const negativeWords = ['bad', 'terrible', 'hate', 'dislike', 'awful', 'horrible', 'sad'];
const lowerContent = content.toLowerCase();
const positiveCount = positiveWords.filter(word => lowerContent.includes(word)).length;
const negativeCount = negativeWords.filter(word => lowerContent.includes(word)).length;
if (positiveCount > negativeCount) return 'positive';
if (negativeCount > positiveCount) return 'negative';
return 'neutral';
}
private calculateImportance(content: string, context: ContextualData): number {
let importance = 5; // Base importance
// Increase importance for certain keywords
const importantKeywords = ['urgent', 'important', 'emergency', 'reminder', 'appointment'];
const hasImportantKeywords = importantKeywords.some(keyword =>
content.toLowerCase().includes(keyword)
);
if (hasImportantKeywords) importance += 3;
// Increase importance during work hours
if (context.temporal.timeOfDay === 'morning' || context.temporal.timeOfDay === 'afternoon') {
importance += 1;
}
return Math.min(10, importance);
}
updateSpatialContext(location?: { latitude: number; longitude: number }, environment?: string): void {
const spatialUpdate = {
spatial: {
...this.getContext().spatial,
...(location && { location }),
...(environment && { environment: environment as any })
}
};
this.store.getState().updateContext(spatialUpdate);
}
updateDeviceContext(deviceInfo: Partial<ContextualData['device']>): void {
const deviceUpdate = {
device: {
...this.getContext().device,
...deviceInfo
}
};
this.store.getState().updateContext(deviceUpdate);
}
updateUserActivity(activityLevel: 'stationary' | 'walking' | 'running' | 'driving'): void {
const userUpdate = {
user: {
...this.getContext().user,
activityLevel
}
};
this.store.getState().updateContext(userUpdate);
}
stop(): void {
if (this.contextUpdateTimer) {
clearInterval(this.contextUpdateTimer);
this.contextUpdateTimer = null;
}
this.persistData();
}
}