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

234 lines (233 loc) 8.41 kB
/** * @file Base class for all rule-based scorers * Provides common functionality for rule evaluation */ import { logger } from "../../../utils/logger.js"; import { BaseScorer, DEFAULT_SCORE_SCALE } from "../baseScorer.js"; /** * Default rule scorer configuration */ export const DEFAULT_RULE_SCORER_CONFIG = { enabled: true, threshold: 0.7, weight: 1.0, timeout: 1000, retries: 0, ruleCombination: "all", }; /** * Abstract base class for rule-based scorers */ export class BaseRuleScorer extends BaseScorer { _ruleConfig; constructor(metadata, config) { super(metadata, config); this._ruleConfig = { ...DEFAULT_RULE_SCORER_CONFIG, ...metadata.defaultConfig, ...config, }; } /** * Get rule-specific configuration */ get ruleConfig() { return this._ruleConfig; } /** * Main scoring method */ async score(input) { return this.executeWithTiming(async () => { // Validate input const validation = this.validateInput(input); if (!validation.valid) { return this.createErrorResult(`Invalid input: ${validation.errors.join(", ")}`); } try { const rules = this.getRules(); if (rules.length === 0) { return this.createScoreResult(10, "No rules configured - passing by default"); } // Evaluate all rules const results = rules.map((rule) => ({ rule, result: this.evaluateRule(rule, input), })); // Combine results based on configuration const combinedScore = this.combineRuleResults(results.map((r) => ({ ruleId: r.rule.id, passed: r.result.passed, score: r.result.score, })), rules); // Generate reasoning const reasoning = this.generateReasoning(results); return this.createScoreResult(combinedScore, reasoning, { metadata: { ruleResults: results.map((r) => ({ ruleId: r.rule.id, ruleDescription: r.rule.description, passed: r.result.passed, score: r.result.score, })), ruleCount: rules.length, passedCount: results.filter((r) => r.result.passed).length, combinationMethod: this._ruleConfig.ruleCombination ?? "all", }, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Rule scorer ${this._metadata.id} failed`, { error: errorMessage, }); return this.createErrorResult(errorMessage); } }); } /** * Combine rule results based on configuration */ combineRuleResults(results, rules) { const combination = this._ruleConfig.ruleCombination ?? "all"; switch (combination) { case "all": { // All rules must pass for full score const passedCount = results.filter((r) => r.passed).length; return (passedCount / results.length) * DEFAULT_SCORE_SCALE.max; } case "any": { // Any rule passing gives partial credit const maxScore = Math.max(...results.map((r) => r.score)); return maxScore * DEFAULT_SCORE_SCALE.max; } case "weighted": { // Weight-based combination let totalWeight = 0; let weightedScore = 0; for (let i = 0; i < results.length; i++) { const rule = rules[i]; const weight = rule.weight ?? 1.0; totalWeight += weight; weightedScore += results[i].score * weight; } return totalWeight > 0 ? (weightedScore / totalWeight) * DEFAULT_SCORE_SCALE.max : 0; } default: return ((results.filter((r) => r.passed).length / results.length) * DEFAULT_SCORE_SCALE.max); } } /** * Generate reasoning from rule results */ generateReasoning(results) { const passed = results.filter((r) => r.result.passed); const failed = results.filter((r) => !r.result.passed); const parts = []; if (passed.length > 0) { parts.push(`${passed.length} rule(s) passed: ${passed.map((r) => r.rule.id).join(", ")}`); } if (failed.length > 0) { parts.push(`${failed.length} rule(s) failed: ${failed.map((r) => r.rule.id).join(", ")}`); } return parts.join(". "); } /** * Helper: Check if text matches a regex pattern */ matchesRegex(text, pattern, flags = "gi") { if (pattern.length > 200) { logger.warn(`[BaseRuleScorer] Regex pattern too long (${pattern.length} chars), skipping`); return false; } try { const regex = new RegExp(pattern, flags); return regex.test(text); } catch { logger.warn(`[BaseRuleScorer] Invalid regex pattern: ${pattern.substring(0, 50)}`); return false; } } /** * Helper: Check if text contains keyword with word boundaries */ containsKeyword(text, keyword, caseInsensitive = true) { const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const flags = caseInsensitive ? "gi" : "g"; const regex = new RegExp(`\\b${escapedKeyword}\\b`, flags); return regex.test(text); } /** * Helper: Count occurrences of a pattern */ countOccurrences(text, pattern, caseInsensitive = true) { if (pattern.length > 200) { logger.warn(`[BaseRuleScorer] Regex pattern too long (${pattern.length} chars), skipping`); return 0; } try { const flags = caseInsensitive ? "gi" : "g"; const regex = new RegExp(pattern, flags); const matches = text.match(regex); return matches ? matches.length : 0; } catch { logger.warn(`[BaseRuleScorer] Invalid regex pattern: ${pattern.substring(0, 50)}`); return 0; } } /** * Helper: Get word count */ getWordCount(text) { return text .trim() .split(/\s+/) .filter((word) => word.length > 0).length; } /** * Helper: Get character count (excluding whitespace) */ getCharacterCount(text, includeWhitespace = true) { if (includeWhitespace) { return text.length; } return text.replace(/\s/g, "").length; } /** * Helper: Check text length is within bounds */ isWithinLengthBounds(text, minWords, maxWords, minChars, maxChars) { const wordCount = this.getWordCount(text); const charCount = text.length; if (minWords !== undefined && wordCount < minWords) { return { passed: false, reason: `Word count ${wordCount} is below minimum ${minWords}`, }; } if (maxWords !== undefined && wordCount > maxWords) { return { passed: false, reason: `Word count ${wordCount} exceeds maximum ${maxWords}`, }; } if (minChars !== undefined && charCount < minChars) { return { passed: false, reason: `Character count ${charCount} is below minimum ${minChars}`, }; } if (maxChars !== undefined && charCount > maxChars) { return { passed: false, reason: `Character count ${charCount} exceeds maximum ${maxChars}`, }; } return { passed: true, reason: "Length within acceptable bounds" }; } }