@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,319 @@
|
|
|
1
|
+
package embed
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/sha256"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
"github.com/sugarme/tokenizer"
|
|
13
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/model"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const defaultManifestName = "embedding.json"
|
|
17
|
+
|
|
18
|
+
type Profile struct {
|
|
19
|
+
Backend string `json:"backend"`
|
|
20
|
+
Family string `json:"family,omitempty"`
|
|
21
|
+
Dimensions int `json:"dimensions"`
|
|
22
|
+
Normalize bool `json:"normalize"`
|
|
23
|
+
MaxContextTokens int `json:"maxContextTokens,omitempty"`
|
|
24
|
+
ModelPath string `json:"modelPath,omitempty"`
|
|
25
|
+
Tokenizer string `json:"tokenizerPath,omitempty"`
|
|
26
|
+
Fingerprint string `json:"fingerprint"`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type embeddingManifest struct {
|
|
30
|
+
Backend string `json:"backend,omitempty"`
|
|
31
|
+
Profile string `json:"profile,omitempty"`
|
|
32
|
+
Family string `json:"family,omitempty"`
|
|
33
|
+
Model string `json:"model"`
|
|
34
|
+
Tokenizer string `json:"tokenizer,omitempty"`
|
|
35
|
+
Dimensions int `json:"dimensions"`
|
|
36
|
+
Normalize *bool `json:"normalize,omitempty"`
|
|
37
|
+
InputNames []string `json:"inputNames,omitempty"`
|
|
38
|
+
OutputName string `json:"outputName,omitempty"`
|
|
39
|
+
AddSpecialTokens *bool `json:"addSpecialTokens,omitempty"`
|
|
40
|
+
Pooling string `json:"pooling,omitempty"`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type onnxLocalSpec struct {
|
|
44
|
+
Backend string
|
|
45
|
+
Family string
|
|
46
|
+
RuntimePath string
|
|
47
|
+
ModelPath string
|
|
48
|
+
TokenizerPath string
|
|
49
|
+
Dimensions int
|
|
50
|
+
Normalize bool
|
|
51
|
+
InputNames []string
|
|
52
|
+
OutputName string
|
|
53
|
+
AddSpecialTokens bool
|
|
54
|
+
Pooling string
|
|
55
|
+
Profile Profile
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type onnxLocalModel struct {
|
|
59
|
+
encoder *model.EncoderModel
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
|
|
63
|
+
return newFileONNXBackend(spec)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func resolveONNXLocalSpec(cfg Config) (onnxLocalSpec, error) {
|
|
67
|
+
manifestPath, err := resolveManifestPath(cfg.ModelPath)
|
|
68
|
+
if err != nil {
|
|
69
|
+
return onnxLocalSpec{}, err
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
manifest, err := readManifest(manifestPath)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return onnxLocalSpec{}, err
|
|
75
|
+
}
|
|
76
|
+
selectedProfile, hasProfile := lookupProfile(firstNonEmpty(cfg.Profile, manifest.Profile))
|
|
77
|
+
|
|
78
|
+
baseDir := filepath.Dir(manifestPath)
|
|
79
|
+
modelPath := resolveManifestAsset(baseDir, cfg.modelAssetOverride(), manifest.Model)
|
|
80
|
+
if modelPath == "" {
|
|
81
|
+
return onnxLocalSpec{}, fmt.Errorf("onnx-local manifest missing model path")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
tokenizerPath := resolveManifestAsset(baseDir, cfg.tokenizerAssetOverride(), manifest.Tokenizer)
|
|
85
|
+
if tokenizerPath == "" {
|
|
86
|
+
return onnxLocalSpec{}, fmt.Errorf("onnx-local manifest missing tokenizer path")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if cfg.Dimensions > 0 && manifest.Dimensions > 0 && cfg.Dimensions != manifest.Dimensions {
|
|
90
|
+
return onnxLocalSpec{}, fmt.Errorf("onnx-local manifest dimensions %d do not match configured dimensions %d", manifest.Dimensions, cfg.Dimensions)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dimensions := manifest.Dimensions
|
|
94
|
+
if dimensions <= 0 && hasProfile {
|
|
95
|
+
dimensions = selectedProfile.Dimensions
|
|
96
|
+
}
|
|
97
|
+
if dimensions <= 0 {
|
|
98
|
+
dimensions = cfg.Dimensions
|
|
99
|
+
}
|
|
100
|
+
if dimensions <= 0 {
|
|
101
|
+
return onnxLocalSpec{}, fmt.Errorf("onnx-local dimensions must be configured in embedding.json or plugin config")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
normalize := cfg.Normalize
|
|
105
|
+
if manifest.Normalize != nil {
|
|
106
|
+
normalize = *manifest.Normalize
|
|
107
|
+
} else if hasProfile {
|
|
108
|
+
normalize = selectedProfile.Normalize
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
inputNames := manifest.InputNames
|
|
112
|
+
if len(inputNames) == 0 {
|
|
113
|
+
inputNames = []string{"input_ids", "attention_mask", "token_type_ids"}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
outputName := strings.TrimSpace(manifest.OutputName)
|
|
117
|
+
if outputName == "" {
|
|
118
|
+
outputName = "sentence_embedding"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
addSpecialTokens := true
|
|
122
|
+
if manifest.AddSpecialTokens != nil {
|
|
123
|
+
addSpecialTokens = *manifest.AddSpecialTokens
|
|
124
|
+
}
|
|
125
|
+
pooling := strings.TrimSpace(manifest.Pooling)
|
|
126
|
+
if pooling == "" && outputName == "last_hidden_state" {
|
|
127
|
+
pooling = "mean"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
profile := buildProfile(Profile{
|
|
131
|
+
Backend: "onnx-local",
|
|
132
|
+
Family: firstNonEmpty(strings.TrimSpace(manifest.Family), selectedProfile.Family, strings.TrimSpace(manifest.Backend), "onnx-local"),
|
|
133
|
+
Dimensions: dimensions,
|
|
134
|
+
Normalize: normalize,
|
|
135
|
+
MaxContextTokens: selectedProfile.MaxContextTokens,
|
|
136
|
+
ModelPath: modelPath,
|
|
137
|
+
Tokenizer: tokenizerPath,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return onnxLocalSpec{
|
|
141
|
+
Backend: "onnx-local",
|
|
142
|
+
Family: profile.Family,
|
|
143
|
+
RuntimePath: strings.TrimSpace(cfg.RuntimePath),
|
|
144
|
+
ModelPath: modelPath,
|
|
145
|
+
TokenizerPath: tokenizerPath,
|
|
146
|
+
Dimensions: dimensions,
|
|
147
|
+
Normalize: normalize,
|
|
148
|
+
InputNames: append([]string(nil), inputNames...),
|
|
149
|
+
OutputName: outputName,
|
|
150
|
+
AddSpecialTokens: addSpecialTokens,
|
|
151
|
+
Pooling: pooling,
|
|
152
|
+
Profile: profile,
|
|
153
|
+
}, nil
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func newFileONNXBackend(spec onnxLocalSpec) (embeddingBackend, error) {
|
|
157
|
+
encoder, err := model.DefaultRegistry().LoadEncoder(model.EncoderSpec{
|
|
158
|
+
Key: spec.Profile.Fingerprint,
|
|
159
|
+
Profile: model.Profile{
|
|
160
|
+
Name: spec.Profile.Fingerprint,
|
|
161
|
+
Family: spec.Profile.Family,
|
|
162
|
+
Task: model.TaskEmbedding,
|
|
163
|
+
Dims: spec.Dimensions,
|
|
164
|
+
Normalize: spec.Normalize,
|
|
165
|
+
ModelPath: spec.ModelPath,
|
|
166
|
+
TokenizerPath: spec.TokenizerPath,
|
|
167
|
+
OrtLibPath: spec.RuntimePath,
|
|
168
|
+
},
|
|
169
|
+
RuntimePath: spec.RuntimePath,
|
|
170
|
+
ModelPath: spec.ModelPath,
|
|
171
|
+
TokenizerPath: spec.TokenizerPath,
|
|
172
|
+
InputNames: spec.InputNames,
|
|
173
|
+
OutputName: spec.OutputName,
|
|
174
|
+
Dimensions: spec.Dimensions,
|
|
175
|
+
AddSpecialTokens: spec.AddSpecialTokens,
|
|
176
|
+
Pooling: spec.Pooling,
|
|
177
|
+
})
|
|
178
|
+
if err != nil {
|
|
179
|
+
return nil, err
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return miniLMBackend{
|
|
183
|
+
model: onnxLocalModel{
|
|
184
|
+
encoder: encoder,
|
|
185
|
+
},
|
|
186
|
+
normalize: spec.Normalize,
|
|
187
|
+
}, nil
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func (m onnxLocalModel) Compute(sentence string, _ bool) ([]float32, error) {
|
|
191
|
+
if m.encoder == nil {
|
|
192
|
+
return nil, fmt.Errorf("encoder model not loaded")
|
|
193
|
+
}
|
|
194
|
+
return m.encoder.EmbedText(sentence)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func (m onnxLocalModel) TokenCount(sentence string, _ bool) (int, error) {
|
|
198
|
+
if m.encoder == nil {
|
|
199
|
+
return 0, fmt.Errorf("encoder model not loaded")
|
|
200
|
+
}
|
|
201
|
+
return m.encoder.TokenCount(sentence)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func (m onnxLocalModel) Encode(sentence string, _ bool) (*tokenizer.Encoding, error) {
|
|
205
|
+
if m.encoder == nil {
|
|
206
|
+
return nil, fmt.Errorf("encoder model not loaded")
|
|
207
|
+
}
|
|
208
|
+
return m.encoder.EncodeText(sentence, true)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func (m onnxLocalModel) ComputeEncoding(encoding tokenizer.Encoding) ([]float32, error) {
|
|
212
|
+
if m.encoder == nil {
|
|
213
|
+
return nil, fmt.Errorf("encoder model not loaded")
|
|
214
|
+
}
|
|
215
|
+
return m.encoder.EmbedEncoding(encoding)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func meanPoolLastHiddenState(flat []float32, attentionMask []int, seqLength int, dimensions int) []float32 {
|
|
219
|
+
vec := make([]float32, dimensions)
|
|
220
|
+
var denom float32
|
|
221
|
+
for tokenIdx := 0; tokenIdx < seqLength; tokenIdx++ {
|
|
222
|
+
if tokenIdx >= len(attentionMask) || attentionMask[tokenIdx] == 0 {
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
denom++
|
|
226
|
+
base := tokenIdx * dimensions
|
|
227
|
+
for dim := 0; dim < dimensions; dim++ {
|
|
228
|
+
vec[dim] += flat[base+dim]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if denom == 0 {
|
|
232
|
+
return vec
|
|
233
|
+
}
|
|
234
|
+
for dim := range vec {
|
|
235
|
+
vec[dim] /= denom
|
|
236
|
+
}
|
|
237
|
+
return vec
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func resolveManifestPath(modelPath string) (string, error) {
|
|
241
|
+
raw := strings.TrimSpace(modelPath)
|
|
242
|
+
if raw == "" {
|
|
243
|
+
return "", fmt.Errorf("onnx-local requires embeddingModelPath pointing to a model directory or embedding.json")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
info, err := os.Stat(raw)
|
|
247
|
+
if err == nil && info.IsDir() {
|
|
248
|
+
return filepath.Join(raw, defaultManifestName), nil
|
|
249
|
+
}
|
|
250
|
+
if err == nil && strings.EqualFold(filepath.Base(raw), defaultManifestName) {
|
|
251
|
+
return raw, nil
|
|
252
|
+
}
|
|
253
|
+
if err == nil {
|
|
254
|
+
return filepath.Join(filepath.Dir(raw), defaultManifestName), nil
|
|
255
|
+
}
|
|
256
|
+
return "", err
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func readManifest(path string) (embeddingManifest, error) {
|
|
260
|
+
data, err := os.ReadFile(path)
|
|
261
|
+
if err != nil {
|
|
262
|
+
return embeddingManifest{}, fmt.Errorf("failed to read %s: %w", path, err)
|
|
263
|
+
}
|
|
264
|
+
var manifest embeddingManifest
|
|
265
|
+
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
266
|
+
return embeddingManifest{}, fmt.Errorf("failed to parse %s: %w", path, err)
|
|
267
|
+
}
|
|
268
|
+
return manifest, nil
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func resolveManifestAsset(baseDir string, values ...string) string {
|
|
272
|
+
asset := firstNonEmpty(values...)
|
|
273
|
+
if asset == "" {
|
|
274
|
+
return ""
|
|
275
|
+
}
|
|
276
|
+
if filepath.IsAbs(asset) {
|
|
277
|
+
return asset
|
|
278
|
+
}
|
|
279
|
+
return filepath.Join(baseDir, asset)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func buildProfile(profile Profile) Profile {
|
|
283
|
+
hash := sha256.Sum256([]byte(strings.Join([]string{
|
|
284
|
+
profile.Backend,
|
|
285
|
+
profile.Family,
|
|
286
|
+
fmt.Sprintf("%d", profile.Dimensions),
|
|
287
|
+
fmt.Sprintf("%t", profile.Normalize),
|
|
288
|
+
fmt.Sprintf("%d", profile.MaxContextTokens),
|
|
289
|
+
profile.ModelPath,
|
|
290
|
+
profile.Tokenizer,
|
|
291
|
+
}, "\x00")))
|
|
292
|
+
profile.Fingerprint = hex.EncodeToString(hash[:])
|
|
293
|
+
return profile
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
func firstNonEmpty(values ...string) string {
|
|
297
|
+
for _, value := range values {
|
|
298
|
+
value = strings.TrimSpace(value)
|
|
299
|
+
if value != "" {
|
|
300
|
+
return value
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return ""
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
func (cfg Config) modelAssetOverride() string {
|
|
307
|
+
modelPath := strings.TrimSpace(cfg.ModelPath)
|
|
308
|
+
if strings.EqualFold(filepath.Base(modelPath), defaultManifestName) {
|
|
309
|
+
return ""
|
|
310
|
+
}
|
|
311
|
+
if strings.HasSuffix(strings.ToLower(modelPath), ".onnx") {
|
|
312
|
+
return modelPath
|
|
313
|
+
}
|
|
314
|
+
return ""
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
func (cfg Config) tokenizerAssetOverride() string {
|
|
318
|
+
return strings.TrimSpace(cfg.TokenizerPath)
|
|
319
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
package embed
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type fakeONNXLocalBackend struct {
|
|
11
|
+
vector []float32
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func (b fakeONNXLocalBackend) Embed(_ string, dimensions int) ([]float32, error) {
|
|
15
|
+
if len(b.vector) != dimensions {
|
|
16
|
+
return nil, nil
|
|
17
|
+
}
|
|
18
|
+
return append([]float32(nil), b.vector...), nil
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func TestONNXLocalLoadsManifestDirectory(t *testing.T) {
|
|
22
|
+
dir := t.TempDir()
|
|
23
|
+
err := os.WriteFile(filepath.Join(dir, "embedding.json"), []byte(`{
|
|
24
|
+
"backend":"onnx-local",
|
|
25
|
+
"profile":"nomic-embed-text-v1.5",
|
|
26
|
+
"family":"nomic-embed-text-v1.5",
|
|
27
|
+
"model":"model.onnx",
|
|
28
|
+
"tokenizer":"tokenizer.json",
|
|
29
|
+
"dimensions":768,
|
|
30
|
+
"normalize":true,
|
|
31
|
+
"inputNames":["input_ids","attention_mask"],
|
|
32
|
+
"outputName":"sentence_embedding"
|
|
33
|
+
}`), 0o644)
|
|
34
|
+
if err != nil {
|
|
35
|
+
t.Fatalf("write manifest: %v", err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
original := newONNXLocalBackend
|
|
39
|
+
t.Cleanup(func() { newONNXLocalBackend = original })
|
|
40
|
+
newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
|
|
41
|
+
if spec.Family != "nomic-embed-text-v1.5" {
|
|
42
|
+
t.Fatalf("unexpected family %q", spec.Family)
|
|
43
|
+
}
|
|
44
|
+
if !strings.HasSuffix(spec.ModelPath, "model.onnx") {
|
|
45
|
+
t.Fatalf("unexpected model path %q", spec.ModelPath)
|
|
46
|
+
}
|
|
47
|
+
if !strings.HasSuffix(spec.TokenizerPath, "tokenizer.json") {
|
|
48
|
+
t.Fatalf("unexpected tokenizer path %q", spec.TokenizerPath)
|
|
49
|
+
}
|
|
50
|
+
if spec.Dimensions != 768 {
|
|
51
|
+
t.Fatalf("unexpected dimensions %d", spec.Dimensions)
|
|
52
|
+
}
|
|
53
|
+
if len(spec.InputNames) != 2 {
|
|
54
|
+
t.Fatalf("unexpected input names %#v", spec.InputNames)
|
|
55
|
+
}
|
|
56
|
+
if spec.RuntimePath != "/opt/onnx/libonnxruntime.so" {
|
|
57
|
+
t.Fatalf("unexpected runtime path %q", spec.RuntimePath)
|
|
58
|
+
}
|
|
59
|
+
return fakeONNXLocalBackend{vector: make([]float32, 768)}, nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
engine := NewWithConfig(Config{
|
|
63
|
+
Backend: "onnx-local",
|
|
64
|
+
RuntimePath: "/opt/onnx/libonnxruntime.so",
|
|
65
|
+
ModelPath: dir,
|
|
66
|
+
Normalize: true,
|
|
67
|
+
})
|
|
68
|
+
if !engine.Ready() {
|
|
69
|
+
t.Fatalf("expected onnx-local engine to be ready, reason=%q", engine.Reason())
|
|
70
|
+
}
|
|
71
|
+
if engine.Mode() != "onnx-local" {
|
|
72
|
+
t.Fatalf("unexpected mode %q", engine.Mode())
|
|
73
|
+
}
|
|
74
|
+
if engine.Dimensions() != 768 {
|
|
75
|
+
t.Fatalf("expected manifest dimensions 768, got %d", engine.Dimensions())
|
|
76
|
+
}
|
|
77
|
+
if engine.Profile().Fingerprint == "" {
|
|
78
|
+
t.Fatalf("expected embedding fingerprint")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func TestONNXLocalRejectsManifestDimensionMismatch(t *testing.T) {
|
|
83
|
+
dir := t.TempDir()
|
|
84
|
+
err := os.WriteFile(filepath.Join(dir, "embedding.json"), []byte(`{
|
|
85
|
+
"model":"model.onnx",
|
|
86
|
+
"tokenizer":"tokenizer.json",
|
|
87
|
+
"dimensions":768
|
|
88
|
+
}`), 0o644)
|
|
89
|
+
if err != nil {
|
|
90
|
+
t.Fatalf("write manifest: %v", err)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
original := newONNXLocalBackend
|
|
94
|
+
t.Cleanup(func() { newONNXLocalBackend = original })
|
|
95
|
+
newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
|
|
96
|
+
return fakeONNXLocalBackend{vector: make([]float32, spec.Dimensions)}, nil
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
engine := NewWithConfig(Config{
|
|
100
|
+
Backend: "onnx-local",
|
|
101
|
+
RuntimePath: "/opt/onnx/libonnxruntime.so",
|
|
102
|
+
ModelPath: dir,
|
|
103
|
+
Dimensions: 384,
|
|
104
|
+
})
|
|
105
|
+
if engine.Ready() {
|
|
106
|
+
t.Fatalf("expected mismatched dimensions to fail readiness")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func TestONNXLocalCanUseShippedProfileDefaults(t *testing.T) {
|
|
111
|
+
dir := t.TempDir()
|
|
112
|
+
err := os.WriteFile(filepath.Join(dir, "embedding.json"), []byte(`{
|
|
113
|
+
"profile":"nomic-embed-text-v1.5",
|
|
114
|
+
"model":"model.onnx",
|
|
115
|
+
"tokenizer":"tokenizer.json"
|
|
116
|
+
}`), 0o644)
|
|
117
|
+
if err != nil {
|
|
118
|
+
t.Fatalf("write manifest: %v", err)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
original := newONNXLocalBackend
|
|
122
|
+
t.Cleanup(func() { newONNXLocalBackend = original })
|
|
123
|
+
newONNXLocalBackend = func(spec onnxLocalSpec) (embeddingBackend, error) {
|
|
124
|
+
if spec.Dimensions != 768 {
|
|
125
|
+
t.Fatalf("expected profile dimensions 768, got %d", spec.Dimensions)
|
|
126
|
+
}
|
|
127
|
+
if spec.Family != "nomic-embed-text-v1.5" {
|
|
128
|
+
t.Fatalf("expected profile family, got %q", spec.Family)
|
|
129
|
+
}
|
|
130
|
+
return fakeONNXLocalBackend{vector: make([]float32, 768)}, nil
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
engine := NewWithConfig(Config{
|
|
134
|
+
Backend: "onnx-local",
|
|
135
|
+
RuntimePath: "/opt/onnx/libonnxruntime.so",
|
|
136
|
+
ModelPath: dir,
|
|
137
|
+
})
|
|
138
|
+
if !engine.Ready() {
|
|
139
|
+
t.Fatalf("expected profile-backed onnx-local engine to be ready, reason=%q", engine.Reason())
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func TestMeanPoolLastHiddenStateUsesAttentionMask(t *testing.T) {
|
|
144
|
+
flat := []float32{
|
|
145
|
+
1, 2,
|
|
146
|
+
3, 4,
|
|
147
|
+
100, 200,
|
|
148
|
+
}
|
|
149
|
+
mask := []int{1, 1, 0}
|
|
150
|
+
|
|
151
|
+
got := meanPoolLastHiddenState(flat, mask, 3, 2)
|
|
152
|
+
|
|
153
|
+
if len(got) != 2 {
|
|
154
|
+
t.Fatalf("unexpected pooled vector length %d", len(got))
|
|
155
|
+
}
|
|
156
|
+
if got[0] != 2 || got[1] != 3 {
|
|
157
|
+
t.Fatalf("unexpected pooled vector %#v", got)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package embed
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type tokenizerContract struct {
|
|
11
|
+
Truncation *struct {
|
|
12
|
+
MaxLength int `json:"max_length"`
|
|
13
|
+
} `json:"truncation"`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func TestProfileMaxCtxMatchesTokenizerContract(t *testing.T) {
|
|
17
|
+
cases := []struct {
|
|
18
|
+
profile string
|
|
19
|
+
modelDir string
|
|
20
|
+
expectEnforced bool
|
|
21
|
+
expectedMax int
|
|
22
|
+
}{
|
|
23
|
+
{
|
|
24
|
+
profile: "all-minilm-l6-v2",
|
|
25
|
+
modelDir: filepath.Clean(filepath.Join("..", "..", ".models", "all-minilm-l6-v2")),
|
|
26
|
+
expectEnforced: true,
|
|
27
|
+
expectedMax: 128,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
profile: "nomic-embed-text-v1.5",
|
|
31
|
+
modelDir: filepath.Clean(filepath.Join("..", "..", ".models", "nomic-embed-text-v1.5")),
|
|
32
|
+
expectEnforced: false,
|
|
33
|
+
expectedMax: 8192,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for _, tc := range cases {
|
|
38
|
+
t.Run(tc.profile, func(t *testing.T) {
|
|
39
|
+
profile, ok := lookupProfile(tc.profile)
|
|
40
|
+
if !ok {
|
|
41
|
+
t.Fatalf("profile %s not found", tc.profile)
|
|
42
|
+
}
|
|
43
|
+
if profile.MaxContextTokens != tc.expectedMax {
|
|
44
|
+
t.Fatalf("profile %s MaxContextTokens=%d want %d", tc.profile, profile.MaxContextTokens, tc.expectedMax)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
data, err := os.ReadFile(filepath.Join(tc.modelDir, "tokenizer.json"))
|
|
48
|
+
if err != nil {
|
|
49
|
+
t.Fatalf("read tokenizer.json: %v", err)
|
|
50
|
+
}
|
|
51
|
+
var contract tokenizerContract
|
|
52
|
+
if err := json.Unmarshal(data, &contract); err != nil {
|
|
53
|
+
t.Fatalf("parse tokenizer.json: %v", err)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if tc.expectEnforced {
|
|
57
|
+
if contract.Truncation == nil {
|
|
58
|
+
t.Fatalf("expected tokenizer truncation contract for %s", tc.profile)
|
|
59
|
+
}
|
|
60
|
+
if contract.Truncation.MaxLength != tc.expectedMax {
|
|
61
|
+
t.Fatalf("profile %s MaxContextTokens=%d but tokenizer enforces %d", tc.profile, tc.expectedMax, contract.Truncation.MaxLength)
|
|
62
|
+
}
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if contract.Truncation != nil {
|
|
67
|
+
t.Fatalf("expected tokenizer truncation to be disabled for %s, got max_length=%d", tc.profile, contract.Truncation.MaxLength)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|