feat(companion): add personality, relationship, and mood engines

This commit is contained in:
2026-06-10 00:49:01 +03:30
parent 8d99c1856b
commit 147a51ecba
3 changed files with 307 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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(&current); 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,
)
}