mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-13 16:05:00 +00:00
fix: seed empty inline flow scripts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
49844eb240
commit
8041cb8c18
@@ -207,7 +207,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
applyFlowJsonUpdate(flowStore.val, inlineScriptSession, {
|
||||
const result = applyFlowJsonUpdate(flowStore.val, inlineScriptSession, {
|
||||
modules,
|
||||
schema,
|
||||
preprocessorModule,
|
||||
@@ -216,6 +216,7 @@
|
||||
|
||||
// Refresh the state store to update UI
|
||||
refreshStateStore(flowStore)
|
||||
return result
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse or apply JSON: ${error instanceof Error ? error.message : String(error)}`
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import type { ContextElement } from '../context'
|
||||
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
|
||||
import { createInlineScriptSession, type InlineScriptSession } from './inlineScriptsUtils'
|
||||
import type { FlowJsonUpdateResult } from './helperUtils'
|
||||
import { flowModuleSchema, flowModulesSchema } from './openFlowZod'
|
||||
import { collectAllModuleIdsFromArray } from './utils'
|
||||
import { FLOW_CHAT_SPECIAL_MODULES, getFlowPrompt } from '$system_prompts'
|
||||
@@ -276,6 +277,17 @@ type EditableFlowJson = {
|
||||
failure_module: FlowModule | null
|
||||
}
|
||||
|
||||
function formatEmptyInlineScriptWarning({
|
||||
emptyInlineScriptModuleIds
|
||||
}: FlowJsonUpdateResult): string {
|
||||
if (emptyInlineScriptModuleIds.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const moduleList = emptyInlineScriptModuleIds.map((id) => `'${id}'`).join(', ')
|
||||
return ` Warning: inline scripts ${moduleList} are empty for now. Use set_module_code to fill them in.`
|
||||
}
|
||||
|
||||
function validateFlowModules(rawModules: unknown): FlowModule[] {
|
||||
if (!Array.isArray(rawModules)) {
|
||||
throw new Error('Flow modules must be an array')
|
||||
@@ -492,7 +504,7 @@ export interface FlowAIChatHelpers {
|
||||
|
||||
// ai chat tools
|
||||
setCode: (id: string, code: string) => Promise<void>
|
||||
setFlowJson: (update: FlowJsonUpdate) => Promise<void>
|
||||
setFlowJson: (update: FlowJsonUpdate) => Promise<FlowJsonUpdateResult>
|
||||
getFlowInputsSchema: () => Promise<Record<string, any>>
|
||||
/** Update exprsToSet store for InputTransformForm components (only if module is selected) */
|
||||
updateExprsToSet: (id: string, inputTransforms: Record<string, InputTransform>) => void
|
||||
@@ -1018,12 +1030,13 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
helpers.inlineScriptSession.set(moduleId, content)
|
||||
}
|
||||
|
||||
await helpers.setFlowJson({
|
||||
const updateResult = await helpers.setFlowJson({
|
||||
modules: parsedFlow.modules,
|
||||
schema: parsedFlow.schema,
|
||||
preprocessorModule: parsedFlow.preprocessor_module,
|
||||
failureModule: parsedFlow.failure_module
|
||||
})
|
||||
const warning = formatEmptyInlineScriptWarning(updateResult)
|
||||
|
||||
const selectedModule = findModuleInEditableFlow(parsedFlow, selectedId)
|
||||
if (
|
||||
@@ -1039,7 +1052,7 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
result: 'Success'
|
||||
})
|
||||
|
||||
return `Flow JSON updated`
|
||||
return `Flow JSON updated.${warning}`
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1058,7 +1071,8 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
content:
|
||||
parsedModule === null ? 'Removing preprocessor module...' : 'Setting preprocessor module...'
|
||||
})
|
||||
await helpers.setFlowJson({ preprocessorModule: parsedModule })
|
||||
const updateResult = await helpers.setFlowJson({ preprocessorModule: parsedModule })
|
||||
const warning = formatEmptyInlineScriptWarning(updateResult)
|
||||
|
||||
if (
|
||||
parsedModule &&
|
||||
@@ -1071,12 +1085,14 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content:
|
||||
parsedModule === null ? 'Preprocessor module removed' : 'Preprocessor module updated',
|
||||
parsedModule === null
|
||||
? 'Preprocessor module removed'
|
||||
: 'Preprocessor module updated',
|
||||
result: 'Success'
|
||||
})
|
||||
return parsedModule === null
|
||||
? 'Preprocessor module removed'
|
||||
: 'Preprocessor module updated successfully.'
|
||||
: `Preprocessor module updated successfully.${warning}`
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1094,7 +1110,8 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: parsedModule === null ? 'Removing failure module...' : 'Setting failure module...'
|
||||
})
|
||||
await helpers.setFlowJson({ failureModule: parsedModule })
|
||||
const updateResult = await helpers.setFlowJson({ failureModule: parsedModule })
|
||||
const warning = formatEmptyInlineScriptWarning(updateResult)
|
||||
|
||||
if (
|
||||
parsedModule &&
|
||||
@@ -1106,12 +1123,13 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
}
|
||||
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: parsedModule === null ? 'Failure module removed' : 'Failure module updated',
|
||||
content:
|
||||
parsedModule === null ? 'Failure module removed' : 'Failure module updated',
|
||||
result: 'Success'
|
||||
})
|
||||
return parsedModule === null
|
||||
? 'Failure module removed'
|
||||
: 'Failure module updated successfully.'
|
||||
: `Failure module updated successfully.${warning}`
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1181,7 +1199,7 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Setting flow...`
|
||||
})
|
||||
await helpers.setFlowJson({
|
||||
const updateResult = await helpers.setFlowJson({
|
||||
...(parsedModules !== undefined ? { modules: parsedModules } : {}),
|
||||
...(parsedSchema !== undefined ? { schema: parsedSchema } : {}),
|
||||
...(parsedPreprocessorModule !== undefined
|
||||
@@ -1189,6 +1207,7 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
: {}),
|
||||
...(parsedFailureModule !== undefined ? { failureModule: parsedFailureModule } : {})
|
||||
})
|
||||
const warning = formatEmptyInlineScriptWarning(updateResult)
|
||||
|
||||
// Update exprsToSet if the selected module has input_transforms
|
||||
if (
|
||||
@@ -1218,7 +1237,7 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
content: `Flow updated`,
|
||||
result: 'Success'
|
||||
})
|
||||
return `Flow updated`
|
||||
return `Flow updated.${warning}`
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1490,6 +1509,10 @@ To reduce token usage, rawscript content in the flow you receive is replaced wit
|
||||
**To inspect existing code:**
|
||||
- Use \`inspect_inline_script\` tool to view the current code: \`inspect_inline_script({ moduleId: "step_a" })\`
|
||||
|
||||
**If a flow update tool warns that inline scripts are empty:**
|
||||
- The module structure was created successfully, but the code is still empty
|
||||
- Immediately call \`set_module_code\` for each warned module ID
|
||||
|
||||
### Writing Code for Modules
|
||||
|
||||
**IMPORTANT: Before writing any code for a rawscript module, you MUST call the \`get_instructions_for_code_generation\` tool with the target language.** This tool provides essential language-specific instructions.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { applyFlowJsonUpdate } from './helperUtils'
|
||||
import { createInlineScriptSession } from './inlineScriptsUtils'
|
||||
|
||||
vi.mock('../shared', () => ({
|
||||
SPECIAL_MODULE_IDS: {
|
||||
PREPROCESSOR: 'preprocessor',
|
||||
FAILURE: 'failure'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('$lib/components/flows/previousResults', () => ({
|
||||
dfs: () => []
|
||||
}))
|
||||
|
||||
function makeRawScriptModule(id: string, content: string): FlowModule {
|
||||
return {
|
||||
id,
|
||||
summary: id,
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
language: 'bun',
|
||||
content,
|
||||
input_transforms: {}
|
||||
}
|
||||
} as FlowModule
|
||||
}
|
||||
|
||||
describe('applyFlowJsonUpdate', () => {
|
||||
it('accepts new self-referenced inline scripts and initializes them as empty', () => {
|
||||
const flow = {
|
||||
value: {
|
||||
modules: [makeRawScriptModule('process_data', 'existing code')]
|
||||
}
|
||||
}
|
||||
const inlineScriptSession = createInlineScriptSession()
|
||||
inlineScriptSession.set('process_data', 'existing code')
|
||||
|
||||
const result = applyFlowJsonUpdate(flow as any, inlineScriptSession, {
|
||||
modules: [
|
||||
makeRawScriptModule('process_data', 'inline_script.process_data'),
|
||||
makeRawScriptModule('validate_data', 'inline_script.validate_data')
|
||||
]
|
||||
})
|
||||
const [processDataModule, validateDataModule] = flow.value.modules as Array<FlowModule & { value: any }>
|
||||
|
||||
expect(result.emptyInlineScriptModuleIds).toEqual(['validate_data'])
|
||||
expect(inlineScriptSession.get('validate_data')).toBe('')
|
||||
expect(processDataModule?.value.type).toBe('rawscript')
|
||||
expect(processDataModule?.value.content).toBe('existing code')
|
||||
expect(validateDataModule?.value.type).toBe('rawscript')
|
||||
expect(validateDataModule?.value.content).toBe('')
|
||||
})
|
||||
|
||||
it('still rejects unresolved inline script references that do not match the module id', () => {
|
||||
const flow = {
|
||||
value: {
|
||||
modules: []
|
||||
}
|
||||
}
|
||||
const inlineScriptSession = createInlineScriptSession()
|
||||
|
||||
expect(() =>
|
||||
applyFlowJsonUpdate(flow as any, inlineScriptSession, {
|
||||
modules: [makeRawScriptModule('validate_data', 'inline_script.other_module')]
|
||||
})
|
||||
).toThrow('Unresolved inline script references: other_module')
|
||||
})
|
||||
})
|
||||
@@ -14,6 +14,10 @@ export interface FlowJsonUpdate {
|
||||
failureModule?: FlowModule | null
|
||||
}
|
||||
|
||||
export interface FlowJsonUpdateResult {
|
||||
emptyInlineScriptModuleIds: string[]
|
||||
}
|
||||
|
||||
export function getFlowModuleById(flow: FlowLike | undefined, id: string): FlowModule | undefined {
|
||||
if (!flow) {
|
||||
return undefined
|
||||
@@ -60,8 +64,13 @@ export function applyFlowJsonUpdate(
|
||||
flow: FlowLike,
|
||||
inlineScriptSession: InlineScriptSession,
|
||||
{ modules, schema, preprocessorModule, failureModule }: FlowJsonUpdate
|
||||
): void {
|
||||
): FlowJsonUpdateResult {
|
||||
const emptyInlineScriptModuleIds = new Set<string>()
|
||||
|
||||
if (modules !== undefined) {
|
||||
seedMissingInlineScripts(modules, inlineScriptSession).forEach((id) =>
|
||||
emptyInlineScriptModuleIds.add(id)
|
||||
)
|
||||
flow.value.modules = restoreFlowModules(modules, inlineScriptSession)
|
||||
}
|
||||
|
||||
@@ -73,12 +82,26 @@ export function applyFlowJsonUpdate(
|
||||
flow.value.preprocessor_module =
|
||||
preprocessorModule === null
|
||||
? undefined
|
||||
: restoreFlowModule(preprocessorModule, inlineScriptSession)
|
||||
: restoreFlowModuleWithSeededInlineScripts(
|
||||
preprocessorModule,
|
||||
inlineScriptSession,
|
||||
emptyInlineScriptModuleIds
|
||||
)
|
||||
}
|
||||
|
||||
if (failureModule !== undefined) {
|
||||
flow.value.failure_module =
|
||||
failureModule === null ? undefined : restoreFlowModule(failureModule, inlineScriptSession)
|
||||
failureModule === null
|
||||
? undefined
|
||||
: restoreFlowModuleWithSeededInlineScripts(
|
||||
failureModule,
|
||||
inlineScriptSession,
|
||||
emptyInlineScriptModuleIds
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
emptyInlineScriptModuleIds: Array.from(emptyInlineScriptModuleIds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +123,17 @@ function restoreFlowModule(
|
||||
return restoredModule
|
||||
}
|
||||
|
||||
function restoreFlowModuleWithSeededInlineScripts(
|
||||
module: FlowModule,
|
||||
inlineScriptSession: InlineScriptSession,
|
||||
emptyInlineScriptModuleIds: Set<string>
|
||||
): FlowModule {
|
||||
seedMissingInlineScripts([module], inlineScriptSession).forEach((id) =>
|
||||
emptyInlineScriptModuleIds.add(id)
|
||||
)
|
||||
return restoreFlowModule(module, inlineScriptSession)
|
||||
}
|
||||
|
||||
function assertResolvedInlineScripts(
|
||||
modules: FlowModule[],
|
||||
inlineScriptSession: InlineScriptSession
|
||||
@@ -109,3 +143,67 @@ function assertResolvedInlineScripts(
|
||||
throw new Error(`Unresolved inline script references: ${unresolvedRefs.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
function seedMissingInlineScripts(
|
||||
modules: FlowModule[],
|
||||
inlineScriptSession: InlineScriptSession
|
||||
): string[] {
|
||||
const emptyInlineScriptModuleIds: string[] = []
|
||||
|
||||
function ensureEmptyInlineScript(moduleId: string, inlineScriptRefId: string) {
|
||||
if (moduleId !== inlineScriptRefId || inlineScriptSession.has(moduleId)) {
|
||||
return
|
||||
}
|
||||
|
||||
inlineScriptSession.set(moduleId, '')
|
||||
emptyInlineScriptModuleIds.push(moduleId)
|
||||
}
|
||||
|
||||
function visitModule(module: FlowModule) {
|
||||
if (module.value.type === 'rawscript' && module.value.content) {
|
||||
const match = module.value.content.match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
ensureEmptyInlineScript(module.id, match[1])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
|
||||
module.value.modules?.forEach(visitModule)
|
||||
return
|
||||
}
|
||||
|
||||
if (module.value.type === 'branchone') {
|
||||
module.value.branches?.forEach((branch) => branch.modules?.forEach(visitModule))
|
||||
module.value.default?.forEach(visitModule)
|
||||
return
|
||||
}
|
||||
|
||||
if (module.value.type === 'branchall') {
|
||||
module.value.branches?.forEach((branch) => branch.modules?.forEach(visitModule))
|
||||
return
|
||||
}
|
||||
|
||||
if (module.value.type === 'aiagent') {
|
||||
for (const tool of module.value.tools ?? []) {
|
||||
if (
|
||||
tool.value &&
|
||||
'tool_type' in tool.value &&
|
||||
tool.value.tool_type === 'flowmodule' &&
|
||||
'type' in tool.value &&
|
||||
tool.value.type === 'rawscript' &&
|
||||
'content' in tool.value &&
|
||||
tool.value.content
|
||||
) {
|
||||
const match = (tool.value.content as string).match(/^inline_script\.(.+)$/)
|
||||
if (match) {
|
||||
ensureEmptyInlineScript(tool.id, match[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules.forEach(visitModule)
|
||||
return emptyInlineScriptModuleIds
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user