Skip to content

EasyWorkflow is an extension for LangChain4j that streamlines the design of complex workflows through a fluent, DSL-inspired builder API and greatly simplifies debugging of such workflows.

Notifications You must be signed in to change notification settings

gregory-ledenev/LangChain4j-EasyWorkflow

Repository files navigation

EasyWorkflow for LangChain4j

EasyWorkflow for LangChain4j provides a fluent DSL for building complex agentic workflows on top of the LangChain4j Agentic framework. It removes boilerplate and makes it simple to express AI workflows in a clear, readable way.

On top of clean workflow design, EasyWorkflow gives you other powerful tools:

  • Workflow Debugger – Step inside your workflows with full visibility into context and agent execution results. Set breakpoints on key events, like agent input or output, to watch your workflow come alive in real time.
  • Visual Flow Diagrams – Instantly generate flowcharts for your workflows, making it easy to debug, inspect invocation results and progress, document, and illustrate agent logic at a glance.

With EasyWorkflow, you can define workflows that include sequences of agents, conditional branches, parallel execution, agent groups, and loops, combining flexibility with elegance.

NovelCreator novelCreator = EasyWorkflow.builder(NovelCreator.class)
        .chatModel(BASE_MODEL)
        .agent(CreativeWriter.class)
        .agent(AudienceEditor.class)
        .repeat(agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
            .agent(StyleScorer.class)
            .breakpoint("Score: {{score}}")
            .agent(StyleEditor.class)
        .end()
        .output(OutputComposers.asBean(Novel.class))
        .build();

Novel novel = novelCreator.createNovel("dragons and wizards", "infants", "fantasy");
System.out.println(novel);

This project is currently an incubator and is expected to be merged into the core LangChain4j framework in the future. It is evolving rapidly and may take new directions. We encourage you to explore it, try it out, and share your ideas, feedback, and suggestions with us.

Features

  • Fluent API: A simple and intuitive DSL-style API for defining complex agentic workflows.
  • Workflow Debugger: Debug agentic workflows and examine the results of their execution.
  • Sequential Execution: Define a sequence of agents that will be executed one after another.
  • Conditional Execution: Execute agents based on a condition.
  • Parallel Execution: Execute agents in parallel and compose their outputs.
  • Agent Grouping: Group agents and supervise their execution.
  • Loops: Repeat a sequence of agents till a condition evaluates to true.
  • Logging: Simple switches to turn ON agents' input and output logging.
  • Output Composers: Lots of ready-to-use output composers.

Adding to Your Build

To add EasyWorkflow to your build system, you can use the following Maven dependency:

<dependency>
    <groupId>io.github.gregory-ledenev</groupId>
    <artifactId>langchain4j-easyworkflow</artifactId>
    <version>1.0.0</version>
</dependency>

to get JavaDoc for it:

<dependency>
    <groupId>io.github.gregory-ledenev</groupId>
    <artifactId>langchain4j-easyworkflow</artifactId>
    <version>1.0.0</version>
    <classifier>javadoc</classifier>
</dependency>

How to use EasyWorkflow

The EasyWorkflow is the main entry point for creating workflows. Here’s how to use it for common tasks. To start you can use EasyWorkflow.builder(Class<?> agentClass) method to get a builder object and provide the main agentic interface. Then you can configure it, add agents and finally use the build as a terminal operation to create a configured workflow.

You may explore Building AI Workflows with EasyWorkflow for LangChain4j tutorial for a quick start.

EasyWorkflow allows you to export a workflow to JSON format for documentation or illustration purposes using the AgentWorkflowBuilder.toJson() method. In addition, you can generate an AI-powered summary of your workflow by calling AgentWorkflowBuilder.generateAISummary().

1. Basic Configuration

Before adding agents, you need to configure the workflow. At a minimum, you must provide a ChatModel. You can also provide a ChatMemory instance to maintain conversation history and specify some other properties.

// Import your chat model, e.g., OpenAiChatModel
// Import a chat memory, e.g., MessageWindowChatMemory

ExpertRouterAgent expertRouterAgent = EasyWorkflow.builder(ExpertRouterAgent.class)
        .chatModel(chatModel) // Mandatory: The chat model for the agents
        .chatMemory(chatMemory) // Optional: Shared memory for the agents
        // ... add agents and control flow here
        .build();

It is possible to set up input and output logging by using logInput(true) and logOutput(true) methods.

BeanListEveningPlannerAgent agent = EasyWorkflow.builder(BeanListEveningPlannerAgent.class)
        .chatModel(BASE_MODEL)
        .logInput(true)
        .logOutput(true)
        .doParallel()
            .agent(FoodExpert.class)
            .agent(MovieExpert.class)
        .end()
        .build();

2. Adding Agents

You can add agents to the workflow to be executed sequentially. You can add an agent by its class or by providing an already-created instance. An agent can be defined either by an interface or a class, having a single method, annotated with an @Agent annotation.

See more: Agents in LangChain4j

NovelCreator novelCreator = EasyWorkflow.builder(NovelCreator.class)
        .chatModel(BASE_MODEL)
        // Add agents by their class
        .agent(CreativeWriter.class)
        .agent(AudienceEditor.class)
        // You can also add a pre-configured agent instance
        // .agent(new MyCustomAgent())
        .build();

public interface CreativeWriter {
    @UserMessage('''
                 You are a creative writer. Generate a draft of a story no more than 3 sentences long around the
                 given topic. Return only the story and nothing else. The topic is {{topic}}.
                 ''')
    @Agent(value = "Generates a story based on the given topic", outputName = "story")
    String generateStory(@V("topic") String topic);
}

If you need to access a complete set of properties allowed by agent's builder — create an agent, providing a customizer lambda function:

SupervisorAgent supervisorAgent1 = EasyWorkflow.builder(SupervisorAgent.class)
    .chatModel(BASE_MODEL)
    .doAsGroup()
        .agent(WithdrawAgent.class, builder -> builder.agentBuilderCustomizer(bankTool))
        .agent(CreditAgent.class, builder -> builder.agentBuilderCustomizer(bankTool))
        .agent(ExchangeAgent.class, builder -> builder.agentBuilderCustomizer(new ExchangeTool()))
        .agent(humanInTheLoop)
    .end()
    .build();

3. Adding Control Flow

For more complex workflows, you can use control flow statements like ifThen, dowWhen/match, repeat, doParallel, and group. Each of these statements opens a block that must be closed with the end() method.

Here's an example combining a conditional and a loop:

ExpertRouterAgent expertRouterAgent = EasyWorkflow.builder(ExpertRouterAgent.class)
        .chatModel(BASE_MODEL)
        .agent(CategoryRouter.class)
        // Execute a block of agents only if a condition is met
        .ifThen(scope -> scope.readState("category", "UNKNOWN").equals("LEGAL"))
            .agent(LegalExpert.class)
            // You can nest control flow
            .repeat(2, scope -> scope.readState("isClear", false) == true)
                .agent(LegalClarifier.class)
            .end() // Closes the 'repeat' block
        .end() // Closes the 'ifThen' block
        .build();

You can find more detailed examples in the following sections.

Note: it is not possible to extract conditions and expressions from corresponding lambda functions for illustration or diagramming purposes. So to associate textual representation of conditions and expression with lanbda functions use

  • EasyWorkflow.condition(lambda, String) - for conditions used in ifThen and repeat
  • EasyWorkflow.expression(lambda, String)) - for expressions used in doWhen

4. Workflow Debugging

The WorkflowDebugger provides debugging functionality for agentic workflows. It allows you to:

  • Examine the workflow context, including the results of agent executions.
  • Define and handle breakpoints for events like agent input and output.

To use the debugger, create an instance of WorkflowDebugger and attach it to a workflow using the builder's debugger(...) method:

WorkflowDebugger debugger = new WorkflowDebugger();
NovelCreator novelCreator = EasyWorkflow.builder(NovelCreator.class)
        .chatModel(BASE_MODEL)
        .debugger(debugger)
        .agent(CreativeWriter.class)
        .agent(AudienceEditor.class)
        .repeat(5, agenticScope -> agenticScope.readState("score", 0.0) < 0.8)
            .agent(StyleScorer.class)
            .agent(StyleEditor.class)
        .end()
        .build();

String story = novelCreator.createNovel("dragons and wizards", "infants", "fantasy");

4.1 Examining the Workflow Context

The WorkflowDebugger captures the AgenticScope, which can be accessed with getAgenticScope() to examine the workflow's context and data. Note that the scope is only available after the workflow has started.

You can check if a workflow is running with isStarted(), get its input parameters with getWorkflowInput(), and retrieve the final result with getWorkflowResult(). If a workflow failed - you can obtain a failure via the getWorkflowFailure() method. The failure may be complex, containing lots of nested exceptions, so to get the cause use the getFailureCauseException() method.

The debugger also tracks and caches all agent invocations. You can access this history with getAgentInvocationTraceEntries() to analyze the workflow process. Each entry contains details about the agent, its input, and its output.

For a human-readable summary of the workflow execution, you can use the WorkflowDebugger.toString(true) method:

↓ IN > "audience": infants
↓ IN > "topic": dragons and wizards
↓ IN > "style": fantasy
-----------------------
      ↓ IN: UserMessage { name = null contents = [TextContent { text = "You are a creative writer..
1. ▷︎ CreativeWriter
      ↓ OUT > "story": In the mystical realm of Aethoria, where dragons soared the skies and wizards...
-----------------------
      ↓ IN: UserMessage { name = null contents = [TextContent { text = "You are a professional editor...
2. ▷︎ AudienceEditor
      ↓ OUT > "story": In a magical land, a little friend named Eryndor made a best buddy, a big, friendly...
-----------------------
...
-----------------------
◼ RESULT: Novel[story=In the enchanted realm of Aethoria, a young companion named Eryndor befriended...

4.2 Defining and Handling Breakpoints

The debugger allows you to define breakpoints that trigger custom actions. These actions can be used for logging, custom processing, or to pause execution for IDE debugging.

The available breakpoint types are defined in Breakpoint.Type:

  • AGENT_INPUT: Triggers when an agent receives input.
  • AGENT_OUTPUT: Triggers when an agent produces output.
  • SESSION_STARTED: Triggers when a workflow session starts.
  • SESSION_STOPPED: Triggers when a workflow session stops.
  • SESSION_FAILED: Triggers when a workflow session fails.
  • LINE: A procedural breakpoint that triggers when reached in the workflow definition.

For most breakpoint types, you can define them using Breakpoint.builder() and add them with WorkflowDebugger.addBreakpoint().

The following code shows how to add breakpoints to print the input for StyleScorer.class and to print the score output when it's greater than or equal to 0.2:

WorkflowDebugger debugger = new WorkflowDebugger();

debugger.addBreakpoint(Breakpoint.builder((b, ctx) -> {
            System.out.println("INPUT: " + ctx.getOrDefault(WorkflowDebugger.KEY_INPUT, null)");" +
        "})
        .agentClasses(StyleScorer.class)
        .type(WorkflowDebugger.Breakpoint.Type.INPUT)
        .build());
            
debugger.addBreakpoint(Breakpoint.builder((b, ctx) -> {
            System.out.println("SCORE: " + ctx.getOrDefault("score", 0.0)");
        })
        .outputNames("score")
        .condition(ctx -> (double) ctx.getOrDefault("score", 0.0) >= 0.2)
        .build());

LINE breakpoints are added directly into the workflow definition using the breakpoint() method on the workflow builder:

NovelCreator novelCreator = EasyWorkflow.builder(NovelCreator.class)
        .chatModel(BASE_MODEL)
        .workflowDebugger(workflowDebugger)
        .agent(CreativeWriter.class)
        .agent(AudienceEditor.class)
        .repeat(agenticScope -> agenticScope.readState("score", 0.0) < 0.8)
            .agent(StyleScorer.class)
            .breakpoint("SCORE (INLINE): {{score}}", ctx -> (double) ctx.getOrDefault("score", 0.0) >= 0.0)
            .agent(StyleEditor.class)
        .end()
        .output(OutputComposers.asBean(Novel.class))
        .build();

The BreakpointActions class provides several methods that allow creation of common utility breakpoint actions:

  • log(...) - action that logs a message using a prompt template.
  • toggleBreakpoints(...) - action that toggles the enabled state of other specified breakpoints.
  • toHtmlFile(...) - action that generates an HTML file representing the workflow execution.

4.3 Debugging Support for Non-AI Agents

It is possible to implement and use non-AI agents that are coded-manually and produce results without involving AI functionality. Unfortunately, such agents can't be tracked automatically by WofkflowDebugger so they should be adopted to support debugging. To support debugging, each agent must:

  1. Implement WorkflowDebuggerSupport interface and save provided instances of WofkflowDebugger.
  2. Call an inputReceived(...) method to notify debugger about input received.
  3. Call the outputProduced(...) method to notify debugger about output produced.
public static class QualityScorer implements WorkflowDebuggerSupport {
    private WorkflowDebugger workflowDebugger;
    
    @Agent(outputName = "quality")
    public double scoreStyle(@V("story") String story) {
        if (workflowDebugger != null) {
            inputReceived(story);
            outputProduced(0.74);
        }
        return 0.74;
    }

    @Override
    public WorkflowDebugger getWorkflowDebugger() { return workflowDebugger; }

    @Override
    public void setWorkflowDebugger(WorkflowDebugger workflowDebugger) { this.workflowDebugger = workflowDebugger; }
}

4.4 Interactive Flow Chart Diagrams

EasyWorkflow provides functionality to generate visual flowchart diagrams of agentic workflows, accompanied by the workflow results, as HTML files. These diagrams are invaluable for debugging, illustration, and documentation purposes. All agent nodes that have been invoked are highlighted with bold borders and are clickable. Clicking a node opens the Inspector, allowing you to preview the agent’s input and output.

You can create these diagrams using the WorkflowDebugger.toHtmlFile(...) method to save the diagram directly to an HTML file, or the WorkflowDebugger.toHtml() method to obtain the HTML content as a string. Generating the HTML file at breakpoints enables visualization of workflow progress. Some IDEs, such as IntelliJ IDEA, offer auto-refreshable HTML views, so opening the diagram in such a view lets you monitor progress and data in real time.

You may check a sample Expert Agentic Workflow Diagram by opening it in the browser.

4.5 Workflow Expert Agent

There is a special WorkflowExpert agent that provides an ability to chat with the debugger about workflow, its structure and properties, and about its execution results. A WorkflowExpert agent can be obtained via the WorkflowExpertSupport.getWorkflowExpert(...) method. There's also a convenience method WorkflowExpertSupport.play(...) that starts a playground session with a WorkflowExpert.

Playground

EasyWorkflow provides an interactive playground for experimenting with agents (workflows) through a familiar chat-style interface. You can type a query, send it to an agent, receive a response, and repeat the process as often as needed to test and refine your agents.

A playground is created with Playground.createPlayground(...), where you specify the agent’s class and the playground type. To start a chat session, call Playground.play(), passing the agent and optionally an initial user message.

Playground playground = Playground.createPlayground(NovelCreator.class, Playground.Type.GUI);
playground.play(novelCreator, Map.of(
        "topic", "dragons and wizards",
        "audience", "infants",
        "style", "fantasy"
));

You may specify instructions for Playground how to present and edit certain agent method parameters using !PlaygroundParam annotation.

@PlaygroundParam(description = "Story style", 
        editorType = FormEditorType.EditableDropdown, 
        editorChoices = {"comedy", "horror", "fantasy", "romance"})
@V("style")
String style);

If your workflow requires user input during execution, use an agent created via the HumanInTheLoopAgents.playgroundAgent(...). This ensures that the input will be collected automatically in a way appropriate for the selected playground type.

There are two available playground types:

Console

The console playground lets you chat with an agent directly via the terminal. If the agent expects multiple parameters, you can pass them as a semicolon-separated string.

PLAYGROUND (type your questions or arguments, or 'exit' to quit)

To enter specific arguments, delimit them with a semicolon.
Arguments:
- topic (Type: String)
- audience (Type: String)
- style (Type: String)

> {topic=dragons and wizards; audience=infants; style=fantasy}
[thinking...]
Answer: Novel[story=In the enchanted realm of Aethoria...
> 

GUI

The GUI playground offers a modern and convenient interface to allow testing the workflows using the Chat interface, inspect their structure, execution results, and summary.

By default, GUI Playground shows a simple Chat UI. To show GUI Playground in advanced mode with ability to see structure and execution results — specify a Workflow Debugger.

Playground playground = Playground.createPlayground(NovelCreator.class, 
        Playground.Type.GUI, 
        workflowDebugger);

If an agent requires multiple arguments, they are presented in a form layout for easier input. To customize form - use @PlaygroundParam annotation, where you can specify how each agent's method parameter should be rendered, its description, value choices etc.

public interface NovelCreator extends AgenticScopeOwner {
    @Agent(outputName = "story")
    Novel createNovel(
            @PlaygroundParam(editorType = FormEditorType.Note, description = "Story topic")
            @V("topic")
            String topic,

            @PlaygroundParam(description = "Story audience", editorType = FormEditorType.EditableDropdown, editorChoices = {"infants", "children", "teenagers", "adults"})
            @V("audience")
            String audience,

            @PlaygroundParam(description = "Story style", editorType = FormEditorType.EditableDropdown, editorChoices = {"comedy", "horror", "fantasy", "romance"})
            @V("style")
            String style);
}

You may check the runnable GUI playground at SampleConditionalAgentsPlayground.java

Sample for Sequential and Repeatable Agents

The following example shows how to create a sequential workflow with a repeatable block of agents. You may check the Sequential Workflow and Loop Workflow for complete samples description or check the runnable test at SampleSequentialAndRepeatableAgents.java

NovelCreator novelCreator = EasyWorkflow.builder(NovelCreator.class)
        .chatModel(BASE_MODEL)
        .agent(CreativeWriter.class)
        .agent(AudienceEditor.class)
        .repeat(5, agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
            .agent(StyleScorer.class)
            .agent(StyleEditor.class)
        .end()
        .build();

String story = novelCreator.createNovel("dragons and wizards", "infants", "fantasy");

Sample for Conditional Agents Using ifThen

The following example shows how to create a workflow with conditional execution of agents using a set of ifThen statements. You may check the Conditional Workflow for complete samples description or check the runnable test at SampleConditionalAgents.java

ExpertRouterAgent expertRouterAgent = EasyWorkflow.builder(ExpertRouterAgent.class)
        .chatModel(BASE_MODEL)
        .chatMemory(chatMemory)
        .agent(CategoryRouter.class)
        .ifThen(agenticScope -> agenticScope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.MEDICAL)
            .agent(MedicalExpert.class).end()
        .ifThen(agenticScope -> agenticScope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.LEGAL)
            .agent(LegalExpert.class).end()
        .ifThen(agenticScope -> agenticScope.readState("category", RequestCategory.UNKNOWN) == RequestCategory.TECHNICAL)
            .agent(TechnicalExpert.class).end()
        .build();

expertRouterAgent.ask("Should I sue my neighbor who caused this damage?");

Sample for Conditional Agents Using doWhen

The following example shows how to create a workflow with conditional execution of agents using a doWhen statement. You may check the Conditional Workflow for complete samples description or check the runnable test at SampleSwitchAgents.java

        ExpertRouterAgent expertRouterAgent = EasyWorkflow.builder(ExpertRouterAgent.class)
        .chatModel(BASE_MODEL)
        .chatMemory(chatMemory)
        .agent(CategoryRouter.class)
        .doWhen(agenticScope -> agenticScope.readState("category", RequestCategory.UNKNOWN))
            .match(RequestCategory.MEDICAL)
                .agent(MedicalExpert.class)
            .end()
            .match(RequestCategory.LEGAL)
                .agent(LegalExpert.class)
            .end()
            .match(RequestCategory.TECHNICAL)
                .agent(TechnicalExpert.class)
            .end()
        .end()
        .build();

expertRouterAgent.ask("How to setup a VPN?");

Sample for Parallel Agents

The following example shows how to create a workflow with parallel execution of agents. You may check the Parallel Workflow for complete samples description or check the runnable test at SampleParallelAgents.java

Function<AgenticScope, Object> resultFunction = agenticScope -> {
    List<String> movies = agenticScope.readState("movies", List.of());
    List<String> meals = agenticScope.readState("meals", List.of());

    List<EveningPlan> moviesAndMeals = new ArrayList<>();
    for (int i = 0; i < movies.size(); i++) {
        if (i >= meals.size()) {
            break;
        }
        moviesAndMeals.add(new EveningPlan(movies.get(i), meals.get(i)));
    }
    return moviesAndMeals;
};

EveningPlannerAgent eveningPlannerAgent = EasyWorkflow.builder(EveningPlannerAgent.class)
        .chatModel(BASE_MODEL)
        .doParallel(resultFunction)
            .agent(FoodExpert.class)
            .agent(MovieExpert.class)
        .end()
        .build();

eveningPlannerAgent.plan("happy");

Sample for Pure Agentic AI

The following example shows how to create a workflow with a supervised group of agents that form a pure agentic AI. You may check the Pure Agentic AI for complete samples description or check the runnable test at SampleSupervisedAgents.java

SupervisorAgent supervisorAgent1 = EasyWorkflow.builder(SupervisorAgent.class)
        .chatModel(BASE_MODEL)
        .doAsGroup()
            .agent(WithdrawAgent.class, builder -> builder.agentBuilderCustomizer(bankTool))
            .agent(CreditAgent.class, builder -> builder.agentBuilderCustomizer(bankTool))
            .agent(ExchangeAgent.class, builder -> builder.agentBuilderCustomizer(new ExchangeTool()))
            .agent(humanInTheLoop)
        .end()
        .build();

License

The EasyWorkflow for LangChain4j is licensed under the terms of the MIT License.

About

EasyWorkflow is an extension for LangChain4j that streamlines the design of complex workflows through a fluent, DSL-inspired builder API and greatly simplifies debugging of such workflows.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published