Chapter 1: Shared State (shared
dictionary)
Welcome to your first step into the world of PocketFlow! Building powerful AI applications often involves breaking down complex tasks into smaller, manageable steps. But how do these steps communicate with each other? How does one part of your AI know what another part has done or figured out? That’s where the shared
dictionary comes into play.
Imagine you’re building a simple AI assistant.
- First, it needs to get your question (e.g., “What’s the weather like in London?”).
- Then, it might need to search the web for “weather in London.”
- Finally, it uses your original question and the search results to give you an answer.
For this to work, the “question understanding” step needs to pass the question to the “web searching” step. Then, both the original question and the search results need to be available to the “answering” step. The shared
dictionary is the magic message board that lets all these steps share information.
What is the shared
Dictionary?
At its heart, the shared
dictionary is a standard Python dictionary (dict
). Think of it like a communal backpack or a shared whiteboard. As your PocketFlow application (which we call a Flow (Flow
, AsyncFlow
)) runs, different components (which we call Nodes (BaseNode
, Node
, AsyncNode
)) can:
- Put things into it (write data).
- Look at what’s inside (read data).
- Update things that are already there.
This shared
dictionary becomes the primary way for different parts of your workflow to pass data, intermediate results, and context to each other. It’s available throughout the entire lifecycle of a single execution of a Flow (Flow
, AsyncFlow
).
How to Use the shared
Dictionary
Let’s see how this works with a few simple examples.
1. Initializing shared
with Starting Data
Before your Flow (Flow
, AsyncFlow
) even starts, you usually prepare some initial data. This data is placed into the shared
dictionary.
Consider this snippet from one of our example projects (cookbook/pocketflow-node/main.py
):
# This is how we can start with some data
text_to_summarize = """
PocketFlow is a minimalist LLM framework...
"""
shared = {"data": text_to_summarize}
# Later, this 'shared' dictionary is passed when running the flow:
# flow.run(shared)
In this code:
- We have some
text_to_summarize
. - We create a Python dictionary named
shared
. - We add an entry to this dictionary: the key is
"data"
and its value is ourtext_to_summarize
. When the Flow (Flow
,AsyncFlow
) starts, thisshared
dictionary will be its starting point.
Here’s another example from cookbook/pocketflow-a2a/main.py
where a question is put into shared
:
# Default question or one from command line
question = "Who won the Nobel Prize in Physics 2024?"
# Process the question
shared = {"question": question}
# agent_flow.run(shared)
Here, the shared
dictionary is initialized with the question
under the key "question"
.
2. A Node (BaseNode
, Node
, AsyncNode
) Reading from shared
Nodes (BaseNode
, Node
, AsyncNode
) are the workers in your Flow (Flow
, AsyncFlow
). They often need to read data from the shared
dictionary to know what to do. This usually happens in a Node’s prep
method.
Let’s look at the Summarize
Node (BaseNode
, Node
, AsyncNode
) from cookbook/pocketflow-node/flow.py
:
# Inside the Summarize Node class
# def prep(self, shared):
# """Read and preprocess data from shared store."""
# return shared["data"] # Accesses the 'data' we set earlier
When this Summarize
Node (BaseNode
, Node
, AsyncNode
) is about to run, its prep
method is called. PocketFlow automatically passes the current shared
dictionary to this method. The line shared["data"]
retrieves the value associated with the key "data"
– which is the text we want to summarize.
Another example from cookbook/pocketflow-a2a/nodes.py
, in the DecideAction
Node (BaseNode
, Node
, AsyncNode
):
# Inside the DecideAction Node's prep method
# def prep(self, shared):
# Get the current context (default if none exists)
context = shared.get("context", "No previous search")
# Get the question from the shared store
question = shared["question"]
return question, context
This prep
method reads two items:
shared.get("context", "No previous search")
: This tries to get the value for the key"context"
. If"context"
isn’t found (maybe it’s the first time this runs), it defaults to"No previous search"
. Using.get()
is a safe way to read, as it prevents errors if a key might be missing.shared["question"]
: This directly retrieves the value for the key"question"
, assuming it will always be there.
3. A Node (BaseNode
, Node
, AsyncNode
) Writing Results Back to shared
After a Node (BaseNode
, Node
, AsyncNode
) does its work (e.g., summarizes text, gets search results), it often needs to save its findings back into the shared
dictionary. This typically happens in a Node’s post
method.
Continuing with our Summarize
Node (BaseNode
, Node
, AsyncNode
) (cookbook/pocketflow-node/flow.py
):
# Inside the Summarize Node class
# 'exec_res' below is the result from the Node's main task
# def post(self, shared, prep_res, exec_res):
# """Store the summary in shared store."""
# shared["summary"] = exec_res # Stores the result
Here, exec_res
holds the summary generated by the Node (BaseNode
, Node
, AsyncNode
). The line shared["summary"] = exec_res
creates a new key "summary"
in the shared
dictionary (or updates it if it already exists) and stores the summary there. Now, subsequent Nodes (BaseNode
, Node
, AsyncNode
) can access this summary!
Similarly, in DecideAction
’s post
method (cookbook/pocketflow-a2a/nodes.py
):
# Inside DecideAction Node's post method
# def post(self, shared, prep_res, exec_res):
# 'exec_res' contains the decision made by an LLM
if exec_res["action"] == "search":
shared["search_query"] = exec_res["search_query"]
# ...
else:
shared["context"] = exec_res["answer"]
# ...
# ...
Depending on the action
decided, this post
method writes either a "search_query"
or an updated "context"
(which is the answer) into the shared
dictionary.
4. Modifying Existing Data in shared
Sometimes, a Node (BaseNode
, Node
, AsyncNode
) needs to update or add to existing information in shared
. For example, in a chat application, you maintain a history of messages.
From cookbook/pocketflow-chat/main.py
, the ChatNode
’s prep
method does this:
# Inside ChatNode's prep method
# def prep(self, shared):
# Initialize messages if this is the first run
if "messages" not in shared:
shared["messages"] = [] # Create an empty list if no history
# ... user_input is obtained ...
# Add user message to history
shared["messages"].append({"role": "user", "content": user_input})
# ...
Here:
- It checks if
"messages"
(our chat history) exists inshared
. If not, it initializesshared["messages"]
as an empty list. - It then appends the new user message to this list. The
shared["messages"]
list grows with each turn of the conversation.
5. Accessing Final Results from shared
Once your Flow (Flow
, AsyncFlow
) has completed all its steps, the shared
dictionary will contain the final outputs and any important intermediate data you chose to store. You can then access these results from your main script.
Back to cookbook/pocketflow-node/main.py
:
# After the flow.run(shared) call:
# The 'shared' dictionary now contains the summary
print("\nSummary:", shared["summary"])
This line simply prints the value associated with the key "summary"
from the shared
dictionary, which was put there by the Summarize
Node (BaseNode
, Node
, AsyncNode
).
Key Characteristics of shared
- It’s a Python Dictionary: This makes it incredibly flexible and easy to use. If you know how to use dictionaries in Python (e.g.,
my_dict['key'] = value
,value = my_dict['key']
,my_dict.get('key', default_value)
), you already know how to interact withshared
. - Scoped to a Single Flow Execution: Each time you run a Flow (
Flow
,AsyncFlow
) (e.g., by callingflow.run(shared_input)
), it operates on its own instance of theshared
dictionary. If you run the same Flow (Flow
,AsyncFlow
) twice, even simultaneously for different requests, they will have completely separateshared
dictionaries. They won’t interfere with each other. Think of it like two people filling out their own copies of the same form. - Persistent Throughout One Flow Execution: The
shared
dictionary is created (or you provide an initial one) when a Flow (Flow
,AsyncFlow
) starts. The exact same dictionary object is then passed from one Node (BaseNode
,Node
,AsyncNode
) to the next. Any modifications made by one Node (BaseNode
,Node
,AsyncNode
) are visible to all subsequent Nodes (BaseNode
,Node
,AsyncNode
).
What Happens Under the Hood? (A Simplified View)
You don’t need to manage the passing of the shared
dictionary yourself; PocketFlow handles it for you. Here’s a simplified step-by-step:
- You start a Flow: You call something like
my_flow.run(initial_shared_data)
.initial_shared_data
is the dictionary you’ve prepared. - PocketFlow takes over: It takes your
initial_shared_data
and passes it to the first Node (BaseNode
,Node
,AsyncNode
) in your Flow (Flow
,AsyncFlow
). - Node executes:
- The Node (
BaseNode
,Node
,AsyncNode
)’sprep
method is called with theshared
dictionary. It can read from it. - The Node (
BaseNode
,Node
,AsyncNode
)’sexec
method (the main workhorse) is called. - The Node (
BaseNode
,Node
,AsyncNode
)’spost
method is called with theshared
dictionary. It can write results back to it.
- The Node (
- Pass it on: PocketFlow determines the next Node (
BaseNode
,Node
,AsyncNode
) to run and passes the same, possibly modified,shared
dictionary to it. - Repeat: Steps 3 and 4 repeat until there are no more Nodes (
BaseNode
,Node
,AsyncNode
) to run in the Flow (Flow
,AsyncFlow
). - Flow ends: The
run
method finishes, and theshared
dictionary you originally passed in now contains all the updates made by the Nodes (BaseNode
,Node
,AsyncNode
).
Here’s a visual way to think about it:
sequenceDiagram
participant You
participant PocketFlowEngine as PocketFlow Engine
participant NodeA as First Node
participant NodeB as Second Node
participant SharedDict as Shared Dictionary
You->>PocketFlowEngine: my_flow.run(initial_shared)
PocketFlowEngine->>SharedDict: Initialize with initial_shared
PocketFlowEngine->>NodeA: process(SharedDict)
NodeA->>SharedDict: Reads input (e.g., shared['question'])
NodeA->>SharedDict: Writes output (e.g., shared['data_from_A'] = ...)
PocketFlowEngine->>NodeB: process(SharedDict)
NodeB->>SharedDict: Reads input (e.g., shared['data_from_A'])
NodeB->>SharedDict: Writes output (e.g., shared['final_answer'] = ...)
PocketFlowEngine->>You: Flow complete (initial_shared is now updated)
Analogy Time!
Think of the shared
dictionary as:
- A Relay Race Baton (but smarter!): Each runner (Node (
BaseNode
,Node
,AsyncNode
)) takes the baton (shared
dictionary), maybe adds a small note or a sticker to it, and then passes it to the next runner. By the end of the race, the baton has collected contributions from everyone. - A Project’s Shared Folder: Imagine a team working on a project. They have a shared folder (
shared
dictionary) on a server. The first person creates a document (initial data). The next person opens it, adds their part, and saves it. The next person does the same. Everyone works on the same set of files in that folder.
Conclusion
You’ve now learned about the shared
dictionary, the backbone of communication within a PocketFlow Flow (Flow
, AsyncFlow
). It’s a simple yet powerful Python dictionary that allows different Nodes (BaseNode
, Node
, AsyncNode
) to share data and context seamlessly. By reading from and writing to shared
, your Nodes (BaseNode
, Node
, AsyncNode
) can collaborate to achieve complex tasks.
Now that you understand how data is passed around, you’re probably wondering about the “workers” themselves – the Nodes (BaseNode
, Node
, AsyncNode
). What are they, and how do you build them? Let’s dive into that in the next chapter!
Next up: Chapter 2: Node (BaseNode
, Node
, AsyncNode
)
Generated by AI Codebase Knowledge Builder