feat(companion): add personality, relationship, and mood engines
This commit is contained in:
79
internal/companion/mood.go
Normal file
79
internal/companion/mood.go
Normal 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
|
||||||
|
}
|
||||||
107
internal/companion/personality.go
Normal file
107
internal/companion/personality.go
Normal 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
|
||||||
|
}
|
||||||
121
internal/companion/relationship.go
Normal file
121
internal/companion/relationship.go
Normal 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(¤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,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user