@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 (394 loc) • 16.5 kB
JavaScript
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,
},
};
}
}