weifuwu 0.3.0 → 0.4.0
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/README.md +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +532 -0
- package/dist/router.d.ts +7 -1
- package/dist/workflow/engine.d.ts +7 -0
- package/dist/workflow/index.d.ts +6 -0
- package/dist/workflow/llm.d.ts +10 -0
- package/dist/workflow/nodes.d.ts +10 -0
- package/dist/workflow/reference.d.ts +3 -0
- package/dist/workflow/sse.d.ts +2 -0
- package/dist/workflow/tool.d.ts +8 -0
- package/dist/workflow/types.d.ts +86 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Features like `tsx()`, WebSocket, GraphQL, and AI streaming all follow the same
|
|
|
18
18
|
- **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
|
|
19
19
|
- **GraphQL** — `router.graphql()` with GraphiQL IDE
|
|
20
20
|
- **AI streaming** — `router.ai()` via Vercel AI SDK
|
|
21
|
+
- **AI workflows** — `router.workflow()` — intent-to-execution pipelines with `tool()` + SSE
|
|
21
22
|
- **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
|
|
22
23
|
- **Request validation** — `validate()` with Zod (body / query / params / headers)
|
|
23
24
|
- **File upload** — `upload()` multipart parser with disk save, size & type limits
|
|
@@ -430,6 +431,35 @@ const app = new Router()
|
|
|
430
431
|
serve(app.handler(), { port: 3000 })
|
|
431
432
|
```
|
|
432
433
|
|
|
434
|
+
## Workflow
|
|
435
|
+
|
|
436
|
+
Define tools (business capabilities) and let AI generate and execute multi-step workflows.
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import { Router, tool, createWorkflowEngine } from 'weifuwu'
|
|
440
|
+
import { z } from 'zod'
|
|
441
|
+
|
|
442
|
+
const tools = {
|
|
443
|
+
queryUser: tool({
|
|
444
|
+
description: '查询用户信息,返回 email, name',
|
|
445
|
+
inputSchema: z.object({ userId: z.string() }),
|
|
446
|
+
execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
|
|
447
|
+
}),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const app = new Router()
|
|
451
|
+
|
|
452
|
+
// Router method — accepts { nodes } or { goal } with model
|
|
453
|
+
app.workflow('/api/agent', { tools })
|
|
454
|
+
|
|
455
|
+
// Or manual execution
|
|
456
|
+
const engine = createWorkflowEngine({ tools })
|
|
457
|
+
const result = await engine.execute({ nodes: [
|
|
458
|
+
{ id: 's1', tool: 'set', input: { name: 'msg', value: 'hello' } },
|
|
459
|
+
{ id: 'g1', tool: 'get', input: { name: 'msg' } },
|
|
460
|
+
]})
|
|
461
|
+
```
|
|
462
|
+
|
|
433
463
|
## Graceful shutdown
|
|
434
464
|
|
|
435
465
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -19,3 +19,5 @@ export { rateLimit } from './rate-limit.ts';
|
|
|
19
19
|
export type { RateLimitOptions } from './rate-limit.ts';
|
|
20
20
|
export { compress } from './compress.ts';
|
|
21
21
|
export type { CompressOptions } from './compress.ts';
|
|
22
|
+
export { tool, createWorkflowEngine, createSSEManager, generateWorkflow } from './workflow/index.ts';
|
|
23
|
+
export type { Tool, Workflow, WorkflowEngine, WorkflowState, SSEEvent } from './workflow/types.ts';
|
package/dist/index.js
CHANGED
|
@@ -110,6 +110,490 @@ import { WebSocketServer } from "ws";
|
|
|
110
110
|
import { buildSchema, graphql } from "graphql";
|
|
111
111
|
import { makeExecutableSchema } from "@graphql-tools/schema";
|
|
112
112
|
import { streamText } from "ai";
|
|
113
|
+
|
|
114
|
+
// workflow/tool.ts
|
|
115
|
+
function tool(def) {
|
|
116
|
+
return {
|
|
117
|
+
name: def.name ?? "",
|
|
118
|
+
description: def.description,
|
|
119
|
+
inputSchema: def.inputSchema,
|
|
120
|
+
execute: def.execute
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// workflow/reference.ts
|
|
125
|
+
function getByPath(obj, path) {
|
|
126
|
+
let current = obj;
|
|
127
|
+
for (const key of path) {
|
|
128
|
+
if (current === null || current === void 0) return void 0;
|
|
129
|
+
if (typeof current === "object" && key in current) {
|
|
130
|
+
current = current[key];
|
|
131
|
+
} else {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
function resolveRef(path, ctx) {
|
|
138
|
+
if (path.startsWith("$nodes.")) {
|
|
139
|
+
const afterNodes = path.slice(7);
|
|
140
|
+
const dotIdx = afterNodes.indexOf(".");
|
|
141
|
+
if (dotIdx === -1) {
|
|
142
|
+
return ctx.nodeOutputs.get(afterNodes);
|
|
143
|
+
}
|
|
144
|
+
const id2 = afterNodes.slice(0, dotIdx);
|
|
145
|
+
const propPath = afterNodes.slice(dotIdx + 1);
|
|
146
|
+
const output = ctx.nodeOutputs.get(id2);
|
|
147
|
+
if (output === void 0) {
|
|
148
|
+
throw new Error(`Node "${id2}" has no output yet`);
|
|
149
|
+
}
|
|
150
|
+
if (propPath.startsWith("output")) {
|
|
151
|
+
return getByPath(output, propPath.slice(7).split(".").filter(Boolean));
|
|
152
|
+
}
|
|
153
|
+
return getByPath(output, propPath.split("."));
|
|
154
|
+
}
|
|
155
|
+
if (path.startsWith("$var.")) {
|
|
156
|
+
const name = path.slice(5);
|
|
157
|
+
if (!ctx.variables.has(name)) {
|
|
158
|
+
throw new Error(`Variable "${name}" is not defined`);
|
|
159
|
+
}
|
|
160
|
+
return ctx.variables.get(name);
|
|
161
|
+
}
|
|
162
|
+
if (path.startsWith("$input.")) {
|
|
163
|
+
const key = path.slice(7);
|
|
164
|
+
return ctx.input[key];
|
|
165
|
+
}
|
|
166
|
+
if (path === "true") return true;
|
|
167
|
+
if (path === "false") return false;
|
|
168
|
+
if (path === "null") return null;
|
|
169
|
+
const num = Number(path);
|
|
170
|
+
if (!isNaN(num) && path.trim() !== "") return num;
|
|
171
|
+
return path;
|
|
172
|
+
}
|
|
173
|
+
function resolveValue(v, ctx) {
|
|
174
|
+
if (typeof v === "string" && v.startsWith("$")) {
|
|
175
|
+
return resolveRef(v, ctx);
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(v)) {
|
|
178
|
+
return v.map((item) => resolveValue(item, ctx));
|
|
179
|
+
}
|
|
180
|
+
if (typeof v === "object" && v !== null) {
|
|
181
|
+
const result = {};
|
|
182
|
+
for (const [k, val] of Object.entries(v)) {
|
|
183
|
+
result[k] = resolveValue(val, ctx);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
return v;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// workflow/nodes.ts
|
|
191
|
+
function evaluateExpression(expr, ctx) {
|
|
192
|
+
const operators = [
|
|
193
|
+
{ op: "===", fn: (a, b) => a === b },
|
|
194
|
+
{ op: "!==", fn: (a, b) => a !== b },
|
|
195
|
+
{ op: ">=", fn: (a, b) => Number(a) >= Number(b) },
|
|
196
|
+
{ op: "<=", fn: (a, b) => Number(a) <= Number(b) },
|
|
197
|
+
{ op: ">", fn: (a, b) => Number(a) > Number(b) },
|
|
198
|
+
{ op: "<", fn: (a, b) => Number(a) < Number(b) },
|
|
199
|
+
{ op: "==", fn: (a, b) => a == b },
|
|
200
|
+
{ op: "!=", fn: (a, b) => a != b },
|
|
201
|
+
{ op: "+", fn: (a, b) => Number(a) + Number(b) },
|
|
202
|
+
{ op: "-", fn: (a, b) => Number(a) - Number(b) },
|
|
203
|
+
{ op: "*", fn: (a, b) => Number(a) * Number(b) },
|
|
204
|
+
{ op: "/", fn: (a, b) => Number(a) / Number(b) },
|
|
205
|
+
{ op: "%", fn: (a, b) => Number(a) % Number(b) },
|
|
206
|
+
{ op: "&&", fn: (a, b) => Boolean(a) && Boolean(b) },
|
|
207
|
+
{ op: "||", fn: (a, b) => Boolean(a) || Boolean(b) }
|
|
208
|
+
];
|
|
209
|
+
for (const { op, fn } of operators) {
|
|
210
|
+
const idx = expr.indexOf(op);
|
|
211
|
+
if (idx > 0) {
|
|
212
|
+
const leftRaw = expr.slice(0, idx).trim();
|
|
213
|
+
const rightRaw = expr.slice(idx + op.length).trim();
|
|
214
|
+
const left = resolveValue(leftRaw, ctx);
|
|
215
|
+
const right = resolveValue(rightRaw, ctx);
|
|
216
|
+
return fn(left, right);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const trimmed = expr.trim();
|
|
220
|
+
if (trimmed === "true") return true;
|
|
221
|
+
if (trimmed === "false") return false;
|
|
222
|
+
if (trimmed === "null") return null;
|
|
223
|
+
const num = Number(trimmed);
|
|
224
|
+
if (!isNaN(num) && trimmed !== "") return num;
|
|
225
|
+
return resolveValue(expr, ctx);
|
|
226
|
+
}
|
|
227
|
+
async function executeEval(node, ctx) {
|
|
228
|
+
const expression = node.input.expression;
|
|
229
|
+
if (!expression) throw new Error('eval node requires "expression" field');
|
|
230
|
+
const result = evaluateExpression(expression, ctx);
|
|
231
|
+
return { result };
|
|
232
|
+
}
|
|
233
|
+
async function executeSet(node, ctx) {
|
|
234
|
+
const name = node.input.name;
|
|
235
|
+
const value = node.input.value;
|
|
236
|
+
if (!name) throw new Error('set node requires "name" field');
|
|
237
|
+
let resolved;
|
|
238
|
+
if (typeof value === "string") {
|
|
239
|
+
resolved = evaluateExpression(value, ctx);
|
|
240
|
+
} else {
|
|
241
|
+
resolved = resolveValue(value ?? null, ctx);
|
|
242
|
+
}
|
|
243
|
+
ctx.variables.set(name, resolved);
|
|
244
|
+
return resolved;
|
|
245
|
+
}
|
|
246
|
+
async function executeGet(node, ctx) {
|
|
247
|
+
const name = node.input.name;
|
|
248
|
+
if (!name) throw new Error('get node requires "name" field');
|
|
249
|
+
if (!ctx.variables.has(name)) {
|
|
250
|
+
throw new Error(`Variable "${name}" is not defined`);
|
|
251
|
+
}
|
|
252
|
+
return ctx.variables.get(name);
|
|
253
|
+
}
|
|
254
|
+
async function executeIf(node, ctx) {
|
|
255
|
+
const conditions = node.conditions ?? [];
|
|
256
|
+
for (const condition of conditions) {
|
|
257
|
+
const test = typeof condition.test === "string" ? Boolean(resolveValue(condition.test, ctx)) : condition.test;
|
|
258
|
+
if (test && condition.body) {
|
|
259
|
+
let lastOutput = void 0;
|
|
260
|
+
for (const bodyNode of condition.body) {
|
|
261
|
+
lastOutput = await executeNode(bodyNode, ctx);
|
|
262
|
+
}
|
|
263
|
+
return lastOutput;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return void 0;
|
|
267
|
+
}
|
|
268
|
+
async function executeWhile(node, ctx) {
|
|
269
|
+
const conditionExpr = node.input.condition;
|
|
270
|
+
if (!conditionExpr) throw new Error('while node requires "condition" field');
|
|
271
|
+
let lastOutput = void 0;
|
|
272
|
+
let iterations = 0;
|
|
273
|
+
const maxIterations = 1e3;
|
|
274
|
+
while (iterations < maxIterations) {
|
|
275
|
+
iterations++;
|
|
276
|
+
ctx.stepCount++;
|
|
277
|
+
if (ctx.stepCount > ctx.maxSteps) {
|
|
278
|
+
throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
|
|
279
|
+
}
|
|
280
|
+
const condition = Boolean(evaluateExpression(conditionExpr, ctx));
|
|
281
|
+
if (!condition) break;
|
|
282
|
+
for (const bodyNode of node.body ?? []) {
|
|
283
|
+
lastOutput = await executeNode(bodyNode, ctx);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return lastOutput;
|
|
287
|
+
}
|
|
288
|
+
async function executeCall(node, ctx) {
|
|
289
|
+
const toolName = node.input.tool;
|
|
290
|
+
const args = node.input.args ?? {};
|
|
291
|
+
if (toolName && ctx.toolRegistry.has(toolName)) {
|
|
292
|
+
const tool3 = ctx.toolRegistry.get(toolName);
|
|
293
|
+
const resolvedInput = resolveValue(args, ctx);
|
|
294
|
+
const parsed = tool3.inputSchema.parse(resolvedInput);
|
|
295
|
+
return tool3.execute(parsed, {
|
|
296
|
+
nodeId: node.id,
|
|
297
|
+
workflowId: ctx.workflowId,
|
|
298
|
+
onStream: async (event) => {
|
|
299
|
+
if (ctx.sseManager && ctx.workflowId) {
|
|
300
|
+
ctx.sseManager.send(ctx.workflowId, { event: "llm-stream", data: { nodeId: node.id, ...event } });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const functionName = node.input.function;
|
|
306
|
+
if (functionName && ctx.functions[functionName]) {
|
|
307
|
+
const fn = ctx.functions[functionName];
|
|
308
|
+
const prevFunctions = ctx.functions;
|
|
309
|
+
const prevInput = ctx.input;
|
|
310
|
+
ctx.input = resolveValue(args, ctx);
|
|
311
|
+
let lastOutput = void 0;
|
|
312
|
+
for (const bodyNode of fn.workflow.nodes) {
|
|
313
|
+
lastOutput = await executeNode(bodyNode, ctx);
|
|
314
|
+
}
|
|
315
|
+
ctx.input = prevInput;
|
|
316
|
+
return lastOutput;
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`call node: tool "${toolName ?? functionName}" not found`);
|
|
319
|
+
}
|
|
320
|
+
async function executeHttp(node, ctx) {
|
|
321
|
+
const input = resolveValue(node.input, ctx);
|
|
322
|
+
const url = input.url;
|
|
323
|
+
if (!url) throw new Error('http node requires "url" field');
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const timeout = input.timeout ?? 3e4;
|
|
326
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
327
|
+
try {
|
|
328
|
+
const fetchInit = {
|
|
329
|
+
method: input.method ?? "GET",
|
|
330
|
+
headers: input.headers ?? {},
|
|
331
|
+
signal: controller.signal
|
|
332
|
+
};
|
|
333
|
+
if (input.body && fetchInit.method !== "GET") {
|
|
334
|
+
fetchInit.body = JSON.stringify(input.body);
|
|
335
|
+
}
|
|
336
|
+
const response = await fetch(url, fetchInit);
|
|
337
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
338
|
+
const body = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
339
|
+
return {
|
|
340
|
+
status: response.status,
|
|
341
|
+
statusText: response.statusText,
|
|
342
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
343
|
+
body
|
|
344
|
+
};
|
|
345
|
+
} finally {
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
var executors = {
|
|
350
|
+
eval: executeEval,
|
|
351
|
+
set: executeSet,
|
|
352
|
+
get: executeGet,
|
|
353
|
+
if: executeIf,
|
|
354
|
+
while: executeWhile,
|
|
355
|
+
call: executeCall,
|
|
356
|
+
http: executeHttp
|
|
357
|
+
};
|
|
358
|
+
async function executeNode(node, ctx) {
|
|
359
|
+
const executor = executors[node.tool];
|
|
360
|
+
if (!executor) {
|
|
361
|
+
throw new Error(`Unknown node type: "${node.tool}"`);
|
|
362
|
+
}
|
|
363
|
+
return executor(node, ctx);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// workflow/llm.ts
|
|
367
|
+
function buildToolsDescription(tools) {
|
|
368
|
+
return Object.entries(tools).map(([key, t]) => {
|
|
369
|
+
const name = t.name || key;
|
|
370
|
+
const schema = t.inputSchema;
|
|
371
|
+
return `- ${name}: ${t.description}
|
|
372
|
+
Input schema: describe as JSON object fields`;
|
|
373
|
+
}).join("\n");
|
|
374
|
+
}
|
|
375
|
+
var SYSTEM_PROMPT_TEMPLATE = `You are a workflow generator. Given a user goal and available tools, output a workflow JSON.
|
|
376
|
+
|
|
377
|
+
Available tools:
|
|
378
|
+
{{TOOLS}}
|
|
379
|
+
|
|
380
|
+
Workflow format:
|
|
381
|
+
{
|
|
382
|
+
"name": "workflow name",
|
|
383
|
+
"nodes": [
|
|
384
|
+
{
|
|
385
|
+
"id": "step1",
|
|
386
|
+
"tool": "set",
|
|
387
|
+
"input": { "name": "varName", "value": "initialValue" }
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"id": "step2",
|
|
391
|
+
"tool": "call",
|
|
392
|
+
"input": { "tool": "toolName", "args": { "param1": "$var.varName" } }
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"id": "step3",
|
|
396
|
+
"tool": "if",
|
|
397
|
+
"input": {},
|
|
398
|
+
"conditions": [
|
|
399
|
+
{ "test": "$nodes.step2.output.someField", "body": [
|
|
400
|
+
{ "id": "step4", "tool": "call", "input": { "tool": "toolName", "args": {} } }
|
|
401
|
+
]}
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
Node types:
|
|
408
|
+
- eval: evaluate an expression. input: { expression: "..." }
|
|
409
|
+
- set: assign a variable. input: { name, value }
|
|
410
|
+
- get: read a variable. input: { name }
|
|
411
|
+
- if: conditional branch. input: {}, conditions: [{ test, body }]
|
|
412
|
+
- while: loop. input: { condition }, body: [nodes]
|
|
413
|
+
- call: call a registered tool. input: { tool, args }
|
|
414
|
+
- http: HTTP request. input: { url, method?, headers?, body? }
|
|
415
|
+
|
|
416
|
+
Reference syntax:
|
|
417
|
+
- $var.name - read a variable
|
|
418
|
+
- $nodes.id.output - output of a previous node
|
|
419
|
+
- $nodes.id.output.field - specific field of a node's output
|
|
420
|
+
- $input.field - workflow input parameter
|
|
421
|
+
|
|
422
|
+
Output ONLY valid JSON. No explanation, no markdown.`;
|
|
423
|
+
async function generateWorkflow(goal, tools, generateFn) {
|
|
424
|
+
const toolsDesc = buildToolsDescription(tools);
|
|
425
|
+
const system = SYSTEM_PROMPT_TEMPLATE.replace("{{TOOLS}}", toolsDesc);
|
|
426
|
+
const result = await generateFn({
|
|
427
|
+
system,
|
|
428
|
+
messages: [{ role: "user", content: goal }]
|
|
429
|
+
});
|
|
430
|
+
const text = result.text.trim();
|
|
431
|
+
const jsonStart = text.indexOf("{");
|
|
432
|
+
const jsonEnd = text.lastIndexOf("}");
|
|
433
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
434
|
+
throw new Error(`LLM output is not valid JSON: ${text.slice(0, 200)}`);
|
|
435
|
+
}
|
|
436
|
+
const jsonStr = text.slice(jsonStart, jsonEnd + 1);
|
|
437
|
+
try {
|
|
438
|
+
const workflow = JSON.parse(jsonStr);
|
|
439
|
+
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
|
|
440
|
+
throw new Error("Generated workflow has no nodes array");
|
|
441
|
+
}
|
|
442
|
+
return workflow;
|
|
443
|
+
} catch (err) {
|
|
444
|
+
if (err instanceof SyntaxError) {
|
|
445
|
+
throw new Error(`Failed to parse LLM output as JSON: ${err.message}`);
|
|
446
|
+
}
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// workflow/engine.ts
|
|
452
|
+
import { generateText } from "ai";
|
|
453
|
+
function createWorkflowEngine(options) {
|
|
454
|
+
const toolRegistry = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const [key, t] of Object.entries(options.tools)) {
|
|
456
|
+
t.name = t.name || key;
|
|
457
|
+
toolRegistry.set(t.name, t);
|
|
458
|
+
}
|
|
459
|
+
const states = /* @__PURE__ */ new Map();
|
|
460
|
+
async function execute(workflow, opts) {
|
|
461
|
+
const ctx = {
|
|
462
|
+
variables: /* @__PURE__ */ new Map(),
|
|
463
|
+
nodeOutputs: /* @__PURE__ */ new Map(),
|
|
464
|
+
functions: workflow.functions ?? {},
|
|
465
|
+
stepCount: 0,
|
|
466
|
+
maxSteps: opts?.maxSteps ?? 1e3,
|
|
467
|
+
input: opts?.initialInput ?? {},
|
|
468
|
+
toolRegistry,
|
|
469
|
+
sseManager: options.sseManager,
|
|
470
|
+
workflowId: opts?.workflowId
|
|
471
|
+
};
|
|
472
|
+
let lastOutput = void 0;
|
|
473
|
+
for (const node of workflow.nodes) {
|
|
474
|
+
ctx.stepCount++;
|
|
475
|
+
if (ctx.stepCount > ctx.maxSteps) {
|
|
476
|
+
throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
|
|
477
|
+
}
|
|
478
|
+
options.sseManager?.send(ctx.workflowId ?? "", { event: "node-start", data: { nodeId: node.id, tool: node.tool, input: node.input } });
|
|
479
|
+
const output = await executeNode(node, ctx);
|
|
480
|
+
ctx.nodeOutputs.set(node.id, output);
|
|
481
|
+
lastOutput = output;
|
|
482
|
+
options.sseManager?.send(ctx.workflowId ?? "", { event: "node-end", data: { nodeId: node.id, output } });
|
|
483
|
+
}
|
|
484
|
+
return lastOutput;
|
|
485
|
+
}
|
|
486
|
+
async function runAsync(workflowId, workflow, opts) {
|
|
487
|
+
const state = {
|
|
488
|
+
workflowId,
|
|
489
|
+
status: "running",
|
|
490
|
+
goal: workflow.name ?? "",
|
|
491
|
+
startTime: Date.now()
|
|
492
|
+
};
|
|
493
|
+
states.set(workflowId, state);
|
|
494
|
+
const sse = options.sseManager;
|
|
495
|
+
sse?.send(workflowId, { event: "workflow-start", data: { workflowId, goal: state.goal } });
|
|
496
|
+
try {
|
|
497
|
+
const result = await execute(workflow, { ...opts, workflowId });
|
|
498
|
+
state.status = "completed";
|
|
499
|
+
state.result = result;
|
|
500
|
+
state.endTime = Date.now();
|
|
501
|
+
sse?.send(workflowId, { event: "complete", data: { result, duration: state.endTime - state.startTime } });
|
|
502
|
+
} catch (err) {
|
|
503
|
+
state.status = "error";
|
|
504
|
+
state.error = err instanceof Error ? err.message : String(err);
|
|
505
|
+
state.endTime = Date.now();
|
|
506
|
+
sse?.send(workflowId, { event: "error", data: { error: state.error } });
|
|
507
|
+
} finally {
|
|
508
|
+
sse?.close(workflowId);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function generateWorkflow2(goal) {
|
|
512
|
+
if (!options.model) {
|
|
513
|
+
throw new Error('LLM model is required for generateWorkflow. Pass "model" to createWorkflowEngine.');
|
|
514
|
+
}
|
|
515
|
+
return generateWorkflow(goal, options.tools, async (prompt) => {
|
|
516
|
+
const result = await generateText({
|
|
517
|
+
model: options.model,
|
|
518
|
+
system: prompt.system,
|
|
519
|
+
messages: prompt.messages
|
|
520
|
+
});
|
|
521
|
+
return { text: result.text };
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
execute,
|
|
526
|
+
runAsync,
|
|
527
|
+
generateWorkflow: generateWorkflow2,
|
|
528
|
+
getState(workflowId) {
|
|
529
|
+
return states.get(workflowId);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// workflow/sse.ts
|
|
535
|
+
function createSSEManager() {
|
|
536
|
+
const streams = /* @__PURE__ */ new Map();
|
|
537
|
+
const encoder = new TextEncoder();
|
|
538
|
+
function createStream(workflowId) {
|
|
539
|
+
const state = {
|
|
540
|
+
controller: null,
|
|
541
|
+
encoder,
|
|
542
|
+
closed: false,
|
|
543
|
+
buffer: []
|
|
544
|
+
};
|
|
545
|
+
const stream = new ReadableStream({
|
|
546
|
+
start(controller) {
|
|
547
|
+
state.controller = controller;
|
|
548
|
+
streams.set(workflowId, state);
|
|
549
|
+
for (const event of state.buffer) {
|
|
550
|
+
try {
|
|
551
|
+
controller.enqueue(encoder.encode(event));
|
|
552
|
+
} catch {
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
state.buffer = [];
|
|
557
|
+
},
|
|
558
|
+
cancel() {
|
|
559
|
+
state.closed = true;
|
|
560
|
+
streams.delete(workflowId);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
return stream;
|
|
564
|
+
}
|
|
565
|
+
function send(workflowId, event) {
|
|
566
|
+
const state = streams.get(workflowId);
|
|
567
|
+
if (!state || state.closed) return;
|
|
568
|
+
const data = `event: ${event.event}
|
|
569
|
+
data: ${JSON.stringify(event.data)}
|
|
570
|
+
|
|
571
|
+
`;
|
|
572
|
+
if (state.controller) {
|
|
573
|
+
try {
|
|
574
|
+
state.controller.enqueue(encoder.encode(data));
|
|
575
|
+
} catch {
|
|
576
|
+
state.closed = true;
|
|
577
|
+
streams.delete(workflowId);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
state.buffer.push(data);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function close(workflowId) {
|
|
584
|
+
const state = streams.get(workflowId);
|
|
585
|
+
if (!state) return;
|
|
586
|
+
state.closed = true;
|
|
587
|
+
streams.delete(workflowId);
|
|
588
|
+
try {
|
|
589
|
+
state.controller?.close();
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return { createStream, send, close };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// router.ts
|
|
113
597
|
var createTrieNode = () => ({
|
|
114
598
|
children: /* @__PURE__ */ new Map(),
|
|
115
599
|
handlers: /* @__PURE__ */ new Map(),
|
|
@@ -302,6 +786,50 @@ var Router = class _Router {
|
|
|
302
786
|
};
|
|
303
787
|
return this.post(path, ...middlewares, routeHandler);
|
|
304
788
|
}
|
|
789
|
+
workflow(path, options) {
|
|
790
|
+
const sseManager = options.stream ? createSSEManager() : void 0;
|
|
791
|
+
if (options.stream && sseManager) {
|
|
792
|
+
this.get(`${path}/:workflowId/events`, async (req, ctx) => {
|
|
793
|
+
const stream = sseManager.createStream(ctx.params.workflowId);
|
|
794
|
+
return new Response(stream, {
|
|
795
|
+
headers: {
|
|
796
|
+
"Content-Type": "text/event-stream",
|
|
797
|
+
"Cache-Control": "no-cache",
|
|
798
|
+
"Connection": "keep-alive"
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
this.post(path, async (req) => {
|
|
804
|
+
const body = await req.json();
|
|
805
|
+
const engine = createWorkflowEngine({
|
|
806
|
+
tools: options.tools,
|
|
807
|
+
model: options.model,
|
|
808
|
+
sseManager
|
|
809
|
+
});
|
|
810
|
+
let wf;
|
|
811
|
+
if (body.goal && options.model) {
|
|
812
|
+
wf = await engine.generateWorkflow(body.goal);
|
|
813
|
+
} else if (body.workflow) {
|
|
814
|
+
wf = body.workflow;
|
|
815
|
+
} else if (body.nodes) {
|
|
816
|
+
wf = { nodes: body.nodes };
|
|
817
|
+
} else {
|
|
818
|
+
return Response.json(
|
|
819
|
+
{ error: 'Provide "goal" (with model) or "workflow"/"nodes"' },
|
|
820
|
+
{ status: 400 }
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
if (options.stream && sseManager) {
|
|
824
|
+
const workflowId = crypto.randomUUID();
|
|
825
|
+
engine.runAsync(workflowId, wf);
|
|
826
|
+
return Response.json({ workflowId, eventsUrl: `${path}/${workflowId}/events` });
|
|
827
|
+
}
|
|
828
|
+
const result = await engine.execute(wf);
|
|
829
|
+
return Response.json({ workflow: wf, result });
|
|
830
|
+
});
|
|
831
|
+
return this;
|
|
832
|
+
}
|
|
305
833
|
handler() {
|
|
306
834
|
return (req, ctx) => {
|
|
307
835
|
const url = new URL(req.url);
|
|
@@ -1472,13 +2000,17 @@ export {
|
|
|
1472
2000
|
auth,
|
|
1473
2001
|
compress,
|
|
1474
2002
|
cors,
|
|
2003
|
+
createSSEManager,
|
|
2004
|
+
createWorkflowEngine,
|
|
1475
2005
|
deleteCookie,
|
|
2006
|
+
generateWorkflow,
|
|
1476
2007
|
getCookies,
|
|
1477
2008
|
logger,
|
|
1478
2009
|
rateLimit,
|
|
1479
2010
|
serve,
|
|
1480
2011
|
serveStatic,
|
|
1481
2012
|
setCookie,
|
|
2013
|
+
tool,
|
|
1482
2014
|
tsx,
|
|
1483
2015
|
upload,
|
|
1484
2016
|
useTsx,
|
package/dist/router.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { type WebSocket } from 'ws';
|
|
|
2
2
|
import type { IncomingMessage } from 'node:http';
|
|
3
3
|
import type { Duplex } from 'node:stream';
|
|
4
4
|
import { type GraphQLSchema } from 'graphql';
|
|
5
|
-
import { streamText } from 'ai';
|
|
5
|
+
import { streamText, generateText } from 'ai';
|
|
6
6
|
import type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
|
|
7
|
+
import type { Tool as WfTool } from './workflow/types.ts';
|
|
7
8
|
type StreamTextParams = Parameters<typeof streamText>[0];
|
|
8
9
|
export type WebSocketHandler = {
|
|
9
10
|
open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
|
|
@@ -44,6 +45,11 @@ export declare class Router {
|
|
|
44
45
|
ws(path: string, ...args: [...Middleware[], WebSocketHandler]): this;
|
|
45
46
|
graphql(path: string, ...args: [...Middleware[], GraphQLOptions]): this;
|
|
46
47
|
ai(path: string, ...args: [...Middleware[], AIHandler]): this;
|
|
48
|
+
workflow(path: string, options: {
|
|
49
|
+
tools: Record<string, WfTool>;
|
|
50
|
+
model?: Parameters<typeof generateText>[0]['model'];
|
|
51
|
+
stream?: boolean;
|
|
52
|
+
}): this;
|
|
47
53
|
handler(): Handler;
|
|
48
54
|
websocketHandler(): WsUpgradeHandler;
|
|
49
55
|
private splitPath;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Tool, WorkflowEngine, SSEManager } from './types.ts';
|
|
2
|
+
import { type LanguageModel } from 'ai';
|
|
3
|
+
export declare function createWorkflowEngine(options: {
|
|
4
|
+
tools: Record<string, Tool>;
|
|
5
|
+
sseManager?: SSEManager;
|
|
6
|
+
model?: LanguageModel;
|
|
7
|
+
}): WorkflowEngine;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { tool } from './tool.ts';
|
|
2
|
+
export { createWorkflowEngine } from './engine.ts';
|
|
3
|
+
export { createSSEManager } from './sse.ts';
|
|
4
|
+
export { resolveRef, resolveValue } from './reference.ts';
|
|
5
|
+
export { generateWorkflow } from './llm.ts';
|
|
6
|
+
export type { Tool, ToolContext, Workflow, Node, WorkflowState, SSEEvent, SSEManager, WorkflowEngine, ExecuteOptions, StreamEvent, Condition, SubWorkflow } from './types.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Tool, Workflow } from './types.ts';
|
|
2
|
+
export declare function generateWorkflow(goal: string, tools: Record<string, Tool>, generateFn: (prompt: {
|
|
3
|
+
system: string;
|
|
4
|
+
messages: {
|
|
5
|
+
role: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}[];
|
|
8
|
+
}) => Promise<{
|
|
9
|
+
text: string;
|
|
10
|
+
}>): Promise<Workflow>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Node, WorkflowContext } from './types.ts';
|
|
2
|
+
export type NodeExecutor = (node: Node, ctx: WorkflowContext) => Promise<unknown>;
|
|
3
|
+
export declare function executeEval(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
4
|
+
export declare function executeSet(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
5
|
+
export declare function executeGet(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
6
|
+
export declare function executeIf(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
7
|
+
export declare function executeWhile(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
8
|
+
export declare function executeCall(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
9
|
+
export declare function executeHttp(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
10
|
+
export declare function executeNode(node: Node, ctx: WorkflowContext): Promise<unknown>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { Tool, ToolContext } from './types.ts';
|
|
3
|
+
export declare function tool<TInput = unknown, TOutput = unknown>(def: {
|
|
4
|
+
name?: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodSchema<TInput>;
|
|
7
|
+
execute: (input: TInput, ctx: ToolContext) => Promise<TOutput>;
|
|
8
|
+
}): Tool<TInput, TOutput>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
export type NodeType = 'eval' | 'set' | 'get' | 'if' | 'while' | 'call' | 'http';
|
|
3
|
+
export interface Tool<TInput = unknown, TOutput = unknown> {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodSchema<TInput>;
|
|
7
|
+
execute: (input: TInput, ctx: ToolContext) => Promise<TOutput>;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolContext {
|
|
10
|
+
workflowId?: string;
|
|
11
|
+
nodeId: string;
|
|
12
|
+
onStream?: (event: StreamEvent) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface StreamEvent {
|
|
15
|
+
type: string;
|
|
16
|
+
chunk?: string;
|
|
17
|
+
accumulated?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface Node {
|
|
21
|
+
id: string;
|
|
22
|
+
tool: NodeType | string;
|
|
23
|
+
input: Record<string, unknown>;
|
|
24
|
+
conditions?: Condition[];
|
|
25
|
+
body?: Node[];
|
|
26
|
+
}
|
|
27
|
+
export interface Condition {
|
|
28
|
+
test: string | boolean;
|
|
29
|
+
body: Node[];
|
|
30
|
+
}
|
|
31
|
+
export interface Workflow {
|
|
32
|
+
name?: string;
|
|
33
|
+
nodes: Node[];
|
|
34
|
+
functions?: Record<string, SubWorkflow>;
|
|
35
|
+
}
|
|
36
|
+
export interface SubWorkflow {
|
|
37
|
+
inputSchema: Record<string, unknown>;
|
|
38
|
+
workflow: {
|
|
39
|
+
nodes: Node[];
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface WorkflowState {
|
|
43
|
+
workflowId: string;
|
|
44
|
+
status: 'running' | 'completed' | 'error';
|
|
45
|
+
goal: string;
|
|
46
|
+
result?: unknown;
|
|
47
|
+
error?: string;
|
|
48
|
+
startTime: number;
|
|
49
|
+
endTime?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface SSEEvent {
|
|
52
|
+
event: string;
|
|
53
|
+
data: unknown;
|
|
54
|
+
}
|
|
55
|
+
export interface ExecuteOptions {
|
|
56
|
+
initialInput?: Record<string, unknown>;
|
|
57
|
+
maxSteps?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface WorkflowContext {
|
|
60
|
+
variables: Map<string, unknown>;
|
|
61
|
+
nodeOutputs: Map<string, unknown>;
|
|
62
|
+
functions: Record<string, SubWorkflow>;
|
|
63
|
+
stepCount: number;
|
|
64
|
+
maxSteps: number;
|
|
65
|
+
input: Record<string, unknown>;
|
|
66
|
+
toolRegistry: Map<string, Tool>;
|
|
67
|
+
sseManager?: SSEManager;
|
|
68
|
+
workflowId?: string;
|
|
69
|
+
onNodeEvent?: (event: SSEEvent) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface SSEManager {
|
|
72
|
+
createStream: (workflowId: string) => ReadableStream<Uint8Array>;
|
|
73
|
+
send: (workflowId: string, event: SSEEvent) => void;
|
|
74
|
+
close: (workflowId: string) => void;
|
|
75
|
+
}
|
|
76
|
+
export interface EngineOptions {
|
|
77
|
+
tools: Record<string, Tool>;
|
|
78
|
+
model?: unknown;
|
|
79
|
+
sseManager?: SSEManager;
|
|
80
|
+
}
|
|
81
|
+
export interface WorkflowEngine {
|
|
82
|
+
execute: (workflow: Workflow, options?: ExecuteOptions) => Promise<unknown>;
|
|
83
|
+
getState: (workflowId: string) => WorkflowState | undefined;
|
|
84
|
+
runAsync: (workflowId: string, workflow: Workflow, options?: ExecuteOptions) => Promise<void>;
|
|
85
|
+
generateWorkflow: (goal: string) => Promise<Workflow>;
|
|
86
|
+
}
|