From 147a51ecba0fdb21d45034b202df7df053686b34 Mon Sep 17 00:00:00 2001 From: db123-ai Date: Wed, 10 Jun 2026 00:49:01 +0330 Subject: [PATCH] feat(companion): add personality, relationship, and mood engines --- internal/companion/mood.go | 79 +++++++++++++++++++ internal/companion/personality.go | 107 +++++++++++++++++++++++++ internal/companion/relationship.go | 121 +++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 internal/companion/mood.go create mode 100644 internal/companion/personality.go create mode 100644 internal/companion/relationship.go diff --git a/internal/companion/mood.go b/internal/companion/mood.go new file mode 100644 index 0000000..11fe48c --- /dev/null +++ b/internal/companion/mood.go @@ -0,0 +1,79 @@ +package companion + +import ( + "math/rand" + "sync" + "time" + + "git.db123.ir/db123/divacode/pkg/types" +) + +type MoodEngine struct { + mu sync.Mutex + current types.Mood + updatedAt time.Time + energy float64 +} + +func NewMoodEngine() *MoodEngine { + return &MoodEngine{ + current: types.MoodNeutral, + updatedAt: time.Now(), + energy: 1.0, + } +} + +func (m *MoodEngine) Current() types.Mood { + m.mu.Lock() + defer m.mu.Unlock() + return m.current +} + +func (m *MoodEngine) Set(mood types.Mood) { + m.mu.Lock() + defer m.mu.Unlock() + m.current = mood + m.updatedAt = time.Now() +} + +func (m *MoodEngine) DeriveFromInteraction(success bool, topicIntensity float64) types.Mood { + m.mu.Lock() + defer m.mu.Unlock() + + m.energy -= 0.05 + if m.energy < 0 { + m.energy = 0 + } + + if !success { + m.current = types.MoodAnnoyed + } else if topicIntensity > 0.7 { + if rand.Float64() > 0.5 { + m.current = types.MoodCurious + } else { + m.current = types.MoodPlayful + } + } else if m.energy < 0.3 { + m.current = types.MoodTired + } else { + m.current = types.MoodNeutral + } + + m.updatedAt = time.Now() + return m.current +} + +func (m *MoodEngine) Recharge() { + m.mu.Lock() + defer m.mu.Unlock() + m.energy += 0.1 + if m.energy > 1.0 { + m.energy = 1.0 + } +} + +func (m *MoodEngine) Energy() float64 { + m.mu.Lock() + defer m.mu.Unlock() + return m.energy +} diff --git a/internal/companion/personality.go b/internal/companion/personality.go new file mode 100644 index 0000000..c8dc936 --- /dev/null +++ b/internal/companion/personality.go @@ -0,0 +1,107 @@ +package companion + +import ( + "fmt" + "log/slog" + "sync" + + "git.db123.ir/db123/divacode/internal/storage" + "git.db123.ir/db123/divacode/pkg/personality" +) + +type PersonalityEngine struct { + mu sync.Mutex + db *storage.DB + def map[personality.Trait]personality.TraitValue +} + +func NewPersonalityEngine(db *storage.DB) *PersonalityEngine { + e := &PersonalityEngine{ + db: db, + def: map[personality.Trait]personality.TraitValue{ + personality.TraitHumor: {Trait: personality.TraitHumor, Value: 0.5, MinValue: 0.3, MaxValue: 0.7}, + personality.TraitCuriosity: {Trait: personality.TraitCuriosity, Value: 0.8, MinValue: 0.7, MaxValue: 0.9}, + personality.TraitPlayfulness: {Trait: personality.TraitPlayfulness, Value: 0.5, MinValue: 0.3, MaxValue: 0.7}, + personality.TraitFormality: {Trait: personality.TraitFormality, Value: 0.3, MinValue: 0.2, MaxValue: 0.5}, + personality.TraitAffection: {Trait: personality.TraitAffection, Value: 0.5, MinValue: 0.3, MaxValue: 0.7}, + }, + } + e.seed() + return e +} + +func (e *PersonalityEngine) seed() { + for _, tv := range e.def { + err := e.db.Exec( + "INSERT OR IGNORE INTO personality_traits (id, trait, value, min_value, max_value) VALUES (?, ?, ?, ?, ?)", + fmt.Sprintf("trait_%s", tv.Trait), string(tv.Trait), tv.Value, tv.MinValue, tv.MaxValue, + ) + if err != nil { + slog.Warn("seed trait failed", "trait", tv.Trait, "error", err) + } + } +} + +func (e *PersonalityEngine) Current() []personality.TraitValue { + e.mu.Lock() + defer e.mu.Unlock() + rows, err := e.db.Query("SELECT trait, value, min_value, max_value, updated_at FROM personality_traits") + if err != nil { + slog.Warn("read traits failed", "error", err) + return e.defaults() + } + defer rows.Close() + + var traits []personality.TraitValue + for rows.Next() { + var tv personality.TraitValue + var t, updated string + if err := rows.Scan(&t, &tv.Value, &tv.MinValue, &tv.MaxValue, &updated); err != nil { + continue + } + tv.Trait = personality.Trait(t) + traits = append(traits, tv) + } + if len(traits) == 0 { + return e.defaults() + } + return traits +} + +func (e *PersonalityEngine) Update(trait personality.Trait, delta float64) error { + e.mu.Lock() + defer e.mu.Unlock() + + current := e.get(trait) + newVal := current.Value + delta + if newVal < current.MinValue { + newVal = current.MinValue + } + if newVal > current.MaxValue { + newVal = current.MaxValue + } + + return e.db.Exec( + "UPDATE personality_traits SET value = ?, updated_at = datetime('now') WHERE trait = ?", + newVal, string(trait), + ) +} + +func (e *PersonalityEngine) get(trait personality.Trait) personality.TraitValue { + row := e.db.QueryRow("SELECT trait, value, min_value, max_value FROM personality_traits WHERE trait = ?", string(trait)) + var tv personality.TraitValue + var t string + if err := row.Scan(&t, &tv.Value, &tv.MinValue, &tv.MaxValue); err != nil { + return e.def[trait] + } + tv.Trait = personality.Trait(t) + return tv +} + +func (e *PersonalityEngine) defaults() []personality.TraitValue { + var out []personality.TraitValue + for _, tv := range e.def { + out = append(out, tv) + } + return out +} diff --git a/internal/companion/relationship.go b/internal/companion/relationship.go new file mode 100644 index 0000000..15780b3 --- /dev/null +++ b/internal/companion/relationship.go @@ -0,0 +1,121 @@ +package companion + +import ( + "log/slog" + "sync" + + "git.db123.ir/db123/divacode/internal/storage" + "git.db123.ir/db123/divacode/pkg/personality" +) + +type RelationshipEngine struct { + mu sync.Mutex + db *storage.DB +} + +func NewRelationshipEngine(db *storage.DB) *RelationshipEngine { + e := &RelationshipEngine{db: db} + e.seed() + return e +} + +func (e *RelationshipEngine) seed() { + defaults := map[string]float64{ + "familiarity": 0.0, + "trust": 0.3, + } + for metric, val := range defaults { + err := e.db.Exec( + "INSERT OR IGNORE INTO relationship_metrics (id, metric, value) VALUES (?, ?, ?)", + "rel_"+metric, metric, val, + ) + if err != nil { + slog.Warn("seed metric failed", "metric", metric, "error", err) + } + } +} + +func (e *RelationshipEngine) Current() *personality.RelationshipMetrics { + e.mu.Lock() + defer e.mu.Unlock() + + rm := &personality.RelationshipMetrics{} + + row := e.db.QueryRow("SELECT value FROM relationship_metrics WHERE metric = 'familiarity'") + row.Scan(&rm.Familiarity) + + row = e.db.QueryRow("SELECT value FROM relationship_metrics WHERE metric = 'trust'") + row.Scan(&rm.Trust) + + topics, _ := e.db.Query("SELECT topic FROM shared_topics ORDER BY count DESC LIMIT 20") + if topics != nil { + defer topics.Close() + for topics.Next() { + var t string + topics.Scan(&t) + rm.SharedTopics = append(rm.SharedTopics, t) + } + } + + jokes, _ := e.db.Query("SELECT joke FROM inside_jokes LIMIT 20") + if jokes != nil { + defer jokes.Close() + for jokes.Next() { + var j string + jokes.Scan(&j) + rm.InsideJokes = append(rm.InsideJokes, j) + } + } + + return rm +} + +func (e *RelationshipEngine) Trust() float64 { + var v float64 + err := e.db.QueryRow("SELECT value FROM relationship_metrics WHERE metric = 'trust'").Scan(&v) + if err != nil { + return 0.3 + } + return v +} + +func (e *RelationshipEngine) UpdateFamiliarity(delta float64) error { + return e.adjust("familiarity", delta) +} + +func (e *RelationshipEngine) UpdateTrust(delta float64) error { + return e.adjust("trust", delta) +} + +func (e *RelationshipEngine) AddSharedTopic(topic string) error { + return e.db.Exec( + `INSERT INTO shared_topics (id, topic, count, last_discussed) + VALUES (?, ?, 1, datetime('now')) + ON CONFLICT(topic) DO UPDATE SET count = count + 1, last_discussed = datetime('now')`, + "topic_"+topic, topic, + ) +} + +func (e *RelationshipEngine) adjust(metric string, delta float64) error { + e.mu.Lock() + defer e.mu.Unlock() + + var current float64 + row := e.db.QueryRow("SELECT value FROM relationship_metrics WHERE metric = ?", metric) + if err := row.Scan(¤t); err != nil { + current = 0 + } + + newVal := current + delta + if newVal < 0 { + newVal = 0 + } + if newVal > 1 { + newVal = 1 + } + + return e.db.Exec( + "UPDATE relationship_metrics SET value = ?, updated_at = datetime('now') WHERE metric = ?", + newVal, metric, + ) +}