mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-05 05:04:10 -05:00
feat(guardrails): added guardrails block/tools and docs (#1605)
* Adding guardrails block * ack PR comments * cleanup checkbox in dark mode * cleanup * fix supabase tools
This commit is contained in:
251
apps/docs/content/docs/en/blocks/guardrails.mdx
Normal file
251
apps/docs/content/docs/en/blocks/guardrails.mdx
Normal file
@@ -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.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/guardrails.png"
|
||||
alt="Guardrails Block"
|
||||
width={500}
|
||||
height={350}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Overview
|
||||
|
||||
The Guardrails block enables you to:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Validate JSON Structure</strong>: Ensure LLM outputs are valid JSON before parsing
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Match Regex Patterns</strong>: Verify content matches specific formats (emails, phone numbers, URLs, etc.)
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Detect Hallucinations</strong>: Use RAG + LLM scoring to validate AI outputs against knowledge base content
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Detect PII</strong>: Identify and optionally mask personally identifiable information across 40+ entity types
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 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.
|
||||
|
||||
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
|
||||
<Video src="guardrails.mp4" width={500} height={350} />
|
||||
</div>
|
||||
|
||||
**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: `<agent.content>`
|
||||
- Function block results: `<function.output>`
|
||||
- API responses: `<api.output>`
|
||||
- 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:
|
||||
|
||||
- **`<guardrails.passed>`**: Boolean indicating if validation passed
|
||||
- **`<guardrails.validationType>`**: The type of validation performed
|
||||
- **`<guardrails.input>`**: The original input that was validated
|
||||
- **`<guardrails.error>`**: Error message if validation failed (optional)
|
||||
|
||||
Additional outputs by type:
|
||||
|
||||
**Hallucination Check:**
|
||||
- **`<guardrails.score>`**: Confidence score (0-10)
|
||||
- **`<guardrails.reasoning>`**: LLM's explanation
|
||||
|
||||
**PII Detection:**
|
||||
- **`<guardrails.detectedEntities>`**: Array of detected PII entities
|
||||
- **`<guardrails.maskedText>`**: Content with PII masked (if mode = "Mask")
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
### Validate JSON Before Parsing
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Ensure Agent output is valid JSON</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent generates structured JSON response</li>
|
||||
<li>Guardrails validates JSON format</li>
|
||||
<li>Condition block checks `<guardrails.passed>`</li>
|
||||
<li>If passed → Parse and use data, If failed → Retry or handle error</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Prevent Hallucinations
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Validate customer support responses</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent generates response to customer question</li>
|
||||
<li>Guardrails checks against support documentation knowledge base</li>
|
||||
<li>If confidence score ≥ 3 → Send response</li>
|
||||
<li>If confidence score \< 3 → Flag for human review</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Block PII in User Inputs
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Sanitize user-submitted content</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>User submits form with text content</li>
|
||||
<li>Guardrails detects PII (emails, phone numbers, SSN, etc.)</li>
|
||||
<li>If PII detected → Reject submission or mask sensitive data</li>
|
||||
<li>If no PII → Process normally</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
|
||||
<Video src="guardrails-example.mp4" width={500} height={350} />
|
||||
</div>
|
||||
|
||||
### Validate Email Format
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Check email address format</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent extracts email from text</li>
|
||||
<li>Guardrails validates with regex pattern</li>
|
||||
<li>If valid → Use email for notification</li>
|
||||
<li>If invalid → Request correction</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Chain with Condition blocks**: Use `<guardrails.passed>` 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 `<guardrails.error>` messages to identify common validation issues
|
||||
|
||||
<Callout type="info">
|
||||
Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical.
|
||||
</Callout>
|
||||
|
||||
BIN
apps/docs/public/static/blocks/guardrails.png
Normal file
BIN
apps/docs/public/static/blocks/guardrails.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
239
apps/sim/app/api/guardrails/validate/route.ts
Normal file
239
apps/sim/app/api/guardrails/validate/route.ts
Normal file
@@ -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',
|
||||
}
|
||||
}
|
||||
@@ -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<string, any>
|
||||
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<string, { label: string; id: string }[]> = {}
|
||||
|
||||
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 <span className='text-muted-foreground text-sm'>None selected</span>
|
||||
}
|
||||
if (allSelected) {
|
||||
return <span className='text-sm'>All selected</span>
|
||||
}
|
||||
return <span className='text-sm'>{selectedValues.length} selected</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-10 w-full justify-between border-input bg-background px-3 font-normal text-sm hover:bg-accent hover:text-accent-foreground'
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className='flex items-center gap-2 text-muted-foreground'>
|
||||
<Settings2 className='h-4 w-4' />
|
||||
<span>Configure PII Types</span>
|
||||
</span>
|
||||
<SelectedCountDisplay />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className='flex max-h-[80vh] max-w-2xl flex-col'
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select PII Types to Detect</DialogTitle>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Choose which types of personally identifiable information to detect and block.
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Header with Select All and Clear */}
|
||||
<div className='flex items-center justify-between border-b pb-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
id='select-all'
|
||||
checked={allSelected}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleSelectAll()
|
||||
} else {
|
||||
handleClear()
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<label
|
||||
htmlFor='select-all'
|
||||
className='cursor-pointer font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
>
|
||||
Select all entities
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={handleClear}
|
||||
disabled={disabled || noneSelected}
|
||||
className='w-[85px]'
|
||||
>
|
||||
<span className='flex items-center gap-1'>
|
||||
Clear{!noneSelected && <span>({selectedValues.length})</span>}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable grouped checkboxes */}
|
||||
<div
|
||||
className='flex-1 overflow-y-auto pr-4'
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
style={{ maxHeight: '60vh' }}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{Object.entries(groupedOptions).map(([groupName, groupOptions]) => (
|
||||
<div key={groupName}>
|
||||
<h3 className='mb-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider'>
|
||||
{groupName}
|
||||
</h3>
|
||||
<div className='space-y-3'>
|
||||
{groupOptions.map((option) => (
|
||||
<div key={option.id} className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
id={`${subBlockId}-${option.id}`}
|
||||
checked={selectedValues.includes(option.id)}
|
||||
onCheckedChange={() => handleToggle(option.id)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${subBlockId}-${option.id}`}
|
||||
className='cursor-pointer text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
>
|
||||
{option.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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 (
|
||||
<GroupedCheckboxList
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
title={config.title ?? ''}
|
||||
options={config.options as { label: string; id: string; group?: string }[]}
|
||||
layout={config.layout}
|
||||
isPreview={isPreview}
|
||||
subBlockValues={subBlockValues ?? {}}
|
||||
disabled={isDisabled}
|
||||
maxHeight={config.maxHeight}
|
||||
/>
|
||||
)
|
||||
case 'condition-input':
|
||||
return (
|
||||
<ConditionInput
|
||||
|
||||
374
apps/sim/blocks/blocks/guardrails.ts
Normal file
374
apps/sim/blocks/blocks/guardrails.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import { ShieldCheckIcon } from '@/components/icons'
|
||||
import { isHosted } from '@/lib/environment'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { getBaseModelProviders, getHostedModels, getProviderIcon } from '@/providers/utils'
|
||||
import { useProvidersStore } from '@/stores/providers/store'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
const getCurrentOllamaModels = () => {
|
||||
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<GuardrailsResponse> = {
|
||||
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 <blockName.output> 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 <guardrails.passed> (true/false)
|
||||
- For hallucination check, access <guardrails.score> (0-10 confidence) and <guardrails.reasoning>
|
||||
- For PII detection, access <guardrails.detectedEntities> and <guardrails.maskedText>
|
||||
- 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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<string, BlockConfig> = {
|
||||
generic_webhook: GenericWebhookBlock,
|
||||
github: GitHubBlock,
|
||||
gmail: GmailBlock,
|
||||
guardrails: GuardrailsBlock,
|
||||
google_calendar: GoogleCalendarBlock,
|
||||
google_docs: GoogleDocsBlock,
|
||||
google_drive: GoogleDriveBlock,
|
||||
|
||||
@@ -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<string, unknown> | Array<unknown>
|
||||
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, any>) => string
|
||||
grouped?: boolean
|
||||
scrollable?: boolean
|
||||
maxHeight?: number
|
||||
selectAllOption?: boolean
|
||||
condition?:
|
||||
| {
|
||||
field: string
|
||||
|
||||
@@ -2991,6 +2991,26 @@ export const OllamaIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<path d='M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z' />
|
||||
</svg>
|
||||
)
|
||||
export function ShieldCheckIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10' />
|
||||
<path d='m9 12 2 2 4-4' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function WealthboxIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -12,13 +12,13 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||
<Check className='h-4 w-4' />
|
||||
<Check className='h-3.5 w-3.5 stroke-[3]' />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
13
apps/sim/lib/guardrails/.gitignore
vendored
Normal file
13
apps/sim/lib/guardrails/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Python virtual environment
|
||||
venv/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
|
||||
# Presidio cache
|
||||
.presidio/
|
||||
|
||||
102
apps/sim/lib/guardrails/README.md
Normal file
102
apps/sim/lib/guardrails/README.md
Normal file
@@ -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 `<ENTITY_TYPE>` 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)
|
||||
|
||||
4
apps/sim/lib/guardrails/requirements.txt
Normal file
4
apps/sim/lib/guardrails/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
# Microsoft Presidio for PII detection
|
||||
presidio-analyzer>=2.2.0
|
||||
presidio-anonymizer>=2.2.0
|
||||
|
||||
37
apps/sim/lib/guardrails/setup.sh
Executable file
37
apps/sim/lib/guardrails/setup.sh
Executable file
@@ -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"
|
||||
|
||||
266
apps/sim/lib/guardrails/validate_hallucination.ts
Normal file
266
apps/sim/lib/guardrails/validate_hallucination.ts
Normal file
@@ -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<string[]> {
|
||||
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": <number between 0-10>,
|
||||
"reasoning": "<brief explanation of your score>"
|
||||
}
|
||||
|
||||
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<HallucinationValidationResult> {
|
||||
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}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
19
apps/sim/lib/guardrails/validate_json.ts
Normal file
19
apps/sim/lib/guardrails/validate_json.ts
Normal file
@@ -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}` }
|
||||
}
|
||||
}
|
||||
168
apps/sim/lib/guardrails/validate_pii.py
Normal file
168
apps/sim/lib/guardrails/validate_pii.py
Normal file
@@ -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 <ENTITY_TYPE> 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()
|
||||
|
||||
242
apps/sim/lib/guardrails/validate_pii.ts
Normal file
242
apps/sim/lib/guardrails/validate_pii.ts
Normal file
@@ -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<PIIValidationResult> {
|
||||
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<PIIValidationResult> {
|
||||
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
|
||||
21
apps/sim/lib/guardrails/validate_regex.ts
Normal file
21
apps/sim/lib/guardrails/validate_regex.ts
Normal file
@@ -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}` }
|
||||
}
|
||||
}
|
||||
2
apps/sim/tools/guardrails/index.ts
Normal file
2
apps/sim/tools/guardrails/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { GuardrailsValidateInput, GuardrailsValidateOutput } from './validate'
|
||||
export { guardrailsValidateTool } from './validate'
|
||||
183
apps/sim/tools/guardrails/validate.ts
Normal file
183
apps/sim/tools/guardrails/validate.ts
Normal file
@@ -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<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<GuardrailsValidateOutput> => {
|
||||
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
|
||||
},
|
||||
}
|
||||
@@ -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<string, ToolConfig> = {
|
||||
firecrawl_search: searchTool,
|
||||
firecrawl_crawl: crawlTool,
|
||||
google_search: googleSearchTool,
|
||||
guardrails_validate: guardrailsValidateTool,
|
||||
jina_read_url: readUrlTool,
|
||||
linkup_search: linkupSearchTool,
|
||||
resend_send: mailSendTool,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const deleteTool: ToolConfig<SupabaseDeleteParams, SupabaseDeleteResponse
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the Supabase table to delete from',
|
||||
},
|
||||
filter: {
|
||||
@@ -29,7 +29,7 @@ export const deleteTool: ToolConfig<SupabaseDeleteParams, SupabaseDeleteResponse
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
visibility: 'user-only',
|
||||
description: 'Your Supabase service role secret key',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export const getRowTool: ToolConfig<SupabaseGetRowParams, SupabaseGetRowResponse
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the Supabase table to query',
|
||||
},
|
||||
filter: {
|
||||
@@ -29,7 +29,7 @@ export const getRowTool: ToolConfig<SupabaseGetRowParams, SupabaseGetRowResponse
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
visibility: 'user-only',
|
||||
description: 'Your Supabase service role secret key',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export const insertTool: ToolConfig<SupabaseInsertParams, SupabaseInsertResponse
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the Supabase table to insert data into',
|
||||
},
|
||||
data: {
|
||||
@@ -29,7 +29,7 @@ export const insertTool: ToolConfig<SupabaseInsertParams, SupabaseInsertResponse
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
visibility: 'user-only',
|
||||
description: 'Your Supabase service role secret key',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export const queryTool: ToolConfig<SupabaseQueryParams, SupabaseQueryResponse> =
|
||||
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<SupabaseQueryParams, SupabaseQueryResponse> =
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
visibility: 'user-only',
|
||||
description: 'Your Supabase service role secret key',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export const updateTool: ToolConfig<SupabaseUpdateParams, SupabaseUpdateResponse
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the Supabase table to update',
|
||||
},
|
||||
filter: {
|
||||
@@ -35,7 +35,7 @@ export const updateTool: ToolConfig<SupabaseUpdateParams, SupabaseUpdateResponse
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
visibility: 'user-only',
|
||||
description: 'Your Supabase service role secret key',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export const upsertTool: ToolConfig<SupabaseUpsertParams, SupabaseUpsertResponse
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the Supabase table to upsert data into',
|
||||
},
|
||||
data: {
|
||||
@@ -29,7 +29,7 @@ export const upsertTool: ToolConfig<SupabaseUpsertParams, SupabaseUpsertResponse
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
visibility: 'user-only',
|
||||
description: 'Your Supabase service role secret key',
|
||||
},
|
||||
},
|
||||
|
||||
11
biome.json
11
biome.json
@@ -24,7 +24,9 @@
|
||||
"!**/public/workbox-*.js",
|
||||
"!**/public/worker-*.js",
|
||||
"!**/public/fallback-*.js",
|
||||
"!**/apps/docs/.source/**"
|
||||
"!**/apps/docs/.source/**",
|
||||
"!**/venv/**",
|
||||
"!**/.venv/**"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
@@ -58,7 +60,9 @@
|
||||
"!**/public/workbox-*.js",
|
||||
"!**/public/worker-*.js",
|
||||
"!**/public/fallback-*.js",
|
||||
"!**/apps/docs/.source/**"
|
||||
"!**/apps/docs/.source/**",
|
||||
"!**/venv/**",
|
||||
"!**/.venv/**"
|
||||
]
|
||||
},
|
||||
"assist": {
|
||||
@@ -139,7 +143,8 @@
|
||||
},
|
||||
"performance": {
|
||||
"noAccumulatingSpread": "off",
|
||||
"noDelete": "error"
|
||||
"noDelete": "error",
|
||||
"noImgElement": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user