@sparkleideas/embeddings 3.0.0-alpha.12-patch.17 → 3.0.0-alpha.12-patch.19
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/package.json +3 -3
- package/dist/__tests__/embedding-service.test.d.ts +0 -2
- package/dist/__tests__/embedding-service.test.d.ts.map +0 -1
- package/dist/__tests__/embedding-service.test.js +0 -98
- package/dist/__tests__/embedding-service.test.js.map +0 -1
- package/dist/chunking.d.ts +0 -68
- package/dist/chunking.d.ts.map +0 -1
- package/dist/chunking.js +0 -251
- package/dist/chunking.js.map +0 -1
- package/dist/embedding-service.d.ts +0 -207
- package/dist/embedding-service.d.ts.map +0 -1
- package/dist/embedding-service.js +0 -965
- package/dist/embedding-service.js.map +0 -1
- package/dist/hyperbolic.d.ts +0 -103
- package/dist/hyperbolic.d.ts.map +0 -1
- package/dist/hyperbolic.js +0 -343
- package/dist/hyperbolic.js.map +0 -1
- package/dist/index.d.ts +0 -31
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -37
- package/dist/index.js.map +0 -1
- package/dist/neural-integration.d.ts +0 -203
- package/dist/neural-integration.d.ts.map +0 -1
- package/dist/neural-integration.js +0 -213
- package/dist/neural-integration.js.map +0 -1
- package/dist/normalization.d.ts +0 -73
- package/dist/normalization.d.ts.map +0 -1
- package/dist/normalization.js +0 -192
- package/dist/normalization.js.map +0 -1
- package/dist/persistent-cache.d.ts +0 -119
- package/dist/persistent-cache.d.ts.map +0 -1
- package/dist/persistent-cache.js +0 -337
- package/dist/persistent-cache.js.map +0 -1
- package/dist/rvf-embedding-cache.d.ts +0 -118
- package/dist/rvf-embedding-cache.d.ts.map +0 -1
- package/dist/rvf-embedding-cache.js +0 -458
- package/dist/rvf-embedding-cache.js.map +0 -1
- package/dist/rvf-embedding-service.d.ts +0 -79
- package/dist/rvf-embedding-service.d.ts.map +0 -1
- package/dist/rvf-embedding-service.js +0 -318
- package/dist/rvf-embedding-service.js.map +0 -1
- package/dist/types.d.ts +0 -237
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -15
- package/dist/types.js.map +0 -1
|
@@ -1,965 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* V3 Embedding Service Implementation
|
|
3
|
-
*
|
|
4
|
-
* Production embedding service aligned with agentic-flow@alpha:
|
|
5
|
-
* - OpenAI provider (text-embedding-3-small/large)
|
|
6
|
-
* - Transformers.js provider (local ONNX models)
|
|
7
|
-
* - Mock provider (development/testing)
|
|
8
|
-
*
|
|
9
|
-
* Performance Targets:
|
|
10
|
-
* - Single embedding: <100ms (API), <50ms (local)
|
|
11
|
-
* - Batch embedding: <500ms for 10 items
|
|
12
|
-
* - Cache hit: <1ms
|
|
13
|
-
*/
|
|
14
|
-
import { EventEmitter } from 'events';
|
|
15
|
-
import { normalize } from './normalization.js';
|
|
16
|
-
import { PersistentEmbeddingCache } from './persistent-cache.js';
|
|
17
|
-
import { RvfEmbeddingService } from './rvf-embedding-service.js';
|
|
18
|
-
// ============================================================================
|
|
19
|
-
// LRU Cache Implementation
|
|
20
|
-
// ============================================================================
|
|
21
|
-
class LRUCache {
|
|
22
|
-
maxSize;
|
|
23
|
-
cache = new Map();
|
|
24
|
-
hits = 0;
|
|
25
|
-
misses = 0;
|
|
26
|
-
constructor(maxSize) {
|
|
27
|
-
this.maxSize = maxSize;
|
|
28
|
-
}
|
|
29
|
-
get(key) {
|
|
30
|
-
const value = this.cache.get(key);
|
|
31
|
-
if (value !== undefined) {
|
|
32
|
-
// Move to end (most recently used)
|
|
33
|
-
this.cache.delete(key);
|
|
34
|
-
this.cache.set(key, value);
|
|
35
|
-
this.hits++;
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
this.misses++;
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
set(key, value) {
|
|
42
|
-
if (this.cache.has(key)) {
|
|
43
|
-
this.cache.delete(key);
|
|
44
|
-
}
|
|
45
|
-
else if (this.cache.size >= this.maxSize) {
|
|
46
|
-
// Remove oldest (first) entry
|
|
47
|
-
const firstKey = this.cache.keys().next().value;
|
|
48
|
-
if (firstKey !== undefined) {
|
|
49
|
-
this.cache.delete(firstKey);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
this.cache.set(key, value);
|
|
53
|
-
}
|
|
54
|
-
clear() {
|
|
55
|
-
this.cache.clear();
|
|
56
|
-
this.hits = 0;
|
|
57
|
-
this.misses = 0;
|
|
58
|
-
}
|
|
59
|
-
get size() {
|
|
60
|
-
return this.cache.size;
|
|
61
|
-
}
|
|
62
|
-
get hitRate() {
|
|
63
|
-
const total = this.hits + this.misses;
|
|
64
|
-
return total > 0 ? this.hits / total : 0;
|
|
65
|
-
}
|
|
66
|
-
getStats() {
|
|
67
|
-
return {
|
|
68
|
-
size: this.cache.size,
|
|
69
|
-
maxSize: this.maxSize,
|
|
70
|
-
hits: this.hits,
|
|
71
|
-
misses: this.misses,
|
|
72
|
-
hitRate: this.hitRate,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// ============================================================================
|
|
77
|
-
// Base Embedding Service
|
|
78
|
-
// ============================================================================
|
|
79
|
-
class BaseEmbeddingService extends EventEmitter {
|
|
80
|
-
config;
|
|
81
|
-
cache;
|
|
82
|
-
persistentCache = null;
|
|
83
|
-
embeddingListeners = new Set();
|
|
84
|
-
normalizationType;
|
|
85
|
-
constructor(config) {
|
|
86
|
-
super();
|
|
87
|
-
this.config = config;
|
|
88
|
-
this.cache = new LRUCache(config.cacheSize ?? 1000);
|
|
89
|
-
this.normalizationType = config.normalization ?? 'none';
|
|
90
|
-
// Initialize persistent cache if configured
|
|
91
|
-
if (config.persistentCache?.enabled) {
|
|
92
|
-
const pcConfig = config.persistentCache;
|
|
93
|
-
this.persistentCache = new PersistentEmbeddingCache({
|
|
94
|
-
dbPath: pcConfig.dbPath ?? '.cache/embeddings.db',
|
|
95
|
-
maxSize: pcConfig.maxSize ?? 10000,
|
|
96
|
-
ttlMs: pcConfig.ttlMs,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Apply normalization to embedding if configured
|
|
102
|
-
*/
|
|
103
|
-
applyNormalization(embedding) {
|
|
104
|
-
if (this.normalizationType === 'none') {
|
|
105
|
-
return embedding;
|
|
106
|
-
}
|
|
107
|
-
return normalize(embedding, { type: this.normalizationType });
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Check persistent cache for embedding
|
|
111
|
-
*/
|
|
112
|
-
async checkPersistentCache(text) {
|
|
113
|
-
if (!this.persistentCache)
|
|
114
|
-
return null;
|
|
115
|
-
return this.persistentCache.get(text);
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Store embedding in persistent cache
|
|
119
|
-
*/
|
|
120
|
-
async storePersistentCache(text, embedding) {
|
|
121
|
-
if (!this.persistentCache)
|
|
122
|
-
return;
|
|
123
|
-
await this.persistentCache.set(text, embedding);
|
|
124
|
-
}
|
|
125
|
-
emitEvent(event) {
|
|
126
|
-
for (const listener of this.embeddingListeners) {
|
|
127
|
-
try {
|
|
128
|
-
listener(event);
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
console.error('Error in embedding event listener:', error);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
this.emit(event.type, event);
|
|
135
|
-
}
|
|
136
|
-
addEventListener(listener) {
|
|
137
|
-
this.embeddingListeners.add(listener);
|
|
138
|
-
}
|
|
139
|
-
removeEventListener(listener) {
|
|
140
|
-
this.embeddingListeners.delete(listener);
|
|
141
|
-
}
|
|
142
|
-
clearCache() {
|
|
143
|
-
const size = this.cache.size;
|
|
144
|
-
this.cache.clear();
|
|
145
|
-
this.emitEvent({ type: 'cache_eviction', size });
|
|
146
|
-
}
|
|
147
|
-
getCacheStats() {
|
|
148
|
-
const stats = this.cache.getStats();
|
|
149
|
-
return {
|
|
150
|
-
size: stats.size,
|
|
151
|
-
maxSize: stats.maxSize,
|
|
152
|
-
hitRate: stats.hitRate,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
async shutdown() {
|
|
156
|
-
this.clearCache();
|
|
157
|
-
this.embeddingListeners.clear();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// ============================================================================
|
|
161
|
-
// OpenAI Embedding Service
|
|
162
|
-
// ============================================================================
|
|
163
|
-
export class OpenAIEmbeddingService extends BaseEmbeddingService {
|
|
164
|
-
provider = 'openai';
|
|
165
|
-
apiKey;
|
|
166
|
-
model;
|
|
167
|
-
baseURL;
|
|
168
|
-
timeout;
|
|
169
|
-
maxRetries;
|
|
170
|
-
constructor(config) {
|
|
171
|
-
super(config);
|
|
172
|
-
this.apiKey = config.apiKey;
|
|
173
|
-
this.model = config.model ?? 'text-embedding-3-small';
|
|
174
|
-
this.baseURL = config.baseURL ?? 'https://api.openai.com/v1/embeddings';
|
|
175
|
-
this.timeout = config.timeout ?? 30000;
|
|
176
|
-
this.maxRetries = config.maxRetries ?? 3;
|
|
177
|
-
}
|
|
178
|
-
async embed(text) {
|
|
179
|
-
// Check cache
|
|
180
|
-
const cached = this.cache.get(text);
|
|
181
|
-
if (cached) {
|
|
182
|
-
this.emitEvent({ type: 'cache_hit', text });
|
|
183
|
-
return {
|
|
184
|
-
embedding: cached,
|
|
185
|
-
latencyMs: 0,
|
|
186
|
-
cached: true,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
this.emitEvent({ type: 'embed_start', text });
|
|
190
|
-
const startTime = performance.now();
|
|
191
|
-
try {
|
|
192
|
-
const response = await this.callOpenAI([text]);
|
|
193
|
-
const embedding = new Float32Array(response.data[0].embedding);
|
|
194
|
-
// Cache result
|
|
195
|
-
this.cache.set(text, embedding);
|
|
196
|
-
const latencyMs = performance.now() - startTime;
|
|
197
|
-
this.emitEvent({ type: 'embed_complete', text, latencyMs });
|
|
198
|
-
return {
|
|
199
|
-
embedding,
|
|
200
|
-
latencyMs,
|
|
201
|
-
usage: {
|
|
202
|
-
promptTokens: response.usage?.prompt_tokens ?? 0,
|
|
203
|
-
totalTokens: response.usage?.total_tokens ?? 0,
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
209
|
-
this.emitEvent({ type: 'embed_error', text, error: message });
|
|
210
|
-
throw new Error(`OpenAI embedding failed: ${message}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
async embedBatch(texts) {
|
|
214
|
-
this.emitEvent({ type: 'batch_start', count: texts.length });
|
|
215
|
-
const startTime = performance.now();
|
|
216
|
-
// Check cache for each text
|
|
217
|
-
const cached = [];
|
|
218
|
-
const uncached = [];
|
|
219
|
-
texts.forEach((text, index) => {
|
|
220
|
-
const cachedEmbedding = this.cache.get(text);
|
|
221
|
-
if (cachedEmbedding) {
|
|
222
|
-
cached.push({ index, embedding: cachedEmbedding });
|
|
223
|
-
this.emitEvent({ type: 'cache_hit', text });
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
uncached.push({ index, text });
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
// Fetch uncached embeddings
|
|
230
|
-
let apiEmbeddings = [];
|
|
231
|
-
let usage = { promptTokens: 0, totalTokens: 0 };
|
|
232
|
-
if (uncached.length > 0) {
|
|
233
|
-
const response = await this.callOpenAI(uncached.map(u => u.text));
|
|
234
|
-
apiEmbeddings = response.data.map(d => new Float32Array(d.embedding));
|
|
235
|
-
// Cache results
|
|
236
|
-
uncached.forEach((item, i) => {
|
|
237
|
-
this.cache.set(item.text, apiEmbeddings[i]);
|
|
238
|
-
});
|
|
239
|
-
usage = {
|
|
240
|
-
promptTokens: response.usage?.prompt_tokens ?? 0,
|
|
241
|
-
totalTokens: response.usage?.total_tokens ?? 0,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
// Reconstruct result array in original order
|
|
245
|
-
const embeddings = new Array(texts.length);
|
|
246
|
-
cached.forEach(c => {
|
|
247
|
-
embeddings[c.index] = c.embedding;
|
|
248
|
-
});
|
|
249
|
-
uncached.forEach((u, i) => {
|
|
250
|
-
embeddings[u.index] = apiEmbeddings[i];
|
|
251
|
-
});
|
|
252
|
-
const totalLatencyMs = performance.now() - startTime;
|
|
253
|
-
this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
|
|
254
|
-
return {
|
|
255
|
-
embeddings,
|
|
256
|
-
totalLatencyMs,
|
|
257
|
-
avgLatencyMs: totalLatencyMs / texts.length,
|
|
258
|
-
usage,
|
|
259
|
-
cacheStats: {
|
|
260
|
-
hits: cached.length,
|
|
261
|
-
misses: uncached.length,
|
|
262
|
-
},
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
async callOpenAI(texts) {
|
|
266
|
-
const config = this.config;
|
|
267
|
-
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
268
|
-
try {
|
|
269
|
-
const controller = new AbortController();
|
|
270
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
271
|
-
const response = await fetch(this.baseURL, {
|
|
272
|
-
method: 'POST',
|
|
273
|
-
headers: {
|
|
274
|
-
'Content-Type': 'application/json',
|
|
275
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
276
|
-
},
|
|
277
|
-
body: JSON.stringify({
|
|
278
|
-
model: this.model,
|
|
279
|
-
input: texts,
|
|
280
|
-
dimensions: config.dimensions,
|
|
281
|
-
}),
|
|
282
|
-
signal: controller.signal,
|
|
283
|
-
});
|
|
284
|
-
clearTimeout(timeoutId);
|
|
285
|
-
if (!response.ok) {
|
|
286
|
-
const error = await response.text();
|
|
287
|
-
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
288
|
-
}
|
|
289
|
-
return await response.json();
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
if (attempt === this.maxRetries - 1) {
|
|
293
|
-
throw error;
|
|
294
|
-
}
|
|
295
|
-
// Exponential backoff
|
|
296
|
-
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100));
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
throw new Error('Max retries exceeded');
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// ============================================================================
|
|
303
|
-
// Transformers.js Embedding Service
|
|
304
|
-
// ============================================================================
|
|
305
|
-
export class TransformersEmbeddingService extends BaseEmbeddingService {
|
|
306
|
-
provider = 'transformers';
|
|
307
|
-
pipeline = null;
|
|
308
|
-
modelName;
|
|
309
|
-
initialized = false;
|
|
310
|
-
constructor(config) {
|
|
311
|
-
super(config);
|
|
312
|
-
this.modelName = config.model ?? 'Xenova/all-MiniLM-L6-v2';
|
|
313
|
-
}
|
|
314
|
-
async initialize() {
|
|
315
|
-
if (this.initialized)
|
|
316
|
-
return;
|
|
317
|
-
try {
|
|
318
|
-
const { pipeline } = await import('@xenova/transformers');
|
|
319
|
-
this.pipeline = await pipeline('feature-extraction', this.modelName);
|
|
320
|
-
this.initialized = true;
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
throw new Error(`Failed to initialize transformers.js: ${error}`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
async embed(text) {
|
|
327
|
-
await this.initialize();
|
|
328
|
-
// Check cache
|
|
329
|
-
const cached = this.cache.get(text);
|
|
330
|
-
if (cached) {
|
|
331
|
-
this.emitEvent({ type: 'cache_hit', text });
|
|
332
|
-
return {
|
|
333
|
-
embedding: cached,
|
|
334
|
-
latencyMs: 0,
|
|
335
|
-
cached: true,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
this.emitEvent({ type: 'embed_start', text });
|
|
339
|
-
const startTime = performance.now();
|
|
340
|
-
try {
|
|
341
|
-
const output = await this.pipeline(text, { pooling: 'mean', normalize: true });
|
|
342
|
-
const embedding = new Float32Array(output.data);
|
|
343
|
-
// Cache result
|
|
344
|
-
this.cache.set(text, embedding);
|
|
345
|
-
const latencyMs = performance.now() - startTime;
|
|
346
|
-
this.emitEvent({ type: 'embed_complete', text, latencyMs });
|
|
347
|
-
return {
|
|
348
|
-
embedding,
|
|
349
|
-
latencyMs,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
catch (error) {
|
|
353
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
354
|
-
this.emitEvent({ type: 'embed_error', text, error: message });
|
|
355
|
-
throw new Error(`Transformers.js embedding failed: ${message}`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
async embedBatch(texts) {
|
|
359
|
-
await this.initialize();
|
|
360
|
-
this.emitEvent({ type: 'batch_start', count: texts.length });
|
|
361
|
-
const startTime = performance.now();
|
|
362
|
-
const embeddings = [];
|
|
363
|
-
let cacheHits = 0;
|
|
364
|
-
for (const text of texts) {
|
|
365
|
-
const cached = this.cache.get(text);
|
|
366
|
-
if (cached) {
|
|
367
|
-
embeddings.push(cached);
|
|
368
|
-
cacheHits++;
|
|
369
|
-
this.emitEvent({ type: 'cache_hit', text });
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
const output = await this.pipeline(text, { pooling: 'mean', normalize: true });
|
|
373
|
-
const embedding = new Float32Array(output.data);
|
|
374
|
-
this.cache.set(text, embedding);
|
|
375
|
-
embeddings.push(embedding);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
const totalLatencyMs = performance.now() - startTime;
|
|
379
|
-
this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
|
|
380
|
-
return {
|
|
381
|
-
embeddings,
|
|
382
|
-
totalLatencyMs,
|
|
383
|
-
avgLatencyMs: totalLatencyMs / texts.length,
|
|
384
|
-
cacheStats: {
|
|
385
|
-
hits: cacheHits,
|
|
386
|
-
misses: texts.length - cacheHits,
|
|
387
|
-
},
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
// ============================================================================
|
|
392
|
-
// Mock Embedding Service
|
|
393
|
-
// ============================================================================
|
|
394
|
-
export class MockEmbeddingService extends BaseEmbeddingService {
|
|
395
|
-
provider = 'mock';
|
|
396
|
-
dimensions;
|
|
397
|
-
simulatedLatency;
|
|
398
|
-
constructor(config = {}) {
|
|
399
|
-
const fullConfig = {
|
|
400
|
-
provider: 'mock',
|
|
401
|
-
dimensions: config.dimensions ?? 384,
|
|
402
|
-
cacheSize: config.cacheSize ?? 1000,
|
|
403
|
-
simulatedLatency: config.simulatedLatency ?? 0,
|
|
404
|
-
enableCache: config.enableCache ?? true,
|
|
405
|
-
};
|
|
406
|
-
super(fullConfig);
|
|
407
|
-
this.dimensions = fullConfig.dimensions;
|
|
408
|
-
this.simulatedLatency = fullConfig.simulatedLatency;
|
|
409
|
-
}
|
|
410
|
-
async embed(text) {
|
|
411
|
-
// Check cache
|
|
412
|
-
const cached = this.cache.get(text);
|
|
413
|
-
if (cached) {
|
|
414
|
-
this.emitEvent({ type: 'cache_hit', text });
|
|
415
|
-
return {
|
|
416
|
-
embedding: cached,
|
|
417
|
-
latencyMs: 0,
|
|
418
|
-
cached: true,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
this.emitEvent({ type: 'embed_start', text });
|
|
422
|
-
const startTime = performance.now();
|
|
423
|
-
// Simulate latency
|
|
424
|
-
if (this.simulatedLatency > 0) {
|
|
425
|
-
await new Promise(resolve => setTimeout(resolve, this.simulatedLatency));
|
|
426
|
-
}
|
|
427
|
-
const embedding = this.hashEmbedding(text);
|
|
428
|
-
this.cache.set(text, embedding);
|
|
429
|
-
const latencyMs = performance.now() - startTime;
|
|
430
|
-
this.emitEvent({ type: 'embed_complete', text, latencyMs });
|
|
431
|
-
return {
|
|
432
|
-
embedding,
|
|
433
|
-
latencyMs,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
async embedBatch(texts) {
|
|
437
|
-
this.emitEvent({ type: 'batch_start', count: texts.length });
|
|
438
|
-
const startTime = performance.now();
|
|
439
|
-
const embeddings = [];
|
|
440
|
-
let cacheHits = 0;
|
|
441
|
-
for (const text of texts) {
|
|
442
|
-
const cached = this.cache.get(text);
|
|
443
|
-
if (cached) {
|
|
444
|
-
embeddings.push(cached);
|
|
445
|
-
cacheHits++;
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
const embedding = this.hashEmbedding(text);
|
|
449
|
-
this.cache.set(text, embedding);
|
|
450
|
-
embeddings.push(embedding);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
const totalLatencyMs = performance.now() - startTime;
|
|
454
|
-
this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
|
|
455
|
-
return {
|
|
456
|
-
embeddings,
|
|
457
|
-
totalLatencyMs,
|
|
458
|
-
avgLatencyMs: totalLatencyMs / texts.length,
|
|
459
|
-
cacheStats: {
|
|
460
|
-
hits: cacheHits,
|
|
461
|
-
misses: texts.length - cacheHits,
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Generate deterministic hash-based embedding
|
|
467
|
-
*/
|
|
468
|
-
hashEmbedding(text) {
|
|
469
|
-
const embedding = new Float32Array(this.dimensions);
|
|
470
|
-
// Seed with text hash
|
|
471
|
-
let hash = 0;
|
|
472
|
-
for (let i = 0; i < text.length; i++) {
|
|
473
|
-
hash = (hash << 5) - hash + text.charCodeAt(i);
|
|
474
|
-
hash = hash & hash;
|
|
475
|
-
}
|
|
476
|
-
// Generate pseudo-random embedding
|
|
477
|
-
for (let i = 0; i < this.dimensions; i++) {
|
|
478
|
-
const seed = hash + i * 2654435761;
|
|
479
|
-
const x = Math.sin(seed) * 10000;
|
|
480
|
-
embedding[i] = x - Math.floor(x);
|
|
481
|
-
}
|
|
482
|
-
// Normalize to unit vector
|
|
483
|
-
const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
|
|
484
|
-
for (let i = 0; i < this.dimensions; i++) {
|
|
485
|
-
embedding[i] /= norm;
|
|
486
|
-
}
|
|
487
|
-
return embedding;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// ============================================================================
|
|
491
|
-
// Agentic-Flow Embedding Service
|
|
492
|
-
// ============================================================================
|
|
493
|
-
/**
|
|
494
|
-
* Agentic-Flow embedding service using OptimizedEmbedder
|
|
495
|
-
*
|
|
496
|
-
* Features:
|
|
497
|
-
* - ONNX-based embeddings with SIMD acceleration
|
|
498
|
-
* - 256-entry LRU cache with FNV-1a hash
|
|
499
|
-
* - 8x loop unrolling for cosine similarity
|
|
500
|
-
* - Pre-allocated buffers (no GC pressure)
|
|
501
|
-
* - 3-4x faster batch processing
|
|
502
|
-
*/
|
|
503
|
-
export class AgenticFlowEmbeddingService extends BaseEmbeddingService {
|
|
504
|
-
provider = 'agentic-flow';
|
|
505
|
-
embedder = null;
|
|
506
|
-
initialized = false;
|
|
507
|
-
modelId;
|
|
508
|
-
dimensions;
|
|
509
|
-
embedderCacheSize;
|
|
510
|
-
modelDir;
|
|
511
|
-
autoDownload;
|
|
512
|
-
constructor(config) {
|
|
513
|
-
super(config);
|
|
514
|
-
this.modelId = config.modelId ?? 'all-MiniLM-L6-v2';
|
|
515
|
-
this.dimensions = config.dimensions ?? 384;
|
|
516
|
-
this.embedderCacheSize = config.embedderCacheSize ?? 256;
|
|
517
|
-
this.modelDir = config.modelDir;
|
|
518
|
-
this.autoDownload = config.autoDownload ?? false;
|
|
519
|
-
}
|
|
520
|
-
async initialize() {
|
|
521
|
-
if (this.initialized)
|
|
522
|
-
return;
|
|
523
|
-
let lastError;
|
|
524
|
-
const createEmbedder = async (modulePath) => {
|
|
525
|
-
try {
|
|
526
|
-
// Use file:// protocol for absolute paths
|
|
527
|
-
const importPath = modulePath.startsWith('/') ? `file://${modulePath}` : modulePath;
|
|
528
|
-
const module = await import(/* webpackIgnore: true */ importPath);
|
|
529
|
-
const getOptimizedEmbedder = module.getOptimizedEmbedder || module.default?.getOptimizedEmbedder;
|
|
530
|
-
if (!getOptimizedEmbedder) {
|
|
531
|
-
lastError = new Error(`Module loaded but getOptimizedEmbedder not found`);
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
// Only include defined values to not override defaults
|
|
535
|
-
const embedderConfig = {
|
|
536
|
-
modelId: this.modelId,
|
|
537
|
-
dimension: this.dimensions,
|
|
538
|
-
cacheSize: this.embedderCacheSize,
|
|
539
|
-
autoDownload: this.autoDownload,
|
|
540
|
-
};
|
|
541
|
-
if (this.modelDir !== undefined) {
|
|
542
|
-
embedderConfig.modelDir = this.modelDir;
|
|
543
|
-
}
|
|
544
|
-
this.embedder = getOptimizedEmbedder(embedderConfig);
|
|
545
|
-
await this.embedder.init();
|
|
546
|
-
this.initialized = true;
|
|
547
|
-
return true;
|
|
548
|
-
}
|
|
549
|
-
catch (error) {
|
|
550
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
// Build list of possible module paths to try
|
|
555
|
-
const possiblePaths = [];
|
|
556
|
-
// Try proper package exports first (preferred)
|
|
557
|
-
possiblePaths.push('agentic-flow/embeddings');
|
|
558
|
-
// Try node_modules resolution from different locations (for file:// imports)
|
|
559
|
-
try {
|
|
560
|
-
const path = await import('path');
|
|
561
|
-
const { existsSync } = await import('fs');
|
|
562
|
-
const cwd = process.cwd();
|
|
563
|
-
// Prioritize absolute paths that exist (for file:// import fallback)
|
|
564
|
-
const absolutePaths = [
|
|
565
|
-
path.join(cwd, 'node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'),
|
|
566
|
-
path.join(cwd, '../node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'),
|
|
567
|
-
'/workspaces/claude-flow/node_modules/agentic-flow/dist/embeddings/optimized-embedder.js',
|
|
568
|
-
];
|
|
569
|
-
for (const p of absolutePaths) {
|
|
570
|
-
if (existsSync(p)) {
|
|
571
|
-
possiblePaths.push(p);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
catch {
|
|
576
|
-
// fs/path module not available
|
|
577
|
-
}
|
|
578
|
-
// Try each path
|
|
579
|
-
for (const modulePath of possiblePaths) {
|
|
580
|
-
if (await createEmbedder(modulePath)) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
const errorDetail = lastError?.message ? ` Last error: ${lastError.message}` : '';
|
|
585
|
-
throw new Error(`Failed to initialize agentic-flow embeddings.${errorDetail} ` +
|
|
586
|
-
`Ensure agentic-flow is installed and ONNX model is downloaded: ` +
|
|
587
|
-
`npx agentic-flow@alpha embeddings init`);
|
|
588
|
-
}
|
|
589
|
-
async embed(text) {
|
|
590
|
-
await this.initialize();
|
|
591
|
-
// Check our LRU cache first
|
|
592
|
-
const cached = this.cache.get(text);
|
|
593
|
-
if (cached) {
|
|
594
|
-
this.emitEvent({ type: 'cache_hit', text });
|
|
595
|
-
return {
|
|
596
|
-
embedding: cached,
|
|
597
|
-
latencyMs: 0,
|
|
598
|
-
cached: true,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
this.emitEvent({ type: 'embed_start', text });
|
|
602
|
-
const startTime = performance.now();
|
|
603
|
-
try {
|
|
604
|
-
// Use agentic-flow's optimized embedder (has its own internal cache)
|
|
605
|
-
const embedding = await this.embedder.embed(text);
|
|
606
|
-
// Store in our cache as well
|
|
607
|
-
this.cache.set(text, embedding);
|
|
608
|
-
const latencyMs = performance.now() - startTime;
|
|
609
|
-
this.emitEvent({ type: 'embed_complete', text, latencyMs });
|
|
610
|
-
return {
|
|
611
|
-
embedding,
|
|
612
|
-
latencyMs,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
catch (error) {
|
|
616
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
617
|
-
this.emitEvent({ type: 'embed_error', text, error: message });
|
|
618
|
-
throw new Error(`Agentic-flow embedding failed: ${message}`);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
async embedBatch(texts) {
|
|
622
|
-
await this.initialize();
|
|
623
|
-
this.emitEvent({ type: 'batch_start', count: texts.length });
|
|
624
|
-
const startTime = performance.now();
|
|
625
|
-
// Check cache for each text
|
|
626
|
-
const cached = [];
|
|
627
|
-
const uncached = [];
|
|
628
|
-
texts.forEach((text, index) => {
|
|
629
|
-
const cachedEmbedding = this.cache.get(text);
|
|
630
|
-
if (cachedEmbedding) {
|
|
631
|
-
cached.push({ index, embedding: cachedEmbedding });
|
|
632
|
-
this.emitEvent({ type: 'cache_hit', text });
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
uncached.push({ index, text });
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
// Use optimized batch embedding for uncached texts
|
|
639
|
-
let batchEmbeddings = [];
|
|
640
|
-
if (uncached.length > 0) {
|
|
641
|
-
const uncachedTexts = uncached.map(u => u.text);
|
|
642
|
-
batchEmbeddings = await this.embedder.embedBatch(uncachedTexts);
|
|
643
|
-
// Cache results
|
|
644
|
-
uncached.forEach((item, i) => {
|
|
645
|
-
this.cache.set(item.text, batchEmbeddings[i]);
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
// Reconstruct result array in original order
|
|
649
|
-
const embeddings = new Array(texts.length);
|
|
650
|
-
cached.forEach(c => {
|
|
651
|
-
embeddings[c.index] = c.embedding;
|
|
652
|
-
});
|
|
653
|
-
uncached.forEach((u, i) => {
|
|
654
|
-
embeddings[u.index] = batchEmbeddings[i];
|
|
655
|
-
});
|
|
656
|
-
const totalLatencyMs = performance.now() - startTime;
|
|
657
|
-
this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
|
|
658
|
-
return {
|
|
659
|
-
embeddings,
|
|
660
|
-
totalLatencyMs,
|
|
661
|
-
avgLatencyMs: totalLatencyMs / texts.length,
|
|
662
|
-
cacheStats: {
|
|
663
|
-
hits: cached.length,
|
|
664
|
-
misses: uncached.length,
|
|
665
|
-
},
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Get combined cache statistics from both our LRU cache and embedder's internal cache
|
|
670
|
-
*/
|
|
671
|
-
getCacheStats() {
|
|
672
|
-
const baseStats = super.getCacheStats();
|
|
673
|
-
if (this.embedder && this.embedder.getCacheStats) {
|
|
674
|
-
const embedderStats = this.embedder.getCacheStats();
|
|
675
|
-
return {
|
|
676
|
-
size: baseStats.size + embedderStats.size,
|
|
677
|
-
maxSize: baseStats.maxSize + embedderStats.maxSize,
|
|
678
|
-
hitRate: baseStats.hitRate,
|
|
679
|
-
embedderCache: embedderStats,
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
return baseStats;
|
|
683
|
-
}
|
|
684
|
-
async shutdown() {
|
|
685
|
-
if (this.embedder && this.embedder.clearCache) {
|
|
686
|
-
this.embedder.clearCache();
|
|
687
|
-
}
|
|
688
|
-
await super.shutdown();
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
// ============================================================================
|
|
692
|
-
// Factory Functions
|
|
693
|
-
// ============================================================================
|
|
694
|
-
/**
|
|
695
|
-
* Check if agentic-flow is available
|
|
696
|
-
*/
|
|
697
|
-
async function isAgenticFlowAvailable() {
|
|
698
|
-
try {
|
|
699
|
-
await import('agentic-flow/embeddings');
|
|
700
|
-
return true;
|
|
701
|
-
}
|
|
702
|
-
catch {
|
|
703
|
-
return false;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Auto-install agentic-flow and initialize model
|
|
708
|
-
*/
|
|
709
|
-
async function autoInstallAgenticFlow() {
|
|
710
|
-
const { exec } = await import('child_process');
|
|
711
|
-
const { promisify } = await import('util');
|
|
712
|
-
const execAsync = promisify(exec);
|
|
713
|
-
try {
|
|
714
|
-
// Check if already available
|
|
715
|
-
if (await isAgenticFlowAvailable()) {
|
|
716
|
-
return true;
|
|
717
|
-
}
|
|
718
|
-
console.log('[embeddings] Installing agentic-flow@alpha...');
|
|
719
|
-
await execAsync('npm install agentic-flow@alpha --save', { timeout: 120000 });
|
|
720
|
-
// Initialize the model
|
|
721
|
-
console.log('[embeddings] Downloading embedding model...');
|
|
722
|
-
await execAsync('npx agentic-flow@alpha embeddings init', { timeout: 300000 });
|
|
723
|
-
// Verify installation
|
|
724
|
-
return await isAgenticFlowAvailable();
|
|
725
|
-
}
|
|
726
|
-
catch (error) {
|
|
727
|
-
console.warn('[embeddings] Auto-install failed:', error instanceof Error ? error.message : error);
|
|
728
|
-
return false;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Create embedding service based on configuration (sync version)
|
|
733
|
-
* Note: For 'auto' provider or smart fallback, use createEmbeddingServiceAsync
|
|
734
|
-
*/
|
|
735
|
-
export function createEmbeddingService(config) {
|
|
736
|
-
switch (config.provider) {
|
|
737
|
-
case 'openai':
|
|
738
|
-
return new OpenAIEmbeddingService(config);
|
|
739
|
-
case 'transformers':
|
|
740
|
-
return new TransformersEmbeddingService(config);
|
|
741
|
-
case 'mock':
|
|
742
|
-
return new MockEmbeddingService(config);
|
|
743
|
-
case 'agentic-flow':
|
|
744
|
-
return new AgenticFlowEmbeddingService(config);
|
|
745
|
-
case 'rvf':
|
|
746
|
-
return new RvfEmbeddingService(config);
|
|
747
|
-
default:
|
|
748
|
-
console.warn(`Unknown provider, using mock`);
|
|
749
|
-
return new MockEmbeddingService({ provider: 'mock', dimensions: 384 });
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
/**
|
|
753
|
-
* Create embedding service with automatic provider detection and fallback
|
|
754
|
-
*
|
|
755
|
-
* Features:
|
|
756
|
-
* - 'auto' provider picks best available: agentic-flow > transformers > mock
|
|
757
|
-
* - Automatic fallback if primary provider fails to initialize
|
|
758
|
-
* - Pre-validates provider availability before returning
|
|
759
|
-
*
|
|
760
|
-
* @example
|
|
761
|
-
* // Auto-select best provider
|
|
762
|
-
* const service = await createEmbeddingServiceAsync({ provider: 'auto' });
|
|
763
|
-
*
|
|
764
|
-
* // Try agentic-flow, fallback to transformers
|
|
765
|
-
* const service = await createEmbeddingServiceAsync({
|
|
766
|
-
* provider: 'agentic-flow',
|
|
767
|
-
* fallback: 'transformers'
|
|
768
|
-
* });
|
|
769
|
-
*/
|
|
770
|
-
export async function createEmbeddingServiceAsync(config) {
|
|
771
|
-
const { provider, fallback, autoInstall = true, ...rest } = config;
|
|
772
|
-
// Auto provider selection
|
|
773
|
-
if (provider === 'auto') {
|
|
774
|
-
// Try RVF first (52KB, always available, fast hash embeddings)
|
|
775
|
-
try {
|
|
776
|
-
const service = new RvfEmbeddingService({
|
|
777
|
-
provider: 'rvf',
|
|
778
|
-
dimensions: rest.dimensions ?? 384,
|
|
779
|
-
cacheSize: rest.cacheSize,
|
|
780
|
-
});
|
|
781
|
-
await service.embed('test');
|
|
782
|
-
return service;
|
|
783
|
-
}
|
|
784
|
-
catch { /* fall through */ }
|
|
785
|
-
// Try agentic-flow (fastest neural, ONNX-based)
|
|
786
|
-
let agenticFlowAvailable = await isAgenticFlowAvailable();
|
|
787
|
-
// Auto-install if not available and autoInstall is enabled
|
|
788
|
-
if (!agenticFlowAvailable && autoInstall) {
|
|
789
|
-
agenticFlowAvailable = await autoInstallAgenticFlow();
|
|
790
|
-
}
|
|
791
|
-
if (agenticFlowAvailable) {
|
|
792
|
-
try {
|
|
793
|
-
const service = new AgenticFlowEmbeddingService({
|
|
794
|
-
provider: 'agentic-flow',
|
|
795
|
-
modelId: rest.modelId ?? 'all-MiniLM-L6-v2',
|
|
796
|
-
dimensions: rest.dimensions ?? 384,
|
|
797
|
-
cacheSize: rest.cacheSize,
|
|
798
|
-
});
|
|
799
|
-
// Validate it can initialize
|
|
800
|
-
await service.embed('test');
|
|
801
|
-
return service;
|
|
802
|
-
}
|
|
803
|
-
catch {
|
|
804
|
-
// Fall through to next option
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
// Try transformers (good quality, built-in)
|
|
808
|
-
try {
|
|
809
|
-
const service = new TransformersEmbeddingService({
|
|
810
|
-
provider: 'transformers',
|
|
811
|
-
model: rest.model ?? 'Xenova/all-MiniLM-L6-v2',
|
|
812
|
-
cacheSize: rest.cacheSize,
|
|
813
|
-
});
|
|
814
|
-
// Validate it can initialize
|
|
815
|
-
await service.embed('test');
|
|
816
|
-
return service;
|
|
817
|
-
}
|
|
818
|
-
catch {
|
|
819
|
-
// Fall through to mock
|
|
820
|
-
}
|
|
821
|
-
// Fallback to mock (always works)
|
|
822
|
-
console.warn('[embeddings] Using mock provider - install agentic-flow or @xenova/transformers for real embeddings');
|
|
823
|
-
return new MockEmbeddingService({
|
|
824
|
-
dimensions: rest.dimensions ?? 384,
|
|
825
|
-
cacheSize: rest.cacheSize,
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
// Specific provider with optional fallback
|
|
829
|
-
const createPrimary = () => {
|
|
830
|
-
switch (provider) {
|
|
831
|
-
case 'agentic-flow':
|
|
832
|
-
return new AgenticFlowEmbeddingService({
|
|
833
|
-
provider: 'agentic-flow',
|
|
834
|
-
modelId: rest.modelId ?? 'all-MiniLM-L6-v2',
|
|
835
|
-
dimensions: rest.dimensions ?? 384,
|
|
836
|
-
cacheSize: rest.cacheSize,
|
|
837
|
-
});
|
|
838
|
-
case 'transformers':
|
|
839
|
-
return new TransformersEmbeddingService({
|
|
840
|
-
provider: 'transformers',
|
|
841
|
-
model: rest.model ?? 'Xenova/all-MiniLM-L6-v2',
|
|
842
|
-
cacheSize: rest.cacheSize,
|
|
843
|
-
});
|
|
844
|
-
case 'openai':
|
|
845
|
-
if (!rest.apiKey)
|
|
846
|
-
throw new Error('OpenAI provider requires apiKey');
|
|
847
|
-
return new OpenAIEmbeddingService({
|
|
848
|
-
provider: 'openai',
|
|
849
|
-
apiKey: rest.apiKey,
|
|
850
|
-
dimensions: rest.dimensions,
|
|
851
|
-
cacheSize: rest.cacheSize,
|
|
852
|
-
});
|
|
853
|
-
case 'rvf':
|
|
854
|
-
return new RvfEmbeddingService({
|
|
855
|
-
provider: 'rvf',
|
|
856
|
-
dimensions: rest.dimensions ?? 384,
|
|
857
|
-
cacheSize: rest.cacheSize,
|
|
858
|
-
});
|
|
859
|
-
case 'mock':
|
|
860
|
-
return new MockEmbeddingService({
|
|
861
|
-
dimensions: rest.dimensions ?? 384,
|
|
862
|
-
cacheSize: rest.cacheSize,
|
|
863
|
-
});
|
|
864
|
-
default:
|
|
865
|
-
throw new Error(`Unknown provider: ${provider}`);
|
|
866
|
-
}
|
|
867
|
-
};
|
|
868
|
-
const primary = createPrimary();
|
|
869
|
-
// Try to validate primary provider
|
|
870
|
-
try {
|
|
871
|
-
await primary.embed('test');
|
|
872
|
-
return primary;
|
|
873
|
-
}
|
|
874
|
-
catch (error) {
|
|
875
|
-
if (!fallback) {
|
|
876
|
-
throw error;
|
|
877
|
-
}
|
|
878
|
-
// Try fallback
|
|
879
|
-
console.warn(`[embeddings] Primary provider '${provider}' failed, using fallback '${fallback}'`);
|
|
880
|
-
const fallbackConfig = { ...rest, provider: fallback };
|
|
881
|
-
return createEmbeddingServiceAsync(fallbackConfig);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Convenience function for quick embeddings
|
|
886
|
-
*/
|
|
887
|
-
export async function getEmbedding(text, config) {
|
|
888
|
-
const service = createEmbeddingService({
|
|
889
|
-
provider: 'mock',
|
|
890
|
-
dimensions: 384,
|
|
891
|
-
...config,
|
|
892
|
-
});
|
|
893
|
-
try {
|
|
894
|
-
const result = await service.embed(text);
|
|
895
|
-
return result.embedding;
|
|
896
|
-
}
|
|
897
|
-
finally {
|
|
898
|
-
await service.shutdown();
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
// ============================================================================
|
|
902
|
-
// Similarity Functions
|
|
903
|
-
// ============================================================================
|
|
904
|
-
/**
|
|
905
|
-
* Compute cosine similarity between two embeddings
|
|
906
|
-
*/
|
|
907
|
-
export function cosineSimilarity(a, b) {
|
|
908
|
-
if (a.length !== b.length) {
|
|
909
|
-
throw new Error('Embedding dimensions must match');
|
|
910
|
-
}
|
|
911
|
-
let dot = 0;
|
|
912
|
-
let normA = 0;
|
|
913
|
-
let normB = 0;
|
|
914
|
-
for (let i = 0; i < a.length; i++) {
|
|
915
|
-
dot += a[i] * b[i];
|
|
916
|
-
normA += a[i] * a[i];
|
|
917
|
-
normB += b[i] * b[i];
|
|
918
|
-
}
|
|
919
|
-
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
920
|
-
return denom > 0 ? dot / denom : 0;
|
|
921
|
-
}
|
|
922
|
-
/**
|
|
923
|
-
* Compute Euclidean distance between two embeddings
|
|
924
|
-
*/
|
|
925
|
-
export function euclideanDistance(a, b) {
|
|
926
|
-
if (a.length !== b.length) {
|
|
927
|
-
throw new Error('Embedding dimensions must match');
|
|
928
|
-
}
|
|
929
|
-
let sum = 0;
|
|
930
|
-
for (let i = 0; i < a.length; i++) {
|
|
931
|
-
const diff = a[i] - b[i];
|
|
932
|
-
sum += diff * diff;
|
|
933
|
-
}
|
|
934
|
-
return Math.sqrt(sum);
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Compute dot product between two embeddings
|
|
938
|
-
*/
|
|
939
|
-
export function dotProduct(a, b) {
|
|
940
|
-
if (a.length !== b.length) {
|
|
941
|
-
throw new Error('Embedding dimensions must match');
|
|
942
|
-
}
|
|
943
|
-
let dot = 0;
|
|
944
|
-
for (let i = 0; i < a.length; i++) {
|
|
945
|
-
dot += a[i] * b[i];
|
|
946
|
-
}
|
|
947
|
-
return dot;
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
|
-
* Compute similarity using specified metric
|
|
951
|
-
*/
|
|
952
|
-
export function computeSimilarity(a, b, metric = 'cosine') {
|
|
953
|
-
switch (metric) {
|
|
954
|
-
case 'cosine':
|
|
955
|
-
return { score: cosineSimilarity(a, b), metric };
|
|
956
|
-
case 'euclidean':
|
|
957
|
-
// Convert distance to similarity (closer = higher score)
|
|
958
|
-
return { score: 1 / (1 + euclideanDistance(a, b)), metric };
|
|
959
|
-
case 'dot':
|
|
960
|
-
return { score: dotProduct(a, b), metric };
|
|
961
|
-
default:
|
|
962
|
-
return { score: cosineSimilarity(a, b), metric: 'cosine' };
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
//# sourceMappingURL=embedding-service.js.map
|