diff --git a/frontend/minifiedOpenflowJson.sh b/frontend/minifiedOpenflowJson.sh index 71f51067c3..055688b204 100755 --- a/frontend/minifiedOpenflowJson.sh +++ b/frontend/minifiedOpenflowJson.sh @@ -2,15 +2,15 @@ set -e -# Script to generate minified OpenFlow JSON for frontend AI system prompt +# Script to generate minified OpenFlow JSON and Zod schemas script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source_file="${script_dirpath}/../windmill-yaml-validator/src/gen/openflow.json" output_dirpath="${script_dirpath}/src/lib/components/copilot/chat/flow" -output_file="${output_dirpath}/openFlow.json" +json_output_file="${output_dirpath}/openFlow.json" +zod_output_file="${output_dirpath}/openFlowZod.ts" -echo "Generating minified OpenFlow JSON..." +echo "Generating OpenFlow JSON and Zod schemas..." -# Validate source file exists if [ ! -f "${source_file}" ]; then echo "Error: Source file not found: ${source_file}" echo "Please run windmill-yaml-validator/gen_openflow_schema.sh first" @@ -20,33 +20,68 @@ fi # Create output directory if it doesn't exist mkdir -p "${output_dirpath}" -# Minify JSON by removing all whitespace node -e " +const { jsonSchemaToZod } = require('json-schema-to-zod'); const fs = require('fs'); + const sourceFile = '${source_file}'; -const outputFile = '${output_file}'; +const jsonOutputFile = '${json_output_file}'; +const zodOutputFile = '${zod_output_file}'; -try { - const schema = JSON.parse(fs.readFileSync(sourceFile, 'utf8')); +const openApiSchema = JSON.parse(fs.readFileSync(sourceFile, 'utf8')); +const definitions = openApiSchema.components?.schemas || {}; - // Minify: stringify without spaces - const minified = JSON.stringify(schema); +// === 1. Generate minified OpenFlow JSON === +const minified = JSON.stringify(openApiSchema); +fs.writeFileSync(jsonOutputFile, minified); - fs.writeFileSync(outputFile, minified); +const originalSize = fs.statSync(sourceFile).size; +const minifiedSize = fs.statSync(jsonOutputFile).size; +const savings = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1); - const originalSize = fs.statSync(sourceFile).size; - const minifiedSize = fs.statSync(outputFile).size; - const savings = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1); +console.log('✓ Minified OpenFlow JSON generated'); +console.log(' Original: ' + (originalSize / 1024).toFixed(1) + ' KB → Minified: ' + (minifiedSize / 1024).toFixed(1) + ' KB (saved ' + savings + '%)'); - console.log(' Minified OpenFlow JSON generated successfully'); - console.log(' Original size: ' + (originalSize / 1024).toFixed(1) + ' KB'); - console.log(' Minified size: ' + (minifiedSize / 1024).toFixed(1) + ' KB'); - console.log(' Savings: ' + savings + '%'); - console.log(' Output: ' + outputFile); -} catch (e) { - console.error('Error minifying JSON:', e.message); - process.exit(1); +// === 2. Generate Zod schema === +// Inline \$refs, treating circular references as z.object({}).passthrough() +function inlineRefs(obj, seenRefs = new Set()) { + if (typeof obj !== 'object' || obj === null) return obj; + if (Array.isArray(obj)) return obj.map(item => inlineRefs(item, seenRefs)); + + if (obj['\$ref']) { + const match = obj['\$ref'].match(/#\\/components\\/schemas\\/(.+)\$/); + if (match) { + const refName = match[1]; + if (seenRefs.has(refName)) { + // Mark circular ref with placeholder for z.lazy() replacement + return { type: 'string', const: '__CIRCULAR_REF_FLOWMODULE__' }; + } + if (definitions[refName]) { + return inlineRefs(definitions[refName], new Set([...seenRefs, refName])); + } + } + return { type: 'object' }; + } + + const result = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = inlineRefs(value, seenRefs); + } + return result; } + +const inlinedSchema = inlineRefs(definitions.FlowModule, new Set(['FlowModule'])); + +let zodCode = jsonSchemaToZod(inlinedSchema, { name: 'flowModuleSchema', module: 'esm' }); + +// Replace circular reference placeholders with z.lazy() for proper recursive typing +zodCode = zodCode.replace(/z\.literal\(\"__CIRCULAR_REF_FLOWMODULE__\"\)/g, 'z.lazy(() => flowModuleSchema)'); + +zodCode = zodCode.replace('from \"zod\"', 'from \"zod/v3\"'); +zodCode += '\n\nexport const flowModulesSchema = z.array(flowModuleSchema)\n'; + +fs.writeFileSync(zodOutputFile, zodCode); +console.log('✓ Generated Zod schema: ' + zodOutputFile); " echo "Done!" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b63272bd99..9d0fda39b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -123,6 +123,8 @@ "eslint": "^8.47.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-svelte": "^2.45.1", + "json-refs": "^3.0.15", + "json-schema-to-zod": "^2.7.0", "path-browserify": "^1.0.1", "playwright": "^1.56.1", "postcss": "^8.4.49", @@ -2394,6 +2396,19 @@ "@tybys/wasm-util": "^0.10.1" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2452,6 +2467,16 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@petamoriken/float16": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", @@ -4199,6 +4224,13 @@ "node": ">=0.10.0" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -4973,6 +5005,16 @@ "node": ">=18" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5007,6 +5049,13 @@ "node": ">= 0.6" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -5680,6 +5729,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6346,6 +6406,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -6546,6 +6620,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -6720,6 +6801,22 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -7140,6 +7237,16 @@ "dev": true, "license": "MIT" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/graphql": { "version": "16.11.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", @@ -7894,6 +8001,73 @@ "license": "MIT", "peer": true }, + "node_modules/json-refs": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/json-refs/-/json-refs-3.0.15.tgz", + "integrity": "sha512-0vOQd9eLNBL18EGl5yYaO44GhixmImes2wiYn9Z3sag3QnehWrYWlB9AFtMxCL2Bj3fyxgDYkxGFEU/chlYssw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "~4.1.1", + "graphlib": "^2.1.8", + "js-yaml": "^3.13.1", + "lodash": "^4.17.15", + "native-promise-only": "^0.8.1", + "path-loader": "^1.0.10", + "slash": "^3.0.0", + "uri-js": "^4.2.2" + }, + "bin": { + "json-refs": "bin/json-refs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/json-refs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/json-refs/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/json-refs/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-zod": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/json-schema-to-zod/-/json-schema-to-zod-2.7.0.tgz", + "integrity": "sha512-eW59l3NQ6sa3HcB+Ahf7pP6iGU7MY4we5JsPqXQ2ZcIPF8QxSg/lkY8lN0Js/AG0NjMbk+nZGUfHlceiHF+bwQ==", + "dev": true, + "license": "ISC", + "bin": { + "json-schema-to-zod": "dist/cjs/cli.js" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -8934,6 +9108,16 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -9511,6 +9695,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -9811,6 +10008,13 @@ "license": "MIT", "optional": true }, + "node_modules/native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", + "dev": true, + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10019,6 +10223,19 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-is": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", @@ -10334,6 +10551,17 @@ "node": ">=8" } }, + "node_modules/path-loader": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/path-loader/-/path-loader-1.0.12.tgz", + "integrity": "sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "native-promise-only": "^0.8.1", + "superagent": "^7.1.6" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -11429,6 +11657,22 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quadprog": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/quadprog/-/quadprog-1.6.1.tgz", @@ -11701,8 +11945,8 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -12154,6 +12398,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -12363,6 +12683,13 @@ "license": "CC0-1.0", "peer": true }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -12750,6 +13077,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/superagent": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.6.tgz", + "integrity": "sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 337ea6f5ec..c07592085a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,11 +25,11 @@ "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.2", "@playwright/test": "^1.34.3", - "@tailwindcss/forms": "^0.5.3", "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.28.0", "@sveltejs/package": "^2.3.7", "@sveltejs/vite-plugin-svelte": "^6.1.1", + "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.8", "@types/d3": "^7.4.0", "@types/d3-zoom": "^3.0.3", @@ -47,6 +47,8 @@ "eslint": "^8.47.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-svelte": "^2.45.1", + "json-refs": "^3.0.15", + "json-schema-to-zod": "^2.7.0", "path-browserify": "^1.0.1", "playwright": "^1.56.1", "postcss": "^8.4.49", @@ -548,4 +550,4 @@ "@rollup/rollup-linux-x64-gnu": "^4.35.0", "fsevents": "^2.3.3" } -} \ No newline at end of file +} diff --git a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte index 2a6351a4b8..44cec01f38 100644 --- a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte @@ -3,7 +3,7 @@ import { getContext, untrack } from 'svelte' import type { ExtendedOpenFlow, FlowEditorContext } from '$lib/components/flows/types' import { dfs } from '$lib/components/flows/previousResults' - import type { InputTransform, OpenFlow } from '$lib/gen' + import type { FlowModule, InputTransform, OpenFlow } from '$lib/gen' import type { FlowAIChatHelpers } from './core' import { restoreInlineScriptReferences } from './inlineScriptsUtils' import { loadSchemaFromModule } from '$lib/components/flows/flowInfers' @@ -170,62 +170,28 @@ return await onTestFlow?.(conversationId) }, - setFlowJson: async (json: string) => { + setFlowJson: async ( + modules: FlowModule[] | undefined, + schema: Record | undefined + ) => { try { - // Parse JSON to JavaScript object - const parsed = JSON.parse(json) + if (modules) { + // Restore inline script references back to full content + const restoredModules = restoreInlineScriptReferences(modules) - // Validate that it has the expected structure - if (!parsed.modules || !Array.isArray(parsed.modules)) { - throw new Error('JSON must contain a "modules" array') - } - - // Restore inline script references back to full content - const restoredModules = restoreInlineScriptReferences(parsed.modules) - - // Also restore preprocessor and failure modules if they have references - let restoredPreprocessor = parsed.preprocessor_module - if ( - restoredPreprocessor?.value?.type === 'rawscript' && - restoredPreprocessor.value.content - ) { - const match = restoredPreprocessor.value.content.match(/^inline_script\.(.+)$/) - if (match) { - // Wrap in array to reuse the restoration function - const restored = restoreInlineScriptReferences([restoredPreprocessor]) - restoredPreprocessor = restored[0] + // Take snapshot of current flowStore BEFORE making changes + if (!diffManager?.hasPendingChanges) { + const snapshot = $state.snapshot(flowStore).val + diffManager?.setBeforeFlow(snapshot) } - } - let restoredFailure = parsed.failure_module - if (restoredFailure?.value?.type === 'rawscript' && restoredFailure.value.content) { - const match = restoredFailure.value.content.match(/^inline_script\.(.+)$/) - if (match) { - const restored = restoreInlineScriptReferences([restoredFailure]) - restoredFailure = restored[0] - } - } - - // Take snapshot of current flowStore BEFORE making changes - if (!diffManager?.hasPendingChanges) { - const snapshot = $state.snapshot(flowStore).val - diffManager?.setBeforeFlow(snapshot) - } - - // Directly modify flowStore (immediate effect) - flowStore.val.value.modules = restoredModules - - if (parsed.preprocessor_module !== undefined) { - flowStore.val.value.preprocessor_module = restoredPreprocessor || undefined - } - - if (parsed.failure_module !== undefined) { - flowStore.val.value.failure_module = restoredFailure || undefined + // Directly modify flowStore (immediate effect) + flowStore.val.value.modules = restoredModules } // Update schema if provided - if (parsed.schema !== undefined) { - flowStore.val.schema = parsed.schema + if (schema !== undefined) { + flowStore.val.schema = schema } diffManager?.setEditMode(true) diff --git a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/evalHelpers.ts b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/evalHelpers.ts index 506a540c95..aecea05cc6 100644 --- a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/evalHelpers.ts +++ b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/evalHelpers.ts @@ -49,18 +49,19 @@ export function createEvalHelpers( inlineScriptStore.set(id, code) }, - setFlowJson: async (json: string) => { - const parsed = JSON.parse(json) - - // Restore inline script references back to full content (mirrors FlowAIChat.svelte) - if (parsed.modules && Array.isArray(parsed.modules)) { - parsed.modules = restoreInlineScriptReferences(parsed.modules) + setFlowJson: async ( + modules: FlowModule[] | undefined, + schema: Record | undefined + ) => { + if (modules) { + // Restore inline script references back to full content + const restoredModules = restoreInlineScriptReferences(modules) + flow.value.modules = restoredModules } - flow.value = { ...flow.value, ...parsed } - // Also update schema if provided - if (parsed.schema !== undefined) { - flow.schema = parsed.schema + // Update schema if provided + if (schema !== undefined) { + flow.schema = schema } }, diff --git a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/flowChat.eval.test.ts b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/flowChat.eval.test.ts index ca4cb327d3..7c4882eda5 100644 --- a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/flowChat.eval.test.ts +++ b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/flowChat.eval.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest' import { runVariantComparison, type ExpectedFlow } from './evalRunner' import { writeComparisonResults } from './evalResultsWriter' -import { BASELINE_VARIANT, MINIMAL_SINGLE_TOOL_VARIANT, NO_FULL_SCHEMA_VARIANT } from './variants' +import { BASELINE_VARIANT, MINIMAL_SINGLE_TOOL_VARIANT } from './variants' // @ts-ignore - JSON import import expectedTest1 from './expected/test1.json' // @ts-ignore - JSON import @@ -41,11 +41,6 @@ const VARIANTS = [ model, name: `baseline-${model.replace('/', '-')}` })), - ...MODELS.map((model) => ({ - ...NO_FULL_SCHEMA_VARIANT, - model, - name: `no-full-schema-${model.replace('/', '-')}` - })), ...MODELS.map((model) => ({ ...MINIMAL_SINGLE_TOOL_VARIANT, model, diff --git a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/index.ts b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/index.ts index ffaec19031..52fc0058d3 100644 --- a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/index.ts +++ b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/index.ts @@ -1,7 +1,6 @@ // Re-export all variant configurations export { BASELINE_VARIANT } from './baseline' export { MINIMAL_SINGLE_TOOL_VARIANT, setFlowJsonTool } from './minimal-single-tool' -export { NO_FULL_SCHEMA_VARIANT } from './no-full-schema' // Re-export types for convenience export type { VariantConfig } from '../evalVariants' diff --git a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/minimal-single-tool.ts b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/minimal-single-tool.ts index 7201169257..3e53998b8c 100644 --- a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/minimal-single-tool.ts +++ b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/minimal-single-tool.ts @@ -1,7 +1,8 @@ import type { VariantConfig } from '../evalVariants' import type { Tool } from '../../../../shared' import type { FlowAIChatHelpers } from '../../../core' -import { flowTools, formatOpenFlowSchemaForPrompt } from '../../../core' +import { flowTools } from '../../../core' +import openFlowSchema from '../../../openFlow.json' /** * IDs of the granular flow editing tools that should be replaced by set_flow_json. @@ -48,11 +49,7 @@ export const setFlowJsonTool: Tool = { }, fn: async ({ args, helpers }) => { const { modules, schema } = args - const flowValue: Record = { modules } - if (schema) { - flowValue.schema = schema - } - await helpers.setFlowJson(JSON.stringify(flowValue)) + await helpers.setFlowJson(modules, schema) return `Flow updated with ${modules.length} module(s): [${modules.map((m: any) => m.id).join(', ')}]` } } @@ -370,7 +367,7 @@ On Windmill, credentials and configuration are stored in resources. Resource typ Below is the complete OpenAPI schema for OpenFlow. All field descriptions and behaviors are defined here. Refer to this as the authoritative reference when generating flow JSON: \`\`\`json -${formatOpenFlowSchemaForPrompt()} +${JSON.stringify(openFlowSchema, null, 2)} \`\`\` The schema includes detailed descriptions for: diff --git a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/no-full-schema.ts b/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/no-full-schema.ts deleted file mode 100644 index ee269fb6b3..0000000000 --- a/frontend/src/lib/components/copilot/chat/flow/__tests__/eval/variants/no-full-schema.ts +++ /dev/null @@ -1,914 +0,0 @@ -import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions.mjs' -import type { VariantConfig } from '../evalVariants' -import { flowTools } from '../../../core' -import type { Tool } from '../../../../shared' -import type { FlowAIChatHelpers } from '../../../core' -import { findModuleInFlow } from '../../../utils' -import type { FlowModule } from '$lib/gen' - -/** - * Simplified InputTransform schema (inline, no $ref) - */ -const inputTransformSchema = { - oneOf: [ - { - type: 'object', - description: - "Static value passed directly. For resources, use format '$res:path/to/resource'", - properties: { - value: { description: 'The static value' }, - type: { type: 'string', enum: ['static'] } - }, - required: ['type'] - }, - { - type: 'object', - description: - "JavaScript expression evaluated at runtime. Use 'results.step_id' or 'flow_input.property'. Inside loops, use 'flow_input.iter.value'", - properties: { - expr: { type: 'string', description: 'JavaScript expression returning the value' }, - type: { type: 'string', enum: ['javascript'] } - }, - required: ['expr', 'type'] - } - ] -} - -/** - * Simplified FlowModuleValue schema without circular references. - * Container types (forloopflow, whileloopflow, branchone, branchall, aiagent) - * have their nested modules/tools arrays marked as must-be-empty. - */ -const simplifiedFlowModuleValueSchema = { - description: - 'The module implementation. For containers (loops, branches), modules array must be empty - use add_module with insideId to add steps inside.', - oneOf: [ - // RawScript - no nested modules, keep full schema - { - type: 'object', - description: - "Inline script with code. Use 'bun' as default language. Script receives arguments from input_transforms", - properties: { - type: { type: 'string', enum: ['rawscript'] }, - content: { - type: 'string', - description: "Script source code. Should export a 'main' function" - }, - language: { - type: 'string', - enum: [ - 'deno', - 'bun', - 'python3', - 'go', - 'bash', - 'powershell', - 'postgresql', - 'mysql', - 'bigquery', - 'snowflake', - 'mssql', - 'oracledb', - 'graphql', - 'nativets', - 'php' - ], - description: 'Programming language' - }, - input_transforms: { - type: 'object', - description: 'Map of parameter names to values (static or JavaScript expressions)', - additionalProperties: inputTransformSchema - }, - path: { type: 'string', description: 'Optional path for saving this script' }, - lock: { type: 'string', description: 'Lock file content for dependencies' }, - tag: { type: 'string', description: 'Worker group tag for execution routing' }, - concurrent_limit: { type: 'number' }, - concurrency_time_window_s: { type: 'number' }, - custom_concurrency_key: { type: 'string' }, - is_trigger: { type: 'boolean' } - }, - required: ['type', 'content', 'language', 'input_transforms'] - }, - // PathScript - reference to existing script - { - type: 'object', - description: 'Reference to an existing script by path', - properties: { - type: { type: 'string', enum: ['script'] }, - path: { type: 'string', description: "Path to script (e.g., 'f/scripts/send_email')" }, - hash: { type: 'string', description: 'Optional specific version hash' }, - input_transforms: { - type: 'object', - description: 'Map of parameter names to values', - additionalProperties: inputTransformSchema - }, - tag_override: { type: 'string' }, - is_trigger: { type: 'boolean' } - }, - required: ['type', 'path', 'input_transforms'] - }, - // PathFlow - reference to existing flow - { - type: 'object', - description: 'Reference to an existing flow as a subflow', - properties: { - type: { type: 'string', enum: ['flow'] }, - path: { type: 'string', description: "Path to flow (e.g., 'f/flows/process_user')" }, - input_transforms: { - type: 'object', - description: 'Map of parameter names to values', - additionalProperties: inputTransformSchema - } - }, - required: ['type', 'path', 'input_transforms'] - }, - // ForloopFlow - modules MUST be empty - { - type: 'object', - description: - "For loop over an iterator. IMPORTANT: 'modules' must be an empty array []. Use add_module with insideId and branchPath='modules' to add steps inside.", - properties: { - type: { type: 'string', enum: ['forloopflow'] }, - modules: { - type: 'array', - items: {}, - description: - "MUST be empty []. Use add_module({ insideId: 'loop_id', branchPath: 'modules', value: {...} }) to add steps" - }, - iterator: { - ...inputTransformSchema, - description: - "JavaScript expression returning array to iterate. Use 'flow_input.iter.value' inside loop to access current item" - }, - skip_failures: { - type: 'boolean', - description: "If true, iteration failures don't stop the loop" - }, - parallel: { - type: 'boolean', - description: 'If true, iterations run concurrently' - }, - parallelism: { - ...inputTransformSchema, - description: 'Max concurrent iterations when parallel=true' - } - }, - required: ['type', 'modules', 'iterator', 'skip_failures'] - }, - // WhileloopFlow - modules MUST be empty - { - type: 'object', - description: - "While loop that repeats until stop_after_if triggers. IMPORTANT: 'modules' must be an empty array []. Use add_module to add steps inside.", - properties: { - type: { type: 'string', enum: ['whileloopflow'] }, - modules: { - type: 'array', - items: {}, - description: - "MUST be empty []. Use add_module({ insideId: 'loop_id', branchPath: 'modules', value: {...} }) to add steps" - }, - skip_failures: { - type: 'boolean', - description: "If true, iteration failures don't stop the loop" - }, - parallel: { type: 'boolean' }, - parallelism: inputTransformSchema - }, - required: ['type', 'modules', 'skip_failures'] - }, - // BranchOne - branches and default MUST be empty - { - type: 'object', - description: - "Conditional branching (first match wins). IMPORTANT: Create with empty 'branches' array [], then use add_module with branchPath=null to add branches, and branchPath='branches.N' to add modules inside branches.", - properties: { - type: { type: 'string', enum: ['branchone'] }, - branches: { - type: 'array', - items: {}, - description: - "MUST be empty []. Use add_module({ insideId: 'branch_id', branchPath: null, value: { summary, expr, modules: [] } }) to add branches" - }, - default: { - type: 'array', - items: {}, - description: - "MUST be empty []. Use add_module({ insideId: 'branch_id', branchPath: 'default', value: {...} }) to add steps to default branch" - } - }, - required: ['type', 'branches', 'default'] - }, - // BranchAll - branches MUST be empty - { - type: 'object', - description: - "Parallel branching (all branches execute). IMPORTANT: Create with empty 'branches' array [], then use add_module with branchPath=null to add branches.", - properties: { - type: { type: 'string', enum: ['branchall'] }, - branches: { - type: 'array', - items: {}, - description: - "MUST be empty []. Use add_module({ insideId: 'branch_id', branchPath: null, value: { summary, skip_failure, modules: [] } }) to add branches" - }, - parallel: { - type: 'boolean', - description: 'If true, all branches execute concurrently' - } - }, - required: ['type', 'branches'] - }, - // Identity - pass-through - { - type: 'object', - description: 'Pass-through module that returns input unchanged', - properties: { - type: { type: 'string', enum: ['identity'] }, - flow: { type: 'boolean' } - }, - required: ['type'] - }, - // AiAgent - tools MUST be empty - { - type: 'object', - description: - "AI agent step. IMPORTANT: 'tools' must be an empty array []. Use add_module with branchPath='tools' to add tools.", - properties: { - type: { type: 'string', enum: ['aiagent'] }, - input_transforms: { - type: 'object', - description: 'Agent input parameters', - properties: { - provider: inputTransformSchema, - output_type: inputTransformSchema, - user_message: inputTransformSchema, - system_prompt: inputTransformSchema, - streaming: inputTransformSchema, - messages_context_length: inputTransformSchema, - output_schema: inputTransformSchema, - user_images: inputTransformSchema, - max_completion_tokens: inputTransformSchema, - temperature: inputTransformSchema - }, - required: ['provider', 'user_message', 'output_type'] - }, - tools: { - type: 'array', - items: {}, - description: - "MUST be empty []. Use add_module({ insideId: 'agent_id', branchPath: 'tools', value: {...} }) to add tools" - }, - parallel: { type: 'boolean' } - }, - required: ['type', 'tools', 'input_transforms'] - } - ] -} - -/** - * Simplified FlowModule schema without circular references in nested modules - */ -const simplifiedFlowModuleSchema = { - type: 'object', - description: 'A single step in a flow', - properties: { - id: { - type: 'string', - description: - "Unique identifier. Used to reference results via 'results.step_id'. Must be alphanumeric with underscores/hyphens" - }, - value: simplifiedFlowModuleValueSchema, - summary: { type: 'string', description: 'Short description of what this step does' }, - stop_after_if: { - type: 'object', - description: 'Early termination condition evaluated after step completes', - properties: { - skip_if_stopped: { type: 'boolean' }, - expr: { type: 'string', description: "Expression using 'result' or 'flow_input'" }, - error_message: { type: 'string' } - }, - required: ['expr'] - }, - stop_after_all_iters_if: { - type: 'object', - description: 'For loops - condition evaluated after all iterations', - properties: { - skip_if_stopped: { type: 'boolean' }, - expr: { type: 'string' }, - error_message: { type: 'string' } - }, - required: ['expr'] - }, - skip_if: { - type: 'object', - properties: { - expr: { type: 'string', description: 'Expression returning true to skip this step' } - }, - required: ['expr'] - }, - sleep: { - ...inputTransformSchema, - description: 'Delay before executing (seconds)' - }, - cache_ttl: { type: 'number', description: 'Cache duration in seconds' }, - timeout: { - ...inputTransformSchema, - description: 'Max execution time in seconds' - }, - delete_after_use: { type: 'boolean' }, - mock: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - return_value: {} - } - }, - suspend: { - type: 'object', - description: 'Approval/resume configuration', - properties: { - required_events: { type: 'integer' }, - timeout: { type: 'integer' }, - resume_form: { - type: 'object', - properties: { schema: { type: 'object' } } - }, - user_auth_required: { type: 'boolean' }, - user_groups_required: inputTransformSchema, - self_approval_disabled: { type: 'boolean' }, - hide_cancel: { type: 'boolean' }, - continue_on_disapprove_timeout: { type: 'boolean' } - } - }, - priority: { type: 'number' }, - continue_on_error: { type: 'boolean' }, - retry: { - type: 'object', - properties: { - constant: { - type: 'object', - properties: { - attempts: { type: 'integer' }, - seconds: { type: 'integer' } - } - }, - exponential: { - type: 'object', - properties: { - attempts: { type: 'integer' }, - multiplier: { type: 'integer' }, - seconds: { type: 'integer', minimum: 1 }, - random_factor: { type: 'integer', minimum: 0, maximum: 100 } - } - }, - retry_if: { - type: 'object', - properties: { - expr: { type: 'string' } - }, - required: ['expr'] - } - } - } - }, - required: ['value', 'id'] -} - -/** - * Custom add_module tool definition with simplified schema (no circular refs) - */ -const noSchemaAddModuleToolDef: ChatCompletionFunctionTool = { - type: 'function', - function: { - strict: false, - name: 'add_module', - description: - "Add a new module to the flow. For containers (loops, branches, agents), add with EMPTY modules array, then use additional add_module calls to add steps inside. Reserved IDs: 'failure', 'preprocessor', 'Input'.", - parameters: { - type: 'object', - properties: { - afterId: { - type: ['string', 'null'], - description: 'ID of module to insert after. Use null to insert at beginning.' - }, - insideId: { - type: ['string', 'null'], - description: - 'ID of container module (branch/loop/agent) to insert into. Use with branchPath.' - }, - branchPath: { - type: ['string', 'null'], - description: - "Path inside container: 'modules' (loops), 'branches.0'/'branches.1'/etc (specific branch), 'default' (branchone default), 'tools' (aiagent). Use null with insideId to add NEW branch to branchall/branchone." - }, - value: simplifiedFlowModuleSchema - }, - required: ['value'] - } - } -} - -/** - * Custom modify_module tool definition with simplified schema (no circular refs) - */ -const noSchemaModifyModuleToolDef: ChatCompletionFunctionTool = { - type: 'function', - function: { - strict: false, - name: 'modify_module', - description: - "Modify an existing module (full replacement). Use for changing configuration, transforms, or conditions. NOT for adding/removing nested modules - use add_module/remove_module. Reserved IDs: 'failure', 'preprocessor', 'Input'.", - parameters: { - type: 'object', - properties: { - id: { - type: 'string', - description: 'ID of the module to modify' - }, - value: simplifiedFlowModuleSchema - }, - required: ['id', 'value'] - } - } -} - -/** - * Dedicated add_branch tool for adding branches to branchone/branchall containers. - * This makes it clearer how to add branches without nested modules. - */ -const addBranchToolDef: ChatCompletionFunctionTool = { - type: 'function', - function: { - strict: false, - name: 'add_branch', - description: - 'Add a new branch to a branchone or branchall container. The branch will have an empty modules array - use add_module with insideId and branchPath to add steps inside the branch.', - parameters: { - type: 'object', - properties: { - containerId: { - type: 'string', - description: 'ID of the branchone or branchall container to add a branch to' - }, - summary: { - type: 'string', - description: - 'Short description of the branch (e.g., "Handle admin users", "Process errors")' - }, - expr: { - type: 'string', - description: - "JavaScript expression for branchone only. Return true to execute this branch. Can use 'results.step_id' or 'flow_input'. Example: \"results.check_role === 'admin'\"" - }, - skip_failure: { - type: 'boolean', - description: - 'For branchall only. If true, failure in this branch does not fail the entire flow. Default: false' - } - }, - required: ['containerId'] - } - } -} - -/** - * Implementation for add_branch tool. - * Adds a new branch with empty modules array to a branchone or branchall container. - */ -async function addBranchImpl({ - helpers, - args -}: { - args: { containerId: string; summary?: string; expr?: string; skip_failure?: boolean } - helpers: FlowAIChatHelpers -}): Promise { - const { containerId, summary = '', expr, skip_failure = false } = args - - const flow = helpers.getFlowAndSelectedId().flow - const container = findModuleInFlow(flow.value.modules, containerId) - - if (!container) { - return `Error: Container with ID '${containerId}' not found in flow` - } - - if (container.value.type !== 'branchone' && container.value.type !== 'branchall') { - return `Error: Module '${containerId}' is not a branchone or branchall container (type: ${container.value.type})` - } - - // Add branch to the container - if (container.value.type === 'branchone') { - const newBranch = { - summary: summary, - expr: expr || 'false', - modules: [] as FlowModule[] - } - container.value.branches = [...(container.value.branches || []), newBranch] - const branchIndex = container.value.branches.length - 1 - helpers.setFlowJson(JSON.stringify(flow)) - return `Added branch ${branchIndex} to branchone '${containerId}' with expr: "${newBranch.expr}". Use add_module({ insideId: "${containerId}", branchPath: "branches.${branchIndex}", value: {...} }) to add modules inside this branch.` - } else { - // branchall - const newBranch = { - summary: summary, - skip_failure: skip_failure, - modules: [] as FlowModule[] - } - container.value.branches = [...(container.value.branches || []), newBranch] - const branchIndex = container.value.branches.length - 1 - helpers.setFlowJson(JSON.stringify(flow)) - return `Added branch ${branchIndex} to branchall '${containerId}'. Use add_module({ insideId: "${containerId}", branchPath: "branches.${branchIndex}", value: {...} }) to add modules inside this branch.` - } -} - -/** - * Additional system prompt content for container module creation - */ -const CONTAINER_MODULE_INSTRUCTIONS = ` - -## Creating Container Modules (Loops, Branches, AI Agents) - -IMPORTANT: When creating container modules, you MUST use a multi-step process: - -1. **First**: Add the container module with an EMPTY modules/branches/tools array -2. **For branches**: Use the \`add_branch\` tool to add branches to branchone/branchall -3. **Then**: Use separate \`add_module\` calls to add modules inside the container - -### For Loops (forloopflow) -\`\`\`javascript -// Step 1: Create the loop container (modules MUST be empty []) -add_module({ afterId: "previous_step", value: { - id: "my_loop", - value: { type: "forloopflow", modules: [], iterator: { type: "javascript", expr: "results.step_a" }, skip_failures: false } -}}) - -// Step 2: Add modules inside the loop -add_module({ insideId: "my_loop", branchPath: "modules", value: { id: "step_in_loop", value: { type: "rawscript", ... } }}) -\`\`\` - -### While Loops (whileloopflow) -\`\`\`javascript -// Step 1: Create with empty modules -add_module({ afterId: "previous_step", value: { - id: "my_while", - value: { type: "whileloopflow", modules: [], skip_failures: false } -}}) - -// Step 2: Add modules inside (use stop_after_if to control loop termination) -add_module({ insideId: "my_while", branchPath: "modules", value: { - id: "check_condition", - stop_after_if: { expr: "result.done === true" }, - value: { type: "rawscript", ... } -}}) -\`\`\` - -### Conditional Branches (branchone) -\`\`\`javascript -// Step 1: Create branch container with empty arrays -add_module({ afterId: "previous_step", value: { - id: "my_branch", - value: { type: "branchone", branches: [], default: [] } -}}) - -// Step 2: Use add_branch to add conditional branches -add_branch({ containerId: "my_branch", summary: "Condition 1", expr: "results.step_a > 10" }) -add_branch({ containerId: "my_branch", summary: "Condition 2", expr: "results.step_a < 0" }) - -// Step 3: Add modules inside each branch (branches.0, branches.1, etc.) -add_module({ insideId: "my_branch", branchPath: "branches.0", value: { id: "step_if_positive", value: {...} }}) -add_module({ insideId: "my_branch", branchPath: "branches.1", value: { id: "step_if_negative", value: {...} }}) - -// Step 4: Add modules to default branch (executed if no conditions match) -add_module({ insideId: "my_branch", branchPath: "default", value: { id: "step_default", value: {...} }}) -\`\`\` - -### Parallel Branches (branchall) -\`\`\`javascript -// Step 1: Create with empty branches -add_module({ afterId: "previous_step", value: { - id: "parallel_tasks", - value: { type: "branchall", branches: [], parallel: true } -}}) - -// Step 2: Use add_branch to add parallel branches -add_branch({ containerId: "parallel_tasks", summary: "Branch A" }) -add_branch({ containerId: "parallel_tasks", summary: "Branch B", skip_failure: true }) - -// Step 3: Add modules to each branch (branches.0, branches.1, etc.) -add_module({ insideId: "parallel_tasks", branchPath: "branches.0", value: { id: "task_a", value: {...} }}) -add_module({ insideId: "parallel_tasks", branchPath: "branches.1", value: { id: "task_b", value: {...} }}) -\`\`\` - -### AI Agents (aiagent) -\`\`\`javascript -// Step 1: Create agent with empty tools -add_module({ afterId: "previous_step", value: { - id: "my_agent", - value: { - type: "aiagent", - tools: [], - input_transforms: { - provider: { type: "static", value: "openai" }, - user_message: { type: "javascript", expr: "flow_input.question" }, - output_type: { type: "static", value: "text" } - } - } -}}) - -// Step 2: Add tools to the agent -add_module({ insideId: "my_agent", branchPath: "tools", value: { - id: "search_tool", - summary: "search_database", - value: { tool_type: "flowmodule", type: "rawscript", language: "bun", content: "...", input_transforms: {} } -}}) -\`\`\` -` - -/** - * Full system prompt for the no-full-schema variant - */ -const NO_FULL_SCHEMA_SYSTEM_PROMPT = `You are a helpful assistant that creates and edits workflows on the Windmill platform. - -## IMPORTANT RULES - -**Reserved IDs - Do NOT use these in add_module, modify_module, or remove_module:** -- \`failure\` - Reserved for failure handler module -- \`preprocessor\` - Reserved for preprocessor module -- \`Input\` - Reserved for flow input reference - -## Tool Selection Guide - -**Flow Modification:** -- **Add a new module** → \`add_module\` -- **Remove a module** → \`remove_module\` -- **Add a new branch to branchall/branchone** → \`add_branch\` (NOT add_module) -- **Remove a branch from branchall/branchone** → \`remove_branch\` -- **Change module code only** → \`set_module_code\` -- **Change module config/transforms/conditions** → \`modify_module\` -- **Update flow input parameters** → \`set_flow_schema\` - -**Code & Scripts:** -- **View existing inline script code** → \`inspect_inline_script\` -- **Get language-specific coding instructions** → \`get_instructions_for_code_generation\` (call BEFORE writing code) -- **Find workspace scripts** → \`search_scripts\` -- **Find Windmill Hub scripts** → \`search_hub_scripts\` - -**Testing:** -- **Test entire flow** → \`test_run_flow\` -- **Test single step** → \`test_run_step\` - -**Resources & Schema:** -- **Search resource types** → \`resource_type\` -- **Get database schema** → \`get_db_schema\` - -## Common Mistakes to Avoid - -- **Don't use \`modify_module\` to add/remove nested modules** - Use \`add_module\`/\`remove_module\` instead -- **Don't forget \`input_transforms\`** - Rawscript parameters won't receive values without them -- **Don't use spaces in module IDs** - Use underscores (e.g., \`fetch_data\` not \`fetch data\`) -- **Don't reference future steps** - \`results.step_id\` only works for steps that execute before the current one -- **Don't create duplicate IDs** - Each module ID must be unique in the flow. Always generate fresh, unique IDs for new modules. Never reuse IDs from existing or previously removed modules -- **Don't provide nested modules directly** - Always create containers with empty arrays, then add modules via separate add_module calls -${CONTAINER_MODULE_INSTRUCTIONS} -## User Instructions - -Follow the user instructions carefully. -At the end of your changes, explain precisely what you did and what the flow does now. -ALWAYS test your modifications. You have access to the \`test_run_flow\` and \`test_run_step\` tools to test the flow and steps. If you only modified a single step, use the \`test_run_step\` tool to test it. If you modified the flow, use the \`test_run_flow\` tool to test it. If the user cancels the test run, do not try again and wait for the next user instruction. -When testing steps that are sql scripts, the arguments to be passed are { database: $res: }. - -### Inline Script References (Token Optimization) - -To reduce token usage, rawscript content in the flow you receive is replaced with references in the format \`inline_script.{module_id}\`. For example: - -\`\`\`json -{ - "id": "step_a", - "value": { - "type": "rawscript", - "content": "inline_script.step_a", - "language": "bun" - } -} -\`\`\` - -**To modify existing script code:** -- Use \`set_module_code\` tool for code-only changes: \`set_module_code({ moduleId: "step_a", code: "..." })\` - -**To add a new inline script module:** -- Use \`add_module\` with the full code content directly (not a reference) -- Avoid coding in single lines, always use multi-line code blocks. -- The system will automatically store and optimize it - -**To inspect existing code:** -- Use \`inspect_inline_script\` tool to view the current code: \`inspect_inline_script({ moduleId: "step_a" })\` - -### Input Transforms for Rawscripts - -Rawscript modules use \`input_transforms\` to map function parameters to values. Each key in \`input_transforms\` corresponds to a parameter name in your script's \`main\` function. - -**Transform Types:** -- \`static\`: Fixed value passed directly -- \`javascript\`: Dynamic expression evaluated at runtime - -**Available Variables in JavaScript Expressions:** -- \`flow_input.{property}\` - Access flow input parameters -- \`results.{step_id}\` - Access output from a previous step -- \`flow_input.iter.value\` - Current item when inside a for-loop -- \`flow_input.iter.index\` - Current index when inside a for-loop - -**Example - Rawscript using flow input and previous step result:** -\`\`\`json -{ - "id": "step_b", - "value": { - "type": "rawscript", - "language": "bun", - "content": "export async function main(userId: string, data: any[]) { - return "Hello, world!"; - }", - "input_transforms": { - "userId": { - "type": "javascript", - "expr": "flow_input.user_id" - }, - "data": { - "type": "javascript", - "expr": "results.step_a" - } - } - } -} -\`\`\` - -**Example - Static value:** -\`\`\`json -{ - "input_transforms": { - "limit": { - "type": "static", - "value": 100 - } - } -} -\`\`\` - -**Important:** The parameter names in \`input_transforms\` must match the function parameter names in your script. When you create or modify a rawscript, always define \`input_transforms\` to connect it to flow inputs or results from other steps. - -### Other Key Concepts -- **Resources**: For flow inputs, use type "object" with format "resource-". For step inputs, use "$res:path/to/resource" -- **Module IDs**: Must be unique and valid identifiers. Used to reference results via \`results.step_id\` -- **Module types**: Use 'bun' as default language for rawscript if unspecified - -### 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. - -Always call this tool first when: -- Creating a new rawscript module -- Modifying existing code in a module -- Setting code via \`set_module_code\` - -Example: Before writing TypeScript/Bun code, call \`get_instructions_for_code_generation({ language: "bun" })\` - -### Creating New Steps - -1. **Search for existing scripts first** (unless user explicitly asks to write from scratch): - - First: \`search_scripts\` to find workspace scripts - - Then: \`search_hub_scripts\` (only consider highly relevant results) - - Only create a raw script if no suitable script is found - -2. **Add the module using \`add_module\`:** - - If using existing script: \`add_module({ afterId: "previous_step", value: { id: "new_step", value: { type: "script", path: "f/folder/script" } } })\` - - If creating rawscript: - - Default language is 'bun' if not specified - - **First call \`get_instructions_for_code_generation\` to get the correct code format** - - Include full code in the content field - - Always define \`input_transforms\` to connect parameters to flow inputs or previous step results - -3. **Update flow schema if needed:** - - If your module references flow_input properties that don't exist yet, add them using \`set_flow_schema\` - -### AI Agent Tools - -AI agents can use tools to accomplish tasks. To manage tools for an AI agent: - -- **Adding a tool to an AI agent**: Use \`add_module\` with \`insideId\` set to the agent's ID and \`branchPath: "tools"\` - - Tool order doesn't affect execution, so you can omit \`afterId\` (defaults to inserting at beginning) - - Example: \`add_module({ insideId: "ai_agent_step", branchPath: "tools", value: { id: "search_docs", summary: "Search documentation", value: { tool_type: "flowmodule", type: "rawscript", language: "bun", content: "...", input_transforms: {} } } })\` - -- **Removing a tool from an AI agent**: Use \`remove_module\` with the tool's ID - - The tool will be found and removed from the agent's tools array - -- **Modifying a tool**: Use \`modify_module\` with the tool's ID - - Example: \`modify_module({ id: "search_docs", value: { ... } })\` - -- **Tool IDs**: Cannot contain spaces - use underscores (e.g., \`get_user_data\` not \`get user data\`) -- **Tool summaries**: Unlike other module summaries, tool summaries cannot contain spaces, use underscores instead. - -- **Tool types**: - - \`flowmodule\`: A script/flow that the agent can call (same as regular flow modules but with \`tool_type: "flowmodule"\`) - - \`mcp\`: Reference to an MCP server tool - -**Example - Adding a rawscript tool to an agent:** -\`\`\`json -add_module({ - insideId: "my_agent", - branchPath: "tools", - value: { - id: "fetch_weather", - summary: "Get current weather for a location", - value: { - tool_type: "flowmodule", - type: "rawscript", - language: "bun", - content: "export async function main(location: string) { ... }", - input_transforms: { - location: { type: "static", value: "" } - } - } - } -}) -\`\`\` - -## Resource Types -On Windmill, credentials and configuration are stored in resources. Resource types define the format of the resource. -- Use the \`resource_type\` tool to search for available resource types (e.g. stripe, google, postgresql, etc.) -- If the user needs a resource as flow input, set the property type in the schema to "object" and add a key called "format" set to "resource-nameofresourcetype" (e.g. "resource-stripe") -- If the user wants a specific resource as step input, set the step value to a static string in the format: "$res:path/to/resource" - -### Contexts - -You have access to the following contexts: -- Database schemas: Schema of databases the user is using -- Flow diffs: Diff between current flow and last deployed flow -- Focused flow modules: IDs of modules the user is focused on. Your response should focus on these modules -` - -/** - * Get the production tool by name - */ -function getProductionTool(name: string): Tool | undefined { - return flowTools.find((t) => t.def.function.name === name) -} - -/** - * Build the tools array for the no-full-schema variant. - * Uses all production tools except add_module and modify_module, - * which are replaced with simplified schema versions. - * Also adds a dedicated add_branch tool for clearer branch creation. - */ -function buildNoSchemaTools(): Tool[] { - const productionAddModule = getProductionTool('add_module') - const productionModifyModule = getProductionTool('modify_module') - - if (!productionAddModule || !productionModifyModule) { - throw new Error('Could not find add_module or modify_module in production tools') - } - - // Get all production tools except add_module and modify_module - const otherTools = flowTools.filter( - (t) => t.def.function.name !== 'add_module' && t.def.function.name !== 'modify_module' - ) - - // Create custom tools with simplified schemas but same implementations - const customAddModule: Tool = { - ...productionAddModule, - def: noSchemaAddModuleToolDef - } - - const customModifyModule: Tool = { - ...productionModifyModule, - def: noSchemaModifyModuleToolDef - } - - // Create the add_branch tool with custom implementation - const addBranchTool: Tool = { - def: addBranchToolDef, - fn: addBranchImpl - } - - return [...otherTools, customAddModule, customModifyModule, addBranchTool] -} - -/** - * No-Full-Schema Variant - * - * Uses simplified tool schemas that avoid circular references. - * Container types (loops, branches, agents) must have empty modules arrays, - * and the LLM must use separate add_module calls to add steps inside. - */ -export const NO_FULL_SCHEMA_VARIANT: VariantConfig = { - name: 'no-full-schema', - description: - 'Simplified tool schemas without circular refs. Containers require nested add_module calls.', - systemPrompt: { - type: 'custom', - content: NO_FULL_SCHEMA_SYSTEM_PROMPT - }, - tools: { - type: 'custom', - tools: buildNoSchemaTools() - } -} diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index 4f9b6c5ac9..1c2a7044f8 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -35,16 +35,9 @@ import { import type { ContextElement } from '../context' import type { ExtendedOpenFlow } from '$lib/components/flows/types' import openFlowSchema from './openFlow.json' -import { - resolveSchemaRefs, - collectAllModuleIds, - findModuleInFlow, - addModuleToFlow, - removeModuleFromFlow, - removeBranchFromFlow, - replaceModuleInFlow -} from './utils' import { inlineScriptStore, extractAndReplaceInlineScripts } from './inlineScriptsUtils' +import { flowModulesSchema } from './openFlowZod' +import { collectAllModuleIdsFromArray } from './utils' /** * Helper interface for AI chat flow operations @@ -65,7 +58,10 @@ export interface FlowAIChatHelpers { // ai chat tools setCode: (id: string, code: string) => Promise - setFlowJson: (json: string) => Promise + setFlowJson: ( + modules: FlowModule[] | undefined, + schema: Record | undefined + ) => Promise getFlowInputsSchema: () => Promise> /** Update exprsToSet store for InputTransformForm components (only if module is selected) */ updateExprsToSet: (id: string, inputTransforms: Record) => void @@ -123,114 +119,31 @@ const getInstructionsForCodeGenerationToolDef = createToolDef( 'Get instructions for code generation for a raw script step' ) -const addModuleToolDef: ChatCompletionFunctionTool = { +// Using string for modules and schema because Gemini-2.5-flash performs better with strings (MALFORMED_FUNCTION_CALL errors happens more often with objects) +const setFlowJsonToolDef: ChatCompletionFunctionTool = { type: 'function', function: { - strict: false, - name: 'add_module', + name: 'set_flow_json', description: - "Add a new module to the flow. Use afterId to insert after a specific module (null to insert at the beginning), or insideId+branchPath to insert into branches/loops. Note: The IDs 'failure', 'preprocessor', and 'Input' are reserved and cannot be used.", + 'Set the entire flow by providing the complete flow object. This replaces all existing modules and schema.', + strict: false, parameters: { type: 'object', properties: { - afterId: { - type: ['string', 'null'], - description: - 'ID of the module to insert after. Use null to insert at the beginning. Can be used with insideId+branchPath to specify position within a container.' - }, - insideId: { - type: ['string', 'null'], - description: - 'ID of the container module (branch/loop/branchall/branchone) to insert into. Use with branchPath to add a module inside a container, or with branchPath=null to add a new branch to branchall/branchone.' - }, - branchPath: { - type: ['string', 'null'], - description: - "Path to insert a module inside a container: 'modules' (for loops), 'branches.0'/'branches.1'/etc (to add inside a specific branch), 'default' (for branchone default branch), or 'tools' (for aiagent). Use null with insideId pointing to a branchall/branchone to add a NEW branch (value should be a branch object with summary, modules, etc.)." - }, - value: { - ...resolveSchemaRefs(openFlowSchema.components.schemas.FlowModule, openFlowSchema), - description: 'Complete module object including id, summary, and value fields' - } - }, - required: ['value'] - } - } -} - -const removeModuleSchema = z.object({ - id: z.string().describe('ID of the module to remove') -}) - -const removeModuleToolDef = createToolDef( - removeModuleSchema, - 'remove_module', - "Remove a module from the flow by its ID. Searches recursively through all nested structures. Note: The IDs 'failure', 'preprocessor', and 'Input' are reserved and cannot be removed." -) - -const removeBranchSchema = z.object({ - insideId: z.string().describe('ID of the branchall/branchone container'), - branchIndex: z.number().int().min(0).describe('Index of the branch to remove (0-based)') -}) - -const removeBranchToolDef = createToolDef( - removeBranchSchema, - 'remove_branch', - 'Remove a branch from a branchall/branchone by its index. Use this to delete an entire branch including all modules inside it.' -) - -const modifyModuleToolDef: ChatCompletionFunctionTool = { - type: 'function', - function: { - strict: false, - name: 'modify_module', - description: - "Modify an existing module (full replacement). Use for changing configuration, transforms, or conditions. Not for adding/removing nested modules. Note: The IDs 'failure', 'preprocessor', and 'Input' are reserved and cannot be modified.", - parameters: { - type: 'object', - properties: { - id: { + modules: { type: 'string', - description: 'ID of the module to modify' + description: 'JSON string containing the flow modules' }, - value: { - ...resolveSchemaRefs(openFlowSchema.components.schemas.FlowModule, openFlowSchema), - description: - 'Complete new module object (full replacement). Use this to change module configuration, input_transforms, branch conditions, etc. Do NOT use this to add/remove modules inside branches/loops - use add_module/remove_module for that.' - } - }, - required: ['id', 'value'] - } - } -} - -const setFlowSchemaToolDef: ChatCompletionFunctionTool = { - type: 'function', - function: { - strict: false, - name: 'set_flow_schema', - description: - 'Set or update the flow input schema. Defines what parameters the flow accepts when executed.', - parameters: { - type: 'object', - properties: { schema: { - type: 'object', - description: 'Flow input schema defining the parameters the flow accepts' + type: 'string', + description: 'JSON string containing the flow input schema' } }, - required: ['schema'] + required: [] } } } -/** Restricted module IDs that cannot be used in add/modify/remove operations */ -const RESTRICTED_MODULE_IDS = Object.values(SPECIAL_MODULE_IDS) - -function isRestrictedModuleId(id: string): boolean { - return RESTRICTED_MODULE_IDS.includes(id as (typeof RESTRICTED_MODULE_IDS)[number]) -} - class WorkspaceScriptsSearch { private uf: uFuzzy private workspace: string | undefined = undefined @@ -596,318 +509,98 @@ export const flowTools: Tool[] = [ } }, { - def: { ...addModuleToolDef, function: { ...addModuleToolDef.function, strict: false } }, + def: setFlowJsonToolDef, streamArguments: true, showDetails: true, showFade: true, fn: async ({ args, helpers, toolId, toolCallbacks }) => { - const afterId = (args.afterId ?? null) as string | null - const insideId = (args.insideId ?? null) as string | null - const branchPath = (args.branchPath ?? null) as string | null - let value = args.value + const { modules, schema } = args - // Parse value if it's a JSON string - if (typeof value === 'string') { - try { - value = JSON.parse(value) - } catch (e) { - throw new Error(`Failed to parse value as JSON: ${(e as Error).message}`) - } + let parsedModules: FlowModule[] | undefined + let parsedSchema: Record | undefined + + // Parse JSON strings + try { + parsedModules = modules + ? typeof modules === 'string' + ? JSON.parse(modules) + : modules + : undefined + parsedSchema = schema + ? typeof schema === 'string' + ? JSON.parse(schema) + : schema + : undefined + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e) + throw new Error(`Invalid JSON: ${errorMessage}`) } - // Validation - // branchPath can be null when adding a new branch to branchall/branchone - // In that case, value should be a branch object with summary, modules, etc. - const isAddingNewBranch = insideId && branchPath === null + // Validate modules against OpenFlow schema + if (parsedModules) { + const result = flowModulesSchema.safeParse(parsedModules) + if (!result.success) { + const errors = result.error.errors.slice(0, 5).map((e) => { + const path = e.path + // Try to find module id for better context + const moduleIndex = typeof path[0] === 'number' ? path[0] : undefined + const moduleId = moduleIndex !== undefined ? parsedModules[moduleIndex]?.id : undefined + const fieldPath = path.slice(1).join('.') - if (!isAddingNewBranch) { - // Adding a regular module - requires id - if (!value.id) { - throw new Error('Module value must include an id field') - } - // Check for restricted IDs - if (isRestrictedModuleId(value.id)) { - throw new Error(`Restricted id '${value.id}', can't be used, should choose an other`) - } - } - - const statusMessage = isAddingNewBranch - ? `Adding new branch to '${insideId}'...` - : `Adding module '${value.id}'...` - toolCallbacks.setToolStatus(toolId, { content: statusMessage }) - - const { flow } = helpers.getFlowAndSelectedId() - - let processedValue = value - - // When adding a branch (not a module), skip ID checks and inline script handling - if (!isAddingNewBranch) { - // Check for duplicate IDs (including nested modules) - const allNewIds = collectAllModuleIds(processedValue as FlowModule) - for (const newId of allNewIds) { - const existing = findModuleInFlow(flow.value.modules, newId) - if (existing) { - throw new Error( - `Module with id '${newId}' already exists in the flow. Each module ID must be unique.` - ) - } - } - - // Handle inline script storage if this is a rawscript with full content - if ( - processedValue.value?.type === 'rawscript' && - processedValue.value?.content && - !processedValue.value.content.startsWith('inline_script.') - ) { - // Store the content and replace with reference - inlineScriptStore.set(processedValue.id, processedValue.value.content) - processedValue = { - ...processedValue, - value: { - ...processedValue.value, - content: `inline_script.${processedValue.id}` + let message = e.message + if (e.code === 'invalid_type') { + message = `expected ${(e as any).expected}, got ${(e as any).received}` } + + if (moduleId) { + return `Module "${moduleId}" -> ${fieldPath}: ${message}` + } + return `${path.join('.')}: ${message}` + }) + + throw new Error(`Invalid flow modules:\n${errors.join('\n')}`) + } else { + // check for duplicate ids + const ids = collectAllModuleIdsFromArray(parsedModules) + if (ids.length !== new Set(ids).size) { + throw new Error('Duplicate module IDs found in flow') } } } - // Add the module - const updatedModules = addModuleToFlow( - flow.value.modules, - afterId, - insideId, - branchPath, - processedValue as FlowModule - ) + toolCallbacks.setToolStatus(toolId, { + content: `Setting flow...` + }) + await helpers.setFlowJson(parsedModules, parsedSchema) - // Apply via setFlowJson to trigger proper snapshot and diff tracking - const updatedFlow = { - ...flow.value, - modules: updatedModules - } - - await helpers.setFlowJson(JSON.stringify(updatedFlow)) - - // Update exprsToSet if this module is selected and has input_transforms - if (value.id && value.value?.input_transforms) { - helpers.updateExprsToSet(value.id, value.value.input_transforms) + // Update exprsToSet if the selected module has input_transforms + if (parsedModules) { + const { selectedId } = helpers.getFlowAndSelectedId() + const selectedModule = findModuleById(parsedModules, selectedId) + if ( + selectedModule && + 'input_transforms' in selectedModule.value && + selectedModule.value.input_transforms + ) { + helpers.updateExprsToSet(selectedId, selectedModule.value.input_transforms) + } } toolCallbacks.setToolStatus(toolId, { - content: `Module '${value.id}' added successfully`, + content: `Flow updated`, result: 'Success' }) - return `Module '${value.id}' has been added to the flow.` - } - }, - { - def: { ...removeModuleToolDef, function: { ...removeModuleToolDef.function, strict: false } }, - fn: async ({ args, helpers, toolId, toolCallbacks }) => { - const parsedArgs = removeModuleSchema.parse(args) - const { id } = parsedArgs - - // Check for restricted IDs - if (isRestrictedModuleId(id)) { - throw new Error(`Restricted id '${id}', can't be used, should choose an other`) - } - - toolCallbacks.setToolStatus(toolId, { content: `Removing module '${id}'...` }) - - const { flow } = helpers.getFlowAndSelectedId() - - // Check module exists - const existing = findModuleInFlow(flow.value.modules, id) - if (!existing) { - throw new Error(`Module with id '${id}' not found`) - } - - // Remove the module - const updatedModules = removeModuleFromFlow(flow.value.modules, id) - - // Apply via setFlowJson to trigger proper snapshot and diff tracking - const updatedFlow = { - ...flow.value, - modules: updatedModules - } - - await helpers.setFlowJson(JSON.stringify(updatedFlow)) - - toolCallbacks.setToolStatus(toolId, { content: `Module '${id}' removed successfully` }) - return `Module '${id}' has been removed from the flow.` - } - }, - { - def: removeBranchToolDef, - fn: async ({ args, helpers, toolId, toolCallbacks }) => { - const parsedArgs = removeBranchSchema.parse(args) - const { insideId, branchIndex } = parsedArgs - - toolCallbacks.setToolStatus(toolId, { - content: `Removing branch ${branchIndex} from '${insideId}'...` - }) - - const { flow } = helpers.getFlowAndSelectedId() - - // Check container exists - const container = findModuleInFlow(flow.value.modules, insideId) - if (!container) { - throw new Error(`Container module with id '${insideId}' not found`) - } - - // Validate it's a branchall/branchone - if (container.value.type !== 'branchall' && container.value.type !== 'branchone') { - throw new Error( - `Module '${insideId}' is not a branchall/branchone (type: ${container.value.type})` - ) - } - - // Remove the branch - const updatedModules = removeBranchFromFlow(flow.value.modules, insideId, branchIndex) - - // Apply via setFlowJson - const updatedFlow = { - ...flow.value, - modules: updatedModules - } - - await helpers.setFlowJson(JSON.stringify(updatedFlow)) - - toolCallbacks.setToolStatus(toolId, { - content: `Branch ${branchIndex} removed from '${insideId}'` - }) - return `Branch ${branchIndex} has been removed from '${insideId}'.` - } - }, - { - def: { ...modifyModuleToolDef, function: { ...modifyModuleToolDef.function, strict: false } }, - streamArguments: true, - showDetails: true, - showFade: true, - fn: async ({ args, helpers, toolId, toolCallbacks }) => { - let { id, value } = args - - // Check for restricted IDs - if (isRestrictedModuleId(id)) { - throw new Error(`Restricted id '${id}', can't be used, should choose an other`) - } - - // Parse value if it's a JSON string - if (typeof value === 'string') { - try { - value = JSON.parse(value) - } catch (e) { - throw new Error(`Failed to parse value as JSON: ${(e as Error).message}`) - } - } - - toolCallbacks.setToolStatus(toolId, { content: `Modifying module '${id}'...` }) - - const { flow } = helpers.getFlowAndSelectedId() - - // Check module exists - const existing = findModuleInFlow(flow.value.modules, id) - if (!existing) { - throw new Error(`Module with id '${id}' not found`) - } - - // Handle inline script storage if this is a rawscript with full content - let processedValue = value - if ( - processedValue.value?.type === 'rawscript' && - processedValue.value?.content && - !processedValue.value.content.startsWith('inline_script.') - ) { - // Store the content and replace with reference - inlineScriptStore.set(id, processedValue.value.content) - processedValue = { - ...processedValue, - value: { - ...processedValue.value, - content: `inline_script.${id}` - } - } - } - - // Replace the module - const updatedModules = replaceModuleInFlow( - flow.value.modules, - id, - processedValue as FlowModule - ) - - // Apply via setFlowJson to trigger proper snapshot and diff tracking - const updatedFlow = { - ...flow.value, - modules: updatedModules - } - - await helpers.setFlowJson(JSON.stringify(updatedFlow)) - - // Update exprsToSet if this module is selected and has input_transforms - if (value.value?.input_transforms) { - helpers.updateExprsToSet(id, value.value.input_transforms) - } - - toolCallbacks.setToolStatus(toolId, { - content: `Module '${id}' modified successfully`, - result: 'Success' - }) - return `Module '${id}' has been modified.` - } - }, - { - def: { ...setFlowSchemaToolDef, function: { ...setFlowSchemaToolDef.function, strict: false } }, - fn: async ({ args, helpers, toolId, toolCallbacks }) => { - let { schema } = args - - // If schema is a JSON string, parse it to an object - if (typeof schema === 'string') { - try { - schema = JSON.parse(schema) - } catch (e) { - // If it fails to parse, keep it as-is and let it fail downstream - console.warn('SCHEMA failed to parse as JSON string', e) - } - } - - toolCallbacks.setToolStatus(toolId, { content: 'Setting flow input schema...' }) - - const { flow } = helpers.getFlowAndSelectedId() - - // Update the flow with new schema - const updatedFlow = { - ...flow.value, - schema - } - - await helpers.setFlowJson(JSON.stringify(updatedFlow)) - - toolCallbacks.setToolStatus(toolId, { content: 'Flow input schema updated successfully' }) - return 'Flow input schema has been updated.' + return `Flow updated` } } ] -/** - * Formats the OpenFlow schema for inclusion in the AI system prompt. - * Extracts only the component schemas and formats them as JSON for the AI to reference. - */ -export function formatOpenFlowSchemaForPrompt(): string { - const schemas = openFlowSchema.components?.schemas - if (!schemas) { - return 'Schema not available' - } - - // Create a simplified schema reference that's easier for the AI to parse - return JSON.stringify(schemas, null, 2) -} - export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionSystemMessageParam { let content = `You are a helpful assistant that creates and edits workflows on the Windmill platform. ## IMPORTANT RULES -**Reserved IDs - Do NOT use these in add_module, modify_module, or remove_module:** +**Reserved IDs - Do NOT use these module IDs:** - \`failure\` - Reserved for failure handler module - \`preprocessor\` - Reserved for preprocessor module - \`Input\` - Reserved for flow input reference @@ -915,16 +608,11 @@ export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionS ## Tool Selection Guide **Flow Modification:** -- **Add a new module** → \`add_module\` -- **Remove a module** → \`remove_module\` -- **Add a new branch to branchall/branchone** → \`add_module\` with \`branchPath: null\` -- **Remove a branch from branchall/branchone** → \`remove_branch\` -- **Change module code only** → \`set_module_code\` -- **Change module config/transforms/conditions** → \`modify_module\` -- **Update flow input parameters** → \`set_flow_schema\` +- **Create or modify the entire flow** → \`set_flow_json\` (provide complete modules array and optional schema) **Code & Scripts:** - **View existing inline script code** → \`inspect_inline_script\` +- **Change module code only** → \`set_module_code\` - **Get language-specific coding instructions** → \`get_instructions_for_code_generation\` (call BEFORE writing code) - **Find workspace scripts** → \`search_scripts\` - **Find Windmill Hub scripts** → \`search_hub_scripts\` @@ -939,93 +627,171 @@ export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionS ## Common Mistakes to Avoid -- **Don't use \`modify_module\` to add/remove nested modules** - Use \`add_module\`/\`remove_module\` instead - **Don't forget \`input_transforms\`** - Rawscript parameters won't receive values without them - **Don't use spaces in module IDs** - Use underscores (e.g., \`fetch_data\` not \`fetch data\`) - **Don't reference future steps** - \`results.step_id\` only works for steps that execute before the current one -- **Don't create duplicate IDs** - Each module ID must be unique in the flow. Always generate fresh, unique IDs for new modules. Never reuse IDs from existing or previously removed modules +- **Don't create duplicate IDs** - Each module ID must be unique in the flow -## Flow Modification Tools +## Flow Modification with set_flow_json -### add_module -Add a new module to the flow, or add a new branch to a branchall/branchone. +Use the \`set_flow_json\` tool to set the entire flow structure at once. Provide the complete modules array and optionally the flow input schema. **Parameters:** -- \`afterId\`: ID of module to insert after, or \`null\` to insert at beginning -- \`insideId\` + \`branchPath\`: For inserting into containers (branches/loops/AI agents) -- \`insideId\` + \`branchPath: null\`: For adding a NEW branch to branchall/branchone -- \`value\`: The module object (or branch object when adding a new branch) +- \`modules\`: Array of flow modules (required) +- \`schema\`: Flow input schema in JSON Schema format (optional) -**Valid \`branchPath\` values:** -- \`"modules"\` - for forloopflow/whileloopflow -- \`"branches.0"\`, \`"branches.1"\`, etc. - to add inside a specific branch -- \`"default"\` - for branchone only -- \`"tools"\` - for aiagent -- \`null\` - to add a NEW branch to branchall/branchone - -**Examples:** +**Example - Simple flow:** \`\`\`javascript -// Insert after step_a -add_module({ afterId: "step_a", value: {...} }) - -// Insert at beginning of flow -add_module({ afterId: null, value: {...} }) - -// Insert into first branch, at beginning -add_module({ insideId: "branch_step", branchPath: "branches.0", afterId: null, value: {...} }) - -// Insert into first branch, after step_x -add_module({ insideId: "branch_step", branchPath: "branches.0", afterId: "step_x", value: {...} }) - -// Insert into loop -add_module({ insideId: "loop_step", branchPath: "modules", afterId: null, value: {...} }) - -// Add a NEW branch to branchall (branchPath: null) -add_module({ insideId: "my_branchall", branchPath: null, value: { summary: "New Branch", skip_failure: false, modules: [] } }) - -// Add a NEW branch to branchone (branchPath: null) -add_module({ insideId: "my_branchone", branchPath: null, value: { summary: "New Condition", expr: "results.step_a > 10", modules: [] } }) +set_flow_json({ + modules: [ + { + id: "fetch_data", + summary: "Fetch user data from API", + value: { + type: "rawscript", + language: "bun", + content: "export async function main(userId: string) { return { id: userId, name: 'John' }; }", + input_transforms: { + userId: { type: "javascript", expr: "flow_input.user_id" } + } + } + }, + { + id: "process_data", + summary: "Process the fetched data", + value: { + type: "rawscript", + language: "bun", + content: "export async function main(data: any) { return { processed: true, ...data }; }", + input_transforms: { + data: { type: "javascript", expr: "results.fetch_data" } + } + } + } + ], + schema: { + type: "object", + properties: { + user_id: { type: "string", description: "User ID to fetch" } + }, + required: ["user_id"] + } +}) \`\`\` -### remove_module -Remove a module by ID. +**Example - Flow with for loop:** \`\`\`javascript -remove_module({ id: "step_b" }) +set_flow_json({ + modules: [ + { + id: "get_items", + summary: "Get list of items", + value: { + type: "rawscript", + language: "bun", + content: "export async function main() { return [1, 2, 3]; }", + input_transforms: {} + } + }, + { + id: "loop_items", + summary: "Process each item", + value: { + type: "forloopflow", + iterator: { type: "javascript", expr: "results.get_items" }, + skip_failures: false, + parallel: true, + modules: [ + { + id: "process_item", + summary: "Process single item", + value: { + type: "rawscript", + language: "bun", + content: "export async function main(item: number) { return item * 2; }", + input_transforms: { + item: { type: "javascript", expr: "flow_input.iter.value" } + } + } + } + ] + } + } + ] +}) \`\`\` -### remove_branch -Remove a branch from a branchall/branchone by its index (0-based). +**Example - Flow with branches (branchone):** \`\`\`javascript -// Remove the first branch (index 0) from a branchall -remove_branch({ insideId: "my_branchall", branchIndex: 0 }) - -// Remove the second branch (index 1) from a branchone -remove_branch({ insideId: "my_branchone", branchIndex: 1 }) -\`\`\` -**Note:** This removes the entire branch including all modules inside it. - -### modify_module -Update an existing module (full replacement). Use for changing configuration, input_transforms, branch conditions, etc. -Do NOT use for adding/removing nested modules - use add_module/remove_module instead. -\`\`\`javascript -modify_module({ id: "step_a", value: {...} }) -\`\`\` - -### set_module_code -Modify only the code of an existing inline script module. Use for quick code-only changes. -\`\`\`javascript -set_module_code({ moduleId: "step_a", code: "..." }) -\`\`\` - -### set_flow_schema -Set/update flow input parameters. -\`\`\`javascript -set_flow_schema({ schema: { type: "object", properties: { user_id: { type: "string" } }, required: ["user_id"] } }) +set_flow_json({ + modules: [ + { + id: "get_value", + summary: "Get a value to branch on", + value: { + type: "rawscript", + language: "bun", + content: "export async function main() { return 50; }", + input_transforms: {} + } + }, + { + id: "branch_on_value", + summary: "Branch based on value", + value: { + type: "branchone", + branches: [ + { + summary: "High value", + expr: "results.get_value > 75", + modules: [ + { + id: "high_handler", + value: { + type: "rawscript", + language: "bun", + content: "export async function main() { return 'high'; }", + input_transforms: {} + } + } + ] + }, + { + summary: "Medium value", + expr: "results.get_value > 25", + modules: [ + { + id: "medium_handler", + value: { + type: "rawscript", + language: "bun", + content: "export async function main() { return 'medium'; }", + input_transforms: {} + } + } + ] + } + ], + default: [ + { + id: "low_handler", + value: { + type: "rawscript", + language: "bun", + content: "export async function main() { return 'low'; }", + input_transforms: {} + } + } + ] + } + } + ] +}) \`\`\` Follow the user instructions carefully. At the end of your changes, explain precisely what you did and what the flow does now. -ALWAYS test your modifications. You have access to the \`test_run_flow\` and \`test_run_step\` tools to test the flow and steps. If you only modified a single step, use the \`test_run_step\` tool to test it. If you modified the flow, use the \`test_run_flow\` tool to test it. If the user cancels the test run, do not try again and wait for the next user instruction. +ALWAYS test your modifications using the \`test_run_flow\` tool. If the user cancels the test run, do not try again and wait for the next user instruction. When testing steps that are sql scripts, the arguments to be passed are { database: $res: }. ### Inline Script References (Token Optimization) @@ -1046,11 +812,6 @@ To reduce token usage, rawscript content in the flow you receive is replaced wit **To modify existing script code:** - Use \`set_module_code\` tool for code-only changes: \`set_module_code({ moduleId: "step_a", code: "..." })\` -**To add a new inline script module:** -- Use \`add_module\` with the full code content directly (not a reference) -- Avoid coding in single lines, always use multi-line code blocks. -- The system will automatically store and optimize it - **To inspect existing code:** - Use \`inspect_inline_script\` tool to view the current code: \`inspect_inline_script({ moduleId: "step_a" })\` @@ -1075,36 +836,16 @@ Rawscript modules use \`input_transforms\` to map function parameters to values. "value": { "type": "rawscript", "language": "bun", - "content": "export async function main(userId: string, data: any[]) { - return "Hello, world!"; - }", + "content": "export async function main(userId: string, data: any[]) { return 'Hello, world!'; }", "input_transforms": { - "userId": { - "type": "javascript", - "expr": "flow_input.user_id" - }, - "data": { - "type": "javascript", - "expr": "results.step_a" - } + "userId": { "type": "javascript", "expr": "flow_input.user_id" }, + "data": { "type": "javascript", "expr": "results.step_a" } } } } \`\`\` -**Example - Static value:** -\`\`\`json -{ - "input_transforms": { - "limit": { - "type": "static", - "value": 100 - } - } -} -\`\`\` - -**Important:** The parameter names in \`input_transforms\` must match the function parameter names in your script. When you create or modify a rawscript, always define \`input_transforms\` to connect it to flow inputs or results from other steps. +**Important:** The parameter names in \`input_transforms\` must match the function parameter names in your script. ### Other Key Concepts - **Resources**: For flow inputs, use type "object" with format "resource-". For step inputs, use "$res:path/to/resource" @@ -1115,73 +856,58 @@ Rawscript modules use \`input_transforms\` to map function parameters to values. **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. -Always call this tool first when: -- Creating a new rawscript module -- Modifying existing code in a module -- Setting code via \`set_module_code\` - Example: Before writing TypeScript/Bun code, call \`get_instructions_for_code_generation({ language: "bun" })\` -### Creating New Steps +### Creating Flows 1. **Search for existing scripts first** (unless user explicitly asks to write from scratch): - First: \`search_scripts\` to find workspace scripts - Then: \`search_hub_scripts\` (only consider highly relevant results) - - Only create a raw script if no suitable script is found + - Only create raw scripts if no suitable script is found -2. **Add the module using \`add_module\`:** - - If using existing script: \`add_module({ afterId: "previous_step", value: { id: "new_step", value: { type: "script", path: "f/folder/script" } } })\` - - If creating rawscript: - - Default language is 'bun' if not specified - - **First call \`get_instructions_for_code_generation\` to get the correct code format** - - Include full code in the content field - - Always define \`input_transforms\` to connect parameters to flow inputs or previous step results +2. **Build the complete flow using \`set_flow_json\`:** + - If using existing script: use \`type: "script"\` with \`path\` + - If creating rawscript: use \`type: "rawscript"\` with \`language\` and \`content\` + - **First call \`get_instructions_for_code_generation\` to get the correct code format** + - Always define \`input_transforms\` to connect parameters to flow inputs or previous step results -3. **Update flow schema if needed:** - - If your module references flow_input properties that don't exist yet, add them using \`set_flow_schema\` +### AI Agent Modules -### AI Agent Tools +AI agents can use tools to accomplish tasks. When creating an AI agent module: -AI agents can use tools to accomplish tasks. To manage tools for an AI agent: - -- **Adding a tool to an AI agent**: Use \`add_module\` with \`insideId\` set to the agent's ID and \`branchPath: "tools"\` - - Tool order doesn't affect execution, so you can omit \`afterId\` (defaults to inserting at beginning) - - Example: \`add_module({ insideId: "ai_agent_step", branchPath: "tools", value: { id: "search_docs", summary: "Search documentation", value: { tool_type: "flowmodule", type: "rawscript", language: "bun", content: "...", input_transforms: {} } } })\` - -- **Removing a tool from an AI agent**: Use \`remove_module\` with the tool's ID - - The tool will be found and removed from the agent's tools array - -- **Modifying a tool**: Use \`modify_module\` with the tool's ID - - Example: \`modify_module({ id: "search_docs", value: { ... } })\` - -- **Tool IDs**: Cannot contain spaces - use underscores (e.g., \`get_user_data\` not \`get user data\`) -- **Tool summaries**: Unlike other module summaries, tool summaries cannot contain spaces, use underscores instead. - -- **Tool types**: - - \`flowmodule\`: A script/flow that the agent can call (same as regular flow modules but with \`tool_type: "flowmodule"\`) - - \`mcp\`: Reference to an MCP server tool - -**Example - Adding a rawscript tool to an agent:** -\`\`\`json -add_module({ - insideId: "my_agent", - branchPath: "tools", +\`\`\`javascript +{ + id: "support_agent", + summary: "AI agent for customer support", value: { - id: "fetch_weather", - summary: "Get current weather for a location", - value: { - tool_type: "flowmodule", - type: "rawscript", - language: "bun", - content: "export async function main(location: string) { ... }", - input_transforms: { - location: { type: "static", value: "" } + type: "aiagent", + input_transforms: { + provider: { type: "static", value: "$res:f/ai_providers/openai" }, + output_type: { type: "static", value: "text" }, + user_message: { type: "javascript", expr: "flow_input.query" }, + system_prompt: { type: "static", value: "You are a helpful assistant." } + }, + tools: [ + { + id: "search_docs", + summary: "Search_documentation", + value: { + tool_type: "flowmodule", + type: "rawscript", + language: "bun", + content: "export async function main(query: string) { return ['doc1', 'doc2']; }", + input_transforms: { query: { type: "static", value: "" } } + } } - } + ] } -}) +} \`\`\` +- **Tool IDs**: Cannot contain spaces - use underscores +- **Tool summaries**: Cannot contain spaces - use underscores +- **Tool types**: \`flowmodule\` for scripts/flows, \`mcp\` for MCP server tools + ## Resource Types On Windmill, credentials and configuration are stored in resources. Resource types define the format of the resource. - Use the \`resource_type\` tool to search for available resource types (e.g. stripe, google, postgresql, etc.) @@ -1192,7 +918,7 @@ On Windmill, credentials and configuration are stored in resources. Resource typ Below is the complete OpenAPI schema for OpenFlow. All field descriptions and behaviors are defined here. Refer to this as the authoritative reference when generating flow JSON: \`\`\`json -${formatOpenFlowSchemaForPrompt()} +${JSON.stringify(openFlowSchema, null, 2)} \`\`\` The schema includes detailed descriptions for: diff --git a/frontend/src/lib/components/copilot/chat/flow/openFlow.json b/frontend/src/lib/components/copilot/chat/flow/openFlow.json index 9815a90252..7688dec45c 100644 --- a/frontend/src/lib/components/copilot/chat/flow/openFlow.json +++ b/frontend/src/lib/components/copilot/chat/flow/openFlow.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"version":"1.583.3","title":"OpenFlow Spec","contact":{"name":"Ruben Fiszel","email":"ruben@windmill.dev","url":"https://windmill.dev"},"license":{"name":"Apache 2.0","url":"https://www.apache.org/licenses/LICENSE-2.0.html"}},"paths":{},"externalDocs":{"description":"documentation portal","url":"https://windmill.dev"},"components":{"schemas":{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"number","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"flow_env":{"type":"object","description":"Environment variables available to all steps","additionalProperties":{"type":"string"}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","description":"Custom error message shown when stopping"}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"}],"discriminator":{"propertyName":"type"}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type"}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake"]},"access_type":{"type":"string","description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"}]},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/InputTransform"},"output_type":{"$ref":"#/components/schemas/InputTransform"},"user_message":{"$ref":"#/components/schemas/InputTransform"},"system_prompt":{"$ref":"#/components/schemas/InputTransform"},"streaming":{"$ref":"#/components/schemas/InputTransform"},"messages_context_length":{"$ref":"#/components/schemas/InputTransform"},"output_schema":{"$ref":"#/components/schemas/InputTransform"},"user_images":{"$ref":"#/components/schemas/InputTransform"},"max_completion_tokens":{"$ref":"#/components/schemas/InputTransform"},"temperature":{"$ref":"#/components/schemas/InputTransform"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}}} \ No newline at end of file +{"openapi":"3.0.3","info":{"version":"1.590.0","title":"OpenFlow Spec","contact":{"name":"Ruben Fiszel","email":"ruben@windmill.dev","url":"https://windmill.dev"},"license":{"name":"Apache 2.0","url":"https://www.apache.org/licenses/LICENSE-2.0.html"}},"paths":{},"externalDocs":{"description":"documentation portal","url":"https://windmill.dev"},"components":{"schemas":{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"number","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps","additionalProperties":{"type":"string"}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","description":"Custom error message shown when stopping"}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"}],"discriminator":{"propertyName":"type"}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type"}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable"]},"access_type":{"type":"string","description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"}]},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/InputTransform"},"output_type":{"$ref":"#/components/schemas/InputTransform"},"user_message":{"$ref":"#/components/schemas/InputTransform"},"system_prompt":{"$ref":"#/components/schemas/InputTransform"},"streaming":{"$ref":"#/components/schemas/InputTransform"},"messages_context_length":{"$ref":"#/components/schemas/InputTransform"},"output_schema":{"$ref":"#/components/schemas/InputTransform"},"user_images":{"$ref":"#/components/schemas/InputTransform"},"max_completion_tokens":{"$ref":"#/components/schemas/InputTransform"},"temperature":{"$ref":"#/components/schemas/InputTransform"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}}} \ No newline at end of file diff --git a/frontend/src/lib/components/copilot/chat/flow/openFlowZod.ts b/frontend/src/lib/components/copilot/chat/flow/openFlowZod.ts new file mode 100644 index 0000000000..b9e9e3b113 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/flow/openFlowZod.ts @@ -0,0 +1,24 @@ +import { z } from "zod/v3" + +export const flowModuleSchema = z.object({ "id": z.string().describe("Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"), "value": z.discriminatedUnion("type", [z.object({ "input_transforms": z.record(z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs")).describe("Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments"), "content": z.string().describe("The script source code. Should export a 'main' function"), "language": z.enum(["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php"]).describe("Programming language for this script"), "path": z.string().describe("Optional path for saving this script").optional(), "lock": z.string().describe("Lock file content for dependencies").optional(), "type": z.literal("rawscript"), "tag": z.string().describe("Worker group tag for execution routing").optional(), "concurrent_limit": z.number().describe("Maximum concurrent executions of this script").optional(), "concurrency_time_window_s": z.number().describe("Time window for concurrent_limit").optional(), "custom_concurrency_key": z.string().describe("Custom key for grouping concurrent executions").optional(), "is_trigger": z.boolean().describe("If true, this script is a trigger that can start the flow").optional(), "assets": z.array(z.object({ "path": z.string().describe("Path to the asset"), "kind": z.enum(["s3object","resource","ducklake","datatable"]).describe("Type of asset"), "access_type": z.enum(["r","w","rw"]).describe("Access level for this asset").optional(), "alt_access_type": z.enum(["r","w","rw"]).describe("Alternative access level").optional() })).describe("External resources this script accesses (S3 objects, resources, etc.)").optional() }).describe("Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms"), z.object({ "input_transforms": z.record(z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs")).describe("Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments"), "path": z.string().describe("Path to the script in the workspace (e.g., 'f/scripts/send_email')"), "hash": z.string().describe("Optional specific version hash of the script to use").optional(), "type": z.literal("script"), "tag_override": z.string().describe("Override the script's default worker group tag").optional(), "is_trigger": z.boolean().describe("If true, this script is a trigger that can start the flow").optional() }).describe("Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code"), z.object({ "input_transforms": z.record(z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs")).describe("Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments"), "path": z.string().describe("Path to the flow in the workspace (e.g., 'f/flows/process_user')"), "type": z.literal("flow") }).describe("Reference to an existing flow by path. Use this to call another flow as a subflow"), z.object({ "modules": z.array(z.lazy(() => flowModuleSchema)).describe("Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'"), "iterator": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs"), "skip_failures": z.boolean().describe("If true, iteration failures don't stop the loop. Failed iterations return null"), "type": z.literal("forloopflow"), "parallel": z.boolean().describe("If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency").optional(), "parallelism": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "squash": z.boolean().optional() }).describe("Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations"), z.object({ "modules": z.array(z.lazy(() => flowModuleSchema)).describe("Steps to execute in each iteration. Use stop_after_if to control when the loop ends"), "skip_failures": z.boolean().describe("If true, iteration failures don't stop the loop. Failed iterations return null"), "type": z.literal("whileloopflow"), "parallel": z.boolean().describe("If true, iterations run concurrently (use with caution in while loops)").optional(), "parallelism": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "squash": z.boolean().optional() }).describe("Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination"), z.object({ "branches": z.array(z.object({ "summary": z.string().describe("Short description of this branch condition").optional(), "expr": z.string().describe("JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"), "modules": z.array(z.lazy(() => flowModuleSchema)).describe("Steps to execute if this branch's expr is true") })).describe("Array of branches to evaluate in order. The first branch with expr evaluating to true executes"), "default": z.array(z.lazy(() => flowModuleSchema)).describe("Steps to execute if no branch expressions match"), "type": z.literal("branchone") }).describe("Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes"), z.object({ "branches": z.array(z.object({ "summary": z.string().describe("Short description of this branch's purpose").optional(), "skip_failure": z.boolean().describe("If true, failure in this branch doesn't fail the entire flow").optional(), "modules": z.array(z.lazy(() => flowModuleSchema)).describe("Steps to execute in this branch") })).describe("Array of branches that all execute (either in parallel or sequentially)"), "type": z.literal("branchall"), "parallel": z.boolean().describe("If true, all branches execute concurrently. If false, they execute sequentially").optional() }).describe("Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently"), z.object({ "type": z.literal("identity"), "flow": z.boolean().describe("If true, marks this as a flow identity (special handling)").optional() }).describe("Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder"), z.object({ "input_transforms": z.object({ "provider": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs"), "output_type": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs"), "user_message": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs"), "system_prompt": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "streaming": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "messages_context_length": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "output_schema": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "user_images": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "max_completion_tokens": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "temperature": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional() }).describe("Input parameters for the AI agent mapped to their values"), "tools": z.array(z.object({ "id": z.string().describe("Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"), "summary": z.string().describe("Short description of what this tool does (shown to the AI)").optional(), "value": z.any().superRefine((x, ctx) => { + const schemas = [z.intersection(z.object({ "tool_type": z.literal("flowmodule") }), z.lazy(() => flowModuleSchema)).describe("A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module"), z.object({ "tool_type": z.literal("mcp"), "resource_path": z.string().describe("Path to the MCP resource/server configuration"), "include_tools": z.array(z.string()).describe("Whitelist of specific tools to include from this MCP server").optional(), "exclude_tools": z.array(z.string()).describe("Blacklist of tools to exclude from this MCP server").optional() }).describe("Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers")]; + const errors = schemas.reduce( + (errors, schema) => + ((result) => + result.error ? [...errors, result.error] : errors)( + schema.safeParse(x), + ), + [], + ); + if (schemas.length - errors.length !== 1) { + ctx.addIssue({ + path: ctx.path, + code: "invalid_union", + unionErrors: errors, + message: "Invalid input: Should pass single schema", + }); + } + }).describe("The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference") }).describe("A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool")).describe("Array of tools the agent can use. The agent decides which tools to call based on the task"), "type": z.literal("aiagent"), "parallel": z.boolean().describe("If true, the agent can execute multiple tool calls in parallel").optional() }).describe("AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task")]).describe("The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type"), "stop_after_if": z.object({ "skip_if_stopped": z.boolean().describe("If true, following steps are skipped when this condition triggers").optional(), "expr": z.string().describe("JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"), "error_message": z.string().describe("Custom error message shown when stopping").optional() }).describe("Early termination condition for a module").optional(), "stop_after_all_iters_if": z.object({ "skip_if_stopped": z.boolean().describe("If true, following steps are skipped when this condition triggers").optional(), "expr": z.string().describe("JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"), "error_message": z.string().describe("Custom error message shown when stopping").optional() }).describe("Early termination condition for a module").optional(), "skip_if": z.object({ "expr": z.string().describe("JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'") }).describe("Conditionally skip this step based on previous results or flow inputs").optional(), "sleep": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "cache_ttl": z.number().describe("Cache duration in seconds for this step's results").optional(), "cache_ignore_s3_path": z.boolean().optional(), "timeout": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "delete_after_use": z.boolean().describe("If true, this step's result is deleted after use to save memory").optional(), "summary": z.string().describe("Short description of what this step does").optional(), "mock": z.object({ "enabled": z.boolean().describe("If true, return mock value instead of executing").optional(), "return_value": z.any().describe("Value to return when mocked").optional() }).describe("Mock configuration for testing without executing the actual step").optional(), "suspend": z.object({ "required_events": z.number().int().describe("Number of approvals required before continuing").optional(), "timeout": z.number().int().describe("Timeout in seconds before auto-continuing or canceling").optional(), "resume_form": z.object({ "schema": z.record(z.any()).describe("JSON Schema for the resume form").optional() }).describe("Form schema for collecting input when resuming").optional(), "user_auth_required": z.boolean().describe("If true, only authenticated users can approve").optional(), "user_groups_required": z.discriminatedUnion("type", [z.object({ "value": z.any().describe("The static value. For resources, use format '$res:path/to/resource'").optional(), "type": z.literal("static") }).describe("Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'"), z.object({ "expr": z.string().describe("JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"), "type": z.literal("javascript") }).describe("JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value")]).describe("Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs").optional(), "self_approval_disabled": z.boolean().describe("If true, the user who started the flow cannot approve").optional(), "hide_cancel": z.boolean().describe("If true, hide the cancel button on the approval form").optional(), "continue_on_disapprove_timeout": z.boolean().describe("If true, continue flow on timeout instead of canceling").optional() }).describe("Configuration for approval/resume steps that wait for user input").optional(), "priority": z.number().describe("Execution priority for this step (higher numbers run first)").optional(), "continue_on_error": z.boolean().describe("If true, flow continues even if this step fails").optional(), "retry": z.object({ "constant": z.object({ "attempts": z.number().int().describe("Number of retry attempts").optional(), "seconds": z.number().int().describe("Seconds to wait between retries").optional() }).describe("Retry with constant delay between attempts").optional(), "exponential": z.object({ "attempts": z.number().int().describe("Number of retry attempts").optional(), "multiplier": z.number().int().describe("Multiplier for exponential backoff").optional(), "seconds": z.number().int().gte(1).describe("Initial delay in seconds").optional(), "random_factor": z.number().int().gte(0).lte(100).describe("Random jitter percentage (0-100) to avoid thundering herd").optional() }).describe("Retry with exponential backoff (delay doubles each time)").optional(), "retry_if": z.object({ "expr": z.string().describe("JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables") }).describe("Conditional retry based on error or result").optional() }).describe("Retry configuration for failed module executions").optional() }).describe("A single step in a flow. Can be a script, subflow, loop, or branch") + + +export const flowModulesSchema = z.array(flowModuleSchema) diff --git a/frontend/src/lib/components/copilot/chat/flow/utils.ts b/frontend/src/lib/components/copilot/chat/flow/utils.ts index 0fec4c6c8c..965b56edc3 100644 --- a/frontend/src/lib/components/copilot/chat/flow/utils.ts +++ b/frontend/src/lib/components/copilot/chat/flow/utils.ts @@ -66,85 +66,16 @@ export function getIndexInNestedModules( modules: parent } } -export function getNestedModules(flow: OpenFlow, id: string, branchIndex?: number) { - const result = getIndexInNestedModules(flow, id) - if (!result) { - throw new Error(`Module not found: ${id}`) - } - const { index, modules } = result - - // we know index is correct because we've already checked it in getIndexInNestedModules - const module = modules[index] - - if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') { - return module.value.modules - } else if ( - branchIndex !== undefined && - (module.value.type === 'branchall' || module.value.type === 'branchone') - ) { - if (module.value.type === 'branchone' && branchIndex === -1) { - return module.value.default - } - - const branch = module.value.branches[branchIndex] - - if (!branch) { - throw new Error( - `Branch not found: ${id} in ${module.value.branches.map((b) => b.modules.map((m) => m.id).join(', ')).join(';')}` - ) - } - - return branch.modules - } else if (module.value.type === 'aiagent') { - return module.value.tools - } else { - throw new Error('Module is not a loop or branch') - } -} /** - * Recursively resolves all $ref references in a JSON Schema by inlining them. - * This ensures the schema is fully self-contained for AI providers that don't - * support external references or have strict schema validation (e.g., Google/Gemini). - * - * @param schema - The schema object to resolve - * @param rootSchema - The root schema document containing all definitions - * @param visited - Set of visited $ref paths to prevent infinite recursion - * @returns Fully resolved schema with all $ref references inlined + * Collects all module IDs from an array of modules and their nested structures */ -export function resolveSchemaRefs(schema: any, rootSchema: any, visited = new Set()): any { - if (!schema || typeof schema !== 'object') return schema - - // Handle $ref - if (schema.$ref) { - const refPath = schema.$ref.replace('#/', '').split('/') - - // Prevent infinite recursion with circular refs - if (visited.has(schema.$ref)) { - return { type: 'object' } // Fallback for circular refs - } - visited.add(schema.$ref) - - let resolved = rootSchema - for (const part of refPath) { - resolved = resolved[part] - } - - // Recursively resolve the referenced schema - return resolveSchemaRefs(resolved, rootSchema, new Set(visited)) +export function collectAllModuleIdsFromArray(modules: FlowModule[]): string[] { + const ids: string[] = [] + for (const module of modules) { + ids.push(...collectAllModuleIds(module)) } - - // Handle arrays - if (Array.isArray(schema)) { - return schema.map((item) => resolveSchemaRefs(item, rootSchema, visited)) - } - - // Handle objects - recursively process all properties - const result: any = {} - for (const key in schema) { - result[key] = resolveSchemaRefs(schema[key], rootSchema, visited) - } - return result + return ids } /** @@ -194,624 +125,3 @@ export function collectAllModuleIds(module: FlowModule): string[] { return ids } - -/** - * Recursively finds a module by ID in the flow structure - */ -export function findModuleInFlow(modules: FlowModule[], id: string): FlowModule | undefined { - for (const module of modules) { - if (module.id === id) { - return module - } - - // Search in nested structures - if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') { - if (module.value.modules) { - const found = findModuleInFlow(module.value.modules, id) - if (found) return found - } - } else if (module.value.type === 'branchone') { - if (module.value.branches) { - for (const branch of module.value.branches) { - if (branch.modules) { - const found = findModuleInFlow(branch.modules, id) - if (found) return found - } - } - } - if (module.value.default) { - const found = findModuleInFlow(module.value.default, id) - if (found) return found - } - } else if (module.value.type === 'branchall') { - if (module.value.branches) { - for (const branch of module.value.branches) { - if (branch.modules) { - const found = findModuleInFlow(branch.modules, id) - if (found) return found - } - } - } - } else if (module.value.type === 'aiagent') { - // Search in AI agent tools - if (module.value.tools) { - for (const tool of module.value.tools) { - if (tool.id === id) { - // Return a pseudo-FlowModule for compatibility - return { id: tool.id, value: tool.value, summary: tool.summary } as FlowModule - } - } - } - } - } - return undefined -} - -/** - * Recursively removes a module by ID from the flow structure - * Returns the updated modules array - */ -export function removeModuleFromFlow(modules: FlowModule[], id: string): FlowModule[] { - const result: FlowModule[] = [] - - for (const module of modules) { - if (module.id === id) { - // Skip this module (remove it) - continue - } - - const newModule = { ...module } - - // Recursively remove from nested structures - if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') { - if (newModule.value.modules) { - newModule.value = { - ...newModule.value, - modules: removeModuleFromFlow(newModule.value.modules, id) - } - } - } else if (newModule.value.type === 'branchone') { - if (newModule.value.branches) { - newModule.value = { - ...newModule.value, - branches: newModule.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules ? removeModuleFromFlow(branch.modules, id) : [] - })) - } - } - if (newModule.value.default) { - newModule.value = { - ...newModule.value, - default: removeModuleFromFlow(newModule.value.default, id) - } - } - } else if (newModule.value.type === 'branchall') { - if (newModule.value.branches) { - newModule.value = { - ...newModule.value, - branches: newModule.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules ? removeModuleFromFlow(branch.modules, id) : [] - })) - } - } - } else if (newModule.value.type === 'aiagent') { - // Remove tool from AI agent's tools array - if (newModule.value.tools) { - newModule.value = { - ...newModule.value, - tools: newModule.value.tools.filter((tool) => tool.id !== id) - } - } - } - - result.push(newModule) - } - - return result -} - -/** - * Recursively removes a branch by index from a branchall/branchone container - * Returns the updated modules array - */ -export function removeBranchFromFlow( - modules: FlowModule[], - containerId: string, - branchIndex: number -): FlowModule[] { - return modules.map((module) => { - if (module.id === containerId) { - if (module.value.type === 'branchall') { - const branches = module.value.branches || [] - if (branchIndex < 0 || branchIndex >= branches.length) { - throw new Error(`Branch index ${branchIndex} out of bounds (0-${branches.length - 1})`) - } - return { - ...module, - value: { - ...module.value, - branches: branches.filter((_, i) => i !== branchIndex) - } - } as FlowModule - } - if (module.value.type === 'branchone') { - const branches = module.value.branches || [] - if (branchIndex < 0 || branchIndex >= branches.length) { - throw new Error(`Branch index ${branchIndex} out of bounds (0-${branches.length - 1})`) - } - return { - ...module, - value: { - ...module.value, - branches: branches.filter((_, i) => i !== branchIndex) - } - } as FlowModule - } - throw new Error(`Module '${containerId}' is not a branchall/branchone container`) - } - - // Recursively search nested structures - const newModule = { ...module } - if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') { - if (newModule.value.modules) { - newModule.value = { - ...newModule.value, - modules: removeBranchFromFlow(newModule.value.modules, containerId, branchIndex) - } - } - } else if (newModule.value.type === 'branchone') { - if (newModule.value.branches) { - newModule.value = { - ...newModule.value, - branches: newModule.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules - ? removeBranchFromFlow(branch.modules, containerId, branchIndex) - : [] - })) - } - } - if (newModule.value.default) { - newModule.value = { - ...newModule.value, - default: removeBranchFromFlow(newModule.value.default, containerId, branchIndex) - } - } - } else if (newModule.value.type === 'branchall') { - if (newModule.value.branches) { - newModule.value = { - ...newModule.value, - branches: newModule.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules - ? removeBranchFromFlow(branch.modules, containerId, branchIndex) - : [] - })) - } - } - } - - return newModule - }) -} - -/** - * Parses a branch path string into navigation components - * Examples: 'branches.0' -> {type: 'branches', index: 0} - * 'default' -> {type: 'default'} - * 'modules' -> {type: 'modules'} - */ -export function parseBranchPath(path: string): { type: string; index?: number } { - if (path === 'default') { - return { type: 'default' } - } - if (path === 'modules') { - return { type: 'modules' } - } - if (path === 'tools') { - return { type: 'tools' } - } - - const match = path.match(/^(branches)\.(\d+)$/) - if (match) { - return { type: match[1], index: parseInt(match[2], 10) } - } - - throw new Error(`Invalid branch path: ${path}`) -} - -/** - * Gets the target array for module insertion based on insideId and branchPath - */ -function getTargetArray( - modules: FlowModule[], - insideId: string, - branchPath: string -): FlowModule[] | undefined { - const container = findModuleInFlow(modules, insideId) - if (!container) { - return undefined - } - - const parsed = parseBranchPath(branchPath) - - if (container.value.type === 'forloopflow' || container.value.type === 'whileloopflow') { - if (parsed.type === 'modules') { - return container.value.modules || [] - } - throw new Error(`Invalid branchPath '${branchPath}' for loop module. Use 'modules'`) - } else if (container.value.type === 'branchone') { - if (parsed.type === 'branches' && parsed.index !== undefined) { - return container.value.branches?.[parsed.index]?.modules - } else if (parsed.type === 'default') { - return container.value.default - } - throw new Error( - `Invalid branchPath '${branchPath}' for branchone module. Use 'branches.N' or 'default'` - ) - } else if (container.value.type === 'branchall') { - if (parsed.type === 'branches' && parsed.index !== undefined) { - return container.value.branches?.[parsed.index]?.modules - } - throw new Error(`Invalid branchPath '${branchPath}' for branchall module. Use 'branches.N'`) - } else if (container.value.type === 'aiagent') { - if (parsed.type === 'tools') { - // Return tools array (AgentTool[]), caller handles the different structure - return (container.value.tools as any) || [] - } - throw new Error(`Invalid branchPath '${branchPath}' for aiagent module. Use 'tools'`) - } - - throw new Error(`Module '${insideId}' is not a container type`) -} - -/** - * Updates a nested array within a container module - */ -function updateNestedArray( - module: FlowModule, - branchPath: string, - updatedArray: FlowModule[] -): FlowModule { - const parsed = parseBranchPath(branchPath) - const newModule = { ...module } - - if (newModule.value.type === 'forloopflow' || newModule.value.type === 'whileloopflow') { - if (parsed.type === 'modules') { - newModule.value = { - ...newModule.value, - modules: updatedArray - } - } - } else if (newModule.value.type === 'branchone') { - if (parsed.type === 'branches' && parsed.index !== undefined && newModule.value.branches) { - const newBranches = [...newModule.value.branches] - newBranches[parsed.index] = { - ...newBranches[parsed.index], - modules: updatedArray - } - newModule.value = { - ...newModule.value, - branches: newBranches - } - } else if (parsed.type === 'default') { - newModule.value = { - ...newModule.value, - default: updatedArray - } - } - } else if (newModule.value.type === 'branchall') { - if (parsed.type === 'branches' && parsed.index !== undefined && newModule.value.branches) { - const newBranches = [...newModule.value.branches] - newBranches[parsed.index] = { - ...newBranches[parsed.index], - modules: updatedArray - } - newModule.value = { - ...newModule.value, - branches: newBranches - } - } - } else if (newModule.value.type === 'aiagent') { - if (parsed.type === 'tools') { - // Note: updatedArray is actually AgentTool[] when dealing with AI agents - newModule.value = { - ...newModule.value, - tools: updatedArray as any - } - } - } - - return newModule -} - -/** - * Recursively adds a module to the flow structure - */ -export function addModuleToFlow( - modules: FlowModule[], - afterId: string | null, - insideId: string | null, - branchPath: string | null, - newModule: FlowModule -): FlowModule[] { - // Case 1a: Adding a NEW branch to branchall/branchone (insideId set, branchPath null) - if (insideId && branchPath === null) { - return modules.map((module) => { - if (module.id === insideId) { - // Adding a new branch to branchall - if (module.value.type === 'branchall') { - const newBranch = { - summary: (newModule as any).summary || '', - skip_failure: (newModule as any).skip_failure ?? false, - modules: (newModule as any).modules || [] - } - return { - ...module, - value: { - ...module.value, - branches: [...(module.value.branches || []), newBranch] - } - } as FlowModule - } - // Adding a new branch to branchone - if (module.value.type === 'branchone') { - const newBranch = { - summary: (newModule as any).summary || '', - expr: (newModule as any).expr || 'false', - modules: (newModule as any).modules || [] - } - return { - ...module, - value: { - ...module.value, - branches: [...(module.value.branches || []), newBranch] - } - } as FlowModule - } - throw new Error( - `Cannot add branch to module '${insideId}': branchPath=null is only valid for branchall/branchone containers` - ) - } - - // Recursively search nested structures for the target container - const newModuleCopy = { ...module } - if ( - newModuleCopy.value.type === 'forloopflow' || - newModuleCopy.value.type === 'whileloopflow' - ) { - if (newModuleCopy.value.modules) { - newModuleCopy.value = { - ...newModuleCopy.value, - modules: addModuleToFlow( - newModuleCopy.value.modules, - afterId, - insideId, - branchPath, - newModule - ) - } - } - } else if (newModuleCopy.value.type === 'branchone') { - if (newModuleCopy.value.branches) { - newModuleCopy.value = { - ...newModuleCopy.value, - branches: newModuleCopy.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules - ? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule) - : [] - })) - } - } - if (newModuleCopy.value.default) { - newModuleCopy.value = { - ...newModuleCopy.value, - default: addModuleToFlow( - newModuleCopy.value.default, - afterId, - insideId, - branchPath, - newModule - ) - } - } - } else if (newModuleCopy.value.type === 'branchall') { - if (newModuleCopy.value.branches) { - newModuleCopy.value = { - ...newModuleCopy.value, - branches: newModuleCopy.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules - ? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule) - : [] - })) - } - } - } - return newModuleCopy - }) - } - - // Case 1b: Adding inside a container (insideId + branchPath both set) - if (insideId && branchPath) { - return modules.map((module) => { - if (module.id === insideId) { - // Special handling for AI agent tools - if (module.value.type === 'aiagent' && branchPath === 'tools') { - // For AI agents, newModule structure is { id, summary, value: { tool_type, ...FlowModuleValue } } - // The value should already include tool_type from the caller - const newTool = { - id: newModule.id, - summary: newModule.summary, - value: newModule.value as any - } - return { - ...module, - value: { - ...module.value, - tools: [...(module.value.tools || []), newTool] - } - } as FlowModule - } - - const targetArray = getTargetArray(modules, insideId, branchPath) - if (!targetArray) { - throw new Error( - `Cannot find target array for insideId '${insideId}' with branchPath '${branchPath}'` - ) - } - const updatedArray = - afterId !== null - ? addModuleToFlow(targetArray, afterId, null, null, newModule) - : [newModule, ...targetArray] // afterId null = insert at beginning - return updateNestedArray(module, branchPath, updatedArray) - } - - // Recursively search nested structures - const newModuleCopy = { ...module } - if ( - newModuleCopy.value.type === 'forloopflow' || - newModuleCopy.value.type === 'whileloopflow' - ) { - if (newModuleCopy.value.modules) { - newModuleCopy.value = { - ...newModuleCopy.value, - modules: addModuleToFlow( - newModuleCopy.value.modules, - afterId, - insideId, - branchPath, - newModule - ) - } - } - } else if (newModuleCopy.value.type === 'branchone') { - if (newModuleCopy.value.branches) { - newModuleCopy.value = { - ...newModuleCopy.value, - branches: newModuleCopy.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules - ? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule) - : [] - })) - } - } - if (newModuleCopy.value.default) { - newModuleCopy.value = { - ...newModuleCopy.value, - default: addModuleToFlow( - newModuleCopy.value.default, - afterId, - insideId, - branchPath, - newModule - ) - } - } - } else if (newModuleCopy.value.type === 'branchall') { - if (newModuleCopy.value.branches) { - newModuleCopy.value = { - ...newModuleCopy.value, - branches: newModuleCopy.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules - ? addModuleToFlow(branch.modules, afterId, insideId, branchPath, newModule) - : [] - })) - } - } - } - - return newModuleCopy - }) - } - - // Case 2: Adding at current level after a specific module - if (afterId !== null) { - const result: FlowModule[] = [] - for (const module of modules) { - result.push(module) - if (module.id === afterId) { - result.push(newModule) - } - } - return result - } - - // Case 3: afterId is null - insert at the beginning - return [newModule, ...modules] -} - -/** - * Recursively replaces a module by ID - */ -export function replaceModuleInFlow( - modules: FlowModule[], - id: string, - newModule: FlowModule -): FlowModule[] { - return modules.map((module) => { - if (module.id === id) { - return { ...newModule, id } // Ensure ID remains the same - } - - const newModuleCopy = { ...module } - - // Recursively replace in nested structures - if ( - newModuleCopy.value.type === 'forloopflow' || - newModuleCopy.value.type === 'whileloopflow' - ) { - if (newModuleCopy.value.modules) { - newModuleCopy.value = { - ...newModuleCopy.value, - modules: replaceModuleInFlow(newModuleCopy.value.modules, id, newModule) - } - } - } else if (newModuleCopy.value.type === 'branchone') { - if (newModuleCopy.value.branches) { - newModuleCopy.value = { - ...newModuleCopy.value, - branches: newModuleCopy.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules ? replaceModuleInFlow(branch.modules, id, newModule) : [] - })) - } - } - if (newModuleCopy.value.default) { - newModuleCopy.value = { - ...newModuleCopy.value, - default: replaceModuleInFlow(newModuleCopy.value.default, id, newModule) - } - } - } else if (newModuleCopy.value.type === 'branchall') { - if (newModuleCopy.value.branches) { - newModuleCopy.value = { - ...newModuleCopy.value, - branches: newModuleCopy.value.branches.map((branch) => ({ - ...branch, - modules: branch.modules ? replaceModuleInFlow(branch.modules, id, newModule) : [] - })) - } - } - } else if (newModuleCopy.value.type === 'aiagent') { - // Replace tool in AI agent's tools array - if (newModuleCopy.value.tools) { - newModuleCopy.value = { - ...newModuleCopy.value, - tools: newModuleCopy.value.tools.map((tool) => - tool.id === id - ? { id, summary: newModule.summary, value: newModule.value as any } - : tool - ) - } - } - } - - return newModuleCopy - }) -} diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index 5ac32889f4..240b3bb77c 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -857,7 +857,7 @@ export async function parseOpenAICompletion( _abortController?: AbortController // unused, for signature compatibility with parseAnthropicCompletion ): Promise { const finalToolCalls: Record = {} - const streamingTools: Record = {} // Track which tools should stream + let malformedFunctionCallError = false let answer = '' for await (const chunk of completion) { @@ -865,6 +865,17 @@ export async function parseOpenAICompletion( continue } const c = chunk as ChatCompletionChunk + + // Check for malformed function call error (e.g. from Gemini models) + const finishReason = c.choices[0].finish_reason + if ( + finishReason && + typeof finishReason === 'string' && + finishReason.includes('MALFORMED_FUNCTION_CALL') + ) { + malformedFunctionCallError = true + } + const delta = c.choices[0].delta.content if (delta) { answer += delta @@ -915,17 +926,11 @@ export async function parseOpenAICompletion( } = finalToolCall if (funcName && toolCallId) { const tool = tools.find((t) => t.def.function.name === funcName) - - // Track if this tool should stream (only set once per tool) - if (streamingTools[index] === undefined) { - streamingTools[index] = tool?.streamArguments ?? false - } - if (tool && tool.preAction) { tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId }) } - const shouldStream = streamingTools[index] + const shouldStream = tool?.streamArguments ?? false const accumulatedArgs = finalToolCall.function.arguments let parameters: any = undefined if (accumulatedArgs) { @@ -993,6 +998,43 @@ export async function parseOpenAICompletion( messages.push(messageToAdd) addedMessages.push(messageToAdd) } + } else if (malformedFunctionCallError) { + // Malformed function call with no tool calls - create artificial tool call to inform AI + const fakeToolCallId = generateRandomString() + + // Show error status to user + callbacks.setToolStatus(fakeToolCallId, { + isLoading: false, + content: 'Malformed function call', + error: 'Invalid input given to function call', + toolName: 'unknown' + }) + + // Add assistant message with fake tool call + const assistantMessage = { + role: 'assistant' as const, + tool_calls: [ + { + id: fakeToolCallId, + type: 'function' as const, + function: { + name: 'unknown', + arguments: '{}' + } + } + ] + } + messages.push(assistantMessage) + addedMessages.push(assistantMessage) + + // Add tool response telling AI to retry + const toolResponse = { + role: 'tool' as const, + tool_call_id: fakeToolCallId, + content: 'Invalid input given to function call, MUST TRY WITH SIMPLER ARGUMENTS' + } + messages.push(toolResponse) + addedMessages.push(toolResponse) } else { return false } diff --git a/frontend/src/lib/components/flows/flowDiff.ts b/frontend/src/lib/components/flows/flowDiff.ts index 3daa35122a..1f1a510ef5 100644 --- a/frontend/src/lib/components/flows/flowDiff.ts +++ b/frontend/src/lib/components/flows/flowDiff.ts @@ -30,18 +30,41 @@ export type ModuleActionInfo = { * Normalizes a FlowModule for comparison by removing properties that * should be ignored when determining if a module has changed. * Specifically, removes empty `assets` arrays since their presence/absence - * is not a meaningful difference. + * is not a meaningful difference. Recursively normalizes nested modules + * within container types (branchone, branchall, forloopflow, whileloopflow, aiagent). */ function normalizeModuleForComparison(module: FlowModule): FlowModule { - const normalized = { ...module } - if ('value' in normalized && normalized.value && typeof normalized.value === 'object') { - const value = { ...normalized.value } as Record - // Remove empty assets array - it's not a meaningful difference + // Deep clone to avoid mutating the original and to handle nested structures + const normalized = JSON.parse(JSON.stringify(module)) as FlowModule + + // Helper to remove empty assets from a value object + function removeEmptyAssets(value: Record): void { if (Array.isArray(value.assets) && value.assets.length === 0) { delete value.assets } - normalized.value = value as FlowModule['value'] } + + if ('value' in normalized && normalized.value && typeof normalized.value === 'object') { + removeEmptyAssets(normalized.value as Record) + + // Recursively normalize nested modules based on type + const value = normalized.value + if (value.type === 'forloopflow' || value.type === 'whileloopflow') { + value.modules = value.modules.map((m) => normalizeModuleForComparison(m)) + } else if (value.type === 'branchone') { + value.default = value.default.map((m) => normalizeModuleForComparison(m)) + value.branches = value.branches.map((branch) => ({ + ...branch, + modules: branch.modules.map((m) => normalizeModuleForComparison(m)) + })) + } else if (value.type === 'branchall') { + value.branches = value.branches.map((branch) => ({ + ...branch, + modules: branch.modules.map((m) => normalizeModuleForComparison(m)) + })) + } + } + return normalized } @@ -201,7 +224,10 @@ function getAllModulesWithLocation(flow: FlowValue): Map('FlowEditorContext') diff --git a/frontend/src/lib/components/flows/map/VirtualItem.svelte b/frontend/src/lib/components/flows/map/VirtualItem.svelte index 8c4335cdc4..d3f67ccb2f 100644 --- a/frontend/src/lib/components/flows/map/VirtualItem.svelte +++ b/frontend/src/lib/components/flows/map/VirtualItem.svelte @@ -86,8 +86,8 @@ : undefined : undefined ) - // Execution state takes priority over AI action colors, fallback to _VirtualItem - const effectiveState = $derived(outputType ?? aiActionToNodeState(action) ?? '_VirtualItem') + // AI action colors take priority over execution state, fallback to _VirtualItem + const effectiveState = $derived(aiActionToNodeState(action) ?? outputType ?? '_VirtualItem') let colorClasses = $derived(getNodeColorClasses(effectiveState, selected))