├── .cursorrules
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── ci.yml
│ └── code-quality.yml
├── .gitignore
├── .npmignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cookbook
├── pocketflow-batch-flow
│ ├── README.md
│ ├── output
│ │ ├── bird_blur.jpg
│ │ ├── bird_grayscale.jpg
│ │ ├── bird_sepia.jpg
│ │ ├── cat_blur.jpg
│ │ ├── cat_grayscale.jpg
│ │ ├── cat_sepia.jpg
│ │ ├── dog_blur.jpg
│ │ ├── dog_grayscale.jpg
│ │ └── dog_sepia.jpg
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── flow.ts
│ │ ├── images
│ │ ├── bird.jpg
│ │ ├── cat.jpg
│ │ └── dog.jpg
│ │ ├── main.ts
│ │ ├── nodes.ts
│ │ └── type.ts
├── pocketflow-batch-node
│ ├── README.md
│ ├── data
│ │ └── sales.csv
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── flow.ts
│ │ ├── main.ts
│ │ ├── nodes.ts
│ │ └── types.ts
├── pocketflow-hello-world
│ ├── README.md
│ ├── hello-world.ts
│ ├── package-lock.json
│ └── package.json
└── pocketflow-parallel-batch-flow
│ ├── README.md
│ ├── design.md
│ ├── output
│ ├── bird_blur.jpg
│ ├── bird_grayscale.jpg
│ ├── bird_sepia.jpg
│ ├── cat_blur.jpg
│ ├── cat_grayscale.jpg
│ ├── cat_sepia.jpg
│ ├── dog_blur.jpg
│ ├── dog_grayscale.jpg
│ ├── dog_sepia.jpg
│ └── report.txt
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ ├── flows
│ └── image_processing_flow.ts
│ ├── images
│ ├── bird.jpg
│ ├── cat.jpg
│ └── dog.jpg
│ ├── index.ts
│ ├── nodes
│ ├── completion_report_node.ts
│ ├── image_processing_node.ts
│ ├── image_scanner_node.ts
│ └── index.ts
│ ├── types.ts
│ └── utils
│ ├── index.ts
│ ├── process_image.ts
│ └── read_directory.ts
├── docs
├── _config.yml
├── core_abstraction
│ ├── batch.md
│ ├── communication.md
│ ├── flow.md
│ ├── index.md
│ ├── node.md
│ └── parallel.md
├── design_pattern
│ ├── agent.md
│ ├── index.md
│ ├── mapreduce.md
│ ├── multi_agent.md
│ ├── rag.md
│ ├── structure.md
│ └── workflow.md
├── guide.md
├── index.md
└── utility_function
│ ├── chunking.md
│ ├── embedding.md
│ ├── index.md
│ ├── llm.md
│ ├── text_to_speech.md
│ ├── vector.md
│ ├── viz.md
│ └── websearch.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
└── index.ts
├── tests
├── batch-flow.test.ts
├── batch-node.test.ts
├── core-abstraction-examples.test.ts
├── fallback.test.ts
├── flow-basic.test.ts
├── flow-composition.test.ts
├── mapreduce-pattern.test.ts
├── multi-agent-pattern.test.ts
├── parallel-batch-flow.test.ts
├── parallel-batch-node.test.ts
├── qa-pattern.test.ts
└── rag-pattern.test.ts
├── tsconfig.json
└── tsup.config.ts
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve
4 | title: "[BUG] "
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | ## Bug Description
10 |
11 | A clear and concise description of the bug.
12 |
13 | ## Steps To Reproduce
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | ## Expected Behavior
21 |
22 | A clear and concise description of what you expected to happen.
23 |
24 | ## Actual Behavior
25 |
26 | A clear and concise description of what actually happened.
27 |
28 | ## Screenshots
29 |
30 | If applicable, add screenshots to help explain your problem.
31 |
32 | ## Environment
33 |
34 | - OS: [e.g. Windows 10, macOS 12.3, Ubuntu 22.04]
35 | - Node.js version: [e.g. 16.14.2]
36 | - npm/yarn version: [e.g. npm 8.5.0]
37 | - PocketFlow version: [e.g. 1.0.0]
38 |
39 | ## Additional Context
40 |
41 | Add any other context about the problem here.
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE] "
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 | ## Problem Statement
10 |
11 | A clear and concise description of the problem you're experiencing or the use case this feature would address. Ex. I'm always frustrated when [...]
12 |
13 | ## Proposed Solution
14 |
15 | A clear and concise description of what you want to happen.
16 |
17 | ## Alternative Solutions
18 |
19 | A clear and concise description of any alternative solutions or features you've considered.
20 |
21 | ## Example Implementation
22 |
23 | If possible, provide a sketch, mockup, or example of how this feature might work.
24 |
25 | ## Additional Context
26 |
27 | Add any other context, references, or screenshots about the feature request here.
28 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 |
5 | ## Related Issue
6 |
7 |
8 |
9 |
10 | ## Type of Change
11 |
12 |
13 |
14 | - [ ] Bug fix (non-breaking change which fixes an issue)
15 | - [ ] New feature (non-breaking change which adds functionality)
16 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
17 | - [ ] Documentation update
18 | - [ ] Performance improvement
19 | - [ ] Code cleanup or refactor
20 |
21 | ## How Has This Been Tested?
22 |
23 |
24 |
25 |
26 | ## Checklist
27 |
28 |
29 |
30 | - [ ] My code follows the code style of this project
31 | - [ ] I have added tests that prove my fix is effective or that my feature works
32 | - [ ] I have updated the documentation accordingly
33 | - [ ] My changes generate no new warnings
34 | - [ ] Any dependent changes have been merged and published
35 |
36 | ## Screenshots (if appropriate)
37 |
38 |
39 |
40 | ## Additional Notes
41 |
42 |
43 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 | branches: [main, master]
8 |
9 | jobs:
10 | test-and-build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | node-version: [18.x, 20.x]
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | cache: "npm"
24 |
25 | - name: Install dependencies
26 | run: npm ci
27 |
28 | - name: Run tests
29 | run: npm test
30 |
31 | - name: Build
32 | run: npm run build
33 |
34 | - name: Upload build artifacts
35 | uses: actions/upload-artifact@v4
36 | with:
37 | name: dist-${{ matrix.node-version }}
38 | path: dist/
39 |
--------------------------------------------------------------------------------
/.github/workflows/code-quality.yml:
--------------------------------------------------------------------------------
1 | name: Code Quality
2 |
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 | branches: [main, master]
8 |
9 | jobs:
10 | typescript-check:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Use Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: "18.x"
19 | cache: "npm"
20 |
21 | - name: Install dependencies
22 | run: npm ci
23 |
24 | - name: TypeScript compilation check
25 | run: npx tsc --noEmit
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS generated files
2 | .DS_Store
3 | .DS_Store?
4 | ._*
5 | .direnv
6 | .Spotlight-V100
7 | .Trashes
8 | ehthumbs.db
9 | Thumbs.db
10 |
11 |
12 | # IDE specific files
13 | .idea/
14 | .vscode/
15 | *.swp
16 | *.swo
17 | *~
18 |
19 | # Node
20 | node_modules/
21 | npm-debug.log
22 | yarn-debug.log
23 | yarn-error.log
24 | .env
25 | .env.local
26 | .env.development.local
27 | .env.test.local
28 | .env.production.local
29 |
30 | # Python
31 | __pycache__/
32 | *.py[cod]
33 | *$py.class
34 | *.so
35 | .Python
36 | build/
37 | develop-eggs/
38 | dist/
39 | downloads/
40 | eggs/
41 | .eggs/
42 | lib/
43 | lib64/
44 | parts/
45 | sdist/
46 | var/
47 | wheels/
48 | *.egg-info/
49 | .installed.cfg
50 | *.egg
51 | venv/
52 | ENV/
53 |
54 | # Logs and databases
55 | *.log
56 | *.sql
57 | *.sqlite
58 |
59 | # Build output
60 | dist/
61 | build/
62 | out/
63 |
64 | # Coverage reports
65 | coverage/
66 | .coverage
67 | .coverage.*
68 | htmlcov/
69 |
70 | # Misc
71 | *.bak
72 | *.tmp
73 | *.temp
74 |
75 |
76 | test.ipynb
77 | .pytest_cache/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | tests/
3 | jest.config.js
4 | tsconfig.json
5 | .npmignore
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Project maintainers are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Project maintainers have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies
54 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Pocket Flow TypeScript
2 |
3 | Thank you for your interest in contributing to Pocket Flow! This document provides guidelines and instructions for contributing to this project.
4 |
5 | ## Table of Contents
6 |
7 | - [Code of Conduct](#code-of-conduct)
8 | - [Getting Started](#getting-started)
9 | - [Development Workflow](#development-workflow)
10 | - [Pull Request Process](#pull-request-process)
11 | - [Coding Standards](#coding-standards)
12 | - [Testing](#testing)
13 | - [Documentation](#documentation)
14 | - [Community](#community)
15 |
16 | ## Code of Conduct
17 |
18 | By participating in this project, you agree to abide by the [Code of Conduct](CODE_OF_CONDUCT.md). Please read it to understand the expectations for all contributors.
19 |
20 | ## Getting Started
21 |
22 | ### Prerequisites
23 |
24 | - Node.js (recommended version: 18.x or later)
25 | - npm (recommended version: 8.x or later)
26 | - Git
27 |
28 | ### Setup Development Environment
29 |
30 | 1. Fork the repository on GitHub
31 | 2. Clone your fork:
32 | ```bash
33 | git clone https://github.com/YOUR_USERNAME/PocketFlow-Typescript.git
34 | cd PocketFlow-Typescript
35 | ```
36 | 3. Add the original repository as upstream:
37 | ```bash
38 | git remote add upstream https://github.com/The-Pocket/PocketFlow-Typescript.git
39 | ```
40 | 4. Install dependencies:
41 | ```bash
42 | npm install
43 | ```
44 |
45 | ## Development Workflow
46 |
47 | 1. Create a new branch for your feature or bugfix:
48 |
49 | ```bash
50 | git checkout -b feature/your-feature-name
51 | # or
52 | git checkout -b fix/issue-you-are-fixing
53 | ```
54 |
55 | 2. Make your changes
56 |
57 | 3. Run tests to ensure your changes don't break existing functionality:
58 |
59 | ```bash
60 | npm test
61 | ```
62 |
63 | 4. Update documentation if necessary
64 |
65 | 5. Commit your changes with a descriptive commit message:
66 |
67 | ```bash
68 | git commit -m "Description of changes"
69 | ```
70 |
71 | 6. Push your branch to your fork:
72 |
73 | ```bash
74 | git push origin feature/your-feature-name
75 | ```
76 |
77 | 7. Create a Pull Request from your fork to the original repository
78 |
79 | ## Pull Request Process
80 |
81 | 1. Ensure your PR has a clear title and description
82 | 2. Link any relevant issues with "Fixes #issue-number" in the PR description
83 | 3. Make sure all tests pass
84 | 4. Request a code review from maintainers
85 | 5. Address any feedback from code reviews
86 | 6. Once approved, a maintainer will merge your PR
87 |
88 | ## Coding Standards
89 |
90 | - Follow the established code style in the project
91 | - Use TypeScript for type safety
92 | - Keep functions small and focused on a single responsibility
93 | - Use meaningful variable and function names
94 | - Comment your code when necessary, but prefer self-documenting code
95 | - Avoid any unnecessary dependencies
96 |
97 | ## Testing
98 |
99 | - Write tests for new features and bug fixes
100 | - Ensure all tests pass before submitting a PR
101 | - Aim for high test coverage
102 | - Follow existing testing patterns
103 |
104 | ## Documentation
105 |
106 | - Update relevant documentation for any new features
107 | - Include JSDoc comments for public APIs
108 | - Keep documentation up-to-date with code changes
109 | - Consider adding examples for complex features
110 |
111 | ## Community
112 |
113 | - Join our [Discord](https://discord.gg/hUHHE9Sa6T) for discussions and questions
114 | - Be respectful and helpful to other community members
115 | - Share knowledge and help others learn
116 |
117 | Thank you for contributing to Pocket Flow!
118 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Victor Duarte and Zachary Huang
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | 
6 | [](https://the-pocket.github.io/PocketFlow/)
7 |
8 |
9 |
10 |
11 | # PocketFlow.js
12 |
13 | PocketFlow.js is a TypeScript port of the original [Python version](https://github.com/The-Pocket/PocketFlow) - a minimalist LLM framework.
14 |
15 | ## Table of Contents
16 |
17 | - [Features](#features)
18 | - [Installation](#installation)
19 | - [Quick Start](#quick-start)
20 | - [Documentation](#documentation)
21 | - [Testing](#testing)
22 | - [Contributing](#contributing)
23 | - [Community](#community)
24 | - [License](#license)
25 |
26 | ## Features
27 |
28 | - **Lightweight**: Zero bloat, zero dependencies, zero vendor lock-in.
29 |
30 | - **Expressive**: Everything you love—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agents](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), and more.
31 |
32 | - **[Agentic Coding](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Let AI Agents (e.g., Cursor AI) build Agents—10x productivity boost!
33 |
34 | ## Installation
35 |
36 | ```bash
37 | npm install pocketflow
38 | ```
39 |
40 | Alternatively, you can simply copy the [source code](src/index.ts) directly into your project.
41 |
42 | ## Quick Start
43 |
44 | Run the following command to create a new PocketFlow project:
45 |
46 | ```bash
47 | npx create-pocketflow
48 | ```
49 |
50 | Use cursor/windsurf/any other LLM builtin IDE to open the project.
51 | You can type the following prompt to the agent to confirm the project is setup correctly:
52 |
53 | ```
54 | Help me describe briefly about PocketFlow.js
55 | ```
56 |
57 | Simply start typing your prompt, and the AI agent will build the project for you.
58 | Here's a simple example:
59 |
60 | ```
61 | I want to create an application that can write novel:
62 |
63 | 1. User can enter a novel title
64 | 2. It will generate a outline of the novel
65 | 3. It will generate a chapter based on the outline
66 | 4. It will save the chapter to ./output/title_name.md
67 |
68 | First, read the requirements carefully.
69 | Then, start with design.md first. Stop there until further instructions.
70 | ```
71 |
72 | Once you have the design, and you have no questions, start the implementation by simply typing:
73 |
74 | ```
75 | Start implementing the design.
76 | ```
77 |
78 | ## Documentation
79 |
80 | - Check out the [official documentation](https://the-pocket.github.io/PocketFlow/) for comprehensive guides and examples. The TypeScript version is still under development, so some features may not be available.
81 | - For an in-depth design explanation, read our [design essay](https://github.com/The-Pocket/.github/blob/main/profile/pocketflow.md)
82 |
83 | ## Testing
84 |
85 | To run tests locally:
86 |
87 | ```bash
88 | # Install dependencies
89 | npm install
90 |
91 | # Run tests
92 | npm test
93 | ```
94 |
95 | ## Contributing
96 |
97 | We welcome contributions from the community! Here's how you can help:
98 |
99 | ### Code of Conduct
100 |
101 | Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md) to foster an inclusive community.
102 |
103 | ### CI/CD Workflow
104 |
105 | We use GitHub Actions for continuous integration and deployment:
106 |
107 | - **CI Workflow**: Automatically runs tests and builds the project on each push and pull request to the main branch.
108 | - **Code Quality**: Checks TypeScript compilation to ensure code quality.
109 | - **Release**: Publishes the package to npm when a new release is created.
110 |
111 | Note: To publish to npm, maintainers need to configure the `NPM_TOKEN` secret in the repository settings.
112 |
113 | ### How to Contribute
114 |
115 | 1. **Fork the Repository**
116 |
117 | - Create your own fork of the repo
118 |
119 | 2. **Create a Branch**
120 |
121 | - Create a feature branch (`git checkout -b feature/amazing-feature`)
122 | - For bug fixes, use (`git checkout -b fix/bug-description`)
123 |
124 | 3. **Make Your Changes**
125 |
126 | - Follow the code style and conventions
127 | - Add or update tests as needed
128 | - Keep your changes focused and related to a single issue
129 |
130 | 4. **Test Your Changes**
131 |
132 | - Ensure all tests pass with `npm test`
133 | - Add new tests if appropriate
134 |
135 | 5. **Commit Your Changes**
136 |
137 | - Use clear and descriptive commit messages
138 | - Reference issue numbers in commit messages when applicable
139 |
140 | 6. **Submit a Pull Request**
141 | - Provide a clear description of the changes
142 | - Link any related issues
143 | - Answer any questions or feedback during review
144 |
145 | ### Creating a CursorRule
146 |
147 | To create a CursorRule to make AI agents work more effectively on the codebase:
148 |
149 | 1. Visit [gitingest.com](https://gitingest.com/)
150 | 2. Paste the link to the docs folder (e.g., https://github.com/The-Pocket/PocketFlow-Typescript/tree/main/docs) to generate content
151 | 3. Remove the following from the generated result:
152 | - All utility function files except for llm
153 | - The design_pattern/multi_agent.md file
154 | - All \_config.yaml and index.md files, except for docs/index.md
155 | 4. Save the result as a CursorRule to help AI agents understand the codebase structure better
156 |
157 | ### Development Setup
158 |
159 | ```bash
160 | # Clone your forked repository
161 | git clone https://github.com/yourusername/PocketFlow-Typescript.git
162 | cd PocketFlow-Typescript
163 |
164 | # Install dependencies
165 | npm install
166 |
167 | # Run tests
168 | npm test
169 | ```
170 |
171 | ### Reporting Bugs
172 |
173 | When reporting bugs, please include:
174 |
175 | - A clear, descriptive title
176 | - Detailed steps to reproduce the issue
177 | - Expected and actual behavior
178 | - Environment information (OS, Node.js version, etc.)
179 | - Any additional context or screenshots
180 |
181 | ## Community
182 |
183 | - Join our [Discord server](https://discord.gg/hUHHE9Sa6T) for discussions and support
184 | - Follow us on [GitHub](https://github.com/The-Pocket)
185 |
186 | ## License
187 |
188 | This project is licensed under the MIT License - see the LICENSE file for details.
189 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/README.md:
--------------------------------------------------------------------------------
1 | # PocketFlow BatchFlow Example
2 |
3 | This example demonstrates the BatchFlow concept in PocketFlow by implementing an image processor that applies different filters to multiple images.
4 |
5 | ## What this Example Demonstrates
6 |
7 | - How to use BatchFlow to run a Flow multiple times with different parameters
8 | - Key concepts of BatchFlow:
9 | 1. Creating a base Flow for single-item processing
10 | 2. Using BatchFlow to process multiple items with different parameters
11 | 3. Managing parameters across multiple Flow executions
12 |
13 | ## Project Structure
14 |
15 | ```
16 | pocketflow-batch-flow/
17 | ├── README.md
18 | ├── package.json
19 | ├── src/
20 | │ ├── images/
21 | │ │ ├── cat.jpg # Sample image 1
22 | │ │ ├── dog.jpg # Sample image 2
23 | │ │ └── bird.jpg # Sample image 3
24 | │ ├── main.ts # Entry point
25 | │ ├── flow.ts # Flow and BatchFlow definitions
26 | │ └── nodes.ts # Node implementations for image processing
27 | ```
28 |
29 | ## How it Works
30 |
31 | The example processes multiple images with different filters:
32 |
33 | 1. **Base Flow**: Processes a single image
34 |
35 | - Load image
36 | - Apply filter (grayscale, blur, or sepia)
37 | - Save processed image
38 |
39 | 2. **BatchFlow**: Processes multiple image-filter combinations
40 | - Takes a list of parameters (image + filter combinations)
41 | - Runs the base Flow for each parameter set
42 | - Organizes output in a structured way
43 |
44 | ## Installation
45 |
46 | ```bash
47 | npm install
48 | ```
49 |
50 | ## Usage
51 |
52 | ```bash
53 | npm run main
54 | ```
55 |
56 | ## Sample Output
57 |
58 | ```
59 | Processing images with filters...
60 |
61 | Processing cat.jpg with grayscale filter...
62 | Processing cat.jpg with blur filter...
63 | Processing dog.jpg with sepia filter...
64 | ...
65 |
66 | All images processed successfully!
67 | Check the 'output' directory for results.
68 | ```
69 |
70 | ## Key Concepts Illustrated
71 |
72 | 1. **Parameter Management**: Shows how BatchFlow manages different parameter sets
73 | 2. **Flow Reuse**: Demonstrates running the same Flow multiple times
74 | 3. **Batch Processing**: Shows how to process multiple items efficiently
75 | 4. **Real-world Application**: Provides a practical example of batch processing
76 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/bird_blur.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/bird_blur.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/bird_grayscale.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/bird_grayscale.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/bird_sepia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/bird_sepia.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/cat_blur.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/cat_blur.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/cat_grayscale.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/cat_grayscale.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/cat_sepia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/cat_sepia.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/dog_blur.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/dog_blur.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/dog_grayscale.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/dog_grayscale.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/output/dog_sepia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/output/dog_sepia.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pocketflow-batch-flow",
3 | "dependencies": {
4 | "pocketflow": ">=1.0.3",
5 | "sharp": "^0.32.6"
6 | },
7 | "devDependencies": {
8 | "@types/node": "^18.0.0",
9 | "ts-node": "^10.9.1",
10 | "typescript": "^5.0.0"
11 | },
12 | "scripts": {
13 | "main": "npx ts-node src/main.ts"
14 | }
15 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/src/flow.ts:
--------------------------------------------------------------------------------
1 | import { Flow, BatchFlow } from 'pocketflow';
2 | import { loadImageNode, applyFilterNode, saveImageNode } from './nodes';
3 | import { SharedData, FilterType } from './type';
4 |
5 | // Types for the image processing parameters
6 | export type ImageProcessingParams = {
7 | imageName: string;
8 | filterType: FilterType;
9 | };
10 |
11 | // Connect nodes to create the processing pipeline
12 | // Each node passes its output to the next node as input
13 | loadImageNode.on("apply_filter", applyFilterNode);
14 | applyFilterNode.on("save", saveImageNode);
15 |
16 | // Create the base flow for processing a single image with a specific filter
17 | export const imageProcessingFlow = new Flow(loadImageNode);
18 |
19 | // Create the batch flow for processing multiple images with different filters
20 | export const batchImageProcessingFlow = new BatchFlow(loadImageNode);
21 |
22 | // Set batch parameters in main.ts before running
23 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/src/images/bird.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/src/images/bird.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/src/images/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/src/images/cat.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/src/images/dog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-batch-flow/src/images/dog.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/src/main.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { batchImageProcessingFlow, ImageProcessingParams } from './flow';
4 | import { SharedData } from './type';
5 |
6 | async function main() {
7 | // Get all image files from the images directory
8 | const imageDir = path.join(__dirname, 'images');
9 | const imageFiles = fs.readdirSync(imageDir)
10 | .filter(file => /\.(jpg|jpeg|png)$/i.test(file));
11 |
12 | console.log('PocketFlow BatchFlow Example - Image Processor');
13 | console.log('==============================================');
14 |
15 | // Define the filter types to use
16 | const filterTypes: Array<'grayscale' | 'blur' | 'sepia'> = ['grayscale', 'blur', 'sepia'];
17 |
18 | // Create a list of parameters for batch processing
19 | const batchParams: ImageProcessingParams[] = [];
20 |
21 | // Create combinations of images and filters
22 | for (const image of imageFiles) {
23 | for (const filter of filterTypes) {
24 | batchParams.push({
25 | imageName: image,
26 | filterType: filter
27 | });
28 | }
29 | }
30 |
31 | console.log(`\nProcessing ${imageFiles.length} images with ${filterTypes.length} filters...`);
32 | console.log(`Total operations: ${batchParams.length}\n`);
33 |
34 | try {
35 | // Define the prep method for the BatchFlow to return our batch parameters
36 | const originalPrep = batchImageProcessingFlow.prep;
37 | batchImageProcessingFlow.prep = async () => {
38 | return batchParams;
39 | };
40 |
41 | // Create shared context - can be used to track progress or share data between runs
42 | const sharedContext: SharedData = {};
43 |
44 | // Execute the batch flow with the shared context
45 | console.time('Batch processing time');
46 | await batchImageProcessingFlow.run(sharedContext);
47 | console.timeEnd('Batch processing time');
48 |
49 | // Restore original prep method
50 | batchImageProcessingFlow.prep = originalPrep;
51 |
52 | console.log('\nAll images processed successfully!');
53 | console.log('Check the \'output\' directory for results.');
54 |
55 | // Output summary of results
56 | console.log('\nProcessed images:');
57 | batchParams.forEach((params) => {
58 | console.log(`- ${params.imageName} with ${params.filterType} filter`);
59 | });
60 | } catch (error) {
61 | console.error('Error processing images:', error);
62 | }
63 | }
64 |
65 | // Run the main function
66 | main().catch(console.error);
67 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/src/nodes.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { Node } from 'pocketflow';
4 | import sharp from 'sharp';
5 | import { FilterType, SharedData } from './type';
6 |
7 | // Create output directory if it doesn't exist
8 | if (!fs.existsSync(path.join(__dirname, '../output'))) {
9 | fs.mkdirSync(path.join(__dirname, '../output'));
10 | }
11 |
12 | // Define interfaces for node parameters and results that satisfy NonIterableObject constraint
13 | type LoadImageParams ={
14 | imageName: string;
15 | }
16 |
17 | type LoadImageResult = {
18 | imageBuffer: Buffer;
19 | }
20 |
21 | type ApplyFilterParams = {
22 | filterType: FilterType;
23 | }
24 |
25 | type SaveImageResult = {
26 | outputPath: string;
27 | }
28 |
29 | // Node to load an image
30 | export class LoadImageNode extends Node {
31 |
32 | async prep(): Promise {
33 | const imageName = this._params.imageName;
34 | return imageName;
35 | }
36 |
37 | async exec(prepRes: string): Promise {
38 | console.log(`Loading image: ${prepRes}...`);
39 | const imagePath = path.join(__dirname, 'images', prepRes);
40 | const imageBuffer = fs.readFileSync(imagePath);
41 |
42 | return { imageBuffer };
43 | }
44 |
45 | async post(shared: SharedData, prepRes: string, execRes: LoadImageResult): Promise {
46 | shared.imageBuffer = execRes.imageBuffer;
47 | shared.imageName = prepRes;
48 | return "apply_filter";
49 | }
50 | }
51 |
52 | // Node to apply a filter to an image
53 | export class ApplyFilterNode extends Node {
54 |
55 | async prep(shared: SharedData): Promise<{
56 | filterType: FilterType;
57 | imageBuffer: Buffer;
58 | }> {
59 | const filterType = this._params.filterType;
60 | return {
61 | filterType,
62 | imageBuffer: shared.imageBuffer || Buffer.from([])
63 | }
64 | }
65 |
66 | async exec(prepRes: {
67 | filterType: FilterType;
68 | imageBuffer: Buffer;
69 | }): Promise {
70 | const filterType = prepRes.filterType;
71 |
72 | console.log(`Processing image with ${filterType} filter...`);
73 | let processedImage;
74 | switch (filterType) {
75 | case 'grayscale':
76 | processedImage = await sharp(prepRes.imageBuffer).grayscale().toBuffer();
77 | break;
78 | case 'blur':
79 | processedImage = await sharp(prepRes.imageBuffer).blur(10).toBuffer();
80 | break;
81 | case 'sepia':
82 | processedImage = await sharp(prepRes.imageBuffer)
83 | .modulate({ brightness: 1, saturation: 0.8 })
84 | .tint({ r: 255, g: 220, b: 180 })
85 | .toBuffer();
86 | break;
87 | default:
88 | throw new Error(`Unsupported filter type: ${filterType}`);
89 | }
90 |
91 | return processedImage;
92 | }
93 |
94 | async post(shared: SharedData, prepRes: {
95 | filterType: FilterType;
96 | }, execRes: Buffer): Promise {
97 | shared.processedImage = execRes;
98 | shared.filterType = prepRes.filterType;
99 | return "save";
100 | }
101 | }
102 |
103 | // Node to save a processed image
104 | export class SaveImageNode extends Node {
105 |
106 | async prep(shared: SharedData): Promise<{
107 | imageBuffer: Buffer;
108 | imageName: string;
109 | filterType: FilterType;
110 | }> {
111 | return {
112 | imageBuffer: shared.processedImage || Buffer.from([]),
113 | imageName: shared.imageName || "",
114 | filterType: shared.filterType || "grayscale"
115 | }
116 | }
117 |
118 | async exec(prepRes: {
119 | imageBuffer: Buffer;
120 | imageName: string;
121 | filterType: FilterType;
122 | }): Promise {
123 | const outputName = `${path.parse(prepRes.imageName).name}_${prepRes.filterType}${path.parse(prepRes.imageName).ext}`;
124 | const outputPath = path.join(__dirname, '../output', outputName);
125 |
126 | fs.writeFileSync(outputPath, prepRes.imageBuffer);
127 | console.log(`Saved processed image to: ${outputPath}`);
128 |
129 | return { outputPath };
130 | }
131 | }
132 |
133 | export const loadImageNode = new LoadImageNode();
134 | export const applyFilterNode = new ApplyFilterNode();
135 | export const saveImageNode = new SaveImageNode();
136 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-flow/src/type.ts:
--------------------------------------------------------------------------------
1 | export type FilterType = 'grayscale' | 'blur' | 'sepia';
2 |
3 | export interface SharedData {
4 | imageName?: string;
5 | imageBuffer?: Buffer;
6 | processedImage?: Buffer;
7 | filterType?: FilterType;
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-node/README.md:
--------------------------------------------------------------------------------
1 | # PocketFlow BatchNode Example
2 |
3 | This example demonstrates the BatchNode concept in PocketFlow by implementing a CSV processor that handles large files by processing them in chunks.
4 |
5 | ## What this Example Demonstrates
6 |
7 | - How to use BatchNode to process large inputs in chunks
8 | - The three key methods of BatchNode:
9 | 1. `prep`: Splits input into chunks
10 | 2. `exec`: Processes each chunk independently
11 | 3. `post`: Combines results from all chunks
12 |
13 | ## Project Structure
14 |
15 | ```
16 | pocketflow-batch-node/
17 | ├── README.md
18 | ├── data/
19 | │ └── sales.csv # Sample large CSV file
20 | ├── package.json
21 | ├── src/
22 | │ ├── main.ts # Entry point
23 | │ ├── flow.ts # Flow definition
24 | │ └── nodes.ts # BatchNode implementation
25 | ```
26 |
27 | ## How it Works
28 |
29 | The example processes a large CSV file containing sales data:
30 |
31 | 1. **Chunking (prep)**: The CSV file is read and split into chunks of N rows
32 | 2. **Processing (exec)**: Each chunk is processed to calculate:
33 | - Total sales
34 | - Average sale value
35 | - Number of transactions
36 | 3. **Combining (post)**: Results from all chunks are aggregated into final statistics
37 |
38 | ## Installation
39 |
40 | ```bash
41 | npm install
42 | ```
43 |
44 | ## Usage
45 |
46 | ```bash
47 | npm run main
48 | ```
49 |
50 | ## Sample Output
51 |
52 | ```
53 | Processing sales.csv in chunks...
54 |
55 | Final Statistics:
56 | - Total Sales: $999,359.04
57 | - Average Sale: $99.94
58 | - Total Transactions: 10,000
59 | ```
60 |
61 | ## Key Concepts Illustrated
62 |
63 | 1. **Chunk-based Processing**: Shows how BatchNode handles large inputs by breaking them into manageable pieces
64 | 2. **Independent Processing**: Demonstrates how each chunk is processed separately
65 | 3. **Result Aggregation**: Shows how individual results are combined into a final output
66 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pocketflow-batch-node",
3 | "dependencies": {
4 | "pocketflow": ">=1.0.3",
5 | "csv-parser": "^3.0.0",
6 | "fs-extra": "^11.1.1"
7 | },
8 | "devDependencies": {
9 | "@types/fs-extra": "^11.0.2",
10 | "@types/node": "^18.0.0",
11 | "ts-node": "^10.9.1",
12 | "typescript": "^5.0.0"
13 | },
14 | "scripts": {
15 | "main": "npx ts-node src/main.ts"
16 | }
17 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-node/src/flow.ts:
--------------------------------------------------------------------------------
1 | import { Flow } from 'pocketflow';
2 | import { csvProcessorNode, displayResultsNode } from './nodes';
3 | import { SharedData } from './types';
4 |
5 | // Connect nodes to create the processing pipeline
6 | csvProcessorNode.on("display_results", displayResultsNode);
7 |
8 | // Create and export the flow
9 | export const csvProcessingFlow = new Flow(csvProcessorNode);
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-node/src/main.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { csvProcessingFlow } from './flow';
3 | import { SharedData } from './types';
4 | import { csvProcessorNode } from './nodes';
5 |
6 | async function main() {
7 | console.log('PocketFlow BatchNode Example - CSV Processor');
8 | console.log('=============================================');
9 |
10 | const filePath = path.join(__dirname, '../data/sales.csv');
11 | console.log(`\nProcessing ${path.basename(filePath)} in chunks...`);
12 |
13 | try {
14 | // Create shared context with the file path
15 | const sharedContext: SharedData = {
16 | filePath
17 | };
18 |
19 | // Set parameters for the CSV processor node
20 | csvProcessorNode.setParams({
21 | filePath,
22 | chunkSize: 5 // Process 5 records at a time
23 | });
24 |
25 | // Execute the flow with the shared context
26 | console.time('Processing time');
27 | await csvProcessingFlow.run(sharedContext);
28 | console.timeEnd('Processing time');
29 |
30 | console.log('\nProcessing completed successfully!');
31 | } catch (error) {
32 | console.error('Error processing CSV:', error);
33 | }
34 | }
35 |
36 | // Run the main function
37 | main().catch(console.error);
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-node/src/nodes.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { BatchNode, Node } from 'pocketflow';
4 | import csv from 'csv-parser';
5 | import { SalesRecord, ChunkResult, SharedData } from './types';
6 |
7 | // Number of records per chunk
8 | const CHUNK_SIZE = 5;
9 |
10 | // Define parameters type for the CSV processor with index signature
11 | type CsvProcessorParams = {
12 | filePath: string;
13 | chunkSize?: number;
14 | }
15 |
16 | // Node to process CSV file in batches
17 | export class CsvProcessorNode extends BatchNode {
18 | async prep(shared: SharedData): Promise {
19 | // Use filePath from params or shared context
20 | const filePath = this._params?.filePath || shared.filePath;
21 | // Use chunkSize from params or default
22 | const chunkSize = this._params?.chunkSize || CHUNK_SIZE;
23 |
24 | console.log(`Reading CSV file: ${filePath}`);
25 |
26 | // Read the entire CSV file
27 | const allRecords: SalesRecord[] = [];
28 |
29 | // Return a promise that resolves when the CSV is fully read
30 | return new Promise((resolve, reject) => {
31 | fs.createReadStream(filePath || '')
32 | .pipe(csv())
33 | .on('data', (data: SalesRecord) => {
34 | allRecords.push(data);
35 | })
36 | .on('end', () => {
37 | console.log(`Total records loaded: ${allRecords.length}`);
38 |
39 | // Split all records into chunks of specified size
40 | const chunks: SalesRecord[][] = [];
41 | for (let i = 0; i < allRecords.length; i += chunkSize) {
42 | chunks.push(allRecords.slice(i, i + chunkSize));
43 | }
44 |
45 | console.log(`Split into ${chunks.length} chunks of ~${chunkSize} records each`);
46 | resolve(chunks);
47 | })
48 | .on('error', (error) => {
49 | reject(error);
50 | });
51 | });
52 | }
53 |
54 | async exec(chunk: SalesRecord[]): Promise {
55 | console.log(`Processing chunk with ${chunk.length} records...`);
56 |
57 | let totalSales = 0;
58 | let sumForAverage = 0;
59 | const totalTransactions = chunk.length;
60 |
61 | // Calculate statistics for this chunk
62 | for (const record of chunk) {
63 | const saleAmount = parseFloat(record.amount);
64 |
65 | totalSales += saleAmount;
66 | sumForAverage += saleAmount;
67 | }
68 |
69 | return {
70 | totalSales,
71 | totalTransactions,
72 | sumForAverage
73 | };
74 | }
75 |
76 | async post(shared: SharedData, chunks: SalesRecord[][], results: ChunkResult[]): Promise {
77 | console.log(`Combining results from ${results.length} chunks...`);
78 |
79 | // Store batch results in shared data
80 | shared.batchResults = results;
81 |
82 | // Aggregate final statistics
83 | const totalSales = results.reduce((sum, chunk) => sum + chunk.totalSales, 0);
84 | const totalTransactions = results.reduce((sum, chunk) => sum + chunk.totalTransactions, 0);
85 | const sumForAverage = results.reduce((sum, chunk) => sum + chunk.sumForAverage, 0);
86 | const averageSale = sumForAverage / totalTransactions;
87 |
88 | // Store final statistics in shared data
89 | shared.finalStats = {
90 | totalSales,
91 | averageSale,
92 | totalTransactions
93 | };
94 |
95 | return "display_results";
96 | }
97 | }
98 |
99 | // Node to display the final results
100 | export class DisplayResultsNode extends Node {
101 |
102 | async prep(shared: SharedData): Promise {
103 | return shared.finalStats;
104 | }
105 |
106 | async exec(finalStats: SharedData['finalStats']): Promise {
107 | // Check if finalStats exists to avoid errors
108 | if (!finalStats) {
109 | console.error("Error: Final statistics not available");
110 | return;
111 | }
112 |
113 | console.log("\nFinal Statistics:");
114 | console.log(`- Total Sales: $${finalStats.totalSales.toLocaleString(undefined, {
115 | minimumFractionDigits: 2,
116 | maximumFractionDigits: 2
117 | })}`);
118 | console.log(`- Average Sale: $${finalStats.averageSale.toLocaleString(undefined, {
119 | minimumFractionDigits: 2,
120 | maximumFractionDigits: 2
121 | })}`);
122 | console.log(`- Total Transactions: ${finalStats.totalTransactions.toLocaleString()}`);
123 | }
124 | }
125 |
126 | // Create instances of the nodes
127 | export const csvProcessorNode = new CsvProcessorNode();
128 | export const displayResultsNode = new DisplayResultsNode();
--------------------------------------------------------------------------------
/cookbook/pocketflow-batch-node/src/types.ts:
--------------------------------------------------------------------------------
1 | // Define the structure of a sales record from the CSV
2 | export interface SalesRecord {
3 | date: string;
4 | product: string;
5 | amount: string;
6 | }
7 |
8 | // Define the analysis result for a single chunk
9 | export interface ChunkResult {
10 | totalSales: number;
11 | totalTransactions: number;
12 | sumForAverage: number;
13 | }
14 |
15 | // Define the shared data structure for our flow
16 | export interface SharedData {
17 | filePath?: string;
18 | batchResults?: ChunkResult[];
19 | finalStats?: {
20 | totalSales: number;
21 | averageSale: number;
22 | totalTransactions: number;
23 | };
24 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-hello-world/README.md:
--------------------------------------------------------------------------------
1 | - `npm install`
2 | - `npx ts-node hello-world.ts`
--------------------------------------------------------------------------------
/cookbook/pocketflow-hello-world/hello-world.ts:
--------------------------------------------------------------------------------
1 | import { Node, Flow } from 'pocketflow';
2 |
3 | // Define a shared storage
4 | type SharedStorage = { text?: string };
5 |
6 | // Node that stores text
7 | class StoreTextNode extends Node {
8 | constructor(private text: string) {
9 | super();
10 | }
11 |
12 | async prep(shared: SharedStorage): Promise {
13 | shared.text = this.text;
14 | }
15 | }
16 |
17 | // Node that prints text
18 | class PrintTextNode extends Node {
19 | async prep(shared: SharedStorage): Promise {
20 | console.log(shared.text || "No text");
21 | }
22 | }
23 |
24 | // Run example
25 | async function main() {
26 | const shared: SharedStorage = {};
27 |
28 | const storeNode = new StoreTextNode("Hello World");
29 | const printNode = new PrintTextNode();
30 | storeNode.next(printNode);
31 |
32 | const flow = new Flow(storeNode);
33 | await flow.run(shared);
34 | }
35 |
36 | main();
--------------------------------------------------------------------------------
/cookbook/pocketflow-hello-world/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hello-pocketflow",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "name": "hello-pocketflow",
8 | "dependencies": {
9 | "pocketflow": ">=1.0.3"
10 | }
11 | },
12 | "node_modules/pocketflow": {
13 | "version": "1.0.3",
14 | "resolved": "https://registry.npmjs.org/pocketflow/-/pocketflow-1.0.3.tgz",
15 | "integrity": "sha512-6i+9GRD1E75/5x8OsGAaefB38Rib9icLUFX5Or8wUHLEW9/kuS8hB0Utw/ovRQYa1Q0GSRFbDqTwwn17pMikIg==",
16 | "engines": {
17 | "node": ">=18.0.0"
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-hello-world/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hello-pocketflow",
3 | "dependencies": {
4 | "pocketflow": ">=1.0.3"
5 | }
6 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/README.md:
--------------------------------------------------------------------------------
1 | # Parallel Image Processor
2 |
3 | Demonstrates how AsyncParallelBatchFlow processes multiple images with multiple filters >8x faster than sequential processing.
4 |
5 | ## Features
6 |
7 | ```mermaid
8 | graph TD
9 | subgraph AsyncParallelBatchFlow[Image Processing Flow]
10 | subgraph AsyncFlow[Per Image-Filter Flow]
11 | A[Load Image] --> B[Apply Filter]
12 | B --> C[Save Image]
13 | end
14 | end
15 | ```
16 |
17 | - Processes images with multiple filters in parallel
18 | - Applies three different filters (grayscale, blur, sepia)
19 | - Shows significant speed improvement over sequential processing
20 | - Manages system resources with semaphores
21 |
22 | ## Run It
23 |
24 | ```bash
25 | npm install
26 | npm run start
27 | ```
28 |
29 | ## Output
30 |
31 | ```
32 | Starting Image Filter Application...
33 | Scanning for images in C:\WorkSpace\PocketFlow-Typescript\cookbook\pocketflow-parallel-batch-flow\src\images
34 | Found 3 images to process
35 | Created 9 image processing tasks
36 | Processing bird.jpg with blur filter
37 | Processing bird.jpg with grayscale filter
38 | Processing bird.jpg with sepia filter
39 | Processing cat.jpg with blur filter
40 | Processing cat.jpg with grayscale filter
41 | Processing cat.jpg with sepia filter
42 | Processing dog.jpg with blur filter
43 | Processing dog.jpg with grayscale filter
44 | Processing dog.jpg with sepia filter
45 | Successfully processed 3 images
46 | Report generated at report.txt
47 | Image processing completed successfully!
48 | Processed 3 images with 3 filters (blur, grayscale, sepia).
49 | Output files and report can be found in the 'output' directory.
50 | ```
51 |
52 | ## Key Points
53 |
54 | - **Sequential**: Total time = sum of all item times
55 |
56 | - Good for: Rate-limited APIs, maintaining order
57 |
58 | - **Parallel**: Total time ≈ longest single item time
59 | - Good for: I/O-bound tasks, independent operations
60 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/design.md:
--------------------------------------------------------------------------------
1 | # Design Doc: Image Filter Application
2 |
3 | > Please DON'T remove notes for AI
4 |
5 | ## Requirements
6 |
7 | > Notes for AI: Keep it simple and clear.
8 | > If the requirements are abstract, write concrete user stories
9 |
10 | 1. The application must process multiple images located in the src/images directory
11 | 2. Three filters must be applied to each image: blur, grayscale, and sepia
12 | 3. Each image must be processed with all three filters, generating three output images
13 | 4. All processed images must be saved in an output folder with naming pattern originalName_filterName
14 | 5. Image processing must be parallelized using ParallelBatchFlow for efficiency
15 | 6. The application must generate a report of all processed images
16 | 7. The application must be compatible with Node.js environments
17 |
18 | **User Stories:**
19 |
20 | - As a user, I want to process multiple images with different filters so I can choose the best visual effect
21 | - As a user, I want the processing to happen in parallel to save time
22 | - As a user, I want processed images to be named consistently so I can easily identify them
23 | - As a user, I want a report of all processed images to track what was done
24 | - As a user, I want the application to run in any Node.js environment without special dependencies
25 |
26 | ## Technology Stack
27 |
28 | - **Node.js**: Runtime environment
29 | - **TypeScript**: Programming language
30 | - **PocketFlow**: Framework for parallel processing
31 | - **Sharp**: High-performance image processing library for Node.js
32 |
33 | ## Flow Design
34 |
35 | > Notes for AI:
36 | >
37 | > 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
38 | > 2. Present a concise, high-level description of the workflow.
39 |
40 | ### Applicable Design Pattern:
41 |
42 | ParallelBatchFlow is perfect for this use case because:
43 |
44 | 1. Each image needs the same set of filters applied (consistent workload)
45 | 2. The filter operations are independent and can run in parallel
46 | 3. Batch processing provides efficient resource utilization
47 |
48 | ### Flow high-level Design:
49 |
50 | 1. **Image Scanner Node**: Finds all images in src/images directory
51 | 2. **Image Processing Node**: Uses ParallelBatchFlow to apply filters to images
52 | 3. **Completion Report Node**: Generates a report of all processed images
53 |
54 | ```mermaid
55 | flowchart TD
56 | scanNode[Image Scanner Node] --> processNode[Image Processing Node]
57 | processNode --> reportNode[Completion Report Node]
58 |
59 | subgraph processNode[Image Processing Node - ParallelBatchFlow]
60 | subgraph batch1[Batch 1]
61 | image1[Image 1] --> filter1_1[Apply Filters]
62 | image2[Image 2] --> filter1_2[Apply Filters]
63 | end
64 | subgraph batch2[Batch 2]
65 | image3[Image 3] --> filter2_1[Apply Filters]
66 | image4[Image 4] --> filter2_2[Apply Filters]
67 | end
68 | end
69 | ```
70 |
71 | ## Utility Functions
72 |
73 | > Notes for AI:
74 | >
75 | > 1. Understand the utility function definition thoroughly by reviewing the doc.
76 | > 2. Include only the necessary utility functions, based on nodes in the flow.
77 |
78 | 1. **Read Directory** (`src/utils/read_directory.ts`)
79 |
80 | - _Input_: directoryPath (string)
81 | - _Output_: files (string[])
82 | - _Implementation_: Uses Node.js fs module to read directory and filter for image files
83 | - Used by Image Scanner Node to get all image files
84 |
85 | 2. **Image Processing** (`src/utils/process_image.ts`)
86 | - _Input_: imagePath (string), filter (string)
87 | - _Output_: processedImagePath (string)
88 | - _Implementation_: Uses Sharp library to apply filters to images
89 | - Filter implementations:
90 | - Blur: Uses Sharp's blur function with radius 5
91 | - Grayscale: Uses Sharp's built-in grayscale function
92 | - Sepia: Uses Sharp's recomb function with a sepia color matrix
93 | - Used by Image Processing Node to apply filters to images
94 |
95 | ## Node Design
96 |
97 | ### Shared Memory
98 |
99 | > Notes for AI: Try to minimize data redundancy
100 |
101 | The shared memory structure is organized as follows:
102 |
103 | ```typescript
104 | interface SharedData {
105 | // Input data
106 | inputImages: string[]; // Paths to input images
107 |
108 | // Processing data
109 | filters: string[]; // List of filters to apply (blur, grayscale, sepia)
110 |
111 | // Output data
112 | outputFolder: string; // Path to output folder
113 | processedImages: {
114 | // Tracking processed images
115 | imagePath: string;
116 | appliedFilters: string[];
117 | }[];
118 | }
119 | ```
120 |
121 | ### Node Steps
122 |
123 | > Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
124 |
125 | 1. Image Scanner Node
126 |
127 | - _Purpose_: Scan the src/images directory to find all input images
128 | - _Type_: Regular
129 | - _Steps_:
130 | - _prep_: Initialize empty inputImages array in shared store
131 | - _exec_: Read all files from src/images directory, filter for image files
132 | - _post_: Write image paths to inputImages in shared store and initialize filters array with ["blur", "grayscale", "sepia"]
133 |
134 | 2. Image Processing Node
135 |
136 | - _Purpose_: Apply filters to images in parallel
137 | - _Type_: ParallelBatchFlow
138 | - _Batch Configuration_:
139 | - _itemsPerBatch_: 5 (process 5 images at a time)
140 | - _concurrency_: 3 (run 3 parallel batches)
141 | - _Sub-flow_:
142 | - Apply Filter Node (for each image)
143 | - _Type_: Batch
144 | - _Input_: Single image path and array of filters
145 | - _Steps_:
146 | - _prep_: Load the image data using Sharp
147 | - _exec_: For each filter, apply the filter to the image using Sharp's API
148 | - _post_: Save processed images to output folder with naming pattern originalName_filterName
149 |
150 | 3. Completion Report Node
151 |
152 | - _Purpose_: Generate a report of all processed images
153 | - _Type_: Regular
154 | - _Steps_:
155 | - _prep_: Read processedImages from shared store
156 | - _exec_: Generate a summary report of all images and filters applied
157 | - _post_: Write report to output folder
158 |
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/bird_blur.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/bird_blur.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/bird_grayscale.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/bird_grayscale.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/bird_sepia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/bird_sepia.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/cat_blur.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/cat_blur.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/cat_grayscale.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/cat_grayscale.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/cat_sepia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/cat_sepia.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/dog_blur.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/dog_blur.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/dog_grayscale.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/dog_grayscale.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/dog_sepia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/output/dog_sepia.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/output/report.txt:
--------------------------------------------------------------------------------
1 | ===================================================
2 | IMAGE PROCESSING COMPLETION REPORT
3 | ===================================================
4 |
5 | Report generated on: 30/3/2025 上午9:31:46
6 | Total images processed: 3
7 |
8 | PROCESSING TIME SUMMARY:
9 | Total processing time: 0.07 seconds
10 | Theoretical sequential time: 0.39 seconds
11 | Speedup factor (parallel vs sequential): 5.43x
12 | Average processing time per task: 0.043 seconds
13 |
14 | PROCESSED IMAGES:
15 |
16 | - bird.jpg
17 | Filters applied: blur, grayscale, sepia
18 | Processing times:
19 | - blur: 0.037 seconds
20 | - grayscale: 0.032 seconds
21 | - sepia: 0.031 seconds
22 |
23 | - cat.jpg
24 | Filters applied: blur, grayscale, sepia
25 | Processing times:
26 | - blur: 0.032 seconds
27 | - grayscale: 0.043 seconds
28 | - sepia: 0.043 seconds
29 |
30 | - dog.jpg
31 | Filters applied: blur, grayscale, sepia
32 | Processing times:
33 | - blur: 0.066 seconds
34 | - grayscale: 0.052 seconds
35 | - sepia: 0.055 seconds
36 |
37 | ===================================================
38 | END OF REPORT
39 | ===================================================
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "image-filter-app",
3 | "scripts": {
4 | "start": "npx ts-node src/index.ts"
5 | },
6 | "dependencies": {
7 | "sharp": "^0.32.1",
8 | "pocketflow": "^1.0.0"
9 | },
10 | "devDependencies": {
11 | "@types/node": "^14.18.63",
12 | "typescript": "^4.9.5"
13 | }
14 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/flows/image_processing_flow.ts:
--------------------------------------------------------------------------------
1 | import { Flow } from 'pocketflow';
2 | import { ImageScannerNode } from '../nodes/image_scanner_node';
3 | import { ImageProcessingNode } from '../nodes/image_processing_node';
4 | import { CompletionReportNode } from '../nodes/completion_report_node';
5 | import { SharedData } from '../types';
6 |
7 | /**
8 | * Image Processing Flow
9 | * Orchestrates the entire image processing pipeline
10 | */
11 | export class ImageProcessingFlow extends Flow {
12 | /**
13 | * Constructor
14 | * @param itemsPerBatch Number of images to process in each batch
15 | * @param concurrency Number of parallel batches to run
16 | */
17 | constructor(itemsPerBatch: number = 5, concurrency: number = 3) {
18 | // Create nodes
19 | const scannerNode = new ImageScannerNode();
20 | const processingNode = new ImageProcessingNode();
21 | const reportNode = new CompletionReportNode();
22 |
23 | // Configure parallel batch processing if custom values are provided
24 | if (itemsPerBatch !== 5 || concurrency !== 3) {
25 | processingNode.setParams({
26 | itemsPerBatch,
27 | concurrency
28 | });
29 | }
30 |
31 | // Connect nodes
32 | scannerNode.next(processingNode);
33 | processingNode.next(reportNode);
34 |
35 | // Create flow with the scanner node as the starting point
36 | super(scannerNode);
37 | }
38 |
39 | /**
40 | * Run the flow with initial shared data
41 | */
42 | async process(): Promise {
43 | const shared: SharedData = {
44 | inputImages: [],
45 | filters: [],
46 | outputFolder: '',
47 | processedImages: []
48 | };
49 |
50 | await this.run(shared);
51 |
52 | return shared;
53 | }
54 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/images/bird.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/src/images/bird.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/images/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/src/images/cat.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/images/dog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/The-Pocket/PocketFlow-Typescript/ac46414861646efbd7b82cccefd55e4f236eafa4/cookbook/pocketflow-parallel-batch-flow/src/images/dog.jpg
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/index.ts:
--------------------------------------------------------------------------------
1 | import { ImageProcessingFlow } from './flows/image_processing_flow';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | /**
6 | * Main function to run the image filter application
7 | */
8 | async function main() {
9 | console.log("Starting Image Filter Application...");
10 |
11 | // Ensure the images directory exists
12 | const imagesDir = path.join(process.cwd(), 'src', 'images');
13 | if (!fs.existsSync(imagesDir)) {
14 | fs.mkdirSync(imagesDir, { recursive: true });
15 | console.log(`Created images directory at: ${imagesDir}`);
16 | console.log("Please add some image files to this directory and run the application again.");
17 | return;
18 | }
19 |
20 | // Check if there are any images in the directory
21 | const files = fs.readdirSync(imagesDir);
22 | const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif)$/i.test(file));
23 |
24 | if (imageFiles.length === 0) {
25 | console.log("No image files found in the images directory.");
26 | console.log("Please add some image files to the src/images directory and run the application again.");
27 | return;
28 | }
29 |
30 | console.log("--------------------------------------------------");
31 | console.log("Starting parallel image processing...");
32 | console.log("--------------------------------------------------");
33 |
34 | // Create and run the image processing flow
35 | try {
36 | // Start time for overall application
37 | const appStartTime = Date.now();
38 |
39 | // 5 images per batch, 3 parallel batches
40 | const processingFlow = new ImageProcessingFlow(5, 3);
41 | const result = await processingFlow.process();
42 |
43 | // End time for overall application
44 | const appEndTime = Date.now();
45 | const totalAppTime = (appEndTime - appStartTime) / 1000;
46 |
47 | console.log("--------------------------------------------------");
48 | console.log("Image processing completed successfully!");
49 | console.log(`Processed ${imageFiles.length} images with 3 filters (blur, grayscale, sepia).`);
50 | console.log(`Output files and report can be found in the 'output' directory.`);
51 | console.log("--------------------------------------------------");
52 | console.log(`Total application time: ${totalAppTime.toFixed(2)} seconds`);
53 | console.log("--------------------------------------------------");
54 | } catch (error) {
55 | console.error("Error processing images:", error);
56 | }
57 | }
58 |
59 | // Run the main function
60 | main().catch(console.error);
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/nodes/completion_report_node.ts:
--------------------------------------------------------------------------------
1 | import { Node } from 'pocketflow';
2 | import { SharedData } from '../types';
3 | import fs from 'fs';
4 | import path from 'path';
5 |
6 | /**
7 | * Completion Report Node
8 | * Generates a report of all processed images
9 | */
10 | export class CompletionReportNode extends Node {
11 | /**
12 | * Read processedImages from shared store
13 | */
14 | async prep(shared: SharedData): Promise {
15 | // Pass the entire shared data to include timing information
16 | return shared;
17 | }
18 |
19 | /**
20 | * Generate a summary report of all images and filters applied
21 | */
22 | async exec(shared: SharedData): Promise {
23 | const { processedImages, startTime, endTime, processingTimes } = shared;
24 |
25 | // Calculate overall processing time
26 | const totalProcessingTimeMs = endTime && startTime ? endTime - startTime : 0;
27 | const totalProcessingTimeSec = (totalProcessingTimeMs / 1000).toFixed(2);
28 |
29 | // Calculate average processing time per image/filter
30 | const avgProcessingTimeMs = processingTimes && processingTimes.length > 0
31 | ? processingTimes.reduce((sum, item) => sum + item.timeMs, 0) / processingTimes.length
32 | : 0;
33 |
34 | // Calculate theoretical sequential time (sum of all processing times)
35 | const sequentialTimeMs = processingTimes
36 | ? processingTimes.reduce((sum, item) => sum + item.timeMs, 0)
37 | : 0;
38 | const sequentialTimeSec = (sequentialTimeMs / 1000).toFixed(2);
39 |
40 | // Calculate speedup factor
41 | const speedupFactor = sequentialTimeMs > 0 && totalProcessingTimeMs > 0
42 | ? (sequentialTimeMs / totalProcessingTimeMs).toFixed(2)
43 | : 'N/A';
44 |
45 | const reportLines = [
46 | '===================================================',
47 | ' IMAGE PROCESSING COMPLETION REPORT ',
48 | '===================================================',
49 | '',
50 | `Report generated on: ${new Date().toLocaleString()}`,
51 | `Total images processed: ${processedImages.length}`,
52 | '',
53 | 'PROCESSING TIME SUMMARY:',
54 | `Total processing time: ${totalProcessingTimeSec} seconds`,
55 | `Theoretical sequential time: ${sequentialTimeSec} seconds`,
56 | `Speedup factor (parallel vs sequential): ${speedupFactor}x`,
57 | `Average processing time per task: ${(avgProcessingTimeMs / 1000).toFixed(3)} seconds`,
58 | '',
59 | 'PROCESSED IMAGES:',
60 | ''
61 | ];
62 |
63 | // Add details for each processed image
64 | for (const image of processedImages) {
65 | const imageName = path.basename(image.imagePath);
66 | reportLines.push(`- ${imageName}`);
67 | reportLines.push(` Filters applied: ${image.appliedFilters.join(', ')}`);
68 |
69 | // Add processing times for this image
70 | if (processingTimes) {
71 | const imageTimes = processingTimes.filter(t => t.imagePath === imageName);
72 | if (imageTimes.length > 0) {
73 | reportLines.push(' Processing times:');
74 | for (const time of imageTimes) {
75 | reportLines.push(` - ${time.filter}: ${(time.timeMs / 1000).toFixed(3)} seconds`);
76 | }
77 | }
78 | }
79 |
80 | reportLines.push('');
81 | }
82 |
83 | reportLines.push('===================================================');
84 | reportLines.push(' END OF REPORT ');
85 | reportLines.push('===================================================');
86 |
87 | return reportLines.join('\n');
88 | }
89 |
90 | /**
91 | * Write report to output folder
92 | */
93 | async post(shared: SharedData, _: SharedData, execRes: string): Promise {
94 | // Create output directory if it doesn't exist
95 | if (!fs.existsSync(shared.outputFolder)) {
96 | fs.mkdirSync(shared.outputFolder, { recursive: true });
97 | }
98 |
99 | // Write report to file
100 | const reportPath = path.join(shared.outputFolder, 'report.txt');
101 | fs.writeFileSync(reportPath, execRes);
102 |
103 | // Log summary of timing information to console
104 | const totalTime = shared.endTime && shared.startTime
105 | ? (shared.endTime - shared.startTime) / 1000
106 | : 0;
107 |
108 | console.log(`Report generated at ${reportPath}`);
109 | console.log(`Total processing time: ${totalTime.toFixed(2)} seconds`);
110 |
111 | if (shared.processingTimes && shared.processingTimes.length > 0) {
112 | const sequentialTime = shared.processingTimes.reduce((sum, item) => sum + item.timeMs, 0) / 1000;
113 | const speedup = (sequentialTime / totalTime).toFixed(2);
114 | console.log(`Sequential processing would have taken approximately ${sequentialTime.toFixed(2)} seconds`);
115 | console.log(`Parallel processing achieved a ${speedup}x speedup`);
116 | }
117 |
118 | return undefined; // End of flow
119 | }
120 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/nodes/image_processing_node.ts:
--------------------------------------------------------------------------------
1 | import { ParallelBatchNode } from 'pocketflow';
2 | import { SharedData } from '../types';
3 | import { processImage } from '../utils';
4 | import path from 'path';
5 |
6 | /**
7 | * Image Processing Input
8 | * Structure for each batch item in the ParallelBatchNode
9 | */
10 | interface ImageProcessingInput {
11 | imagePath: string;
12 | filter: string;
13 | }
14 |
15 | /**
16 | * Image Processing Result
17 | * Structure for the output of processing each image
18 | */
19 | interface ImageProcessingResult {
20 | originalPath: string;
21 | processedPath: string;
22 | filter: string;
23 | success: boolean;
24 | processingTimeMs: number; // Time taken to process
25 | }
26 |
27 | /**
28 | * Image Processing Node
29 | * Uses ParallelBatchFlow to apply filters to images in parallel
30 | */
31 | export class ImageProcessingNode extends ParallelBatchNode {
32 | // Set default batch parameters
33 | constructor() {
34 | super();
35 | this.setParams({
36 | itemsPerBatch: 5,
37 | concurrency: 3
38 | });
39 | }
40 |
41 | /**
42 | * Create a batch of image processing tasks
43 | * Each task consists of an image and a filter to apply
44 | */
45 | async prep(shared: SharedData): Promise {
46 | const tasks: ImageProcessingInput[] = [];
47 |
48 | // For each image, create a task for each filter
49 | for (const imagePath of shared.inputImages) {
50 | for (const filter of shared.filters) {
51 | tasks.push({
52 | imagePath,
53 | filter
54 | });
55 | }
56 | }
57 |
58 | console.log(`Created ${tasks.length} image processing tasks`);
59 | return tasks;
60 | }
61 |
62 | /**
63 | * Process a single image with the specified filter
64 | */
65 | async exec(input: ImageProcessingInput): Promise {
66 | console.log(`Processing ${path.basename(input.imagePath)} with ${input.filter} filter`);
67 |
68 | const startTime = Date.now();
69 | try {
70 | const processedPath = await processImage(input.imagePath, input.filter);
71 | const endTime = Date.now();
72 | const processingTimeMs = endTime - startTime;
73 |
74 | return {
75 | originalPath: input.imagePath,
76 | processedPath,
77 | filter: input.filter,
78 | success: true,
79 | processingTimeMs
80 | };
81 | } catch (error) {
82 | console.error(`Failed to process ${input.imagePath} with ${input.filter}:`, error);
83 | const endTime = Date.now();
84 | const processingTimeMs = endTime - startTime;
85 |
86 | return {
87 | originalPath: input.imagePath,
88 | processedPath: '',
89 | filter: input.filter,
90 | success: false,
91 | processingTimeMs
92 | };
93 | }
94 | }
95 |
96 | /**
97 | * Update the shared store with the results of all processed images
98 | */
99 | async post(
100 | shared: SharedData,
101 | _: ImageProcessingInput[],
102 | results: ImageProcessingResult[]
103 | ): Promise {
104 | // Group results by original image path
105 | const imageMap = new Map();
106 |
107 | // Record processing times and update processed images
108 | for (const result of results) {
109 | // Store processing time
110 | if (!shared.processingTimes) {
111 | shared.processingTimes = [];
112 | }
113 |
114 | shared.processingTimes.push({
115 | imagePath: path.basename(result.originalPath),
116 | filter: result.filter,
117 | timeMs: result.processingTimeMs
118 | });
119 |
120 | if (result.success) {
121 | const appliedFilters = imageMap.get(result.originalPath) || [];
122 | appliedFilters.push(result.filter);
123 | imageMap.set(result.originalPath, appliedFilters);
124 | }
125 | }
126 |
127 | // Update processedImages in shared store
128 | for (const [imagePath, appliedFilters] of imageMap.entries()) {
129 | shared.processedImages.push({
130 | imagePath,
131 | appliedFilters
132 | });
133 | }
134 |
135 | console.log(`Successfully processed ${shared.processedImages.length} images`);
136 |
137 | // Set the end time
138 | shared.endTime = Date.now();
139 |
140 | return 'default'; // Go to the next node
141 | }
142 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/nodes/image_scanner_node.ts:
--------------------------------------------------------------------------------
1 | import { Node } from 'pocketflow';
2 | import { SharedData } from '../types';
3 | import { readDirectory } from '../utils';
4 | import path from 'path';
5 |
6 | /**
7 | * Image Scanner Node
8 | * Scans the src/images directory to find all input images
9 | */
10 | export class ImageScannerNode extends Node {
11 | /**
12 | * Initialize empty inputImages array in shared store
13 | */
14 | async prep(shared: SharedData): Promise {
15 | shared.inputImages = [];
16 | // Initialize timing data
17 | shared.startTime = Date.now();
18 | shared.processingTimes = [];
19 | return null;
20 | }
21 |
22 | /**
23 | * Read all files from src/images directory, filter for image files
24 | */
25 | async exec(_: null): Promise {
26 | const imagesPath = path.join(process.cwd(), 'src', 'images');
27 | console.log(`Scanning for images in ${imagesPath}`);
28 | return readDirectory(imagesPath);
29 | }
30 |
31 | /**
32 | * Write image paths to inputImages in shared store and initialize filters array
33 | */
34 | async post(shared: SharedData, _: null, execRes: string[]): Promise {
35 | shared.inputImages = execRes;
36 | shared.filters = ['blur', 'grayscale', 'sepia'];
37 | shared.outputFolder = path.join(process.cwd(), 'output');
38 | shared.processedImages = [];
39 |
40 | console.log(`Found ${shared.inputImages.length} images to process`);
41 |
42 | return 'default'; // Go to the next node
43 | }
44 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/nodes/index.ts:
--------------------------------------------------------------------------------
1 | export { ImageScannerNode } from './image_scanner_node';
2 | export { ImageProcessingNode } from './image_processing_node';
3 | export { CompletionReportNode } from './completion_report_node';
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Interface for the shared memory data structure
3 | */
4 | export interface SharedData {
5 | // Input data
6 | inputImages: string[]; // Paths to input images
7 |
8 | // Processing data
9 | filters: string[]; // List of filters to apply (blur, grayscale, sepia)
10 |
11 | // Output data
12 | outputFolder: string; // Path to output folder
13 | processedImages: {
14 | // Tracking processed images
15 | imagePath: string;
16 | appliedFilters: string[];
17 | }[];
18 |
19 | // Timing data
20 | startTime?: number; // Start time in milliseconds
21 | endTime?: number; // End time in milliseconds
22 | processingTimes?: {
23 | // Individual processing times for each image/filter combination
24 | imagePath: string;
25 | filter: string;
26 | timeMs: number;
27 | }[];
28 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { readDirectory } from './read_directory';
2 | export { processImage } from './process_image';
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/utils/process_image.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import sharp from 'sharp';
4 |
5 | /**
6 | * Applies a filter to an image
7 | * @param imagePath Path to the image file
8 | * @param filter Filter to apply (blur, grayscale, sepia)
9 | * @returns Path to the processed image
10 | */
11 | export async function processImage(imagePath: string, filter: string): Promise {
12 | try {
13 | // Create a sharp instance with the input image
14 | let sharpImage = sharp(imagePath);
15 |
16 | // Apply the filter
17 | switch (filter) {
18 | case 'blur':
19 | sharpImage = sharpImage.blur(5);
20 | break;
21 | case 'grayscale':
22 | sharpImage = sharpImage.grayscale();
23 | break;
24 | case 'sepia':
25 | // Sepia is implemented using a color matrix
26 | sharpImage = sharpImage.recomb([
27 | [0.393, 0.769, 0.189],
28 | [0.349, 0.686, 0.168],
29 | [0.272, 0.534, 0.131]
30 | ]);
31 | break;
32 | default:
33 | console.warn(`Unknown filter: ${filter}`);
34 | }
35 |
36 | // Generate output path
37 | const { dir, name, ext } = path.parse(imagePath);
38 | const outputDir = path.join(process.cwd(), 'output');
39 |
40 | // Create output directory if it doesn't exist
41 | if (!fs.existsSync(outputDir)) {
42 | fs.mkdirSync(outputDir, { recursive: true });
43 | }
44 |
45 | const outputPath = path.join(outputDir, `${name}_${filter}${ext}`);
46 |
47 | // Save the processed image
48 | await sharpImage.toFile(outputPath);
49 |
50 | return outputPath;
51 | } catch (error) {
52 | console.error(`Error processing image ${imagePath} with filter ${filter}:`, error);
53 | throw error;
54 | }
55 | }
--------------------------------------------------------------------------------
/cookbook/pocketflow-parallel-batch-flow/src/utils/read_directory.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | /**
5 | * Reads all files from a directory
6 | * @param directoryPath Path to the directory to read
7 | * @returns Array of file paths
8 | */
9 | export function readDirectory(directoryPath: string): string[] {
10 | try {
11 | const files = fs.readdirSync(directoryPath);
12 | // Filter for image files (jpg, png, jpeg, gif)
13 | return files
14 | .filter(file =>
15 | /\.(jpg|jpeg|png|gif)$/i.test(file))
16 | .map(file => path.join(directoryPath, file));
17 | } catch (error) {
18 | console.error(`Error reading directory ${directoryPath}:`, error);
19 | return [];
20 | }
21 | }
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Basic site settings
2 | title: PocketFlow.js
3 | tagline: A 100-line LLM framework
4 | description: Minimalist LLM Framework in 100 Lines, Enabling LLMs to Program Themselves
5 |
6 | # Theme settings
7 | remote_theme: just-the-docs/just-the-docs
8 |
9 | # Navigation
10 | nav_sort: case_sensitive
11 |
12 | # Aux links (shown in upper right)
13 | aux_links:
14 | "View on GitHub":
15 | - "//github.com/the-pocket/PocketFlow"
16 |
17 | # Color scheme
18 | color_scheme: light
19 |
20 | # Author settings
21 | author:
22 | name: Zachary Huang
23 | url: https://www.columbia.edu/~zh2408/
24 | twitter: ZacharyHuang12
25 |
26 | # Mermaid settings
27 | mermaid:
28 | version: "9.1.3" # Pick the version you want
29 | # Default configuration
30 | config: |
31 | directionLR
32 |
33 | # Callouts settings
34 | callouts:
35 | warning:
36 | title: Warning
37 | color: red
38 | note:
39 | title: Note
40 | color: blue
41 | best-practice:
42 | title: Best Practice
43 | color: green
44 |
45 | # The custom navigation
46 | nav:
47 | - Home: index.md # Link to your main docs index
48 | - GitHub: "https://github.com/the-pocket/PocketFlow"
49 | - Discord: "https://discord.gg/hUHHE9Sa6T"
50 |
--------------------------------------------------------------------------------
/docs/core_abstraction/batch.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Batch"
4 | parent: "Core Abstraction"
5 | nav_order: 4
6 | ---
7 |
8 | # Batch
9 |
10 | **Batch** makes it easier to handle large inputs in one Node or **rerun** a Flow multiple times. Example use cases:
11 |
12 | - **Chunk-based** processing (e.g., splitting large texts).
13 | - **Iterative** processing over lists of input items (e.g., user queries, files, URLs).
14 |
15 | ## 1. BatchNode
16 |
17 | A **BatchNode** extends `Node` but changes `prep()` and `exec()`:
18 |
19 | - **`prep(shared)`**: returns an **array** of items to process.
20 | - **`exec(item)`**: called **once** per item in that iterable.
21 | - **`post(shared, prepRes, execResList)`**: after all items are processed, receives a **list** of results (`execResList`) and returns an **Action**.
22 |
23 | ### Example: Summarize a Large File
24 |
25 | ```typescript
26 | type SharedStorage = {
27 | data: string;
28 | summary?: string;
29 | };
30 |
31 | class MapSummaries extends BatchNode {
32 | async prep(shared: SharedStorage): Promise {
33 | // Chunk content into manageable pieces
34 | const content = shared.data;
35 | const chunks: string[] = [];
36 | const chunkSize = 10000;
37 |
38 | for (let i = 0; i < content.length; i += chunkSize) {
39 | chunks.push(content.slice(i, i + chunkSize));
40 | }
41 |
42 | return chunks;
43 | }
44 |
45 | async exec(chunk: string): Promise {
46 | const prompt = `Summarize this chunk in 10 words: ${chunk}`;
47 | return await callLlm(prompt);
48 | }
49 |
50 | async post(
51 | shared: SharedStorage,
52 | _: string[],
53 | summaries: string[]
54 | ): Promise {
55 | shared.summary = summaries.join("\n");
56 | return "default";
57 | }
58 | }
59 |
60 | // Usage
61 | const flow = new Flow(new MapSummaries());
62 | await flow.run({ data: "very long text content..." });
63 | ```
64 |
65 | ---
66 |
67 | ## 2. BatchFlow
68 |
69 | A **BatchFlow** runs a **Flow** multiple times, each time with different `params`. Think of it as a loop that replays the Flow for each parameter set.
70 |
71 | ### Example: Summarize Many Files
72 |
73 | ```typescript
74 | type SharedStorage = {
75 | files: string[];
76 | };
77 |
78 | type FileParams = {
79 | filename: string;
80 | };
81 |
82 | class SummarizeAllFiles extends BatchFlow {
83 | async prep(shared: SharedStorage): Promise {
84 | return shared.files.map((filename) => ({ filename }));
85 | }
86 | }
87 |
88 | // Create a per-file summarization flow
89 | const summarizeFile = new SummarizeFile();
90 | const summarizeAllFiles = new SummarizeAllFiles(summarizeFile);
91 |
92 | await summarizeAllFiles.run({ files: ["file1.txt", "file2.txt"] });
93 | ```
94 |
95 | ### Under the Hood
96 |
97 | 1. `prep(shared)` returns a list of param objects—e.g., `[{filename: "file1.txt"}, {filename: "file2.txt"}, ...]`.
98 | 2. The **BatchFlow** loops through each object and:
99 | - Merges it with the BatchFlow's own `params`
100 | - Calls `flow.run(shared)` using the merged result
101 | 3. This means the sub-Flow runs **repeatedly**, once for every param object.
102 |
103 | ---
104 |
105 | ## 3. Nested Batches
106 |
107 | You can nest BatchFlows to handle hierarchical data processing:
108 |
109 | ```typescript
110 | type DirectoryParams = {
111 | directory: string;
112 | };
113 |
114 | type FileParams = DirectoryParams & {
115 | filename: string;
116 | };
117 |
118 | class FileBatchFlow extends BatchFlow {
119 | async prep(shared: SharedStorage): Promise {
120 | const directory = this._params.directory;
121 | const files = await getFilesInDirectory(directory).filter((f) =>
122 | f.endsWith(".txt")
123 | );
124 |
125 | return files.map((filename) => ({
126 | directory, // Pass on directory from parent
127 | filename, // Add filename for this batch item
128 | }));
129 | }
130 | }
131 |
132 | class DirectoryBatchFlow extends BatchFlow {
133 | async prep(shared: SharedStorage): Promise {
134 | return ["/path/to/dirA", "/path/to/dirB"].map((directory) => ({
135 | directory,
136 | }));
137 | }
138 | }
139 |
140 | // Process all files in all directories
141 | const processingNode = new ProcessingNode();
142 | const fileFlow = new FileBatchFlow(processingNode);
143 | const dirFlow = new DirectoryBatchFlow(fileFlow);
144 | await dirFlow.run({});
145 | ```
146 |
--------------------------------------------------------------------------------
/docs/core_abstraction/communication.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Communication"
4 | parent: "Core Abstraction"
5 | nav_order: 3
6 | ---
7 |
8 | # Communication
9 |
10 | Nodes and Flows **communicate** in 2 ways:
11 |
12 | 1. **Shared Store (for almost all the cases)**
13 |
14 | - A global data structure (often an in-mem dict) that all nodes can read ( `prep()`) and write (`post()`).
15 | - Great for data results, large content, or anything multiple nodes need.
16 | - You shall design the data structure and populate it ahead.
17 | - > **Separation of Concerns:** Use `Shared Store` for almost all cases to separate _Data Schema_ from _Compute Logic_! This approach is both flexible and easy to manage, resulting in more maintainable code. `Params` is more a syntax sugar for [Batch](./batch.md).
18 | > {: .best-practice }
19 |
20 | 2. **Params (only for [Batch](./batch.md))**
21 | - Each node has a local, ephemeral `params` dict passed in by the **parent Flow**, used as an identifier for tasks. Parameter keys and values shall be **immutable**.
22 | - Good for identifiers like filenames or numeric IDs, in Batch mode.
23 |
24 | If you know memory management, think of the **Shared Store** like a **heap** (shared by all function calls), and **Params** like a **stack** (assigned by the caller).
25 |
26 | ---
27 |
28 | ## 1. Shared Store
29 |
30 | ### Overview
31 |
32 | A shared store is typically an in-mem dictionary, like:
33 |
34 | ```typescript
35 | interface SharedStore {
36 | data: Record;
37 | summary: Record;
38 | config: Record;
39 | // ...other properties
40 | }
41 |
42 | const shared: SharedStore = { data: {}, summary: {}, config: {} /* ... */ };
43 | ```
44 |
45 | It can also contain local file handlers, DB connections, or a combination for persistence. We recommend deciding the data structure or DB schema first based on your app requirements.
46 |
47 | ### Example
48 |
49 | ```typescript
50 | interface SharedStore {
51 | data: string;
52 | summary: string;
53 | }
54 |
55 | class LoadData extends Node {
56 | async post(
57 | shared: SharedStore,
58 | prepRes: unknown,
59 | execRes: unknown
60 | ): Promise {
61 | // We write data to shared store
62 | shared.data = "Some text content";
63 | return "default";
64 | }
65 | }
66 |
67 | class Summarize extends Node {
68 | async prep(shared: SharedStore): Promise {
69 | // We read data from shared store
70 | return shared.data;
71 | }
72 |
73 | async exec(prepRes: unknown): Promise {
74 | // Call LLM to summarize
75 | const prompt = `Summarize: ${prepRes}`;
76 | const summary = await callLlm(prompt);
77 | return summary;
78 | }
79 |
80 | async post(
81 | shared: SharedStore,
82 | prepRes: unknown,
83 | execRes: unknown
84 | ): Promise {
85 | // We write summary to shared store
86 | shared.summary = execRes as string;
87 | return "default";
88 | }
89 | }
90 |
91 | const loadData = new LoadData();
92 | const summarize = new Summarize();
93 | loadData.next(summarize);
94 | const flow = new Flow(loadData);
95 |
96 | const shared: SharedStore = { data: "", summary: "" };
97 | flow.run(shared);
98 | ```
99 |
100 | Here:
101 |
102 | - `LoadData` writes to `shared.data`.
103 | - `Summarize` reads from `shared.data`, summarizes, and writes to `shared.summary`.
104 |
105 | ---
106 |
107 | ## 2. Params
108 |
109 | **Params** let you store _per-Node_ or _per-Flow_ config that doesn't need to live in the shared store. They are:
110 |
111 | - **Immutable** during a Node's run cycle (i.e., they don't change mid-`prep->exec->post`).
112 | - **Set** via `setParams()`.
113 | - **Cleared** and updated each time a parent Flow calls it.
114 |
115 | > Only set the uppermost Flow params because others will be overwritten by the parent Flow.
116 | >
117 | > If you need to set child node params, see [Batch](./batch.md).
118 | > {: .warning }
119 |
120 | Typically, **Params** are identifiers (e.g., file name, page number). Use them to fetch the task you assigned or write to a specific part of the shared store.
121 |
122 | ### Example
123 |
124 | ```typescript
125 | interface SharedStore {
126 | data: Record;
127 | summary: Record;
128 | }
129 |
130 | interface SummarizeParams {
131 | filename: string;
132 | }
133 |
134 | // 1) Create a Node that uses params
135 | class SummarizeFile extends Node {
136 | async prep(shared: SharedStore): Promise {
137 | // Access the node's param
138 | const filename = this._params.filename;
139 | return shared.data[filename] || "";
140 | }
141 |
142 | async exec(prepRes: unknown): Promise {
143 | const prompt = `Summarize: ${prepRes}`;
144 | return await callLlm(prompt);
145 | }
146 |
147 | async post(
148 | shared: SharedStore,
149 | prepRes: unknown,
150 | execRes: unknown
151 | ): Promise {
152 | const filename = this._params.filename;
153 | shared.summary[filename] = execRes as string;
154 | return "default";
155 | }
156 | }
157 |
158 | // 2) Set params
159 | const node = new SummarizeFile();
160 |
161 | // 3) Set Node params directly (for testing)
162 | node.setParams({ filename: "doc1.txt" });
163 | node.run(shared);
164 |
165 | // 4) Create Flow
166 | const flow = new Flow(node);
167 |
168 | // 5) Set Flow params (overwrites node params)
169 | flow.setParams({ filename: "doc2.txt" });
170 | flow.run(shared); // The node summarizes doc2, not doc1
171 | ```
172 |
--------------------------------------------------------------------------------
/docs/core_abstraction/flow.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Flow"
4 | parent: "Core Abstraction"
5 | nav_order: 2
6 | ---
7 |
8 | # Flow
9 |
10 | A **Flow** orchestrates a graph of Nodes. You can chain Nodes in a sequence or create branching depending on the **Actions** returned from each Node's `post()`.
11 |
12 | ## 1. Action-based Transitions
13 |
14 | Each Node's `post()` returns an **Action** string. By default, if `post()` doesn't return anything, we treat that as `"default"`.
15 |
16 | You define transitions with the syntax:
17 |
18 | 1. **Basic default transition**: `nodeA.next(nodeB)`
19 | This means if `nodeA.post()` returns `"default"`, go to `nodeB`.
20 | (Equivalent to `nodeA.on("default", nodeB)`)
21 |
22 | 2. **Named action transition**: `nodeA.on("actionName", nodeB)`
23 | This means if `nodeA.post()` returns `"actionName"`, go to `nodeB`.
24 |
25 | It's possible to create loops, branching, or multi-step flows.
26 |
27 | ## 2. Method Chaining
28 |
29 | The transition methods support **chaining** for more concise flow creation:
30 |
31 | ### Chaining `on()` Methods
32 |
33 | The `on()` method returns the current node, so you can chain multiple action definitions:
34 |
35 | ```typescript
36 | // All transitions from the same node
37 | nodeA.on("approved", nodeB).on("rejected", nodeC).on("needs_review", nodeD);
38 |
39 | // Equivalent to:
40 | nodeA.on("approved", nodeB);
41 | nodeA.on("rejected", nodeC);
42 | nodeA.on("needs_review", nodeD);
43 | ```
44 |
45 | ### Chaining `next()` Methods
46 |
47 | The `next()` method returns the target node, allowing you to create linear sequences in a single expression:
48 |
49 | ```typescript
50 | // Creates a linear A → B → C → D sequence
51 | nodeA.next(nodeB).next(nodeC).next(nodeD);
52 |
53 | // Equivalent to:
54 | nodeA.next(nodeB);
55 | nodeB.next(nodeC);
56 | nodeC.next(nodeD);
57 | ```
58 |
59 | ### Combining Chain Types
60 |
61 | You can combine both chaining styles for complex flows:
62 |
63 | ```typescript
64 | nodeA
65 | .on("action1", nodeB.next(nodeC).next(nodeD))
66 | .on("action2", nodeE.on("success", nodeF).on("failure", nodeG));
67 | ```
68 |
69 | ## 3. Creating a Flow
70 |
71 | A **Flow** begins with a **start** node. You call `const flow = new Flow(someNode)` to specify the entry point. When you call `flow.run(shared)`, it executes the start node, looks at its returned Action from `post()`, follows the transition, and continues until there's no next node.
72 |
73 | ### Example: Simple Sequence
74 |
75 | Here's a minimal flow of two nodes in a chain:
76 |
77 | ```typescript
78 | nodeA.next(nodeB);
79 | const flow = new Flow(nodeA);
80 | flow.run(shared);
81 | ```
82 |
83 | - When you run the flow, it executes `nodeA`.
84 | - Suppose `nodeA.post()` returns `"default"`.
85 | - The flow then sees `"default"` Action is linked to `nodeB` and runs `nodeB`.
86 | - `nodeB.post()` returns `"default"` but we didn't define a successor for `nodeB`. So the flow ends there.
87 |
88 | ### Example: Branching & Looping
89 |
90 | Here's a simple expense approval flow that demonstrates branching and looping. The `ReviewExpense` node can return three possible Actions:
91 |
92 | - `"approved"`: expense is approved, move to payment processing
93 | - `"needs_revision"`: expense needs changes, send back for revision
94 | - `"rejected"`: expense is denied, finish the process
95 |
96 | We can wire them like this:
97 |
98 | ```typescript
99 | // Define the flow connections
100 | review.on("approved", payment); // If approved, process payment
101 | review.on("needs_revision", revise); // If needs changes, go to revision
102 | review.on("rejected", finish); // If rejected, finish the process
103 |
104 | revise.next(review); // After revision, go back for another review
105 | payment.next(finish); // After payment, finish the process
106 |
107 | const flow = new Flow(review);
108 | ```
109 |
110 | Let's see how it flows:
111 |
112 | 1. If `review.post()` returns `"approved"`, the expense moves to the `payment` node
113 | 2. If `review.post()` returns `"needs_revision"`, it goes to the `revise` node, which then loops back to `review`
114 | 3. If `review.post()` returns `"rejected"`, it moves to the `finish` node and stops
115 |
116 | ```mermaid
117 | flowchart TD
118 | review[Review Expense] -->|approved| payment[Process Payment]
119 | review -->|needs_revision| revise[Revise Report]
120 | review -->|rejected| finish[Finish Process]
121 |
122 | revise --> review
123 | payment --> finish
124 | ```
125 |
126 | ### Running Individual Nodes vs. Running a Flow
127 |
128 | - `node.run(shared)`: Just runs that node alone (calls `prep->exec->post()`), returns an Action.
129 | - `flow.run(shared)`: Executes from the start node, follows Actions to the next node, and so on until the flow can't continue.
130 |
131 | > `node.run(shared)` **does not** proceed to the successor.
132 | > This is mainly for debugging or testing a single node.
133 | >
134 | > Always use `flow.run(...)` in production to ensure the full pipeline runs correctly.
135 | {: .warning }
136 |
137 | ## 4. Nested Flows
138 |
139 | A **Flow** can act like a Node, which enables powerful composition patterns. This means you can:
140 |
141 | 1. Use a Flow as a Node within another Flow's transitions.
142 | 2. Combine multiple smaller Flows into a larger Flow for reuse.
143 | 3. Node `params` will be a merging of **all** parents' `params`.
144 |
145 | ### Flow's Node Methods
146 |
147 | A **Flow** is also a **Node**, so it will run `prep()` and `post()`. However:
148 |
149 | - It **won't** run `exec()`, as its main logic is to orchestrate its nodes.
150 | - `post()` always receives `undefined` for `execRes` and should instead get the flow execution results from the shared store.
151 |
152 | ### Basic Flow Nesting
153 |
154 | Here's how to connect a flow to another node:
155 |
156 | ```typescript
157 | // Create a sub-flow
158 | nodeA.next(nodeB);
159 | const subflow = new Flow(nodeA);
160 |
161 | // Connect it to another node
162 | subflow.next(nodeC);
163 |
164 | // Create the parent flow
165 | const parentFlow = new Flow(subflow);
166 | ```
167 |
168 | When `parentFlow.run()` executes:
169 |
170 | 1. It starts `subflow`
171 | 2. `subflow` runs through its nodes (`nodeA->nodeB`)
172 | 3. After `subflow` completes, execution continues to `nodeC`
173 |
174 | ### Example: Order Processing Pipeline
175 |
176 | Here's a practical example that breaks down order processing into nested flows:
177 |
178 | ```typescript
179 | // Payment processing sub-flow
180 | validatePayment.next(processPayment).next(paymentConfirmation);
181 | const paymentFlow = new Flow(validatePayment);
182 |
183 | // Inventory sub-flow
184 | checkStock.next(reserveItems).next(updateInventory);
185 | const inventoryFlow = new Flow(checkStock);
186 |
187 | // Shipping sub-flow
188 | createLabel.next(assignCarrier).next(schedulePickup);
189 | const shippingFlow = new Flow(createLabel);
190 |
191 | // Connect the flows into a main order pipeline
192 | paymentFlow.next(inventoryFlow).next(shippingFlow);
193 |
194 | // Create the master flow
195 | const orderPipeline = new Flow(paymentFlow);
196 |
197 | // Run the entire pipeline
198 | orderPipeline.run(sharedData);
199 | ```
200 |
201 | This creates a clean separation of concerns while maintaining a clear execution path:
202 |
203 | ```mermaid
204 | flowchart LR
205 | subgraph order_pipeline[Order Pipeline]
206 | subgraph paymentFlow["Payment Flow"]
207 | A[Validate Payment] --> B[Process Payment] --> C[Payment Confirmation]
208 | end
209 |
210 | subgraph inventoryFlow["Inventory Flow"]
211 | D[Check Stock] --> E[Reserve Items] --> F[Update Inventory]
212 | end
213 |
214 | subgraph shippingFlow["Shipping Flow"]
215 | G[Create Label] --> H[Assign Carrier] --> I[Schedule Pickup]
216 | end
217 |
218 | paymentFlow --> inventoryFlow
219 | inventoryFlow --> shippingFlow
220 | end
221 | ```
222 |
--------------------------------------------------------------------------------
/docs/core_abstraction/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Core Abstraction"
4 | nav_order: 2
5 | has_children: true
6 | ---
--------------------------------------------------------------------------------
/docs/core_abstraction/node.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Node"
4 | parent: "Core Abstraction"
5 | nav_order: 1
6 | ---
7 |
8 | # Node
9 |
10 | A **Node** is the smallest building block. Each Node has 3 steps `prep->exec->post`:
11 |
12 |
13 |

14 |
15 |
16 | 1. `prep(shared)`
17 |
18 | - **Read and preprocess data** from `shared` store.
19 | - Examples: _query DB, read files, or serialize data into a string_.
20 | - Return `prepRes`, which is used by `exec()` and `post()`.
21 |
22 | 2. `exec(prepRes)`
23 |
24 | - **Execute compute logic**, with optional retries and error handling (below).
25 | - Examples: _(mostly) LLM calls, remote APIs, tool use_.
26 | - ⚠️ This shall be only for compute and **NOT** access `shared`.
27 | - ⚠️ If retries enabled, ensure idempotent implementation.
28 | - Return `execRes`, which is passed to `post()`.
29 |
30 | 3. `post(shared, prepRes, execRes)`
31 | - **Postprocess and write data** back to `shared`.
32 | - Examples: _update DB, change states, log results_.
33 | - **Decide the next action** by returning a _string_ (`action = "default"` if _None_).
34 |
35 | > **Why 3 steps?** To enforce the principle of _separation of concerns_. The data storage and data processing are operated separately.
36 | >
37 | > All steps are _optional_. E.g., you can only implement `prep` and `post` if you just need to process data.
38 | > {: .note }
39 |
40 | ### Fault Tolerance & Retries
41 |
42 | You can **retry** `exec()` if it raises an exception via two parameters when define the Node:
43 |
44 | - `max_retries` (int): Max times to run `exec()`. The default is `1` (**no** retry).
45 | - `wait` (int): The time to wait (in **seconds**) before next retry. By default, `wait=0` (no waiting).
46 | `wait` is helpful when you encounter rate-limits or quota errors from your LLM provider and need to back off.
47 |
48 | ```typescript
49 | const myNode = new SummarizeFile(3, 10); // maxRetries = 3, wait = 10 seconds
50 | ```
51 |
52 | When an exception occurs in `exec()`, the Node automatically retries until:
53 |
54 | - It either succeeds, or
55 | - The Node has retried `maxRetries - 1` times already and fails on the last attempt.
56 |
57 | You can get the current retry times (0-based) from `this.currentRetry`.
58 |
59 | ### Graceful Fallback
60 |
61 | To **gracefully handle** the exception (after all retries) rather than raising it, override:
62 |
63 | ```typescript
64 | execFallback(prepRes: unknown, error: Error): unknown {
65 | return "There was an error processing your request.";
66 | }
67 | ```
68 |
69 | By default, it just re-raises the exception.
70 |
71 | ### Example: Summarize file
72 |
73 | ```typescript
74 | type SharedStore = {
75 | data: string;
76 | summary?: string;
77 | };
78 |
79 | class SummarizeFile extends Node {
80 | prep(shared: SharedStore): string {
81 | return shared.data;
82 | }
83 |
84 | exec(content: string): string {
85 | if (!content) return "Empty file content";
86 |
87 | const prompt = `Summarize this text in 10 words: ${content}`;
88 | return callLlm(prompt);
89 | }
90 |
91 | execFallback(_: string, error: Error): string {
92 | return "There was an error processing your request.";
93 | }
94 |
95 | post(shared: SharedStore, _: string, summary: string): string | undefined {
96 | shared.summary = summary;
97 | return undefined; // "default" action
98 | }
99 | }
100 |
101 | // Example usage
102 | const node = new SummarizeFile(3); // maxRetries = 3
103 | const shared: SharedStore = { data: "Long text to summarize..." };
104 | const action = node.run(shared);
105 |
106 | console.log("Action:", action);
107 | console.log("Summary:", shared.summary);
108 | ```
109 |
--------------------------------------------------------------------------------
/docs/core_abstraction/parallel.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "(Advanced) Parallel"
4 | parent: "Core Abstraction"
5 | nav_order: 6
6 | ---
7 |
8 | # (Advanced) Parallel
9 |
10 | **Parallel** Nodes and Flows let you run multiple operations **concurrently**—for example, summarizing multiple texts at once. This can improve performance by overlapping I/O and compute.
11 |
12 | > Parallel nodes and flows excel at overlapping I/O-bound work—like LLM calls, database queries, API requests, or file I/O. TypeScript's Promise-based implementation allows for truly concurrent execution of asynchronous operations.
13 | > {: .warning }
14 |
15 | > - **Ensure Tasks Are Independent**: If each item depends on the output of a previous item, **do not** parallelize.
16 | >
17 | > - **Beware of Rate Limits**: Parallel calls can **quickly** trigger rate limits on LLM services. You may need a **throttling** mechanism.
18 | >
19 | > - **Consider Single-Node Batch APIs**: Some LLMs offer a **batch inference** API where you can send multiple prompts in a single call. This is more complex to implement but can be more efficient than launching many parallel requests and mitigates rate limits.
20 | > {: .best-practice }
21 |
22 | ## ParallelBatchNode
23 |
24 | Like **BatchNode**, but runs operations in **parallel** using Promise.all():
25 |
26 | ```typescript
27 | class TextSummarizer extends ParallelBatchNode {
28 | async prep(shared: SharedStorage): Promise {
29 | // e.g., multiple texts
30 | return shared.texts || [];
31 | }
32 |
33 | async exec(text: string): Promise {
34 | const prompt = `Summarize: ${text}`;
35 | return await callLlm(prompt);
36 | }
37 |
38 | async post(
39 | shared: SharedStorage,
40 | prepRes: string[],
41 | execRes: string[]
42 | ): Promise {
43 | shared.summaries = execRes;
44 | return "default";
45 | }
46 | }
47 |
48 | const node = new TextSummarizer();
49 | const flow = new Flow(node);
50 | ```
51 |
52 | ## ParallelBatchFlow
53 |
54 | Parallel version of **BatchFlow**. Each iteration of the sub-flow runs **concurrently** using Promise.all():
55 |
56 | ```typescript
57 | class SummarizeMultipleFiles extends ParallelBatchFlow {
58 | async prep(shared: SharedStorage): Promise[]> {
59 | return (shared.files || []).map((f) => ({ filename: f }));
60 | }
61 | }
62 |
63 | const subFlow = new Flow(new LoadAndSummarizeFile());
64 | const parallelFlow = new SummarizeMultipleFiles(subFlow);
65 | await parallelFlow.run(shared);
66 | ```
67 |
--------------------------------------------------------------------------------
/docs/design_pattern/agent.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Agent"
4 | parent: "Design Pattern"
5 | nav_order: 1
6 | ---
7 |
8 | # Agent
9 |
10 | Agent is a powerful design pattern in which nodes can take dynamic actions based on the context.
11 |
12 |
13 |

14 |
15 |
16 | ## Implement Agent with Graph
17 |
18 | 1. **Context and Action:** Implement nodes that supply context and perform actions.
19 | 2. **Branching:** Use branching to connect each action node to an agent node. Use action to allow the agent to direct the [flow](../core_abstraction/flow.md) between nodes—and potentially loop back for multi-step.
20 | 3. **Agent Node:** Provide a prompt to decide action—for example:
21 |
22 | ```typescript
23 | `
24 | ### CONTEXT
25 | Task: ${task}
26 | Previous Actions: ${prevActions}
27 | Current State: ${state}
28 |
29 | ### ACTION SPACE
30 | [1] search
31 | Description: Use web search to get results
32 | Parameters: query (str)
33 |
34 | [2] answer
35 | Description: Conclude based on the results
36 | Parameters: result (str)
37 |
38 | ### NEXT ACTION
39 | Decide the next action based on the current context.
40 | Return your response in YAML format:
41 |
42 | \`\`\`yaml
43 | thinking:
44 | action:
45 | parameters:
46 | \`\`\``;
47 | ```
48 |
49 | The core of building **high-performance** and **reliable** agents boils down to:
50 |
51 | 1. **Context Management:** Provide _relevant, minimal context._ For example, rather than including an entire chat history, retrieve the most relevant via [RAG](./rag.md).
52 |
53 | 2. **Action Space:** Provide _a well-structured and unambiguous_ set of actions—avoiding overlap like separate `read_databases` or `read_csvs`.
54 |
55 | ## Example Good Action Design
56 |
57 | - **Incremental:** Feed content in manageable chunks instead of all at once.
58 | - **Overview-zoom-in:** First provide high-level structure, then allow drilling into details.
59 | - **Parameterized/Programmable:** Enable parameterized or programmable actions.
60 | - **Backtracking:** Let the agent undo the last step instead of restarting entirely.
61 |
62 | ## Example: Search Agent
63 |
64 | This agent:
65 |
66 | 1. Decides whether to search or answer
67 | 2. If searches, loops back to decide if more search needed
68 | 3. Answers when enough context gathered
69 |
70 | ````typescript
71 | interface SharedState {
72 | query?: string;
73 | context?: Array<{ term: string; result: string }>;
74 | search_term?: string;
75 | answer?: string;
76 | }
77 |
78 | class DecideAction extends Node {
79 | async prep(shared: SharedState): Promise<[string, string]> {
80 | const context = shared.context
81 | ? JSON.stringify(shared.context)
82 | : "No previous search";
83 | return [shared.query || "", context];
84 | }
85 |
86 | async exec([query, context]: [string, string]): Promise {
87 | const prompt = `
88 | Given input: ${query}
89 | Previous search results: ${context}
90 | Should I: 1) Search web for more info 2) Answer with current knowledge
91 | Output in yaml:
92 | \`\`\`yaml
93 | action: search/answer
94 | reason: why this action
95 | search_term: search phrase if action is search
96 | \`\`\``;
97 | const resp = await callLlm(prompt);
98 | const yamlStr = resp.split("```yaml")[1].split("```")[0].trim();
99 | return yaml.load(yamlStr);
100 | }
101 |
102 | async post(
103 | shared: SharedState,
104 | _: [string, string],
105 | result: any
106 | ): Promise {
107 | if (result.action === "search") {
108 | shared.search_term = result.search_term;
109 | }
110 | return result.action;
111 | }
112 | }
113 |
114 | class SearchWeb extends Node {
115 | async prep(shared: SharedState): Promise {
116 | return shared.search_term || "";
117 | }
118 |
119 | async exec(searchTerm: string): Promise {
120 | return await searchWeb(searchTerm);
121 | }
122 |
123 | async post(shared: SharedState, _: string, execRes: string): Promise {
124 | shared.context = [
125 | ...(shared.context || []),
126 | { term: shared.search_term || "", result: execRes },
127 | ];
128 | return "decide";
129 | }
130 | }
131 |
132 | class DirectAnswer extends Node {
133 | async prep(shared: SharedState): Promise<[string, string]> {
134 | return [
135 | shared.query || "",
136 | shared.context ? JSON.stringify(shared.context) : "",
137 | ];
138 | }
139 |
140 | async exec([query, context]: [string, string]): Promise {
141 | return await callLlm(`Context: ${context}\nAnswer: ${query}`);
142 | }
143 |
144 | async post(
145 | shared: SharedState,
146 | _: [string, string],
147 | execRes: string
148 | ): Promise {
149 | shared.answer = execRes;
150 | return undefined;
151 | }
152 | }
153 |
154 | // Connect nodes
155 | const decide = new DecideAction();
156 | const search = new SearchWeb();
157 | const answer = new DirectAnswer();
158 |
159 | decide.on("search", search);
160 | decide.on("answer", answer);
161 | search.on("decide", decide); // Loop back
162 |
163 | const flow = new Flow(decide);
164 | await flow.run({ query: "Who won the Nobel Prize in Physics 2024?" });
165 | ````
166 |
--------------------------------------------------------------------------------
/docs/design_pattern/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Design Pattern"
4 | nav_order: 3
5 | has_children: true
6 | ---
--------------------------------------------------------------------------------
/docs/design_pattern/mapreduce.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Map Reduce"
4 | parent: "Design Pattern"
5 | nav_order: 4
6 | ---
7 |
8 | # Map Reduce
9 |
10 | MapReduce is a design pattern suitable when you have either:
11 |
12 | - Large input data (e.g., multiple files to process), or
13 | - Large output data (e.g., multiple forms to fill)
14 |
15 | and there is a logical way to break the task into smaller, ideally independent parts.
16 |
17 |
18 |

19 |
20 |
21 | You first break down the task using [BatchNode](../core_abstraction/batch.md) in the map phase, followed by aggregation in the reduce phase.
22 |
23 | ### Example: Document Summarization
24 |
25 | ```typescript
26 | type SharedStorage = {
27 | files?: Record;
28 | file_summaries?: Record;
29 | all_files_summary?: string;
30 | };
31 |
32 | class SummarizeAllFiles extends BatchNode {
33 | async prep(shared: SharedStorage): Promise<[string, string][]> {
34 | return Object.entries(shared.files || {}); // [["file1.txt", "aaa..."], ["file2.txt", "bbb..."], ...]
35 | }
36 |
37 | async exec([filename, content]: [string, string]): Promise<[string, string]> {
38 | const summary = await callLLM(`Summarize the following file:\n${content}`);
39 | return [filename, summary];
40 | }
41 |
42 | async post(
43 | shared: SharedStorage,
44 | _: [string, string][],
45 | summaries: [string, string][]
46 | ): Promise {
47 | shared.file_summaries = Object.fromEntries(summaries);
48 | return "summarized";
49 | }
50 | }
51 |
52 | class CombineSummaries extends Node {
53 | async prep(shared: SharedStorage): Promise> {
54 | return shared.file_summaries || {};
55 | }
56 |
57 | async exec(summaries: Record): Promise {
58 | const text_list = Object.entries(summaries).map(
59 | ([fname, summ]) => `${fname} summary:\n${summ}\n`
60 | );
61 |
62 | return await callLLM(
63 | `Combine these file summaries into one final summary:\n${text_list.join(
64 | "\n---\n"
65 | )}`
66 | );
67 | }
68 |
69 | async post(
70 | shared: SharedStorage,
71 | _: Record,
72 | finalSummary: string
73 | ): Promise {
74 | shared.all_files_summary = finalSummary;
75 | return "combined";
76 | }
77 | }
78 |
79 | // Create and connect flow
80 | const batchNode = new SummarizeAllFiles();
81 | const combineNode = new CombineSummaries();
82 | batchNode.on("summarized", combineNode);
83 |
84 | // Run the flow with test data
85 | const flow = new Flow(batchNode);
86 | flow.run({
87 | files: {
88 | "file1.txt":
89 | "Alice was beginning to get very tired of sitting by her sister...",
90 | "file2.txt": "Some other interesting text ...",
91 | },
92 | });
93 | ```
94 |
95 | > **Performance Tip**: The example above works sequentially. You can speed up the map phase by using `ParallelBatchNode` instead of `BatchNode`. See [(Advanced) Parallel](../core_abstraction/parallel.md) for more details.
96 | > {: .note }
97 |
--------------------------------------------------------------------------------
/docs/design_pattern/rag.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "RAG"
4 | parent: "Design Pattern"
5 | nav_order: 3
6 | ---
7 |
8 | # RAG (Retrieval Augmented Generation)
9 |
10 | For certain LLM tasks like answering questions, providing relevant context is essential. One common architecture is a **two-stage** RAG pipeline:
11 |
12 |
13 |

14 |
15 |
16 | 1. **Offline stage**: Preprocess and index documents ("building the index").
17 | 2. **Online stage**: Given a question, generate answers by retrieving the most relevant context.
18 |
19 | ---
20 |
21 | ## Stage 1: Offline Indexing
22 |
23 | We create three Nodes:
24 |
25 | 1. `ChunkDocs` – [chunks](../utility_function/chunking.md) raw text.
26 | 2. `EmbedDocs` – [embeds](../utility_function/embedding.md) each chunk.
27 | 3. `StoreIndex` – stores embeddings into a [vector database](../utility_function/vector.md).
28 |
29 | ```typescript
30 | type SharedStore = {
31 | files?: string[];
32 | allChunks?: string[];
33 | allEmbeds?: number[][];
34 | index?: any;
35 | };
36 |
37 | class ChunkDocs extends BatchNode {
38 | async prep(shared: SharedStore): Promise {
39 | return shared.files || [];
40 | }
41 |
42 | async exec(filepath: string): Promise {
43 | const text = fs.readFileSync(filepath, "utf-8");
44 | // Simplified chunking for example
45 | const chunks: string[] = [];
46 | const size = 100;
47 | for (let i = 0; i < text.length; i += size) {
48 | chunks.push(text.substring(i, i + size));
49 | }
50 | return chunks;
51 | }
52 |
53 | async post(
54 | shared: SharedStore,
55 | _: string[],
56 | chunks: string[][]
57 | ): Promise {
58 | shared.allChunks = chunks.flat();
59 | return undefined;
60 | }
61 | }
62 |
63 | class EmbedDocs extends BatchNode {
64 | async prep(shared: SharedStore): Promise {
65 | return shared.allChunks || [];
66 | }
67 |
68 | async exec(chunk: string): Promise {
69 | return await getEmbedding(chunk);
70 | }
71 |
72 | async post(
73 | shared: SharedStore,
74 | _: string[],
75 | embeddings: number[][]
76 | ): Promise {
77 | shared.allEmbeds = embeddings;
78 | return undefined;
79 | }
80 | }
81 |
82 | class StoreIndex extends Node {
83 | async prep(shared: SharedStore): Promise {
84 | return shared.allEmbeds || [];
85 | }
86 |
87 | async exec(allEmbeds: number[][]): Promise {
88 | return await createIndex(allEmbeds);
89 | }
90 |
91 | async post(
92 | shared: SharedStore,
93 | _: number[][],
94 | index: unknown
95 | ): Promise {
96 | shared.index = index;
97 | return undefined;
98 | }
99 | }
100 |
101 | // Create indexing flow
102 | const chunkNode = new ChunkDocs();
103 | const embedNode = new EmbedDocs();
104 | const storeNode = new StoreIndex();
105 |
106 | chunkNode.next(embedNode).next(storeNode);
107 | const offlineFlow = new Flow(chunkNode);
108 | ```
109 |
110 | ---
111 |
112 | ## Stage 2: Online Query & Answer
113 |
114 | We have 3 nodes:
115 |
116 | 1. `EmbedQuery` – embeds the user's question.
117 | 2. `RetrieveDocs` – retrieves top chunk from the index.
118 | 3. `GenerateAnswer` – calls the LLM with the question + chunk to produce the final answer.
119 |
120 | ```typescript
121 | type OnlineStore = SharedStore & {
122 | question?: string;
123 | qEmb?: number[];
124 | retrievedChunk?: string;
125 | answer?: string;
126 | };
127 |
128 | class EmbedQuery extends Node {
129 | async prep(shared: OnlineStore): Promise {
130 | return shared.question || "";
131 | }
132 |
133 | async exec(question: string): Promise {
134 | return await getEmbedding(question);
135 | }
136 |
137 | async post(
138 | shared: OnlineStore,
139 | _: string,
140 | qEmb: number[]
141 | ): Promise {
142 | shared.qEmb = qEmb;
143 | return undefined;
144 | }
145 | }
146 |
147 | class RetrieveDocs extends Node {
148 | async prep(shared: OnlineStore): Promise<[number[], any, string[]]> {
149 | return [shared.qEmb || [], shared.index, shared.allChunks || []];
150 | }
151 |
152 | async exec([qEmb, index, chunks]: [
153 | number[],
154 | any,
155 | string[]
156 | ]): Promise {
157 | const [ids] = await searchIndex(index, qEmb, { topK: 1 });
158 | return chunks[ids[0][0]];
159 | }
160 |
161 | async post(
162 | shared: OnlineStore,
163 | _: [number[], any, string[]],
164 | chunk: string
165 | ): Promise {
166 | shared.retrievedChunk = chunk;
167 | return undefined;
168 | }
169 | }
170 |
171 | class GenerateAnswer extends Node {
172 | async prep(shared: OnlineStore): Promise<[string, string]> {
173 | return [shared.question || "", shared.retrievedChunk || ""];
174 | }
175 |
176 | async exec([question, chunk]: [string, string]): Promise {
177 | return await callLlm(`Question: ${question}\nContext: ${chunk}\nAnswer:`);
178 | }
179 |
180 | async post(
181 | shared: OnlineStore,
182 | _: [string, string],
183 | answer: string
184 | ): Promise {
185 | shared.answer = answer;
186 | return undefined;
187 | }
188 | }
189 |
190 | // Create query flow
191 | const embedQNode = new EmbedQuery();
192 | const retrieveNode = new RetrieveDocs();
193 | const generateNode = new GenerateAnswer();
194 |
195 | embedQNode.next(retrieveNode).next(generateNode);
196 | const onlineFlow = new Flow(embedQNode);
197 | ```
198 |
199 | Usage example:
200 |
201 | ```typescript
202 | const shared = {
203 | files: ["doc1.txt", "doc2.txt"], // any text files
204 | };
205 | await offlineFlow.run(shared);
206 | ```
207 |
--------------------------------------------------------------------------------
/docs/design_pattern/structure.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Structured Output"
4 | parent: "Design Pattern"
5 | nav_order: 5
6 | ---
7 |
8 | # Structured Output
9 |
10 | In many use cases, you may want the LLM to output a specific structure, such as a list or a dictionary with predefined keys.
11 |
12 | There are several approaches to achieve a structured output:
13 |
14 | - **Prompting** the LLM to strictly return a defined structure.
15 | - Using LLMs that natively support **schema enforcement**.
16 | - **Post-processing** the LLM's response to extract structured content.
17 |
18 | In practice, **Prompting** is simple and reliable for modern LLMs.
19 |
20 | ### Example Use Cases
21 |
22 | - Extracting Key Information
23 |
24 | ```yaml
25 | product:
26 | name: Widget Pro
27 | price: 199.99
28 | description: |
29 | A high-quality widget designed for professionals.
30 | Recommended for advanced users.
31 | ```
32 |
33 | - Summarizing Documents into Bullet Points
34 |
35 | ```yaml
36 | summary:
37 | - This product is easy to use.
38 | - It is cost-effective.
39 | - Suitable for all skill levels.
40 | ```
41 |
42 | ## TypeScript Implementation
43 |
44 | When using PocketFlow with structured output, follow these TypeScript patterns:
45 |
46 | 1. **Define Types** for your structured input/output
47 | 2. **Implement Validation** in your Node methods
48 | 3. **Use Type-Safe Operations** throughout your flow
49 |
50 | ### Example Text Summarization
51 |
52 | ````typescript
53 | // Define types
54 | type SummaryResult = {
55 | summary: string[];
56 | };
57 |
58 | type SharedStorage = {
59 | text?: string;
60 | result?: SummaryResult;
61 | };
62 |
63 | class SummarizeNode extends Node {
64 | async prep(shared: SharedStorage): Promise {
65 | return shared.text;
66 | }
67 |
68 | async exec(text: string | undefined): Promise {
69 | if (!text) return { summary: ["No text provided"] };
70 |
71 | const prompt = `
72 | Please summarize the following text as YAML, with exactly 3 bullet points
73 |
74 | ${text}
75 |
76 | Output:
77 | \`\`\`yaml
78 | summary:
79 | - bullet 1
80 | - bullet 2
81 | - bullet 3
82 | \`\`\``;
83 |
84 | // Simulated LLM call
85 | const response =
86 | "```yaml\nsummary:\n - First point\n - Second insight\n - Final conclusion\n```";
87 |
88 | // Parse YAML response
89 | const yamlStr = response.split("```yaml")[1].split("```")[0].trim();
90 |
91 | // Extract bullet points
92 | const result: SummaryResult = {
93 | summary: yamlStr
94 | .split("\n")
95 | .filter((line) => line.trim().startsWith("- "))
96 | .map((line) => line.trim().substring(2)),
97 | };
98 |
99 | // Validate
100 | if (!result.summary || !Array.isArray(result.summary)) {
101 | throw new Error("Invalid summary structure");
102 | }
103 |
104 | return result;
105 | }
106 |
107 | async post(
108 | shared: SharedStorage,
109 | _: string | undefined,
110 | result: SummaryResult
111 | ): Promise {
112 | shared.result = result;
113 | return "default";
114 | }
115 | }
116 | ````
117 |
118 | ### Why YAML instead of JSON?
119 |
120 | Current LLMs struggle with escaping. YAML is easier with strings since they don't always need quotes.
121 |
122 | **In JSON**
123 |
124 | ```json
125 | {
126 | "dialogue": "Alice said: \"Hello Bob.\\nHow are you?\\nI am good.\""
127 | }
128 | ```
129 |
130 | **In YAML**
131 |
132 | ```yaml
133 | dialogue: |
134 | Alice said: "Hello Bob.
135 | How are you?
136 | I am good."
137 | ```
138 |
--------------------------------------------------------------------------------
/docs/design_pattern/workflow.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Workflow"
4 | parent: "Design Pattern"
5 | nav_order: 2
6 | ---
7 |
8 | # Workflow
9 |
10 | Many real-world tasks are too complex for one LLM call. The solution is to **Task Decomposition**: decompose them into a [chain](../core_abstraction/flow.md) of multiple Nodes.
11 |
12 |
13 |

14 |
15 |
16 | > - You don't want to make each task **too coarse**, because it may be _too complex for one LLM call_.
17 | > - You don't want to make each task **too granular**, because then _the LLM call doesn't have enough context_ and results are _not consistent across nodes_.
18 | >
19 | > You usually need multiple _iterations_ to find the _sweet spot_. If the task has too many _edge cases_, consider using [Agents](./agent.md).
20 | > {: .best-practice }
21 |
22 | ### Example: Article Writing
23 |
24 | ```typescript
25 | interface SharedState {
26 | topic?: string;
27 | outline?: string;
28 | draft?: string;
29 | final_article?: string;
30 | }
31 |
32 | // Helper function to simulate LLM call
33 | async function callLLM(prompt: string): Promise {
34 | return `Response to: ${prompt}`;
35 | }
36 |
37 | class GenerateOutline extends Node {
38 | async prep(shared: SharedState): Promise {
39 | return shared.topic || "";
40 | }
41 |
42 | async exec(topic: string): Promise {
43 | return await callLLM(
44 | `Create a detailed outline for an article about ${topic}`
45 | );
46 | }
47 |
48 | async post(shared: SharedState, _: string, outline: string): Promise {
49 | shared.outline = outline;
50 | return "default";
51 | }
52 | }
53 |
54 | class WriteSection extends Node {
55 | async prep(shared: SharedState): Promise {
56 | return shared.outline || "";
57 | }
58 |
59 | async exec(outline: string): Promise {
60 | return await callLLM(`Write content based on this outline: ${outline}`);
61 | }
62 |
63 | async post(shared: SharedState, _: string, draft: string): Promise {
64 | shared.draft = draft;
65 | return "default";
66 | }
67 | }
68 |
69 | class ReviewAndRefine extends Node {
70 | async prep(shared: SharedState): Promise {
71 | return shared.draft || "";
72 | }
73 |
74 | async exec(draft: string): Promise {
75 | return await callLLM(`Review and improve this draft: ${draft}`);
76 | }
77 |
78 | async post(
79 | shared: SharedState,
80 | _: string,
81 | final: string
82 | ): Promise {
83 | shared.final_article = final;
84 | return undefined;
85 | }
86 | }
87 |
88 | // Connect nodes in sequence
89 | const outline = new GenerateOutline();
90 | const write = new WriteSection();
91 | const review = new ReviewAndRefine();
92 |
93 | outline.next(write).next(review);
94 |
95 | // Create and run flow
96 | const writingFlow = new Flow(outline);
97 | writingFlow.run({ topic: "AI Safety" });
98 | ```
99 |
100 | For _dynamic cases_, consider using [Agents](./agent.md).
101 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Home"
4 | nav_order: 1
5 | ---
6 |
7 | # PocketFlow.js
8 |
9 | A [100-line](https://github.com/The-Pocket/PocketFlow-Typescript/blob/main/src/index.ts) minimalist LLM framework for _Agents, Task Decomposition, RAG, etc_.
10 |
11 | - **Lightweight**: Just the core graph abstraction in 100 lines. ZERO dependencies, and vendor lock-in.
12 | - **Expressive**: Everything you love from larger frameworks—([Multi-](./design_pattern/multi_agent.html))[Agents](./design_pattern/agent.html), [Workflow](./design_pattern/workflow.html), [RAG](./design_pattern/rag.html), and more.
13 | - **Agentic-Coding**: Intuitive enough for AI agents to help humans build complex LLM applications.
14 |
15 |
16 |

17 |
18 |
19 | ## Core Abstraction
20 |
21 | We model the LLM workflow as a **Graph + Shared Store**:
22 |
23 | - [Node](./core_abstraction/node.md) handles simple (LLM) tasks.
24 | - [Flow](./core_abstraction/flow.md) connects nodes through **Actions** (labeled edges).
25 | - [Shared Store](./core_abstraction/communication.md) enables communication between nodes within flows.
26 | - [Batch](./core_abstraction/batch.md) nodes/flows allow for data-intensive tasks.
27 | - [(Advanced) Parallel](./core_abstraction/parallel.md) nodes/flows handle I/O-bound tasks.
28 |
29 |
30 |

31 |
32 |
33 | ## Design Pattern
34 |
35 | From there, it’s easy to implement popular design patterns:
36 |
37 | - [Agent](./design_pattern/agent.md) autonomously makes decisions.
38 | - [Workflow](./design_pattern/workflow.md) chains multiple tasks into pipelines.
39 | - [RAG](./design_pattern/rag.md) integrates data retrieval with generation.
40 | - [Map Reduce](./design_pattern/mapreduce.md) splits data tasks into Map and Reduce steps.
41 | - [Structured Output](./design_pattern/structure.md) formats outputs consistently.
42 | - [(Advanced) Multi-Agents](./design_pattern/multi_agent.md) coordinate multiple agents.
43 |
44 |
45 |

46 |
47 |
48 | ## Utility Function
49 |
50 | We **do not** provide built-in utilities. Instead, we offer _examples_—please _implement your own_:
51 |
52 | - [LLM Wrapper](./utility_function/llm.md)
53 | - [Viz and Debug](./utility_function/viz.md)
54 | - [Web Search](./utility_function/websearch.md)
55 | - [Chunking](./utility_function/chunking.md)
56 | - [Embedding](./utility_function/embedding.md)
57 | - [Vector Databases](./utility_function/vector.md)
58 | - [Text-to-Speech](./utility_function/text_to_speech.md)
59 |
60 | **Why not built-in?**: I believe it's a _bad practice_ for vendor-specific APIs in a general framework:
61 |
62 | - _API Volatility_: Frequent changes lead to heavy maintenance for hardcoded APIs.
63 | - _Flexibility_: You may want to switch vendors, use fine-tuned models, or run them locally.
64 | - _Optimizations_: Prompt caching, batching, and streaming are easier without vendor lock-in.
65 |
66 | ## Ready to build your Apps?
67 |
68 | Check out [Agentic Coding Guidance](./guide.md), the fastest way to develop LLM projects with PocketFlow.js!
69 |
--------------------------------------------------------------------------------
/docs/utility_function/chunking.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Text Chunking"
4 | parent: "Utility Function"
5 | nav_order: 4
6 | ---
7 |
8 | # Text Chunking
9 |
10 | We recommend some implementations of commonly used text chunking approaches.
11 |
12 | > Text Chunking is more a micro optimization, compared to the Flow Design.
13 | >
14 | > It's recommended to start with the Naive Chunking and optimize later.
15 | > {: .best-practice }
16 |
17 | ---
18 |
19 | ## Example TypeScript Code Samples
20 |
21 | ### 1. Naive (Fixed-Size) Chunking
22 |
23 | Splits text by a fixed number of characters, ignoring sentence or semantic boundaries.
24 |
25 | ```typescript
26 | function fixedSizeChunk(text: string, chunkSize: number = 100): string[] {
27 | const chunks: string[] = [];
28 | for (let i = 0; i < text.length; i += chunkSize) {
29 | chunks.push(text.substring(i, i + chunkSize));
30 | }
31 | return chunks;
32 | }
33 | ```
34 |
35 | However, sentences are often cut awkwardly, losing coherence.
36 |
37 | ### 2. Sentence-Based Chunking
38 |
39 | Using the popular [compromise](https://github.com/spencermountain/compromise) library (11.6k+ GitHub stars).
40 |
41 | ```typescript
42 | import nlp from "compromise";
43 |
44 | function sentenceBasedChunk(text: string, maxSentences: number = 2): string[] {
45 | // Parse the text into sentences
46 | const doc = nlp(text);
47 | const sentences = doc.sentences().out("array");
48 |
49 | // Group sentences into chunks
50 | const chunks: string[] = [];
51 | for (let i = 0; i < sentences.length; i += maxSentences) {
52 | chunks.push(sentences.slice(i, i + maxSentences).join(" "));
53 | }
54 |
55 | return chunks;
56 | }
57 | ```
58 |
59 | However, might not handle very long sentences or paragraphs well.
60 |
61 | ### 3. Other Chunking
62 |
63 | - **Paragraph-Based**: Split text by paragraphs (e.g., newlines). Large paragraphs can create big chunks.
64 |
65 | ```typescript
66 | function paragraphBasedChunk(text: string): string[] {
67 | // Split by double newlines (paragraphs)
68 | return text
69 | .split(/\n\s*\n/)
70 | .filter((paragraph) => paragraph.trim().length > 0);
71 | }
72 | ```
73 |
74 | - **Semantic**: Use embeddings or topic modeling to chunk by semantic boundaries.
75 | - **Agentic**: Use an LLM to decide chunk boundaries based on context or meaning.
76 |
--------------------------------------------------------------------------------
/docs/utility_function/embedding.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Embedding"
4 | parent: "Utility Function"
5 | nav_order: 5
6 | ---
7 |
8 | # Embedding
9 |
10 | Below you will find an overview table of various text embedding APIs, along with example TypeScript code.
11 |
12 | > Embedding is more a micro optimization, compared to the Flow Design.
13 | >
14 | > It's recommended to start with the most convenient one and optimize later.
15 | > {: .best-practice }
16 |
17 | | **API** | **Free Tier** | **Pricing Model** | **Docs** |
18 | | -------------------- | --------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
19 | | **OpenAI** | ~$5 credit | ~$0.0001/1K tokens | [OpenAI Embeddings](https://platform.openai.com/docs/api-reference/embeddings) |
20 | | **Azure OpenAI** | $200 credit | Same as OpenAI (~$0.0001/1K tokens) | [Azure OpenAI Embeddings](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?tabs=portal) |
21 | | **Google Vertex AI** | $300 credit | ~$0.025 / million chars | [Vertex AI Embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) |
22 | | **AWS Bedrock** | No free tier, but AWS credits may apply | ~$0.00002/1K tokens (Titan V2) | [Amazon Bedrock](https://docs.aws.amazon.com/bedrock/) |
23 | | **Cohere** | Limited free tier | ~$0.0001/1K tokens | [Cohere Embeddings](https://docs.cohere.com/docs/cohere-embed) |
24 | | **Hugging Face** | ~$0.10 free compute monthly | Pay per second of compute | [HF Inference API](https://huggingface.co/docs/api-inference) |
25 | | **Jina** | 1M tokens free | Pay per token after | [Jina Embeddings](https://jina.ai/embeddings/) |
26 |
27 | ## Example TypeScript Code
28 |
29 | ### 1. OpenAI
30 |
31 | ```typescript
32 | import OpenAI from "openai";
33 |
34 | const openai = new OpenAI({
35 | apiKey: "YOUR_API_KEY",
36 | });
37 |
38 | async function getEmbedding(text: string) {
39 | const response = await openai.embeddings.create({
40 | model: "text-embedding-ada-002",
41 | input: text,
42 | });
43 |
44 | // Extract the embedding vector from the response
45 | const embedding = response.data[0].embedding;
46 | console.log(embedding);
47 | return embedding;
48 | }
49 |
50 | // Usage
51 | getEmbedding("Hello world");
52 | ```
53 |
54 | ### 2. Azure OpenAI
55 |
56 | ```typescript
57 | import { AzureOpenAI } from "openai";
58 | import {
59 | getBearerTokenProvider,
60 | DefaultAzureCredential,
61 | } from "@azure/identity";
62 |
63 | // Using Azure credentials (recommended)
64 | const credential = new DefaultAzureCredential();
65 | const scope = "https://cognitiveservices.azure.com/.default";
66 | const azureADTokenProvider = getBearerTokenProvider(credential, scope);
67 |
68 | // Or using API key directly
69 | const client = new AzureOpenAI({
70 | apiKey: "YOUR_AZURE_API_KEY",
71 | endpoint: "https://YOUR_RESOURCE_NAME.openai.azure.com",
72 | apiVersion: "2023-05-15", // Update to the latest version
73 | });
74 |
75 | async function getEmbedding(text: string) {
76 | const response = await client.embeddings.create({
77 | model: "text-embedding-ada-002", // Or your deployment name
78 | input: text,
79 | });
80 |
81 | const embedding = response.data[0].embedding;
82 | console.log(embedding);
83 | return embedding;
84 | }
85 |
86 | // Usage
87 | getEmbedding("Hello world");
88 | ```
89 |
90 | ### 3. Google Vertex AI
91 |
92 | ```typescript
93 | import { VertexAI } from "@google-cloud/vertexai";
94 |
95 | // Initialize Vertex with your Google Cloud project and location
96 | const vertex = new VertexAI({
97 | project: "YOUR_GCP_PROJECT_ID",
98 | location: "us-central1",
99 | });
100 |
101 | // Access embeddings model
102 | const model = vertex.preview.getTextEmbeddingModel("textembedding-gecko@001");
103 |
104 | async function getEmbedding(text: string) {
105 | const response = await model.getEmbeddings({
106 | texts: [text],
107 | });
108 |
109 | const embedding = response.embeddings[0].values;
110 | console.log(embedding);
111 | return embedding;
112 | }
113 |
114 | // Usage
115 | getEmbedding("Hello world");
116 | ```
117 |
118 | ### 4. AWS Bedrock
119 |
120 | ```typescript
121 | import {
122 | BedrockRuntimeClient,
123 | InvokeModelCommand,
124 | } from "@aws-sdk/client-bedrock-runtime";
125 |
126 | const client = new BedrockRuntimeClient({
127 | region: "us-east-1", // Use your AWS region
128 | });
129 |
130 | async function getEmbedding(text: string) {
131 | const modelId = "amazon.titan-embed-text-v2:0";
132 | const input = {
133 | inputText: text,
134 | dimensions: 1536, // Optional: specify embedding dimensions
135 | normalize: true, // Optional: normalize embeddings
136 | };
137 |
138 | const command = new InvokeModelCommand({
139 | modelId: modelId,
140 | contentType: "application/json",
141 | accept: "application/json",
142 | body: JSON.stringify(input),
143 | });
144 |
145 | const response = await client.send(command);
146 | const responseBody = JSON.parse(new TextDecoder().decode(response.body));
147 | const embedding = responseBody.embedding;
148 |
149 | console.log(embedding);
150 | return embedding;
151 | }
152 |
153 | // Usage
154 | getEmbedding("Hello world");
155 | ```
156 |
157 | ### 5. Cohere
158 |
159 | ```typescript
160 | import { CohereClient } from "cohere-ai";
161 |
162 | const cohere = new CohereClient({
163 | token: "YOUR_API_KEY",
164 | });
165 |
166 | async function getEmbedding(text: string) {
167 | const response = await cohere.embed({
168 | texts: [text],
169 | model: "embed-english-v3.0", // Use the latest model
170 | inputType: "search_query",
171 | });
172 |
173 | const embedding = response.embeddings[0];
174 | console.log(embedding);
175 | return embedding;
176 | }
177 |
178 | // Usage
179 | getEmbedding("Hello world");
180 | ```
181 |
182 | ### 6. Hugging Face
183 |
184 | ```typescript
185 | import { InferenceClient } from "@huggingface/inference";
186 |
187 | const hf = new InferenceClient({
188 | apiToken: "YOUR_HF_TOKEN",
189 | });
190 |
191 | async function getEmbedding(text: string) {
192 | const model = "sentence-transformers/all-MiniLM-L6-v2";
193 |
194 | const response = await hf.featureExtraction({
195 | model: model,
196 | inputs: text,
197 | });
198 |
199 | console.log(response);
200 | return response;
201 | }
202 |
203 | // Usage
204 | getEmbedding("Hello world");
205 | ```
206 |
207 | ### 7. Jina
208 |
209 | ```typescript
210 | import axios from "axios";
211 |
212 | async function getEmbedding(text: string) {
213 | const url = "https://api.jina.ai/v1/embeddings";
214 | const headers = {
215 | Authorization: `Bearer YOUR_JINA_TOKEN`,
216 | "Content-Type": "application/json",
217 | };
218 |
219 | const payload = {
220 | model: "jina-embeddings-v3",
221 | input: [text],
222 | normalized: true,
223 | };
224 |
225 | const response = await axios.post(url, payload, { headers });
226 | const embedding = response.data.data[0].embedding;
227 |
228 | console.log(embedding);
229 | return embedding;
230 | }
231 |
232 | // Usage
233 | getEmbedding("Hello world");
234 | ```
235 |
--------------------------------------------------------------------------------
/docs/utility_function/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Utility Function"
4 | nav_order: 4
5 | has_children: true
6 | ---
--------------------------------------------------------------------------------
/docs/utility_function/llm.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "LLM Wrapper"
4 | parent: "Utility Function"
5 | nav_order: 1
6 | ---
7 |
8 | # LLM Wrappers
9 |
10 | Check out popular libraries like [LangChain](https://github.com/langchain-ai/langchainjs) (13.8k+ GitHub stars), [ModelFusion](https://github.com/vercel/modelfusion) (1.2k+ GitHub stars), or [Firebase GenKit](https://firebase.google.com/docs/genkit) for unified LLM interfaces.
11 | Here, we provide some minimal example implementations:
12 |
13 | 1. OpenAI
14 |
15 | ```typescript
16 | import { OpenAI } from "openai";
17 |
18 | async function callLlm(prompt: string): Promise {
19 | const client = new OpenAI({ apiKey: "YOUR_API_KEY_HERE" });
20 | const r = await client.chat.completions.create({
21 | model: "gpt-4o",
22 | messages: [{ role: "user", content: prompt }],
23 | });
24 | return r.choices[0].message.content || "";
25 | }
26 |
27 | // Example usage
28 | callLlm("How are you?").then(console.log);
29 | ```
30 |
31 | > Store the API key in an environment variable like OPENAI_API_KEY for security.
32 | > {: .best-practice }
33 |
34 | 2. Claude (Anthropic)
35 |
36 | ```typescript
37 | import Anthropic from "@anthropic-ai/sdk";
38 |
39 | async function callLlm(prompt: string): Promise {
40 | const client = new Anthropic({
41 | apiKey: "YOUR_API_KEY_HERE",
42 | });
43 | const response = await client.messages.create({
44 | model: "claude-3-7-sonnet-20250219",
45 | max_tokens: 3000,
46 | messages: [{ role: "user", content: prompt }],
47 | });
48 | return response.content[0].text;
49 | }
50 | ```
51 |
52 | 3. Google (Vertex AI)
53 |
54 | ```typescript
55 | import { VertexAI } from "@google-cloud/vertexai";
56 |
57 | async function callLlm(prompt: string): Promise {
58 | const vertexAI = new VertexAI({
59 | project: "YOUR_PROJECT_ID",
60 | location: "us-central1",
61 | });
62 |
63 | const generativeModel = vertexAI.getGenerativeModel({
64 | model: "gemini-1.5-flash",
65 | });
66 |
67 | const response = await generativeModel.generateContent({
68 | contents: [{ role: "user", parts: [{ text: prompt }] }],
69 | });
70 |
71 | return response.response.candidates[0].content.parts[0].text;
72 | }
73 | ```
74 |
75 | 4. Azure (Azure OpenAI)
76 |
77 | ```typescript
78 | import { AzureOpenAI } from "openai";
79 |
80 | async function callLlm(prompt: string): Promise {
81 | const client = new AzureOpenAI({
82 | apiKey: "YOUR_API_KEY_HERE",
83 | azure: {
84 | apiVersion: "2023-05-15",
85 | endpoint: "https://.openai.azure.com/",
86 | },
87 | });
88 |
89 | const r = await client.chat.completions.create({
90 | model: "",
91 | messages: [{ role: "user", content: prompt }],
92 | });
93 |
94 | return r.choices[0].message.content || "";
95 | }
96 | ```
97 |
98 | 5. Ollama (Local LLM)
99 |
100 | ```typescript
101 | import ollama from "ollama";
102 |
103 | async function callLlm(prompt: string): Promise {
104 | const response = await ollama.chat({
105 | model: "llama2",
106 | messages: [{ role: "user", content: prompt }],
107 | });
108 | return response.message.content;
109 | }
110 | ```
111 |
112 | ## Improvements
113 |
114 | Feel free to enhance your `callLlm` function as needed. Here are examples:
115 |
116 | - Handle chat history:
117 |
118 | ```typescript
119 | interface Message {
120 | role: "user" | "assistant" | "system";
121 | content: string;
122 | }
123 |
124 | async function callLlm(messages: Message[]): Promise {
125 | const client = new OpenAI({ apiKey: "YOUR_API_KEY_HERE" });
126 | const r = await client.chat.completions.create({
127 | model: "gpt-4o",
128 | messages: messages,
129 | });
130 | return r.choices[0].message.content || "";
131 | }
132 | ```
133 |
134 | - Add in-memory caching
135 |
136 | ```typescript
137 | import { memoize } from "lodash";
138 |
139 | const callLlmMemoized = memoize(async (prompt: string): Promise => {
140 | // Your implementation here
141 | return "";
142 | });
143 |
144 | async function callLlm(prompt: string, useCache = true): Promise {
145 | if (useCache) {
146 | return callLlmMemoized(prompt);
147 | }
148 | // Call the underlying function directly
149 | return callLlmInternal(prompt);
150 | }
151 |
152 | class SummarizeNode {
153 | private curRetry = 0;
154 |
155 | async exec(text: string): Promise {
156 | return callLlm(`Summarize: ${text}`, this.curRetry === 0);
157 | }
158 | }
159 | ```
160 |
161 | - Enable logging:
162 |
163 | ```typescript
164 | async function callLlm(prompt: string): Promise {
165 | console.info(`Prompt: ${prompt}`);
166 | // Your implementation here
167 | const response = ""; // Response from your implementation
168 | console.info(`Response: ${response}`);
169 | return response;
170 | }
171 | ```
172 |
--------------------------------------------------------------------------------
/docs/utility_function/text_to_speech.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: "Text-to-Speech"
4 | parent: "Utility Function"
5 | nav_order: 7
6 | ---
7 |
8 | # Text-to-Speech
9 |
10 | | **Service** | **Free Tier** | **Pricing Model** | **Docs** |
11 | | -------------------- | ------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
12 | | **Amazon Polly** | 5M std + 1M neural | ~$4 /M (std), ~$16 /M (neural) after free tier | [Polly Docs](https://aws.amazon.com/polly/) |
13 | | **Google Cloud TTS** | 4M std + 1M WaveNet | ~$4 /M (std), ~$16 /M (WaveNet) pay-as-you-go | [Cloud TTS Docs](https://cloud.google.com/text-to-speech) |
14 | | **Azure TTS** | 500K neural ongoing | ~$15 /M (neural), discount at higher volumes | [Azure TTS Docs](https://azure.microsoft.com/products/cognitive-services/text-to-speech/) |
15 | | **IBM Watson TTS** | 10K chars Lite plan | ~$0.02 /1K (i.e. ~$20 /M). Enterprise options available | [IBM Watson Docs](https://www.ibm.com/cloud/watson-text-to-speech) |
16 | | **ElevenLabs** | 10K chars monthly | From ~$5/mo (30K chars) up to $330/mo (2M chars). Enterprise | [ElevenLabs Docs](https://elevenlabs.io) |
17 |
18 | ## Example TypeScript Code
19 |
20 | ### Amazon Polly
21 |
22 | ```typescript
23 | import { PollyClient, SynthesizeSpeechCommand } from "@aws-sdk/client-polly";
24 | import { writeFileSync } from "fs";
25 |
26 | async function synthesizeText() {
27 | // Create a Polly client
28 | const polly = new PollyClient({
29 | region: "us-east-1",
30 | credentials: {
31 | accessKeyId: "YOUR_AWS_ACCESS_KEY_ID",
32 | secretAccessKey: "YOUR_AWS_SECRET_ACCESS_KEY",
33 | },
34 | });
35 |
36 | // Set the parameters
37 | const params = {
38 | Text: "Hello from Polly!",
39 | OutputFormat: "mp3",
40 | VoiceId: "Joanna",
41 | };
42 |
43 | try {
44 | // Synthesize speech
45 | const command = new SynthesizeSpeechCommand(params);
46 | const response = await polly.send(command);
47 |
48 | // Convert AudioStream to Buffer and save to file
49 | if (response.AudioStream) {
50 | const audioBuffer = Buffer.from(
51 | await response.AudioStream.transformToByteArray()
52 | );
53 | writeFileSync("polly.mp3", audioBuffer);
54 | console.log("Audio content written to file: polly.mp3");
55 | }
56 | } catch (error) {
57 | console.error("Error:", error);
58 | }
59 | }
60 |
61 | synthesizeText();
62 | ```
63 |
64 | ### Google Cloud TTS
65 |
66 | ```typescript
67 | import { TextToSpeechClient } from "@google-cloud/text-to-speech";
68 | import { writeFileSync } from "fs";
69 |
70 | async function synthesizeText() {
71 | // Creates a client
72 | const client = new TextToSpeechClient();
73 |
74 | // The text to synthesize
75 | const text = "Hello from Google Cloud TTS!";
76 |
77 | // Construct the request
78 | const request = {
79 | input: { text: text },
80 | // Select the language and SSML voice gender
81 | voice: { languageCode: "en-US", ssmlGender: "NEUTRAL" },
82 | // Select the type of audio encoding
83 | audioConfig: { audioEncoding: "MP3" },
84 | };
85 |
86 | try {
87 | // Performs the text-to-speech request
88 | const [response] = await client.synthesizeSpeech(request);
89 | // Write the binary audio content to a local file
90 | writeFileSync("gcloud_tts.mp3", response.audioContent as Buffer);
91 | console.log("Audio content written to file: gcloud_tts.mp3");
92 | } catch (error) {
93 | console.error("Error:", error);
94 | }
95 | }
96 |
97 | synthesizeText();
98 | ```
99 |
100 | ### Azure TTS
101 |
102 | ```typescript
103 | import {
104 | SpeechConfig,
105 | AudioConfig,
106 | SpeechSynthesizer,
107 | } from "microsoft-cognitiveservices-speech-sdk";
108 |
109 | async function synthesizeText() {
110 | // Create a speech configuration with subscription information
111 | const speechConfig = SpeechConfig.fromSubscription(
112 | "AZURE_KEY",
113 | "AZURE_REGION"
114 | );
115 |
116 | // Set speech synthesis output format
117 | speechConfig.speechSynthesisOutputFormat = 1; // 1 corresponds to Audio16Khz128KBitRateMonoMp3
118 |
119 | // Create an audio configuration for file output
120 | const audioConfig = AudioConfig.fromAudioFileOutput("azure_tts.mp3");
121 |
122 | // Create a speech synthesizer with the given configurations
123 | const synthesizer = new SpeechSynthesizer(speechConfig, audioConfig);
124 |
125 | try {
126 | // Synthesize text to speech
127 | const result = await new Promise((resolve, reject) => {
128 | synthesizer.speakTextAsync(
129 | "Hello from Azure TTS!",
130 | (result) => {
131 | synthesizer.close();
132 | resolve(result);
133 | },
134 | (error) => {
135 | synthesizer.close();
136 | reject(error);
137 | }
138 | );
139 | });
140 |
141 | console.log("Audio content written to file: azure_tts.mp3");
142 | } catch (error) {
143 | console.error("Error:", error);
144 | }
145 | }
146 |
147 | synthesizeText();
148 | ```
149 |
150 | ### IBM Watson TTS
151 |
152 | ```typescript
153 | import { TextToSpeechV1 } from "ibm-watson/text-to-speech/v1";
154 | import { IamAuthenticator } from "ibm-watson/auth";
155 | import { writeFileSync } from "fs";
156 |
157 | async function synthesizeText() {
158 | // Create a TextToSpeech client with authentication
159 | const textToSpeech = new TextToSpeechV1({
160 | authenticator: new IamAuthenticator({ apikey: "IBM_API_KEY" }),
161 | serviceUrl: "IBM_SERVICE_URL",
162 | });
163 |
164 | try {
165 | // Synthesize speech
166 | const params = {
167 | text: "Hello from IBM Watson!",
168 | voice: "en-US_AllisonV3Voice",
169 | accept: "audio/mp3",
170 | };
171 |
172 | const response = await textToSpeech.synthesize(params);
173 | const audio = response.result;
174 |
175 | // The wav header requires a file length, but this is unknown until after the header is already generated
176 | const repairedAudio = await textToSpeech.repairWavHeaderStream(audio);
177 |
178 | // Write audio to file
179 | writeFileSync("ibm_tts.mp3", repairedAudio);
180 | console.log("Audio content written to file: ibm_tts.mp3");
181 | } catch (error) {
182 | console.error("Error:", error);
183 | }
184 | }
185 |
186 | synthesizeText();
187 | ```
188 |
189 | ### ElevenLabs
190 |
191 | ```typescript
192 | import { ElevenLabs } from "elevenlabs";
193 | import { writeFileSync } from "fs";
194 |
195 | async function synthesizeText() {
196 | // Initialize the ElevenLabs client
197 | const eleven = new ElevenLabs({
198 | apiKey: "ELEVENLABS_KEY",
199 | });
200 |
201 | try {
202 | // Generate speech
203 | const voiceId = "ELEVENLABS_VOICE_ID";
204 | const text = "Hello from ElevenLabs!";
205 |
206 | // Generate audio
207 | const audioResponse = await eleven.generate({
208 | voice: voiceId,
209 | text: text,
210 | model_id: "eleven_monolingual_v1",
211 | voice_settings: {
212 | stability: 0.75,
213 | similarity_boost: 0.75,
214 | },
215 | });
216 |
217 | // Convert to buffer and save to file
218 | const audioBuffer = Buffer.from(await audioResponse.arrayBuffer());
219 | writeFileSync("elevenlabs.mp3", audioBuffer);
220 | console.log("Audio content written to file: elevenlabs.mp3");
221 | } catch (error) {
222 | console.error("Error:", error);
223 | }
224 | }
225 |
226 | synthesizeText();
227 | ```
228 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['**/tests/**/*.test.ts'],
5 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pocketflow",
3 | "description": "A minimalist LLM framework",
4 | "version": "1.0.4",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "engines": {
9 | "node": ">=18.0.0"
10 | },
11 | "exports": {
12 | ".": {
13 | "types": "./dist/index.d.ts",
14 | "import": "./dist/index.mjs",
15 | "require": "./dist/index.js"
16 | }
17 | },
18 | "files": [
19 | "dist"
20 | ],
21 | "license": "MIT",
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/The-Pocket/PocketFlow-Typescript.git"
25 | },
26 | "keywords": [
27 | "pocketflow",
28 | "typescript",
29 | "llm",
30 | "ai",
31 | "framework",
32 | "workflow",
33 | "minimalist"
34 | ],
35 | "scripts": {
36 | "build": "tsup",
37 | "test": "jest",
38 | "test:watch": "jest --watch",
39 | "prepublishOnly": "npm run build"
40 | },
41 | "devDependencies": {
42 | "@types/jest": "^29.5.14",
43 | "jest": "^29.7.0",
44 | "ts-jest": "^29.3.0",
45 | "tsup": "^8.4.0",
46 | "typescript": "^5.8.2"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | type NonIterableObject = Partial> & { [Symbol.iterator]?: never }; type Action = string;
2 | class BaseNode {
3 | protected _params: P = {} as P; protected _successors: Map = new Map();
4 | protected async _exec(prepRes: unknown): Promise { return await this.exec(prepRes); }
5 | async prep(shared: S): Promise { return undefined; }
6 | async exec(prepRes: unknown): Promise { return undefined; }
7 | async post(shared: S, prepRes: unknown, execRes: unknown): Promise { return undefined; }
8 | async _run(shared: S): Promise {
9 | const p = await this.prep(shared), e = await this._exec(p); return await this.post(shared, p, e);
10 | }
11 | async run(shared: S): Promise {
12 | if (this._successors.size > 0) console.warn("Node won't run successors. Use Flow.");
13 | return await this._run(shared);
14 | }
15 | setParams(params: P): this { this._params = params; return this; }
16 | next(node: T): T { this.on("default", node); return node; }
17 | on(action: Action, node: BaseNode): this {
18 | if (this._successors.has(action)) console.warn(`Overwriting successor for action '${action}'`);
19 | this._successors.set(action, node); return this;
20 | }
21 | getNextNode(action: Action = "default"): BaseNode | undefined {
22 | const nextAction = action || 'default', next = this._successors.get(nextAction)
23 | if (!next && this._successors.size > 0)
24 | console.warn(`Flow ends: '${nextAction}' not found in [${Array.from(this._successors.keys())}]`)
25 | return next
26 | }
27 | clone(): this {
28 | const clonedNode = Object.create(Object.getPrototypeOf(this)); Object.assign(clonedNode, this);
29 | clonedNode._params = { ...this._params }; clonedNode._successors = new Map(this._successors);
30 | return clonedNode;
31 | }
32 | }
33 | class Node extends BaseNode {
34 | maxRetries: number; wait: number; currentRetry: number = 0;
35 | constructor(maxRetries: number = 1, wait: number = 0) {
36 | super(); this.maxRetries = maxRetries; this.wait = wait;
37 | }
38 | async execFallback(prepRes: unknown, error: Error): Promise { throw error; }
39 | async _exec(prepRes: unknown): Promise {
40 | for (this.currentRetry = 0; this.currentRetry < this.maxRetries; this.currentRetry++) {
41 | try { return await this.exec(prepRes); }
42 | catch (e) {
43 | if (this.currentRetry === this.maxRetries - 1) return await this.execFallback(prepRes, e as Error);
44 | if (this.wait > 0) await new Promise(resolve => setTimeout(resolve, this.wait * 1000));
45 | }
46 | }
47 | return undefined;
48 | }
49 | }
50 | class BatchNode extends Node {
51 | async _exec(items: unknown[]): Promise {
52 | if (!items || !Array.isArray(items)) return [];
53 | const results = []; for (const item of items) results.push(await super._exec(item)); return results;
54 | }
55 | }
56 | class ParallelBatchNode extends Node {
57 | async _exec(items: unknown[]): Promise {
58 | if (!items || !Array.isArray(items)) return []
59 | return Promise.all(items.map((item) => super._exec(item)))
60 | }
61 | }
62 | class Flow extends BaseNode {
63 | start: BaseNode;
64 | constructor(start: BaseNode) { super(); this.start = start; }
65 | protected async _orchestrate(shared: S, params?: P): Promise {
66 | let current: BaseNode | undefined = this.start.clone();
67 | const p = params || this._params;
68 | while (current) {
69 | current.setParams(p); const action = await current._run(shared);
70 | current = current.getNextNode(action); current = current?.clone();
71 | }
72 | }
73 | async _run(shared: S): Promise {
74 | const pr = await this.prep(shared); await this._orchestrate(shared);
75 | return await this.post(shared, pr, undefined);
76 | }
77 | async exec(prepRes: unknown): Promise { throw new Error("Flow can't exec."); }
78 | }
79 | class BatchFlow extends Flow {
80 | async _run(shared: S): Promise {
81 | const batchParams = await this.prep(shared);
82 | for (const bp of batchParams) {
83 | const mergedParams = { ...this._params, ...bp };
84 | await this._orchestrate(shared, mergedParams);
85 | }
86 | return await this.post(shared, batchParams, undefined);
87 | }
88 | async prep(shared: S): Promise { const empty: readonly NonIterableObject[] = []; return empty as NP; }
89 | }
90 | class ParallelBatchFlow extends BatchFlow {
91 | async _run(shared: S): Promise {
92 | const batchParams = await this.prep(shared);
93 | await Promise.all(batchParams.map(bp => {
94 | const mergedParams = { ...this._params, ...bp };
95 | return this._orchestrate(shared, mergedParams);
96 | }));
97 | return await this.post(shared, batchParams, undefined);
98 | }
99 | }
100 | export { BaseNode, Node, BatchNode, ParallelBatchNode, Flow, BatchFlow, ParallelBatchFlow };
--------------------------------------------------------------------------------
/tests/batch-flow.test.ts:
--------------------------------------------------------------------------------
1 | // tests/async-batch-flow.test.ts
2 | import { Node, BatchFlow } from '../src/index';
3 |
4 | // Define shared storage type
5 | type SharedStorage = {
6 | inputData?: Record;
7 | results?: Record;
8 | intermediateResults?: Record;
9 | };
10 |
11 | // Parameters type
12 | type BatchParams = {
13 | key: string;
14 | multiplier?: number;
15 | };
16 |
17 | class AsyncDataProcessNode extends Node {
18 | constructor(maxRetries: number = 1, wait: number = 0) {
19 | super(maxRetries, wait);
20 | }
21 |
22 | async prep(shared: SharedStorage): Promise {
23 | const key = this._params.key;
24 | const data = shared.inputData?.[key] ?? 0;
25 |
26 | if (!shared.results) {
27 | shared.results = {};
28 | }
29 |
30 | shared.results[key] = data;
31 | return data;
32 | }
33 |
34 | async exec(prepRes: number): Promise {
35 | return prepRes; // Just return the prep result as-is
36 | }
37 |
38 | async post(
39 | shared: SharedStorage,
40 | prepRes: number,
41 | execRes: number
42 | ): Promise {
43 | await new Promise((resolve) => setTimeout(resolve, 10)); // Simulate async work
44 | const key = this._params.key;
45 |
46 | if (!shared.results) {
47 | shared.results = {};
48 | }
49 |
50 | shared.results[key] = execRes * 2; // Double the value
51 | return 'processed';
52 | }
53 | }
54 |
55 | class AsyncErrorNode extends Node {
56 | constructor(maxRetries: number = 1, wait: number = 0) {
57 | super(maxRetries, wait);
58 | }
59 |
60 | async prep(shared: SharedStorage): Promise {
61 | return undefined;
62 | }
63 |
64 | async exec(prepRes: any): Promise {
65 | return undefined;
66 | }
67 |
68 | async post(
69 | shared: SharedStorage,
70 | prepRes: any,
71 | execRes: any
72 | ): Promise {
73 | const key = this._params.key;
74 | if (key === 'errorKey') {
75 | throw new Error(`Async error processing key: ${key}`);
76 | }
77 | return 'processed';
78 | }
79 | }
80 |
81 | describe('BatchFlow Tests', () => {
82 | let processNode: AsyncDataProcessNode;
83 |
84 | beforeEach(() => {
85 | processNode = new AsyncDataProcessNode();
86 | });
87 |
88 | test('basic async batch processing', async () => {
89 | class SimpleTestBatchFlow extends BatchFlow {
90 | async prep(shared: SharedStorage): Promise {
91 | return Object.keys(shared.inputData || {}).map((k) => ({ key: k }));
92 | }
93 | }
94 |
95 | const shared: SharedStorage = {
96 | inputData: {
97 | a: 1,
98 | b: 2,
99 | c: 3,
100 | },
101 | };
102 |
103 | const flow = new SimpleTestBatchFlow(processNode);
104 | await flow.run(shared);
105 |
106 | expect(shared.results).toEqual({
107 | a: 2, // 1 * 2
108 | b: 4, // 2 * 2
109 | c: 6, // 3 * 2
110 | });
111 | });
112 |
113 | test('empty async batch', async () => {
114 | class EmptyTestBatchFlow extends BatchFlow {
115 | async prep(shared: SharedStorage): Promise {
116 | // Initialize results as an empty object
117 | if (!shared.results) {
118 | shared.results = {};
119 | }
120 | return Object.keys(shared.inputData || {}).map((k) => ({ key: k }));
121 | }
122 |
123 | // Ensure post is called even if batch is empty
124 | async post(
125 | shared: SharedStorage,
126 | prepRes: BatchParams[],
127 | execRes: any
128 | ): Promise {
129 | if (!shared.results) {
130 | shared.results = {};
131 | }
132 | return undefined;
133 | }
134 | }
135 |
136 | const shared: SharedStorage = {
137 | inputData: {},
138 | };
139 |
140 | const flow = new EmptyTestBatchFlow(processNode);
141 | await flow.run(shared);
142 |
143 | expect(shared.results).toEqual({});
144 | });
145 |
146 | test('async error handling', async () => {
147 | class ErrorTestBatchFlow extends BatchFlow {
148 | async prep(shared: SharedStorage): Promise {
149 | return Object.keys(shared.inputData || {}).map((k) => ({ key: k }));
150 | }
151 | }
152 |
153 | const shared: SharedStorage = {
154 | inputData: {
155 | normalKey: 1,
156 | errorKey: 2,
157 | anotherKey: 3,
158 | },
159 | };
160 |
161 | const flow = new ErrorTestBatchFlow(new AsyncErrorNode());
162 |
163 | await expect(async () => {
164 | await flow.run(shared);
165 | }).rejects.toThrow('Async error processing key: errorKey');
166 | });
167 |
168 | test('nested async flow', async () => {
169 | class AsyncInnerNode extends Node {
170 | async prep(shared: SharedStorage): Promise {
171 | return undefined;
172 | }
173 |
174 | async exec(prepRes: any): Promise {
175 | return undefined;
176 | }
177 |
178 | async post(
179 | shared: SharedStorage,
180 | prepRes: any,
181 | execRes: any
182 | ): Promise {
183 | const key = this._params.key;
184 |
185 | if (!shared.intermediateResults) {
186 | shared.intermediateResults = {};
187 | }
188 |
189 | // Safely access inputData
190 | const inputValue = shared.inputData?.[key] ?? 0;
191 | shared.intermediateResults[key] = inputValue + 1;
192 |
193 | await new Promise((resolve) => setTimeout(resolve, 10));
194 | return 'next';
195 | }
196 | }
197 |
198 | class AsyncOuterNode extends Node {
199 | async prep(shared: SharedStorage): Promise {
200 | return undefined;
201 | }
202 |
203 | async exec(prepRes: any): Promise {
204 | return undefined;
205 | }
206 |
207 | async post(
208 | shared: SharedStorage,
209 | prepRes: any,
210 | execRes: any
211 | ): Promise {
212 | const key = this._params.key;
213 |
214 | if (!shared.results) {
215 | shared.results = {};
216 | }
217 |
218 | if (!shared.intermediateResults) {
219 | shared.intermediateResults = {};
220 | }
221 |
222 | shared.results[key] = shared.intermediateResults[key] * 2;
223 | await new Promise((resolve) => setTimeout(resolve, 10));
224 | return 'done';
225 | }
226 | }
227 |
228 | class NestedBatchFlow extends BatchFlow {
229 | async prep(shared: SharedStorage): Promise {
230 | return Object.keys(shared.inputData || {}).map((k) => ({ key: k }));
231 | }
232 | }
233 |
234 | // Create inner flow
235 | const innerNode = new AsyncInnerNode();
236 | const outerNode = new AsyncOuterNode();
237 | innerNode.on('next', outerNode);
238 |
239 | const shared: SharedStorage = {
240 | inputData: {
241 | x: 1,
242 | y: 2,
243 | },
244 | };
245 |
246 | const flow = new NestedBatchFlow(innerNode);
247 | await flow.run(shared);
248 |
249 | expect(shared.results).toEqual({
250 | x: 4, // (1 + 1) * 2
251 | y: 6, // (2 + 1) * 2
252 | });
253 | });
254 |
255 | test('custom async parameters', async () => {
256 | class CustomParamNode extends Node {
257 | async prep(shared: SharedStorage): Promise {
258 | return undefined;
259 | }
260 |
261 | async exec(prepRes: any): Promise {
262 | return undefined;
263 | }
264 |
265 | async post(
266 | shared: SharedStorage,
267 | prepRes: any,
268 | execRes: any
269 | ): Promise {
270 | const key = this._params.key;
271 | const multiplier = this._params.multiplier || 1;
272 |
273 | await new Promise((resolve) => setTimeout(resolve, 10));
274 |
275 | if (!shared.results) {
276 | shared.results = {};
277 | }
278 |
279 | // Safely access inputData with default value
280 | const inputValue = shared.inputData?.[key] ?? 0;
281 | shared.results[key] = inputValue * multiplier;
282 |
283 | return 'done';
284 | }
285 | }
286 |
287 | class CustomParamBatchFlow extends BatchFlow {
288 | async prep(shared: SharedStorage): Promise {
289 | return Object.keys(shared.inputData || {}).map((k, i) => ({
290 | key: k,
291 | multiplier: i + 1,
292 | }));
293 | }
294 | }
295 |
296 | const shared: SharedStorage = {
297 | inputData: {
298 | a: 1,
299 | b: 2,
300 | c: 3,
301 | },
302 | };
303 |
304 | const flow = new CustomParamBatchFlow(new CustomParamNode());
305 | await flow.run(shared);
306 |
307 | expect(shared.results).toEqual({
308 | a: 1 * 1, // first item, multiplier = 1
309 | b: 2 * 2, // second item, multiplier = 2
310 | c: 3 * 3, // third item, multiplier = 3
311 | });
312 | });
313 | });
314 |
--------------------------------------------------------------------------------
/tests/flow-basic.test.ts:
--------------------------------------------------------------------------------
1 | // tests/flow_basic.test.ts
2 | import { Node, Flow } from '../src/index';
3 |
4 | // Define a shared storage type
5 | type SharedStorage = {
6 | current?: number;
7 | execResult?: any;
8 | };
9 |
10 | class NumberNode extends Node> {
11 | constructor(
12 | private number: number,
13 | maxRetries: number = 1,
14 | wait: number = 0
15 | ) {
16 | super(maxRetries, wait);
17 | }
18 |
19 | async prep(shared: SharedStorage): Promise {
20 | shared.current = this.number;
21 | }
22 | }
23 |
24 | class AddNode extends Node {
25 | constructor(
26 | private number: number,
27 | maxRetries: number = 1,
28 | wait: number = 0
29 | ) {
30 | super(maxRetries, wait);
31 | }
32 |
33 | async prep(shared: SharedStorage): Promise {
34 | if (shared.current !== undefined) {
35 | shared.current += this.number;
36 | }
37 | }
38 | }
39 |
40 | class MultiplyNode extends Node {
41 | constructor(
42 | private number: number,
43 | maxRetries: number = 1,
44 | wait: number = 0
45 | ) {
46 | super(maxRetries, wait);
47 | }
48 |
49 | async prep(shared: SharedStorage): Promise {
50 | if (shared.current !== undefined) {
51 | shared.current *= this.number;
52 | }
53 | }
54 | }
55 |
56 | class CheckPositiveNode extends Node {
57 | constructor(maxRetries: number = 1, wait: number = 0) {
58 | super(maxRetries, wait);
59 | }
60 |
61 | async post(shared: SharedStorage): Promise {
62 | if (shared.current !== undefined && shared.current >= 0) {
63 | return 'positive';
64 | } else {
65 | return 'negative';
66 | }
67 | }
68 | }
69 |
70 | class NoOpNode extends Node {
71 | constructor(maxRetries: number = 1, wait: number = 0) {
72 | super(maxRetries, wait);
73 | }
74 |
75 | async prep(): Promise {
76 | // Do nothing, just pass
77 | }
78 | }
79 |
80 | // New class to demonstrate Node's retry capabilities
81 | class FlakyNode extends Node {
82 | private attemptCount = 0;
83 |
84 | constructor(
85 | private failUntilAttempt: number,
86 | maxRetries: number = 3,
87 | wait: number = 0.1
88 | ) {
89 | super(maxRetries, wait);
90 | }
91 |
92 | async exec(): Promise {
93 | this.attemptCount++;
94 |
95 | if (this.attemptCount < this.failUntilAttempt) {
96 | throw new Error(`Attempt ${this.attemptCount} failed`);
97 | }
98 |
99 | return `Success on attempt ${this.attemptCount}`;
100 | }
101 |
102 | async post(
103 | shared: SharedStorage,
104 | prepRes: any,
105 | execRes: any
106 | ): Promise {
107 | shared.execResult = execRes;
108 | return 'default';
109 | }
110 | }
111 |
112 | // New class to demonstrate using exec method more explicitly
113 | class ExecNode extends Node {
114 | constructor(
115 | private operation: string,
116 | maxRetries: number = 1,
117 | wait: number = 0
118 | ) {
119 | super(maxRetries, wait);
120 | }
121 |
122 | async prep(shared: SharedStorage): Promise {
123 | // Return the current value for processing in exec
124 | return shared.current || 0;
125 | }
126 |
127 | async exec(currentValue: number): Promise {
128 | switch (this.operation) {
129 | case 'square':
130 | return currentValue * currentValue;
131 | case 'double':
132 | return currentValue * 2;
133 | case 'negate':
134 | return -currentValue;
135 | default:
136 | return currentValue;
137 | }
138 | }
139 |
140 | async post(
141 | shared: SharedStorage,
142 | prepRes: any,
143 | execRes: any
144 | ): Promise {
145 | shared.current = execRes;
146 | return 'default';
147 | }
148 | }
149 |
150 | describe('Pocket Flow Tests with Node', () => {
151 | test('single number', async () => {
152 | const shared: SharedStorage = {};
153 | const start = new NumberNode(5);
154 | const pipeline = new Flow(start);
155 | await pipeline.run(shared);
156 | expect(shared.current).toBe(5);
157 | });
158 |
159 | test('sequence with chaining', async () => {
160 | /**
161 | * Test a simple linear pipeline:
162 | * NumberNode(5) -> AddNode(3) -> MultiplyNode(2)
163 | *
164 | * Expected result:
165 | * (5 + 3) * 2 = 16
166 | */
167 | const shared: SharedStorage = {};
168 | const n1 = new NumberNode(5);
169 | const n2 = new AddNode(3);
170 | const n3 = new MultiplyNode(2);
171 |
172 | // Chain them in sequence using method chaining
173 | n1.next(n2).next(n3);
174 |
175 | const pipeline = new Flow(n1);
176 | await pipeline.run(shared);
177 |
178 | expect(shared.current).toBe(16);
179 | });
180 |
181 | test('branching positive', async () => {
182 | /**
183 | * Test a branching pipeline with positive route:
184 | * start = NumberNode(5)
185 | * check = CheckPositiveNode()
186 | * if 'positive' -> AddNode(10)
187 | * if 'negative' -> AddNode(-20)
188 | */
189 | const shared: SharedStorage = {};
190 | const start = new NumberNode(5);
191 | const check = new CheckPositiveNode();
192 | const addIfPositive = new AddNode(10);
193 | const addIfNegative = new AddNode(-20);
194 |
195 | // Setup with chaining
196 | start
197 | .next(check)
198 | .on('positive', addIfPositive)
199 | .on('negative', addIfNegative);
200 |
201 | const pipeline = new Flow(start);
202 | await pipeline.run(shared);
203 |
204 | expect(shared.current).toBe(15);
205 | });
206 |
207 | test('negative branch', async () => {
208 | /**
209 | * Same branching pipeline, but starting with -5.
210 | * Final result: (-5) + (-20) = -25.
211 | */
212 | const shared: SharedStorage = {};
213 | const start = new NumberNode(-5);
214 | const check = new CheckPositiveNode();
215 | const addIfPositive = new AddNode(10);
216 | const addIfNegative = new AddNode(-20);
217 |
218 | // Build the flow with chaining
219 | start
220 | .next(check)
221 | .on('positive', addIfPositive)
222 | .on('negative', addIfNegative);
223 |
224 | const pipeline = new Flow(start);
225 | await pipeline.run(shared);
226 |
227 | expect(shared.current).toBe(-25);
228 | });
229 |
230 | test('cycle until negative', async () => {
231 | /**
232 | * Demonstrate a cyclical pipeline:
233 | * Start with 10, check if positive -> subtract 3, then go back to check.
234 | * Repeat until the number becomes negative.
235 | */
236 | const shared: SharedStorage = {};
237 | const n1 = new NumberNode(10);
238 | const check = new CheckPositiveNode();
239 | const subtract3 = new AddNode(-3);
240 | const noOp = new NoOpNode();
241 |
242 | // Build the cycle with chaining
243 | n1.next(check).on('positive', subtract3).on('negative', noOp);
244 |
245 | subtract3.next(check);
246 |
247 | const pipeline = new Flow(n1);
248 | await pipeline.run(shared);
249 |
250 | // final result should be -2: (10 -> 7 -> 4 -> 1 -> -2)
251 | expect(shared.current).toBe(-2);
252 | });
253 |
254 | // New tests demonstrating Node features
255 |
256 | test('retry functionality', async () => {
257 | const shared: SharedStorage = {};
258 |
259 | // This node will fail on the first attempt but succeed on the second
260 | const flakyNode = new FlakyNode(2, 3, 0.01);
261 |
262 | const pipeline = new Flow(flakyNode);
263 | await pipeline.run(shared);
264 |
265 | // Check that we got a success message indicating it was the second attempt
266 | expect(shared.execResult).toBe('Success on attempt 2');
267 | });
268 |
269 | test('retry with fallback', async () => {
270 | const shared: SharedStorage = {};
271 |
272 | // This node will always fail (requires 5 attempts, but we only allow 2)
273 | const flakyNode = new FlakyNode(5, 2, 0.01);
274 |
275 | // Override the execFallback method to handle the failure
276 | flakyNode.execFallback = async (
277 | prepRes: any,
278 | error: Error
279 | ): Promise => {
280 | return 'Fallback executed due to failure';
281 | };
282 |
283 | const pipeline = new Flow(flakyNode);
284 | await pipeline.run(shared);
285 |
286 | // Check that we got the fallback result
287 | expect(shared.execResult).toBe('Fallback executed due to failure');
288 | });
289 |
290 | test('exec method processing', async () => {
291 | const shared: SharedStorage = { current: 5 };
292 |
293 | const squareNode = new ExecNode('square');
294 | const doubleNode = new ExecNode('double');
295 | const negateNode = new ExecNode('negate');
296 |
297 | squareNode.next(doubleNode).next(negateNode);
298 |
299 | const pipeline = new Flow(squareNode);
300 | await pipeline.run(shared);
301 |
302 | // 5 → square → 25 → double → 50 → negate → -50
303 | expect(shared.current).toBe(-50);
304 | });
305 | });
306 |
--------------------------------------------------------------------------------
/tests/flow-composition.test.ts:
--------------------------------------------------------------------------------
1 | // tests/flow_composition.test.ts
2 | import { Node, Flow } from '../src/index';
3 |
4 | // Define a shared storage type
5 | type SharedStorage = {
6 | current?: number;
7 | };
8 |
9 | class NumberNode extends Node {
10 | constructor(private number: number, maxRetries: number = 1, wait: number = 0) {
11 | super(maxRetries, wait);
12 | }
13 |
14 | async prep(shared: SharedStorage): Promise {
15 | shared.current = this.number;
16 | }
17 | }
18 |
19 | class AddNode extends Node {
20 | constructor(private number: number, maxRetries: number = 1, wait: number = 0) {
21 | super(maxRetries, wait);
22 | }
23 |
24 | async prep(shared: SharedStorage): Promise {
25 | if (shared.current !== undefined) {
26 | shared.current += this.number;
27 | }
28 | }
29 | }
30 |
31 | class MultiplyNode extends Node {
32 | constructor(private number: number, maxRetries: number = 1, wait: number = 0) {
33 | super(maxRetries, wait);
34 | }
35 |
36 | async prep(shared: SharedStorage): Promise {
37 | if (shared.current !== undefined) {
38 | shared.current *= this.number;
39 | }
40 | }
41 | }
42 |
43 | describe('Flow Composition Tests with Node', () => {
44 | test('flow as node', async () => {
45 | /**
46 | * 1) Create a Flow (f1) starting with NumberNode(5), then AddNode(10), then MultiplyNode(2).
47 | * 2) Create a second Flow (f2) whose start is f1.
48 | * 3) Create a wrapper Flow (f3) that contains f2 to ensure proper execution.
49 | * Expected final result in shared.current: (5 + 10) * 2 = 30.
50 | */
51 | const shared: SharedStorage = {};
52 |
53 | // Inner flow f1
54 | const numberNode = new NumberNode(5);
55 | const addNode = new AddNode(10);
56 | const multiplyNode = new MultiplyNode(2);
57 |
58 | numberNode.next(addNode);
59 | addNode.next(multiplyNode);
60 |
61 | const f1 = new Flow(numberNode);
62 |
63 | // f2 starts with f1
64 | const f2 = new Flow(f1);
65 |
66 | // Wrapper flow f3 to ensure proper execution
67 | const f3 = new Flow(f2);
68 | await f3.run(shared);
69 |
70 | expect(shared.current).toBe(30);
71 | });
72 |
73 | test('nested flow', async () => {
74 | /**
75 | * Demonstrates nested flows with proper wrapping:
76 | * inner_flow: NumberNode(5) -> AddNode(3)
77 | * middle_flow: starts with inner_flow -> MultiplyNode(4)
78 | * wrapper_flow: contains middle_flow to ensure proper execution
79 | * Expected final result: (5 + 3) * 4 = 32.
80 | */
81 | const shared: SharedStorage = {};
82 |
83 | // Build the inner flow
84 | const numberNode = new NumberNode(5);
85 | const addNode = new AddNode(3);
86 | numberNode.next(addNode);
87 | const innerFlow = new Flow(numberNode);
88 |
89 | // Build the middle flow, whose start is the inner flow
90 | const multiplyNode = new MultiplyNode(4);
91 | innerFlow.next(multiplyNode);
92 | const middleFlow = new Flow(innerFlow);
93 |
94 | // Wrapper flow to ensure proper execution
95 | const wrapperFlow = new Flow(middleFlow);
96 | await wrapperFlow.run(shared);
97 |
98 | expect(shared.current).toBe(32);
99 | });
100 |
101 | test('flow chaining flows', async () => {
102 | /**
103 | * Demonstrates chaining two flows with proper wrapping:
104 | * flow1: NumberNode(10) -> AddNode(10) # final = 20
105 | * flow2: MultiplyNode(2) # final = 40
106 | * wrapper_flow: contains both flow1 and flow2 to ensure proper execution
107 | * Expected final result: (10 + 10) * 2 = 40.
108 | */
109 | const shared: SharedStorage = {};
110 |
111 | // flow1
112 | const numberNode = new NumberNode(10);
113 | const addNode = new AddNode(10);
114 | numberNode.next(addNode);
115 | const flow1 = new Flow(numberNode);
116 |
117 | // flow2
118 | const multiplyNode = new MultiplyNode(2);
119 | const flow2 = new Flow(multiplyNode);
120 |
121 | // Chain flow1 to flow2
122 | flow1.next(flow2);
123 |
124 | // Wrapper flow to ensure proper execution
125 | const wrapperFlow = new Flow(flow1);
126 | await wrapperFlow.run(shared);
127 |
128 | expect(shared.current).toBe(40);
129 | });
130 |
131 | test('flow with retry handling', async () => {
132 | /**
133 | * Demonstrates retry capabilities in flow composition:
134 | * Using nodes with retry logic in a flow structure
135 | */
136 | const shared: SharedStorage = {};
137 |
138 | // Create a faulty NumberNode that will fail once before succeeding
139 | class FaultyNumberNode extends Node {
140 | private attempts = 0;
141 |
142 | constructor(private number: number, maxRetries: number = 2, wait: number = 0.01) {
143 | super(maxRetries, wait);
144 | }
145 |
146 | async prep(shared: SharedStorage): Promise {
147 | return this.number;
148 | }
149 |
150 | async exec(number: number): Promise {
151 | this.attempts++;
152 | if (this.attempts === 1) {
153 | throw new Error("Simulated failure on first attempt");
154 | }
155 | return number;
156 | }
157 |
158 | async post(shared: SharedStorage, prepRes: any, execRes: any): Promise {
159 | shared.current = execRes;
160 | return "default";
161 | }
162 | }
163 |
164 | // Create a flow with the faulty node followed by standard nodes
165 | const faultyNumberNode = new FaultyNumberNode(5);
166 | const addNode = new AddNode(10);
167 | const multiplyNode = new MultiplyNode(2);
168 |
169 | faultyNumberNode.next(addNode);
170 | addNode.next(multiplyNode);
171 |
172 | const flow = new Flow(faultyNumberNode);
173 | await flow.run(shared);
174 |
175 | // Despite the initial failure, the retry mechanism should allow
176 | // the flow to complete successfully: (5 + 10) * 2 = 30
177 | expect(shared.current).toBe(30);
178 | });
179 |
180 | test('nested flows with mixed retry configurations', async () => {
181 | /**
182 | * Demonstrates nesting flows with different retry configurations
183 | */
184 | const shared: SharedStorage = {};
185 |
186 | // Inner flow with a node that has high retry count
187 | const numberNode = new NumberNode(5, 5, 0.01); // 5 retries
188 | const addNode = new AddNode(3, 2, 0.01); // 2 retries
189 | numberNode.next(addNode);
190 | const innerFlow = new Flow(numberNode);
191 |
192 | // Middle flow with a node that has medium retry count
193 | const multiplyNode = new MultiplyNode(4, 3, 0.01); // 3 retries
194 | innerFlow.next(multiplyNode);
195 | const middleFlow = new Flow(innerFlow);
196 |
197 | // Wrapper flow
198 | const wrapperFlow = new Flow(middleFlow);
199 | await wrapperFlow.run(shared);
200 |
201 | // The result should be the same: (5 + 3) * 4 = 32
202 | expect(shared.current).toBe(32);
203 | });
204 | });
--------------------------------------------------------------------------------
/tests/mapreduce-pattern.test.ts:
--------------------------------------------------------------------------------
1 | // tests/mapreduce-pattern.test.ts
2 | import { BatchNode, Node, Flow, ParallelBatchNode } from '../src/index';
3 |
4 | // Mock utility function to simulate LLM calls
5 | async function callLLM(prompt: string): Promise {
6 | // In a real implementation, this would call an actual LLM
7 | return `Summary for: ${prompt.slice(0, 20)}...`;
8 | }
9 |
10 | // Define shared storage type for MapReduce pattern
11 | type MapReduceSharedStorage = {
12 | files?: Record;
13 | file_summaries?: Record;
14 | all_files_summary?: string;
15 | text_to_process?: string;
16 | text_chunks?: string[];
17 | processed_chunks?: string[];
18 | final_result?: string;
19 | };
20 |
21 | // Document Summarization Example (Sequential)
22 | class SummarizeAllFiles extends BatchNode {
23 | async prep(shared: MapReduceSharedStorage): Promise<[string, string][]> {
24 | const files = shared.files || {};
25 | return Object.entries(files);
26 | }
27 |
28 | async exec(one_file: [string, string]): Promise<[string, string]> {
29 | const [filename, file_content] = one_file;
30 | const summary_text = await callLLM(
31 | `Summarize the following file:\n${file_content}`
32 | );
33 | return [filename, summary_text];
34 | }
35 |
36 | async post(
37 | shared: MapReduceSharedStorage,
38 | prepRes: [string, string][],
39 | execRes: [string, string][]
40 | ): Promise {
41 | shared.file_summaries = Object.fromEntries(execRes);
42 | return "summarized";
43 | }
44 | }
45 |
46 | class CombineSummaries extends Node {
47 | async prep(shared: MapReduceSharedStorage): Promise> {
48 | return shared.file_summaries || {};
49 | }
50 |
51 | async exec(file_summaries: Record): Promise {
52 | // Format as: "File1: summary\nFile2: summary...\n"
53 | const text_list: string[] = [];
54 | for (const [fname, summ] of Object.entries(file_summaries)) {
55 | text_list.push(`${fname} summary:\n${summ}\n`);
56 | }
57 | const big_text = text_list.join("\n---\n");
58 |
59 | return await callLLM(
60 | `Combine these file summaries into one final summary:\n${big_text}`
61 | );
62 | }
63 |
64 | async post(
65 | shared: MapReduceSharedStorage,
66 | prepRes: Record,
67 | final_summary: string
68 | ): Promise {
69 | shared.all_files_summary = final_summary;
70 | return "combined";
71 | }
72 | }
73 |
74 | // Generic MapReduce Example with Parallel Processing
75 | class MapChunks extends ParallelBatchNode {
76 | async prep(shared: MapReduceSharedStorage): Promise {
77 | // Split text into chunks
78 | const text = shared.text_to_process || "";
79 | const chunkSize = 10;
80 | const chunks: string[] = [];
81 |
82 | for (let i = 0; i < text.length; i += chunkSize) {
83 | chunks.push(text.slice(i, i + chunkSize));
84 | }
85 |
86 | shared.text_chunks = chunks;
87 | return chunks;
88 | }
89 |
90 | async exec(chunk: string): Promise {
91 | // Process each chunk (map phase)
92 | // In a real application, this could be any transformation
93 | return chunk.toUpperCase();
94 | }
95 |
96 | async post(
97 | shared: MapReduceSharedStorage,
98 | prepRes: string[],
99 | execRes: string[]
100 | ): Promise