Skip to main content
Glama
trajectory-tracker.ts15.7 kB
/** * Emotional Trajectory Tracker * * Tracks emotional states over time, detects shifts, recognizes patterns, * identifies triggers, and generates insights about emotional dynamics. */ import type { CircumplexState, EmotionalPattern, EmotionalShift, EmotionalTrigger, EmotionType, TrajectoryInsight, TrajectoryStatistics, } from "./types"; /** * Tracks emotional trajectory over time */ export class EmotionalTrajectoryTracker { private history: CircumplexState[] = []; private triggers: Map< string, { emotionType: EmotionType; intensities: number[]; timestamps: Date[] } > = new Map(); /** * Track a new emotional state * @param state - Circumplex emotional state to track * @param trigger - Optional trigger that caused this state */ trackEmotionalState(state: CircumplexState, trigger?: string): void { // Validate state this.validateState(state); // Add to history this.history.push(state); // Track trigger if provided and there was a shift if (trigger && this.history.length > 1) { const prevState = this.history[this.history.length - 2]; const magnitude = this.calculateShiftMagnitude(prevState, state); if (magnitude > 0.2) { // Significant shift const emotionType = this.inferEmotionType(state); const intensity = Math.sqrt(state.valence ** 2 + state.arousal ** 2 + state.dominance ** 2); if (!this.triggers.has(trigger)) { this.triggers.set(trigger, { emotionType, intensities: [], timestamps: [], }); } const triggerData = this.triggers.get(trigger); if (triggerData) { triggerData.intensities.push(intensity); triggerData.timestamps.push(state.timestamp); } } } } /** * Get emotional state history * @param limit - Optional limit on number of states to return * @returns Array of emotional states */ getHistory(limit?: number): CircumplexState[] { if (limit === undefined) { return [...this.history]; } return this.history.slice(-limit); } /** * Clear all tracked history */ clearHistory(): void { this.history = []; this.triggers.clear(); } /** * Get trajectory statistics * @returns Statistics about the emotional trajectory */ getStatistics(): TrajectoryStatistics { if (this.history.length === 0) { return { totalStates: 0, averageValence: 0, averageArousal: 0, averageDominance: 0, volatility: 0, timeSpan: 0, }; } const totalStates = this.history.length; // Calculate averages const averageValence = this.history.reduce((sum, s) => sum + s.valence, 0) / totalStates; const averageArousal = this.history.reduce((sum, s) => sum + s.arousal, 0) / totalStates; const averageDominance = this.history.reduce((sum, s) => sum + s.dominance, 0) / totalStates; // Calculate volatility (average magnitude of changes) let volatility = 0; if (totalStates > 1) { let totalChange = 0; for (let i = 1; i < totalStates; i++) { totalChange += this.calculateShiftMagnitude(this.history[i - 1], this.history[i]); } volatility = totalChange / (totalStates - 1); } // Calculate time span let timeSpan = 0; if (totalStates > 1) { const firstTime = this.history[0].timestamp.getTime(); const lastTime = this.history[totalStates - 1].timestamp.getTime(); timeSpan = lastTime - firstTime; } return { totalStates, averageValence, averageArousal, averageDominance, volatility, timeSpan, }; } /** * Detect emotional shift between last two states * @param threshold - Minimum magnitude to consider a shift (0-1) * @returns Emotional shift if detected, null otherwise */ detectEmotionalShift(threshold: number): EmotionalShift | null { // Validate threshold if (threshold < 0 || threshold > 1) { throw new Error("Threshold must be between 0 and 1"); } if (this.history.length < 2) { return null; } const fromState = this.history[this.history.length - 2]; const toState = this.history[this.history.length - 1]; const magnitude = this.calculateShiftMagnitude(fromState, toState); if (magnitude < threshold) { return null; } return { fromState, toState, magnitude, timestamp: toState.timestamp, }; } /** * Recognize emotional patterns in the trajectory * @returns Array of detected patterns */ recognizePatterns(): EmotionalPattern[] { if (this.history.length < 3) { return []; } const patterns: EmotionalPattern[] = []; // Detect recurring pattern const recurring = this.detectRecurringPattern(); if (recurring) { patterns.push(recurring); } // Detect progressive pattern const progressive = this.detectProgressivePattern(); if (progressive) { patterns.push(progressive); } // Detect reactive pattern const reactive = this.detectReactivePattern(); if (reactive) { patterns.push(reactive); } return patterns; } /** * Identify emotional triggers * @returns Array of identified triggers */ identifyTriggers(): EmotionalTrigger[] { const triggers: EmotionalTrigger[] = []; for (const [trigger, data] of this.triggers.entries()) { const averageIntensity = data.intensities.reduce((sum, i) => sum + i, 0) / data.intensities.length; triggers.push({ trigger, emotionType: data.emotionType, frequency: data.intensities.length, averageIntensity: Math.min(1.0, averageIntensity / Math.sqrt(3)), // Normalize lastOccurrence: data.timestamps[data.timestamps.length - 1], }); } return triggers; } /** * Generate trajectory insights * @returns Array of insights about the emotional trajectory */ generateTrajectoryInsights(): TrajectoryInsight[] { if (this.history.length < 3) { return []; } const insights: TrajectoryInsight[] = []; const stats = this.getStatistics(); // Trend insight const trendInsight = this.generateTrendInsight(); if (trendInsight) { insights.push(trendInsight); } // Volatility insight if (stats.volatility > 0.4) { insights.push({ type: "volatility", description: "Emotions are highly volatile with frequent significant changes", confidence: Math.min(1.0, stats.volatility), recommendation: "Consider identifying triggers for emotional instability", }); } // Stability insight if (stats.volatility < 0.15) { insights.push({ type: "stability", description: "Emotions are stable with minimal fluctuation", confidence: 1.0 - stats.volatility, }); } // Recovery insight const recoveryInsight = this.generateRecoveryInsight(); if (recoveryInsight) { insights.push(recoveryInsight); } return insights; } /** * Predict emotional trend based on history * @returns Predicted future emotional state */ predictEmotionalTrend(): CircumplexState { if (this.history.length === 0) { throw new Error("Cannot predict trend with empty history"); } if (this.history.length === 1) { return { ...this.history[0] }; } // Use simple moving average with trend extrapolation const recentStates = this.history.slice(-5); // Last 5 states const n = recentStates.length; // Calculate trend let trendValence = 0; let trendArousal = 0; let trendDominance = 0; for (let i = 1; i < n; i++) { trendValence += recentStates[i].valence - recentStates[i - 1].valence; trendArousal += recentStates[i].arousal - recentStates[i - 1].arousal; trendDominance += recentStates[i].dominance - recentStates[i - 1].dominance; } if (n > 1) { trendValence /= n - 1; trendArousal /= n - 1; trendDominance /= n - 1; } // Extrapolate - use last value plus trend for more aggressive prediction const lastState = recentStates[n - 1]; const predictedValence = this.clamp(lastState.valence + trendValence * 2, -1, 1); const predictedArousal = this.clamp(lastState.arousal + trendArousal * 2, 0, 1); const predictedDominance = this.clamp(lastState.dominance + trendDominance * 2, -1, 1); // Calculate confidence based on consistency const stats = this.getStatistics(); const confidence = Math.max(0.3, 1.0 - stats.volatility); return { valence: predictedValence, arousal: predictedArousal, dominance: predictedDominance, confidence, timestamp: new Date(), }; } // Private helper methods private validateState(state: CircumplexState): void { if (isNaN(state.valence) || isNaN(state.arousal) || isNaN(state.dominance)) { throw new Error("State values cannot be NaN"); } if (state.valence < -1 || state.valence > 1) { throw new Error("Valence must be between -1 and 1"); } if (state.arousal < 0 || state.arousal > 1) { throw new Error("Arousal must be between 0 and 1"); } if (state.dominance < -1 || state.dominance > 1) { throw new Error("Dominance must be between -1 and 1"); } } private calculateShiftMagnitude(from: CircumplexState, to: CircumplexState): number { // Euclidean distance normalized by maximum possible distance const valenceDiff = to.valence - from.valence; const arousalDiff = to.arousal - from.arousal; const dominanceDiff = to.dominance - from.dominance; const distance = Math.sqrt(valenceDiff ** 2 + arousalDiff ** 2 + dominanceDiff ** 2); // Maximum possible distance: valence (-1 to 1), arousal (0 to 1), dominance (-1 to 1) // Max change: valence=2, arousal=1, dominance=2 // Max distance = sqrt(2^2 + 1^2 + 2^2) = sqrt(9) = 3 // But for the test case (0,0,0) to (1,1,1), distance = sqrt(3) // To normalize sqrt(3) to 1.0, we divide by sqrt(3) return distance / Math.sqrt(3); } private inferEmotionType(state: CircumplexState): EmotionType { // Simple heuristic based on circumplex dimensions if (state.valence > 0.3 && state.arousal > 0.5) { return "joy"; } else if (state.valence < -0.3 && state.arousal > 0.5) { return "anger"; } else if (state.valence < -0.3 && state.arousal < 0.5) { return "sadness"; } else if (state.valence > 0.3 && state.arousal < 0.5) { return "gratitude"; } else if (state.arousal > 0.7) { return "surprise"; } else { return "fear"; } } private detectRecurringPattern(): EmotionalPattern | null { // Look for similar states appearing multiple times const clusters: CircumplexState[][] = []; for (const state of this.history) { let foundCluster = false; for (const cluster of clusters) { const representative = cluster[0]; const distance = this.calculateShiftMagnitude(representative, state); if (distance < 0.2) { // Similar enough cluster.push(state); foundCluster = true; break; } } if (!foundCluster) { clusters.push([state]); } } // Find largest cluster const largestCluster = clusters.reduce( (max, cluster) => (cluster.length > max.length ? cluster : max), clusters[0] ); if (largestCluster.length >= 3) { const frequency = largestCluster.length; const confidence = Math.min(1.0, frequency / this.history.length); return { type: "recurring", description: `Recurring emotional state appears ${frequency} times`, frequency, confidence, examples: largestCluster.slice(0, 3), }; } return null; } private detectProgressivePattern(): EmotionalPattern | null { // Look for gradual change in one direction let valenceTrend = 0; for (let i = 1; i < this.history.length; i++) { valenceTrend += this.history[i].valence - this.history[i - 1].valence; } const avgValenceTrend = valenceTrend / (this.history.length - 1); if (Math.abs(avgValenceTrend) > 0.1) { const direction = avgValenceTrend > 0 ? "improving" : "declining"; const confidence = Math.min(1.0, Math.abs(avgValenceTrend) * 2); return { type: "progressive", description: `Emotions are progressively ${direction}`, frequency: 1, confidence, examples: [ this.history[0], this.history[Math.floor(this.history.length / 2)], this.history[this.history.length - 1], ], }; } return null; } private detectReactivePattern(): EmotionalPattern | null { // Look for rapid changes let rapidChanges = 0; for (let i = 1; i < this.history.length; i++) { const magnitude = this.calculateShiftMagnitude(this.history[i - 1], this.history[i]); if (magnitude > 0.4) { rapidChanges++; } } const rapidChangeRatio = rapidChanges / (this.history.length - 1); if (rapidChangeRatio > 0.5) { return { type: "reactive", description: `Emotions show rapid changes (${rapidChanges} significant shifts)`, frequency: rapidChanges, confidence: rapidChangeRatio, examples: this.history.slice(-3), }; } return null; } private generateTrendInsight(): TrajectoryInsight | null { const recentStates = this.history.slice(-5); if (recentStates.length < 3) { return null; } const firstValence = recentStates[0].valence; const lastValence = recentStates[recentStates.length - 1].valence; const change = lastValence - firstValence; if (Math.abs(change) > 0.2) { const direction = change > 0 ? "improving" : "declining"; const confidence = Math.min(1.0, Math.abs(change)); let recommendation: string | undefined; if (direction === "declining") { recommendation = "Consider identifying factors contributing to emotional decline"; } return { type: "trend", description: `Emotional trend is ${direction}`, confidence, recommendation, }; } return null; } private generateRecoveryInsight(): TrajectoryInsight | null { if (this.history.length < 4) { return null; } // Look for recovery from negative state const recentStates = this.history.slice(-4); const hasNegativeStart = recentStates[0].valence < -0.3; const hasPositiveEnd = recentStates[recentStates.length - 1].valence > 0.3; if (hasNegativeStart && hasPositiveEnd) { // Check if it's a consistent recovery let isConsistent = true; for (let i = 1; i < recentStates.length; i++) { if (recentStates[i].valence < recentStates[i - 1].valence) { isConsistent = false; break; } } if (isConsistent) { const recovery = recentStates[recentStates.length - 1].valence - recentStates[0].valence; const confidence = Math.min(1.0, recovery); return { type: "recovery", description: "Emotions are recovering from negative state", confidence, recommendation: "Continue current coping strategies", }; } } return null; } private clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server