From 43ceaf3f0e775766a2c05c5d2370a2267998e55c Mon Sep 17 00:00:00 2001 From: Devang Jhabakh Date: Thu, 9 Oct 2025 18:49:35 -0700 Subject: [PATCH] Adding guardrails block --- .../content/docs/en/blocks/guardrails.mdx | 233 +++++++++++ apps/sim/app/api/guardrails/validate/route.ts | 232 +++++++++++ .../components/grouped-checkbox-list.tsx | 182 +++++++++ .../components/sub-block/components/index.ts | 1 + .../components/sub-block/sub-block.tsx | 15 + apps/sim/blocks/blocks/guardrails.ts | 376 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/blocks/types.ts | 9 +- apps/sim/components/icons.tsx | 20 + apps/sim/executor/index.ts | 8 +- apps/sim/lib/guardrails/.gitignore | 13 + apps/sim/lib/guardrails/README.md | 102 +++++ apps/sim/lib/guardrails/requirements.txt | 4 + apps/sim/lib/guardrails/setup.sh | 37 ++ .../lib/guardrails/validate_hallucination.ts | 275 +++++++++++++ apps/sim/lib/guardrails/validate_json.ts | 20 + apps/sim/lib/guardrails/validate_pii.py | 168 ++++++++ apps/sim/lib/guardrails/validate_pii.ts | 247 ++++++++++++ apps/sim/lib/guardrails/validate_regex.ts | 23 ++ apps/sim/tools/guardrails/index.ts | 3 + apps/sim/tools/guardrails/validate.ts | 187 +++++++++ apps/sim/tools/registry.ts | 2 + docker/app.Dockerfile | 13 + 23 files changed, 2165 insertions(+), 7 deletions(-) create mode 100644 apps/docs/content/docs/en/blocks/guardrails.mdx create mode 100644 apps/sim/app/api/guardrails/validate/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/grouped-checkbox-list.tsx create mode 100644 apps/sim/blocks/blocks/guardrails.ts create mode 100644 apps/sim/lib/guardrails/.gitignore create mode 100644 apps/sim/lib/guardrails/README.md create mode 100644 apps/sim/lib/guardrails/requirements.txt create mode 100755 apps/sim/lib/guardrails/setup.sh create mode 100644 apps/sim/lib/guardrails/validate_hallucination.ts create mode 100644 apps/sim/lib/guardrails/validate_json.ts create mode 100644 apps/sim/lib/guardrails/validate_pii.py create mode 100644 apps/sim/lib/guardrails/validate_pii.ts create mode 100644 apps/sim/lib/guardrails/validate_regex.ts create mode 100644 apps/sim/tools/guardrails/index.ts create mode 100644 apps/sim/tools/guardrails/validate.ts diff --git a/apps/docs/content/docs/en/blocks/guardrails.mdx b/apps/docs/content/docs/en/blocks/guardrails.mdx new file mode 100644 index 0000000000..3cf58cbf47 --- /dev/null +++ b/apps/docs/content/docs/en/blocks/guardrails.mdx @@ -0,0 +1,233 @@ +--- +title: Guardrails +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { Image } from '@/components/ui/image' +import { Video } from '@/components/ui/video' + +The Guardrails block validates and protects your AI workflows by checking content against multiple validation types. Ensure data quality, prevent hallucinations, detect PII, and enforce format requirements before content moves through your workflow. + +## Overview + +The Guardrails block enables you to: + + + + Validate JSON Structure: Ensure LLM outputs are valid JSON before parsing + + + Match Regex Patterns: Verify content matches specific formats (emails, phone numbers, URLs, etc.) + + + Detect Hallucinations: Use RAG + LLM scoring to validate AI outputs against knowledge base content + + + Detect PII: Identify and optionally mask personally identifiable information across 40+ entity types + + + +## Validation Types + +### JSON Validation + +Validates that content is properly formatted JSON. Perfect for ensuring structured LLM outputs can be safely parsed. + +**Use Cases:** +- Validate JSON responses from Agent blocks before parsing +- Ensure API payloads are properly formatted +- Check structured data integrity + +**Output:** +- `passed`: `true` if valid JSON, `false` otherwise +- `error`: Error message if validation fails (e.g., "Invalid JSON: Unexpected token...") + +### Regex Validation + +Checks if content matches a specified regular expression pattern. + +**Use Cases:** +- Validate email addresses +- Check phone number formats +- Verify URLs or custom identifiers +- Enforce specific text patterns + +**Configuration:** +- **Regex Pattern**: The regular expression to match against (e.g., `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` for emails) + +**Output:** +- `passed`: `true` if content matches pattern, `false` otherwise +- `error`: Error message if validation fails + +### Hallucination Detection + +Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-generated content contradicts or isn't grounded in your knowledge base. + +**How It Works:** +1. Queries your knowledge base for relevant context +2. Sends both the AI output and retrieved context to an LLM +3. LLM assigns a confidence score (0-10 scale) + - **0** = Full hallucination (completely ungrounded) + - **10** = Fully grounded (completely supported by knowledge base) +4. Validation passes if score ≥ threshold (default: 3) + +**Configuration:** +- **Knowledge Base**: Select from your existing knowledge bases +- **Model**: Choose LLM for scoring (requires strong reasoning - GPT-4o, Claude 3.7 Sonnet recommended) +- **API Key**: Authentication for selected LLM provider (auto-hidden for hosted/Ollama models) +- **Confidence Threshold**: Minimum score to pass (0-10, default: 3) +- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 10) + +**Output:** +- `passed`: `true` if confidence score ≥ threshold +- `score`: Confidence score (0-10) +- `reasoning`: LLM's explanation for the score +- `error`: Error message if validation fails + +**Use Cases:** +- Validate Agent responses against documentation +- Ensure customer support answers are factually accurate +- Verify generated content matches source material +- Quality control for RAG applications + +### PII Detection + +Detects personally identifiable information using Microsoft Presidio. Supports 40+ entity types across multiple countries and languages. + +**How It Works:** +1. Scans content for PII entities using pattern matching and NLP +2. Returns detected entities with locations and confidence scores +3. Optionally masks detected PII in the output + +**Configuration:** +- **PII Types to Detect**: Select from grouped categories via modal selector + - **Common**: Person name, Email, Phone, Credit card, IP address, etc. + - **USA**: SSN, Driver's license, Passport, etc. + - **UK**: NHS number, National insurance number + - **Spain**: NIF, NIE, CIF + - **Italy**: Fiscal code, Driver's license, VAT code + - **Poland**: PESEL, NIP, REGON + - **Singapore**: NRIC/FIN, UEN + - **Australia**: ABN, ACN, TFN, Medicare + - **India**: Aadhaar, PAN, Passport, Voter number +- **Mode**: + - **Detect**: Only identify PII (default) + - **Mask**: Replace detected PII with masked values +- **Language**: Detection language (default: English) + +**Output:** +- `passed`: `false` if any selected PII types are detected +- `detectedEntities`: Array of detected PII with type, location, and confidence +- `maskedText`: Content with PII masked (only if mode = "Mask") +- `error`: Error message if validation fails + +**Use Cases:** +- Block content containing sensitive personal information +- Mask PII before logging or storing data +- Compliance with GDPR, HIPAA, and other privacy regulations +- Sanitize user inputs before processing + +## Configuration + +### Content to Validate + +The input content to validate. This typically comes from: +- Agent block outputs: `` +- Function block results: `` +- API responses: `` +- Any other block output + +### Validation Type + +Choose from four validation types: +- **Valid JSON**: Check if content is properly formatted JSON +- **Regex Match**: Verify content matches a regex pattern +- **Hallucination Check**: Validate against knowledge base with LLM scoring +- **PII Detection**: Detect and optionally mask personally identifiable information + +## Outputs + +All validation types return: + +- **``**: Boolean indicating if validation passed +- **``**: The type of validation performed +- **``**: The original input that was validated +- **``**: Error message if validation failed (optional) + +Additional outputs by type: + +**Hallucination Check:** +- **``**: Confidence score (0-10) +- **``**: LLM's explanation + +**PII Detection:** +- **``**: Array of detected PII entities +- **``**: Content with PII masked (if mode = "Mask") + +## Example Use Cases + +### Validate JSON Before Parsing + +
+

Scenario: Ensure Agent output is valid JSON

+
    +
  1. Agent generates structured JSON response
  2. +
  3. Guardrails validates JSON format
  4. +
  5. Condition block checks ``
  6. +
  7. If passed → Parse and use data, If failed → Retry or handle error
  8. +
+
+ +### Prevent Hallucinations + +
+

Scenario: Validate customer support responses

+
    +
  1. Agent generates response to customer question
  2. +
  3. Guardrails checks against support documentation knowledge base
  4. +
  5. If confidence score ≥ 3 → Send response
  6. +
  7. If confidence score \< 3 → Flag for human review
  8. +
+
+ +### Block PII in User Inputs + +
+

Scenario: Sanitize user-submitted content

+
    +
  1. User submits form with text content
  2. +
  3. Guardrails detects PII (emails, phone numbers, SSN, etc.)
  4. +
  5. If PII detected → Reject submission or mask sensitive data
  6. +
  7. If no PII → Process normally
  8. +
+
+ +### Validate Email Format + +
+

Scenario: Check email address format

+
    +
  1. Agent extracts email from text
  2. +
  3. Guardrails validates with regex pattern
  4. +
  5. If valid → Use email for notification
  6. +
  7. If invalid → Request correction
  8. +
+
+ +## Best Practices + +- **Chain with Condition blocks**: Use `` to branch workflow logic based on validation results +- **Use JSON validation before parsing**: Always validate JSON structure before attempting to parse LLM outputs +- **Choose appropriate PII types**: Only select the PII entity types relevant to your use case for better performance +- **Set reasonable confidence thresholds**: For hallucination detection, adjust threshold based on your accuracy requirements (higher = stricter) +- **Use strong models for hallucination detection**: GPT-4o or Claude 3.7 Sonnet provide more accurate confidence scoring +- **Mask PII for logging**: Use "Mask" mode when you need to log or store content that may contain PII +- **Test regex patterns**: Validate your regex patterns thoroughly before deploying to production +- **Monitor validation failures**: Track `` messages to identify common validation issues + + + Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical. + + diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts new file mode 100644 index 0000000000..7243539fd2 --- /dev/null +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -0,0 +1,232 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console/logger' +import { generateRequestId } from '@/lib/utils' +import { validateJson } from '@/lib/guardrails/validate_json' +import { validateRegex } from '@/lib/guardrails/validate_regex' +import { validateHallucination } from '@/lib/guardrails/validate_hallucination' +import { validatePII } from '@/lib/guardrails/validate_pii' + +const logger = createLogger('GuardrailsValidateAPI') + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + logger.info(`[${requestId}] Guardrails validation request received`) + + try { + const body = await request.json() + const { validationType, input, regex, knowledgeBaseId, threshold, topK, model, apiKey, workflowId, piiEntityTypes, piiMode, piiLanguage } = body + + // Validate required fields + if (!validationType) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType: 'unknown', + input: input || '', + error: 'Missing required field: validationType', + }, + }) + } + + // Handle empty or missing input - but allow empty strings for JSON validation + // (empty string is invalid JSON, which is a valid validation result) + if (input === undefined || input === null) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: '', + error: 'Input is missing or undefined', + }, + }) + } + + // Validate validationType + if (validationType !== 'json' && validationType !== 'regex' && validationType !== 'hallucination' && validationType !== 'pii') { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: 'Invalid validationType. Must be "json", "regex", "hallucination", or "pii"', + }, + }) + } + + // For regex validation, ensure regex pattern is provided + if (validationType === 'regex' && !regex) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: 'Regex pattern is required for regex validation', + }, + }) + } + + // For hallucination validation, ensure model is provided + if (validationType === 'hallucination' && !model) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: 'Model is required for hallucination validation', + }, + }) + } + + // Convert input to string for validation + const inputStr = convertInputToString(input) + + logger.info(`[${requestId}] Executing validation locally`, { + validationType, + inputType: typeof input, + }) + + // Execute validation + const validationResult = await executeValidation( + validationType, + inputStr, + regex, + knowledgeBaseId, + threshold, + topK, + model, + apiKey, + workflowId, + piiEntityTypes, + piiMode, + piiLanguage, + requestId + ) + + logger.info(`[${requestId}] Validation completed`, { + passed: validationResult.passed, + hasError: !!validationResult.error, + score: validationResult.score, + }) + + return NextResponse.json({ + success: true, + output: { + passed: validationResult.passed, + validationType, + input, + error: validationResult.error, + score: validationResult.score, + reasoning: validationResult.reasoning, + detectedEntities: validationResult.detectedEntities, + maskedText: validationResult.maskedText, + }, + }) + } catch (error: any) { + logger.error(`[${requestId}] Guardrails validation failed`, { error }) + // Return validation failure instead of 500 error + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType: 'unknown', + input: '', + error: error.message || 'Validation failed due to unexpected error', + }, + }) + } +} + +/** + * Convert input to strfing for validation + */ +function convertInputToString(input: any): string { + if (typeof input === 'string') { + return input + } else if (input === null || input === undefined) { + return '' + } else if (typeof input === 'object') { + return JSON.stringify(input) + } else { + return String(input) + } +} + +/** + * Execute validation using TypeScript validators + */ +async function executeValidation( + validationType: string, + inputStr: string, + regex: string | undefined, + knowledgeBaseId: string | undefined, + threshold: string | undefined, + topK: string | undefined, + model: string, + apiKey: string | undefined, + workflowId: string | undefined, + piiEntityTypes: string[] | undefined, + piiMode: string | undefined, + piiLanguage: string | undefined, + requestId: string +): Promise<{ + passed: boolean + error?: string + score?: number + reasoning?: string + detectedEntities?: any[] + maskedText?: string +}> { + // Use TypeScript validators for all validation types + if (validationType === 'json') { + return validateJson(inputStr) + } else if (validationType === 'regex') { + if (!regex) { + return { + passed: false, + error: 'Regex pattern is required', + } + } + return validateRegex(inputStr, regex) + } else if (validationType === 'hallucination') { + if (!knowledgeBaseId) { + return { + passed: false, + error: 'Knowledge base ID is required for hallucination check', + } + } + + // Use TypeScript hallucination validator with RAG + LLM scoring + return await validateHallucination({ + userInput: inputStr, + knowledgeBaseId, + threshold: threshold != null ? parseFloat(threshold) : 3, // Default threshold is 3 (confidence score, scores < 3 fail) + topK: topK ? parseInt(topK) : 10, // Default topK is 10 + model: model, + apiKey, + workflowId, + requestId, + }) + } else if (validationType === 'pii') { + // PII validation using Presidio + return await validatePII({ + text: inputStr, + entityTypes: piiEntityTypes || [], // Empty array = detect all PII types + mode: (piiMode as 'block' | 'mask') || 'block', // Default to block mode + language: piiLanguage || 'en', + requestId, + }) + } else { + return { + passed: false, + error: 'Unknown validation type', + } + } +} + + + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/grouped-checkbox-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/grouped-checkbox-list.tsx new file mode 100644 index 0000000000..7a0c1d56a5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/grouped-checkbox-list.tsx @@ -0,0 +1,182 @@ +'use client' + +import React, { useState, useMemo } from 'react' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { Settings2 } from 'lucide-react' + +interface GroupedCheckboxListProps { + blockId: string + subBlockId: string + title: string + options: { label: string; id: string; group?: string }[] + layout?: 'full' | 'half' + isPreview?: boolean + subBlockValues: Record + disabled?: boolean + maxHeight?: number +} + +export function GroupedCheckboxList({ + blockId, + subBlockId, + title, + options, + layout = 'full', + isPreview = false, + subBlockValues, + disabled = false, + maxHeight = 400, +}: GroupedCheckboxListProps) { + const [open, setOpen] = useState(false) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) + + // Get preview value or use store value + const previewValue = isPreview && subBlockValues ? subBlockValues[subBlockId]?.value : undefined + const selectedValues = (isPreview ? previewValue : storeValue) as string[] || [] + + // Group options by their group property + const groupedOptions = useMemo(() => { + const groups: Record = {} + + options.forEach((option) => { + const groupName = option.group || 'Other' + if (!groups[groupName]) { + groups[groupName] = [] + } + groups[groupName].push({ label: option.label, id: option.id }) + }) + + return groups + }, [options]) + + const handleToggle = (optionId: string) => { + if (isPreview || disabled) return + + const currentValues = (selectedValues || []) as string[] + const newValues = currentValues.includes(optionId) + ? currentValues.filter((id) => id !== optionId) + : [...currentValues, optionId] + + setStoreValue(newValues) + } + + const handleSelectAll = () => { + if (isPreview || disabled) return + const allIds = options.map((opt) => opt.id) + setStoreValue(allIds) + } + + const handleClear = () => { + if (isPreview || disabled) return + setStoreValue([]) + } + + const allSelected = selectedValues.length === options.length + const noneSelected = selectedValues.length === 0 + + const SelectedCountDisplay = () => { + if (noneSelected) { + return None selected + } + if (allSelected) { + return All selected + } + return {selectedValues.length} selected + } + + return ( + + + + + e.stopPropagation()} + > + + Select PII Types to Detect +

+ Choose which types of personally identifiable information to detect and block. +

+
+ + {/* Header with Select All and Clear */} +
+
+ { + if (checked) { + handleSelectAll() + } else { + handleClear() + } + }} + disabled={disabled} + /> + +
+ +
+ + {/* Scrollable grouped checkboxes */} +
e.stopPropagation()} + style={{ maxHeight: '60vh' }} + > +
+ {Object.entries(groupedOptions).map(([groupName, groupOptions]) => ( +
+

+ {groupName} +

+
+ {groupOptions.map((option) => ( +
+ handleToggle(option.id)} + disabled={disabled} + /> + +
+ ))} +
+
+ ))} +
+
+
+
+ ) +} + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts index 2d16bb19ef..9db98e6355 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts @@ -6,6 +6,7 @@ export { ConditionInput } from './condition-input' export { CredentialSelector } from './credential-selector/credential-selector' export { DocumentSelector } from './document-selector/document-selector' export { Dropdown } from './dropdown' +export { GroupedCheckboxList } from './grouped-checkbox-list' export { EvalInput } from './eval-input' export { FileSelectorInput } from './file-selector/file-selector-input' export { FileUpload } from './file-upload' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index 4511803a25..19f896e1fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -17,6 +17,7 @@ import { FileSelectorInput, FileUpload, FolderSelectorInput, + GroupedCheckboxList, InputFormat, InputMapping, KnowledgeBaseSelector, @@ -254,6 +255,20 @@ export function SubBlock({ disabled={isDisabled} /> ) + case 'grouped-checkbox-list': + return ( + + ) case 'condition-input': return ( { + const providersState = useProvidersStore.getState() + return providersState.providers.ollama.models +} + +export interface GuardrailsResponse extends ToolResponse { + output: { + passed: boolean + validationType: string + input: string + error?: string + score?: number + reasoning?: string + } +} + +export const GuardrailsBlock: BlockConfig = { + type: 'guardrails', + name: 'Guardrails', + description: 'Validate content with guardrails', + longDescription: + 'Validate content using guardrails. Check if content is valid JSON, matches a regex pattern, detect hallucinations using RAG + LLM scoring, or detect PII.', + bestPractices: ` + - Reference block outputs using syntax in the Content field + - Use JSON validation to ensure structured output from LLMs before parsing + - Use regex validation for format checking (emails, phone numbers, URLs, etc.) + - Use hallucination check to validate LLM outputs against knowledge base content + - Use PII detection to block or mask sensitive personal information + - Access validation result with (true/false) + - For hallucination check, access (0-10 confidence) and + - For PII detection, access and + - Chain with Condition block to handle validation failures + `, + docsLink: 'https://docs.sim.ai/blocks/guardrails', + category: 'tools', + bgColor: '#9333EA', + icon: ShieldCheckIcon, + subBlocks: [ + { + id: 'input', + title: 'Content to Validate', + type: 'long-input', + layout: 'full', + placeholder: 'Reference block output: ', + required: true, + }, + { + id: 'validationType', + title: 'Validation Type', + type: 'dropdown', + layout: 'full', + required: true, + options: [ + { label: 'Valid JSON', id: 'json' }, + { label: 'Regex Match', id: 'regex' }, + { label: 'Hallucination Check', id: 'hallucination' }, + { label: 'PII Detection', id: 'pii' }, + ], + defaultValue: 'json', + }, + { + id: 'regex', + title: 'Regex Pattern', + type: 'short-input', + layout: 'full', + placeholder: 'e.g., ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + required: true, + condition: { + field: 'validationType', + value: ['regex'], + }, + }, + { + id: 'knowledgeBaseId', + title: 'Knowledge Base', + type: 'knowledge-base-selector', + layout: 'full', + placeholder: 'Select knowledge base', + multiSelect: false, + required: true, + condition: { + field: 'validationType', + value: ['hallucination'], + }, + }, + { + id: 'threshold', + title: 'Confidence Threshold (0-10)', + type: 'slider', + layout: 'full', + min: 0, + max: 10, + step: 1, + defaultValue: 3, + condition: { + field: 'validationType', + value: ['hallucination'], + }, + }, + { + id: 'topK', + title: 'Number of Chunks to Retrieve', + type: 'slider', + layout: 'full', + min: 1, + max: 20, + step: 1, + defaultValue: 5, + mode: 'advanced', + condition: { + field: 'validationType', + value: ['hallucination'], + }, + }, + { + id: 'model', + title: 'Model', + type: 'combobox', + layout: 'full', + placeholder: 'Type or select a model...', + required: true, + options: () => { + const providersState = useProvidersStore.getState() + const ollamaModels = providersState.providers.ollama.models + const openrouterModels = providersState.providers.openrouter.models + const baseModels = Object.keys(getBaseModelProviders()) + const allModels = Array.from(new Set([...baseModels, ...ollamaModels, ...openrouterModels])) + + return allModels.map((model) => { + const icon = getProviderIcon(model) + return { label: model, id: model, ...(icon && { icon }) } + }) + }, + condition: { + field: 'validationType', + value: ['hallucination'], + }, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your API key', + password: true, + connectionDroppable: false, + required: true, + // Show API key field only for hallucination validation + // Hide for hosted models and Ollama models + condition: () => { + const baseCondition = { + field: 'validationType' as const, + value: ['hallucination'], + } + + if (isHosted) { + // In hosted mode, hide for hosted models + return { + ...baseCondition, + and: { + field: 'model' as const, + value: getHostedModels(), + not: true, // Show for all models EXCEPT hosted ones + }, + } + } else { + // In self-hosted mode, hide for Ollama models + return { + ...baseCondition, + and: { + field: 'model' as const, + value: getCurrentOllamaModels(), + not: true, // Show for all models EXCEPT Ollama ones + }, + } + } + }, + }, + { + id: 'piiEntityTypes', + title: 'PII Types to Detect', + type: 'grouped-checkbox-list', + layout: 'full', + maxHeight: 400, + options: [ + // Common PII types + { label: 'Person name', id: 'PERSON', group: 'Common' }, + { label: 'Email address', id: 'EMAIL_ADDRESS', group: 'Common' }, + { label: 'Phone number', id: 'PHONE_NUMBER', group: 'Common' }, + { label: 'Location', id: 'LOCATION', group: 'Common' }, + { label: 'Date or time', id: 'DATE_TIME', group: 'Common' }, + { label: 'IP address', id: 'IP_ADDRESS', group: 'Common' }, + { label: 'URL', id: 'URL', group: 'Common' }, + { label: 'Credit card number', id: 'CREDIT_CARD', group: 'Common' }, + { label: 'International bank account number (IBAN)', id: 'IBAN_CODE', group: 'Common' }, + { label: 'Cryptocurrency wallet address', id: 'CRYPTO', group: 'Common' }, + { label: 'Medical license number', id: 'MEDICAL_LICENSE', group: 'Common' }, + { label: 'Nationality / religion / political group', id: 'NRP', group: 'Common' }, + + // USA + { label: 'US bank account number', id: 'US_BANK_NUMBER', group: 'USA' }, + { label: 'US driver license number', id: 'US_DRIVER_LICENSE', group: 'USA' }, + { label: 'US individual taxpayer identification number (ITIN)', id: 'US_ITIN', group: 'USA' }, + { label: 'US passport number', id: 'US_PASSPORT', group: 'USA' }, + { label: 'US Social Security number', id: 'US_SSN', group: 'USA' }, + + // UK + { label: 'UK National Insurance number', id: 'UK_NINO', group: 'UK' }, + { label: 'UK NHS number', id: 'UK_NHS', group: 'UK' }, + + // Spain + { label: 'Spanish NIF number', id: 'ES_NIF', group: 'Spain' }, + { label: 'Spanish NIE number', id: 'ES_NIE', group: 'Spain' }, + + // Italy + { label: 'Italian fiscal code', id: 'IT_FISCAL_CODE', group: 'Italy' }, + { label: 'Italian driver license', id: 'IT_DRIVER_LICENSE', group: 'Italy' }, + { label: 'Italian identity card', id: 'IT_IDENTITY_CARD', group: 'Italy' }, + { label: 'Italian passport', id: 'IT_PASSPORT', group: 'Italy' }, + + // Poland + { label: 'Polish PESEL', id: 'PL_PESEL', group: 'Poland' }, + + // Singapore + { label: 'Singapore NRIC/FIN', id: 'SG_NRIC_FIN', group: 'Singapore' }, + + // Australia + { label: 'Australian business number (ABN)', id: 'AU_ABN', group: 'Australia' }, + { label: 'Australian company number (ACN)', id: 'AU_ACN', group: 'Australia' }, + { label: 'Australian tax file number (TFN)', id: 'AU_TFN', group: 'Australia' }, + { label: 'Australian Medicare number', id: 'AU_MEDICARE', group: 'Australia' }, + + // India + { label: 'Indian Aadhaar', id: 'IN_AADHAAR', group: 'India' }, + { label: 'Indian PAN', id: 'IN_PAN', group: 'India' }, + { label: 'Indian vehicle registration', id: 'IN_VEHICLE_REGISTRATION', group: 'India' }, + { label: 'Indian voter number', id: 'IN_VOTER', group: 'India' }, + { label: 'Indian passport', id: 'IN_PASSPORT', group: 'India' }, + ], + condition: { + field: 'validationType', + value: ['pii'], + }, + }, + { + id: 'piiMode', + title: 'Action', + type: 'dropdown', + layout: 'full', + required: true, + options: [ + { label: 'Block Request', id: 'block' }, + { label: 'Mask PII', id: 'mask' }, + ], + defaultValue: 'block', + condition: { + field: 'validationType', + value: ['pii'], + }, + }, + { + id: 'piiLanguage', + title: 'Language', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'English', id: 'en' }, + { label: 'Spanish', id: 'es' }, + { label: 'Italian', id: 'it' }, + { label: 'Polish', id: 'pl' }, + { label: 'Finnish', id: 'fi' }, + ], + defaultValue: 'en', + condition: { + field: 'validationType', + value: ['pii'], + }, + }, + ], + tools: { + access: ['guardrails_validate'], + }, + inputs: { + input: { + type: 'string', + description: 'Content to validate (automatically receives input from wired block)', + }, + validationType: { + type: 'string', + description: 'Type of validation to perform (json, regex, hallucination, or pii)', + }, + regex: { + type: 'string', + description: 'Regex pattern for regex validation', + }, + knowledgeBaseId: { + type: 'string', + description: 'Knowledge base ID for hallucination check', + }, + threshold: { + type: 'string', + description: 'Confidence threshold (0-10 scale, default: 3, scores below fail)', + }, + topK: { + type: 'string', + description: 'Number of chunks to retrieve from knowledge base (default: 5)', + }, + model: { + type: 'string', + description: 'LLM model for hallucination scoring (default: gpt-4o-mini)', + }, + apiKey: { + type: 'string', + description: 'API key for LLM provider (optional if using hosted)', + }, + piiEntityTypes: { + type: 'json', + description: 'PII entity types to detect (array of strings, empty = detect all)', + }, + piiMode: { + type: 'string', + description: 'PII action mode: block or mask', + }, + piiLanguage: { + type: 'string', + description: 'Language for PII detection (default: en)', + }, + }, + outputs: { + passed: { + type: 'boolean', + description: 'Whether validation passed (true/false)', + }, + validationType: { + type: 'string', + description: 'Type of validation performed', + }, + input: { + type: 'string', + description: 'Original input that was validated', + }, + error: { + type: 'string', + description: 'Error message if validation failed', + }, + score: { + type: 'number', + description: 'Confidence score (0-10, 0=hallucination, 10=grounded, only for hallucination check)', + }, + reasoning: { + type: 'string', + description: 'Reasoning for confidence score (only for hallucination check)', + }, + detectedEntities: { + type: 'array', + description: 'Detected PII entities (only for PII detection)', + }, + maskedText: { + type: 'string', + description: 'Text with PII masked (only for PII detection in mask mode)', + }, + }, +} + diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 1f30aea2c2..bf952674de 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -23,6 +23,7 @@ import { FunctionBlock } from '@/blocks/blocks/function' import { GenericWebhookBlock } from '@/blocks/blocks/generic_webhook' import { GitHubBlock } from '@/blocks/blocks/github' import { GmailBlock } from '@/blocks/blocks/gmail' +import { GuardrailsBlock } from '@/blocks/blocks/guardrails' import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleCalendarBlock } from '@/blocks/blocks/google_calendar' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' @@ -108,6 +109,7 @@ export const registry: Record = { generic_webhook: GenericWebhookBlock, github: GitHubBlock, gmail: GmailBlock, + guardrails: GuardrailsBlock, google_calendar: GoogleCalendarBlock, google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 3b970ebf3a..d97ce04821 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -40,6 +40,7 @@ export type SubBlockType = | 'switch' // Toggle button | 'tool-input' // Tool configuration | 'checkbox-list' // Multiple selection + | 'grouped-checkbox-list' // Grouped, scrollable checkbox list with select all | 'condition-input' // Conditional logic | 'eval-input' // Evaluation input | 'time-input' // Time input @@ -116,8 +117,8 @@ export interface SubBlockConfig { required?: boolean defaultValue?: string | number | boolean | Record | Array options?: - | { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[] - | (() => { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[]) + | { label: string; id: string; icon?: React.ComponentType<{ className?: string }>; group?: string }[] + | (() => { label: string; id: string; icon?: React.ComponentType<{ className?: string }>; group?: string }[]) min?: number max?: number columns?: string[] @@ -127,6 +128,10 @@ export interface SubBlockConfig { hidden?: boolean description?: string value?: (params: Record) => string + grouped?: boolean + scrollable?: boolean + maxHeight?: number + selectAllOption?: boolean condition?: | { field: string diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 39f356cc61..c8c1324539 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2991,6 +2991,26 @@ export const OllamaIcon = (props: SVGProps) => ( ) +export function ShieldCheckIcon(props: SVGProps) { + return ( + + + + + ) +} + export function WealthboxIcon(props: SVGProps) { return ( ` placeholders + +**Configuration:** +- `piiEntityTypes` (optional): Array of PII types to detect (empty = detect all) +- `piiMode` (optional): `block` or `mask`, default `block` +- `piiLanguage` (optional): Language code, default `en` + +**Supported PII Types:** +- **Common**: Person name, Email, Phone, Credit card, Location, IP address, Date/time, URL +- **USA**: SSN, Passport, Driver license, Bank account, ITIN +- **UK**: NHS number, National Insurance Number +- **Other**: Spanish NIF/NIE, Italian fiscal code, Polish PESEL, Singapore NRIC, Australian ABN/TFN, Indian Aadhaar/PAN, and more + +See [Presidio documentation](https://microsoft.github.io/presidio/supported_entities/) for full list. + +## Files + +- `validate_json.ts` - JSON validation (TypeScript) +- `validate_regex.ts` - Regex validation (TypeScript) +- `validate_hallucination.ts` - Hallucination detection with RAG + LLM scoring (TypeScript) +- `validate_pii.ts` - PII detection TypeScript wrapper (TypeScript) +- `validate_pii.py` - PII detection using Microsoft Presidio (Python) +- `validate.test.ts` - Test suite for JSON and regex validators +- `validate_hallucination.py` - Legacy Python hallucination detector (deprecated) +- `requirements.txt` - Python dependencies for PII detection (and legacy hallucination) +- `setup.sh` - Legacy installation script (deprecated) + diff --git a/apps/sim/lib/guardrails/requirements.txt b/apps/sim/lib/guardrails/requirements.txt new file mode 100644 index 0000000000..135efae05b --- /dev/null +++ b/apps/sim/lib/guardrails/requirements.txt @@ -0,0 +1,4 @@ +# Microsoft Presidio for PII detection +presidio-analyzer>=2.2.0 +presidio-anonymizer>=2.2.0 + diff --git a/apps/sim/lib/guardrails/setup.sh b/apps/sim/lib/guardrails/setup.sh new file mode 100755 index 0000000000..233e9a51a2 --- /dev/null +++ b/apps/sim/lib/guardrails/setup.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Setup script for guardrails validators +# This creates a virtual environment and installs Python dependencies + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="$SCRIPT_DIR/venv" + +echo "Setting up Python environment for guardrails..." + +# Check if Python 3 is available +if ! command -v python3 &> /dev/null; then + echo "Error: python3 is not installed. Please install Python 3 first." + exit 1 +fi + +# Create virtual environment if it doesn't exist +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + python3 -m venv "$VENV_DIR" +else + echo "Virtual environment already exists." +fi + +# Activate virtual environment and install dependencies +echo "Installing Python dependencies..." +source "$VENV_DIR/bin/activate" +pip install --upgrade pip +pip install -r "$SCRIPT_DIR/requirements.txt" + +echo "" +echo "✅ Setup complete! Guardrails validators are ready to use." +echo "" +echo "Virtual environment created at: $VENV_DIR" + diff --git a/apps/sim/lib/guardrails/validate_hallucination.ts b/apps/sim/lib/guardrails/validate_hallucination.ts new file mode 100644 index 0000000000..fe10938365 --- /dev/null +++ b/apps/sim/lib/guardrails/validate_hallucination.ts @@ -0,0 +1,275 @@ +import { createLogger } from '@/lib/logs/console/logger' +import { executeProviderRequest } from '@/providers' +import { getApiKey, getProviderFromModel } from '@/providers/utils' + +const logger = createLogger('HallucinationValidator') + +export interface HallucinationValidationResult { + passed: boolean + error?: string + score?: number + reasoning?: string +} + +export interface HallucinationValidationInput { + userInput: string + knowledgeBaseId: string + threshold: number // 0-10 confidence scale, default 3 (scores below 3 fail) + topK: number // Number of chunks to retrieve, default 10 + model: string + apiKey?: string + workflowId?: string + requestId: string +} + +/** + * Query knowledge base to get relevant context chunks using the search API + */ +async function queryKnowledgeBase( + knowledgeBaseId: string, + query: string, + topK: number, + requestId: string, + workflowId?: string +): Promise { + try { + logger.info(`[${requestId}] Querying knowledge base`, { + knowledgeBaseId, + query: query.substring(0, 100), + topK, + }) + + // Call the knowledge base search API directly + const searchUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/knowledge/search` + + const response = await fetch(searchUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + knowledgeBaseIds: [knowledgeBaseId], + query, + topK, + workflowId, + }), + }) + + if (!response.ok) { + logger.error(`[${requestId}] Knowledge base query failed`, { + status: response.status, + }) + return [] + } + + const result = await response.json() + const results = result.data?.results || [] + + // Extract content from search results + const chunks = results.map((r: any) => r.content || '').filter((c: string) => c.length > 0) + + logger.info(`[${requestId}] Retrieved ${chunks.length} chunks from knowledge base`) + + return chunks + } catch (error: any) { + logger.error(`[${requestId}] Error querying knowledge base`, { + error: error.message, + }) + return [] + } +} + +/** + * Use an LLM to score confidence based on RAG context + * Returns a confidence score from 0-10 where: + * - 0 = full hallucination (completely unsupported) + * - 10 = fully grounded (completely supported) + */ +async function scoreHallucinationWithLLM( + userInput: string, + ragContext: string[], + model: string, + apiKey: string, + requestId: string +): Promise<{ score: number; reasoning: string }> { + try { + const contextText = ragContext.join('\n\n---\n\n') + + const systemPrompt = `You are a confidence scoring system. Your job is to evaluate how well a user's input is supported by the provided reference context from a knowledge base. + +Score the input on a confidence scale from 0 to 10: +- 0-2: Full hallucination - completely unsupported by context, contradicts the context +- 3-4: Low confidence - mostly unsupported, significant claims not in context +- 5-6: Medium confidence - partially supported, some claims not in context +- 7-8: High confidence - mostly supported, minor details not in context +- 9-10: Very high confidence - fully supported by context, all claims verified + +Respond ONLY with valid JSON in this exact format: +{ + "score": , + "reasoning": "" +} + +Do not include any other text, markdown formatting, or code blocks. Only output the raw JSON object. Be strict - only give high scores (7+) if the input is well-supported by the context.` + + const userPrompt = `Reference Context: +${contextText} + +User Input to Evaluate: +${userInput} + +Evaluate the consistency and provide your score and reasoning in JSON format.` + + logger.info(`[${requestId}] Calling LLM for hallucination scoring`, { + model, + contextChunks: ragContext.length, + }) + + const providerId = getProviderFromModel(model) + + const response = await executeProviderRequest(providerId, { + model, + systemPrompt, + messages: [ + { + role: 'user', + content: userPrompt, + }, + ], + temperature: 0.1, // Low temperature for consistent scoring + apiKey, + }) + + // Handle streaming response (shouldn't happen with our request, but just in case) + if (response instanceof ReadableStream || ('stream' in response && 'execution' in response)) { + throw new Error('Unexpected streaming response from LLM') + } + + const content = response.content.trim() + logger.debug(`[${requestId}] LLM response:`, { content }) + + // Try to parse JSON from the response + let jsonContent = content + + // Remove markdown code blocks if present + if (content.includes('```')) { + const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/) + if (jsonMatch) { + jsonContent = jsonMatch[1] + } + } + + const result = JSON.parse(jsonContent) + + if (typeof result.score !== 'number' || result.score < 0 || result.score > 10) { + throw new Error('Invalid score format from LLM') + } + + logger.info(`[${requestId}] Confidence score: ${result.score}/10`, { + reasoning: result.reasoning, + }) + + return { + score: result.score, + reasoning: result.reasoning || 'No reasoning provided', + } + } catch (error: any) { + logger.error(`[${requestId}] Error scoring with LLM`, { + error: error.message, + }) + throw new Error(`Failed to score confidence: ${error.message}`) + } +} + +/** + * Validate user input against knowledge base using RAG + LLM scoring + */ +export async function validateHallucination( + input: HallucinationValidationInput +): Promise { + const { + userInput, + knowledgeBaseId, + threshold, + topK, + model, + apiKey, + workflowId, + requestId, + } = input + + try { + // Validate inputs + if (!userInput || userInput.trim().length === 0) { + return { + passed: false, + error: 'User input is required', + } + } + + if (!knowledgeBaseId) { + return { + passed: false, + error: 'Knowledge base ID is required', + } + } + + // Get API key for the model + let finalApiKey: string + try { + const providerId = getProviderFromModel(model) + finalApiKey = getApiKey(providerId, model, apiKey) + } catch (error: any) { + return { + passed: false, + error: `API key error: ${error.message}`, + } + } + + // Step 1: Query knowledge base with RAG + const ragContext = await queryKnowledgeBase(knowledgeBaseId, userInput, topK, requestId, workflowId) + + if (ragContext.length === 0) { + return { + passed: false, + error: 'No relevant context found in knowledge base', + } + } + + // Step 2: Use LLM to score confidence + const { score, reasoning } = await scoreHallucinationWithLLM( + userInput, + ragContext, + model, + finalApiKey, + requestId + ) + + logger.info(`[${requestId}] Confidence score: ${score}`, { + reasoning, + threshold, + }) + + // Step 3: Check against threshold + // Lower scores = less confidence = fail validation + const passed = score >= threshold + + return { + passed, + score, + reasoning, + error: passed + ? undefined + : `Low confidence: score ${score}/10 is below threshold ${threshold}`, + } + } catch (error: any) { + logger.error(`[${requestId}] Hallucination validation error`, { + error: error.message, + }) + return { + passed: false, + error: `Validation error: ${error.message}`, + } + } +} + diff --git a/apps/sim/lib/guardrails/validate_json.ts b/apps/sim/lib/guardrails/validate_json.ts new file mode 100644 index 0000000000..bd2507513e --- /dev/null +++ b/apps/sim/lib/guardrails/validate_json.ts @@ -0,0 +1,20 @@ +/** + * Validate if input is valid JSON + */ +export interface ValidationResult { + passed: boolean + error?: string +} + +export function validateJson(inputStr: string): ValidationResult { + try { + JSON.parse(inputStr) + return { passed: true } + } catch (error: any) { + if (error instanceof SyntaxError) { + return { passed: false, error: `Invalid JSON: ${error.message}` } + } + return { passed: false, error: `Validation error: ${error.message}` } + } +} + diff --git a/apps/sim/lib/guardrails/validate_pii.py b/apps/sim/lib/guardrails/validate_pii.py new file mode 100644 index 0000000000..570786b8d9 --- /dev/null +++ b/apps/sim/lib/guardrails/validate_pii.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +PII Detection Validator using Microsoft Presidio + +Detects personally identifiable information (PII) in text and either: +- Blocks the request if PII is detected (block mode) +- Masks the PII and returns the masked text (mask mode) +""" + +import sys +import json +from typing import List, Dict, Any + +try: + from presidio_analyzer import AnalyzerEngine + from presidio_anonymizer import AnonymizerEngine + from presidio_anonymizer.entities import OperatorConfig +except ImportError: + print(json.dumps({ + "passed": False, + "error": "Presidio not installed. Run: pip install presidio-analyzer presidio-anonymizer", + "detectedEntities": [] + })) + sys.exit(0) + + +def detect_pii( + text: str, + entity_types: List[str], + mode: str = "block", + language: str = "en" +) -> Dict[str, Any]: + """ + Detect PII in text using Presidio + + Args: + text: Input text to analyze + entity_types: List of PII entity types to detect (e.g., ["PERSON", "EMAIL_ADDRESS"]) + mode: "block" to fail validation if PII found, "mask" to return masked text + language: Language code (default: "en") + + Returns: + Dictionary with validation result + """ + try: + # Initialize Presidio engines + analyzer = AnalyzerEngine() + + # Analyze text for PII + results = analyzer.analyze( + text=text, + entities=entity_types if entity_types else None, # None = detect all + language=language + ) + + # Extract detected entities + detected_entities = [] + for result in results: + detected_entities.append({ + "type": result.entity_type, + "start": result.start, + "end": result.end, + "score": result.score, + "text": text[result.start:result.end] + }) + + # If no PII detected, validation passes + if not results: + return { + "passed": True, + "detectedEntities": [], + "maskedText": None + } + + # Block mode: fail validation if PII detected + if mode == "block": + entity_summary = {} + for entity in detected_entities: + entity_type = entity["type"] + entity_summary[entity_type] = entity_summary.get(entity_type, 0) + 1 + + summary_str = ", ".join([f"{count} {etype}" for etype, count in entity_summary.items()]) + + return { + "passed": False, + "error": f"PII detected: {summary_str}", + "detectedEntities": detected_entities, + "maskedText": None + } + + # Mask mode: anonymize PII and return masked text + elif mode == "mask": + anonymizer = AnonymizerEngine() + + # Use as the replacement pattern + operators = {} + for entity_type in set([r.entity_type for r in results]): + operators[entity_type] = OperatorConfig("replace", {"new_value": f"<{entity_type}>"}) + + anonymized_result = anonymizer.anonymize( + text=text, + analyzer_results=results, + operators=operators + ) + + return { + "passed": True, + "detectedEntities": detected_entities, + "maskedText": anonymized_result.text + } + + else: + return { + "passed": False, + "error": f"Invalid mode: {mode}. Must be 'block' or 'mask'", + "detectedEntities": [] + } + + except Exception as e: + return { + "passed": False, + "error": f"PII detection failed: {str(e)}", + "detectedEntities": [] + } + + +def main(): + """Main entry point for CLI usage""" + try: + # Read input from stdin + input_data = sys.stdin.read() + data = json.loads(input_data) + + text = data.get("text", "") + entity_types = data.get("entityTypes", []) + mode = data.get("mode", "block") + language = data.get("language", "en") + + # Validate inputs + if not text: + result = { + "passed": False, + "error": "No text provided", + "detectedEntities": [] + } + else: + result = detect_pii(text, entity_types, mode, language) + + # Output result with marker for parsing + print(f"__SIM_RESULT__={json.dumps(result)}") + + except json.JSONDecodeError as e: + print(f"__SIM_RESULT__={json.dumps({ + 'passed': False, + 'error': f'Invalid JSON input: {str(e)}', + 'detectedEntities': [] + })}") + except Exception as e: + print(f"__SIM_RESULT__={json.dumps({ + 'passed': False, + 'error': f'Unexpected error: {str(e)}', + 'detectedEntities': [] + })}") + + +if __name__ == "__main__": + main() + diff --git a/apps/sim/lib/guardrails/validate_pii.ts b/apps/sim/lib/guardrails/validate_pii.ts new file mode 100644 index 0000000000..5ce801557a --- /dev/null +++ b/apps/sim/lib/guardrails/validate_pii.ts @@ -0,0 +1,247 @@ +import { spawn } from 'child_process' +import path from 'path' +import fs from 'fs' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('PIIValidator') +const DEFAULT_TIMEOUT = 30000 // 30 seconds + +export interface PIIValidationInput { + text: string + entityTypes: string[] // e.g., ["PERSON", "EMAIL_ADDRESS", "CREDIT_CARD"] + mode: 'block' | 'mask' // block = fail if PII found, mask = return masked text + language?: string // default: "en" + requestId: string +} + +export interface DetectedPIIEntity { + type: string + start: number + end: number + score: number + text: string +} + +export interface PIIValidationResult { + passed: boolean + error?: string + detectedEntities: DetectedPIIEntity[] + maskedText?: string +} + +/** + * Validate text for PII using Microsoft Presidio + * + * Supports two modes: + * - block: Fails validation if any PII is detected + * - mask: Passes validation and returns masked text with PII replaced + */ +export async function validatePII(input: PIIValidationInput): Promise { + const { text, entityTypes, mode, language = 'en', requestId } = input + + logger.info(`[${requestId}] Starting PII validation`, { + textLength: text.length, + entityTypes, + mode, + language, + }) + + try { + // Call Python script for PII detection + const result = await executePythonPIIDetection(text, entityTypes, mode, language, requestId) + + logger.info(`[${requestId}] PII validation completed`, { + passed: result.passed, + detectedCount: result.detectedEntities.length, + hasMaskedText: !!result.maskedText, + }) + + return result + } catch (error: any) { + logger.error(`[${requestId}] PII validation failed`, { + error: error.message, + }) + + return { + passed: false, + error: `PII validation failed: ${error.message}`, + detectedEntities: [], + } + } +} + +/** + * Execute Python PII detection script + */ +async function executePythonPIIDetection( + text: string, + entityTypes: string[], + mode: string, + language: string, + requestId: string +): Promise { + return new Promise((resolve) => { + // Use path relative to project root + // In Next.js, process.cwd() returns the project root + const guardrailsDir = path.join(process.cwd(), 'lib/guardrails') + const scriptPath = path.join(guardrailsDir, 'validate_pii.py') + const venvPython = path.join(guardrailsDir, 'venv/bin/python3') + + // Use venv Python if it exists, otherwise fall back to system python3 + const pythonCmd = fs.existsSync(venvPython) ? venvPython : 'python3' + + const python = spawn(pythonCmd, [scriptPath]) + + let stdout = '' + let stderr = '' + + const timeout = setTimeout(() => { + python.kill() + resolve({ + passed: false, + error: 'PII validation timeout', + detectedEntities: [], + }) + }, DEFAULT_TIMEOUT) + + // Write input to stdin as JSON + const inputData = JSON.stringify({ + text, + entityTypes, + mode, + language, + }) + python.stdin.write(inputData) + python.stdin.end() + + python.stdout.on('data', (data) => { + stdout += data.toString() + }) + + python.stderr.on('data', (data) => { + stderr += data.toString() + }) + + python.on('close', (code) => { + clearTimeout(timeout) + + if (code !== 0) { + logger.error(`[${requestId}] Python PII detection failed`, { + code, + stderr, + }) + resolve({ + passed: false, + error: stderr || 'PII detection failed', + detectedEntities: [], + }) + return + } + + // Parse result from stdout + try { + const prefix = '__SIM_RESULT__=' + const lines = stdout.split('\n') + const marker = lines.find((l) => l.startsWith(prefix)) + + if (marker) { + const jsonPart = marker.slice(prefix.length) + const result = JSON.parse(jsonPart) + resolve(result) + } else { + logger.error(`[${requestId}] No result marker found`, { + stdout, + stderr, + stdoutLines: lines, + }) + resolve({ + passed: false, + error: `No result marker found in output. stdout: ${stdout.substring(0, 200)}, stderr: ${stderr.substring(0, 200)}`, + detectedEntities: [], + }) + } + } catch (error: any) { + logger.error(`[${requestId}] Failed to parse Python result`, { + error: error.message, + stdout, + stderr, + }) + resolve({ + passed: false, + error: `Failed to parse result: ${error.message}. stdout: ${stdout.substring(0, 200)}`, + detectedEntities: [], + }) + } + }) + + python.on('error', (error) => { + clearTimeout(timeout) + logger.error(`[${requestId}] Failed to spawn Python process`, { + error: error.message, + }) + resolve({ + passed: false, + error: `Failed to execute Python: ${error.message}. Make sure Python 3 and Presidio are installed.`, + detectedEntities: [], + }) + }) + }) +} + +/** + * List of all supported PII entity types + * Based on Microsoft Presidio's supported entities + */ +export const SUPPORTED_PII_ENTITIES = { + // Common/Global + CREDIT_CARD: 'Credit card number', + CRYPTO: 'Cryptocurrency wallet address', + DATE_TIME: 'Date or time', + EMAIL_ADDRESS: 'Email address', + IBAN_CODE: 'International Bank Account Number', + IP_ADDRESS: 'IP address', + NRP: 'Nationality, religious or political group', + LOCATION: 'Location', + PERSON: 'Person name', + PHONE_NUMBER: 'Phone number', + MEDICAL_LICENSE: 'Medical license number', + URL: 'URL', + + // USA + US_BANK_NUMBER: 'US bank account number', + US_DRIVER_LICENSE: 'US driver license', + US_ITIN: 'US Individual Taxpayer Identification Number', + US_PASSPORT: 'US passport number', + US_SSN: 'US Social Security Number', + + // UK + UK_NHS: 'UK NHS number', + UK_NINO: 'UK National Insurance Number', + + // Other countries + ES_NIF: 'Spanish NIF number', + ES_NIE: 'Spanish NIE number', + IT_FISCAL_CODE: 'Italian fiscal code', + IT_DRIVER_LICENSE: 'Italian driver license', + IT_VAT_CODE: 'Italian VAT code', + IT_PASSPORT: 'Italian passport', + IT_IDENTITY_CARD: 'Italian identity card', + PL_PESEL: 'Polish PESEL number', + SG_NRIC_FIN: 'Singapore NRIC/FIN', + SG_UEN: 'Singapore Unique Entity Number', + AU_ABN: 'Australian Business Number', + AU_ACN: 'Australian Company Number', + AU_TFN: 'Australian Tax File Number', + AU_MEDICARE: 'Australian Medicare number', + IN_PAN: 'Indian Permanent Account Number', + IN_AADHAAR: 'Indian Aadhaar number', + IN_VEHICLE_REGISTRATION: 'Indian vehicle registration', + IN_VOTER: 'Indian voter ID', + IN_PASSPORT: 'Indian passport', + FI_PERSONAL_IDENTITY_CODE: 'Finnish Personal Identity Code', + KR_RRN: 'Korean Resident Registration Number', + TH_TNIN: 'Thai National ID Number', +} as const + +export type PIIEntityType = keyof typeof SUPPORTED_PII_ENTITIES + diff --git a/apps/sim/lib/guardrails/validate_regex.ts b/apps/sim/lib/guardrails/validate_regex.ts new file mode 100644 index 0000000000..e1e8d5826e --- /dev/null +++ b/apps/sim/lib/guardrails/validate_regex.ts @@ -0,0 +1,23 @@ +/** + * Validate if input matches regex pattern + */ +export interface ValidationResult { + passed: boolean + error?: string +} + +export function validateRegex(inputStr: string, pattern: string): ValidationResult { + try { + const regex = new RegExp(pattern) + const match = regex.test(inputStr) + + if (match) { + return { passed: true } + } else { + return { passed: false, error: 'Input does not match regex pattern' } + } + } catch (error: any) { + return { passed: false, error: `Invalid regex pattern: ${error.message}` } + } +} + diff --git a/apps/sim/tools/guardrails/index.ts b/apps/sim/tools/guardrails/index.ts new file mode 100644 index 0000000000..adcbc03b49 --- /dev/null +++ b/apps/sim/tools/guardrails/index.ts @@ -0,0 +1,3 @@ +export { guardrailsValidateTool } from './validate' +export type { GuardrailsValidateInput, GuardrailsValidateOutput } from './validate' + diff --git a/apps/sim/tools/guardrails/validate.ts b/apps/sim/tools/guardrails/validate.ts new file mode 100644 index 0000000000..a4bb1b6dc3 --- /dev/null +++ b/apps/sim/tools/guardrails/validate.ts @@ -0,0 +1,187 @@ +import type { ToolConfig } from '@/tools/types' + +export interface GuardrailsValidateInput { + input: string + validationType: 'json' | 'regex' | 'hallucination' | 'pii' + regex?: string + knowledgeBaseId?: string + threshold?: string + topK?: string + model?: string + apiKey?: string + piiEntityTypes?: string[] + piiMode?: string + piiLanguage?: string + _context?: { + workflowId?: string + workspaceId?: string + } +} + +export interface GuardrailsValidateOutput { + success: boolean + output: { + passed: boolean + validationType: string + content: string + error?: string + score?: number + reasoning?: string + detectedEntities?: any[] + maskedText?: string + } + error?: string +} + +export const guardrailsValidateTool: ToolConfig< + GuardrailsValidateInput, + GuardrailsValidateOutput +> = { + id: 'guardrails_validate', + name: 'Guardrails Validate', + description: 'Validate content using guardrails (JSON, regex, hallucination check, or PII detection)', + version: '1.0.0', + + params: { + input: { + type: 'string', + required: true, + description: 'Content to validate (from wired block)', + }, + validationType: { + type: 'string', + required: true, + description: 'Type of validation: json, regex, hallucination, or pii', + }, + regex: { + type: 'string', + required: false, + description: 'Regex pattern (required for regex validation)', + }, + knowledgeBaseId: { + type: 'string', + required: false, + description: 'Knowledge base ID (required for hallucination check)', + }, + threshold: { + type: 'string', + required: false, + description: 'Confidence threshold (0-10 scale, default: 3, scores below fail)', + }, + topK: { + type: 'string', + required: false, + description: 'Number of chunks to retrieve from knowledge base (default: 10)', + }, + model: { + type: 'string', + required: false, + description: 'LLM model for confidence scoring (default: gpt-4o-mini)', + }, + apiKey: { + type: 'string', + required: false, + description: 'API key for LLM provider (optional if using hosted)', + }, + piiEntityTypes: { + type: 'array', + required: false, + description: 'PII entity types to detect (empty = detect all)', + }, + piiMode: { + type: 'string', + required: false, + description: 'PII action mode: block or mask (default: block)', + }, + piiLanguage: { + type: 'string', + required: false, + description: 'Language for PII detection (default: en)', + }, + }, + + outputs: { + passed: { + type: 'boolean', + description: 'Whether validation passed', + }, + validationType: { + type: 'string', + description: 'Type of validation performed', + }, + input: { + type: 'string', + description: 'Original input', + }, + error: { + type: 'string', + description: 'Error message if validation failed', + optional: true, + }, + score: { + type: 'number', + description: 'Confidence score (0-10, 0=hallucination, 10=grounded, only for hallucination check)', + optional: true, + }, + reasoning: { + type: 'string', + description: 'Reasoning for confidence score (only for hallucination check)', + optional: true, + }, + detectedEntities: { + type: 'array', + description: 'Detected PII entities (only for PII detection)', + optional: true, + }, + maskedText: { + type: 'string', + description: 'Text with PII masked (only for PII detection in mask mode)', + optional: true, + }, + }, + + request: { + url: '/api/guardrails/validate', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: GuardrailsValidateInput) => ({ + input: params.input, + validationType: params.validationType, + regex: params.regex, + knowledgeBaseId: params.knowledgeBaseId, + threshold: params.threshold, + topK: params.topK, + model: params.model, + apiKey: params.apiKey, + piiEntityTypes: params.piiEntityTypes, + piiMode: params.piiMode, + piiLanguage: params.piiLanguage, + workflowId: params._context?.workflowId, + workspaceId: params._context?.workspaceId, + }), + }, + + transformResponse: async (response: Response): Promise => { + // Always parse the JSON response, even for non-OK status codes + // Our API always returns a structured response with validation results + const result = await response.json() + + // If the API returned an error structure (shouldn't happen now, but just in case) + if (!response.ok && !result.output) { + return { + success: true, + output: { + passed: false, + validationType: 'unknown', + content: '', + error: result.error || `Validation failed with status ${response.status}`, + }, + } + } + + return result + }, +} + diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 9fc727f15a..c41d9a551c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -26,6 +26,7 @@ import { import { fileParseTool } from '@/tools/file' import { crawlTool, scrapeTool, searchTool } from '@/tools/firecrawl' import { functionExecuteTool } from '@/tools/function' +import { guardrailsValidateTool } from '@/tools/guardrails' import { githubCommentTool, githubLatestCommitTool, @@ -215,6 +216,7 @@ export const tools: Record = { firecrawl_search: searchTool, firecrawl_crawl: crawlTool, google_search: googleSearchTool, + guardrails_validate: guardrailsValidateTool, jina_read_url: readUrlTool, linkup_search: linkupSearchTool, resend_send: mailSendTool, diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile index a6d012f4ca..73254e5184 100644 --- a/docker/app.Dockerfile +++ b/docker/app.Dockerfile @@ -58,12 +58,25 @@ RUN bun run build FROM base AS runner WORKDIR /app +# Install Python and dependencies for guardrails PII detection +RUN apk add --no-cache python3 py3-pip bash + ENV NODE_ENV=production COPY --from=builder /app/apps/sim/public ./apps/sim/public COPY --from=builder /app/apps/sim/.next/standalone ./ COPY --from=builder /app/apps/sim/.next/static ./apps/sim/.next/static +# Copy guardrails setup script and requirements +COPY --from=builder /app/apps/sim/lib/guardrails/setup.sh ./apps/sim/lib/guardrails/setup.sh +COPY --from=builder /app/apps/sim/lib/guardrails/requirements.txt ./apps/sim/lib/guardrails/requirements.txt +COPY --from=builder /app/apps/sim/lib/guardrails/validate_pii.py ./apps/sim/lib/guardrails/validate_pii.py + +# Run guardrails setup to create venv and install Python dependencies +RUN chmod +x ./apps/sim/lib/guardrails/setup.sh && \ + cd ./apps/sim/lib/guardrails && \ + ./setup.sh + EXPOSE 3000 ENV PORT=3000 \ HOSTNAME="0.0.0.0"