Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -868,22 +868,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 @@ -894,9 +891,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 @@ -395,18 +396,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 @@ -691,13 +692,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 @@ -710,7 +715,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 @@ -724,9 +734,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 @@ -1006,6 +1016,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 @@ -1019,7 +1075,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 All @@ -1041,19 +1103,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 @@ -1069,6 +1166,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
3 changes: 2 additions & 1 deletion frontend/src/components/workflow/WorkflowNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1208,8 +1208,9 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {
{/* Parameters Display (Required + Select types) */}
{(() => {
// Show required parameters and important select parameters (like mode)
// Exclude nested parameters (those with visibleWhen) like schemaType
const selectParams = componentParameters.filter(
param => param.type === 'select' && !param.required
param => param.type === 'select' && !param.required && !param.visibleWhen
)
const paramsToShow = [...requiredParams, ...selectParams]

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/schemas/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export const ParameterSchema = z.object({
placeholder: z.string().optional(),
description: z.string().optional(),
helpText: z.string().optional(),
/** Conditional visibility: parameter is shown only when all conditions are met */
visibleWhen: z.record(z.string(), z.any()).optional(),
})

export type Parameter = z.infer<typeof ParameterSchema>
Expand Down
2 changes: 2 additions & 0 deletions packages/component-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ export interface ComponentParameterMetadata {
min?: number;
max?: number;
rows?: number;
/** Conditional visibility: parameter is shown only when all conditions are met */
visibleWhen?: Record<string, unknown>;
}

export type ComponentAuthorType = 'shipsecai' | 'community';
Expand Down
Loading