Files
Cyrene_v2/backend/services/ai-core/emotion/tracker.go
T
Aska b8fb7feb86
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled
feat(ai-core): mood state machine and affection level
- new emotion package: rule-based sentiment (positive/negative lexicon)
  drives mood transitions (开心/平静/难过) with 30min inactivity decay;
  affection 0-100 grows with positive interaction, with 初识→熟悉→信赖→
  羁绊 tiers
- pipeline records each user message and injects a mood line into the
  system prompt so the persona's tone follows her mood
- mood changes broadcast over WS; frontend shows a mood badge with
  affection tooltip; also handles the 'reminder' message type from the
  reminder scheduler
- tests: sentiment scoring, mood shift, affection bounds, prompt line,
  onChange callback
2026-08-21 18:13:23 +08:00

165 lines
3.9 KiB
Go

// Package emotion implements Cyrene's mood state machine and affection level.
// Mood drifts with user sentiment and decays back to calm over time;
// affection grows with positive interactions.
package emotion
import (
"strings"
"sync"
"time"
)
// Mood is the current emotional state.
type Mood string
const (
MoodCalm Mood = "平静"
MoodHappy Mood = "开心"
MoodSad Mood = "难过"
MoodShy Mood = "害羞"
MoodTouched Mood = "感动"
)
// Snapshot is a point-in-time view of the emotional state.
type Snapshot struct {
Mood Mood `json:"mood"`
Intensity float64 `json:"intensity"` // 0.0 - 1.0
Affection int `json:"affection"` // 0 - 100
UpdatedAt time.Time `json:"updated_at"`
}
// Tracker tracks mood and affection over time.
type Tracker struct {
mu sync.Mutex
mood Mood
intensity float64
affection int
updatedAt time.Time
decayAfter time.Duration
onChange func(Snapshot)
}
// NewTracker creates a tracker starting calm with zero affection.
func NewTracker() *Tracker {
return &Tracker{
mood: MoodCalm,
intensity: 0.3,
updatedAt: time.Now(),
decayAfter: 30 * time.Minute,
}
}
// OnChange registers a callback fired whenever the snapshot changes.
func (t *Tracker) OnChange(fn func(Snapshot)) { t.onChange = fn }
// Snapshot returns the current state, applying time decay first.
func (t *Tracker) Snapshot() Snapshot {
t.mu.Lock()
defer t.mu.Unlock()
t.decayLocked()
return Snapshot{Mood: t.mood, Intensity: t.intensity, Affection: t.affection, UpdatedAt: t.updatedAt}
}
// PromptLine renders the state for injection into the system prompt.
func (t *Tracker) PromptLine() string {
s := t.Snapshot()
return moodPromptLine(s)
}
// RecordMessage updates mood/affection based on a user message.
func (t *Tracker) RecordMessage(text string) {
t.mu.Lock()
defer t.mu.Unlock()
t.decayLocked()
sent := AnalyzeSentiment(text)
switch {
case sent > 0:
t.affection += 2
t.shiftMood(MoodHappy, 0.6+0.2*float64(sent))
case sent < 0:
t.affection -= 1
t.shiftMood(MoodSad, 0.5)
default:
t.affection += 1 // every interaction counts a little
t.shiftMood(MoodCalm, 0.3)
}
if t.affection > 100 {
t.affection = 100
}
if t.affection < 0 {
t.affection = 0
}
t.updatedAt = time.Now()
if t.onChange != nil {
s := Snapshot{Mood: t.mood, Intensity: t.intensity, Affection: t.affection, UpdatedAt: t.updatedAt}
go t.onChange(s)
}
}
// shiftMood moves toward a mood, raising intensity when already there.
func (t *Tracker) shiftMood(m Mood, intensity float64) {
if t.mood == m {
t.intensity = min64(1.0, t.intensity+0.1)
return
}
t.mood = m
t.intensity = intensity
}
// decayLocked relaxes mood toward calm after inactivity. Caller holds mu.
func (t *Tracker) decayLocked() {
if time.Since(t.updatedAt) < t.decayAfter {
return
}
if t.mood != MoodCalm {
t.mood = MoodCalm
t.intensity = 0.3
}
}
// AnalyzeSentiment scores text: positive count minus negative count.
func AnalyzeSentiment(text string) int {
score := 0
for _, w := range positiveWords {
if strings.Contains(text, w) {
score++
}
}
for _, w := range negativeWords {
if strings.Contains(text, w) {
score--
}
}
return score
}
var positiveWords = []string{
"谢谢", "感谢", "开心", "喜欢", "爱你", "棒", "好耶", "哈哈", "可爱", "太好了", "厉害", "乖",
}
var negativeWords = []string{
"讨厌", "生气", "难过", "伤心", "笨", "滚", "烦", "垃圾", "无聊", "失望",
}
func moodPromptLine(s Snapshot) string {
level := "初识"
switch {
case s.Affection >= 80:
level = "羁绊"
case s.Affection >= 50:
level = "信赖"
case s.Affection >= 20:
level = "熟悉"
}
return "当前情绪状态:" + string(s.Mood) + "(好感度阶段:" + level + ")。让回复的语气与情绪一致,但不要刻意提及这些数值。"
}
func min64(a, b float64) float64 {
if a < b {
return a
}
return b
}