- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
101 lines
2.6 KiB
Go
101 lines
2.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"switchboard-core/knowledge"
|
|
"switchboard-core/models"
|
|
"switchboard-core/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)
|
|
}
|