mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-03 08:36:01 +00:00
public commit
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import type { StaticDecode, TSchema } from "@sinclair/typebox";
|
||||
import type { NodeProps } from "@xyflow/react";
|
||||
import { BkndError, SimpleRenderer } from "core";
|
||||
import { type Static, type TObject, Type, Value, parse, ucFirst } from "core/utils";
|
||||
import type { ExecutionEvent, InputsMap } from "../flows/Execution";
|
||||
//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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { uuid } from "core/utils";
|
||||
import { get } from "lodash-es";
|
||||
import type { Task, TaskResult } from "./Task";
|
||||
|
||||
type TaskConnectionConfig = {
|
||||
condition?: Condition;
|
||||
max_retries?: number;
|
||||
};
|
||||
|
||||
export class TaskConnection {
|
||||
source: Task;
|
||||
target: Task;
|
||||
config: TaskConnectionConfig;
|
||||
public id: string;
|
||||
|
||||
constructor(source: Task, target: Task, config?: TaskConnectionConfig, id?: string) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.config = config ?? {};
|
||||
|
||||
if (!(this.config.condition instanceof Condition)) {
|
||||
this.config.condition = Condition.default();
|
||||
}
|
||||
|
||||
this.id = id ?? uuid();
|
||||
}
|
||||
|
||||
get condition(): Condition {
|
||||
return this.config.condition as any;
|
||||
}
|
||||
|
||||
get max_retries(): number {
|
||||
return this.config.max_retries ?? 0;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
source: this.source.name,
|
||||
target: this.target.name,
|
||||
config: {
|
||||
...this.config,
|
||||
condition: this.config.condition?.toJSON()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Condition {
|
||||
private constructor(
|
||||
public type: "success" | "error" | "matches",
|
||||
public path: string = "",
|
||||
public value: any = undefined
|
||||
) {}
|
||||
|
||||
static default() {
|
||||
return Condition.success();
|
||||
}
|
||||
|
||||
static success() {
|
||||
return new Condition("success");
|
||||
}
|
||||
|
||||
static error() {
|
||||
return new Condition("error");
|
||||
}
|
||||
|
||||
static matches(path: string, value: any) {
|
||||
if (typeof path !== "string" || path.length === 0) {
|
||||
throw new Error("Invalid path");
|
||||
}
|
||||
|
||||
return new Condition("matches", path, value);
|
||||
}
|
||||
|
||||
isMet(result: TaskResult) {
|
||||
switch (this.type) {
|
||||
case "success":
|
||||
return result.success;
|
||||
case "error":
|
||||
return result.success === false;
|
||||
case "matches":
|
||||
return get(result.output, this.path) === this.value;
|
||||
//return this.value === output[this.path];
|
||||
}
|
||||
}
|
||||
|
||||
sameAs(condition: Condition = Condition.default()) {
|
||||
return (
|
||||
this.type === condition.type &&
|
||||
this.path === condition.path &&
|
||||
this.value === condition.value
|
||||
);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: this.type,
|
||||
path: this.path.length === 0 ? undefined : this.path,
|
||||
value: this.value
|
||||
};
|
||||
}
|
||||
|
||||
static fromObject(obj: ReturnType<Condition["toJSON"]>) {
|
||||
return new Condition(obj.type, obj.path, obj.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { StringEnum, Type } from "core/utils";
|
||||
import type { InputsMap } from "../../flows/Execution";
|
||||
import { Task, dynamic } from "../Task";
|
||||
|
||||
const FetchMethods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
||||
|
||||
export class FetchTask<Output extends Record<string, any>> extends Task<
|
||||
typeof FetchTask.schema,
|
||||
Output
|
||||
> {
|
||||
type = "fetch";
|
||||
|
||||
static override schema = Type.Object({
|
||||
url: Type.String({
|
||||
pattern: "^(http|https)://"
|
||||
}),
|
||||
//method: Type.Optional(Type.Enum(FetchMethodsEnum)),
|
||||
//method: Type.Optional(dynamic(Type.String({ enum: FetchMethods, default: "GET" }))),
|
||||
method: Type.Optional(dynamic(StringEnum(FetchMethods, { default: "GET" }))),
|
||||
headers: Type.Optional(
|
||||
dynamic(
|
||||
Type.Array(
|
||||
Type.Object({
|
||||
key: Type.String(),
|
||||
value: Type.String()
|
||||
})
|
||||
),
|
||||
JSON.parse
|
||||
)
|
||||
),
|
||||
body: Type.Optional(dynamic(Type.String())),
|
||||
normal: Type.Optional(dynamic(Type.Number(), Number.parseInt))
|
||||
});
|
||||
|
||||
protected getBody(): string | undefined {
|
||||
const body = this.params.body;
|
||||
if (!body) return;
|
||||
if (typeof body === "string") return body;
|
||||
if (typeof body === "object") return JSON.stringify(body);
|
||||
|
||||
throw new Error(`Invalid body type: ${typeof body}`);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
//console.log(`method: (${this.params.method})`);
|
||||
if (!FetchMethods.includes(this.params.method ?? "GET")) {
|
||||
throw this.error("Invalid method", {
|
||||
given: this.params.method,
|
||||
valid: FetchMethods
|
||||
});
|
||||
}
|
||||
|
||||
const body = this.getBody();
|
||||
const headers = new Headers(this.params.headers?.map((h) => [h.key, h.value]));
|
||||
|
||||
/*console.log("[FETCH]", {
|
||||
url: this.params.url,
|
||||
method: this.params.method ?? "GET",
|
||||
headers,
|
||||
body
|
||||
});*/
|
||||
const result = await fetch(this.params.url, {
|
||||
method: this.params.method ?? "GET",
|
||||
headers,
|
||||
body
|
||||
});
|
||||
|
||||
//console.log("fetch:response", result);
|
||||
if (!result.ok) {
|
||||
throw this.error("Failed to fetch", {
|
||||
status: result.status,
|
||||
statusText: result.statusText
|
||||
});
|
||||
}
|
||||
|
||||
const data = (await result.json()) as Output;
|
||||
//console.log("fetch:response:data", data);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Type } from "core/utils";
|
||||
import { Task } from "../Task";
|
||||
|
||||
export class LogTask extends Task<typeof LogTask.schema> {
|
||||
type = "log";
|
||||
|
||||
static override schema = Type.Object({
|
||||
delay: Type.Number({ default: 10 })
|
||||
});
|
||||
|
||||
async execute() {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.params.delay));
|
||||
console.log(`[DONE] LogTask: ${this.name}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Type } from "core/utils";
|
||||
import { Task } from "../Task";
|
||||
|
||||
export class RenderTask<Output extends Record<string, any>> extends Task<
|
||||
typeof RenderTask.schema,
|
||||
Output
|
||||
> {
|
||||
type = "render";
|
||||
|
||||
static override schema = Type.Object({
|
||||
render: Type.String()
|
||||
});
|
||||
|
||||
async execute() {
|
||||
return this.params.render as unknown as Output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Type } from "core/utils";
|
||||
import { Flow } from "../../flows/Flow";
|
||||
import { Task, dynamic } from "../Task";
|
||||
|
||||
export class SubFlowTask<Output extends Record<string, any>> extends Task<
|
||||
typeof SubFlowTask.schema,
|
||||
Output
|
||||
> {
|
||||
type = "subflow";
|
||||
|
||||
static override schema = Type.Object({
|
||||
flow: Type.Any(),
|
||||
input: Type.Optional(dynamic(Type.Any(), JSON.parse)),
|
||||
loop: Type.Optional(Type.Boolean())
|
||||
});
|
||||
|
||||
async execute() {
|
||||
const flow = this.params.flow;
|
||||
if (!(flow instanceof Flow)) {
|
||||
throw new Error("Invalid flow provided");
|
||||
}
|
||||
|
||||
if (this.params.loop) {
|
||||
const _input = Array.isArray(this.params.input) ? this.params.input : [this.params.input];
|
||||
|
||||
const results: any[] = [];
|
||||
for (const input of _input) {
|
||||
const execution = flow.createExecution();
|
||||
await execution.start(input);
|
||||
results.push(await execution.getResponse());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start(this.params.input);
|
||||
return execution.getResponse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user