UNPKG

@xynehq/jaf

Version:

Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools

553 lines (552 loc) 24.9 kB
import { jsonParseLLMOutput, createRunId, createTraceId, getTextContent, validateGuardrailsConfig } from './types.js'; import { safeConsole } from '../utils/logger.js'; // Constants for content length limits const SHORT_TIMEOUT_MAX_CONTENT = 10000; const LONG_TIMEOUT_MAX_CONTENT = 50000; const CIRCUIT_BREAKER_CLEANUP_MAX_AGE = 10 * 60 * 1000; // 10 minutes class GuardrailCircuitBreaker { failures = 0; lastFailureTime = 0; maxFailures = 5; resetTimeMs = 60000; // 1 minute isOpen() { if (this.failures < this.maxFailures) return false; const timeSinceLastFailure = Date.now() - this.lastFailureTime; if (timeSinceLastFailure > this.resetTimeMs) { this.failures = 0; return false; } return true; } recordFailure() { this.failures++; this.lastFailureTime = Date.now(); } recordSuccess() { this.failures = 0; } // Public method for cleanup eligibility check shouldBeCleanedUp(maxAge) { const now = Date.now(); return this.lastFailureTime > 0 && (now - this.lastFailureTime) > maxAge && !this.isOpen(); } } const circuitBreakers = new Map(); class GuardrailCache { cache = new Map(); maxSize; ttlMs; constructor(maxSize = 1000, ttlMs = 300000) { this.maxSize = maxSize; this.ttlMs = ttlMs; } createKey(stage, rulePrompt, content, modelName) { const contentHash = this.hashString(content.substring(0, 1000)); const ruleHash = this.hashString(rulePrompt); return `guardrail_${stage}_${modelName}_${ruleHash}_${contentHash}_${content.length}`; } hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } isExpired(entry) { return Date.now() - entry.timestamp > this.ttlMs; } evictLRU() { if (this.cache.size < this.maxSize) return; let lruKey = null; let lruScore = Infinity; const now = Date.now(); for (const [key, entry] of this.cache.entries()) { const ageHours = (now - entry.timestamp) / (1000 * 60 * 60); const score = entry.hitCount / (1 + ageHours); if (score < lruScore) { lruScore = score; lruKey = key; } } if (lruKey) { this.cache.delete(lruKey); } } get(stage, rulePrompt, content, modelName) { const key = this.createKey(stage, rulePrompt, content, modelName); const entry = this.cache.get(key); if (!entry || this.isExpired(entry)) { if (entry) this.cache.delete(key); return null; } entry.hitCount++; entry.timestamp = Date.now(); return entry.result; } set(stage, rulePrompt, content, modelName, result) { const key = this.createKey(stage, rulePrompt, content, modelName); this.evictLRU(); this.cache.set(key, { result, timestamp: Date.now(), hitCount: 1 }); } clear() { this.cache.clear(); } getStats() { return { size: this.cache.size, maxSize: this.maxSize }; } } const guardrailCache = new GuardrailCache(); function getCircuitBreaker(stage, modelName) { const key = `${stage}-${modelName}`; if (!circuitBreakers.has(key)) { circuitBreakers.set(key, new GuardrailCircuitBreaker()); } return circuitBreakers.get(key); } async function withTimeout(promise, timeoutMs, errorMessage) { const timeoutPromise = new Promise((_, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Timeout: ${errorMessage}`)); }, timeoutMs); promise.finally(() => clearTimeout(timeoutId)); }); return Promise.race([promise, timeoutPromise]); } async function createLLMGuardrail(config, stage, rulePrompt, fastModel, failSafe = 'allow', timeoutMs = 30000) { return async (content) => { const modelToUse = fastModel || config.defaultFastModel; if (!modelToUse) { const message = `[JAF:GUARDRAILS] No fast model available for LLM guardrail evaluation, using failSafe: ${failSafe}`; safeConsole.warn(message); return failSafe === 'allow' ? { isValid: true } : { isValid: false, errorMessage: 'No model available for guardrail evaluation' }; } const cachedResult = guardrailCache.get(stage, rulePrompt, content, modelToUse); if (cachedResult) { safeConsole.log(`[JAF:GUARDRAILS] Cache hit for ${stage} guardrail`); safeConsole.log(`[JAF:GUARDRAILS] Cache performance - hit for ${stage} on model ${modelToUse}`); return cachedResult; } const circuitBreaker = getCircuitBreaker(stage, modelToUse); if (circuitBreaker.isOpen()) { const message = `[JAF:GUARDRAILS] Circuit breaker open for ${stage} guardrail on model ${modelToUse}, using failSafe: ${failSafe}`; safeConsole.warn(message); return failSafe === 'allow' ? { isValid: true } : { isValid: false, errorMessage: 'Circuit breaker open - too many recent failures' }; } if (!content || typeof content !== 'string') { safeConsole.warn(`[JAF:GUARDRAILS] Invalid content provided to ${stage} guardrail`); return failSafe === 'allow' ? { isValid: true } : { isValid: false, errorMessage: 'Invalid content provided to guardrail' }; } const maxContentLength = timeoutMs < 10000 ? SHORT_TIMEOUT_MAX_CONTENT : LONG_TIMEOUT_MAX_CONTENT; if (content.length > maxContentLength) { safeConsole.warn(`[JAF:GUARDRAILS] Content too large for ${stage} guardrail (${content.length} chars, max: ${maxContentLength})`); return failSafe === 'allow' ? { isValid: true } : { isValid: false, errorMessage: `Content too large for guardrail evaluation (${content.length} > ${maxContentLength} chars)` }; } const sanitizedContent = content .replace(/"""/g, '[TRIPLE_QUOTE]') .replace(/\n/g, ' ') .substring(0, Math.min(content.length, 2000)); const evalPrompt = `You are a guardrail validator for ${stage}. Rules: ${rulePrompt} Decide if the ${stage === "input" ? "user message" : "assistant output"} complies with the rules. Return a JSON object with keys: {"allowed": boolean, "reason": string}. Do not include extra text. ${stage === "input" ? "User message" : "Assistant output"}: """ ${sanitizedContent} """`; try { const tempState = { runId: createRunId('guardrail-eval'), traceId: createTraceId('guardrail-eval'), messages: [{ role: 'user', content: evalPrompt }], currentAgentName: 'guardrail-evaluator', context: {}, turnCount: 0 }; const evalAgent = { name: 'guardrail-evaluator', instructions: () => 'You are a guardrail validator. Return only valid JSON.', modelConfig: { name: modelToUse } }; const guardrailConfig = { modelProvider: config.modelProvider, agentRegistry: config.agentRegistry, maxTurns: 1, defaultFastModel: config.defaultFastModel, modelOverride: modelToUse, initialInputGuardrails: undefined, finalOutputGuardrails: undefined, onEvent: undefined }; const completionPromise = config.modelProvider.getCompletion(tempState, evalAgent, guardrailConfig); const response = await withTimeout(completionPromise, timeoutMs, `${stage} guardrail evaluation timed out after ${timeoutMs}ms`); if (!response.message?.content) { return { isValid: true }; } const parsed = jsonParseLLMOutput(response.message.content); const allowed = Boolean(parsed?.allowed); const reason = typeof parsed?.reason === "string" ? parsed.reason : "Guardrail violation"; circuitBreaker.recordSuccess(); const result = allowed ? { isValid: true } : { isValid: false, errorMessage: reason }; guardrailCache.set(stage, rulePrompt, content, modelToUse, result); return result; } catch (e) { circuitBreaker.recordFailure(); let errorMessage = 'Unknown error'; let isTimeout = false; if (e instanceof Error) { errorMessage = e.message; isTimeout = e.message.includes('Timeout'); } const logMessage = `[JAF:GUARDRAILS] ${stage} guardrail evaluation failed`; if (isTimeout) { safeConsole.warn(`${logMessage} due to timeout (${timeoutMs}ms), using failSafe: ${failSafe}`, { stage, modelToUse, contentLength: content.length, timeoutMs }); } else { safeConsole.warn(`${logMessage}, using failSafe: ${failSafe}`, { stage, modelToUse, error: errorMessage, contentLength: content.length }); } return failSafe === 'allow' ? { isValid: true } : { isValid: false, errorMessage: `Guardrail evaluation failed: ${errorMessage}` }; } }; } export async function buildEffectiveGuardrails(currentAgent, config) { let effectiveInputGuardrails = []; let effectiveOutputGuardrails = []; try { const rawGuardrailsCfg = currentAgent.advancedConfig?.guardrails || {}; const guardrailsCfg = validateGuardrailsConfig(rawGuardrailsCfg); const fastModel = guardrailsCfg.fastModel || config.defaultFastModel; if (!fastModel && (guardrailsCfg.inputPrompt || guardrailsCfg.outputPrompt)) { safeConsole.warn('[JAF:GUARDRAILS] No fast model available for LLM guardrails - skipping LLM-based validation'); } safeConsole.log('[JAF:GUARDRAILS] Configuration:', { hasInputPrompt: !!guardrailsCfg.inputPrompt, hasOutputPrompt: !!guardrailsCfg.outputPrompt, requireCitations: guardrailsCfg.requireCitations, executionMode: guardrailsCfg.executionMode, failSafe: guardrailsCfg.failSafe, timeoutMs: guardrailsCfg.timeoutMs, fastModel: fastModel || 'none' }); const llmGuardrail = async (stage, rulePrompt, content) => { const failSafe = guardrailsCfg.failSafe || 'allow'; const timeoutMs = guardrailsCfg.timeoutMs || 30000; if (!fastModel) { safeConsole.warn(`[JAF:GUARDRAILS] No model available for ${stage} guardrail - using failSafe: ${failSafe}`); return failSafe === 'allow' ? { isValid: true } : { isValid: false, errorMessage: 'No model available for guardrail evaluation' }; } safeConsole.log(`[JAF:GUARDRAILS] Evaluating ${stage} guardrail`); config.onEvent?.({ type: 'guardrail_check', data: { guardrailName: `${stage}-guardrail`, content, isValid: undefined } }); try { const evaluator = await createLLMGuardrail(config, stage, rulePrompt, fastModel, failSafe, timeoutMs); const result = await evaluator(content); safeConsole.log(`[JAF:GUARDRAILS] ${stage} guardrail result:`, result); config.onEvent?.({ type: 'guardrail_check', data: { guardrailName: `${stage}-guardrail`, content, isValid: result.isValid, errorMessage: result.isValid ? undefined : result.errorMessage } }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error in guardrail evaluation'; safeConsole.error(`[JAF:GUARDRAILS] Failed to create or execute ${stage} guardrail:`, error); config.onEvent?.({ type: 'guardrail_check', data: { guardrailName: `${stage}-guardrail`, content, isValid: false, errorMessage } }); return failSafe === 'allow' ? { isValid: true } : { isValid: false, errorMessage }; } }; effectiveInputGuardrails = [...(config.initialInputGuardrails || [])]; if (guardrailsCfg?.inputPrompt && typeof guardrailsCfg.inputPrompt === "string" && guardrailsCfg.inputPrompt.trim().length > 0) { const inputPrompt = guardrailsCfg.inputPrompt; effectiveInputGuardrails.push(async (userText) => { const content = typeof userText === "string" ? userText : String(userText); return llmGuardrail("input", inputPrompt, content); }); } effectiveOutputGuardrails = [...(config.finalOutputGuardrails || [])]; if (guardrailsCfg?.requireCitations) { effectiveOutputGuardrails.push((output) => { const findText = (val) => { if (typeof val === "string") return val; if (Array.isArray(val)) return val.map(findText).join(" "); if (val && typeof val === "object") return Object.values(val).map(findText).join(" "); return ""; }; const str = typeof output === "string" ? output : findText(output); const ok = /\[(\d+)\]/.test(str); return ok ? { isValid: true } : { isValid: false, errorMessage: "Missing required [n] citation in output" }; }); } if (guardrailsCfg?.outputPrompt && typeof guardrailsCfg.outputPrompt === "string" && guardrailsCfg.outputPrompt.trim().length > 0) { const outputPrompt = guardrailsCfg.outputPrompt; effectiveOutputGuardrails.push(async (output) => { const toString = (val) => { try { if (typeof val === "string") return val; return JSON.stringify(val); } catch { return String(val); } }; const content = toString(output); return llmGuardrail("output", outputPrompt, content); }); } } catch (e) { safeConsole.error('[JAF:GUARDRAILS] Failed to configure advanced guardrails:', e); effectiveInputGuardrails = [...(config.initialInputGuardrails || [])]; effectiveOutputGuardrails = [...(config.finalOutputGuardrails || [])]; } return { inputGuardrails: effectiveInputGuardrails, outputGuardrails: effectiveOutputGuardrails }; } export async function executeInputGuardrailsSequential(inputGuardrails, firstUserMessage, config) { if (inputGuardrails.length === 0) { return { isValid: true }; } safeConsole.log(`[JAF:GUARDRAILS] Starting ${inputGuardrails.length} input guardrails (sequential)`); const messageContent = firstUserMessage?.content; const content = getTextContent(messageContent); for (let i = 0; i < inputGuardrails.length; i++) { const guardrail = inputGuardrails[i]; const guardrailName = `input-guardrail-${i + 1}`; try { safeConsole.log(`[JAF:GUARDRAILS] Starting ${guardrailName}`); const timeoutMs = 10000; const guardrailResult = guardrail(content); const result = await withTimeout(Promise.resolve(guardrailResult), timeoutMs, `${guardrailName} execution timed out after ${timeoutMs}ms`); safeConsole.log(`[JAF:GUARDRAILS] ${guardrailName} completed:`, result); if (!result.isValid) { const errorMessage = 'errorMessage' in result ? result.errorMessage : 'Guardrail violation'; safeConsole.log(`🚨 ${guardrailName} violation: ${errorMessage}`); config.onEvent?.({ type: 'guardrail_violation', data: { stage: 'input', reason: errorMessage } }); return { isValid: false, errorMessage }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; safeConsole.error(`[JAF:GUARDRAILS] ${guardrailName} failed:`, errorMessage); const isSystemError = errorMessage.includes('Timeout') || errorMessage.includes('Circuit breaker'); if (isSystemError) { safeConsole.warn(`[JAF:GUARDRAILS] ${guardrailName} system error, continuing: ${errorMessage}`); continue; } else { config.onEvent?.({ type: 'guardrail_violation', data: { stage: 'input', reason: errorMessage } }); return { isValid: false, errorMessage }; } } } safeConsole.log(`✅ All input guardrails passed (sequential).`); return { isValid: true }; } export async function executeInputGuardrailsParallel(inputGuardrails, firstUserMessage, config) { if (inputGuardrails.length === 0) { return { isValid: true }; } safeConsole.log(`[JAF:GUARDRAILS] Starting ${inputGuardrails.length} input guardrails`); const inputGuardrailPromises = inputGuardrails.map(async (guardrail, index) => { const guardrailName = `input-guardrail-${index + 1}`; try { safeConsole.log(`[JAF:GUARDRAILS] Starting ${guardrailName}`); const timeoutMs = config.defaultFastModel ? 10000 : 5000; const messageContent = firstUserMessage?.content; const content = getTextContent(messageContent); const guardrailResult = guardrail(content); const result = await withTimeout(Promise.resolve(guardrailResult), timeoutMs, `${guardrailName} execution timed out after ${timeoutMs}ms`); safeConsole.log(`[JAF:GUARDRAILS] ${guardrailName} completed:`, result); return { ...result, guardrailIndex: index }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; safeConsole.error(`[JAF:GUARDRAILS] ${guardrailName} failed:`, { error: errorMessage, index, stack: error instanceof Error ? error.stack : undefined }); return { isValid: true, guardrailIndex: index, warning: `Guardrail ${index + 1} failed but was skipped: ${errorMessage}` }; } }); try { const settledResults = await Promise.allSettled(inputGuardrailPromises); safeConsole.log(`[JAF:GUARDRAILS] Input guardrails completed. Checking results...`); const results = []; const warnings = []; for (let i = 0; i < settledResults.length; i++) { const settled = settledResults[i]; if (settled.status === 'fulfilled') { const result = settled.value; results.push(result); if ('warning' in result && result.warning) { warnings.push(result.warning); } } else { const errorMessage = settled.reason instanceof Error ? settled.reason.message : 'Unknown error'; safeConsole.warn(`[JAF:GUARDRAILS] Input guardrail ${i + 1} promise rejected:`, errorMessage); warnings.push(`Guardrail ${i + 1} failed: ${errorMessage}`); results.push({ isValid: true, guardrailIndex: i, warning: `Promise rejected: ${errorMessage}` }); } } if (warnings.length > 0) { safeConsole.warn(`[JAF:GUARDRAILS] ${warnings.length} guardrail warnings:`, warnings); } for (const result of results) { if (!result.isValid) { const errorMessage = 'errorMessage' in result ? result.errorMessage : 'Guardrail violation'; safeConsole.log(`🚨 Input guardrail ${result.guardrailIndex + 1} violation: ${errorMessage}`); config.onEvent?.({ type: 'guardrail_violation', data: { stage: 'input', reason: errorMessage } }); return { isValid: false, errorMessage }; } } safeConsole.log(`✅ All input guardrails passed.`); return { isValid: true }; } catch (error) { safeConsole.error(`[JAF:GUARDRAILS] Catastrophic failure in input guardrail execution:`, error); return { isValid: true }; } } export async function executeOutputGuardrails(outputGuardrails, output, config) { if (outputGuardrails.length === 0) { return { isValid: true }; } safeConsole.log(`[JAF:GUARDRAILS] Checking ${outputGuardrails.length} output guardrails`); for (let i = 0; i < outputGuardrails.length; i++) { const guardrail = outputGuardrails[i]; const guardrailName = `output-guardrail-${i + 1}`; try { const timeoutMs = 15000; const guardrailResult = guardrail(output); const result = await withTimeout(Promise.resolve(guardrailResult), timeoutMs, `${guardrailName} execution timed out after ${timeoutMs}ms`); if (!result.isValid) { const errorMessage = 'errorMessage' in result ? result.errorMessage : 'Guardrail violation'; safeConsole.log(`🚨 ${guardrailName} violation: ${errorMessage}`); config.onEvent?.({ type: 'guardrail_violation', data: { stage: 'output', reason: errorMessage } }); return { isValid: false, errorMessage }; } safeConsole.log(`✅ ${guardrailName} passed`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; safeConsole.error(`[JAF:GUARDRAILS] ${guardrailName} failed:`, { error: errorMessage, index: i, stack: error instanceof Error ? error.stack : undefined }); const isSystemError = errorMessage.includes('Timeout') || errorMessage.includes('Circuit breaker'); if (isSystemError) { safeConsole.warn(`[JAF:GUARDRAILS] ${guardrailName} system error, allowing output: ${errorMessage}`); continue; } else { config.onEvent?.({ type: 'guardrail_violation', data: { stage: 'output', reason: errorMessage } }); return { isValid: false, errorMessage }; } } } safeConsole.log(`✅ All output guardrails passed`); return { isValid: true }; } export function cleanupCircuitBreakers() { for (const [key, breaker] of circuitBreakers.entries()) { if (breaker.shouldBeCleanedUp(CIRCUIT_BREAKER_CLEANUP_MAX_AGE)) { circuitBreakers.delete(key); } } } export const guardrailCacheManager = { getStats: () => guardrailCache.getStats(), clear: () => guardrailCache.clear(), getMetrics: () => { const stats = guardrailCache.getStats(); return { ...stats, utilizationPercent: (stats.size / stats.maxSize) * 100, circuitBreakersCount: circuitBreakers.size }; }, logStats: () => { const metrics = guardrailCacheManager.getMetrics(); safeConsole.log('[JAF:GUARDRAILS] Cache stats:', metrics); }, cleanup: () => { cleanupCircuitBreakers(); } }; //# sourceMappingURL=guardrails.js.map