From 41ecb66b4ae798448507b9b2a6972fbe28d5fd89 Mon Sep 17 00:00:00 2001
From: Zach <33015448+zachary62@users.noreply.github.com>
Date: Wed, 2 Apr 2025 16:56:35 -0400
Subject: [PATCH] Initial commit
---
.clinerules | 1664 +++++++++++++++++++++++++++++++++++++++++++++
.cursorrules | 1664 +++++++++++++++++++++++++++++++++++++++++++++
.gitignore | 92 +++
.windsurfrules | 1664 +++++++++++++++++++++++++++++++++++++++++++++
README.md | 20 +
assets/banner.png | Bin 0 -> 4132203 bytes
docs/design.md | 79 +++
flow.py | 14 +
main.py | 16 +
nodes.py | 26 +
requirements.txt | 1 +
utils/__init__.py | 0
utils/call_llm.py | 14 +
13 files changed, 5254 insertions(+)
create mode 100644 .clinerules
create mode 100644 .cursorrules
create mode 100644 .gitignore
create mode 100644 .windsurfrules
create mode 100644 README.md
create mode 100644 assets/banner.png
create mode 100644 docs/design.md
create mode 100644 flow.py
create mode 100644 main.py
create mode 100644 nodes.py
create mode 100644 requirements.txt
create mode 100644 utils/__init__.py
create mode 100644 utils/call_llm.py
diff --git a/.clinerules b/.clinerules
new file mode 100644
index 0000000..8f05631
--- /dev/null
+++ b/.clinerules
@@ -0,0 +1,1664 @@
+---
+layout: default
+title: "Agentic Coding"
+---
+
+# Agentic Coding: Humans Design, Agents code!
+
+> If you are an AI agents involved in building LLM Systems, read this guide **VERY, VERY** carefully! This is the most important chapter in the entire document. Throughout development, you should always (1) start with a small and simple solution, (2) design at a high level (`docs/design.md`) before implementation, and (3) frequently ask humans for feedback and clarification.
+{: .warning }
+
+## Agentic Coding Steps
+
+Agentic Coding should be a collaboration between Human System Design and Agent Implementation:
+
+| Steps | Human | AI | Comment |
+|:-----------------------|:----------:|:---------:|:------------------------------------------------------------------------|
+| 1. Requirements | ★★★ High | ★☆☆ Low | Humans understand the requirements and context. |
+| 2. Flow | ★★☆ Medium | ★★☆ Medium | Humans specify the high-level design, and the AI fills in the details. |
+| 3. Utilities | ★★☆ Medium | ★★☆ Medium | Humans provide available external APIs and integrations, and the AI helps with implementation. |
+| 4. Node | ★☆☆ Low | ★★★ High | The AI helps design the node types and data handling based on the flow. |
+| 5. Implementation | ★☆☆ Low | ★★★ High | The AI implements the flow based on the design. |
+| 6. Optimization | ★★☆ Medium | ★★☆ Medium | Humans evaluate the results, and the AI helps optimize. |
+| 7. Reliability | ★☆☆ Low | ★★★ High | The AI writes test cases and addresses corner cases. |
+
+1. **Requirements**: Clarify the requirements for your project, and evaluate whether an AI system is a good fit.
+ - Understand AI systems' strengths and limitations:
+ - **Good for**: Routine tasks requiring common sense (filling forms, replying to emails)
+ - **Good for**: Creative tasks with well-defined inputs (building slides, writing SQL)
+ - **Not good for**: Ambiguous problems requiring complex decision-making (business strategy, startup planning)
+ - **Keep It User-Centric:** Explain the "problem" from the user's perspective rather than just listing features.
+ - **Balance complexity vs. impact**: Aim to deliver the highest value features with minimal complexity early.
+
+2. **Flow Design**: Outline at a high level, describe how your AI system orchestrates nodes.
+ - Identify applicable design patterns (e.g., [Map Reduce](./design_pattern/mapreduce.md), [Agent](./design_pattern/agent.md), [RAG](./design_pattern/rag.md)).
+ - For each node in the flow, start with a high-level one-line description of what it does.
+ - If using **Map Reduce**, specify how to map (what to split) and how to reduce (how to combine).
+ - If using **Agent**, specify what are the inputs (context) and what are the possible actions.
+ - If using **RAG**, specify what to embed, noting that there's usually both offline (indexing) and online (retrieval) workflows.
+ - Outline the flow and draw it in a mermaid diagram. For example:
+ ```mermaid
+ flowchart LR
+ start[Start] --> batch[Batch]
+ batch --> check[Check]
+ check -->|OK| process
+ check -->|Error| fix[Fix]
+ fix --> check
+
+ subgraph process[Process]
+ step1[Step 1] --> step2[Step 2]
+ end
+
+ process --> endNode[End]
+ ```
+ - > **If Humans can't specify the flow, AI Agents can't automate it!** Before building an LLM system, thoroughly understand the problem and potential solution by manually solving example inputs to develop intuition.
+ {: .best-practice }
+
+3. **Utilities**: Based on the Flow Design, identify and implement necessary utility functions.
+ - Think of your AI system as the brain. It needs a body—these *external utility functions*—to interact with the real world:
+

+
+ - Reading inputs (e.g., retrieving Slack messages, reading emails)
+ - Writing outputs (e.g., generating reports, sending emails)
+ - Using external tools (e.g., calling LLMs, searching the web)
+ - **NOTE**: *LLM-based tasks* (e.g., summarizing text, analyzing sentiment) are **NOT** utility functions; rather, they are *core functions* internal in the AI system.
+ - For each utility function, implement it and write a simple test.
+ - Document their input/output, as well as why they are necessary. For example:
+ - `name`: `get_embedding` (`utils/get_embedding.py`)
+ - `input`: `str`
+ - `output`: a vector of 3072 floats
+ - `necessity`: Used by the second node to embed text
+ - Example utility implementation:
+ ```python
+ # utils/call_llm.py
+ from openai import OpenAI
+
+ def call_llm(prompt):
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+
+ if __name__ == "__main__":
+ prompt = "What is the meaning of life?"
+ print(call_llm(prompt))
+ ```
+ - > **Sometimes, design Utilies before Flow:** For example, for an LLM project to automate a legacy system, the bottleneck will likely be the available interface to that system. Start by designing the hardest utilities for interfacing, and then build the flow around them.
+ {: .best-practice }
+
+4. **Node Design**: Plan how each node will read and write data, and use utility functions.
+ - One core design principle for PocketFlow is to use a [shared store](./core_abstraction/communication.md), so start with a shared store design:
+ - For simple systems, use an in-memory dictionary.
+ - For more complex systems or when persistence is required, use a database.
+ - **Don't Repeat Yourself**: Use in-memory references or foreign keys.
+ - Example shared store design:
+ ```python
+ shared = {
+ "user": {
+ "id": "user123",
+ "context": { # Another nested dict
+ "weather": {"temp": 72, "condition": "sunny"},
+ "location": "San Francisco"
+ }
+ },
+ "results": {} # Empty dict to store outputs
+ }
+ ```
+ - For each [Node](./core_abstraction/node.md), describe its type, how it reads and writes data, and which utility function it uses. Keep it specific but high-level without codes. For example:
+ - `type`: Regular (or Batch, or Async)
+ - `prep`: Read "text" from the shared store
+ - `exec`: Call the embedding utility function
+ - `post`: Write "embedding" to the shared store
+
+5. **Implementation**: Implement the initial nodes and flows based on the design.
+ - 🎉 If you've reached this step, humans have finished the design. Now *Agentic Coding* begins!
+ - **"Keep it simple, stupid!"** Avoid complex features and full-scale type checking.
+ - **FAIL FAST**! Avoid `try` logic so you can quickly identify any weak points in the system.
+ - Add logging throughout the code to facilitate debugging.
+
+7. **Optimization**:
+ - **Use Intuition**: For a quick initial evaluation, human intuition is often a good start.
+ - **Redesign Flow (Back to Step 3)**: Consider breaking down tasks further, introducing agentic decisions, or better managing input contexts.
+ - If your flow design is already solid, move on to micro-optimizations:
+ - **Prompt Engineering**: Use clear, specific instructions with examples to reduce ambiguity.
+ - **In-Context Learning**: Provide robust examples for tasks that are difficult to specify with instructions alone.
+
+ - > **You'll likely iterate a lot!** Expect to repeat Steps 3–6 hundreds of times.
+ >
+ > 
+ {: .best-practice }
+
+8. **Reliability**
+ - **Node Retries**: Add checks in the node `exec` to ensure outputs meet requirements, and consider increasing `max_retries` and `wait` times.
+ - **Logging and Visualization**: Maintain logs of all attempts and visualize node results for easier debugging.
+ - **Self-Evaluation**: Add a separate node (powered by an LLM) to review outputs when results are uncertain.
+
+## Example LLM Project File Structure
+
+```
+my_project/
+├── main.py
+├── nodes.py
+├── flow.py
+├── utils/
+│ ├── __init__.py
+│ ├── call_llm.py
+│ └── search_web.py
+├── requirements.txt
+└── docs/
+ └── design.md
+```
+
+- **`docs/design.md`**: Contains project documentation for each step above. This should be *high-level* and *no-code*.
+- **`utils/`**: Contains all utility functions.
+ - It's recommended to dedicate one Python file to each API call, for example `call_llm.py` or `search_web.py`.
+ - Each file should also include a `main()` function to try that API call
+- **`nodes.py`**: Contains all the node definitions.
+ ```python
+ # nodes.py
+ from pocketflow import Node
+ from utils.call_llm import call_llm
+
+ class GetQuestionNode(Node):
+ def exec(self, _):
+ # Get question directly from user input
+ user_question = input("Enter your question: ")
+ return user_question
+
+ def post(self, shared, prep_res, exec_res):
+ # Store the user's question
+ shared["question"] = exec_res
+ return "default" # Go to the next node
+
+ class AnswerNode(Node):
+ def prep(self, shared):
+ # Read question from shared
+ return shared["question"]
+
+ def exec(self, question):
+ # Call LLM to get the answer
+ return call_llm(question)
+
+ def post(self, shared, prep_res, exec_res):
+ # Store the answer in shared
+ shared["answer"] = exec_res
+ ```
+- **`flow.py`**: Implements functions that create flows by importing node definitions and connecting them.
+ ```python
+ # flow.py
+ from pocketflow import Flow
+ from nodes import GetQuestionNode, AnswerNode
+
+ def create_qa_flow():
+ """Create and return a question-answering flow."""
+ # Create nodes
+ get_question_node = GetQuestionNode()
+ answer_node = AnswerNode()
+
+ # Connect nodes in sequence
+ get_question_node >> answer_node
+
+ # Create flow starting with input node
+ return Flow(start=get_question_node)
+ ```
+- **`main.py`**: Serves as the project's entry point.
+ ```python
+ # main.py
+ from flow import create_qa_flow
+
+ # Example main function
+ # Please replace this with your own main function
+ def main():
+ shared = {
+ "question": None, # Will be populated by GetQuestionNode from user input
+ "answer": None # Will be populated by AnswerNode
+ }
+
+ # Create the flow and run it
+ qa_flow = create_qa_flow()
+ qa_flow.run(shared)
+ print(f"Question: {shared['question']}")
+ print(f"Answer: {shared['answer']}")
+
+ if __name__ == "__main__":
+ main()
+ ```
+
+================================================
+File: docs/index.md
+================================================
+---
+layout: default
+title: "Home"
+nav_order: 1
+---
+
+# Pocket Flow
+
+A [100-line](https://github.com/the-pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalist LLM framework for *Agents, Task Decomposition, RAG, etc*.
+
+- **Lightweight**: Just the core graph abstraction in 100 lines. ZERO dependencies, and vendor lock-in.
+- **Expressive**: Everything you love from larger frameworks—([Multi-](./design_pattern/multi_agent.html))[Agents](./design_pattern/agent.html), [Workflow](./design_pattern/workflow.html), [RAG](./design_pattern/rag.html), and more.
+- **Agentic-Coding**: Intuitive enough for AI agents to help humans build complex LLM applications.
+
+
+

+
+
+## Core Abstraction
+
+We model the LLM workflow as a **Graph + Shared Store**:
+
+- [Node](./core_abstraction/node.md) handles simple (LLM) tasks.
+- [Flow](./core_abstraction/flow.md) connects nodes through **Actions** (labeled edges).
+- [Shared Store](./core_abstraction/communication.md) enables communication between nodes within flows.
+- [Batch](./core_abstraction/batch.md) nodes/flows allow for data-intensive tasks.
+- [Async](./core_abstraction/async.md) nodes/flows allow waiting for asynchronous tasks.
+- [(Advanced) Parallel](./core_abstraction/parallel.md) nodes/flows handle I/O-bound tasks.
+
+
+

+
+
+## Design Pattern
+
+From there, it’s easy to implement popular design patterns:
+
+- [Agent](./design_pattern/agent.md) autonomously makes decisions.
+- [Workflow](./design_pattern/workflow.md) chains multiple tasks into pipelines.
+- [RAG](./design_pattern/rag.md) integrates data retrieval with generation.
+- [Map Reduce](./design_pattern/mapreduce.md) splits data tasks into Map and Reduce steps.
+- [Structured Output](./design_pattern/structure.md) formats outputs consistently.
+- [(Advanced) Multi-Agents](./design_pattern/multi_agent.md) coordinate multiple agents.
+
+
+

+
+
+## Utility Function
+
+We **do not** provide built-in utilities. Instead, we offer *examples*—please *implement your own*:
+
+- [LLM Wrapper](./utility_function/llm.md)
+- [Viz and Debug](./utility_function/viz.md)
+- [Web Search](./utility_function/websearch.md)
+- [Chunking](./utility_function/chunking.md)
+- [Embedding](./utility_function/embedding.md)
+- [Vector Databases](./utility_function/vector.md)
+- [Text-to-Speech](./utility_function/text_to_speech.md)
+
+**Why not built-in?**: I believe it's a *bad practice* for vendor-specific APIs in a general framework:
+- *API Volatility*: Frequent changes lead to heavy maintenance for hardcoded APIs.
+- *Flexibility*: You may want to switch vendors, use fine-tuned models, or run them locally.
+- *Optimizations*: Prompt caching, batching, and streaming are easier without vendor lock-in.
+
+## Ready to build your Apps?
+
+Check out [Agentic Coding Guidance](./guide.md), the fastest way to develop LLM projects with Pocket Flow!
+
+================================================
+File: docs/core_abstraction/async.md
+================================================
+---
+layout: default
+title: "(Advanced) Async"
+parent: "Core Abstraction"
+nav_order: 5
+---
+
+# (Advanced) Async
+
+**Async** Nodes implement `prep_async()`, `exec_async()`, `exec_fallback_async()`, and/or `post_async()`. This is useful for:
+
+1. **prep_async()**: For *fetching/reading data (files, APIs, DB)* in an I/O-friendly way.
+2. **exec_async()**: Typically used for async LLM calls.
+3. **post_async()**: For *awaiting user feedback*, *coordinating across multi-agents* or any additional async steps after `exec_async()`.
+
+**Note**: `AsyncNode` must be wrapped in `AsyncFlow`. `AsyncFlow` can also include regular (sync) nodes.
+
+### Example
+
+```python
+class SummarizeThenVerify(AsyncNode):
+ async def prep_async(self, shared):
+ # Example: read a file asynchronously
+ doc_text = await read_file_async(shared["doc_path"])
+ return doc_text
+
+ async def exec_async(self, prep_res):
+ # Example: async LLM call
+ summary = await call_llm_async(f"Summarize: {prep_res}")
+ return summary
+
+ async def post_async(self, shared, prep_res, exec_res):
+ # Example: wait for user feedback
+ decision = await gather_user_feedback(exec_res)
+ if decision == "approve":
+ shared["summary"] = exec_res
+ return "approve"
+ return "deny"
+
+summarize_node = SummarizeThenVerify()
+final_node = Finalize()
+
+# Define transitions
+summarize_node - "approve" >> final_node
+summarize_node - "deny" >> summarize_node # retry
+
+flow = AsyncFlow(start=summarize_node)
+
+async def main():
+ shared = {"doc_path": "document.txt"}
+ await flow.run_async(shared)
+ print("Final Summary:", shared.get("summary"))
+
+asyncio.run(main())
+```
+
+================================================
+File: docs/core_abstraction/batch.md
+================================================
+---
+layout: default
+title: "Batch"
+parent: "Core Abstraction"
+nav_order: 4
+---
+
+# Batch
+
+**Batch** makes it easier to handle large inputs in one Node or **rerun** a Flow multiple times. Example use cases:
+- **Chunk-based** processing (e.g., splitting large texts).
+- **Iterative** processing over lists of input items (e.g., user queries, files, URLs).
+
+## 1. BatchNode
+
+A **BatchNode** extends `Node` but changes `prep()` and `exec()`:
+
+- **`prep(shared)`**: returns an **iterable** (e.g., list, generator).
+- **`exec(item)`**: called **once** per item in that iterable.
+- **`post(shared, prep_res, exec_res_list)`**: after all items are processed, receives a **list** of results (`exec_res_list`) and returns an **Action**.
+
+
+### Example: Summarize a Large File
+
+```python
+class MapSummaries(BatchNode):
+ def prep(self, shared):
+ # Suppose we have a big file; chunk it
+ content = shared["data"]
+ chunk_size = 10000
+ chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
+ return chunks
+
+ def exec(self, chunk):
+ prompt = f"Summarize this chunk in 10 words: {chunk}"
+ summary = call_llm(prompt)
+ return summary
+
+ def post(self, shared, prep_res, exec_res_list):
+ combined = "\n".join(exec_res_list)
+ shared["summary"] = combined
+ return "default"
+
+map_summaries = MapSummaries()
+flow = Flow(start=map_summaries)
+flow.run(shared)
+```
+
+---
+
+## 2. BatchFlow
+
+A **BatchFlow** runs a **Flow** multiple times, each time with different `params`. Think of it as a loop that replays the Flow for each parameter set.
+
+### Example: Summarize Many Files
+
+```python
+class SummarizeAllFiles(BatchFlow):
+ def prep(self, shared):
+ # Return a list of param dicts (one per file)
+ filenames = list(shared["data"].keys()) # e.g., ["file1.txt", "file2.txt", ...]
+ return [{"filename": fn} for fn in filenames]
+
+# Suppose we have a per-file Flow (e.g., load_file >> summarize >> reduce):
+summarize_file = SummarizeFile(start=load_file)
+
+# Wrap that flow into a BatchFlow:
+summarize_all_files = SummarizeAllFiles(start=summarize_file)
+summarize_all_files.run(shared)
+```
+
+### Under the Hood
+1. `prep(shared)` returns a list of param dicts—e.g., `[{filename: "file1.txt"}, {filename: "file2.txt"}, ...]`.
+2. The **BatchFlow** loops through each dict. For each one:
+ - It merges the dict with the BatchFlow’s own `params`.
+ - It calls `flow.run(shared)` using the merged result.
+3. This means the sub-Flow is run **repeatedly**, once for every param dict.
+
+---
+
+## 3. Nested or Multi-Level Batches
+
+You can nest a **BatchFlow** in another **BatchFlow**. For instance:
+- **Outer** batch: returns a list of diretory param dicts (e.g., `{"directory": "/pathA"}`, `{"directory": "/pathB"}`, ...).
+- **Inner** batch: returning a list of per-file param dicts.
+
+At each level, **BatchFlow** merges its own param dict with the parent’s. By the time you reach the **innermost** node, the final `params` is the merged result of **all** parents in the chain. This way, a nested structure can keep track of the entire context (e.g., directory + file name) at once.
+
+```python
+
+class FileBatchFlow(BatchFlow):
+ def prep(self, shared):
+ directory = self.params["directory"]
+ # e.g., files = ["file1.txt", "file2.txt", ...]
+ files = [f for f in os.listdir(directory) if f.endswith(".txt")]
+ return [{"filename": f} for f in files]
+
+class DirectoryBatchFlow(BatchFlow):
+ def prep(self, shared):
+ directories = [ "/path/to/dirA", "/path/to/dirB"]
+ return [{"directory": d} for d in directories]
+
+# MapSummaries have params like {"directory": "/path/to/dirA", "filename": "file1.txt"}
+inner_flow = FileBatchFlow(start=MapSummaries())
+outer_flow = DirectoryBatchFlow(start=inner_flow)
+```
+
+================================================
+File: docs/core_abstraction/communication.md
+================================================
+---
+layout: default
+title: "Communication"
+parent: "Core Abstraction"
+nav_order: 3
+---
+
+# Communication
+
+Nodes and Flows **communicate** in 2 ways:
+
+1. **Shared Store (for almost all the cases)**
+
+ - A global data structure (often an in-mem dict) that all nodes can read ( `prep()`) and write (`post()`).
+ - Great for data results, large content, or anything multiple nodes need.
+ - You shall design the data structure and populate it ahead.
+
+ - > **Separation of Concerns:** Use `Shared Store` for almost all cases to separate *Data Schema* from *Compute Logic*! This approach is both flexible and easy to manage, resulting in more maintainable code. `Params` is more a syntax sugar for [Batch](./batch.md).
+ {: .best-practice }
+
+2. **Params (only for [Batch](./batch.md))**
+ - Each node has a local, ephemeral `params` dict passed in by the **parent Flow**, used as an identifier for tasks. Parameter keys and values shall be **immutable**.
+ - Good for identifiers like filenames or numeric IDs, in Batch mode.
+
+If you know memory management, think of the **Shared Store** like a **heap** (shared by all function calls), and **Params** like a **stack** (assigned by the caller).
+
+---
+
+## 1. Shared Store
+
+### Overview
+
+A shared store is typically an in-mem dictionary, like:
+```python
+shared = {"data": {}, "summary": {}, "config": {...}, ...}
+```
+
+It can also contain local file handlers, DB connections, or a combination for persistence. We recommend deciding the data structure or DB schema first based on your app requirements.
+
+### Example
+
+```python
+class LoadData(Node):
+ def post(self, shared, prep_res, exec_res):
+ # We write data to shared store
+ shared["data"] = "Some text content"
+ return None
+
+class Summarize(Node):
+ def prep(self, shared):
+ # We read data from shared store
+ return shared["data"]
+
+ def exec(self, prep_res):
+ # Call LLM to summarize
+ prompt = f"Summarize: {prep_res}"
+ summary = call_llm(prompt)
+ return summary
+
+ def post(self, shared, prep_res, exec_res):
+ # We write summary to shared store
+ shared["summary"] = exec_res
+ return "default"
+
+load_data = LoadData()
+summarize = Summarize()
+load_data >> summarize
+flow = Flow(start=load_data)
+
+shared = {}
+flow.run(shared)
+```
+
+Here:
+- `LoadData` writes to `shared["data"]`.
+- `Summarize` reads from `shared["data"]`, summarizes, and writes to `shared["summary"]`.
+
+---
+
+## 2. Params
+
+**Params** let you store *per-Node* or *per-Flow* config that doesn't need to live in the shared store. They are:
+- **Immutable** during a Node's run cycle (i.e., they don't change mid-`prep->exec->post`).
+- **Set** via `set_params()`.
+- **Cleared** and updated each time a parent Flow calls it.
+
+> Only set the uppermost Flow params because others will be overwritten by the parent Flow.
+>
+> If you need to set child node params, see [Batch](./batch.md).
+{: .warning }
+
+Typically, **Params** are identifiers (e.g., file name, page number). Use them to fetch the task you assigned or write to a specific part of the shared store.
+
+### Example
+
+```python
+# 1) Create a Node that uses params
+class SummarizeFile(Node):
+ def prep(self, shared):
+ # Access the node's param
+ filename = self.params["filename"]
+ return shared["data"].get(filename, "")
+
+ def exec(self, prep_res):
+ prompt = f"Summarize: {prep_res}"
+ return call_llm(prompt)
+
+ def post(self, shared, prep_res, exec_res):
+ filename = self.params["filename"]
+ shared["summary"][filename] = exec_res
+ return "default"
+
+# 2) Set params
+node = SummarizeFile()
+
+# 3) Set Node params directly (for testing)
+node.set_params({"filename": "doc1.txt"})
+node.run(shared)
+
+# 4) Create Flow
+flow = Flow(start=node)
+
+# 5) Set Flow params (overwrites node params)
+flow.set_params({"filename": "doc2.txt"})
+flow.run(shared) # The node summarizes doc2, not doc1
+```
+
+================================================
+File: docs/core_abstraction/flow.md
+================================================
+---
+layout: default
+title: "Flow"
+parent: "Core Abstraction"
+nav_order: 2
+---
+
+# Flow
+
+A **Flow** orchestrates a graph of Nodes. You can chain Nodes in a sequence or create branching depending on the **Actions** returned from each Node's `post()`.
+
+## 1. Action-based Transitions
+
+Each Node's `post()` returns an **Action** string. By default, if `post()` doesn't return anything, we treat that as `"default"`.
+
+You define transitions with the syntax:
+
+1. **Basic default transition**: `node_a >> node_b`
+ This means if `node_a.post()` returns `"default"`, go to `node_b`.
+ (Equivalent to `node_a - "default" >> node_b`)
+
+2. **Named action transition**: `node_a - "action_name" >> node_b`
+ This means if `node_a.post()` returns `"action_name"`, go to `node_b`.
+
+It's possible to create loops, branching, or multi-step flows.
+
+## 2. Creating a Flow
+
+A **Flow** begins with a **start** node. You call `Flow(start=some_node)` to specify the entry point. When you call `flow.run(shared)`, it executes the start node, looks at its returned Action from `post()`, follows the transition, and continues until there's no next node.
+
+### Example: Simple Sequence
+
+Here's a minimal flow of two nodes in a chain:
+
+```python
+node_a >> node_b
+flow = Flow(start=node_a)
+flow.run(shared)
+```
+
+- When you run the flow, it executes `node_a`.
+- Suppose `node_a.post()` returns `"default"`.
+- The flow then sees `"default"` Action is linked to `node_b` and runs `node_b`.
+- `node_b.post()` returns `"default"` but we didn't define `node_b >> something_else`. So the flow ends there.
+
+### Example: Branching & Looping
+
+Here's a simple expense approval flow that demonstrates branching and looping. The `ReviewExpense` node can return three possible Actions:
+
+- `"approved"`: expense is approved, move to payment processing
+- `"needs_revision"`: expense needs changes, send back for revision
+- `"rejected"`: expense is denied, finish the process
+
+We can wire them like this:
+
+```python
+# Define the flow connections
+review - "approved" >> payment # If approved, process payment
+review - "needs_revision" >> revise # If needs changes, go to revision
+review - "rejected" >> finish # If rejected, finish the process
+
+revise >> review # After revision, go back for another review
+payment >> finish # After payment, finish the process
+
+flow = Flow(start=review)
+```
+
+Let's see how it flows:
+
+1. If `review.post()` returns `"approved"`, the expense moves to the `payment` node
+2. If `review.post()` returns `"needs_revision"`, it goes to the `revise` node, which then loops back to `review`
+3. If `review.post()` returns `"rejected"`, it moves to the `finish` node and stops
+
+```mermaid
+flowchart TD
+ review[Review Expense] -->|approved| payment[Process Payment]
+ review -->|needs_revision| revise[Revise Report]
+ review -->|rejected| finish[Finish Process]
+
+ revise --> review
+ payment --> finish
+```
+
+### Running Individual Nodes vs. Running a Flow
+
+- `node.run(shared)`: Just runs that node alone (calls `prep->exec->post()`), returns an Action.
+- `flow.run(shared)`: Executes from the start node, follows Actions to the next node, and so on until the flow can't continue.
+
+> `node.run(shared)` **does not** proceed to the successor.
+> This is mainly for debugging or testing a single node.
+>
+> Always use `flow.run(...)` in production to ensure the full pipeline runs correctly.
+{: .warning }
+
+## 3. Nested Flows
+
+A **Flow** can act like a Node, which enables powerful composition patterns. This means you can:
+
+1. Use a Flow as a Node within another Flow's transitions.
+2. Combine multiple smaller Flows into a larger Flow for reuse.
+3. Node `params` will be a merging of **all** parents' `params`.
+
+### Flow's Node Methods
+
+A **Flow** is also a **Node**, so it will run `prep()` and `post()`. However:
+
+- It **won't** run `exec()`, as its main logic is to orchestrate its nodes.
+- `post()` always receives `None` for `exec_res` and should instead get the flow execution results from the shared store.
+
+### Basic Flow Nesting
+
+Here's how to connect a flow to another node:
+
+```python
+# Create a sub-flow
+node_a >> node_b
+subflow = Flow(start=node_a)
+
+# Connect it to another node
+subflow >> node_c
+
+# Create the parent flow
+parent_flow = Flow(start=subflow)
+```
+
+When `parent_flow.run()` executes:
+1. It starts `subflow`
+2. `subflow` runs through its nodes (`node_a->node_b`)
+3. After `subflow` completes, execution continues to `node_c`
+
+### Example: Order Processing Pipeline
+
+Here's a practical example that breaks down order processing into nested flows:
+
+```python
+# Payment processing sub-flow
+validate_payment >> process_payment >> payment_confirmation
+payment_flow = Flow(start=validate_payment)
+
+# Inventory sub-flow
+check_stock >> reserve_items >> update_inventory
+inventory_flow = Flow(start=check_stock)
+
+# Shipping sub-flow
+create_label >> assign_carrier >> schedule_pickup
+shipping_flow = Flow(start=create_label)
+
+# Connect the flows into a main order pipeline
+payment_flow >> inventory_flow >> shipping_flow
+
+# Create the master flow
+order_pipeline = Flow(start=payment_flow)
+
+# Run the entire pipeline
+order_pipeline.run(shared_data)
+```
+
+This creates a clean separation of concerns while maintaining a clear execution path:
+
+```mermaid
+flowchart LR
+ subgraph order_pipeline[Order Pipeline]
+ subgraph paymentFlow["Payment Flow"]
+ A[Validate Payment] --> B[Process Payment] --> C[Payment Confirmation]
+ end
+
+ subgraph inventoryFlow["Inventory Flow"]
+ D[Check Stock] --> E[Reserve Items] --> F[Update Inventory]
+ end
+
+ subgraph shippingFlow["Shipping Flow"]
+ G[Create Label] --> H[Assign Carrier] --> I[Schedule Pickup]
+ end
+
+ paymentFlow --> inventoryFlow
+ inventoryFlow --> shippingFlow
+ end
+```
+
+================================================
+File: docs/core_abstraction/node.md
+================================================
+---
+layout: default
+title: "Node"
+parent: "Core Abstraction"
+nav_order: 1
+---
+
+# Node
+
+A **Node** is the smallest building block. Each Node has 3 steps `prep->exec->post`:
+
+
+

+
+
+1. `prep(shared)`
+ - **Read and preprocess data** from `shared` store.
+ - Examples: *query DB, read files, or serialize data into a string*.
+ - Return `prep_res`, which is used by `exec()` and `post()`.
+
+2. `exec(prep_res)`
+ - **Execute compute logic**, with optional retries and error handling (below).
+ - Examples: *(mostly) LLM calls, remote APIs, tool use*.
+ - ⚠️ This shall be only for compute and **NOT** access `shared`.
+ - ⚠️ If retries enabled, ensure idempotent implementation.
+ - Return `exec_res`, which is passed to `post()`.
+
+3. `post(shared, prep_res, exec_res)`
+ - **Postprocess and write data** back to `shared`.
+ - Examples: *update DB, change states, log results*.
+ - **Decide the next action** by returning a *string* (`action = "default"` if *None*).
+
+> **Why 3 steps?** To enforce the principle of *separation of concerns*. The data storage and data processing are operated separately.
+>
+> All steps are *optional*. E.g., you can only implement `prep` and `post` if you just need to process data.
+{: .note }
+
+### Fault Tolerance & Retries
+
+You can **retry** `exec()` if it raises an exception via two parameters when define the Node:
+
+- `max_retries` (int): Max times to run `exec()`. The default is `1` (**no** retry).
+- `wait` (int): The time to wait (in **seconds**) before next retry. By default, `wait=0` (no waiting).
+`wait` is helpful when you encounter rate-limits or quota errors from your LLM provider and need to back off.
+
+```python
+my_node = SummarizeFile(max_retries=3, wait=10)
+```
+
+When an exception occurs in `exec()`, the Node automatically retries until:
+
+- It either succeeds, or
+- The Node has retried `max_retries - 1` times already and fails on the last attempt.
+
+You can get the current retry times (0-based) from `self.cur_retry`.
+
+```python
+class RetryNode(Node):
+ def exec(self, prep_res):
+ print(f"Retry {self.cur_retry} times")
+ raise Exception("Failed")
+```
+
+### Graceful Fallback
+
+To **gracefully handle** the exception (after all retries) rather than raising it, override:
+
+```python
+def exec_fallback(self, prep_res, exc):
+ raise exc
+```
+
+By default, it just re-raises exception. But you can return a fallback result instead, which becomes the `exec_res` passed to `post()`.
+
+### Example: Summarize file
+
+```python
+class SummarizeFile(Node):
+ def prep(self, shared):
+ return shared["data"]
+
+ def exec(self, prep_res):
+ if not prep_res:
+ return "Empty file content"
+ prompt = f"Summarize this text in 10 words: {prep_res}"
+ summary = call_llm(prompt) # might fail
+ return summary
+
+ def exec_fallback(self, prep_res, exc):
+ # Provide a simple fallback instead of crashing
+ return "There was an error processing your request."
+
+ def post(self, shared, prep_res, exec_res):
+ shared["summary"] = exec_res
+ # Return "default" by not returning
+
+summarize_node = SummarizeFile(max_retries=3)
+
+# node.run() calls prep->exec->post
+# If exec() fails, it retries up to 3 times before calling exec_fallback()
+action_result = summarize_node.run(shared)
+
+print("Action returned:", action_result) # "default"
+print("Summary stored:", shared["summary"])
+```
+
+
+================================================
+File: docs/core_abstraction/parallel.md
+================================================
+---
+layout: default
+title: "(Advanced) Parallel"
+parent: "Core Abstraction"
+nav_order: 6
+---
+
+# (Advanced) Parallel
+
+**Parallel** Nodes and Flows let you run multiple **Async** Nodes and Flows **concurrently**—for example, summarizing multiple texts at once. This can improve performance by overlapping I/O and compute.
+
+> Because of Python’s GIL, parallel nodes and flows can’t truly parallelize CPU-bound tasks (e.g., heavy numerical computations). However, they excel at overlapping I/O-bound work—like LLM calls, database queries, API requests, or file I/O.
+{: .warning }
+
+> - **Ensure Tasks Are Independent**: If each item depends on the output of a previous item, **do not** parallelize.
+>
+> - **Beware of Rate Limits**: Parallel calls can **quickly** trigger rate limits on LLM services. You may need a **throttling** mechanism (e.g., semaphores or sleep intervals).
+>
+> - **Consider Single-Node Batch APIs**: Some LLMs offer a **batch inference** API where you can send multiple prompts in a single call. This is more complex to implement but can be more efficient than launching many parallel requests and mitigates rate limits.
+{: .best-practice }
+
+## AsyncParallelBatchNode
+
+Like **AsyncBatchNode**, but run `exec_async()` in **parallel**:
+
+```python
+class ParallelSummaries(AsyncParallelBatchNode):
+ async def prep_async(self, shared):
+ # e.g., multiple texts
+ return shared["texts"]
+
+ async def exec_async(self, text):
+ prompt = f"Summarize: {text}"
+ return await call_llm_async(prompt)
+
+ async def post_async(self, shared, prep_res, exec_res_list):
+ shared["summary"] = "\n\n".join(exec_res_list)
+ return "default"
+
+node = ParallelSummaries()
+flow = AsyncFlow(start=node)
+```
+
+## AsyncParallelBatchFlow
+
+Parallel version of **BatchFlow**. Each iteration of the sub-flow runs **concurrently** using different parameters:
+
+```python
+class SummarizeMultipleFiles(AsyncParallelBatchFlow):
+ async def prep_async(self, shared):
+ return [{"filename": f} for f in shared["files"]]
+
+sub_flow = AsyncFlow(start=LoadAndSummarizeFile())
+parallel_flow = SummarizeMultipleFiles(start=sub_flow)
+await parallel_flow.run_async(shared)
+```
+
+================================================
+File: docs/design_pattern/agent.md
+================================================
+---
+layout: default
+title: "Agent"
+parent: "Design Pattern"
+nav_order: 1
+---
+
+# Agent
+
+Agent is a powerful design pattern in which nodes can take dynamic actions based on the context.
+
+
+

+
+
+## Implement Agent with Graph
+
+1. **Context and Action:** Implement nodes that supply context and perform actions.
+2. **Branching:** Use branching to connect each action node to an agent node. Use action to allow the agent to direct the [flow](../core_abstraction/flow.md) between nodes—and potentially loop back for multi-step.
+3. **Agent Node:** Provide a prompt to decide action—for example:
+
+```python
+f"""
+### CONTEXT
+Task: {task_description}
+Previous Actions: {previous_actions}
+Current State: {current_state}
+
+### ACTION SPACE
+[1] search
+ Description: Use web search to get results
+ Parameters:
+ - query (str): What to search for
+
+[2] answer
+ Description: Conclude based on the results
+ Parameters:
+ - result (str): Final answer to provide
+
+### NEXT ACTION
+Decide the next action based on the current context and available action space.
+Return your response in the following format:
+
+```yaml
+thinking: |
+
+action:
+parameters:
+ :
+```"""
+```
+
+The core of building **high-performance** and **reliable** agents boils down to:
+
+1. **Context Management:** Provide *relevant, minimal context.* For example, rather than including an entire chat history, retrieve the most relevant via [RAG](./rag.md). Even with larger context windows, LLMs still fall victim to ["lost in the middle"](https://arxiv.org/abs/2307.03172), overlooking mid-prompt content.
+
+2. **Action Space:** Provide *a well-structured and unambiguous* set of actions—avoiding overlap like separate `read_databases` or `read_csvs`. Instead, import CSVs into the database.
+
+## Example Good Action Design
+
+- **Incremental:** Feed content in manageable chunks (500 lines or 1 page) instead of all at once.
+
+- **Overview-zoom-in:** First provide high-level structure (table of contents, summary), then allow drilling into details (raw texts).
+
+- **Parameterized/Programmable:** Instead of fixed actions, enable parameterized (columns to select) or programmable (SQL queries) actions, for example, to read CSV files.
+
+- **Backtracking:** Let the agent undo the last step instead of restarting entirely, preserving progress when encountering errors or dead ends.
+
+## Example: Search Agent
+
+This agent:
+1. Decides whether to search or answer
+2. If searches, loops back to decide if more search needed
+3. Answers when enough context gathered
+
+```python
+class DecideAction(Node):
+ def prep(self, shared):
+ context = shared.get("context", "No previous search")
+ query = shared["query"]
+ return query, context
+
+ def exec(self, inputs):
+ query, context = inputs
+ prompt = f"""
+Given input: {query}
+Previous search results: {context}
+Should I: 1) Search web for more info 2) Answer with current knowledge
+Output in yaml:
+```yaml
+action: search/answer
+reason: why this action
+search_term: search phrase if action is search
+```"""
+ resp = call_llm(prompt)
+ yaml_str = resp.split("```yaml")[1].split("```")[0].strip()
+ result = yaml.safe_load(yaml_str)
+
+ assert isinstance(result, dict)
+ assert "action" in result
+ assert "reason" in result
+ assert result["action"] in ["search", "answer"]
+ if result["action"] == "search":
+ assert "search_term" in result
+
+ return result
+
+ def post(self, shared, prep_res, exec_res):
+ if exec_res["action"] == "search":
+ shared["search_term"] = exec_res["search_term"]
+ return exec_res["action"]
+
+class SearchWeb(Node):
+ def prep(self, shared):
+ return shared["search_term"]
+
+ def exec(self, search_term):
+ return search_web(search_term)
+
+ def post(self, shared, prep_res, exec_res):
+ prev_searches = shared.get("context", [])
+ shared["context"] = prev_searches + [
+ {"term": shared["search_term"], "result": exec_res}
+ ]
+ return "decide"
+
+class DirectAnswer(Node):
+ def prep(self, shared):
+ return shared["query"], shared.get("context", "")
+
+ def exec(self, inputs):
+ query, context = inputs
+ return call_llm(f"Context: {context}\nAnswer: {query}")
+
+ def post(self, shared, prep_res, exec_res):
+ print(f"Answer: {exec_res}")
+ shared["answer"] = exec_res
+
+# Connect nodes
+decide = DecideAction()
+search = SearchWeb()
+answer = DirectAnswer()
+
+decide - "search" >> search
+decide - "answer" >> answer
+search - "decide" >> decide # Loop back
+
+flow = Flow(start=decide)
+flow.run({"query": "Who won the Nobel Prize in Physics 2024?"})
+```
+
+================================================
+File: docs/design_pattern/mapreduce.md
+================================================
+---
+layout: default
+title: "Map Reduce"
+parent: "Design Pattern"
+nav_order: 4
+---
+
+# Map Reduce
+
+MapReduce is a design pattern suitable when you have either:
+- Large input data (e.g., multiple files to process), or
+- Large output data (e.g., multiple forms to fill)
+
+and there is a logical way to break the task into smaller, ideally independent parts.
+
+
+

+
+
+You first break down the task using [BatchNode](../core_abstraction/batch.md) in the map phase, followed by aggregation in the reduce phase.
+
+### Example: Document Summarization
+
+```python
+class SummarizeAllFiles(BatchNode):
+ def prep(self, shared):
+ files_dict = shared["files"] # e.g. 10 files
+ return list(files_dict.items()) # [("file1.txt", "aaa..."), ("file2.txt", "bbb..."), ...]
+
+ def exec(self, one_file):
+ filename, file_content = one_file
+ summary_text = call_llm(f"Summarize the following file:\n{file_content}")
+ return (filename, summary_text)
+
+ def post(self, shared, prep_res, exec_res_list):
+ shared["file_summaries"] = dict(exec_res_list)
+
+class CombineSummaries(Node):
+ def prep(self, shared):
+ return shared["file_summaries"]
+
+ def exec(self, file_summaries):
+ # format as: "File1: summary\nFile2: summary...\n"
+ text_list = []
+ for fname, summ in file_summaries.items():
+ text_list.append(f"{fname} summary:\n{summ}\n")
+ big_text = "\n---\n".join(text_list)
+
+ return call_llm(f"Combine these file summaries into one final summary:\n{big_text}")
+
+ def post(self, shared, prep_res, final_summary):
+ shared["all_files_summary"] = final_summary
+
+batch_node = SummarizeAllFiles()
+combine_node = CombineSummaries()
+batch_node >> combine_node
+
+flow = Flow(start=batch_node)
+
+shared = {
+ "files": {
+ "file1.txt": "Alice was beginning to get very tired of sitting by her sister...",
+ "file2.txt": "Some other interesting text ...",
+ # ...
+ }
+}
+flow.run(shared)
+print("Individual Summaries:", shared["file_summaries"])
+print("\nFinal Summary:\n", shared["all_files_summary"])
+```
+
+================================================
+File: docs/design_pattern/rag.md
+================================================
+---
+layout: default
+title: "RAG"
+parent: "Design Pattern"
+nav_order: 3
+---
+
+# RAG (Retrieval Augmented Generation)
+
+For certain LLM tasks like answering questions, providing relevant context is essential. One common architecture is a **two-stage** RAG pipeline:
+
+
+

+
+
+1. **Offline stage**: Preprocess and index documents ("building the index").
+2. **Online stage**: Given a question, generate answers by retrieving the most relevant context.
+
+---
+## Stage 1: Offline Indexing
+
+We create three Nodes:
+1. `ChunkDocs` – [chunks](../utility_function/chunking.md) raw text.
+2. `EmbedDocs` – [embeds](../utility_function/embedding.md) each chunk.
+3. `StoreIndex` – stores embeddings into a [vector database](../utility_function/vector.md).
+
+```python
+class ChunkDocs(BatchNode):
+ def prep(self, shared):
+ # A list of file paths in shared["files"]. We process each file.
+ return shared["files"]
+
+ def exec(self, filepath):
+ # read file content. In real usage, do error handling.
+ with open(filepath, "r", encoding="utf-8") as f:
+ text = f.read()
+ # chunk by 100 chars each
+ chunks = []
+ size = 100
+ for i in range(0, len(text), size):
+ chunks.append(text[i : i + size])
+ return chunks
+
+ def post(self, shared, prep_res, exec_res_list):
+ # exec_res_list is a list of chunk-lists, one per file.
+ # flatten them all into a single list of chunks.
+ all_chunks = []
+ for chunk_list in exec_res_list:
+ all_chunks.extend(chunk_list)
+ shared["all_chunks"] = all_chunks
+
+class EmbedDocs(BatchNode):
+ def prep(self, shared):
+ return shared["all_chunks"]
+
+ def exec(self, chunk):
+ return get_embedding(chunk)
+
+ def post(self, shared, prep_res, exec_res_list):
+ # Store the list of embeddings.
+ shared["all_embeds"] = exec_res_list
+ print(f"Total embeddings: {len(exec_res_list)}")
+
+class StoreIndex(Node):
+ def prep(self, shared):
+ # We'll read all embeds from shared.
+ return shared["all_embeds"]
+
+ def exec(self, all_embeds):
+ # Create a vector index (faiss or other DB in real usage).
+ index = create_index(all_embeds)
+ return index
+
+ def post(self, shared, prep_res, index):
+ shared["index"] = index
+
+# Wire them in sequence
+chunk_node = ChunkDocs()
+embed_node = EmbedDocs()
+store_node = StoreIndex()
+
+chunk_node >> embed_node >> store_node
+
+OfflineFlow = Flow(start=chunk_node)
+```
+
+Usage example:
+
+```python
+shared = {
+ "files": ["doc1.txt", "doc2.txt"], # any text files
+}
+OfflineFlow.run(shared)
+```
+
+---
+## Stage 2: Online Query & Answer
+
+We have 3 nodes:
+1. `EmbedQuery` – embeds the user’s question.
+2. `RetrieveDocs` – retrieves top chunk from the index.
+3. `GenerateAnswer` – calls the LLM with the question + chunk to produce the final answer.
+
+```python
+class EmbedQuery(Node):
+ def prep(self, shared):
+ return shared["question"]
+
+ def exec(self, question):
+ return get_embedding(question)
+
+ def post(self, shared, prep_res, q_emb):
+ shared["q_emb"] = q_emb
+
+class RetrieveDocs(Node):
+ def prep(self, shared):
+ # We'll need the query embedding, plus the offline index/chunks
+ return shared["q_emb"], shared["index"], shared["all_chunks"]
+
+ def exec(self, inputs):
+ q_emb, index, chunks = inputs
+ I, D = search_index(index, q_emb, top_k=1)
+ best_id = I[0][0]
+ relevant_chunk = chunks[best_id]
+ return relevant_chunk
+
+ def post(self, shared, prep_res, relevant_chunk):
+ shared["retrieved_chunk"] = relevant_chunk
+ print("Retrieved chunk:", relevant_chunk[:60], "...")
+
+class GenerateAnswer(Node):
+ def prep(self, shared):
+ return shared["question"], shared["retrieved_chunk"]
+
+ def exec(self, inputs):
+ question, chunk = inputs
+ prompt = f"Question: {question}\nContext: {chunk}\nAnswer:"
+ return call_llm(prompt)
+
+ def post(self, shared, prep_res, answer):
+ shared["answer"] = answer
+ print("Answer:", answer)
+
+embed_qnode = EmbedQuery()
+retrieve_node = RetrieveDocs()
+generate_node = GenerateAnswer()
+
+embed_qnode >> retrieve_node >> generate_node
+OnlineFlow = Flow(start=embed_qnode)
+```
+
+Usage example:
+
+```python
+# Suppose we already ran OfflineFlow and have:
+# shared["all_chunks"], shared["index"], etc.
+shared["question"] = "Why do people like cats?"
+
+OnlineFlow.run(shared)
+# final answer in shared["answer"]
+```
+
+================================================
+File: docs/design_pattern/structure.md
+================================================
+---
+layout: default
+title: "Structured Output"
+parent: "Design Pattern"
+nav_order: 5
+---
+
+# Structured Output
+
+In many use cases, you may want the LLM to output a specific structure, such as a list or a dictionary with predefined keys.
+
+There are several approaches to achieve a structured output:
+- **Prompting** the LLM to strictly return a defined structure.
+- Using LLMs that natively support **schema enforcement**.
+- **Post-processing** the LLM's response to extract structured content.
+
+In practice, **Prompting** is simple and reliable for modern LLMs.
+
+### Example Use Cases
+
+- Extracting Key Information
+
+```yaml
+product:
+ name: Widget Pro
+ price: 199.99
+ description: |
+ A high-quality widget designed for professionals.
+ Recommended for advanced users.
+```
+
+- Summarizing Documents into Bullet Points
+
+```yaml
+summary:
+ - This product is easy to use.
+ - It is cost-effective.
+ - Suitable for all skill levels.
+```
+
+- Generating Configuration Files
+
+```yaml
+server:
+ host: 127.0.0.1
+ port: 8080
+ ssl: true
+```
+
+## Prompt Engineering
+
+When prompting the LLM to produce **structured** output:
+1. **Wrap** the structure in code fences (e.g., `yaml`).
+2. **Validate** that all required fields exist (and let `Node` handles retry).
+
+### Example Text Summarization
+
+```python
+class SummarizeNode(Node):
+ def exec(self, prep_res):
+ # Suppose `prep_res` is the text to summarize.
+ prompt = f"""
+Please summarize the following text as YAML, with exactly 3 bullet points
+
+{prep_res}
+
+Now, output:
+```yaml
+summary:
+ - bullet 1
+ - bullet 2
+ - bullet 3
+```"""
+ response = call_llm(prompt)
+ yaml_str = response.split("```yaml")[1].split("```")[0].strip()
+
+ import yaml
+ structured_result = yaml.safe_load(yaml_str)
+
+ assert "summary" in structured_result
+ assert isinstance(structured_result["summary"], list)
+
+ return structured_result
+```
+
+> Besides using `assert` statements, another popular way to validate schemas is [Pydantic](https://github.com/pydantic/pydantic)
+{: .note }
+
+### Why YAML instead of JSON?
+
+Current LLMs struggle with escaping. YAML is easier with strings since they don't always need quotes.
+
+**In JSON**
+
+```json
+{
+ "dialogue": "Alice said: \"Hello Bob.\\nHow are you?\\nI am good.\""
+}
+```
+
+- Every double quote inside the string must be escaped with `\"`.
+- Each newline in the dialogue must be represented as `\n`.
+
+**In YAML**
+
+```yaml
+dialogue: |
+ Alice said: "Hello Bob.
+ How are you?
+ I am good."
+```
+
+- No need to escape interior quotes—just place the entire text under a block literal (`|`).
+- Newlines are naturally preserved without needing `\n`.
+
+================================================
+File: docs/design_pattern/workflow.md
+================================================
+---
+layout: default
+title: "Workflow"
+parent: "Design Pattern"
+nav_order: 2
+---
+
+# Workflow
+
+Many real-world tasks are too complex for one LLM call. The solution is to **Task Decomposition**: decompose them into a [chain](../core_abstraction/flow.md) of multiple Nodes.
+
+
+

+
+
+> - You don't want to make each task **too coarse**, because it may be *too complex for one LLM call*.
+> - You don't want to make each task **too granular**, because then *the LLM call doesn't have enough context* and results are *not consistent across nodes*.
+>
+> You usually need multiple *iterations* to find the *sweet spot*. If the task has too many *edge cases*, consider using [Agents](./agent.md).
+{: .best-practice }
+
+### Example: Article Writing
+
+```python
+class GenerateOutline(Node):
+ def prep(self, shared): return shared["topic"]
+ def exec(self, topic): return call_llm(f"Create a detailed outline for an article about {topic}")
+ def post(self, shared, prep_res, exec_res): shared["outline"] = exec_res
+
+class WriteSection(Node):
+ def prep(self, shared): return shared["outline"]
+ def exec(self, outline): return call_llm(f"Write content based on this outline: {outline}")
+ def post(self, shared, prep_res, exec_res): shared["draft"] = exec_res
+
+class ReviewAndRefine(Node):
+ def prep(self, shared): return shared["draft"]
+ def exec(self, draft): return call_llm(f"Review and improve this draft: {draft}")
+ def post(self, shared, prep_res, exec_res): shared["final_article"] = exec_res
+
+# Connect nodes
+outline = GenerateOutline()
+write = WriteSection()
+review = ReviewAndRefine()
+
+outline >> write >> review
+
+# Create and run flow
+writing_flow = Flow(start=outline)
+shared = {"topic": "AI Safety"}
+writing_flow.run(shared)
+```
+
+For *dynamic cases*, consider using [Agents](./agent.md).
+
+================================================
+File: docs/utility_function/llm.md
+================================================
+---
+layout: default
+title: "LLM Wrapper"
+parent: "Utility Function"
+nav_order: 1
+---
+
+# LLM Wrappers
+
+Check out libraries like [litellm](https://github.com/BerriAI/litellm).
+Here, we provide some minimal example implementations:
+
+1. OpenAI
+ ```python
+ def call_llm(prompt):
+ from openai import OpenAI
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+
+ # Example usage
+ call_llm("How are you?")
+ ```
+ > Store the API key in an environment variable like OPENAI_API_KEY for security.
+ {: .best-practice }
+
+2. Claude (Anthropic)
+ ```python
+ def call_llm(prompt):
+ from anthropic import Anthropic
+ client = Anthropic(api_key="YOUR_API_KEY_HERE")
+ response = client.messages.create(
+ model="claude-2",
+ messages=[{"role": "user", "content": prompt}],
+ max_tokens=100
+ )
+ return response.content
+ ```
+
+3. Google (Generative AI Studio / PaLM API)
+ ```python
+ def call_llm(prompt):
+ import google.generativeai as genai
+ genai.configure(api_key="YOUR_API_KEY_HERE")
+ response = genai.generate_text(
+ model="models/text-bison-001",
+ prompt=prompt
+ )
+ return response.result
+ ```
+
+4. Azure (Azure OpenAI)
+ ```python
+ def call_llm(prompt):
+ from openai import AzureOpenAI
+ client = AzureOpenAI(
+ azure_endpoint="https://.openai.azure.com/",
+ api_key="YOUR_API_KEY_HERE",
+ api_version="2023-05-15"
+ )
+ r = client.chat.completions.create(
+ model="",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+ ```
+
+5. Ollama (Local LLM)
+ ```python
+ def call_llm(prompt):
+ from ollama import chat
+ response = chat(
+ model="llama2",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return response.message.content
+ ```
+
+## Improvements
+Feel free to enhance your `call_llm` function as needed. Here are examples:
+
+- Handle chat history:
+
+```python
+def call_llm(messages):
+ from openai import OpenAI
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=messages
+ )
+ return r.choices[0].message.content
+```
+
+- Add in-memory caching
+
+```python
+from functools import lru_cache
+
+@lru_cache(maxsize=1000)
+def call_llm(prompt):
+ # Your implementation here
+ pass
+```
+
+> ⚠️ Caching conflicts with Node retries, as retries yield the same result.
+>
+> To address this, you could use cached results only if not retried.
+{: .warning }
+
+
+```python
+from functools import lru_cache
+
+@lru_cache(maxsize=1000)
+def cached_call(prompt):
+ pass
+
+def call_llm(prompt, use_cache):
+ if use_cache:
+ return cached_call(prompt)
+ # Call the underlying function directly
+ return cached_call.__wrapped__(prompt)
+
+class SummarizeNode(Node):
+ def exec(self, text):
+ return call_llm(f"Summarize: {text}", self.cur_retry==0)
+```
+
+- Enable logging:
+
+```python
+def call_llm(prompt):
+ import logging
+ logging.info(f"Prompt: {prompt}")
+ response = ... # Your implementation here
+ logging.info(f"Response: {response}")
+ return response
+```
\ No newline at end of file
diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 0000000..8f05631
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1,1664 @@
+---
+layout: default
+title: "Agentic Coding"
+---
+
+# Agentic Coding: Humans Design, Agents code!
+
+> If you are an AI agents involved in building LLM Systems, read this guide **VERY, VERY** carefully! This is the most important chapter in the entire document. Throughout development, you should always (1) start with a small and simple solution, (2) design at a high level (`docs/design.md`) before implementation, and (3) frequently ask humans for feedback and clarification.
+{: .warning }
+
+## Agentic Coding Steps
+
+Agentic Coding should be a collaboration between Human System Design and Agent Implementation:
+
+| Steps | Human | AI | Comment |
+|:-----------------------|:----------:|:---------:|:------------------------------------------------------------------------|
+| 1. Requirements | ★★★ High | ★☆☆ Low | Humans understand the requirements and context. |
+| 2. Flow | ★★☆ Medium | ★★☆ Medium | Humans specify the high-level design, and the AI fills in the details. |
+| 3. Utilities | ★★☆ Medium | ★★☆ Medium | Humans provide available external APIs and integrations, and the AI helps with implementation. |
+| 4. Node | ★☆☆ Low | ★★★ High | The AI helps design the node types and data handling based on the flow. |
+| 5. Implementation | ★☆☆ Low | ★★★ High | The AI implements the flow based on the design. |
+| 6. Optimization | ★★☆ Medium | ★★☆ Medium | Humans evaluate the results, and the AI helps optimize. |
+| 7. Reliability | ★☆☆ Low | ★★★ High | The AI writes test cases and addresses corner cases. |
+
+1. **Requirements**: Clarify the requirements for your project, and evaluate whether an AI system is a good fit.
+ - Understand AI systems' strengths and limitations:
+ - **Good for**: Routine tasks requiring common sense (filling forms, replying to emails)
+ - **Good for**: Creative tasks with well-defined inputs (building slides, writing SQL)
+ - **Not good for**: Ambiguous problems requiring complex decision-making (business strategy, startup planning)
+ - **Keep It User-Centric:** Explain the "problem" from the user's perspective rather than just listing features.
+ - **Balance complexity vs. impact**: Aim to deliver the highest value features with minimal complexity early.
+
+2. **Flow Design**: Outline at a high level, describe how your AI system orchestrates nodes.
+ - Identify applicable design patterns (e.g., [Map Reduce](./design_pattern/mapreduce.md), [Agent](./design_pattern/agent.md), [RAG](./design_pattern/rag.md)).
+ - For each node in the flow, start with a high-level one-line description of what it does.
+ - If using **Map Reduce**, specify how to map (what to split) and how to reduce (how to combine).
+ - If using **Agent**, specify what are the inputs (context) and what are the possible actions.
+ - If using **RAG**, specify what to embed, noting that there's usually both offline (indexing) and online (retrieval) workflows.
+ - Outline the flow and draw it in a mermaid diagram. For example:
+ ```mermaid
+ flowchart LR
+ start[Start] --> batch[Batch]
+ batch --> check[Check]
+ check -->|OK| process
+ check -->|Error| fix[Fix]
+ fix --> check
+
+ subgraph process[Process]
+ step1[Step 1] --> step2[Step 2]
+ end
+
+ process --> endNode[End]
+ ```
+ - > **If Humans can't specify the flow, AI Agents can't automate it!** Before building an LLM system, thoroughly understand the problem and potential solution by manually solving example inputs to develop intuition.
+ {: .best-practice }
+
+3. **Utilities**: Based on the Flow Design, identify and implement necessary utility functions.
+ - Think of your AI system as the brain. It needs a body—these *external utility functions*—to interact with the real world:
+
+
+ - Reading inputs (e.g., retrieving Slack messages, reading emails)
+ - Writing outputs (e.g., generating reports, sending emails)
+ - Using external tools (e.g., calling LLMs, searching the web)
+ - **NOTE**: *LLM-based tasks* (e.g., summarizing text, analyzing sentiment) are **NOT** utility functions; rather, they are *core functions* internal in the AI system.
+ - For each utility function, implement it and write a simple test.
+ - Document their input/output, as well as why they are necessary. For example:
+ - `name`: `get_embedding` (`utils/get_embedding.py`)
+ - `input`: `str`
+ - `output`: a vector of 3072 floats
+ - `necessity`: Used by the second node to embed text
+ - Example utility implementation:
+ ```python
+ # utils/call_llm.py
+ from openai import OpenAI
+
+ def call_llm(prompt):
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+
+ if __name__ == "__main__":
+ prompt = "What is the meaning of life?"
+ print(call_llm(prompt))
+ ```
+ - > **Sometimes, design Utilies before Flow:** For example, for an LLM project to automate a legacy system, the bottleneck will likely be the available interface to that system. Start by designing the hardest utilities for interfacing, and then build the flow around them.
+ {: .best-practice }
+
+4. **Node Design**: Plan how each node will read and write data, and use utility functions.
+ - One core design principle for PocketFlow is to use a [shared store](./core_abstraction/communication.md), so start with a shared store design:
+ - For simple systems, use an in-memory dictionary.
+ - For more complex systems or when persistence is required, use a database.
+ - **Don't Repeat Yourself**: Use in-memory references or foreign keys.
+ - Example shared store design:
+ ```python
+ shared = {
+ "user": {
+ "id": "user123",
+ "context": { # Another nested dict
+ "weather": {"temp": 72, "condition": "sunny"},
+ "location": "San Francisco"
+ }
+ },
+ "results": {} # Empty dict to store outputs
+ }
+ ```
+ - For each [Node](./core_abstraction/node.md), describe its type, how it reads and writes data, and which utility function it uses. Keep it specific but high-level without codes. For example:
+ - `type`: Regular (or Batch, or Async)
+ - `prep`: Read "text" from the shared store
+ - `exec`: Call the embedding utility function
+ - `post`: Write "embedding" to the shared store
+
+5. **Implementation**: Implement the initial nodes and flows based on the design.
+ - 🎉 If you've reached this step, humans have finished the design. Now *Agentic Coding* begins!
+ - **"Keep it simple, stupid!"** Avoid complex features and full-scale type checking.
+ - **FAIL FAST**! Avoid `try` logic so you can quickly identify any weak points in the system.
+ - Add logging throughout the code to facilitate debugging.
+
+7. **Optimization**:
+ - **Use Intuition**: For a quick initial evaluation, human intuition is often a good start.
+ - **Redesign Flow (Back to Step 3)**: Consider breaking down tasks further, introducing agentic decisions, or better managing input contexts.
+ - If your flow design is already solid, move on to micro-optimizations:
+ - **Prompt Engineering**: Use clear, specific instructions with examples to reduce ambiguity.
+ - **In-Context Learning**: Provide robust examples for tasks that are difficult to specify with instructions alone.
+
+ - > **You'll likely iterate a lot!** Expect to repeat Steps 3–6 hundreds of times.
+ >
+ >
+ {: .best-practice }
+
+8. **Reliability**
+ - **Node Retries**: Add checks in the node `exec` to ensure outputs meet requirements, and consider increasing `max_retries` and `wait` times.
+ - **Logging and Visualization**: Maintain logs of all attempts and visualize node results for easier debugging.
+ - **Self-Evaluation**: Add a separate node (powered by an LLM) to review outputs when results are uncertain.
+
+## Example LLM Project File Structure
+
+```
+my_project/
+├── main.py
+├── nodes.py
+├── flow.py
+├── utils/
+│ ├── __init__.py
+│ ├── call_llm.py
+│ └── search_web.py
+├── requirements.txt
+└── docs/
+ └── design.md
+```
+
+- **`docs/design.md`**: Contains project documentation for each step above. This should be *high-level* and *no-code*.
+- **`utils/`**: Contains all utility functions.
+ - It's recommended to dedicate one Python file to each API call, for example `call_llm.py` or `search_web.py`.
+ - Each file should also include a `main()` function to try that API call
+- **`nodes.py`**: Contains all the node definitions.
+ ```python
+ # nodes.py
+ from pocketflow import Node
+ from utils.call_llm import call_llm
+
+ class GetQuestionNode(Node):
+ def exec(self, _):
+ # Get question directly from user input
+ user_question = input("Enter your question: ")
+ return user_question
+
+ def post(self, shared, prep_res, exec_res):
+ # Store the user's question
+ shared["question"] = exec_res
+ return "default" # Go to the next node
+
+ class AnswerNode(Node):
+ def prep(self, shared):
+ # Read question from shared
+ return shared["question"]
+
+ def exec(self, question):
+ # Call LLM to get the answer
+ return call_llm(question)
+
+ def post(self, shared, prep_res, exec_res):
+ # Store the answer in shared
+ shared["answer"] = exec_res
+ ```
+- **`flow.py`**: Implements functions that create flows by importing node definitions and connecting them.
+ ```python
+ # flow.py
+ from pocketflow import Flow
+ from nodes import GetQuestionNode, AnswerNode
+
+ def create_qa_flow():
+ """Create and return a question-answering flow."""
+ # Create nodes
+ get_question_node = GetQuestionNode()
+ answer_node = AnswerNode()
+
+ # Connect nodes in sequence
+ get_question_node >> answer_node
+
+ # Create flow starting with input node
+ return Flow(start=get_question_node)
+ ```
+- **`main.py`**: Serves as the project's entry point.
+ ```python
+ # main.py
+ from flow import create_qa_flow
+
+ # Example main function
+ # Please replace this with your own main function
+ def main():
+ shared = {
+ "question": None, # Will be populated by GetQuestionNode from user input
+ "answer": None # Will be populated by AnswerNode
+ }
+
+ # Create the flow and run it
+ qa_flow = create_qa_flow()
+ qa_flow.run(shared)
+ print(f"Question: {shared['question']}")
+ print(f"Answer: {shared['answer']}")
+
+ if __name__ == "__main__":
+ main()
+ ```
+
+================================================
+File: docs/index.md
+================================================
+---
+layout: default
+title: "Home"
+nav_order: 1
+---
+
+# Pocket Flow
+
+A [100-line](https://github.com/the-pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalist LLM framework for *Agents, Task Decomposition, RAG, etc*.
+
+- **Lightweight**: Just the core graph abstraction in 100 lines. ZERO dependencies, and vendor lock-in.
+- **Expressive**: Everything you love from larger frameworks—([Multi-](./design_pattern/multi_agent.html))[Agents](./design_pattern/agent.html), [Workflow](./design_pattern/workflow.html), [RAG](./design_pattern/rag.html), and more.
+- **Agentic-Coding**: Intuitive enough for AI agents to help humans build complex LLM applications.
+
+
+

+
+
+## Core Abstraction
+
+We model the LLM workflow as a **Graph + Shared Store**:
+
+- [Node](./core_abstraction/node.md) handles simple (LLM) tasks.
+- [Flow](./core_abstraction/flow.md) connects nodes through **Actions** (labeled edges).
+- [Shared Store](./core_abstraction/communication.md) enables communication between nodes within flows.
+- [Batch](./core_abstraction/batch.md) nodes/flows allow for data-intensive tasks.
+- [Async](./core_abstraction/async.md) nodes/flows allow waiting for asynchronous tasks.
+- [(Advanced) Parallel](./core_abstraction/parallel.md) nodes/flows handle I/O-bound tasks.
+
+
+

+
+
+## Design Pattern
+
+From there, it’s easy to implement popular design patterns:
+
+- [Agent](./design_pattern/agent.md) autonomously makes decisions.
+- [Workflow](./design_pattern/workflow.md) chains multiple tasks into pipelines.
+- [RAG](./design_pattern/rag.md) integrates data retrieval with generation.
+- [Map Reduce](./design_pattern/mapreduce.md) splits data tasks into Map and Reduce steps.
+- [Structured Output](./design_pattern/structure.md) formats outputs consistently.
+- [(Advanced) Multi-Agents](./design_pattern/multi_agent.md) coordinate multiple agents.
+
+
+

+
+
+## Utility Function
+
+We **do not** provide built-in utilities. Instead, we offer *examples*—please *implement your own*:
+
+- [LLM Wrapper](./utility_function/llm.md)
+- [Viz and Debug](./utility_function/viz.md)
+- [Web Search](./utility_function/websearch.md)
+- [Chunking](./utility_function/chunking.md)
+- [Embedding](./utility_function/embedding.md)
+- [Vector Databases](./utility_function/vector.md)
+- [Text-to-Speech](./utility_function/text_to_speech.md)
+
+**Why not built-in?**: I believe it's a *bad practice* for vendor-specific APIs in a general framework:
+- *API Volatility*: Frequent changes lead to heavy maintenance for hardcoded APIs.
+- *Flexibility*: You may want to switch vendors, use fine-tuned models, or run them locally.
+- *Optimizations*: Prompt caching, batching, and streaming are easier without vendor lock-in.
+
+## Ready to build your Apps?
+
+Check out [Agentic Coding Guidance](./guide.md), the fastest way to develop LLM projects with Pocket Flow!
+
+================================================
+File: docs/core_abstraction/async.md
+================================================
+---
+layout: default
+title: "(Advanced) Async"
+parent: "Core Abstraction"
+nav_order: 5
+---
+
+# (Advanced) Async
+
+**Async** Nodes implement `prep_async()`, `exec_async()`, `exec_fallback_async()`, and/or `post_async()`. This is useful for:
+
+1. **prep_async()**: For *fetching/reading data (files, APIs, DB)* in an I/O-friendly way.
+2. **exec_async()**: Typically used for async LLM calls.
+3. **post_async()**: For *awaiting user feedback*, *coordinating across multi-agents* or any additional async steps after `exec_async()`.
+
+**Note**: `AsyncNode` must be wrapped in `AsyncFlow`. `AsyncFlow` can also include regular (sync) nodes.
+
+### Example
+
+```python
+class SummarizeThenVerify(AsyncNode):
+ async def prep_async(self, shared):
+ # Example: read a file asynchronously
+ doc_text = await read_file_async(shared["doc_path"])
+ return doc_text
+
+ async def exec_async(self, prep_res):
+ # Example: async LLM call
+ summary = await call_llm_async(f"Summarize: {prep_res}")
+ return summary
+
+ async def post_async(self, shared, prep_res, exec_res):
+ # Example: wait for user feedback
+ decision = await gather_user_feedback(exec_res)
+ if decision == "approve":
+ shared["summary"] = exec_res
+ return "approve"
+ return "deny"
+
+summarize_node = SummarizeThenVerify()
+final_node = Finalize()
+
+# Define transitions
+summarize_node - "approve" >> final_node
+summarize_node - "deny" >> summarize_node # retry
+
+flow = AsyncFlow(start=summarize_node)
+
+async def main():
+ shared = {"doc_path": "document.txt"}
+ await flow.run_async(shared)
+ print("Final Summary:", shared.get("summary"))
+
+asyncio.run(main())
+```
+
+================================================
+File: docs/core_abstraction/batch.md
+================================================
+---
+layout: default
+title: "Batch"
+parent: "Core Abstraction"
+nav_order: 4
+---
+
+# Batch
+
+**Batch** makes it easier to handle large inputs in one Node or **rerun** a Flow multiple times. Example use cases:
+- **Chunk-based** processing (e.g., splitting large texts).
+- **Iterative** processing over lists of input items (e.g., user queries, files, URLs).
+
+## 1. BatchNode
+
+A **BatchNode** extends `Node` but changes `prep()` and `exec()`:
+
+- **`prep(shared)`**: returns an **iterable** (e.g., list, generator).
+- **`exec(item)`**: called **once** per item in that iterable.
+- **`post(shared, prep_res, exec_res_list)`**: after all items are processed, receives a **list** of results (`exec_res_list`) and returns an **Action**.
+
+
+### Example: Summarize a Large File
+
+```python
+class MapSummaries(BatchNode):
+ def prep(self, shared):
+ # Suppose we have a big file; chunk it
+ content = shared["data"]
+ chunk_size = 10000
+ chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
+ return chunks
+
+ def exec(self, chunk):
+ prompt = f"Summarize this chunk in 10 words: {chunk}"
+ summary = call_llm(prompt)
+ return summary
+
+ def post(self, shared, prep_res, exec_res_list):
+ combined = "\n".join(exec_res_list)
+ shared["summary"] = combined
+ return "default"
+
+map_summaries = MapSummaries()
+flow = Flow(start=map_summaries)
+flow.run(shared)
+```
+
+---
+
+## 2. BatchFlow
+
+A **BatchFlow** runs a **Flow** multiple times, each time with different `params`. Think of it as a loop that replays the Flow for each parameter set.
+
+### Example: Summarize Many Files
+
+```python
+class SummarizeAllFiles(BatchFlow):
+ def prep(self, shared):
+ # Return a list of param dicts (one per file)
+ filenames = list(shared["data"].keys()) # e.g., ["file1.txt", "file2.txt", ...]
+ return [{"filename": fn} for fn in filenames]
+
+# Suppose we have a per-file Flow (e.g., load_file >> summarize >> reduce):
+summarize_file = SummarizeFile(start=load_file)
+
+# Wrap that flow into a BatchFlow:
+summarize_all_files = SummarizeAllFiles(start=summarize_file)
+summarize_all_files.run(shared)
+```
+
+### Under the Hood
+1. `prep(shared)` returns a list of param dicts—e.g., `[{filename: "file1.txt"}, {filename: "file2.txt"}, ...]`.
+2. The **BatchFlow** loops through each dict. For each one:
+ - It merges the dict with the BatchFlow’s own `params`.
+ - It calls `flow.run(shared)` using the merged result.
+3. This means the sub-Flow is run **repeatedly**, once for every param dict.
+
+---
+
+## 3. Nested or Multi-Level Batches
+
+You can nest a **BatchFlow** in another **BatchFlow**. For instance:
+- **Outer** batch: returns a list of diretory param dicts (e.g., `{"directory": "/pathA"}`, `{"directory": "/pathB"}`, ...).
+- **Inner** batch: returning a list of per-file param dicts.
+
+At each level, **BatchFlow** merges its own param dict with the parent’s. By the time you reach the **innermost** node, the final `params` is the merged result of **all** parents in the chain. This way, a nested structure can keep track of the entire context (e.g., directory + file name) at once.
+
+```python
+
+class FileBatchFlow(BatchFlow):
+ def prep(self, shared):
+ directory = self.params["directory"]
+ # e.g., files = ["file1.txt", "file2.txt", ...]
+ files = [f for f in os.listdir(directory) if f.endswith(".txt")]
+ return [{"filename": f} for f in files]
+
+class DirectoryBatchFlow(BatchFlow):
+ def prep(self, shared):
+ directories = [ "/path/to/dirA", "/path/to/dirB"]
+ return [{"directory": d} for d in directories]
+
+# MapSummaries have params like {"directory": "/path/to/dirA", "filename": "file1.txt"}
+inner_flow = FileBatchFlow(start=MapSummaries())
+outer_flow = DirectoryBatchFlow(start=inner_flow)
+```
+
+================================================
+File: docs/core_abstraction/communication.md
+================================================
+---
+layout: default
+title: "Communication"
+parent: "Core Abstraction"
+nav_order: 3
+---
+
+# Communication
+
+Nodes and Flows **communicate** in 2 ways:
+
+1. **Shared Store (for almost all the cases)**
+
+ - A global data structure (often an in-mem dict) that all nodes can read ( `prep()`) and write (`post()`).
+ - Great for data results, large content, or anything multiple nodes need.
+ - You shall design the data structure and populate it ahead.
+
+ - > **Separation of Concerns:** Use `Shared Store` for almost all cases to separate *Data Schema* from *Compute Logic*! This approach is both flexible and easy to manage, resulting in more maintainable code. `Params` is more a syntax sugar for [Batch](./batch.md).
+ {: .best-practice }
+
+2. **Params (only for [Batch](./batch.md))**
+ - Each node has a local, ephemeral `params` dict passed in by the **parent Flow**, used as an identifier for tasks. Parameter keys and values shall be **immutable**.
+ - Good for identifiers like filenames or numeric IDs, in Batch mode.
+
+If you know memory management, think of the **Shared Store** like a **heap** (shared by all function calls), and **Params** like a **stack** (assigned by the caller).
+
+---
+
+## 1. Shared Store
+
+### Overview
+
+A shared store is typically an in-mem dictionary, like:
+```python
+shared = {"data": {}, "summary": {}, "config": {...}, ...}
+```
+
+It can also contain local file handlers, DB connections, or a combination for persistence. We recommend deciding the data structure or DB schema first based on your app requirements.
+
+### Example
+
+```python
+class LoadData(Node):
+ def post(self, shared, prep_res, exec_res):
+ # We write data to shared store
+ shared["data"] = "Some text content"
+ return None
+
+class Summarize(Node):
+ def prep(self, shared):
+ # We read data from shared store
+ return shared["data"]
+
+ def exec(self, prep_res):
+ # Call LLM to summarize
+ prompt = f"Summarize: {prep_res}"
+ summary = call_llm(prompt)
+ return summary
+
+ def post(self, shared, prep_res, exec_res):
+ # We write summary to shared store
+ shared["summary"] = exec_res
+ return "default"
+
+load_data = LoadData()
+summarize = Summarize()
+load_data >> summarize
+flow = Flow(start=load_data)
+
+shared = {}
+flow.run(shared)
+```
+
+Here:
+- `LoadData` writes to `shared["data"]`.
+- `Summarize` reads from `shared["data"]`, summarizes, and writes to `shared["summary"]`.
+
+---
+
+## 2. Params
+
+**Params** let you store *per-Node* or *per-Flow* config that doesn't need to live in the shared store. They are:
+- **Immutable** during a Node's run cycle (i.e., they don't change mid-`prep->exec->post`).
+- **Set** via `set_params()`.
+- **Cleared** and updated each time a parent Flow calls it.
+
+> Only set the uppermost Flow params because others will be overwritten by the parent Flow.
+>
+> If you need to set child node params, see [Batch](./batch.md).
+{: .warning }
+
+Typically, **Params** are identifiers (e.g., file name, page number). Use them to fetch the task you assigned or write to a specific part of the shared store.
+
+### Example
+
+```python
+# 1) Create a Node that uses params
+class SummarizeFile(Node):
+ def prep(self, shared):
+ # Access the node's param
+ filename = self.params["filename"]
+ return shared["data"].get(filename, "")
+
+ def exec(self, prep_res):
+ prompt = f"Summarize: {prep_res}"
+ return call_llm(prompt)
+
+ def post(self, shared, prep_res, exec_res):
+ filename = self.params["filename"]
+ shared["summary"][filename] = exec_res
+ return "default"
+
+# 2) Set params
+node = SummarizeFile()
+
+# 3) Set Node params directly (for testing)
+node.set_params({"filename": "doc1.txt"})
+node.run(shared)
+
+# 4) Create Flow
+flow = Flow(start=node)
+
+# 5) Set Flow params (overwrites node params)
+flow.set_params({"filename": "doc2.txt"})
+flow.run(shared) # The node summarizes doc2, not doc1
+```
+
+================================================
+File: docs/core_abstraction/flow.md
+================================================
+---
+layout: default
+title: "Flow"
+parent: "Core Abstraction"
+nav_order: 2
+---
+
+# Flow
+
+A **Flow** orchestrates a graph of Nodes. You can chain Nodes in a sequence or create branching depending on the **Actions** returned from each Node's `post()`.
+
+## 1. Action-based Transitions
+
+Each Node's `post()` returns an **Action** string. By default, if `post()` doesn't return anything, we treat that as `"default"`.
+
+You define transitions with the syntax:
+
+1. **Basic default transition**: `node_a >> node_b`
+ This means if `node_a.post()` returns `"default"`, go to `node_b`.
+ (Equivalent to `node_a - "default" >> node_b`)
+
+2. **Named action transition**: `node_a - "action_name" >> node_b`
+ This means if `node_a.post()` returns `"action_name"`, go to `node_b`.
+
+It's possible to create loops, branching, or multi-step flows.
+
+## 2. Creating a Flow
+
+A **Flow** begins with a **start** node. You call `Flow(start=some_node)` to specify the entry point. When you call `flow.run(shared)`, it executes the start node, looks at its returned Action from `post()`, follows the transition, and continues until there's no next node.
+
+### Example: Simple Sequence
+
+Here's a minimal flow of two nodes in a chain:
+
+```python
+node_a >> node_b
+flow = Flow(start=node_a)
+flow.run(shared)
+```
+
+- When you run the flow, it executes `node_a`.
+- Suppose `node_a.post()` returns `"default"`.
+- The flow then sees `"default"` Action is linked to `node_b` and runs `node_b`.
+- `node_b.post()` returns `"default"` but we didn't define `node_b >> something_else`. So the flow ends there.
+
+### Example: Branching & Looping
+
+Here's a simple expense approval flow that demonstrates branching and looping. The `ReviewExpense` node can return three possible Actions:
+
+- `"approved"`: expense is approved, move to payment processing
+- `"needs_revision"`: expense needs changes, send back for revision
+- `"rejected"`: expense is denied, finish the process
+
+We can wire them like this:
+
+```python
+# Define the flow connections
+review - "approved" >> payment # If approved, process payment
+review - "needs_revision" >> revise # If needs changes, go to revision
+review - "rejected" >> finish # If rejected, finish the process
+
+revise >> review # After revision, go back for another review
+payment >> finish # After payment, finish the process
+
+flow = Flow(start=review)
+```
+
+Let's see how it flows:
+
+1. If `review.post()` returns `"approved"`, the expense moves to the `payment` node
+2. If `review.post()` returns `"needs_revision"`, it goes to the `revise` node, which then loops back to `review`
+3. If `review.post()` returns `"rejected"`, it moves to the `finish` node and stops
+
+```mermaid
+flowchart TD
+ review[Review Expense] -->|approved| payment[Process Payment]
+ review -->|needs_revision| revise[Revise Report]
+ review -->|rejected| finish[Finish Process]
+
+ revise --> review
+ payment --> finish
+```
+
+### Running Individual Nodes vs. Running a Flow
+
+- `node.run(shared)`: Just runs that node alone (calls `prep->exec->post()`), returns an Action.
+- `flow.run(shared)`: Executes from the start node, follows Actions to the next node, and so on until the flow can't continue.
+
+> `node.run(shared)` **does not** proceed to the successor.
+> This is mainly for debugging or testing a single node.
+>
+> Always use `flow.run(...)` in production to ensure the full pipeline runs correctly.
+{: .warning }
+
+## 3. Nested Flows
+
+A **Flow** can act like a Node, which enables powerful composition patterns. This means you can:
+
+1. Use a Flow as a Node within another Flow's transitions.
+2. Combine multiple smaller Flows into a larger Flow for reuse.
+3. Node `params` will be a merging of **all** parents' `params`.
+
+### Flow's Node Methods
+
+A **Flow** is also a **Node**, so it will run `prep()` and `post()`. However:
+
+- It **won't** run `exec()`, as its main logic is to orchestrate its nodes.
+- `post()` always receives `None` for `exec_res` and should instead get the flow execution results from the shared store.
+
+### Basic Flow Nesting
+
+Here's how to connect a flow to another node:
+
+```python
+# Create a sub-flow
+node_a >> node_b
+subflow = Flow(start=node_a)
+
+# Connect it to another node
+subflow >> node_c
+
+# Create the parent flow
+parent_flow = Flow(start=subflow)
+```
+
+When `parent_flow.run()` executes:
+1. It starts `subflow`
+2. `subflow` runs through its nodes (`node_a->node_b`)
+3. After `subflow` completes, execution continues to `node_c`
+
+### Example: Order Processing Pipeline
+
+Here's a practical example that breaks down order processing into nested flows:
+
+```python
+# Payment processing sub-flow
+validate_payment >> process_payment >> payment_confirmation
+payment_flow = Flow(start=validate_payment)
+
+# Inventory sub-flow
+check_stock >> reserve_items >> update_inventory
+inventory_flow = Flow(start=check_stock)
+
+# Shipping sub-flow
+create_label >> assign_carrier >> schedule_pickup
+shipping_flow = Flow(start=create_label)
+
+# Connect the flows into a main order pipeline
+payment_flow >> inventory_flow >> shipping_flow
+
+# Create the master flow
+order_pipeline = Flow(start=payment_flow)
+
+# Run the entire pipeline
+order_pipeline.run(shared_data)
+```
+
+This creates a clean separation of concerns while maintaining a clear execution path:
+
+```mermaid
+flowchart LR
+ subgraph order_pipeline[Order Pipeline]
+ subgraph paymentFlow["Payment Flow"]
+ A[Validate Payment] --> B[Process Payment] --> C[Payment Confirmation]
+ end
+
+ subgraph inventoryFlow["Inventory Flow"]
+ D[Check Stock] --> E[Reserve Items] --> F[Update Inventory]
+ end
+
+ subgraph shippingFlow["Shipping Flow"]
+ G[Create Label] --> H[Assign Carrier] --> I[Schedule Pickup]
+ end
+
+ paymentFlow --> inventoryFlow
+ inventoryFlow --> shippingFlow
+ end
+```
+
+================================================
+File: docs/core_abstraction/node.md
+================================================
+---
+layout: default
+title: "Node"
+parent: "Core Abstraction"
+nav_order: 1
+---
+
+# Node
+
+A **Node** is the smallest building block. Each Node has 3 steps `prep->exec->post`:
+
+
+

+
+
+1. `prep(shared)`
+ - **Read and preprocess data** from `shared` store.
+ - Examples: *query DB, read files, or serialize data into a string*.
+ - Return `prep_res`, which is used by `exec()` and `post()`.
+
+2. `exec(prep_res)`
+ - **Execute compute logic**, with optional retries and error handling (below).
+ - Examples: *(mostly) LLM calls, remote APIs, tool use*.
+ - ⚠️ This shall be only for compute and **NOT** access `shared`.
+ - ⚠️ If retries enabled, ensure idempotent implementation.
+ - Return `exec_res`, which is passed to `post()`.
+
+3. `post(shared, prep_res, exec_res)`
+ - **Postprocess and write data** back to `shared`.
+ - Examples: *update DB, change states, log results*.
+ - **Decide the next action** by returning a *string* (`action = "default"` if *None*).
+
+> **Why 3 steps?** To enforce the principle of *separation of concerns*. The data storage and data processing are operated separately.
+>
+> All steps are *optional*. E.g., you can only implement `prep` and `post` if you just need to process data.
+{: .note }
+
+### Fault Tolerance & Retries
+
+You can **retry** `exec()` if it raises an exception via two parameters when define the Node:
+
+- `max_retries` (int): Max times to run `exec()`. The default is `1` (**no** retry).
+- `wait` (int): The time to wait (in **seconds**) before next retry. By default, `wait=0` (no waiting).
+`wait` is helpful when you encounter rate-limits or quota errors from your LLM provider and need to back off.
+
+```python
+my_node = SummarizeFile(max_retries=3, wait=10)
+```
+
+When an exception occurs in `exec()`, the Node automatically retries until:
+
+- It either succeeds, or
+- The Node has retried `max_retries - 1` times already and fails on the last attempt.
+
+You can get the current retry times (0-based) from `self.cur_retry`.
+
+```python
+class RetryNode(Node):
+ def exec(self, prep_res):
+ print(f"Retry {self.cur_retry} times")
+ raise Exception("Failed")
+```
+
+### Graceful Fallback
+
+To **gracefully handle** the exception (after all retries) rather than raising it, override:
+
+```python
+def exec_fallback(self, prep_res, exc):
+ raise exc
+```
+
+By default, it just re-raises exception. But you can return a fallback result instead, which becomes the `exec_res` passed to `post()`.
+
+### Example: Summarize file
+
+```python
+class SummarizeFile(Node):
+ def prep(self, shared):
+ return shared["data"]
+
+ def exec(self, prep_res):
+ if not prep_res:
+ return "Empty file content"
+ prompt = f"Summarize this text in 10 words: {prep_res}"
+ summary = call_llm(prompt) # might fail
+ return summary
+
+ def exec_fallback(self, prep_res, exc):
+ # Provide a simple fallback instead of crashing
+ return "There was an error processing your request."
+
+ def post(self, shared, prep_res, exec_res):
+ shared["summary"] = exec_res
+ # Return "default" by not returning
+
+summarize_node = SummarizeFile(max_retries=3)
+
+# node.run() calls prep->exec->post
+# If exec() fails, it retries up to 3 times before calling exec_fallback()
+action_result = summarize_node.run(shared)
+
+print("Action returned:", action_result) # "default"
+print("Summary stored:", shared["summary"])
+```
+
+
+================================================
+File: docs/core_abstraction/parallel.md
+================================================
+---
+layout: default
+title: "(Advanced) Parallel"
+parent: "Core Abstraction"
+nav_order: 6
+---
+
+# (Advanced) Parallel
+
+**Parallel** Nodes and Flows let you run multiple **Async** Nodes and Flows **concurrently**—for example, summarizing multiple texts at once. This can improve performance by overlapping I/O and compute.
+
+> Because of Python’s GIL, parallel nodes and flows can’t truly parallelize CPU-bound tasks (e.g., heavy numerical computations). However, they excel at overlapping I/O-bound work—like LLM calls, database queries, API requests, or file I/O.
+{: .warning }
+
+> - **Ensure Tasks Are Independent**: If each item depends on the output of a previous item, **do not** parallelize.
+>
+> - **Beware of Rate Limits**: Parallel calls can **quickly** trigger rate limits on LLM services. You may need a **throttling** mechanism (e.g., semaphores or sleep intervals).
+>
+> - **Consider Single-Node Batch APIs**: Some LLMs offer a **batch inference** API where you can send multiple prompts in a single call. This is more complex to implement but can be more efficient than launching many parallel requests and mitigates rate limits.
+{: .best-practice }
+
+## AsyncParallelBatchNode
+
+Like **AsyncBatchNode**, but run `exec_async()` in **parallel**:
+
+```python
+class ParallelSummaries(AsyncParallelBatchNode):
+ async def prep_async(self, shared):
+ # e.g., multiple texts
+ return shared["texts"]
+
+ async def exec_async(self, text):
+ prompt = f"Summarize: {text}"
+ return await call_llm_async(prompt)
+
+ async def post_async(self, shared, prep_res, exec_res_list):
+ shared["summary"] = "\n\n".join(exec_res_list)
+ return "default"
+
+node = ParallelSummaries()
+flow = AsyncFlow(start=node)
+```
+
+## AsyncParallelBatchFlow
+
+Parallel version of **BatchFlow**. Each iteration of the sub-flow runs **concurrently** using different parameters:
+
+```python
+class SummarizeMultipleFiles(AsyncParallelBatchFlow):
+ async def prep_async(self, shared):
+ return [{"filename": f} for f in shared["files"]]
+
+sub_flow = AsyncFlow(start=LoadAndSummarizeFile())
+parallel_flow = SummarizeMultipleFiles(start=sub_flow)
+await parallel_flow.run_async(shared)
+```
+
+================================================
+File: docs/design_pattern/agent.md
+================================================
+---
+layout: default
+title: "Agent"
+parent: "Design Pattern"
+nav_order: 1
+---
+
+# Agent
+
+Agent is a powerful design pattern in which nodes can take dynamic actions based on the context.
+
+
+

+
+
+## Implement Agent with Graph
+
+1. **Context and Action:** Implement nodes that supply context and perform actions.
+2. **Branching:** Use branching to connect each action node to an agent node. Use action to allow the agent to direct the [flow](../core_abstraction/flow.md) between nodes—and potentially loop back for multi-step.
+3. **Agent Node:** Provide a prompt to decide action—for example:
+
+```python
+f"""
+### CONTEXT
+Task: {task_description}
+Previous Actions: {previous_actions}
+Current State: {current_state}
+
+### ACTION SPACE
+[1] search
+ Description: Use web search to get results
+ Parameters:
+ - query (str): What to search for
+
+[2] answer
+ Description: Conclude based on the results
+ Parameters:
+ - result (str): Final answer to provide
+
+### NEXT ACTION
+Decide the next action based on the current context and available action space.
+Return your response in the following format:
+
+```yaml
+thinking: |
+
+action:
+parameters:
+ :
+```"""
+```
+
+The core of building **high-performance** and **reliable** agents boils down to:
+
+1. **Context Management:** Provide *relevant, minimal context.* For example, rather than including an entire chat history, retrieve the most relevant via [RAG](./rag.md). Even with larger context windows, LLMs still fall victim to ["lost in the middle"](https://arxiv.org/abs/2307.03172), overlooking mid-prompt content.
+
+2. **Action Space:** Provide *a well-structured and unambiguous* set of actions—avoiding overlap like separate `read_databases` or `read_csvs`. Instead, import CSVs into the database.
+
+## Example Good Action Design
+
+- **Incremental:** Feed content in manageable chunks (500 lines or 1 page) instead of all at once.
+
+- **Overview-zoom-in:** First provide high-level structure (table of contents, summary), then allow drilling into details (raw texts).
+
+- **Parameterized/Programmable:** Instead of fixed actions, enable parameterized (columns to select) or programmable (SQL queries) actions, for example, to read CSV files.
+
+- **Backtracking:** Let the agent undo the last step instead of restarting entirely, preserving progress when encountering errors or dead ends.
+
+## Example: Search Agent
+
+This agent:
+1. Decides whether to search or answer
+2. If searches, loops back to decide if more search needed
+3. Answers when enough context gathered
+
+```python
+class DecideAction(Node):
+ def prep(self, shared):
+ context = shared.get("context", "No previous search")
+ query = shared["query"]
+ return query, context
+
+ def exec(self, inputs):
+ query, context = inputs
+ prompt = f"""
+Given input: {query}
+Previous search results: {context}
+Should I: 1) Search web for more info 2) Answer with current knowledge
+Output in yaml:
+```yaml
+action: search/answer
+reason: why this action
+search_term: search phrase if action is search
+```"""
+ resp = call_llm(prompt)
+ yaml_str = resp.split("```yaml")[1].split("```")[0].strip()
+ result = yaml.safe_load(yaml_str)
+
+ assert isinstance(result, dict)
+ assert "action" in result
+ assert "reason" in result
+ assert result["action"] in ["search", "answer"]
+ if result["action"] == "search":
+ assert "search_term" in result
+
+ return result
+
+ def post(self, shared, prep_res, exec_res):
+ if exec_res["action"] == "search":
+ shared["search_term"] = exec_res["search_term"]
+ return exec_res["action"]
+
+class SearchWeb(Node):
+ def prep(self, shared):
+ return shared["search_term"]
+
+ def exec(self, search_term):
+ return search_web(search_term)
+
+ def post(self, shared, prep_res, exec_res):
+ prev_searches = shared.get("context", [])
+ shared["context"] = prev_searches + [
+ {"term": shared["search_term"], "result": exec_res}
+ ]
+ return "decide"
+
+class DirectAnswer(Node):
+ def prep(self, shared):
+ return shared["query"], shared.get("context", "")
+
+ def exec(self, inputs):
+ query, context = inputs
+ return call_llm(f"Context: {context}\nAnswer: {query}")
+
+ def post(self, shared, prep_res, exec_res):
+ print(f"Answer: {exec_res}")
+ shared["answer"] = exec_res
+
+# Connect nodes
+decide = DecideAction()
+search = SearchWeb()
+answer = DirectAnswer()
+
+decide - "search" >> search
+decide - "answer" >> answer
+search - "decide" >> decide # Loop back
+
+flow = Flow(start=decide)
+flow.run({"query": "Who won the Nobel Prize in Physics 2024?"})
+```
+
+================================================
+File: docs/design_pattern/mapreduce.md
+================================================
+---
+layout: default
+title: "Map Reduce"
+parent: "Design Pattern"
+nav_order: 4
+---
+
+# Map Reduce
+
+MapReduce is a design pattern suitable when you have either:
+- Large input data (e.g., multiple files to process), or
+- Large output data (e.g., multiple forms to fill)
+
+and there is a logical way to break the task into smaller, ideally independent parts.
+
+
+

+
+
+You first break down the task using [BatchNode](../core_abstraction/batch.md) in the map phase, followed by aggregation in the reduce phase.
+
+### Example: Document Summarization
+
+```python
+class SummarizeAllFiles(BatchNode):
+ def prep(self, shared):
+ files_dict = shared["files"] # e.g. 10 files
+ return list(files_dict.items()) # [("file1.txt", "aaa..."), ("file2.txt", "bbb..."), ...]
+
+ def exec(self, one_file):
+ filename, file_content = one_file
+ summary_text = call_llm(f"Summarize the following file:\n{file_content}")
+ return (filename, summary_text)
+
+ def post(self, shared, prep_res, exec_res_list):
+ shared["file_summaries"] = dict(exec_res_list)
+
+class CombineSummaries(Node):
+ def prep(self, shared):
+ return shared["file_summaries"]
+
+ def exec(self, file_summaries):
+ # format as: "File1: summary\nFile2: summary...\n"
+ text_list = []
+ for fname, summ in file_summaries.items():
+ text_list.append(f"{fname} summary:\n{summ}\n")
+ big_text = "\n---\n".join(text_list)
+
+ return call_llm(f"Combine these file summaries into one final summary:\n{big_text}")
+
+ def post(self, shared, prep_res, final_summary):
+ shared["all_files_summary"] = final_summary
+
+batch_node = SummarizeAllFiles()
+combine_node = CombineSummaries()
+batch_node >> combine_node
+
+flow = Flow(start=batch_node)
+
+shared = {
+ "files": {
+ "file1.txt": "Alice was beginning to get very tired of sitting by her sister...",
+ "file2.txt": "Some other interesting text ...",
+ # ...
+ }
+}
+flow.run(shared)
+print("Individual Summaries:", shared["file_summaries"])
+print("\nFinal Summary:\n", shared["all_files_summary"])
+```
+
+================================================
+File: docs/design_pattern/rag.md
+================================================
+---
+layout: default
+title: "RAG"
+parent: "Design Pattern"
+nav_order: 3
+---
+
+# RAG (Retrieval Augmented Generation)
+
+For certain LLM tasks like answering questions, providing relevant context is essential. One common architecture is a **two-stage** RAG pipeline:
+
+
+

+
+
+1. **Offline stage**: Preprocess and index documents ("building the index").
+2. **Online stage**: Given a question, generate answers by retrieving the most relevant context.
+
+---
+## Stage 1: Offline Indexing
+
+We create three Nodes:
+1. `ChunkDocs` – [chunks](../utility_function/chunking.md) raw text.
+2. `EmbedDocs` – [embeds](../utility_function/embedding.md) each chunk.
+3. `StoreIndex` – stores embeddings into a [vector database](../utility_function/vector.md).
+
+```python
+class ChunkDocs(BatchNode):
+ def prep(self, shared):
+ # A list of file paths in shared["files"]. We process each file.
+ return shared["files"]
+
+ def exec(self, filepath):
+ # read file content. In real usage, do error handling.
+ with open(filepath, "r", encoding="utf-8") as f:
+ text = f.read()
+ # chunk by 100 chars each
+ chunks = []
+ size = 100
+ for i in range(0, len(text), size):
+ chunks.append(text[i : i + size])
+ return chunks
+
+ def post(self, shared, prep_res, exec_res_list):
+ # exec_res_list is a list of chunk-lists, one per file.
+ # flatten them all into a single list of chunks.
+ all_chunks = []
+ for chunk_list in exec_res_list:
+ all_chunks.extend(chunk_list)
+ shared["all_chunks"] = all_chunks
+
+class EmbedDocs(BatchNode):
+ def prep(self, shared):
+ return shared["all_chunks"]
+
+ def exec(self, chunk):
+ return get_embedding(chunk)
+
+ def post(self, shared, prep_res, exec_res_list):
+ # Store the list of embeddings.
+ shared["all_embeds"] = exec_res_list
+ print(f"Total embeddings: {len(exec_res_list)}")
+
+class StoreIndex(Node):
+ def prep(self, shared):
+ # We'll read all embeds from shared.
+ return shared["all_embeds"]
+
+ def exec(self, all_embeds):
+ # Create a vector index (faiss or other DB in real usage).
+ index = create_index(all_embeds)
+ return index
+
+ def post(self, shared, prep_res, index):
+ shared["index"] = index
+
+# Wire them in sequence
+chunk_node = ChunkDocs()
+embed_node = EmbedDocs()
+store_node = StoreIndex()
+
+chunk_node >> embed_node >> store_node
+
+OfflineFlow = Flow(start=chunk_node)
+```
+
+Usage example:
+
+```python
+shared = {
+ "files": ["doc1.txt", "doc2.txt"], # any text files
+}
+OfflineFlow.run(shared)
+```
+
+---
+## Stage 2: Online Query & Answer
+
+We have 3 nodes:
+1. `EmbedQuery` – embeds the user’s question.
+2. `RetrieveDocs` – retrieves top chunk from the index.
+3. `GenerateAnswer` – calls the LLM with the question + chunk to produce the final answer.
+
+```python
+class EmbedQuery(Node):
+ def prep(self, shared):
+ return shared["question"]
+
+ def exec(self, question):
+ return get_embedding(question)
+
+ def post(self, shared, prep_res, q_emb):
+ shared["q_emb"] = q_emb
+
+class RetrieveDocs(Node):
+ def prep(self, shared):
+ # We'll need the query embedding, plus the offline index/chunks
+ return shared["q_emb"], shared["index"], shared["all_chunks"]
+
+ def exec(self, inputs):
+ q_emb, index, chunks = inputs
+ I, D = search_index(index, q_emb, top_k=1)
+ best_id = I[0][0]
+ relevant_chunk = chunks[best_id]
+ return relevant_chunk
+
+ def post(self, shared, prep_res, relevant_chunk):
+ shared["retrieved_chunk"] = relevant_chunk
+ print("Retrieved chunk:", relevant_chunk[:60], "...")
+
+class GenerateAnswer(Node):
+ def prep(self, shared):
+ return shared["question"], shared["retrieved_chunk"]
+
+ def exec(self, inputs):
+ question, chunk = inputs
+ prompt = f"Question: {question}\nContext: {chunk}\nAnswer:"
+ return call_llm(prompt)
+
+ def post(self, shared, prep_res, answer):
+ shared["answer"] = answer
+ print("Answer:", answer)
+
+embed_qnode = EmbedQuery()
+retrieve_node = RetrieveDocs()
+generate_node = GenerateAnswer()
+
+embed_qnode >> retrieve_node >> generate_node
+OnlineFlow = Flow(start=embed_qnode)
+```
+
+Usage example:
+
+```python
+# Suppose we already ran OfflineFlow and have:
+# shared["all_chunks"], shared["index"], etc.
+shared["question"] = "Why do people like cats?"
+
+OnlineFlow.run(shared)
+# final answer in shared["answer"]
+```
+
+================================================
+File: docs/design_pattern/structure.md
+================================================
+---
+layout: default
+title: "Structured Output"
+parent: "Design Pattern"
+nav_order: 5
+---
+
+# Structured Output
+
+In many use cases, you may want the LLM to output a specific structure, such as a list or a dictionary with predefined keys.
+
+There are several approaches to achieve a structured output:
+- **Prompting** the LLM to strictly return a defined structure.
+- Using LLMs that natively support **schema enforcement**.
+- **Post-processing** the LLM's response to extract structured content.
+
+In practice, **Prompting** is simple and reliable for modern LLMs.
+
+### Example Use Cases
+
+- Extracting Key Information
+
+```yaml
+product:
+ name: Widget Pro
+ price: 199.99
+ description: |
+ A high-quality widget designed for professionals.
+ Recommended for advanced users.
+```
+
+- Summarizing Documents into Bullet Points
+
+```yaml
+summary:
+ - This product is easy to use.
+ - It is cost-effective.
+ - Suitable for all skill levels.
+```
+
+- Generating Configuration Files
+
+```yaml
+server:
+ host: 127.0.0.1
+ port: 8080
+ ssl: true
+```
+
+## Prompt Engineering
+
+When prompting the LLM to produce **structured** output:
+1. **Wrap** the structure in code fences (e.g., `yaml`).
+2. **Validate** that all required fields exist (and let `Node` handles retry).
+
+### Example Text Summarization
+
+```python
+class SummarizeNode(Node):
+ def exec(self, prep_res):
+ # Suppose `prep_res` is the text to summarize.
+ prompt = f"""
+Please summarize the following text as YAML, with exactly 3 bullet points
+
+{prep_res}
+
+Now, output:
+```yaml
+summary:
+ - bullet 1
+ - bullet 2
+ - bullet 3
+```"""
+ response = call_llm(prompt)
+ yaml_str = response.split("```yaml")[1].split("```")[0].strip()
+
+ import yaml
+ structured_result = yaml.safe_load(yaml_str)
+
+ assert "summary" in structured_result
+ assert isinstance(structured_result["summary"], list)
+
+ return structured_result
+```
+
+> Besides using `assert` statements, another popular way to validate schemas is [Pydantic](https://github.com/pydantic/pydantic)
+{: .note }
+
+### Why YAML instead of JSON?
+
+Current LLMs struggle with escaping. YAML is easier with strings since they don't always need quotes.
+
+**In JSON**
+
+```json
+{
+ "dialogue": "Alice said: \"Hello Bob.\\nHow are you?\\nI am good.\""
+}
+```
+
+- Every double quote inside the string must be escaped with `\"`.
+- Each newline in the dialogue must be represented as `\n`.
+
+**In YAML**
+
+```yaml
+dialogue: |
+ Alice said: "Hello Bob.
+ How are you?
+ I am good."
+```
+
+- No need to escape interior quotes—just place the entire text under a block literal (`|`).
+- Newlines are naturally preserved without needing `\n`.
+
+================================================
+File: docs/design_pattern/workflow.md
+================================================
+---
+layout: default
+title: "Workflow"
+parent: "Design Pattern"
+nav_order: 2
+---
+
+# Workflow
+
+Many real-world tasks are too complex for one LLM call. The solution is to **Task Decomposition**: decompose them into a [chain](../core_abstraction/flow.md) of multiple Nodes.
+
+
+

+
+
+> - You don't want to make each task **too coarse**, because it may be *too complex for one LLM call*.
+> - You don't want to make each task **too granular**, because then *the LLM call doesn't have enough context* and results are *not consistent across nodes*.
+>
+> You usually need multiple *iterations* to find the *sweet spot*. If the task has too many *edge cases*, consider using [Agents](./agent.md).
+{: .best-practice }
+
+### Example: Article Writing
+
+```python
+class GenerateOutline(Node):
+ def prep(self, shared): return shared["topic"]
+ def exec(self, topic): return call_llm(f"Create a detailed outline for an article about {topic}")
+ def post(self, shared, prep_res, exec_res): shared["outline"] = exec_res
+
+class WriteSection(Node):
+ def prep(self, shared): return shared["outline"]
+ def exec(self, outline): return call_llm(f"Write content based on this outline: {outline}")
+ def post(self, shared, prep_res, exec_res): shared["draft"] = exec_res
+
+class ReviewAndRefine(Node):
+ def prep(self, shared): return shared["draft"]
+ def exec(self, draft): return call_llm(f"Review and improve this draft: {draft}")
+ def post(self, shared, prep_res, exec_res): shared["final_article"] = exec_res
+
+# Connect nodes
+outline = GenerateOutline()
+write = WriteSection()
+review = ReviewAndRefine()
+
+outline >> write >> review
+
+# Create and run flow
+writing_flow = Flow(start=outline)
+shared = {"topic": "AI Safety"}
+writing_flow.run(shared)
+```
+
+For *dynamic cases*, consider using [Agents](./agent.md).
+
+================================================
+File: docs/utility_function/llm.md
+================================================
+---
+layout: default
+title: "LLM Wrapper"
+parent: "Utility Function"
+nav_order: 1
+---
+
+# LLM Wrappers
+
+Check out libraries like [litellm](https://github.com/BerriAI/litellm).
+Here, we provide some minimal example implementations:
+
+1. OpenAI
+ ```python
+ def call_llm(prompt):
+ from openai import OpenAI
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+
+ # Example usage
+ call_llm("How are you?")
+ ```
+ > Store the API key in an environment variable like OPENAI_API_KEY for security.
+ {: .best-practice }
+
+2. Claude (Anthropic)
+ ```python
+ def call_llm(prompt):
+ from anthropic import Anthropic
+ client = Anthropic(api_key="YOUR_API_KEY_HERE")
+ response = client.messages.create(
+ model="claude-2",
+ messages=[{"role": "user", "content": prompt}],
+ max_tokens=100
+ )
+ return response.content
+ ```
+
+3. Google (Generative AI Studio / PaLM API)
+ ```python
+ def call_llm(prompt):
+ import google.generativeai as genai
+ genai.configure(api_key="YOUR_API_KEY_HERE")
+ response = genai.generate_text(
+ model="models/text-bison-001",
+ prompt=prompt
+ )
+ return response.result
+ ```
+
+4. Azure (Azure OpenAI)
+ ```python
+ def call_llm(prompt):
+ from openai import AzureOpenAI
+ client = AzureOpenAI(
+ azure_endpoint="https://.openai.azure.com/",
+ api_key="YOUR_API_KEY_HERE",
+ api_version="2023-05-15"
+ )
+ r = client.chat.completions.create(
+ model="",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+ ```
+
+5. Ollama (Local LLM)
+ ```python
+ def call_llm(prompt):
+ from ollama import chat
+ response = chat(
+ model="llama2",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return response.message.content
+ ```
+
+## Improvements
+Feel free to enhance your `call_llm` function as needed. Here are examples:
+
+- Handle chat history:
+
+```python
+def call_llm(messages):
+ from openai import OpenAI
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=messages
+ )
+ return r.choices[0].message.content
+```
+
+- Add in-memory caching
+
+```python
+from functools import lru_cache
+
+@lru_cache(maxsize=1000)
+def call_llm(prompt):
+ # Your implementation here
+ pass
+```
+
+> ⚠️ Caching conflicts with Node retries, as retries yield the same result.
+>
+> To address this, you could use cached results only if not retried.
+{: .warning }
+
+
+```python
+from functools import lru_cache
+
+@lru_cache(maxsize=1000)
+def cached_call(prompt):
+ pass
+
+def call_llm(prompt, use_cache):
+ if use_cache:
+ return cached_call(prompt)
+ # Call the underlying function directly
+ return cached_call.__wrapped__(prompt)
+
+class SummarizeNode(Node):
+ def exec(self, text):
+ return call_llm(f"Summarize: {text}", self.cur_retry==0)
+```
+
+- Enable logging:
+
+```python
+def call_llm(prompt):
+ import logging
+ logging.info(f"Prompt: {prompt}")
+ response = ... # Your implementation here
+ logging.info(f"Response: {response}")
+ return response
+```
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e17aaf0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,92 @@
+# Dependencies
+node_modules/
+vendor/
+.pnp/
+.pnp.js
+
+# Build outputs
+dist/
+build/
+out/
+*.pyc
+__pycache__/
+
+# Environment files
+.env
+.env.local
+.env.*.local
+.env.development
+.env.test
+.env.production
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# IDE - JetBrains
+.idea/
+*.iml
+*.iws
+*.ipr
+
+# IDE - Eclipse
+.project
+.classpath
+.settings/
+
+# Logs
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Operating System
+.DS_Store
+Thumbs.db
+*.swp
+*.swo
+
+# Testing
+coverage/
+.nyc_output/
+
+# Temporary files
+*.tmp
+*.temp
+.cache/
+
+# Compiled files
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+
+# Package files
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Database
+*.sqlite
+*.sqlite3
+*.db
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
\ No newline at end of file
diff --git a/.windsurfrules b/.windsurfrules
new file mode 100644
index 0000000..8f05631
--- /dev/null
+++ b/.windsurfrules
@@ -0,0 +1,1664 @@
+---
+layout: default
+title: "Agentic Coding"
+---
+
+# Agentic Coding: Humans Design, Agents code!
+
+> If you are an AI agents involved in building LLM Systems, read this guide **VERY, VERY** carefully! This is the most important chapter in the entire document. Throughout development, you should always (1) start with a small and simple solution, (2) design at a high level (`docs/design.md`) before implementation, and (3) frequently ask humans for feedback and clarification.
+{: .warning }
+
+## Agentic Coding Steps
+
+Agentic Coding should be a collaboration between Human System Design and Agent Implementation:
+
+| Steps | Human | AI | Comment |
+|:-----------------------|:----------:|:---------:|:------------------------------------------------------------------------|
+| 1. Requirements | ★★★ High | ★☆☆ Low | Humans understand the requirements and context. |
+| 2. Flow | ★★☆ Medium | ★★☆ Medium | Humans specify the high-level design, and the AI fills in the details. |
+| 3. Utilities | ★★☆ Medium | ★★☆ Medium | Humans provide available external APIs and integrations, and the AI helps with implementation. |
+| 4. Node | ★☆☆ Low | ★★★ High | The AI helps design the node types and data handling based on the flow. |
+| 5. Implementation | ★☆☆ Low | ★★★ High | The AI implements the flow based on the design. |
+| 6. Optimization | ★★☆ Medium | ★★☆ Medium | Humans evaluate the results, and the AI helps optimize. |
+| 7. Reliability | ★☆☆ Low | ★★★ High | The AI writes test cases and addresses corner cases. |
+
+1. **Requirements**: Clarify the requirements for your project, and evaluate whether an AI system is a good fit.
+ - Understand AI systems' strengths and limitations:
+ - **Good for**: Routine tasks requiring common sense (filling forms, replying to emails)
+ - **Good for**: Creative tasks with well-defined inputs (building slides, writing SQL)
+ - **Not good for**: Ambiguous problems requiring complex decision-making (business strategy, startup planning)
+ - **Keep It User-Centric:** Explain the "problem" from the user's perspective rather than just listing features.
+ - **Balance complexity vs. impact**: Aim to deliver the highest value features with minimal complexity early.
+
+2. **Flow Design**: Outline at a high level, describe how your AI system orchestrates nodes.
+ - Identify applicable design patterns (e.g., [Map Reduce](./design_pattern/mapreduce.md), [Agent](./design_pattern/agent.md), [RAG](./design_pattern/rag.md)).
+ - For each node in the flow, start with a high-level one-line description of what it does.
+ - If using **Map Reduce**, specify how to map (what to split) and how to reduce (how to combine).
+ - If using **Agent**, specify what are the inputs (context) and what are the possible actions.
+ - If using **RAG**, specify what to embed, noting that there's usually both offline (indexing) and online (retrieval) workflows.
+ - Outline the flow and draw it in a mermaid diagram. For example:
+ ```mermaid
+ flowchart LR
+ start[Start] --> batch[Batch]
+ batch --> check[Check]
+ check -->|OK| process
+ check -->|Error| fix[Fix]
+ fix --> check
+
+ subgraph process[Process]
+ step1[Step 1] --> step2[Step 2]
+ end
+
+ process --> endNode[End]
+ ```
+ - > **If Humans can't specify the flow, AI Agents can't automate it!** Before building an LLM system, thoroughly understand the problem and potential solution by manually solving example inputs to develop intuition.
+ {: .best-practice }
+
+3. **Utilities**: Based on the Flow Design, identify and implement necessary utility functions.
+ - Think of your AI system as the brain. It needs a body—these *external utility functions*—to interact with the real world:
+
+
+ - Reading inputs (e.g., retrieving Slack messages, reading emails)
+ - Writing outputs (e.g., generating reports, sending emails)
+ - Using external tools (e.g., calling LLMs, searching the web)
+ - **NOTE**: *LLM-based tasks* (e.g., summarizing text, analyzing sentiment) are **NOT** utility functions; rather, they are *core functions* internal in the AI system.
+ - For each utility function, implement it and write a simple test.
+ - Document their input/output, as well as why they are necessary. For example:
+ - `name`: `get_embedding` (`utils/get_embedding.py`)
+ - `input`: `str`
+ - `output`: a vector of 3072 floats
+ - `necessity`: Used by the second node to embed text
+ - Example utility implementation:
+ ```python
+ # utils/call_llm.py
+ from openai import OpenAI
+
+ def call_llm(prompt):
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+
+ if __name__ == "__main__":
+ prompt = "What is the meaning of life?"
+ print(call_llm(prompt))
+ ```
+ - > **Sometimes, design Utilies before Flow:** For example, for an LLM project to automate a legacy system, the bottleneck will likely be the available interface to that system. Start by designing the hardest utilities for interfacing, and then build the flow around them.
+ {: .best-practice }
+
+4. **Node Design**: Plan how each node will read and write data, and use utility functions.
+ - One core design principle for PocketFlow is to use a [shared store](./core_abstraction/communication.md), so start with a shared store design:
+ - For simple systems, use an in-memory dictionary.
+ - For more complex systems or when persistence is required, use a database.
+ - **Don't Repeat Yourself**: Use in-memory references or foreign keys.
+ - Example shared store design:
+ ```python
+ shared = {
+ "user": {
+ "id": "user123",
+ "context": { # Another nested dict
+ "weather": {"temp": 72, "condition": "sunny"},
+ "location": "San Francisco"
+ }
+ },
+ "results": {} # Empty dict to store outputs
+ }
+ ```
+ - For each [Node](./core_abstraction/node.md), describe its type, how it reads and writes data, and which utility function it uses. Keep it specific but high-level without codes. For example:
+ - `type`: Regular (or Batch, or Async)
+ - `prep`: Read "text" from the shared store
+ - `exec`: Call the embedding utility function
+ - `post`: Write "embedding" to the shared store
+
+5. **Implementation**: Implement the initial nodes and flows based on the design.
+ - 🎉 If you've reached this step, humans have finished the design. Now *Agentic Coding* begins!
+ - **"Keep it simple, stupid!"** Avoid complex features and full-scale type checking.
+ - **FAIL FAST**! Avoid `try` logic so you can quickly identify any weak points in the system.
+ - Add logging throughout the code to facilitate debugging.
+
+7. **Optimization**:
+ - **Use Intuition**: For a quick initial evaluation, human intuition is often a good start.
+ - **Redesign Flow (Back to Step 3)**: Consider breaking down tasks further, introducing agentic decisions, or better managing input contexts.
+ - If your flow design is already solid, move on to micro-optimizations:
+ - **Prompt Engineering**: Use clear, specific instructions with examples to reduce ambiguity.
+ - **In-Context Learning**: Provide robust examples for tasks that are difficult to specify with instructions alone.
+
+ - > **You'll likely iterate a lot!** Expect to repeat Steps 3–6 hundreds of times.
+ >
+ >
+ {: .best-practice }
+
+8. **Reliability**
+ - **Node Retries**: Add checks in the node `exec` to ensure outputs meet requirements, and consider increasing `max_retries` and `wait` times.
+ - **Logging and Visualization**: Maintain logs of all attempts and visualize node results for easier debugging.
+ - **Self-Evaluation**: Add a separate node (powered by an LLM) to review outputs when results are uncertain.
+
+## Example LLM Project File Structure
+
+```
+my_project/
+├── main.py
+├── nodes.py
+├── flow.py
+├── utils/
+│ ├── __init__.py
+│ ├── call_llm.py
+│ └── search_web.py
+├── requirements.txt
+└── docs/
+ └── design.md
+```
+
+- **`docs/design.md`**: Contains project documentation for each step above. This should be *high-level* and *no-code*.
+- **`utils/`**: Contains all utility functions.
+ - It's recommended to dedicate one Python file to each API call, for example `call_llm.py` or `search_web.py`.
+ - Each file should also include a `main()` function to try that API call
+- **`nodes.py`**: Contains all the node definitions.
+ ```python
+ # nodes.py
+ from pocketflow import Node
+ from utils.call_llm import call_llm
+
+ class GetQuestionNode(Node):
+ def exec(self, _):
+ # Get question directly from user input
+ user_question = input("Enter your question: ")
+ return user_question
+
+ def post(self, shared, prep_res, exec_res):
+ # Store the user's question
+ shared["question"] = exec_res
+ return "default" # Go to the next node
+
+ class AnswerNode(Node):
+ def prep(self, shared):
+ # Read question from shared
+ return shared["question"]
+
+ def exec(self, question):
+ # Call LLM to get the answer
+ return call_llm(question)
+
+ def post(self, shared, prep_res, exec_res):
+ # Store the answer in shared
+ shared["answer"] = exec_res
+ ```
+- **`flow.py`**: Implements functions that create flows by importing node definitions and connecting them.
+ ```python
+ # flow.py
+ from pocketflow import Flow
+ from nodes import GetQuestionNode, AnswerNode
+
+ def create_qa_flow():
+ """Create and return a question-answering flow."""
+ # Create nodes
+ get_question_node = GetQuestionNode()
+ answer_node = AnswerNode()
+
+ # Connect nodes in sequence
+ get_question_node >> answer_node
+
+ # Create flow starting with input node
+ return Flow(start=get_question_node)
+ ```
+- **`main.py`**: Serves as the project's entry point.
+ ```python
+ # main.py
+ from flow import create_qa_flow
+
+ # Example main function
+ # Please replace this with your own main function
+ def main():
+ shared = {
+ "question": None, # Will be populated by GetQuestionNode from user input
+ "answer": None # Will be populated by AnswerNode
+ }
+
+ # Create the flow and run it
+ qa_flow = create_qa_flow()
+ qa_flow.run(shared)
+ print(f"Question: {shared['question']}")
+ print(f"Answer: {shared['answer']}")
+
+ if __name__ == "__main__":
+ main()
+ ```
+
+================================================
+File: docs/index.md
+================================================
+---
+layout: default
+title: "Home"
+nav_order: 1
+---
+
+# Pocket Flow
+
+A [100-line](https://github.com/the-pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalist LLM framework for *Agents, Task Decomposition, RAG, etc*.
+
+- **Lightweight**: Just the core graph abstraction in 100 lines. ZERO dependencies, and vendor lock-in.
+- **Expressive**: Everything you love from larger frameworks—([Multi-](./design_pattern/multi_agent.html))[Agents](./design_pattern/agent.html), [Workflow](./design_pattern/workflow.html), [RAG](./design_pattern/rag.html), and more.
+- **Agentic-Coding**: Intuitive enough for AI agents to help humans build complex LLM applications.
+
+
+

+
+
+## Core Abstraction
+
+We model the LLM workflow as a **Graph + Shared Store**:
+
+- [Node](./core_abstraction/node.md) handles simple (LLM) tasks.
+- [Flow](./core_abstraction/flow.md) connects nodes through **Actions** (labeled edges).
+- [Shared Store](./core_abstraction/communication.md) enables communication between nodes within flows.
+- [Batch](./core_abstraction/batch.md) nodes/flows allow for data-intensive tasks.
+- [Async](./core_abstraction/async.md) nodes/flows allow waiting for asynchronous tasks.
+- [(Advanced) Parallel](./core_abstraction/parallel.md) nodes/flows handle I/O-bound tasks.
+
+
+

+
+
+## Design Pattern
+
+From there, it’s easy to implement popular design patterns:
+
+- [Agent](./design_pattern/agent.md) autonomously makes decisions.
+- [Workflow](./design_pattern/workflow.md) chains multiple tasks into pipelines.
+- [RAG](./design_pattern/rag.md) integrates data retrieval with generation.
+- [Map Reduce](./design_pattern/mapreduce.md) splits data tasks into Map and Reduce steps.
+- [Structured Output](./design_pattern/structure.md) formats outputs consistently.
+- [(Advanced) Multi-Agents](./design_pattern/multi_agent.md) coordinate multiple agents.
+
+
+

+
+
+## Utility Function
+
+We **do not** provide built-in utilities. Instead, we offer *examples*—please *implement your own*:
+
+- [LLM Wrapper](./utility_function/llm.md)
+- [Viz and Debug](./utility_function/viz.md)
+- [Web Search](./utility_function/websearch.md)
+- [Chunking](./utility_function/chunking.md)
+- [Embedding](./utility_function/embedding.md)
+- [Vector Databases](./utility_function/vector.md)
+- [Text-to-Speech](./utility_function/text_to_speech.md)
+
+**Why not built-in?**: I believe it's a *bad practice* for vendor-specific APIs in a general framework:
+- *API Volatility*: Frequent changes lead to heavy maintenance for hardcoded APIs.
+- *Flexibility*: You may want to switch vendors, use fine-tuned models, or run them locally.
+- *Optimizations*: Prompt caching, batching, and streaming are easier without vendor lock-in.
+
+## Ready to build your Apps?
+
+Check out [Agentic Coding Guidance](./guide.md), the fastest way to develop LLM projects with Pocket Flow!
+
+================================================
+File: docs/core_abstraction/async.md
+================================================
+---
+layout: default
+title: "(Advanced) Async"
+parent: "Core Abstraction"
+nav_order: 5
+---
+
+# (Advanced) Async
+
+**Async** Nodes implement `prep_async()`, `exec_async()`, `exec_fallback_async()`, and/or `post_async()`. This is useful for:
+
+1. **prep_async()**: For *fetching/reading data (files, APIs, DB)* in an I/O-friendly way.
+2. **exec_async()**: Typically used for async LLM calls.
+3. **post_async()**: For *awaiting user feedback*, *coordinating across multi-agents* or any additional async steps after `exec_async()`.
+
+**Note**: `AsyncNode` must be wrapped in `AsyncFlow`. `AsyncFlow` can also include regular (sync) nodes.
+
+### Example
+
+```python
+class SummarizeThenVerify(AsyncNode):
+ async def prep_async(self, shared):
+ # Example: read a file asynchronously
+ doc_text = await read_file_async(shared["doc_path"])
+ return doc_text
+
+ async def exec_async(self, prep_res):
+ # Example: async LLM call
+ summary = await call_llm_async(f"Summarize: {prep_res}")
+ return summary
+
+ async def post_async(self, shared, prep_res, exec_res):
+ # Example: wait for user feedback
+ decision = await gather_user_feedback(exec_res)
+ if decision == "approve":
+ shared["summary"] = exec_res
+ return "approve"
+ return "deny"
+
+summarize_node = SummarizeThenVerify()
+final_node = Finalize()
+
+# Define transitions
+summarize_node - "approve" >> final_node
+summarize_node - "deny" >> summarize_node # retry
+
+flow = AsyncFlow(start=summarize_node)
+
+async def main():
+ shared = {"doc_path": "document.txt"}
+ await flow.run_async(shared)
+ print("Final Summary:", shared.get("summary"))
+
+asyncio.run(main())
+```
+
+================================================
+File: docs/core_abstraction/batch.md
+================================================
+---
+layout: default
+title: "Batch"
+parent: "Core Abstraction"
+nav_order: 4
+---
+
+# Batch
+
+**Batch** makes it easier to handle large inputs in one Node or **rerun** a Flow multiple times. Example use cases:
+- **Chunk-based** processing (e.g., splitting large texts).
+- **Iterative** processing over lists of input items (e.g., user queries, files, URLs).
+
+## 1. BatchNode
+
+A **BatchNode** extends `Node` but changes `prep()` and `exec()`:
+
+- **`prep(shared)`**: returns an **iterable** (e.g., list, generator).
+- **`exec(item)`**: called **once** per item in that iterable.
+- **`post(shared, prep_res, exec_res_list)`**: after all items are processed, receives a **list** of results (`exec_res_list`) and returns an **Action**.
+
+
+### Example: Summarize a Large File
+
+```python
+class MapSummaries(BatchNode):
+ def prep(self, shared):
+ # Suppose we have a big file; chunk it
+ content = shared["data"]
+ chunk_size = 10000
+ chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
+ return chunks
+
+ def exec(self, chunk):
+ prompt = f"Summarize this chunk in 10 words: {chunk}"
+ summary = call_llm(prompt)
+ return summary
+
+ def post(self, shared, prep_res, exec_res_list):
+ combined = "\n".join(exec_res_list)
+ shared["summary"] = combined
+ return "default"
+
+map_summaries = MapSummaries()
+flow = Flow(start=map_summaries)
+flow.run(shared)
+```
+
+---
+
+## 2. BatchFlow
+
+A **BatchFlow** runs a **Flow** multiple times, each time with different `params`. Think of it as a loop that replays the Flow for each parameter set.
+
+### Example: Summarize Many Files
+
+```python
+class SummarizeAllFiles(BatchFlow):
+ def prep(self, shared):
+ # Return a list of param dicts (one per file)
+ filenames = list(shared["data"].keys()) # e.g., ["file1.txt", "file2.txt", ...]
+ return [{"filename": fn} for fn in filenames]
+
+# Suppose we have a per-file Flow (e.g., load_file >> summarize >> reduce):
+summarize_file = SummarizeFile(start=load_file)
+
+# Wrap that flow into a BatchFlow:
+summarize_all_files = SummarizeAllFiles(start=summarize_file)
+summarize_all_files.run(shared)
+```
+
+### Under the Hood
+1. `prep(shared)` returns a list of param dicts—e.g., `[{filename: "file1.txt"}, {filename: "file2.txt"}, ...]`.
+2. The **BatchFlow** loops through each dict. For each one:
+ - It merges the dict with the BatchFlow’s own `params`.
+ - It calls `flow.run(shared)` using the merged result.
+3. This means the sub-Flow is run **repeatedly**, once for every param dict.
+
+---
+
+## 3. Nested or Multi-Level Batches
+
+You can nest a **BatchFlow** in another **BatchFlow**. For instance:
+- **Outer** batch: returns a list of diretory param dicts (e.g., `{"directory": "/pathA"}`, `{"directory": "/pathB"}`, ...).
+- **Inner** batch: returning a list of per-file param dicts.
+
+At each level, **BatchFlow** merges its own param dict with the parent’s. By the time you reach the **innermost** node, the final `params` is the merged result of **all** parents in the chain. This way, a nested structure can keep track of the entire context (e.g., directory + file name) at once.
+
+```python
+
+class FileBatchFlow(BatchFlow):
+ def prep(self, shared):
+ directory = self.params["directory"]
+ # e.g., files = ["file1.txt", "file2.txt", ...]
+ files = [f for f in os.listdir(directory) if f.endswith(".txt")]
+ return [{"filename": f} for f in files]
+
+class DirectoryBatchFlow(BatchFlow):
+ def prep(self, shared):
+ directories = [ "/path/to/dirA", "/path/to/dirB"]
+ return [{"directory": d} for d in directories]
+
+# MapSummaries have params like {"directory": "/path/to/dirA", "filename": "file1.txt"}
+inner_flow = FileBatchFlow(start=MapSummaries())
+outer_flow = DirectoryBatchFlow(start=inner_flow)
+```
+
+================================================
+File: docs/core_abstraction/communication.md
+================================================
+---
+layout: default
+title: "Communication"
+parent: "Core Abstraction"
+nav_order: 3
+---
+
+# Communication
+
+Nodes and Flows **communicate** in 2 ways:
+
+1. **Shared Store (for almost all the cases)**
+
+ - A global data structure (often an in-mem dict) that all nodes can read ( `prep()`) and write (`post()`).
+ - Great for data results, large content, or anything multiple nodes need.
+ - You shall design the data structure and populate it ahead.
+
+ - > **Separation of Concerns:** Use `Shared Store` for almost all cases to separate *Data Schema* from *Compute Logic*! This approach is both flexible and easy to manage, resulting in more maintainable code. `Params` is more a syntax sugar for [Batch](./batch.md).
+ {: .best-practice }
+
+2. **Params (only for [Batch](./batch.md))**
+ - Each node has a local, ephemeral `params` dict passed in by the **parent Flow**, used as an identifier for tasks. Parameter keys and values shall be **immutable**.
+ - Good for identifiers like filenames or numeric IDs, in Batch mode.
+
+If you know memory management, think of the **Shared Store** like a **heap** (shared by all function calls), and **Params** like a **stack** (assigned by the caller).
+
+---
+
+## 1. Shared Store
+
+### Overview
+
+A shared store is typically an in-mem dictionary, like:
+```python
+shared = {"data": {}, "summary": {}, "config": {...}, ...}
+```
+
+It can also contain local file handlers, DB connections, or a combination for persistence. We recommend deciding the data structure or DB schema first based on your app requirements.
+
+### Example
+
+```python
+class LoadData(Node):
+ def post(self, shared, prep_res, exec_res):
+ # We write data to shared store
+ shared["data"] = "Some text content"
+ return None
+
+class Summarize(Node):
+ def prep(self, shared):
+ # We read data from shared store
+ return shared["data"]
+
+ def exec(self, prep_res):
+ # Call LLM to summarize
+ prompt = f"Summarize: {prep_res}"
+ summary = call_llm(prompt)
+ return summary
+
+ def post(self, shared, prep_res, exec_res):
+ # We write summary to shared store
+ shared["summary"] = exec_res
+ return "default"
+
+load_data = LoadData()
+summarize = Summarize()
+load_data >> summarize
+flow = Flow(start=load_data)
+
+shared = {}
+flow.run(shared)
+```
+
+Here:
+- `LoadData` writes to `shared["data"]`.
+- `Summarize` reads from `shared["data"]`, summarizes, and writes to `shared["summary"]`.
+
+---
+
+## 2. Params
+
+**Params** let you store *per-Node* or *per-Flow* config that doesn't need to live in the shared store. They are:
+- **Immutable** during a Node's run cycle (i.e., they don't change mid-`prep->exec->post`).
+- **Set** via `set_params()`.
+- **Cleared** and updated each time a parent Flow calls it.
+
+> Only set the uppermost Flow params because others will be overwritten by the parent Flow.
+>
+> If you need to set child node params, see [Batch](./batch.md).
+{: .warning }
+
+Typically, **Params** are identifiers (e.g., file name, page number). Use them to fetch the task you assigned or write to a specific part of the shared store.
+
+### Example
+
+```python
+# 1) Create a Node that uses params
+class SummarizeFile(Node):
+ def prep(self, shared):
+ # Access the node's param
+ filename = self.params["filename"]
+ return shared["data"].get(filename, "")
+
+ def exec(self, prep_res):
+ prompt = f"Summarize: {prep_res}"
+ return call_llm(prompt)
+
+ def post(self, shared, prep_res, exec_res):
+ filename = self.params["filename"]
+ shared["summary"][filename] = exec_res
+ return "default"
+
+# 2) Set params
+node = SummarizeFile()
+
+# 3) Set Node params directly (for testing)
+node.set_params({"filename": "doc1.txt"})
+node.run(shared)
+
+# 4) Create Flow
+flow = Flow(start=node)
+
+# 5) Set Flow params (overwrites node params)
+flow.set_params({"filename": "doc2.txt"})
+flow.run(shared) # The node summarizes doc2, not doc1
+```
+
+================================================
+File: docs/core_abstraction/flow.md
+================================================
+---
+layout: default
+title: "Flow"
+parent: "Core Abstraction"
+nav_order: 2
+---
+
+# Flow
+
+A **Flow** orchestrates a graph of Nodes. You can chain Nodes in a sequence or create branching depending on the **Actions** returned from each Node's `post()`.
+
+## 1. Action-based Transitions
+
+Each Node's `post()` returns an **Action** string. By default, if `post()` doesn't return anything, we treat that as `"default"`.
+
+You define transitions with the syntax:
+
+1. **Basic default transition**: `node_a >> node_b`
+ This means if `node_a.post()` returns `"default"`, go to `node_b`.
+ (Equivalent to `node_a - "default" >> node_b`)
+
+2. **Named action transition**: `node_a - "action_name" >> node_b`
+ This means if `node_a.post()` returns `"action_name"`, go to `node_b`.
+
+It's possible to create loops, branching, or multi-step flows.
+
+## 2. Creating a Flow
+
+A **Flow** begins with a **start** node. You call `Flow(start=some_node)` to specify the entry point. When you call `flow.run(shared)`, it executes the start node, looks at its returned Action from `post()`, follows the transition, and continues until there's no next node.
+
+### Example: Simple Sequence
+
+Here's a minimal flow of two nodes in a chain:
+
+```python
+node_a >> node_b
+flow = Flow(start=node_a)
+flow.run(shared)
+```
+
+- When you run the flow, it executes `node_a`.
+- Suppose `node_a.post()` returns `"default"`.
+- The flow then sees `"default"` Action is linked to `node_b` and runs `node_b`.
+- `node_b.post()` returns `"default"` but we didn't define `node_b >> something_else`. So the flow ends there.
+
+### Example: Branching & Looping
+
+Here's a simple expense approval flow that demonstrates branching and looping. The `ReviewExpense` node can return three possible Actions:
+
+- `"approved"`: expense is approved, move to payment processing
+- `"needs_revision"`: expense needs changes, send back for revision
+- `"rejected"`: expense is denied, finish the process
+
+We can wire them like this:
+
+```python
+# Define the flow connections
+review - "approved" >> payment # If approved, process payment
+review - "needs_revision" >> revise # If needs changes, go to revision
+review - "rejected" >> finish # If rejected, finish the process
+
+revise >> review # After revision, go back for another review
+payment >> finish # After payment, finish the process
+
+flow = Flow(start=review)
+```
+
+Let's see how it flows:
+
+1. If `review.post()` returns `"approved"`, the expense moves to the `payment` node
+2. If `review.post()` returns `"needs_revision"`, it goes to the `revise` node, which then loops back to `review`
+3. If `review.post()` returns `"rejected"`, it moves to the `finish` node and stops
+
+```mermaid
+flowchart TD
+ review[Review Expense] -->|approved| payment[Process Payment]
+ review -->|needs_revision| revise[Revise Report]
+ review -->|rejected| finish[Finish Process]
+
+ revise --> review
+ payment --> finish
+```
+
+### Running Individual Nodes vs. Running a Flow
+
+- `node.run(shared)`: Just runs that node alone (calls `prep->exec->post()`), returns an Action.
+- `flow.run(shared)`: Executes from the start node, follows Actions to the next node, and so on until the flow can't continue.
+
+> `node.run(shared)` **does not** proceed to the successor.
+> This is mainly for debugging or testing a single node.
+>
+> Always use `flow.run(...)` in production to ensure the full pipeline runs correctly.
+{: .warning }
+
+## 3. Nested Flows
+
+A **Flow** can act like a Node, which enables powerful composition patterns. This means you can:
+
+1. Use a Flow as a Node within another Flow's transitions.
+2. Combine multiple smaller Flows into a larger Flow for reuse.
+3. Node `params` will be a merging of **all** parents' `params`.
+
+### Flow's Node Methods
+
+A **Flow** is also a **Node**, so it will run `prep()` and `post()`. However:
+
+- It **won't** run `exec()`, as its main logic is to orchestrate its nodes.
+- `post()` always receives `None` for `exec_res` and should instead get the flow execution results from the shared store.
+
+### Basic Flow Nesting
+
+Here's how to connect a flow to another node:
+
+```python
+# Create a sub-flow
+node_a >> node_b
+subflow = Flow(start=node_a)
+
+# Connect it to another node
+subflow >> node_c
+
+# Create the parent flow
+parent_flow = Flow(start=subflow)
+```
+
+When `parent_flow.run()` executes:
+1. It starts `subflow`
+2. `subflow` runs through its nodes (`node_a->node_b`)
+3. After `subflow` completes, execution continues to `node_c`
+
+### Example: Order Processing Pipeline
+
+Here's a practical example that breaks down order processing into nested flows:
+
+```python
+# Payment processing sub-flow
+validate_payment >> process_payment >> payment_confirmation
+payment_flow = Flow(start=validate_payment)
+
+# Inventory sub-flow
+check_stock >> reserve_items >> update_inventory
+inventory_flow = Flow(start=check_stock)
+
+# Shipping sub-flow
+create_label >> assign_carrier >> schedule_pickup
+shipping_flow = Flow(start=create_label)
+
+# Connect the flows into a main order pipeline
+payment_flow >> inventory_flow >> shipping_flow
+
+# Create the master flow
+order_pipeline = Flow(start=payment_flow)
+
+# Run the entire pipeline
+order_pipeline.run(shared_data)
+```
+
+This creates a clean separation of concerns while maintaining a clear execution path:
+
+```mermaid
+flowchart LR
+ subgraph order_pipeline[Order Pipeline]
+ subgraph paymentFlow["Payment Flow"]
+ A[Validate Payment] --> B[Process Payment] --> C[Payment Confirmation]
+ end
+
+ subgraph inventoryFlow["Inventory Flow"]
+ D[Check Stock] --> E[Reserve Items] --> F[Update Inventory]
+ end
+
+ subgraph shippingFlow["Shipping Flow"]
+ G[Create Label] --> H[Assign Carrier] --> I[Schedule Pickup]
+ end
+
+ paymentFlow --> inventoryFlow
+ inventoryFlow --> shippingFlow
+ end
+```
+
+================================================
+File: docs/core_abstraction/node.md
+================================================
+---
+layout: default
+title: "Node"
+parent: "Core Abstraction"
+nav_order: 1
+---
+
+# Node
+
+A **Node** is the smallest building block. Each Node has 3 steps `prep->exec->post`:
+
+
+

+
+
+1. `prep(shared)`
+ - **Read and preprocess data** from `shared` store.
+ - Examples: *query DB, read files, or serialize data into a string*.
+ - Return `prep_res`, which is used by `exec()` and `post()`.
+
+2. `exec(prep_res)`
+ - **Execute compute logic**, with optional retries and error handling (below).
+ - Examples: *(mostly) LLM calls, remote APIs, tool use*.
+ - ⚠️ This shall be only for compute and **NOT** access `shared`.
+ - ⚠️ If retries enabled, ensure idempotent implementation.
+ - Return `exec_res`, which is passed to `post()`.
+
+3. `post(shared, prep_res, exec_res)`
+ - **Postprocess and write data** back to `shared`.
+ - Examples: *update DB, change states, log results*.
+ - **Decide the next action** by returning a *string* (`action = "default"` if *None*).
+
+> **Why 3 steps?** To enforce the principle of *separation of concerns*. The data storage and data processing are operated separately.
+>
+> All steps are *optional*. E.g., you can only implement `prep` and `post` if you just need to process data.
+{: .note }
+
+### Fault Tolerance & Retries
+
+You can **retry** `exec()` if it raises an exception via two parameters when define the Node:
+
+- `max_retries` (int): Max times to run `exec()`. The default is `1` (**no** retry).
+- `wait` (int): The time to wait (in **seconds**) before next retry. By default, `wait=0` (no waiting).
+`wait` is helpful when you encounter rate-limits or quota errors from your LLM provider and need to back off.
+
+```python
+my_node = SummarizeFile(max_retries=3, wait=10)
+```
+
+When an exception occurs in `exec()`, the Node automatically retries until:
+
+- It either succeeds, or
+- The Node has retried `max_retries - 1` times already and fails on the last attempt.
+
+You can get the current retry times (0-based) from `self.cur_retry`.
+
+```python
+class RetryNode(Node):
+ def exec(self, prep_res):
+ print(f"Retry {self.cur_retry} times")
+ raise Exception("Failed")
+```
+
+### Graceful Fallback
+
+To **gracefully handle** the exception (after all retries) rather than raising it, override:
+
+```python
+def exec_fallback(self, prep_res, exc):
+ raise exc
+```
+
+By default, it just re-raises exception. But you can return a fallback result instead, which becomes the `exec_res` passed to `post()`.
+
+### Example: Summarize file
+
+```python
+class SummarizeFile(Node):
+ def prep(self, shared):
+ return shared["data"]
+
+ def exec(self, prep_res):
+ if not prep_res:
+ return "Empty file content"
+ prompt = f"Summarize this text in 10 words: {prep_res}"
+ summary = call_llm(prompt) # might fail
+ return summary
+
+ def exec_fallback(self, prep_res, exc):
+ # Provide a simple fallback instead of crashing
+ return "There was an error processing your request."
+
+ def post(self, shared, prep_res, exec_res):
+ shared["summary"] = exec_res
+ # Return "default" by not returning
+
+summarize_node = SummarizeFile(max_retries=3)
+
+# node.run() calls prep->exec->post
+# If exec() fails, it retries up to 3 times before calling exec_fallback()
+action_result = summarize_node.run(shared)
+
+print("Action returned:", action_result) # "default"
+print("Summary stored:", shared["summary"])
+```
+
+
+================================================
+File: docs/core_abstraction/parallel.md
+================================================
+---
+layout: default
+title: "(Advanced) Parallel"
+parent: "Core Abstraction"
+nav_order: 6
+---
+
+# (Advanced) Parallel
+
+**Parallel** Nodes and Flows let you run multiple **Async** Nodes and Flows **concurrently**—for example, summarizing multiple texts at once. This can improve performance by overlapping I/O and compute.
+
+> Because of Python’s GIL, parallel nodes and flows can’t truly parallelize CPU-bound tasks (e.g., heavy numerical computations). However, they excel at overlapping I/O-bound work—like LLM calls, database queries, API requests, or file I/O.
+{: .warning }
+
+> - **Ensure Tasks Are Independent**: If each item depends on the output of a previous item, **do not** parallelize.
+>
+> - **Beware of Rate Limits**: Parallel calls can **quickly** trigger rate limits on LLM services. You may need a **throttling** mechanism (e.g., semaphores or sleep intervals).
+>
+> - **Consider Single-Node Batch APIs**: Some LLMs offer a **batch inference** API where you can send multiple prompts in a single call. This is more complex to implement but can be more efficient than launching many parallel requests and mitigates rate limits.
+{: .best-practice }
+
+## AsyncParallelBatchNode
+
+Like **AsyncBatchNode**, but run `exec_async()` in **parallel**:
+
+```python
+class ParallelSummaries(AsyncParallelBatchNode):
+ async def prep_async(self, shared):
+ # e.g., multiple texts
+ return shared["texts"]
+
+ async def exec_async(self, text):
+ prompt = f"Summarize: {text}"
+ return await call_llm_async(prompt)
+
+ async def post_async(self, shared, prep_res, exec_res_list):
+ shared["summary"] = "\n\n".join(exec_res_list)
+ return "default"
+
+node = ParallelSummaries()
+flow = AsyncFlow(start=node)
+```
+
+## AsyncParallelBatchFlow
+
+Parallel version of **BatchFlow**. Each iteration of the sub-flow runs **concurrently** using different parameters:
+
+```python
+class SummarizeMultipleFiles(AsyncParallelBatchFlow):
+ async def prep_async(self, shared):
+ return [{"filename": f} for f in shared["files"]]
+
+sub_flow = AsyncFlow(start=LoadAndSummarizeFile())
+parallel_flow = SummarizeMultipleFiles(start=sub_flow)
+await parallel_flow.run_async(shared)
+```
+
+================================================
+File: docs/design_pattern/agent.md
+================================================
+---
+layout: default
+title: "Agent"
+parent: "Design Pattern"
+nav_order: 1
+---
+
+# Agent
+
+Agent is a powerful design pattern in which nodes can take dynamic actions based on the context.
+
+
+

+
+
+## Implement Agent with Graph
+
+1. **Context and Action:** Implement nodes that supply context and perform actions.
+2. **Branching:** Use branching to connect each action node to an agent node. Use action to allow the agent to direct the [flow](../core_abstraction/flow.md) between nodes—and potentially loop back for multi-step.
+3. **Agent Node:** Provide a prompt to decide action—for example:
+
+```python
+f"""
+### CONTEXT
+Task: {task_description}
+Previous Actions: {previous_actions}
+Current State: {current_state}
+
+### ACTION SPACE
+[1] search
+ Description: Use web search to get results
+ Parameters:
+ - query (str): What to search for
+
+[2] answer
+ Description: Conclude based on the results
+ Parameters:
+ - result (str): Final answer to provide
+
+### NEXT ACTION
+Decide the next action based on the current context and available action space.
+Return your response in the following format:
+
+```yaml
+thinking: |
+
+action:
+parameters:
+ :
+```"""
+```
+
+The core of building **high-performance** and **reliable** agents boils down to:
+
+1. **Context Management:** Provide *relevant, minimal context.* For example, rather than including an entire chat history, retrieve the most relevant via [RAG](./rag.md). Even with larger context windows, LLMs still fall victim to ["lost in the middle"](https://arxiv.org/abs/2307.03172), overlooking mid-prompt content.
+
+2. **Action Space:** Provide *a well-structured and unambiguous* set of actions—avoiding overlap like separate `read_databases` or `read_csvs`. Instead, import CSVs into the database.
+
+## Example Good Action Design
+
+- **Incremental:** Feed content in manageable chunks (500 lines or 1 page) instead of all at once.
+
+- **Overview-zoom-in:** First provide high-level structure (table of contents, summary), then allow drilling into details (raw texts).
+
+- **Parameterized/Programmable:** Instead of fixed actions, enable parameterized (columns to select) or programmable (SQL queries) actions, for example, to read CSV files.
+
+- **Backtracking:** Let the agent undo the last step instead of restarting entirely, preserving progress when encountering errors or dead ends.
+
+## Example: Search Agent
+
+This agent:
+1. Decides whether to search or answer
+2. If searches, loops back to decide if more search needed
+3. Answers when enough context gathered
+
+```python
+class DecideAction(Node):
+ def prep(self, shared):
+ context = shared.get("context", "No previous search")
+ query = shared["query"]
+ return query, context
+
+ def exec(self, inputs):
+ query, context = inputs
+ prompt = f"""
+Given input: {query}
+Previous search results: {context}
+Should I: 1) Search web for more info 2) Answer with current knowledge
+Output in yaml:
+```yaml
+action: search/answer
+reason: why this action
+search_term: search phrase if action is search
+```"""
+ resp = call_llm(prompt)
+ yaml_str = resp.split("```yaml")[1].split("```")[0].strip()
+ result = yaml.safe_load(yaml_str)
+
+ assert isinstance(result, dict)
+ assert "action" in result
+ assert "reason" in result
+ assert result["action"] in ["search", "answer"]
+ if result["action"] == "search":
+ assert "search_term" in result
+
+ return result
+
+ def post(self, shared, prep_res, exec_res):
+ if exec_res["action"] == "search":
+ shared["search_term"] = exec_res["search_term"]
+ return exec_res["action"]
+
+class SearchWeb(Node):
+ def prep(self, shared):
+ return shared["search_term"]
+
+ def exec(self, search_term):
+ return search_web(search_term)
+
+ def post(self, shared, prep_res, exec_res):
+ prev_searches = shared.get("context", [])
+ shared["context"] = prev_searches + [
+ {"term": shared["search_term"], "result": exec_res}
+ ]
+ return "decide"
+
+class DirectAnswer(Node):
+ def prep(self, shared):
+ return shared["query"], shared.get("context", "")
+
+ def exec(self, inputs):
+ query, context = inputs
+ return call_llm(f"Context: {context}\nAnswer: {query}")
+
+ def post(self, shared, prep_res, exec_res):
+ print(f"Answer: {exec_res}")
+ shared["answer"] = exec_res
+
+# Connect nodes
+decide = DecideAction()
+search = SearchWeb()
+answer = DirectAnswer()
+
+decide - "search" >> search
+decide - "answer" >> answer
+search - "decide" >> decide # Loop back
+
+flow = Flow(start=decide)
+flow.run({"query": "Who won the Nobel Prize in Physics 2024?"})
+```
+
+================================================
+File: docs/design_pattern/mapreduce.md
+================================================
+---
+layout: default
+title: "Map Reduce"
+parent: "Design Pattern"
+nav_order: 4
+---
+
+# Map Reduce
+
+MapReduce is a design pattern suitable when you have either:
+- Large input data (e.g., multiple files to process), or
+- Large output data (e.g., multiple forms to fill)
+
+and there is a logical way to break the task into smaller, ideally independent parts.
+
+
+

+
+
+You first break down the task using [BatchNode](../core_abstraction/batch.md) in the map phase, followed by aggregation in the reduce phase.
+
+### Example: Document Summarization
+
+```python
+class SummarizeAllFiles(BatchNode):
+ def prep(self, shared):
+ files_dict = shared["files"] # e.g. 10 files
+ return list(files_dict.items()) # [("file1.txt", "aaa..."), ("file2.txt", "bbb..."), ...]
+
+ def exec(self, one_file):
+ filename, file_content = one_file
+ summary_text = call_llm(f"Summarize the following file:\n{file_content}")
+ return (filename, summary_text)
+
+ def post(self, shared, prep_res, exec_res_list):
+ shared["file_summaries"] = dict(exec_res_list)
+
+class CombineSummaries(Node):
+ def prep(self, shared):
+ return shared["file_summaries"]
+
+ def exec(self, file_summaries):
+ # format as: "File1: summary\nFile2: summary...\n"
+ text_list = []
+ for fname, summ in file_summaries.items():
+ text_list.append(f"{fname} summary:\n{summ}\n")
+ big_text = "\n---\n".join(text_list)
+
+ return call_llm(f"Combine these file summaries into one final summary:\n{big_text}")
+
+ def post(self, shared, prep_res, final_summary):
+ shared["all_files_summary"] = final_summary
+
+batch_node = SummarizeAllFiles()
+combine_node = CombineSummaries()
+batch_node >> combine_node
+
+flow = Flow(start=batch_node)
+
+shared = {
+ "files": {
+ "file1.txt": "Alice was beginning to get very tired of sitting by her sister...",
+ "file2.txt": "Some other interesting text ...",
+ # ...
+ }
+}
+flow.run(shared)
+print("Individual Summaries:", shared["file_summaries"])
+print("\nFinal Summary:\n", shared["all_files_summary"])
+```
+
+================================================
+File: docs/design_pattern/rag.md
+================================================
+---
+layout: default
+title: "RAG"
+parent: "Design Pattern"
+nav_order: 3
+---
+
+# RAG (Retrieval Augmented Generation)
+
+For certain LLM tasks like answering questions, providing relevant context is essential. One common architecture is a **two-stage** RAG pipeline:
+
+
+

+
+
+1. **Offline stage**: Preprocess and index documents ("building the index").
+2. **Online stage**: Given a question, generate answers by retrieving the most relevant context.
+
+---
+## Stage 1: Offline Indexing
+
+We create three Nodes:
+1. `ChunkDocs` – [chunks](../utility_function/chunking.md) raw text.
+2. `EmbedDocs` – [embeds](../utility_function/embedding.md) each chunk.
+3. `StoreIndex` – stores embeddings into a [vector database](../utility_function/vector.md).
+
+```python
+class ChunkDocs(BatchNode):
+ def prep(self, shared):
+ # A list of file paths in shared["files"]. We process each file.
+ return shared["files"]
+
+ def exec(self, filepath):
+ # read file content. In real usage, do error handling.
+ with open(filepath, "r", encoding="utf-8") as f:
+ text = f.read()
+ # chunk by 100 chars each
+ chunks = []
+ size = 100
+ for i in range(0, len(text), size):
+ chunks.append(text[i : i + size])
+ return chunks
+
+ def post(self, shared, prep_res, exec_res_list):
+ # exec_res_list is a list of chunk-lists, one per file.
+ # flatten them all into a single list of chunks.
+ all_chunks = []
+ for chunk_list in exec_res_list:
+ all_chunks.extend(chunk_list)
+ shared["all_chunks"] = all_chunks
+
+class EmbedDocs(BatchNode):
+ def prep(self, shared):
+ return shared["all_chunks"]
+
+ def exec(self, chunk):
+ return get_embedding(chunk)
+
+ def post(self, shared, prep_res, exec_res_list):
+ # Store the list of embeddings.
+ shared["all_embeds"] = exec_res_list
+ print(f"Total embeddings: {len(exec_res_list)}")
+
+class StoreIndex(Node):
+ def prep(self, shared):
+ # We'll read all embeds from shared.
+ return shared["all_embeds"]
+
+ def exec(self, all_embeds):
+ # Create a vector index (faiss or other DB in real usage).
+ index = create_index(all_embeds)
+ return index
+
+ def post(self, shared, prep_res, index):
+ shared["index"] = index
+
+# Wire them in sequence
+chunk_node = ChunkDocs()
+embed_node = EmbedDocs()
+store_node = StoreIndex()
+
+chunk_node >> embed_node >> store_node
+
+OfflineFlow = Flow(start=chunk_node)
+```
+
+Usage example:
+
+```python
+shared = {
+ "files": ["doc1.txt", "doc2.txt"], # any text files
+}
+OfflineFlow.run(shared)
+```
+
+---
+## Stage 2: Online Query & Answer
+
+We have 3 nodes:
+1. `EmbedQuery` – embeds the user’s question.
+2. `RetrieveDocs` – retrieves top chunk from the index.
+3. `GenerateAnswer` – calls the LLM with the question + chunk to produce the final answer.
+
+```python
+class EmbedQuery(Node):
+ def prep(self, shared):
+ return shared["question"]
+
+ def exec(self, question):
+ return get_embedding(question)
+
+ def post(self, shared, prep_res, q_emb):
+ shared["q_emb"] = q_emb
+
+class RetrieveDocs(Node):
+ def prep(self, shared):
+ # We'll need the query embedding, plus the offline index/chunks
+ return shared["q_emb"], shared["index"], shared["all_chunks"]
+
+ def exec(self, inputs):
+ q_emb, index, chunks = inputs
+ I, D = search_index(index, q_emb, top_k=1)
+ best_id = I[0][0]
+ relevant_chunk = chunks[best_id]
+ return relevant_chunk
+
+ def post(self, shared, prep_res, relevant_chunk):
+ shared["retrieved_chunk"] = relevant_chunk
+ print("Retrieved chunk:", relevant_chunk[:60], "...")
+
+class GenerateAnswer(Node):
+ def prep(self, shared):
+ return shared["question"], shared["retrieved_chunk"]
+
+ def exec(self, inputs):
+ question, chunk = inputs
+ prompt = f"Question: {question}\nContext: {chunk}\nAnswer:"
+ return call_llm(prompt)
+
+ def post(self, shared, prep_res, answer):
+ shared["answer"] = answer
+ print("Answer:", answer)
+
+embed_qnode = EmbedQuery()
+retrieve_node = RetrieveDocs()
+generate_node = GenerateAnswer()
+
+embed_qnode >> retrieve_node >> generate_node
+OnlineFlow = Flow(start=embed_qnode)
+```
+
+Usage example:
+
+```python
+# Suppose we already ran OfflineFlow and have:
+# shared["all_chunks"], shared["index"], etc.
+shared["question"] = "Why do people like cats?"
+
+OnlineFlow.run(shared)
+# final answer in shared["answer"]
+```
+
+================================================
+File: docs/design_pattern/structure.md
+================================================
+---
+layout: default
+title: "Structured Output"
+parent: "Design Pattern"
+nav_order: 5
+---
+
+# Structured Output
+
+In many use cases, you may want the LLM to output a specific structure, such as a list or a dictionary with predefined keys.
+
+There are several approaches to achieve a structured output:
+- **Prompting** the LLM to strictly return a defined structure.
+- Using LLMs that natively support **schema enforcement**.
+- **Post-processing** the LLM's response to extract structured content.
+
+In practice, **Prompting** is simple and reliable for modern LLMs.
+
+### Example Use Cases
+
+- Extracting Key Information
+
+```yaml
+product:
+ name: Widget Pro
+ price: 199.99
+ description: |
+ A high-quality widget designed for professionals.
+ Recommended for advanced users.
+```
+
+- Summarizing Documents into Bullet Points
+
+```yaml
+summary:
+ - This product is easy to use.
+ - It is cost-effective.
+ - Suitable for all skill levels.
+```
+
+- Generating Configuration Files
+
+```yaml
+server:
+ host: 127.0.0.1
+ port: 8080
+ ssl: true
+```
+
+## Prompt Engineering
+
+When prompting the LLM to produce **structured** output:
+1. **Wrap** the structure in code fences (e.g., `yaml`).
+2. **Validate** that all required fields exist (and let `Node` handles retry).
+
+### Example Text Summarization
+
+```python
+class SummarizeNode(Node):
+ def exec(self, prep_res):
+ # Suppose `prep_res` is the text to summarize.
+ prompt = f"""
+Please summarize the following text as YAML, with exactly 3 bullet points
+
+{prep_res}
+
+Now, output:
+```yaml
+summary:
+ - bullet 1
+ - bullet 2
+ - bullet 3
+```"""
+ response = call_llm(prompt)
+ yaml_str = response.split("```yaml")[1].split("```")[0].strip()
+
+ import yaml
+ structured_result = yaml.safe_load(yaml_str)
+
+ assert "summary" in structured_result
+ assert isinstance(structured_result["summary"], list)
+
+ return structured_result
+```
+
+> Besides using `assert` statements, another popular way to validate schemas is [Pydantic](https://github.com/pydantic/pydantic)
+{: .note }
+
+### Why YAML instead of JSON?
+
+Current LLMs struggle with escaping. YAML is easier with strings since they don't always need quotes.
+
+**In JSON**
+
+```json
+{
+ "dialogue": "Alice said: \"Hello Bob.\\nHow are you?\\nI am good.\""
+}
+```
+
+- Every double quote inside the string must be escaped with `\"`.
+- Each newline in the dialogue must be represented as `\n`.
+
+**In YAML**
+
+```yaml
+dialogue: |
+ Alice said: "Hello Bob.
+ How are you?
+ I am good."
+```
+
+- No need to escape interior quotes—just place the entire text under a block literal (`|`).
+- Newlines are naturally preserved without needing `\n`.
+
+================================================
+File: docs/design_pattern/workflow.md
+================================================
+---
+layout: default
+title: "Workflow"
+parent: "Design Pattern"
+nav_order: 2
+---
+
+# Workflow
+
+Many real-world tasks are too complex for one LLM call. The solution is to **Task Decomposition**: decompose them into a [chain](../core_abstraction/flow.md) of multiple Nodes.
+
+
+

+
+
+> - You don't want to make each task **too coarse**, because it may be *too complex for one LLM call*.
+> - You don't want to make each task **too granular**, because then *the LLM call doesn't have enough context* and results are *not consistent across nodes*.
+>
+> You usually need multiple *iterations* to find the *sweet spot*. If the task has too many *edge cases*, consider using [Agents](./agent.md).
+{: .best-practice }
+
+### Example: Article Writing
+
+```python
+class GenerateOutline(Node):
+ def prep(self, shared): return shared["topic"]
+ def exec(self, topic): return call_llm(f"Create a detailed outline for an article about {topic}")
+ def post(self, shared, prep_res, exec_res): shared["outline"] = exec_res
+
+class WriteSection(Node):
+ def prep(self, shared): return shared["outline"]
+ def exec(self, outline): return call_llm(f"Write content based on this outline: {outline}")
+ def post(self, shared, prep_res, exec_res): shared["draft"] = exec_res
+
+class ReviewAndRefine(Node):
+ def prep(self, shared): return shared["draft"]
+ def exec(self, draft): return call_llm(f"Review and improve this draft: {draft}")
+ def post(self, shared, prep_res, exec_res): shared["final_article"] = exec_res
+
+# Connect nodes
+outline = GenerateOutline()
+write = WriteSection()
+review = ReviewAndRefine()
+
+outline >> write >> review
+
+# Create and run flow
+writing_flow = Flow(start=outline)
+shared = {"topic": "AI Safety"}
+writing_flow.run(shared)
+```
+
+For *dynamic cases*, consider using [Agents](./agent.md).
+
+================================================
+File: docs/utility_function/llm.md
+================================================
+---
+layout: default
+title: "LLM Wrapper"
+parent: "Utility Function"
+nav_order: 1
+---
+
+# LLM Wrappers
+
+Check out libraries like [litellm](https://github.com/BerriAI/litellm).
+Here, we provide some minimal example implementations:
+
+1. OpenAI
+ ```python
+ def call_llm(prompt):
+ from openai import OpenAI
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+
+ # Example usage
+ call_llm("How are you?")
+ ```
+ > Store the API key in an environment variable like OPENAI_API_KEY for security.
+ {: .best-practice }
+
+2. Claude (Anthropic)
+ ```python
+ def call_llm(prompt):
+ from anthropic import Anthropic
+ client = Anthropic(api_key="YOUR_API_KEY_HERE")
+ response = client.messages.create(
+ model="claude-2",
+ messages=[{"role": "user", "content": prompt}],
+ max_tokens=100
+ )
+ return response.content
+ ```
+
+3. Google (Generative AI Studio / PaLM API)
+ ```python
+ def call_llm(prompt):
+ import google.generativeai as genai
+ genai.configure(api_key="YOUR_API_KEY_HERE")
+ response = genai.generate_text(
+ model="models/text-bison-001",
+ prompt=prompt
+ )
+ return response.result
+ ```
+
+4. Azure (Azure OpenAI)
+ ```python
+ def call_llm(prompt):
+ from openai import AzureOpenAI
+ client = AzureOpenAI(
+ azure_endpoint="https://.openai.azure.com/",
+ api_key="YOUR_API_KEY_HERE",
+ api_version="2023-05-15"
+ )
+ r = client.chat.completions.create(
+ model="",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return r.choices[0].message.content
+ ```
+
+5. Ollama (Local LLM)
+ ```python
+ def call_llm(prompt):
+ from ollama import chat
+ response = chat(
+ model="llama2",
+ messages=[{"role": "user", "content": prompt}]
+ )
+ return response.message.content
+ ```
+
+## Improvements
+Feel free to enhance your `call_llm` function as needed. Here are examples:
+
+- Handle chat history:
+
+```python
+def call_llm(messages):
+ from openai import OpenAI
+ client = OpenAI(api_key="YOUR_API_KEY_HERE")
+ r = client.chat.completions.create(
+ model="gpt-4o",
+ messages=messages
+ )
+ return r.choices[0].message.content
+```
+
+- Add in-memory caching
+
+```python
+from functools import lru_cache
+
+@lru_cache(maxsize=1000)
+def call_llm(prompt):
+ # Your implementation here
+ pass
+```
+
+> ⚠️ Caching conflicts with Node retries, as retries yield the same result.
+>
+> To address this, you could use cached results only if not retried.
+{: .warning }
+
+
+```python
+from functools import lru_cache
+
+@lru_cache(maxsize=1000)
+def cached_call(prompt):
+ pass
+
+def call_llm(prompt, use_cache):
+ if use_cache:
+ return cached_call(prompt)
+ # Call the underlying function directly
+ return cached_call.__wrapped__(prompt)
+
+class SummarizeNode(Node):
+ def exec(self, text):
+ return call_llm(f"Summarize: {text}", self.cur_retry==0)
+```
+
+- Enable logging:
+
+```python
+def call_llm(prompt):
+ import logging
+ logging.info(f"Prompt: {prompt}")
+ response = ... # Your implementation here
+ logging.info(f"Response: {response}")
+ return response
+```
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..02af832
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+Agentic Coding - Project Template
+
+
+
+
+
+
+
+
+This is a project template for Agentic Coding with [Pocket Flow](https://github.com/The-Pocket/PocketFlow), a 100-line LLM framework, and Cursor.
+
+- We have included the [.cursorrules](.cursorrules) file to let Cursor AI help you build LLM projects.
+
+- Want to learn how to build LLM projects with Agentic Coding?
+
+ - Check out the [Agentic Coding Guidance](https://the-pocket.github.io/PocketFlow/guide.html)
+
+ - Check out the [YouTube Tutorial](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1)
diff --git a/assets/banner.png b/assets/banner.png
new file mode 100644
index 0000000000000000000000000000000000000000..09f0f0437c14c0300523e140570de4069198ff2c
GIT binary patch
literal 4132203
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