├── .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 | ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) 6 | [![Docs](https://img.shields.io/badge/docs-latest-blue)](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 { 101 | shared.processed_chunks = execRes; 102 | return "mapped"; 103 | } 104 | } 105 | 106 | class ReduceResults extends Node { 107 | async prep(shared: MapReduceSharedStorage): Promise { 108 | return shared.processed_chunks || []; 109 | } 110 | 111 | async exec(processedChunks: string[]): Promise { 112 | // Combine processed chunks (reduce phase) 113 | // In a real application, this could be any aggregation function 114 | return processedChunks.join(" + "); 115 | } 116 | 117 | async post( 118 | shared: MapReduceSharedStorage, 119 | prepRes: string[], 120 | result: string 121 | ): Promise { 122 | shared.final_result = result; 123 | return "reduced"; 124 | } 125 | } 126 | 127 | // Tests for the MapReduce pattern 128 | describe('MapReduce Pattern Tests', () => { 129 | // Test Document Summarization Example 130 | test('Document Summarization MapReduce', async () => { 131 | // Create and connect nodes 132 | const batchNode = new SummarizeAllFiles(); 133 | const combineNode = new CombineSummaries(); 134 | 135 | batchNode.on("summarized", combineNode); 136 | 137 | const flow = new Flow(batchNode); 138 | 139 | // Prepare test data 140 | const shared: MapReduceSharedStorage = { 141 | files: { 142 | "file1.txt": "Alice was beginning to get very tired of sitting by her sister...", 143 | "file2.txt": "Some other interesting text ...", 144 | "file3.txt": "Yet another file with some content to summarize..." 145 | } 146 | }; 147 | 148 | // Run the flow 149 | await flow.run(shared); 150 | 151 | // Verify results 152 | expect(shared.file_summaries).toBeDefined(); 153 | expect(Object.keys(shared.file_summaries || {}).length).toBe(3); 154 | expect(shared.all_files_summary).toBeDefined(); 155 | expect(typeof shared.all_files_summary).toBe('string'); 156 | }); 157 | 158 | // Test Generic MapReduce with ParallelBatchNode 159 | test('Parallel Text Processing MapReduce', async () => { 160 | // Create and connect nodes 161 | const mapNode = new MapChunks(); 162 | const reduceNode = new ReduceResults(); 163 | 164 | mapNode.on("mapped", reduceNode); 165 | 166 | const flow = new Flow(mapNode); 167 | 168 | // Prepare test data 169 | const shared: MapReduceSharedStorage = { 170 | text_to_process: "This is a longer text that will be processed in parallel using the MapReduce pattern." 171 | }; 172 | 173 | // Run the flow 174 | await flow.run(shared); 175 | 176 | // Verify results 177 | expect(shared.text_chunks).toBeDefined(); 178 | expect(shared.text_chunks?.length).toBeGreaterThan(0); 179 | expect(shared.processed_chunks).toBeDefined(); 180 | expect(shared.processed_chunks?.length).toBe(shared.text_chunks?.length); 181 | expect(shared.final_result).toBeDefined(); 182 | expect(typeof shared.final_result).toBe('string'); 183 | 184 | // Verify the content is actually transformed 185 | expect(shared.processed_chunks?.every(chunk => 186 | chunk === chunk.toUpperCase() 187 | )).toBe(true); 188 | }); 189 | 190 | // Test changing chunk size affects parallel processing 191 | test('Varying Chunk Size in MapReduce', async () => { 192 | // This test demonstrates how chunk size affects the MapReduce process 193 | 194 | // Create a custom MapChunks class with configurable chunk size 195 | class ConfigurableMapChunks extends ParallelBatchNode { 196 | private chunkSize: number; 197 | 198 | constructor(chunkSize: number) { 199 | super(); 200 | this.chunkSize = chunkSize; 201 | } 202 | 203 | async prep(shared: MapReduceSharedStorage): Promise { 204 | const text = shared.text_to_process || ""; 205 | const chunks: string[] = []; 206 | 207 | for (let i = 0; i < text.length; i += this.chunkSize) { 208 | chunks.push(text.slice(i, i + this.chunkSize)); 209 | } 210 | 211 | shared.text_chunks = chunks; 212 | return chunks; 213 | } 214 | 215 | async exec(chunk: string): Promise { 216 | return chunk.toUpperCase(); 217 | } 218 | 219 | async post( 220 | shared: MapReduceSharedStorage, 221 | prepRes: string[], 222 | execRes: string[] 223 | ): Promise { 224 | shared.processed_chunks = execRes; 225 | return "mapped"; 226 | } 227 | } 228 | 229 | // Test with small chunk size 230 | const mapNodeSmall = new ConfigurableMapChunks(5); 231 | const reduceNodeSmall = new ReduceResults(); 232 | mapNodeSmall.on("mapped", reduceNodeSmall); 233 | const flowSmall = new Flow(mapNodeSmall); 234 | 235 | // Test with larger chunk size 236 | const mapNodeLarge = new ConfigurableMapChunks(20); 237 | const reduceNodeLarge = new ReduceResults(); 238 | mapNodeLarge.on("mapped", reduceNodeLarge); 239 | const flowLarge = new Flow(mapNodeLarge); 240 | 241 | // Same input for both flows 242 | const text = "This is a test text for demonstrating chunk size effects."; 243 | 244 | // Run with small chunks 245 | const sharedSmall: MapReduceSharedStorage = { 246 | text_to_process: text 247 | }; 248 | await flowSmall.run(sharedSmall); 249 | 250 | // Run with large chunks 251 | const sharedLarge: MapReduceSharedStorage = { 252 | text_to_process: text 253 | }; 254 | await flowLarge.run(sharedLarge); 255 | 256 | // Verify different chunk counts 257 | if (sharedSmall.text_chunks && sharedLarge.text_chunks) { 258 | expect(sharedSmall.text_chunks.length).toBeGreaterThan(sharedLarge.text_chunks.length); 259 | } 260 | 261 | // Verify end results are identical despite different chunking 262 | expect(sharedSmall.final_result?.replace(/\s\+\s/g, '')).toBe( 263 | sharedLarge.final_result?.replace(/\s\+\s/g, '') 264 | ); 265 | }); 266 | }); -------------------------------------------------------------------------------- /tests/parallel-batch-flow.test.ts: -------------------------------------------------------------------------------- 1 | // tests/parallel-batch-flow.test.ts 2 | import { Node, ParallelBatchNode, Flow, ParallelBatchFlow } from '../src/index'; 3 | 4 | // Define shared storage type 5 | type SharedStorage = { 6 | batches?: number[][]; 7 | processedNumbers?: Record; 8 | total?: number; 9 | }; 10 | 11 | class AsyncParallelNumberProcessor extends ParallelBatchNode< 12 | SharedStorage, 13 | { batchId: number } 14 | > { 15 | private delay: number; 16 | 17 | constructor(delay: number = 0.1, maxRetries: number = 1, wait: number = 0) { 18 | super(maxRetries, wait); 19 | this.delay = delay; 20 | } 21 | 22 | async prep(shared: SharedStorage): Promise { 23 | const batchId = this._params.batchId; 24 | return shared.batches?.[batchId] || []; 25 | } 26 | 27 | async exec(number: number): Promise { 28 | // Simulate async processing 29 | await new Promise((resolve) => setTimeout(resolve, this.delay * 1000)); 30 | return number * 2; 31 | } 32 | 33 | async post( 34 | shared: SharedStorage, 35 | prepRes: number[], 36 | execRes: number[] 37 | ): Promise { 38 | if (!shared.processedNumbers) { 39 | shared.processedNumbers = {}; 40 | } 41 | shared.processedNumbers[this._params.batchId] = execRes; 42 | return 'processed'; 43 | } 44 | } 45 | 46 | class AsyncAggregatorNode extends Node { 47 | constructor(maxRetries: number = 1, wait: number = 0) { 48 | super(maxRetries, wait); 49 | } 50 | 51 | async prep(shared: SharedStorage): Promise { 52 | // Combine all batch results in order 53 | const allResults: number[] = []; 54 | const processed = shared.processedNumbers || {}; 55 | 56 | for (let i = 0; i < Object.keys(processed).length; i++) { 57 | allResults.push(...processed[i]); 58 | } 59 | 60 | return allResults; 61 | } 62 | 63 | async exec(prepResult: number[]): Promise { 64 | await new Promise((resolve) => setTimeout(resolve, 10)); 65 | return prepResult.reduce((sum, val) => sum + val, 0); 66 | } 67 | 68 | async post( 69 | shared: SharedStorage, 70 | prepRes: number[], 71 | execRes: number 72 | ): Promise { 73 | shared.total = execRes; 74 | return 'aggregated'; 75 | } 76 | } 77 | 78 | // Custom ParallelBatchFlow that processes batches based on batchId 79 | class TestParallelBatchFlow extends ParallelBatchFlow { 80 | async prep(shared: SharedStorage): Promise[]> { 81 | return (shared.batches || []).map((_, i) => ({ batchId: i })); 82 | } 83 | } 84 | 85 | describe('ParallelBatchFlow Tests', () => { 86 | test('parallel batch flow', async () => { 87 | /** 88 | * Test basic parallel batch processing flow with batch IDs 89 | */ 90 | const shared: SharedStorage = { 91 | batches: [ 92 | [1, 2, 3], // batchId: 0 93 | [4, 5, 6], // batchId: 1 94 | [7, 8, 9], // batchId: 2 95 | ], 96 | }; 97 | 98 | const processor = new AsyncParallelNumberProcessor(0.1); 99 | const aggregator = new AsyncAggregatorNode(); 100 | 101 | processor.on('processed', aggregator); 102 | const flow = new TestParallelBatchFlow(processor); 103 | 104 | const startTime = Date.now(); 105 | await flow.run(shared); 106 | const executionTime = (Date.now() - startTime) / 1000; 107 | 108 | // Verify each batch was processed correctly 109 | const expectedBatchResults = { 110 | 0: [2, 4, 6], // [1,2,3] * 2 111 | 1: [8, 10, 12], // [4,5,6] * 2 112 | 2: [14, 16, 18], // [7,8,9] * 2 113 | }; 114 | 115 | expect(shared.processedNumbers).toEqual(expectedBatchResults); 116 | 117 | // Verify total 118 | const expectedTotal = shared 119 | .batches!.flat() 120 | .reduce((sum, num) => sum + num * 2, 0); 121 | expect(shared.total).toBe(expectedTotal); 122 | 123 | // Verify parallel execution 124 | expect(executionTime).toBeLessThan(0.2); 125 | }); 126 | 127 | test('error handling', async () => { 128 | /** 129 | * Test error handling in parallel batch flow 130 | */ 131 | class ErrorProcessor extends AsyncParallelNumberProcessor { 132 | async exec(item: number): Promise { 133 | if (item === 2) { 134 | throw new Error(`Error processing item ${item}`); 135 | } 136 | return item; 137 | } 138 | } 139 | 140 | const shared: SharedStorage = { 141 | batches: [ 142 | [1, 2, 3], // Contains error-triggering value 143 | [4, 5, 6], 144 | ], 145 | }; 146 | 147 | const processor = new ErrorProcessor(); 148 | const flow = new TestParallelBatchFlow(processor); 149 | 150 | await expect(async () => { 151 | await flow.run(shared); 152 | }).rejects.toThrow('Error processing item 2'); 153 | }); 154 | 155 | test('multiple batch sizes', async () => { 156 | /** 157 | * Test parallel batch flow with varying batch sizes 158 | */ 159 | const shared: SharedStorage = { 160 | batches: [ 161 | [1], // batchId: 0 162 | [2, 3, 4], // batchId: 1 163 | [5, 6], // batchId: 2 164 | [7, 8, 9, 10], // batchId: 3 165 | ], 166 | }; 167 | 168 | const processor = new AsyncParallelNumberProcessor(0.05); 169 | const aggregator = new AsyncAggregatorNode(); 170 | 171 | processor.on('processed', aggregator); 172 | const flow = new TestParallelBatchFlow(processor); 173 | 174 | await flow.run(shared); 175 | 176 | // Verify each batch was processed correctly 177 | const expectedBatchResults = { 178 | 0: [2], // [1] * 2 179 | 1: [4, 6, 8], // [2,3,4] * 2 180 | 2: [10, 12], // [5,6] * 2 181 | 3: [14, 16, 18, 20], // [7,8,9,10] * 2 182 | }; 183 | 184 | expect(shared.processedNumbers).toEqual(expectedBatchResults); 185 | 186 | // Verify total 187 | const expectedTotal = shared 188 | .batches!.flat() 189 | .reduce((sum, num) => sum + num * 2, 0); 190 | expect(shared.total).toBe(expectedTotal); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /tests/parallel-batch-node.test.ts: -------------------------------------------------------------------------------- 1 | // tests/parallel-batch-node.test.ts 2 | import { ParallelBatchNode, Flow } from '../src/index'; 3 | 4 | // Define shared storage type 5 | type SharedStorage = { 6 | inputNumbers?: number[]; 7 | processedNumbers?: number[]; 8 | executionOrder?: number[]; 9 | finalResults?: number[]; 10 | }; 11 | 12 | class AsyncParallelNumberProcessor extends ParallelBatchNode { 13 | private delay: number; 14 | 15 | constructor(delay: number = 0.1, maxRetries: number = 1, wait: number = 0) { 16 | super(maxRetries, wait); 17 | this.delay = delay; 18 | } 19 | 20 | async prep(shared: SharedStorage): Promise { 21 | return shared.inputNumbers || []; 22 | } 23 | 24 | async exec(number: number): Promise { 25 | await new Promise((resolve) => setTimeout(resolve, this.delay * 1000)); 26 | return number * 2; 27 | } 28 | 29 | async post( 30 | shared: SharedStorage, 31 | prepRes: number[], 32 | execRes: number[] 33 | ): Promise { 34 | shared.processedNumbers = execRes; 35 | return 'processed'; 36 | } 37 | } 38 | 39 | class ErrorProcessor extends ParallelBatchNode { 40 | constructor(maxRetries: number = 1, wait: number = 0) { 41 | super(maxRetries, wait); 42 | } 43 | 44 | async prep(shared: SharedStorage): Promise { 45 | return shared.inputNumbers || []; 46 | } 47 | 48 | async exec(item: number): Promise { 49 | if (item === 2) { 50 | throw new Error(`Error processing item ${item}`); 51 | } 52 | return item; 53 | } 54 | } 55 | 56 | class OrderTrackingProcessor extends ParallelBatchNode { 57 | private executionOrder: number[] = []; 58 | 59 | constructor(maxRetries: number = 1, wait: number = 0) { 60 | super(maxRetries, wait); 61 | } 62 | 63 | async prep(shared: SharedStorage): Promise { 64 | this.executionOrder = []; 65 | shared.executionOrder = this.executionOrder; 66 | return shared.inputNumbers || []; 67 | } 68 | 69 | async exec(item: number): Promise { 70 | const delay = item % 2 === 0 ? 0.1 : 0.05; 71 | await new Promise((resolve) => setTimeout(resolve, delay * 1000)); 72 | this.executionOrder.push(item); 73 | return item; 74 | } 75 | 76 | async post( 77 | shared: SharedStorage, 78 | prepRes: number[], 79 | execRes: number[] 80 | ): Promise { 81 | shared.executionOrder = this.executionOrder; 82 | return undefined; 83 | } 84 | } 85 | 86 | describe('AsyncParallelBatchNode Tests', () => { 87 | test('parallel processing', async () => { 88 | // Test that numbers are processed in parallel by measuring execution time 89 | const shared: SharedStorage = { 90 | inputNumbers: Array.from({ length: 5 }, (_, i) => i), 91 | }; 92 | 93 | const processor = new AsyncParallelNumberProcessor(0.1); 94 | 95 | // Record start time 96 | const startTime = Date.now(); 97 | await processor.run(shared); 98 | const endTime = Date.now(); 99 | 100 | // Check results 101 | const expected = [0, 2, 4, 6, 8]; // Each number doubled 102 | expect(shared.processedNumbers).toEqual(expected); 103 | 104 | // Since processing is parallel, total time should be approximately 105 | // equal to the delay of a single operation, not delay * number_of_items 106 | const executionTime = endTime - startTime; 107 | expect(executionTime).toBeLessThan(200); // Should be around 100ms plus minimal overhead 108 | }); 109 | 110 | test('empty input', async () => { 111 | // Test processing of empty input 112 | const shared: SharedStorage = { 113 | inputNumbers: [], 114 | }; 115 | 116 | const processor = new AsyncParallelNumberProcessor(); 117 | await processor.run(shared); 118 | 119 | expect(shared.processedNumbers).toEqual([]); 120 | }); 121 | 122 | test('single item', async () => { 123 | // Test processing of a single item 124 | const shared: SharedStorage = { 125 | inputNumbers: [42], 126 | }; 127 | 128 | const processor = new AsyncParallelNumberProcessor(); 129 | await processor.run(shared); 130 | 131 | expect(shared.processedNumbers).toEqual([84]); 132 | }); 133 | 134 | test('large batch', async () => { 135 | // Test processing of a large batch of numbers 136 | const inputSize = 100; 137 | const shared: SharedStorage = { 138 | inputNumbers: Array.from({ length: inputSize }, (_, i) => i), 139 | }; 140 | 141 | const processor = new AsyncParallelNumberProcessor(0.01); 142 | await processor.run(shared); 143 | 144 | const expected = Array.from({ length: inputSize }, (_, i) => i * 2); 145 | expect(shared.processedNumbers).toEqual(expected); 146 | }); 147 | 148 | test('error handling', async () => { 149 | // Test error handling during parallel processing 150 | const shared: SharedStorage = { 151 | inputNumbers: [1, 2, 3], 152 | }; 153 | 154 | const processor = new ErrorProcessor(); 155 | 156 | await expect(async () => { 157 | await processor.run(shared); 158 | }).rejects.toThrow('Error processing item 2'); 159 | }); 160 | 161 | test('concurrent execution', async () => { 162 | // Test that tasks are actually running concurrently by tracking execution order 163 | const shared: SharedStorage = { 164 | inputNumbers: Array.from({ length: 4 }, (_, i) => i), // [0, 1, 2, 3] 165 | }; 166 | 167 | const processor = new OrderTrackingProcessor(); 168 | await processor.run(shared); 169 | 170 | // Odd numbers should finish before even numbers due to shorter delay 171 | expect(shared.executionOrder?.indexOf(1)).toBeLessThan( 172 | shared.executionOrder?.indexOf(0) as number 173 | ); 174 | expect(shared.executionOrder?.indexOf(3)).toBeLessThan( 175 | shared.executionOrder?.indexOf(2) as number 176 | ); 177 | }); 178 | 179 | test('integration with Flow', async () => { 180 | // Test integration with Flow 181 | const shared: SharedStorage = { 182 | inputNumbers: Array.from({ length: 5 }, (_, i) => i), 183 | }; 184 | 185 | class ProcessResultsNode extends ParallelBatchNode { 186 | async prep(shared: SharedStorage): Promise { 187 | return shared.processedNumbers || []; 188 | } 189 | 190 | async exec(num: number): Promise { 191 | return num + 1; 192 | } 193 | 194 | async post( 195 | shared: SharedStorage, 196 | prepRes: number[], 197 | execRes: number[] 198 | ): Promise { 199 | shared.finalResults = execRes; 200 | return 'completed'; 201 | } 202 | } 203 | 204 | const processor = new AsyncParallelNumberProcessor(); 205 | const resultsProcessor = new ProcessResultsNode(); 206 | 207 | processor.on('processed', resultsProcessor); 208 | 209 | const pipeline = new Flow(processor); 210 | await pipeline.run(shared); 211 | 212 | // Each number should be doubled and then incremented 213 | const expected = [1, 3, 5, 7, 9]; 214 | expect(shared.finalResults).toEqual(expected); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /tests/qa-pattern.test.ts: -------------------------------------------------------------------------------- 1 | // tests/qa-pattern.test.ts 2 | import { Node, Flow } from '../src/index'; 3 | 4 | // Mock the prompt function since we're in a test environment 5 | const mockUserInput = "What is PocketFlow?"; 6 | global.prompt = jest.fn().mockImplementation(() => mockUserInput); 7 | 8 | // Mock utility function for LLM calls 9 | async function callLlm(question: string): Promise { 10 | // Simple mock LLM call that returns a predefined answer based on the question 11 | if (question.includes("PocketFlow")) { 12 | return "PocketFlow is a TypeScript library for building reliable AI pipelines with a focus on composition and reusability."; 13 | } 14 | return "I don't know the answer to that question."; 15 | } 16 | 17 | // Define the shared store type as shown in the guide 18 | interface QASharedStore { 19 | question?: string; 20 | answer?: string; 21 | [key: string]: unknown; 22 | } 23 | 24 | // Implement the GetQuestionNode from the guide 25 | class GetQuestionNode extends Node { 26 | async exec(_: unknown): Promise { 27 | // Get question directly from user input 28 | const userQuestion = prompt("Enter your question: ") || ""; 29 | return userQuestion; 30 | } 31 | 32 | async post( 33 | shared: QASharedStore, 34 | _: unknown, 35 | execRes: string 36 | ): Promise { 37 | // Store the user's question 38 | shared.question = execRes; 39 | return "default"; // Go to the next node 40 | } 41 | } 42 | 43 | // Implement the AnswerNode from the guide 44 | class AnswerNode extends Node { 45 | async prep(shared: QASharedStore): Promise { 46 | // Read question from shared 47 | return shared.question || ""; 48 | } 49 | 50 | async exec(question: string): Promise { 51 | // Call LLM to get the answer 52 | return await callLlm(question); 53 | } 54 | 55 | async post( 56 | shared: QASharedStore, 57 | _: unknown, 58 | execRes: string 59 | ): Promise { 60 | // Store the answer in shared 61 | shared.answer = execRes; 62 | return undefined; 63 | } 64 | } 65 | 66 | // Create a function to set up the QA flow 67 | function createQaFlow(): Flow { 68 | // Create nodes 69 | const getQuestionNode = new GetQuestionNode(); 70 | const answerNode = new AnswerNode(); 71 | 72 | // Connect nodes in sequence 73 | getQuestionNode.next(answerNode); 74 | 75 | // Create flow starting with input node 76 | return new Flow(getQuestionNode); 77 | } 78 | 79 | // Tests for the QA pattern 80 | describe('QA Pattern Tests', () => { 81 | // Test the basic QA flow 82 | test('Basic QA Flow with mocked user input', async () => { 83 | // Create shared store 84 | const shared: QASharedStore = { 85 | question: undefined, 86 | answer: undefined, 87 | }; 88 | 89 | // Create and run the flow 90 | const qaFlow = createQaFlow(); 91 | await qaFlow.run(shared); 92 | 93 | // Verify results 94 | expect(shared.question).toBe(mockUserInput); 95 | expect(shared.answer).toBe("PocketFlow is a TypeScript library for building reliable AI pipelines with a focus on composition and reusability."); 96 | }); 97 | 98 | // Test with a different question (simulating a different user input) 99 | test('QA Flow with unknown question', async () => { 100 | // Change the mock implementation for this test 101 | global.prompt = jest.fn().mockImplementation(() => "What is the meaning of life?"); 102 | 103 | const shared: QASharedStore = { 104 | question: undefined, 105 | answer: undefined, 106 | }; 107 | 108 | const qaFlow = createQaFlow(); 109 | await qaFlow.run(shared); 110 | 111 | expect(shared.question).toBe("What is the meaning of life?"); 112 | expect(shared.answer).toBe("I don't know the answer to that question."); 113 | }); 114 | 115 | // Test error handling (missing question) 116 | test('QA Flow with missing question', async () => { 117 | // Mock a null or empty response 118 | global.prompt = jest.fn().mockImplementation(() => ""); 119 | 120 | const shared: QASharedStore = { 121 | question: undefined, 122 | answer: undefined, 123 | }; 124 | 125 | const qaFlow = createQaFlow(); 126 | await qaFlow.run(shared); 127 | 128 | expect(shared.question).toBe(""); 129 | expect(shared.answer).toBe("I don't know the answer to that question."); 130 | }); 131 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "rootDir": "./src" 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist", "tests"] 15 | } -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | dts: true, 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | outDir: 'dist', 11 | outExtension({ format }) { 12 | return { 13 | js: format === 'cjs' ? '.js' : '.mjs', 14 | }; 15 | } 16 | }); --------------------------------------------------------------------------------