Skip to content

Commit

Permalink
docs: update workflow doc (#1637)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
  • Loading branch information
thucpn and marcusschiesser authored Feb 11, 2025
1 parent d924c63 commit 7bd5d93
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-teachers-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@llamaindex/doc": patch
---

docs: update workflow doc
118 changes: 56 additions & 62 deletions apps/next/src/content/docs/llamaindex/modules/workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ When a step function is added to a workflow, you need to specify the input and o

You can create a `Workflow` to do anything! Build an agent, a RAG flow, an extraction flow, or anything else you want.

import { Tab, Tabs } from "fumadocs-ui/components/tabs";

<Tabs groupId="install" items={["npm", "yarn", "pnpm"]} persist>
```shell tab="npm"
npm install @llamaindex/workflow
```

```shell tab="yarn"
yarn add @llamaindex/workflow
```

```shell tab="pnpm"
pnpm add @llamaindex/workflow
```
</Tabs>

## Getting Started

As an illustrative example, let's consider a naive workflow where a joke is generated and then critiqued.
Expand All @@ -34,51 +50,59 @@ Events are user-defined classes that extend `WorkflowEvent` and contain arbitrar
```typescript
const llm = new OpenAI();
...
const jokeFlow = new Workflow({ verbose: true });
const jokeFlow = new Workflow<unknown, string, string>();
```

Our workflow is implemented by initiating the `Workflow` class. For simplicity, we created a `OpenAI` llm instance.
Our workflow is implemented by initiating the `Workflow` class with three generic types: the context type (unknown), input type (string), and output type (string). The context type is `unknown`, as we're not using a shared context in this example.

For simplicity, we created an `OpenAI` llm instance that we're using for inference in our workflow.

### Workflow Entry Points

```typescript
const generateJoke = async (_context: Context, ev: StartEvent) => {
const prompt = `Write your best joke about ${ev.data.input}.`;
const generateJoke = async (_: unknown, ev: StartEvent<string>) => {
const prompt = `Write your best joke about ${ev.data}.`;
const response = await llm.complete({ prompt });
return new JokeEvent({ joke: response.text });
};
```

Here, we come to the entry-point of our workflow. While events are user-defined, there are two special-case events, the `StartEvent` and the `StopEvent`. Here, the `StartEvent` signifies where to send the initial workflow input.

The `StartEvent` is a bit of a special object since it can hold arbitrary attributes. Here, we accessed the topic with `ev.data.input`.
Here, we come to the entry-point of our workflow. While events are user-defined, there are two special-case events, the `StartEvent` and the `StopEvent`. These events are predefined, but we can specify the payload type using generic types. We're using `StartEvent<string>` to indicate that we're going to send an input of type string.

At this point, you may have noticed that we haven't explicitly told the workflow what events are handled by which steps.

To do so, we use the `addStep` method which adds a step to the workflow. The first argument is the event type that the step will handle, and the second argument is the previously defined step function:
To add this step to the workflow, we use the `addStep` method with an object specifying the input and output event types:

```typescript
jokeFlow.addStep(StartEvent, generateJoke);
jokeFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [JokeEvent],
},
generateJoke
);
```

### Workflow Exit Points

```typescript
const critiqueJoke = async (_context: Context, ev: JokeEvent) => {
const critiqueJoke = async (_: unknown, ev: JokeEvent) => {
const prompt = `Give a thorough critique of the following joke: ${ev.data.joke}`;
const response = await llm.complete({ prompt });
return new StopEvent({ result: response.text });
return new StopEvent(response.text);
};
```

Here, we have our second, and last step, in the workflow. We know its the last step because the special `StopEvent` is returned. When the workflow encounters a returned `StopEvent`, it immediately stops the workflow and returns whatever the result was.

In this case, the result is a string, but it could be a map, array, or any other object.
Here, we have our second and last step in the workflow. We know it's the last step because the special `StopEvent` is returned. When the workflow encounters a returned `StopEvent`, it immediately stops the workflow and returns the result. Note that we're using the generic type `StopEvent<string>` to indicate that we're returning a string.

Don't forget to add the step to the workflow:
Add this step to the workflow:

```typescript
jokeFlow.addStep(JokeEvent, critiqueJoke);
jokeFlow.addStep(
{
inputs: [JokeEvent],
outputs: [StopEvent<string>],
},
critiqueJoke
);
```

### Running the Workflow
Expand All @@ -90,42 +114,25 @@ console.log(result.data.result);

Lastly, we run the workflow. The `.run()` method is async, so we use await here to wait for the result.

### Validating Workflows

To tell the workflow what events are produced by each step, you can optionally provide a third argument to `addStep` to specify the output event type:

```typescript
jokeFlow.addStep(StartEvent, generateJoke, { outputs: JokeEvent });
jokeFlow.addStep(JokeEvent, critiqueJoke, { outputs: StopEvent });
```
## Working with Shared Context/State

To validate a workflow, you need to call the `validate` method:
Optionally, you can choose to use a shared context between steps by specifying a context type when creating the workflow. Here's an example where multiple steps access a shared state:

```typescript
jokeFlow.validate();
```
import { HandlerContext } from "@llamaindex/workflow";

To automatically validate a workflow when you run it, you can set the `validate` flag to `true` at initialization:

```typescript
const jokeFlow = new Workflow({ verbose: true, validate: true });
```
type MyContextData = {
query: string;
intermediateResults: any[];
}

## Working with Global Context/State

Optionally, you can choose to use global context between steps. For example, maybe multiple steps access the original `query` input from the user. You can store this in global context so that every step has access.

```typescript
import { Context } from "llamaindex";

const query = async (context: Context, ev: MyEvent) => {
const query = async (context: HandlerContext<MyContextData>, ev: MyEvent) => {
// get the query from the context
const query = context.get("query");
const query = context.data.query;
// do something with context and event
const val = ...
const result = ...
// store in context
context.set("key", val);
context.data.intermediateResults.push(val);

return new StopEvent({ result });
};
Expand All @@ -138,28 +145,15 @@ The context does more than just hold data, it also provides utilities to buffer
For example, you might have a step that waits for a query and retrieved nodes before synthesizing a response:

```typescript
const synthesize = async (context: Context, ev: QueryEvent | RetrieveEvent) => {
const events = context.collectEvents(ev, [QueryEvent | RetrieveEvent]);
if (!events) {
return;
}
const prompt = events
.map((event) => {
if (event instanceof QueryEvent) {
return `Answer this query using the context provided: ${event.data.query}`;
} else if (event instanceof RetrieveEvent) {
return `Context: ${event.data.context}`;
}
return "";
})
.join("\n");

const synthesize = async (context: Context, ev1: QueryEvent, ev2: RetrieveEvent) => {
const subPrompts = [`Answer this query using the context provided: ${ev1.data.query}`, `Context: ${ev2.data.context}`];
const prompt = subPrompts.join("\n");
const response = await llm.complete({ prompt });
return new StopEvent({ result: response.text });
};
```

Using `ctx.collectEvents()` we can buffer and wait for ALL expected events to arrive. This function will only return events (in the requested order) once all events have arrived.
Passing multiple events, we can buffer and wait for ALL expected events to arrive. The receiving step function will only be called once all events have arrived.

## Manually Triggering Events

Expand Down

0 comments on commit 7bd5d93

Please sign in to comment.