package handlers import ( "context" "fmt" "log" "strings" "chat-switchboard/knowledge" "chat-switchboard/models" "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) }