feat(aichat): use single tool for flow chat (#7326)

* setup first tests

* better

* variants

* use openrouter + cleaning

* write to files

* cleaning

* cleaning

* inline scripts

* more tests

* cleaning

* better

* test all

* few

* better errors

* cleaning

* single set flow tool

* handle malformed error

* single tool

* validate with zod

* cleaning + check duplicates

* exprtoset + aiaction over test

* regen openflow

* cleaning

* better zod

* recursive ignore of assets

* cleaning

* fix merge

* rm

* cleaning

* comment
This commit is contained in:
centdix
2025-12-10 16:29:10 +01:00
committed by GitHub
parent 36cb5bde1a
commit 888a6a4e60
17 changed files with 833 additions and 2273 deletions
+57 -22
View File
@@ -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!"
+352 -1
View File
@@ -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",
+4 -2
View File
@@ -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"
}
}
}
@@ -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<string, any> | 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)
@@ -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<string, any> | 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
}
},
@@ -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,
@@ -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'
@@ -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<FlowAIChatHelpers> = {
},
fn: async ({ args, helpers }) => {
const { modules, schema } = args
const flowValue: Record<string, unknown> = { 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:
@@ -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<string> {
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:<db_resource> }.
### 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-<type>". 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<FlowAIChatHelpers> | 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<FlowAIChatHelpers>[] {
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<FlowAIChatHelpers> = {
...productionAddModule,
def: noSchemaAddModuleToolDef
}
const customModifyModule: Tool<FlowAIChatHelpers> = {
...productionModifyModule,
def: noSchemaModifyModuleToolDef
}
// Create the add_branch tool with custom implementation
const addBranchTool: Tool<FlowAIChatHelpers> = {
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()
}
}
@@ -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<void>
setFlowJson: (json: string) => Promise<void>
setFlowJson: (
modules: FlowModule[] | undefined,
schema: Record<string, any> | undefined
) => Promise<void>
getFlowInputsSchema: () => Promise<Record<string, any>>
/** Update exprsToSet store for InputTransformForm components (only if module is selected) */
updateExprsToSet: (id: string, inputTransforms: Record<string, InputTransform>) => 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<FlowAIChatHelpers>[] = [
}
},
{
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<string, any> | 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:<db_resource> }.
### 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-<type>". 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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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<string>()): 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
})
}
+50 -8
View File
@@ -857,7 +857,7 @@ export async function parseOpenAICompletion(
_abortController?: AbortController // unused, for signature compatibility with parseAnthropicCompletion
): Promise<boolean> {
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
const streamingTools: Record<number, boolean> = {} // 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
}
+33 -7
View File
@@ -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<string, unknown>
// 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<string, unknown>): 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<string, unknown>)
// 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<string, ModuleWithLocat
* Two locations are equal if they refer to the same parent container.
* Index within the container is not considered (modules can be reordered).
*/
export function locationsEqual(a: ModuleParentLocation | null, b: ModuleParentLocation | null): boolean {
export function locationsEqual(
a: ModuleParentLocation | null,
b: ModuleParentLocation | null
): boolean {
if (!a || !b) return a === b
if (a.type !== b.type) return false
@@ -126,8 +126,8 @@
maximizeSubflow = undefined
}: Props = $props()
// Execution state takes priority over AI action colors
let effectiveState = $derived(nodeState ?? aiActionToNodeState(moduleAction?.action))
// AI action colors take priority over execution state
let effectiveState = $derived(aiActionToNodeState(moduleAction?.action) ?? nodeState)
let colorClasses = $derived(getNodeColorClasses(effectiveState, selected))
const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
@@ -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))
</script>