mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
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:
@@ -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!"
|
||||
|
||||
Generated
+352
-1
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
+4
-7
@@ -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:
|
||||
|
||||
-914
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user