fix: flow step missing input warnings (#5916)

* fix: flow step missing input warnings

* nit
This commit is contained in:
HugoCasa
2025-06-12 21:38:26 +02:00
committed by GitHub
parent cf2d09e7a8
commit f077849b8f
9 changed files with 170 additions and 274 deletions
@@ -3,7 +3,7 @@
import { VariableService, type InputTransform } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { createEventDispatcher, untrack } from 'svelte'
import { untrack } from 'svelte'
import { Button } from './common'
import StepInputsGen from './copilot/StepInputsGen.svelte'
import type { PickableProperties } from './flows/previousResults'
@@ -40,8 +40,6 @@
let inputCheck: { [id: string]: boolean } = $state({})
const dispatch = createEventDispatcher()
$effect(() => {
isValid = allTrue(inputCheck) ?? false
})
@@ -117,10 +115,6 @@
{noDynamicToggle}
{pickableProperties}
{enableAi}
on:change={(e) => {
const { argName } = e.detail
dispatch('changeArg', { argName })
}}
/>
</div>
{/if}
@@ -22,7 +22,7 @@
flowModuleSchemaMap: FlowModuleSchemaMap | undefined
} = $props()
const { flowStore, flowStateStore, selectedId, currentEditor, flowInputsStore } =
const { flowStore, flowStateStore, selectedId, currentEditor } =
getContext<FlowEditorContext>('FlowEditorContext')
const { exprsToSet } = getContext<FlowCopilotContext | undefined>('FlowCopilotContext') ?? {}
@@ -185,10 +185,6 @@
flowModuleSchemaMap?.removeAtId(modules, id)
}
if ($flowInputsStore) {
delete $flowInputsStore[id]
}
refreshStateStore(flowStore)
flowModuleSchemaMap?.updateFlowInputsStore()
+2 -2
View File
@@ -73,9 +73,9 @@ function getModelSpecificConfig(
}
: {
model: modelProvider.model,
temperature: 0,
...(tools && tools.length > 0 ? { tools } : {})
temperature: 0
}),
...(tools && tools.length > 0 ? { tools } : {}),
max_completion_tokens: getModelMaxTokens(modelProvider.model)
}
}
@@ -1,5 +1,5 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
import FlowSettings from './FlowSettings.svelte'
@@ -7,24 +7,35 @@
import FlowFailureModule from './FlowFailureModule.svelte'
import FlowConstants from './FlowConstants.svelte'
import type { FlowModule, Flow } from '$lib/gen'
import { initFlowStepWarnings } from '../utils'
import { dfs } from '../dfs'
import FlowPreprocessorModule from './FlowPreprocessorModule.svelte'
import type { TriggerContext } from '$lib/components/triggers'
import { insertNewPreprocessorModule } from '../flowStateUtils.svelte'
import TriggersEditor from '../../triggers/TriggersEditor.svelte'
import { handleSelectTriggerFromKind, type Trigger } from '$lib/components/triggers/utils'
import { computeMissingInputWarnings } from '../missingInputWarnings'
interface Props {
noEditor?: boolean
enableAi?: boolean
newFlow?: boolean
disabledFlowInputs?: boolean
savedFlow?:
| (Flow & {
draft?: Flow | undefined
})
| undefined
onDeployTrigger?: (trigger: Trigger) => void
}
let {
noEditor = false,
enableAi = false,
newFlow = false,
disabledFlowInputs = false,
savedFlow = undefined,
onDeployTrigger = () => {}
}: Props = $props()
export let noEditor = false
export let enableAi = false
export let newFlow = false
export let disabledFlowInputs = false
export let savedFlow:
| (Flow & {
draft?: Flow | undefined
})
| undefined = undefined
export let onDeployTrigger: (trigger: Trigger) => void = () => {}
const {
selectedId,
flowStore,
@@ -50,28 +61,8 @@
}
}
async function initWarnings() {
for (const module of flowStore.val?.value?.modules) {
if (!module) {
continue
}
if (!$flowInputsStore) {
$flowInputsStore = {}
}
$flowInputsStore[module?.id] = {
flowStepWarnings: await initFlowStepWarnings(
module.value,
$flowStateStore?.[module?.id]?.schema,
dfs(flowStore.val.value.modules, (fm) => fm.id)
)
}
}
}
onMount(() => {
initWarnings()
$effect(() => {
computeMissingInputWarnings(flowStore, $flowStateStore, flowInputsStore)
})
</script>
@@ -46,9 +46,6 @@
import { enterpriseLicense } from '$lib/stores'
import { isCloudHosted } from '$lib/cloud'
import { loadSchemaFromModule } from '../flowInfers'
import { computeFlowStepWarning, initFlowStepWarnings } from '../utils'
import { debounce } from '$lib/utils'
import { dfs } from '../dfs'
import FlowModuleSkip from './FlowModuleSkip.svelte'
import { type Job, JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -65,7 +62,6 @@
flowStore,
pathStore,
saveDraft,
flowInputsStore,
customUi,
executionCount
} = getContext<FlowEditorContext>('FlowEditorContext')
@@ -147,15 +143,6 @@
if (inputTransformSchemaForm) {
inputTransformSchemaForm.setArgs(input_transforms)
if (!deepEqual(schema, $flowStateStore[flowModule.id]?.schema)) {
$flowInputsStore[flowModule?.id] = {
flowStepWarnings: await initFlowStepWarnings(
flowModule.value,
schema ?? {},
dfs(flowStore.val.value.modules, (fm) => fm.id)
)
}
}
} else {
if (
flowModule.value.type == 'rawscript' ||
@@ -202,27 +189,6 @@
}
}
let debouncedWarning = debounce((argName: string) => {
if ($flowInputsStore) {
computeFlowStepWarning(
argName,
flowModule.value,
$flowInputsStore[flowModule.id].flowStepWarnings ?? {},
$flowStateStore[$selectedId]?.schema,
dfs(flowStore.val?.value?.modules ?? [], (fm) => fm.id) ?? []
).then((flowStepWarnings) => {
$flowInputsStore[flowModule.id].flowStepWarnings = flowStepWarnings
})
}
}, 100)
function setFlowInput(argName: string) {
if ($flowInputsStore && flowModule.id && $flowInputsStore?.[flowModule.id] === undefined) {
$flowInputsStore[flowModule.id] = {}
}
debouncedWarning(argName)
}
async function getLastJob() {
if (
!$flowStateStore ||
@@ -545,10 +511,6 @@
}
extraLib={stepPropPicker.extraLib}
{enableAi}
on:changeArg={(e) => {
const { argName } = e.detail
setFlowInput(argName)
}}
/>
</PropPickerWrapper>
</div>
@@ -19,13 +19,10 @@
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
import FlowWhileLoop from './FlowWhileLoop.svelte'
import { initFlowStepWarnings } from '../utils'
import { dfs } from '../dfs'
import type { TriggerContext } from '$lib/components/triggers'
import { formatCron } from '$lib/utils'
const { selectedId, flowStateStore, flowInputsStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
@@ -106,16 +103,6 @@
flowModule = module
$flowStateStore[module.id] = state
if ($flowInputsStore) {
$flowInputsStore[module?.id] = {
flowStepWarnings: await initFlowStepWarnings(
module?.value,
$flowStateStore[module?.id]?.schema,
dfs(flowStore.val.value.modules, (fm) => fm.id)
)
}
}
}
</script>
@@ -192,16 +179,6 @@
flowModule = module
$flowStateStore[module.id] = state
if ($flowInputsStore) {
$flowInputsStore[module.id] = {
flowStepWarnings: await initFlowStepWarnings(
module.value,
$flowStateStore[module.id].schema,
dfs(flowStore.val.value.modules, (fm) => fm.id)
)
}
}
}}
failureModule={$selectedId === 'failure'}
preprocessorModule={$selectedId === 'preprocessor'}
@@ -14,7 +14,7 @@
insertNewPreprocessorModule
} from '$lib/components/flows/flowStateUtils.svelte'
import type { FlowModule, ScriptLang } from '$lib/gen'
import { emptyFlowModuleState, initFlowStepWarnings } from '../utils'
import { emptyFlowModuleState } from '../utils'
import FlowSettingsItem from './FlowSettingsItem.svelte'
import FlowConstantsItem from './FlowConstantsItem.svelte'
@@ -242,30 +242,6 @@
change: void
}>()
export async function updateFlowInputsStore() {
const keys = Object.keys(dependents ?? {})
for (const key of keys) {
const module = flowStore.val.value.modules.find((m) => m.id === key)
if (!module) {
continue
}
if (!$flowInputsStore) {
$flowInputsStore = {}
}
$flowInputsStore[module.id] = {
flowStepWarnings: await initFlowStepWarnings(
module.value,
$flowStateStore?.[module.id]?.schema,
dfs(flowStore.val.value.modules, (fm) => fm.id)
)
}
}
}
export function setExpr(module: FlowModule, expr: string) {
if (module.value.type == 'forloopflow') {
module.value.iterator = { type: 'javascript', expr }
@@ -377,13 +353,8 @@
} else {
selectNextId(id)
removeAtId(flowStore.val.value.modules, id)
if ($flowInputsStore) {
delete $flowInputsStore[id]
}
}
refreshStateStore(flowStore)
updateFlowInputsStore()
}
if (Object.keys(dependents).length > 0) {
@@ -0,0 +1,137 @@
import type { Schema } from '$lib/common'
import type { InputTransform, OpenFlow } from '$lib/gen'
import { parseOutputs } from '$lib/infer'
import type { Writable } from 'svelte/store'
import type { FlowInput } from './types'
import type { StateStore } from '$lib/utils'
import type { FlowState } from './flowState'
import { dfs } from './dfs'
function isInputFilled(
inputTransforms: Record<string, InputTransform>,
key: string,
schema: Schema | undefined
): boolean {
const required = schema?.required?.includes(key) ?? false
if (!required) {
return true
}
if (inputTransforms.hasOwnProperty(key)) {
const transform = inputTransforms[key]
if (
transform?.type === 'static' &&
(transform?.value === undefined || transform?.value === '' || transform?.value === null)
) {
return false
} else if (
transform?.type === 'javascript' &&
(transform?.expr === undefined || transform?.expr === '' || transform?.expr === null)
) {
return false
}
}
return true
}
async function isConnectedToMissingModule(
argName: string,
input_transform: InputTransform,
moduleIds: string[]
): Promise<string | undefined> {
const val: string =
input_transform.type === 'static' ? String(input_transform.value) : input_transform.expr
try {
const outputs = await parseOutputs(val, true)
let error: string = ''
outputs?.forEach(([componentId, id]) => {
if (componentId === 'results') {
if (!moduleIds.includes(id)) {
error += `Input ${argName} is connected to a missing module with id ${id}\n`
}
}
})
return error
} catch (e) {
return `Input ${argName} expression is invalid`
}
}
type FlowStepWarnings = Record<string, { message: string; type: 'error' | 'warning' }>
async function computeFlowStepWarnings(
input: {
input_transforms: Record<string, InputTransform>
id: string
schema: Schema | undefined
},
moduleIds: string[] = []
) {
const messages: FlowStepWarnings = {}
const { input_transforms, schema } = input
const keys = Object.keys(input_transforms ?? {})
const promises = keys.map(async (key) => {
if (!isInputFilled(input_transforms, key, schema)) {
messages[key] = {
message: `Input ${key} is required but not filled`,
type: 'warning'
}
} else {
const errorMessage = await isConnectedToMissingModule(key, input_transforms[key], moduleIds)
if (errorMessage) {
messages[key] = {
message: errorMessage,
type: 'error'
}
}
}
})
await Promise.all(promises)
return messages
}
export async function computeMissingInputWarnings(
flowStore: StateStore<OpenFlow>,
flowState: FlowState,
flowInputsStore: Writable<FlowInput>
) {
const inputs = dfs(flowStore.val.value.modules, (module) => {
if (
module.value.type === 'script' ||
module.value.type === 'rawscript' ||
module.value.type === 'flow'
) {
const schema = flowState[module.id]?.schema
return {
input_transforms: module.value.input_transforms,
id: module.id,
schema
}
}
return undefined
}).filter((x) => x !== undefined)
const moduleIds = dfs(flowStore.val.value.modules, (module) => module.id)
const promises = inputs.map(async (input) => {
const warnings = await computeFlowStepWarnings(input, moduleIds)
return [input.id, warnings] as const
})
const warnings = Object.fromEntries(await Promise.all(promises))
for (const key in warnings) {
flowInputsStore.update((fi) => {
return {
...fi,
[key]: {
flowStepWarnings: warnings[key]
}
}
})
}
}
+1 -133
View File
@@ -5,8 +5,7 @@ import {
type InputTransform,
type Job,
type RestartedFrom,
type OpenFlow,
type FlowModuleValue
type OpenFlow
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { cleanExpr, emptySchema } from '$lib/utils'
@@ -15,8 +14,6 @@ import type { FlowModuleState } from './flowState'
import { type PickableProperties, dfs } from './previousResults'
import { NEVER_TESTED_THIS_FAR } from './models'
import { sendUserToast } from '$lib/toast'
import type { Schema } from '$lib/common'
import { parseOutputs } from '$lib/infer'
import type { ExtendedOpenFlow } from './types'
function create_context_function_template(eval_string: string, context: Record<string, any>) {
@@ -188,135 +185,6 @@ export function emptyFlowModuleState(): FlowModuleState {
}
}
export function isInputFilled(
inputTransforms: Record<string, InputTransform>,
key: string,
schema: Schema | undefined
): boolean {
const required = schema?.required?.includes(key) ?? false
if (!required) {
return true
}
if (inputTransforms.hasOwnProperty(key)) {
const transform = inputTransforms[key]
if (
transform?.type === 'static' &&
(transform?.value === undefined || transform?.value === '' || transform?.value === null)
) {
return false
} else if (
transform?.type === 'javascript' &&
(transform?.expr === undefined || transform?.expr === '' || transform?.expr === null)
) {
return false
}
}
return true
}
async function isConnectedToMissingModule(
argName: string,
flowModuleValue: FlowModuleValue,
moduleIds: string[]
): Promise<string | undefined> {
const type = flowModuleValue.type
if (type === 'rawscript' || type === 'script' || type === 'flow') {
const input = flowModuleValue?.input_transforms[argName]
const val: string = input.type === 'static' ? String(input.value) : input.expr
try {
const outputs = await parseOutputs(val, true)
let error: string = ''
outputs?.forEach(([componentId, id]) => {
if (componentId === 'results') {
if (!moduleIds.includes(id)) {
error += `Input ${argName} is connected to a missing module with id ${id}\n`
}
}
})
return error
} catch (e) {
return `Input ${argName} expression is invalid`
}
}
return
}
export async function computeFlowStepWarning(
argName: string,
flowModuleValue: FlowModuleValue,
messages: Record<
string,
{
message: string
type: 'error' | 'warning'
}
>,
schema: Schema | undefined,
moduleIds: string[] = []
) {
if (messages[argName]) {
delete messages[argName]
}
const type = flowModuleValue.type
if (type == 'rawscript' || type == 'script' || type == 'flow') {
if (!isInputFilled(flowModuleValue.input_transforms, argName, schema)) {
messages[argName] = {
message: `Input ${argName} is required but not filled`,
type: 'warning'
}
}
const errorMessage = await isConnectedToMissingModule(argName, flowModuleValue, moduleIds)
if (errorMessage) {
messages[argName] = {
message: errorMessage,
type: 'error'
}
} else {
if (messages[argName]?.type === 'error') {
delete messages[argName]
}
}
}
return messages
}
export async function initFlowStepWarnings(
flowModuleValue: FlowModuleValue,
schema: Schema | undefined,
moduleIds: string[] = []
) {
const messages: Record<
string,
{
message: string
type: 'error' | 'warning'
}
> = {}
const type = flowModuleValue.type
if (type == 'rawscript' || type == 'script' || type == 'flow') {
const keys = Object.keys(flowModuleValue.input_transforms ?? {})
const promises = keys.map(async (key) => {
await computeFlowStepWarning(key, flowModuleValue, messages, schema, moduleIds)
})
await Promise.all(promises)
}
return messages
}
export function checkIfParentLoop(
flowStore: ExtendedOpenFlow,
modId: string