@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
305 lines (304 loc) • 10.8 kB
JavaScript
/**
* RAG Retry Handler
*
* Provides retry logic with exponential backoff and jitter
* specifically designed for RAG operations including embeddings,
* vector queries, and LLM-based extraction.
*/
import { withRetry } from "../../core/infrastructure/index.js";
import { isAbortError } from "../../utils/errorHandling.js";
import { logger } from "../../utils/logger.js";
import { EmbeddingError, isRetryableRAGError, MetadataExtractionError, RAGError, RAGErrorCodes, VectorQueryError, } from "../errors/RAGError.js";
/**
* Default retry configuration
*/
export const DEFAULT_RAG_RETRY_CONFIG = {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
jitter: true,
retryableErrorCodes: [
RAGErrorCodes.EMBEDDING_RATE_LIMIT,
RAGErrorCodes.VECTOR_QUERY_TIMEOUT,
RAGErrorCodes.VECTOR_STORE_CONNECTION_ERROR,
RAGErrorCodes.METADATA_EXTRACTION_TIMEOUT,
RAGErrorCodes.RERANKER_API_ERROR,
RAGErrorCodes.OPERATION_TIMEOUT,
],
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
};
/**
* Calculate delay with exponential backoff
*/
function calculateDelay(attempt, config) {
const exponentialDelay = config.initialDelay * config.backoffMultiplier ** attempt;
const delay = Math.min(exponentialDelay, config.maxDelay);
if (config.jitter) {
// Add random jitter between 0 and 50% of the delay
const jitterFactor = 0.5 * Math.random();
return delay * (1 + jitterFactor);
}
return delay;
}
/**
* Sleep utility
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Check if an error is retryable based on configuration
*/
export function isRetryable(error, config = DEFAULT_RAG_RETRY_CONFIG) {
// Never retry abort errors - the operation was intentionally cancelled
if (isAbortError(error)) {
return false;
}
// Use custom shouldRetry if provided
if (config.shouldRetry) {
return config.shouldRetry(error);
}
// Check if it's a RAG error with retryable flag
if (isRetryableRAGError(error)) {
return true;
}
// Check error code
if (error instanceof RAGError && config.retryableErrorCodes) {
if (config.retryableErrorCodes.includes(error.code)) {
return true;
}
}
// Check for common network/timeout errors
if (error && typeof error === "object") {
const errorObj = error;
// Timeout errors
if (errorObj.name === "TimeoutError" ||
errorObj.code === "TIMEOUT" ||
errorObj.code === "ETIMEDOUT") {
return true;
}
// Network errors
if (errorObj.code === "ECONNRESET" ||
errorObj.code === "ENOTFOUND" ||
errorObj.code === "ECONNREFUSED" ||
errorObj.code === "ECONNABORTED" ||
errorObj.code === "EPIPE" ||
errorObj.code === "ENETUNREACH" ||
errorObj.code === "EHOSTUNREACH") {
return true;
}
// HTTP status codes
if (typeof errorObj.status === "number" &&
config.retryableStatusCodes?.includes(errorObj.status)) {
return true;
}
if (typeof errorObj.statusCode === "number" &&
config.retryableStatusCodes?.includes(errorObj.statusCode)) {
return true;
}
}
return false;
}
/**
* Execute a RAG operation with retry logic
*
* Implements exponential backoff with jitter to prevent thundering herd.
* Only retries on errors that are considered retryable.
*
* @param operation - Async operation to execute with retries
* @param config - Partial retry configuration (merged with defaults)
* @returns Result of the operation
* @throws Last error if all retry attempts fail
*/
export async function withRAGRetry(operation, config = {}) {
const mergedConfig = {
...DEFAULT_RAG_RETRY_CONFIG,
...config,
};
let lastError;
let attempt = 0;
while (attempt <= mergedConfig.maxRetries) {
try {
return await operation();
}
catch (error) {
lastError = error;
// Don't retry if this is the last attempt
if (attempt === mergedConfig.maxRetries) {
logger.debug(`[RAGRetryHandler] All ${mergedConfig.maxRetries} attempts exhausted`);
break;
}
// Check if we should retry this error
if (!isRetryable(error, mergedConfig)) {
logger.debug(`[RAGRetryHandler] Non-retryable error encountered: ${error instanceof Error ? error.message : String(error)}`);
break;
}
// Calculate delay with backoff and jitter
const delay = calculateDelay(attempt, mergedConfig);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.warn(`[RAGRetryHandler] Attempt ${attempt + 1}/${mergedConfig.maxRetries + 1} failed: ${errorMessage}. Retrying in ${Math.round(delay)}ms...`);
await sleep(delay);
attempt++;
}
}
// Wrap the last error if it's not already a RAG error
if (lastError instanceof RAGError) {
throw lastError;
}
throw new RAGError(`RAG operation failed after ${attempt + 1} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`, RAGErrorCodes.RETRY_EXHAUSTED, {
retryable: false,
cause: lastError instanceof Error ? lastError : undefined,
details: {
totalAttempts: attempt + 1,
maxRetries: mergedConfig.maxRetries,
},
});
}
/**
* RAG Retry Handler class for more complex retry scenarios
*/
export class RAGRetryHandler {
config;
constructor(config = {}) {
this.config = { ...DEFAULT_RAG_RETRY_CONFIG, ...config };
}
/**
* Execute an operation with retry logic
*/
async executeWithRetry(operation, maxRetries) {
const config = maxRetries !== undefined ? { ...this.config, maxRetries } : this.config;
return withRAGRetry(operation, config);
}
/**
* Execute multiple operations with retry, collecting results
* Returns successful results and failed operations with their errors
*/
async executeBatch(items, operation, options) {
const { concurrency = 5, continueOnError = true } = options ?? {};
const successful = [];
const failed = [];
// Process in batches for concurrency control
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const batchPromises = batch.map(async (item, batchIndex) => {
const index = i + batchIndex;
try {
const result = await this.executeWithRetry(() => operation(item, index));
successful.push({ item, result, index });
}
catch (error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
failed.push({ item, error: errorObj, index });
if (!continueOnError) {
throw error;
}
}
});
await Promise.all(batchPromises);
}
const total = successful.length + failed.length;
const successRate = total > 0 ? successful.length / total : 0;
return { successful, failed, successRate };
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Update configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
}
}
/**
* Specialized retry handler for embedding operations
*/
export class EmbeddingRetryHandler extends RAGRetryHandler {
constructor(config) {
super({
maxRetries: 5, // More retries for rate-limited embedding APIs
initialDelay: 2000, // Longer initial delay
retryableErrorCodes: [
RAGErrorCodes.EMBEDDING_ERROR,
RAGErrorCodes.EMBEDDING_RATE_LIMIT,
RAGErrorCodes.EMBEDDING_PROVIDER_ERROR,
],
shouldRetry: (error) => {
// Always retry rate limit errors
if (error instanceof EmbeddingError) {
return error.retryable;
}
return isRetryable(error);
},
...config,
});
}
}
/**
* Specialized retry handler for vector store operations
*/
export class VectorStoreRetryHandler extends RAGRetryHandler {
constructor(config) {
super({
maxRetries: 3,
initialDelay: 1000,
retryableErrorCodes: [
RAGErrorCodes.VECTOR_QUERY_ERROR,
RAGErrorCodes.VECTOR_QUERY_TIMEOUT,
RAGErrorCodes.VECTOR_STORE_UNAVAILABLE,
RAGErrorCodes.VECTOR_STORE_CONNECTION_ERROR,
],
shouldRetry: (error) => {
if (error instanceof VectorQueryError) {
return error.retryable;
}
return isRetryable(error);
},
...config,
});
}
}
/**
* Specialized retry handler for metadata extraction
*/
export class MetadataExtractionRetryHandler extends RAGRetryHandler {
constructor(config) {
super({
maxRetries: 3,
initialDelay: 1500,
retryableErrorCodes: [
RAGErrorCodes.METADATA_EXTRACTION_ERROR,
RAGErrorCodes.METADATA_EXTRACTION_TIMEOUT,
],
shouldRetry: (error) => {
if (error instanceof MetadataExtractionError) {
return error.retryable;
}
return isRetryable(error);
},
...config,
});
}
}
/**
* Create a retry handler with the core infrastructure withRetry
*/
export function createRetryHandler(config = {}) {
const mergedConfig = { ...DEFAULT_RAG_RETRY_CONFIG, ...config };
return (operation) => withRetry(operation, {
maxRetries: mergedConfig.maxRetries,
baseDelayMs: mergedConfig.initialDelay,
maxDelayMs: mergedConfig.maxDelay,
shouldRetry: (error) => isRetryable(error, mergedConfig),
});
}
/**
* Global retry handlers for common RAG operations
*/
export const embeddingRetryHandler = new EmbeddingRetryHandler();
export const vectorStoreRetryHandler = new VectorStoreRetryHandler();
export const metadataExtractionRetryHandler = new MetadataExtractionRetryHandler();