Updating docs package with code + tests from examples (#183)

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
Clelia (Astra) Bertelli
2025-09-15 06:07:22 +02:00
committed by GitHub
parent ff3ea0f181
commit 3cda388550
25 changed files with 609 additions and 8 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@llamaindex/workflow-viz": patch
"@llamaindex/workflow-docs": patch
---
Adding an export for type WorkflowWithDrawing in viz; Adding example code + tests in docs
+31
View File
@@ -0,0 +1,31 @@
{
"root": false,
"extends": "//",
"files": {
"includes": ["src/**", "tests/**"]
},
"linter": {
"rules": {
"style": {
"useTemplate": "off",
"noNonNullAssertion": "off"
},
"complexity": {
"noStaticOnlyClass": "off",
"useOptionalChain": "off",
"noBannedTypes": "off"
},
"correctness": {
"noUnreachable": "off"
},
"suspicious": {
"noExplicitAny": "off",
"noConfusingVoidType": "off",
"useIterableCallbackReturn": "off",
"noShadowRestrictedNames": "off",
"noDoubleEquals": "off",
"noAssignInExpressions": "off"
}
}
}
}
+41 -1
View File
@@ -1,8 +1,48 @@
{
"name": "@llamaindex/workflow-docs",
"version": "0.1.4",
"type": "module",
"main": "index.ts",
"packageManager": "pnpm@10.15.0",
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/run-llama/workflows-ts.git"
},
"keywords": [
"workflows",
"typescript",
"llamaindex",
"ai",
"llms",
"genai"
],
"author": "LlamaIndex",
"license": "MIT",
"bugs": {
"url": "https://github.com/run-llama/workflows-ts/issues"
},
"homepage": "https://github.com/run-llama/workflows-ts/blob/main/examples/README.md",
"devDependencies": {
"@llamaindex/workflow-core": "workspace:*",
"@llamaindex/workflow-otel": "workspace:^",
"@llamaindex/workflow-viz": "workspace:^",
"@opentelemetry/sdk-node": "^0.203.0",
"@opentelemetry/sdk-trace-base": "^2.0.1"
},
"peerDependencies": {
"@llamaindex/workflow-core": "workspace:*",
"@llamaindex/workflow-otel": "workspace:^",
"@llamaindex/workflow-viz": "workspace:^",
"@opentelemetry/sdk-node": "^0.203.0",
"@opentelemetry/sdk-trace-base": "^2.0.1"
},
"files": [
"workflows"
"workflows",
"src"
],
"publishConfig": {
"access": "public"
+35
View File
@@ -0,0 +1,35 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
export const workflow = createWorkflow();
export const inputEvent = workflowEvent<string | number>();
const processStringEvent = workflowEvent<string>();
const processNumberEvent = workflowEvent<number>();
export const successEvent = workflowEvent<string>();
workflow.handle([inputEvent], async (context, event) => {
if (typeof event.data === "string") {
return processStringEvent.with(event.data);
} else {
return processNumberEvent.with(event.data);
}
});
workflow.handle([processStringEvent], async (context, event) => {
return successEvent.with(`Processed string ${event.data}`);
});
workflow.handle([processNumberEvent], async (context, event) => {
return successEvent.with(`Processed number ${event.data}`);
});
let context1 = workflow.createContext();
context1.sendEvent(inputEvent.with("I am some data"));
const result = await context1.stream.until(successEvent).toArray();
console.log(result.at(-1)!.data);
let context2 = workflow.createContext();
context2.sendEvent(inputEvent.with(1));
const result2 = await context2.stream.until(successEvent).toArray();
console.log(result2.at(-1)!.data);
+70
View File
@@ -0,0 +1,70 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state";
// Define the events we'll use
export const startEvent = workflowEvent<string>(); // Triggers the fan-out process
export const processItemEvent = workflowEvent<number>(); // Individual items to process
export const resultEvent = workflowEvent<string>(); // Results from processed items
export const completeEvent = workflowEvent<string[]>(); // Final aggregated results
const { withState } = createStatefulMiddleware(() => ({
itemsToProcess: 10,
itemsProcessed: 0,
processResults: [] as string[],
}));
export const workflow = withState(createWorkflow());
workflow.handle([startEvent], async (context, start) => {
const { sendEvent, state } = context;
state.itemsProcessed = 0; // Reset counter for this execution
// Fan out: emit multiple events to be processed in parallel
for (let i = 0; i < state.itemsToProcess; i++) {
sendEvent(processItemEvent.with(i));
}
});
workflow.handle([processItemEvent], async (context, event) => {
const { sendEvent, state } = context;
// Simulate some async work (like API calls, database operations, etc.)
await new Promise((resolve) => setTimeout(resolve, Math.random() * 100));
// Process the item
const processedValue = `Processed: ${event.data}`;
// Update the shared counter after processing completes
state.itemsProcessed++;
// Return the result event
sendEvent(resultEvent.with(processedValue));
});
workflow.handle([resultEvent], async (context, event) => {
const { sendEvent, state } = context;
// store the processed message
state.processResults.push(event.data);
// return completeEvent if the processing is completed
if (state.itemsProcessed === state.itemsToProcess) {
sendEvent(completeEvent.with(state.processResults));
}
});
async function runFanOut() {
console.log("Running fan-out workflow");
const { stream, sendEvent } = workflow.createContext();
// Start the fan-out process
sendEvent(startEvent.with("Start fan-out"));
// Listen to all events as they occur
for await (const event of stream) {
if (processItemEvent.include(event)) {
console.log(`Processing item: ${event.data}`);
} else if (resultEvent.include(event)) {
console.log(`Result received: ${event.data}`);
} else if (completeEvent.include(event)) {
console.log("Final aggregated results:", event.data);
break; // All done!
}
}
}
runFanOut().catch(console.error);
+35
View File
@@ -0,0 +1,35 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state";
// Define events
export const startEvent = workflowEvent<string>();
export const humanRequestEvent = workflowEvent<void>();
export const humanResponseEvent = workflowEvent<string>();
export const stopEvent = workflowEvent<string>();
const { withState } = createStatefulMiddleware(() => ({}));
export const workflow = withState(createWorkflow());
// Workflow that needs human input
workflow.handle([startEvent], () => {
return humanRequestEvent.with();
});
workflow.handle([humanResponseEvent], (context, event) => {
return stopEvent.with(`Human said: ${event.data}`);
});
// Usage with snapshot/resume
const { sendEvent, snapshot, stream } = workflow.createContext();
sendEvent(startEvent.with("begin"));
// Wait for a human request and take a snapshot
await stream.until(humanRequestEvent).toArray();
const snapshotData = await snapshot();
// Later (in another request): resume and provide human input
const resumedContext = workflow.resume(snapshotData);
resumedContext.sendEvent(humanResponseEvent.with("hello world"));
const resultEvent = await resumedContext.stream.untilEvent(stopEvent);
console.log(resultEvent.data); // "Human said: hello world"
+50
View File
@@ -0,0 +1,50 @@
import * as branching from "./branching";
import * as fanInFanOut from "./fan_in_fan_out";
import * as humanInTheLoop from "./human_in_the_loop";
import * as loops from "./loops";
import * as state from "./state";
import * as tracingBase from "./tracing_base";
import * as tracingPlugin from "./tracing_plugin";
import * as workflowViz from "./workflow_viz";
// Branching exports
export const branchingWorkflow = branching.workflow;
export const branchingInputEvent = branching.inputEvent;
export const branchingSuccessEvent = branching.successEvent;
// Fan-in Fan-out exports
export const fanInFanOutWorkflow = fanInFanOut.workflow;
export const fanInFanOutStartEvent = fanInFanOut.startEvent;
export const fanInFanOutProcessItemEvent = fanInFanOut.processItemEvent;
export const fanInFanOutResultEvent = fanInFanOut.resultEvent;
export const fanInFanOutCompleteEvent = fanInFanOut.completeEvent;
// Human in the loop exports
export const humanInTheLoopWorkflow = humanInTheLoop.workflow;
export const humanInTheLoopStartEvent = humanInTheLoop.startEvent;
export const humanInTheLoopHumanRequestEvent = humanInTheLoop.humanRequestEvent;
export const humanInTheLoopHumanResponseEvent =
humanInTheLoop.humanResponseEvent;
export const humanInTheLoopStopEvent = humanInTheLoop.stopEvent;
// Loops exports
export const loopsWorkflow = loops.workflow;
export const loopsStartEvent = loops.startEvent;
export const loopsStopEvent = loops.stopEvent;
// State exports
export const stateWorkflow = state.workflow;
export const stateStartEvent = state.startEvent;
export const stateStopEvent = state.stopEvent;
// Tracing base exports
export const tracingBaseWorkflow = tracingBase.workflow;
export const tracingBaseStepEvent = tracingBase.stepEvent;
// Tracing plugin exports
export const tracingPluginWorkflow = tracingPlugin.workflow;
// Workflow viz exports
export const workflowVizWorkflow = workflowViz.workflow;
export const workflowVizStartEvent = workflowViz.startEvent;
export const workflowVizDoneEvent = workflowViz.doneEvent;
+43
View File
@@ -0,0 +1,43 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state";
type AgentWorkflowState = {
counter: number;
max_counter: number;
};
const { withState } = createStatefulMiddleware(
(state: AgentWorkflowState) => state,
);
export const workflow = withState(createWorkflow());
export const startEvent = workflowEvent<void>();
const increaseCounterEvent = workflowEvent<void>();
export const stopEvent = workflowEvent<number>();
workflow.handle([startEvent], async (context, { data }) => {
const { sendEvent, state } = context;
if (state.counter < state.max_counter) {
sendEvent(increaseCounterEvent.with());
} else {
sendEvent(stopEvent.with(state.counter));
}
});
workflow.handle([increaseCounterEvent], async (context, { data }) => {
const { sendEvent, state } = context;
state.counter += 1;
sendEvent(startEvent.with());
});
const { stream, sendEvent } = workflow.createContext({
counter: 0,
max_counter: 5,
});
sendEvent(startEvent.with());
const result = await stream.untilEvent(stopEvent);
// should print 5 since the workflow is looping
console.log(result.data);
+39
View File
@@ -0,0 +1,39 @@
import { createWorkflow } from "@llamaindex/workflow-core";
import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state";
import { workflowEvent } from "@llamaindex/workflow-core";
type MyWorkflowState = {
previous_message: string;
};
const { withState } = createStatefulMiddleware(
(state: MyWorkflowState) => state,
);
export const workflow = withState(createWorkflow());
export const startEvent = workflowEvent<{ userInput: string }>();
export const stopEvent = workflowEvent<{ result: string }>();
workflow.handle([startEvent], async (context, { data }) => {
const { sendEvent, state } = context;
const { userInput } = data;
const previous_message = state.previous_message;
state.previous_message = userInput;
return stopEvent.with({
result:
"Processed message: " +
userInput +
" previous message: " +
previous_message,
});
});
const { stream, sendEvent } = workflow.createContext({
previous_message: "my initial previous message",
});
sendEvent(startEvent.with({ userInput: "Hello, how are you?" }));
const result = await stream.untilEvent(stopEvent);
console.log(result.data);
+41
View File
@@ -0,0 +1,41 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { withTraceEvents } from "@llamaindex/workflow-core/middleware/trace-events";
import { openTelemetry } from "@llamaindex/workflow-otel";
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
// Initialize OpenTelemetry SDK (use your preferred exporter in real deployments)
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()),
});
sdk.start();
// Define events
const startEvent = workflowEvent();
export const stepEvent = workflowEvent<{ value: string }>();
// Create workflow and attach the OpenTelemetry plugin
export const workflow = withTraceEvents(createWorkflow(), {
plugins: [openTelemetry],
});
// Handlers automatically produce spans (including errors)
workflow.handle([startEvent], (context) => {
context.sendEvent(stepEvent.with({ value: "hello!" }));
context.sendEvent(stepEvent.with({ value: "crash!" })); // demonstrates error spans
});
workflow.handle([stepEvent], (_context, event) => {
if (event.data.value === "crash!") {
throw new Error("The ultimate error happened!");
}
});
// Run
const { sendEvent, stream } = workflow.createContext();
sendEvent(startEvent.with());
+39
View File
@@ -0,0 +1,39 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import {
withTraceEvents,
createHandlerDecorator,
} from "@llamaindex/workflow-core/middleware/trace-events";
const startEvent = workflowEvent();
// Create a decorator-based plugin
type Timing = { startedAt: number | null };
const timingPlugin = createHandlerDecorator<Timing>({
debugLabel: "timing",
getInitialValue: () => ({ startedAt: null }),
onBeforeHandler:
(h, _ctx, metadata) =>
async (...args) => {
metadata.startedAt = Date.now();
try {
// @ts-expect-error - Expecting: A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
return await h(...(args as any));
} finally {
const durationMs = Date.now() - (metadata.startedAt ?? Date.now());
console.log("[trace] handler duration (ms):", durationMs);
}
},
onAfterHandler: () => ({ startedAt: null }),
});
// Attach your plugin to the workflow
export const workflow = withTraceEvents(createWorkflow(), {
plugins: [timingPlugin],
});
workflow.handle([startEvent], async () => {
await new Promise((r) => setTimeout(r, 50));
});
const { sendEvent } = workflow.createContext();
sendEvent(startEvent.with());
+11
View File
@@ -0,0 +1,11 @@
import { workflow } from "./workflow_viz";
const container = document.getElementById("app") as HTMLElement;
// Optional settings:
// - layout: "force" | "none" (defaults to "force")
// - Any Sigma renderer setting can be passed as well, e.g. `defaultEdgeColor`
workflow.draw(container, {
layout: "force",
defaultEdgeColor: "#999",
});
+17
View File
@@ -0,0 +1,17 @@
import {
createWorkflow,
workflowEvent,
type Workflow,
} from "@llamaindex/workflow-core";
import { withDrawing } from "@llamaindex/workflow-viz";
// Define events (debug labels are used for node names in the graph)
export const startEvent = workflowEvent<string>({ debugLabel: "start" });
export const doneEvent = workflowEvent<string>({ debugLabel: "done" });
// Decorate your workflow to enable drawing
export const workflow = withDrawing(createWorkflow());
workflow.handle([startEvent], (_ctx, start) => {
return doneEvent.with(`Hello ${start.data}`);
});
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
import { workflow, inputEvent, successEvent } from "../src/branching";
describe("Branching workflow should return expected results", () => {
test("Sending event with context1", async () => {
let context1 = workflow.createContext();
context1.sendEvent(inputEvent.with("I am some data"));
const result = await context1.stream.until(successEvent).toArray();
expect(result.at(-1)!.data).toBe("Processed string I am some data");
});
test("Sending event with context2", async () => {
let context2 = workflow.createContext();
context2.sendEvent(inputEvent.with(1));
const result2 = await context2.stream.until(successEvent).toArray();
expect(result2.at(-1)!.data).toBe("Processed number 1");
});
});
+29
View File
@@ -0,0 +1,29 @@
import { describe, expect, test } from "vitest";
import {
workflow,
startEvent,
completeEvent,
processItemEvent,
resultEvent,
} from "../src/fan_in_fan_out";
describe("Fan-In/Fan-Out workflow should stream expected events", () => {
test("Test Fan-In/Fan-Out e2e with streaming", async () => {
const { stream, sendEvent } = workflow.createContext();
sendEvent(startEvent.with("Start fan-out"));
for await (const event of stream) {
if (processItemEvent.include(event)) {
expect(event.data).toBeLessThan(10);
} else if (resultEvent.include(event)) {
expect(event.data.startsWith("Processed: ")).toBeTruthy();
} else if (completeEvent.include(event)) {
for (let i = 0; i < 10; i++) {
const contained = `Processed: ${i}`;
expect(event.data).toContain(contained);
}
break;
}
}
});
});
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, test } from "vitest";
import {
workflow,
startEvent,
stopEvent,
humanRequestEvent,
humanResponseEvent,
} from "../src/human_in_the_loop";
describe("Human in the loop returns expected results", () => {
test("Test Human In The Loop e2e", async () => {
const { sendEvent, snapshot, stream } = workflow.createContext();
sendEvent(startEvent.with("begin"));
// Wait for a human request and take a snapshot
await stream.until(humanRequestEvent).toArray();
const snapshotData = await snapshot();
// Later (in another request): resume and provide human input
const resumedContext = workflow.resume(snapshotData);
resumedContext.sendEvent(humanResponseEvent.with("hello world"));
const events = await resumedContext.stream.until(stopEvent).toArray();
expect(events[events.length - 1].data).toBe("Human said: hello world");
});
});
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import { workflow, startEvent, stopEvent } from "../src/loops";
describe("Loops Workflow returns expected results", () => {
test("Test Loops Workflow e2e", async () => {
const { stream, sendEvent } = workflow.createContext({
counter: 0,
max_counter: 5,
});
sendEvent(startEvent.with());
const result = await stream.until(stopEvent).toArray();
expect(result[result.length - 1].data).toBe(5);
});
});
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import { workflow, startEvent, stopEvent } from "../src/state";
describe("State Workflow returns expected results", () => {
test("Test State Workflow e2e", async () => {
const { stream, sendEvent } = workflow.createContext({
previous_message: "my initial previous message",
});
sendEvent(startEvent.with({ userInput: "Hello, how are you?" }));
const result = await stream.untilEvent(stopEvent);
expect(result.data.result).toBe(
"Processed message: Hello, how are you? previous message: my initial previous message",
);
});
});
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "./lib",
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"paths": {
"@llamaindex/workflow-docs": ["./src/index.ts"]
}
},
"include": ["./src", "./workflows"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["tests/*.test.ts"],
},
});
@@ -35,7 +35,7 @@ const completeEvent = workflowEvent<string[]>(); // Final aggregated results
Next, we create our workflow and set up tracking variables in the state:
```javascript
```ts
const { withState } = createStatefulMiddleware(() => ({
itemsToProcess: 10,
itemsProcessed: 0,
+2 -2
View File
@@ -33,7 +33,7 @@ const workflow = withState(createWorkflow());
And then, in your step handling logic, you can access and modify state properties:
```ts
```ts
import { workflowEvent } from "@llamaindex/workflow-core";
const startEvent = workflowEvent<{ userInput: string }>();
@@ -52,7 +52,7 @@ workflow.handle([startEvent], async (context, { data }) => {
Before running the workflow, remember to initialize the context object with the state:
```typescript
```ts
const { stream, sendEvent } = workflow.createContext({
previous_message: "my initial previous message",
});
+2 -2
View File
@@ -5,8 +5,8 @@
"type": "module",
"packageManager": "pnpm@10.15.0",
"scripts": {
"build": "turbo build --filter=\"./packages/*\"",
"test": "turbo test --filter=\"./packages/*\"",
"build": "turbo build --filter=\"./packages/*\" --filter=\"./docs\"",
"test": "turbo test --filter=\"./packages/*\" --filter=\"./docs\"",
"typecheck": "tsc -b --diagnostics",
"format": "biome format .",
"format:write": "biome format --write .",
+1 -1
View File
@@ -1 +1 @@
export { withDrawing } from "./drawing";
export { withDrawing, type WithDrawingWorkflow } from "./drawing";
+17 -1
View File
@@ -277,7 +277,23 @@ importers:
specifier: 5.8.3
version: 5.8.3
docs: {}
docs:
devDependencies:
'@llamaindex/workflow-core':
specifier: workspace:*
version: link:../packages/core
'@llamaindex/workflow-otel':
specifier: workspace:^
version: link:../packages/otel
'@llamaindex/workflow-viz':
specifier: workspace:^
version: link:../packages/viz
'@opentelemetry/sdk-node':
specifier: ^0.203.0
version: 0.203.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base':
specifier: ^2.0.1
version: 2.0.1(@opentelemetry/api@1.9.0)
packages/core:
devDependencies: