I tried using the ConditionalAwaitingTool to wait for a users confirmation before a tool is executed. However it does not provide a mechanism to capture the users response via an llm. One would need to handle that manually. Usually outside of an agent process to convert the users response to an instance of ConfirmationResponse. This defeats the purpose of leveraging ai to handle the response accordingly. Here is my suggestion below. Would love to hear your thoughts on this approach:
package com.embabel.experiments.tools;
import com.embabel.agent.api.tool.DelegatingTool;
import com.embabel.agent.api.tool.Tool;
import com.embabel.agent.api.tool.ToolCallContext;
import com.embabel.agent.core.AgentProcess;
import com.embabel.agent.core.Blackboard;
import com.embabel.agent.core.hitl.AwaitableResponseException;
import com.embabel.agent.core.hitl.ConfirmationRequest;
import com.embabel.agent.core.hitl.ConfirmationResponse;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jspecify.annotations.NonNull;
import java.time.Instant;
import java.util.UUID;
public class ConfirmationAwaitingTool implements DelegatingTool {
private final Tool delegate;
private final String message;
private final ObjectMapper objectMapper;
private final Tool.Definition confirmDefinition;
public ConfirmationAwaitingTool(Tool delegate, String message, ObjectMapper objectMapper) {
this.delegate = delegate;
this.message = message;
this.objectMapper = objectMapper;
this.confirmDefinition = Tool.Definition.create(
delegate.getDefinition().getName(),
"Confirm or cancel: " + delegate.getDefinition().getDescription(),
Tool.InputSchema.of(ConfirmInput.class)
);
}
@Override
public @NonNull Tool getDelegate() {
return delegate;
}
@Override
public Tool.@NonNull Definition getDefinition() {
return isPending() ? confirmDefinition : delegate.getDefinition();
}
@Override
public Tool.@NonNull Metadata getMetadata() {
return delegate.getMetadata();
}
@Override
public Tool.@NonNull Result call(@NonNull String input, @NonNull ToolCallContext context) {
if (isPending()) {
return handleConfirmation(input, context);
}
return handleAwaiting(input, context);
}
private Tool.Result handleAwaiting(String input, ToolCallContext context) {
var agentProcess = AgentProcess.get();
if (agentProcess == null) throw new IllegalStateException("No AgentProcess available");
var blackboard = agentProcess.getBlackboard();
var lastRequest = blackboard.last(ConfirmationRequest.class);
var lastResponse = blackboard.last(ConfirmationResponse.class);
boolean alreadyConfirmed = lastRequest != null
&& lastResponse != null
&& lastResponse.getAwaitableId().equals(lastRequest.getId())
&& lastResponse.getAccepted();
if (!alreadyConfirmed) {
throw new AwaitableResponseException(
new ConfirmationRequest<>(input, message)
);
}
return delegate.call(input, context);
}
private Tool.Result handleConfirmation(String input, ToolCallContext context) {
var agentProcess = AgentProcess.get();
var blackboard = agentProcess.getBlackboard();
ConfirmationRequest pending = blackboard.last(ConfirmationRequest.class);
try {
ConfirmInput confirmInput = objectMapper.readValue(input, ConfirmInput.class);
agentProcess.plusAssign(new ConfirmationResponse(
UUID.randomUUID().toString(),
pending.getId(),
confirmInput.accepted(),
false,
Instant.now()
));
if (confirmInput.accepted()) {
return delegate.call((String) pending.getPayload(), context);
}
return Tool.Result.text("Action cancelled");
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private boolean isPending() {
var agentProcess = AgentProcess.get();
if (agentProcess == null) return false;
var pending = agentProcess.getBlackboard().last(ConfirmationRequest.class);
return pending != null
&& !isResolved(agentProcess.getBlackboard(), pending);
}
private boolean isResolved(Blackboard blackboard, ConfirmationRequest<?> request) {
var response = blackboard.last(ConfirmationResponse.class);
return response != null && response.getAwaitableId().equals(request.getId());
}
public record ConfirmInput(
@JsonPropertyDescription("whether to confirm or not")
Boolean accepted){}
}
This can then be used like below:
Tool createTask = Tool.fromFunction(
"createTask",
"Create a task.",
TaskRequest.class,
TaskResponse.class,
input -> {
log.info("Creating task: {}", input.title());
return "Task with title %s created".formatted(input.title());
}
);
Tool confirmTaskCreation = new ConfirmationAwaitingTool(createTaskRequest,
"Do you want me to proceed with this task creation?", objectMapper);
I tried using the
ConditionalAwaitingToolto wait for a users confirmation before a tool is executed. However it does not provide a mechanism to capture the users response via an llm. One would need to handle that manually. Usually outside of an agent process to convert the users response to an instance ofConfirmationResponse. This defeats the purpose of leveraging ai to handle the response accordingly. Here is my suggestion below. Would love to hear your thoughts on this approach:This can then be used like below: