Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
import { logger } from '../../utils/logger.js';
import { LocalGPUEmbeddingsProvider } from './LocalGPUEmbeddings.js';
export interface EmbeddingProvider {
name: string;
dimensions: number;
generateEmbedding(text: string): Promise<number[]>;
generateEmbeddings(texts: string[]): Promise<number[][]>;
}
/**
* HuggingFace Embeddings Provider
* Uses HuggingFace Inference API
*/
class HuggingFaceEmbeddingsProvider implements EmbeddingProvider {
name = 'huggingface';
dimensions = 768;
private apiKey: string;
private model = 'sentence-transformers/all-MiniLM-L6-v2';
constructor(apiKey?: string) {
this.apiKey = apiKey || process.env.HUGGINGFACE_API_KEY || '';
}
async generateEmbedding(text: string): Promise<number[]> {
if (!this.apiKey) {
throw new Error('HuggingFace API key not configured');
}
const response = await fetch(
`https://api-inference.huggingface.co/pipeline/feature-extraction/${this.model}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ inputs: text }),
}
);
if (!response.ok) {
throw new Error(`HuggingFace API error: ${response.statusText}`);
}
const embedding = await response.json();
return embedding;
}
async generateEmbeddings(texts: string[]): Promise<number[][]> {
const embeddings = await Promise.all(texts.map(t => this.generateEmbedding(t)));
return embeddings;
}
}
/**
* OpenAI Embeddings Provider
* Uses OpenAI Embeddings API
*/
class OpenAIEmbeddingsProvider implements EmbeddingProvider {
name = 'openai';
dimensions = 1536;
private apiKey: string;
private model = 'text-embedding-3-small';
constructor(apiKey?: string) {
this.apiKey = apiKey || process.env.OPENAI_API_KEY || '';
}
async generateEmbedding(text: string): Promise<number[]> {
if (!this.apiKey) {
throw new Error('OpenAI API key not configured');
}
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
input: text,
}),
});
if (!response.ok) {
throw new Error(`OpenAI API error: ${response.statusText}`);
}
const data = await response.json();
return data.data[0].embedding;
}
async generateEmbeddings(texts: string[]): Promise<number[][]> {
if (!this.apiKey) {
throw new Error('OpenAI API key not configured');
}
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.model,
input: texts,
}),
});
if (!response.ok) {
throw new Error(`OpenAI API error: ${response.statusText}`);
}
const data = await response.json();
return data.data.map((item: any) => item.embedding);
}
}
/**
* Local Transformers.js Provider (Fallback)
* Uses browser-compatible ML models
*/
class TransformersEmbeddingsProvider implements EmbeddingProvider {
name = 'transformers';
dimensions = 384;
private isInitialized = false;
private pipeline: any;
async initialize(): Promise<void> {
if (this.isInitialized) return;
// Skip transformers in Docker/production - ONNX runtime has architecture issues on Alpine
const isDocker = process.env.NODE_ENV === 'production' || process.cwd().startsWith('/app');
if (isDocker) {
throw new Error('Transformers.js disabled in Docker mode (ONNX incompatibility)');
}
try {
// Dynamic import to avoid bundling issues
const { pipeline } = await import('@xenova/transformers');
this.pipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
this.isInitialized = true;
logger.info('✅ Local Transformers.js embeddings initialized');
} catch (error: any) {
logger.warn('⚠️ Transformers.js not available:', error.message);
throw error;
}
}
async generateEmbedding(text: string): Promise<number[]> {
if (!this.isInitialized) {
await this.initialize();
}
const output = await this.pipeline(text, { pooling: 'mean', normalize: true });
return Array.from(output.data);
}
async generateEmbeddings(texts: string[]): Promise<number[][]> {
const embeddings = await Promise.all(texts.map(t => this.generateEmbedding(t)));
return embeddings;
}
}
/**
* Unified Embedding Service
* Auto-selects best available provider
*/
export class EmbeddingService {
private provider: EmbeddingProvider | null = null;
private preferredProvider: string;
constructor(preferredProvider?: string) {
this.preferredProvider = preferredProvider || process.env.EMBEDDING_PROVIDER || 'auto';
}
async initialize(): Promise<void> {
if (this.provider) return;
// Try providers in order of preference
const providers: Array<{ name: string; factory: () => EmbeddingProvider }> = [
{ name: 'local-gpu', factory: () => new LocalGPUEmbeddingsProvider() },
{ name: 'openai', factory: () => new OpenAIEmbeddingsProvider() },
{ name: 'huggingface', factory: () => new HuggingFaceEmbeddingsProvider() },
{ name: 'transformers', factory: () => new TransformersEmbeddingsProvider() },
];
// Check if GPU is explicitly enabled in environment (Docker/HF Spaces)
const useGpu = process.env.USE_GPU === 'true';
// If specific provider requested, try it first
if (this.preferredProvider !== 'auto') {
const preferred = providers.find(p => p.name === this.preferredProvider);
if (preferred) {
providers.unshift(preferred);
}
} else if (useGpu) {
// Prioritize GPU if environment says so
const gpuProvider = providers.find(p => p.name === 'local-gpu');
if (gpuProvider) {
providers.unshift(gpuProvider);
}
}
for (const { name, factory } of providers) {
try {
// Skip GPU provider if not explicitly enabled to avoid spawning python processes locally unnecessarily
if (name === 'local-gpu' && !useGpu && this.preferredProvider !== 'local-gpu') {
continue;
}
const provider = factory();
// Initialize provider
if (provider instanceof TransformersEmbeddingsProvider) {
await provider.initialize();
} else if (provider instanceof LocalGPUEmbeddingsProvider) {
await provider.initialize();
} else {
// Quick test with small text
await provider.generateEmbedding('test');
}
this.provider = provider;
logger.info(`🧠 Embedding provider initialized: ${name} (${provider.dimensions}D)`);
return;
} catch (error: any) {
logger.warn(`⚠️ ${name} embeddings not available: ${error.message}`);
}
}
throw new Error(
'No embedding provider available. Please configure API keys or install @xenova/transformers.'
);
}
async generateEmbedding(text: string): Promise<number[]> {
if (!this.provider) {
await this.initialize();
}
return this.provider!.generateEmbedding(text);
}
async generateEmbeddings(texts: string[]): Promise<number[][]> {
if (!this.provider) {
await this.initialize();
}
return this.provider!.generateEmbeddings(texts);
}
getDimensions(): number {
if (!this.provider) {
throw new Error('Embedding service not initialized');
}
return this.provider.dimensions;
}
getProviderName(): string {
if (!this.provider) {
throw new Error('Embedding service not initialized');
}
return this.provider.name;
}
}
// Singleton instance
let embeddingServiceInstance: EmbeddingService | null = null;
export function getEmbeddingService(): EmbeddingService {
if (!embeddingServiceInstance) {
embeddingServiceInstance = new EmbeddingService();
}
return embeddingServiceInstance;
}