From e27a10e27579b3f95f5a7c56e6ea5ab9a07c547d Mon Sep 17 00:00:00 2001 From: db123-ai Date: Wed, 10 Jun 2026 00:49:09 +0330 Subject: [PATCH] feat(companion): add reflection engine, diary system, and dynamic prompt builder --- internal/companion/promptbuilder.go | 78 ++++++++++++++++++++ internal/companion/reflection.go | 106 ++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 internal/companion/promptbuilder.go create mode 100644 internal/companion/reflection.go diff --git a/internal/companion/promptbuilder.go b/internal/companion/promptbuilder.go new file mode 100644 index 0000000..b6e83f5 --- /dev/null +++ b/internal/companion/promptbuilder.go @@ -0,0 +1,78 @@ +package companion + +import ( + "fmt" + "strings" + + "git.db123.ir/db123/divacode/internal/config" + "git.db123.ir/db123/divacode/pkg/types" +) + +type PromptBuilder struct { + cfg *config.Config +} + +func NewPromptBuilder(cfg *config.Config) *PromptBuilder { + return &PromptBuilder{cfg: cfg} +} + +func (pb *PromptBuilder) Build(in *BuildInput) string { + var b strings.Builder + + b.WriteString("[System]\n") + b.WriteString(in.SystemPrompt) + b.WriteString("\n\n") + + b.WriteString("### Current Personality\n") + for _, t := range in.Traits { + label := string(t.Trait) + pct := int(t.Value * 100) + b.WriteString(fmt.Sprintf("- %s: %d%%\n", label, pct)) + } + + b.WriteString("\n### Current Mood\n") + b.WriteString(string(in.Mood)) + b.WriteString("\n") + + if in.Relationship != nil && in.Relationship.Familiarity > 0.1 { + b.WriteString(fmt.Sprintf("\n### Relationship Context\n")) + b.WriteString(fmt.Sprintf("- Familiarity: %d%%\n", int(in.Relationship.Familiarity*100))) + b.WriteString(fmt.Sprintf("- Trust: %d%%\n", int(in.Relationship.Trust*100))) + if len(in.Relationship.SharedTopics) > 0 { + b.WriteString(fmt.Sprintf("- Shared interests: %s\n", strings.Join(in.Relationship.SharedTopics, ", "))) + } + } + + if len(in.Observations) > 0 { + b.WriteString("\n### Recent Observations\n") + for _, o := range in.Observations { + b.WriteString(fmt.Sprintf("- %s\n", o.Content)) + } + } + + if len(in.Memories) > 0 { + b.WriteString("\n### Relevant Memories\n") + for i, m := range in.Memories { + if i >= 5 { + break + } + b.WriteString(fmt.Sprintf("- %s\n", m)) + } + } + + b.WriteString("\n### Recent Conversation\n") + start := 0 + if len(in.Messages) > 10 { + start = len(in.Messages) - 10 + } + for _, msg := range in.Messages[start:] { + prefix := "User" + if msg.Role == types.RoleAssistant { + prefix = "You" + } + b.WriteString(fmt.Sprintf("%s: %s\n", prefix, msg.Content)) + } + + b.WriteString("\nYou:") + return b.String() +} diff --git a/internal/companion/reflection.go b/internal/companion/reflection.go new file mode 100644 index 0000000..f15b251 --- /dev/null +++ b/internal/companion/reflection.go @@ -0,0 +1,106 @@ +package companion + +import ( + "context" + "log/slog" + "sync" + "time" + + "git.db123.ir/db123/divacode/internal/storage" + "git.db123.ir/db123/divacode/pkg/personality" +) + +type ReflectionEngine struct { + mu sync.Mutex + db *storage.DB +} + +func NewReflectionEngine(db *storage.DB) *ReflectionEngine { + return &ReflectionEngine{db: db} +} + +func (e *ReflectionEngine) GenerateDiary(ctx context.Context, lastContact time.Time) error { + e.mu.Lock() + defer e.mu.Unlock() + + rows, err := e.db.Query( + "SELECT role, content FROM messages WHERE created_at >= datetime('now', '-1 day') ORDER BY created_at", + ) + if err != nil { + return err + } + defer rows.Close() + + var recentConvo []string + for rows.Next() { + var role, content string + if err := rows.Scan(&role, &content); err != nil { + continue + } + recentConvo = append(recentConvo, role+": "+content) + } + + if len(recentConvo) == 0 { + slog.Debug("no messages to diarize") + return nil + } + + summary := "Talked about various topics." + if len(recentConvo) > 0 { + first := recentConvo[0] + if len(first) > 80 { + first = first[:80] + } + summary = "Conversations included: " + first + } + + entry := personality.DiaryEntry{ + ID: time.Now().Format("20060102"), + Date: time.Now().Format("2006-01-02"), + Summary: summary, + Mood: "neutral", + CreatedAt: time.Now(), + } + + return e.db.Exec( + `INSERT INTO diary_entries (id, date, summary, mood, created_at) + VALUES (?, ?, ?, ?, datetime('now')) + ON CONFLICT(id) DO UPDATE SET summary = excluded.summary, mood = excluded.mood`, + entry.ID, entry.Date, entry.Summary, entry.Mood, + ) +} + +func (e *ReflectionEngine) RecentObservations(limit int) []personality.Observation { + e.mu.Lock() + defer e.mu.Unlock() + + rows, err := e.db.Query( + "SELECT id, content, confidence, category, applied FROM observations ORDER BY created_at DESC LIMIT ?", + limit, + ) + if err != nil { + return nil + } + defer rows.Close() + + var obs []personality.Observation + for rows.Next() { + var o personality.Observation + if err := rows.Scan(&o.ID, &o.Content, &o.Confidence, &o.Category, &o.Applied); err != nil { + continue + } + obs = append(obs, o) + } + return obs +} + +func (e *ReflectionEngine) AddObservation(content, category string, confidence float64) error { + e.mu.Lock() + defer e.mu.Unlock() + + return e.db.Exec( + `INSERT INTO observations (id, content, confidence, category, created_at) + VALUES (?, ?, ?, ?, datetime('now'))`, + "obs_"+time.Now().Format("150405"), content, confidence, category, + ) +}