NewDakera v0.11.107 — temporal precision re-ranking, session-cohesion scoring, and reliability fixesSee what's new →
Advanced Agent Behavior

Emotional Context Tracking

⏱ ~35 min to implement 📦 Requires: Dakera v0.11+

Users don't interact with AI in a vacuum — they come frustrated after a bad day, excited after a breakthrough, or anxious about a deadline. An agent that ignores emotional signals responds with the same clinical tone regardless of context, eroding trust. This pattern persists emotional observations across sessions so your agent can adapt its communication style, recognize distress trajectories, and escalate appropriately.

Start Building Free →
Prerequisites
  • Dakera instance running (quickstart)
  • SDK installed: pip install dakera / npm i @dakera-ai/dakera
  • A sentiment/emotion detection step — even a simple keyword classifier is sufficient to start
  • Clear escalation policies defined before going live with emotional tracking in sensitive contexts

The Problem

Users interact with AI systems during some of their most challenging moments. Without emotional memory, agents are tone-deaf across three critical dimensions:

Architecture

This pattern stores emotional observations with sentiment metadata (label, intensity, trigger) in Dakera. Negative signals receive higher importance so they surface reliably during recall. At the start of each session, the agent retrieves recent emotional history and uses it to calibrate tone. A running sentiment trend analysis flags sustained negative trajectories for escalation.

SENTIMENT TIMELINE ACROSS SESSIONS +1.0 0.0 -1.0 Session 1 Session 2 Session 3 Session 4 Session 5 Session 6 hopeful neutral frustrated distressed escalation zone improving recovered

Sentiment timeline: emotional observations stored across six sessions reveal a distress trajectory (sessions 3–4) followed by recovery. The agent adapted tone at each stage based on recalled history.

EMOTIONAL CONTEXT INFLUENCE ON RESPONSE SELECTION Detect Emotion keyword signals: "still broken" "weeks of this" frustrated → 0.82 Dakera Memory store: frustrated @ 0.88 recall: last 5 sessions trend: declining trajectory score: -0.6 escalation: pending Empathetic Mode softer tone, validate frustration explicitly Escalate to Human route to specialist if trajectory < -0.7

Emotional context influence diagram: detected sentiment is stored, recalled history is aggregated into a trajectory score, and the score drives response mode selection between empathetic and escalation paths.

Implementation Steps

Note: Short TTL is essential for emotional memories

Set ttl_seconds=604800 (7 days) on emotional observations. A user's frustration from last month is not relevant context today, and carrying it forward creates a distorted view of the current relationship. Use longer TTL (30 days) only for structural preferences derived from emotional patterns — e.g., "user prefers concise responses when stressed."

Implementation

# Store an emotional observation
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "support-agent-user-42",
    "content": "User expressed sustained frustration. Mentioned the billing issue has recurred for the third consecutive week. Signal: repeated-complaint pattern. Intensity: high.",
    "importance": 0.88,
    "memory_type": "episodic",
    "tags": ["emotion:frustrated", "intensity:high", "trigger:billing"],
    "ttl_seconds": 604800,
    "metadata": {
      "sentiment": "negative",
      "emotion": "frustrated",
      "intensity": 0.82,
      "trigger": "recurring_billing_issue",
      "session_id": "sess_20240521_001"
    }
  }'

# Recall emotional history for trajectory analysis
curl "http://localhost:3000/v1/memory/recall?agent_id=support-agent-user-42&query=user+emotional+state+frustration+distress&top_k=7&min_importance=0.5" \
  -H "Authorization: Bearer dk-..."

# Store an escalation event as persistent memory
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "support-agent-user-42",
    "content": "Escalated to human specialist on 2024-05-21 due to sustained distress trajectory (-0.74 over 4 sessions). Routed to billing team senior agent.",
    "importance": 0.95,
    "memory_type": "episodic",
    "tags": ["escalation", "human-handoff"],
    "metadata": {"escalated_at": "2024-05-21T14:32:00Z", "trajectory_score": -0.74}
  }'
from dakera import DakeraClient
from dataclasses import dataclass
from typing import Optional
import re

client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")

@dataclass
class EmotionSignal:
    sentiment: str          # "positive" | "neutral" | "negative"
    emotion: str            # "frustrated" | "distressed" | "satisfied" | "excited"
    intensity: float        # 0.0 - 1.0
    trigger: Optional[str]  # what caused it

def detect_emotion(message: str) -> EmotionSignal:
    """Lightweight heuristic emotion detector."""
    message_lower = message.lower()

    # Frustration signals
    frustration_signals = [
        "still broken", "still not working", "weeks of this", "i give up",
        "this is ridiculous", "unacceptable", "so frustrating", "again?!"
    ]
    distress_signals = [
        "i can't take", "desperate", "need help now", "this is destroying",
        "really struggling", "at my limit", "please help me"
    ]
    positive_signals = [
        "thank you", "that worked", "perfect", "exactly what i needed",
        "amazing", "finally", "great job", "love this"
    ]

    frust_score = sum(1 for s in frustration_signals if s in message_lower)
    dist_score = sum(1 for s in distress_signals if s in message_lower)
    pos_score = sum(1 for s in positive_signals if s in message_lower)

    # Capitals and punctuation amplify intensity
    caps_ratio = sum(1 for c in message if c.isupper()) / max(len(message), 1)
    exclamations = message.count('!')

    if dist_score > 0:
        return EmotionSignal("negative", "distressed", min(0.9 + caps_ratio, 1.0), "user_message")
    elif frust_score > 0:
        intensity = min(0.6 + frust_score * 0.1 + caps_ratio * 0.2, 0.95)
        return EmotionSignal("negative", "frustrated", intensity, "user_message")
    elif pos_score > 0:
        return EmotionSignal("positive", "satisfied", 0.7 + pos_score * 0.05, None)
    else:
        return EmotionSignal("neutral", "neutral", 0.3, None)

def store_emotional_observation(agent_id: str, user_id: str, observation: str, signal: EmotionSignal) -> None:
    """Persist emotional state observation with appropriate importance and TTL."""
    # Higher importance for negative/intense signals
    importance_base = {"positive": 0.55, "neutral": 0.45, "negative": 0.80}
    importance = min(importance_base[signal.sentiment] + signal.intensity * 0.1, 0.95)

    client.store_memory(
        agent_id=agent_id,
        content=f"Emotional observation: {observation}. Emotion: {signal.emotion}. Intensity: {signal.intensity:.2f}.",
        importance=importance,
        memory_type="episodic",
        tags=[f"emotion:{signal.emotion}", f"sentiment:{signal.sentiment}", f"intensity:{'high' if signal.intensity > 0.7 else 'medium'}"],
        ttl_seconds=604_800  # 7 days
    )

def get_emotional_trajectory(agent_id: str) -> dict:
    """Recall recent emotional history and compute trajectory score."""
    result = client.recall(
        agent_id=agent_id,
        query="user emotional state frustration distress satisfaction",
        top_k=7,
        min_importance=0.4
    )

    memories = result.get("memories", [])
    if not memories:
        return {"trajectory_score": 0.0, "dominant_emotion": "neutral", "escalate": False}

    # Weighted average: most recent = highest weight
    total_weight = 0.0
    weighted_score = 0.0
    sentiment_map = {"positive": 1.0, "neutral": 0.0, "negative": -1.0}

    for i, mem in enumerate(memories):
        weight = 1.0 / (i + 1)  # recency decay: index 0 gets weight 1.0
        meta = mem.get("metadata", {})
        sentiment = meta.get("sentiment", "neutral")
        intensity = meta.get("intensity", 0.5)
        score = sentiment_map.get(sentiment, 0.0) * intensity
        weighted_score += score * weight
        total_weight += weight

    trajectory = weighted_score / total_weight if total_weight > 0 else 0.0

    # Count consecutive negative sessions for escalation trigger
    consecutive_neg = 0
    for mem in memories:
        if mem.get("metadata", {}).get("sentiment") == "negative":
            consecutive_neg += 1
        else:
            break

    return {
        "trajectory_score": round(trajectory, 3),
        "dominant_emotion": memories[0].get("metadata", {}).get("emotion", "neutral") if memories else "neutral",
        "escalate": trajectory < -0.65 and consecutive_neg >= 3,
        "consecutive_negative": consecutive_neg,
        "sample_count": len(memories)
    }

def build_emotional_context_prompt(trajectory: dict) -> str:
    """Generate a system prompt note from emotional trajectory data."""
    score = trajectory["trajectory_score"]
    emotion = trajectory["dominant_emotion"]
    escalate = trajectory["escalate"]

    if escalate:
        return (
            "ESCALATION ALERT: This user has shown sustained distress across multiple sessions "
            f"(trajectory score: {score:.2f}). Immediately acknowledge their difficulties, express genuine empathy, "
            "and offer to connect them with a specialist or supervisor. Do not attempt to solve the issue independently."
        )
    elif score < -0.4:
        return (
            f"Emotional context: User is currently {emotion} (trajectory: {score:.2f}). "
            "Adopt a warm, patient tone. Explicitly acknowledge their frustration before any technical response. "
            "Avoid efficiency-first language. Focus on validation first, solution second."
        )
    elif score > 0.4:
        return (
            f"Emotional context: User is in a positive state (trajectory: {score:.2f}). "
            "Engage warmly, match their energy. This is a good moment to introduce new features or offer additional help."
        )
    else:
        return "Emotional context: User appears to be in a neutral state. Maintain a professional, helpful tone."

# --- Usage in a support agent ---
def handle_support_message(user_id: str, message: str) -> str:
    agent_id = f"support-agent-{user_id}"

    # 1. Detect emotion in incoming message
    signal = detect_emotion(message)

    # 2. Store the observation
    store_emotional_observation(
        agent_id, user_id,
        f"User message indicates {signal.emotion}",
        signal
    )

    # 3. Compute trajectory from history
    trajectory = get_emotional_trajectory(agent_id)

    # 4. Build context-aware system prompt
    emotional_note = build_emotional_context_prompt(trajectory)

    # 5. Use in your LLM call
    # system_prompt = f"{BASE_SYSTEM_PROMPT}

{emotional_note}"
    # response = llm.complete(system=system_prompt, user=message)

    if trajectory["escalate"]:
        return trigger_human_escalation(user_id, trajectory)

    return emotional_note  # For demonstration

def trigger_human_escalation(user_id: str, trajectory: dict) -> str:
    agent_id = f"support-agent-{user_id}"
    client.store_memory(
        agent_id=agent_id,
        content=f"Escalated to human support. Trajectory score: {trajectory['trajectory_score']:.2f}. Consecutive negative sessions: {trajectory['consecutive_negative']}.",
        importance=0.95,
        memory_type="episodic",
        tags=["escalation", "human-handoff"]
    )
    return "Routing to human specialist..."
import { DakeraClient } from '@dakera-ai/dakera';

const client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-...' });

type Sentiment = 'positive' | 'neutral' | 'negative';
type Emotion = 'frustrated' | 'distressed' | 'satisfied' | 'excited' | 'neutral';

interface EmotionSignal {
  sentiment: Sentiment;
  emotion: Emotion;
  intensity: number;
  trigger?: string;
}

function detectEmotion(message: string): EmotionSignal {
  const lower = message.toLowerCase();
  const frustrationPatterns = ['still broken', 'weeks of this', 'i give up', 'ridiculous', 'unacceptable'];
  const distressPatterns = ['desperate', 'need help now', 'really struggling', 'at my limit'];
  const positivePatterns = ['thank you', 'that worked', 'perfect', 'amazing', 'love this'];

  const frustScore = frustrationPatterns.filter(p => lower.includes(p)).length;
  const distScore = distressPatterns.filter(p => lower.includes(p)).length;
  const posScore = positivePatterns.filter(p => lower.includes(p)).length;

  if (distScore > 0) return { sentiment: 'negative', emotion: 'distressed', intensity: 0.92 };
  if (frustScore > 0) return { sentiment: 'negative', emotion: 'frustrated', intensity: Math.min(0.6 + frustScore * 0.1, 0.95) };
  if (posScore > 0) return { sentiment: 'positive', emotion: 'satisfied', intensity: 0.75 };
  return { sentiment: 'neutral', emotion: 'neutral', intensity: 0.3 };
}

async function storeEmotionalObservation(agentId: string, observation: string, signal: EmotionSignal): Promise<void> {
  const importanceBase: Record<Sentiment, number> = { positive: 0.55, neutral: 0.45, negative: 0.80 };
  const importance = Math.min(importanceBase[signal.sentiment] + signal.intensity * 0.1, 0.95);

  await client.storeMemory(agentId, {
    content: `Emotional observation: ${observation}. Emotion: ${signal.emotion}. Intensity: ${signal.intensity.toFixed(2)}.`,
    importance,
    memoryType: 'episodic',
    tags: [`emotion:${signal.emotion}`, `sentiment:${signal.sentiment}`],
    ttl_seconds: 604_800, // 7 days
  });
}

async function getEmotionalTrajectory(agentId: string): Promise<{
  trajectoryScore: number;
  dominantEmotion: string;
  escalate: boolean;
  consecutiveNegative: number;
}> {
  const result = await client.recall(agentId, 'user emotional state frustration distress', { top_k: 7, min_importance: 0.4 });

  const sentimentMap: Record<Sentiment, number> = { positive: 1.0, neutral: 0.0, negative: -1.0 };
  let weightedScore = 0, totalWeight = 0;

  for (let i = 0; i < result.memories.length; i++) {
    const weight = 1 / (i + 1);
    const sentiment = (result.memories[i].metadata?.sentiment as Sentiment) ?? 'neutral';
    const intensity = (result.memories[i].metadata?.intensity as number) ?? 0.5;
    weightedScore += sentimentMap[sentiment] * intensity * weight;
    totalWeight += weight;
  }

  const trajectoryScore = totalWeight > 0 ? weightedScore / totalWeight : 0;

  let consecutiveNegative = 0;
  for (const mem of result.memories) {
    if (mem.metadata?.sentiment === 'negative') consecutiveNegative++;
    else break;
  }

  return {
    trajectoryScore: Math.round(trajectoryScore * 1000) / 1000,
    dominantEmotion: result.memories[0]?.metadata?.emotion as string ?? 'neutral',
    escalate: trajectoryScore < -0.65 && consecutiveNegative >= 3,
    consecutiveNegative,
  };
}

// Compose system prompt from trajectory
function buildEmotionalContextNote(trajectory: Awaited<ReturnType<typeof getEmotionalTrajectory>>): string {
  if (trajectory.escalate) {
    return `ESCALATION ALERT: User in sustained distress (score: ${trajectory.trajectoryScore}). Route to human specialist immediately.`;
  }
  if (trajectory.trajectoryScore < -0.4) {
    return `Emotional context: User is ${trajectory.dominantEmotion} (score: ${trajectory.trajectoryScore}). Acknowledge frustration before providing technical help. Be warm and patient.`;
  }
  return `Emotional context: User appears ${trajectory.dominantEmotion}. Maintain professional, helpful tone.`;
}
use dakera_rs::{Client, StoreMemoryRequest, RecallRequest};

let client = Client::new("http://localhost:3000", "dk-...");

// Store a negative emotional observation
client.store_memory("support-agent-user-42", StoreMemoryRequest {
    content: "Emotional observation: User expressed sustained frustration about recurring billing issue. Emotion: frustrated. Intensity: 0.82.".into(),
    importance: Some(0.88),
    memory_type: "episodic".into(),
    tags: vec!["emotion:frustrated".into(), "sentiment:negative".into()],
    ttl_seconds: Some(604_800),
    ..Default::default()
}).await?;

// Recall emotional history and compute trajectory
let history = client.recall("support-agent-user-42", RecallRequest {
    query: "user emotional state frustration distress satisfaction".into(),
    top_k: Some(7),
    min_importance: Some(0.4),
    ..Default::default()
}).await?;

// Compute weighted trajectory score
let mut weighted_score = 0.0f64;
let mut total_weight = 0.0f64;
for (i, mem) in history.memories.iter().enumerate() {
    let weight = 1.0 / (i as f64 + 1.0);
    let sentiment = mem.metadata.as_ref()
        .and_then(|m| m.get("sentiment"))
        .and_then(|v| v.as_str())
        .unwrap_or("neutral");
    let intensity = mem.metadata.as_ref()
        .and_then(|m| m.get("intensity"))
        .and_then(|v| v.as_f64())
        .unwrap_or(0.5);
    let score = match sentiment {
        "positive" => 1.0,
        "negative" => -1.0,
        _ => 0.0,
    } * intensity;
    weighted_score += score * weight;
    total_weight += weight;
}

let trajectory = if total_weight > 0.0 { weighted_score / total_weight } else { 0.0 };
let escalate = trajectory < -0.65;
println!("Trajectory score: {:.3}, escalate: {}", trajectory, escalate);
client := dakera.NewClient("http://localhost:3000", "dk-...")
ctx := context.Background()

// Store emotional observation
client.StoreMemory(ctx, "support-agent-user-42", dakera.StoreMemoryRequest{
    Content:    "Emotional observation: User frustrated about recurring billing issue. Intensity: 0.82.",
    Importance: 0.88,
    MemoryType: "episodic",
    Tags:       []string{"emotion:frustrated", "sentiment:negative"},
    TTLSeconds: 604_800,
    Metadata: map[string]interface{}{
        "sentiment": "negative",
        "emotion":   "frustrated",
        "intensity":  0.82,
    },
})

// Recall and compute trajectory
result, _ := client.Recall(ctx, "support-agent-user-42", dakera.RecallRequest{
    Query:         "user emotional state frustration distress",
    TopK:          7,
    MinImportance: 0.4,
})

sentimentMap := map[string]float64{"positive": 1.0, "neutral": 0.0, "negative": -1.0}
var weightedScore, totalWeight float64

for i, mem := range result.Memories {
    weight := 1.0 / float64(i+1)
    sentiment, _ := mem.Metadata["sentiment"].(string)
    intensity, _ := mem.Metadata["intensity"].(float64)
    score := sentimentMap[sentiment] * intensity
    weightedScore += score * weight
    totalWeight += weight
}

trajectory := 0.0
if totalWeight > 0 {
    trajectory = weightedScore / totalWeight
}
fmt.Printf("Trajectory: %.3f, Escalate: %v
", trajectory, trajectory < -0.65)

Build emotionally-aware AI experiences

Dakera memory gives your support agents the emotional continuity users expect from human agents.

Try Free →

Real-World Scenario: Mental Health Support Agent

A digital mental health platform deploys a support agent for users between therapy sessions. The agent handles 400+ daily conversations across a user base with varying emotional states. Emotional context tracking delivers measurable improvements:

Before / After Memory State

Before: No Emotional Memory
// Memory store: empty

// Session 5 opens:
"Hello! How can I help
you today? 😊"

// User just spent 4 sessions
// describing mounting anxiety.
// Agent has no idea.

// Agent responds with same
// upbeat default tone regardless
// of emotional history.

// User feels unheard, abandons
// the platform.
After: Emotional Memory Active
// Memory store:
{
  "content": "User frustrated about recurring anxiety. Intensity 0.82.",
  "importance": 0.88,
  "tags": ["emotion:frustrated"]
}
// trajectory_score: -0.61
// consecutive_negative: 4

// Session 5 opens:
"I'm glad you came back.
You've had some tough weeks.
How are you feeling today?"

// Warm, trauma-informed tone
// based on recalled history.
// Escalation pending at -0.65.

SDK Method Reference

MethodSDKPurpose in this pattern
store_memory(agent_id, content, importance, memory_type, tags, ttl_seconds)PythonPersist emotional observation with short TTL
recall(agent_id, query, top_k, min_importance)PythonRetrieve recent emotional history for trajectory analysis
storeMemory(agentId, {content, importance, memoryType, tags, ttl_seconds})TypeScriptStore emotional signal with 7-day expiry
recall(agentId, query, {top_k, min_importance})TypeScriptRecall emotional history for trajectory scoring
client.store_memory("agent", StoreMemoryRequest{...}).await?RustAsync emotional observation storage with TTL
client.recall("agent", RecallRequest{...}).await?RustAsync history recall with min_importance filter
client.StoreMemory(ctx, "agent", StoreMemoryRequest{...})GoStore with sentiment metadata map
client.Recall(ctx, "agent", RecallRequest{...})GoRecall emotional history for scoring

Edge Cases and Gotchas

Warning: Never expose raw emotional data to users

Emotional memory is an internal tool for tone calibration — it should never be surfaced directly. Telling a user "I see you were frustrated 3 sessions ago" can feel surveillance-like and damage trust. Use emotional memory to inform your agent's tone without disclosing that tracking is happening. Always document this in your privacy policy.

Performance Considerations

~8ms
Emotional history recall (top_k=7)
7 days
Recommended TTL for emotional observations
91%
Escalation routing accuracy with trajectory scoring
Advanced Configuration: Multi-signal emotion fusion

For higher-accuracy emotion detection, fuse multiple signal sources rather than relying on keyword matching alone:

def fuse_emotion_signals(
    keyword_signal: EmotionSignal,
    message_stats: dict,     # {"avg_words_per_sentence": 4.2, "caps_ratio": 0.18}
    response_time_ms: int,   # time between agent response and user reply
    previous_emotion: EmotionSignal
) -> EmotionSignal:
    """Fuse multiple signals for higher-accuracy emotion detection."""

    score = keyword_signal.intensity

    # Short responses + rapid replies = frustration signal
    if message_stats["avg_words_per_sentence"] < 5 and response_time_ms < 8000:
        score = min(score + 0.15, 1.0)

    # High caps ratio amplifies negative signals
    if keyword_signal.sentiment == "negative":
        score = min(score + message_stats["caps_ratio"] * 0.3, 1.0)

    # Emotional continuity: slight persistence toward previous state
    score = score * 0.85 + previous_emotion.intensity * 0.15

    return EmotionSignal(
        sentiment=keyword_signal.sentiment,
        emotion=keyword_signal.emotion,
        intensity=round(score, 3)
    )
Try it live

Run this pattern against a real Dakera.

The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.