Add iteration block MVP and trim outdated docs

This commit is contained in:
Lucio Lelii 2026-03-13 17:19:08 +01:00
parent 560a6ba97d
commit ef8856bc6d
11 changed files with 271 additions and 1 deletions

View File

@ -0,0 +1,37 @@
package it.cnr.isti.workflow.manager.blocks.configurations;
import com.fasterxml.jackson.annotation.JsonProperty;
import it.cnr.isti.workflow.manager.blocks.types.IterationBlockType;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
@Getter
@EqualsAndHashCode(callSuper = true)
public class IterationBlockConfiguration extends BlockConfiguration<IterationBlockType> {
@JsonProperty(required = false)
private String itemVariableName;
@Builder
public IterationBlockConfiguration(@NonNull String name, String itemVariableName) {
super(name);
this.itemVariableName = itemVariableName;
}
@Override
public Class<IterationBlockType> getBlockType() {
return IterationBlockType.class;
}
public static IterationBlockConfiguration empty() {
IterationBlockConfiguration configuration = new IterationBlockConfiguration();
configuration.name = IterationBlockType.TYPE;
configuration.itemVariableName = "item";
return configuration;
}
}

View File

@ -85,6 +85,7 @@ public interface BlockFactory<T extends BlockType, C extends BlockConfiguration<
return switch (type) {
case TEXT -> IOCapabilityType.TEXT;
case FILE, CSV -> IOCapabilityType.FILE;
case ANY -> IOCapabilityType.ANY;
};
}

View File

@ -0,0 +1,59 @@
package it.cnr.isti.workflow.manager.blocks.factories;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.IOCapability;
import it.cnr.isti.workflow.manager.blocks.IOCapabilityType;
import it.cnr.isti.workflow.manager.blocks.configurations.IterationBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.types.IterationBlockType;
import it.cnr.isti.workflow.manager.ios.IODescriptor;
import it.cnr.isti.workflow.manager.ios.IOType;
@Component
public class IterationBlockFactory implements BlockFactory<IterationBlockType, IterationBlockConfiguration> {
public static final String INPUT_NAME = "items";
public static final String OUTPUT_NAME = "results";
private static final List<IOCapability> INPUT_CAPABILITIES = List.of(
new IOCapability(IOCapabilityType.ANY, true));
private static final List<IOCapability> OUTPUT_CAPABILITIES = List.of(
new IOCapability(IOCapabilityType.ANY, true));
@Autowired
private IterationBlockType blockType;
@Override
public Block<IterationBlockType> create(IterationBlockConfiguration configuration) {
return Block.<IterationBlockType>builder()
.input(IODescriptor.input(INPUT_NAME, IOType.ANY, true, INPUT_CAPABILITIES))
.output(IODescriptor.output(OUTPUT_NAME, IOType.ANY, true, OUTPUT_CAPABILITIES))
.specificConfiguration(configuration)
.type(blockType)
.build();
}
@Override
public Block<IterationBlockType> createEmpty() {
return create(IterationBlockConfiguration.empty());
}
@Override
public Class<IterationBlockType> getBlockType() {
return IterationBlockType.class;
}
@Override
public List<IOCapability> supportedInputCapabilities() {
return INPUT_CAPABILITIES;
}
@Override
public List<IOCapability> supportedOutputCapabilities() {
return OUTPUT_CAPABILITIES;
}
}

View File

@ -0,0 +1,37 @@
package it.cnr.isti.workflow.manager.blocks.types;
import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.IterationBlockConfiguration;
@Component(IterationBlockType.TYPE)
public class IterationBlockType implements BlockType {
public static final String TYPE = "IterationBlock";
@Override
public String getName() {
return TYPE;
}
@Override
public String getDescription() {
return "Iterates over an input collection. The current MVP forwards the collection as-is and reserves nested subflow execution for a future version.";
}
@Override
public boolean validate() {
return true;
}
@Override
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
return IterationBlockConfiguration.class;
}
@Override
public boolean isUserInteractive() {
return false;
}
}

View File

@ -0,0 +1,32 @@
package it.cnr.isti.workflow.manager.executions.executors;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;
import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.factories.IterationBlockFactory;
import it.cnr.isti.workflow.manager.blocks.types.IterationBlockType;
import it.cnr.isti.workflow.manager.executions.steps.Input;
@Component
public class IterationExecutor implements BlockExecutor<IterationBlockType> {
@Override
public Map<String, Object> execute(Block<IterationBlockType> block, List<Input> inputs, Map<String, Object> context) {
if (inputs.isEmpty()) {
throw new IllegalArgumentException("IterationBlock requires the items input");
}
Object value = inputs.getFirst().getValue();
if (!(value instanceof List<?> values)) {
throw new IllegalArgumentException("IterationBlock expects a list input");
}
return Map.of(IterationBlockFactory.OUTPUT_NAME, List.copyOf(values));
}
@Override
public Class<IterationBlockType> getBlockType() {
return IterationBlockType.class;
}
}

View File

@ -98,6 +98,9 @@ public class Input {
throw new IllegalArgumentException("Input " + descriptor.getName() + " expects a file value");
}
}
case ANY -> {
// Accept any single runtime value.
}
}
}
}

View File

@ -67,6 +67,7 @@ public class IODescriptor {
return switch (type) {
case FILE, CSV -> IOCapabilityType.FILE;
case TEXT -> IOCapabilityType.TEXT;
case ANY -> IOCapabilityType.ANY;
};
}

View File

@ -6,7 +6,8 @@ import com.fasterxml.jackson.annotation.JsonValue;
public enum IOType {
TEXT,
FILE,
CSV;
CSV,
ANY;
@JsonCreator
public static IOType fromString(String key) {

View File

@ -11,13 +11,16 @@ import org.springframework.test.context.TestPropertySource;
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.IterationBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.MCPBridgeBlockConfiguration;
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
import it.cnr.isti.workflow.manager.blocks.factories.BlockFactory;
import it.cnr.isti.workflow.manager.blocks.factories.HTTPServerCallBlockFactory;
import it.cnr.isti.workflow.manager.blocks.factories.IterationBlockFactory;
import it.cnr.isti.workflow.manager.blocks.factories.LLMBlockFactory;
import it.cnr.isti.workflow.manager.blocks.factories.MCPBridgeBlockFactory;
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
import it.cnr.isti.workflow.manager.blocks.types.IterationBlockType;
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
import it.cnr.isti.workflow.manager.blocks.types.MCPBridgeBlockType;
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
@ -38,6 +41,9 @@ public class BlockTest {
@Autowired
HTTPServerCallBlockFactory httpServerCallBlockFactory;
@Autowired
IterationBlockFactory iterationBlockFactory;
@Test
void createLLMBlock() {
@ -110,4 +116,20 @@ public class BlockTest {
assertNotNull(block.getOutputs());
}
@Test
void createIterationBlock() {
BlockFactory<IterationBlockType, IterationBlockConfiguration> factory = iterationBlockFactory;
IterationBlockConfiguration config = IterationBlockConfiguration.builder()
.name("Iterate candidates")
.itemVariableName("candidate")
.build();
Block<IterationBlockType> block = factory.create(config);
assertNotNull(block);
assertNotNull(block.getSpecificConfiguration());
assertNotNull(block.getInputs());
assertNotNull(block.getOutputs());
}
}

View File

@ -19,10 +19,12 @@ import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.IOCapabilityType;
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.IterationBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.MCPBridgeBlockConfiguration;
import it.cnr.isti.workflow.manager.controllers.BlocksController.BlockConfigurationDescriptor;
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
import it.cnr.isti.workflow.manager.blocks.types.IterationBlockType;
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
import it.cnr.isti.workflow.manager.blocks.types.MCPBridgeBlockType;
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
@ -211,6 +213,44 @@ public class BlocksControllerTest {
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
}
@Test
public void getIterationExampleForType() {
Block<IterationBlockType> block = blocksController.getExampleForType(IterationBlockType.TYPE);
assertNotNull(block);
assertEquals(IterationBlockType.TYPE, block.getType().getName());
assertEquals(IterationBlockType.TYPE, block.getName());
assertNotNull(block.getSpecificConfiguration());
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("items")));
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("results")));
assertTrue(block.getInputs().stream()
.filter(input -> input.getName().equals("items"))
.findFirst()
.orElseThrow()
.isMultiple());
assertTrue(block.getInputs().stream()
.filter(input -> input.getName().equals("items"))
.findFirst()
.orElseThrow()
.getValueKinds()
.stream()
.anyMatch(capability -> capability.type() == IOCapabilityType.ANY && capability.multiple()));
}
@Test
public void createIterationBlock() {
IterationBlockConfiguration configuration = IterationBlockConfiguration.builder()
.name("Iterate")
.itemVariableName("item")
.build();
Block<IterationBlockType> block = blocksController.create(configuration);
assertNotNull(block);
assertEquals(IterationBlockType.TYPE, block.getType().getName());
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("items")));
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("results")));
}
@Test
public void httpServerCallSchemaContainsAuthorizationHints() {
BlockConfigurationDescriptor descriptor = blocksController

View File

@ -25,10 +25,13 @@ import it.cnr.isti.workflow.manager.flows.FlowTestCreator;
import it.cnr.isti.workflow.manager.flows.model.Flow;
import it.cnr.isti.workflow.manager.blocks.Block;
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.IterationBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
import it.cnr.isti.workflow.manager.blocks.factories.HTTPServerCallBlockFactory;
import it.cnr.isti.workflow.manager.blocks.factories.IterationBlockFactory;
import it.cnr.isti.workflow.manager.blocks.factories.LLMBlockFactory;
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
import it.cnr.isti.workflow.manager.blocks.types.IterationBlockType;
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
import it.cnr.isti.workflow.manager.ios.IODescriptor;
import it.cnr.isti.workflow.manager.ios.IOType;
@ -79,6 +82,9 @@ public class ExecutionTest {
@Autowired
HTTPServerCallBlockFactory httpServerCallBlockFactory;
@Autowired
IterationBlockFactory iterationBlockFactory;
LLMDescriptor llmBrick = LLMDescriptor.builder()
.provider("testProvider")
.model("testModel")
@ -207,6 +213,37 @@ public class ExecutionTest {
assertEquals(ExecutionStatus.READY, execObject.getContext().getStatus());
}
@Test
public void iterationBlockPassesThroughInputArray() {
Block<IterationBlockType> block = iterationBlockFactory.create(IterationBlockConfiguration.builder()
.name("Iterate values")
.itemVariableName("value")
.build());
FlowData flow = FlowData.builder().block(block).build();
ExecutionObject execObject = executionsService.createExecution("Iteration", flow);
assertEquals(ExecutionStatus.CREATED, execObject.getContext().getStatus());
executionsService.prepareInput(execObject.getId(), block.getId(), "items", List.of("a", "b", "c"));
execObject = executionsService.getExecution(execObject.getId());
assertEquals(ExecutionStatus.READY, execObject.getContext().getStatus());
execObject = executionsService.startExecution(execObject.getId());
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
execObject = executionsService.getExecution(execObject.getId());
}
assertTrue(execObject.getContext().getStatus().isFinalState());
Object result = execObject.getContext().getResult().values().stream().findFirst().orElseThrow();
assertEquals(List.of("a", "b", "c"), result);
}
@Test
public void singleTextInputRejectsMultipleValues() {
Flow flow = flowTestCreator.createFlowwithLLMUnpromptedWithConnection(llmBrick);