mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
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:
co-authored by
Claude Opus 4.5
parent
4cfedd26b0
commit
d4a1b4abed
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
Reference in New Issue
Block a user