Package dyntabs.ai

Class FlowBuilder

java.lang.Object
dyntabs.ai.FlowBuilder

public final class FlowBuilder extends Object
Builder for an EasyAI.flow() pipeline: you lay out the named steps in the order they must run, then build() returns a ready-to-run Flow.

Familiar analogy: writing a recipe card, or laying out the stations of an assembly line in order. Each step(String, FlowStep) call adds the next station; nothing runs until you build() the line and Flow.run(Object) it.


 Flow flow = EasyAI.flow()
     .step("understand", ctx -> EasyAI.extract(OrderRequest.class).from(ctx.inputText()))
     .step("checkStock", ctx -> inventory.checkStock(ctx.get("understand", OrderRequest.class)))
     .step("pay",        ctx -> payment.charge(ctx.get("understand", OrderRequest.class)))
     .step("summarize",  ctx -> EasyAI.chat().build().send("Summarize:\n" + ctx.trail()))
     .withEventListener(e -> log.info("{}", e))
     .build();
 

Place in the chain

   EasyAI.flow()  → new FlowBuilder()
        → .step(name, fn) (repeated, order preserved) [.withEventListener(...)]
        → .build()  → Flow  → Flow.run(input)
 
See Also:
  • Method Details

    • step

      public FlowBuilder step(String name, FlowStep<?> step)
      Appends a named step to the pipeline.

      Steps run in the order you add them. The name is how later steps refer back to this step's result (ctx.get(name, Type.class)), how it appears in FlowContext.trail(), and how a FlowException identifies it if it fails — so pick a short, meaningful name ("checkStock", "pay"). Names must be unique within a flow.

      Do whatever you need inside the step: call plain Java services, or reach for the LLM (EasyAI.extract(...), EasyAI.chat()) at the edges where language is the point.

      Parameters:
      name - a short, unique, non-blank name for this step
      step - the work to run — a function of the shared FlowContext
      Returns:
      this builder, for chaining
      Throws:
      IllegalArgumentException - if name is blank, step is null, or the name is already used
    • stepIf

      public FlowBuilder stepIf(String name, Predicate<FlowContext> condition, FlowStep<?> step)
      Appends a step that runs only if the given condition holds at that point in the flow; otherwise the step is skipped entirely (it stores no result, so ctx.has(name) stays false for later steps).

      This makes a branch a first-class, named, skippable step — visible in FlowContext.trail(), in the live event stream (a skipped step emits a STEP/WARNING "skipped" row), and testable on its own. Use it when the branch is a whole sub-path worth naming; for a trivial in-step choice a plain Java if inside a normal step(String, FlowStep) is still perfectly fine.

      Familiar analogy: a station on the assembly line with a gate in front of it — the folder only rolls in if the gate condition is met, otherwise it slides straight past.

      
       .step("checkStock", ctx -> inventory.checkStock(ctx.get("understand", OrderRequest.class)))
       .stepIf("reserve",
               ctx -> !ctx.get("checkStock", Boolean.class),               // only when NOT in stock
               ctx -> inventory.getFromWarehouse(ctx.get("understand", OrderRequest.class)))
       
      Parameters:
      name - a short, unique, non-blank name for this step
      condition - the guard evaluated against the live FlowContext when the flow reaches this step; the step runs only if it returns true
      step - the work to run when the condition holds
      Returns:
      this builder, for chaining
      Throws:
      IllegalArgumentException - if name is blank/duplicate, or condition/step is null
    • orElse

      public FlowBuilder orElse(FlowStep<?> alternative)
      Attaches a fallback alternative to the most recently added step: if that step's primary work throws, this alternative is tried instead (and further orElse calls chain more alternatives, tried in order until one succeeds). If every attempt throws, the flow aborts with a FlowException naming the step.

      This is the deterministic, declared form of "if a step fails, try another way" — you name the alternatives up front; nothing is discovered or searched. The alternative receives the same FlowContext as the primary, and whichever attempt succeeds stores its result under the step's name.

      Familiar analogy: a backup machine at the same station — if the primary jams, the folder is handed to the backup; only if all of them jam does the line stop.

      
       .step("reserve", ctx -> inventory.reserveFromMain(ctx.get("understand", OrderRequest.class)))
       .orElse(ctx -> inventory.reserveFromWarehouse(ctx.get("understand", OrderRequest.class), "WH-EU"))
       .orElse(ctx -> inventory.backorder(ctx.get("understand", OrderRequest.class)))
       
      Parameters:
      alternative - the fallback work to try if the current step's primary (and any earlier alternatives) throw
      Returns:
      this builder, for chaining
      Throws:
      IllegalStateException - if no step has been added yet
      IllegalArgumentException - if alternative is null
    • withEventListener

      public FlowBuilder withEventListener(EasyAIListener eventListener)
      Registers a transport-agnostic EasyAIListener that receives the flow's live EasyAIEvent stream (STARTEDSTEP_STARTED/STEP per step → FINISHED/ERROR).

      This is the same observability hook agent() uses, so a flow run can feed a log, a metric, or the TabForge demo's Activity panel with no extra plumbing. Optional — omit it and the flow runs silently.

      Parameters:
      eventListener - the listener to receive events, or null to disable emission
      Returns:
      this builder, for chaining
      See Also:
    • build

      public Flow build()
      Builds the ready-to-run Flow from the steps registered so far.
      Returns:
      a new Flow
      Throws:
      IllegalStateException - if no steps were added