@xdarkicex/openclaw-memory-libravdb 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +46 -0
  2. package/docs/README.md +14 -0
  3. package/docs/architecture-decisions/README.md +6 -0
  4. package/docs/architecture-decisions/adr-001-onnx-over-ollama.md +21 -0
  5. package/docs/architecture-decisions/adr-002-libravdb-over-lancedb.md +19 -0
  6. package/docs/architecture-decisions/adr-003-convex-gating-over-threshold.md +27 -0
  7. package/docs/architecture-decisions/adr-004-sidecar-over-native-ts.md +21 -0
  8. package/docs/architecture.md +188 -0
  9. package/docs/contributing.md +76 -0
  10. package/docs/dependencies.md +38 -0
  11. package/docs/embedding-profiles.md +42 -0
  12. package/docs/gating.md +329 -0
  13. package/docs/implementation.md +381 -0
  14. package/docs/installation.md +272 -0
  15. package/docs/mathematics.md +695 -0
  16. package/docs/models.md +63 -0
  17. package/docs/problem.md +64 -0
  18. package/docs/security.md +86 -0
  19. package/openclaw.plugin.json +84 -0
  20. package/package.json +41 -0
  21. package/scripts/build-sidecar.sh +30 -0
  22. package/scripts/postinstall.js +169 -0
  23. package/scripts/setup.sh +20 -0
  24. package/scripts/setup.ts +505 -0
  25. package/scripts/sidecar-release.d.ts +4 -0
  26. package/scripts/sidecar-release.js +17 -0
  27. package/sidecar/cmd/inspect_onnx/main.go +105 -0
  28. package/sidecar/compact/gate.go +273 -0
  29. package/sidecar/compact/gate_test.go +85 -0
  30. package/sidecar/compact/summarize.go +345 -0
  31. package/sidecar/compact/summarize_test.go +319 -0
  32. package/sidecar/compact/tokens.go +11 -0
  33. package/sidecar/config/config.go +119 -0
  34. package/sidecar/config/config_test.go +75 -0
  35. package/sidecar/embed/engine.go +696 -0
  36. package/sidecar/embed/engine_test.go +349 -0
  37. package/sidecar/embed/matryoshka.go +93 -0
  38. package/sidecar/embed/matryoshka_test.go +150 -0
  39. package/sidecar/embed/onnx_local.go +319 -0
  40. package/sidecar/embed/onnx_local_test.go +159 -0
  41. package/sidecar/embed/profile_contract_test.go +71 -0
  42. package/sidecar/embed/profile_eval_test.go +923 -0
  43. package/sidecar/embed/profiles.go +39 -0
  44. package/sidecar/go.mod +21 -0
  45. package/sidecar/go.sum +30 -0
  46. package/sidecar/health/check.go +33 -0
  47. package/sidecar/health/check_test.go +55 -0
  48. package/sidecar/main.go +151 -0
  49. package/sidecar/model/encoder.go +222 -0
  50. package/sidecar/model/registry.go +262 -0
  51. package/sidecar/model/registry_test.go +102 -0
  52. package/sidecar/model/seq2seq.go +133 -0
  53. package/sidecar/server/rpc.go +343 -0
  54. package/sidecar/server/rpc_test.go +350 -0
  55. package/sidecar/server/transport.go +160 -0
  56. package/sidecar/store/libravdb.go +676 -0
  57. package/sidecar/store/libravdb_test.go +472 -0
  58. package/sidecar/summarize/engine.go +360 -0
  59. package/sidecar/summarize/engine_test.go +148 -0
  60. package/sidecar/summarize/onnx_local.go +494 -0
  61. package/sidecar/summarize/onnx_local_test.go +48 -0
  62. package/sidecar/summarize/profiles.go +52 -0
  63. package/sidecar/summarize/tokenizer.go +13 -0
  64. package/sidecar/summarize/tokenizer_hf.go +76 -0
  65. package/sidecar/summarize/util.go +13 -0
  66. package/src/cli.ts +205 -0
  67. package/src/context-engine.ts +195 -0
  68. package/src/index.ts +27 -0
  69. package/src/memory-provider.ts +24 -0
  70. package/src/openclaw-plugin-sdk.d.ts +53 -0
  71. package/src/plugin-runtime.ts +67 -0
  72. package/src/recall-cache.ts +34 -0
  73. package/src/recall-utils.ts +22 -0
  74. package/src/rpc.ts +84 -0
  75. package/src/scoring.ts +58 -0
  76. package/src/sidecar.ts +506 -0
  77. package/src/tokens.ts +36 -0
  78. package/src/types.ts +146 -0
  79. package/tsconfig.json +20 -0
  80. package/tsconfig.tests.json +12 -0
@@ -0,0 +1,319 @@
1
+ package compact
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "errors"
7
+ "log"
8
+ "testing"
9
+
10
+ "github.com/xDarkicex/openclaw-memory-libravdb/sidecar/store"
11
+ "github.com/xDarkicex/openclaw-memory-libravdb/sidecar/summarize"
12
+ )
13
+
14
+ type fakeStore struct {
15
+ results []store.SearchResult
16
+ insertCalls []insertCall
17
+ deleteCalls []deleteCall
18
+ deleteErr error
19
+ listErr error
20
+ insertErr error
21
+ }
22
+
23
+ type insertCall struct {
24
+ collection string
25
+ id string
26
+ text string
27
+ meta map[string]any
28
+ }
29
+
30
+ type deleteCall struct {
31
+ collection string
32
+ ids []string
33
+ }
34
+
35
+ type fakeSummarizer struct {
36
+ summaries []summarize.Summary
37
+ err error
38
+ calls [][]summarize.Turn
39
+ mode string
40
+ }
41
+
42
+ func (f *fakeStore) ListByMeta(_ context.Context, collection, key, value string) ([]store.SearchResult, error) {
43
+ if f.listErr != nil {
44
+ return nil, f.listErr
45
+ }
46
+ return append([]store.SearchResult(nil), f.results...), nil
47
+ }
48
+
49
+ func (f *fakeStore) InsertText(_ context.Context, collection, id, text string, meta map[string]any) error {
50
+ if f.insertErr != nil {
51
+ return f.insertErr
52
+ }
53
+ f.insertCalls = append(f.insertCalls, insertCall{
54
+ collection: collection,
55
+ id: id,
56
+ text: text,
57
+ meta: cloneMeta(meta),
58
+ })
59
+ return nil
60
+ }
61
+
62
+ func (f *fakeStore) DeleteBatch(_ context.Context, collection string, ids []string) error {
63
+ f.deleteCalls = append(f.deleteCalls, deleteCall{
64
+ collection: collection,
65
+ ids: append([]string(nil), ids...),
66
+ })
67
+ return f.deleteErr
68
+ }
69
+
70
+ func (f *fakeSummarizer) Summarize(_ context.Context, turns []summarize.Turn, _ summarize.SummaryOpts) (summarize.Summary, error) {
71
+ f.calls = append(f.calls, append([]summarize.Turn(nil), turns...))
72
+ if f.err != nil {
73
+ return summarize.Summary{}, f.err
74
+ }
75
+ index := len(f.calls) - 1
76
+ if index < len(f.summaries) {
77
+ return f.summaries[index], nil
78
+ }
79
+ sourceIDs := make([]string, 0, len(turns))
80
+ for _, turn := range turns {
81
+ sourceIDs = append(sourceIDs, turn.ID)
82
+ }
83
+ return summarize.Summary{
84
+ Text: "summary",
85
+ SourceIDs: sourceIDs,
86
+ Method: "extractive",
87
+ TokenCount: 3,
88
+ Confidence: 0.8,
89
+ }, nil
90
+ }
91
+
92
+ func (f *fakeSummarizer) Profile() summarize.Profile { return summarize.Profile{Backend: "extractive"} }
93
+ func (f *fakeSummarizer) Warmup(context.Context) error { return nil }
94
+ func (f *fakeSummarizer) Unload() {}
95
+ func (f *fakeSummarizer) Close() error { return nil }
96
+ func (f *fakeSummarizer) Ready() bool { return true }
97
+ func (f *fakeSummarizer) Reason() string { return "" }
98
+ func (f *fakeSummarizer) Mode() string {
99
+ if f.mode != "" {
100
+ return f.mode
101
+ }
102
+ return "extractive"
103
+ }
104
+
105
+ func TestCompactSessionSkipsBelowThresholdWithoutForce(t *testing.T) {
106
+ st := &fakeStore{
107
+ results: []store.SearchResult{
108
+ {ID: "a", Text: "alpha", Metadata: map[string]any{"sessionId": "s1", "ts": int64(10)}},
109
+ {ID: "b", Text: "beta", Metadata: map[string]any{"sessionId": "s1", "ts": int64(20)}},
110
+ },
111
+ }
112
+ sum := &fakeSummarizer{}
113
+
114
+ got, err := CompactSession(context.Background(), st, sum, nil, "s1", false, 20)
115
+ if err != nil {
116
+ t.Fatalf("CompactSession() error = %v", err)
117
+ }
118
+ if got.DidCompact {
119
+ t.Fatalf("expected no compaction below threshold, got %+v", got)
120
+ }
121
+ if len(sum.calls) != 0 || len(st.insertCalls) != 0 || len(st.deleteCalls) != 0 {
122
+ t.Fatalf("expected no summarizer/store writes, got calls=%d inserts=%d deletes=%d", len(sum.calls), len(st.insertCalls), len(st.deleteCalls))
123
+ }
124
+ }
125
+
126
+ func TestCompactSessionPartitionsDeterministicallyByTimestamp(t *testing.T) {
127
+ st := &fakeStore{
128
+ results: []store.SearchResult{
129
+ {ID: "c", Text: "third", Metadata: map[string]any{"sessionId": "s1", "ts": int64(30)}},
130
+ {ID: "a", Text: "first", Metadata: map[string]any{"sessionId": "s1", "ts": int64(10)}},
131
+ {ID: "d", Text: "fourth", Metadata: map[string]any{"sessionId": "s1", "ts": int64(40)}},
132
+ {ID: "b", Text: "second", Metadata: map[string]any{"sessionId": "s1", "ts": int64(20)}},
133
+ },
134
+ }
135
+ sum := &fakeSummarizer{
136
+ summaries: []summarize.Summary{
137
+ {Text: "summary-1", SourceIDs: []string{"a", "b"}, Method: "extractive", TokenCount: 2, Confidence: 0.6},
138
+ {Text: "summary-2", SourceIDs: []string{"c", "d"}, Method: "extractive", TokenCount: 2, Confidence: 0.8},
139
+ },
140
+ }
141
+
142
+ got, err := CompactSession(context.Background(), st, sum, nil, "s1", true, 2)
143
+ if err != nil {
144
+ t.Fatalf("CompactSession() error = %v", err)
145
+ }
146
+ if !got.DidCompact || got.ClustersFormed != 2 || got.TurnsRemoved != 4 {
147
+ t.Fatalf("unexpected result: %+v", got)
148
+ }
149
+ if got.SummaryMethod != "extractive" {
150
+ t.Fatalf("unexpected summary method: %+v", got)
151
+ }
152
+ if got.MeanConfidence != 0.7 {
153
+ t.Fatalf("expected mean confidence 0.7, got %f", got.MeanConfidence)
154
+ }
155
+ if len(sum.calls) != 2 {
156
+ t.Fatalf("expected 2 summarize calls, got %d", len(sum.calls))
157
+ }
158
+ assertTurnIDs(t, sum.calls[0], []string{"a", "b"})
159
+ assertTurnIDs(t, sum.calls[1], []string{"c", "d"})
160
+ }
161
+
162
+ func TestCompactSessionInsertsBeforeDeleteAndPreservesDataOnDeleteFailure(t *testing.T) {
163
+ st := &fakeStore{
164
+ results: []store.SearchResult{
165
+ {ID: "a", Text: "alpha", Metadata: map[string]any{"sessionId": "s1", "ts": int64(10), "userId": "u1"}},
166
+ {ID: "b", Text: "beta", Metadata: map[string]any{"sessionId": "s1", "ts": int64(20), "userId": "u1"}},
167
+ },
168
+ deleteErr: errors.New("boom"),
169
+ }
170
+ sum := &fakeSummarizer{
171
+ summaries: []summarize.Summary{
172
+ {Text: "summary-1", SourceIDs: []string{"a", "b"}, Method: "extractive", TokenCount: 5, Confidence: 0.75},
173
+ },
174
+ }
175
+
176
+ got, err := CompactSession(context.Background(), st, sum, nil, "s1", true, 20)
177
+ if err != nil {
178
+ t.Fatalf("CompactSession() error = %v", err)
179
+ }
180
+ if len(st.insertCalls) != 1 {
181
+ t.Fatalf("expected summary insert before delete, got %d insert calls", len(st.insertCalls))
182
+ }
183
+ if len(st.deleteCalls) != 1 {
184
+ t.Fatalf("expected delete attempt after insert, got %d delete calls", len(st.deleteCalls))
185
+ }
186
+ if got.TurnsRemoved != 0 {
187
+ t.Fatalf("expected no removed turns when delete fails, got %+v", got)
188
+ }
189
+
190
+ meta := st.insertCalls[0].meta
191
+ if meta["type"] != "summary" {
192
+ t.Fatalf("expected summary metadata type, got %+v", meta)
193
+ }
194
+ if meta["method"] != "extractive" {
195
+ t.Fatalf("expected method metadata, got %+v", meta)
196
+ }
197
+ if meta["confidence"] != 0.75 {
198
+ t.Fatalf("expected confidence metadata, got %+v", meta)
199
+ }
200
+ if meta["decay_rate"] != 0.25 {
201
+ t.Fatalf("expected decay rate metadata, got %+v", meta)
202
+ }
203
+ if meta["userId"] != "u1" {
204
+ t.Fatalf("expected userId carried forward, got %+v", meta)
205
+ }
206
+
207
+ sourceIDs, ok := meta["source_ids"].([]string)
208
+ if !ok {
209
+ t.Fatalf("expected source_ids to be []string, got %T", meta["source_ids"])
210
+ }
211
+ if len(sourceIDs) != 2 || sourceIDs[0] != "a" || sourceIDs[1] != "b" {
212
+ t.Fatalf("unexpected source_ids: %+v", sourceIDs)
213
+ }
214
+ }
215
+
216
+ func TestCompactSessionPreservesSourceTurnsWhenInsertFails(t *testing.T) {
217
+ st := &fakeStore{
218
+ results: []store.SearchResult{
219
+ {ID: "a", Text: "alpha", Metadata: map[string]any{"sessionId": "s1", "ts": int64(10)}},
220
+ {ID: "b", Text: "beta", Metadata: map[string]any{"sessionId": "s1", "ts": int64(20)}},
221
+ },
222
+ insertErr: errors.New("insert failed"),
223
+ }
224
+ sum := &fakeSummarizer{}
225
+
226
+ _, err := CompactSession(context.Background(), st, sum, nil, "s1", true, 20)
227
+ if err == nil {
228
+ t.Fatalf("expected insert failure")
229
+ }
230
+ if len(st.deleteCalls) != 0 {
231
+ t.Fatalf("expected no delete call when insert fails, got %d", len(st.deleteCalls))
232
+ }
233
+ }
234
+
235
+ func TestCompactSessionRoutesHighGatingClustersToAbstractive(t *testing.T) {
236
+ st := &fakeStore{
237
+ results: []store.SearchResult{
238
+ {ID: "a", Text: "alpha", Metadata: map[string]any{"sessionId": "s1", "ts": int64(10), "gating_score": 0.8}},
239
+ {ID: "b", Text: "beta", Metadata: map[string]any{"sessionId": "s1", "ts": int64(20), "gating_score": 0.7}},
240
+ },
241
+ }
242
+ extractive := &fakeSummarizer{
243
+ summaries: []summarize.Summary{{Text: "extractive-summary", Method: "extractive", TokenCount: 2, Confidence: 0.5}},
244
+ mode: "extractive",
245
+ }
246
+ abstractive := &fakeSummarizer{
247
+ summaries: []summarize.Summary{{Text: "abstractive-summary", Method: "onnx-t5", TokenCount: 3, Confidence: 0.9}},
248
+ mode: "onnx-local",
249
+ }
250
+
251
+ got, err := CompactSession(context.Background(), st, extractive, abstractive, "s1", true, 20)
252
+ if err != nil {
253
+ t.Fatalf("CompactSession() error = %v", err)
254
+ }
255
+ if !got.DidCompact {
256
+ t.Fatalf("expected compaction, got %+v", got)
257
+ }
258
+ if len(abstractive.calls) != 1 {
259
+ t.Fatalf("expected abstractive summarizer to be used, got %d calls", len(abstractive.calls))
260
+ }
261
+ if len(extractive.calls) != 0 {
262
+ t.Fatalf("expected extractive summarizer to be skipped, got %d calls", len(extractive.calls))
263
+ }
264
+ if got.SummaryMethod != "onnx-t5" {
265
+ t.Fatalf("expected onnx-t5 method, got %+v", got)
266
+ }
267
+ }
268
+
269
+ func TestCompactSessionRoutesMissingGatingScoreToExtractiveAndLogsDecision(t *testing.T) {
270
+ st := &fakeStore{
271
+ results: []store.SearchResult{
272
+ {ID: "a", Text: "alpha", Metadata: map[string]any{"sessionId": "s1", "ts": int64(10)}},
273
+ {ID: "b", Text: "beta", Metadata: map[string]any{"sessionId": "s1", "ts": int64(20)}},
274
+ },
275
+ }
276
+ extractive := &fakeSummarizer{
277
+ summaries: []summarize.Summary{{Text: "extractive-summary", Method: "extractive", TokenCount: 2, Confidence: 0.5}},
278
+ mode: "extractive",
279
+ }
280
+ abstractive := &fakeSummarizer{
281
+ summaries: []summarize.Summary{{Text: "abstractive-summary", Method: "onnx-t5", TokenCount: 3, Confidence: 0.9}},
282
+ mode: "onnx-local",
283
+ }
284
+
285
+ var buf bytes.Buffer
286
+ prevWriter := log.Writer()
287
+ log.SetOutput(&buf)
288
+ defer log.SetOutput(prevWriter)
289
+
290
+ got, err := CompactSession(context.Background(), st, extractive, abstractive, "s1", true, 20)
291
+ if err != nil {
292
+ t.Fatalf("CompactSession() error = %v", err)
293
+ }
294
+ if !got.DidCompact {
295
+ t.Fatalf("expected compaction, got %+v", got)
296
+ }
297
+ if len(extractive.calls) != 1 {
298
+ t.Fatalf("expected extractive summarizer to be used, got %d calls", len(extractive.calls))
299
+ }
300
+ if len(abstractive.calls) != 0 {
301
+ t.Fatalf("expected abstractive summarizer to be skipped, got %d calls", len(abstractive.calls))
302
+ }
303
+ logged := buf.String()
304
+ if !bytes.Contains([]byte(logged), []byte("cluster_id=0")) || !bytes.Contains([]byte(logged), []byte("mean_gating_score=0.000")) || !bytes.Contains([]byte(logged), []byte("summarizer_used=extractive")) {
305
+ t.Fatalf("expected routing telemetry log, got %q", logged)
306
+ }
307
+ }
308
+
309
+ func assertTurnIDs(t *testing.T, turns []summarize.Turn, want []string) {
310
+ t.Helper()
311
+ if len(turns) != len(want) {
312
+ t.Fatalf("unexpected turns length: got %d want %d", len(turns), len(want))
313
+ }
314
+ for i, turn := range turns {
315
+ if turn.ID != want[i] {
316
+ t.Fatalf("unexpected turn order at %d: got %q want %q", i, turn.ID, want[i])
317
+ }
318
+ }
319
+ }
@@ -0,0 +1,11 @@
1
+ package compact
2
+
3
+ // EstimateTokens returns a stable token count approximation.
4
+ // Contract: one token is approximated as four bytes of UTF-8 text.
5
+ func EstimateTokens(t string) int {
6
+ n := len(t) / 4
7
+ if n < 1 {
8
+ return 1
9
+ }
10
+ return n
11
+ }
@@ -0,0 +1,119 @@
1
+ package config
2
+
3
+ import (
4
+ "os"
5
+ "os/user"
6
+ "path/filepath"
7
+ "strconv"
8
+ "strings"
9
+ )
10
+
11
+ type Config struct {
12
+ DBPath string
13
+ ONNXRuntimePath string
14
+ EmbeddingBackend string
15
+ EmbeddingProfile string
16
+ FallbackProfile string
17
+ EmbeddingModelPath string
18
+ EmbeddingTokenizerPath string
19
+ EmbeddingDimensions int
20
+ EmbeddingNormalize bool
21
+ SummarizerBackend string
22
+ SummarizerProfile string
23
+ SummarizerRuntimePath string
24
+ SummarizerModelPath string
25
+ SummarizerTokenizerPath string
26
+ SummarizerModel string
27
+ SummarizerEndpoint string
28
+ GatingW1c float64
29
+ GatingW2c float64
30
+ GatingW3c float64
31
+ GatingW1t float64
32
+ GatingW2t float64
33
+ GatingW3t float64
34
+ GatingTechNorm float64
35
+ GatingThreshold float64
36
+ GatingCentroidK int
37
+ }
38
+
39
+ func FromEnv() Config {
40
+ return Config{
41
+ DBPath: envOrDefault("LIBRAVDB_DB_PATH", defaultDBPath()),
42
+ ONNXRuntimePath: os.Getenv("LIBRAVDB_ONNX_RUNTIME"),
43
+ EmbeddingBackend: envOrDefault("LIBRAVDB_EMBEDDING_BACKEND", "bundled"),
44
+ EmbeddingProfile: envOrDefault("LIBRAVDB_EMBEDDING_PROFILE", "nomic-embed-text-v1.5"),
45
+ FallbackProfile: envOrDefault("LIBRAVDB_FALLBACK_PROFILE", "all-minilm-l6-v2"),
46
+ EmbeddingModelPath: os.Getenv("LIBRAVDB_EMBEDDING_MODEL"),
47
+ EmbeddingTokenizerPath: os.Getenv("LIBRAVDB_EMBEDDING_TOKENIZER"),
48
+ EmbeddingDimensions: envIntOrDefault("LIBRAVDB_EMBEDDING_DIMENSIONS", 0),
49
+ EmbeddingNormalize: envBoolOrDefault("LIBRAVDB_EMBEDDING_NORMALIZE", true),
50
+ SummarizerBackend: envOrDefault("LIBRAVDB_SUMMARIZER_BACKEND", "bundled"),
51
+ SummarizerProfile: strings.TrimSpace(os.Getenv("LIBRAVDB_SUMMARIZER_PROFILE")),
52
+ SummarizerRuntimePath: os.Getenv("LIBRAVDB_SUMMARIZER_RUNTIME"),
53
+ SummarizerModelPath: os.Getenv("LIBRAVDB_SUMMARIZER_MODEL_PATH"),
54
+ SummarizerTokenizerPath: os.Getenv("LIBRAVDB_SUMMARIZER_TOKENIZER"),
55
+ SummarizerModel: os.Getenv("LIBRAVDB_SUMMARIZER_MODEL"),
56
+ SummarizerEndpoint: os.Getenv("LIBRAVDB_SUMMARIZER_ENDPOINT"),
57
+ GatingW1c: envFloatOrDefault("LIBRAVDB_GATING_W1C", 0.35),
58
+ GatingW2c: envFloatOrDefault("LIBRAVDB_GATING_W2C", 0.40),
59
+ GatingW3c: envFloatOrDefault("LIBRAVDB_GATING_W3C", 0.25),
60
+ GatingW1t: envFloatOrDefault("LIBRAVDB_GATING_W1T", 0.40),
61
+ GatingW2t: envFloatOrDefault("LIBRAVDB_GATING_W2T", 0.35),
62
+ GatingW3t: envFloatOrDefault("LIBRAVDB_GATING_W3T", 0.25),
63
+ GatingTechNorm: envFloatOrDefault("LIBRAVDB_GATING_TECH_NORM", 1.5),
64
+ GatingThreshold: envFloatOrDefault("LIBRAVDB_GATING_THRESHOLD", 0.35),
65
+ GatingCentroidK: envIntOrDefault("LIBRAVDB_GATING_CENTROID_K", 10),
66
+ }
67
+ }
68
+
69
+ func envOrDefault(key, fallback string) string {
70
+ value := strings.TrimSpace(os.Getenv(key))
71
+ if value == "" {
72
+ return fallback
73
+ }
74
+ return value
75
+ }
76
+
77
+ func envIntOrDefault(key string, fallback int) int {
78
+ raw := strings.TrimSpace(os.Getenv(key))
79
+ if raw == "" {
80
+ return fallback
81
+ }
82
+ value, err := strconv.Atoi(raw)
83
+ if err != nil || value <= 0 {
84
+ return fallback
85
+ }
86
+ return value
87
+ }
88
+
89
+ func envBoolOrDefault(key string, fallback bool) bool {
90
+ raw := strings.TrimSpace(os.Getenv(key))
91
+ if raw == "" {
92
+ return fallback
93
+ }
94
+ value, err := strconv.ParseBool(raw)
95
+ if err != nil {
96
+ return fallback
97
+ }
98
+ return value
99
+ }
100
+
101
+ func envFloatOrDefault(key string, fallback float64) float64 {
102
+ raw := strings.TrimSpace(os.Getenv(key))
103
+ if raw == "" {
104
+ return fallback
105
+ }
106
+ value, err := strconv.ParseFloat(raw, 64)
107
+ if err != nil {
108
+ return fallback
109
+ }
110
+ return value
111
+ }
112
+
113
+ func defaultDBPath() string {
114
+ currentUser, err := user.Current()
115
+ if err != nil || strings.TrimSpace(currentUser.HomeDir) == "" {
116
+ return "./libravdb-data"
117
+ }
118
+ return filepath.Join(currentUser.HomeDir, ".clawdb", "data")
119
+ }
@@ -0,0 +1,75 @@
1
+ package config
2
+
3
+ import "testing"
4
+
5
+ func TestFromEnvDefaults(t *testing.T) {
6
+ t.Setenv("LIBRAVDB_DB_PATH", "")
7
+ t.Setenv("LIBRAVDB_ONNX_RUNTIME", "")
8
+ t.Setenv("LIBRAVDB_EMBEDDING_BACKEND", "")
9
+ t.Setenv("LIBRAVDB_EMBEDDING_PROFILE", "")
10
+ t.Setenv("LIBRAVDB_EMBEDDING_MODEL", "")
11
+ t.Setenv("LIBRAVDB_EMBEDDING_TOKENIZER", "")
12
+ t.Setenv("LIBRAVDB_EMBEDDING_DIMENSIONS", "")
13
+ t.Setenv("LIBRAVDB_EMBEDDING_NORMALIZE", "")
14
+
15
+ cfg := FromEnv()
16
+ if cfg.DBPath == "" {
17
+ t.Fatalf("expected non-empty default db path")
18
+ }
19
+ if cfg.EmbeddingBackend != "bundled" {
20
+ t.Fatalf("expected bundled backend, got %q", cfg.EmbeddingBackend)
21
+ }
22
+ if cfg.EmbeddingProfile != "nomic-embed-text-v1.5" {
23
+ t.Fatalf("expected Nomic default profile, got %q", cfg.EmbeddingProfile)
24
+ }
25
+ if cfg.FallbackProfile != "all-minilm-l6-v2" {
26
+ t.Fatalf("expected MiniLM fallback profile, got %q", cfg.FallbackProfile)
27
+ }
28
+ if cfg.EmbeddingDimensions != 0 {
29
+ t.Fatalf("expected unspecified dimensions to default to 0, got %d", cfg.EmbeddingDimensions)
30
+ }
31
+ if !cfg.EmbeddingNormalize {
32
+ t.Fatalf("expected normalize=true by default")
33
+ }
34
+ }
35
+
36
+ func TestFromEnvReadsPowerUserEmbeddingSettings(t *testing.T) {
37
+ t.Setenv("LIBRAVDB_DB_PATH", "/tmp/libravdb")
38
+ t.Setenv("LIBRAVDB_ONNX_RUNTIME", "/opt/onnx/libonnxruntime.so")
39
+ t.Setenv("LIBRAVDB_EMBEDDING_BACKEND", "custom-local")
40
+ t.Setenv("LIBRAVDB_EMBEDDING_PROFILE", "nomic-embed-text-v1.5")
41
+ t.Setenv("LIBRAVDB_FALLBACK_PROFILE", "all-minilm-l6-v2")
42
+ t.Setenv("LIBRAVDB_EMBEDDING_MODEL", "/models/custom.onnx")
43
+ t.Setenv("LIBRAVDB_EMBEDDING_TOKENIZER", "/models/tokenizer.json")
44
+ t.Setenv("LIBRAVDB_EMBEDDING_DIMENSIONS", "768")
45
+ t.Setenv("LIBRAVDB_EMBEDDING_NORMALIZE", "false")
46
+
47
+ cfg := FromEnv()
48
+ if cfg.DBPath != "/tmp/libravdb" {
49
+ t.Fatalf("unexpected db path %q", cfg.DBPath)
50
+ }
51
+ if cfg.ONNXRuntimePath != "/opt/onnx/libonnxruntime.so" {
52
+ t.Fatalf("unexpected runtime path %q", cfg.ONNXRuntimePath)
53
+ }
54
+ if cfg.EmbeddingBackend != "custom-local" {
55
+ t.Fatalf("unexpected backend %q", cfg.EmbeddingBackend)
56
+ }
57
+ if cfg.EmbeddingProfile != "nomic-embed-text-v1.5" {
58
+ t.Fatalf("unexpected profile %q", cfg.EmbeddingProfile)
59
+ }
60
+ if cfg.FallbackProfile != "all-minilm-l6-v2" {
61
+ t.Fatalf("unexpected fallback profile %q", cfg.FallbackProfile)
62
+ }
63
+ if cfg.EmbeddingModelPath != "/models/custom.onnx" {
64
+ t.Fatalf("unexpected model path %q", cfg.EmbeddingModelPath)
65
+ }
66
+ if cfg.EmbeddingTokenizerPath != "/models/tokenizer.json" {
67
+ t.Fatalf("unexpected tokenizer path %q", cfg.EmbeddingTokenizerPath)
68
+ }
69
+ if cfg.EmbeddingDimensions != 768 {
70
+ t.Fatalf("unexpected dimensions %d", cfg.EmbeddingDimensions)
71
+ }
72
+ if cfg.EmbeddingNormalize {
73
+ t.Fatalf("expected normalize=false")
74
+ }
75
+ }