UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

307 lines (299 loc) 11.8 kB
/** * Reranker Implementation * * Multi-factor scoring system for reranking retrieval results. * Combines semantic relevance (LLM-based), vector similarity, and position. */ import { withSpan } from "../../telemetry/withSpan.js"; import { tracers } from "../../telemetry/tracers.js"; import { logger } from "../../utils/logger.js"; /** * Default scoring weights */ const DEFAULT_WEIGHTS = { semantic: 0.4, vector: 0.4, position: 0.2, }; /** * Rerank vector search results using multi-factor scoring * * Combines three scoring factors: * 1. Semantic score: LLM-based relevance assessment * 2. Vector score: Original similarity score from vector search * 3. Position score: Inverse of original ranking position * * @param results - Vector search results to rerank * @param query - Original search query * @param model - Language model for semantic scoring * @param options - Reranking options * @returns Reranked results with detailed scores */ export async function rerank(results, query, model, options) { return withSpan({ name: "neurolink.rag.rerank", tracer: tracers.rag, attributes: { "rag.reranker.input_count": results.length, "rag.reranker.top_k": options?.topK ?? 3, "rag.reranker.query_length": query.length, }, }, async (span) => { const { queryEmbedding: _queryEmbedding, topK = 3, weights = DEFAULT_WEIGHTS, } = options || {}; if (results.length === 0) { span.setAttribute("rag.reranker.output_count", 0); return []; } // Validate weights sum to 1.0 const totalWeight = (weights.semantic || DEFAULT_WEIGHTS.semantic) + (weights.vector || DEFAULT_WEIGHTS.vector) + (weights.position || DEFAULT_WEIGHTS.position); if (Math.abs(totalWeight - 1.0) > 0.01) { logger.warn("[Reranker] Weights do not sum to 1.0, normalizing", { original: weights, total: totalWeight, }); } const normalizedWeights = { semantic: (weights.semantic || DEFAULT_WEIGHTS.semantic) / totalWeight, vector: (weights.vector || DEFAULT_WEIGHTS.vector) / totalWeight, position: (weights.position || DEFAULT_WEIGHTS.position) / totalWeight, }; const rerankedResults = []; // Process results in parallel batches for efficiency const batchSize = 5; for (let i = 0; i < results.length; i += batchSize) { const batch = results.slice(i, i + batchSize); const batchPromises = batch.map(async (result, batchIndex) => { const globalIndex = i + batchIndex; // Calculate vector score (use existing score or 0) const vectorScore = result.score ?? 0; // Calculate position score (inverse of position) const positionScore = 1 - globalIndex / results.length; // Calculate semantic score using LLM const semanticResult = await calculateSemanticScore(query, result.text || result.metadata?.text || "", model); // Combine scores const combinedScore = normalizedWeights.semantic * semanticResult.score + normalizedWeights.vector * vectorScore + normalizedWeights.position * positionScore; return { result, score: combinedScore, details: { semantic: semanticResult.score, vector: vectorScore, position: positionScore, queryAnalysis: semanticResult.analysis, }, }; }); const batchResults = await Promise.all(batchPromises); rerankedResults.push(...batchResults); } // Sort by combined score descending rerankedResults.sort((a, b) => b.score - a.score); // Return top K results const output = rerankedResults.slice(0, topK); span.setAttribute("rag.reranker.output_count", output.length); return output; }); // end withSpan } /** * Calculate semantic relevance score using LLM * * @param query - Search query * @param text - Document text to score * @param model - Language model for scoring * @returns Score between 0 and 1 with optional analysis */ async function calculateSemanticScore(query, text, model) { const prompt = `Rate the relevance of the following text to the query on a scale of 0 to 1. Query: ${query} Text: ${text.slice(0, 1000)} Respond with only a number between 0 and 1, where: - 0 means completely irrelevant - 0.5 means somewhat relevant - 1 means highly relevant Score:`; try { const result = await model.generate({ prompt, maxTokens: 10, temperature: 0, }); const scoreText = result?.content?.trim() || "0"; const score = parseFloat(scoreText); if (isNaN(score) || score < 0 || score > 1) { return { score: 0.5 }; } return { score }; } catch (error) { logger.warn("[Reranker] Semantic scoring failed, using default", { error: error instanceof Error ? error.message : String(error), }); return { score: 0.5 }; } } /** * Batch rerank with optimized LLM calls * Scores multiple documents in a single prompt for efficiency * * @param results - Results to rerank * @param query - Search query * @param model - Language model * @param options - Reranking options * @returns Reranked results */ export async function batchRerank(results, query, model, options) { return withSpan({ name: "neurolink.rag.batchRerank", tracer: tracers.rag, attributes: { "rag.reranker.input_count": results.length, "rag.reranker.top_k": options?.topK ?? 3, "rag.reranker.query_length": query.length, "rag.reranker.batch": true, }, }, async (span) => { const { topK = 3, weights = DEFAULT_WEIGHTS } = options || {}; if (results.length === 0) { span.setAttribute("rag.reranker.output_count", 0); return []; } // Normalize weights const totalWeight = (weights.semantic || DEFAULT_WEIGHTS.semantic) + (weights.vector || DEFAULT_WEIGHTS.vector) + (weights.position || DEFAULT_WEIGHTS.position); const normalizedWeights = { semantic: (weights.semantic || DEFAULT_WEIGHTS.semantic) / totalWeight, vector: (weights.vector || DEFAULT_WEIGHTS.vector) / totalWeight, position: (weights.position || DEFAULT_WEIGHTS.position) / totalWeight, }; // Build batch scoring prompt const documentsText = results .map((r, i) => `[${i + 1}] ${(r.text || r.metadata?.text || "").slice(0, 300)}`) .join("\n\n"); const prompt = `Rate the relevance of each document to the query on a scale of 0 to 1. Query: ${query} Documents: ${documentsText} For each document, provide a score between 0 and 1. Respond with only the scores, one per line, in order:`; try { const result = await model.generate({ prompt, maxTokens: 50, temperature: 0, }); // Parse scores from response const scoreLines = (result?.content || "") .trim() .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0); const semanticScores = []; for (let i = 0; i < results.length; i++) { const scoreLine = scoreLines[i]; if (scoreLine) { const score = parseFloat(scoreLine.match(/[\d.]+/)?.[0] || "0.5"); semanticScores.push(isNaN(score) || score < 0 || score > 1 ? 0.5 : score); } else { semanticScores.push(0.5); } } // Calculate combined scores const rerankedResults = results.map((result, i) => { const vectorScore = result.score ?? 0; const positionScore = 1 - i / results.length; const semanticScore = semanticScores[i] ?? 0.5; const combinedScore = normalizedWeights.semantic * semanticScore + normalizedWeights.vector * vectorScore + normalizedWeights.position * positionScore; return { result, score: combinedScore, details: { semantic: semanticScore, vector: vectorScore, position: positionScore, }, }; }); // Sort and return top K rerankedResults.sort((a, b) => b.score - a.score); const output = rerankedResults.slice(0, topK); span.setAttribute("rag.reranker.output_count", output.length); return output; } catch (error) { logger.warn("[Reranker] Batch scoring failed, using individual scoring", { error: error instanceof Error ? error.message : String(error), }); // Fall back to individual scoring return rerank(results, query, model, options); } }); // end withSpan } /** * Simple position-based reranker (no LLM required) * Uses only vector score and position * * @param results - Results to rerank * @param options - Reranking options * @returns Reranked results */ export function simpleRerank(results, options) { const { topK = 3, vectorWeight = 0.8, positionWeight = 0.2 } = options || {}; const totalWeight = vectorWeight + positionWeight; const normalizedVectorWeight = vectorWeight / totalWeight; const normalizedPositionWeight = positionWeight / totalWeight; const rerankedResults = results.map((result, i) => { const vectorScore = result.score ?? 0; const positionScore = 1 - i / results.length; const combinedScore = normalizedVectorWeight * vectorScore + normalizedPositionWeight * positionScore; return { result, score: combinedScore, details: { semantic: 0, vector: vectorScore, position: positionScore, }, }; }); rerankedResults.sort((a, b) => b.score - a.score); return rerankedResults.slice(0, topK); } /** * Cohere-style relevance scorer interface * Placeholder for integration with Cohere's rerank API */ export class CohereRelevanceScorer { modelName; constructor(modelName = "rerank-v3.5") { this.modelName = modelName; } async score(_query, _documents) { // Placeholder - would use Cohere's rerank API throw new Error("CohereRelevanceScorer requires Cohere API integration. " + "Install @cohere-ai/cohere and provide API key."); } } /** * Cross-encoder style reranker interface * Placeholder for integration with cross-encoder models */ export class CrossEncoderReranker { modelName; constructor(modelName = "ms-marco-MiniLM-L-6-v2") { this.modelName = modelName; } async rerank(_query, _documents) { // Placeholder - would use cross-encoder model throw new Error("CrossEncoderReranker requires a cross-encoder model. " + "Consider using the LLM-based rerank function instead."); } }