CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-agentic

LangChain4j Agentic Framework provides a comprehensive Java library for building multi-agent AI systems with support for workflow orchestration, supervisor agents, planning-based execution, declarative configuration, agent-to-agent communication, and human-in-the-loop workflows.

Overview
Eval results
Files

human-in-loop.mddocs/advanced/

Human-in-the-Loop

Incorporate human feedback and approval into agent workflows for validation, guidance, and decision-making.

Overview

Human-in-the-loop enables:

  • Manual approval gates
  • Human review and editing
  • Interactive feedback loops
  • Multi-stage approval processes
  • Choice selection by humans
  • Validation with retry

Factory Method

static HumanInTheLoop.HumanInTheLoopBuilder humanInTheLoopBuilder();

Quick Start:

HumanInTheLoop hitl = AgenticServices.humanInTheLoopBuilder()
    .description("Review the generated report")
    .requestWriter(request -> System.out.println("Review: " + request))
    .responseReader(() -> new Scanner(System.in).nextLine())
    .build();

// Use in workflow
UntypedAgent workflow = AgenticServices.sequenceBuilder()
    .subAgents(generatorAgent, hitl, publisherAgent)
    .build();

Configuration

HumanInTheLoopBuilder

interface HumanInTheLoopBuilder {
    HumanInTheLoopBuilder description(String description);
    HumanInTheLoopBuilder outputKey(String outputKey);
    HumanInTheLoopBuilder async(boolean async);
    HumanInTheLoopBuilder inputKey(String inputKey);
    HumanInTheLoopBuilder requestWriter(Consumer<String> requestWriter);
    HumanInTheLoopBuilder responseReader(Supplier<String> responseReader);
    HumanInTheLoop build();
}

Request Writer

Present information to human for review:

// Simple console output
.requestWriter(request -> System.out.println("Review: " + request))

// Formatted output
.requestWriter(request -> {
    System.out.println("\n" + "=".repeat(50));
    System.out.println("HUMAN REVIEW REQUIRED");
    System.out.println("=".repeat(50));
    System.out.println(request);
    System.out.println("=".repeat(50) + "\n");
})

// File output
.requestWriter(request -> {
    String filename = "review_" + System.currentTimeMillis() + ".txt";
    Files.writeString(Path.of(filename), request);
    System.out.println("Review request: " + filename);
})

// Slack notification
.requestWriter(request -> {
    slackClient.postMessage("#approvals", "Review Required:\n" + request);
})

Response Reader

Get input from human:

// Console input
.responseReader(() -> new Scanner(System.in).nextLine())

// Multi-line input
.responseReader(() -> {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter response (type 'END' to finish):");
    StringBuilder response = new StringBuilder();
    String line;
    while (!(line = scanner.nextLine()).equals("END")) {
        response.append(line).append("\n");
    }
    return response.toString();
})

// GUI dialog
.responseReader(() -> {
    return JOptionPane.showInputDialog("Enter your response:");
})

// Database polling
.responseReader(() -> {
    while (true) {
        String response = database.checkForResponse();
        if (response != null) return response;
        Thread.sleep(5000);
    }
})

Input/Output Keys

HumanInTheLoop hitl = AgenticServices.humanInTheLoopBuilder()
    .description("Review generated article")
    .inputKey("generated_article")   // Read this from scope
    .outputKey("reviewed_article")    // Store response here
    .requestWriter(System.out::println)
    .responseReader(() -> new Scanner(System.in).nextLine())
    .build();

Async Execution

// Synchronous (blocks until response)
.async(false)

// Asynchronous (doesn't block workflow)
.async(true)
.requestWriter(request -> {
    emailService.send("reviewer@company.com", "Review", request);
})
.responseReader(() -> {
    return emailService.waitForReply("reviewer@company.com");
})

Common Patterns

Simple Approval

HumanInTheLoop approval = AgenticServices.humanInTheLoopBuilder()
    .description("Approve to continue")
    .inputKey("action_plan")
    .outputKey("approved")
    .requestWriter(plan -> {
        System.out.println("Action Plan:\n" + plan);
        System.out.print("Approve? (yes/no): ");
    })
    .responseReader(() -> new Scanner(System.in).nextLine())
    .build();

UntypedAgent workflow = AgenticServices.sequenceBuilder()
    .subAgents(
        planningAgent,
        approval,
        AgenticServices.conditionalBuilder()
            .subAgent(
                scope -> scope.readState("approved").equals("yes"),
                executionAgent
            )
            .build()
    )
    .build();

Review and Edit

HumanInTheLoop reviewEdit = AgenticServices.humanInTheLoopBuilder()
    .description("Review and edit the draft")
    .inputKey("draft")
    .outputKey("final_version")
    .requestWriter(draft -> {
        System.out.println("Draft:\n" + draft);
        System.out.println("\nEnter final version (or press Enter to keep):");
    })
    .responseReader(() -> {
        String edit = new Scanner(System.in).nextLine();
        return edit.isEmpty() ? null : edit;
    })
    .build();

UntypedAgent workflow = AgenticServices.sequenceBuilder()
    .subAgents(draftGenerator, reviewEdit)
    .output(scope -> {
        String finalVersion = (String) scope.readState("final_version");
        return finalVersion != null ? finalVersion : scope.readState("draft");
    })
    .build();

Multi-Stage Approval

// Manager approval
HumanInTheLoop managerApproval = AgenticServices.humanInTheLoopBuilder()
    .description("Manager approval")
    .inputKey("budget_request")
    .outputKey("manager_approved")
    .requestWriter(req -> emailService.send("manager@company.com", "Approval", req))
    .responseReader(() -> emailService.waitForReply("manager@company.com"))
    .async(true)
    .build();

// Director approval
HumanInTheLoop directorApproval = AgenticServices.humanInTheLoopBuilder()
    .description("Director approval")
    .inputKey("budget_request")
    .outputKey("director_approved")
    .requestWriter(req -> emailService.send("director@company.com", "Approval", req))
    .responseReader(() -> emailService.waitForReply("director@company.com"))
    .async(true)
    .build();

UntypedAgent approvalWorkflow = AgenticServices.sequenceBuilder()
    .subAgents(
        requestGenerator,
        managerApproval,
        AgenticServices.conditionalBuilder()
            .subAgent(
                scope -> scope.readState("manager_approved").equals("yes"),
                directorApproval
            )
            .build(),
        AgenticServices.conditionalBuilder()
            .subAgent(
                scope -> scope.readState("manager_approved").equals("yes") &&
                        scope.readState("director_approved").equals("yes"),
                executionAgent
            )
            .build()
    )
    .build();

Feedback Loop

HumanInTheLoop feedback = AgenticServices.humanInTheLoopBuilder()
    .description("Provide feedback")
    .inputKey("iteration_result")
    .outputKey("feedback")
    .requestWriter(result -> {
        System.out.println("Current result:\n" + result);
        System.out.print("Feedback (or 'done' to finish): ");
    })
    .responseReader(() -> new Scanner(System.in).nextLine())
    .build();

UntypedAgent iterativeWorkflow = AgenticServices.loopBuilder()
    .maxIterations(5)
    .exitCondition(scope -> {
        String feedback = (String) scope.readState("feedback");
        return "done".equalsIgnoreCase(feedback);
    })
    .subAgents(
        processingAgent,
        feedback,
        AgenticServices.conditionalBuilder()
            .subAgent(
                scope -> !scope.readState("feedback").equalsIgnoreCase("done"),
                improvementAgent
            )
            .build()
    )
    .build();

Choice Selection

HumanInTheLoop choice = AgenticServices.humanInTheLoopBuilder()
    .description("Select best option")
    .inputKey("options")
    .outputKey("selected_option")
    .requestWriter(options -> {
        System.out.println("Available options:");
        String[] opts = options.split("\n");
        for (int i = 0; i < opts.length; i++) {
            System.out.println((i + 1) + ". " + opts[i]);
        }
        System.out.print("Select option (1-" + opts.length + "): ");
    })
    .responseReader(() -> new Scanner(System.in).nextLine())
    .build();

UntypedAgent decisionWorkflow = AgenticServices.sequenceBuilder()
    .subAgents(
        optionGenerator,
        choice,
        AgenticServices.agentBuilder()
            .name("executor")
            .context(scope -> "Execute option: " + scope.readState("selected_option"))
            .build()
    )
    .build();

Validation with Retry

HumanInTheLoop validation = AgenticServices.humanInTheLoopBuilder()
    .description("Validate output")
    .inputKey("generated_output")
    .outputKey("validation_status")
    .requestWriter(output -> {
        System.out.println("Generated output:\n" + output);
        System.out.print("Valid? (yes/no): ");
    })
    .responseReader(() -> new Scanner(System.in).nextLine())
    .build();

UntypedAgent validationWorkflow = AgenticServices.loopBuilder()
    .maxIterations(3)
    .exitCondition(scope -> {
        String status = (String) scope.readState("validation_status");
        return "yes".equalsIgnoreCase(status);
    })
    .subAgents(
        generatorAgent,
        validation,
        AgenticServices.conditionalBuilder()
            .subAgent(
                scope -> scope.readState("validation_status").equalsIgnoreCase("no"),
                refinementAgent
            )
            .build()
    )
    .build();

Declarative API

Example:

interface ContentWorkflow {
    @SequenceAgent(
        name = "content-pipeline",
        subAgents = {Generator.class, HumanReview.class, Publisher.class}
    )
    String createContent(String topic);

    @HumanInTheLoopResponseSupplier
    default String getHumanReview() {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Your review: ");
        return scanner.nextLine();
    }
}

interface HumanReview {
    @HumanInTheLoop(
        name = "human-review",
        description = "Review and approve content",
        inputKey = "draft",
        outputKey = "approved_content",
        async = false
    )
    String review(AgenticScope scope);
}

ContentWorkflow workflow = AgenticServices.createAgenticSystem(
    ContentWorkflow.class,
    chatModel
);

String result = workflow.createContent("AI in Healthcare");

Integration Examples

Web UI Integration

class WebUIHumanInTheLoop {
    private final WebReviewService webService;

    public HumanInTheLoop build() {
        return AgenticServices.humanInTheLoopBuilder()
            .description("Web-based review")
            .inputKey("content")
            .outputKey("reviewed_content")
            .async(true)
            .requestWriter(request -> {
                String taskId = webService.createReviewTask(request);
                System.out.println("Review task created: " + taskId);
            })
            .responseReader(() -> {
                return webService.waitForReviewCompletion();
            })
            .build();
    }
}

Email-Based Approval

HumanInTheLoop emailApproval = AgenticServices.humanInTheLoopBuilder()
    .description("Email-based approval")
    .inputKey("proposal")
    .outputKey("decision")
    .async(true)
    .requestWriter(proposal -> {
        emailService.send(
            "approver@company.com",
            "Approval Required",
            "Proposal:\n" + proposal + "\n\nReply with APPROVE or REJECT"
        );
    })
    .responseReader(() -> {
        // Poll inbox for response
        while (true) {
            String response = emailService.checkInbox("approver@company.com");
            if (response != null &&
                (response.contains("APPROVE") || response.contains("REJECT"))) {
                return response.contains("APPROVE") ? "approved" : "rejected";
            }
            Thread.sleep(30000); // Check every 30 seconds
        }
    })
    .build();

Slack Integration

HumanInTheLoop slackApproval = AgenticServices.humanInTheLoopBuilder()
    .description("Slack approval")
    .inputKey("changes")
    .outputKey("approval_result")
    .requestWriter(changes -> {
        slackClient.postMessage(
            "#approvals",
            "Approval needed:\n" + changes + "\n\nReact with :white_check_mark: or :x:"
        );
    })
    .responseReader(() -> {
        return slackClient.waitForReaction("#approvals");
    })
    .build();

Best Practices

  1. Clear descriptions - Explain what human needs to do
  2. Structured input/output - Use consistent formats
  3. Timeout handling - Don't wait forever
  4. Async for long waits - Use async=true for email/web approvals
  5. Provide context - Include relevant information in request
  6. Validate responses - Check human input is valid
  7. Track approval history - Store decisions in scope
  8. Test manually - Ensure human interaction works as expected

See Also

  • Sequential Workflows - Integrating HITL in sequences
  • Loop Workflows - HITL in loops
  • Conditional Workflows - Conditional HITL
  • Declarative Annotations - @HumanInTheLoop annotation

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-agentic

docs

advanced

a2a.md

error-handling.md

human-in-loop.md

persistence.md

index.md

tile.json