@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,696 @@
|
|
|
1
|
+
package embed
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"math"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"runtime"
|
|
12
|
+
"strings"
|
|
13
|
+
|
|
14
|
+
"github.com/sugarme/tokenizer"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const (
|
|
18
|
+
DefaultBackend = "bundled"
|
|
19
|
+
DefaultDimensions = 768
|
|
20
|
+
longDocWindowSize = 512
|
|
21
|
+
longDocStride = 256
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
type Config struct {
|
|
25
|
+
Backend string
|
|
26
|
+
Profile string
|
|
27
|
+
FallbackProfile string
|
|
28
|
+
RuntimePath string
|
|
29
|
+
ModelPath string
|
|
30
|
+
TokenizerPath string
|
|
31
|
+
Dimensions int
|
|
32
|
+
Normalize bool
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Embedder interface {
|
|
36
|
+
EmbedDocument(ctx context.Context, text string) ([]float32, error)
|
|
37
|
+
EmbedQuery(ctx context.Context, text string) ([]float32, error)
|
|
38
|
+
Dimensions() int
|
|
39
|
+
Profile() Profile
|
|
40
|
+
Ready() bool
|
|
41
|
+
Reason() string
|
|
42
|
+
Mode() string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Engine struct {
|
|
46
|
+
dimensions int
|
|
47
|
+
ready bool
|
|
48
|
+
reason string
|
|
49
|
+
mode string
|
|
50
|
+
backend embeddingBackend
|
|
51
|
+
profile Profile
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type embeddingBackend interface {
|
|
55
|
+
Embed(text string, dimensions int) ([]float32, error)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type deterministicBackend struct {
|
|
59
|
+
normalize bool
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type miniLMModel interface {
|
|
63
|
+
Compute(sentence string, addSpecialTokens bool) ([]float32, error)
|
|
64
|
+
TokenCount(sentence string, addSpecialTokens bool) (int, error)
|
|
65
|
+
Encode(sentence string, addSpecialTokens bool) (*tokenizer.Encoding, error)
|
|
66
|
+
ComputeEncoding(encoding tokenizer.Encoding) ([]float32, error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type miniLMBackend struct {
|
|
70
|
+
model miniLMModel
|
|
71
|
+
normalize bool
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var resolveBundledModelDir = func(profile string) (string, error) {
|
|
75
|
+
profile = strings.TrimSpace(profile)
|
|
76
|
+
if profile == "" {
|
|
77
|
+
profile = DefaultEmbeddingProfile
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
candidates := make([]string, 0, 16)
|
|
81
|
+
if exe, err := os.Executable(); err == nil && strings.TrimSpace(exe) != "" {
|
|
82
|
+
exeDir := filepath.Dir(exe)
|
|
83
|
+
candidates = append(candidates,
|
|
84
|
+
filepath.Join(exeDir, "models", profile),
|
|
85
|
+
filepath.Join(exeDir, "..", ".models", profile),
|
|
86
|
+
filepath.Join(exeDir, "..", "models", profile),
|
|
87
|
+
)
|
|
88
|
+
candidates = append(candidates, ancestorModelDirCandidates(exeDir, profile)...)
|
|
89
|
+
}
|
|
90
|
+
if cwd, err := os.Getwd(); err == nil && strings.TrimSpace(cwd) != "" {
|
|
91
|
+
candidates = append(candidates,
|
|
92
|
+
filepath.Join(cwd, "..", ".models", profile),
|
|
93
|
+
filepath.Join(cwd, ".models", profile),
|
|
94
|
+
filepath.Join(cwd, "models", profile),
|
|
95
|
+
)
|
|
96
|
+
candidates = append(candidates, ancestorModelDirCandidates(cwd, profile)...)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
seen := map[string]struct{}{}
|
|
100
|
+
for _, candidate := range candidates {
|
|
101
|
+
candidate = filepath.Clean(candidate)
|
|
102
|
+
if _, ok := seen[candidate]; ok {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
seen[candidate] = struct{}{}
|
|
106
|
+
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
|
107
|
+
manifestPath := filepath.Join(candidate, defaultManifestName)
|
|
108
|
+
if _, err := os.Stat(manifestPath); err == nil {
|
|
109
|
+
return candidate, nil
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return "", fmt.Errorf("bundled profile %q assets not found; expected embedding.json under a shipped model directory", profile)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
var resolveBundledSpec = func(cfg Config) (onnxLocalSpec, error) {
|
|
118
|
+
profileName := strings.TrimSpace(cfg.Profile)
|
|
119
|
+
if profileName == "" {
|
|
120
|
+
profileName = DefaultEmbeddingProfile
|
|
121
|
+
}
|
|
122
|
+
modelDir := strings.TrimSpace(cfg.ModelPath)
|
|
123
|
+
if modelDir == "" {
|
|
124
|
+
var err error
|
|
125
|
+
modelDir, err = resolveBundledModelDir(profileName)
|
|
126
|
+
if err != nil {
|
|
127
|
+
return onnxLocalSpec{}, err
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
runtimePath := strings.TrimSpace(cfg.RuntimePath)
|
|
132
|
+
if runtimePath == "" {
|
|
133
|
+
var err error
|
|
134
|
+
runtimePath, err = resolveBundledRuntimePath()
|
|
135
|
+
if err != nil {
|
|
136
|
+
return onnxLocalSpec{}, err
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return resolveONNXLocalSpec(Config{
|
|
141
|
+
Backend: "onnx-local",
|
|
142
|
+
Profile: profileName,
|
|
143
|
+
RuntimePath: runtimePath,
|
|
144
|
+
ModelPath: modelDir,
|
|
145
|
+
TokenizerPath: cfg.TokenizerPath,
|
|
146
|
+
Dimensions: cfg.Dimensions,
|
|
147
|
+
Normalize: cfg.Normalize,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var resolveBundledRuntimePath = func() (string, error) {
|
|
152
|
+
libName := bundledRuntimeLibName()
|
|
153
|
+
if libName == "" {
|
|
154
|
+
return "", fmt.Errorf("unsupported platform for bundled onnx runtime: %s/%s", runtime.GOOS, runtime.GOARCH)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
candidates := make([]string, 0, 20)
|
|
158
|
+
if exe, err := os.Executable(); err == nil && strings.TrimSpace(exe) != "" {
|
|
159
|
+
exeDir := filepath.Dir(exe)
|
|
160
|
+
candidates = append(candidates,
|
|
161
|
+
filepath.Join(exeDir, "onnxruntime", "lib", libName),
|
|
162
|
+
filepath.Join(exeDir, "..", ".models", "onnxruntime", "*", "lib", libName),
|
|
163
|
+
filepath.Join(exeDir, "..", "models", "onnxruntime", "*", "lib", libName),
|
|
164
|
+
)
|
|
165
|
+
candidates = append(candidates, ancestorRuntimeCandidates(exeDir, libName)...)
|
|
166
|
+
}
|
|
167
|
+
if cwd, err := os.Getwd(); err == nil && strings.TrimSpace(cwd) != "" {
|
|
168
|
+
candidates = append(candidates,
|
|
169
|
+
filepath.Join(cwd, "..", ".models", "onnxruntime", "*", "lib", libName),
|
|
170
|
+
filepath.Join(cwd, ".models", "onnxruntime", "*", "lib", libName),
|
|
171
|
+
filepath.Join(cwd, "models", "onnxruntime", "*", "lib", libName),
|
|
172
|
+
)
|
|
173
|
+
candidates = append(candidates, ancestorRuntimeCandidates(cwd, libName)...)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
seen := map[string]struct{}{}
|
|
177
|
+
for _, pattern := range candidates {
|
|
178
|
+
matches := []string{pattern}
|
|
179
|
+
if strings.Contains(pattern, "*") {
|
|
180
|
+
globbed, _ := filepath.Glob(pattern)
|
|
181
|
+
matches = globbed
|
|
182
|
+
}
|
|
183
|
+
for _, match := range matches {
|
|
184
|
+
match = filepath.Clean(match)
|
|
185
|
+
if _, ok := seen[match]; ok {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
seen[match] = struct{}{}
|
|
189
|
+
if info, err := os.Stat(match); err == nil && !info.IsDir() {
|
|
190
|
+
return match, nil
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return "", fmt.Errorf("bundled onnx runtime library %q not found in shipped asset locations", libName)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func bundledRuntimeLibName() string {
|
|
199
|
+
switch runtime.GOOS {
|
|
200
|
+
case "darwin":
|
|
201
|
+
return "libonnxruntime.dylib"
|
|
202
|
+
case "linux":
|
|
203
|
+
return "libonnxruntime.so"
|
|
204
|
+
case "windows":
|
|
205
|
+
return "onnxruntime.dll"
|
|
206
|
+
default:
|
|
207
|
+
return ""
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func ResolveRuntimePath(cfg Config) (string, error) {
|
|
212
|
+
runtimePath := strings.TrimSpace(cfg.RuntimePath)
|
|
213
|
+
if runtimePath != "" {
|
|
214
|
+
return runtimePath, nil
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
switch strings.TrimSpace(cfg.Backend) {
|
|
218
|
+
case "", "bundled":
|
|
219
|
+
return resolveBundledRuntimePath()
|
|
220
|
+
case "onnx-local":
|
|
221
|
+
return "", fmt.Errorf("onnx-local embedder requires runtime path or unpacked bundled runtime")
|
|
222
|
+
default:
|
|
223
|
+
return "", fmt.Errorf("backend %q does not use ONNX runtime resolution", cfg.Backend)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func ancestorModelDirCandidates(start, profile string) []string {
|
|
228
|
+
return walkAncestors(start, func(dir string) []string {
|
|
229
|
+
return []string{
|
|
230
|
+
filepath.Join(dir, ".models", profile),
|
|
231
|
+
filepath.Join(dir, "models", profile),
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func ancestorRuntimeCandidates(start, libName string) []string {
|
|
237
|
+
return walkAncestors(start, func(dir string) []string {
|
|
238
|
+
return []string{
|
|
239
|
+
filepath.Join(dir, ".models", "onnxruntime", "*", "lib", libName),
|
|
240
|
+
filepath.Join(dir, "models", "onnxruntime", "*", "lib", libName),
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func walkAncestors(start string, build func(string) []string) []string {
|
|
246
|
+
start = filepath.Clean(start)
|
|
247
|
+
var results []string
|
|
248
|
+
dir := start
|
|
249
|
+
for {
|
|
250
|
+
results = append(results, build(dir)...)
|
|
251
|
+
parent := filepath.Dir(dir)
|
|
252
|
+
if parent == dir {
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
dir = parent
|
|
256
|
+
}
|
|
257
|
+
return results
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func New(dimensions int) *Engine {
|
|
261
|
+
return &Engine{dimensions: dimensions}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
func (e *Engine) Dimensions() int {
|
|
265
|
+
return e.dimensions
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func (e *Engine) Ready() bool {
|
|
269
|
+
return e.ready
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func (e *Engine) Reason() string {
|
|
273
|
+
return e.reason
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func (e *Engine) Mode() string {
|
|
277
|
+
return e.mode
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
func (e *Engine) Profile() Profile {
|
|
281
|
+
return e.profile
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func NewUnavailable(reason string) *Engine {
|
|
285
|
+
return &Engine{
|
|
286
|
+
dimensions: DefaultDimensions,
|
|
287
|
+
ready: false,
|
|
288
|
+
reason: reason,
|
|
289
|
+
mode: "unavailable",
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func NewPrimary(runtimePath string) *Engine {
|
|
294
|
+
return NewWithConfig(Config{
|
|
295
|
+
Backend: DefaultBackend,
|
|
296
|
+
Profile: DefaultEmbeddingProfile,
|
|
297
|
+
FallbackProfile: FallbackEmbeddingProfile,
|
|
298
|
+
RuntimePath: runtimePath,
|
|
299
|
+
Dimensions: DefaultDimensions,
|
|
300
|
+
Normalize: true,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func NewWithConfig(cfg Config) *Engine {
|
|
305
|
+
cfg = normalizeConfig(cfg)
|
|
306
|
+
|
|
307
|
+
switch cfg.Backend {
|
|
308
|
+
case "bundled":
|
|
309
|
+
engine, err := newBundledEngine(cfg)
|
|
310
|
+
if err != nil {
|
|
311
|
+
return NewFallbackWithConfig(cfg, fmt.Sprintf("bundled embedder unavailable (%v); using deterministic local fallback", err))
|
|
312
|
+
}
|
|
313
|
+
return engine
|
|
314
|
+
case "onnx-local":
|
|
315
|
+
if cfg.RuntimePath == "" || cfg.ModelPath == "" {
|
|
316
|
+
return NewUnavailable("onnx-local requires ONNX runtime path and embedding model directory/manifest")
|
|
317
|
+
}
|
|
318
|
+
spec, err := resolveONNXLocalSpec(cfg)
|
|
319
|
+
if err != nil {
|
|
320
|
+
return NewUnavailable(err.Error())
|
|
321
|
+
}
|
|
322
|
+
backend, err := newONNXLocalBackend(spec)
|
|
323
|
+
if err != nil {
|
|
324
|
+
return NewUnavailable(err.Error())
|
|
325
|
+
}
|
|
326
|
+
engine := &Engine{
|
|
327
|
+
dimensions: spec.Dimensions,
|
|
328
|
+
ready: true,
|
|
329
|
+
reason: "",
|
|
330
|
+
mode: "onnx-local",
|
|
331
|
+
backend: backend,
|
|
332
|
+
profile: spec.Profile,
|
|
333
|
+
}
|
|
334
|
+
if err := verifyDimensions(engine); err != nil {
|
|
335
|
+
return NewUnavailable(err.Error())
|
|
336
|
+
}
|
|
337
|
+
return engine
|
|
338
|
+
case "custom-local":
|
|
339
|
+
if cfg.ModelPath == "" {
|
|
340
|
+
return NewFallbackWithConfig(cfg, "missing custom embedding model path; using deterministic local fallback")
|
|
341
|
+
}
|
|
342
|
+
engine := &Engine{
|
|
343
|
+
dimensions: cfg.Dimensions,
|
|
344
|
+
ready: true,
|
|
345
|
+
reason: "",
|
|
346
|
+
mode: "custom-local",
|
|
347
|
+
backend: deterministicBackend{normalize: cfg.Normalize},
|
|
348
|
+
profile: buildProfile(Profile{
|
|
349
|
+
Backend: "custom-local",
|
|
350
|
+
Family: "custom-local",
|
|
351
|
+
Dimensions: cfg.Dimensions,
|
|
352
|
+
Normalize: cfg.Normalize,
|
|
353
|
+
MaxContextTokens: 0,
|
|
354
|
+
ModelPath: cfg.ModelPath,
|
|
355
|
+
Tokenizer: cfg.TokenizerPath,
|
|
356
|
+
}),
|
|
357
|
+
}
|
|
358
|
+
if err := verifyDimensions(engine); err != nil {
|
|
359
|
+
return NewUnavailable(err.Error())
|
|
360
|
+
}
|
|
361
|
+
return engine
|
|
362
|
+
default:
|
|
363
|
+
return NewUnavailable(fmt.Sprintf("unsupported embedding backend: %s", cfg.Backend))
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
func normalizeConfig(cfg Config) Config {
|
|
368
|
+
cfg.Backend = strings.TrimSpace(cfg.Backend)
|
|
369
|
+
if cfg.Backend == "" {
|
|
370
|
+
cfg.Backend = DefaultBackend
|
|
371
|
+
}
|
|
372
|
+
cfg.Profile = strings.TrimSpace(cfg.Profile)
|
|
373
|
+
cfg.FallbackProfile = strings.TrimSpace(cfg.FallbackProfile)
|
|
374
|
+
if cfg.Profile == "" && cfg.Backend == "bundled" {
|
|
375
|
+
cfg.Profile = DefaultEmbeddingProfile
|
|
376
|
+
}
|
|
377
|
+
if cfg.FallbackProfile == "" && cfg.Backend == "bundled" {
|
|
378
|
+
cfg.FallbackProfile = FallbackEmbeddingProfile
|
|
379
|
+
}
|
|
380
|
+
if cfg.Dimensions <= 0 && cfg.Backend != "onnx-local" {
|
|
381
|
+
if profile, ok := lookupProfile(cfg.Profile); ok {
|
|
382
|
+
cfg.Dimensions = profile.Dimensions
|
|
383
|
+
cfg.Normalize = profile.Normalize
|
|
384
|
+
} else {
|
|
385
|
+
cfg.Dimensions = DefaultDimensions
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return cfg
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
func newBundledEngine(cfg Config) (*Engine, error) {
|
|
392
|
+
profiles := []string{cfg.Profile}
|
|
393
|
+
if cfg.ModelPath == "" && cfg.FallbackProfile != "" && cfg.FallbackProfile != cfg.Profile {
|
|
394
|
+
profiles = append(profiles, cfg.FallbackProfile)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
var failures []string
|
|
398
|
+
for i, profile := range profiles {
|
|
399
|
+
candidate := cfg
|
|
400
|
+
candidate.Profile = profile
|
|
401
|
+
if i > 0 {
|
|
402
|
+
candidate.ModelPath = ""
|
|
403
|
+
candidate.TokenizerPath = ""
|
|
404
|
+
candidate.Dimensions = 0
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
spec, err := resolveBundledSpec(candidate)
|
|
408
|
+
if err != nil {
|
|
409
|
+
failures = append(failures, fmt.Sprintf("%s: %v", profile, err))
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
backend, err := newONNXLocalBackend(spec)
|
|
413
|
+
if err != nil {
|
|
414
|
+
failures = append(failures, fmt.Sprintf("%s: %v", profile, err))
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
engine := &Engine{
|
|
418
|
+
dimensions: spec.Dimensions,
|
|
419
|
+
ready: true,
|
|
420
|
+
reason: "",
|
|
421
|
+
mode: "primary",
|
|
422
|
+
backend: backend,
|
|
423
|
+
profile: spec.Profile,
|
|
424
|
+
}
|
|
425
|
+
if err := verifyDimensions(engine); err != nil {
|
|
426
|
+
failures = append(failures, fmt.Sprintf("%s: %v", profile, err))
|
|
427
|
+
continue
|
|
428
|
+
}
|
|
429
|
+
return engine, nil
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return nil, errors.New(strings.Join(failures, "; "))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
func NewFallback(reason string) *Engine {
|
|
436
|
+
return NewFallbackWithConfig(Config{
|
|
437
|
+
Backend: DefaultBackend,
|
|
438
|
+
Dimensions: DefaultDimensions,
|
|
439
|
+
Normalize: true,
|
|
440
|
+
}, reason)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
func NewFallbackWithConfig(cfg Config, reason string) *Engine {
|
|
444
|
+
cfg = normalizeConfig(cfg)
|
|
445
|
+
engine := &Engine{
|
|
446
|
+
dimensions: cfg.Dimensions,
|
|
447
|
+
ready: true,
|
|
448
|
+
reason: reason,
|
|
449
|
+
mode: "fallback",
|
|
450
|
+
backend: deterministicBackend{normalize: cfg.Normalize},
|
|
451
|
+
profile: buildProfile(Profile{
|
|
452
|
+
Backend: "fallback",
|
|
453
|
+
Family: "deterministic-local",
|
|
454
|
+
Dimensions: cfg.Dimensions,
|
|
455
|
+
Normalize: cfg.Normalize,
|
|
456
|
+
MaxContextTokens: 0,
|
|
457
|
+
}),
|
|
458
|
+
}
|
|
459
|
+
if err := verifyDimensions(engine); err != nil {
|
|
460
|
+
return NewUnavailable(err.Error())
|
|
461
|
+
}
|
|
462
|
+
return engine
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
func verifyDimensions(e *Engine) error {
|
|
466
|
+
vec, err := e.EmbedDocument(context.Background(), "dimension probe")
|
|
467
|
+
if err != nil {
|
|
468
|
+
return fmt.Errorf("dimension verification failed: %w", err)
|
|
469
|
+
}
|
|
470
|
+
if got := len(vec); got != e.dimensions {
|
|
471
|
+
return fmt.Errorf("dimension verification failed: got %d, want %d", got, e.dimensions)
|
|
472
|
+
}
|
|
473
|
+
return nil
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func (e *Engine) EmbedDocument(_ context.Context, text string) ([]float32, error) {
|
|
477
|
+
prefixed := e.documentPrefix() + text
|
|
478
|
+
if vec, ok, err := e.embedLongDocument(prefixed); ok {
|
|
479
|
+
return vec, err
|
|
480
|
+
}
|
|
481
|
+
return e.embed(prefixed)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
func (e *Engine) EmbedQuery(_ context.Context, text string) ([]float32, error) {
|
|
485
|
+
return e.embed(e.queryPrefix() + text)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
func (e *Engine) documentPrefix() string {
|
|
489
|
+
switch e.profile.Family {
|
|
490
|
+
case "nomic-embed-text-v1.5":
|
|
491
|
+
return "search_document: "
|
|
492
|
+
default:
|
|
493
|
+
return ""
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
func (e *Engine) queryPrefix() string {
|
|
498
|
+
switch e.profile.Family {
|
|
499
|
+
case "nomic-embed-text-v1.5":
|
|
500
|
+
return "search_query: "
|
|
501
|
+
default:
|
|
502
|
+
return ""
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
func (e *Engine) embed(text string) ([]float32, error) {
|
|
507
|
+
if !e.ready {
|
|
508
|
+
return nil, fmt.Errorf("embedding engine not ready: %s", e.reason)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if e.backend == nil {
|
|
512
|
+
return nil, errors.New("embedding backend not configured")
|
|
513
|
+
}
|
|
514
|
+
return e.backend.Embed(text, e.dimensions)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
func (e *Engine) TokenCountDocument(_ context.Context, text string) (int, error) {
|
|
518
|
+
return e.tokenCount(e.documentPrefix() + text)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
func (e *Engine) TokenCountQuery(_ context.Context, text string) (int, error) {
|
|
522
|
+
return e.tokenCount(e.queryPrefix() + text)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
func (e *Engine) tokenCount(text string) (int, error) {
|
|
526
|
+
if !e.ready {
|
|
527
|
+
return 0, fmt.Errorf("embedding engine not ready: %s", e.reason)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
counter, ok := e.backend.(interface {
|
|
531
|
+
TokenCount(text string) (int, error)
|
|
532
|
+
})
|
|
533
|
+
if !ok {
|
|
534
|
+
return 0, fmt.Errorf("token count unavailable for backend family %q", e.profile.Family)
|
|
535
|
+
}
|
|
536
|
+
return counter.TokenCount(text)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
func (e *Engine) embedLongDocument(text string) ([]float32, bool, error) {
|
|
540
|
+
if !strings.EqualFold(e.profile.Family, "nomic-embed-text-v1.5") {
|
|
541
|
+
return nil, false, nil
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
tokenAware, ok := e.backend.(interface {
|
|
545
|
+
Encode(text string) (*tokenizer.Encoding, error)
|
|
546
|
+
EmbedEncoding(encoding tokenizer.Encoding, dimensions int) ([]float32, error)
|
|
547
|
+
})
|
|
548
|
+
if !ok {
|
|
549
|
+
return nil, false, nil
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
encoding, err := tokenAware.Encode(text)
|
|
553
|
+
if err != nil {
|
|
554
|
+
return nil, true, err
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
windowSize := longDocWindowSize
|
|
558
|
+
if e.profile.MaxContextTokens > 0 && windowSize > e.profile.MaxContextTokens {
|
|
559
|
+
windowSize = e.profile.MaxContextTokens
|
|
560
|
+
}
|
|
561
|
+
if windowSize <= 0 {
|
|
562
|
+
return nil, false, nil
|
|
563
|
+
}
|
|
564
|
+
if len(encoding.Ids) <= windowSize {
|
|
565
|
+
vec, err := tokenAware.EmbedEncoding(*encoding, e.dimensions)
|
|
566
|
+
return vec, true, err
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
stride := longDocStride
|
|
570
|
+
if stride <= 0 || stride >= windowSize {
|
|
571
|
+
stride = windowSize / 2
|
|
572
|
+
}
|
|
573
|
+
if stride <= 0 {
|
|
574
|
+
stride = 1
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
windowed := encoding.Clone()
|
|
578
|
+
if _, err := windowed.Truncate(windowSize, stride); err != nil {
|
|
579
|
+
return nil, true, err
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
windows := make([]tokenizer.Encoding, 0, 1+len(windowed.Overflowing))
|
|
583
|
+
windows = append(windows, *windowed)
|
|
584
|
+
windows = append(windows, windowed.Overflowing...)
|
|
585
|
+
|
|
586
|
+
vecs := make([][]float32, 0, len(windows))
|
|
587
|
+
for _, window := range windows {
|
|
588
|
+
vec, err := tokenAware.EmbedEncoding(window, e.dimensions)
|
|
589
|
+
if err != nil {
|
|
590
|
+
return nil, true, err
|
|
591
|
+
}
|
|
592
|
+
vecs = append(vecs, vec)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return meanPoolVectors(vecs), true, nil
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
func (b deterministicBackend) Embed(text string, dimensions int) ([]float32, error) {
|
|
599
|
+
vec := deterministicEmbedding(text, dimensions)
|
|
600
|
+
if b.normalize {
|
|
601
|
+
normalizeEmbedding(vec)
|
|
602
|
+
}
|
|
603
|
+
return vec, nil
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
func (b miniLMBackend) Embed(text string, dimensions int) ([]float32, error) {
|
|
607
|
+
vec, err := b.model.Compute(text, true)
|
|
608
|
+
if err != nil {
|
|
609
|
+
return nil, err
|
|
610
|
+
}
|
|
611
|
+
if len(vec) != dimensions {
|
|
612
|
+
return nil, fmt.Errorf("unexpected embedding dimensions: got %d, want %d", len(vec), dimensions)
|
|
613
|
+
}
|
|
614
|
+
if b.normalize {
|
|
615
|
+
normalizeEmbedding(vec)
|
|
616
|
+
}
|
|
617
|
+
return vec, nil
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
func (b miniLMBackend) TokenCount(text string) (int, error) {
|
|
621
|
+
return b.model.TokenCount(text, true)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
func (b miniLMBackend) Encode(text string) (*tokenizer.Encoding, error) {
|
|
625
|
+
return b.model.Encode(text, true)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
func (b miniLMBackend) EmbedEncoding(encoding tokenizer.Encoding, dimensions int) ([]float32, error) {
|
|
629
|
+
vec, err := b.model.ComputeEncoding(encoding)
|
|
630
|
+
if err != nil {
|
|
631
|
+
return nil, err
|
|
632
|
+
}
|
|
633
|
+
if len(vec) != dimensions {
|
|
634
|
+
return nil, fmt.Errorf("unexpected embedding dimensions: got %d, want %d", len(vec), dimensions)
|
|
635
|
+
}
|
|
636
|
+
if b.normalize {
|
|
637
|
+
normalizeEmbedding(vec)
|
|
638
|
+
}
|
|
639
|
+
return vec, nil
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
func deterministicEmbedding(text string, dimensions int) []float32 {
|
|
643
|
+
vec := make([]float32, dimensions)
|
|
644
|
+
if dimensions <= 0 {
|
|
645
|
+
return vec
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
tokens := strings.Fields(strings.ToLower(text))
|
|
649
|
+
if len(tokens) == 0 {
|
|
650
|
+
tokens = []string{text}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
for _, token := range tokens {
|
|
654
|
+
sum := sha256.Sum256([]byte(token))
|
|
655
|
+
for i := 0; i < dimensions; i++ {
|
|
656
|
+
b := sum[i%len(sum)]
|
|
657
|
+
value := (float32(b) / 127.5) - 1.0
|
|
658
|
+
vec[i] += value
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
normalizeEmbedding(vec)
|
|
663
|
+
return vec
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
func normalizeEmbedding(vec []float32) {
|
|
667
|
+
var norm float64
|
|
668
|
+
for _, v := range vec {
|
|
669
|
+
norm += float64(v * v)
|
|
670
|
+
}
|
|
671
|
+
if norm == 0 {
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
scale := float32(1.0 / math.Sqrt(norm))
|
|
676
|
+
for i := range vec {
|
|
677
|
+
vec[i] *= scale
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
func meanPoolVectors(vecs [][]float32) []float32 {
|
|
682
|
+
if len(vecs) == 0 {
|
|
683
|
+
return nil
|
|
684
|
+
}
|
|
685
|
+
result := make([]float32, len(vecs[0]))
|
|
686
|
+
for _, vec := range vecs {
|
|
687
|
+
for i, value := range vec {
|
|
688
|
+
result[i] += value
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
denom := float32(len(vecs))
|
|
692
|
+
for i := range result {
|
|
693
|
+
result[i] /= denom
|
|
694
|
+
}
|
|
695
|
+
return result
|
|
696
|
+
}
|