Skip to content

Commit 3093dab

Browse files
Adding Docs for resources (#18980)
* Add ElevenLabs integration * Docs for resources * Revert "Add ElevenLabs integration" This reverts commit 726012e. * applying changed proposed by Massi * Using async steps for workflows + nitting chat messages * update nav --------- Co-authored-by: Logan Markewich <[email protected]>
1 parent 53614e2 commit 3093dab

File tree

5 files changed

+270
-96
lines changed

5 files changed

+270
-96
lines changed

docs/docs/module_guides/workflow/index.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,52 @@ result = await handler
632632
handler = w.run(ctx=handler.ctx)
633633
result = await handler
634634
```
635+
## Resources
636+
637+
Resources are external dependencies you can inject into the steps of a workflow.
638+
639+
A simple example can be:
640+
641+
```python
642+
from llama_index.core.workflow.resource import Resource
643+
from llama_index.core.memory import Memory
644+
645+
646+
def get_memory(*args, **kwargs):
647+
return Memory.from_defaults("user_id_123", token_limit=60000)
648+
649+
650+
class SecondEvent(Event):
651+
msg: str
652+
653+
654+
class WorkflowWithResource(Workflow):
655+
@step
656+
async def first_step(
657+
self,
658+
ev: StartEvent,
659+
memory: Annotated[Memory, Resource(get_memory)],
660+
) -> SecondEvent:
661+
print("Memory before step 1", memory)
662+
await memory.aput(
663+
ChatMessage(role="user", content="This is the first step")
664+
)
665+
print("Memory after step 1", memory)
666+
return SecondEvent(msg="This is an input for step 2")
667+
668+
@step
669+
async def second_step(
670+
self, ev: SecondEvent, memory: Annotated[Memory, Resource(get_memory)]
671+
) -> StopEvent:
672+
print("Memory before step 2", memory)
673+
await memory.aput(ChatMessage(role="user", content=ev.msg))
674+
print("Memory after step 2", memory)
675+
return StopEvent(result="Messages put into memory")
676+
```
677+
678+
The `Resource` wrapper acts as both a type declaration and an executor. At definition time, it specifies the expected type using `Annotated` - for example, a `Memory` object. At runtime, it invokes the associated factory function, such as `get_memory`, to produce the actual instance. The return type of this function must match the declared type, ensuring consistency between what’s expected and what’s provided during execution.
679+
680+
Resources are shared among steps of a workflow, and `Resource` will invoke the factory function only once. In case this is not the desired behavior, passing `cache=False` to `Resource` will inject different resource objects in different steps, invoking the factory function as many times.
635681

636682
## Checkpointing Workflows
637683

docs/docs/understanding/workflows/nested.md

Lines changed: 0 additions & 94 deletions
This file was deleted.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Resources
2+
3+
Resources are a component of workflows that allow us to equip our steps with external dependencies such as memory, LLMs, query engines or chat history.
4+
5+
Resources are a powerful way of binding components to our steps that we otherwise would need to specify by hand every time and, most importantly, resources are **stateful**, meaning that they maintain their state across different steps, unless otherwise specified.
6+
7+
## Using Stateful Resources
8+
9+
In order to use them within our code, we need to import them from the `resource` submodule:
10+
11+
```python
12+
from llama_index.core.workflow.resource import Resource
13+
from llama_index.core.workflow import (
14+
Event,
15+
step,
16+
StartEvent,
17+
StopEvent,
18+
Workflow,
19+
)
20+
```
21+
22+
The `Resource` function works as a wrapper for another function that, when executed, returns an object of a specified type. This is the usage pattern:
23+
24+
```python
25+
from typing import Annotated
26+
from llama_index.core.memory import Memory
27+
28+
29+
def get_memory(*args, **kwargs) -> Memory:
30+
return Memory.from_defaults("user_id_123", token_limit=60000)
31+
32+
33+
resource = Annotated[Memory, Resource(get_memory)]
34+
```
35+
36+
When a step of our workflow will be equipped with this resource, the variable in the step to which the resource is assigned would behave as a memory component:
37+
38+
```python
39+
import random
40+
41+
from typing import Union
42+
from llama_index.core.llms import ChatMessage
43+
44+
RANDOM_MESSAGES = [
45+
"Hello World!",
46+
"Python is awesome!",
47+
"Resources are great!",
48+
]
49+
50+
51+
class CustomStartEvent(StartEvent):
52+
message: str
53+
54+
55+
class SecondEvent(Event):
56+
message: str
57+
58+
59+
class ThirdEvent(Event):
60+
message: str
61+
62+
63+
class WorkflowWithMemory(Workflow):
64+
@step
65+
async def first_step(
66+
self,
67+
ev: CustomStartEvent,
68+
memory: Annotated[Memory, Resource(get_memory)],
69+
) -> SecondEvent:
70+
await memory.aput(
71+
ChatMessage.from_str(
72+
role="user", content="First step: " + ev.message
73+
)
74+
)
75+
return SecondEvent(message=RANDOM_MESSAGES[random.randint(0, 2)])
76+
77+
@step
78+
async def second_step(
79+
self, ev: SecondEvent, memory: Annotated[Memory, Resource(get_memory)]
80+
) -> Union[ThirdEvent, StopEvent]:
81+
await memory.aput(
82+
ChatMessage(role="assistant", content="Second step: " + ev.message)
83+
)
84+
if random.randint(0, 1) == 0:
85+
return ThirdEvent(message=RANDOM_MESSAGES[random.randint(0, 2)])
86+
else:
87+
messages = await memory.aget_all()
88+
return StopEvent(result=messages)
89+
90+
@step
91+
async def third_step(
92+
self, ev: ThirdEvent, memory: Annotated[Memory, Resource(get_memory)]
93+
) -> StopEvent:
94+
await memory.aput(
95+
ChatMessage(role="user", content="Third step: " + ev.message)
96+
)
97+
messages = await memory.aget_all()
98+
return StopEvent(result=messages)
99+
```
100+
101+
As you can see, each step has access to memory and writes to it - the memory is shared among them and we can see it by running the workflow:
102+
103+
```python
104+
wf = WorkflowWithMemory(disable_validation=True)
105+
106+
107+
async def main():
108+
messages = await wf.run(
109+
start_event=CustomStartEvent(message="Happy birthday!")
110+
)
111+
for m in messages:
112+
print(m.blocks[0].text)
113+
114+
115+
if __name__ == "__main__":
116+
import asyncio
117+
118+
asyncio.run(main())
119+
```
120+
121+
A potential result for this might be:
122+
123+
```text
124+
First step: Happy birthday!
125+
Second step: Python is awesome!
126+
Third step: Hello World!
127+
```
128+
129+
This shows that each step added its message to a global memory, which is exactly what we were expecting!
130+
131+
It is important to note, though, the resources are preserved across steps of the same workflow instance, but not across different workflows. If we were to run two `WorkflowWithMemory` instances, their memories would be separate and independent:
132+
133+
```python
134+
wf1 = WorkflowWithMemory(disable_validation=True)
135+
wf2 = WorkflowWithMemory(disable_validation=True)
136+
137+
138+
async def main():
139+
messages1 = await wf1.run(
140+
start_event=CustomStartEvent(message="Happy birthday!")
141+
)
142+
messages2 = await wf1.run(
143+
start_event=CustomStartEvent(message="Happy New Year!")
144+
)
145+
for m in messages1:
146+
print(m.blocks[0].text)
147+
print("===================")
148+
for m in messages2:
149+
print(m.blocks[0].text)
150+
151+
152+
if __name__ == "__main__":
153+
import asyncio
154+
155+
asyncio.run(main())
156+
```
157+
158+
This is a possible output:
159+
160+
```text
161+
First step: Happy birthday!
162+
Second step: Resources are great!
163+
===================
164+
First step: Happy New Year!
165+
Second step: Python is awesome!
166+
```
167+
168+
## Using Steteless Resources
169+
170+
Resources can also be stateless, meaning that we can configure them *not* to be preserved across steps in the same run.
171+
172+
In order to do so, we just need to specify `cache=False` when instantiating `Resource` - let's see this in a simple example, using a custom `Counter` class:
173+
174+
```python
175+
from pydantic import BaseModel, Field
176+
177+
178+
class Counter(BaseModel):
179+
counter: int = Field(description="A simple counter", default=0)
180+
181+
async def increment(self) -> None:
182+
self.counter += 1
183+
184+
185+
def get_counter() -> Counter:
186+
return Counter()
187+
188+
189+
class SecondEvent(Event):
190+
count: int
191+
192+
193+
class WorkflowWithCounter(Workflow):
194+
@step
195+
async def first_step(
196+
self,
197+
ev: StartEvent,
198+
counter: Annotated[Counter, Resource(get_counter, cache=False)],
199+
) -> SecondEvent:
200+
await counter.increment()
201+
return SecondEvent(count=counter.counter)
202+
203+
@step
204+
async def second_step(
205+
self,
206+
ev: SecondEvent,
207+
counter: Annotated[Counter, Resource(get_counter, cache=False)],
208+
) -> StopEvent:
209+
print("Counter at first step: ", ev.count)
210+
await counter.increment()
211+
print("Counter at second step: ", counter.counter)
212+
return StopEvent(result="End of Workflow")
213+
```
214+
215+
If we now run this workflow, we will get out:
216+
217+
```text
218+
Counter at first step: 1
219+
Counter at second step: 1
220+
```
221+
222+
Now that we've mastered resources, let's take a look at [observability and debugging](./observability.md) in workflows.

docs/docs/understanding/workflows/subclass.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,4 @@ draw_all_possible_flows(CustomWorkflow, "custom_workflow.html")
9696

9797
![Custom workflow](subclass.png)
9898

99-
Next, let's look at another way to extend a workflow: [nested workflows](nested.md).
99+
Next, let's look at another way to extend a workflow: [resources](resources.md).

docs/mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ nav:
4949
- Streaming events: ./understanding/workflows/stream.md
5050
- Concurrent execution: ./understanding/workflows/concurrent_execution.md
5151
- Subclassing workflows: ./understanding/workflows/subclass.md
52-
- Nested workflows: ./understanding/workflows/nested.md
52+
- Workflow Resources: ./understanding/workflows/resources.md
5353
- Observability: ./understanding/workflows/observability.md
5454
- Unbound syntax: ./understanding/workflows/unbound_functions.md
5555
- Building a RAG pipeline:

0 commit comments

Comments
 (0)