Personality Engine

Mood state management, RL-style response preference learning, sentiment analysis, and personality-aware prompt building.

Overview

The personality engine gives Luna adaptive, human-feeling responses by tracking three things simultaneously:

  • Mood state — which of 9 moods Luna is currently in, driven by user sentiment and time of day.
  • Style preferences — float values (0–1) for verbosity, humor, formality, depth, emotional support, and question frequency that drift toward what the user responds well to.
  • Implicit RL — every response earns a reward signal; the engine updates style preferences toward patterns the user liked.
ModuleContents
personality/constants.pyMood descriptions, expression rules, style label maps.
personality/sentiment.pyassess_user_sentiment(), compute_implicit_reward(), time-mood mapping.
personality/voice_emotion.pyAudio-feature emotion classifier.
personality/engine.pyPersonalityEngine class.

Mood system

Luna has 9 moods. Each mood modifies word choice, tone, and allowed text stretches:

MoodWhen triggeredEffect
happyMorning hours, positive sentimentWarm reactions — "aww", "nicee"
playfulRL reinforcementLight teasing, word stretches like "nooo"
thoughtfulEvening hoursReflective, emotionally tuned-in
excitedHigh positive sentiment (> 0.7)"yesss", "waittt", high energy
concernedNegative sentiment ≤ −0.5Soft, attentive, emotionally present
warmEvening, after positive exchangesClose, cozy, personally invested
neutralMidday baselineCalm but not flat
curiousAfternoon hours"wait", "oh?", drawn-in reactions
melancholicLate night (≥ 23 h)Softer pacing, quieter energy

update_mood()

Call this with the user's latest message (and optionally a voice emotion) to update Luna's mood state. Returns the new mood string.

signature
def update_mood(
    user_message: str,
    voice_emotion: str | None = None,  # "excited" | "sad" | "angry" | "calm" | "neutral"
) -> str  # returns new mood name

Priority order

  1. Voice emotion (if present and strong — overrides text)
  2. Text sentiment (strong negative → concerned, strong positive → excited)
  3. Blended time-based mood with inertia from current mood
  4. Small random variation (8% chance of a mood refresh)
example.py
from backend.services.personality import PersonalityEngine
from backend.models.database import SessionLocal

db = SessionLocal()
engine = PersonalityEngine(db)

mood = engine.update_mood("I'm so frustrated with this bug")
print(mood)  # "concerned"

mood = engine.update_mood("Finally fixed it, amazing!", voice_emotion="excited")
print(mood)  # "excited"
db.close()

assess_user_sentiment()

Returns a sentiment score from −1.0 (very negative) to +1.0 (very positive). Handles negation ("not happy" → negative), intensifiers ("extremely glad" → stronger), and word boundaries.

signature
def assess_user_sentiment(text: str) -> float  # -1.0 to +1.0
example.py
from backend.services.personality import assess_user_sentiment

print(assess_user_sentiment("I love this so much"))      # 0.67
print(assess_user_sentiment("I'm not happy about this")) # -0.33
print(assess_user_sentiment("It's fine I guess"))        # 0.0

build_personality_prompt()

Builds the full Luna personality section of the system prompt, incorporating the current mood, style preferences, and relationship context.

signature
def build_personality_prompt(user_name: str) -> str
example.py
personality_section = engine.build_personality_prompt(user_name="Alex")
full_system_prompt = personality_section + "\n\n" + memory_context

The generated prompt includes:

  • Luna's core conversational rules (tone, length, emotional expression)
  • Current mood and its expression guidelines
  • Adapted style: verbosity, humor, formality, question frequency
  • Relationship context (total interaction count)

RL reward + style learning

After each exchange, the chat pipeline computes an implicit reward signal from the user's follow-up message and calls apply_rl_reward()to nudge style preferences.

compute_implicit_reward()

signature
def compute_implicit_reward(
    luna_response: str,
    user_next_msg: str,
    tool_succeeded: bool = False,
    task_completed: bool = False,
    user_interrupted_tts: bool = False,
    is_repeat_request: bool = False,
    manual_correction: bool = False,
) -> float  # -1.0 to +1.0

apply_rl_reward()

signature
def apply_rl_reward(
    reward: float,
    response_features: dict,   # from get_response_features()
    conversation_id: int | None = None,
) -> None
example.py
# After Luna responds and the user replies:
reward = compute_implicit_reward(
    luna_response=luna_text,
    user_next_msg=user_follow_up,
    tool_succeeded=True,
)
features = engine.get_response_features(luna_text)
engine.apply_rl_reward(reward, features, conversation_id=conv_id)

Style preference axes

AxisLow (0.0)High (1.0)
verbosityOne or two sentencesComprehensive, detailed
humorSerious tonePlayfully witty
formalityVery casual textingComposed but personal
depthSurface answersTechnical depth + context
emotional_supportPractical balanceEmotionally focused
question_frequencyDeclarativeCurious, dialogue-driven

Voice emotion classification

If the voice pipeline provides audio features, classify_voice_emotion()maps them to an emotion label which then overrides text-derived mood.

signature
def classify_voice_emotion(
    pitch_hz: float | None,        # fundamental frequency
    energy_rms: float | None,      # audio energy 0–1000
    speech_rate_wpm: float | None, # words per minute
    pause_ratio: float | None,     # fraction of time silent
) -> str  # "neutral" | "excited" | "sad" | "angry" | "calm"
example.py
from backend.services.personality import classify_voice_emotion

emotion = classify_voice_emotion(
    pitch_hz=230,     # high pitch
    energy_rms=900,   # loud
    speech_rate_wpm=180,  # fast
    pause_ratio=0.10,
)
print(emotion)  # "excited"
mood = engine.update_mood(user_message, voice_emotion=emotion)

daily_decay()

Slowly pulls all style preferences back toward neutral (0.5) each day. Prevents the personality from drifting too far toward any extreme based on a short run of similar exchanges. Called automatically by the scheduler at midnight.

example.py
# Called automatically — but you can trigger it manually
engine.daily_decay()

Constants and label maps

Import directly from the constants module for your own prompt builders:

example.py
from backend.services.personality import (
    MOODS,            # dict: mood_name → description string
    MOOD_EXPRESSION,  # dict: mood_name → expression guideline
    VERBOSITY_LABELS, # dict: (lo, hi) tuple → label string
    HUMOR_LABELS,
    FORMALITY_LABELS,
)