@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,360 @@
1
+ package summarize
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "math"
7
+ "sort"
8
+ "strings"
9
+
10
+ "github.com/xDarkicex/openclaw-memory-libravdb/sidecar/embed"
11
+ "github.com/xDarkicex/openclaw-memory-libravdb/sidecar/model"
12
+ )
13
+
14
+ const DefaultBackend = "bundled"
15
+
16
+ type Config struct {
17
+ Backend string
18
+ Profile string
19
+ RuntimePath string
20
+ ModelPath string
21
+ TokenizerPath string
22
+ Model string
23
+ Endpoint string
24
+ }
25
+
26
+ type Turn struct {
27
+ ID string
28
+ Text string
29
+ }
30
+
31
+ type Summary struct {
32
+ Text string
33
+ SourceIDs []string
34
+ Method string
35
+ TokenCount int
36
+
37
+ // Confidence ∈ [0,1] — quality signal fed into temporal decay rate
38
+ // of the inserted summary record in the vector store.
39
+ //
40
+ // Extractive: mean cosine similarity of selected turns to cluster centroid.
41
+ // Abstractive (ONNX): normalized mean log-probability of generated tokens.
42
+ // A higher confidence summary decays more slowly in the retrieval model.
43
+ Confidence float64
44
+ }
45
+
46
+ type SummaryOpts struct {
47
+ MaxOutputTokens int
48
+ MinInputTurns int
49
+ TargetDensity float64
50
+ }
51
+
52
+ type Profile struct {
53
+ Backend string `json:"backend"`
54
+ Family string `json:"family,omitempty"`
55
+ Model string `json:"model,omitempty"`
56
+ ModelPath string `json:"modelPath,omitempty"`
57
+ Endpoint string `json:"endpoint,omitempty"`
58
+ Fingerprint string `json:"fingerprint,omitempty"`
59
+ }
60
+
61
+ type Summarizer interface {
62
+ Summarize(context.Context, []Turn, SummaryOpts) (Summary, error)
63
+ Profile() Profile
64
+ Warmup(context.Context) error
65
+ Unload()
66
+ Close() error
67
+ Ready() bool
68
+ Reason() string
69
+ Mode() string
70
+ }
71
+
72
+ type Engine struct {
73
+ backend summarizerBackend
74
+ }
75
+
76
+ type summarizerBackend interface {
77
+ Summarize(context.Context, []Turn, SummaryOpts) (Summary, error)
78
+ Warmup(context.Context) error
79
+ Unload()
80
+ Close() error
81
+ Profile() Profile
82
+ Ready() bool
83
+ Reason() string
84
+ Mode() string
85
+ }
86
+
87
+ type Dependencies struct {
88
+ Embedder embed.Embedder
89
+ Registry *model.Registry
90
+ TokenizerLoader func(string) (Tokenizer, error)
91
+ }
92
+
93
+ type unavailableBackend struct {
94
+ reason string
95
+ mode string
96
+ profile Profile
97
+ }
98
+
99
+ type ExtractiveSummarizer struct {
100
+ embedder embed.Embedder
101
+ profile Profile
102
+ }
103
+
104
+ func NewWithConfig(cfg Config) *Engine {
105
+ return NewWithDeps(cfg, Dependencies{})
106
+ }
107
+
108
+ func NewWithDeps(cfg Config, deps Dependencies) *Engine {
109
+ cfg.Backend = strings.TrimSpace(cfg.Backend)
110
+ if cfg.Backend == "" {
111
+ cfg.Backend = DefaultBackend
112
+ }
113
+ cfg.Profile = strings.TrimSpace(cfg.Profile)
114
+
115
+ if strings.EqualFold(cfg.Backend, "extractive") {
116
+ return NewExtractive(deps.Embedder, cfg.Profile)
117
+ }
118
+
119
+ if cfg.Backend == "onnx-local" {
120
+ backend, err := newONNXLocalBackend(cfg, deps)
121
+ if err == nil {
122
+ return &Engine{backend: backend}
123
+ }
124
+ return &Engine{backend: unavailable(cfg, err.Error())}
125
+ }
126
+
127
+ if cfg.Backend == "bundled" {
128
+ if deps.Embedder != nil && strings.EqualFold(cfg.Profile, "extractive") {
129
+ return NewExtractive(deps.Embedder, firstNonEmpty(cfg.Profile, "extractive"))
130
+ }
131
+ }
132
+
133
+ return &Engine{
134
+ backend: unavailable(cfg, unavailableReason(cfg)),
135
+ }
136
+ }
137
+
138
+ func NewExtractive(embedder embed.Embedder, profileName string) *Engine {
139
+ if embedder == nil {
140
+ return &Engine{
141
+ backend: unavailableBackend{
142
+ reason: "extractive summarizer requires embedder",
143
+ mode: "unavailable",
144
+ profile: Profile{
145
+ Backend: "extractive",
146
+ Family: firstNonEmpty(profileName, "extractive"),
147
+ },
148
+ },
149
+ }
150
+ }
151
+ profile := Profile{
152
+ Backend: "extractive",
153
+ Family: firstNonEmpty(profileName, "extractive"),
154
+ }
155
+ return &Engine{
156
+ backend: &ExtractiveSummarizer{
157
+ embedder: embedder,
158
+ profile: profile,
159
+ },
160
+ }
161
+ }
162
+
163
+ func (e *Engine) Summarize(ctx context.Context, turns []Turn, opts SummaryOpts) (Summary, error) {
164
+ if !e.backend.Ready() {
165
+ return Summary{}, fmt.Errorf("summarizer not ready: %s", e.backend.Reason())
166
+ }
167
+ return e.backend.Summarize(ctx, turns, normalizeSummaryOpts(opts))
168
+ }
169
+
170
+ func (e *Engine) Profile() Profile { return e.backend.Profile() }
171
+ func (e *Engine) Ready() bool { return e.backend.Ready() }
172
+ func (e *Engine) Reason() string { return e.backend.Reason() }
173
+ func (e *Engine) Mode() string { return e.backend.Mode() }
174
+ func (e *Engine) Warmup(ctx context.Context) error { return e.backend.Warmup(ctx) }
175
+ func (e *Engine) Unload() { e.backend.Unload() }
176
+ func (e *Engine) Close() error { return e.backend.Close() }
177
+
178
+ func (b unavailableBackend) Summarize(_ context.Context, _ []Turn, _ SummaryOpts) (Summary, error) {
179
+ return Summary{}, fmt.Errorf("summarizer backend is unavailable: %s", b.reason)
180
+ }
181
+
182
+ func (b unavailableBackend) Warmup(_ context.Context) error {
183
+ return fmt.Errorf("summarizer backend is unavailable: %s", b.reason)
184
+ }
185
+ func (b unavailableBackend) Unload() {}
186
+ func (b unavailableBackend) Close() error { return nil }
187
+ func (b unavailableBackend) Profile() Profile { return b.profile }
188
+ func (b unavailableBackend) Ready() bool { return false }
189
+ func (b unavailableBackend) Reason() string { return b.reason }
190
+ func (b unavailableBackend) Mode() string { return b.mode }
191
+
192
+ func (s *ExtractiveSummarizer) Summarize(_ context.Context, turns []Turn, opts SummaryOpts) (Summary, error) {
193
+ opts = normalizeSummaryOpts(opts)
194
+ if len(turns) == 0 {
195
+ return Summary{}, fmt.Errorf("no turns to summarize")
196
+ }
197
+ if len(turns) < opts.MinInputTurns {
198
+ return Summary{}, fmt.Errorf("need at least %d turns for summarization, got %d", opts.MinInputTurns, len(turns))
199
+ }
200
+
201
+ embeddings := make([][]float32, 0, len(turns))
202
+ for _, turn := range turns {
203
+ vec, err := s.embedder.EmbedDocument(context.Background(), turn.Text)
204
+ if err != nil {
205
+ return Summary{}, err
206
+ }
207
+ embeddings = append(embeddings, vec)
208
+ }
209
+
210
+ centroid := meanVector(embeddings)
211
+ type scoredTurn struct {
212
+ index int
213
+ score float64
214
+ }
215
+ scored := make([]scoredTurn, 0, len(turns))
216
+ for i, vec := range embeddings {
217
+ scored = append(scored, scoredTurn{
218
+ index: i,
219
+ score: cosine(vec, centroid),
220
+ })
221
+ }
222
+ sort.Slice(scored, func(i, j int) bool {
223
+ if scored[i].score == scored[j].score {
224
+ return scored[i].index < scored[j].index
225
+ }
226
+ return scored[i].score > scored[j].score
227
+ })
228
+
229
+ targetCount := int(math.Ceil(float64(len(turns)) * opts.TargetDensity))
230
+ if targetCount < 1 {
231
+ targetCount = 1
232
+ }
233
+ if targetCount > len(turns) {
234
+ targetCount = len(turns)
235
+ }
236
+ selected := scored[:targetCount]
237
+ sort.Slice(selected, func(i, j int) bool { return selected[i].index < selected[j].index })
238
+
239
+ sourceIDs := make([]string, 0, len(selected))
240
+ parts := make([]string, 0, len(selected))
241
+ var totalConfidence float64
242
+ for _, pick := range selected {
243
+ sourceIDs = append(sourceIDs, turns[pick.index].ID)
244
+ parts = append(parts, strings.TrimSpace(turns[pick.index].Text))
245
+ totalConfidence += pick.score
246
+ }
247
+ text := strings.TrimSpace(strings.Join(parts, " "))
248
+ return Summary{
249
+ Text: text,
250
+ SourceIDs: sourceIDs,
251
+ Method: "extractive",
252
+ TokenCount: tokenCount(text),
253
+ Confidence: clamp01(totalConfidence / float64(len(selected))),
254
+ }, nil
255
+ }
256
+
257
+ func (s *ExtractiveSummarizer) Profile() Profile { return s.profile }
258
+ func (s *ExtractiveSummarizer) Warmup(_ context.Context) error { return nil }
259
+ func (s *ExtractiveSummarizer) Unload() {}
260
+ func (s *ExtractiveSummarizer) Close() error { return nil }
261
+ func (s *ExtractiveSummarizer) Ready() bool { return true }
262
+ func (s *ExtractiveSummarizer) Reason() string { return "" }
263
+ func (s *ExtractiveSummarizer) Mode() string { return "extractive" }
264
+
265
+ func normalizeSummaryOpts(opts SummaryOpts) SummaryOpts {
266
+ if opts.MinInputTurns <= 0 {
267
+ opts.MinInputTurns = 2
268
+ }
269
+ if opts.MaxOutputTokens <= 0 {
270
+ opts.MaxOutputTokens = 64
271
+ }
272
+ if opts.TargetDensity <= 0 || opts.TargetDensity > 1 {
273
+ opts.TargetDensity = 0.4
274
+ }
275
+ return opts
276
+ }
277
+
278
+ func unavailableReason(cfg Config) string {
279
+ switch cfg.Backend {
280
+ case "bundled":
281
+ return "bundled summarizer profile not implemented yet"
282
+ case "onnx-local":
283
+ return "onnx-local summarizer requires ONNX runtime path and summarizer model directory/manifest"
284
+ case "ollama-local":
285
+ if strings.TrimSpace(cfg.Model) == "" || strings.TrimSpace(cfg.Endpoint) == "" {
286
+ return "ollama-local summarizer requires endpoint and model"
287
+ }
288
+ return "ollama-local summarizer backend not implemented yet"
289
+ case "custom-local":
290
+ return "custom-local summarizer backend not implemented yet"
291
+ default:
292
+ return fmt.Sprintf("unsupported summarizer backend: %s", cfg.Backend)
293
+ }
294
+ }
295
+
296
+ func unavailable(cfg Config, reason string) unavailableBackend {
297
+ return unavailableBackend{
298
+ reason: reason,
299
+ mode: "unavailable",
300
+ profile: Profile{
301
+ Backend: cfg.Backend,
302
+ Family: firstNonEmpty(cfg.Profile, cfg.Backend),
303
+ Model: strings.TrimSpace(cfg.Model),
304
+ ModelPath: strings.TrimSpace(cfg.ModelPath),
305
+ Endpoint: strings.TrimSpace(cfg.Endpoint),
306
+ },
307
+ }
308
+ }
309
+
310
+ func meanVector(vectors [][]float32) []float32 {
311
+ if len(vectors) == 0 {
312
+ return nil
313
+ }
314
+ centroid := make([]float32, len(vectors[0]))
315
+ for _, vec := range vectors {
316
+ for i := range vec {
317
+ centroid[i] += vec[i]
318
+ }
319
+ }
320
+ scale := float32(len(vectors))
321
+ for i := range centroid {
322
+ centroid[i] /= scale
323
+ }
324
+ return centroid
325
+ }
326
+
327
+ func cosine(a, b []float32) float64 {
328
+ if len(a) == 0 || len(a) != len(b) {
329
+ return 0
330
+ }
331
+ var dot, normA, normB float64
332
+ for i := range a {
333
+ av := float64(a[i])
334
+ bv := float64(b[i])
335
+ dot += av * bv
336
+ normA += av * av
337
+ normB += bv * bv
338
+ }
339
+ if normA == 0 || normB == 0 {
340
+ return 0
341
+ }
342
+ return dot / (math.Sqrt(normA) * math.Sqrt(normB))
343
+ }
344
+
345
+ func tokenCount(text string) int {
346
+ if strings.TrimSpace(text) == "" {
347
+ return 0
348
+ }
349
+ return len(strings.Fields(text))
350
+ }
351
+
352
+ func clamp01(v float64) float64 {
353
+ if v < 0 {
354
+ return 0
355
+ }
356
+ if v > 1 {
357
+ return 1
358
+ }
359
+ return v
360
+ }
@@ -0,0 +1,148 @@
1
+ package summarize
2
+
3
+ import (
4
+ "context"
5
+ "os"
6
+ "path/filepath"
7
+ "testing"
8
+
9
+ "github.com/xDarkicex/openclaw-memory-libravdb/sidecar/embed"
10
+ )
11
+
12
+ type fakeEmbedder struct {
13
+ vectors map[string][]float32
14
+ }
15
+
16
+ type fakeTokenizer struct{}
17
+
18
+ func (f fakeEmbedder) EmbedDocument(_ context.Context, text string) ([]float32, error) {
19
+ if vec, ok := f.vectors[text]; ok {
20
+ return append([]float32(nil), vec...), nil
21
+ }
22
+ return []float32{0, 0}, nil
23
+ }
24
+
25
+ func (f fakeEmbedder) EmbedQuery(_ context.Context, text string) ([]float32, error) {
26
+ return f.EmbedDocument(context.Background(), text)
27
+ }
28
+
29
+ func (f fakeEmbedder) Dimensions() int { return 2 }
30
+ func (f fakeEmbedder) Profile() embed.Profile {
31
+ return embed.Profile{
32
+ Backend: "test",
33
+ Family: "test",
34
+ Dimensions: 2,
35
+ }
36
+ }
37
+ func (f fakeEmbedder) Ready() bool { return true }
38
+ func (f fakeEmbedder) Reason() string { return "" }
39
+ func (f fakeEmbedder) Mode() string { return "primary" }
40
+
41
+ func (f fakeTokenizer) Encode(text string) ([]int64, error) { return []int64{0, 1}, nil }
42
+ func (f fakeTokenizer) Decode(ids []int64) (string, error) { return "decoded", nil }
43
+ func (f fakeTokenizer) VocabSize() int { return 32_128 }
44
+ func (f fakeTokenizer) BOS() int64 { return 0 }
45
+ func (f fakeTokenizer) EOS() int64 { return 1 }
46
+ func (f fakeTokenizer) PAD() int64 { return 0 }
47
+
48
+ func TestNewWithConfigOllamaLocalRequiresEndpointAndModel(t *testing.T) {
49
+ engine := NewWithConfig(Config{
50
+ Backend: "ollama-local",
51
+ Model: "llama3",
52
+ })
53
+ if engine.Ready() {
54
+ t.Fatalf("expected ollama-local without endpoint to be unavailable")
55
+ }
56
+ if engine.Mode() != "unavailable" {
57
+ t.Fatalf("unexpected mode %q", engine.Mode())
58
+ }
59
+ }
60
+
61
+ func TestNewExtractiveSummarizerComputesConfidenceFromCentroidSimilarity(t *testing.T) {
62
+ engine := NewExtractive(fakeEmbedder{
63
+ vectors: map[string][]float32{
64
+ "a": {1, 0},
65
+ "b": {0.9, 0.1},
66
+ "c": {0, 1},
67
+ },
68
+ }, "extractive-test")
69
+
70
+ summary, err := engine.Summarize(context.Background(), []Turn{
71
+ {ID: "a", Text: "a"},
72
+ {ID: "b", Text: "b"},
73
+ {ID: "c", Text: "c"},
74
+ }, SummaryOpts{
75
+ MinInputTurns: 2,
76
+ TargetDensity: 1.0 / 3.0,
77
+ })
78
+ if err != nil {
79
+ t.Fatalf("Summarize() error = %v", err)
80
+ }
81
+ if summary.Method != "extractive" {
82
+ t.Fatalf("unexpected method %q", summary.Method)
83
+ }
84
+ if len(summary.SourceIDs) != 1 || summary.SourceIDs[0] != "b" {
85
+ t.Fatalf("unexpected source ids %#v", summary.SourceIDs)
86
+ }
87
+ if summary.Confidence <= 0 || summary.Confidence > 1 {
88
+ t.Fatalf("expected confidence in (0,1], got %f", summary.Confidence)
89
+ }
90
+ }
91
+
92
+ func TestNewExtractiveRequiresEmbedder(t *testing.T) {
93
+ engine := NewExtractive(nil, "extractive")
94
+ if engine.Ready() {
95
+ t.Fatalf("expected nil-embedder extractive summarizer to be unavailable")
96
+ }
97
+ }
98
+
99
+ func TestNewWithDepsONNXLocalLoadsManifestMetadata(t *testing.T) {
100
+ dir := t.TempDir()
101
+ manifest := `{
102
+ "backend":"onnx-local",
103
+ "profile":"t5-small",
104
+ "family":"t5-small",
105
+ "encoder":"encoder.onnx",
106
+ "decoder":"decoder.onnx",
107
+ "tokenizer":"tokenizer.json",
108
+ "maxContextTokens":512
109
+ }`
110
+ if err := os.WriteFile(filepath.Join(dir, "summarizer.json"), []byte(manifest), 0o644); err != nil {
111
+ t.Fatalf("WriteFile() error = %v", err)
112
+ }
113
+
114
+ engine := NewWithDeps(Config{
115
+ Backend: "onnx-local",
116
+ Profile: "t5-small",
117
+ RuntimePath: "/tmp/libonnxruntime.dylib",
118
+ ModelPath: dir,
119
+ }, Dependencies{
120
+ TokenizerLoader: func(path string) (Tokenizer, error) { return fakeTokenizer{}, nil },
121
+ })
122
+
123
+ if !engine.Ready() {
124
+ t.Fatalf("expected onnx-local engine to be ready, reason=%q", engine.Reason())
125
+ }
126
+ if engine.Mode() != "onnx-local" {
127
+ t.Fatalf("unexpected mode %q", engine.Mode())
128
+ }
129
+ if engine.Profile().Family != "t5-small" {
130
+ t.Fatalf("unexpected family %q", engine.Profile().Family)
131
+ }
132
+ if engine.Profile().Fingerprint == "" {
133
+ t.Fatalf("expected fingerprint to be populated")
134
+ }
135
+ }
136
+
137
+ func TestNewWithDepsONNXLocalRequiresRuntime(t *testing.T) {
138
+ engine := NewWithDeps(Config{
139
+ Backend: "onnx-local",
140
+ ModelPath: t.TempDir(),
141
+ }, Dependencies{
142
+ TokenizerLoader: func(path string) (Tokenizer, error) { return fakeTokenizer{}, nil },
143
+ })
144
+
145
+ if engine.Ready() {
146
+ t.Fatalf("expected onnx-local without runtime to be unavailable")
147
+ }
148
+ }