Skip to content

Commit d7a69e8

Browse files
committed
feat: LLM episode ranking using SimpleCall
- NewLLMRanker creates a RankStrategy that asks the LLM to rank episode summaries by relevance to the query - Falls back to recency ordering on LLM failure - Automatic wiring: MemoryManager uses LLM ranker when llc != nil - Prompt returns comma-separated indices for efficient parsing
1 parent 356d2d1 commit d7a69e8

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

internal/memory/episodes.go

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package memory
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"os"
78
"path/filepath"
89
"sort"
10+
"strings"
911
"time"
1012
)
1113

@@ -178,10 +180,73 @@ func truncateForIndex(summary string) string {
178180
// ── Default ranker ────────────────────────────────────────────────────
179181

180182
// defaultRanker returns all episodes sorted by recency (no LLM).
181-
// The MemoryManager wraps this with a real SimpleCall when available.
182183
func defaultRanker(query string, episodes []EpisodeMeta) ([]EpisodeMeta, error) {
183-
// Already sorted newest-first by ReadIndex
184184
out := make([]EpisodeMeta, len(episodes))
185185
copy(out, episodes)
186186
return out, nil
187187
}
188+
189+
// NewLLMRanker creates a RankStrategy that uses an LLM client to rank
190+
// episodes by semantic relevance to the query. Falls back to recency
191+
// ordering if the LLM call fails or returns unparseable output.
192+
func NewLLMRanker(llm LLMClient) RankStrategy {
193+
return func(query string, episodes []EpisodeMeta) ([]EpisodeMeta, error) {
194+
if len(episodes) == 0 {
195+
return episodes, nil
196+
}
197+
198+
// Build a compact prompt listing episodes
199+
var b strings.Builder
200+
fmt.Fprintf(&b, "Rank these memory summaries by relevance to: %s\n\n", query)
201+
for i, ep := range episodes {
202+
// Truncate summary for the prompt (already truncated in index, but be safe)
203+
summary := ep.Summary
204+
if len(summary) > 200 {
205+
summary = summary[:197] + "..."
206+
}
207+
fmt.Fprintf(&b, "[%d] %s\n", i, summary)
208+
}
209+
b.WriteString("\nReturn only the indices of the most relevant entries, ordered by relevance (most relevant first).\n")
210+
b.WriteString("Format: a single line of comma-separated numbers, e.g. \"3,0,1\"\n")
211+
b.WriteString("If none are relevant, return \"none\".")
212+
213+
resp, err := llm.SimpleCall(context.Background(),
214+
"You are a relevance ranking system. Given a query and a list of items, return the indices of the most relevant items ordered by relevance. Return only a comma-separated list of numbers or the word 'none'.",
215+
b.String(),
216+
)
217+
if err != nil {
218+
return defaultRanker(query, episodes)
219+
}
220+
221+
resp = strings.TrimSpace(resp)
222+
if resp == "" || resp == "none" {
223+
return nil, nil
224+
}
225+
226+
// Parse "3,0,1" or "3, 0, 1" into indices
227+
parts := strings.Split(resp, ",")
228+
seen := make(map[int]bool)
229+
var ranked []EpisodeMeta
230+
for _, p := range parts {
231+
p = strings.TrimSpace(p)
232+
if p == "" {
233+
continue
234+
}
235+
idx := 0
236+
for _, c := range p {
237+
if c >= '0' && c <= '9' {
238+
idx = idx*10 + int(c-'0')
239+
}
240+
}
241+
if idx >= 0 && idx < len(episodes) && !seen[idx] {
242+
ranked = append(ranked, episodes[idx])
243+
seen[idx] = true
244+
}
245+
}
246+
247+
if len(ranked) == 0 {
248+
return defaultRanker(query, episodes)
249+
}
250+
return ranked, nil
251+
}
252+
}

internal/memory/memory.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ func NewMemoryManager(memoryDir string, llc LLMClient, cfg MemoryConfig) *Memory
7373
episodesDir := memoryDir
7474

7575
factStore := NewFactStore(factsDir, cfg.FactsLimitUser, cfg.FactsLimitEnv)
76-
episodeStore := NewEpisodeStore(episodesDir, nil)
76+
// Use LLM-based episode ranker when an LLM client is available
77+
var rankFn RankStrategy
78+
if llc != nil {
79+
rankFn = NewLLMRanker(llc)
80+
}
81+
episodeStore := NewEpisodeStore(episodesDir, rankFn)
7782
mergeDetector := NewMergeDetector(0) // default 256 dims
7883

7984
return &MemoryManager{

0 commit comments

Comments
 (0)