forked from weise25/PocketFlow-PHP
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.cursorrules
More file actions
1477 lines (1145 loc) · 48.4 KB
/
.cursorrules
File metadata and controls
1477 lines (1145 loc) · 48.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Agentic Coding with PocketFlow-PHP
> **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.
## The Golden Rules
1. **THE CORE FRAMEWORK IS READ-ONLY.** The file `src/PocketFlow.php` is the engine. You are **strictly forbidden** from ever editing it.
2. **USE THE THREE MAIN FILES.** All your application code **must** be written in these three files in the project root:
- `nodes.php`: For all `Node`, `BatchNode`, and `AsyncNode` class definitions.
- `flow.php`: For functions that create and wire up `Flows`.
- `main.php`: The single entry point that calls a function from `flow.php`.
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.
4. **STATE MANAGEMENT IS CRITICAL.**
- **DO** use the `$shared` object (`stdClass`) to manage all **mutable application state** (e.g., results, counters, lists of items).
- **DO NOT** use `static` properties or variables inside Node classes to manage state. This will fail in loops.
- **DO NOT** use `$this->params` to store mutable state. It is for **immutable configuration** only.
5. **STRICT TYPE COMPATIBILITY & SCOPE.**
- 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;`.
- All `_async` methods (`prep_async`, `exec_async`, `post_async`) **must** have the return type `React\Promise\PromiseInterface`.
- To return a promise, **always** use the pattern `return async(function() { ... })();`. Do not forget the final `()`.
- **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()`.
6. **ALWAYS IMPORT CLASSES WITH `use`.**
- 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.
7. **DEFINE NODE CONNECTIONS *BEFORE* CREATING THE FLOW.**
- 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.
- **Correct Pattern:**
```php
$myStartNode = new MyNode();
$nextNode = new NextNode();
$myStartNode->on('continue')->next($myStartNode); // Define loop on the node itself
$myStartNode->on('finish')->next($nextNode);
$flow = new Flow($myStartNode); // Then create the flow with the fully wired node
```
8. **USE PHP 8.3 FEATURES WISELY.**
- **DO** use constructor property promotion for cleaner `Node` classes.
- **DO** use the `match` expression inside `post()` for clean, readable action routing.
- **DO NOT** use features like Fibers directly. Rely on the `async()` and `await()` functions provided by `react/async`.
9. **ENABLE STRICT TYPES.**
- 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:
```php
<?php
declare(strict_types=1);
```
10. **MANAGE DEPENDENCIES WITH COMPOSER COMMANDS.**
- You **must not** edit the `composer.json` file manually.
- When a new library is needed for a utility function (e.g., `symfony/yaml` for parsing YAML), you **must** instruct the user to run the `composer require` command.
- **Example Instruction:** "To parse YAML, the `symfony/yaml` package is required. Please run: `composer require symfony/yaml`"
## The Development Workflow
Follow these steps in order. Refer to the correct code examples in this documentation.
1. **Human: Define Requirements.** The human provides a high-level goal.
- > "Build a system that summarizes news articles from a list of URLs."
2. **AI: Propose a Plan.** Based on the documentation, the AI proposes a plan using PocketFlow concepts.
> "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`."
3. **Human: Approve the Plan.** The human confirms the logic.
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.
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.
This collaborative process, guided by this documentation, is the fastest and most reliable way to build LLM applications with PocketFlow-PHP.
## Mandatory Project Structure
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.
/
├── main.php # The application's single entry point.
├── nodes.php # All Node class definitions.
├── flow.php # All Flow creation functions.
├── utils/ # Directory for all helper functions (API calls, etc.).
│ └── ... # e.g., llm_api.php, web_search.php
├── composer.json # Managed by Composer. Do not edit manually.
└── src/
└── PocketFlow.php # The core framework. READ-ONLY.
---
### 1: Introduction to PocketFlow-PHP
# PocketFlow-PHP
<strong>A minimalist LLM framework for PHP, written in 368 lines of PHP.</strong>
Build complex Agents, Workflows, and RAG systems with a tiny, powerful core.
---
**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.
- **Lightweight**: The entire framework core is in a single, well-tested file.
- **Expressive**: Build everything you love from larger frameworks—Multi-Agent systems, RAG pipelines, and complex workflows—using simple, composable building blocks.
## How does it work?
The core abstraction is a **Graph + Shared Store**.
1. **Node**: The smallest unit of work (e.g., call an LLM, read a file).
2. **Flow**: Connects Nodes into a graph. Transitions are determined by simple string "Actions".
3. **Shared Store**: A simple `stdClass` object passed through the entire flow, allowing nodes to communicate and share state.
This simple model is all you need to create powerful design patterns:
## Getting Started in 60 Seconds
Get up and running with your first AI-powered application.
**Prerequisites:** PHP 8.3+ and [Composer](https://getcomposer.org/) must be installed.
1. **Clone the repository:**
```bash
git clone https://github.com/weise25/PocketFlow-PHP.git
cd PocketFlow-PHP
```
2. **Install dependencies:**
```bash
composer install
```
3. **Set up your environment:**
- Create a `.env` file in the root of the project.
- Add your API keys to the `.env` file (e.g., `OPENROUTER_API_KEY=...`).
### Chapter 2: Core Abstraction
# Core Abstraction
This chapter explains the fundamental building blocks of **PocketFlow-PHP**. Understanding these concepts is essential for building any application with the framework.
We model the LLM workflow as a **Graph + Shared Store**.
- [Node] – The smallest unit of work.
- [Flow] – The orchestrator that connects Nodes.
- [Communication] – How Nodes and Flows share data.
- [Batch] – Tools for handling multiple items efficiently.
- [(Advanced) Async] – For non-blocking, I/O-bound tasks.
- [(Advanced) Parallel] – For running multiple async tasks concurrently.
---
### 2.1: Node
# Node
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()`.
### 1. The Lifecycle
1. **`prep(stdClass $shared): mixed`**
* **Purpose:** To read data from the `$shared` store and prepare it for the main task.
* **Example:** Fetch a user ID from `$shared->user->id`.
* **Returns:** The data needed by the `exec()` method.
2. **`exec(mixed $prepResult): mixed`**
* **Purpose:** To perform the core computation. This is where you call LLMs, APIs, or run business logic.
* **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()`.
* **Returns:** The result of the computation.
3. **`post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string`**
* **Purpose:** To write the results from `exec()` back into the `$shared` store and to decide what happens next.
* **Returns:** An **action string**. This string determines which path the `Flow` will take. Returning `null` triggers the default path.
* > **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;`.
### 2. Fault Tolerance
You can make your Nodes resilient to failure using constructor arguments and a fallback method.
**Retries:**
Define the number of retries and the wait time in the constructor.
```php
// nodes.php
use PocketFlow\Node;
class ApiCallNode extends Node
{
public function __construct()
{
// Try exec() up to 3 times, wait 5 seconds between failures.
parent::__construct(maxRetries: 3, wait: 5);
}
public function exec(mixed $prepResult): string
{
// This might throw an exception if the API is down.
return call_external_api();
}
}
```
**Graceful Fallback:**
If all retries fail, the `execFallback()` method is called. Instead of crashing, you can return a safe default value.
```php
// nodes.php
use PocketFlow\Node;
class ApiCallNode extends Node
{
// ... constructor and exec() method ...
public function execFallback(mixed $prepResult, \Throwable $e): string
{
// Log the error and return a safe default.
error_log("API call failed after all retries: " . $e->getMessage());
return "Default value due to API failure.";
}
}
```
### 3. Example: A Complete Node
```php
// nodes.php
use PocketFlow\Node;
class SummarizeArticleNode extends Node
{
public function __construct()
{
parent::__construct(maxRetries: 2, wait: 3);
}
public function prep(stdClass $shared): ?string
{
return $shared->article_text ?? null;
}
public function exec(mixed $articleText): string
{
if (empty($articleText)) {
return "No text provided.";
}
$prompt = "Summarize this article: " . $articleText;
return call_llm($prompt); // This might fail.
}
public function execFallback(mixed $prepResult, \Throwable $e): string
{
return "Could not generate summary due to an error.";
}
public function post(stdClass $shared, mixed $prepResult, mixed $execResult): ?string
{
$shared->summary = $execResult;
return null; // Triggers the "default" action.
}
}
```
---
## 2.2: Flow
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.
### 1. Action-Based Transitions
You define the connections between nodes using a simple, fluent API.
**Default Transition:**
If a Node's `post()` method returns `null` (or the string `"default"`), the default path is taken.
```php
// flow.php
$nodeA = new NodeA();
$nodeB = new NodeB();
// If NodeA returns null, execute NodeB next.
$nodeA->next($nodeB);
$flow = new Flow($nodeA);
```
**Conditional Transitions:**
You can create branches in your logic based on specific action strings.
```php
// flow.php
$reviewNode = new ReviewNode();
$approveNode = new ApproveNode();
$rejectNode = new RejectNode();
// If reviewNode returns "approved", go to approveNode.
$reviewNode->on('approved')->next($approveNode);
// If reviewNode returns "rejected", go to rejectNode.
$reviewNode->on('rejected')->next($rejectNode);
$flow = new Flow($reviewNode);
```
### 2. Nested Flows (Composition)
A `Flow` itself behaves like a `Node`. This allows you to build complex workflows by composing smaller, reusable flows.
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.
**Example:**
```php
// flow.php
use PocketFlow\Flow;
// --- Sub-Flow 1: User Validation ---
$checkUser = new CheckUserNode();
$checkPermissions = new CheckPermissionsNode();
$checkUser->next($checkPermissions);
$validationFlow = new Flow($checkUser); // This flow might return "validation_failed"
// --- Sub-Flow 2: Data Processing ---
$loadData = new LoadDataNode();
$processData = new ProcessDataNode();
$loadData->next($processData);
$processingFlow = new Flow($loadData); // This flow might return "processing_complete"
// --- Main Flow: Composing the sub-flows ---
$endNode = new EndNode();
$errorNode = new ErrorNode();
// Wire the main flow
$validationFlow->next($processingFlow); // Default transition
$validationFlow->on('validation_failed')->next($errorNode);
$processingFlow->on('processing_complete')->next($endNode);
$mainFlow = new Flow($validationFlow);
$mainFlow->run($shared);
```
This creates a clear, modular structure:
```mermaid
graph TD
subgraph Main Flow
A(validationFlow) --> B(processingFlow);
A --> C(errorNode);
B --> D(endNode);
end
```
---
# 2.3: Communication
Nodes and Flows need to share data. PocketFlow provides two mechanisms for this, each with a distinct purpose.
### 1. The Shared Store (`$shared`)
This is the **primary** method of communication and state management.
- **What it is:** A single `stdClass` object that is passed by reference to every Node in a Flow.
- **Purpose:** To hold all **mutable application state**. This includes initial inputs, intermediate results, final outputs, counters, flags, etc.
- **When to use:** For almost everything. It acts as the central "memory" or "database" for your workflow.
- **How to use:**
- Read from it in `prep()`.
- Write to it in `post()`.
> **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`.
**Example:**
```php
// main.php
$shared = new stdClass();
$shared->user_id = 123;
$shared->user_data = null; // To be filled by a node
$shared->final_report = null; // To be filled by another node
$flow->run($shared);
// After the flow runs, $shared will be populated.
echo $shared->final_report;
```
### 2. Params (`$params`)
This is a **secondary** mechanism for configuration.
- **What it is:** An `array` that is passed by value to each Node.
- **Purpose:** To hold **immutable configuration** that does not change during the flow's execution.
- **When to use:** For things like model names, API endpoints, or identifiers in a `BatchFlow`.
- **How to use:**
- Set once on the top-level `Flow` using `$flow->setParams([...])`.
- Access within any Node via `$this->params['my_key']`.
> **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.
**Example:**
```php
// flow.php
$node = new ApiCallNode();
$flow = new Flow($node);
// Set the API endpoint once for the entire flow.
$flow->setParams(['api_endpoint' => 'https://api.example.com/v1']);
$flow->run($shared);
// nodes.php
class ApiCallNode extends Node
{
public function exec(mixed $prepResult): string
{
$endpoint = $this->params['api_endpoint'];
// ... make a call to the endpoint ...
return "Data from API";
}
}
```
---
### 2.4: Batch
# Batch Processing
PocketFlow provides two powerful classes for processing multiple items: `BatchNode` and `BatchFlow`.
### 1. `BatchNode`
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.
**How it works:**
- `prep(stdClass $shared)`: Must return an `array` of items to be processed.
- `exec(mixed $item)`: Is called **sequentially** for each `$item` in the array from `prep()`.
- `post(stdClass $shared, ..., array $execResultList)`: Receives an `array` containing the results of all `exec()` calls.
**Example: Summarize Text Chunks**
```php
// nodes.php
use PocketFlow\BatchNode;
class SummarizeChunksNode extends BatchNode
{
public function prep(stdClass $shared): array
{
// Assume $shared->text_chunks is an array of strings.
return $shared->text_chunks;
}
public function exec(mixed $chunk): string
{
// This method is called for each chunk.
return call_llm("Summarize this chunk: " . $chunk);
}
public function post(stdClass $shared, mixed $p, mixed $summaries): ?string
{
// $summaries is an array of all the individual summaries.
$shared->combined_summary = implode("\n", $summaries);
return null;
}
}
```
### 2. `BatchFlow`
Use `BatchFlow` when you want to run an entire **sub-flow** multiple times, each time with a different **configuration**.
**How it works:**
- `prep(stdClass $shared)`: Must return an `array` of **parameter arrays**.
- The sub-flow is executed once for each parameter array.
- Inside the sub-flow's nodes, the parameters are available via `$this->params`.
**Example: Process Multiple Files**
```php
// flow.php
use PocketFlow\Flow;
use PocketFlow\BatchFlow;
function create_file_processing_flow(): Flow
{
// The sub-flow that processes a single file.
$loadFileNode = new LoadFileNode();
$analyzeFileNode = new AnalyzeFileNode();
$loadFileNode->next($analyzeFileNode);
$singleFileFlow = new Flow($loadFileNode);
// The BatchFlow that orchestrates the process.
$allFilesFlow = new ProcessAllFilesBatchFlow($singleFileFlow);
return $allFilesFlow;
}
// nodes.php
use PocketFlow\Node;
use PocketFlow\BatchFlow;
class ProcessAllFilesBatchFlow extends BatchFlow
{
public function prep(stdClass $shared): array
{
// Returns an array of parameter sets.
// e.g., [['filename' => 'a.txt'], ['filename' => 'b.txt']]
return array_map(fn($f) => ['filename' => $f], $shared->filenames);
}
}
class LoadFileNode extends Node
{
public function exec(mixed $p): string
{
// Accesses the filename from the params set by the BatchFlow.
$filename = $this->params['filename'];
echo "Loading file: {$filename}\n";
return file_get_contents($filename);
}
public function post(stdClass $shared, mixed $p, mixed $content): ?string
{
$shared->current_content = $content;
return null;
}
}
class AnalyzeFileNode extends Node { /* ... analyzes $shared->current_content ... */ }
```
---
### Seite 2.5: (Advanced) Async
# (Advanced) Async
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**.
### 1. `AsyncNode`
An `AsyncNode` is similar to a regular `Node`, but its lifecycle methods are asynchronous.
**Rules for `AsyncNode`:**
1. Implement the `_async` versions of the lifecycle methods: `prep_async()`, `exec_async()`, `post_async()`.
2. Each of these methods **must** return a `React\Promise\PromiseInterface`.
3. The easiest way to return a promise is to wrap your logic in `return async(function() { ... })();`.
4. Inside an `async` block, you can use `await()` to wait for other promises to resolve.
**Example: Asynchronous API Call**
```php
// nodes.php
use PocketFlow\AsyncNode;
use React\Promise\PromiseInterface;
use function React\Async\async;
use function React\Async\await;
class FetchUserDataNode extends AsyncNode
{
public function prep_async(stdClass $shared): PromiseInterface
{
return async(fn() => $shared->user_id)();
}
public function exec_async(mixed $userId): PromiseInterface
{
// Assume call_user_api_async() returns a Promise.
return call_user_api_async($userId);
}
public function post_async(stdClass $shared, mixed $p, mixed $userData): PromiseInterface
{
return async(function() use ($shared, $userData) {
$shared->user_data = $userData;
return null;
})();
}
}
```
### 2. `AsyncFlow`
An `AsyncFlow` is used to orchestrate flows that contain at least one `AsyncNode`. It can also correctly orchestrate regular synchronous `Nodes`.
### 3. Running an Async Workflow
The top-level entry point of any application that uses async features **must** be wrapped in an `async` block.
**Example:**
```php
// main.php
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/flow.php';
use function React\Async\async;
use function React\Async\await;
// Wrap the entire application logic in async().
async(function() {
$shared = new stdClass();
$shared->user_id = 123;
$flow = create_user_data_flow(); // This flow returns an AsyncFlow
// Use await() to run the flow and wait for it to complete.
await($flow->run_async($shared));
print_r($shared->user_data);
})();
```
---
### 2.6: (Advanced) Parallel
# (Advanced) Parallel Execution
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.
> **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.
### 1. `AsyncParallelBatchNode`
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.
**Use Case:** Fetching data for multiple users from an API simultaneously.
**Example: Parallel API Calls**
```php
// nodes.php
use PocketFlow\AsyncParallelBatchNode;
use React\Promise\PromiseInterface;
use function React\Async\async;
class FetchAllUsersNode extends AsyncParallelBatchNode
{
public function prep_async(stdClass $shared): PromiseInterface
{
// Returns an array of user IDs.
return async(fn() => $shared->user_ids)();
}
public function exec_async(mixed $userId): PromiseInterface
{
// This is called concurrently for all user IDs.
// Assume call_user_api_async() returns a Promise.
return call_user_api_async($userId);
}
public function post_async(stdClass $shared, mixed $p, mixed $usersData): PromiseInterface
{
return async(function() use ($shared, $usersData) {
// $usersData is an array of results. The order is preserved.
$shared->users = $usersData;
return null;
})();
}
}
```
### 2. `AsyncParallelBatchFlow`
This is the parallel version of `AsyncBatchFlow`. It runs the entire sub-flow for **all parameter sets concurrently**.
**Use Case:** Processing multiple files, where each file requires a multi-step async workflow (e.g., download, analyze, upload).
**Example: Parallel File Processing**
```php
// flow.php
use PocketFlow\AsyncFlow;
use PocketFlow\AsyncParallelBatchFlow;
function create_parallel_file_flow(): AsyncFlow
{
// The sub-flow for a single file.
$downloadNode = new DownloadFileAsyncNode();
$analyzeNode = new AnalyzeFileAsyncNode();
$downloadNode->next($analyzeNode);
$singleFileFlow = new AsyncFlow($downloadNode);
// The parallel batch flow.
$parallelFlow = new ProcessAllFilesParallelBatchFlow($singleFileFlow);
return $parallelFlow;
}
// nodes.php
use PocketFlow\AsyncParallelBatchFlow;
use React\Promise\PromiseInterface;
use function React\Async\async;
class ProcessAllFilesParallelBatchFlow extends AsyncParallelBatchFlow
{
public function prep_async(stdClass $shared): PromiseInterface
{
// Returns an array of parameter sets.
return async(fn() => array_map(fn($f) => ['url' => $f], $shared->file_urls))();
}
}
// DownloadFileAsyncNode and AnalyzeFileAsyncNode are defined as regular AsyncNodes.
// They will access the URL via $this->params['url'].
```
> **Warning:** Be mindful of API rate limits when using parallel execution. A large number of concurrent requests can quickly exhaust your quota.
# Chapter 3: Design Pattern
# Design Patterns
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.
This chapter provides practical, copy-paste-ready examples for each pattern.
- [Agent] – For creating autonomous, decision-making entities.
- [Workflow] – For breaking down complex tasks into a sequence of simple steps.
- [RAG (Retrieval-Augmented Generation)] – For providing external knowledge to an LLM.
- [Map-Reduce] – For processing large amounts of data efficiently.
- [Structured Output] – For forcing an LLM to return clean, predictable data formats.
- [(Advanced) Multi-Agent Systems] – For orchestrating collaboration between multiple specialized agents.
---
### Seite 3.1: Agent
# Agent
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.
### The Agentic Loop
The core of an agent is a loop:
1. **Think:** The Agent `Node` analyzes the state (`$shared`) and the goal.
2. **Decide:** It calls an LLM to choose the next `action` from a predefined set of tools.
3. **Act:** The `Flow` routes to a `Tool Node` based on the chosen action.
4. **Observe:** The `Tool Node` executes its task (e.g., a web search) and updates the `$shared` store with the result.
5. The `Flow` loops back to the Agent `Node` to start the cycle again with new information.
```mermaid
graph TD
A[Agent Node (Think & Decide)] -- action: 'search' --> B[Search Tool Node (Act)];
A -- action: 'answer' --> C[Answer Node (Final Act)];
B -- result --> D(Update Shared Store);
D --> A;
```
### Example: A Simple Research Agent
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.
**`flow.php`**
```php
<?php
require_once __DIR__ . '/nodes.php';
use PocketFlow\Flow;
function create_research_agent_flow(): Flow
{
$decideNode = new DecideActionNode();
$searchNode = new SearchWebNode();
$answerNode = new AnswerNode();
// Define the agent's decision branches and the loop
$decideNode->on('search')->next($searchNode);
$decideNode->on('answer')->next($answerNode);
$searchNode->next($decideNode); // Loop back to the decision node after acting
return new Flow($decideNode);
}
```
**`nodes.php`**
```php
<?php
require_once __DIR__ . '/vendor/autoload.php';
// require_once __DIR__ . '/utils/llm_api.php';
// require_once __DIR__ . '/utils/web_search.php';
use PocketFlow\Node;
use Symfony\Component\Yaml\Yaml; // `composer require symfony/yaml`
class DecideActionNode extends Node
{
public function prep(stdClass $shared): array
{
$context = empty($shared->search_history) ? "Nothing yet." : implode("\n", $shared->search_history);
return [$shared->query, $context];
}
public function exec(mixed $prepResult): array
{
[$query, $context] = $prepResult;
$prompt = <<<PROMPT
Your goal is to answer the user's query: "{$query}"
You have already gathered this information:
---
{$context}
---
Based on this, do you have enough information to answer, or do you need to search the web?
Respond in YAML format with your reasoning.
```yaml
thinking: "Your reasoning here..."
action: "search" or "answer"
search_term: "Your search query here, if action is search"
```
PROMPT;
$response = call_llm($prompt); // Assume call_llm() is defined
preg_match('/```yaml\s*(.*?)\s*```/s', $response, $matches);
return Yaml::parse($matches[1] ?? '');
}
public function post(stdClass $shared, mixed $p, mixed $decision): ?string
{
if ($decision['action'] === 'search') {
$shared->current_search_term = $decision['search_term'];
}
return $decision['action'];
}
}
class SearchWebNode extends Node
{
public function prep(stdClass $shared): string
{
return $shared->current_search_term;
}
public function exec(mixed $searchTerm): string
{
echo "Agent is searching for: {$searchTerm}\n";
return web_search($searchTerm); // Assume web_search() is defined
}
public function post(stdClass $shared, mixed $p, mixed $searchResult): ?string
{
$shared->search_history[] = $searchResult;
return null; // Triggers default transition back to DecideActionNode
}
}
class AnswerNode extends Node
{
public function exec(mixed $p): string
{
// In a real app, this would also use prep() to get context
return "This is the final answer based on the research.";
}
public function post(stdClass $shared, mixed $p, mixed $answer): ?string
{
$shared->final_answer = $answer;
return null; // End of flow
}
}
```
---
### Chapter 3.2 - 3.6 (Workflow, RAG, etc.)
# Workflow
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.
### Example: Article Writing Workflow
This workflow generates an article by breaking the process into three distinct steps: outlining, drafting, and reviewing.
**`flow.php`**
```php
<?php
require_once __DIR__ . '/nodes.php';
use PocketFlow\Flow;
function create_article_workflow(): Flow
{
$outlineNode = new GenerateOutlineNode();
$draftNode = new DraftArticleNode();
$reviewNode = new ReviewArticleNode();
// A simple, linear chain of nodes
$outlineNode->next($draftNode)->next($reviewNode);
return new Flow($outlineNode);
}
```
**`nodes.php`**
```php
<?php
use PocketFlow\Node;
class GenerateOutlineNode extends Node {
public function prep(stdClass $shared): string { return $shared->topic; }
public function exec(mixed $topic): string { return "Outline for: {$topic}"; /* call_llm(...) */ }
public function post(stdClass $shared, mixed $p, mixed $outline): ?string {
$shared->outline = $outline;
return null;
}
}
class DraftArticleNode extends Node {
public function prep(stdClass $shared): string { return $shared->outline; }
public function exec(mixed $outline): string { return "Draft based on: {$outline}"; /* call_llm(...) */ }
public function post(stdClass $shared, mixed $p, mixed $draft): ?string {
$shared->draft = $draft;
return null;
}
}
class ReviewArticleNode extends Node {
public function prep(stdClass $shared): string { return $shared->draft; }
public function exec(mixed $draft): string { return "Reviewed draft: {$draft}"; /* call_llm(...) */ }
public function post(stdClass $shared, mixed $p, mixed $final): ?string {
$shared->final_article = $final;
return null;
}
}
```
# RAG (Retrieval-Augmented Generation)
RAG allows an LLM to answer questions using information from your own documents. It's a two-stage process.
### Stage 1: Offline Indexing (A Workflow)
This flow reads documents, splits them into chunks, creates vector embeddings for each chunk, and stores them in a vector database.
**`flow.php` (Offline Indexing)**
```php
<?php
use PocketFlow\Flow;
function create_indexing_flow(): Flow
{
$chunkNode = new ChunkFilesNode();
$embedNode = new EmbedChunksNode();
$storeNode = new StoreEmbeddingsNode();
$chunkNode->next($embedNode)->next($storeNode);
return new Flow($chunkNode);
}
```
**`nodes.php` (Offline Indexing)**
```php
<?php
use PocketFlow\BatchNode;
use PocketFlow\Node;
// 1. Chunking (using BatchNode)
class ChunkFilesNode extends BatchNode {
public function prep(stdClass $shared): array { return $shared->file_paths; }
public function exec(mixed $path): array { return sentence_chunk(file_get_contents($path)); }
public function post(stdClass $shared, mixed $p, mixed $chunkLists): ?string {
$shared->all_chunks = array_merge(...$chunkLists);
return null;
}
}
// 2. Embedding (using BatchNode)
class EmbedChunksNode extends BatchNode {
public function prep(stdClass $shared): array { return $shared->all_chunks; }
public function exec(mixed $chunk): array { return get_embedding($chunk); } // Assume get_embedding() exists
public function post(stdClass $shared, mixed $p, mixed $embeddings): ?string {
$shared->embeddings = $embeddings;
return null;
}
}
// 3. Storing
class StoreEmbeddingsNode extends Node {
public function exec(mixed $p): mixed {
/* ... code to store embeddings in a vector DB ... */
return null; // Must return a value compatible with 'mixed'
}
}
```
### Stage 2: Online Retrieval & Answering (A Workflow)
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.
**`flow.php` (Online Answering)**
```php
<?php
use PocketFlow\Flow;
function create_rag_answer_flow(): Flow
{
$embedQuery = new EmbedQueryNode();
$retrieveDocs = new RetrieveDocsNode();
$generateAnswer = new GenerateAnswerNode();
$embedQuery->next($retrieveDocs)->next($generateAnswer);
return new Flow($embedQuery);
}
```
# Map-Reduce