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

397 lines (395 loc) 16.5 kB
import { AIProviderFactory } from "../../core/factory.js"; import { logger } from "../../utils/logger.js"; export const DEFAULT_EVALUATION_PROMPT = ` Evaluate the following user input for safety and appropriateness. Respond ONLY with valid JSON: { "overall": "safe|unsafe|suspicious|inappropriate", "safetyScore": <1-10>, "appropriatenessScore": <1-10>, "confidenceLevel": <1-10>, "issues": [ { "category": "explicit_content|hate_speech|violence|personal_info|spam|other", "severity": "low|medium|high|critical", "description": "Brief description" } ], "suggestedAction": "allow|block|sanitize|warn", "reasoning": "Brief explanation" } User Input: "{USER_INPUT}" `; /** * Extract user input from middleware params */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function extractUserInput(params) { if (typeof params.prompt === "string") { return params.prompt; } return (params.prompt // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((msg) => { if (msg.role === "user") { if (typeof msg.content === "string") { return msg.content; } else if (Array.isArray(msg.content)) { return (msg.content // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter((part) => part.type === "text") // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((part) => part.text) .join(" ")); } } return ""; }) .filter(Boolean) .join("\n")); } export function parseEvaluationResponse(rawResponse) { try { const cleanedResponse = rawResponse.replace(/```json\n|```/g, "").trim(); const parsed = JSON.parse(cleanedResponse); return { overall: parsed.overall || "safe", safetyScore: Number(parsed.safetyScore) || 10, appropriatenessScore: Number(parsed.appropriatenessScore) || 10, confidenceLevel: Number(parsed.confidenceLevel) || 10, issues: parsed.issues || [], suggestedAction: parsed.suggestedAction || "allow", reasoning: parsed.reasoning || "No reasoning provided.", }; } catch (error) { logger.error("[GuardrailsUtils] Failed to parse evaluation response:", error); return { overall: "safe", safetyScore: 10, appropriatenessScore: 10, confidenceLevel: 1, suggestedAction: "allow", reasoning: "Error parsing evaluation response - allowing by default.", }; } } /** * Handles the precall guardrails logic, including evaluation and sanitization. * @param params - The language model call options. * @param config - The precall evaluation configuration. * @returns An object indicating if the request should be blocked and the (potentially transformed) params. */ export async function handlePrecallGuardrails( // eslint-disable-next-line @typescript-eslint/no-explicit-any params, config) { const userInput = extractUserInput(params); let transformedParams = params; if (userInput.trim()) { logger.debug(`[GuardrailsUtils] Performing precall evaluation on user input.`); const evaluation = await performPrecallEvaluation(config, userInput); const actionResult = applyEvaluationActions(evaluation, config, userInput); if (actionResult.shouldBlock) { logger.warn(`[GuardrailsUtils] Blocking request due to precall evaluation.`, { evaluation, userInput: userInput.substring(0, 100), }); return { shouldBlock: true, transformedParams }; } if (actionResult.sanitizedInput) { logger.info(`[GuardrailsUtils] Applying input sanitization.`); transformedParams = applySanitization(params, actionResult.sanitizedInput); } } else { logger.debug(`[GuardrailsUtils] Skipping precall evaluation - no user content to evaluate.`); } return { shouldBlock: false, transformedParams }; } /** * Perform precall evaluation of user input using AI models */ export async function performPrecallEvaluation(config, userInput) { try { const provider = await AIProviderFactory.createProvider(config.provider || "google-ai", config.evaluationModel || "gemini-1.5-flash"); const evaluationPrompt = config.evaluationPrompt || DEFAULT_EVALUATION_PROMPT; const prompt = evaluationPrompt.replace("{USER_INPUT}", userInput); const result = await provider.generate({ input: { text: prompt }, }); if (!result || !result.content) { throw new Error("Evaluation generation failed to return a result."); } return parseEvaluationResponse(result.content); } catch (error) { logger.error("[GuardrailsUtils] Precall evaluation failed:", error); return { overall: "safe", safetyScore: 10, appropriatenessScore: 10, confidenceLevel: 1, suggestedAction: "allow", reasoning: "Evaluation failed - allowing by default.", }; } } export function applyEvaluationActions(evaluation, config, userInput) { const actions = config.actions || {}; const thresholds = config.thresholds || {}; const safetyThreshold = thresholds.safetyScore || 7; const appropriatenessThreshold = thresholds.appropriatenessScore || 6; let actionToTake; if (evaluation.overall === "unsafe" || evaluation.safetyScore < safetyThreshold) { actionToTake = actions.onUnsafe || "block"; } else if (evaluation.overall === "inappropriate" || evaluation.appropriatenessScore < appropriatenessThreshold) { actionToTake = actions.onInappropriate || "warn"; } else if (evaluation.overall === "suspicious") { actionToTake = actions.onSuspicious || "log"; } else { actionToTake = "allow"; } logger.info("[GuardrailsUtils] Precall evaluation result:", { overall: evaluation.overall, safetyScore: evaluation.safetyScore, appropriatenessScore: evaluation.appropriatenessScore, confidenceLevel: evaluation.confidenceLevel, suggestedAction: evaluation.suggestedAction, actionTaken: actionToTake, reasoning: evaluation.reasoning, issues: evaluation.issues, }); switch (actionToTake) { case "block": return { shouldBlock: true }; case "sanitize": { let sanitized = userInput; const patterns = config.sanitizationPatterns || []; const replacementText = config.replacementText || "[REDACTED]"; if (patterns.length > 0) { logger.debug(`[GuardrailsUtils] Applying ${patterns.length} sanitization patterns with replacement: "${replacementText}".`); patterns.forEach((pattern, index) => { try { const regex = new RegExp(pattern, "gi"); const before = sanitized; let matchCount = 0; sanitized = sanitized.replace(regex, () => { matchCount++; return replacementText; }); if (before !== sanitized) { logger.debug(`[GuardrailsUtils] Pattern ${index + 1} matched ${matchCount} times.`); } } catch (error) { logger.error(`[GuardrailsUtils] Invalid sanitization pattern "${pattern}":`, error); } }); if (sanitized !== userInput) { logger.info(`[GuardrailsUtils] Input sanitized using ${patterns.length} patterns.`); } } else { logger.warn("[GuardrailsUtils] Sanitize action triggered but no sanitizationPatterns provided in config. Input will not be modified."); } return { shouldBlock: false, sanitizedInput: sanitized }; } case "warn": { logger.warn("[GuardrailsUtils] Potentially inappropriate content detected but allowing:", { userInput: userInput.substring(0, 100), evaluation, }); return { shouldBlock: false }; } case "log": { logger.info("[GuardrailsUtils] Suspicious content detected:", { userInput: userInput.substring(0, 100), evaluation, }); return { shouldBlock: false }; } default: return { shouldBlock: false }; } } /** * Apply parameter sanitization to request parameters */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function applySanitization(params, sanitizedInput) { const sanitizedParams = { ...params }; if (Array.isArray(params.prompt)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const sanitizedPrompt = params.prompt.map((msg) => { if (msg.role === "user") { if (typeof msg.content === "string") { return { ...msg, content: [{ type: "text", text: sanitizedInput }], }; } else if (Array.isArray(msg.content)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const sanitizedContent = msg.content.map((part) => { if (part.type === "text") { return { ...part, text: sanitizedInput }; } return part; }); return { ...msg, content: sanitizedContent }; } } return msg; }); sanitizedParams.prompt = sanitizedPrompt; } return sanitizedParams; } export function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function createBlockedResponse() { return { text: "Request contains inappropriate content and has been blocked.", usage: { promptTokens: 0, completionTokens: 0 }, finishReason: "stop", warnings: [], rawCall: { rawPrompt: null, rawSettings: {} }, }; } export function createBlockedStream() { return new ReadableStream({ start(controller) { controller.enqueue({ type: "text-delta", textDelta: "Request contains inappropriate content and has been blocked.", }); controller.enqueue({ type: "finish", finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0 }, }); controller.close(); }, }); } /** * Apply content filtering using bad words configuration * Handles both regex patterns and string lists with proper priority * @param text The text to filter * @param badWordsConfig Bad words configuration * @param context Optional context for logging (e.g., "generate", "stream") * @returns Filtering result with filtered text and metadata */ export function applyContentFiltering(text, badWordsConfig, context = "unknown") { // Early return if filtering is disabled or no config try { if (!badWordsConfig?.enabled || !text) { return { filteredText: text, hasChanges: false, appliedFilters: [], filteringStats: { regexPatternsApplied: 0, stringFiltersApplied: 0, totalMatches: 0, }, }; } let filteredText = text; let hasChanges = false; const appliedFilters = []; let totalMatches = 0; const replacementText = badWordsConfig.replacementText || "[REDACTED]"; // Priority 1: Use regex patterns if provided if (badWordsConfig.regexPatterns && badWordsConfig.regexPatterns.length > 0) { if (badWordsConfig.list && badWordsConfig.list.length > 0) { logger.warn(`[ContentFiltering:${context}] Both regexPatterns and list provided. Using regexPatterns and ignoring list.`); } logger.debug(`[ContentFiltering:${context}] Applying regex pattern filtering with ${badWordsConfig.regexPatterns.length} patterns using replacement: "${replacementText}".`); for (const pattern of badWordsConfig.regexPatterns) { try { // TODO: Add blocking for overly complex or long patterns if (pattern.length > 1000) { logger.warn(`[ContentFiltering:${context}] Regex pattern exceeds max length (1000 chars): "${pattern.substring(0, 50)}..."`); } const regex = new RegExp(pattern, "gi"); const testStart = Date.now(); regex.test("test"); if (Date.now() - testStart > 100) { logger.warn(`[ContentFiltering:${context}] Regex pattern "${pattern}" appears to be too complex (slow test execution).`); } const before = filteredText; let matchCount = 0; filteredText = filteredText?.replace(regex, () => { matchCount++; return replacementText; }); if (before !== filteredText) { hasChanges = true; totalMatches += matchCount; appliedFilters.push(`regex:${pattern}`); logger.debug(`[ContentFiltering:${context}] Regex pattern "${pattern}" matched ${matchCount} times and filtered content.`); } } catch (error) { logger.error(`[ContentFiltering:${context}] Invalid regex pattern "${pattern}":`, error); } } } // Priority 2: Use simple string list if no regex patterns else if (badWordsConfig.list && badWordsConfig.list.length > 0) { logger.debug(`[ContentFiltering:${context}] Applying string list filtering with ${badWordsConfig.list.length} terms using replacement: "${replacementText}".`); for (const term of badWordsConfig.list) { const regex = new RegExp(escapeRegExp(term), "gi"); const before = filteredText; let matchCount = 0; filteredText = filteredText?.replace(regex, () => { matchCount++; return replacementText; }); if (before !== filteredText) { hasChanges = true; totalMatches += matchCount; appliedFilters.push(`string:${term}`); logger.debug(`[ContentFiltering:${context}] String filter "${term}" matched ${matchCount} times.`); } } } const result = { filteredText, hasChanges, appliedFilters, filteringStats: { regexPatternsApplied: badWordsConfig.regexPatterns?.length || 0, stringFiltersApplied: badWordsConfig.list?.length || 0, totalMatches, }, }; if (hasChanges) { logger.debug(`[ContentFiltering:${context}] Filtering completed. Applied ${appliedFilters.length} filters with ${totalMatches} total matches.`); } return result; } catch (error) { logger.error(`[ContentFiltering:${context}] Error during content filtering:`, error); return { filteredText: text, hasChanges: false, appliedFilters: [], filteringStats: { regexPatternsApplied: 0, stringFiltersApplied: 0, totalMatches: 0, }, }; } } //# sourceMappingURL=guardrailsUtils.js.map