crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
204 lines (203 loc) • 7.64 kB
JavaScript
/**
* Base Embedder Abstract Class
*
* Defines the interface and common functionality for all embedders
* with optimized performance and memory characteristics
*/
/**
* Abstract base class for all embedders
* Implements common functionality with optimized performance
*/
export class BaseEmbedder {
options;
client;
_model;
retryOptions;
cache = new Map();
constructor(options) {
this.options = options;
const defaultRetry = {
maxRetries: 3,
initialBackoff: 1000,
maxBackoff: 30000
};
this.options = {
...options,
model: options.model || 'text-embedding-3-small',
provider: options.provider || 'unknown',
retry: options.retry || defaultRetry,
debug: options.debug ?? false,
dimensions: options.dimensions ?? 768,
cacheSize: options.cacheSize ?? 1000,
cacheTTL: options.cacheTTL ?? 3600000,
batchSize: options.batchSize ?? 16,
maxConcurrency: options.maxConcurrency ?? 4,
apiKey: options.apiKey ?? '',
apiUrl: options.apiUrl ?? '',
useLocal: options.useLocal ?? false,
localModelPath: options.localModelPath ?? '',
maxLength: options.maxLength ?? 512,
useAveragePooling: options.useAveragePooling ?? true,
timeout: options.timeout ?? 30000,
embeddingFunction: options.embeddingFunction || ((text) => Promise.resolve(new Float32Array(this.options.dimensions))),
normalize: options.normalize ?? false
};
this._model = this.options.model || 'text-embedding-3-small';
this.retryOptions = this.options.retry;
}
async executeWithRetry(operation, maxRetries = this.retryOptions.maxRetries, initialBackoff = this.retryOptions.initialBackoff, maxBackoff = this.retryOptions.maxBackoff) {
let currentBackoff = initialBackoff;
let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await operation();
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (this.isTransientError(lastError)) {
if (attempt === maxRetries - 1) {
throw lastError;
}
await new Promise(resolve => setTimeout(resolve, currentBackoff));
currentBackoff = Math.min(currentBackoff * 2, maxBackoff);
}
else {
throw lastError;
}
}
}
throw lastError || new Error('Unknown error during operation');
}
async executeBatchWithRetry(operation, maxRetries = this.retryOptions.maxRetries, initialBackoff = this.retryOptions.initialBackoff, maxBackoff = this.retryOptions.maxBackoff) {
return await this.executeWithRetry(operation, maxRetries, initialBackoff, maxBackoff);
}
isTransientError(error) {
const message = error.message.toLowerCase();
return (
// Network errors
message.includes('etimedout') ||
message.includes('econnreset') ||
message.includes('econnrefused') ||
message.includes('network error') ||
message.includes('aborted') ||
message.includes('timeout') ||
// Rate limiting and server errors
message.includes('rate limit') ||
message.includes('too many requests') ||
message.includes('429') ||
message.includes('500') ||
message.includes('503'));
}
normalizeVector(vector) {
if (!vector || vector.length === 0) {
throw new Error('Vector is required and must have at least one element');
}
let magnitude = 0;
for (let i = 0; i < vector.length; i++) {
const value = vector[i];
if (value === undefined) {
throw new Error('Vector contains undefined values');
}
magnitude += value * value;
}
magnitude = Math.sqrt(magnitude);
if (magnitude === 0) {
return vector;
}
const normalized = new Float32Array(vector.length);
for (let i = 0; i < vector.length; i++) {
const value = vector[i];
if (value === undefined) {
throw new Error('Vector contains undefined values');
}
normalized[i] = value / magnitude;
}
return normalized;
}
clearCache() {
this.cache.clear();
}
getCacheSize() {
return this.cache.size;
}
generateCacheKey(text) {
if (!text) {
throw new Error('Text is required for cache key generation');
}
return `${this._model}-${text}`;
}
async embedText(text) {
if (!text) {
if (this.options.debug) {
console.warn('Empty text provided for embedding, returning zero vector');
}
return new Float32Array(this.options.dimensions);
}
const cacheKey = this.generateCacheKey(text);
const cached = this.getCachedEmbedding(cacheKey);
if (cached) {
return cached;
}
try {
const embedding = await this.embed(text);
if (!embedding) {
throw new Error('Embedding function returned undefined');
}
// Ensure embedding is Float32Array
const embeddingArray = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
this.cache.set(cacheKey, embeddingArray);
return embeddingArray;
}
catch (error) {
console.error(`Embedding failed:`, error);
return new Float32Array(this.options.dimensions);
}
}
async embedTexts(texts) {
if (!texts?.length) {
return [];
}
const batchSize = this.options.batchSize;
const numBatches = Math.ceil(texts.length / batchSize);
const results = [];
for (let i = 0; i < numBatches; i++) {
const batch = texts.slice(i * batchSize, (i + 1) * batchSize);
try {
const embeddings = await this.embedBatch(batch);
if (!embeddings) {
throw new Error('EmbedBatch returned undefined');
}
results.push(...embeddings);
}
catch (error) {
console.error(`Batch embedding failed:`, error);
const zeroVector = new Float32Array(this.options.dimensions);
results.push(...Array(batch.length).fill(zeroVector));
}
}
return results;
}
getCachedEmbedding(cacheKey) {
if (!cacheKey) {
return null;
}
const cachedItem = this.cache.get(cacheKey);
if (!cachedItem) {
return null;
}
return cachedItem;
}
async batchProcess(items, batchSize, processFn) {
if (!items?.length) {
return [];
}
const results = [];
const numBatches = Math.ceil(items.length / batchSize);
for (let i = 0; i < numBatches; i++) {
const batch = items.slice(i * batchSize, (i + 1) * batchSize);
const batchResults = await processFn(batch);
results.push(...batchResults);
}
return results;
}
}