@avakado.ai/schemas 1.1.19 → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/models/Workflow.js +51 -5
- package/src/models/WorkflowLogs.js +39 -16
- package/src/workflowRunner.js +190 -0
package/package.json
CHANGED
package/src/models/Workflow.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Schema } from "mongoose";
|
|
2
3
|
import { defineModel } from "../runtime.js";
|
|
3
4
|
import { Task_Tree } from "./Action.js";
|
|
4
5
|
import { WORKFLOW_TRIGGERS } from "../enums/index.js";
|
|
6
|
+
import { WorkflowLogs } from "./WorkflowLogs.js";
|
|
7
|
+
import { runTaskTree, stored } from "../workflowRunner.js";
|
|
5
8
|
const WorkflowSchema = new Schema({
|
|
6
9
|
name: { type: String, default: "Untitled Workflow" },
|
|
7
10
|
trigger: { type: String, required: true, enum: WORKFLOW_TRIGGERS },
|
|
@@ -18,11 +21,54 @@ WorkflowSchema.methods.updateStatus = function (status) {
|
|
|
18
21
|
this.status = status;
|
|
19
22
|
return this.save();
|
|
20
23
|
};
|
|
21
|
-
WorkflowSchema.methods.execute = function (payload) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
WorkflowSchema.methods.execute = async function (payload, meta = {}) {
|
|
25
|
+
if (this.status === "disabled") return { skipped: true };
|
|
26
|
+
const businessId = String(this.business?._id ?? this.business ?? "");
|
|
27
|
+
const executionId = randomUUID();
|
|
28
|
+
const startedAt = new Date();
|
|
29
|
+
const log = await WorkflowLogs.create({
|
|
30
|
+
workflow: this._id,
|
|
31
|
+
business: this.business?._id ?? this.business,
|
|
32
|
+
executionId,
|
|
33
|
+
eventId: meta.eventId,
|
|
34
|
+
name: this.name,
|
|
35
|
+
trigger: this.trigger,
|
|
36
|
+
status: "running",
|
|
37
|
+
summary: `Workflow "${this.name}" started for ${this.trigger}.`,
|
|
38
|
+
input: stored(payload),
|
|
39
|
+
steps: [],
|
|
40
|
+
startedAt,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const record = async (step) => {
|
|
44
|
+
log.steps.push(step);
|
|
45
|
+
log.summary = [`Workflow "${this.name}" ran for ${this.trigger}.`, ...log.steps.map((item) => item.summary)].join("\n");
|
|
46
|
+
await log.save();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const outputs = await runTaskTree(this.task, {
|
|
51
|
+
runtime: payload ?? {},
|
|
52
|
+
trigger: this.trigger,
|
|
53
|
+
businessId,
|
|
54
|
+
onStep: record,
|
|
55
|
+
});
|
|
56
|
+
log.status = "completed";
|
|
57
|
+
log.outputs = stored(outputs);
|
|
58
|
+
log.finishedAt = new Date();
|
|
59
|
+
log.summary = [`Workflow "${this.name}" completed for ${this.trigger}.`, ...log.steps.map((item) => item.summary)].join("\n");
|
|
60
|
+
await log.save();
|
|
61
|
+
return { executionId, outputs };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
log.status = "failed";
|
|
64
|
+
log.error = { message: error?.message || String(error), stack: error?.stack };
|
|
65
|
+
log.outputs = stored(error.workflowOutputs ?? []);
|
|
66
|
+
log.finishedAt = new Date();
|
|
67
|
+
log.summary = [`Workflow "${this.name}" failed for ${this.trigger}.`, ...log.steps.map((item) => item.summary)].filter(Boolean).join("\n");
|
|
68
|
+
if (!log.steps.some((step) => step.status === "failed")) log.summary += `\n${error?.message || error}`;
|
|
69
|
+
await log.save();
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
26
72
|
};
|
|
27
73
|
// WorkflowSchema.methods.getNextNode = function ({ currentNode = "", outputPortsOfCurentNode = [] } = {}) {
|
|
28
74
|
// const nextNodes = [];
|
|
@@ -1,22 +1,45 @@
|
|
|
1
1
|
import { Schema } from "mongoose";
|
|
2
2
|
import { defineModel } from "../runtime.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
conversationId: String,
|
|
8
|
-
},
|
|
9
|
-
executionId: String,
|
|
4
|
+
// One document per workflow run. `steps` is the order the task tree actually ran,
|
|
5
|
+
// including the input and output of each step, so a person can read what happened.
|
|
6
|
+
const workflowStepSchema = new Schema({
|
|
10
7
|
nodeId: String,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
label: String,
|
|
9
|
+
type: { type: String, enum: ["task", "conditional"] },
|
|
10
|
+
status: { type: String, enum: ["completed", "failed"], required: true },
|
|
11
|
+
summary: String,
|
|
12
|
+
input: Schema.Types.Mixed,
|
|
13
|
+
output: Schema.Types.Mixed,
|
|
14
|
+
error: { message: String, stack: String },
|
|
15
|
+
startedAt: Date,
|
|
16
|
+
finishedAt: Date,
|
|
17
|
+
}, { _id: false });
|
|
18
|
+
|
|
19
|
+
const WorkflowLogsSchema = new Schema({
|
|
20
|
+
workflow: { type: Schema.Types.ObjectId, ref: "Workflow", required: true },
|
|
21
|
+
business: { type: Schema.Types.ObjectId, ref: "Businesses" },
|
|
22
|
+
executionId: { type: String, required: true },
|
|
23
|
+
eventId: String,
|
|
24
|
+
name: String,
|
|
25
|
+
trigger: String,
|
|
26
|
+
status: { type: String, enum: ["running", "completed", "failed"], default: "running" },
|
|
27
|
+
summary: String,
|
|
28
|
+
input: Schema.Types.Mixed,
|
|
29
|
+
outputs: Schema.Types.Mixed,
|
|
30
|
+
error: { message: String, stack: String },
|
|
31
|
+
steps: [workflowStepSchema],
|
|
32
|
+
startedAt: Date,
|
|
33
|
+
finishedAt: Date,
|
|
17
34
|
}, {
|
|
18
|
-
timestamps: true
|
|
19
|
-
}
|
|
20
|
-
|
|
35
|
+
timestamps: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
WorkflowLogsSchema.index({ executionId: 1 }, { unique: true });
|
|
39
|
+
WorkflowLogsSchema.index({ workflow: 1, createdAt: -1 });
|
|
40
|
+
WorkflowLogsSchema.index({ business: 1, createdAt: -1 });
|
|
41
|
+
WorkflowLogsSchema.index({ trigger: 1, createdAt: -1 });
|
|
42
|
+
WorkflowLogsSchema.index({ status: 1, createdAt: -1 });
|
|
21
43
|
|
|
22
|
-
export
|
|
44
|
+
export { WorkflowLogsSchema };
|
|
45
|
+
export const WorkflowLogs = defineModel("WorkflowLogs", WorkflowLogsSchema, "WorkflowLogs");
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Walks a workflow task the same way tool_utils.executeTool does: each node reads
|
|
2
|
+
// `context.runtime`, `context.preset`, and `context.outputs`, then a task runs its
|
|
3
|
+
// compiled instructions. Conditionals take the first child when the mapped value is
|
|
4
|
+
// truthy and the second child otherwise.
|
|
5
|
+
|
|
6
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
7
|
+
|
|
8
|
+
export function plainValue(value) {
|
|
9
|
+
if (value == null) return value;
|
|
10
|
+
if (typeof value.toObject === "function") return value.toObject({ virtuals: false });
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getTreeMap(tree) {
|
|
15
|
+
const plain = plainValue(tree) ?? {};
|
|
16
|
+
const nodes = new Map((plain.nodes ?? []).map((node) => [node.id, node]));
|
|
17
|
+
return { rootId: plain.rootId, rootKey: plain.rootKey, nodes };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function evaluateExpression(expression, context) {
|
|
21
|
+
return new Function("context", `return (${expression});`)(context);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// tool_utils.evaluateToolInput
|
|
25
|
+
export function evaluateMapper(mapper, context) {
|
|
26
|
+
const treeMap = getTreeMap(mapper);
|
|
27
|
+
const evaluate = (node) => {
|
|
28
|
+
if (!node) return undefined;
|
|
29
|
+
const expression = node.core?.mapper?.expression;
|
|
30
|
+
if (expression) return evaluateExpression(expression, context);
|
|
31
|
+
switch (node.core?.flattenedSchema?.type) {
|
|
32
|
+
case "object":
|
|
33
|
+
return Object.fromEntries((node.children ?? []).map((child) => {
|
|
34
|
+
const childNode = treeMap.nodes.get(child.id);
|
|
35
|
+
return [child.key, childNode ? evaluate(childNode) : undefined];
|
|
36
|
+
}));
|
|
37
|
+
case "array":
|
|
38
|
+
return (node.children ?? []).map((child) => {
|
|
39
|
+
const childNode = treeMap.nodes.get(child.id);
|
|
40
|
+
return childNode ? evaluate(childNode) : undefined;
|
|
41
|
+
});
|
|
42
|
+
default:
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const root = treeMap.nodes.get(treeMap.rootId);
|
|
47
|
+
return root ? evaluate(root) : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function nodeInput(node, context) {
|
|
51
|
+
const compiled = node.core?.input?.compiledMapper;
|
|
52
|
+
if (typeof compiled === "string" && compiled.trim()) return evaluateExpression(compiled, context);
|
|
53
|
+
if (node.core?.input?.mapper) return evaluateMapper(node.core.input.mapper, context);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function nodeLabel(node) {
|
|
58
|
+
return node?.meta?.label || node?.core?.meta?.label || node?.id || "Untitled step";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runInstructions(node, input) {
|
|
62
|
+
const instructions = node.core?.coreExecution?.compiledInstructions;
|
|
63
|
+
if (typeof instructions !== "string" || !instructions.trim()) return undefined;
|
|
64
|
+
return new AsyncFunction("input", instructions)(input);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function stored(value) {
|
|
68
|
+
if (value instanceof Error) return { message: value.message, stack: value.stack };
|
|
69
|
+
const seen = new WeakSet();
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(JSON.stringify(value, (_key, item) => {
|
|
72
|
+
if (typeof item === "bigint") return item.toString();
|
|
73
|
+
if (typeof item === "object" && item !== null) {
|
|
74
|
+
if (seen.has(item)) return undefined;
|
|
75
|
+
seen.add(item);
|
|
76
|
+
}
|
|
77
|
+
return item;
|
|
78
|
+
}));
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function childNode(treeMap, child) {
|
|
85
|
+
if (!child?.id) return undefined;
|
|
86
|
+
const node = treeMap.nodes.get(child.id);
|
|
87
|
+
if (!node) throw new Error(`Invalid tree: referenced node "${child.id}" does not exist`);
|
|
88
|
+
return node;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Runs the task tree from its root. `onStep` is awaited with each step once that
|
|
93
|
+
* step has finished (completed or failed) so the caller can persist it.
|
|
94
|
+
* A failed step rejects after `onStep` returns.
|
|
95
|
+
*/
|
|
96
|
+
export async function runTaskTree(task, { runtime, trigger, businessId, onStep }) {
|
|
97
|
+
const treeMap = getTreeMap(task);
|
|
98
|
+
const outputs = [];
|
|
99
|
+
const root = treeMap.nodes.get(treeMap.rootId);
|
|
100
|
+
if (!root) throw new Error("Workflow task has no root node");
|
|
101
|
+
|
|
102
|
+
const execute = async (node) => {
|
|
103
|
+
const startedAt = new Date();
|
|
104
|
+
const label = nodeLabel(node);
|
|
105
|
+
const context = {
|
|
106
|
+
runtime: runtime ?? {},
|
|
107
|
+
preset: node.core?.input?.preset ?? {},
|
|
108
|
+
outputs,
|
|
109
|
+
trigger,
|
|
110
|
+
businessId,
|
|
111
|
+
payload: runtime ?? {},
|
|
112
|
+
};
|
|
113
|
+
let completed;
|
|
114
|
+
try {
|
|
115
|
+
if (node.core?.type === "task") {
|
|
116
|
+
if ((node.children ?? []).length > 1) {
|
|
117
|
+
throw new Error(`Task "${label}" has multiple outgoing edges`);
|
|
118
|
+
}
|
|
119
|
+
const input = nodeInput(node, context);
|
|
120
|
+
const output = await runInstructions(node, input);
|
|
121
|
+
outputs.push(output);
|
|
122
|
+
const next = (node.children ?? []).length ? childNode(treeMap, node.children[0]) : undefined;
|
|
123
|
+
completed = {
|
|
124
|
+
step: {
|
|
125
|
+
nodeId: node.id,
|
|
126
|
+
label,
|
|
127
|
+
type: "task",
|
|
128
|
+
status: "completed",
|
|
129
|
+
input: stored(input),
|
|
130
|
+
output: stored(output),
|
|
131
|
+
summary: `Completed "${label}".`,
|
|
132
|
+
startedAt,
|
|
133
|
+
finishedAt: new Date(),
|
|
134
|
+
},
|
|
135
|
+
next,
|
|
136
|
+
};
|
|
137
|
+
} else if (node.core?.type === "conditional") {
|
|
138
|
+
if ((node.children ?? []).length > 2) {
|
|
139
|
+
throw new Error(`Condition "${label}" cannot have more than 2 outgoing edges`);
|
|
140
|
+
}
|
|
141
|
+
const condition = nodeInput(node, context);
|
|
142
|
+
outputs.push(condition);
|
|
143
|
+
const nextChild = condition ? node.children?.[0] : node.children?.[1];
|
|
144
|
+
const next = nextChild ? childNode(treeMap, nextChild) : undefined;
|
|
145
|
+
const nextLabel = next ? nodeLabel(next) : "";
|
|
146
|
+
const summary = condition
|
|
147
|
+
? `Condition "${label}" was true${nextLabel ? `, continued to "${nextLabel}"` : ""}.`
|
|
148
|
+
: `Condition "${label}" was false${nextLabel ? `, continued to "${nextLabel}"` : ", so the workflow stopped"}.`;
|
|
149
|
+
completed = {
|
|
150
|
+
step: {
|
|
151
|
+
nodeId: node.id,
|
|
152
|
+
label,
|
|
153
|
+
type: "conditional",
|
|
154
|
+
status: "completed",
|
|
155
|
+
input: stored(condition),
|
|
156
|
+
output: stored({ condition: Boolean(condition), nextNodeId: next?.id ?? null }),
|
|
157
|
+
summary,
|
|
158
|
+
startedAt,
|
|
159
|
+
finishedAt: new Date(),
|
|
160
|
+
},
|
|
161
|
+
next,
|
|
162
|
+
};
|
|
163
|
+
} else {
|
|
164
|
+
throw new Error(`Unknown step type on "${label}"`);
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
try {
|
|
168
|
+
await onStep?.({
|
|
169
|
+
nodeId: node.id,
|
|
170
|
+
label,
|
|
171
|
+
...(node.core?.type === "task" || node.core?.type === "conditional" ? { type: node.core.type } : {}),
|
|
172
|
+
status: "failed",
|
|
173
|
+
error: { message: error?.message || String(error), stack: error?.stack },
|
|
174
|
+
summary: `Failed at "${label}": ${error?.message || error}`,
|
|
175
|
+
startedAt,
|
|
176
|
+
finishedAt: new Date(),
|
|
177
|
+
});
|
|
178
|
+
} catch (logError) {
|
|
179
|
+
console.error("@avakado.ai/schemas: failed to record workflow step", logError?.message || logError);
|
|
180
|
+
}
|
|
181
|
+
error.workflowOutputs = outputs;
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
await onStep?.(completed.step);
|
|
185
|
+
if (completed.next) await execute(completed.next);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
await execute(root);
|
|
189
|
+
return outputs;
|
|
190
|
+
}
|