@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
352 lines • 12.8 kB
JavaScript
/**
* @file Length Scorer
* Evaluates response length against configured constraints
*/
import { BaseRuleScorer, DEFAULT_RULE_SCORER_CONFIG, } from "./baseRuleScorer.js";
/**
* Scorer metadata for length
*/
const LENGTH_METADATA = {
id: "length",
name: "Length Validator",
description: "Evaluates response length against configured constraints (words, characters, sentences)",
type: "rule",
category: "quality",
version: "1.0.0",
defaultConfig: {
...DEFAULT_RULE_SCORER_CONFIG,
threshold: 0.8,
},
requiredInputs: ["response"],
optionalInputs: ["query", "context"],
};
/**
* LengthScorer evaluates response length against configurable constraints
*/
export class LengthScorer extends BaseRuleScorer {
_lengthConfig;
constructor(config) {
super(LENGTH_METADATA, config);
this._lengthConfig = {
unit: "words",
constraintType: "range",
minLength: 10,
maxLength: 500,
tolerance: 0.1, // 10% tolerance for exact match
scoringMode: "proportional",
...config,
};
}
/**
* Get length-specific configuration
*/
get lengthConfig() {
return this._lengthConfig;
}
/**
* Get rules for this scorer
*/
getRules() {
const rules = [];
const constraintType = this._lengthConfig.constraintType ?? "range";
switch (constraintType) {
case "minimum":
rules.push({
id: "length-minimum",
description: `Minimum ${this._lengthConfig.unit} check`,
type: "length",
params: {
check: "minimum",
value: this._lengthConfig.minLength ?? 10,
unit: this._lengthConfig.unit ?? "words",
},
weight: 1.0,
});
break;
case "maximum":
rules.push({
id: "length-maximum",
description: `Maximum ${this._lengthConfig.unit} check`,
type: "length",
params: {
check: "maximum",
value: this._lengthConfig.maxLength ?? 500,
unit: this._lengthConfig.unit ?? "words",
},
weight: 1.0,
});
break;
case "exact":
rules.push({
id: "length-exact",
description: `Exact ${this._lengthConfig.unit} check`,
type: "length",
params: {
check: "exact",
value: this._lengthConfig.exactLength ?? 100,
tolerance: this._lengthConfig.tolerance ?? 0.1,
unit: this._lengthConfig.unit ?? "words",
},
weight: 1.0,
});
break;
case "ratio":
rules.push({
id: "length-ratio",
description: `Length ratio check against ${this._lengthConfig.ratioReference}`,
type: "length",
params: {
check: "ratio",
target: this._lengthConfig.ratioTarget ?? 1.0,
reference: this._lengthConfig.ratioReference ?? "query",
unit: this._lengthConfig.unit ?? "words",
},
weight: 1.0,
});
break;
case "range":
default:
rules.push({
id: "length-minimum",
description: `Minimum ${this._lengthConfig.unit} check`,
type: "length",
params: {
check: "minimum",
value: this._lengthConfig.minLength ?? 10,
unit: this._lengthConfig.unit ?? "words",
},
weight: 0.5,
});
rules.push({
id: "length-maximum",
description: `Maximum ${this._lengthConfig.unit} check`,
type: "length",
params: {
check: "maximum",
value: this._lengthConfig.maxLength ?? 500,
unit: this._lengthConfig.unit ?? "words",
},
weight: 0.5,
});
break;
}
return rules;
}
/**
* Measure text length in various units
*/
_measureLength(text) {
return {
words: this.getWordCount(text),
characters: this.getCharacterCount(text),
sentences: this._countSentences(text),
paragraphs: this._countParagraphs(text),
estimatedTokens: this._estimateTokens(text),
};
}
/**
* Get length in the configured unit
*/
_getLengthInUnit(text, unit) {
const measurement = this._measureLength(text);
switch (unit) {
case "words":
return measurement.words;
case "characters":
return measurement.characters;
case "sentences":
return measurement.sentences;
case "paragraphs":
return measurement.paragraphs;
case "tokens":
return measurement.estimatedTokens;
default:
return measurement.words;
}
}
/**
* Count sentences in text
*/
_countSentences(text) {
// Match sentence-ending punctuation followed by space or end of string
const sentences = text
.split(/(?<=[.!?])\s+/)
.filter((s) => s.trim().length > 0);
return sentences.length;
}
/**
* Count paragraphs in text
*/
_countParagraphs(text) {
// Split by double newlines or paragraph markers
const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
return Math.max(paragraphs.length, 1); // At least 1 paragraph if there's text
}
/**
* Estimate token count (rough approximation)
* GPT-style: ~4 characters per token on average
*/
_estimateTokens(text) {
return Math.ceil(text.length / 4);
}
/**
* Evaluate a single length rule
*/
evaluateRule(rule, input) {
const check = rule.params.check;
const unit = rule.params.unit ?? "words";
const responseLength = this._getLengthInUnit(input.response, unit);
switch (check) {
case "minimum": {
const minValue = rule.params.value;
if (responseLength >= minValue) {
return { passed: true, score: 1.0 };
}
// Proportional scoring
if (this._lengthConfig.scoringMode === "proportional" && minValue > 0) {
return {
passed: false,
score: Math.min(responseLength / minValue, 1.0),
};
}
return { passed: false, score: 0.0 };
}
case "maximum": {
const maxValue = rule.params.value;
if (responseLength <= maxValue) {
return { passed: true, score: 1.0 };
}
// Proportional scoring (inverse)
if (this._lengthConfig.scoringMode === "proportional" &&
responseLength > 0) {
return {
passed: false,
score: Math.max(0, maxValue / responseLength),
};
}
return { passed: false, score: 0.0 };
}
case "exact": {
const exactValue = rule.params.value;
const tolerance = rule.params.tolerance ?? 0.1;
const lowerBound = exactValue * (1 - tolerance);
const upperBound = exactValue * (1 + tolerance);
if (responseLength >= lowerBound && responseLength <= upperBound) {
return { passed: true, score: 1.0 };
}
// Proportional scoring based on distance from target
if (this._lengthConfig.scoringMode === "proportional" &&
exactValue > 0) {
const distance = Math.abs(responseLength - exactValue) / exactValue;
return { passed: false, score: Math.max(0, 1 - distance) };
}
return { passed: false, score: 0.0 };
}
case "ratio": {
const target = rule.params.target;
const reference = rule.params.reference;
let referenceText = "";
if (reference === "query") {
referenceText = input.query;
}
else if (reference === "context" && input.context) {
referenceText = input.context.join(" ");
}
if (!referenceText) {
return { passed: true, score: 1.0 }; // No reference, pass by default
}
const referenceLength = this._getLengthInUnit(referenceText, unit);
if (referenceLength === 0) {
return { passed: true, score: 1.0 };
}
const actualRatio = responseLength / referenceLength;
const tolerance = this._lengthConfig.tolerance ?? 0.2;
if (actualRatio >= target * (1 - tolerance) &&
actualRatio <= target * (1 + tolerance)) {
return { passed: true, score: 1.0 };
}
// Proportional scoring
if (this._lengthConfig.scoringMode === "proportional" && target > 0) {
const ratioDistance = Math.abs(actualRatio - target) / target;
return { passed: false, score: Math.max(0, 1 - ratioDistance) };
}
return { passed: false, score: 0.0 };
}
default:
return { passed: true, score: 1.0 };
}
}
/**
* Override score to add detailed length metrics
*/
async score(input) {
const result = await super.score(input);
// Add detailed measurements
const measurement = this._measureLength(input.response);
const unit = this._lengthConfig.unit ?? "words";
return {
...result,
metadata: {
...result.metadata,
lengthMeasurement: measurement,
configuredUnit: unit,
configuredConstraint: this._lengthConfig.constraintType ?? "range",
actualLength: this._getLengthInUnit(input.response, unit),
minLength: this._lengthConfig.minLength ?? null,
maxLength: this._lengthConfig.maxLength ?? null,
exactLength: this._lengthConfig.exactLength ?? null,
},
};
}
}
/**
* Factory function for creating LengthScorer instances
*/
export async function createLengthScorer(config) {
return new LengthScorer(config);
}
/**
* Pre-configured length scorer presets
*/
export const LengthScorerPresets = {
/** Short response (50-150 words) */
short: () => new LengthScorer({
unit: "words",
constraintType: "range",
minLength: 50,
maxLength: 150,
}),
/** Medium response (100-300 words) */
medium: () => new LengthScorer({
unit: "words",
constraintType: "range",
minLength: 100,
maxLength: 300,
}),
/** Long response (200-500 words) */
long: () => new LengthScorer({
unit: "words",
constraintType: "range",
minLength: 200,
maxLength: 500,
}),
/** Concise response (max 100 words) */
concise: () => new LengthScorer({
unit: "words",
constraintType: "maximum",
maxLength: 100,
}),
/** Detailed response (min 300 words) */
detailed: () => new LengthScorer({
unit: "words",
constraintType: "minimum",
minLength: 300,
}),
/** Tweet-length (max 280 characters) */
tweet: () => new LengthScorer({
unit: "characters",
constraintType: "maximum",
maxLength: 280,
}),
};
//# sourceMappingURL=lengthScorer.js.map