Skip to content

Commit 11e39a0

Browse files
merge all workflows docs into a single place (#234)
1 parent 9fa874b commit 11e39a0

20 files changed

+634
-287
lines changed
511 KB
Loading
130 KB
Loading
136 KB
Loading
68 KB
Loading
78.7 KB
Loading
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
sidebar:
3+
order: 2
4+
title: Branches and loops
5+
---
6+
7+
A key feature of Workflows is their enablement of branching and looping logic, more simply and flexibly than graph-based approaches.
8+
9+
## Loops in workflows
10+
11+
To create a loop, we'll take a `LoopingWorkflow` that randomly loops. It will have a single event that we'll call `LoopEvent` (but it can have any arbitrary name).
12+
13+
```python
14+
from workflows.events import Event
15+
16+
class LoopEvent(Event):
17+
num_loops: int
18+
```
19+
20+
Now we'll `import random` and modify our `step_one` function to randomly decide either to loop or to continue:
21+
22+
```python
23+
import random
24+
from workflows import Workflow, step
25+
from workflows.events import StartEvent, StopEvent
26+
27+
class LoopingWorkflow(Workflow):
28+
@step
29+
async def prepare_input(self, ev: StartEvent) -> LoopEvent:
30+
num_loops = random.randint(0, 10)
31+
return LoopEvent(num_loops=num_loops)
32+
33+
@step
34+
async def loop_step(self, ev: LoopEvent) -> LoopEvent | StopEvent:
35+
if ev.num_loops <= 0:
36+
return StopEvent(result="Done looping!")
37+
38+
return LoopEvent(num_loops=ev.num_loops-1)
39+
```
40+
41+
Let's visualize this:
42+
43+
![A simple loop](./assets/loop.png)
44+
45+
You can create a loop from any step to any other step by defining the appropriate event types and return types.
46+
47+
## Branches in workflows
48+
49+
Closely related to looping is branching. As you've already seen, you can conditionally return different events. Let's see a workflow that branches into two different paths:
50+
51+
```python
52+
import random
53+
from workflows import Workflow, step
54+
from workflows.events import Event, StartEvent, StopEvent
55+
56+
class BranchA1Event(Event):
57+
payload: str
58+
59+
60+
class BranchA2Event(Event):
61+
payload: str
62+
63+
64+
class BranchB1Event(Event):
65+
payload: str
66+
67+
68+
class BranchB2Event(Event):
69+
payload: str
70+
71+
72+
class BranchWorkflow(Workflow):
73+
@step
74+
async def start(self, ev: StartEvent) -> BranchA1Event | BranchB1Event:
75+
if random.randint(0, 1) == 0:
76+
print("Go to branch A")
77+
return BranchA1Event(payload="Branch A")
78+
else:
79+
print("Go to branch B")
80+
return BranchB1Event(payload="Branch B")
81+
82+
@step
83+
async def step_a1(self, ev: BranchA1Event) -> BranchA2Event:
84+
print(ev.payload)
85+
return BranchA2Event(payload=ev.payload)
86+
87+
@step
88+
async def step_b1(self, ev: BranchB1Event) -> BranchB2Event:
89+
print(ev.payload)
90+
return BranchB2Event(payload=ev.payload)
91+
92+
@step
93+
async def step_a2(self, ev: BranchA2Event) -> StopEvent:
94+
print(ev.payload)
95+
return StopEvent(result="Branch A complete.")
96+
97+
@step
98+
async def step_b2(self, ev: BranchB2Event) -> StopEvent:
99+
print(ev.payload)
100+
return StopEvent(result="Branch B complete.")
101+
```
102+
103+
Our imports are the same as before, but we've created 4 new event types. `start` randomly decides to take one branch or another, and then multiple steps in each branch complete the workflow. Let's visualize this:
104+
105+
![A simple branch](./assets/branching.png)
106+
107+
You can of course combine branches and loops in any order to fulfill the needs of your application. Later in this tutorial you'll learn how to run multiple branches in parallel using `send_event` and synchronize them using `collect_events`.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
sidebar:
3+
order: 5
4+
title: Concurrent execution of workflows
5+
---
6+
7+
In addition to looping, branching, and streaming, workflows can run steps concurrently. This is useful when you have multiple steps that can be run independently of each other and they have time-consuming operations that they `await`, allowing other steps to run in parallel.
8+
9+
## Emitting multiple events
10+
11+
To emit multiple events to trigger multiple steps, you can use `ctx.send_event()`:
12+
13+
```python
14+
import asyncio
15+
from workflows import Workflow, Context, step
16+
from workflows.events import Event, StartEvent, StopEvent
17+
18+
class StepTwoEvent(Event):
19+
query: str
20+
21+
class ParallelFlow(Workflow):
22+
@step
23+
async def start(self, ctx: Context, ev: StartEvent) -> StepTwoEvent | None:
24+
ctx.send_event(StepTwoEvent(query="Query 1"))
25+
ctx.send_event(StepTwoEvent(query="Query 2"))
26+
ctx.send_event(StepTwoEvent(query="Query 3"))
27+
28+
@step(num_workers=4)
29+
async def step_two(self, ev: StepTwoEvent) -> StopEvent:
30+
print("Running slow query ", ev.query)
31+
await asyncio.sleep(random.randint(0, 5))
32+
33+
return StopEvent(result=ev.query)
34+
```
35+
36+
In this example, our `start` step emits 3 `StepTwoEvent`s. The `step_two` step is decorated with `num_workers=4`, which tells the workflow to run up to 4 instances of this step concurrently (this is the default).
37+
38+
## Collecting events
39+
40+
If you execute the previous example, you'll note that the workflow stops after whichever query is first to complete. Sometimes that's useful, but other times you'll want to wait for all your slow operations to complete before moving on to another step. You can do this using `collect_events`:
41+
42+
```python
43+
import asyncio
44+
from workflows import Workflow, Context, step
45+
from workflows.events import Event, StartEvent, StopEvent
46+
47+
class StepTwoEvent(Event):
48+
query: str
49+
50+
class StepThreeEvent(Event):
51+
result: str
52+
53+
class ConcurrentFlow(Workflow):
54+
@step
55+
async def start(self, ctx: Context, ev: StartEvent) -> StepTwoEvent | None:
56+
ctx.send_event(StepTwoEvent(query="Query 1"))
57+
ctx.send_event(StepTwoEvent(query="Query 2"))
58+
ctx.send_event(StepTwoEvent(query="Query 3"))
59+
60+
@step(num_workers=4)
61+
async def step_two(self, ctx: Context, ev: StepTwoEvent) -> StepThreeEvent:
62+
print("Running query ", ev.query)
63+
await asyncio.sleep(random.randint(1, 5))
64+
return StepThreeEvent(result=ev.query)
65+
66+
@step
67+
async def step_three(
68+
self, ctx: Context, ev: StepThreeEvent
69+
) -> StopEvent | None:
70+
# wait until we receive 3 events
71+
result = ctx.collect_events(ev, [StepThreeEvent] * 3)
72+
if result is None:
73+
return None
74+
75+
# do something with all 3 results together
76+
print(result)
77+
return StopEvent(result="Done")
78+
```
79+
80+
The `collect_events` method lives on the `Context` and takes the event that triggered the step and an array of event types to wait for. In this case, we are awaiting 3 events of the same `StepThreeEvent` type.
81+
82+
The `step_three` step is fired every time a `StepThreeEvent` is received, but `collect_events` will return `None` until all 3 events have been received. At that point, the step will continue and you can do something with all 3 results together.
83+
84+
The `result` returned from `collect_events` is an array of the events that were collected, in the order that they were received.
85+
86+
## Multiple event types
87+
88+
Of course, you do not need to wait for the same type of event. You can wait for any combination of events you like, such as in this example:
89+
90+
```python
91+
import asyncio
92+
from workflows import Workflow, Context, step
93+
from workflows.events import Event, StartEvent, StopEvent
94+
95+
class StepAEvent(Event):
96+
query: str
97+
98+
class StepBEvent(Event):
99+
query: str
100+
101+
class StepCEvent(Event):
102+
query: str
103+
104+
class StepACompleteEvent(Event):
105+
result: str
106+
107+
class StepBCompleteEvent(Event):
108+
result: str
109+
110+
class StepCCompleteEvent(Event):
111+
result: str
112+
113+
114+
class ConcurrentFlow(Workflow):
115+
@step
116+
async def start(
117+
self, ctx: Context, ev: StartEvent
118+
) -> StepAEvent | StepBEvent | StepCEvent | None:
119+
ctx.send_event(StepAEvent(query="Query 1"))
120+
ctx.send_event(StepBEvent(query="Query 2"))
121+
ctx.send_event(StepCEvent(query="Query 3"))
122+
123+
@step
124+
async def step_a(self, ctx: Context, ev: StepAEvent) -> StepACompleteEvent:
125+
print("Doing something A-ish")
126+
return StepACompleteEvent(result=ev.query)
127+
128+
@step
129+
async def step_b(self, ctx: Context, ev: StepBEvent) -> StepBCompleteEvent:
130+
print("Doing something B-ish")
131+
return StepBCompleteEvent(result=ev.query)
132+
133+
@step
134+
async def step_c(self, ctx: Context, ev: StepCEvent) -> StepCCompleteEvent:
135+
print("Doing something C-ish")
136+
return StepCCompleteEvent(result=ev.query)
137+
138+
@step
139+
async def step_three(
140+
self,
141+
ctx: Context,
142+
ev: StepACompleteEvent | StepBCompleteEvent | StepCCompleteEvent,
143+
) -> StopEvent:
144+
print("Received event ", ev.result)
145+
146+
# wait until we receive 3 events
147+
if (
148+
ctx.collect_events(
149+
ev,
150+
[StepCCompleteEvent, StepACompleteEvent, StepBCompleteEvent],
151+
)
152+
is None
153+
):
154+
return None
155+
156+
# do something with all 3 results together
157+
return StopEvent(result="Done")
158+
```
159+
160+
There are several changes we've made to handle multiple event types:
161+
162+
- `start` is now declared as emitting 3 different event types
163+
- `step_three` is now declared as accepting 3 different event types
164+
- `collect_events` now takes an array of the event types to wait for
165+
166+
Note that the order of the event types in the array passed to `collect_events` is important. The events will be returned in the order they are passed to `collect_events`, regardless of when they were received.
167+
168+
The visualization of this workflow is quite pleasing:
169+
170+
![A concurrent workflow](./assets/different_events.png)

docs/src/content/docs/llamaagents/workflows/customizing_entry_exit_points.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
---
2+
sidebar:
3+
order: 7
24
title: Customizing entry and exit points
35
---
46

5-
Most of the times, relying on the default entry and exit points we have seen in the [Getting Started](/python/workflows/) section is enough.
7+
Most of the times, relying on the default entry and exit points we have seen in the [Getting Started](/python/llamaagents/workflows/) section is enough.
68
However, workflows support custom events where you normally would expect `StartEvent` and `StopEvent`, let's see how.
79

810
## Using a custom `StartEvent`
@@ -41,7 +43,7 @@ class JokeFlow(Workflow):
4143
) -> JokeEvent:
4244
# Build a query engine using the index and the llm from the start event
4345
query_engine = ev.an_index.as_query_engine(llm=ev.an_llm)
44-
topic = query_engine.query(
46+
topic = await query_engine.aquery(
4547
f"What is the closest topic to {a_string_field}"
4648
)
4749
# Use the llm attached to the start event to instruct the model

docs/src/content/docs/llamaagents/workflows/deployment.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
---
2+
sidebar:
3+
order: 12
24
title: Run Your Workflow as a Server
35
---
46

@@ -33,8 +35,9 @@ class GreetingWorkflow(Workflow):
3335
ctx.write_event_to_stream(StreamEvent(sequence=i))
3436
await asyncio.sleep(0.3)
3537

36-
name = getattr(ev, "name", "World")
38+
name = ev.get("name", "World")
3739
return StopEvent(result=f"Hello, {name}!")
40+
3841
greet_wf = GreetingWorkflow()
3942

4043

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
---
2+
sidebar:
3+
order: 8
24
title: Drawing a Workflow
35
---
46

5-
Workflows can be visualized, using the power of type annotations in your step definitions. You can either draw all possible paths through the workflow, or the most recent execution, to help with debugging.
7+
Workflows can be visualized, using the power of type annotations in your step definitions.
8+
9+
There are two main ways to visualize your workflows.
10+
11+
## 1. Converting a Workflow to HTML
612

713
First install:
814

@@ -19,35 +25,40 @@ from llama_index.utils.workflow import (
1925
)
2026

2127
# Draw all
22-
draw_all_possible_flows(JokeFlow, filename="joke_flow_all.html")
28+
draw_all_possible_flows(MyWorkflow, filename="all_paths.html")
2329

2430
# Draw an execution
25-
w = JokeFlow()
31+
w = MyWorkflow()
2632
handler = w.run(topic="Pirates")
2733
await handler
28-
draw_most_recent_execution(handler, filename="joke_flow_recent.html")
34+
draw_most_recent_execution(handler, filename="most_recent.html")
2935
```
3036

31-
<div id="working-with-global-context-state"></div>
32-
## Working with Global Context/State
37+
## 2. Using the `workflow-debugger`
3338

34-
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.
39+
Workflows ship with a [`WorkflowServer`](/python/llamaagents/workflows/deployment) that allows you to convert workflows to API's. As part of the `WorkflowServer`, a debugging UI is provided as the home `/` page.
3540

36-
```python
37-
from workflows import Context
41+
Using this server app, you can visualize and run your workflows.
3842

43+
![workflow debugger](./assets/ui_sample.png)
3944

40-
@step
41-
async def query(self, ctx: Context, ev: MyEvent) -> StopEvent:
42-
# retrieve from context
43-
query = await ctx.store.get("query")
45+
Setting up the server is straightforward:
46+
47+
```python
48+
import asyncio
49+
from workflows import Workflow, step
50+
from workflows.events import StartEvent, StopEvent
4451

45-
# do something with context and event
46-
val = ...
47-
result = ...
52+
class MyWorkflow(Workflow):
53+
@step
54+
async def my_step(self, ev: StartEvent) -> StopEvent:
55+
return StopEvent(result="Done!")
4856

49-
# store in context
50-
await ctx.store.set("key", val)
57+
async def main():
58+
server = WorkflowServer()
59+
server.add_workflow("my_workflow", MyWorkflow())
60+
await server.serve("0.0.0.0", "8080")
5161

52-
return StopEvent(result=result)
62+
if __name__ == "__main__":
63+
asyncio.run(main())
5364
```

0 commit comments

Comments
 (0)