From c62ff68da8f54068c2eb8f4833b50aca54101b3e Mon Sep 17 00:00:00 2001 From: Dakera CTO Date: Fri, 3 Jul 2026 16:53:16 +0000 Subject: [PATCH] fix(recall): prefer smart_score as ranking key in RecalledMemory.UnmarshalJSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server sorts recall results by smart_score (fallback weighted_score → score) but recalledMemoryWire only decoded the raw score field, causing Score to differ from actual rank order. Fix adds SmartScore and WeightedScore to wire struct and RecalledMemory, resolving Score = smart_score ?? weighted_score ?? score. Tests: 4 unit tests covering all fallback paths and flat-format decode. Co-Authored-By: Paperclip --- recalled_memory.go | 22 ++++++++--- recalled_memory_test.go | 82 +++++++++++++++++++++++++++++++++++++++++ types.go | 16 +++++--- 3 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 recalled_memory_test.go diff --git a/recalled_memory.go b/recalled_memory.go index 5a353a1..94ffbca 100644 --- a/recalled_memory.go +++ b/recalled_memory.go @@ -7,7 +7,7 @@ import "encoding/json" // The Dakera server returns recall/search results with memory fields nested // under a "memory" key and score at the top level: // -// {"memory": {"id": "...", "content": "...", ...}, "score": 0.95} +// {"memory": {"id": "...", "content": "...", ...}, "score": 0.95, "smart_score": 0.87} // // The flat fields (ID, Content, …) are retained so that the flat format used // in unit test mocks and by the /v1/agents/{id}/memories endpoint continues to @@ -17,8 +17,10 @@ type recalledMemoryWire struct { Memory *Memory `json:"memory"` // Top-level fields present in all formats - Score float32 `json:"score"` - Depth *int `json:"depth,omitempty"` + Score float32 `json:"score"` + WeightedScore *float32 `json:"weighted_score,omitempty"` + SmartScore *float32 `json:"smart_score,omitempty"` + Depth *int `json:"depth,omitempty"` // Flat fields — populated when the "memory" wrapper is absent ID string `json:"id"` @@ -33,7 +35,7 @@ type recalledMemoryWire struct { // UnmarshalJSON implements json.Unmarshaler for RecalledMemory. // // Handles two wire formats: -// - Nested (server): {"memory": {"id": "...", "content": "..."}, "score": N} +// - Nested (server): {"memory": {"id": "...", "content": "..."}, "score": N, "smart_score": N} // - Flat (unit tests, /v1/agents/{id}/memories): {"id": "...", "content": "...", "score": N} func (r *RecalledMemory) UnmarshalJSON(data []byte) error { var wire recalledMemoryWire @@ -61,7 +63,17 @@ func (r *RecalledMemory) UnmarshalJSON(data []byte) error { r.CreatedAt = wire.CreatedAt } - r.Score = wire.Score + // smart_score is the server's ranking key; prefer it over weighted_score and raw score. + r.SmartScore = wire.SmartScore + r.WeightedScore = wire.WeightedScore + switch { + case wire.SmartScore != nil: + r.Score = *wire.SmartScore + case wire.WeightedScore != nil: + r.Score = *wire.WeightedScore + default: + r.Score = wire.Score + } r.Depth = wire.Depth return nil } diff --git a/recalled_memory_test.go b/recalled_memory_test.go new file mode 100644 index 0000000..82cc25d --- /dev/null +++ b/recalled_memory_test.go @@ -0,0 +1,82 @@ +package dakera + +import ( + "encoding/json" + "testing" +) + +func ptr32(v float32) *float32 { return &v } + +func TestRecalledMemory_SmartScorePriority(t *testing.T) { + data := []byte(`{ + "memory": {"id": "m1", "content": "test", "memory_type": "episodic", "importance": 0.8}, + "score": 0.5, + "weighted_score": 0.7, + "smart_score": 0.9 + }`) + var m RecalledMemory + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + if m.Score != 0.9 { + t.Errorf("Score = %v, want 0.9 (smart_score priority)", m.Score) + } + if m.SmartScore == nil || *m.SmartScore != 0.9 { + t.Errorf("SmartScore = %v, want &0.9", m.SmartScore) + } + if m.WeightedScore == nil || *m.WeightedScore != 0.7 { + t.Errorf("WeightedScore = %v, want &0.7", m.WeightedScore) + } +} + +func TestRecalledMemory_WeightedScoreFallback(t *testing.T) { + data := []byte(`{ + "memory": {"id": "m2", "content": "test", "memory_type": "episodic", "importance": 0.8}, + "score": 0.5, + "weighted_score": 0.7 + }`) + var m RecalledMemory + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + if m.Score != 0.7 { + t.Errorf("Score = %v, want 0.7 (weighted_score fallback)", m.Score) + } + if m.SmartScore != nil { + t.Errorf("SmartScore = %v, want nil", m.SmartScore) + } +} + +func TestRecalledMemory_RawScoreFallback(t *testing.T) { + data := []byte(`{ + "memory": {"id": "m3", "content": "test", "memory_type": "episodic", "importance": 0.8}, + "score": 0.55 + }`) + var m RecalledMemory + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + if m.Score != 0.55 { + t.Errorf("Score = %v, want 0.55 (raw score fallback)", m.Score) + } + if m.SmartScore != nil { + t.Errorf("SmartScore = %v, want nil", m.SmartScore) + } + if m.WeightedScore != nil { + t.Errorf("WeightedScore = %v, want nil", m.WeightedScore) + } +} + +func TestRecalledMemory_FlatFormatScore(t *testing.T) { + data := []byte(`{"id": "m4", "content": "flat", "memory_type": "episodic", "importance": 0.5, "score": 0.6}`) + var m RecalledMemory + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + if m.Score != 0.6 { + t.Errorf("Score = %v, want 0.6 (flat format)", m.Score) + } + if m.SmartScore != nil { + t.Errorf("SmartScore = %v, want nil for flat format", m.SmartScore) + } +} diff --git a/types.go b/types.go index c943ad9..f007090 100644 --- a/types.go +++ b/types.go @@ -518,18 +518,24 @@ type Memory struct { // RecalledMemory represents a recalled memory with similarity score. // // The server wraps memory fields under a nested "memory" key with score at the -// top level: {"memory": {"id": "...", "content": "...", ...}, "score": 0.95}. +// top level: {"memory": {"id": "...", "content": "...", ...}, "score": 0.95, "smart_score": 0.87}. // UnmarshalJSON handles this nested wire format transparently so callers always // see populated ID, Content, and other fields. +// +// Score is set to SmartScore when present, then WeightedScore, then raw Score — +// matching the server's ranking key so results appear in true rank order. type RecalledMemory struct { ID string `json:"id"` Content string `json:"content"` MemoryType string `json:"memory_type"` Importance float32 `json:"importance"` - Score float32 `json:"score"` - Tags []string `json:"tags,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` - CreatedAt int64 `json:"created_at,omitempty"` + // Ranking score — equals SmartScore when present, then WeightedScore, then raw score. + Score float32 `json:"score"` + SmartScore *float32 `json:"smart_score,omitempty"` + WeightedScore *float32 `json:"weighted_score,omitempty"` + Tags []string `json:"tags,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + CreatedAt int64 `json:"created_at,omitempty"` // KG-3: hop depth at which this memory was found (only set on associated memories) Depth *int `json:"depth,omitempty"` }