@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.
- package/README.md +46 -0
- package/docs/README.md +14 -0
- package/docs/architecture-decisions/README.md +6 -0
- package/docs/architecture-decisions/adr-001-onnx-over-ollama.md +21 -0
- package/docs/architecture-decisions/adr-002-libravdb-over-lancedb.md +19 -0
- package/docs/architecture-decisions/adr-003-convex-gating-over-threshold.md +27 -0
- package/docs/architecture-decisions/adr-004-sidecar-over-native-ts.md +21 -0
- package/docs/architecture.md +188 -0
- package/docs/contributing.md +76 -0
- package/docs/dependencies.md +38 -0
- package/docs/embedding-profiles.md +42 -0
- package/docs/gating.md +329 -0
- package/docs/implementation.md +381 -0
- package/docs/installation.md +272 -0
- package/docs/mathematics.md +695 -0
- package/docs/models.md +63 -0
- package/docs/problem.md +64 -0
- package/docs/security.md +86 -0
- package/openclaw.plugin.json +84 -0
- package/package.json +41 -0
- package/scripts/build-sidecar.sh +30 -0
- package/scripts/postinstall.js +169 -0
- package/scripts/setup.sh +20 -0
- package/scripts/setup.ts +505 -0
- package/scripts/sidecar-release.d.ts +4 -0
- package/scripts/sidecar-release.js +17 -0
- package/sidecar/cmd/inspect_onnx/main.go +105 -0
- package/sidecar/compact/gate.go +273 -0
- package/sidecar/compact/gate_test.go +85 -0
- package/sidecar/compact/summarize.go +345 -0
- package/sidecar/compact/summarize_test.go +319 -0
- package/sidecar/compact/tokens.go +11 -0
- package/sidecar/config/config.go +119 -0
- package/sidecar/config/config_test.go +75 -0
- package/sidecar/embed/engine.go +696 -0
- package/sidecar/embed/engine_test.go +349 -0
- package/sidecar/embed/matryoshka.go +93 -0
- package/sidecar/embed/matryoshka_test.go +150 -0
- package/sidecar/embed/onnx_local.go +319 -0
- package/sidecar/embed/onnx_local_test.go +159 -0
- package/sidecar/embed/profile_contract_test.go +71 -0
- package/sidecar/embed/profile_eval_test.go +923 -0
- package/sidecar/embed/profiles.go +39 -0
- package/sidecar/go.mod +21 -0
- package/sidecar/go.sum +30 -0
- package/sidecar/health/check.go +33 -0
- package/sidecar/health/check_test.go +55 -0
- package/sidecar/main.go +151 -0
- package/sidecar/model/encoder.go +222 -0
- package/sidecar/model/registry.go +262 -0
- package/sidecar/model/registry_test.go +102 -0
- package/sidecar/model/seq2seq.go +133 -0
- package/sidecar/server/rpc.go +343 -0
- package/sidecar/server/rpc_test.go +350 -0
- package/sidecar/server/transport.go +160 -0
- package/sidecar/store/libravdb.go +676 -0
- package/sidecar/store/libravdb_test.go +472 -0
- package/sidecar/summarize/engine.go +360 -0
- package/sidecar/summarize/engine_test.go +148 -0
- package/sidecar/summarize/onnx_local.go +494 -0
- package/sidecar/summarize/onnx_local_test.go +48 -0
- package/sidecar/summarize/profiles.go +52 -0
- package/sidecar/summarize/tokenizer.go +13 -0
- package/sidecar/summarize/tokenizer_hf.go +76 -0
- package/sidecar/summarize/util.go +13 -0
- package/src/cli.ts +205 -0
- package/src/context-engine.ts +195 -0
- package/src/index.ts +27 -0
- package/src/memory-provider.ts +24 -0
- package/src/openclaw-plugin-sdk.d.ts +53 -0
- package/src/plugin-runtime.ts +67 -0
- package/src/recall-cache.ts +34 -0
- package/src/recall-utils.ts +22 -0
- package/src/rpc.ts +84 -0
- package/src/scoring.ts +58 -0
- package/src/sidecar.ts +506 -0
- package/src/tokens.ts +36 -0
- package/src/types.ts +146 -0
- package/tsconfig.json +20 -0
- 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
|
+
}
|