add aiagent module support to inline script extraction/replacement (#7773)

* dual build for utils-internal

* bump version

* feat(cli): add aiagent module support to inline script extraction/replacement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* add missing field in openapi

* bump yaml validator version

* cleaning

* cleaning

* cleaning

* nit

* cleaning

* cleaning

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-03 18:00:34 +00:00
committed by GitHub
co-authored by Claude Opus 4.5
parent 4cfedd26b0
commit d4a1b4abed
15 changed files with 216 additions and 101 deletions
+14 -4
View File
@@ -1,12 +1,22 @@
{
"name": "windmill-utils-internal",
"version": "1.3.2",
"version": "1.3.3",
"description": "Internal utility functions for Windmill",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"exports": {
".": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts"
}
},
"scripts": {
"dev": "./gen_wm_client.sh && ./remove-ts-ext.sh",
"build": "./gen_wm_client.sh && ./remove-ts-ext.sh && tsc",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"build": "./gen_wm_client.sh && ./remove-ts-ext.sh && npm run build:cjs && npm run build:esm",
"prepublishOnly": "npm run build"
},
"keywords": [
@@ -1,5 +1,5 @@
import { newPathAssigner, PathAssigner } from "../path-utils/path-assigner.ts";
import { FlowModule } from "../gen/types.gen.ts";
import { FlowModule, RawScript } from "../gen/types.gen.ts";
/**
* Represents an inline script extracted from a flow module
@@ -11,6 +11,28 @@ interface InlineScript {
content: string;
}
function extractRawscriptInline(
id: string,
summary: string | undefined,
rawscript: RawScript,
mapping: Record<string, string>,
separator: string,
assigner: PathAssigner
): InlineScript[] {
const [basePath, ext] = assigner.assignPath(summary ?? id, rawscript.language);
const path = mapping[id] ?? basePath + ext;
const content = rawscript.content;
const r = [{ path: path, content: content }];
rawscript.content = "!inline " + path.replaceAll(separator, "/");
const lock = rawscript.lock;
if (lock && lock != "") {
const lockPath = basePath + "lock";
rawscript.lock = "!inline " + lockPath.replaceAll(separator, "/");
r.push({ path: lockPath, content: lock });
}
return r;
}
/**
* Options for extractInlineScripts function
*/
@@ -44,18 +66,14 @@ export function extractInlineScripts(
return modules.flatMap((m) => {
if (m.value.type == "rawscript") {
const [basePath, ext] = assigner.assignPath(m.summary, m.value.language);
const path = mapping[m.id] ?? basePath + ext;
const content = m.value.content;
const r = [{ path: path, content: content }];
m.value.content = "!inline " + path.replaceAll(separator, "/");
const lock = m.value.lock;
if (lock && lock != "") {
const lockPath = basePath + "lock";
m.value.lock = "!inline " + lockPath.replaceAll(separator, "/");
r.push({ path: lockPath, content: lock });
}
return r;
return extractRawscriptInline(
m.id,
m.summary,
m.value,
mapping,
separator,
assigner
);
} else if (m.value.type == "forloopflow") {
return extractInlineScripts(
m.value.modules,
@@ -95,6 +113,23 @@ export function extractInlineScripts(
assigner
),
];
} else if (m.value.type == "aiagent") {
return (m.value.tools ?? []).flatMap((tool) => {
const toolValue = tool.value;
// Only process flowmodule tools with rawscript type
if (!toolValue || toolValue.tool_type !== 'flowmodule' || toolValue.type !== 'rawscript') {
return [];
}
return extractRawscriptInline(
tool.id,
tool.summary,
toolValue,
mapping,
separator,
assigner
);
});
} else {
return [];
}
@@ -140,6 +175,14 @@ export function extractCurrentMapping(
extractCurrentMapping(b.modules, mapping)
);
extractCurrentMapping(m.value.default, mapping);
} else if (m.value.type === "aiagent") {
(m.value.tools ?? []).forEach((tool) => {
const toolValue = tool.value;
if (!toolValue || toolValue.tool_type !== 'flowmodule' || toolValue.type !== 'rawscript' || !toolValue.content || !toolValue.content.startsWith("!inline")) {
return;
}
mapping[tool.id] = toolValue.content.trim().split(" ")[1];
});
}
});
@@ -1,4 +1,49 @@
import { FlowModule } from "../gen/types.gen.ts";
import { FlowModule, RawScript } from "../gen/types.gen.ts";
async function replaceRawscriptInline(
id: string,
rawscript: RawScript,
fileReader: (path: string) => Promise<string>,
logger: { info: (message: string) => void; error: (message: string) => void },
separator: string,
removeLocks?: string[]
): Promise<void> {
if (!rawscript.content || !rawscript.content.startsWith("!inline")) {
return;
}
const path = rawscript.content.split(" ")[1];
const pathSuffix = path.split(".").slice(1).join(".");
const newPath = id + "." + pathSuffix;
try {
rawscript.content = await fileReader(path);
} catch {
logger.error(`Script file ${path} not found`);
try {
rawscript.content = await fileReader(newPath);
} catch {
logger.error(`Script file ${newPath} not found`);
}
}
const lock = rawscript.lock;
if (removeLocks && removeLocks.includes(path)) {
rawscript.lock = undefined;
} else if (
lock &&
typeof lock === "string" &&
lock.trimStart().startsWith("!inline ")
) {
const lockPath = lock.split(" ")[1];
try {
rawscript.lock = await fileReader(lockPath.replaceAll("/", separator));
} catch {
logger.error(`Lock file ${lockPath} not found, treating as empty`);
rawscript.lock = "";
}
}
}
/**
* Replaces inline script references with actual file content from the filesystem.
@@ -32,66 +77,15 @@ export async function replaceInlineScripts(
throw new Error(`Module value is undefined for module ${module.id}`);
}
if (module.value.type === "rawscript" && module.value.content && module.value.content.startsWith("!inline")) {
const path = module.value.content.split(" ")[1];
// const pathPrefix = path.split(".")[0];
const pathSuffix = path.split(".").slice(1).join(".");
// new path is the module id with the same suffix
const newPath = module.id + "." + pathSuffix;
try {
module.value.content = await fileReader(path);
} catch {
logger.error(`Script file ${path} not found`);
// try new path
try {
module.value.content = await fileReader(newPath);
} catch {
logger.error(`Script file ${newPath} not found`);
}
}
// rename the file if the prefix is different from the module id (fix old naming)
// if (pathPrefix != module.id && renamer) {
// logger.info(`Renaming ${path} to ${module.id}.${pathSuffix}`);
// try {
// renamer(localPath + path, localPath + module.id + "." + pathSuffix);
// } catch {
// logger.info(`Failed to rename ${path} to ${module.id}.${pathSuffix}`);
// }
// }
const lock = module.value.lock;
if (removeLocks && removeLocks.includes(path)) {
module.value.lock = undefined;
// delete the file if the prefix is different from the module id (fix old naming)
// if (lock && lock != "") {
// const path = lock.split(" ")[1];
// const pathPrefix = path.split(".")[0];
// if (pathPrefix != module.id && deleter) {
// logger.info(`Deleting ${path}`);
// try {
// deleter(localPath + path);
// } catch {
// logger.error(`Failed to delete ${path}`);
// }
// }
// }
} else if (
lock &&
typeof lock == "string" &&
lock.trimStart().startsWith("!inline ")
) {
const path = lock.split(" ")[1];
try {
module.value.lock = await fileReader(path.replaceAll("/", separator));
} catch {
logger.error(`Lock file ${path} not found, treating as empty`);
module.value.lock = "";
}
}
if (module.value.type === "rawscript") {
await replaceRawscriptInline(
module.id,
module.value,
fileReader,
logger,
separator,
removeLocks
);
} else if (module.value.type === "forloopflow" || module.value.type === "whileloopflow") {
await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, separator, removeLocks);
} else if (module.value.type === "branchall") {
@@ -103,6 +97,25 @@ export async function replaceInlineScripts(
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks);
}));
await replaceInlineScripts(module.value.default, fileReader, logger, localPath, separator, removeLocks);
} else if (module.value.type === "aiagent") {
await Promise.all((module.value.tools ?? []).map(async (tool) => {
const toolValue = tool.value;
if (
!toolValue ||
toolValue.tool_type !== "flowmodule" ||
toolValue.type !== "rawscript"
) {
return;
}
await replaceRawscriptInline(
tool.id,
toolValue,
fileReader,
logger,
separator,
removeLocks
);
}));
}
}));
}
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist/cjs"
}
}
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ES2022",
"outDir": "./dist/esm"
}
}
+13 -6
View File
@@ -1,8 +1,10 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"module": "commonjs",
"lib": [
"ES2022"
],
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
@@ -11,7 +13,7 @@
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"moduleResolution": "node",
"baseUrl": "./",
"esModuleInterop": true,
"experimentalDecorators": true,
@@ -19,6 +21,11 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
@@ -245,7 +245,7 @@
<p class="font-medium text-secondary pb-2"> Iterator expression: </p>
{#if stepDetail.value.iterator.type == 'static'}
<ObjectViewer json={stepDetail.value.iterator.value} />
{:else}
{:else if stepDetail.value.iterator.type == 'javascript'}
<span class="text-xs">
<Highlight language={typescript} code={cleanExpr(stepDetail.value.iterator.expr)} />
</span>
@@ -42,9 +42,6 @@
import { inputBorderClass } from './text_input/TextInput.svelte'
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
// We add 'ai' for ai agent tools. 'ai' means the field will be filled by the AI agent dynamically.
type PropertyType = InputTransform['type'] | 'ai'
interface Props {
schema: Schema | { properties?: Record<string, any>; required?: string[] }
arg: InputTransform | any
@@ -162,7 +159,7 @@
})
}
function getPropertyType(arg: InputTransform | any): PropertyType {
function getPropertyType(arg: InputTransform | any): InputTransform['type'] {
// For agent tools, if static with undefined/empty value, treat as 'ai', meaning the field will be filled by the AI agent dynamically.
if (
isAgentTool &&
@@ -174,7 +171,7 @@
return 'ai'
}
let type: PropertyType = arg?.type ?? 'static'
let type: InputTransform['type'] = arg?.type ?? 'static'
if (
type == 'javascript' &&
@@ -408,7 +405,7 @@
function updateStaticInput(
inputCat: InputCat,
propertyType: PropertyType,
propertyType: InputTransform['type'],
arg: InputTransform | any
) {
if (!isStaticTemplate(inputCat)) {
@@ -821,7 +818,11 @@
otherArgs={Object.fromEntries(
Object.entries(otherArgs).map(([key, transform]) => [
key,
transform?.type === 'static' ? transform.value : transform?.expr
transform?.type === 'static'
? transform.value
: transform?.type === 'javascript'
? transform.expr
: undefined
])
)}
>
@@ -830,12 +831,14 @@
<S3ArrayHelperButton
{connecting}
onClick={() =>
switchToJsAndConnect((path) => appendPathToArrayExpr(arg.expr, path))}
switchToJsAndConnect((path) =>
appendPathToArrayExpr(arg?.type === 'javascript' ? arg.expr : '', path)
)}
/>
{/if}
{/snippet}
</ArgInput>
{:else if arg.expr != undefined}
{:else if arg?.type === 'javascript' && arg.expr != undefined}
<div
class={`bg-surface-input rounded-md flex flex-col pl-2 overflow-auto ${inputBorderClass({ forceFocus: focused })}`}
>
@@ -33,10 +33,12 @@
{val.value}
</span>
{/if}
{:else}
{:else if val.type == 'javascript'}
<span class="text-xs text-primary whitespace-pre-wrap font-mono">
{cleanExpr(val.expr)}
</span>
{:else if val.type == 'ai'}
<span class="text-xs text-primary whitespace-pre-wrap font-mono">Filled by AI</span>
{/if}
</Cell>
</Row>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,6 +7,8 @@ import type { StateStore } from '$lib/utils'
import type { FlowState } from './flowState'
import { dfs } from './dfs'
const isAiTransform = (transform: InputTransform | undefined) => transform?.type === 'ai'
function isInputFilled(
inputTransforms: Record<string, InputTransform>,
key: string,
@@ -20,6 +22,9 @@ function isInputFilled(
if (inputTransforms.hasOwnProperty(key)) {
const transform = inputTransforms[key]
if (isAiTransform(transform)) {
return true
}
if (
transform?.type === 'static' &&
(transform?.value === undefined || transform?.value === '' || transform?.value === null)
@@ -41,6 +46,9 @@ async function isConnectedToMissingModule(
input_transform: InputTransform,
moduleIds: string[]
): Promise<string | undefined> {
if (isAiTransform(input_transform)) {
return undefined
}
const val: string =
input_transform.type === 'static' ? String(input_transform.value) : input_transform.expr
@@ -49,7 +49,7 @@ export function evalValue(
if (t.type == 'static') {
v = t.value
} else {
} else if (t.type == 'javascript') {
try {
let context = {
flow_input: pickableProperties?.flow_input,
@@ -62,6 +62,8 @@ export function evalValue(
}
v = undefined
}
} else {
v = undefined
}
if (v === NEVER_TESTED_THIS_FAR) {
v = undefined
+13
View File
@@ -333,11 +333,13 @@ components:
oneOf:
- $ref: '#/components/schemas/StaticTransform'
- $ref: '#/components/schemas/JavascriptTransform'
- $ref: '#/components/schemas/AiTransform'
discriminator:
propertyName: type
mapping:
static: '#/components/schemas/StaticTransform'
javascript: '#/components/schemas/JavascriptTransform'
ai: '#/components/schemas/AiTransform'
StaticTransform:
type: object
@@ -367,6 +369,17 @@ components:
- expr
- type
AiTransform:
type: object
description: Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.
properties:
type:
type: string
enum:
- ai
required:
- type
FlowModuleValue:
description: The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type
oneOf:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-yaml-validator",
"version": "1.0.1",
"version": "1.0.2",
"description": "YAML validator for Windmill",
"main": "dist/index.js",
"types": "dist/index.d.ts",