feat(aichat): give advanced options tools to flow mode (#6463)

* add tool to set for loop options

* add tool to set skip and early break

* draft ui intents

* fix option tool

* clean ui intents

* add other ui intents + fixes

* cleaning

* fix

* hide diff mode on apply

* clean

* fix

* fix

* fix typo

* add precision for js expressions
This commit is contained in:
centdix
2025-08-26 16:08:29 +02:00
committed by GitHub
parent a58da796bf
commit b54fb32e4b
10 changed files with 299 additions and 5 deletions
@@ -29,9 +29,7 @@
try {
const parsed = JSON.parse(obj[key])
obj[key] = parsed
} catch (e) {
console.error('Failed to parse JSON:', e)
}
} catch {}
}
return JSON.stringify(obj, null, 2)
} catch {
@@ -98,6 +98,14 @@
if (affectedModule.action === 'removed') {
deleteStep(id)
}
// Hide diff editor if the module is a rawscript
if (
affectedModule.action === 'modified' &&
$currentEditor?.type === 'script' &&
$currentEditor.stepId === id
) {
$currentEditor.hideDiffMode()
}
}
affectedModules = {}
},
@@ -185,7 +193,8 @@
$currentEditor.hideDiffMode()
}
newModule.value = oldModule.value
Object.keys(newModule).forEach((k) => delete newModule[k])
Object.assign(newModule, $state.snapshot(oldModule))
}
refreshStateStore(flowStore)
@@ -486,6 +495,75 @@
refreshStateStore(flowStore)
}
setModuleStatus(id, 'modified')
},
setForLoopOptions: async (id, opts) => {
const module = getModule(id)
if (!module) {
throw new Error('Module not found')
}
if (module.value.type !== 'forloopflow') {
throw new Error('Module is not a forloopflow')
}
// Apply skip_failures if provided
if (typeof opts.skip_failures === 'boolean') {
module.value.skip_failures = opts.skip_failures
}
// Apply parallel if provided
if (typeof opts.parallel === 'boolean') {
module.value.parallel = opts.parallel
}
// Handle parallelism
if (opts.parallel === false) {
// If parallel is disabled, clear parallelism
module.value.parallelism = undefined
} else if (opts.parallelism !== undefined) {
if (opts.parallelism === null) {
// Explicitly clear parallelism
module.value.parallelism = undefined
} else if (module.value.parallel || opts.parallel === true) {
// Only set parallelism if parallel is enabled
const n = Math.max(1, Math.floor(Math.abs(opts.parallelism)))
module.value.parallelism = n
}
}
refreshStateStore(flowStore)
setModuleStatus(id, 'modified')
},
setModuleControlOptions: async (id, opts) => {
const module = getModule(id)
if (!module) {
throw new Error('Module not found')
}
// Handle stop_after_if
if (typeof opts.stop_after_if === 'boolean') {
if (opts.stop_after_if === false) {
module.stop_after_if = undefined
} else {
module.stop_after_if = {
expr: opts.stop_after_if_expr ?? '',
skip_if_stopped: opts.stop_after_if
}
}
}
// Handle skip_if
if (typeof opts.skip_if === 'boolean') {
if (opts.skip_if === false) {
module.skip_if = undefined
} else {
module.skip_if = {
expr: opts.skip_if_expr ?? ''
}
}
}
refreshStateStore(flowStore)
setModuleStatus(id, 'modified')
}
}
@@ -1,4 +1,5 @@
import { ScriptService, type FlowModule, type RawScript, type Script, JobService } from '$lib/gen'
import { emitUiIntent } from './uiIntents'
import type {
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam
@@ -57,6 +58,23 @@ export interface FlowAIChatHelpers {
addBranch: (id: string) => Promise<void>
removeBranch: (id: string, branchIndex: number) => Promise<void>
setForLoopIteratorExpression: (id: string, expression: string) => Promise<void>
setForLoopOptions: (
id: string,
opts: {
skip_failures?: boolean | null
parallel?: boolean | null
parallelism?: number | null
}
) => Promise<void>
setModuleControlOptions: (
id: string,
opts: {
stop_after_if?: boolean | null
stop_after_if_expr?: string | null
skip_if?: boolean | null
skip_if_expr?: string | null
}
) => Promise<void>
setCode: (id: string, code: string) => Promise<void>
}
@@ -196,6 +214,67 @@ const setForLoopIteratorExpressionToolDef = createToolDef(
'Set the iterator JavaScript expression for the given forloop step'
)
const setForLoopOptionsSchema = z.object({
id: z.string().describe('The id of the forloop step to configure'),
skip_failures: z
.boolean()
.nullable()
.optional()
.describe('Whether to skip failures in the loop (null to not change)'),
parallel: z
.boolean()
.nullable()
.optional()
.describe('Whether to run iterations in parallel (null to not change)'),
parallelism: z
.number()
.int()
.min(1)
.nullable()
.optional()
.describe('Maximum number of parallel iterations (null to not change)')
})
const setForLoopOptionsToolDef = createToolDef(
setForLoopOptionsSchema,
'set_forloop_options',
'Set advanced options for a forloop step: skip_failures, parallel, and parallelism'
)
const setModuleControlOptionsSchema = z.object({
id: z.string().describe('The id of the module to configure'),
stop_after_if: z
.boolean()
.nullable()
.optional()
.describe('Early stop condition (true to set, false to clear, null to not change)'),
stop_after_if_expr: z
.string()
.nullable()
.optional()
.describe(
'JavaScript expression for early stop condition. Can use `flow_input` or `result`. `result` is the result of the step. `results.<step_id>` is not supported, do not use it. Only used if stop_after_if is true. Example: `flow_input.x > 10` or `result === "failure"`'
),
skip_if: z
.boolean()
.nullable()
.optional()
.describe('Skip condition (true to set, false to clear, null to not change)'),
skip_if_expr: z
.string()
.nullable()
.optional()
.describe(
'JavaScript expression for skip condition. Can use `flow_input` or `results.<step_id>`. Only used if skip_if is true. Example: `flow_input.x > 10` or `results.a === "failure"`'
)
})
const setModuleControlOptionsToolDef = createToolDef(
setModuleControlOptionsSchema,
'set_module_control_options',
'Set control options for any module: stop_after_if (early stop) and skip_if (conditional skip)'
)
const setBranchPredicateSchema = z.object({
id: z.string().describe('The id of the branchone step to set the predicates for'),
branchIndex: z
@@ -558,6 +637,68 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
return `Forloop '${parsedArgs.id}' iterator expression set`
}
},
{
def: setForLoopOptionsToolDef,
fn: async ({ args, helpers, toolId, toolCallbacks }) => {
const parsedArgs = setForLoopOptionsSchema.parse(args)
await helpers.setForLoopOptions(parsedArgs.id, {
skip_failures: parsedArgs.skip_failures,
parallel: parsedArgs.parallel,
parallelism: parsedArgs.parallelism
})
helpers.selectStep(parsedArgs.id)
const message = `Set forloop '${parsedArgs.id}' options`
toolCallbacks.setToolStatus(toolId, {
content: message
})
return `${message}: ${JSON.stringify(parsedArgs)}`
}
},
{
def: setModuleControlOptionsToolDef,
fn: async ({ args, helpers, toolId, toolCallbacks }) => {
const parsedArgs = setModuleControlOptionsSchema.parse(args)
await helpers.setModuleControlOptions(parsedArgs.id, {
stop_after_if: parsedArgs.stop_after_if,
stop_after_if_expr: parsedArgs.stop_after_if_expr,
skip_if: parsedArgs.skip_if,
skip_if_expr: parsedArgs.skip_if_expr
})
helpers.selectStep(parsedArgs.id)
// Emit UI intent to show early-stop tab when stop_after_if is configured
const modules = helpers.getModules()
const module = findModuleById(modules, parsedArgs.id)
if (!module) {
throw new Error(`Module with id '${parsedArgs.id}' not found in flow.`)
}
const moduleType = module?.value.type
const hasSpecificComponents = ['forloopflow', 'whileloopflow', 'branchall', 'branchone']
const prefix = hasSpecificComponents.includes(moduleType) ? `${moduleType}` : 'flow'
if (typeof parsedArgs.stop_after_if === 'boolean') {
emitUiIntent({
kind: 'open_module_tab',
componentId: `${prefix}-${parsedArgs.id}`,
tab: 'early-stop'
})
}
if (typeof parsedArgs.skip_if === 'boolean') {
emitUiIntent({
kind: 'open_module_tab',
componentId: `${prefix}-${parsedArgs.id}`,
tab: 'skip'
})
}
const message = `Set module '${parsedArgs.id}' control options`
toolCallbacks.setToolStatus(toolId, {
content: message
})
return `${message}: ${JSON.stringify(parsedArgs)}`
}
},
{
def: resourceTypeToolDef,
fn: async ({ args, toolId, workspace, toolCallbacks }) => {
@@ -790,10 +931,17 @@ When creating new steps, follow this process for EACH step:
### Special Step Types
For special step types, follow these additional steps:
- For forloop steps: Set the iterator expression using set_forloop_iterator_expression
- For forloop steps:
- Set the iterator expression using set_forloop_iterator_expression
- Set advanced options (parallel, parallelism, skip_failures) using set_forloop_options
- For branchone steps: Set the predicates for each branch using set_branch_predicate
- For branchall steps: No additional setup needed
### Module Control Options
For any module type, you can set control flow options using set_module_control_options:
- **stop_after_if**: Early stop condition - stops the module if expression evaluates to true. Can use "flow_input" or "result". "result" is the result of the step. "results.<step_id>" is not supported, do not use it. Example: "flow_input.x > 10" or "result === "failure""
- **skip_if**: Skip condition - skips the module entirely if expression evaluates to true. Can use "flow_input" or "results.<step_id>". Example: "flow_input.x > 10" or "results.a === "failure""
### Step Insertion Rules
When adding steps, carefully consider the execution order:
1. Steps are executed in the order they appear in the flow definition, not in the order they were added
@@ -814,6 +962,7 @@ When adding steps, carefully consider the execution order:
### JavaScript Expressions
For step inputs, forloop iterator expressions and branch predicates, use JavaScript expressions with these variables:
- Step results: results.stepid or results.stepid.property_name
- Break condition (stop_after_if) in for loops: result (contains the result of the last iteration)
- Loop iterator: flow_input.iter.value (inside loops)
- Flow inputs: flow_input.property_name
- Static values: Use JavaScript syntax (e.g., "hello", true, 3)
@@ -822,6 +971,12 @@ Note: These variables are only accessible in step inputs, forloop iterator expre
For truly static values in step inputs (those not linked to previous steps or loop iterations), prefer using flow inputs by default unless explicitly specified otherwise. This makes the flow more configurable and reusable. For example, instead of hardcoding an email address in a step input, create a flow input for it.
### For Loop Advanced Options
When configuring for-loop steps, consider these options:
- **parallel: true** - Run iterations in parallel for independent operations (significantly faster for I/O bound tasks)
- **parallelism: N** - Limit concurrent iterations (only applies when parallel=true). Use to prevent overwhelming external APIs
- **skip_failures: true** - Continue processing remaining iterations even if some fail. Failed iterations return error objects as results
### Special Modules
- Preprocessor: Runs before the first step when triggered externally
- ID: 'preprocessor'
@@ -0,0 +1,9 @@
import { writable, type Writable } from 'svelte/store'
export type UiIntent = { kind: 'open_module_tab'; componentId: string; tab: string }
export const uiIntentStore: Writable<UiIntent | null> = writable(null)
export function emitUiIntent(intent: UiIntent) {
uiIntentStore.set(intent)
}
@@ -0,0 +1,17 @@
import { uiIntentStore, type UiIntent } from './uiIntents'
import { onDestroy } from 'svelte'
type Handlers = {
openTab?: (tab: string) => void
}
export function useUiIntent(componentId: string, handlers: Handlers) {
const unsub = uiIntentStore.subscribe((intent: UiIntent | null) => {
if (!intent || intent.componentId !== componentId) return
if (intent.kind === 'open_module_tab') handlers.openTab?.(intent.tab)
uiIntentStore.set(null)
})
onDestroy(() => unsub())
}
@@ -15,6 +15,7 @@
import { enterpriseLicense } from '$lib/stores'
import FlowModuleSkip from './FlowModuleSkip.svelte'
import TabsV2 from '$lib/components/common/tabs/TabsV2.svelte'
import { useUiIntent } from '$lib/components/copilot/chat/flow/useUiIntent'
interface Props {
noEditor: boolean
@@ -31,6 +32,12 @@
})
let selected = $state('early-stop')
useUiIntent(`branchall-${flowModule.id}`, {
openTab: (tab) => {
selected = tab
}
})
</script>
<div class="h-full flex flex-col w-full" id="flow-editor-branch-all-wrapper">
@@ -15,6 +15,7 @@
import FlowModuleMock from './FlowModuleMock.svelte'
import { enterpriseLicense } from '$lib/stores'
import FlowModuleSkip from './FlowModuleSkip.svelte'
import { useUiIntent } from '$lib/components/copilot/chat/flow/useUiIntent'
interface Props {
// import FlowRetries from './FlowRetries.svelte'
@@ -39,6 +40,12 @@
})
let selected = $state('early-stop')
useUiIntent(`branchone-${flowModule.id}`, {
openTab: (tab) => {
selected = tab
}
})
</script>
<div class="h-full" id="flow-editor-branch-one-wrapper">
@@ -27,6 +27,7 @@
import PropPickerWrapper, { CONNECT } from '../propPicker/PropPickerWrapper.svelte'
import type { PropPickerContext } from '$lib/components/prop_picker'
import TabsV2 from '$lib/components/common/tabs/TabsV2.svelte'
import { useUiIntent } from '$lib/components/copilot/chat/flow/useUiIntent'
const { previewArgs, flowStateStore, flowStore, currentEditor } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -50,6 +51,13 @@
let editor: SimpleEditor | undefined = $state(undefined)
let selected: string = $state('early-stop')
// UI Intent handling for AI tool control
useUiIntent(`forloopflow-${mod.id}`, {
openTab: (tab) => {
selected = tab
}
})
const { flowPropPickerConfig } = getContext<PropPickerContext>('PropPickerContext')
flowPropPickerConfig.set(undefined)
@@ -54,6 +54,7 @@
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
import AssetsDropdownButton from '$lib/components/assets/AssetsDropdownButton.svelte'
import { useUiIntent } from '$lib/components/copilot/chat/flow/useUiIntent'
const {
selectedId,
@@ -135,6 +136,13 @@
let assets = $derived((flowModule.value.type === 'rawscript' && flowModule.value.assets) || [])
// UI Intent handling for AI tool control
useUiIntent(`flow-${flowModule.id}`, {
openTab: (tab) => {
selectAdvanced(tab)
}
})
function onModulesChange(savedModule: FlowModule | undefined, flowModule: FlowModule) {
// console.log('onModulesChange', savedModule, flowModule)
return savedModule?.value?.type === 'rawscript' &&
@@ -19,6 +19,7 @@
import FlowModuleDeleteAfterUse from './FlowModuleDeleteAfterUse.svelte'
import FlowModuleSkip from './FlowModuleSkip.svelte'
import TabsV2 from '$lib/components/common/tabs/TabsV2.svelte'
import { useUiIntent } from '$lib/components/copilot/chat/flow/useUiIntent'
const { flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -38,6 +39,12 @@
let job: Job | undefined = $state(undefined)
let previewIterationArgs = $derived(flowStateStore.val[mod.id]?.previewArgs ?? {})
useUiIntent(`whileloopflow-${mod.id}`, {
openTab: (tab) => {
selected = tab
}
})
</script>
<Drawer bind:open={previewOpen} alwaysOpen size="75%">