Skip to main content
Glama
embedding-service.ts8.6 kB
import OpenAI from 'openai'; import { SemanticSearchConfig, SemanticEmbedding } from '../types.js'; import { getSemanticSearchConfig } from '../config.js'; export interface EmbeddingProvider { generateEmbedding(text: string): Promise<number[]>; generateEmbeddings(texts: string[]): Promise<number[][]>; getDimensions(): number; getModel(): string; } export class OpenAIEmbeddingProvider implements EmbeddingProvider { private client: OpenAI; private config: SemanticSearchConfig; constructor(config: SemanticSearchConfig) { if (!config.api_key) { throw new Error('OpenAI API key is required for OpenAI embedding provider'); } this.config = config; // Support custom base URLs for OpenAI-compatible APIs (e.g., LLM Studio) const clientConfig: any = { apiKey: config.api_key, }; if (config.base_url) { clientConfig.baseURL = config.base_url; } this.client = new OpenAI(clientConfig); } async generateEmbedding(text: string): Promise<number[]> { try { // Truncate text if it exceeds max tokens const truncatedText = this.truncateText(text, this.config.max_tokens); const response = await this.client.embeddings.create({ model: this.config.model, input: truncatedText, }); return response.data[0].embedding; } catch (error) { throw new Error(`Failed to generate embedding: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generateEmbeddings(texts: string[]): Promise<number[][]> { try { // Process in batches to avoid API limits const results: number[][] = []; const batchSize = Math.min(this.config.batch_size, texts.length); for (let i = 0; i < texts.length; i += batchSize) { const batch = texts.slice(i, i + batchSize); const truncatedBatch = batch.map(text => this.truncateText(text, this.config.max_tokens)); const response = await this.client.embeddings.create({ model: this.config.model, input: truncatedBatch, }); const batchEmbeddings = response.data.map(item => item.embedding); results.push(...batchEmbeddings); } return results; } catch (error) { throw new Error(`Failed to generate batch embeddings: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getDimensions(): number { return this.config.dimensions; } getModel(): string { return this.config.model; } private truncateText(text: string, maxTokens: number): string { // Simple token estimation: ~4 characters per token const estimatedTokens = text.length / 4; if (estimatedTokens <= maxTokens) { return text; } const maxChars = maxTokens * 4; return text.substring(0, maxChars) + '...'; } } export class OllamaEmbeddingProvider implements EmbeddingProvider { private config: SemanticSearchConfig; private baseUrl: string; constructor(config: SemanticSearchConfig) { this.config = config; this.baseUrl = config.base_url || 'http://localhost:11434'; } async generateEmbedding(text: string): Promise<number[]> { try { const truncatedText = this.truncateText(text, this.config.max_tokens); const response = await fetch(`${this.baseUrl}/api/embeddings`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: this.config.model, prompt: truncatedText, }), }); if (!response.ok) { throw new Error(`Ollama API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.embedding; } catch (error) { throw new Error(`Failed to generate Ollama embedding: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generateEmbeddings(texts: string[]): Promise<number[][]> { try { const results: number[][] = []; const batchSize = Math.min(this.config.batch_size, texts.length); // Ollama doesn't support batch embeddings, so we process individually // but in controlled batches to avoid overwhelming the server for (let i = 0; i < texts.length; i += batchSize) { const batch = texts.slice(i, i + batchSize); const batchPromises = batch.map(text => this.generateEmbedding(text)); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); } return results; } catch (error) { throw new Error(`Failed to generate Ollama batch embeddings: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getDimensions(): number { return this.config.dimensions; } getModel(): string { return this.config.model; } private truncateText(text: string, maxTokens: number): string { // Simple token estimation: ~4 characters per token const estimatedTokens = text.length / 4; if (estimatedTokens <= maxTokens) { return text; } const maxChars = maxTokens * 4; return text.substring(0, maxChars) + '...'; } } export class EmbeddingService { private provider: EmbeddingProvider | null = null; private config: SemanticSearchConfig; constructor(config?: SemanticSearchConfig) { this.config = config || getSemanticSearchConfig(); this.initializeProvider(); } private initializeProvider(): void { if (this.config.provider === 'disabled') { this.provider = null; return; } try { switch (this.config.provider) { case 'openai': this.provider = new OpenAIEmbeddingProvider(this.config); break; case 'ollama': this.provider = new OllamaEmbeddingProvider(this.config); break; default: throw new Error(`Unknown embedding provider: ${this.config.provider}`); } } catch (error) { console.warn(`Failed to initialize embedding provider: ${error instanceof Error ? error.message : 'Unknown error'}`); this.provider = null; } } isEnabled(): boolean { return this.provider !== null; } async generateEmbedding(text: string): Promise<SemanticEmbedding | null> { if (!this.provider) { return null; } try { const vector = await this.provider.generateEmbedding(text); return { vector, model: this.provider.getModel(), version: '1.0', created_at: new Date() }; } catch (error) { console.error(`Failed to generate embedding: ${error instanceof Error ? error.message : 'Unknown error'}`); return null; } } async generateEmbeddings(texts: string[]): Promise<(SemanticEmbedding | null)[]> { if (!this.provider) { return texts.map(() => null); } try { const vectors = await this.provider.generateEmbeddings(texts); return vectors.map(vector => ({ vector, model: this.provider!.getModel(), version: '1.0', created_at: new Date() })); } catch (error) { console.error(`Failed to generate batch embeddings: ${error instanceof Error ? error.message : 'Unknown error'}`); return texts.map(() => null); } } // Extract semantic content from code nodes extractSemanticContent(node: any): string { const parts = []; // Add name and qualified name if (node.name) parts.push(node.name); if (node.qualified_name && node.qualified_name !== node.name) { parts.push(node.qualified_name); } // Add description if (node.description) parts.push(node.description); // Add parameter information if (node.attributes?.parameters) { const paramInfo = node.attributes.parameters .map((p: any) => `${p.name}: ${p.type}${p.description ? ` - ${p.description}` : ''}`) .join(', '); if (paramInfo) parts.push(`Parameters: ${paramInfo}`); } // Add return type if (node.attributes?.return_type) { parts.push(`Returns: ${node.attributes.return_type}`); } // Add annotations/decorators if (node.attributes?.annotations) { const annotations = node.attributes.annotations .map((a: any) => a.name) .join(', '); if (annotations) parts.push(`Annotations: ${annotations}`); } // Add modifiers if (node.modifiers && node.modifiers.length > 0) { parts.push(`Modifiers: ${node.modifiers.join(', ')}`); } return parts.join(' | '); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JonnoC/CodeRAG'

If you have feedback or need assistance with the MCP directory API, please join our Discord server