Changeset 0.18.0 (#79)
This commit is contained in:
100
server/handlers/memory_inject.go
Normal file
100
server/handlers/memory_inject.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/knowledge"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// maxMemoryChars is the approximate character budget for injected memories.
|
||||
const maxMemoryChars = 6000
|
||||
|
||||
// BuildMemoryHint loads active memories for the user (and persona if active)
|
||||
// and formats them as a system prompt injection.
|
||||
//
|
||||
// Phase 2: if an embedder is available and the user's latest message
|
||||
// is provided, it generates a query vector for semantic recall. Otherwise
|
||||
// falls back to keyword/confidence ranking.
|
||||
func BuildMemoryHint(ctx context.Context, stores store.Stores, embedder *knowledge.Embedder, userID, personaID, lastUserMessage string) string {
|
||||
if stores.Memories == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var pID *string
|
||||
if personaID != "" {
|
||||
pID = &personaID
|
||||
}
|
||||
|
||||
var memories []models.Memory
|
||||
var err error
|
||||
|
||||
// Try hybrid recall with embedding if available
|
||||
if embedder != nil && embedder.IsConfigured(ctx) && lastUserMessage != "" {
|
||||
memories, err = hybridRecallForInjection(ctx, stores, embedder, userID, pID, lastUserMessage)
|
||||
}
|
||||
|
||||
// Fall back to keyword recall
|
||||
if err != nil || len(memories) == 0 {
|
||||
memories, err = stores.Memories.Recall(ctx, userID, pID, "", 30)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("⚠ memory injection failed: %v", err)
|
||||
return ""
|
||||
}
|
||||
if len(memories) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Format memories as bullet points, respecting token budget
|
||||
var b strings.Builder
|
||||
b.WriteString("Known facts about this user (from previous conversations):\n")
|
||||
|
||||
totalChars := b.Len()
|
||||
included := 0
|
||||
for _, m := range memories {
|
||||
line := fmt.Sprintf("• %s: %s\n", m.Key, m.Value)
|
||||
if totalChars+len(line) > maxMemoryChars {
|
||||
break
|
||||
}
|
||||
b.WriteString(line)
|
||||
totalChars += len(line)
|
||||
included++
|
||||
}
|
||||
|
||||
if included == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Printf("🧠 Injected %d memories for user %s (persona: %s)", included, userID, personaID)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// hybridRecallForInjection generates a query vector from the user's message
|
||||
// and performs hybrid semantic + keyword recall.
|
||||
func hybridRecallForInjection(ctx context.Context, stores store.Stores, embedder *knowledge.Embedder, userID string, personaID *string, message string) ([]models.Memory, error) {
|
||||
text := message
|
||||
if len(text) > 2000 {
|
||||
text = text[:2000]
|
||||
}
|
||||
|
||||
var teamID *string
|
||||
if stores.Teams != nil {
|
||||
ids, _ := stores.Teams.GetUserTeamIDs(ctx, userID)
|
||||
if len(ids) > 0 {
|
||||
teamID = &ids[0]
|
||||
}
|
||||
}
|
||||
|
||||
result, err := embedder.EmbedChunks(ctx, userID, teamID, []string{text})
|
||||
if err != nil || len(result.Vectors) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stores.Memories.RecallHybrid(ctx, userID, personaID, "", result.Vectors[0], 30)
|
||||
}
|
||||
Reference in New Issue
Block a user