chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:48:47 +08:00
commit 85f20078e8
208 changed files with 57629 additions and 0 deletions
@@ -0,0 +1,252 @@
---
layout: default
title: "Node (BaseNode, Node, AsyncNode)"
parent: "PocketFlow"
nav_order: 2
---
# Chapter 2: Node (`BaseNode`, `Node`, `AsyncNode`)
In [Chapter 1: Shared State (`shared` dictionary)](01_shared_state___shared__dictionary__.md), we learned how different parts of a PocketFlow workflow can communicate using the `shared` dictionary. Now, let's meet the actual "workers" that perform the tasks and use this shared information: **Nodes**.
## What are Nodes and Why Do We Need Them?
Imagine you're building an AI that helps you write a story. This process might involve several steps:
1. Generate a story idea.
2. Write an outline based on the idea.
3. Write the first draft of a chapter using the outline.
4. Review and edit the chapter.
Each of these steps is a distinct task. In PocketFlow, each such task would be handled by a **Node**.
A **Node** is the fundamental building block in PocketFlow. It represents a single, atomic step in your workflow. Think of it as a highly specialized worker on an assembly line, responsible for one specific job. This job could be:
* Calling a Large Language Model (LLM) to generate text.
* Searching the web for information.
* Making a decision based on some data.
* Reading user input.
* Saving results to a file.
By breaking down a complex process into a series of Nodes, we make our AI applications:
* **Modular:** Each Node focuses on one thing, making it easier to develop, test, and understand.
* **Reusable:** A Node designed for web search can be used in many different AI applications.
* **Manageable:** It's easier to build and debug a sequence of simple steps than one giant, monolithic piece of code.
## The Anatomy of a Node: `prep`, `exec`, and `post`
Most Nodes in PocketFlow have a similar structure, typically involving three key methods:
1. **`prep(self, shared)` (Prepare):**
* **Purpose:** This method is called *before* the Node does its main work. Its job is to get any necessary input data from the [shared dictionary](01_shared_state___shared__dictionary__.md).
* **Analogy:** An assembly line worker picking up the necessary parts from a shared bin before starting their task.
* **Input:** It receives the `shared` dictionary.
* **Output:** It usually returns the specific data the Node needs for its core logic.
2. **`exec(self, prep_res)` (Execute):**
* **Purpose:** This is where the Node performs its core task. This is the "brain" or "muscle" of the Node.
* **Analogy:** The assembly line worker actually assembling the parts or performing their specialized action.
* **Input:** It receives the result from the `prep` method (`prep_res`).
* **Output:** It returns the result of its execution (e.g., a summary, search results, a decision).
3. **`post(self, shared, prep_res, exec_res)` (Post-process):**
* **Purpose:** This method is called *after* the Node has finished its main work. Its jobs are:
* To process the results from `exec`.
* To update the [shared dictionary](01_shared_state___shared__dictionary__.md) with these results or any other new information.
* To decide what should happen next in the workflow (this is crucial for [Actions / Transitions](03_actions___transitions_.md), which we'll cover in the next chapter).
* **Analogy:** The assembly line worker placing the finished component onto the conveyor belt (updating `shared`) and signaling if the item needs to go to a different station next (deciding the next action).
* **Input:** It receives the `shared` dictionary, the result from `prep` (`prep_res`), and the result from `exec` (`exec_res`).
* **Output:** It can return an "action" string that tells the [Flow (`Flow`, `AsyncFlow`)](04_flow___flow____asyncflow__.md) which Node to execute next. If it returns nothing (or `None`), a default transition is usually followed.
Let's make this concrete with a simple example: a `SummarizeNode` whose job is to take some text and produce a short summary.
```python
# This is a conceptual Node, actual implementation details might vary slightly
from pocketflow import Node # We'll import the base class
class SummarizeNode(Node):
def prep(self, shared):
# 1. Prepare: Get the text to summarize from 'shared'
print("SummarizeNode: Preparing...")
text_to_summarize = shared.get("document_text", "No text found.")
return text_to_summarize
def exec(self, text_input):
# 2. Execute: Perform the summarization (e.g., call an LLM)
print(f"SummarizeNode: Executing with text: '{text_input[:30]}...'")
if not text_input or text_input == "No text found.":
return "Cannot summarize empty or missing text."
# In a real scenario, this would call an LLM or a summarization library
summary = f"This is a summary of: {text_input[:20]}..."
return summary
def post(self, shared, prep_res, exec_res):
# 3. Post-process: Store the summary in 'shared'
print(f"SummarizeNode: Posting summary: '{exec_res}'")
shared["summary_output"] = exec_res
# We might decide the next step here, e.g., return "summarization_done"
# For now, we'll just let it end by returning nothing (None)
```
Let's imagine how this `SummarizeNode` would work:
1. **Initialization:** You'd start with some text in the `shared` dictionary.
```python
shared_data = {"document_text": "PocketFlow is a cool framework for building AI."}
```
2. **Running the Node (simplified):**
* **`prep(shared_data)` is called:** It looks into `shared_data` and finds `"PocketFlow is a cool framework for building AI."`. It returns this text.
* **`exec("PocketFlow is a cool framework...")` is called:** It takes the text and (in our simplified example) creates a summary like `"This is a summary of: PocketFlow is a cool..."`. It returns this summary.
* **`post(shared_data, text_from_prep, summary_from_exec)` is called:** It takes the `shared_data` and the `summary_from_exec`. It then adds a new entry: `shared_data["summary_output"] = "This is a summary of: PocketFlow is a cool..."`.
After the Node runs, `shared_data` would look like this:
```
{
"document_text": "PocketFlow is a cool framework for building AI.",
"summary_output": "This is a summary of: PocketFlow is a cool..."
}
```
The summary is now available in the `shared` dictionary for other Nodes or for final output!
## Types of Nodes: `BaseNode`, `Node`, `AsyncNode`
PocketFlow provides a few variations of Nodes, built on top of each other:
* **`BaseNode`:**
* This is the most fundamental type of Node. It provides the basic structure with `prep`, `exec`, and `post` methods.
* It's like the basic blueprint for any worker.
* **`Node` (inherits from `BaseNode`):**
* This is the standard synchronous Node you'll often use. "Synchronous" means it performs its task and waits for it to complete before anything else happens.
* It adds helpful features on top of `BaseNode`, like automatic **retries** if the `exec` method fails (e.g., a network error when calling an LLM) and an `exec_fallback` method that can be called if all retries fail.
* From `cookbook/pocketflow-node/flow.py`, our `Summarize` Node is an example of `Node`:
```python
from pocketflow import Node
# ... other imports ...
class Summarize(Node): # Inherits from Node
# ... prep, exec, post methods ...
def exec_fallback(self, shared, prep_res, exc):
"""Provide a simple fallback instead of crashing."""
return "There was an error processing your request."
```
This `Summarize` Node, if its `exec` method fails (e.g., `call_llm` raises an error), will retry (default is 1 retry, but can be configured like `Summarize(max_retries=3)`). If all retries fail, `exec_fallback` is called.
* **`AsyncNode` (inherits from `Node`):**
* This type of Node is for **asynchronous** tasks. Asynchronous tasks are those that might take some time to complete (like waiting for a web request or a user to type something) but don't need to block the entire program while they wait. They can "pause" and let other things run.
* `AsyncNode` uses `async` and `await` keywords from Python's `asyncio` library.
* It has asynchronous versions of the core methods: `prep_async`, `exec_async`, and `post_async`.
* We'll dive much deeper into asynchronous operations in [Chapter 5: Asynchronous Processing (`AsyncNode`, `AsyncFlow`)](05_asynchronous_processing___asyncnode____asyncflow___.md). For now, just know it exists for tasks that involve waiting.
* Example from `cookbook/pocketflow-async-basic/nodes.py`:
```python
from pocketflow import AsyncNode
# ... other imports ...
class FetchRecipes(AsyncNode): # Inherits from AsyncNode
async def prep_async(self, shared):
# ... prepare input asynchronously ...
ingredient = await get_user_input("Enter ingredient: ") # get_user_input is async
return ingredient
async def exec_async(self, ingredient):
# ... execute task asynchronously ...
recipes = await fetch_recipes(ingredient) # fetch_recipes is async
return recipes
async def post_async(self, shared, prep_res, recipes):
# ... post-process asynchronously ...
shared["recipes"] = recipes
return "suggest" # Action for the next step
```
Notice the `async def` and `await` keywords. This `FetchRecipes` Node can wait for user input and web requests without freezing the application.
## How a Node Runs: Under the Hood (Simplified)
When PocketFlow decides it's time for a particular Node to run (as part of a [Flow (`Flow`, `AsyncFlow`)](04_flow___flow____asyncflow__.md)), it essentially orchestrates the calling of its `prep`, `exec`, and `post` methods in sequence.
Here's a simplified view of what happens when a synchronous `Node`'s internal `_run` method is invoked:
1. **Call `prep`:** `prep_result = self.prep(shared)`
* Your Node's `prep` method is called with the current `shared` dictionary.
* Whatever `prep` returns is stored.
2. **Call `_exec` (which internally calls your `exec` with retries):** `exec_result = self._exec(prep_result)`
* The Node's `_exec` method is called with the `prep_result`.
* This `_exec` method in the `Node` class handles the retry logic. It will try to call your `exec(prep_result)` method.
* If your `exec` succeeds, its result is stored.
* If your `exec` raises an exception, `_exec` might wait and try again (up to `max_retries`).
* If all retries fail, `exec_fallback(prep_result, exception)` is called, and its result is used as `exec_result`.
3. **Call `post`:** `action = self.post(shared, prep_result, exec_result)`
* Your Node's `post` method is called with the `shared` dictionary, the `prep_result`, and the `exec_result`.
* `post` can modify `shared` and returns an action string (or `None`).
4. **Return Action:** The `action` returned by `post` is then used by the [Flow (`Flow`, `AsyncFlow`)](04_flow___flow____asyncflow__.md) to determine the next Node to run.
Let's visualize this with a sequence diagram:
```mermaid
sequenceDiagram
participant FlowEngine as PocketFlow Engine
participant YourNode as Your Node Instance
participant SharedDict as Shared Dictionary
FlowEngine->>YourNode: _run(SharedDict)
YourNode->>YourNode: prep(SharedDict)
Note right of YourNode: Reads from SharedDict
YourNode-->>SharedDict: Access data (e.g., shared['input'])
YourNode->>YourNode: _exec(prep_result)
Note right of YourNode: Calls your exec(), handles retries/fallback
YourNode->>YourNode: post(SharedDict, prep_result, exec_result)
Note right of YourNode: Writes to SharedDict, decides next action
YourNode-->>SharedDict: Update data (e.g., shared['output'] = ...)
YourNode-->>FlowEngine: Returns action string
```
**Code Glimpse (from `pocketflow/__init__.py`):**
The `BaseNode` class defines the fundamental execution flow in its `_run` method (this is a direct, slightly simplified version):
```python
# Inside BaseNode class from pocketflow/__init__.py
def _run(self, shared):
prep_output = self.prep(shared)
exec_output = self._exec(prep_output) # _exec calls self.exec
action = self.post(shared, prep_output, exec_output)
return action
```
This is the core loop for a single Node's execution.
The `Node` class (which inherits from `BaseNode`) overrides `_exec` to add retry and fallback logic:
```python
# Simplified concept from Node class in pocketflow/__init__.py
def _exec(self, prep_res):
for self.cur_retry in range(self.max_retries): # Loop for retries
try:
return self.exec(prep_res) # Call your Node's exec method
except Exception as e:
if self.cur_retry == self.max_retries - 1: # If last retry
return self.exec_fallback(prep_res, e) # Call fallback
if self.wait > 0:
time.sleep(self.wait) # Wait before retrying
```
This shows how `Node` makes your worker more robust by automatically handling temporary failures.
For `AsyncNode`, the methods are `prep_async`, `exec_async`, `post_async`, and they are `await`ed, allowing other tasks to run while waiting for I/O operations. This will be detailed in [Chapter 5](05_asynchronous_processing___asyncnode____asyncflow___.md).
## Conclusion
You've now been introduced to **Nodes**, the workhorses of PocketFlow!
* They represent **single, atomic steps** in your workflow.
* They typically follow a **`prep` -> `exec` -> `post`** lifecycle.
* `prep` gets data from the [shared dictionary](01_shared_state___shared__dictionary__.md).
* `exec` performs the core logic.
* `post` updates the `shared` dictionary and can decide what happens next.
* **`Node`** provides synchronous execution with retries and fallbacks.
* **`AsyncNode`** provides asynchronous execution for I/O-bound tasks.
Nodes are the building blocks you'll use to define the individual capabilities of your AI agents and applications. But how do these Nodes connect to form a sequence or a more complex workflow? And how does the `post` method's return value actually control the flow? That's where [Actions / Transitions](03_actions___transitions__.md) come in, which we'll explore in the next chapter!
Next up: [Chapter 3: Actions / Transitions](03_actions___transitions__.md)
---
Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge)