@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
233 lines (232 loc) • 7.67 kB
JavaScript
/**
* @file Abstract base scorer class providing common functionality
* All scorers extend this class for consistent behavior
*/
import { withTimeout, ErrorFactory } from "../../utils/errorHandling.js";
import { logger } from "../../utils/logger.js";
/**
* Default score scale (0-10)
*/
export const DEFAULT_SCORE_SCALE = {
min: 0,
max: 10,
precision: 2,
};
/**
* Default scorer configuration
*/
export const DEFAULT_SCORER_CONFIG = {
enabled: true,
threshold: 0.7,
weight: 1.0,
timeout: 30000,
retries: 2,
};
/**
* Abstract base class for all scorers
* Provides common functionality and enforces interface compliance
*/
export class BaseScorer {
_config;
_metadata;
constructor(metadata, config) {
this._metadata = metadata;
this._config = {
...DEFAULT_SCORER_CONFIG,
...metadata.defaultConfig,
...config,
};
}
/**
* Get scorer metadata
*/
get metadata() {
return this._metadata;
}
/**
* Get current configuration
*/
get config() {
return this._config;
}
/**
* Validate input has required fields
*/
validateInput(input) {
const errors = [];
for (const field of this._metadata.requiredInputs) {
const value = input[field];
if (value === undefined || value === null) {
errors.push(`Missing required input: ${field}`);
}
}
// Check for empty strings in required text fields
if (this._metadata.requiredInputs.includes("query") &&
typeof input.query === "string" &&
!input.query.trim()) {
errors.push("Query cannot be empty");
}
if (this._metadata.requiredInputs.includes("response") &&
typeof input.response === "string" &&
!input.response.trim()) {
errors.push("Response cannot be empty");
}
// Check context array is not empty if required
if (this._metadata.requiredInputs.includes("context") &&
input.context !== undefined) {
if (!Array.isArray(input.context) || input.context.length === 0) {
errors.push("Context must be a non-empty array");
}
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Update configuration
*/
configure(config) {
this._config = {
...this._config,
...config,
};
logger.debug(`Scorer ${this._metadata.id} reconfigured`, {
config: this._config,
});
}
/**
* Normalize a score to 0-1 scale
*/
normalizeScore(score, scale = DEFAULT_SCORE_SCALE) {
// Validate inputs are finite
if (!Number.isFinite(score) ||
!Number.isFinite(scale.min) ||
!Number.isFinite(scale.max)) {
return 0;
}
// Guard against zero denominator
const denominator = scale.max - scale.min;
if (denominator === 0) {
return 0;
}
// Clamp score to scale bounds
const clampedScore = Math.max(scale.min, Math.min(scale.max, score));
const normalized = (clampedScore - scale.min) / denominator;
return Math.max(0, Math.min(1, normalized));
}
/**
* Convert normalized score back to scale
*/
denormalizeScore(normalizedScore, scale = DEFAULT_SCORE_SCALE) {
const clamped = Math.max(0, Math.min(1, normalizedScore));
return scale.min + clamped * (scale.max - scale.min);
}
/**
* Check if score passes threshold
*/
checkThreshold(normalizedScore) {
const threshold = this._config.threshold ?? 0.7;
return normalizedScore >= threshold;
}
/**
* Create a standardized score result
*/
createScoreResult(score, reasoning, options = {}) {
const scale = options.scale ?? DEFAULT_SCORE_SCALE;
const safeScore = Number.isFinite(score) ? score : scale.min;
const clampedScore = Math.max(scale.min, Math.min(scale.max, safeScore));
const normalizedScore = this.normalizeScore(clampedScore, scale);
// Ensure no NaN leaks into published scores
const finalScore = Number.isFinite(clampedScore) ? clampedScore : 0;
const finalNormalized = Number.isFinite(normalizedScore)
? normalizedScore
: 0;
return {
scorerId: this._metadata.id,
scorerName: this._metadata.name,
score: Number(finalScore.toFixed(scale.precision)),
normalizedScore: Number(finalNormalized.toFixed(4)),
scale,
reasoning,
passed: this.checkThreshold(finalNormalized),
threshold: this._config.threshold ?? 0.7,
confidence: options.confidence === undefined
? undefined
: Number.isFinite(options.confidence)
? Math.max(0, Math.min(1, options.confidence))
: 0,
metadata: options.metadata,
computeTime: 0, // Set by caller via executeWithTiming
error: options.error,
};
}
/**
* Create an error score result
*/
createErrorResult(error) {
const errorMessage = error instanceof Error ? error.message : error;
return {
scorerId: this._metadata.id,
scorerName: this._metadata.name,
score: 0,
normalizedScore: 0,
scale: DEFAULT_SCORE_SCALE,
reasoning: `Scoring failed: ${errorMessage}`,
passed: false,
threshold: this._config.threshold ?? 0.7,
computeTime: 0,
error: errorMessage,
};
}
/**
* Execute scoring with timing and error handling
*/
async executeWithTiming(scoringFn) {
const startTime = Date.now();
try {
const result = await scoringFn();
return {
...result,
computeTime: Date.now() - startTime,
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Scorer ${this._metadata.id} failed`, {
error: errorMessage,
});
return {
...this.createErrorResult(errorMessage),
computeTime: Date.now() - startTime,
};
}
}
/**
* Execute scoring with timeout
*/
async executeWithTimeout(fn, timeoutMs, operationName) {
return withTimeout(fn(), timeoutMs, ErrorFactory.evaluationTimeout(operationName, timeoutMs));
}
/**
* Execute with retry logic
*/
async executeWithRetry(operation, retries) {
const maxRetries = retries ?? this._config.retries ?? 2;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries) {
logger.debug(`Scorer ${this._metadata.id} retry ${attempt + 1}/${maxRetries}`, { error: lastError.message });
// Exponential backoff
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
}
}
}
throw lastError ?? new Error("Operation failed after retries");
}
}