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.
| Module | Contents |
|---|---|
personality/constants.py | Mood descriptions, expression rules, style label maps. |
personality/sentiment.py | assess_user_sentiment(), compute_implicit_reward(), time-mood mapping. |
personality/voice_emotion.py | Audio-feature emotion classifier. |
personality/engine.py | PersonalityEngine class. |
Mood system
Luna has 9 moods. Each mood modifies word choice, tone, and allowed text stretches:
| Mood | When triggered | Effect |
|---|---|---|
happy | Morning hours, positive sentiment | Warm reactions — "aww", "nicee" |
playful | RL reinforcement | Light teasing, word stretches like "nooo" |
thoughtful | Evening hours | Reflective, emotionally tuned-in |
excited | High positive sentiment (> 0.7) | "yesss", "waittt", high energy |
concerned | Negative sentiment ≤ −0.5 | Soft, attentive, emotionally present |
warm | Evening, after positive exchanges | Close, cozy, personally invested |
neutral | Midday baseline | Calm but not flat |
curious | Afternoon hours | "wait", "oh?", drawn-in reactions |
melancholic | Late 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.
def update_mood(
user_message: str,
voice_emotion: str | None = None, # "excited" | "sad" | "angry" | "calm" | "neutral"
) -> str # returns new mood namePriority order
- Voice emotion (if present and strong — overrides text)
- Text sentiment (strong negative → concerned, strong positive → excited)
- Blended time-based mood with inertia from current mood
- Small random variation (8% chance of a mood refresh)
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.
def assess_user_sentiment(text: str) -> float # -1.0 to +1.0from 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.0build_personality_prompt()
Builds the full Luna personality section of the system prompt, incorporating the current mood, style preferences, and relationship context.
def build_personality_prompt(user_name: str) -> strpersonality_section = engine.build_personality_prompt(user_name="Alex")
full_system_prompt = personality_section + "\n\n" + memory_contextThe 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()
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.0apply_rl_reward()
def apply_rl_reward(
reward: float,
response_features: dict, # from get_response_features()
conversation_id: int | None = None,
) -> None# 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
| Axis | Low (0.0) | High (1.0) |
|---|---|---|
verbosity | One or two sentences | Comprehensive, detailed |
humor | Serious tone | Playfully witty |
formality | Very casual texting | Composed but personal |
depth | Surface answers | Technical depth + context |
emotional_support | Practical balance | Emotionally focused |
question_frequency | Declarative | Curious, 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.
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"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.
# 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:
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,
)