fix: let HumanDecision blocks support execution simulation
HumanDecisionExecutor explicitly disabled supportsSimulation(), so any execution containing a HumanDecision node lost the aggregate simulationAvailable flag entirely, hiding the simulate feature from clients even though the /simulate endpoint itself was untouched. simulate() now prompts the simulator LLM to pick one of the configured options (and a rationale when required), validates the choice, and returns the same output shape a real human interaction would produce. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a0ed4874d8
commit
2a349befa5
|
|
@ -3,11 +3,15 @@ package it.cnr.isti.workflow.manager.executions.executors.blocks;
|
|||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanDecisionOption;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HumanDecisionBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanDecisionBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionEventLogger;
|
||||
|
|
@ -15,10 +19,15 @@ import it.cnr.isti.workflow.manager.executions.ExecutionEventType;
|
|||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.InteractionResult;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@Component
|
||||
public class HumanDecisionExecutor implements BlockExecutor<HumanDecisionBlockType> {
|
||||
|
||||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<HumanDecisionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
|
|
@ -26,6 +35,96 @@ public class HumanDecisionExecutor implements BlockExecutor<HumanDecisionBlockTy
|
|||
throw new UnsupportedOperationException("HumanDecision blocks require user interaction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> simulate(Block<HumanDecisionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
LLMDescriptor simulatorDescriptor, ExecutionEventLogger eventLogger) {
|
||||
HumanDecisionBlockConfiguration configuration =
|
||||
(HumanDecisionBlockConfiguration) block.getSpecificConfiguration();
|
||||
if (simulatorDescriptor == null) {
|
||||
throw new IllegalArgumentException("Missing simulation descriptor for HumanDecision execution");
|
||||
}
|
||||
LLMProvider llmProvider = llmProviders.get(simulatorDescriptor.provider());
|
||||
if (llmProvider == null) {
|
||||
llmProvider = llmProviders.values().stream()
|
||||
.filter(p -> p.getName().equals(simulatorDescriptor.provider()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
if (llmProvider == null) {
|
||||
throw new IllegalArgumentException("Provider not found: " + simulatorDescriptor.provider());
|
||||
}
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey))))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + simulatorDescriptor.provider());
|
||||
}
|
||||
|
||||
String context = inputs.stream()
|
||||
.map(input -> "%s= %s".formatted(input.getDescriptor().getName(), input.getValue()))
|
||||
.collect(Collectors.joining(", "));
|
||||
String optionsDescription = configuration.getOptions().stream()
|
||||
.map(option -> "- %s: %s".formatted(option.name(), option.label()))
|
||||
.collect(Collectors.joining("\n"));
|
||||
String prompt = """
|
||||
Given the following context as input { %s }, answer this decision question: "%s"
|
||||
|
||||
Choose exactly one of the following options (use its exact name):
|
||||
%s
|
||||
|
||||
Respond in exactly this format:
|
||||
CHOICE: <option name>
|
||||
%s
|
||||
""".formatted(context, configuration.getQuestion(), optionsDescription,
|
||||
configuration.isRationaleRequired() ? "RATIONALE: <short rationale>" : "");
|
||||
|
||||
String response = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(simulatorDescriptor.model(), prompt, String.valueOf(authorizations.get(authKey)))
|
||||
: llmProvider.generate(simulatorDescriptor.model(), prompt);
|
||||
if (!StringUtils.hasText(response)) {
|
||||
throw new IllegalArgumentException("Simulated HumanDecision produced an empty response");
|
||||
}
|
||||
|
||||
String rawChoice = null;
|
||||
String rationale = null;
|
||||
for (String line : response.lines().toList()) {
|
||||
String trimmed = line.strip();
|
||||
if (trimmed.regionMatches(true, 0, "CHOICE:", 0, 7)) {
|
||||
rawChoice = trimmed.substring(7).strip();
|
||||
} else if (trimmed.regionMatches(true, 0, "RATIONALE:", 0, 10)) {
|
||||
rationale = trimmed.substring(10).strip();
|
||||
}
|
||||
}
|
||||
if (!StringUtils.hasText(rawChoice)) {
|
||||
throw new NodeDecisionException("HUMAN_DECISION_SIMULATION_FAILED",
|
||||
"Simulator did not return a choice");
|
||||
}
|
||||
String resolvedChoice = rawChoice;
|
||||
String choice = configuration.getOptions().stream()
|
||||
.map(HumanDecisionOption::name)
|
||||
.filter(name -> name.equalsIgnoreCase(resolvedChoice))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new NodeDecisionException("HUMAN_DECISION_INVALID_CHOICE",
|
||||
"Simulator returned unsupported HumanDecision choice: " + resolvedChoice));
|
||||
if (configuration.isRationaleRequired() && !StringUtils.hasText(rationale)) {
|
||||
throw new NodeDecisionException("HUMAN_DECISION_SIMULATION_FAILED",
|
||||
"Simulator did not provide a required rationale");
|
||||
}
|
||||
|
||||
if (eventLogger != null) {
|
||||
eventLogger.info(ExecutionEventType.HUMAN_DECISION_RECORDED,
|
||||
"Recorded simulated human decision",
|
||||
Map.of("choice", choice, "rationalePresent", rationale != null));
|
||||
}
|
||||
Object payload = inputs.stream()
|
||||
.filter(input -> HumanDecisionBlockFactory.INPUT_NAME.equals(input.getDescriptor().getName()))
|
||||
.findFirst()
|
||||
.map(Input::getValue)
|
||||
.orElseThrow(() -> new IllegalArgumentException("HumanDecision input is missing"));
|
||||
return Map.of(choice, payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult interact(Block<HumanDecisionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> interaction, Map<String, Object> partialResults, Map<String, Object> authorizations,
|
||||
|
|
@ -85,11 +184,6 @@ public class HumanDecisionExecutor implements BlockExecutor<HumanDecisionBlockTy
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSimulation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class NodeDecisionException extends it.cnr.isti.workflow.manager.executions.NodeExecutionException {
|
||||
private NodeDecisionException(String errorCode, String message) {
|
||||
super(errorCode, message);
|
||||
|
|
|
|||
Loading…
Reference in New Issue