Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ lerna-debug.log*
!.vscode/extensions.json
!.vscode/settings.json
.claude/
CLAUDE.md
.idea/
.DS_Store
*.suo
Expand Down
21 changes: 20 additions & 1 deletion docs/components/ai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,16 @@ An autonomous agent that uses reasoning steps and tool-calling to solve complex
| `temperature` | Number | Reasoning creativity (default 0.7) |
| `stepLimit` | Number | Max "Think -> Act -> Observe" loops (1-12) |
| `memorySize` | Number | Number of previous turns to retain in context |
| `structuredOutputEnabled` | Toggle | Enable to enforce a specific JSON output structure |
| `schemaType` | Select | How to define the schema: `json-example` or `json-schema` |
| `jsonExample` | JSON | Example JSON object for schema inference (all properties become required) |
| `jsonSchema` | JSON | Full JSON Schema definition for precise validation |
| `autoFixFormat` | Toggle | Attempt to extract valid JSON from malformed responses |

| Output | Type | Description |
|--------|------|-------------|
| `responseText`| Text | Final answer after reasoning is complete |
| `structuredOutput` | JSON | Parsed structured output (when enabled) |
| `conversationState` | JSON | Updated state to pass to the next agent node |
| `reasoningTrace` | JSON | Detailed step-by-step logs of the agent's thoughts |
| `agentRunId` | Text | Unique session ID for tracking and streaming |
Expand Down Expand Up @@ -168,6 +174,19 @@ Analyze incoming security alerts to filter out false positives.
An agent that searches through logs and performs lookups to investigate a specific IP address.
**Task:** "Investigate the IP {{ip}} using the available Splunk and VirusTotal tools."

### Structured Output for Data Extraction
**Flow:** `Provider` → `AI Agent` (with Structured Output enabled)

Extract structured data from unstructured security reports. Enable **Structured Output** and provide a JSON example:
```json
{
"severity": "high",
"affected_systems": ["web-server-01"],
"remediation_steps": ["Patch CVE-2024-1234", "Restart service"]
}
```
The agent will always return validated JSON matching this schema, ready for downstream processing.

---

## Best Practices
Expand All @@ -177,7 +196,7 @@ An agent that searches through logs and performs lookups to investigate a specif
</Note>

### Prompt Engineering
1. **Format Outputs**: If you need JSON for a downstream node, ask for it explicitly in the prompt: "Return only valid JSON with fields 'risk' and 'reason'."
1. **Use Structured Output**: When you need consistent JSON for downstream nodes, enable **Structured Output** instead of relying on prompt instructions. This guarantees schema compliance and eliminates parsing errors.
2. **Use System Prompts**: Set high-level rules (e.g., "You are a senior security researcher") in the System Prompt parameter instead of the User Input.
3. **Variable Injection**: Use `{{variableName}}` syntax to inject data from upstream nodes into your prompts.

Expand Down
27 changes: 13 additions & 14 deletions frontend/src/components/workflow/ConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -987,22 +987,19 @@ export function ConfigPanel({
defaultOpen={true}
>
<div className="space-y-0 mt-2">
{/* Sort parameters: select types first, then others */}
{componentParameters
.slice()
.sort((a, b) => {
// Select parameters go first
const aIsSelect = a.type === 'select'
const bIsSelect = b.type === 'select'
if (aIsSelect && !bIsSelect) return -1
if (!aIsSelect && bIsSelect) return 1
return 0
})
.map((param, index) => (
{/* Render parameters in component definition order to preserve hierarchy */}
{componentParameters.map((param, index) => {
// Only show border between top-level parameters (not nested ones)
const isTopLevel = !param.visibleWhen
const prevParam = index > 0 ? componentParameters[index - 1] : null
const prevIsTopLevel = prevParam ? !prevParam.visibleWhen : false
const showBorder = index > 0 && isTopLevel && prevIsTopLevel

return (
<div
key={param.id}
className={cn(
index > 0 && "border-t border-border pt-3"
showBorder && "border-t border-border pt-3"
)}
>
<ParameterFieldWrapper
Expand All @@ -1013,9 +1010,11 @@ export function ConfigPanel({
componentId={component.id}
parameters={nodeData.parameters}
onUpdateParameter={handleParameterChange}
allComponentParameters={componentParameters}
/>
</div>
))}
)
})}
</div>
</CollapsibleSection>
)}
Expand Down
146 changes: 125 additions & 21 deletions frontend/src/components/workflow/ParameterField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Switch } from '@/components/ui/switch'
import { Button } from '@/components/ui/button'
import { useNavigate } from 'react-router-dom'
import { RuntimeInputsEditor } from './RuntimeInputsEditor'
Expand Down Expand Up @@ -399,18 +400,18 @@ export function ParameterField({

case 'boolean':
return (
<div className="flex items-center gap-2">
<Checkbox
id={parameter.id}
checked={currentValue || false}
onCheckedChange={(checked) => onChange(checked)}
/>
<div className="flex items-center justify-between">
<label
htmlFor={parameter.id}
className="text-sm text-muted-foreground cursor-pointer select-none"
className="text-sm font-medium cursor-pointer select-none"
>
{currentValue ? 'Enabled' : 'Disabled'}
{parameter.label}
</label>
<Switch
id={parameter.id}
checked={currentValue || false}
onCheckedChange={(checked) => onChange(checked)}
/>
</div>
)

Expand Down Expand Up @@ -695,13 +696,17 @@ export function ParameterField({
if (!jsonTextareaRef.current) return

let textValue = ''
let needsNormalization = false

if (value === undefined || value === null || value === '') {
textValue = ''
} else if (typeof value === 'string') {
textValue = value
} else {
// If Value is an object - normalize to string
try {
textValue = JSON.stringify(value, null, 2)
needsNormalization = true
} catch (error) {
console.error('Failed to serialize JSON parameter value', error)
return
Expand All @@ -714,7 +719,12 @@ export function ParameterField({
setJsonError(null)
isExternalJsonUpdateRef.current = false
}
}, [value])

// Normalize object values to string
if (needsNormalization) {
onChange(textValue)
}
}, [value, onChange])

// Sync to parent only on blur for native undo behavior
const handleJsonBlur = useCallback(() => {
Expand All @@ -728,9 +738,9 @@ export function ParameterField({
}

try {
const parsed = JSON.parse(nextValue)
JSON.parse(nextValue) // Validate JSON syntax
setJsonError(null)
onChange(parsed)
onChange(nextValue) // Pass string, not parsed object
} catch (error) {
setJsonError('Invalid JSON')
// Keep showing error, don't update parent
Expand Down Expand Up @@ -1010,6 +1020,52 @@ interface ParameterFieldWrapperProps {
componentId?: string
parameters?: Record<string, unknown> | undefined
onUpdateParameter?: (paramId: string, value: any) => void
allComponentParameters?: Parameter[]
}

/**
* Checks if a parameter should be visible based on its visibleWhen conditions.
* Returns true if all conditions are met or if no conditions exist.
*/
function shouldShowParameter(
parameter: Parameter,
allParameters: Record<string, unknown> | undefined
): boolean {
// If no visibleWhen conditions, always show
if (!parameter.visibleWhen) {
return true
}

// If we have conditions but no parameter values to check against, hide by default
if (!allParameters) {
return false
}

// Check all conditions in visibleWhen object
for (const [key, expectedValue] of Object.entries(parameter.visibleWhen)) {
const actualValue = allParameters[key]
if (actualValue !== expectedValue) {
return false
}
}

return true
}

/**
* Checks if a boolean parameter acts as a header toggle (controls visibility of other params).
* Returns true if other parameters have visibleWhen conditions referencing this parameter.
*/
function isHeaderToggleParameter(
parameter: Parameter,
allComponentParameters: Parameter[] | undefined
): boolean {
if (parameter.type !== 'boolean' || !allComponentParameters) return false

// Check if any other parameter has visibleWhen referencing this param
return allComponentParameters.some(
(p) => p.visibleWhen && parameter.id in p.visibleWhen
)
}

/**
Expand All @@ -1023,7 +1079,13 @@ export function ParameterFieldWrapper({
componentId,
parameters,
onUpdateParameter,
allComponentParameters,
}: ParameterFieldWrapperProps) {
// Check visibility conditions
if (!shouldShowParameter(parameter, parameters)) {
return null
}

// Special case: Runtime Inputs Editor for Entry Point
if (parameter.id === 'runtimeInputs') {
return (
Expand Down Expand Up @@ -1137,19 +1199,54 @@ export function ParameterFieldWrapper({
)
}

// Standard parameter field rendering
return (
<div className="space-y-2">
<div className="flex items-center justify-between mb-1">
<label className="text-sm font-medium" htmlFor={parameter.id}>
{parameter.label}
</label>
{parameter.required && (
<span className="text-xs text-red-500">*required</span>
// Check if this is a nested/conditional parameter (has visibleWhen)
const isNestedParameter = Boolean(parameter.visibleWhen)

// Check if this is a header toggle (boolean that controls other params' visibility)
const isHeaderToggle = isHeaderToggleParameter(parameter, allComponentParameters)

// Header toggle rendering
if (isHeaderToggle) {
return (
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium" htmlFor={parameter.id}>
{parameter.label}
</label>
<Switch
id={parameter.id}
checked={value || false}
onCheckedChange={(checked) => onChange(checked)}
/>
</div>
{parameter.description && (
<p className="text-xs text-muted-foreground">
{parameter.description}
</p>
)}
</div>
)
}

// Standard parameter field rendering
const isBooleanParameter = parameter.type === 'boolean'

return (
<div className={`space-y-2 ${isNestedParameter ? 'ml-2 px-3 py-2.5 mt-1 bg-muted/80 rounded-lg' : ''}`}>
{/* Label and required indicator - skip for boolean (label is inside) */}
{!isBooleanParameter && (
<div className="flex items-center justify-between mb-1">
<label className={`${isNestedParameter ? 'text-xs' : 'text-sm'} font-medium`} htmlFor={parameter.id}>
{parameter.label}
</label>
{parameter.required && (
<span className="text-xs text-red-500">*required</span>
)}
</div>
)}

{parameter.description && (
{/* Description before the input field - for non-boolean parameters */}
{!isBooleanParameter && parameter.description && (
<p className="text-xs text-muted-foreground mb-2">
{parameter.description}
</p>
Expand All @@ -1165,6 +1262,13 @@ export function ParameterFieldWrapper({
onUpdateParameter={onUpdateParameter}
/>

{/* Description after field (toggle control) - for boolean parameters */}
{isBooleanParameter && parameter.description && (
<p className="text-xs text-muted-foreground">
{parameter.description}
</p>
)}

{parameter.helpText && (
<p className="text-xs text-muted-foreground italic mt-2">
💡 {parameter.helpText}
Expand Down
Loading