├── flow.php
├── main.php
├── nodes.php
├── .gitignore
├── composer.json
├── README.md
├── LICENSE
├── src
└── PocketFlow.php
├── .clinerules
├── CLAUDE.md
└── GEMINI.md
/flow.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/main.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 |
3 | .env
4 | .env.*
5 | !.env.example
6 |
7 | composer.lock
8 |
9 | .DS_Store
10 |
11 | Thumbs.db
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "weise25/pocketflow-php",
3 | "version": "0.1",
4 | "description": "A PHP port of the minimalist LLM framework PocketFlow.",
5 | "type": "library",
6 | "license": "MIT",
7 | "autoload": {
8 | "psr-4": {
9 | "PocketFlow\\": "src/"
10 | },
11 | "files": [
12 | "src/PocketFlow.php"
13 | ]
14 | },
15 | "require": {
16 | "php": ">=8.3",
17 | "react/async": "^4.0",
18 | "react/promise-timer": "^1.9"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
PocketFlow-PHP Template
2 |
3 |
4 | Lightning-fast kickoff for Agentic Coding with PocketFlow-PHP!
5 |
6 |
7 | ---
8 |
9 | ## 🚀 Quick Start
10 |
11 | 1. **Clone** this repo
12 | ```bash
13 | git clone https://github.com/weise25/pocketflow-php-template.git
14 | cd pocketflow-php-template
15 | ```
16 |
17 | 2. **Open in your favorite AI-powered editor**
18 | - [Cline](https://cline.bot)
19 | - [Cursor](https://cursor.sh)
20 | - [Windsurf](https://windsurf.com)
21 | - [Claude Code](https://claude.ai/code)
22 | - Or any other AI Coding assistant with sufficient context window
23 |
24 | 3. **Ask your AI assistant**
25 | > “Explain PocketFlow-PHP and what kinds of applications you can build with it.”
26 |
27 | The agent will return a concise yet comprehensive summary of PocketFlow-PHP and its capabilities.
28 |
29 | 4. **Start Building!**
30 |
31 | ---
32 |
33 |
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Djamal Weise
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/PocketFlow.php:
--------------------------------------------------------------------------------
1 | on('action')->next($node) syntax
18 | class ConditionalTransition
19 | {
20 | public function __construct(private BaseNode $source, private string $action) {}
21 |
22 | public function next(?BaseNode $target): ?BaseNode
23 | {
24 | return $this->source->next($target, $this->action);
25 | }
26 | }
27 |
28 | // Base class for all Nodes and Flows
29 | abstract class BaseNode
30 | {
31 | public array $params = [];
32 | protected array $successors = [];
33 |
34 | public function setParams(array $params): void
35 | {
36 | $this->params = $params;
37 | }
38 |
39 | public function next(?BaseNode $node, string $action = 'default'): ?BaseNode
40 | {
41 | if (isset($this->successors[$action])) {
42 | trigger_error("Overwriting successor for action '{$action}'", E_USER_WARNING);
43 | }
44 | $this->successors[$action] = $node;
45 | return $node;
46 | }
47 |
48 | public function on(string $action): ConditionalTransition
49 | {
50 | return new ConditionalTransition($this, $action);
51 | }
52 |
53 | public function getSuccessors(): array
54 | {
55 | return $this->successors;
56 | }
57 |
58 | public function prep(stdClass $shared): mixed { return null; }
59 | public function exec(mixed $prepResult): mixed { return null; }
60 | public function post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string { return null; }
61 |
62 | protected function _exec(mixed $prepResult): mixed
63 | {
64 | return $this->exec($prepResult);
65 | }
66 |
67 | protected function _run(stdClass $shared): ?string
68 | {
69 | $prepResult = $this->prep($shared);
70 | $execResult = $this->_exec($prepResult);
71 | return $this->post($shared, $prepResult, $execResult);
72 | }
73 |
74 | public function run(stdClass $shared): ?string
75 | {
76 | if (!empty($this->successors)) {
77 | trigger_error("Node won't run successors. Use a Flow to run the full graph.", E_USER_WARNING);
78 | }
79 | return $this->_run($shared);
80 | }
81 | }
82 |
83 | // A standard node with retry and fallback logic
84 | class Node extends BaseNode
85 | {
86 | public function __construct(public int $maxRetries = 1, public int $wait = 0) {}
87 |
88 | public function execFallback(mixed $prepResult, Throwable $e): mixed
89 | {
90 | throw $e;
91 | }
92 |
93 | protected function _exec(mixed $prepResult): mixed
94 | {
95 | for ($retryCount = 0; $retryCount < $this->maxRetries; $retryCount++) {
96 | try {
97 | return $this->exec($prepResult);
98 | } catch (Throwable $e) {
99 | if ($retryCount === $this->maxRetries - 1) {
100 | return $this->execFallback($prepResult, $e);
101 | }
102 | if ($this->wait > 0) {
103 | sleep($this->wait);
104 | }
105 | }
106 | }
107 | return null;
108 | }
109 | }
110 |
111 | // A node that processes a list of items sequentially
112 | class BatchNode extends Node
113 | {
114 | protected function _exec(mixed $items): mixed
115 | {
116 | $results = [];
117 | foreach ($items ?? [] as $item) {
118 | $results[] = parent::_exec($item);
119 | }
120 | return $results;
121 | }
122 | }
123 |
124 | // Orchestrates a graph of nodes
125 | class Flow extends BaseNode
126 | {
127 | public function __construct(protected ?BaseNode $startNode = null) {}
128 |
129 | public function start(BaseNode $startNode): BaseNode
130 | {
131 | $this->startNode = $startNode;
132 | return $startNode;
133 | }
134 |
135 | protected function getNextNode(?BaseNode $current, ?string $action): ?BaseNode
136 | {
137 | if (!$current) return null;
138 | $actionKey = $action ?? 'default';
139 | $successors = $current->getSuccessors();
140 | if (!array_key_exists($actionKey, $successors)) {
141 | if (!empty($successors)) {
142 | $availableActions = implode("', '", array_keys($successors));
143 | trigger_error("Flow ends: Action '{$actionKey}' not found in available actions: '{$availableActions}'", E_USER_WARNING);
144 | }
145 | return null;
146 | }
147 | return $successors[$actionKey];
148 | }
149 |
150 | protected function _orchestrate(stdClass $shared, ?array $params = null): ?string
151 | {
152 | $current = $this->startNode;
153 | $p = $params ?? $this->params;
154 | $lastAction = null;
155 |
156 | while ($current) {
157 | $current->setParams($p);
158 | if ($current instanceof AsyncRunnable) {
159 | $lastAction = await($current->_run_async($shared));
160 | } else {
161 | $lastAction = $current->_run($shared);
162 | }
163 | $current = $this->getNextNode($current, $lastAction);
164 | }
165 | return $lastAction;
166 | }
167 |
168 | protected function _run(stdClass $shared): ?string
169 | {
170 | $prepResult = $this->prep($shared);
171 | $orchestrationResult = $this->_orchestrate($shared);
172 | return $this->post($shared, $prepResult, $orchestrationResult);
173 | }
174 |
175 | public function post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string
176 | {
177 | return $execResult;
178 | }
179 | }
180 |
181 | // A flow that runs its sub-flow for each item returned by prep()
182 | class BatchFlow extends Flow
183 | {
184 | protected function _run(stdClass $shared): ?string
185 | {
186 | $paramList = $this->prep($shared) ?? [];
187 | foreach ($paramList as $batchParams) {
188 | $this->_orchestrate($shared, array_merge($this->params, $batchParams));
189 | }
190 | return $this->post($shared, $paramList, null);
191 | }
192 | }
193 |
194 | // --- ASYNC IMPLEMENTATIONS ---
195 |
196 | trait AsyncLogicTrait
197 | {
198 | public int $maxRetries = 1;
199 | public int $wait = 0;
200 |
201 | public function prep_async(stdClass $shared): PromiseInterface { return async(fn() => null)(); }
202 | public function exec_async(mixed $prepResult): PromiseInterface { return async(fn() => null)(); }
203 | public function post_async(stdClass $shared, mixed $prepResult, mixed $execResult): PromiseInterface { return async(fn() => null)(); }
204 | public function exec_fallback_async(mixed $prepResult, Throwable $e): PromiseInterface { return async(function() use ($e) { throw $e; })(); }
205 |
206 | public function _exec_async(mixed $prepResult): PromiseInterface
207 | {
208 | return async(function () use ($prepResult) {
209 | for ($retryCount = 0; $retryCount < $this->maxRetries; $retryCount++) {
210 | try {
211 | return await($this->exec_async($prepResult));
212 | } catch (Throwable $e) {
213 | if ($retryCount === $this->maxRetries - 1) {
214 | return await($this->exec_fallback_async($prepResult, $e));
215 | }
216 | if ($this->wait > 0) {
217 | await(async_sleep($this->wait));
218 | }
219 | }
220 | }
221 | return null;
222 | })();
223 | }
224 |
225 | public function _run_async(stdClass $shared): PromiseInterface
226 | {
227 | return async(function () use ($shared) {
228 | $prepResult = await($this->prep_async($shared));
229 | $execResult = await($this->_exec_async($prepResult));
230 | return await($this->post_async($shared, $prepResult, $execResult));
231 | })();
232 | }
233 |
234 | public function run_async(stdClass $shared): PromiseInterface
235 | {
236 | if (!empty($this->successors)) {
237 | trigger_error("Node won't run successors. Use an AsyncFlow to run the full graph.", E_USER_WARNING);
238 | }
239 | return $this->_run_async($shared);
240 | }
241 |
242 | public function run(stdClass $shared): ?string
243 | {
244 | throw new \RuntimeException("Cannot call sync 'run' on an async node. Use 'run_async' instead.");
245 | }
246 | }
247 |
248 | class AsyncNode extends BaseNode implements AsyncRunnable
249 | {
250 | use AsyncLogicTrait;
251 | public function __construct(int $maxRetries = 1, int $wait = 0)
252 | {
253 | $this->maxRetries = $maxRetries;
254 | $this->wait = $wait;
255 | }
256 | }
257 |
258 | class AsyncBatchNode extends AsyncNode
259 | {
260 | public function _exec_async(mixed $items): PromiseInterface
261 | {
262 | return async(function () use ($items) {
263 | $results = [];
264 | foreach ($items ?? [] as $item) {
265 | $results[] = await(parent::_exec_async($item));
266 | }
267 | return $results;
268 | })();
269 | }
270 | }
271 |
272 | class AsyncParallelBatchNode extends AsyncNode
273 | {
274 | public function _exec_async(mixed $items): PromiseInterface
275 | {
276 | return async(function () use ($items) {
277 | $promises = [];
278 | foreach ($items ?? [] as $item) {
279 | $promise = parent::_exec_async($item);
280 | // Manually implement "settle" logic
281 | $promises[] = $promise->then(
282 | fn($value) => ['state' => 'fulfilled', 'value' => $value],
283 | fn($reason) => ['state' => 'rejected', 'reason' => $reason]
284 | );
285 | }
286 | $results = await(all($promises));
287 |
288 | $finalResults = [];
289 | foreach ($results as $result) {
290 | if ($result['state'] === 'rejected') {
291 | throw $result['reason'];
292 | }
293 | $finalResults[] = $result['value'];
294 | }
295 | return $finalResults;
296 | })();
297 | }
298 | }
299 |
300 | class AsyncFlow extends Flow implements AsyncRunnable
301 | {
302 | public function _orchestrate_async(stdClass $shared, ?array $params = null): PromiseInterface
303 | {
304 | return async(function () use ($shared, $params) {
305 | $current = $this->startNode;
306 | $p = $params ?? $this->params;
307 | $lastAction = null;
308 |
309 | while ($current) {
310 | $current->setParams($p);
311 | if ($current instanceof AsyncRunnable) {
312 | $lastAction = await($current->_run_async($shared));
313 | } else {
314 | $lastAction = $current->_run($shared);
315 | }
316 | $current = $this->getNextNode($current, $lastAction);
317 | }
318 | return $lastAction;
319 | })();
320 | }
321 |
322 | public function _run_async(stdClass $shared): PromiseInterface
323 | {
324 | return $this->_orchestrate_async($shared, $this->params);
325 | }
326 |
327 | public function run_async(stdClass $shared): PromiseInterface
328 | {
329 | return $this->_run_async($shared);
330 | }
331 |
332 | public function run(stdClass $shared): ?string
333 | {
334 | throw new \RuntimeException("Cannot call sync 'run' on an AsyncFlow. Use 'run_async' instead.");
335 | }
336 |
337 | protected function _run(stdClass $shared): ?string
338 | {
339 | throw new \RuntimeException("Internal error: _run should not be called on AsyncFlow.");
340 | }
341 | }
342 |
343 | class AsyncBatchFlow extends AsyncFlow
344 | {
345 | public function prep_async(stdClass $shared): PromiseInterface { return async(fn() => null)(); }
346 | public function post_async(stdClass $shared, mixed $prepResult, mixed $execResult): PromiseInterface { return async(fn() => $execResult)(); }
347 |
348 | public function run_async(stdClass $shared): PromiseInterface
349 | {
350 | return async(function () use ($shared) {
351 | $paramList = await($this->prep_async($shared)) ?? [];
352 | foreach ($paramList as $batchParams) {
353 | await($this->_orchestrate_async($shared, array_merge($this->params, $batchParams)));
354 | }
355 | return await($this->post_async($shared, $paramList, null));
356 | })();
357 | }
358 | }
359 |
360 | class AsyncParallelBatchFlow extends AsyncFlow
361 | {
362 | public function prep_async(stdClass $shared): PromiseInterface { return async(fn() => null)(); }
363 | public function post_async(stdClass $shared, mixed $prepResult, mixed $execResult): PromiseInterface { return async(fn() => $execResult)(); }
364 |
365 | public function run_async(stdClass $shared): PromiseInterface
366 | {
367 | return async(function () use ($shared) {
368 | $paramList = await($this->prep_async($shared)) ?? [];
369 | $promises = [];
370 | foreach ($paramList as $batchParams) {
371 | $promise = $this->_orchestrate_async($shared, array_merge($this->params, $batchParams));
372 | // Manually implement "settle" logic
373 | $promises[] = $promise->then(
374 | fn($value) => ['state' => 'fulfilled', 'value' => $value],
375 | fn($reason) => ['state' => 'rejected', 'reason' => $reason]
376 | );
377 | }
378 | $results = await(all($promises));
379 | foreach ($results as $result) {
380 | if ($result['state'] === 'rejected') {
381 | throw $result['reason'];
382 | }
383 | }
384 | return await($this->post_async($shared, $paramList, null));
385 | })();
386 | }
387 | }
--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------
1 | # Agentic Coding with PocketFlow-PHP
2 |
3 | > **Attention AI Agent:** This is your primary instruction manual. Follow these rules strictly to generate correct, robust, and bug-free PHP code for the PocketFlow-PHP framework. Failure to adhere to these rules will result in errors.
4 |
5 | ## The Golden Rules
6 |
7 | 1. **THE CORE FRAMEWORK IS READ-ONLY.** The file `src/PocketFlow.php` is the engine. You are **strictly forbidden** from ever editing it.
8 |
9 | 2. **USE THE THREE MAIN FILES.** All your application code **must** be written in these three files in the project root:
10 | - `nodes.php`: For all `Node`, `BatchNode`, and `AsyncNode` class definitions.
11 | - `flow.php`: For functions that create and wire up `Flows`.
12 | - `main.php`: The single entry point that calls a function from `flow.php`.
13 |
14 | 3. **UTILITIES GO IN `utils/`.** Any function that communicates with the outside world (e.g., calling an LLM API, a database, or a web search) **must** be placed in a new file inside the `utils/` directory.
15 |
16 | 4. **STATE MANAGEMENT IS CRITICAL.**
17 | - **DO** use the `$shared` object (`stdClass`) to manage all **mutable application state** (e.g., results, counters, lists of items).
18 | - **DO NOT** use `static` properties or variables inside Node classes to manage state. This will fail in loops.
19 | - **DO NOT** use `$this->params` to store mutable state. It is for **immutable configuration** only.
20 |
21 | 5. **STRICT TYPE COMPATIBILITY & SCOPE.**
22 | - The `post()` method of a `Node` **must** have the return type `?string`. If it does not decide the next action, it **must** end with `return null;`.
23 | - All `_async` methods (`prep_async`, `exec_async`, `post_async`) **must** have the return type `React\Promise\PromiseInterface`.
24 | - To return a promise, **always** use the pattern `return async(function() { ... })();`. Do not forget the final `()`.
25 | - **DO NOT** use `use ($this)` in closures. To access `$this->params` inside a `post_async` or `exec_async` closure, read the required values into local variables *before* the closure and pass them in with `use()`.
26 |
27 | 6. **ALWAYS IMPORT CLASSES WITH `use`.**
28 | - Any file that references a class (e.g., `AsyncNode`, `Flow`, `PromiseInterface`) **must** include a `use` statement for that class at the top of the file.
29 |
30 | 7. **DEFINE NODE CONNECTIONS *BEFORE* CREATING THE FLOW.**
31 | - The `Flow` class does not have a public method to retrieve its start node. Therefore, you **must** configure all node connections (`->next()`, `->on()->next()`) *before* passing the start node to the `Flow` constructor. This is especially important for creating loops.
32 | - **Correct Pattern:**
33 | ```php
34 | $myStartNode = new MyNode();
35 | $nextNode = new NextNode();
36 | $myStartNode->on('continue')->next($myStartNode); // Define loop on the node itself
37 | $myStartNode->on('finish')->next($nextNode);
38 | $flow = new Flow($myStartNode); // Then create the flow with the fully wired node
39 | ```
40 |
41 | 8. **USE PHP 8.3 FEATURES WISELY.**
42 | - **DO** use constructor property promotion for cleaner `Node` classes.
43 | - **DO** use the `match` expression inside `post()` for clean, readable action routing.
44 | - **DO NOT** use features like Fibers directly. Rely on the `async()` and `await()` functions provided by `react/async`.
45 |
46 | 9. **ENABLE STRICT TYPES.**
47 | - To prevent common type-related errors, **every new PHP file** you create (`nodes.php`, `flow.php`, `main.php`, and all utility files) **must** start with the following declaration on the very first line:
48 | ```php
49 | "Build a system that summarizes news articles from a list of URLs."
63 |
64 | 2. **AI: Propose a Plan.** Based on the documentation, the AI proposes a plan using PocketFlow concepts.
65 | > "I will create a `BatchFlow` to process each URL. The sub-flow will have two nodes: `FetchArticleNode` to download the content, and `SummarizeNode` to call an LLM. The final summaries will be stored in `$shared->summaries`."
66 |
67 | 3. **Human: Approve the Plan.** The human confirms the logic.
68 |
69 | 4. **AI: Generate the Code.** The AI writes the code for `utils/`, `nodes.php`, `flow.php`, and `main.php`, strictly following the Golden Rules and the Mandatory Project Structure.
70 |
71 | 5. **Human & AI: Test and Iterate.** The human runs `php main.php`. If there are errors, the AI debugs them by reviewing the Golden Rules.
72 |
73 | This collaborative process, guided by this documentation, is the fastest and most reliable way to build LLM applications with PocketFlow-PHP.
74 |
75 | ## Mandatory Project Structure
76 |
77 | Your final project **must** adhere to this structure. Do **not** create additional top-level directories or files, unless stated otherwise by the user, e.g. when asked to add a frontend.
78 |
79 | /
80 | ├── main.php # The application's single entry point.
81 | ├── nodes.php # All Node class definitions.
82 | ├── flow.php # All Flow creation functions.
83 | ├── utils/ # Directory for all helper functions (API calls, etc.).
84 | │ └── ... # e.g., llm_api.php, web_search.php
85 | ├── composer.json # Managed by Composer. Do not edit manually.
86 | └── src/
87 | └── PocketFlow.php # The core framework. READ-ONLY.
88 |
89 | ---
90 |
91 | ### 1: Introduction to PocketFlow-PHP
92 |
93 | # PocketFlow-PHP
94 |
95 |
96 | A minimalist LLM framework for PHP, written in 368 lines of PHP.
97 |
98 | Build complex Agents, Workflows, and RAG systems with a tiny, powerful core.
99 |
100 |
101 | ---
102 |
103 | **PocketFlow-PHP** is a port of the minimalist Python LLM framework, bringing its core principles to the PHP ecosystem. It's designed for developers who want maximum control and flexibility without the bloat of larger frameworks. It is optimized for **Agentic Coding**, where humans design the system and an AI agent writes the code.
104 |
105 | - **Lightweight**: The entire framework core is in a single, well-tested file.
106 | - **Expressive**: Build everything you love from larger frameworks—Multi-Agent systems, RAG pipelines, and complex workflows—using simple, composable building blocks.
107 |
108 | ## How does it work?
109 |
110 | The core abstraction is a **Graph + Shared Store**.
111 |
112 | 1. **Node**: The smallest unit of work (e.g., call an LLM, read a file).
113 | 2. **Flow**: Connects Nodes into a graph. Transitions are determined by simple string "Actions".
114 | 3. **Shared Store**: A simple `stdClass` object passed through the entire flow, allowing nodes to communicate and share state.
115 |
116 | This simple model is all you need to create powerful design patterns:
117 |
118 | ## Getting Started in 60 Seconds
119 |
120 | Get up and running with your first AI-powered application.
121 |
122 | **Prerequisites:** PHP 8.3+ and [Composer](https://getcomposer.org/) must be installed.
123 |
124 | 1. **Clone the repository:**
125 | ```bash
126 | git clone https://github.com/weise25/PocketFlow-PHP.git
127 | cd PocketFlow-PHP
128 | ```
129 |
130 | 2. **Install dependencies:**
131 | ```bash
132 | composer install
133 | ```
134 |
135 | 3. **Set up your environment:**
136 | - Create a `.env` file in the root of the project.
137 | - Add your API keys to the `.env` file (e.g., `OPENROUTER_API_KEY=...`).
138 |
139 | ### Chapter 2: Core Abstraction
140 |
141 | # Core Abstraction
142 |
143 | This chapter explains the fundamental building blocks of **PocketFlow-PHP**. Understanding these concepts is essential for building any application with the framework.
144 |
145 | We model the LLM workflow as a **Graph + Shared Store**.
146 |
147 | - [Node] – The smallest unit of work.
148 | - [Flow] – The orchestrator that connects Nodes.
149 | - [Communication] – How Nodes and Flows share data.
150 | - [Batch] – Tools for handling multiple items efficiently.
151 | - [(Advanced) Async] – For non-blocking, I/O-bound tasks.
152 | - [(Advanced) Parallel] – For running multiple async tasks concurrently.
153 |
154 |
155 | ---
156 |
157 | ### 2.1: Node
158 |
159 | # Node
160 |
161 | A **`Node`** is the smallest building block in PocketFlow. It represents a single, concrete step in a workflow. Each Node follows a simple, three-step lifecycle: `prep() -> exec() -> post()`.
162 |
163 |
164 | ### 1. The Lifecycle
165 |
166 | 1. **`prep(stdClass $shared): mixed`**
167 | * **Purpose:** To read data from the `$shared` store and prepare it for the main task.
168 | * **Example:** Fetch a user ID from `$shared->user->id`.
169 | * **Returns:** The data needed by the `exec()` method.
170 |
171 | 2. **`exec(mixed $prepResult): mixed`**
172 | * **Purpose:** To perform the core computation. This is where you call LLMs, APIs, or run business logic.
173 | * **Rule:** This method **must not** access the `$shared` store directly. It should be a "pure" function that only works with the data it receives from `prep()`.
174 | * **Returns:** The result of the computation.
175 |
176 | 3. **`post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string`**
177 | * **Purpose:** To write the results from `exec()` back into the `$shared` store and to decide what happens next.
178 | * **Returns:** An **action string**. This string determines which path the `Flow` will take. Returning `null` triggers the default path.
179 | * > **AI Agent Rule:** The return type of this method **must** be `?string`. If you don't need to return a specific action, you **must** `return null;`.
180 |
181 | ### 2. Fault Tolerance
182 |
183 | You can make your Nodes resilient to failure using constructor arguments and a fallback method.
184 |
185 | **Retries:**
186 | Define the number of retries and the wait time in the constructor.
187 |
188 | ```php
189 | // nodes.php
190 | use PocketFlow\Node;
191 |
192 | class ApiCallNode extends Node
193 | {
194 | public function __construct()
195 | {
196 | // Try exec() up to 3 times, wait 5 seconds between failures.
197 | parent::__construct(maxRetries: 3, wait: 5);
198 | }
199 |
200 | public function exec(mixed $prepResult): string
201 | {
202 | // This might throw an exception if the API is down.
203 | return call_external_api();
204 | }
205 | }
206 | ```
207 |
208 | **Graceful Fallback:**
209 | If all retries fail, the `execFallback()` method is called. Instead of crashing, you can return a safe default value.
210 |
211 | ```php
212 | // nodes.php
213 | use PocketFlow\Node;
214 |
215 | class ApiCallNode extends Node
216 | {
217 | // ... constructor and exec() method ...
218 |
219 | public function execFallback(mixed $prepResult, \Throwable $e): string
220 | {
221 | // Log the error and return a safe default.
222 | error_log("API call failed after all retries: " . $e->getMessage());
223 | return "Default value due to API failure.";
224 | }
225 | }
226 | ```
227 |
228 | ### 3. Example: A Complete Node
229 |
230 | ```php
231 | // nodes.php
232 | use PocketFlow\Node;
233 |
234 | class SummarizeArticleNode extends Node
235 | {
236 | public function __construct()
237 | {
238 | parent::__construct(maxRetries: 2, wait: 3);
239 | }
240 |
241 | public function prep(stdClass $shared): ?string
242 | {
243 | return $shared->article_text ?? null;
244 | }
245 |
246 | public function exec(mixed $articleText): string
247 | {
248 | if (empty($articleText)) {
249 | return "No text provided.";
250 | }
251 | $prompt = "Summarize this article: " . $articleText;
252 | return call_llm($prompt); // This might fail.
253 | }
254 |
255 | public function execFallback(mixed $prepResult, \Throwable $e): string
256 | {
257 | return "Could not generate summary due to an error.";
258 | }
259 |
260 | public function post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string
261 | {
262 | $shared->summary = $execResult;
263 | return null; // Triggers the "default" action.
264 | }
265 | }
266 | ```
267 |
268 | ---
269 | ## 2.2: Flow
270 |
271 | A **`Flow`** is the orchestrator. It connects `Nodes` together to form a process graph. The path through the graph is determined by the **action strings** returned by each Node's `post()` method.
272 |
273 | ### 1. Action-Based Transitions
274 |
275 | You define the connections between nodes using a simple, fluent API.
276 |
277 | **Default Transition:**
278 | If a Node's `post()` method returns `null` (or the string `"default"`), the default path is taken.
279 |
280 | ```php
281 | // flow.php
282 | $nodeA = new NodeA();
283 | $nodeB = new NodeB();
284 |
285 | // If NodeA returns null, execute NodeB next.
286 | $nodeA->next($nodeB);
287 |
288 | $flow = new Flow($nodeA);
289 | ```
290 |
291 | **Conditional Transitions:**
292 | You can create branches in your logic based on specific action strings.
293 |
294 | ```php
295 | // flow.php
296 | $reviewNode = new ReviewNode();
297 | $approveNode = new ApproveNode();
298 | $rejectNode = new RejectNode();
299 |
300 | // If reviewNode returns "approved", go to approveNode.
301 | $reviewNode->on('approved')->next($approveNode);
302 |
303 | // If reviewNode returns "rejected", go to rejectNode.
304 | $reviewNode->on('rejected')->next($rejectNode);
305 |
306 | $flow = new Flow($reviewNode);
307 | ```
308 |
309 | ### 2. Nested Flows (Composition)
310 |
311 | A `Flow` itself behaves like a `Node`. This allows you to build complex workflows by composing smaller, reusable flows.
312 |
313 | When a `Flow` is used as a node, the action returned by the **last node** of that inner flow is used to determine the next step in the outer flow.
314 |
315 | **Example:**
316 |
317 | ```php
318 | // flow.php
319 | use PocketFlow\Flow;
320 |
321 | // --- Sub-Flow 1: User Validation ---
322 | $checkUser = new CheckUserNode();
323 | $checkPermissions = new CheckPermissionsNode();
324 | $checkUser->next($checkPermissions);
325 | $validationFlow = new Flow($checkUser); // This flow might return "validation_failed"
326 |
327 | // --- Sub-Flow 2: Data Processing ---
328 | $loadData = new LoadDataNode();
329 | $processData = new ProcessDataNode();
330 | $loadData->next($processData);
331 | $processingFlow = new Flow($loadData); // This flow might return "processing_complete"
332 |
333 | // --- Main Flow: Composing the sub-flows ---
334 | $endNode = new EndNode();
335 | $errorNode = new ErrorNode();
336 |
337 | // Wire the main flow
338 | $validationFlow->next($processingFlow); // Default transition
339 | $validationFlow->on('validation_failed')->next($errorNode);
340 | $processingFlow->on('processing_complete')->next($endNode);
341 |
342 | $mainFlow = new Flow($validationFlow);
343 | $mainFlow->run($shared);
344 | ```
345 |
346 | This creates a clear, modular structure:
347 | ```mermaid
348 | graph TD
349 | subgraph Main Flow
350 | A(validationFlow) --> B(processingFlow);
351 | A --> C(errorNode);
352 | B --> D(endNode);
353 | end
354 | ```
355 | ---
356 |
357 | # 2.3: Communication
358 |
359 | Nodes and Flows need to share data. PocketFlow provides two mechanisms for this, each with a distinct purpose.
360 |
361 | ### 1. The Shared Store (`$shared`)
362 |
363 | This is the **primary** method of communication and state management.
364 |
365 | - **What it is:** A single `stdClass` object that is passed by reference to every Node in a Flow.
366 | - **Purpose:** To hold all **mutable application state**. This includes initial inputs, intermediate results, final outputs, counters, flags, etc.
367 | - **When to use:** For almost everything. It acts as the central "memory" or "database" for your workflow.
368 | - **How to use:**
369 | - Read from it in `prep()`.
370 | - Write to it in `post()`.
371 |
372 | > **AI Agent Golden Rule:** The `$shared` store is the single source of truth for your application's state. Any data that changes or needs to be passed between nodes **must** live in `$shared`.
373 |
374 | **Example:**
375 |
376 | ```php
377 | // main.php
378 | $shared = new stdClass();
379 | $shared->user_id = 123;
380 | $shared->user_data = null; // To be filled by a node
381 | $shared->final_report = null; // To be filled by another node
382 |
383 | $flow->run($shared);
384 |
385 | // After the flow runs, $shared will be populated.
386 | echo $shared->final_report;
387 | ```
388 |
389 | ### 2. Params (`$params`)
390 |
391 | This is a **secondary** mechanism for configuration.
392 |
393 | - **What it is:** An `array` that is passed by value to each Node.
394 | - **Purpose:** To hold **immutable configuration** that does not change during the flow's execution.
395 | - **When to use:** For things like model names, API endpoints, or identifiers in a `BatchFlow`.
396 | - **How to use:**
397 | - Set once on the top-level `Flow` using `$flow->setParams([...])`.
398 | - Access within any Node via `$this->params['my_key']`.
399 |
400 | > **AI Agent Warning:** Do not use `$params` to store state that needs to change (like counters or lists of results). The `$params` array is reset for each node in a loop, so any changes you make will be lost. Use `$shared` for that.
401 |
402 | **Example:**
403 |
404 | ```php
405 | // flow.php
406 | $node = new ApiCallNode();
407 | $flow = new Flow($node);
408 |
409 | // Set the API endpoint once for the entire flow.
410 | $flow->setParams(['api_endpoint' => 'https://api.example.com/v1']);
411 | $flow->run($shared);
412 |
413 | // nodes.php
414 | class ApiCallNode extends Node
415 | {
416 | public function exec(mixed $prepResult): string
417 | {
418 | $endpoint = $this->params['api_endpoint'];
419 | // ... make a call to the endpoint ...
420 | return "Data from API";
421 | }
422 | }
423 | ```
424 | ---
425 |
426 | ### 2.4: Batch
427 |
428 | # Batch Processing
429 |
430 | PocketFlow provides two powerful classes for processing multiple items: `BatchNode` and `BatchFlow`.
431 |
432 | ### 1. `BatchNode`
433 |
434 | Use `BatchNode` when you have a list of **data items** and want to perform the **same operation** on each one. This is the "Map" part of a Map-Reduce pattern.
435 |
436 | **How it works:**
437 | - `prep(stdClass $shared)`: Must return an `array` of items to be processed.
438 | - `exec(mixed $item)`: Is called **sequentially** for each `$item` in the array from `prep()`.
439 | - `post(stdClass $shared, ..., array $execResultList)`: Receives an `array` containing the results of all `exec()` calls.
440 |
441 | **Example: Summarize Text Chunks**
442 |
443 | ```php
444 | // nodes.php
445 | use PocketFlow\BatchNode;
446 |
447 | class SummarizeChunksNode extends BatchNode
448 | {
449 | public function prep(stdClass $shared): array
450 | {
451 | // Assume $shared->text_chunks is an array of strings.
452 | return $shared->text_chunks;
453 | }
454 |
455 | public function exec(mixed $chunk): string
456 | {
457 | // This method is called for each chunk.
458 | return call_llm("Summarize this chunk: " . $chunk);
459 | }
460 |
461 | public function post(stdClass $shared, mixed $p, mixed $summaries): ?string
462 | {
463 | // $summaries is an array of all the individual summaries.
464 | $shared->combined_summary = implode("\n", $summaries);
465 | return null;
466 | }
467 | }
468 | ```
469 |
470 | ### 2. `BatchFlow`
471 |
472 | Use `BatchFlow` when you want to run an entire **sub-flow** multiple times, each time with a different **configuration**.
473 |
474 | **How it works:**
475 | - `prep(stdClass $shared)`: Must return an `array` of **parameter arrays**.
476 | - The sub-flow is executed once for each parameter array.
477 | - Inside the sub-flow's nodes, the parameters are available via `$this->params`.
478 |
479 | **Example: Process Multiple Files**
480 |
481 | ```php
482 | // flow.php
483 | use PocketFlow\Flow;
484 | use PocketFlow\BatchFlow;
485 |
486 | function create_file_processing_flow(): Flow
487 | {
488 | // The sub-flow that processes a single file.
489 | $loadFileNode = new LoadFileNode();
490 | $analyzeFileNode = new AnalyzeFileNode();
491 | $loadFileNode->next($analyzeFileNode);
492 | $singleFileFlow = new Flow($loadFileNode);
493 |
494 | // The BatchFlow that orchestrates the process.
495 | $allFilesFlow = new ProcessAllFilesBatchFlow($singleFileFlow);
496 | return $allFilesFlow;
497 | }
498 |
499 | // nodes.php
500 | use PocketFlow\Node;
501 | use PocketFlow\BatchFlow;
502 |
503 | class ProcessAllFilesBatchFlow extends BatchFlow
504 | {
505 | public function prep(stdClass $shared): array
506 | {
507 | // Returns an array of parameter sets.
508 | // e.g., [['filename' => 'a.txt'], ['filename' => 'b.txt']]
509 | return array_map(fn($f) => ['filename' => $f], $shared->filenames);
510 | }
511 | }
512 |
513 | class LoadFileNode extends Node
514 | {
515 | public function exec(mixed $p): string
516 | {
517 | // Accesses the filename from the params set by the BatchFlow.
518 | $filename = $this->params['filename'];
519 | echo "Loading file: {$filename}\n";
520 | return file_get_contents($filename);
521 | }
522 |
523 | public function post(stdClass $shared, mixed $p, mixed $content): ?string
524 | {
525 | $shared->current_content = $content;
526 | return null;
527 | }
528 | }
529 |
530 | class AnalyzeFileNode extends Node { /* ... analyzes $shared->current_content ... */ }
531 | ```
532 | ---
533 |
534 | ### Seite 2.5: (Advanced) Async
535 | # (Advanced) Async
536 |
537 | Use `AsyncNode` and `AsyncFlow` for tasks that are **I/O-bound**, such as API calls, database queries, or reading large files. This allows your application to perform other work while waiting for these operations to complete, making it much more efficient. All async operations are powered by **ReactPHP**.
538 |
539 | ### 1. `AsyncNode`
540 |
541 | An `AsyncNode` is similar to a regular `Node`, but its lifecycle methods are asynchronous.
542 |
543 | **Rules for `AsyncNode`:**
544 | 1. Implement the `_async` versions of the lifecycle methods: `prep_async()`, `exec_async()`, `post_async()`.
545 | 2. Each of these methods **must** return a `React\Promise\PromiseInterface`.
546 | 3. The easiest way to return a promise is to wrap your logic in `return async(function() { ... })();`.
547 | 4. Inside an `async` block, you can use `await()` to wait for other promises to resolve.
548 |
549 | **Example: Asynchronous API Call**
550 |
551 | ```php
552 | // nodes.php
553 | use PocketFlow\AsyncNode;
554 | use React\Promise\PromiseInterface;
555 | use function React\Async\async;
556 | use function React\Async\await;
557 |
558 | class FetchUserDataNode extends AsyncNode
559 | {
560 | public function prep_async(stdClass $shared): PromiseInterface
561 | {
562 | return async(fn() => $shared->user_id)();
563 | }
564 |
565 | public function exec_async(mixed $userId): PromiseInterface
566 | {
567 | // Assume call_user_api_async() returns a Promise.
568 | return call_user_api_async($userId);
569 | }
570 |
571 | public function post_async(stdClass $shared, mixed $p, mixed $userData): PromiseInterface
572 | {
573 | return async(function() use ($shared, $userData) {
574 | $shared->user_data = $userData;
575 | return null;
576 | })();
577 | }
578 | }
579 | ```
580 |
581 | ### 2. `AsyncFlow`
582 |
583 | An `AsyncFlow` is used to orchestrate flows that contain at least one `AsyncNode`. It can also correctly orchestrate regular synchronous `Nodes`.
584 |
585 | ### 3. Running an Async Workflow
586 |
587 | The top-level entry point of any application that uses async features **must** be wrapped in an `async` block.
588 |
589 | **Example:**
590 |
591 | ```php
592 | // main.php
593 | require_once __DIR__ . '/vendor/autoload.php';
594 | require_once __DIR__ . '/flow.php';
595 | use function React\Async\async;
596 | use function React\Async\await;
597 |
598 | // Wrap the entire application logic in async().
599 | async(function() {
600 | $shared = new stdClass();
601 | $shared->user_id = 123;
602 |
603 | $flow = create_user_data_flow(); // This flow returns an AsyncFlow
604 |
605 | // Use await() to run the flow and wait for it to complete.
606 | await($flow->run_async($shared));
607 |
608 | print_r($shared->user_data);
609 | })();
610 | ```
611 | ---
612 |
613 | ### 2.6: (Advanced) Parallel
614 |
615 | # (Advanced) Parallel Execution
616 |
617 | Parallel execution in PocketFlow-PHP means running multiple **asynchronous** tasks **concurrently**. This is extremely useful for speeding up I/O-bound workflows where tasks don't depend on each other.
618 |
619 | > **Note:** This is **concurrency**, not true parallelism. PHP's single-threaded nature means CPU-bound tasks won't run faster. However, for waiting on multiple API calls or database queries at once, the performance gain is significant.
620 |
621 | ### 1. `AsyncParallelBatchNode`
622 |
623 | This is the parallel version of `AsyncBatchNode`. It calls `exec_async()` for **all items at the same time** and waits for all of them to complete.
624 |
625 | **Use Case:** Fetching data for multiple users from an API simultaneously.
626 |
627 | **Example: Parallel API Calls**
628 |
629 | ```php
630 | // nodes.php
631 | use PocketFlow\AsyncParallelBatchNode;
632 | use React\Promise\PromiseInterface;
633 | use function React\Async\async;
634 |
635 | class FetchAllUsersNode extends AsyncParallelBatchNode
636 | {
637 | public function prep_async(stdClass $shared): PromiseInterface
638 | {
639 | // Returns an array of user IDs.
640 | return async(fn() => $shared->user_ids)();
641 | }
642 |
643 | public function exec_async(mixed $userId): PromiseInterface
644 | {
645 | // This is called concurrently for all user IDs.
646 | // Assume call_user_api_async() returns a Promise.
647 | return call_user_api_async($userId);
648 | }
649 |
650 | public function post_async(stdClass $shared, mixed $p, mixed $usersData): PromiseInterface
651 | {
652 | return async(function() use ($shared, $usersData) {
653 | // $usersData is an array of results. The order is preserved.
654 | $shared->users = $usersData;
655 | return null;
656 | })();
657 | }
658 | }
659 | ```
660 |
661 | ### 2. `AsyncParallelBatchFlow`
662 |
663 | This is the parallel version of `AsyncBatchFlow`. It runs the entire sub-flow for **all parameter sets concurrently**.
664 |
665 | **Use Case:** Processing multiple files, where each file requires a multi-step async workflow (e.g., download, analyze, upload).
666 |
667 | **Example: Parallel File Processing**
668 |
669 | ```php
670 | // flow.php
671 | use PocketFlow\AsyncFlow;
672 | use PocketFlow\AsyncParallelBatchFlow;
673 |
674 | function create_parallel_file_flow(): AsyncFlow
675 | {
676 | // The sub-flow for a single file.
677 | $downloadNode = new DownloadFileAsyncNode();
678 | $analyzeNode = new AnalyzeFileAsyncNode();
679 | $downloadNode->next($analyzeNode);
680 | $singleFileFlow = new AsyncFlow($downloadNode);
681 |
682 | // The parallel batch flow.
683 | $parallelFlow = new ProcessAllFilesParallelBatchFlow($singleFileFlow);
684 | return $parallelFlow;
685 | }
686 |
687 | // nodes.php
688 | use PocketFlow\AsyncParallelBatchFlow;
689 | use React\Promise\PromiseInterface;
690 | use function React\Async\async;
691 |
692 | class ProcessAllFilesParallelBatchFlow extends AsyncParallelBatchFlow
693 | {
694 | public function prep_async(stdClass $shared): PromiseInterface
695 | {
696 | // Returns an array of parameter sets.
697 | return async(fn() => array_map(fn($f) => ['url' => $f], $shared->file_urls))();
698 | }
699 | }
700 |
701 | // DownloadFileAsyncNode and AnalyzeFileAsyncNode are defined as regular AsyncNodes.
702 | // They will access the URL via $this->params['url'].
703 | ```
704 |
705 | > **Warning:** Be mindful of API rate limits when using parallel execution. A large number of concurrent requests can quickly exhaust your quota.
706 |
707 | # Chapter 3: Design Pattern
708 |
709 | # Design Patterns
710 |
711 | Design patterns are reusable blueprints for solving common problems in LLM application development. PocketFlow-PHP's simple core abstractions (`Node`, `Flow`, `Shared Store`) are all you need to implement these powerful patterns.
712 |
713 | This chapter provides practical, copy-paste-ready examples for each pattern.
714 |
715 | - [Agent] – For creating autonomous, decision-making entities.
716 | - [Workflow] – For breaking down complex tasks into a sequence of simple steps.
717 | - [RAG (Retrieval-Augmented Generation)] – For providing external knowledge to an LLM.
718 | - [Map-Reduce] – For processing large amounts of data efficiently.
719 | - [Structured Output] – For forcing an LLM to return clean, predictable data formats.
720 | - [(Advanced) Multi-Agent Systems] – For orchestrating collaboration between multiple specialized agents.
721 | ---
722 |
723 | ### Seite 3.1: Agent
724 |
725 | # Agent
726 |
727 | An **Agent** is a `Node` that can make autonomous decisions. Instead of following a fixed path, it analyzes the current state in the `$shared` store and returns a specific **action string** to direct the `Flow` down different branches, often looping back to itself to create a cycle of reasoning and action.
728 |
729 | ### The Agentic Loop
730 |
731 | The core of an agent is a loop:
732 | 1. **Think:** The Agent `Node` analyzes the state (`$shared`) and the goal.
733 | 2. **Decide:** It calls an LLM to choose the next `action` from a predefined set of tools.
734 | 3. **Act:** The `Flow` routes to a `Tool Node` based on the chosen action.
735 | 4. **Observe:** The `Tool Node` executes its task (e.g., a web search) and updates the `$shared` store with the result.
736 | 5. The `Flow` loops back to the Agent `Node` to start the cycle again with new information.
737 |
738 | ```mermaid
739 | graph TD
740 | A[Agent Node (Think & Decide)] -- action: 'search' --> B[Search Tool Node (Act)];
741 | A -- action: 'answer' --> C[Answer Node (Final Act)];
742 | B -- result --> D(Update Shared Store);
743 | D --> A;
744 | ```
745 |
746 | ### Example: A Simple Research Agent
747 |
748 | This agent can decide whether to search the web for more information or to answer a question based on the context it has already gathered.
749 |
750 | **`flow.php`**
751 | ```php
752 | on('search')->next($searchNode);
764 | $decideNode->on('answer')->next($answerNode);
765 | $searchNode->next($decideNode); // Loop back to the decision node after acting
766 |
767 | return new Flow($decideNode);
768 | }
769 | ```
770 |
771 | **`nodes.php`**
772 | ```php
773 | search_history) ? "Nothing yet." : implode("\n", $shared->search_history);
785 | return [$shared->query, $context];
786 | }
787 |
788 | public function exec(mixed $prepResult): array
789 | {
790 | [$query, $context] = $prepResult;
791 | $prompt = <<current_search_term = $decision['search_term'];
816 | }
817 | return $decision['action'];
818 | }
819 | }
820 |
821 | class SearchWebNode extends Node
822 | {
823 | public function prep(stdClass $shared): string
824 | {
825 | return $shared->current_search_term;
826 | }
827 |
828 | public function exec(mixed $searchTerm): string
829 | {
830 | echo "Agent is searching for: {$searchTerm}\n";
831 | return web_search($searchTerm); // Assume web_search() is defined
832 | }
833 |
834 | public function post(stdClass $shared, mixed $p, mixed $searchResult): ?string
835 | {
836 | $shared->search_history[] = $searchResult;
837 | return null; // Triggers default transition back to DecideActionNode
838 | }
839 | }
840 |
841 | class AnswerNode extends Node
842 | {
843 | public function exec(mixed $p): string
844 | {
845 | // In a real app, this would also use prep() to get context
846 | return "This is the final answer based on the research.";
847 | }
848 |
849 | public function post(stdClass $shared, mixed $p, mixed $answer): ?string
850 | {
851 | $shared->final_answer = $answer;
852 | return null; // End of flow
853 | }
854 | }
855 | ```
856 | ---
857 |
858 | ### Chapter 3.2 - 3.6 (Workflow, RAG, etc.)
859 |
860 | # Workflow
861 |
862 | A **Workflow** is the simplest design pattern. It involves chaining a sequence of `Nodes` together to break a complex task into manageable steps. Each node performs one part of the job and passes its result to the next via the `$shared` store.
863 |
864 | ### Example: Article Writing Workflow
865 |
866 | This workflow generates an article by breaking the process into three distinct steps: outlining, drafting, and reviewing.
867 |
868 | **`flow.php`**
869 | ```php
870 | next($draftNode)->next($reviewNode);
882 |
883 | return new Flow($outlineNode);
884 | }
885 | ```
886 |
887 | **`nodes.php`**
888 | ```php
889 | topic; }
894 | public function exec(mixed $topic): string { return "Outline for: {$topic}"; /* call_llm(...) */ }
895 | public function post(stdClass $shared, mixed $p, mixed $outline): ?string {
896 | $shared->outline = $outline;
897 | return null;
898 | }
899 | }
900 |
901 | class DraftArticleNode extends Node {
902 | public function prep(stdClass $shared): string { return $shared->outline; }
903 | public function exec(mixed $outline): string { return "Draft based on: {$outline}"; /* call_llm(...) */ }
904 | public function post(stdClass $shared, mixed $p, mixed $draft): ?string {
905 | $shared->draft = $draft;
906 | return null;
907 | }
908 | }
909 |
910 | class ReviewArticleNode extends Node {
911 | public function prep(stdClass $shared): string { return $shared->draft; }
912 | public function exec(mixed $draft): string { return "Reviewed draft: {$draft}"; /* call_llm(...) */ }
913 | public function post(stdClass $shared, mixed $p, mixed $final): ?string {
914 | $shared->final_article = $final;
915 | return null;
916 | }
917 | }
918 | ```
919 |
920 | # RAG (Retrieval-Augmented Generation)
921 |
922 | RAG allows an LLM to answer questions using information from your own documents. It's a two-stage process.
923 |
924 | ### Stage 1: Offline Indexing (A Workflow)
925 |
926 | This flow reads documents, splits them into chunks, creates vector embeddings for each chunk, and stores them in a vector database.
927 |
928 | **`flow.php` (Offline Indexing)**
929 | ```php
930 | next($embedNode)->next($storeNode);
940 | return new Flow($chunkNode);
941 | }
942 | ```
943 |
944 | **`nodes.php` (Offline Indexing)**
945 | ```php
946 | file_paths; }
953 | public function exec(mixed $path): array { return sentence_chunk(file_get_contents($path)); }
954 | public function post(stdClass $shared, mixed $p, mixed $chunkLists): ?string {
955 | $shared->all_chunks = array_merge(...$chunkLists);
956 | return null;
957 | }
958 | }
959 |
960 | // 2. Embedding (using BatchNode)
961 | class EmbedChunksNode extends BatchNode {
962 | public function prep(stdClass $shared): array { return $shared->all_chunks; }
963 | public function exec(mixed $chunk): array { return get_embedding($chunk); } // Assume get_embedding() exists
964 | public function post(stdClass $shared, mixed $p, mixed $embeddings): ?string {
965 | $shared->embeddings = $embeddings;
966 | return null;
967 | }
968 | }
969 |
970 | // 3. Storing
971 | class StoreEmbeddingsNode extends Node {
972 | public function exec(mixed $p): mixed {
973 | /* ... code to store embeddings in a vector DB ... */
974 | return null; // Must return a value compatible with 'mixed'
975 | }
976 | }
977 | ```
978 |
979 | ### Stage 2: Online Retrieval & Answering (A Workflow)
980 |
981 | This flow takes a user's question, finds the most relevant chunks from the vector database, and passes them to an LLM as context to generate an answer.
982 |
983 | **`flow.php` (Online Answering)**
984 | ```php
985 | next($retrieveDocs)->next($generateAnswer);
995 | return new Flow($embedQuery);
996 | }
997 | ```
998 |
999 | # Map-Reduce
1000 |
1001 | The Map-Reduce pattern is perfect for processing large datasets that can be broken down into smaller, independent parts.
1002 |
1003 | - **Map Phase:** A `BatchNode` processes each part of the data individually.
1004 | - **Reduce Phase:** A regular `Node` takes the list of results from the map phase and combines them into a single, final output.
1005 |
1006 | ### Example: Summarizing Multiple Documents
1007 |
1008 | **`flow.php`**
1009 | ```php
1010 | next($reduceNode);
1019 | return new Flow($mapNode);
1020 | }
1021 | ```
1022 |
1023 | **`nodes.php`**
1024 | ```php
1025 | documents is an associative array ['filename' => 'content', ...]
1035 | return $shared->documents;
1036 | }
1037 |
1038 | public function exec(mixed $content): string
1039 | {
1040 | return call_llm("Summarize this document: " . $content);
1041 | }
1042 |
1043 | public function post(stdClass $shared, mixed $p, mixed $summaries): ?string
1044 | {
1045 | $shared->individual_summaries = $summaries;
1046 | return null;
1047 | }
1048 | }
1049 |
1050 | // REDUCE: Combine all summaries into one
1051 | class ReduceSummariesNode extends Node
1052 | {
1053 | public function prep(stdClass $shared): array
1054 | {
1055 | return $shared->individual_summaries;
1056 | }
1057 |
1058 | public function exec(mixed $summaries): string
1059 | {
1060 | $combinedText = implode("\n---\n", $summaries);
1061 | return call_llm("Create a final summary from these smaller summaries: " . $combinedText);
1062 | }
1063 |
1064 | public function post(stdClass $shared, mixed $p, mixed $finalSummary): ?string
1065 | {
1066 | $shared->final_summary = $finalSummary;
1067 | return null;
1068 | }
1069 | }
1070 | ```
1071 |
1072 | # Structured Output
1073 |
1074 | Often, you need an LLM to return data in a specific format like JSON or an array. The most reliable way to achieve this is through **prompt engineering**.
1075 |
1076 | ### The YAML Technique
1077 |
1078 | YAML is often easier for LLMs to generate correctly than JSON because it is less strict about quotes and commas.
1079 |
1080 | **The Strategy:**
1081 | 1. In your prompt, provide a clear example of the YAML structure you expect.
1082 | 2. Wrap the example in ```yaml code blocks.
1083 | 3. In your `exec()` method, parse the YAML string from the LLM's response.
1084 |
1085 | ### Example: Extracting Information from Text
1086 |
1087 | ```php
1088 | // nodes.php
1089 | use PocketFlow\Node;
1090 | use Symfony\Component\Yaml\Yaml; // `composer require symfony/yaml`
1091 |
1092 | class ExtractUserInfoNode extends Node
1093 | {
1094 | public function prep(stdClass $shared): string
1095 | {
1096 | return $shared->raw_text; // e.g., "John Doe is 30 years old and lives in New York."
1097 | }
1098 |
1099 | public function exec(mixed $text): array
1100 | {
1101 | $prompt = <<user_info = $userInfo;
1128 | return null;
1129 | }
1130 | }
1131 | ```
1132 |
1133 | # (Advanced) Multi-Agent Systems
1134 |
1135 | A Multi-Agent System involves two or more autonomous `Agents` collaborating to solve a problem. In PocketFlow-PHP, this is achieved by running multiple `AsyncFlows` concurrently, with agents communicating via shared **message queues**.
1136 |
1137 | ### The Core Pattern
1138 |
1139 | 1. **One Flow per Agent:** Each agent's logic is contained in its own `AsyncNode`, which runs in a loop inside its own `AsyncFlow`.
1140 | 2. **Message Queues:** A simple, promise-based queue class is used for communication. Queues are stored in the `$shared` object.
1141 | 3. **Concurrent Execution:** All agent flows are started simultaneously using `React\Promise\all()`.
1142 | 4. **State Management:**
1143 | - **Immutable Config:** Agent-specific configuration (like model name or personality prompt) is passed via `$params`.
1144 | - **Mutable State:** Shared game state (like scores or history) is managed in the `$shared` object.
1145 |
1146 | ### Example: The Taboo Game
1147 |
1148 | This example features a "Hinter" agent and a "Guesser" agent playing the word game Taboo.
1149 |
1150 | **`flow.php`**
1151 | ```php
1152 | hinterQueue = new MessageQueue();
1160 | $shared->guesserQueue = new MessageQueue();
1161 | $shared->past_guesses = [];
1162 | $shared->guess_count = 0;
1163 |
1164 | // 2. Create and configure agent nodes and flows
1165 | $hinterNode = new HinterAgent();
1166 | $guesserNode = new GuesserAgent();
1167 |
1168 | $hinterNode->on('continue')->next($hinterNode); // Loop
1169 | $guesserNode->on('continue')->next($guesserNode); // Loop
1170 |
1171 | $hinterFlow = new AsyncFlow($hinterNode);
1172 | $guesserFlow = new AsyncFlow($guesserNode);
1173 |
1174 | // ... set params for each flow ...
1175 |
1176 | // 3. Start the game and run flows concurrently
1177 | $shared->hinterQueue->put("START");
1178 | await(\React\Promise\all([
1179 | $hinterFlow->run_async($shared),
1180 | $guesserFlow->run_async($shared)
1181 | ]));
1182 | })();
1183 | }
1184 | ```
1185 |
1186 | **`nodes.php`**
1187 | ```php
1188 | hinterQueue->get(); // Wait for a message
1196 | }
1197 | public function exec_async(mixed $message): PromiseInterface {
1198 | // ... generate a hint based on the message ...
1199 | }
1200 | public function post_async(stdClass $shared, mixed $p, mixed $hint): PromiseInterface {
1201 | return async(function() use ($shared, $hint) {
1202 | $shared->guesserQueue->put($hint); // Send hint to the other agent
1203 | return "continue";
1204 | })();
1205 | }
1206 | }
1207 |
1208 | class GuesserAgent extends AsyncNode {
1209 | public function prep_async(stdClass $shared): PromiseInterface {
1210 | return $shared->guesserQueue->get(); // Wait for a hint
1211 | }
1212 | // ... exec and post methods to make a guess and send it back ...
1213 | }
1214 | ```
1215 |
1216 | ---
1217 |
1218 | ### 4: Utility Function
1219 |
1220 | # Utility Functions
1221 |
1222 | PocketFlow-PHP intentionally does not include built-in wrappers for external APIs. This "bring your own" philosophy gives you maximum flexibility and avoids vendor lock-in.
1223 |
1224 | This chapter provides simple, copy-paste-ready snippets for common utility functions. Place these functions in new files inside your `utils/` directory (e.g., `utils/llm_api.php`).
1225 |
1226 | - [LLM Wrapper]
1227 | - [Viz and Debug]
1228 | - [Web Search]
1229 | - [Text Chunking]
1230 | - [Embedding]
1231 | - [Vector Databases]
1232 | - [Text to Speech]
1233 |
1234 | ---
1235 |
1236 | ### 4.1 - 4.7 (Utility Function Examples Pages)
1237 |
1238 | # LLM Wrapper
1239 |
1240 | A centralized function to handle all your LLM API calls is a best practice.
1241 |
1242 | ### Recommended Client: `openai-php/client`
1243 |
1244 | This client is compatible with any OpenAI-compliant API, including OpenRouter, DeepSeek, and many local models.
1245 |
1246 | **`utils/llm_api.php`**
1247 | ```php
1248 | withApiKey(getenv('OPENROUTER_API_KEY'))
1259 | ->withBaseUri('https://openrouter.ai/api/v1')
1260 | ->withHttpHeader('HTTP-Referer', 'http://localhost') // Required by OpenRouter
1261 | ->withHttpHeader('X-Title', 'PocketFlow-PHP')
1262 | ->make();
1263 | }
1264 |
1265 | function call_llm_async(string $model, array $messages): PromiseInterface
1266 | {
1267 | return async(function () use ($model, $messages) {
1268 | try {
1269 | $client = get_openrouter_client();
1270 | $response = $client->chat()->create([
1271 | 'model' => $model,
1272 | 'messages' => $messages,
1273 | ]);
1274 | return $response->choices[0]->message->content;
1275 | } catch (\Exception $e) {
1276 | error_log("API Error: " . $e->getMessage());
1277 | return "Error communicating with the LLM.";
1278 | }
1279 | })();
1280 | }
1281 | ```
1282 |
1283 | # Visualization and Debugging
1284 |
1285 | Simple helpers can make debugging complex flows much easier.
1286 |
1287 | ### Mermaid Graph Generator
1288 |
1289 | This function traverses a `Flow` and generates a Mermaid syntax string, which you can paste into any Mermaid renderer to visualize your graph.
1290 |
1291 | **`utils/visualize.php`**
1292 | ```php
1293 | enqueue($startNode);
1303 | $nodes[$startNode] = "N" . ($nodes->count() + 1);
1304 |
1305 | while (!$queue->isEmpty()) {
1306 | $current = $queue->dequeue();
1307 | $currentId = $nodes[$current];
1308 | $className = (new \ReflectionClass($current))->getShortName();
1309 | $dot .= " {$currentId}[\"{$className}\"]\n";
1310 |
1311 | foreach ($current->getSuccessors() as $action => $successor) {
1312 | if (!$successor) continue;
1313 | if (!$nodes->contains($successor)) {
1314 | $nodes[$successor] = "N" . ($nodes->count() + 1);
1315 | $queue->enqueue($successor);
1316 | }
1317 | $successorId = $nodes[$successor];
1318 | $dot .= " {$currentId} -- \"{$action}\" --> {$successorId}\n";
1319 | }
1320 | }
1321 | return $dot;
1322 | }
1323 | ```
1324 |
1325 | # Web Search
1326 |
1327 | A simple wrapper for a web search API. The DuckDuckGo API is free and requires no API key, making it great for testing.
1328 |
1329 | **`utils/web_search.php`**
1330 | ```php
1331 | ['header' => "User-Agent: PocketFlow-PHP/1.0\r\n"]];
1336 | $context = stream_context_create($options);
1337 | $response = @file_get_contents($url, false, $context);
1338 |
1339 | if ($response === false) {
1340 | return "Error fetching search results.";
1341 | }
1342 |
1343 | $data = json_decode($response, true);
1344 | return $data['AbstractText'] ?? 'No results found.';
1345 | }
1346 | ```
1347 |
1348 | # Text Chunking
1349 |
1350 | Breaking large texts into smaller, manageable chunks is a core task in RAG systems.
1351 |
1352 | **`utils/chunking.php`**
1353 | ```php
1354 | embeddings()->create([
1388 | 'model' => $model,
1389 | 'input' => $text,
1390 | ]);
1391 | return $response->embeddings[0]->embedding;
1392 | })();
1393 | }
1394 | ```
1395 | # Vector Databases
1396 |
1397 | Storing and retrieving embeddings requires a vector database. Here is a very simple in-memory example for prototyping. For production, use a dedicated service like Pinecone, Qdrant, or Redis.
1398 |
1399 | **`utils/memory_vector_store.php`**
1400 | ```php
1401 | vectors[$id] = $vector;
1410 | $this->metadata[$id] = $meta;
1411 | }
1412 |
1413 | public function search(array $queryVector, int $topK = 1): array
1414 | {
1415 | $distances = [];
1416 | foreach ($this->vectors as $id => $vector) {
1417 | // Cosine similarity calculation
1418 | $dotProduct = 0;
1419 | $normA = 0;
1420 | $normB = 0;
1421 | for ($i = 0; $i < count($vector); $i++) {
1422 | $dotProduct += $queryVector[$i] * $vector[$i];
1423 | $normA += $queryVector[$i] ** 2;
1424 | $normB += $vector[$i] ** 2;
1425 | }
1426 | $similarity = $dotProduct / (sqrt($normA) * sqrt($normB));
1427 | $distances[$id] = $similarity;
1428 | }
1429 |
1430 | arsort($distances); // Sort by similarity, descending
1431 |
1432 | $results = [];
1433 | foreach (array_slice($distances, 0, $topK, true) as $id => $score) {
1434 | $results[] = [
1435 | 'id' => $id,
1436 | 'score' => $score,
1437 | 'metadata' => $this->metadata[$id]
1438 | ];
1439 | }
1440 | return $results;
1441 | }
1442 | }
1443 | ```
1444 | # Text to Speech (TTS)
1445 |
1446 | Generate audio from text using a TTS service like ElevenLabs.
1447 |
1448 | **`utils/tts.php`**
1449 | ```php
1450 | post("https://api.elevenlabs.io/v1/text-to-speech/{$voiceId}", [
1461 | 'headers' => [
1462 | 'Accept' => 'audio/mpeg',
1463 | 'Content-Type' => 'application/json',
1464 | 'xi-api-key' => $apiKey,
1465 | ],
1466 | 'json' => [
1467 | 'text' => $text,
1468 | 'model_id' => 'eleven_monolingual_v1',
1469 | ],
1470 | 'sink' => $outputFilePath // Stream response directly to a file
1471 | ]);
1472 | echo "Audio saved to {$outputFilePath}\n";
1473 | } catch (\Exception $e) {
1474 | echo "Error generating audio: " . $e->getMessage() . "\n";
1475 | }
1476 | }
1477 | ```
1478 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # Agentic Coding with PocketFlow-PHP
2 |
3 | > **Attention AI Agent:** This is your primary instruction manual. Follow these rules strictly to generate correct, robust, and bug-free PHP code for the PocketFlow-PHP framework. Failure to adhere to these rules will result in errors.
4 |
5 | ## The Golden Rules
6 |
7 | 1. **THE CORE FRAMEWORK IS READ-ONLY.** The file `src/PocketFlow.php` is the engine. You are **strictly forbidden** from ever editing it.
8 |
9 | 2. **USE THE THREE MAIN FILES.** All your application code **must** be written in these three files in the project root:
10 | - `nodes.php`: For all `Node`, `BatchNode`, and `AsyncNode` class definitions.
11 | - `flow.php`: For functions that create and wire up `Flows`.
12 | - `main.php`: The single entry point that calls a function from `flow.php`.
13 |
14 | 3. **UTILITIES GO IN `utils/`.** Any function that communicates with the outside world (e.g., calling an LLM API, a database, or a web search) **must** be placed in a new file inside the `utils/` directory.
15 |
16 | 4. **STATE MANAGEMENT IS CRITICAL.**
17 | - **DO** use the `$shared` object (`stdClass`) to manage all **mutable application state** (e.g., results, counters, lists of items).
18 | - **DO NOT** use `static` properties or variables inside Node classes to manage state. This will fail in loops.
19 | - **DO NOT** use `$this->params` to store mutable state. It is for **immutable configuration** only.
20 |
21 | 5. **STRICT TYPE COMPATIBILITY & SCOPE.**
22 | - The `post()` method of a `Node` **must** have the return type `?string`. If it does not decide the next action, it **must** end with `return null;`.
23 | - All `_async` methods (`prep_async`, `exec_async`, `post_async`) **must** have the return type `React\Promise\PromiseInterface`.
24 | - To return a promise, **always** use the pattern `return async(function() { ... })();`. Do not forget the final `()`.
25 | - **DO NOT** use `use ($this)` in closures. To access `$this->params` inside a `post_async` or `exec_async` closure, read the required values into local variables *before* the closure and pass them in with `use()`.
26 |
27 | 6. **ALWAYS IMPORT CLASSES WITH `use`.**
28 | - Any file that references a class (e.g., `AsyncNode`, `Flow`, `PromiseInterface`) **must** include a `use` statement for that class at the top of the file.
29 |
30 | 7. **DEFINE NODE CONNECTIONS *BEFORE* CREATING THE FLOW.**
31 | - The `Flow` class does not have a public method to retrieve its start node. Therefore, you **must** configure all node connections (`->next()`, `->on()->next()`) *before* passing the start node to the `Flow` constructor. This is especially important for creating loops.
32 | - **Correct Pattern:**
33 | ```php
34 | $myStartNode = new MyNode();
35 | $nextNode = new NextNode();
36 | $myStartNode->on('continue')->next($myStartNode); // Define loop on the node itself
37 | $myStartNode->on('finish')->next($nextNode);
38 | $flow = new Flow($myStartNode); // Then create the flow with the fully wired node
39 | ```
40 |
41 | 8. **USE PHP 8.3 FEATURES WISELY.**
42 | - **DO** use constructor property promotion for cleaner `Node` classes.
43 | - **DO** use the `match` expression inside `post()` for clean, readable action routing.
44 | - **DO NOT** use features like Fibers directly. Rely on the `async()` and `await()` functions provided by `react/async`.
45 |
46 | 9. **ENABLE STRICT TYPES.**
47 | - To prevent common type-related errors, **every new PHP file** you create (`nodes.php`, `flow.php`, `main.php`, and all utility files) **must** start with the following declaration on the very first line:
48 | ```php
49 | "Build a system that summarizes news articles from a list of URLs."
63 |
64 | 2. **AI: Propose a Plan.** Based on the documentation, the AI proposes a plan using PocketFlow concepts.
65 | > "I will create a `BatchFlow` to process each URL. The sub-flow will have two nodes: `FetchArticleNode` to download the content, and `SummarizeNode` to call an LLM. The final summaries will be stored in `$shared->summaries`."
66 |
67 | 3. **Human: Approve the Plan.** The human confirms the logic.
68 |
69 | 4. **AI: Generate the Code.** The AI writes the code for `utils/`, `nodes.php`, `flow.php`, and `main.php`, strictly following the Golden Rules and the Mandatory Project Structure.
70 |
71 | 5. **Human & AI: Test and Iterate.** The human runs `php main.php`. If there are errors, the AI debugs them by reviewing the Golden Rules.
72 |
73 | This collaborative process, guided by this documentation, is the fastest and most reliable way to build LLM applications with PocketFlow-PHP.
74 |
75 | ## Mandatory Project Structure
76 |
77 | Your final project **must** adhere to this structure. Do **not** create additional top-level directories or files, unless stated otherwise by the user, e.g. when asked to add a frontend.
78 |
79 | /
80 | ├── main.php # The application's single entry point.
81 | ├── nodes.php # All Node class definitions.
82 | ├── flow.php # All Flow creation functions.
83 | ├── utils/ # Directory for all helper functions (API calls, etc.).
84 | │ └── ... # e.g., llm_api.php, web_search.php
85 | ├── composer.json # Managed by Composer. Do not edit manually.
86 | └── src/
87 | └── PocketFlow.php # The core framework. READ-ONLY.
88 |
89 | ---
90 |
91 | ### 1: Introduction to PocketFlow-PHP
92 |
93 | # PocketFlow-PHP
94 |
95 |
96 | A minimalist LLM framework for PHP, written in 368 lines of PHP.
97 |
98 | Build complex Agents, Workflows, and RAG systems with a tiny, powerful core.
99 |
100 |
101 | ---
102 |
103 | **PocketFlow-PHP** is a port of the minimalist Python LLM framework, bringing its core principles to the PHP ecosystem. It's designed for developers who want maximum control and flexibility without the bloat of larger frameworks. It is optimized for **Agentic Coding**, where humans design the system and an AI agent writes the code.
104 |
105 | - **Lightweight**: The entire framework core is in a single, well-tested file.
106 | - **Expressive**: Build everything you love from larger frameworks—Multi-Agent systems, RAG pipelines, and complex workflows—using simple, composable building blocks.
107 |
108 | ## How does it work?
109 |
110 | The core abstraction is a **Graph + Shared Store**.
111 |
112 | 1. **Node**: The smallest unit of work (e.g., call an LLM, read a file).
113 | 2. **Flow**: Connects Nodes into a graph. Transitions are determined by simple string "Actions".
114 | 3. **Shared Store**: A simple `stdClass` object passed through the entire flow, allowing nodes to communicate and share state.
115 |
116 | This simple model is all you need to create powerful design patterns:
117 |
118 | ## Getting Started in 60 Seconds
119 |
120 | Get up and running with your first AI-powered application.
121 |
122 | **Prerequisites:** PHP 8.3+ and [Composer](https://getcomposer.org/) must be installed.
123 |
124 | 1. **Clone the repository:**
125 | ```bash
126 | git clone https://github.com/weise25/PocketFlow-PHP.git
127 | cd PocketFlow-PHP
128 | ```
129 |
130 | 2. **Install dependencies:**
131 | ```bash
132 | composer install
133 | ```
134 |
135 | 3. **Set up your environment:**
136 | - Create a `.env` file in the root of the project.
137 | - Add your API keys to the `.env` file (e.g., `OPENROUTER_API_KEY=...`).
138 |
139 | ### Chapter 2: Core Abstraction
140 |
141 | # Core Abstraction
142 |
143 | This chapter explains the fundamental building blocks of **PocketFlow-PHP**. Understanding these concepts is essential for building any application with the framework.
144 |
145 | We model the LLM workflow as a **Graph + Shared Store**.
146 |
147 | - [Node] – The smallest unit of work.
148 | - [Flow] – The orchestrator that connects Nodes.
149 | - [Communication] – How Nodes and Flows share data.
150 | - [Batch] – Tools for handling multiple items efficiently.
151 | - [(Advanced) Async] – For non-blocking, I/O-bound tasks.
152 | - [(Advanced) Parallel] – For running multiple async tasks concurrently.
153 |
154 |
155 | ---
156 |
157 | ### 2.1: Node
158 |
159 | # Node
160 |
161 | A **`Node`** is the smallest building block in PocketFlow. It represents a single, concrete step in a workflow. Each Node follows a simple, three-step lifecycle: `prep() -> exec() -> post()`.
162 |
163 |
164 | ### 1. The Lifecycle
165 |
166 | 1. **`prep(stdClass $shared): mixed`**
167 | * **Purpose:** To read data from the `$shared` store and prepare it for the main task.
168 | * **Example:** Fetch a user ID from `$shared->user->id`.
169 | * **Returns:** The data needed by the `exec()` method.
170 |
171 | 2. **`exec(mixed $prepResult): mixed`**
172 | * **Purpose:** To perform the core computation. This is where you call LLMs, APIs, or run business logic.
173 | * **Rule:** This method **must not** access the `$shared` store directly. It should be a "pure" function that only works with the data it receives from `prep()`.
174 | * **Returns:** The result of the computation.
175 |
176 | 3. **`post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string`**
177 | * **Purpose:** To write the results from `exec()` back into the `$shared` store and to decide what happens next.
178 | * **Returns:** An **action string**. This string determines which path the `Flow` will take. Returning `null` triggers the default path.
179 | * > **AI Agent Rule:** The return type of this method **must** be `?string`. If you don't need to return a specific action, you **must** `return null;`.
180 |
181 | ### 2. Fault Tolerance
182 |
183 | You can make your Nodes resilient to failure using constructor arguments and a fallback method.
184 |
185 | **Retries:**
186 | Define the number of retries and the wait time in the constructor.
187 |
188 | ```php
189 | // nodes.php
190 | use PocketFlow\Node;
191 |
192 | class ApiCallNode extends Node
193 | {
194 | public function __construct()
195 | {
196 | // Try exec() up to 3 times, wait 5 seconds between failures.
197 | parent::__construct(maxRetries: 3, wait: 5);
198 | }
199 |
200 | public function exec(mixed $prepResult): string
201 | {
202 | // This might throw an exception if the API is down.
203 | return call_external_api();
204 | }
205 | }
206 | ```
207 |
208 | **Graceful Fallback:**
209 | If all retries fail, the `execFallback()` method is called. Instead of crashing, you can return a safe default value.
210 |
211 | ```php
212 | // nodes.php
213 | use PocketFlow\Node;
214 |
215 | class ApiCallNode extends Node
216 | {
217 | // ... constructor and exec() method ...
218 |
219 | public function execFallback(mixed $prepResult, \Throwable $e): string
220 | {
221 | // Log the error and return a safe default.
222 | error_log("API call failed after all retries: " . $e->getMessage());
223 | return "Default value due to API failure.";
224 | }
225 | }
226 | ```
227 |
228 | ### 3. Example: A Complete Node
229 |
230 | ```php
231 | // nodes.php
232 | use PocketFlow\Node;
233 |
234 | class SummarizeArticleNode extends Node
235 | {
236 | public function __construct()
237 | {
238 | parent::__construct(maxRetries: 2, wait: 3);
239 | }
240 |
241 | public function prep(stdClass $shared): ?string
242 | {
243 | return $shared->article_text ?? null;
244 | }
245 |
246 | public function exec(mixed $articleText): string
247 | {
248 | if (empty($articleText)) {
249 | return "No text provided.";
250 | }
251 | $prompt = "Summarize this article: " . $articleText;
252 | return call_llm($prompt); // This might fail.
253 | }
254 |
255 | public function execFallback(mixed $prepResult, \Throwable $e): string
256 | {
257 | return "Could not generate summary due to an error.";
258 | }
259 |
260 | public function post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string
261 | {
262 | $shared->summary = $execResult;
263 | return null; // Triggers the "default" action.
264 | }
265 | }
266 | ```
267 |
268 | ---
269 | ## 2.2: Flow
270 |
271 | A **`Flow`** is the orchestrator. It connects `Nodes` together to form a process graph. The path through the graph is determined by the **action strings** returned by each Node's `post()` method.
272 |
273 | ### 1. Action-Based Transitions
274 |
275 | You define the connections between nodes using a simple, fluent API.
276 |
277 | **Default Transition:**
278 | If a Node's `post()` method returns `null` (or the string `"default"`), the default path is taken.
279 |
280 | ```php
281 | // flow.php
282 | $nodeA = new NodeA();
283 | $nodeB = new NodeB();
284 |
285 | // If NodeA returns null, execute NodeB next.
286 | $nodeA->next($nodeB);
287 |
288 | $flow = new Flow($nodeA);
289 | ```
290 |
291 | **Conditional Transitions:**
292 | You can create branches in your logic based on specific action strings.
293 |
294 | ```php
295 | // flow.php
296 | $reviewNode = new ReviewNode();
297 | $approveNode = new ApproveNode();
298 | $rejectNode = new RejectNode();
299 |
300 | // If reviewNode returns "approved", go to approveNode.
301 | $reviewNode->on('approved')->next($approveNode);
302 |
303 | // If reviewNode returns "rejected", go to rejectNode.
304 | $reviewNode->on('rejected')->next($rejectNode);
305 |
306 | $flow = new Flow($reviewNode);
307 | ```
308 |
309 | ### 2. Nested Flows (Composition)
310 |
311 | A `Flow` itself behaves like a `Node`. This allows you to build complex workflows by composing smaller, reusable flows.
312 |
313 | When a `Flow` is used as a node, the action returned by the **last node** of that inner flow is used to determine the next step in the outer flow.
314 |
315 | **Example:**
316 |
317 | ```php
318 | // flow.php
319 | use PocketFlow\Flow;
320 |
321 | // --- Sub-Flow 1: User Validation ---
322 | $checkUser = new CheckUserNode();
323 | $checkPermissions = new CheckPermissionsNode();
324 | $checkUser->next($checkPermissions);
325 | $validationFlow = new Flow($checkUser); // This flow might return "validation_failed"
326 |
327 | // --- Sub-Flow 2: Data Processing ---
328 | $loadData = new LoadDataNode();
329 | $processData = new ProcessDataNode();
330 | $loadData->next($processData);
331 | $processingFlow = new Flow($loadData); // This flow might return "processing_complete"
332 |
333 | // --- Main Flow: Composing the sub-flows ---
334 | $endNode = new EndNode();
335 | $errorNode = new ErrorNode();
336 |
337 | // Wire the main flow
338 | $validationFlow->next($processingFlow); // Default transition
339 | $validationFlow->on('validation_failed')->next($errorNode);
340 | $processingFlow->on('processing_complete')->next($endNode);
341 |
342 | $mainFlow = new Flow($validationFlow);
343 | $mainFlow->run($shared);
344 | ```
345 |
346 | This creates a clear, modular structure:
347 | ```mermaid
348 | graph TD
349 | subgraph Main Flow
350 | A(validationFlow) --> B(processingFlow);
351 | A --> C(errorNode);
352 | B --> D(endNode);
353 | end
354 | ```
355 | ---
356 |
357 | # 2.3: Communication
358 |
359 | Nodes and Flows need to share data. PocketFlow provides two mechanisms for this, each with a distinct purpose.
360 |
361 | ### 1. The Shared Store (`$shared`)
362 |
363 | This is the **primary** method of communication and state management.
364 |
365 | - **What it is:** A single `stdClass` object that is passed by reference to every Node in a Flow.
366 | - **Purpose:** To hold all **mutable application state**. This includes initial inputs, intermediate results, final outputs, counters, flags, etc.
367 | - **When to use:** For almost everything. It acts as the central "memory" or "database" for your workflow.
368 | - **How to use:**
369 | - Read from it in `prep()`.
370 | - Write to it in `post()`.
371 |
372 | > **AI Agent Golden Rule:** The `$shared` store is the single source of truth for your application's state. Any data that changes or needs to be passed between nodes **must** live in `$shared`.
373 |
374 | **Example:**
375 |
376 | ```php
377 | // main.php
378 | $shared = new stdClass();
379 | $shared->user_id = 123;
380 | $shared->user_data = null; // To be filled by a node
381 | $shared->final_report = null; // To be filled by another node
382 |
383 | $flow->run($shared);
384 |
385 | // After the flow runs, $shared will be populated.
386 | echo $shared->final_report;
387 | ```
388 |
389 | ### 2. Params (`$params`)
390 |
391 | This is a **secondary** mechanism for configuration.
392 |
393 | - **What it is:** An `array` that is passed by value to each Node.
394 | - **Purpose:** To hold **immutable configuration** that does not change during the flow's execution.
395 | - **When to use:** For things like model names, API endpoints, or identifiers in a `BatchFlow`.
396 | - **How to use:**
397 | - Set once on the top-level `Flow` using `$flow->setParams([...])`.
398 | - Access within any Node via `$this->params['my_key']`.
399 |
400 | > **AI Agent Warning:** Do not use `$params` to store state that needs to change (like counters or lists of results). The `$params` array is reset for each node in a loop, so any changes you make will be lost. Use `$shared` for that.
401 |
402 | **Example:**
403 |
404 | ```php
405 | // flow.php
406 | $node = new ApiCallNode();
407 | $flow = new Flow($node);
408 |
409 | // Set the API endpoint once for the entire flow.
410 | $flow->setParams(['api_endpoint' => 'https://api.example.com/v1']);
411 | $flow->run($shared);
412 |
413 | // nodes.php
414 | class ApiCallNode extends Node
415 | {
416 | public function exec(mixed $prepResult): string
417 | {
418 | $endpoint = $this->params['api_endpoint'];
419 | // ... make a call to the endpoint ...
420 | return "Data from API";
421 | }
422 | }
423 | ```
424 | ---
425 |
426 | ### 2.4: Batch
427 |
428 | # Batch Processing
429 |
430 | PocketFlow provides two powerful classes for processing multiple items: `BatchNode` and `BatchFlow`.
431 |
432 | ### 1. `BatchNode`
433 |
434 | Use `BatchNode` when you have a list of **data items** and want to perform the **same operation** on each one. This is the "Map" part of a Map-Reduce pattern.
435 |
436 | **How it works:**
437 | - `prep(stdClass $shared)`: Must return an `array` of items to be processed.
438 | - `exec(mixed $item)`: Is called **sequentially** for each `$item` in the array from `prep()`.
439 | - `post(stdClass $shared, ..., array $execResultList)`: Receives an `array` containing the results of all `exec()` calls.
440 |
441 | **Example: Summarize Text Chunks**
442 |
443 | ```php
444 | // nodes.php
445 | use PocketFlow\BatchNode;
446 |
447 | class SummarizeChunksNode extends BatchNode
448 | {
449 | public function prep(stdClass $shared): array
450 | {
451 | // Assume $shared->text_chunks is an array of strings.
452 | return $shared->text_chunks;
453 | }
454 |
455 | public function exec(mixed $chunk): string
456 | {
457 | // This method is called for each chunk.
458 | return call_llm("Summarize this chunk: " . $chunk);
459 | }
460 |
461 | public function post(stdClass $shared, mixed $p, mixed $summaries): ?string
462 | {
463 | // $summaries is an array of all the individual summaries.
464 | $shared->combined_summary = implode("\n", $summaries);
465 | return null;
466 | }
467 | }
468 | ```
469 |
470 | ### 2. `BatchFlow`
471 |
472 | Use `BatchFlow` when you want to run an entire **sub-flow** multiple times, each time with a different **configuration**.
473 |
474 | **How it works:**
475 | - `prep(stdClass $shared)`: Must return an `array` of **parameter arrays**.
476 | - The sub-flow is executed once for each parameter array.
477 | - Inside the sub-flow's nodes, the parameters are available via `$this->params`.
478 |
479 | **Example: Process Multiple Files**
480 |
481 | ```php
482 | // flow.php
483 | use PocketFlow\Flow;
484 | use PocketFlow\BatchFlow;
485 |
486 | function create_file_processing_flow(): Flow
487 | {
488 | // The sub-flow that processes a single file.
489 | $loadFileNode = new LoadFileNode();
490 | $analyzeFileNode = new AnalyzeFileNode();
491 | $loadFileNode->next($analyzeFileNode);
492 | $singleFileFlow = new Flow($loadFileNode);
493 |
494 | // The BatchFlow that orchestrates the process.
495 | $allFilesFlow = new ProcessAllFilesBatchFlow($singleFileFlow);
496 | return $allFilesFlow;
497 | }
498 |
499 | // nodes.php
500 | use PocketFlow\Node;
501 | use PocketFlow\BatchFlow;
502 |
503 | class ProcessAllFilesBatchFlow extends BatchFlow
504 | {
505 | public function prep(stdClass $shared): array
506 | {
507 | // Returns an array of parameter sets.
508 | // e.g., [['filename' => 'a.txt'], ['filename' => 'b.txt']]
509 | return array_map(fn($f) => ['filename' => $f], $shared->filenames);
510 | }
511 | }
512 |
513 | class LoadFileNode extends Node
514 | {
515 | public function exec(mixed $p): string
516 | {
517 | // Accesses the filename from the params set by the BatchFlow.
518 | $filename = $this->params['filename'];
519 | echo "Loading file: {$filename}\n";
520 | return file_get_contents($filename);
521 | }
522 |
523 | public function post(stdClass $shared, mixed $p, mixed $content): ?string
524 | {
525 | $shared->current_content = $content;
526 | return null;
527 | }
528 | }
529 |
530 | class AnalyzeFileNode extends Node { /* ... analyzes $shared->current_content ... */ }
531 | ```
532 | ---
533 |
534 | ### Seite 2.5: (Advanced) Async
535 | # (Advanced) Async
536 |
537 | Use `AsyncNode` and `AsyncFlow` for tasks that are **I/O-bound**, such as API calls, database queries, or reading large files. This allows your application to perform other work while waiting for these operations to complete, making it much more efficient. All async operations are powered by **ReactPHP**.
538 |
539 | ### 1. `AsyncNode`
540 |
541 | An `AsyncNode` is similar to a regular `Node`, but its lifecycle methods are asynchronous.
542 |
543 | **Rules for `AsyncNode`:**
544 | 1. Implement the `_async` versions of the lifecycle methods: `prep_async()`, `exec_async()`, `post_async()`.
545 | 2. Each of these methods **must** return a `React\Promise\PromiseInterface`.
546 | 3. The easiest way to return a promise is to wrap your logic in `return async(function() { ... })();`.
547 | 4. Inside an `async` block, you can use `await()` to wait for other promises to resolve.
548 |
549 | **Example: Asynchronous API Call**
550 |
551 | ```php
552 | // nodes.php
553 | use PocketFlow\AsyncNode;
554 | use React\Promise\PromiseInterface;
555 | use function React\Async\async;
556 | use function React\Async\await;
557 |
558 | class FetchUserDataNode extends AsyncNode
559 | {
560 | public function prep_async(stdClass $shared): PromiseInterface
561 | {
562 | return async(fn() => $shared->user_id)();
563 | }
564 |
565 | public function exec_async(mixed $userId): PromiseInterface
566 | {
567 | // Assume call_user_api_async() returns a Promise.
568 | return call_user_api_async($userId);
569 | }
570 |
571 | public function post_async(stdClass $shared, mixed $p, mixed $userData): PromiseInterface
572 | {
573 | return async(function() use ($shared, $userData) {
574 | $shared->user_data = $userData;
575 | return null;
576 | })();
577 | }
578 | }
579 | ```
580 |
581 | ### 2. `AsyncFlow`
582 |
583 | An `AsyncFlow` is used to orchestrate flows that contain at least one `AsyncNode`. It can also correctly orchestrate regular synchronous `Nodes`.
584 |
585 | ### 3. Running an Async Workflow
586 |
587 | The top-level entry point of any application that uses async features **must** be wrapped in an `async` block.
588 |
589 | **Example:**
590 |
591 | ```php
592 | // main.php
593 | require_once __DIR__ . '/vendor/autoload.php';
594 | require_once __DIR__ . '/flow.php';
595 | use function React\Async\async;
596 | use function React\Async\await;
597 |
598 | // Wrap the entire application logic in async().
599 | async(function() {
600 | $shared = new stdClass();
601 | $shared->user_id = 123;
602 |
603 | $flow = create_user_data_flow(); // This flow returns an AsyncFlow
604 |
605 | // Use await() to run the flow and wait for it to complete.
606 | await($flow->run_async($shared));
607 |
608 | print_r($shared->user_data);
609 | })();
610 | ```
611 | ---
612 |
613 | ### 2.6: (Advanced) Parallel
614 |
615 | # (Advanced) Parallel Execution
616 |
617 | Parallel execution in PocketFlow-PHP means running multiple **asynchronous** tasks **concurrently**. This is extremely useful for speeding up I/O-bound workflows where tasks don't depend on each other.
618 |
619 | > **Note:** This is **concurrency**, not true parallelism. PHP's single-threaded nature means CPU-bound tasks won't run faster. However, for waiting on multiple API calls or database queries at once, the performance gain is significant.
620 |
621 | ### 1. `AsyncParallelBatchNode`
622 |
623 | This is the parallel version of `AsyncBatchNode`. It calls `exec_async()` for **all items at the same time** and waits for all of them to complete.
624 |
625 | **Use Case:** Fetching data for multiple users from an API simultaneously.
626 |
627 | **Example: Parallel API Calls**
628 |
629 | ```php
630 | // nodes.php
631 | use PocketFlow\AsyncParallelBatchNode;
632 | use React\Promise\PromiseInterface;
633 | use function React\Async\async;
634 |
635 | class FetchAllUsersNode extends AsyncParallelBatchNode
636 | {
637 | public function prep_async(stdClass $shared): PromiseInterface
638 | {
639 | // Returns an array of user IDs.
640 | return async(fn() => $shared->user_ids)();
641 | }
642 |
643 | public function exec_async(mixed $userId): PromiseInterface
644 | {
645 | // This is called concurrently for all user IDs.
646 | // Assume call_user_api_async() returns a Promise.
647 | return call_user_api_async($userId);
648 | }
649 |
650 | public function post_async(stdClass $shared, mixed $p, mixed $usersData): PromiseInterface
651 | {
652 | return async(function() use ($shared, $usersData) {
653 | // $usersData is an array of results. The order is preserved.
654 | $shared->users = $usersData;
655 | return null;
656 | })();
657 | }
658 | }
659 | ```
660 |
661 | ### 2. `AsyncParallelBatchFlow`
662 |
663 | This is the parallel version of `AsyncBatchFlow`. It runs the entire sub-flow for **all parameter sets concurrently**.
664 |
665 | **Use Case:** Processing multiple files, where each file requires a multi-step async workflow (e.g., download, analyze, upload).
666 |
667 | **Example: Parallel File Processing**
668 |
669 | ```php
670 | // flow.php
671 | use PocketFlow\AsyncFlow;
672 | use PocketFlow\AsyncParallelBatchFlow;
673 |
674 | function create_parallel_file_flow(): AsyncFlow
675 | {
676 | // The sub-flow for a single file.
677 | $downloadNode = new DownloadFileAsyncNode();
678 | $analyzeNode = new AnalyzeFileAsyncNode();
679 | $downloadNode->next($analyzeNode);
680 | $singleFileFlow = new AsyncFlow($downloadNode);
681 |
682 | // The parallel batch flow.
683 | $parallelFlow = new ProcessAllFilesParallelBatchFlow($singleFileFlow);
684 | return $parallelFlow;
685 | }
686 |
687 | // nodes.php
688 | use PocketFlow\AsyncParallelBatchFlow;
689 | use React\Promise\PromiseInterface;
690 | use function React\Async\async;
691 |
692 | class ProcessAllFilesParallelBatchFlow extends AsyncParallelBatchFlow
693 | {
694 | public function prep_async(stdClass $shared): PromiseInterface
695 | {
696 | // Returns an array of parameter sets.
697 | return async(fn() => array_map(fn($f) => ['url' => $f], $shared->file_urls))();
698 | }
699 | }
700 |
701 | // DownloadFileAsyncNode and AnalyzeFileAsyncNode are defined as regular AsyncNodes.
702 | // They will access the URL via $this->params['url'].
703 | ```
704 |
705 | > **Warning:** Be mindful of API rate limits when using parallel execution. A large number of concurrent requests can quickly exhaust your quota.
706 |
707 | # Chapter 3: Design Pattern
708 |
709 | # Design Patterns
710 |
711 | Design patterns are reusable blueprints for solving common problems in LLM application development. PocketFlow-PHP's simple core abstractions (`Node`, `Flow`, `Shared Store`) are all you need to implement these powerful patterns.
712 |
713 | This chapter provides practical, copy-paste-ready examples for each pattern.
714 |
715 | - [Agent] – For creating autonomous, decision-making entities.
716 | - [Workflow] – For breaking down complex tasks into a sequence of simple steps.
717 | - [RAG (Retrieval-Augmented Generation)] – For providing external knowledge to an LLM.
718 | - [Map-Reduce] – For processing large amounts of data efficiently.
719 | - [Structured Output] – For forcing an LLM to return clean, predictable data formats.
720 | - [(Advanced) Multi-Agent Systems] – For orchestrating collaboration between multiple specialized agents.
721 | ---
722 |
723 | ### Seite 3.1: Agent
724 |
725 | # Agent
726 |
727 | An **Agent** is a `Node` that can make autonomous decisions. Instead of following a fixed path, it analyzes the current state in the `$shared` store and returns a specific **action string** to direct the `Flow` down different branches, often looping back to itself to create a cycle of reasoning and action.
728 |
729 | ### The Agentic Loop
730 |
731 | The core of an agent is a loop:
732 | 1. **Think:** The Agent `Node` analyzes the state (`$shared`) and the goal.
733 | 2. **Decide:** It calls an LLM to choose the next `action` from a predefined set of tools.
734 | 3. **Act:** The `Flow` routes to a `Tool Node` based on the chosen action.
735 | 4. **Observe:** The `Tool Node` executes its task (e.g., a web search) and updates the `$shared` store with the result.
736 | 5. The `Flow` loops back to the Agent `Node` to start the cycle again with new information.
737 |
738 | ```mermaid
739 | graph TD
740 | A[Agent Node (Think & Decide)] -- action: 'search' --> B[Search Tool Node (Act)];
741 | A -- action: 'answer' --> C[Answer Node (Final Act)];
742 | B -- result --> D(Update Shared Store);
743 | D --> A;
744 | ```
745 |
746 | ### Example: A Simple Research Agent
747 |
748 | This agent can decide whether to search the web for more information or to answer a question based on the context it has already gathered.
749 |
750 | **`flow.php`**
751 | ```php
752 | on('search')->next($searchNode);
764 | $decideNode->on('answer')->next($answerNode);
765 | $searchNode->next($decideNode); // Loop back to the decision node after acting
766 |
767 | return new Flow($decideNode);
768 | }
769 | ```
770 |
771 | **`nodes.php`**
772 | ```php
773 | search_history) ? "Nothing yet." : implode("\n", $shared->search_history);
785 | return [$shared->query, $context];
786 | }
787 |
788 | public function exec(mixed $prepResult): array
789 | {
790 | [$query, $context] = $prepResult;
791 | $prompt = <<current_search_term = $decision['search_term'];
816 | }
817 | return $decision['action'];
818 | }
819 | }
820 |
821 | class SearchWebNode extends Node
822 | {
823 | public function prep(stdClass $shared): string
824 | {
825 | return $shared->current_search_term;
826 | }
827 |
828 | public function exec(mixed $searchTerm): string
829 | {
830 | echo "Agent is searching for: {$searchTerm}\n";
831 | return web_search($searchTerm); // Assume web_search() is defined
832 | }
833 |
834 | public function post(stdClass $shared, mixed $p, mixed $searchResult): ?string
835 | {
836 | $shared->search_history[] = $searchResult;
837 | return null; // Triggers default transition back to DecideActionNode
838 | }
839 | }
840 |
841 | class AnswerNode extends Node
842 | {
843 | public function exec(mixed $p): string
844 | {
845 | // In a real app, this would also use prep() to get context
846 | return "This is the final answer based on the research.";
847 | }
848 |
849 | public function post(stdClass $shared, mixed $p, mixed $answer): ?string
850 | {
851 | $shared->final_answer = $answer;
852 | return null; // End of flow
853 | }
854 | }
855 | ```
856 | ---
857 |
858 | ### Chapter 3.2 - 3.6 (Workflow, RAG, etc.)
859 |
860 | # Workflow
861 |
862 | A **Workflow** is the simplest design pattern. It involves chaining a sequence of `Nodes` together to break a complex task into manageable steps. Each node performs one part of the job and passes its result to the next via the `$shared` store.
863 |
864 | ### Example: Article Writing Workflow
865 |
866 | This workflow generates an article by breaking the process into three distinct steps: outlining, drafting, and reviewing.
867 |
868 | **`flow.php`**
869 | ```php
870 | next($draftNode)->next($reviewNode);
882 |
883 | return new Flow($outlineNode);
884 | }
885 | ```
886 |
887 | **`nodes.php`**
888 | ```php
889 | topic; }
894 | public function exec(mixed $topic): string { return "Outline for: {$topic}"; /* call_llm(...) */ }
895 | public function post(stdClass $shared, mixed $p, mixed $outline): ?string {
896 | $shared->outline = $outline;
897 | return null;
898 | }
899 | }
900 |
901 | class DraftArticleNode extends Node {
902 | public function prep(stdClass $shared): string { return $shared->outline; }
903 | public function exec(mixed $outline): string { return "Draft based on: {$outline}"; /* call_llm(...) */ }
904 | public function post(stdClass $shared, mixed $p, mixed $draft): ?string {
905 | $shared->draft = $draft;
906 | return null;
907 | }
908 | }
909 |
910 | class ReviewArticleNode extends Node {
911 | public function prep(stdClass $shared): string { return $shared->draft; }
912 | public function exec(mixed $draft): string { return "Reviewed draft: {$draft}"; /* call_llm(...) */ }
913 | public function post(stdClass $shared, mixed $p, mixed $final): ?string {
914 | $shared->final_article = $final;
915 | return null;
916 | }
917 | }
918 | ```
919 |
920 | # RAG (Retrieval-Augmented Generation)
921 |
922 | RAG allows an LLM to answer questions using information from your own documents. It's a two-stage process.
923 |
924 | ### Stage 1: Offline Indexing (A Workflow)
925 |
926 | This flow reads documents, splits them into chunks, creates vector embeddings for each chunk, and stores them in a vector database.
927 |
928 | **`flow.php` (Offline Indexing)**
929 | ```php
930 | next($embedNode)->next($storeNode);
940 | return new Flow($chunkNode);
941 | }
942 | ```
943 |
944 | **`nodes.php` (Offline Indexing)**
945 | ```php
946 | file_paths; }
953 | public function exec(mixed $path): array { return sentence_chunk(file_get_contents($path)); }
954 | public function post(stdClass $shared, mixed $p, mixed $chunkLists): ?string {
955 | $shared->all_chunks = array_merge(...$chunkLists);
956 | return null;
957 | }
958 | }
959 |
960 | // 2. Embedding (using BatchNode)
961 | class EmbedChunksNode extends BatchNode {
962 | public function prep(stdClass $shared): array { return $shared->all_chunks; }
963 | public function exec(mixed $chunk): array { return get_embedding($chunk); } // Assume get_embedding() exists
964 | public function post(stdClass $shared, mixed $p, mixed $embeddings): ?string {
965 | $shared->embeddings = $embeddings;
966 | return null;
967 | }
968 | }
969 |
970 | // 3. Storing
971 | class StoreEmbeddingsNode extends Node {
972 | public function exec(mixed $p): mixed {
973 | /* ... code to store embeddings in a vector DB ... */
974 | return null; // Must return a value compatible with 'mixed'
975 | }
976 | }
977 | ```
978 |
979 | ### Stage 2: Online Retrieval & Answering (A Workflow)
980 |
981 | This flow takes a user's question, finds the most relevant chunks from the vector database, and passes them to an LLM as context to generate an answer.
982 |
983 | **`flow.php` (Online Answering)**
984 | ```php
985 | next($retrieveDocs)->next($generateAnswer);
995 | return new Flow($embedQuery);
996 | }
997 | ```
998 |
999 | # Map-Reduce
1000 |
1001 | The Map-Reduce pattern is perfect for processing large datasets that can be broken down into smaller, independent parts.
1002 |
1003 | - **Map Phase:** A `BatchNode` processes each part of the data individually.
1004 | - **Reduce Phase:** A regular `Node` takes the list of results from the map phase and combines them into a single, final output.
1005 |
1006 | ### Example: Summarizing Multiple Documents
1007 |
1008 | **`flow.php`**
1009 | ```php
1010 | next($reduceNode);
1019 | return new Flow($mapNode);
1020 | }
1021 | ```
1022 |
1023 | **`nodes.php`**
1024 | ```php
1025 | documents is an associative array ['filename' => 'content', ...]
1035 | return $shared->documents;
1036 | }
1037 |
1038 | public function exec(mixed $content): string
1039 | {
1040 | return call_llm("Summarize this document: " . $content);
1041 | }
1042 |
1043 | public function post(stdClass $shared, mixed $p, mixed $summaries): ?string
1044 | {
1045 | $shared->individual_summaries = $summaries;
1046 | return null;
1047 | }
1048 | }
1049 |
1050 | // REDUCE: Combine all summaries into one
1051 | class ReduceSummariesNode extends Node
1052 | {
1053 | public function prep(stdClass $shared): array
1054 | {
1055 | return $shared->individual_summaries;
1056 | }
1057 |
1058 | public function exec(mixed $summaries): string
1059 | {
1060 | $combinedText = implode("\n---\n", $summaries);
1061 | return call_llm("Create a final summary from these smaller summaries: " . $combinedText);
1062 | }
1063 |
1064 | public function post(stdClass $shared, mixed $p, mixed $finalSummary): ?string
1065 | {
1066 | $shared->final_summary = $finalSummary;
1067 | return null;
1068 | }
1069 | }
1070 | ```
1071 |
1072 | # Structured Output
1073 |
1074 | Often, you need an LLM to return data in a specific format like JSON or an array. The most reliable way to achieve this is through **prompt engineering**.
1075 |
1076 | ### The YAML Technique
1077 |
1078 | YAML is often easier for LLMs to generate correctly than JSON because it is less strict about quotes and commas.
1079 |
1080 | **The Strategy:**
1081 | 1. In your prompt, provide a clear example of the YAML structure you expect.
1082 | 2. Wrap the example in ```yaml code blocks.
1083 | 3. In your `exec()` method, parse the YAML string from the LLM's response.
1084 |
1085 | ### Example: Extracting Information from Text
1086 |
1087 | ```php
1088 | // nodes.php
1089 | use PocketFlow\Node;
1090 | use Symfony\Component\Yaml\Yaml; // `composer require symfony/yaml`
1091 |
1092 | class ExtractUserInfoNode extends Node
1093 | {
1094 | public function prep(stdClass $shared): string
1095 | {
1096 | return $shared->raw_text; // e.g., "John Doe is 30 years old and lives in New York."
1097 | }
1098 |
1099 | public function exec(mixed $text): array
1100 | {
1101 | $prompt = <<user_info = $userInfo;
1128 | return null;
1129 | }
1130 | }
1131 | ```
1132 |
1133 | # (Advanced) Multi-Agent Systems
1134 |
1135 | A Multi-Agent System involves two or more autonomous `Agents` collaborating to solve a problem. In PocketFlow-PHP, this is achieved by running multiple `AsyncFlows` concurrently, with agents communicating via shared **message queues**.
1136 |
1137 | ### The Core Pattern
1138 |
1139 | 1. **One Flow per Agent:** Each agent's logic is contained in its own `AsyncNode`, which runs in a loop inside its own `AsyncFlow`.
1140 | 2. **Message Queues:** A simple, promise-based queue class is used for communication. Queues are stored in the `$shared` object.
1141 | 3. **Concurrent Execution:** All agent flows are started simultaneously using `React\Promise\all()`.
1142 | 4. **State Management:**
1143 | - **Immutable Config:** Agent-specific configuration (like model name or personality prompt) is passed via `$params`.
1144 | - **Mutable State:** Shared game state (like scores or history) is managed in the `$shared` object.
1145 |
1146 | ### Example: The Taboo Game
1147 |
1148 | This example features a "Hinter" agent and a "Guesser" agent playing the word game Taboo.
1149 |
1150 | **`flow.php`**
1151 | ```php
1152 | hinterQueue = new MessageQueue();
1160 | $shared->guesserQueue = new MessageQueue();
1161 | $shared->past_guesses = [];
1162 | $shared->guess_count = 0;
1163 |
1164 | // 2. Create and configure agent nodes and flows
1165 | $hinterNode = new HinterAgent();
1166 | $guesserNode = new GuesserAgent();
1167 |
1168 | $hinterNode->on('continue')->next($hinterNode); // Loop
1169 | $guesserNode->on('continue')->next($guesserNode); // Loop
1170 |
1171 | $hinterFlow = new AsyncFlow($hinterNode);
1172 | $guesserFlow = new AsyncFlow($guesserNode);
1173 |
1174 | // ... set params for each flow ...
1175 |
1176 | // 3. Start the game and run flows concurrently
1177 | $shared->hinterQueue->put("START");
1178 | await(\React\Promise\all([
1179 | $hinterFlow->run_async($shared),
1180 | $guesserFlow->run_async($shared)
1181 | ]));
1182 | })();
1183 | }
1184 | ```
1185 |
1186 | **`nodes.php`**
1187 | ```php
1188 | hinterQueue->get(); // Wait for a message
1196 | }
1197 | public function exec_async(mixed $message): PromiseInterface {
1198 | // ... generate a hint based on the message ...
1199 | }
1200 | public function post_async(stdClass $shared, mixed $p, mixed $hint): PromiseInterface {
1201 | return async(function() use ($shared, $hint) {
1202 | $shared->guesserQueue->put($hint); // Send hint to the other agent
1203 | return "continue";
1204 | })();
1205 | }
1206 | }
1207 |
1208 | class GuesserAgent extends AsyncNode {
1209 | public function prep_async(stdClass $shared): PromiseInterface {
1210 | return $shared->guesserQueue->get(); // Wait for a hint
1211 | }
1212 | // ... exec and post methods to make a guess and send it back ...
1213 | }
1214 | ```
1215 |
1216 | ---
1217 |
1218 | ### 4: Utility Function
1219 |
1220 | # Utility Functions
1221 |
1222 | PocketFlow-PHP intentionally does not include built-in wrappers for external APIs. This "bring your own" philosophy gives you maximum flexibility and avoids vendor lock-in.
1223 |
1224 | This chapter provides simple, copy-paste-ready snippets for common utility functions. Place these functions in new files inside your `utils/` directory (e.g., `utils/llm_api.php`).
1225 |
1226 | - [LLM Wrapper]
1227 | - [Viz and Debug]
1228 | - [Web Search]
1229 | - [Text Chunking]
1230 | - [Embedding]
1231 | - [Vector Databases]
1232 | - [Text to Speech]
1233 |
1234 | ---
1235 |
1236 | ### 4.1 - 4.7 (Utility Function Examples Pages)
1237 |
1238 | # LLM Wrapper
1239 |
1240 | A centralized function to handle all your LLM API calls is a best practice.
1241 |
1242 | ### Recommended Client: `openai-php/client`
1243 |
1244 | This client is compatible with any OpenAI-compliant API, including OpenRouter, DeepSeek, and many local models.
1245 |
1246 | **`utils/llm_api.php`**
1247 | ```php
1248 | withApiKey(getenv('OPENROUTER_API_KEY'))
1259 | ->withBaseUri('https://openrouter.ai/api/v1')
1260 | ->withHttpHeader('HTTP-Referer', 'http://localhost') // Required by OpenRouter
1261 | ->withHttpHeader('X-Title', 'PocketFlow-PHP')
1262 | ->make();
1263 | }
1264 |
1265 | function call_llm_async(string $model, array $messages): PromiseInterface
1266 | {
1267 | return async(function () use ($model, $messages) {
1268 | try {
1269 | $client = get_openrouter_client();
1270 | $response = $client->chat()->create([
1271 | 'model' => $model,
1272 | 'messages' => $messages,
1273 | ]);
1274 | return $response->choices[0]->message->content;
1275 | } catch (\Exception $e) {
1276 | error_log("API Error: " . $e->getMessage());
1277 | return "Error communicating with the LLM.";
1278 | }
1279 | })();
1280 | }
1281 | ```
1282 |
1283 | # Visualization and Debugging
1284 |
1285 | Simple helpers can make debugging complex flows much easier.
1286 |
1287 | ### Mermaid Graph Generator
1288 |
1289 | This function traverses a `Flow` and generates a Mermaid syntax string, which you can paste into any Mermaid renderer to visualize your graph.
1290 |
1291 | **`utils/visualize.php`**
1292 | ```php
1293 | enqueue($startNode);
1303 | $nodes[$startNode] = "N" . ($nodes->count() + 1);
1304 |
1305 | while (!$queue->isEmpty()) {
1306 | $current = $queue->dequeue();
1307 | $currentId = $nodes[$current];
1308 | $className = (new \ReflectionClass($current))->getShortName();
1309 | $dot .= " {$currentId}[\"{$className}\"]\n";
1310 |
1311 | foreach ($current->getSuccessors() as $action => $successor) {
1312 | if (!$successor) continue;
1313 | if (!$nodes->contains($successor)) {
1314 | $nodes[$successor] = "N" . ($nodes->count() + 1);
1315 | $queue->enqueue($successor);
1316 | }
1317 | $successorId = $nodes[$successor];
1318 | $dot .= " {$currentId} -- \"{$action}\" --> {$successorId}\n";
1319 | }
1320 | }
1321 | return $dot;
1322 | }
1323 | ```
1324 |
1325 | # Web Search
1326 |
1327 | A simple wrapper for a web search API. The DuckDuckGo API is free and requires no API key, making it great for testing.
1328 |
1329 | **`utils/web_search.php`**
1330 | ```php
1331 | ['header' => "User-Agent: PocketFlow-PHP/1.0\r\n"]];
1336 | $context = stream_context_create($options);
1337 | $response = @file_get_contents($url, false, $context);
1338 |
1339 | if ($response === false) {
1340 | return "Error fetching search results.";
1341 | }
1342 |
1343 | $data = json_decode($response, true);
1344 | return $data['AbstractText'] ?? 'No results found.';
1345 | }
1346 | ```
1347 |
1348 | # Text Chunking
1349 |
1350 | Breaking large texts into smaller, manageable chunks is a core task in RAG systems.
1351 |
1352 | **`utils/chunking.php`**
1353 | ```php
1354 | embeddings()->create([
1388 | 'model' => $model,
1389 | 'input' => $text,
1390 | ]);
1391 | return $response->embeddings[0]->embedding;
1392 | })();
1393 | }
1394 | ```
1395 | # Vector Databases
1396 |
1397 | Storing and retrieving embeddings requires a vector database. Here is a very simple in-memory example for prototyping. For production, use a dedicated service like Pinecone, Qdrant, or Redis.
1398 |
1399 | **`utils/memory_vector_store.php`**
1400 | ```php
1401 | vectors[$id] = $vector;
1410 | $this->metadata[$id] = $meta;
1411 | }
1412 |
1413 | public function search(array $queryVector, int $topK = 1): array
1414 | {
1415 | $distances = [];
1416 | foreach ($this->vectors as $id => $vector) {
1417 | // Cosine similarity calculation
1418 | $dotProduct = 0;
1419 | $normA = 0;
1420 | $normB = 0;
1421 | for ($i = 0; $i < count($vector); $i++) {
1422 | $dotProduct += $queryVector[$i] * $vector[$i];
1423 | $normA += $queryVector[$i] ** 2;
1424 | $normB += $vector[$i] ** 2;
1425 | }
1426 | $similarity = $dotProduct / (sqrt($normA) * sqrt($normB));
1427 | $distances[$id] = $similarity;
1428 | }
1429 |
1430 | arsort($distances); // Sort by similarity, descending
1431 |
1432 | $results = [];
1433 | foreach (array_slice($distances, 0, $topK, true) as $id => $score) {
1434 | $results[] = [
1435 | 'id' => $id,
1436 | 'score' => $score,
1437 | 'metadata' => $this->metadata[$id]
1438 | ];
1439 | }
1440 | return $results;
1441 | }
1442 | }
1443 | ```
1444 | # Text to Speech (TTS)
1445 |
1446 | Generate audio from text using a TTS service like ElevenLabs.
1447 |
1448 | **`utils/tts.php`**
1449 | ```php
1450 | post("https://api.elevenlabs.io/v1/text-to-speech/{$voiceId}", [
1461 | 'headers' => [
1462 | 'Accept' => 'audio/mpeg',
1463 | 'Content-Type' => 'application/json',
1464 | 'xi-api-key' => $apiKey,
1465 | ],
1466 | 'json' => [
1467 | 'text' => $text,
1468 | 'model_id' => 'eleven_monolingual_v1',
1469 | ],
1470 | 'sink' => $outputFilePath // Stream response directly to a file
1471 | ]);
1472 | echo "Audio saved to {$outputFilePath}\n";
1473 | } catch (\Exception $e) {
1474 | echo "Error generating audio: " . $e->getMessage() . "\n";
1475 | }
1476 | }
1477 | ```
1478 |
--------------------------------------------------------------------------------
/GEMINI.md:
--------------------------------------------------------------------------------
1 | # Agentic Coding with PocketFlow-PHP
2 |
3 | > **Attention AI Agent:** This is your primary instruction manual. Follow these rules strictly to generate correct, robust, and bug-free PHP code for the PocketFlow-PHP framework. Failure to adhere to these rules will result in errors.
4 |
5 | ## The Golden Rules
6 |
7 | 1. **THE CORE FRAMEWORK IS READ-ONLY.** The file `src/PocketFlow.php` is the engine. You are **strictly forbidden** from ever editing it.
8 |
9 | 2. **USE THE THREE MAIN FILES.** All your application code **must** be written in these three files in the project root:
10 | - `nodes.php`: For all `Node`, `BatchNode`, and `AsyncNode` class definitions.
11 | - `flow.php`: For functions that create and wire up `Flows`.
12 | - `main.php`: The single entry point that calls a function from `flow.php`.
13 |
14 | 3. **UTILITIES GO IN `utils/`.** Any function that communicates with the outside world (e.g., calling an LLM API, a database, or a web search) **must** be placed in a new file inside the `utils/` directory.
15 |
16 | 4. **STATE MANAGEMENT IS CRITICAL.**
17 | - **DO** use the `$shared` object (`stdClass`) to manage all **mutable application state** (e.g., results, counters, lists of items).
18 | - **DO NOT** use `static` properties or variables inside Node classes to manage state. This will fail in loops.
19 | - **DO NOT** use `$this->params` to store mutable state. It is for **immutable configuration** only.
20 |
21 | 5. **STRICT TYPE COMPATIBILITY & SCOPE.**
22 | - The `post()` method of a `Node` **must** have the return type `?string`. If it does not decide the next action, it **must** end with `return null;`.
23 | - All `_async` methods (`prep_async`, `exec_async`, `post_async`) **must** have the return type `React\Promise\PromiseInterface`.
24 | - To return a promise, **always** use the pattern `return async(function() { ... })();`. Do not forget the final `()`.
25 | - **DO NOT** use `use ($this)` in closures. To access `$this->params` inside a `post_async` or `exec_async` closure, read the required values into local variables *before* the closure and pass them in with `use()`.
26 |
27 | 6. **ALWAYS IMPORT CLASSES WITH `use`.**
28 | - Any file that references a class (e.g., `AsyncNode`, `Flow`, `PromiseInterface`) **must** include a `use` statement for that class at the top of the file.
29 |
30 | 7. **DEFINE NODE CONNECTIONS *BEFORE* CREATING THE FLOW.**
31 | - The `Flow` class does not have a public method to retrieve its start node. Therefore, you **must** configure all node connections (`->next()`, `->on()->next()`) *before* passing the start node to the `Flow` constructor. This is especially important for creating loops.
32 | - **Correct Pattern:**
33 | ```php
34 | $myStartNode = new MyNode();
35 | $nextNode = new NextNode();
36 | $myStartNode->on('continue')->next($myStartNode); // Define loop on the node itself
37 | $myStartNode->on('finish')->next($nextNode);
38 | $flow = new Flow($myStartNode); // Then create the flow with the fully wired node
39 | ```
40 |
41 | 8. **USE PHP 8.3 FEATURES WISELY.**
42 | - **DO** use constructor property promotion for cleaner `Node` classes.
43 | - **DO** use the `match` expression inside `post()` for clean, readable action routing.
44 | - **DO NOT** use features like Fibers directly. Rely on the `async()` and `await()` functions provided by `react/async`.
45 |
46 | 9. **ENABLE STRICT TYPES.**
47 | - To prevent common type-related errors, **every new PHP file** you create (`nodes.php`, `flow.php`, `main.php`, and all utility files) **must** start with the following declaration on the very first line:
48 | ```php
49 | "Build a system that summarizes news articles from a list of URLs."
63 |
64 | 2. **AI: Propose a Plan.** Based on the documentation, the AI proposes a plan using PocketFlow concepts.
65 | > "I will create a `BatchFlow` to process each URL. The sub-flow will have two nodes: `FetchArticleNode` to download the content, and `SummarizeNode` to call an LLM. The final summaries will be stored in `$shared->summaries`."
66 |
67 | 3. **Human: Approve the Plan.** The human confirms the logic.
68 |
69 | 4. **AI: Generate the Code.** The AI writes the code for `utils/`, `nodes.php`, `flow.php`, and `main.php`, strictly following the Golden Rules and the Mandatory Project Structure.
70 |
71 | 5. **Human & AI: Test and Iterate.** The human runs `php main.php`. If there are errors, the AI debugs them by reviewing the Golden Rules.
72 |
73 | This collaborative process, guided by this documentation, is the fastest and most reliable way to build LLM applications with PocketFlow-PHP.
74 |
75 | ## Mandatory Project Structure
76 |
77 | Your final project **must** adhere to this structure. Do **not** create additional top-level directories or files, unless stated otherwise by the user, e.g. when asked to add a frontend.
78 |
79 | /
80 | ├── main.php # The application's single entry point.
81 | ├── nodes.php # All Node class definitions.
82 | ├── flow.php # All Flow creation functions.
83 | ├── utils/ # Directory for all helper functions (API calls, etc.).
84 | │ └── ... # e.g., llm_api.php, web_search.php
85 | ├── composer.json # Managed by Composer. Do not edit manually.
86 | └── src/
87 | └── PocketFlow.php # The core framework. READ-ONLY.
88 |
89 | ---
90 |
91 | ### 1: Introduction to PocketFlow-PHP
92 |
93 | # PocketFlow-PHP
94 |
95 |
96 | A minimalist LLM framework for PHP, written in 368 lines of PHP.
97 |
98 | Build complex Agents, Workflows, and RAG systems with a tiny, powerful core.
99 |
100 |
101 | ---
102 |
103 | **PocketFlow-PHP** is a port of the minimalist Python LLM framework, bringing its core principles to the PHP ecosystem. It's designed for developers who want maximum control and flexibility without the bloat of larger frameworks. It is optimized for **Agentic Coding**, where humans design the system and an AI agent writes the code.
104 |
105 | - **Lightweight**: The entire framework core is in a single, well-tested file.
106 | - **Expressive**: Build everything you love from larger frameworks—Multi-Agent systems, RAG pipelines, and complex workflows—using simple, composable building blocks.
107 |
108 | ## How does it work?
109 |
110 | The core abstraction is a **Graph + Shared Store**.
111 |
112 | 1. **Node**: The smallest unit of work (e.g., call an LLM, read a file).
113 | 2. **Flow**: Connects Nodes into a graph. Transitions are determined by simple string "Actions".
114 | 3. **Shared Store**: A simple `stdClass` object passed through the entire flow, allowing nodes to communicate and share state.
115 |
116 | This simple model is all you need to create powerful design patterns:
117 |
118 | ## Getting Started in 60 Seconds
119 |
120 | Get up and running with your first AI-powered application.
121 |
122 | **Prerequisites:** PHP 8.3+ and [Composer](https://getcomposer.org/) must be installed.
123 |
124 | 1. **Clone the repository:**
125 | ```bash
126 | git clone https://github.com/weise25/PocketFlow-PHP.git
127 | cd PocketFlow-PHP
128 | ```
129 |
130 | 2. **Install dependencies:**
131 | ```bash
132 | composer install
133 | ```
134 |
135 | 3. **Set up your environment:**
136 | - Create a `.env` file in the root of the project.
137 | - Add your API keys to the `.env` file (e.g., `OPENROUTER_API_KEY=...`).
138 |
139 | ### Chapter 2: Core Abstraction
140 |
141 | # Core Abstraction
142 |
143 | This chapter explains the fundamental building blocks of **PocketFlow-PHP**. Understanding these concepts is essential for building any application with the framework.
144 |
145 | We model the LLM workflow as a **Graph + Shared Store**.
146 |
147 | - [Node] – The smallest unit of work.
148 | - [Flow] – The orchestrator that connects Nodes.
149 | - [Communication] – How Nodes and Flows share data.
150 | - [Batch] – Tools for handling multiple items efficiently.
151 | - [(Advanced) Async] – For non-blocking, I/O-bound tasks.
152 | - [(Advanced) Parallel] – For running multiple async tasks concurrently.
153 |
154 |
155 | ---
156 |
157 | ### 2.1: Node
158 |
159 | # Node
160 |
161 | A **`Node`** is the smallest building block in PocketFlow. It represents a single, concrete step in a workflow. Each Node follows a simple, three-step lifecycle: `prep() -> exec() -> post()`.
162 |
163 |
164 | ### 1. The Lifecycle
165 |
166 | 1. **`prep(stdClass $shared): mixed`**
167 | * **Purpose:** To read data from the `$shared` store and prepare it for the main task.
168 | * **Example:** Fetch a user ID from `$shared->user->id`.
169 | * **Returns:** The data needed by the `exec()` method.
170 |
171 | 2. **`exec(mixed $prepResult): mixed`**
172 | * **Purpose:** To perform the core computation. This is where you call LLMs, APIs, or run business logic.
173 | * **Rule:** This method **must not** access the `$shared` store directly. It should be a "pure" function that only works with the data it receives from `prep()`.
174 | * **Returns:** The result of the computation.
175 |
176 | 3. **`post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string`**
177 | * **Purpose:** To write the results from `exec()` back into the `$shared` store and to decide what happens next.
178 | * **Returns:** An **action string**. This string determines which path the `Flow` will take. Returning `null` triggers the default path.
179 | * > **AI Agent Rule:** The return type of this method **must** be `?string`. If you don't need to return a specific action, you **must** `return null;`.
180 |
181 | ### 2. Fault Tolerance
182 |
183 | You can make your Nodes resilient to failure using constructor arguments and a fallback method.
184 |
185 | **Retries:**
186 | Define the number of retries and the wait time in the constructor.
187 |
188 | ```php
189 | // nodes.php
190 | use PocketFlow\Node;
191 |
192 | class ApiCallNode extends Node
193 | {
194 | public function __construct()
195 | {
196 | // Try exec() up to 3 times, wait 5 seconds between failures.
197 | parent::__construct(maxRetries: 3, wait: 5);
198 | }
199 |
200 | public function exec(mixed $prepResult): string
201 | {
202 | // This might throw an exception if the API is down.
203 | return call_external_api();
204 | }
205 | }
206 | ```
207 |
208 | **Graceful Fallback:**
209 | If all retries fail, the `execFallback()` method is called. Instead of crashing, you can return a safe default value.
210 |
211 | ```php
212 | // nodes.php
213 | use PocketFlow\Node;
214 |
215 | class ApiCallNode extends Node
216 | {
217 | // ... constructor and exec() method ...
218 |
219 | public function execFallback(mixed $prepResult, \Throwable $e): string
220 | {
221 | // Log the error and return a safe default.
222 | error_log("API call failed after all retries: " . $e->getMessage());
223 | return "Default value due to API failure.";
224 | }
225 | }
226 | ```
227 |
228 | ### 3. Example: A Complete Node
229 |
230 | ```php
231 | // nodes.php
232 | use PocketFlow\Node;
233 |
234 | class SummarizeArticleNode extends Node
235 | {
236 | public function __construct()
237 | {
238 | parent::__construct(maxRetries: 2, wait: 3);
239 | }
240 |
241 | public function prep(stdClass $shared): ?string
242 | {
243 | return $shared->article_text ?? null;
244 | }
245 |
246 | public function exec(mixed $articleText): string
247 | {
248 | if (empty($articleText)) {
249 | return "No text provided.";
250 | }
251 | $prompt = "Summarize this article: " . $articleText;
252 | return call_llm($prompt); // This might fail.
253 | }
254 |
255 | public function execFallback(mixed $prepResult, \Throwable $e): string
256 | {
257 | return "Could not generate summary due to an error.";
258 | }
259 |
260 | public function post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string
261 | {
262 | $shared->summary = $execResult;
263 | return null; // Triggers the "default" action.
264 | }
265 | }
266 | ```
267 |
268 | ---
269 | ## 2.2: Flow
270 |
271 | A **`Flow`** is the orchestrator. It connects `Nodes` together to form a process graph. The path through the graph is determined by the **action strings** returned by each Node's `post()` method.
272 |
273 | ### 1. Action-Based Transitions
274 |
275 | You define the connections between nodes using a simple, fluent API.
276 |
277 | **Default Transition:**
278 | If a Node's `post()` method returns `null` (or the string `"default"`), the default path is taken.
279 |
280 | ```php
281 | // flow.php
282 | $nodeA = new NodeA();
283 | $nodeB = new NodeB();
284 |
285 | // If NodeA returns null, execute NodeB next.
286 | $nodeA->next($nodeB);
287 |
288 | $flow = new Flow($nodeA);
289 | ```
290 |
291 | **Conditional Transitions:**
292 | You can create branches in your logic based on specific action strings.
293 |
294 | ```php
295 | // flow.php
296 | $reviewNode = new ReviewNode();
297 | $approveNode = new ApproveNode();
298 | $rejectNode = new RejectNode();
299 |
300 | // If reviewNode returns "approved", go to approveNode.
301 | $reviewNode->on('approved')->next($approveNode);
302 |
303 | // If reviewNode returns "rejected", go to rejectNode.
304 | $reviewNode->on('rejected')->next($rejectNode);
305 |
306 | $flow = new Flow($reviewNode);
307 | ```
308 |
309 | ### 2. Nested Flows (Composition)
310 |
311 | A `Flow` itself behaves like a `Node`. This allows you to build complex workflows by composing smaller, reusable flows.
312 |
313 | When a `Flow` is used as a node, the action returned by the **last node** of that inner flow is used to determine the next step in the outer flow.
314 |
315 | **Example:**
316 |
317 | ```php
318 | // flow.php
319 | use PocketFlow\Flow;
320 |
321 | // --- Sub-Flow 1: User Validation ---
322 | $checkUser = new CheckUserNode();
323 | $checkPermissions = new CheckPermissionsNode();
324 | $checkUser->next($checkPermissions);
325 | $validationFlow = new Flow($checkUser); // This flow might return "validation_failed"
326 |
327 | // --- Sub-Flow 2: Data Processing ---
328 | $loadData = new LoadDataNode();
329 | $processData = new ProcessDataNode();
330 | $loadData->next($processData);
331 | $processingFlow = new Flow($loadData); // This flow might return "processing_complete"
332 |
333 | // --- Main Flow: Composing the sub-flows ---
334 | $endNode = new EndNode();
335 | $errorNode = new ErrorNode();
336 |
337 | // Wire the main flow
338 | $validationFlow->next($processingFlow); // Default transition
339 | $validationFlow->on('validation_failed')->next($errorNode);
340 | $processingFlow->on('processing_complete')->next($endNode);
341 |
342 | $mainFlow = new Flow($validationFlow);
343 | $mainFlow->run($shared);
344 | ```
345 |
346 | This creates a clear, modular structure:
347 | ```mermaid
348 | graph TD
349 | subgraph Main Flow
350 | A(validationFlow) --> B(processingFlow);
351 | A --> C(errorNode);
352 | B --> D(endNode);
353 | end
354 | ```
355 | ---
356 |
357 | # 2.3: Communication
358 |
359 | Nodes and Flows need to share data. PocketFlow provides two mechanisms for this, each with a distinct purpose.
360 |
361 | ### 1. The Shared Store (`$shared`)
362 |
363 | This is the **primary** method of communication and state management.
364 |
365 | - **What it is:** A single `stdClass` object that is passed by reference to every Node in a Flow.
366 | - **Purpose:** To hold all **mutable application state**. This includes initial inputs, intermediate results, final outputs, counters, flags, etc.
367 | - **When to use:** For almost everything. It acts as the central "memory" or "database" for your workflow.
368 | - **How to use:**
369 | - Read from it in `prep()`.
370 | - Write to it in `post()`.
371 |
372 | > **AI Agent Golden Rule:** The `$shared` store is the single source of truth for your application's state. Any data that changes or needs to be passed between nodes **must** live in `$shared`.
373 |
374 | **Example:**
375 |
376 | ```php
377 | // main.php
378 | $shared = new stdClass();
379 | $shared->user_id = 123;
380 | $shared->user_data = null; // To be filled by a node
381 | $shared->final_report = null; // To be filled by another node
382 |
383 | $flow->run($shared);
384 |
385 | // After the flow runs, $shared will be populated.
386 | echo $shared->final_report;
387 | ```
388 |
389 | ### 2. Params (`$params`)
390 |
391 | This is a **secondary** mechanism for configuration.
392 |
393 | - **What it is:** An `array` that is passed by value to each Node.
394 | - **Purpose:** To hold **immutable configuration** that does not change during the flow's execution.
395 | - **When to use:** For things like model names, API endpoints, or identifiers in a `BatchFlow`.
396 | - **How to use:**
397 | - Set once on the top-level `Flow` using `$flow->setParams([...])`.
398 | - Access within any Node via `$this->params['my_key']`.
399 |
400 | > **AI Agent Warning:** Do not use `$params` to store state that needs to change (like counters or lists of results). The `$params` array is reset for each node in a loop, so any changes you make will be lost. Use `$shared` for that.
401 |
402 | **Example:**
403 |
404 | ```php
405 | // flow.php
406 | $node = new ApiCallNode();
407 | $flow = new Flow($node);
408 |
409 | // Set the API endpoint once for the entire flow.
410 | $flow->setParams(['api_endpoint' => 'https://api.example.com/v1']);
411 | $flow->run($shared);
412 |
413 | // nodes.php
414 | class ApiCallNode extends Node
415 | {
416 | public function exec(mixed $prepResult): string
417 | {
418 | $endpoint = $this->params['api_endpoint'];
419 | // ... make a call to the endpoint ...
420 | return "Data from API";
421 | }
422 | }
423 | ```
424 | ---
425 |
426 | ### 2.4: Batch
427 |
428 | # Batch Processing
429 |
430 | PocketFlow provides two powerful classes for processing multiple items: `BatchNode` and `BatchFlow`.
431 |
432 | ### 1. `BatchNode`
433 |
434 | Use `BatchNode` when you have a list of **data items** and want to perform the **same operation** on each one. This is the "Map" part of a Map-Reduce pattern.
435 |
436 | **How it works:**
437 | - `prep(stdClass $shared)`: Must return an `array` of items to be processed.
438 | - `exec(mixed $item)`: Is called **sequentially** for each `$item` in the array from `prep()`.
439 | - `post(stdClass $shared, ..., array $execResultList)`: Receives an `array` containing the results of all `exec()` calls.
440 |
441 | **Example: Summarize Text Chunks**
442 |
443 | ```php
444 | // nodes.php
445 | use PocketFlow\BatchNode;
446 |
447 | class SummarizeChunksNode extends BatchNode
448 | {
449 | public function prep(stdClass $shared): array
450 | {
451 | // Assume $shared->text_chunks is an array of strings.
452 | return $shared->text_chunks;
453 | }
454 |
455 | public function exec(mixed $chunk): string
456 | {
457 | // This method is called for each chunk.
458 | return call_llm("Summarize this chunk: " . $chunk);
459 | }
460 |
461 | public function post(stdClass $shared, mixed $p, mixed $summaries): ?string
462 | {
463 | // $summaries is an array of all the individual summaries.
464 | $shared->combined_summary = implode("\n", $summaries);
465 | return null;
466 | }
467 | }
468 | ```
469 |
470 | ### 2. `BatchFlow`
471 |
472 | Use `BatchFlow` when you want to run an entire **sub-flow** multiple times, each time with a different **configuration**.
473 |
474 | **How it works:**
475 | - `prep(stdClass $shared)`: Must return an `array` of **parameter arrays**.
476 | - The sub-flow is executed once for each parameter array.
477 | - Inside the sub-flow's nodes, the parameters are available via `$this->params`.
478 |
479 | **Example: Process Multiple Files**
480 |
481 | ```php
482 | // flow.php
483 | use PocketFlow\Flow;
484 | use PocketFlow\BatchFlow;
485 |
486 | function create_file_processing_flow(): Flow
487 | {
488 | // The sub-flow that processes a single file.
489 | $loadFileNode = new LoadFileNode();
490 | $analyzeFileNode = new AnalyzeFileNode();
491 | $loadFileNode->next($analyzeFileNode);
492 | $singleFileFlow = new Flow($loadFileNode);
493 |
494 | // The BatchFlow that orchestrates the process.
495 | $allFilesFlow = new ProcessAllFilesBatchFlow($singleFileFlow);
496 | return $allFilesFlow;
497 | }
498 |
499 | // nodes.php
500 | use PocketFlow\Node;
501 | use PocketFlow\BatchFlow;
502 |
503 | class ProcessAllFilesBatchFlow extends BatchFlow
504 | {
505 | public function prep(stdClass $shared): array
506 | {
507 | // Returns an array of parameter sets.
508 | // e.g., [['filename' => 'a.txt'], ['filename' => 'b.txt']]
509 | return array_map(fn($f) => ['filename' => $f], $shared->filenames);
510 | }
511 | }
512 |
513 | class LoadFileNode extends Node
514 | {
515 | public function exec(mixed $p): string
516 | {
517 | // Accesses the filename from the params set by the BatchFlow.
518 | $filename = $this->params['filename'];
519 | echo "Loading file: {$filename}\n";
520 | return file_get_contents($filename);
521 | }
522 |
523 | public function post(stdClass $shared, mixed $p, mixed $content): ?string
524 | {
525 | $shared->current_content = $content;
526 | return null;
527 | }
528 | }
529 |
530 | class AnalyzeFileNode extends Node { /* ... analyzes $shared->current_content ... */ }
531 | ```
532 | ---
533 |
534 | ### Seite 2.5: (Advanced) Async
535 | # (Advanced) Async
536 |
537 | Use `AsyncNode` and `AsyncFlow` for tasks that are **I/O-bound**, such as API calls, database queries, or reading large files. This allows your application to perform other work while waiting for these operations to complete, making it much more efficient. All async operations are powered by **ReactPHP**.
538 |
539 | ### 1. `AsyncNode`
540 |
541 | An `AsyncNode` is similar to a regular `Node`, but its lifecycle methods are asynchronous.
542 |
543 | **Rules for `AsyncNode`:**
544 | 1. Implement the `_async` versions of the lifecycle methods: `prep_async()`, `exec_async()`, `post_async()`.
545 | 2. Each of these methods **must** return a `React\Promise\PromiseInterface`.
546 | 3. The easiest way to return a promise is to wrap your logic in `return async(function() { ... })();`.
547 | 4. Inside an `async` block, you can use `await()` to wait for other promises to resolve.
548 |
549 | **Example: Asynchronous API Call**
550 |
551 | ```php
552 | // nodes.php
553 | use PocketFlow\AsyncNode;
554 | use React\Promise\PromiseInterface;
555 | use function React\Async\async;
556 | use function React\Async\await;
557 |
558 | class FetchUserDataNode extends AsyncNode
559 | {
560 | public function prep_async(stdClass $shared): PromiseInterface
561 | {
562 | return async(fn() => $shared->user_id)();
563 | }
564 |
565 | public function exec_async(mixed $userId): PromiseInterface
566 | {
567 | // Assume call_user_api_async() returns a Promise.
568 | return call_user_api_async($userId);
569 | }
570 |
571 | public function post_async(stdClass $shared, mixed $p, mixed $userData): PromiseInterface
572 | {
573 | return async(function() use ($shared, $userData) {
574 | $shared->user_data = $userData;
575 | return null;
576 | })();
577 | }
578 | }
579 | ```
580 |
581 | ### 2. `AsyncFlow`
582 |
583 | An `AsyncFlow` is used to orchestrate flows that contain at least one `AsyncNode`. It can also correctly orchestrate regular synchronous `Nodes`.
584 |
585 | ### 3. Running an Async Workflow
586 |
587 | The top-level entry point of any application that uses async features **must** be wrapped in an `async` block.
588 |
589 | **Example:**
590 |
591 | ```php
592 | // main.php
593 | require_once __DIR__ . '/vendor/autoload.php';
594 | require_once __DIR__ . '/flow.php';
595 | use function React\Async\async;
596 | use function React\Async\await;
597 |
598 | // Wrap the entire application logic in async().
599 | async(function() {
600 | $shared = new stdClass();
601 | $shared->user_id = 123;
602 |
603 | $flow = create_user_data_flow(); // This flow returns an AsyncFlow
604 |
605 | // Use await() to run the flow and wait for it to complete.
606 | await($flow->run_async($shared));
607 |
608 | print_r($shared->user_data);
609 | })();
610 | ```
611 | ---
612 |
613 | ### 2.6: (Advanced) Parallel
614 |
615 | # (Advanced) Parallel Execution
616 |
617 | Parallel execution in PocketFlow-PHP means running multiple **asynchronous** tasks **concurrently**. This is extremely useful for speeding up I/O-bound workflows where tasks don't depend on each other.
618 |
619 | > **Note:** This is **concurrency**, not true parallelism. PHP's single-threaded nature means CPU-bound tasks won't run faster. However, for waiting on multiple API calls or database queries at once, the performance gain is significant.
620 |
621 | ### 1. `AsyncParallelBatchNode`
622 |
623 | This is the parallel version of `AsyncBatchNode`. It calls `exec_async()` for **all items at the same time** and waits for all of them to complete.
624 |
625 | **Use Case:** Fetching data for multiple users from an API simultaneously.
626 |
627 | **Example: Parallel API Calls**
628 |
629 | ```php
630 | // nodes.php
631 | use PocketFlow\AsyncParallelBatchNode;
632 | use React\Promise\PromiseInterface;
633 | use function React\Async\async;
634 |
635 | class FetchAllUsersNode extends AsyncParallelBatchNode
636 | {
637 | public function prep_async(stdClass $shared): PromiseInterface
638 | {
639 | // Returns an array of user IDs.
640 | return async(fn() => $shared->user_ids)();
641 | }
642 |
643 | public function exec_async(mixed $userId): PromiseInterface
644 | {
645 | // This is called concurrently for all user IDs.
646 | // Assume call_user_api_async() returns a Promise.
647 | return call_user_api_async($userId);
648 | }
649 |
650 | public function post_async(stdClass $shared, mixed $p, mixed $usersData): PromiseInterface
651 | {
652 | return async(function() use ($shared, $usersData) {
653 | // $usersData is an array of results. The order is preserved.
654 | $shared->users = $usersData;
655 | return null;
656 | })();
657 | }
658 | }
659 | ```
660 |
661 | ### 2. `AsyncParallelBatchFlow`
662 |
663 | This is the parallel version of `AsyncBatchFlow`. It runs the entire sub-flow for **all parameter sets concurrently**.
664 |
665 | **Use Case:** Processing multiple files, where each file requires a multi-step async workflow (e.g., download, analyze, upload).
666 |
667 | **Example: Parallel File Processing**
668 |
669 | ```php
670 | // flow.php
671 | use PocketFlow\AsyncFlow;
672 | use PocketFlow\AsyncParallelBatchFlow;
673 |
674 | function create_parallel_file_flow(): AsyncFlow
675 | {
676 | // The sub-flow for a single file.
677 | $downloadNode = new DownloadFileAsyncNode();
678 | $analyzeNode = new AnalyzeFileAsyncNode();
679 | $downloadNode->next($analyzeNode);
680 | $singleFileFlow = new AsyncFlow($downloadNode);
681 |
682 | // The parallel batch flow.
683 | $parallelFlow = new ProcessAllFilesParallelBatchFlow($singleFileFlow);
684 | return $parallelFlow;
685 | }
686 |
687 | // nodes.php
688 | use PocketFlow\AsyncParallelBatchFlow;
689 | use React\Promise\PromiseInterface;
690 | use function React\Async\async;
691 |
692 | class ProcessAllFilesParallelBatchFlow extends AsyncParallelBatchFlow
693 | {
694 | public function prep_async(stdClass $shared): PromiseInterface
695 | {
696 | // Returns an array of parameter sets.
697 | return async(fn() => array_map(fn($f) => ['url' => $f], $shared->file_urls))();
698 | }
699 | }
700 |
701 | // DownloadFileAsyncNode and AnalyzeFileAsyncNode are defined as regular AsyncNodes.
702 | // They will access the URL via $this->params['url'].
703 | ```
704 |
705 | > **Warning:** Be mindful of API rate limits when using parallel execution. A large number of concurrent requests can quickly exhaust your quota.
706 |
707 | # Chapter 3: Design Pattern
708 |
709 | # Design Patterns
710 |
711 | Design patterns are reusable blueprints for solving common problems in LLM application development. PocketFlow-PHP's simple core abstractions (`Node`, `Flow`, `Shared Store`) are all you need to implement these powerful patterns.
712 |
713 | This chapter provides practical, copy-paste-ready examples for each pattern.
714 |
715 | - [Agent] – For creating autonomous, decision-making entities.
716 | - [Workflow] – For breaking down complex tasks into a sequence of simple steps.
717 | - [RAG (Retrieval-Augmented Generation)] – For providing external knowledge to an LLM.
718 | - [Map-Reduce] – For processing large amounts of data efficiently.
719 | - [Structured Output] – For forcing an LLM to return clean, predictable data formats.
720 | - [(Advanced) Multi-Agent Systems] – For orchestrating collaboration between multiple specialized agents.
721 | ---
722 |
723 | ### Seite 3.1: Agent
724 |
725 | # Agent
726 |
727 | An **Agent** is a `Node` that can make autonomous decisions. Instead of following a fixed path, it analyzes the current state in the `$shared` store and returns a specific **action string** to direct the `Flow` down different branches, often looping back to itself to create a cycle of reasoning and action.
728 |
729 | ### The Agentic Loop
730 |
731 | The core of an agent is a loop:
732 | 1. **Think:** The Agent `Node` analyzes the state (`$shared`) and the goal.
733 | 2. **Decide:** It calls an LLM to choose the next `action` from a predefined set of tools.
734 | 3. **Act:** The `Flow` routes to a `Tool Node` based on the chosen action.
735 | 4. **Observe:** The `Tool Node` executes its task (e.g., a web search) and updates the `$shared` store with the result.
736 | 5. The `Flow` loops back to the Agent `Node` to start the cycle again with new information.
737 |
738 | ```mermaid
739 | graph TD
740 | A[Agent Node (Think & Decide)] -- action: 'search' --> B[Search Tool Node (Act)];
741 | A -- action: 'answer' --> C[Answer Node (Final Act)];
742 | B -- result --> D(Update Shared Store);
743 | D --> A;
744 | ```
745 |
746 | ### Example: A Simple Research Agent
747 |
748 | This agent can decide whether to search the web for more information or to answer a question based on the context it has already gathered.
749 |
750 | **`flow.php`**
751 | ```php
752 | on('search')->next($searchNode);
764 | $decideNode->on('answer')->next($answerNode);
765 | $searchNode->next($decideNode); // Loop back to the decision node after acting
766 |
767 | return new Flow($decideNode);
768 | }
769 | ```
770 |
771 | **`nodes.php`**
772 | ```php
773 | search_history) ? "Nothing yet." : implode("\n", $shared->search_history);
785 | return [$shared->query, $context];
786 | }
787 |
788 | public function exec(mixed $prepResult): array
789 | {
790 | [$query, $context] = $prepResult;
791 | $prompt = <<current_search_term = $decision['search_term'];
816 | }
817 | return $decision['action'];
818 | }
819 | }
820 |
821 | class SearchWebNode extends Node
822 | {
823 | public function prep(stdClass $shared): string
824 | {
825 | return $shared->current_search_term;
826 | }
827 |
828 | public function exec(mixed $searchTerm): string
829 | {
830 | echo "Agent is searching for: {$searchTerm}\n";
831 | return web_search($searchTerm); // Assume web_search() is defined
832 | }
833 |
834 | public function post(stdClass $shared, mixed $p, mixed $searchResult): ?string
835 | {
836 | $shared->search_history[] = $searchResult;
837 | return null; // Triggers default transition back to DecideActionNode
838 | }
839 | }
840 |
841 | class AnswerNode extends Node
842 | {
843 | public function exec(mixed $p): string
844 | {
845 | // In a real app, this would also use prep() to get context
846 | return "This is the final answer based on the research.";
847 | }
848 |
849 | public function post(stdClass $shared, mixed $p, mixed $answer): ?string
850 | {
851 | $shared->final_answer = $answer;
852 | return null; // End of flow
853 | }
854 | }
855 | ```
856 | ---
857 |
858 | ### Chapter 3.2 - 3.6 (Workflow, RAG, etc.)
859 |
860 | # Workflow
861 |
862 | A **Workflow** is the simplest design pattern. It involves chaining a sequence of `Nodes` together to break a complex task into manageable steps. Each node performs one part of the job and passes its result to the next via the `$shared` store.
863 |
864 | ### Example: Article Writing Workflow
865 |
866 | This workflow generates an article by breaking the process into three distinct steps: outlining, drafting, and reviewing.
867 |
868 | **`flow.php`**
869 | ```php
870 | next($draftNode)->next($reviewNode);
882 |
883 | return new Flow($outlineNode);
884 | }
885 | ```
886 |
887 | **`nodes.php`**
888 | ```php
889 | topic; }
894 | public function exec(mixed $topic): string { return "Outline for: {$topic}"; /* call_llm(...) */ }
895 | public function post(stdClass $shared, mixed $p, mixed $outline): ?string {
896 | $shared->outline = $outline;
897 | return null;
898 | }
899 | }
900 |
901 | class DraftArticleNode extends Node {
902 | public function prep(stdClass $shared): string { return $shared->outline; }
903 | public function exec(mixed $outline): string { return "Draft based on: {$outline}"; /* call_llm(...) */ }
904 | public function post(stdClass $shared, mixed $p, mixed $draft): ?string {
905 | $shared->draft = $draft;
906 | return null;
907 | }
908 | }
909 |
910 | class ReviewArticleNode extends Node {
911 | public function prep(stdClass $shared): string { return $shared->draft; }
912 | public function exec(mixed $draft): string { return "Reviewed draft: {$draft}"; /* call_llm(...) */ }
913 | public function post(stdClass $shared, mixed $p, mixed $final): ?string {
914 | $shared->final_article = $final;
915 | return null;
916 | }
917 | }
918 | ```
919 |
920 | # RAG (Retrieval-Augmented Generation)
921 |
922 | RAG allows an LLM to answer questions using information from your own documents. It's a two-stage process.
923 |
924 | ### Stage 1: Offline Indexing (A Workflow)
925 |
926 | This flow reads documents, splits them into chunks, creates vector embeddings for each chunk, and stores them in a vector database.
927 |
928 | **`flow.php` (Offline Indexing)**
929 | ```php
930 | next($embedNode)->next($storeNode);
940 | return new Flow($chunkNode);
941 | }
942 | ```
943 |
944 | **`nodes.php` (Offline Indexing)**
945 | ```php
946 | file_paths; }
953 | public function exec(mixed $path): array { return sentence_chunk(file_get_contents($path)); }
954 | public function post(stdClass $shared, mixed $p, mixed $chunkLists): ?string {
955 | $shared->all_chunks = array_merge(...$chunkLists);
956 | return null;
957 | }
958 | }
959 |
960 | // 2. Embedding (using BatchNode)
961 | class EmbedChunksNode extends BatchNode {
962 | public function prep(stdClass $shared): array { return $shared->all_chunks; }
963 | public function exec(mixed $chunk): array { return get_embedding($chunk); } // Assume get_embedding() exists
964 | public function post(stdClass $shared, mixed $p, mixed $embeddings): ?string {
965 | $shared->embeddings = $embeddings;
966 | return null;
967 | }
968 | }
969 |
970 | // 3. Storing
971 | class StoreEmbeddingsNode extends Node {
972 | public function exec(mixed $p): mixed {
973 | /* ... code to store embeddings in a vector DB ... */
974 | return null; // Must return a value compatible with 'mixed'
975 | }
976 | }
977 | ```
978 |
979 | ### Stage 2: Online Retrieval & Answering (A Workflow)
980 |
981 | This flow takes a user's question, finds the most relevant chunks from the vector database, and passes them to an LLM as context to generate an answer.
982 |
983 | **`flow.php` (Online Answering)**
984 | ```php
985 | next($retrieveDocs)->next($generateAnswer);
995 | return new Flow($embedQuery);
996 | }
997 | ```
998 |
999 | # Map-Reduce
1000 |
1001 | The Map-Reduce pattern is perfect for processing large datasets that can be broken down into smaller, independent parts.
1002 |
1003 | - **Map Phase:** A `BatchNode` processes each part of the data individually.
1004 | - **Reduce Phase:** A regular `Node` takes the list of results from the map phase and combines them into a single, final output.
1005 |
1006 | ### Example: Summarizing Multiple Documents
1007 |
1008 | **`flow.php`**
1009 | ```php
1010 | next($reduceNode);
1019 | return new Flow($mapNode);
1020 | }
1021 | ```
1022 |
1023 | **`nodes.php`**
1024 | ```php
1025 | documents is an associative array ['filename' => 'content', ...]
1035 | return $shared->documents;
1036 | }
1037 |
1038 | public function exec(mixed $content): string
1039 | {
1040 | return call_llm("Summarize this document: " . $content);
1041 | }
1042 |
1043 | public function post(stdClass $shared, mixed $p, mixed $summaries): ?string
1044 | {
1045 | $shared->individual_summaries = $summaries;
1046 | return null;
1047 | }
1048 | }
1049 |
1050 | // REDUCE: Combine all summaries into one
1051 | class ReduceSummariesNode extends Node
1052 | {
1053 | public function prep(stdClass $shared): array
1054 | {
1055 | return $shared->individual_summaries;
1056 | }
1057 |
1058 | public function exec(mixed $summaries): string
1059 | {
1060 | $combinedText = implode("\n---\n", $summaries);
1061 | return call_llm("Create a final summary from these smaller summaries: " . $combinedText);
1062 | }
1063 |
1064 | public function post(stdClass $shared, mixed $p, mixed $finalSummary): ?string
1065 | {
1066 | $shared->final_summary = $finalSummary;
1067 | return null;
1068 | }
1069 | }
1070 | ```
1071 |
1072 | # Structured Output
1073 |
1074 | Often, you need an LLM to return data in a specific format like JSON or an array. The most reliable way to achieve this is through **prompt engineering**.
1075 |
1076 | ### The YAML Technique
1077 |
1078 | YAML is often easier for LLMs to generate correctly than JSON because it is less strict about quotes and commas.
1079 |
1080 | **The Strategy:**
1081 | 1. In your prompt, provide a clear example of the YAML structure you expect.
1082 | 2. Wrap the example in ```yaml code blocks.
1083 | 3. In your `exec()` method, parse the YAML string from the LLM's response.
1084 |
1085 | ### Example: Extracting Information from Text
1086 |
1087 | ```php
1088 | // nodes.php
1089 | use PocketFlow\Node;
1090 | use Symfony\Component\Yaml\Yaml; // `composer require symfony/yaml`
1091 |
1092 | class ExtractUserInfoNode extends Node
1093 | {
1094 | public function prep(stdClass $shared): string
1095 | {
1096 | return $shared->raw_text; // e.g., "John Doe is 30 years old and lives in New York."
1097 | }
1098 |
1099 | public function exec(mixed $text): array
1100 | {
1101 | $prompt = <<user_info = $userInfo;
1128 | return null;
1129 | }
1130 | }
1131 | ```
1132 |
1133 | # (Advanced) Multi-Agent Systems
1134 |
1135 | A Multi-Agent System involves two or more autonomous `Agents` collaborating to solve a problem. In PocketFlow-PHP, this is achieved by running multiple `AsyncFlows` concurrently, with agents communicating via shared **message queues**.
1136 |
1137 | ### The Core Pattern
1138 |
1139 | 1. **One Flow per Agent:** Each agent's logic is contained in its own `AsyncNode`, which runs in a loop inside its own `AsyncFlow`.
1140 | 2. **Message Queues:** A simple, promise-based queue class is used for communication. Queues are stored in the `$shared` object.
1141 | 3. **Concurrent Execution:** All agent flows are started simultaneously using `React\Promise\all()`.
1142 | 4. **State Management:**
1143 | - **Immutable Config:** Agent-specific configuration (like model name or personality prompt) is passed via `$params`.
1144 | - **Mutable State:** Shared game state (like scores or history) is managed in the `$shared` object.
1145 |
1146 | ### Example: The Taboo Game
1147 |
1148 | This example features a "Hinter" agent and a "Guesser" agent playing the word game Taboo.
1149 |
1150 | **`flow.php`**
1151 | ```php
1152 | hinterQueue = new MessageQueue();
1160 | $shared->guesserQueue = new MessageQueue();
1161 | $shared->past_guesses = [];
1162 | $shared->guess_count = 0;
1163 |
1164 | // 2. Create and configure agent nodes and flows
1165 | $hinterNode = new HinterAgent();
1166 | $guesserNode = new GuesserAgent();
1167 |
1168 | $hinterNode->on('continue')->next($hinterNode); // Loop
1169 | $guesserNode->on('continue')->next($guesserNode); // Loop
1170 |
1171 | $hinterFlow = new AsyncFlow($hinterNode);
1172 | $guesserFlow = new AsyncFlow($guesserNode);
1173 |
1174 | // ... set params for each flow ...
1175 |
1176 | // 3. Start the game and run flows concurrently
1177 | $shared->hinterQueue->put("START");
1178 | await(\React\Promise\all([
1179 | $hinterFlow->run_async($shared),
1180 | $guesserFlow->run_async($shared)
1181 | ]));
1182 | })();
1183 | }
1184 | ```
1185 |
1186 | **`nodes.php`**
1187 | ```php
1188 | hinterQueue->get(); // Wait for a message
1196 | }
1197 | public function exec_async(mixed $message): PromiseInterface {
1198 | // ... generate a hint based on the message ...
1199 | }
1200 | public function post_async(stdClass $shared, mixed $p, mixed $hint): PromiseInterface {
1201 | return async(function() use ($shared, $hint) {
1202 | $shared->guesserQueue->put($hint); // Send hint to the other agent
1203 | return "continue";
1204 | })();
1205 | }
1206 | }
1207 |
1208 | class GuesserAgent extends AsyncNode {
1209 | public function prep_async(stdClass $shared): PromiseInterface {
1210 | return $shared->guesserQueue->get(); // Wait for a hint
1211 | }
1212 | // ... exec and post methods to make a guess and send it back ...
1213 | }
1214 | ```
1215 |
1216 | ---
1217 |
1218 | ### 4: Utility Function
1219 |
1220 | # Utility Functions
1221 |
1222 | PocketFlow-PHP intentionally does not include built-in wrappers for external APIs. This "bring your own" philosophy gives you maximum flexibility and avoids vendor lock-in.
1223 |
1224 | This chapter provides simple, copy-paste-ready snippets for common utility functions. Place these functions in new files inside your `utils/` directory (e.g., `utils/llm_api.php`).
1225 |
1226 | - [LLM Wrapper]
1227 | - [Viz and Debug]
1228 | - [Web Search]
1229 | - [Text Chunking]
1230 | - [Embedding]
1231 | - [Vector Databases]
1232 | - [Text to Speech]
1233 |
1234 | ---
1235 |
1236 | ### 4.1 - 4.7 (Utility Function Examples Pages)
1237 |
1238 | # LLM Wrapper
1239 |
1240 | A centralized function to handle all your LLM API calls is a best practice.
1241 |
1242 | ### Recommended Client: `openai-php/client`
1243 |
1244 | This client is compatible with any OpenAI-compliant API, including OpenRouter, DeepSeek, and many local models.
1245 |
1246 | **`utils/llm_api.php`**
1247 | ```php
1248 | withApiKey(getenv('OPENROUTER_API_KEY'))
1259 | ->withBaseUri('https://openrouter.ai/api/v1')
1260 | ->withHttpHeader('HTTP-Referer', 'http://localhost') // Required by OpenRouter
1261 | ->withHttpHeader('X-Title', 'PocketFlow-PHP')
1262 | ->make();
1263 | }
1264 |
1265 | function call_llm_async(string $model, array $messages): PromiseInterface
1266 | {
1267 | return async(function () use ($model, $messages) {
1268 | try {
1269 | $client = get_openrouter_client();
1270 | $response = $client->chat()->create([
1271 | 'model' => $model,
1272 | 'messages' => $messages,
1273 | ]);
1274 | return $response->choices[0]->message->content;
1275 | } catch (\Exception $e) {
1276 | error_log("API Error: " . $e->getMessage());
1277 | return "Error communicating with the LLM.";
1278 | }
1279 | })();
1280 | }
1281 | ```
1282 |
1283 | # Visualization and Debugging
1284 |
1285 | Simple helpers can make debugging complex flows much easier.
1286 |
1287 | ### Mermaid Graph Generator
1288 |
1289 | This function traverses a `Flow` and generates a Mermaid syntax string, which you can paste into any Mermaid renderer to visualize your graph.
1290 |
1291 | **`utils/visualize.php`**
1292 | ```php
1293 | enqueue($startNode);
1303 | $nodes[$startNode] = "N" . ($nodes->count() + 1);
1304 |
1305 | while (!$queue->isEmpty()) {
1306 | $current = $queue->dequeue();
1307 | $currentId = $nodes[$current];
1308 | $className = (new \ReflectionClass($current))->getShortName();
1309 | $dot .= " {$currentId}[\"{$className}\"]\n";
1310 |
1311 | foreach ($current->getSuccessors() as $action => $successor) {
1312 | if (!$successor) continue;
1313 | if (!$nodes->contains($successor)) {
1314 | $nodes[$successor] = "N" . ($nodes->count() + 1);
1315 | $queue->enqueue($successor);
1316 | }
1317 | $successorId = $nodes[$successor];
1318 | $dot .= " {$currentId} -- \"{$action}\" --> {$successorId}\n";
1319 | }
1320 | }
1321 | return $dot;
1322 | }
1323 | ```
1324 |
1325 | # Web Search
1326 |
1327 | A simple wrapper for a web search API. The DuckDuckGo API is free and requires no API key, making it great for testing.
1328 |
1329 | **`utils/web_search.php`**
1330 | ```php
1331 | ['header' => "User-Agent: PocketFlow-PHP/1.0\r\n"]];
1336 | $context = stream_context_create($options);
1337 | $response = @file_get_contents($url, false, $context);
1338 |
1339 | if ($response === false) {
1340 | return "Error fetching search results.";
1341 | }
1342 |
1343 | $data = json_decode($response, true);
1344 | return $data['AbstractText'] ?? 'No results found.';
1345 | }
1346 | ```
1347 |
1348 | # Text Chunking
1349 |
1350 | Breaking large texts into smaller, manageable chunks is a core task in RAG systems.
1351 |
1352 | **`utils/chunking.php`**
1353 | ```php
1354 | embeddings()->create([
1388 | 'model' => $model,
1389 | 'input' => $text,
1390 | ]);
1391 | return $response->embeddings[0]->embedding;
1392 | })();
1393 | }
1394 | ```
1395 | # Vector Databases
1396 |
1397 | Storing and retrieving embeddings requires a vector database. Here is a very simple in-memory example for prototyping. For production, use a dedicated service like Pinecone, Qdrant, or Redis.
1398 |
1399 | **`utils/memory_vector_store.php`**
1400 | ```php
1401 | vectors[$id] = $vector;
1410 | $this->metadata[$id] = $meta;
1411 | }
1412 |
1413 | public function search(array $queryVector, int $topK = 1): array
1414 | {
1415 | $distances = [];
1416 | foreach ($this->vectors as $id => $vector) {
1417 | // Cosine similarity calculation
1418 | $dotProduct = 0;
1419 | $normA = 0;
1420 | $normB = 0;
1421 | for ($i = 0; $i < count($vector); $i++) {
1422 | $dotProduct += $queryVector[$i] * $vector[$i];
1423 | $normA += $queryVector[$i] ** 2;
1424 | $normB += $vector[$i] ** 2;
1425 | }
1426 | $similarity = $dotProduct / (sqrt($normA) * sqrt($normB));
1427 | $distances[$id] = $similarity;
1428 | }
1429 |
1430 | arsort($distances); // Sort by similarity, descending
1431 |
1432 | $results = [];
1433 | foreach (array_slice($distances, 0, $topK, true) as $id => $score) {
1434 | $results[] = [
1435 | 'id' => $id,
1436 | 'score' => $score,
1437 | 'metadata' => $this->metadata[$id]
1438 | ];
1439 | }
1440 | return $results;
1441 | }
1442 | }
1443 | ```
1444 | # Text to Speech (TTS)
1445 |
1446 | Generate audio from text using a TTS service like ElevenLabs.
1447 |
1448 | **`utils/tts.php`**
1449 | ```php
1450 | post("https://api.elevenlabs.io/v1/text-to-speech/{$voiceId}", [
1461 | 'headers' => [
1462 | 'Accept' => 'audio/mpeg',
1463 | 'Content-Type' => 'application/json',
1464 | 'xi-api-key' => $apiKey,
1465 | ],
1466 | 'json' => [
1467 | 'text' => $text,
1468 | 'model_id' => 'eleven_monolingual_v1',
1469 | ],
1470 | 'sink' => $outputFilePath // Stream response directly to a file
1471 | ]);
1472 | echo "Audio saved to {$outputFilePath}\n";
1473 | } catch (\Exception $e) {
1474 | echo "Error generating audio: " . $e->getMessage() . "\n";
1475 | }
1476 | }
1477 | ```
1478 |
--------------------------------------------------------------------------------