Skip to content

Commit 2ddf8d4

Browse files
committed
feat: content quality scoring, claude code config command, eval improvements
1 parent 763bbbf commit 2ddf8d4

15 files changed

Lines changed: 475 additions & 133 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ models/*.gguf
88
website/node_modules/
99
website/build/
1010
website/.docusaurus/
11+
eval-bin

cmd/memoryd/claude.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"log"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
)
10+
11+
// registerMCPServers attempts to register memoryd as an MCP server in every
12+
// supported coding agent found on this machine. Each registration is
13+
// idempotent and non-fatal — failures are logged and skipped.
14+
func registerMCPServers() {
15+
execPath, err := os.Executable()
16+
if err != nil {
17+
log.Printf("[mcp-register] could not determine executable path: %v", err)
18+
return
19+
}
20+
21+
home, err := os.UserHomeDir()
22+
if err != nil {
23+
log.Printf("[mcp-register] could not determine home directory: %v", err)
24+
return
25+
}
26+
27+
entry := mcpEntry(execPath)
28+
29+
registrars := []struct {
30+
name string
31+
fn func() error
32+
}{
33+
{"Claude Code", func() error { return registerClaudeCode(home, entry) }},
34+
{"Cursor", func() error { return registerStandard(home, cursorMCPPath(home), entry) }},
35+
{"Windsurf", func() error { return registerStandard(home, windsurfMCPPath(home), entry) }},
36+
{"Cline", func() error { return registerStandard(home, clineMCPPath(home), entry) }},
37+
}
38+
39+
for _, r := range registrars {
40+
if err := r.fn(); err != nil {
41+
log.Printf("[mcp-register] %s: %v", r.name, err)
42+
}
43+
}
44+
}
45+
46+
// mcpEntry returns the standard MCP server entry for memoryd.
47+
func mcpEntry(execPath string) map[string]any {
48+
return map[string]any{
49+
"command": execPath,
50+
"args": []string{"mcp"},
51+
"env": map[string]any{},
52+
}
53+
}
54+
55+
// --- Claude Code ---
56+
57+
// registerClaudeCode writes to ~/.claude/settings.json under
58+
// projects[home].mcpServers. Created if it doesn't exist.
59+
func registerClaudeCode(home string, entry map[string]any) error {
60+
settingsPath := filepath.Join(home, ".claude", "settings.json")
61+
62+
var settings map[string]any
63+
data, err := os.ReadFile(settingsPath)
64+
if err != nil && !os.IsNotExist(err) {
65+
return err
66+
}
67+
if len(data) > 0 {
68+
if err := json.Unmarshal(data, &settings); err != nil {
69+
return err
70+
}
71+
}
72+
if settings == nil {
73+
settings = map[string]any{}
74+
}
75+
76+
projects, _ := settings["projects"].(map[string]any)
77+
if projects == nil {
78+
projects = map[string]any{}
79+
settings["projects"] = projects
80+
}
81+
82+
homeProject, _ := projects[home].(map[string]any)
83+
if homeProject == nil {
84+
homeProject = map[string]any{}
85+
projects[home] = homeProject
86+
}
87+
88+
mcpServers, _ := homeProject["mcpServers"].(map[string]any)
89+
if mcpServers == nil {
90+
mcpServers = map[string]any{}
91+
homeProject["mcpServers"] = mcpServers
92+
}
93+
94+
if _, exists := mcpServers["memoryd"]; exists {
95+
return nil
96+
}
97+
98+
mcpServers["memoryd"] = entry
99+
100+
return writeJSON(settingsPath, settings)
101+
}
102+
103+
// --- Standard format (Cursor, Windsurf, Cline) ---
104+
105+
// registerStandard writes an {"mcpServers":{"memoryd":{...}}} config file.
106+
// It only proceeds if the agent's config directory already exists (proving
107+
// the agent is installed). The config file itself is created if absent.
108+
func registerStandard(home, configPath string, entry map[string]any) error {
109+
dir := filepath.Dir(configPath)
110+
if _, err := os.Stat(dir); os.IsNotExist(err) {
111+
return nil // agent not installed, skip silently
112+
}
113+
114+
var cfg map[string]any
115+
data, err := os.ReadFile(configPath)
116+
if err != nil && !os.IsNotExist(err) {
117+
return err
118+
}
119+
if len(data) > 0 {
120+
if err := json.Unmarshal(data, &cfg); err != nil {
121+
return err
122+
}
123+
}
124+
if cfg == nil {
125+
cfg = map[string]any{}
126+
}
127+
128+
mcpServers, _ := cfg["mcpServers"].(map[string]any)
129+
if mcpServers == nil {
130+
mcpServers = map[string]any{}
131+
cfg["mcpServers"] = mcpServers
132+
}
133+
134+
if _, exists := mcpServers["memoryd"]; exists {
135+
return nil
136+
}
137+
138+
mcpServers["memoryd"] = entry
139+
140+
if err := writeJSON(configPath, cfg); err != nil {
141+
return err
142+
}
143+
144+
log.Printf("[mcp-register] registered in %s", configPath)
145+
return nil
146+
}
147+
148+
// --- Path helpers ---
149+
150+
func cursorMCPPath(home string) string {
151+
return filepath.Join(home, ".cursor", "mcp.json")
152+
}
153+
154+
func windsurfMCPPath(home string) string {
155+
return filepath.Join(home, ".codeium", "windsurf", "mcp_config.json")
156+
}
157+
158+
func clineMCPPath(home string) string {
159+
switch runtime.GOOS {
160+
case "darwin":
161+
return filepath.Join(home, "Library", "Application Support", "Code", "User",
162+
"globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
163+
default:
164+
return filepath.Join(home, ".config", "Code", "User",
165+
"globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
166+
}
167+
}
168+
169+
// --- Helpers ---
170+
171+
func writeJSON(path string, v any) error {
172+
out, err := json.MarshalIndent(v, "", " ")
173+
if err != nil {
174+
return err
175+
}
176+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
177+
return err
178+
}
179+
return os.WriteFile(path, out, 0644)
180+
}

cmd/memoryd/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ func startCmd() *cobra.Command {
7474
return err
7575
}
7676

77+
registerMCPServers()
78+
7779
if cfg.MongoDBAtlasURI == "" {
7880
return fmt.Errorf("mongodb_atlas_uri is required -- edit %s", config.Path())
7981
}
@@ -161,7 +163,12 @@ func startCmd() *cobra.Command {
161163
// Write pipeline uses primary store (default write target).
162164
qt := quality.NewTracker(primary.Mongo, quality.DefaultThreshold)
163165
read := pipeline.NewReadPipeline(emb, multi, cfg, pipeline.WithQualityTracker(qt))
164-
write := pipeline.NewWritePipeline(emb, primary.Store)
166+
167+
scorer, err := quality.NewContentScorer(ctx, emb)
168+
if err != nil {
169+
log.Printf("warning: content scorer unavailable, chunks will not be quality-scored: %v", err)
170+
}
171+
write := pipeline.NewWritePipeline(emb, primary.Store, pipeline.WithContentScorer(scorer))
165172

166173
// 4. Build ingester (operates on primary database)
167174
ing := ingest.NewIngester(emb, primary.Mongo, primary.Mongo)

eval/harness.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ type Scenario struct {
2424
}
2525

2626
type RunResult struct {
27-
Condition string `json:"condition"`
28-
Response string `json:"response"`
29-
Latency time.Duration `json:"latency_ms"`
30-
Context string `json:"context,omitempty"`
27+
Condition string `json:"condition"`
28+
Response string `json:"response"`
29+
Latency time.Duration `json:"latency_ms"`
30+
Context string `json:"context,omitempty"`
31+
RetrievalScores []float64 `json:"retrieval_scores,omitempty"`
3132
}
3233

3334
type JudgeScore struct {
@@ -169,22 +170,23 @@ func (h *Harness) clearMemories(ctx context.Context) error {
169170
return nil
170171
}
171172

172-
func (h *Harness) retrieveContext(ctx context.Context, query string) (string, error) {
173+
func (h *Harness) retrieveContext(ctx context.Context, query string) (string, []float64, error) {
173174
body, _ := json.Marshal(map[string]string{"query": query})
174175
req, _ := http.NewRequestWithContext(ctx, "POST", h.cfg.MemorydURL+"/api/search", bytes.NewReader(body))
175176
req.Header.Set("Content-Type", "application/json")
176177
resp, err := h.client.Do(req)
177178
if err != nil {
178-
return "", err
179+
return "", nil, err
179180
}
180181
defer resp.Body.Close()
181182
var result struct {
182-
Context string `json:"context"`
183+
Context string `json:"context"`
184+
Scores []float64 `json:"scores"`
183185
}
184186
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
185-
return "", err
187+
return "", nil, err
186188
}
187-
return result.Context, nil
189+
return result.Context, result.Scores, nil
188190
}
189191

190192
func (h *Harness) callClaude(ctx context.Context, system, user string) (string, time.Duration, error) {
@@ -237,7 +239,7 @@ func (h *Harness) runBare(ctx context.Context, sc Scenario) (*RunResult, error)
237239
}
238240

239241
func (h *Harness) runAugmented(ctx context.Context, sc Scenario) (*RunResult, error) {
240-
retrieved, err := h.retrieveContext(ctx, sc.Prompt)
242+
retrieved, scores, err := h.retrieveContext(ctx, sc.Prompt)
241243
if err != nil {
242244
return nil, fmt.Errorf("retrieve: %w", err)
243245
}
@@ -249,7 +251,7 @@ func (h *Harness) runAugmented(ctx context.Context, sc Scenario) (*RunResult, er
249251
if err != nil {
250252
return nil, err
251253
}
252-
return &RunResult{Condition: "augmented", Response: text, Latency: latency, Context: retrieved}, nil
254+
return &RunResult{Condition: "augmented", Response: text, Latency: latency, Context: retrieved, RetrievalScores: scores}, nil
253255
}
254256

255257
func (h *Harness) judge(ctx context.Context, sc Scenario, bare, aug *RunResult) ([]JudgeScore, error) {

eval/report.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7+
"math"
78
"strings"
89
)
910

@@ -30,6 +31,22 @@ func Report(w io.Writer, results []ScenarioResult) {
3031
fmt.Fprintf(w, " %s\n", s.Explanation)
3132
}
3233

34+
if len(r.Augmented.RetrievalScores) > 0 {
35+
min, max, avg := scoreStats(r.Augmented.RetrievalScores)
36+
fmt.Fprintf(w, "\n retrieval: n=%d min=%.3f max=%.3f avg=%.3f",
37+
len(r.Augmented.RetrievalScores), min, max, avg)
38+
if r.Delta > 0 {
39+
fmt.Fprintf(w, " → helped")
40+
} else if r.Delta < 0 {
41+
fmt.Fprintf(w, " → hurt")
42+
} else {
43+
fmt.Fprintf(w, " → neutral")
44+
}
45+
fmt.Fprintf(w, "\n")
46+
} else {
47+
fmt.Fprintf(w, "\n retrieval: no scores recorded\n")
48+
}
49+
3350
fmt.Fprintf(w, "\n TOTAL: bare=%d aug=%d delta=%+d\n\n", r.BareTotal, r.AugTotal, r.Delta)
3451

3552
totalBare += r.BareTotal
@@ -46,9 +63,71 @@ func Report(w io.Writer, results []ScenarioResult) {
4663
pct := float64(totalAug-totalBare) / float64(totalBare) * 100
4764
fmt.Fprintf(w, " Improvement: %.1f%%\n", pct)
4865
}
66+
67+
// Score-vs-delta breakdown: bucket by avg retrieval score, show avg delta per bucket.
68+
fmt.Fprintf(w, "\n Retrieval score vs quality delta:\n")
69+
type bucket struct {
70+
count int
71+
deltaSum int
72+
}
73+
buckets := map[string]*bucket{
74+
"<0.50": {},
75+
"0.50-0.60": {},
76+
"0.60-0.70": {},
77+
"0.70-0.80": {},
78+
">=0.80": {},
79+
}
80+
bucketOrder := []string{"<0.50", "0.50-0.60", "0.60-0.70", "0.70-0.80", ">=0.80"}
81+
for _, r := range results {
82+
if len(r.Augmented.RetrievalScores) == 0 {
83+
continue
84+
}
85+
_, _, avg := scoreStats(r.Augmented.RetrievalScores)
86+
var key string
87+
switch {
88+
case avg < 0.50:
89+
key = "<0.50"
90+
case avg < 0.60:
91+
key = "0.50-0.60"
92+
case avg < 0.70:
93+
key = "0.60-0.70"
94+
case avg < 0.80:
95+
key = "0.70-0.80"
96+
default:
97+
key = ">=0.80"
98+
}
99+
buckets[key].count++
100+
buckets[key].deltaSum += r.Delta
101+
}
102+
for _, k := range bucketOrder {
103+
b := buckets[k]
104+
if b.count == 0 {
105+
continue
106+
}
107+
avgDelta := float64(b.deltaSum) / float64(b.count)
108+
fmt.Fprintf(w, " avg score %-12s n=%d avg delta=%+.2f\n", k, b.count, avgDelta)
109+
}
110+
49111
fmt.Fprintf(w, "%s\n\n", strings.Repeat("=", 72))
50112
}
51113

114+
func scoreStats(scores []float64) (min, max, avg float64) {
115+
min = math.MaxFloat64
116+
max = -math.MaxFloat64
117+
var sum float64
118+
for _, s := range scores {
119+
if s < min {
120+
min = s
121+
}
122+
if s > max {
123+
max = s
124+
}
125+
sum += s
126+
}
127+
avg = sum / float64(len(scores))
128+
return
129+
}
130+
52131
// ReportJSON writes machine-readable JSON output.
53132
func ReportJSON(w io.Writer, results []ScenarioResult) error {
54133
enc := json.NewEncoder(w)

internal/config/config.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ const (
2525

2626
// DatabaseConfig describes a single team database connection.
2727
type DatabaseConfig struct {
28-
Name string `yaml:"name"` // Human label (e.g., "platform", "payments")
29-
Database string `yaml:"database"` // MongoDB database name
30-
Role string `yaml:"role"` // "full" or "read-only"
31-
URI string `yaml:"uri,omitempty"` // Connection string (empty = use primary URI)
28+
Name string `yaml:"name"` // Human label (e.g., "platform", "payments")
29+
Database string `yaml:"database"` // MongoDB database name
30+
Role string `yaml:"role"` // "full" or "read-only"
31+
URI string `yaml:"uri,omitempty"` // Connection string (empty = use primary URI)
3232
Enabled *bool `yaml:"enabled,omitempty"` // nil/true = enabled, false = disabled
3333
}
3434

0 commit comments

Comments
 (0)