mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
237 lines
6.6 KiB
TypeScript
237 lines
6.6 KiB
TypeScript
import type { StaticDecode, TSchema } from "@sinclair/typebox";
|
|
import { BkndError, SimpleRenderer } from "core";
|
|
import { type Static, type TObject, Value, parse, ucFirst } from "core/utils";
|
|
import type { InputsMap } from "../flows/Execution";
|
|
import * as tbbox from "@sinclair/typebox";
|
|
const { Type } = tbbox;
|
|
//type InstanceOf<T> = T extends new (...args: any) => infer R ? R : never;
|
|
|
|
export type TaskResult<Output = any> = {
|
|
start: Date;
|
|
output?: Output;
|
|
error?: any;
|
|
success: boolean;
|
|
params: any;
|
|
};
|
|
|
|
/*export type TaskRenderProps<T extends Task = Task> = NodeProps<{
|
|
task: T;
|
|
state: { i: number; isStartTask: boolean; isRespondingTask; event: ExecutionEvent | undefined };
|
|
}>;*/
|
|
export type TaskRenderProps<T extends Task = Task> = any;
|
|
|
|
export function dynamic<Type extends TSchema>(
|
|
type: Type,
|
|
parse?: (val: any | string) => Static<Type>,
|
|
) {
|
|
const guessDecode = (val: unknown): Static<Type> => {
|
|
if (typeof val === "string") {
|
|
switch (type.type) {
|
|
case "object":
|
|
case "array":
|
|
return JSON.parse(val);
|
|
case "number":
|
|
return Number.parseInt(val);
|
|
case "boolean":
|
|
return val === "true" || val === "1";
|
|
}
|
|
}
|
|
|
|
return val as Static<Type>;
|
|
};
|
|
|
|
const decode = (val: unknown): Static<Type> => {
|
|
if (typeof val === "string") {
|
|
return parse ? parse(val) : guessDecode(val);
|
|
}
|
|
|
|
return val as Static<Type>;
|
|
};
|
|
const title = (type.title ?? type.type) ? ucFirst(type.type) : "Raw";
|
|
|
|
return (
|
|
Type.Transform(Type.Union([{ title, ...type }, Type.String({ title: "Template" })]))
|
|
.Decode(decode)
|
|
// @ts-ignore
|
|
.Encode((val) => val)
|
|
);
|
|
}
|
|
|
|
export abstract class Task<Params extends TObject = TObject, Output = unknown> {
|
|
abstract type: string;
|
|
name: string;
|
|
|
|
/**
|
|
* The schema of the task's parameters.
|
|
*/
|
|
static schema = Type.Object({});
|
|
|
|
/**
|
|
* The task's parameters.
|
|
*/
|
|
_params: Static<Params>;
|
|
|
|
constructor(name: string, params?: Static<Params>) {
|
|
if (typeof name !== "string") {
|
|
throw new Error(`Task name must be a string, got ${typeof name}`);
|
|
}
|
|
|
|
// @todo: should name be easier for object access?
|
|
|
|
this.name = name;
|
|
|
|
const schema = (this.constructor as typeof Task).schema;
|
|
|
|
if (
|
|
schema === Task.schema &&
|
|
typeof params !== "undefined" &&
|
|
Object.keys(params).length > 0
|
|
) {
|
|
throw new Error(
|
|
`Task "${name}" has no schema defined but params passed: ${JSON.stringify(params)}`,
|
|
);
|
|
}
|
|
|
|
// @todo: string enums fail to validate
|
|
this._params = parse(schema, params || {});
|
|
|
|
/*const validator = new Validator(schema as any);
|
|
const _params = Default(schema, params || {});
|
|
const result = validator.validate(_params);
|
|
if (!result.valid) {
|
|
//console.log("---errors", result, { params, _params });
|
|
const error = result.errors[0]!;
|
|
throw new Error(
|
|
`Invalid params for task "${name}.${error.keyword}": "${
|
|
error.error
|
|
}". Params given: ${JSON.stringify(params)}`
|
|
);
|
|
}
|
|
|
|
this._params = _params as Static<Params>;*/
|
|
}
|
|
|
|
get params() {
|
|
return this._params as StaticDecode<Params>;
|
|
}
|
|
|
|
protected clone(name: string, params: Static<Params>): Task {
|
|
return new (this.constructor as any)(name, params);
|
|
}
|
|
|
|
static async resolveParams<S extends TSchema>(
|
|
schema: S,
|
|
params: any,
|
|
inputs: object = {},
|
|
): Promise<StaticDecode<S>> {
|
|
const newParams: any = {};
|
|
const renderer = new SimpleRenderer(inputs, { strictVariables: true, renderKeys: true });
|
|
|
|
//console.log("--resolveParams", params);
|
|
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value && SimpleRenderer.hasMarkup(value)) {
|
|
//console.log("--- has markup", value);
|
|
try {
|
|
newParams[key] = await renderer.render(value as string);
|
|
} catch (e: any) {
|
|
// wrap in bknd error for better error display
|
|
if (!(e instanceof BkndError)) {
|
|
throw new BkndError(
|
|
"Failed to resolve param",
|
|
{
|
|
key,
|
|
value,
|
|
error: e.message,
|
|
},
|
|
"resolve-params",
|
|
);
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
continue;
|
|
} else {
|
|
//console.log("-- no markup", key, value);
|
|
}
|
|
|
|
newParams[key] = value;
|
|
}
|
|
|
|
//console.log("--beforeDecode", newParams);
|
|
const v = Value.Decode(schema, newParams);
|
|
//console.log("--afterDecode", v);
|
|
//process.exit();
|
|
return v;
|
|
}
|
|
|
|
private async cloneWithResolvedParams(_inputs: Map<string, any>) {
|
|
const inputs = Object.fromEntries(_inputs.entries());
|
|
//console.log("--clone:inputs", inputs, this.params);
|
|
const newParams = await Task.resolveParams(
|
|
(this.constructor as any).schema,
|
|
this._params,
|
|
inputs,
|
|
);
|
|
//console.log("--clone:newParams", this.name, newParams);
|
|
|
|
return this.clone(this.name, newParams as any);
|
|
}
|
|
|
|
/**
|
|
* The internal execution of the flow.
|
|
* Wraps the execute() function to gather log results.
|
|
*/
|
|
async run(inputs: InputsMap = new Map()) {
|
|
const start = new Date();
|
|
let output: Output | undefined;
|
|
let error: any;
|
|
let success: boolean;
|
|
let params: any;
|
|
let time: number;
|
|
|
|
const starttime = performance.now();
|
|
|
|
try {
|
|
// create a copy with resolved params
|
|
const newTask = await this.cloneWithResolvedParams(inputs);
|
|
params = newTask.params;
|
|
|
|
output = (await newTask.execute(inputs)) as any;
|
|
success = true;
|
|
} catch (e: any) {
|
|
success = false;
|
|
//status.output = undefined;
|
|
|
|
if (e instanceof BkndError) {
|
|
error = e.toJSON();
|
|
} else {
|
|
error = {
|
|
type: "unknown",
|
|
message: (e as any).message,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { start, output, error, success, params, time: performance.now() - starttime };
|
|
}
|
|
|
|
protected error(message: string, details?: Record<string, any>) {
|
|
return new BkndError(message, details, "runtime");
|
|
}
|
|
|
|
abstract execute(inputs: Map<string, any>): Promise<Output>;
|
|
|
|
// that's for react flow's default node
|
|
get label() {
|
|
return this.name;
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
type: this.type,
|
|
params: this.params,
|
|
};
|
|
}
|
|
}
|