diff --git a/src/main/java/it/cnr/isti/workflow/manager/containers/Container.java b/src/main/java/it/cnr/isti/workflow/manager/containers/Container.java index abb630e..b00e525 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/containers/Container.java +++ b/src/main/java/it/cnr/isti/workflow/manager/containers/Container.java @@ -39,8 +39,6 @@ public class Container implements FlowNode { List outputs; - List biasAnnotations = List.of(); - String laneId; ContainerConfiguration specificConfiguration; @@ -61,21 +59,27 @@ public class Container implements FlowNode { return type; } + /** + * Containers are not bias-annotatable: bias annotations live only on the + * nodes of the inner subflow and are activated via {@code includeSubflow} + * on a bias rerun. Always returns an empty list. + */ + @Override + @JsonIgnore public List getBiasAnnotations() { - return biasAnnotations == null ? List.of() : biasAnnotations; + return List.of(); } @Builder public Container(@NonNull ContainerConfiguration specificConfiguration, @Singular List inputs, @Singular List outputs, - @Singular("biasAnnotation") List biasAnnotations, T type, + T type, @JsonProperty("typeName") @JsonAlias("type") String resolvedTypeName, Position position, String laneId) { this.specificConfiguration = specificConfiguration; this.name = specificConfiguration.getName(); this.inputs = inputs; this.outputs = outputs; - this.biasAnnotations = biasAnnotations == null ? List.of() : List.copyOf(biasAnnotations); this.typeName = type != null ? type.getName() : resolvedTypeName; this.position = position; this.type = type != null ? type : resolveContainerType(this.typeName); diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsController.java index d88c102..72903d3 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsController.java +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsController.java @@ -36,7 +36,6 @@ public class BiasAnnotationsController { public record BiasAnnotationDescriptor( String type, String blockProperty, - String containerProperty, boolean multiple, int maxItems, JsonNode schema, @@ -61,7 +60,6 @@ public class BiasAnnotationsController { return new BiasAnnotationDescriptor( BlockBiasAnnotation.class.getSimpleName(), "biasAnnotations", - "biasAnnotations", true, BlockBiasAnnotation.MAX_PER_BLOCK, schemaProducer.generateSchemaNode(BlockBiasAnnotation.class), diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/ContainerBiasCapabilitiesController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/ContainerBiasCapabilitiesController.java index e559968..82d533b 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/controllers/ContainerBiasCapabilitiesController.java +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/ContainerBiasCapabilitiesController.java @@ -41,6 +41,7 @@ public class ContainerBiasCapabilitiesController { boolean fullFlowExperimentSupported, boolean externalSideEffects, boolean configurationDependent, + boolean subflowPropagationSupported, List activationModes) { } @@ -69,14 +70,19 @@ public class ContainerBiasCapabilitiesController { } private ContainerBiasCapabilityDescriptor descriptor(Container container) { + // Containers are not bias-annotatable at their boundary: they carry no + // activation modes of their own. They still participate in a full-flow + // bias rerun via subflow propagation (includeSubflow), which activates + // the executable annotations on their inner subflow nodes. List modes = adapterRegistry.supportedModes(container).stream().sorted().toList(); return new ContainerBiasCapabilityDescriptor( container.getType().getName(), - !modes.isEmpty(), + true, false, - !modes.isEmpty(), + true, false, false, + true, modes); } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/InputBiasBehaviorAdapter.java b/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/InputBiasBehaviorAdapter.java index 8901b9e..93f1734 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/InputBiasBehaviorAdapter.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/InputBiasBehaviorAdapter.java @@ -6,6 +6,7 @@ import java.util.Set; import org.springframework.stereotype.Component; +import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.executions.steps.Input; import it.cnr.isti.workflow.manager.flows.model.FlowNode; import it.cnr.isti.workflow.manager.flows.model.bias.BiasActivationMode; @@ -17,7 +18,7 @@ public class InputBiasBehaviorAdapter implements BiasBehaviorAdapter { @Override public boolean supports(FlowNode node, BiasActivationMode mode) { - return mode == BiasActivationMode.INPUT_TRANSFORMATION; + return mode == BiasActivationMode.INPUT_TRANSFORMATION && node instanceof Block; } @Override @@ -37,6 +38,6 @@ public class InputBiasBehaviorAdapter implements BiasBehaviorAdapter { @Override public Set supportedModes(FlowNode node) { - return Set.of(BiasActivationMode.INPUT_TRANSFORMATION); + return node instanceof Block ? Set.of(BiasActivationMode.INPUT_TRANSFORMATION) : Set.of(); } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/OutputBiasBehaviorAdapter.java b/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/OutputBiasBehaviorAdapter.java index f722011..618508e 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/OutputBiasBehaviorAdapter.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/bias/runtime/OutputBiasBehaviorAdapter.java @@ -4,6 +4,7 @@ import java.util.Set; import org.springframework.stereotype.Component; +import it.cnr.isti.workflow.manager.blocks.Block; import it.cnr.isti.workflow.manager.flows.model.FlowNode; import it.cnr.isti.workflow.manager.flows.model.bias.BiasActivationMode; import it.cnr.isti.workflow.manager.flows.model.bias.BlockBiasAnnotation; @@ -13,7 +14,7 @@ public class OutputBiasBehaviorAdapter implements BiasBehaviorAdapter { @Override public boolean supports(FlowNode node, BiasActivationMode mode) { - return mode == BiasActivationMode.OUTPUT_TRANSFORMATION; + return mode == BiasActivationMode.OUTPUT_TRANSFORMATION && node instanceof Block; } @Override @@ -23,6 +24,6 @@ public class OutputBiasBehaviorAdapter implements BiasBehaviorAdapter { @Override public Set supportedModes(FlowNode node) { - return Set.of(BiasActivationMode.OUTPUT_TRANSFORMATION); + return node instanceof Block ? Set.of(BiasActivationMode.OUTPUT_TRANSFORMATION) : Set.of(); } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java b/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java index 8b0f537..2714159 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/model/capabilities/NodeTypeCapabilities.java @@ -30,6 +30,8 @@ public record NodeTypeCapabilities( } public static NodeTypeCapabilities container() { - return new NodeTypeCapabilities(NodeVisualRole.CONTAINER, false, true, true, true, true, true); + // biasAnnotationsAllowed = false: containers are not bias-annotatable; + // bias lives only on the inner subflow nodes (activated via includeSubflow). + return new NodeTypeCapabilities(NodeVisualRole.CONTAINER, false, false, true, true, true, true); } } diff --git a/src/test/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsControllerTest.java b/src/test/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsControllerTest.java index 978cc85..cd3f79f 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsControllerTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/controllers/BiasAnnotationsControllerTest.java @@ -23,8 +23,6 @@ import it.cnr.isti.workflow.manager.containers.types.GenericContainerType; import it.cnr.isti.workflow.manager.controllers.BiasAnnotationsController.BiasAnnotationDescriptor; import it.cnr.isti.workflow.manager.flows.model.bias.BiasAnnotationSource; import it.cnr.isti.workflow.manager.flows.model.bias.BiasAnnotationStatus; -import it.cnr.isti.workflow.manager.flows.model.bias.BiasActivationMode; -import it.cnr.isti.workflow.manager.flows.model.bias.BiasBehavioralProbe; import it.cnr.isti.workflow.manager.flows.model.bias.BiasCategory; import it.cnr.isti.workflow.manager.flows.model.bias.BiasSeverity; import it.cnr.isti.workflow.manager.flows.model.bias.BlockBiasAnnotation; @@ -50,9 +48,6 @@ class BiasAnnotationsControllerTest { @Autowired GenericContainerFactory genericContainerFactory; - @Autowired - GenericContainerType genericContainerType; - @Autowired FlowService flowService; @@ -62,7 +57,6 @@ class BiasAnnotationsControllerTest { assertEquals(BlockBiasAnnotation.class.getSimpleName(), descriptor.type()); assertEquals("biasAnnotations", descriptor.blockProperty()); - assertEquals("biasAnnotations", descriptor.containerProperty()); assertTrue(descriptor.multiple()); assertEquals(BlockBiasAnnotation.MAX_PER_BLOCK, descriptor.maxItems()); assertEquals("PROPOSED", descriptor.defaults().get("status")); @@ -138,27 +132,24 @@ class BiasAnnotationsControllerTest { } @Test - void biasAnnotationRoundTripsWithContainerJson() throws Exception { - BlockBiasAnnotation annotation = new BlockBiasAnnotation( - null, - BiasCategory.SELECTION_BIAS, - BiasSeverity.HIGH, - "The subflow may discard relevant candidates.", - null, - "Review the subflow boundary inputs and outputs.", - BiasAnnotationStatus.CONFIRMED, - BiasAnnotationSource.MANUAL, - null, - new BiasBehavioralProbe(BiasActivationMode.INPUT_TRANSFORMATION, - "biased:${original}", java.util.List.of(), "Boundary input changes")); - Container container = annotatedContainer(annotation); + void containerDropsBiasAnnotationsFromJson() throws Exception { + // Containers are not bias-annotatable: any biasAnnotations present in + // incoming JSON must be ignored, and getBiasAnnotations() stays empty. + Container container = genericContainerFactory.create( + GenericContainerConfiguration.builder().name("Container") + .subFlow(FlowData.builder().build()).build()); + ObjectNode json = ObjectMapperHolder.mapper.valueToTree(container); + assertFalse(json.has("biasAnnotations")); + var annotations = json.putArray("biasAnnotations"); + ObjectNode annotationNode = annotations.addObject(); + annotationNode.put("category", BiasCategory.SELECTION_BIAS.name()); + annotationNode.put("severity", BiasSeverity.HIGH.name()); + annotationNode.put("issue", "Should be ignored on a container."); - String json = ObjectMapperHolder.mapper.writeValueAsString(container); - Container restored = ObjectMapperHolder.mapper.readValue(json, Container.class); + Container restored = ObjectMapperHolder.mapper.treeToValue(json, Container.class); - assertEquals(1, restored.getBiasAnnotations().size()); - assertEquals(BiasCategory.SELECTION_BIAS, restored.getBiasAnnotations().getFirst().category()); - assertEquals(annotation.id(), restored.getBiasAnnotations().getFirst().id()); + assertNotNull(restored.getBiasAnnotations()); + assertTrue(restored.getBiasAnnotations().isEmpty()); } @Test @@ -226,57 +217,6 @@ class BiasAnnotationsControllerTest { assertTrue(exception.getMessage().contains("BIAS_CATEGORY_REQUIRED")); } - @Test - void annotatedContainerIsValidatedAndPersistedInsideFlowJson() { - BlockBiasAnnotation annotation = new BlockBiasAnnotation( - null, - BiasCategory.TRANSPARENCY_RISK, - BiasSeverity.MEDIUM, - "The nested process is opaque.", - null, - "Expose the nested process rationale.", - BiasAnnotationStatus.MITIGATED, - BiasAnnotationSource.MANUAL, - null, - null); - FlowCreateRequest request = new FlowCreateRequest( - "Container bias persistence test", - "Flow with a container bias annotation", - FlowData.builder().container(annotatedContainer(annotation)).build()); - - FlowView created = flowService.createFlow("bias-container-test-user", request); - FlowView restored = flowService.getFlow(created.id(), "bias-container-test-user"); - - assertEquals(1, restored.flow().getContainers().getFirst().getBiasAnnotations().size()); - assertEquals(BiasCategory.TRANSPARENCY_RISK, - restored.flow().getContainers().getFirst().getBiasAnnotations().getFirst().category()); - } - - @Test - void flowValidationRejectsUnsupportedContainerProbe() { - BlockBiasAnnotation annotation = new BlockBiasAnnotation( - null, - BiasCategory.AUTOMATION_BIAS, - BiasSeverity.MEDIUM, - "Invalid prompt probe on a container.", - null, - null, - null, - null, - null, - new BiasBehavioralProbe(BiasActivationMode.PROMPT_DIRECTIVE, - "Prefer the initial assumption", java.util.List.of(), null)); - FlowCreateRequest request = new FlowCreateRequest( - "Invalid container probe test", - null, - FlowData.builder().container(annotatedContainer(annotation)).build()); - - RuntimeException exception = assertThrows(RuntimeException.class, - () -> flowService.createFlow("bias-container-test-user", request)); - - assertTrue(exception.getMessage().contains("BIAS_PROBE_MODE_UNSUPPORTED")); - assertTrue(exception.getMessage().contains("container")); - } private Block annotatedBlock(BlockBiasAnnotation annotation) { LLMBlockConfiguration configuration = LLMBlockConfiguration.builder() @@ -294,19 +234,4 @@ class BiasAnnotationsControllerTest { .build(); } - private Container annotatedContainer(BlockBiasAnnotation annotation) { - GenericContainerConfiguration configuration = GenericContainerConfiguration.builder() - .name("Annotated container") - .subFlow(FlowData.builder().build()) - .build(); - Container canonical = genericContainerFactory.create(configuration); - return Container.builder() - .specificConfiguration(configuration) - .inputs(canonical.getInputs()) - .outputs(canonical.getOutputs()) - .type(genericContainerType) - .position(canonical.getPosition()) - .biasAnnotation(annotation) - .build(); - } } diff --git a/src/test/java/it/cnr/isti/workflow/manager/executions/bias/BiasExperimentsIntegrationTest.java b/src/test/java/it/cnr/isti/workflow/manager/executions/bias/BiasExperimentsIntegrationTest.java index 19ec388..64d58f2 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/executions/bias/BiasExperimentsIntegrationTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/executions/bias/BiasExperimentsIntegrationTest.java @@ -37,9 +37,6 @@ import it.cnr.isti.workflow.manager.executions.ExecutionObject; import it.cnr.isti.workflow.manager.executions.ExecutionStatus; import it.cnr.isti.workflow.manager.executions.ExecutionsService; import it.cnr.isti.workflow.manager.executions.executors.NodeExecutors; -import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasBehaviorAdapterRegistry; -import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasPreparedExecution; -import it.cnr.isti.workflow.manager.executions.steps.Input; import it.cnr.isti.workflow.manager.flows.model.FlowData; import it.cnr.isti.workflow.manager.flows.model.bias.BiasActivationMode; import it.cnr.isti.workflow.manager.flows.model.bias.BiasAnnotationSource; @@ -51,8 +48,6 @@ import it.cnr.isti.workflow.manager.flows.model.bias.BlockBiasAnnotation; import it.cnr.isti.workflow.manager.llms.LLMDescriptor; import it.cnr.isti.workflow.manager.llms.providers.LLMProvider; import it.cnr.isti.workflow.manager.flows.validation.ValidationErrorCode; -import it.cnr.isti.workflow.manager.ios.IODescriptor; -import it.cnr.isti.workflow.manager.ios.IOType; @SpringBootTest @TestPropertySource(locations = "classpath:test.properties") @@ -116,9 +111,6 @@ class BiasExperimentsIntegrationTest { @Autowired ContainerBiasCapabilitiesController containerCapabilitiesController; - @Autowired - GenericContainerType genericContainerType; - @Autowired GenericContainerFactory genericContainerFactory; @@ -156,100 +148,10 @@ class BiasExperimentsIntegrationTest { assertTrue(containerCapabilities.supported()); assertFalse(containerCapabilities.isolatedExperimentSupported()); assertTrue(containerCapabilities.fullFlowExperimentSupported()); - assertEquals(List.of(BiasActivationMode.INPUT_TRANSFORMATION, BiasActivationMode.OUTPUT_TRANSFORMATION), - containerCapabilities.activationModes()); - } - - @Test - void containerBoundaryAppliesInputAndOutputTransformations() { - BlockBiasAnnotation inputAnnotation = annotation( - "container-input-bias", - new BiasBehavioralProbe(BiasActivationMode.INPUT_TRANSFORMATION, - "input:${original}", List.of("payload"), "Input is transformed")); - BlockBiasAnnotation outputAnnotation = annotation( - "container-output-bias", - new BiasBehavioralProbe(BiasActivationMode.OUTPUT_TRANSFORMATION, - "output:${original}", List.of(), "Output is transformed")); - Container container = Container.builder() - .specificConfiguration(GenericContainerConfiguration.builder() - .name("Biased boundary") - .subFlow(FlowData.builder().build()) - .build()) - .input(IODescriptor.of("payload", IOType.TEXT)) - .output(IODescriptor.of("result", IOType.TEXT)) - .type(genericContainerType) - .biasAnnotation(inputAnnotation) - .biasAnnotation(outputAnnotation) - .build(); - BiasExecutionContext context = new BiasExecutionContext( - "container-experiment", - BiasExecutionMode.BIAS_VARIANT, - Map.of(container.getId(), List.of(inputAnnotation.id(), outputAnnotation.id())), - ExternalSideEffectPolicy.BLOCK, - false); - - BiasPreparedExecution prepared = BiasBehaviorAdapterRegistry.prepare( - container, - List.of(Input.detached(container.getInputs().getFirst(), "candidate")), - Map.of(), - context, - null); - Map completed = BiasBehaviorAdapterRegistry.complete(prepared, Map.of("result", "accepted")); - - assertEquals("input:candidate", prepared.getInputs().getFirst().getValue()); - assertEquals("output:accepted", completed.get("result")); - } - - @Test - void fullFlowBiasRerunActivatesContainerAnnotation() { - BlockBiasAnnotation annotation = annotation( - "container-full-flow-bias", - new BiasBehavioralProbe(BiasActivationMode.OUTPUT_TRANSFORMATION, - "container:${original}", List.of(), "Container output is transformed")); - LLMBlockConfiguration innerConfiguration = LLMBlockConfiguration.builder() - .name("Inner decision") - .prompt("Return a decision") - .llmDescriptor(LLMDescriptor.builder() - .provider("biasExperimentProvider") - .model("bias-test-model") - .build()) - .build(); - Block innerBlock = llmBlockFactory.create(innerConfiguration); - GenericContainerConfiguration containerConfiguration = GenericContainerConfiguration.builder() - .name("Annotated process") - .subFlow(FlowData.builder().block(innerBlock).build()) - .build(); - Container canonical = genericContainerFactory.create(containerConfiguration); - Container container = Container.builder() - .specificConfiguration(containerConfiguration) - .inputs(canonical.getInputs()) - .outputs(canonical.getOutputs()) - .type(genericContainerType) - .biasAnnotation(annotation) - .build(); - ExecutionObject baseline = executionsService.createExecutionForFlow( - "container-bias-flow", - "Container bias flow", - FlowData.builder().container(container).build(), - OWNER); - executionsService.startExecution(baseline.getId()); - waitUntilFinal(baseline); - assertEquals(ExecutionStatus.SUCCESS, baseline.getContext().getStatus()); - - ExecutionObject variant = executionsService.createBiasRerun( - baseline.getId(), - OWNER, - new BiasRerunRequest( - List.of(new BiasActivation(container.getId(), List.of(annotation.id()))), - ExternalSideEffectPolicy.BLOCK, - false)); - executionsService.startExecution(variant.getId()); - waitUntilFinal(variant); - - assertEquals(ExecutionStatus.SUCCESS, variant.getContext().getStatus()); - BiasImpactReport report = biasImpactService.compareFullFlow(baseline.getId(), variant.getId(), true, OWNER); - assertTrue(report.immediateImpact().outputChanged()); - assertEquals(container.getId(), report.nodeId()); + // Containers are not bias-annotatable at their boundary: no activation + // modes of their own; bias participation is via subflow propagation. + assertTrue(containerCapabilities.subflowPropagationSupported()); + assertTrue(containerCapabilities.activationModes().isEmpty()); } @Test @@ -567,20 +469,6 @@ class BiasExperimentsIntegrationTest { return annotation(mode, instruction, targetInputs, Map.of()); } - private BlockBiasAnnotation annotation(String id, BiasBehavioralProbe probe) { - return new BlockBiasAnnotation( - id, - BiasCategory.CONFIRMATION_BIAS, - BiasSeverity.HIGH, - "The node may reinforce an initial conclusion.", - "The output can underweight contrary evidence.", - "Require independent evidence.", - BiasAnnotationStatus.CONFIRMED, - BiasAnnotationSource.MANUAL, - null, - probe); - } - private BlockBiasAnnotation annotation(BiasActivationMode mode, String instruction, List targetInputs, Map mockOutputs) { return new BlockBiasAnnotation(