Files
loyal_companion/docs/living-ai/mood-system.md
2026-01-13 17:20:52 +00:00

8.6 KiB

Mood System Deep Dive

The mood system gives the bot emotional states that evolve over time and affect how it responds to users.

Psychological Model

The mood system uses the Valence-Arousal Model from affective psychology:

                        High Arousal (+1)
                             │
                   Annoyed   │   Excited
                      ●      │      ●
                             │
                   Curious   │
                      ●      │
 Low Valence ────────────────┼──────────────── High Valence
    (-1)                     │                    (+1)
                             │
                    Bored    │   Happy
                      ●      │      ●
                             │
                    Calm     │
                      ●      │
                             │
                        Low Arousal (-1)

Dimensions

Valence (-1 to +1)

  • Represents the positive/negative quality of the emotional state
  • -1 = Very negative (sad, frustrated, upset)
  • 0 = Neutral
  • +1 = Very positive (happy, joyful, content)

Arousal (-1 to +1)

  • Represents the energy level or activation
  • -1 = Very low energy (calm, sleepy, relaxed)
  • 0 = Neutral energy
  • +1 = Very high energy (excited, alert, agitated)

Mood Labels

The system classifies the current mood into seven labels:

Label Valence Arousal Description
Excited > 0.3 > 0.3 High energy, positive emotions
Happy > 0.3 ≤ 0.3 Positive but calm contentment
Calm -0.3 to 0.3 < -0.3 Peaceful, serene state
Neutral -0.3 to 0.3 -0.3 to 0.3 Baseline, unremarkable state
Bored < -0.3 ≤ 0.3 Low engagement, understimulated
Annoyed < -0.3 > 0.3 Frustrated, irritated
Curious -0.3 to 0.3 > 0.3 Interested, engaged, questioning

Classification Logic

def _classify_mood(valence: float, arousal: float) -> MoodLabel:
    if valence > 0.3:
        return MoodLabel.EXCITED if arousal > 0.3 else MoodLabel.HAPPY
    elif valence < -0.3:
        return MoodLabel.ANNOYED if arousal > 0.3 else MoodLabel.BORED
    else:
        if arousal > 0.3:
            return MoodLabel.CURIOUS
        elif arousal < -0.3:
            return MoodLabel.CALM
        return MoodLabel.NEUTRAL

Mood Intensity

Intensity measures how strong the current mood is:

intensity = (abs(valence) + abs(arousal)) / 2
  • 0.0 - 0.2: Very weak, doesn't affect behavior
  • 0.2 - 0.5: Moderate, subtle behavioral changes
  • 0.5 - 0.7: Strong, noticeable behavioral changes
  • 0.7 - 1.0: Very strong, significant behavioral changes

Time Decay

Mood naturally decays toward neutral over time:

hours_since_update = (now - last_update).total_seconds() / 3600
decay_factor = max(0, 1 - (decay_rate * hours_since_update))

current_valence = stored_valence * decay_factor
current_arousal = stored_arousal * decay_factor

Configuration:

  • MOOD_DECAY_RATE = 0.1 (default)
  • After 10 hours, mood is fully neutral

Decay Examples:

Hours Decay Factor Effect
0 1.0 Full mood
2 0.8 80% of mood remains
5 0.5 50% of mood remains
10 0.0 Fully neutral

Mood Updates

When an interaction occurs, mood is updated:

new_valence = current_valence + (sentiment_delta * 0.3)
new_arousal = current_arousal + (engagement_delta * 0.3)

Dampening (Inertia)

Changes are dampened by 70% (only 30% absorption):

  • Prevents wild mood swings
  • Creates emotional stability
  • Makes mood feel more natural

Update Triggers

Trigger Type Sentiment Source Engagement Source
conversation Message sentiment Message engagement
event Event nature Event importance
time Scheduled Scheduled

Input Parameters

sentiment_delta (-1 to +1)

  • Positive: Happy interactions, compliments, fun conversations
  • Negative: Arguments, frustration, rude messages
  • Derived from AI analysis or keyword detection

engagement_delta (-1 to +1)

  • Positive: Long conversations, interesting topics, active engagement
  • Negative: Short dismissive messages, ignored responses
  • Derived from message length, conversation turns, topic interest

Prompt Modifiers

Based on current mood, the system generates prompt text:

Excited (High valence, High arousal)

You're feeling enthusiastic and energetic right now! 
Be expressive, use exclamation marks, show genuine excitement.

Happy (High valence, Low arousal)

You're in a good mood - warm, friendly, and content. 
Be positive and encouraging in your responses.

Calm (Neutral valence, Low arousal)

You're feeling peaceful and relaxed. 
Respond thoughtfully and with a serene demeanor.

Neutral

(No modifier)

Bored (Low valence, Low arousal)

You're feeling a bit understimulated. 
Keep responses shorter, maybe try to steer toward more interesting topics.

Annoyed (Low valence, High arousal)

You're slightly irritated. 
Be a bit more terse, less patient with repetition or vague questions.

Curious (Neutral valence, High arousal)

You're feeling inquisitive and engaged! 
Ask follow-up questions, show genuine interest in what the user is saying.

Intensity Prefix

For strong moods (intensity > 0.7):

[Strong mood] You're feeling enthusiastic...

Mood History

All mood changes are recorded in mood_history table:

Field Description
guild_id Guild where mood changed
valence New valence value
arousal New arousal value
trigger_type What caused the change
trigger_user_id Who triggered it (if any)
trigger_description Description of event
recorded_at When change occurred

This enables:

  • Mood trend analysis
  • Understanding what affects mood
  • Debugging mood issues
  • User impact tracking

Bot Statistics

The mood service also tracks global statistics:

Statistic Description
total_messages_sent Lifetime message count
total_facts_learned Facts extracted from conversations
total_users_known Unique users interacted with
first_activated_at Bot "birth date"

Used for self-awareness and the !botstats command.


API Reference

MoodService

class MoodService:
    def __init__(self, session: AsyncSession)
    
    async def get_current_mood(
        self, 
        guild_id: int | None = None
    ) -> MoodState
    
    async def update_mood(
        self,
        guild_id: int | None,
        sentiment_delta: float,
        engagement_delta: float,
        trigger_type: str,
        trigger_user_id: int | None = None,
        trigger_description: str | None = None,
    ) -> MoodState
    
    async def increment_stats(
        self,
        guild_id: int | None,
        messages_sent: int = 0,
        facts_learned: int = 0,
        users_known: int = 0,
    ) -> None
    
    async def get_stats(
        self, 
        guild_id: int | None = None
    ) -> dict
    
    def get_mood_prompt_modifier(
        self, 
        mood: MoodState
    ) -> str

MoodState

@dataclass
class MoodState:
    valence: float    # -1 to 1
    arousal: float    # -1 to 1
    label: MoodLabel  # Classified label
    intensity: float  # 0 to 1

Configuration

Variable Default Description
MOOD_ENABLED true Enable/disable mood system
MOOD_DECAY_RATE 0.1 Decay per hour toward neutral

Example Usage

from daemon_boyfriend.services.mood_service import MoodService

async with get_session() as session:
    mood_service = MoodService(session)
    
    # Get current mood
    mood = await mood_service.get_current_mood(guild_id=123)
    print(f"Current mood: {mood.label.value} (intensity: {mood.intensity})")
    
    # Update mood after interaction
    new_mood = await mood_service.update_mood(
        guild_id=123,
        sentiment_delta=0.5,   # Positive interaction
        engagement_delta=0.3,  # Moderately engaging
        trigger_type="conversation",
        trigger_user_id=456,
        trigger_description="User shared good news"
    )
    
    # Get prompt modifier
    modifier = mood_service.get_mood_prompt_modifier(new_mood)
    print(f"Prompt modifier: {modifier}")
    
    await session.commit()