From ee77dea2d6e378d59d54a3b96ebca12b4e92f949 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 11 Oct 2025 19:55:17 -0700 Subject: [PATCH] feat(guardrails): added guardrails block/tools and docs (#1605) * Adding guardrails block * ack PR comments * cleanup checkbox in dark mode * cleanup * fix supabase tools --- .../content/docs/en/blocks/guardrails.mdx | 251 ++++++++++++ apps/docs/public/static/blocks/guardrails.png | Bin 0 -> 109411 bytes apps/sim/app/api/guardrails/validate/route.ts | 239 +++++++++++ .../components/grouped-checkbox-list.tsx | 192 +++++++++ .../components/sub-block/components/index.ts | 1 + .../components/sub-block/sub-block.tsx | 15 + apps/sim/blocks/blocks/guardrails.ts | 374 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/blocks/types.ts | 19 +- apps/sim/components/icons.tsx | 20 + apps/sim/components/ui/checkbox.tsx | 4 +- apps/sim/executor/index.ts | 6 +- 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 | 266 +++++++++++++ apps/sim/lib/guardrails/validate_json.ts | 19 + apps/sim/lib/guardrails/validate_pii.py | 168 ++++++++ apps/sim/lib/guardrails/validate_pii.ts | 242 ++++++++++++ apps/sim/lib/guardrails/validate_regex.ts | 21 + apps/sim/tools/guardrails/index.ts | 2 + apps/sim/tools/guardrails/validate.ts | 183 +++++++++ apps/sim/tools/registry.ts | 2 + apps/sim/tools/supabase/delete.ts | 4 +- apps/sim/tools/supabase/get_row.ts | 4 +- apps/sim/tools/supabase/insert.ts | 4 +- apps/sim/tools/supabase/query.ts | 4 +- apps/sim/tools/supabase/update.ts | 4 +- apps/sim/tools/supabase/upsert.ts | 4 +- biome.json | 11 +- docker/app.Dockerfile | 13 + 32 files changed, 2206 insertions(+), 24 deletions(-) create mode 100644 apps/docs/content/docs/en/blocks/guardrails.mdx create mode 100644 apps/docs/public/static/blocks/guardrails.png 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 000000000..f2d6a95f8 --- /dev/null +++ b/apps/docs/content/docs/en/blocks/guardrails.mdx @@ -0,0 +1,251 @@ +--- +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. + +
+ Guardrails Block +
+ +## 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/docs/public/static/blocks/guardrails.png b/apps/docs/public/static/blocks/guardrails.png new file mode 100644 index 0000000000000000000000000000000000000000..db276e72ab3ac7c33c7e62289c89ccd9d805e24d GIT binary patch literal 109411 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 literal 0 HcmV?d00001 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 000000000..83c27eb13 --- /dev/null +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -0,0 +1,239 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { validateHallucination } from '@/lib/guardrails/validate_hallucination' +import { validateJson } from '@/lib/guardrails/validate_json' +import { validatePII } from '@/lib/guardrails/validate_pii' +import { validateRegex } from '@/lib/guardrails/validate_regex' +import { createLogger } from '@/lib/logs/console/logger' +import { generateRequestId } from '@/lib/utils' + +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 + + if (!validationType) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType: 'unknown', + input: input || '', + error: 'Missing required field: validationType', + }, + }) + } + + if (input === undefined || input === null) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: '', + error: 'Input is missing or undefined', + }, + }) + } + + 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"', + }, + }) + } + + if (validationType === 'regex' && !regex) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: 'Regex pattern is required for regex validation', + }, + }) + } + + if (validationType === 'hallucination' && !model) { + return NextResponse.json({ + success: true, + output: { + passed: false, + validationType, + input: input || '', + error: 'Model is required for hallucination validation', + }, + }) + } + + const inputStr = convertInputToString(input) + + logger.info(`[${requestId}] Executing validation locally`, { + validationType, + inputType: typeof input, + }) + + 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 NextResponse.json({ + success: true, + output: { + passed: false, + validationType: 'unknown', + input: '', + error: error.message || 'Validation failed due to unexpected error', + }, + }) + } +} + +/** + * Convert input to string for validation + */ +function convertInputToString(input: any): string { + if (typeof input === 'string') { + return input + } + if (input === null || input === undefined) { + return '' + } + if (typeof input === 'object') { + return JSON.stringify(input) + } + 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) + } + if (validationType === 'regex') { + if (!regex) { + return { + passed: false, + error: 'Regex pattern is required', + } + } + return validateRegex(inputStr, regex) + } + if (validationType === 'hallucination') { + if (!knowledgeBaseId) { + return { + passed: false, + error: 'Knowledge base ID is required for hallucination check', + } + } + + return await validateHallucination({ + userInput: inputStr, + knowledgeBaseId, + threshold: threshold != null ? Number.parseFloat(threshold) : 3, // Default threshold is 3 (confidence score, scores < 3 fail) + topK: topK ? Number.parseInt(topK) : 10, // Default topK is 10 + model: model, + apiKey, + workflowId, + requestId, + }) + } + if (validationType === 'pii') { + 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, + }) + } + 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 000000000..d248118b7 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/grouped-checkbox-list.tsx @@ -0,0 +1,192 @@ +'use client' + +import { useMemo, useState } from 'react' +import { Settings2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' + +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) + + const previewValue = isPreview && subBlockValues ? subBlockValues[subBlockId]?.value : undefined + const selectedValues = ((isPreview ? previewValue : storeValue) as string[]) || [] + + 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 2d16bb19e..82a4ed8aa 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 @@ -10,6 +10,7 @@ export { EvalInput } from './eval-input' export { FileSelectorInput } from './file-selector/file-selector-input' export { FileUpload } from './file-upload' export { FolderSelectorInput } from './folder-selector/components/folder-selector-input' +export { GroupedCheckboxList } from './grouped-checkbox-list' export { InputMapping } from './input-mapping/input-mapping' export { KnowledgeBaseSelector } from './knowledge-base-selector/knowledge-base-selector' export { LongInput } from './long-input' 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 4511803a2..19f896e1f 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: 'blocks', + bgColor: '#3D642D', + icon: ShieldCheckIcon, + subBlocks: [ + { + id: 'input', + title: 'Content to Validate', + type: 'long-input', + layout: 'full', + placeholder: 'Enter content to validate', + 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: 'model', + title: 'Model', + type: 'combobox', + layout: 'half', + 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: 'threshold', + title: 'Confidence', + type: 'slider', + layout: 'half', + 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: '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 + }, + } + } + // 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 1f30aea2c..31ed5f5c6 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -30,6 +30,7 @@ import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_form' import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets' import { GoogleVaultBlock } from '@/blocks/blocks/google_vault' +import { GuardrailsBlock } from '@/blocks/blocks/guardrails' import { HuggingFaceBlock } from '@/blocks/blocks/huggingface' import { HunterBlock } from '@/blocks/blocks/hunter' import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator' @@ -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 4153b9315..4593d3b05 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -47,6 +47,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 @@ -123,8 +124,18 @@ 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[] @@ -134,6 +145,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 39f356cc6..c8c132453 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 ( - + )) diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index 16ee641f7..b1601dc44 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -2009,11 +2009,7 @@ export class Executor { // Handle error outputs and ensure object structure const output: NormalizedBlockOutput = - rawOutput && typeof rawOutput === 'object' && rawOutput.error - ? { error: rawOutput.error, status: rawOutput.status || 500 } - : typeof rawOutput === 'object' && rawOutput !== null - ? rawOutput - : { result: rawOutput } + typeof rawOutput === 'object' && rawOutput !== null ? rawOutput : { result: rawOutput } // Update the context with the execution result // Use virtual block ID for parallel executions diff --git a/apps/sim/lib/guardrails/.gitignore b/apps/sim/lib/guardrails/.gitignore new file mode 100644 index 000000000..3485e9bdf --- /dev/null +++ b/apps/sim/lib/guardrails/.gitignore @@ -0,0 +1,13 @@ +# Python virtual environment +venv/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# Presidio cache +.presidio/ + diff --git a/apps/sim/lib/guardrails/README.md b/apps/sim/lib/guardrails/README.md new file mode 100644 index 000000000..6ce7802d2 --- /dev/null +++ b/apps/sim/lib/guardrails/README.md @@ -0,0 +1,102 @@ +# Guardrails Validators + +Validation scripts for the Guardrails block. + +## Validators + +- **JSON Validation** - Validates if content is valid JSON (TypeScript) +- **Regex Validation** - Validates content against regex patterns (TypeScript) +- **Hallucination Detection** - Validates LLM output against knowledge base using RAG + LLM scoring (TypeScript) +- **PII Detection** - Detects personally identifiable information using Microsoft Presidio (Python) + +## Setup + +### TypeScript Validators (JSON, Regex, Hallucination) + +No additional setup required! These validators work out of the box. + +For **hallucination detection**, you'll need: +- A knowledge base with documents +- An LLM provider API key (or use hosted models) + +### Python Validators (PII Detection) + +For **PII detection**, you need to set up a Python virtual environment and install Microsoft Presidio: + +```bash +cd apps/sim/lib/guardrails +./setup.sh +``` + +This will: +1. Create a Python virtual environment in `apps/sim/lib/guardrails/venv` +2. Install required dependencies: + - `presidio-analyzer` - PII detection engine + - `presidio-anonymizer` - PII masking/anonymization + +The TypeScript wrapper will automatically use the virtual environment's Python interpreter. + +## Usage + +### JSON & Regex Validation + +These are implemented in TypeScript and work out of the box - no additional dependencies needed. + +### Hallucination Detection + +The hallucination detector uses a modern RAG + LLM confidence scoring approach: + +1. **RAG Query** - Calls the knowledge base search API to retrieve relevant chunks +2. **LLM Confidence Scoring** - Uses an LLM to score how well the user input is supported by the retrieved context on a 0-10 confidence scale: + - 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 +3. **Threshold Check** - Compares the confidence score against your threshold (default: 3) +4. **Result** - Returns `passed: true/false` with confidence score and reasoning + +**Configuration:** +- `knowledgeBaseId` (required): Select from dropdown of available knowledge bases +- `threshold` (optional): Confidence threshold 0-10, default 3 (scores below 3 fail) +- `topK` (optional): Number of chunks to retrieve, default 10 +- `model` (required): Select from dropdown of available LLM models, default `gpt-4o-mini` +- `apiKey` (conditional): API key for the LLM provider (hidden for hosted models and Ollama) + +### PII Detection + +The PII detector uses Microsoft Presidio to identify personally identifiable information: + +1. **Analysis** - Scans text for PII entities using pattern matching, NER, and context +2. **Detection** - Identifies PII types like names, emails, phone numbers, SSNs, credit cards, etc. +3. **Action** - Either blocks the request or masks the PII based on mode + +**Modes:** +- **Block Mode** (default): Fails validation if any PII is detected +- **Mask Mode**: Passes validation and returns text with PII replaced by `` 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 000000000..135efae05 --- /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 000000000..233e9a51a --- /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 000000000..49f08dca8 --- /dev/null +++ b/apps/sim/lib/guardrails/validate_hallucination.ts @@ -0,0 +1,266 @@ +import { env } from '@/lib/env' +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 = `${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 || [] + + 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, + }) + + 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 }) + + let jsonContent = content + + 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 { + 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', + } + } + + 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 000000000..8edc7f3bd --- /dev/null +++ b/apps/sim/lib/guardrails/validate_json.ts @@ -0,0 +1,19 @@ +/** + * 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 000000000..570786b8d --- /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 000000000..241d994b0 --- /dev/null +++ b/apps/sim/lib/guardrails/validate_pii.ts @@ -0,0 +1,242 @@ +import { spawn } from 'child_process' +import fs from 'fs' +import path from 'path' +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, reject) => { + // 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() + reject(new Error('PII validation timeout')) + }, 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, + }) + reject( + new Error( + `Failed to execute Python: ${error.message}. Make sure Python 3 and Presidio are installed.` + ) + ) + }) + }) +} + +/** + * 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 000000000..16bd78ebf --- /dev/null +++ b/apps/sim/lib/guardrails/validate_regex.ts @@ -0,0 +1,21 @@ +/** + * 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 } + } + 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 000000000..84dde2eb8 --- /dev/null +++ b/apps/sim/tools/guardrails/index.ts @@ -0,0 +1,2 @@ +export type { GuardrailsValidateInput, GuardrailsValidateOutput } from './validate' +export { guardrailsValidateTool } from './validate' diff --git a/apps/sim/tools/guardrails/validate.ts b/apps/sim/tools/guardrails/validate.ts new file mode 100644 index 000000000..9bb8d6c17 --- /dev/null +++ b/apps/sim/tools/guardrails/validate.ts @@ -0,0 +1,183 @@ +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 = + { + 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 => { + const result = await response.json() + + 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 9fc727f15..f110ef012 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -64,6 +64,7 @@ import { listMattersHoldsTool, listMattersTool, } from '@/tools/google_vault' +import { guardrailsValidateTool } from '@/tools/guardrails' import { requestTool as httpRequest } from '@/tools/http' import { huggingfaceChatTool } from '@/tools/huggingface' import { @@ -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/apps/sim/tools/supabase/delete.ts b/apps/sim/tools/supabase/delete.ts index 9b3eee85e..74eece386 100644 --- a/apps/sim/tools/supabase/delete.ts +++ b/apps/sim/tools/supabase/delete.ts @@ -17,7 +17,7 @@ export const deleteTool: ToolConfig = table: { type: 'string', required: true, - visibility: 'user-only', + visibility: 'user-or-llm', description: 'The name of the Supabase table to query', }, filter: { @@ -41,7 +41,7 @@ export const queryTool: ToolConfig = apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Your Supabase service role secret key', }, }, diff --git a/apps/sim/tools/supabase/update.ts b/apps/sim/tools/supabase/update.ts index f213b629b..91ce02410 100644 --- a/apps/sim/tools/supabase/update.ts +++ b/apps/sim/tools/supabase/update.ts @@ -17,7 +17,7 @@ export const updateTool: ToolConfig