A minimalist LLM framework, ported from Python to C++.
PocketFlow C++ is a port of the original Python PocketFlow. It provides a lightweight, flexible system for building and executing workflows through a simple node-based architecture using modern C++.
Note: This is an initial implementation ported from the Python version. It currently only supports synchronous operations. Community contributions are welcome to enhance and maintain this project, particularly in adding asynchronous capabilities and comprehensive testing.
- Node-Based Architecture: Define workflows by connecting distinct processing units (nodes).
- Type-Safe (within C++ limits): Uses C++ templates for node input/output types.
std::anyis used for flexible context and parameters. - Synchronous Execution: Simple, predictable execution flow (async is a future goal).
- Context Propagation: Share data between nodes using a
Contextmap (std::map<std::string, std::any>). - Configurable Nodes: Pass parameters to nodes using a
Paramsmap (alsostd::map<std::string, std::any>). - Retry Logic: Built-in optional retry mechanism for
Nodeoperations. - Batch Processing: Includes
BatchNodeandBatchFlowfor processing lists of items or parameter sets. - Header-Only: The core library is provided in
PocketFlow.hfor easy integration.
- C++17 Compliant Compiler: Required for
std::anyandstd::optional. (e.g., GCC 7+, Clang 5+, MSVC 19.14+) - CMake: Version 3.10 or higher (for C++17 support).
The library itself is header-only (PocketFlow.h). To build the example provided (main.cpp):
- Ensure you have CMake and a C++17 compiler installed.
- Create a build directory:
mkdir build cd build - Run CMake to configure the project:
cmake ..
- Build the executable:
cmake --build . # Or use make, ninja, etc. depending on your generator # make
- The example executable (e.g.,
pocketflow_example) will be inside thebuilddirectory../pocketflow_example
Here's a simple example demonstrating how to define and run a workflow:
#include "pocketflow.h" // Include the library header
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <memory> // For std::make_shared
// Use the namespace
using namespace pocketflow;
// --- Define Custom Nodes ---
// Start Node: Takes no input (nullptr_t), returns a string action
class MyStartNode : public Node<std::nullptr_t, std::string> {
public:
std::string exec(std::nullptr_t /*prepResult*/) override {
std::cout << "Starting workflow..." << std::endl;
return "started"; // This string determines the next node
}
// Optional: override post to return the action explicitly
std::optional<std::string> post(Context& ctx, const std::nullptr_t& p, const std::string& e) override {
return e; // Return exec result as the action
}
};
// End Node: Takes a string from prep, returns nothing (nullptr_t)
class MyEndNode : public Node<std::string, std::nullptr_t> {
public:
// Optional: Prepare input for exec, potentially using context
std::string prep(Context& ctx) override {
return "Preparing to end workflow";
}
// Execute the node's main logic
std::nullptr_t exec(std::string prepResult) override {
std::cout << "Ending workflow with: " << prepResult << std::endl;
return nullptr; // Return value for void exec
}
// Optional: post can modify context, default returns no action (ends flow here)
};
int main() {
// --- Create Node Instances (use std::shared_ptr) ---
auto startNode = std::make_shared<MyStartNode>();
auto endNode = std::make_shared<MyEndNode>();
// --- Connect Nodes ---
// When startNode returns the action "started", execute endNode next.
startNode->next(endNode, "started");
// --- Create and Configure Flow ---
Flow flow(startNode); // Initialize the flow with the starting node
// --- Prepare Context and Run ---
Context sharedContext; // Map to share data between nodes
std::cout << "Executing workflow..." << std::endl;
flow.run(sharedContext); // Execute the workflow
std::cout << "Workflow completed successfully." << std::endl;
return 0;
}IBaseNode/BaseNode<P, E>: The fundamental building block.P: The type returned by theprepmethod (input toexec). Usestd::nullptr_tfor no input.E: The type returned by theexecmethod (input topost). Usestd::nullptr_tfor no return value.prep(Context&): Prepare data needed forexec. Can use/modify the shared context.exec(P): Execute the core logic of the node.post(Context&, P, E): Process results afterexec. Can use/modify context. Returnsstd::optional<std::string>which is the action determining the next node.std::nullopttriggers the default transition.next(node, action): Connects this node to thenodewhen theactionstring is returned bypost.next(node)connects via the default action.
Node<P, E>: ABaseNodewith added retry logic (maxRetries,waitMillis,execFallback).BatchNode<IN, OUT>: ANodethat processes astd::vector<IN>and produces astd::vector<OUT>, handling retries per item viaexecItemandexecItemFallback.Flow: Orchestrates the execution of connected nodes starting from a designatedstartNode.BatchFlow: AFlowthat runs its entire sequence for multiple parameter sets generated byprepBatch.Context(std::map<std::string, std::any>): A shared data store passed through the workflow, allowing nodes to communicate indirectly. Requires careful type casting (std::any_cast).Params(std::map<std::string, std::any>): Configuration parameters passed to a node instance, typically set before execution or by aBatchFlow.
- Memory Management: Uses
std::shared_ptrfor managing node object lifetimes within the workflow graph. - Type Erasure:
std::anyis used forContextandParams, requiring explicitstd::any_castand careful handling of potentialstd::bad_any_castexceptions. voidTypes:std::nullptr_tis used as a placeholder template argument forPorEwhen a node doesn't logically take input (prepreturns nothing) or produce output (execreturns nothing).- Actions: Node transitions are determined by
std::optional<std::string>returned frompost.std::nulloptsignifies the default transition (if one is defined usingnext(node)). - Header-Only: Simplifies integration – just include
PocketFlow.h.
Use the CMake instructions provided in the "Building" section.
(Currently, no automated test suite like JUnit is included. This is a key area for contribution!) You would typically integrate a testing framework like GoogleTest:
- Set up GoogleTest in your project.
- Write test cases in a separate
.cppfile (e.g.,PocketFlow_test.cpp). - Configure CMake (as shown commented out in the example
CMakeLists.txt) to build and run the tests.
# Example CMake commands after GoogleTest setup
cd build
cmake ..
cmake --build .
ctest # Run testsContributions are highly welcome! We are particularly looking for help with:
- Asynchronous Operations: Implementing non-blocking node execution (e.g., using
std::async,std::thread, futures, or an external async library). - Testing: Adding comprehensive unit and integration tests using a framework like GoogleTest.
- Documentation: Improving explanations, adding more examples, and documenting edge cases.
- Error Handling: Refining exception types and context propagation for errors.
- Examples: Providing more practical examples, potentially related to LLM interactions.
Please feel free to submit pull requests or open issues for discussion.