PocketFlow

repository·main·Indexed 42 days ago

https://github.com/the-pocket/pocketflow-typescript

A minimalist, lightweight TypeScript framework for building LLM-powered applications, including multi-agent systems, workflows, and RAG (Retrieval-Augmented Generation). Version 1.0.4 features include BatchFlow for parameter-based reuse, BatchNode for chunking large datasets via prep, exec, and post lifecycle methods, and AsyncParallelBatchFlow for high-throughput concurrent processing.

Tokens
44.3K
Snippets
95
Records
115
Agent score
51%

What's inside PocketFlow

  1. Overview of Text Embedding APIs

    main

    Embedding is considered a micro-optimization compared to Flow Design. It is recommended to start with the most convenient embedding provider and optimize later. Below is a comparison of available embedding APIs, their free tiers, and pricing models.

    | **API**              | **Free Tier**                           | **Pricing Model**                   | **Docs**                                                                                                                  |
    | -------------------- | --------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
    | **OpenAI**           | ~$5 credit                              | ~$0.0001/1K tokens                  | [OpenAI Embeddings](https://platform.openai.com/docs/api-reference/embeddings)                                            |
    | **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) |
    | **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)              |
    | **AWS Bedrock**      | No free tier, but AWS credits may apply | ~$0.00002/1K tokens (Titan V2)      | [Amazon Bedrock](https://docs.aws.amazon.com/bedrock/)                                                                   |
    | **Cohere**           | Limited free tier                       | ~$0.0001/1K tokens                  | [Cohere Embeddings](https://docs.cohere.com/docs/cohere-embed)                                                               |
    | **Hugging Face**     | ~$0.10 free compute monthly             | Pay per second of compute           | [HF Inference API](https://huggingface.co/docs/api-inference)                                                             |
    | **Jina**             | 1M tokens free                          | Pay per token after                 | [Jina Embeddings](https://jina.ai/embeddings/)                                                                            |
  2. What is PocketFlow.js?

    main

    PocketFlow.js is a minimalist, lightweight LLM framework designed for building Agents, Task Decomposition, RAG, and more. It is built around a core graph abstraction that is approximately 100 lines of code and has zero dependencies, preventing vendor lock-in.

    Key characteristics:

    • Lightweight: Minimalist core graph abstraction.
    • Expressive: Supports complex patterns like Multi-Agents, Workflows, and RAG.
    • Agentic-Coding: Designed to be intuitive enough for AI agents to assist in building complex LLM applications.
  3. What is a BatchFlow and how does it work

    main

    A BatchFlow is a mechanism to rerun an entire Flow multiple times, each time with different parameters. It acts as a loop that replays a sub-flow for every parameter set generated by its prep() method.

    How it works under the hood:

    1. prep(shared) returns a list of parameter objects (e.g., [{id: 1}, {id: 2}]).
    2. The BatchFlow iterates through these objects.
    3. For each object, it merges it with the BatchFlow's own params.
    4. It then calls flow.run(mergedParams) for the sub-flow.

    Use a BatchFlow when you want to execute a complete sequence of nodes (a Flow) repeatedly for different inputs (e.g., running a full analysis pipeline for every file in a list).

    type SharedStorage = {
      files: string[];
    };
    
    type FileParams = {
      filename: string;
    };
    
    class SummarizeAllFiles extends BatchFlow<SharedStorage> {
      async prep(shared: SharedStorage): Promise<FileParams[]> {
        return shared.files.map((filename) => ({ filename }));
      }
    }
    
    // Create a per-file summarization flow
    const summarizeFile = new SummarizeFile();
    const summarizeAllFiles = new SummarizeAllFiles(summarizeFile);
    
    await summarizeAllFiles.run({ files: ["file1.txt", "file2.txt"] });
  4. What is a BatchNode and when to use it

    main

    A BatchNode is a specialized type of Node designed for chunk-based or iterative processing within a single node execution. Instead of processing one input, it splits an input into multiple items, processes them individually, and then aggregates the results.

    Use a BatchNode when you need to:

    • Chunk-based processing: e.g., splitting a large text into smaller pieces for LLM processing.
    • Iterative processing: e.g., looping over a list of URLs or files within a single step of a Flow.
    class MyBatchNode extends BatchNode<SharedStorage> {
      async prep(shared: SharedStorage): Promise<string[]> {
        // Return an array of items to process
        return ['item1', 'item2'];
      }
    
      async exec(item: string): Promise<string> {
        // Called once per item in the array returned by prep()
        return `processed ${item}`;
      }
    
      async post(shared: SharedStorage, prepRes: string[], execResList: string[]): Promise<string> {
        // Called once after all items are processed
        // execResList contains the results from all exec() calls
        return 'default';
      }
    }
  5. How Parallel Nodes and Flows work

    main

    Parallel Nodes and Flows allow you to run multiple operations concurrently using TypeScript's Promise.all(). This is primarily used to overlap I/O-bound work such as LLM calls, database queries, API requests, or file I/O to improve performance.

    Best Practices and Constraints

    • Task Independence: Only use parallelization if each task is independent. If a task depends on the output of a previous item, do not parallelize.
    • Rate Limiting: Parallel execution can quickly trigger rate limits on external services (like LLM providers). Consider implementing a throttling mechanism.
    • Batch APIs: For maximum efficiency and to avoid rate limits, check if your provider offers a single-call batch inference API instead of multiple parallel requests.
  6. Implement Node lifecycle steps (prep, exec, post)

    main

    Nodes in PocketFlow follow a structured lifecycle consisting of three main phases. This pattern ensures that state is correctly initialized, operations are performed, and results are persisted or passed to the next node.

    1. prep: Initialize or prepare data in the shared store (e.g., creating empty arrays or loading initial configuration).
    2. exec: The core execution logic (e.g., reading files, performing calculations, or calling external APIs like Sharp).
    3. post: Finalize the node's work by writing results back to the shared store or saving output files (e.g., updating the processedImages list or writing a report).
  7. Design a shared memory structure for batch flows

    main

    In a PocketFlow application, nodes communicate and share state via a shared memory structure. For a parallel batch processing task like image filtering, the SharedData interface should organize input, processing parameters, and output tracking to minimize redundancy.

    Example structure for an image processing flow:

    interface SharedData {
      // Input data
      inputImages: string[]; // Paths to input images
    
      // Processing data
      filters: string[]; // List of filters to apply (blur, grayscale, sepia)
    
      // Output data
      outputFolder: string; // Path to output folder
      processedImages: {
        // Tracking processed images
        imagePath: string;
        appliedFilters: string[];
      }[];
    }
    interface SharedData {
      inputImages: string[];
      filters: string[];
      outputFolder: string;
      processedImages: {
        imagePath: string;
        appliedFilters: string[];
      }[];
    }
  8. How Nodes and Flows communicate

    main

    In PocketFlow, communication between Nodes and Flows occurs through two primary mechanisms: Shared Store and Params.

    This is a global data structure (typically an in-memory dictionary) that all nodes can access.

    • Reading: Nodes access data during the prep() phase.
    • Writing: Nodes write data during the post() phase.
    • Best Practice: Use the Shared Store for almost all data exchange. This separates your Data Schema from your Compute Logic, making your code more maintainable and flexible. It is ideal for large content or data that multiple nodes need to access.

    2. Params (Batch Mode Only)

    Params are local, ephemeral dictionaries passed from a parent Flow to a Node.

    • Usage: Primarily used in Batch mode as identifiers (e.g., filenames, numeric IDs).
    • Immutability: Parameter keys and values must be immutable during a Node's run cycle (prep -> exec -> post).
    • Lifecycle: Params are set via setParams(), cleared, and updated each time a parent Flow calls them.
    FeatureShared StoreParams
    Mental ModelLike a Heap (shared by all calls)Like a Stack (assigned by the caller)
    Primary UseData results, large content, shared stateTask identifiers, per-node/flow config
    ScopeGlobal to the FlowLocal to the Node/Flow
    /* Conceptual comparison of Shared Store vs Params */
    // Shared Store: Global/Heap-like
    // Params: Local/Stack-like
  9. How the RAG Offline Indexing stage works

    main

    The Offline Indexing stage builds a searchable index from raw files. It uses a SharedStore to pass data between nodes.

    Key components:

    • ChunkDocs (BatchNode): Reads files, splits text into chunks, and saves them to shared.allChunks.
    • EmbedDocs (BatchNode): Takes chunks from shared.allChunks, generates embeddings, and saves them to shared.allEmbeds.
    • StoreIndex (Node): Takes all embeddings, creates a vector index, and saves it to shared.index.

    Nodes are connected via .next() and wrapped in a Flow.

    const chunkNode = new ChunkDocs();
    const embedNode = new EmbedDocs();
    const storeNode = new StoreIndex();
    
    chunkNode.next(embedNode).next(storeNode);
    const offlineFlow = new Flow(chunkNode);
    type SharedStore = {
      files?: string[];
      allChunks?: string[];
      allEmbeds?: number[][];
      index?: any;
    };
    
    class ChunkDocs extends BatchNode<SharedStore> {
      async prep(shared: SharedStore): Promise<string[]> {
        return shared.files || [];
      }
    
      async exec(filepath: string): Promise<string[]> {
        const text = fs.readFileSync(filepath, "utf-8");
        const chunks: string[] = [];
        const size = 100;
        for (let i = 0; i < text.length; i += size) {
          chunks.push(text.substring(i, i + size));
        }
        return chunks;
      }
    
      async post(
        shared: SharedStore,
        _: string[],
        chunks: string[][]
      ): Promise<undefined> {
        shared.allChunks = chunks.flat();
        return undefined;
      }
    }
    
    class EmbedDocs extends BatchNode<SharedStore> {
      async prep(shared: SharedStore): Promise<string[]> {
        return shared.allChunks || [];
      }
    
      async exec(chunk: string): Promise<number[]> {
        return await getEmbedding(chunk);
      }
    
      async post(
        shared: SharedStore,
        _: string[],
        embeddings: number[][]
      ): Promise<undefined> {
        shared.allEmbeds = embeddings;
        return undefined;
      }
    }
    
    class StoreIndex extends Node<SharedStore> {
      async prep(shared: SharedStore): Promise<number[][]> {
        return shared.allEmbeds || [];
      }
    
      async exec(allEmbeds: number[][]): Promise<unknown> {
        return await createIndex(allEmbeds);
      }
    
      async post(
        shared: SharedStore,
        _: number[][],
        index: unknown
      ): Promise<undefined> {
        shared.index = index;
        return undefined;
      }
    }
    
    const chunkNode = new ChunkDocs();
    const embedNode = new EmbedDocs();
    const storeNode = new StoreIndex();
    
    chunkNode.next(embedNode).next(storeNode);
    const offlineFlow = new Flow(chunkNode);
  10. Design Nodes using a Shared Store

    main

    PocketFlow relies on a shared store for communication between nodes. When designing nodes, plan how they will read from and write to this central state.

    Shared Store Strategies

    • Simple Systems: Use an in-memory object.
    • Complex/Persistent Systems: Use a database.
    • Principle: Use in-memory references or foreign keys to avoid data duplication.

    Node Specification Template

    For each node, define:

    • type: (e.g., Node, BatchNode, or ParallelBatchNode)
    • prep: How it reads data from the shared store.
    • exec: The core logic or utility function it calls.
    • post: How it writes the result back to the shared store.
    interface SharedStore {
      user: {
        id: string;
        context: {
          weather: { temp: number; condition: string };
          location: string;
        };
      };
      results: Record<string, unknown>;
    }
    
    const shared: SharedStore = {
      user: {
        id: "user123",
        context: {
          weather: { temp: 72, condition: "sunny" },
          location: "San Francisco",
        },
      },
      results: {}, // Empty object to store outputs
    };
  11. How PocketFlow.js core abstractions work

    main

    PocketFlow.js models LLM workflows using a Graph + Shared Store mental model. The workflow is composed of the following building blocks:

    • Node: Handles individual, simple tasks (such as a single LLM call).
    • Flow: Connects nodes together using Actions (which act as labeled edges in the graph).
    • Shared Store: Provides a mechanism for communication and state sharing between nodes within a flow.
    • Batch: Specialized nodes or flows designed for data-intensive tasks.
    • Parallel: (Advanced) Specialized nodes or flows designed to handle I/O-bound tasks.
  12. How Nested Flows work

    main

    A Flow can act as a Node within another Flow. This allows you to compose complex pipelines from smaller, reusable sub-flows.

    Composition Rules:

    • Execution: When a parent Flow runs a sub-flow, it executes the sub-flow's nodes. Once the sub-flow completes, the parent Flow continues to the next node in its own sequence.
    • Logic: A Flow acting as a Node will run its own prep() and post() methods, but it will not run exec(), as its purpose is orchestration.
    • Data: In a Flow's post() method, execRes is always undefined. Instead, use the shared store to access execution results.
    • Parameters: A Node's params will be a merge of all its parent Flows' params.
    // Create a sub-flow
    nodeA.next(nodeB);
    const subflow = new Flow(nodeA);
    
    // Connect the sub-flow to another node in a parent flow
    subflow.next(nodeC);
    
    // Create the parent flow
    const parentFlow = new Flow(subflow);