Skip to content

Use of strong typing for Context and more... #1

Description

@jmdevall

Congratulations on your project! I find the simplicity very intriguing without compromising on power.

I’ve reworked the PocketFlow library with a focus on modularity, type safety, and decoupled components while keeping the core concepts intact. Below is an overview of key changes and enhancements compared to the original design:

  • Alignments with Your Original Design: Your foundational ideas (e.g., workflows, nodes, exec/prep/post patterns) are preserved and improved in PFJ
  • Strong typing instead of Map<String, Object> for Context
  • Replaced raw maps with generics (C, I, O) for compile-time safety and type clarity.
  • Preference Interfaces over inheritance: Node is an interface, and Step implements it without forced hierarchies (no BaseNode or subclass chains).
  • Reliable for retry logic:
  • A standalone class wraps functions (not tied to a class hierarchy), enabling retries with customizable delays.
  • Flows as Steps:
    • The newFlow() method creates a Step that orchestrates sub-flows, enabling nesting.
    • Flows can act as Steps within other flows (e.g., newFlow(startNode).run()).
  • Simplifies configuration with minimal boilerplate (e.g., Step.builder() thanks to Lombok).
  • 100 lines of code (but I removed the batch classes)
package pocketflowj;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import lombok.Builder;
public class PFJ {
static final String DEFAULT_ACTION = "default";
private static void logWarn(String message) { System.err.println("WARN: PocketFlow - " + message); }
static class PocketFlowException extends RuntimeException {
public PocketFlowException(String message) { super(message); }
public PocketFlowException(String message, Throwable cause) { super(message, cause); }
}
interface Node<C> { String run(C context); Node<C> getNextNode(String action); }
interface ContextAdapter<C, I, O> { I prep(C context); String post(C context, I input, O output); }
@FunctionalInterface interface TriFunction<A, B, C, R> { R apply(A a, B b, C c); }
static class Reliable<I, O> implements Function<I, O> {
private final int maxRetries;
private final long waitMillis;
private final Function<I, O> function;
public Reliable(Function<I, O> function) { this(function, 1, 0); }
public Reliable(Function<I, O> function, int maxRetries, long waitMillis) {
if(maxRetries <1) throw new IllegalArgumentException("maxRetries must be at least 1");
if(waitMillis <0) throw new IllegalArgumentException("waitMillis cannot be negative");
this.function = function; this.maxRetries = maxRetries; this.waitMillis = waitMillis;
}
@Override
public O apply(I input) {
Exception lastException = null;
for(int currentRetry=0; currentRetry < maxRetries; currentRetry++) {
try { return function.apply(input); }
catch(Exception e) { lastException = e; if(currentRetry < maxRetries-1 && waitMillis>0) waitTheTime();
}
}
if(lastException == null) throw new PocketFlowException("Execution failed, but no exception was captured.");
if(lastException instanceof RuntimeException) throw (RuntimeException)lastException;
throw new PocketFlowException("Final execution failed", lastException);
}
private void waitTheTime() {
try { TimeUnit.MILLISECONDS.sleep(waitMillis); }
catch(InterruptedException ie) { Thread.currentThread().interrupt();
throw new PocketFlowException("Thread interrupted during retry wait", ie); }
}
}
@Builder static class DefaultContextAdapter<C, I, O> implements ContextAdapter<C, I, O> {
private Function<C, I> prep;
private TriFunction<C, I, O, String> post;
public I prep(C context) { return prep.apply(context); }
public String post(C context, I input, O output) { return post.apply(context, input, output); }
}
@Builder static class Step<C, I, O> implements Node<C> {
private Function<C, I> prep;
private TriFunction<C, I, O, String> post;
private final ContextAdapter<C, I, O> adapter;
private Function<I, O> exec;
@Builder.Default private final int maxRetries = 1;
@Builder.Default private final long waitMillis = 0;
private final Map<String, Node<C>> successors = new HashMap<>();
private Function<I, O> getWrappedFunction() { return maxRetries>1 ? new Reliable<>(exec, maxRetries, waitMillis) : exec; }
private ContextAdapter<C, I, O> getWrappedAdapter(){ return this.adapter!=null?this.adapter:new DefaultContextAdapter<>(prep,post); }
public String run(C context) {
ContextAdapter<C,I,O> a=getWrappedAdapter();
I prepRes = a.prep(context);
O execRes = getWrappedFunction().apply(prepRes);
return a.post(context, prepRes, execRes);
}
public Node<C> getNextNode(String action) {
String actionKey = Objects.requireNonNullElse(action, DEFAULT_ACTION);
Node<C> nextNode = successors.get(actionKey);
if(nextNode == null && !successors.isEmpty()) logWarn("Action '"+actionKey+"' not found in successors of "+getClass().getSimpleName());
return nextNode;
}
public Node<C> on(String action, Node<C> next) { successors.put(action, next); return next; }
public Node<C> defaultNext(Node<C> next) { return on(DEFAULT_ACTION, next); }
}
static <C> Step<C, C, String> newFlow(Node<C> firstStep) {
ContextAdapter<C, C, String> adapter = DefaultContextAdapter.<C, C, String>builder().prep(Function.identity())
.post((c, i, o) -> o).build();
Function<C, String> exec = context -> {
String lastAction = null;
for (Node<C> current = firstStep; current != null; current = current.getNextNode(lastAction)) {
lastAction = current.run(context);
}
return lastAction;
};
return Step.<C, C, String>builder().adapter(adapter).exec(exec).build();
}
}

AgentTest.java.txt

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions