- 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
165 lines
3.9 KiB
Go
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
|
|
}
|