NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee 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.

Workflow from an emotional signal to the reply's tone. Your agent detects emotion in the user's message from keywords, punctuation and self-report, and stores an episodic observation in Dakera with store_memory, with sentiment metadata, a 7-day TTL and an importance set by intensity. It recalls the recent emotional history with top_k 7, orders it newest first and computes a recency-weighted average trajectory, which becomes a tone note in the system prompt. Sustained distress escalates instead: a human specialist takes over and the escalation is stored as a memory at importance 0.95.Workflow from an emotional signal to the reply's tone. Your agent detects emotion in the user's message from keywords, punctuation and self-report, and stores an episodic observation in Dakera with store_memory, with sentiment metadata, a 7-day TTL and an importance set by intensity. It recalls the recent emotional history with top_k 7, orders it newest first and computes a recency-weighted average trajectory, which becomes a tone note in the system prompt. Sustained distress escalates instead: a human specialist takes over and the escalation is stored as a memory at importance 0.95.
Each message adds an observation; the trajectory over recent ones sets the tone.Full size ↗Full size ↗
State diagram of the tone an agent takes as the emotional trajectory changes. From Neutral, with a professional and helpful tone, a score below -0.4 moves to Negative trend (validation first, solution second) and a score above 0.4 moves to Positive trend (match their energy, offer more help); each returns to Neutral when the score moves back. A negative trend below -0.65, with the 3 or more most recent observations negative, escalates to a human specialist, and the escalation event is stored at importance 0.95.State diagram of the tone an agent takes as the emotional trajectory changes. From Neutral, with a professional and helpful tone, a score below -0.4 moves to Negative trend (validation first, solution second) and a score above 0.4 moves to Positive trend (match their energy, offer more help); each returns to Neutral when the score moves back. A negative trend below -0.65, with the 3 or more most recent observations negative, escalates to a human specialist, and the escalation event is stored at importance 0.95.
The trajectory score picks the tone, and sustained distress hands the user to a person.Full size ↗Full size ↗

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 -X POST http://localhost:3000/v1/memory/recall \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "support-agent-user-42", "query": "user emotional state frustration distress", "top_k": 7, "min_importance": 0.4}'

# Store an escalation event as a persistent memory (no TTL)
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'}"],
        metadata={  # read back by get_emotional_trajectory
            "sentiment": signal.sentiment,
            "emotion": signal.emotion,
            "intensity": signal.intensity,
            "trigger": signal.trigger,
        },
        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
    )

    # Recall ranks by relevance; order by creation time for the trajectory
    memories = sorted(result.memories, key=lambda m: float(m.created_at or 0), reverse=True)
    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)  # most recent observation gets weight 1.0
        meta = mem.metadata or {}
        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 observations (newest first) for the escalation trigger
    consecutive_neg = 0
    for mem in memories:
        if (mem.metadata or {}).get("sentiment") == "negative":
            consecutive_neg += 1
        else:
            break

    return {
        "trajectory_score": round(trajectory, 3),
        "dominant_emotion": (memories[0].metadata or {}).get("emotion", "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}\n\n{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 observations: {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,
    memory_type: 'episodic',
    tags: [`emotion:${signal.emotion}`, `sentiment:${signal.sentiment}`],
    metadata: { sentiment: signal.sentiment, emotion: signal.emotion, intensity: signal.intensity, trigger: signal.trigger },
    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 });
  // Recall ranks by relevance; order by creation time for the trajectory (newest first)
  const memories = [...result.memories].sort((a, b) => Number(b.created_at ?? 0) - Number(a.created_at ?? 0));

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

  for (let i = 0; i < memories.length; i++) {
    const weight = 1 / (i + 1);
    const sentiment = (memories[i].metadata?.sentiment as Sentiment) ?? 'neutral';
    const intensity = (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 memories) {
    if (mem.metadata?.sentiment === 'negative') consecutiveNegative++;
    else break;
  }

  return {
    trajectoryScore: Math.round(trajectoryScore * 1000) / 1000,
    dominantEmotion: (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_client::{DakeraClient, MemoryType, RecallRequest, StoreMemoryRequest};
use serde_json::json;

let client = DakeraClient::builder("http://localhost:3000").api_key("dk-...").build()?;
let agent_id = "support-agent-user-42";

// Store a negative emotional observation (sentiment metadata is read back below)
client.store_memory(
    StoreMemoryRequest::new(agent_id, "Emotional observation: User expressed sustained frustration about recurring billing issue. Emotion: frustrated. Intensity: 0.82.")
        .with_type(MemoryType::Episodic)
        .with_importance(0.88)
        .with_tags(vec!["emotion:frustrated".into(), "sentiment:negative".into()])
        .with_metadata(json!({ "sentiment": "negative", "emotion": "frustrated", "intensity": 0.82 }))
        .with_ttl(604_800), // 7 days
).await?;

// Recall emotional history, newest first
let history = client.recall(
    RecallRequest::new(agent_id, "user emotional state frustration distress satisfaction")
        .with_top_k(7)
        .with_min_importance(0.4),
).await?;
let mut memories = history.memories;
memories.sort_by(|a, b| b.created_at.cmp(&a.created_at));

// Recency-weighted trajectory score
let mut weighted_score = 0.0f64;
let mut total_weight = 0.0f64;
for (i, mem) in 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 consecutive_negative = memories.iter()
    .take_while(|m| m.metadata.as_ref().and_then(|v| v.get("sentiment")).and_then(|v| v.as_str()) == Some("negative"))
    .count();
let escalate = trajectory < -0.65 && consecutive_negative >= 3;
println!("Trajectory score: {:.3}, escalate: {}", trajectory, escalate);
client := dakera.NewClientWithOptions(dakera.ClientOptions{
    BaseURL: "http://localhost:3000",
    APIKey:  "dk-...",
})
ctx := context.Background()
agentID := "support-agent-user-42"

// Store an emotional observation (sentiment metadata is read back below)
importance, ttl := float32(0.88), 604_800 // 7 days
client.StoreMemory(ctx, agentID, dakera.StoreMemoryRequest{
    Content:    "Emotional observation: User frustrated about recurring billing issue. Intensity: 0.82.",
    Importance: &importance,
    MemoryType: "episodic",
    Tags:       []string{"emotion:frustrated", "sentiment:negative"},
    TTLSeconds: &ttl,
    Metadata: map[string]interface{}{
        "sentiment": "negative",
        "emotion":   "frustrated",
        "intensity": 0.82,
    },
})

// Recall emotional history, newest first
minImp := float32(0.4)
result, err := client.Recall(ctx, agentID, dakera.RecallRequest{
    Query:         "user emotional state frustration distress",
    TopK:          7,
    MinImportance: &minImp,
})
if err != nil {
    log.Fatal(err)
}
memories := result.Memories
sort.Slice(memories, func(i, j int) bool { return memories[i].CreatedAt > memories[j].CreatedAt })

// Recency-weighted trajectory score
sentimentMap := map[string]float64{"positive": 1.0, "neutral": 0.0, "negative": -1.0}
var weightedScore, totalWeight float64
consecutiveNegative, streak := 0, true
for i, mem := range memories {
    weight := 1.0 / float64(i+1)
    sentiment, _ := mem.Metadata["sentiment"].(string)
    intensity, ok := mem.Metadata["intensity"].(float64)
    if !ok {
        intensity = 0.5
    }
    weightedScore += sentimentMap[sentiment] * intensity * weight
    totalWeight += weight
    if streak && sentiment == "negative" {
        consecutiveNegative++
    } else {
        streak = false
    }
}

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

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, memory_type, tags, metadata, 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(StoreMemoryRequest::new(agent_id, content).with_ttl(604_800)).await?RustAsync emotional observation storage with TTL
client.recall(RecallRequest::new(agent_id, query).with_min_importance(0.4)).await?RustAsync history recall with min_importance filter
client.StoreMemory(ctx, agentID, dakera.StoreMemoryRequest{...})GoStore with sentiment metadata map
client.Recall(ctx, agentID, dakera.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

7
Observations recalled for the trajectory (top_k)
7 days
Recommended TTL for emotional observations
-0.65
Trajectory score below which the code escalates
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),
        trigger=keyword_signal.trigger
    )
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.