Add shared execution variables and execution-level simulation
This commit is contained in:
parent
1deeac6e6f
commit
e233d27026
|
|
@ -87,7 +87,7 @@ public class FlowAssistantPromptService {
|
|||
Rules:
|
||||
- Configure only the requested block.
|
||||
- The config object must contain only task-specific fields.
|
||||
- Do not include system-managed fields like provider, model, llmDescriptor, simulateWith, ids, inputs, outputs.
|
||||
- Do not include system-managed fields like provider, model, llmDescriptor, ids, inputs, outputs.
|
||||
- Use placeholders like ${{variable}} when needed.
|
||||
- Return valid JSON with no markdown fences.
|
||||
|
||||
|
|
|
|||
|
|
@ -286,7 +286,6 @@ public class FlowAssistantService {
|
|||
String model) {
|
||||
switch (descriptor.configurationType()) {
|
||||
case "LLMBlockConfiguration" -> config.set("llmDescriptor", llmDescriptorNode(model));
|
||||
case "HumanInteractiveBlockConfiguration" -> config.set("simulateWith", llmDescriptorNode(model));
|
||||
case "ChatInteractionBlockConfiguration" -> config.set("llmDescriptor", llmDescriptorNode(model));
|
||||
case "ConditionalBlockConfiguration" -> {
|
||||
boolean useLlm = inferConditionalUseLlm(config);
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ChatInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiUniqueItemsBy;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
|
@ -27,6 +29,11 @@ public class ChatInteractionBlockConfiguration extends BlockConfiguration<ChatIn
|
|||
@JsonProperty(required = true)
|
||||
private LLMDescriptor llmDescriptor;
|
||||
|
||||
@NotBlank
|
||||
@LongText(placeholder = "Describe the goal of the simulated chat")
|
||||
@JsonProperty(required = true)
|
||||
private String goalDescription;
|
||||
|
||||
@Structural
|
||||
@UiUniqueItemsBy("name")
|
||||
@Valid
|
||||
|
|
@ -39,9 +46,11 @@ public class ChatInteractionBlockConfiguration extends BlockConfiguration<ChatIn
|
|||
|
||||
@Builder
|
||||
public ChatInteractionBlockConfiguration(@NonNull String name, LLMDescriptor llmDescriptor,
|
||||
String goalDescription,
|
||||
List<ChatInteractionInput> inputs) {
|
||||
super(name);
|
||||
this.llmDescriptor = llmDescriptor;
|
||||
this.goalDescription = goalDescription;
|
||||
this.inputs = inputs == null ? List.of() : List.copyOf(inputs);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiDependency;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiEnabledWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiRequiredWhen;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
|
|
@ -22,7 +22,7 @@ import lombok.NonNull;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ConditionalBlockConfiguration extends BlockConfiguration<ConditionalBlockType> {
|
||||
|
||||
@UiDependency(field = "useLlm", equals = "false")
|
||||
@UiEnabledWhen(field = "useLlm", equals = "false")
|
||||
@Structural
|
||||
@LongText(
|
||||
placeholder = "Add a deterministic condition like ${{amount}} > 1000",
|
||||
|
|
@ -35,11 +35,11 @@ public class ConditionalBlockConfiguration extends BlockConfiguration<Conditiona
|
|||
boolean useLlm;
|
||||
|
||||
@Valid
|
||||
@UiDependency(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
LLMDescriptor llmDescriptor;
|
||||
|
||||
@UiDependency(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
@Structural
|
||||
@LongText(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiDependency;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiEnabledWhen;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
|
@ -28,7 +28,7 @@ public class HTTPServerCallBlockConfiguration extends BlockConfiguration<HTTPSer
|
|||
private HTTPMethod method;
|
||||
|
||||
@Structural
|
||||
@UiDependency(field = "method", equalsAny = { "POST", "PUT" })
|
||||
@UiEnabledWhen(field = "method", equalsAny = { "POST", "PUT" })
|
||||
@LongText(
|
||||
placeholder = "Optional HTTP request body.",
|
||||
tip = "Use ${{}} placeholders to inject workflow variables into the URL or request body.",
|
||||
|
|
@ -41,11 +41,11 @@ public class HTTPServerCallBlockConfiguration extends BlockConfiguration<HTTPSer
|
|||
private Boolean requiresAuthorization;
|
||||
|
||||
@NotNull
|
||||
@UiDependency(field = "requiresAuthorization", equals = "true", group = "authorization")
|
||||
@UiEnabledWhen(field = "requiresAuthorization", equals = "true", group = "authorization")
|
||||
@JsonProperty(required = true)
|
||||
private HTTPAuthorizationType authorizationType;
|
||||
|
||||
@UiDependency(field = "authorizationType", equals = "API_KEY", group = "authorization")
|
||||
@UiEnabledWhen(field = "authorizationType", equals = "API_KEY", group = "authorization")
|
||||
@JsonProperty(required = false)
|
||||
private String authorizationHeaderName;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
|
@ -25,22 +22,15 @@ public class HumanInteractiveBlockConfiguration extends BlockConfiguration<Human
|
|||
@JsonProperty(required = true)
|
||||
private String actionDescription;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty(required = true)
|
||||
private LLMDescriptor simulateWith;
|
||||
|
||||
@Override
|
||||
public Class<HumanInteractionBlockType> getBlockType() {
|
||||
return HumanInteractionBlockType.class;
|
||||
}
|
||||
|
||||
@Builder
|
||||
public HumanInteractiveBlockConfiguration(@NonNull String name, @NonNull String actionDescription,
|
||||
@NonNull LLMDescriptor simulateWith) {
|
||||
public HumanInteractiveBlockConfiguration(@NonNull String name, @NonNull String actionDescription) {
|
||||
super(name);
|
||||
this.actionDescription = actionDescription;
|
||||
this.simulateWith = simulateWith;
|
||||
}
|
||||
|
||||
public static HumanInteractiveBlockConfiguration empty() {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
|
|||
import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiDependency;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.DynamicSchema;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.ConfigurableAsInput;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.SchemaAllowedValues;
|
||||
|
|
@ -56,7 +55,6 @@ public class JsonSchemaProducer {
|
|||
Map<Class<?>, Map<String, DynamicSchema>> dynamicSchemaMap = collectDynamicSchemaMetadata(type);
|
||||
Map<Class<?>, Map<String, LongText>> longTextMap = collectLongTextMetadata(type);
|
||||
Map<Class<?>, Map<String, Structural>> structuralMap = collectStructuralMetadata(type);
|
||||
Map<Class<?>, Map<String, UiDependency>> uiDependencyMap = collectUiDependencyMetadata(type);
|
||||
Map<Class<?>, Map<String, UiEnabledWhen>> uiEnabledWhenMap = collectUiEnabledWhenMetadata(type);
|
||||
Map<Class<?>, Map<String, UiOptionsFromNode>> uiOptionsFromNodeMap = collectUiOptionsFromNodeMetadata(type);
|
||||
Map<Class<?>, Map<String, UiRequiredWhen>> uiRequiredWhenMap = collectUiRequiredWhenMetadata(type);
|
||||
|
|
@ -70,7 +68,6 @@ public class JsonSchemaProducer {
|
|||
applyDynamicSchemaMetadata(root, dynamicSchemaMap.getOrDefault(type, Map.of()));
|
||||
applyLongTextMetadata(root, longTextMap.getOrDefault(type, Map.of()));
|
||||
applyStructuralMetadata(root, structuralMap.getOrDefault(type, Map.of()));
|
||||
applyUiDependencyMetadata(root, uiDependencyMap.getOrDefault(type, Map.of()));
|
||||
applyUiEnabledWhenMetadata(root, uiEnabledWhenMap.getOrDefault(type, Map.of()));
|
||||
applyUiOptionsFromNodeMetadata(root, uiOptionsFromNodeMap.getOrDefault(type, Map.of()));
|
||||
applyUiRequiredWhenMetadata(root, uiRequiredWhenMap.getOrDefault(type, Map.of()));
|
||||
|
|
@ -87,7 +84,6 @@ public class JsonSchemaProducer {
|
|||
metadataClasses.addAll(dynamicSchemaMap.keySet());
|
||||
metadataClasses.addAll(longTextMap.keySet());
|
||||
metadataClasses.addAll(structuralMap.keySet());
|
||||
metadataClasses.addAll(uiDependencyMap.keySet());
|
||||
metadataClasses.addAll(uiEnabledWhenMap.keySet());
|
||||
metadataClasses.addAll(uiOptionsFromNodeMap.keySet());
|
||||
metadataClasses.addAll(uiRequiredWhenMap.keySet());
|
||||
|
|
@ -107,7 +103,6 @@ public class JsonSchemaProducer {
|
|||
applyDynamicSchemaMetadata(classSchema, dynamicSchemaMap.get(matchedClass));
|
||||
applyLongTextMetadata(classSchema, longTextMap.get(matchedClass));
|
||||
applyStructuralMetadata(classSchema, structuralMap.get(matchedClass));
|
||||
applyUiDependencyMetadata(classSchema, uiDependencyMap.get(matchedClass));
|
||||
applyUiEnabledWhenMetadata(classSchema, uiEnabledWhenMap.get(matchedClass));
|
||||
applyUiOptionsFromNodeMetadata(classSchema, uiOptionsFromNodeMap.get(matchedClass));
|
||||
applyUiRequiredWhenMetadata(classSchema, uiRequiredWhenMap.get(matchedClass));
|
||||
|
|
@ -556,45 +551,6 @@ public class JsonSchemaProducer {
|
|||
}
|
||||
}
|
||||
|
||||
private Map<Class<?>, Map<String, UiDependency>> collectUiDependencyMetadata(Class<?> rootClass) {
|
||||
Map<Class<?>, Map<String, UiDependency>> result = new HashMap<>();
|
||||
Set<Class<?>> visited = new HashSet<>();
|
||||
Queue<Class<?>> queue = new ArrayDeque<>();
|
||||
queue.add(rootClass);
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
Class<?> current = queue.poll();
|
||||
if (current == null || !visited.add(current) || isTerminalType(current)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, UiDependency> metadata = new LinkedHashMap<>();
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
UiDependency uiDependency = field.getAnnotation(UiDependency.class);
|
||||
if (uiDependency != null) {
|
||||
metadata.put(field.getName(), uiDependency);
|
||||
}
|
||||
enqueueRelatedTypes(queue, field.getGenericType(), field.getType());
|
||||
}
|
||||
|
||||
if (current.isRecord()) {
|
||||
for (RecordComponent component : current.getRecordComponents()) {
|
||||
UiDependency uiDependency = component.getAnnotation(UiDependency.class);
|
||||
if (uiDependency != null) {
|
||||
metadata.put(component.getName(), uiDependency);
|
||||
}
|
||||
enqueueRelatedTypes(queue, component.getGenericType(), component.getType());
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata.isEmpty()) {
|
||||
result.put(current, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Class<?>, Map<String, UiUniqueItemsBy>> collectUiUniqueItemsByMetadata(Class<?> rootClass) {
|
||||
Map<Class<?>, Map<String, UiUniqueItemsBy>> result = new HashMap<>();
|
||||
Set<Class<?>> visited = new HashSet<>();
|
||||
|
|
@ -634,39 +590,6 @@ public class JsonSchemaProducer {
|
|||
return result;
|
||||
}
|
||||
|
||||
private void applyUiDependencyMetadata(ObjectNode classSchema, Map<String, UiDependency> metadata) {
|
||||
if (metadata == null || metadata.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
JsonNode propsNode = classSchema.get("properties");
|
||||
if (!(propsNode instanceof ObjectNode properties)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Entry<String, UiDependency> entry : metadata.entrySet()) {
|
||||
JsonNode propNode = properties.get(entry.getKey());
|
||||
if (!(propNode instanceof ObjectNode propertySchema)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UiDependency dependency = entry.getValue();
|
||||
if (!dependency.group().isBlank()) {
|
||||
propertySchema.put("x-ui-group", dependency.group());
|
||||
}
|
||||
ObjectNode visibleWhen = propertySchema.putObject("x-ui-visible-when");
|
||||
visibleWhen.put("field", dependency.field());
|
||||
if (!dependency.equals().isBlank()) {
|
||||
visibleWhen.put("equals", dependency.equals());
|
||||
}
|
||||
if (dependency.equalsAny().length > 0) {
|
||||
ArrayNode equalsAny = visibleWhen.putArray("in");
|
||||
for (String value : dependency.equalsAny()) {
|
||||
equalsAny.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyUiUniqueItemsByMetadata(ObjectNode classSchema, Map<String, UiUniqueItemsBy> metadata) {
|
||||
if (metadata == null || metadata.isEmpty()) {
|
||||
return;
|
||||
|
|
@ -926,6 +849,9 @@ public class JsonSchemaProducer {
|
|||
}
|
||||
|
||||
UiEnabledWhen dependency = entry.getValue();
|
||||
if (!dependency.group().isBlank()) {
|
||||
propertySchema.put("x-ui-group", dependency.group());
|
||||
}
|
||||
ObjectNode enabledWhen = propertySchema.putObject("x-ui-enabled-when");
|
||||
enabledWhen.put("field", dependency.field());
|
||||
if (!dependency.equals().isBlank()) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
|||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.DynamicSchema;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.ConfigurableAsInput;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiContextKeys;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiEnabledWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiRequiredWhen;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
|
@ -27,6 +30,7 @@ public class MCPAgentBlockConfiguration extends BlockConfiguration<MCPAgentBlock
|
|||
|
||||
@Structural
|
||||
@ConfigurableAsInput
|
||||
@UiEnabledWhen(field = "useSharedSession", equals = "false")
|
||||
@FieldRetriever(name = "LLM", url = "/retriever/LLM/models?provider=InternalOllama")
|
||||
@JsonProperty(required = false)
|
||||
private String model;
|
||||
|
|
@ -41,14 +45,43 @@ public class MCPAgentBlockConfiguration extends BlockConfiguration<MCPAgentBlock
|
|||
|
||||
@JsonProperty(required = false)
|
||||
@Valid
|
||||
@UiEnabledWhen(field = "useSharedSession", equals = "false")
|
||||
private List<MCPServerBinding> mcpServers;
|
||||
|
||||
@Structural
|
||||
@JsonProperty(required = false)
|
||||
private Boolean shareSession;
|
||||
|
||||
@Structural
|
||||
@UiEnabledWhen(field = "shareSession", equals = "true")
|
||||
@UiRequiredWhen(field = "shareSession", equals = "true")
|
||||
@JsonProperty(required = false)
|
||||
private String sharedSessionName;
|
||||
|
||||
@Structural
|
||||
@UiEnabledWhen(field = "shareSession", equals = "false")
|
||||
@JsonProperty(required = false)
|
||||
private Boolean useSharedSession;
|
||||
|
||||
@Structural
|
||||
@UiEnabledWhen(field = "useSharedSession", equals = "true")
|
||||
@UiRequiredWhen(field = "useSharedSession", equals = "true")
|
||||
@FieldRetriever(name = "ExecutionVariables", url = "/secure-retriever/ExecutionVariables/shared/items?kind=MCP_SESSION",
|
||||
dependsOn = {UiContextKeys.FLOW_ID}, requiresAuth = true)
|
||||
@JsonProperty(required = false)
|
||||
private String sharedSessionRef;
|
||||
|
||||
@Builder
|
||||
public MCPAgentBlockConfiguration(@NonNull String name, String model, String prompt, List<MCPServerBinding> mcpServers) {
|
||||
public MCPAgentBlockConfiguration(@NonNull String name, String model, String prompt, List<MCPServerBinding> mcpServers,
|
||||
Boolean shareSession, String sharedSessionName, Boolean useSharedSession, String sharedSessionRef) {
|
||||
super(name);
|
||||
this.model = model;
|
||||
this.prompt = prompt;
|
||||
this.mcpServers = mcpServers == null ? List.of() : List.copyOf(mcpServers);
|
||||
this.shareSession = shareSession;
|
||||
this.sharedSessionName = sharedSessionName;
|
||||
this.useSharedSession = useSharedSession;
|
||||
this.sharedSessionRef = sharedSessionRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -60,6 +93,8 @@ public class MCPAgentBlockConfiguration extends BlockConfiguration<MCPAgentBlock
|
|||
MCPAgentBlockConfiguration configuration = new MCPAgentBlockConfiguration();
|
||||
configuration.name = MCPAgentBlockType.TYPE;
|
||||
configuration.mcpServers = List.of();
|
||||
configuration.shareSession = false;
|
||||
configuration.useSharedSession = false;
|
||||
return configuration;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import it.cnr.isti.workflow.manager.blocks.types.MCPAgentChatBlockType;
|
|||
import it.cnr.isti.workflow.manager.configurations.annotations.ConfigurableAsInput;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.DynamicSchema;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiEnabledWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiContextKeys;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiRequiredWhen;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
|
@ -26,6 +30,7 @@ public class MCPAgentChatBlockConfiguration extends BlockConfiguration<MCPAgentC
|
|||
|
||||
@Structural
|
||||
@ConfigurableAsInput
|
||||
@UiEnabledWhen(field = "useSharedSession", equals = "false")
|
||||
@FieldRetriever(name = "LLM", url = "/retriever/LLM/models?provider=InternalOllama")
|
||||
@JsonProperty(required = false)
|
||||
private String model;
|
||||
|
|
@ -34,17 +39,53 @@ public class MCPAgentChatBlockConfiguration extends BlockConfiguration<MCPAgentC
|
|||
@Valid
|
||||
private List<ChatInteractionInput> inputs = List.of();
|
||||
|
||||
@NotBlank
|
||||
@LongText(placeholder = "Describe the goal of the simulated chat")
|
||||
@JsonProperty(required = true)
|
||||
private String goalDescription;
|
||||
|
||||
@JsonProperty(required = false)
|
||||
@Valid
|
||||
@UiEnabledWhen(field = "useSharedSession", equals = "false")
|
||||
private List<MCPServerBinding> mcpServers = List.of();
|
||||
|
||||
@Structural
|
||||
@JsonProperty(required = false)
|
||||
private Boolean shareSession;
|
||||
|
||||
@Structural
|
||||
@UiEnabledWhen(field = "shareSession", equals = "true")
|
||||
@UiRequiredWhen(field = "shareSession", equals = "true")
|
||||
@JsonProperty(required = false)
|
||||
private String sharedSessionName;
|
||||
|
||||
@Structural
|
||||
@UiEnabledWhen(field = "shareSession", equals = "false")
|
||||
@JsonProperty(required = false)
|
||||
private Boolean useSharedSession;
|
||||
|
||||
@Structural
|
||||
@UiEnabledWhen(field = "useSharedSession", equals = "true")
|
||||
@UiRequiredWhen(field = "useSharedSession", equals = "true")
|
||||
@FieldRetriever(name = "ExecutionVariables", url = "/secure-retriever/ExecutionVariables/shared/items?kind=MCP_SESSION",
|
||||
dependsOn = {UiContextKeys.FLOW_ID}, requiresAuth = true)
|
||||
@JsonProperty(required = false)
|
||||
private String sharedSessionRef;
|
||||
|
||||
@Builder
|
||||
public MCPAgentChatBlockConfiguration(@NonNull String name, String model, List<ChatInteractionInput> inputs,
|
||||
List<MCPServerBinding> mcpServers) {
|
||||
String goalDescription,
|
||||
List<MCPServerBinding> mcpServers,
|
||||
Boolean shareSession, String sharedSessionName, Boolean useSharedSession, String sharedSessionRef) {
|
||||
super(name);
|
||||
this.model = model;
|
||||
this.inputs = inputs == null ? List.of() : List.copyOf(inputs);
|
||||
this.goalDescription = goalDescription;
|
||||
this.mcpServers = mcpServers == null ? List.of() : List.copyOf(mcpServers);
|
||||
this.shareSession = shareSession;
|
||||
this.sharedSessionName = sharedSessionName;
|
||||
this.useSharedSession = useSharedSession;
|
||||
this.sharedSessionRef = sharedSessionRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -57,6 +98,8 @@ public class MCPAgentChatBlockConfiguration extends BlockConfiguration<MCPAgentC
|
|||
configuration.name = MCPAgentChatBlockType.TYPE;
|
||||
configuration.inputs = List.of();
|
||||
configuration.mcpServers = List.of();
|
||||
configuration.shareSession = false;
|
||||
configuration.useSharedSession = false;
|
||||
return configuration;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import java.util.Objects;
|
|||
|
||||
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.IOCapability;
|
||||
|
|
@ -84,7 +85,10 @@ public class ChatInteractionBlockFactory
|
|||
}
|
||||
|
||||
private void validateConfiguration(ChatInteractionBlockConfiguration configuration) {
|
||||
if (configuration == null || configuration.getInputs() == null || configuration.getInputs().isEmpty()) {
|
||||
if (configuration == null) {
|
||||
return;
|
||||
}
|
||||
if (configuration.getInputs() == null || configuration.getInputs().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long distinctNames = configuration.getInputs().stream()
|
||||
|
|
|
|||
|
|
@ -75,7 +75,10 @@ public class ConditionalBlockFactory implements BlockFactory<ConditionalBlockTyp
|
|||
}
|
||||
Matcher matcher = pattern.matcher(source);
|
||||
while (matcher.find()) {
|
||||
target.add(matcher.group(group));
|
||||
String placeholder = matcher.group(group);
|
||||
if (!PlaceholderInputs.isRuntimeExecutionVariable(placeholder)) {
|
||||
target.add(placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,10 @@ public class HTTPServerCallBlockFactory implements BlockFactory<HTTPServerCallBl
|
|||
}
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
|
||||
while (matcher.find()) {
|
||||
names.add(matcher.group(1));
|
||||
String placeholder = matcher.group(1);
|
||||
if (!PlaceholderInputs.isRuntimeExecutionVariable(placeholder)) {
|
||||
names.add(placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,12 @@ public class LLMBlockFactory implements BlockFactory<LLMBlockType, LLMBlockConfi
|
|||
Matcher matcher = pattern.matcher(input);
|
||||
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
while (matcher.find())
|
||||
names.add(matcher.group(1));
|
||||
while (matcher.find()) {
|
||||
String placeholder = matcher.group(1);
|
||||
if (!PlaceholderInputs.isRuntimeExecutionVariable(placeholder)) {
|
||||
names.add(placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,9 +34,12 @@ public class MCPAgentBlockFactory implements BlockFactory<MCPAgentBlockType, MCP
|
|||
|
||||
@Override
|
||||
public Block<MCPAgentBlockType> create(MCPAgentBlockConfiguration configuration) {
|
||||
validateConfiguration(configuration);
|
||||
List<IODescriptor> inputs = Stream.concat(
|
||||
retrieveInputs(configuration.getPrompt()).stream(),
|
||||
configurableInputDescriptors(configuration).stream())
|
||||
configurableInputDescriptors(configuration).stream()
|
||||
.filter(descriptor -> !Boolean.TRUE.equals(configuration.getUseSharedSession())
|
||||
|| !"model".equals(descriptor.getName())))
|
||||
.distinct()
|
||||
.toList();
|
||||
return Block.<MCPAgentBlockType>builder()
|
||||
|
|
@ -64,7 +67,10 @@ public class MCPAgentBlockFactory implements BlockFactory<MCPAgentBlockType, MCP
|
|||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(prompt);
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
while (matcher.find()) {
|
||||
names.add(matcher.group(1));
|
||||
String placeholder = matcher.group(1);
|
||||
if (!PlaceholderInputs.isRuntimeExecutionVariable(placeholder)) {
|
||||
names.add(placeholder);
|
||||
}
|
||||
}
|
||||
return names.stream().map(name -> IODescriptor.input(name, IOType.TEXT, false, INPUT_CAPABILITIES)).toList();
|
||||
}
|
||||
|
|
@ -78,4 +84,9 @@ public class MCPAgentBlockFactory implements BlockFactory<MCPAgentBlockType, MCP
|
|||
public List<IOCapability> supportedOutputCapabilities() {
|
||||
return OUTPUT_CAPABILITIES;
|
||||
}
|
||||
|
||||
private void validateConfiguration(MCPAgentBlockConfiguration configuration) {
|
||||
// Factory creation must allow partial draft configurations.
|
||||
// Executability is validated later at flow-validation/runtime level.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import java.util.Objects;
|
|||
|
||||
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.IOCapability;
|
||||
|
|
@ -70,7 +71,11 @@ public class MCPAgentChatBlockFactory
|
|||
}
|
||||
|
||||
private List<IODescriptor> resolveInputs(MCPAgentChatBlockConfiguration configuration) {
|
||||
java.util.ArrayList<IODescriptor> inputs = new java.util.ArrayList<>(configurableInputDescriptors(configuration));
|
||||
java.util.ArrayList<IODescriptor> inputs = new java.util.ArrayList<>(
|
||||
configurableInputDescriptors(configuration).stream()
|
||||
.filter(descriptor -> !Boolean.TRUE.equals(configuration.getUseSharedSession())
|
||||
|| !MODEL_INPUT.equals(descriptor.getName()))
|
||||
.toList());
|
||||
if (configuration.getInputs() != null) {
|
||||
inputs.addAll(configuration.getInputs().stream().map(this::toDescriptor).toList());
|
||||
}
|
||||
|
|
@ -84,7 +89,10 @@ public class MCPAgentChatBlockFactory
|
|||
}
|
||||
|
||||
private void validateConfiguration(MCPAgentChatBlockConfiguration configuration) {
|
||||
if (configuration == null || configuration.getInputs() == null || configuration.getInputs().isEmpty()) {
|
||||
if (configuration == null) {
|
||||
return;
|
||||
}
|
||||
if (configuration.getInputs() == null || configuration.getInputs().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long distinctNames = configuration.getInputs().stream()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package it.cnr.isti.workflow.manager.blocks.factories;
|
||||
|
||||
final class PlaceholderInputs {
|
||||
|
||||
private PlaceholderInputs() {
|
||||
}
|
||||
|
||||
static boolean isRuntimeExecutionVariable(String placeholder) {
|
||||
return placeholder != null && placeholder.startsWith("vars.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package it.cnr.isti.workflow.manager.configurations.annotations;
|
||||
|
||||
public final class UiContextKeys {
|
||||
|
||||
public static final String FLOW_ID = "$context.flowId";
|
||||
public static final String BLOCK_ID = "$context.blockId";
|
||||
public static final String INPUT_NAMES = "$context.inputNames";
|
||||
public static final String OUTPUT_NAMES = "$context.outputNames";
|
||||
|
||||
private UiContextKeys() {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package it.cnr.isti.workflow.manager.configurations.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface UiDependency {
|
||||
String field();
|
||||
|
||||
String equals() default "";
|
||||
|
||||
String[] equalsAny() default {};
|
||||
|
||||
String group() default "";
|
||||
}
|
||||
|
|
@ -15,4 +15,6 @@ public @interface UiEnabledWhen {
|
|||
String[] equalsAny() default {};
|
||||
|
||||
boolean present() default false;
|
||||
|
||||
String group() default "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package it.cnr.isti.workflow.manager.configurations.retrievers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableKind;
|
||||
import it.cnr.isti.workflow.manager.executions.design.FlowSharedVariableCatalogService;
|
||||
|
||||
@Component
|
||||
public class ExecutionVariablesFieldRetriever implements SecureDynamicFieldRetriever {
|
||||
|
||||
private final FlowSharedVariableCatalogService catalogService;
|
||||
|
||||
public ExecutionVariablesFieldRetriever(FlowSharedVariableCatalogService catalogService) {
|
||||
this.catalogService = catalogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "ExecutionVariables";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RetrieverItem> retrieve(String parameter, Map<String, String> params, LoginEntity user) {
|
||||
if (!"shared".equals(parameter)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
||||
"Unknown ExecutionVariables retriever parameter: " + parameter);
|
||||
}
|
||||
String flowId = params.get("flowId");
|
||||
String kindParam = params.get("kind");
|
||||
ExecutionVariableKind kind = kindParam == null || kindParam.isBlank()
|
||||
? null
|
||||
: ExecutionVariableKind.valueOf(kindParam.trim().toUpperCase());
|
||||
return catalogService.listForFlow(flowId, user.getUsername(), kind).stream()
|
||||
.map(definition -> new RetrieverItem(
|
||||
new RetrieverItemDescriptor(
|
||||
definition.name(),
|
||||
"Shared variable exposed by " + definition.producerBlockName(),
|
||||
Map.of(
|
||||
"kind", definition.kind().name(),
|
||||
"producerBlockId", definition.producerBlockId(),
|
||||
"producerBlockName", definition.producerBlockName())),
|
||||
definition.name(),
|
||||
false,
|
||||
true,
|
||||
List.of()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.JsonSchemaProducer;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.BlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.design.FlowSharedVariableCatalogService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
|
@ -40,6 +42,9 @@ public class BlocksController {
|
|||
@Autowired
|
||||
JsonSchemaProducer schemaProducer;
|
||||
|
||||
@Autowired
|
||||
FlowSharedVariableCatalogService flowSharedVariableCatalogService;
|
||||
|
||||
public record BlockConfigurationDescriptor(
|
||||
String type,
|
||||
String description,
|
||||
|
|
@ -167,11 +172,23 @@ public class BlocksController {
|
|||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends BlockType, C extends BlockConfiguration<T>> Block<T> create(C blockConfiguration) {
|
||||
return create(blockConfiguration, null, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends BlockType, C extends BlockConfiguration<T>> Block<T> create(C blockConfiguration, String flowId) {
|
||||
return create(blockConfiguration, flowId, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@PostMapping
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@Operation(summary = "create block", description = "Creates a block from the provided block configuration.")
|
||||
public <T extends BlockType, C extends BlockConfiguration<T>> Block<T> create(@RequestBody C blockConfiguration) {
|
||||
public <T extends BlockType, C extends BlockConfiguration<T>> Block<T> create(@RequestBody C blockConfiguration,
|
||||
@RequestParam(required = false) String flowId,
|
||||
@RequestParam(required = false) String replacesBlockId) {
|
||||
logger.debug("Creating block of type: {}", blockConfiguration.getBlockType().getName());
|
||||
BlockFactory<T, C> factory = (BlockFactory<T, C>) blockFactories.stream()
|
||||
.filter(f -> f.getBlockType().equals(blockConfiguration.getBlockType()))
|
||||
|
|
@ -179,6 +196,7 @@ public class BlocksController {
|
|||
.orElseThrow(() -> new IllegalArgumentException("Block factory not found for type: " + blockConfiguration.getBlockType()));
|
||||
|
||||
Block<T> block = factory.create(blockConfiguration);
|
||||
flowSharedVariableCatalogService.registerDraftBlock(flowId, block, replacesBlockId);
|
||||
String blockAsJson = null;
|
||||
try {
|
||||
blockAsJson = ObjectMapperHolder.mapper.writeValueAsString(block);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||
import it.cnr.isti.workflow.manager.executions.ExecutionContext;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionObject;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionAuthorizationValueRequest;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionSimulationRequest;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionsService;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowEntity;
|
||||
import it.cnr.isti.workflow.manager.flows.repo.FlowRepository;
|
||||
|
|
@ -175,6 +176,13 @@ public class ExecutionsController {
|
|||
return executionService.startExecution(id);
|
||||
}
|
||||
|
||||
@PutMapping(path = "{id}/simulate")
|
||||
@Operation(summary = "Starts an execution in simulation mode",
|
||||
description = "Starts the execution and enables simulation for interactive nodes.")
|
||||
public ExecutionObject simulate(@PathVariable String id, @RequestBody @jakarta.validation.Valid ExecutionSimulationRequest request) {
|
||||
return executionService.startSimulationExecution(id, request.simulator());
|
||||
}
|
||||
|
||||
@PutMapping(path = "{id}/resume")
|
||||
@Operation(summary = "Resumes an execution", description = "Rebuilds a persisted execution and resumes it when possible.")
|
||||
public ExecutionObject resume(@PathVariable String id) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import it.cnr.isti.workflow.manager.executions.persistence.ExecutionSnapshot;
|
|||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Step;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.StepStatus;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
|
@ -25,6 +26,8 @@ public class ExecutionContext implements ExecutionListener {
|
|||
|
||||
Map<FieldKey, Object> inputs = new HashMap<>();
|
||||
Map<String, Object> authorizations = new HashMap<>();
|
||||
Map<String, Object> executionVariables = new HashMap<>();
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors = new HashMap<>();
|
||||
|
||||
@Getter()
|
||||
private Map<FieldKey, Object> result = new HashMap<>();
|
||||
|
|
@ -47,11 +50,27 @@ public class ExecutionContext implements ExecutionListener {
|
|||
@JsonIgnore
|
||||
Runnable stateChangeListener;
|
||||
|
||||
boolean interactionSimulationEnabled = false;
|
||||
|
||||
LLMDescriptor interactionSimulationDescriptor;
|
||||
|
||||
protected void setInteractionSimulationEnabled(boolean interactionSimulationEnabled) {
|
||||
this.interactionSimulationEnabled = interactionSimulationEnabled;
|
||||
}
|
||||
|
||||
protected void setInteractionSimulationDescriptor(LLMDescriptor interactionSimulationDescriptor) {
|
||||
this.interactionSimulationDescriptor = interactionSimulationDescriptor;
|
||||
this.steps.values().forEach(step -> step.setInteractionSimulationDescriptor(interactionSimulationDescriptor));
|
||||
}
|
||||
|
||||
public ExecutionContext(Map<String, Step<?>> steps) {
|
||||
this.steps = steps;
|
||||
this.steps.values().forEach(step -> {
|
||||
step.setListener(this);
|
||||
step.setAuthorizations(this.authorizations);
|
||||
step.setExecutionVariables(this.executionVariables);
|
||||
step.setExecutionVariableDescriptors(this.executionVariableDescriptors);
|
||||
step.setInteractionSimulationDescriptor(this.interactionSimulationDescriptor);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -224,6 +243,55 @@ public class ExecutionContext implements ExecutionListener {
|
|||
notifyStateChanged();
|
||||
}
|
||||
|
||||
protected void setExecutionVariable(String key, Object value) {
|
||||
registerExecutionVariable(ExecutionVariableDescriptor.builder()
|
||||
.name(key)
|
||||
.value(value)
|
||||
.kind(ExecutionVariableKind.ANY)
|
||||
.cleanupPolicy(ExecutionVariableCleanupPolicy.NONE)
|
||||
.build());
|
||||
}
|
||||
|
||||
protected void registerExecutionVariable(ExecutionVariableDescriptor descriptor) {
|
||||
ExecutionVariableDescriptor normalized = ExecutionVariableRegistry.normalize(descriptor);
|
||||
this.executionVariableDescriptors.put(normalized.getName(), normalized);
|
||||
this.executionVariables.put(normalized.getName(), normalized.getValue());
|
||||
notifyStateChanged();
|
||||
}
|
||||
|
||||
protected void setExecutionVariables(Map<String, Object> executionVariables) {
|
||||
setExecutionVariableDescriptors(ExecutionVariableRegistry.descriptorsFromValues(executionVariables));
|
||||
}
|
||||
|
||||
protected void setExecutionVariableDescriptors(Map<String, ExecutionVariableDescriptor> descriptors) {
|
||||
this.executionVariableDescriptors.clear();
|
||||
this.executionVariables.clear();
|
||||
if (descriptors != null) {
|
||||
descriptors.values().stream()
|
||||
.map(ExecutionVariableRegistry::normalize)
|
||||
.forEach(descriptor -> {
|
||||
this.executionVariableDescriptors.put(descriptor.getName(), descriptor);
|
||||
this.executionVariables.put(descriptor.getName(), descriptor.getValue());
|
||||
});
|
||||
}
|
||||
notifyStateChanged();
|
||||
}
|
||||
|
||||
protected Object getExecutionVariable(String key) {
|
||||
return this.executionVariables.get(key);
|
||||
}
|
||||
|
||||
protected ExecutionVariableDescriptor getExecutionVariableDescriptor(String key) {
|
||||
return this.executionVariableDescriptors.get(key);
|
||||
}
|
||||
|
||||
protected Object removeExecutionVariable(String key) {
|
||||
this.executionVariableDescriptors.remove(key);
|
||||
Object removed = this.executionVariables.remove(key);
|
||||
notifyStateChanged();
|
||||
return removed;
|
||||
}
|
||||
|
||||
protected void start(ExecutorService executorService) {
|
||||
this.setStatus(ExecutionStatus.RUNNING);
|
||||
this.steps.values().forEach(step -> step.start(executorService));
|
||||
|
|
@ -235,6 +303,8 @@ public class ExecutionContext implements ExecutionListener {
|
|||
this.authorizations.clear();
|
||||
this.result.clear();
|
||||
this.partialResult.clear();
|
||||
this.executionVariables.clear();
|
||||
this.executionVariableDescriptors.clear();
|
||||
this.errors.clear();
|
||||
this.warnings.clear();
|
||||
this.waitingSteps.clear();
|
||||
|
|
@ -252,6 +322,10 @@ public class ExecutionContext implements ExecutionListener {
|
|||
.status(this.status)
|
||||
.startTime(this.startTime)
|
||||
.endTime(this.endTime)
|
||||
.interactionSimulationEnabled(this.interactionSimulationEnabled)
|
||||
.interactionSimulationDescriptor(this.interactionSimulationDescriptor)
|
||||
.executionVariables(Map.copyOf(this.executionVariables))
|
||||
.executionVariableDescriptors(Map.copyOf(this.executionVariableDescriptors))
|
||||
.providedAuthorizations(providedAuthorizations == null ? Map.of() : Map.copyOf(providedAuthorizations))
|
||||
.inputs(Map.copyOf(this.inputs))
|
||||
.result(Map.copyOf(this.result))
|
||||
|
|
@ -272,6 +346,27 @@ public class ExecutionContext implements ExecutionListener {
|
|||
if (snapshot.getProvidedAuthorizations() != null) {
|
||||
this.authorizations.putAll(snapshot.getProvidedAuthorizations());
|
||||
}
|
||||
this.executionVariables.clear();
|
||||
this.executionVariableDescriptors.clear();
|
||||
if (snapshot.getExecutionVariableDescriptors() != null && !snapshot.getExecutionVariableDescriptors().isEmpty()) {
|
||||
snapshot.getExecutionVariableDescriptors().values().stream()
|
||||
.map(ExecutionVariableRegistry::normalize)
|
||||
.forEach(descriptor -> {
|
||||
this.executionVariableDescriptors.put(descriptor.getName(), descriptor);
|
||||
this.executionVariables.put(descriptor.getName(), descriptor.getValue());
|
||||
});
|
||||
} else if (snapshot.getExecutionVariables() != null) {
|
||||
snapshot.getExecutionVariables().forEach((key, value) -> {
|
||||
ExecutionVariableDescriptor descriptor = ExecutionVariableDescriptor.builder()
|
||||
.name(key)
|
||||
.value(value)
|
||||
.kind(ExecutionVariableKind.ANY)
|
||||
.cleanupPolicy(ExecutionVariableCleanupPolicy.NONE)
|
||||
.build();
|
||||
this.executionVariableDescriptors.put(key, descriptor);
|
||||
this.executionVariables.put(key, value);
|
||||
});
|
||||
}
|
||||
this.result.clear();
|
||||
if (snapshot.getResult() != null) {
|
||||
this.result.putAll(snapshot.getResult());
|
||||
|
|
@ -294,6 +389,8 @@ public class ExecutionContext implements ExecutionListener {
|
|||
}
|
||||
this.startTime = snapshot.getStartTime();
|
||||
this.endTime = snapshot.getEndTime();
|
||||
this.interactionSimulationEnabled = snapshot.isInteractionSimulationEnabled();
|
||||
this.setInteractionSimulationDescriptor(snapshot.getInteractionSimulationDescriptor());
|
||||
this.status = normalizeRestoredStatus(snapshot.getStatus());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import it.cnr.isti.workflow.manager.executions.steps.Step;
|
|||
import it.cnr.isti.workflow.manager.flows.model.FlowNode;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Connection;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
|
@ -51,6 +52,12 @@ public class ExecutionObject {
|
|||
@JsonIgnore
|
||||
ExecutorService executorService;
|
||||
|
||||
boolean interactionSimulationEnabled = false;
|
||||
|
||||
boolean simulationAvailable = false;
|
||||
|
||||
LLMDescriptor interactionSimulationDescriptor;
|
||||
|
||||
@Builder
|
||||
public ExecutionObject(String executionName, FlowData flow, List<ExecutionAuthorizationRequirement> requiredAuthorizations) {
|
||||
this.name = executionName;
|
||||
|
|
@ -68,6 +75,7 @@ public class ExecutionObject {
|
|||
.distinct()
|
||||
.filter(this.providedAuthorizations::containsKey)
|
||||
.forEach(key -> this.context.setAuthorization(key, this.providedAuthorizations.get(key)));
|
||||
this.simulationAvailable = hasSimulationAvailable(steps);
|
||||
refreshInitializationStatus();
|
||||
|
||||
}
|
||||
|
|
@ -111,11 +119,63 @@ public class ExecutionObject {
|
|||
refreshInitializationStatus();
|
||||
}
|
||||
|
||||
protected void setExecutionVariable(String key, Object value) {
|
||||
this.context.setExecutionVariable(key, value);
|
||||
}
|
||||
|
||||
protected void setExecutionVariables(Map<String, Object> executionVariables) {
|
||||
this.context.setExecutionVariables(executionVariables);
|
||||
}
|
||||
|
||||
protected void setExecutionVariableDescriptors(Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
this.context.setExecutionVariableDescriptors(executionVariableDescriptors);
|
||||
}
|
||||
|
||||
protected void registerExecutionVariable(ExecutionVariableDescriptor descriptor) {
|
||||
this.context.registerExecutionVariable(descriptor);
|
||||
}
|
||||
|
||||
protected Object removeExecutionVariable(String key) {
|
||||
return this.context.removeExecutionVariable(key);
|
||||
}
|
||||
|
||||
protected Object getExecutionVariable(String key) {
|
||||
return this.context.getExecutionVariable(key);
|
||||
}
|
||||
|
||||
protected ExecutionVariableDescriptor getExecutionVariableDescriptor(String key) {
|
||||
return this.context.getExecutionVariableDescriptor(key);
|
||||
}
|
||||
|
||||
protected void start() {
|
||||
start(false);
|
||||
}
|
||||
|
||||
protected void startSimulation() {
|
||||
if (!this.simulationAvailable) {
|
||||
throw new IllegalStateException("Execution with id " + this.getId() + " does not contain simulable steps");
|
||||
}
|
||||
start(true);
|
||||
}
|
||||
|
||||
protected void setInteractionSimulationDescriptor(LLMDescriptor interactionSimulationDescriptor) {
|
||||
this.interactionSimulationDescriptor = interactionSimulationDescriptor;
|
||||
this.context.setInteractionSimulationDescriptor(interactionSimulationDescriptor);
|
||||
}
|
||||
|
||||
protected void start(boolean simulateInteractions) {
|
||||
List<String> missingAuthorizations = getMissingAuthorizationKeys();
|
||||
if (!missingAuthorizations.isEmpty()) {
|
||||
throw new IllegalStateException("Missing required authorizations: " + String.join(", ", missingAuthorizations));
|
||||
}
|
||||
this.interactionSimulationEnabled = simulateInteractions;
|
||||
this.context.setInteractionSimulationEnabled(simulateInteractions);
|
||||
this.context.setInteractionSimulationDescriptor(this.interactionSimulationDescriptor);
|
||||
this.context.getSteps().values().forEach(step -> {
|
||||
if (step.getNode().isUserInteractive()) {
|
||||
step.setSimulated(simulateInteractions);
|
||||
}
|
||||
});
|
||||
if (this.context.getStatus() == ExecutionStatus.READY){
|
||||
this.context.start(executorService);
|
||||
} else
|
||||
|
|
@ -126,7 +186,7 @@ public class ExecutionObject {
|
|||
protected ExecutionStatus resume() {
|
||||
ExecutionStatus resumedStatus = this.context.resumeFromSuspended();
|
||||
if (resumedStatus == ExecutionStatus.READY) {
|
||||
start();
|
||||
start(this.interactionSimulationEnabled);
|
||||
return this.context.getStatus();
|
||||
}
|
||||
return resumedStatus;
|
||||
|
|
@ -186,9 +246,27 @@ public class ExecutionObject {
|
|||
}
|
||||
if (snapshot != null) {
|
||||
this.context.restore(snapshot);
|
||||
this.interactionSimulationEnabled = snapshot.isInteractionSimulationEnabled();
|
||||
this.interactionSimulationDescriptor = snapshot.getInteractionSimulationDescriptor();
|
||||
}
|
||||
this.providedAuthorizations.forEach(this.context::setAuthorization);
|
||||
this.context.setInteractionSimulationDescriptor(this.interactionSimulationDescriptor);
|
||||
this.simulationAvailable = hasSimulationAvailable(new ArrayList<>(this.context.getSteps().values()));
|
||||
refreshInitializationStatus();
|
||||
}
|
||||
|
||||
private boolean hasSimulationAvailable(List<Step<?>> steps) {
|
||||
boolean hasInteractiveSteps = false;
|
||||
for (Step<?> step : steps) {
|
||||
if (!step.getNode().isUserInteractive()) {
|
||||
continue;
|
||||
}
|
||||
hasInteractiveSteps = true;
|
||||
if (!NodeExecutors.supportsSimulation(step.getNode())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return hasInteractiveSteps;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package it.cnr.isti.workflow.manager.executions;
|
||||
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record ExecutionSimulationRequest(
|
||||
@Valid @NotNull LLMDescriptor simulator) {
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package it.cnr.isti.workflow.manager.executions;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
|
||||
public final class ExecutionTemplateResolver {
|
||||
|
||||
private ExecutionTemplateResolver() {
|
||||
}
|
||||
|
||||
public static String resolve(String template, Iterable<Input> inputs, Map<String, Object> executionVariables) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
if (inputs != null) {
|
||||
for (Input input : inputs) {
|
||||
values.put(input.getDescriptor().getName(), input.getValue());
|
||||
}
|
||||
}
|
||||
return resolve(template, values, executionVariables);
|
||||
}
|
||||
|
||||
public static String resolve(String template, Map<String, ?> values, Map<String, Object> executionVariables) {
|
||||
if (!StringUtils.hasText(template)) {
|
||||
return template;
|
||||
}
|
||||
String resolved = template;
|
||||
if (values != null) {
|
||||
for (Map.Entry<String, ?> entry : values.entrySet()) {
|
||||
resolved = resolved.replace("${{" + entry.getKey() + "}}", formatValue(entry.getValue()));
|
||||
}
|
||||
}
|
||||
if (executionVariables != null) {
|
||||
for (Map.Entry<String, Object> entry : executionVariables.entrySet()) {
|
||||
resolved = resolved.replace("${{vars." + entry.getKey() + "}}", formatValue(entry.getValue()));
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public static String formatValue(Object value) {
|
||||
if (value instanceof Collection<?> collection) {
|
||||
return collection.stream()
|
||||
.map(item -> item == null ? "null" : item.toString())
|
||||
.reduce((left, right) -> left + System.lineSeparator() + right)
|
||||
.orElse("");
|
||||
}
|
||||
return value == null ? "null" : value.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package it.cnr.isti.workflow.manager.executions;
|
||||
|
||||
public enum ExecutionVariableCleanupPolicy {
|
||||
NONE,
|
||||
CLOSE_RESOURCE,
|
||||
DELETE_FILE
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package it.cnr.isti.workflow.manager.executions;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ExecutionVariableDescriptor {
|
||||
|
||||
private String name;
|
||||
@Builder.Default
|
||||
private ExecutionVariableKind kind = ExecutionVariableKind.ANY;
|
||||
private Object value;
|
||||
private String producerBlockId;
|
||||
private String description;
|
||||
@Builder.Default
|
||||
private ExecutionVariableCleanupPolicy cleanupPolicy = ExecutionVariableCleanupPolicy.NONE;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package it.cnr.isti.workflow.manager.executions;
|
||||
|
||||
public enum ExecutionVariableKind {
|
||||
ANY,
|
||||
TEXT,
|
||||
JSON,
|
||||
FILE_PATH,
|
||||
HTTP_RESOURCE,
|
||||
MCP_SESSION
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package it.cnr.isti.workflow.manager.executions;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public final class ExecutionVariableRegistry {
|
||||
|
||||
private ExecutionVariableRegistry() {
|
||||
}
|
||||
|
||||
public static ExecutionVariableDescriptor normalize(ExecutionVariableDescriptor descriptor) {
|
||||
if (descriptor == null || !StringUtils.hasText(descriptor.getName())) {
|
||||
throw new IllegalArgumentException("Execution variable name must not be blank");
|
||||
}
|
||||
return ExecutionVariableDescriptor.builder()
|
||||
.name(descriptor.getName().trim())
|
||||
.kind(descriptor.getKind() == null ? ExecutionVariableKind.ANY : descriptor.getKind())
|
||||
.value(descriptor.getValue())
|
||||
.producerBlockId(descriptor.getProducerBlockId())
|
||||
.description(descriptor.getDescription())
|
||||
.cleanupPolicy(descriptor.getCleanupPolicy() == null
|
||||
? ExecutionVariableCleanupPolicy.NONE
|
||||
: descriptor.getCleanupPolicy())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Map<String, Object> valuesView(Map<String, ExecutionVariableDescriptor> descriptors) {
|
||||
LinkedHashMap<String, Object> values = new LinkedHashMap<>();
|
||||
if (descriptors == null) {
|
||||
return values;
|
||||
}
|
||||
descriptors.values().stream()
|
||||
.map(ExecutionVariableRegistry::normalize)
|
||||
.forEach(descriptor -> values.put(descriptor.getName(), descriptor.getValue()));
|
||||
return values;
|
||||
}
|
||||
|
||||
public static Map<String, ExecutionVariableDescriptor> descriptorsFromValues(Map<String, Object> values) {
|
||||
LinkedHashMap<String, ExecutionVariableDescriptor> descriptors = new LinkedHashMap<>();
|
||||
if (values == null) {
|
||||
return descriptors;
|
||||
}
|
||||
values.forEach((key, value) -> descriptors.put(key, ExecutionVariableDescriptor.builder()
|
||||
.name(key)
|
||||
.kind(ExecutionVariableKind.ANY)
|
||||
.value(value)
|
||||
.cleanupPolicy(ExecutionVariableCleanupPolicy.NONE)
|
||||
.build()));
|
||||
return descriptors;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import java.util.Objects;
|
|||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;
|
||||
|
|
@ -15,6 +17,7 @@ import it.cnr.isti.workflow.manager.blocks.configurations.ChatInteractionBlockCo
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentChatBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.containers.Container;
|
||||
import it.cnr.isti.workflow.manager.containers.configurations.ContainerConfiguration;
|
||||
import it.cnr.isti.workflow.manager.executions.persistence.ExecutionSnapshot;
|
||||
|
|
@ -25,6 +28,8 @@ import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
|||
import it.cnr.isti.workflow.manager.flows.validation.FlowExecutionValidator;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPAgentService;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPSharedSessionRegistry;
|
||||
|
||||
@Service
|
||||
public class ExecutionsService {
|
||||
|
|
@ -40,6 +45,9 @@ public class ExecutionsService {
|
|||
@Autowired
|
||||
ExecutionRepository executionRepository;
|
||||
|
||||
@Autowired
|
||||
MCPAgentService mcpAgentService;
|
||||
|
||||
public ExecutionObject createExecution(String executionName, FlowData flow) {
|
||||
flowExecutionValidator.validate(flow);
|
||||
List<ExecutionAuthorizationRequirement> requiredAuthorizations = resolveRequiredAuthorizations(flow);
|
||||
|
|
@ -132,12 +140,64 @@ public class ExecutionsService {
|
|||
return eo;
|
||||
}
|
||||
|
||||
public ExecutionObject setExecutionVariable(String executionId, String key, Object value) {
|
||||
ExecutionObject eo = getExecution(executionId);
|
||||
eo.setExecutionVariable(key, value);
|
||||
return eo;
|
||||
}
|
||||
|
||||
public ExecutionObject setExecutionVariables(String executionId, Map<String, Object> executionVariables) {
|
||||
ExecutionObject eo = getExecution(executionId);
|
||||
eo.setExecutionVariables(executionVariables);
|
||||
return eo;
|
||||
}
|
||||
|
||||
public ExecutionObject setExecutionVariableDescriptors(String executionId,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
ExecutionObject eo = getExecution(executionId);
|
||||
eo.setExecutionVariableDescriptors(executionVariableDescriptors);
|
||||
return eo;
|
||||
}
|
||||
|
||||
public ExecutionObject registerExecutionVariable(String executionId, ExecutionVariableDescriptor descriptor) {
|
||||
ExecutionObject eo = getExecution(executionId);
|
||||
eo.registerExecutionVariable(descriptor);
|
||||
return eo;
|
||||
}
|
||||
|
||||
public Object getExecutionVariable(String executionId, String key) {
|
||||
return getExecution(executionId).getExecutionVariable(key);
|
||||
}
|
||||
|
||||
public ExecutionVariableDescriptor getExecutionVariableDescriptor(String executionId, String key) {
|
||||
return getExecution(executionId).getExecutionVariableDescriptor(key);
|
||||
}
|
||||
|
||||
public Object removeExecutionVariable(String executionId, String key) {
|
||||
return getExecution(executionId).removeExecutionVariable(key);
|
||||
}
|
||||
|
||||
public ExecutionObject startExecution(String id) {
|
||||
ExecutionObject eo = getExecution(id);
|
||||
eo.start();
|
||||
return eo;
|
||||
}
|
||||
|
||||
public ExecutionObject startSimulationExecution(String id, LLMDescriptor simulatorDescriptor) {
|
||||
ExecutionObject eo = getExecution(id);
|
||||
if (!eo.isSimulationAvailable()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Execution does not contain simulable steps");
|
||||
}
|
||||
if (simulatorDescriptor == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Simulation requires a simulator descriptor");
|
||||
}
|
||||
eo.setInteractionSimulationDescriptor(simulatorDescriptor);
|
||||
eo.startSimulation();
|
||||
return eo;
|
||||
}
|
||||
|
||||
public ExecutionObject resumeExecution(String id) {
|
||||
ExecutionObject eo = getExecution(id);
|
||||
if (eo.getContext().getStatus().isFinalState()) {
|
||||
|
|
@ -173,7 +233,8 @@ public class ExecutionsService {
|
|||
return;
|
||||
}
|
||||
for (Block<?> block : flow.getBlocks() == null ? List.<Block<?>>of() : flow.getBlocks()) {
|
||||
collectRequirement(requirements, block, resolveDescriptor(block), block == null ? null : block.getName());
|
||||
resolveDescriptors(block).forEach(descriptor ->
|
||||
collectRequirement(requirements, block, descriptor, block == null ? null : block.getName()));
|
||||
collectHttpRequirement(requirements, block);
|
||||
}
|
||||
for (Container<?> container : flow.getContainers() == null ? List.<Container<?>>of() : flow.getContainers()) {
|
||||
|
|
@ -183,25 +244,34 @@ public class ExecutionsService {
|
|||
}
|
||||
}
|
||||
|
||||
private LLMDescriptor resolveDescriptor(Block<?> block) {
|
||||
private List<LLMDescriptor> resolveDescriptors(Block<?> block) {
|
||||
if (block == null || block.getSpecificConfiguration() == null) {
|
||||
return null;
|
||||
return List.of();
|
||||
}
|
||||
Object configuration = block.getSpecificConfiguration();
|
||||
if (configuration instanceof LLMBlockConfiguration llmConfiguration) {
|
||||
return llmConfiguration.getLlmDescriptor();
|
||||
return listOfDescriptors(llmConfiguration.getLlmDescriptor());
|
||||
}
|
||||
if (configuration instanceof HumanInteractiveBlockConfiguration humanConfiguration) {
|
||||
return humanConfiguration.getSimulateWith();
|
||||
return List.of();
|
||||
}
|
||||
if (configuration instanceof ChatInteractionBlockConfiguration chatConfiguration) {
|
||||
return chatConfiguration.getLlmDescriptor();
|
||||
return listOfDescriptors(chatConfiguration.getLlmDescriptor());
|
||||
}
|
||||
if (configuration instanceof MCPAgentChatBlockConfiguration mcpAgentChatConfiguration) {
|
||||
return List.of();
|
||||
}
|
||||
if (configuration instanceof ConditionalBlockConfiguration conditionalConfiguration
|
||||
&& conditionalConfiguration.isUseLlm()) {
|
||||
return conditionalConfiguration.getLlmDescriptor();
|
||||
return listOfDescriptors(conditionalConfiguration.getLlmDescriptor());
|
||||
}
|
||||
return null;
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<LLMDescriptor> listOfDescriptors(LLMDescriptor... descriptors) {
|
||||
return java.util.Arrays.stream(descriptors)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void collectRequirement(Map<String, RequirementAccumulator> requirements, Block<?> block, LLMDescriptor descriptor,
|
||||
|
|
@ -246,7 +316,10 @@ public class ExecutionsService {
|
|||
}
|
||||
|
||||
private void attachPersistence(ExecutionObject executionObject) {
|
||||
executionObject.setStateChangeListener(() -> persist(executionObject));
|
||||
executionObject.setStateChangeListener(() -> {
|
||||
persist(executionObject);
|
||||
cleanupManagedResourcesIfFinal(executionObject);
|
||||
});
|
||||
}
|
||||
|
||||
private void persist(ExecutionObject executionObject) {
|
||||
|
|
@ -275,6 +348,31 @@ public class ExecutionsService {
|
|||
return executionObject;
|
||||
}
|
||||
|
||||
private void cleanupManagedResourcesIfFinal(ExecutionObject executionObject) {
|
||||
if (executionObject == null || !executionObject.getContext().getStatus().isFinalState()) {
|
||||
return;
|
||||
}
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors =
|
||||
executionObject.getContext().getExecutionVariableDescriptors();
|
||||
if (executionVariableDescriptors == null || executionVariableDescriptors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
java.util.LinkedHashSet<String> sessionIds = new java.util.LinkedHashSet<>();
|
||||
java.util.List<ExecutionVariableDescriptor> cleanedDescriptors = new java.util.ArrayList<>();
|
||||
for (ExecutionVariableDescriptor descriptor : executionVariableDescriptors.values()) {
|
||||
if (descriptor == null
|
||||
|| descriptor.getKind() != ExecutionVariableKind.MCP_SESSION
|
||||
|| descriptor.getCleanupPolicy() != ExecutionVariableCleanupPolicy.CLOSE_RESOURCE
|
||||
|| descriptor.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
sessionIds.add(String.valueOf(descriptor.getValue()));
|
||||
cleanedDescriptors.add(descriptor);
|
||||
}
|
||||
sessionIds.forEach(mcpAgentService::closeSessionQuietly);
|
||||
cleanedDescriptors.forEach(descriptor -> descriptor.setCleanupPolicy(ExecutionVariableCleanupPolicy.NONE));
|
||||
}
|
||||
|
||||
private static class RequirementAccumulator {
|
||||
private final String key;
|
||||
private final String provider;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
package it.cnr.isti.workflow.manager.executions.design;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentChatBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableKind;
|
||||
import it.cnr.isti.workflow.manager.flows.FlowService;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowView;
|
||||
|
||||
@Service
|
||||
public class FlowSharedVariableCatalogService {
|
||||
|
||||
public record SharedVariableDefinition(
|
||||
String name,
|
||||
ExecutionVariableKind kind,
|
||||
String producerBlockId,
|
||||
String producerBlockName) {
|
||||
}
|
||||
|
||||
private final FlowService flowService;
|
||||
private final Map<String, Map<String, Block<?>>> draftBlocksByFlowId = new ConcurrentHashMap<>();
|
||||
private final Map<String, Set<String>> removedBlockIdsByFlowId = new ConcurrentHashMap<>();
|
||||
|
||||
public FlowSharedVariableCatalogService(FlowService flowService) {
|
||||
this.flowService = flowService;
|
||||
}
|
||||
|
||||
public List<SharedVariableDefinition> listForFlow(String flowId, String owner, ExecutionVariableKind kind) {
|
||||
List<SharedVariableDefinition> definitions = List.of();
|
||||
if (StringUtils.hasText(flowId)) {
|
||||
LinkedHashMap<String, Block<?>> mergedBlocks = new LinkedHashMap<>();
|
||||
FlowView flowView = flowService.getFlow(flowId, owner);
|
||||
if (flowView != null && flowView.flow() != null && flowView.flow().getBlocks() != null) {
|
||||
flowView.flow().getBlocks().forEach(block -> {
|
||||
if (block != null && StringUtils.hasText(block.getId())) {
|
||||
mergedBlocks.put(block.getId(), block);
|
||||
}
|
||||
});
|
||||
}
|
||||
removedBlockIdsByFlowId.getOrDefault(flowId, Set.of()).forEach(mergedBlocks::remove);
|
||||
mergedBlocks.putAll(draftBlocksByFlowId.getOrDefault(flowId, Map.of()));
|
||||
definitions = analyze(new ArrayList<>(mergedBlocks.values()));
|
||||
}
|
||||
return definitions.stream()
|
||||
.filter(definition -> kind == null || definition.kind() == kind)
|
||||
.sorted(Comparator.comparing(SharedVariableDefinition::name, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void registerDraftBlock(String flowId, Block<?> block) {
|
||||
registerDraftBlock(flowId, block, null);
|
||||
}
|
||||
|
||||
public void registerDraftBlock(String flowId, Block<?> block, String replacedBlockId) {
|
||||
if (!StringUtils.hasText(flowId) || block == null) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.hasText(replacedBlockId) && !replacedBlockId.equals(block.getId())) {
|
||||
removedBlockIdsByFlowId.computeIfAbsent(flowId, ignored -> ConcurrentHashMap.newKeySet())
|
||||
.add(replacedBlockId.trim());
|
||||
}
|
||||
draftBlocksByFlowId.compute(flowId, (ignored, current) -> {
|
||||
LinkedHashMap<String, Block<?>> updated = new LinkedHashMap<>();
|
||||
if (current != null) {
|
||||
updated.putAll(current);
|
||||
}
|
||||
updated.put(block.getId(), block);
|
||||
return updated;
|
||||
});
|
||||
removedBlockIdsByFlowId.computeIfPresent(flowId, (ignored, removed) -> {
|
||||
removed.remove(block.getId());
|
||||
return removed.isEmpty() ? null : removed;
|
||||
});
|
||||
}
|
||||
|
||||
public void removeDraftBlock(String flowId, String blockId) {
|
||||
if (!StringUtils.hasText(flowId) || !StringUtils.hasText(blockId)) {
|
||||
return;
|
||||
}
|
||||
draftBlocksByFlowId.computeIfPresent(flowId, (ignored, current) -> {
|
||||
current.remove(blockId);
|
||||
return current.isEmpty() ? null : current;
|
||||
});
|
||||
removedBlockIdsByFlowId.computeIfAbsent(flowId, ignored -> ConcurrentHashMap.newKeySet())
|
||||
.add(blockId.trim());
|
||||
}
|
||||
|
||||
private List<SharedVariableDefinition> analyze(FlowData flowData) {
|
||||
if (flowData == null) {
|
||||
return List.of();
|
||||
}
|
||||
return analyze(flowData.getBlocks() == null ? List.of() : flowData.getBlocks());
|
||||
}
|
||||
|
||||
private List<SharedVariableDefinition> analyze(List<Block<?>> blocks) {
|
||||
LinkedHashMap<String, SharedVariableDefinition> definitionsByKey = new LinkedHashMap<>();
|
||||
for (Block<?> block : blocks) {
|
||||
if (block == null || block.getSpecificConfiguration() == null) {
|
||||
continue;
|
||||
}
|
||||
if (block.getSpecificConfiguration() instanceof MCPAgentBlockConfiguration configuration) {
|
||||
String producedName = producedName(configuration.getShareSession(), configuration.getSharedSessionName());
|
||||
if (StringUtils.hasText(producedName)) {
|
||||
SharedVariableDefinition definition = new SharedVariableDefinition(producedName, ExecutionVariableKind.MCP_SESSION,
|
||||
block.getId(), block.getName());
|
||||
definitionsByKey.put(definition.kind().name() + ":" + definition.name(), definition);
|
||||
}
|
||||
} else if (block.getSpecificConfiguration() instanceof MCPAgentChatBlockConfiguration configuration) {
|
||||
String producedName = producedName(configuration.getShareSession(), configuration.getSharedSessionName());
|
||||
if (StringUtils.hasText(producedName)) {
|
||||
SharedVariableDefinition definition = new SharedVariableDefinition(producedName, ExecutionVariableKind.MCP_SESSION,
|
||||
block.getId(), block.getName());
|
||||
definitionsByKey.put(definition.kind().name() + ":" + definition.name(), definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(definitionsByKey.values());
|
||||
}
|
||||
|
||||
private String producedName(Boolean shareSession, String sharedSessionName) {
|
||||
if (Boolean.TRUE.equals(shareSession) && StringUtils.hasText(sharedSessionName)) {
|
||||
return sharedSessionName.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,11 @@ import java.util.Map;
|
|||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.containers.Container;
|
||||
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.flows.model.FlowNode;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
|
||||
public final class NodeExecutors {
|
||||
|
||||
|
|
@ -24,28 +26,52 @@ public final class NodeExecutors {
|
|||
return false;
|
||||
}
|
||||
|
||||
public static Map<String, Object> execute(FlowNode node, List<Input> inputs, Map<String, Object> authorizations) {
|
||||
public static boolean supportsSimulation(FlowNode node) {
|
||||
if (node instanceof Block<?> block) {
|
||||
return BlockExecutors.get(block.getType()).execute((Block) block, inputs, authorizations);
|
||||
return BlockExecutors.get(block.getType()).supportsSimulation();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Map<String, Object> execute(FlowNode node, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
if (node instanceof Block<?> block) {
|
||||
return BlockExecutors.get(block.getType()).execute((Block) block, inputs, authorizations, executionVariables,
|
||||
executionVariableDescriptors);
|
||||
}
|
||||
if (node instanceof Container<?> container) {
|
||||
return ContainerExecutors.get(container.getType()).execute((Container) container, inputs, authorizations);
|
||||
return ContainerExecutors.get(container.getType()).execute((Container) container, inputs, authorizations,
|
||||
executionVariables, executionVariableDescriptors);
|
||||
}
|
||||
throw new IllegalStateException("No executor found for node type " + node.getClass().getName());
|
||||
}
|
||||
|
||||
public static InteractionResult interact(FlowNode node, List<Input> inputs, Map<String, Object> interaction,
|
||||
Map<String, Object> partialResults, Map<String, Object> authorizations) {
|
||||
public static Map<String, Object> simulate(FlowNode node, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
LLMDescriptor simulatorDescriptor) {
|
||||
if (node instanceof Block<?> block) {
|
||||
return BlockExecutors.get(block.getType()).interact((Block) block, inputs, interaction, partialResults, authorizations);
|
||||
return BlockExecutors.get(block.getType()).simulate((Block) block, inputs, authorizations, executionVariables,
|
||||
executionVariableDescriptors, simulatorDescriptor);
|
||||
}
|
||||
throw new IllegalStateException("No simulation executor found for node type " + node.getClass().getName());
|
||||
}
|
||||
|
||||
public static InteractionResult interact(FlowNode node, List<Input> inputs, Map<String, Object> interaction,
|
||||
Map<String, Object> partialResults, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
if (node instanceof Block<?> block) {
|
||||
return BlockExecutors.get(block.getType()).interact((Block) block, inputs, interaction, partialResults,
|
||||
authorizations, executionVariables, executionVariableDescriptors);
|
||||
}
|
||||
throw new IllegalStateException("No interactive executor found for node type " + node.getClass().getName());
|
||||
}
|
||||
|
||||
public static void cancel(FlowNode node, List<Input> inputs, Map<String, Object> partialResults,
|
||||
Map<String, Object> authorizations) {
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
if (node instanceof Block<?> block) {
|
||||
BlockExecutors.get(block.getType()).cancel((Block) block, inputs, partialResults, authorizations);
|
||||
BlockExecutors.get(block.getType()).cancel((Block) block, inputs, partialResults, authorizations,
|
||||
executionVariables, executionVariableDescriptors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ import java.util.Map;
|
|||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.executions.InteractionResult;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
|
||||
public interface BlockExecutor<B extends BlockType> {
|
||||
|
||||
Map<String, Object> execute(Block<B> block, List<Input> inputs, Map<String, Object> context);
|
||||
Map<String, Object> execute(Block<B> block, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors);
|
||||
|
||||
Class<B> getBlockType();
|
||||
|
||||
|
|
@ -18,18 +21,26 @@ public interface BlockExecutor<B extends BlockType> {
|
|||
return false;
|
||||
}
|
||||
|
||||
default Map<String, Object> simulate(Block<B> block, List<Input> inputs, Map<String, Object> authorizations)
|
||||
default boolean supportsSimulation() {
|
||||
return isInteractive();
|
||||
}
|
||||
|
||||
default Map<String, Object> simulate(Block<B> block, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
LLMDescriptor simulatorDescriptor)
|
||||
{
|
||||
throw new UnsupportedOperationException("Simulation not supported for this block type");
|
||||
}
|
||||
|
||||
default InteractionResult interact(Block<B> block, List<Input> inputs, Map<String, Object> interaction,
|
||||
Map<String, Object> partialResults, Map<String, Object> authorizations) {
|
||||
Map<String, Object> partialResults, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
throw new UnsupportedOperationException("Interaction not supported for this block type");
|
||||
}
|
||||
|
||||
default void cancel(Block<B> block, List<Input> inputs, Map<String, Object> partialResults,
|
||||
Map<String, Object> authorizations) {
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import it.cnr.isti.workflow.manager.blocks.Block;
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.ChatInteractionBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ChatInteractionBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ChatInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionTemplateResolver;
|
||||
import it.cnr.isti.workflow.manager.executions.InteractionResult;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.llms.ChatMessage;
|
||||
|
|
@ -24,13 +26,59 @@ import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
|||
@Component
|
||||
public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlockType> {
|
||||
|
||||
private static final int MAX_SIMULATED_INTERACTIONS = 5;
|
||||
|
||||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<ChatInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
throw new UnsupportedOperationException("ChatInteraction blocks require user interaction.");
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
throw new UnsupportedOperationException("ChatInteraction blocks require user interaction unless execution simulation is enabled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> simulate(Block<ChatInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
LLMDescriptor simulatorDescriptor) {
|
||||
ChatInteractionBlockConfiguration configuration =
|
||||
(ChatInteractionBlockConfiguration) block.getSpecificConfiguration();
|
||||
if (simulatorDescriptor == null) {
|
||||
throw new IllegalArgumentException("Missing simulation descriptor for ChatInteraction execution");
|
||||
}
|
||||
if (!StringUtils.hasText(configuration.getGoalDescription())) {
|
||||
throw new IllegalArgumentException("Simulated ChatInteraction requires goalDescription");
|
||||
}
|
||||
|
||||
LLMDescriptor chatDescriptor = configuration.getLlmDescriptor();
|
||||
LLMProvider chatProvider = resolveProvider(chatDescriptor.provider());
|
||||
String chatAuthorization = resolveAuthorization(chatProvider, chatDescriptor, authorizations);
|
||||
|
||||
LLMProvider simulatorProvider = resolveProvider(simulatorDescriptor.provider());
|
||||
String simulatorAuthorization = resolveAuthorization(simulatorProvider, simulatorDescriptor, authorizations);
|
||||
|
||||
String resolvedGoal = resolvePlaceholders(configuration.getGoalDescription(), inputs, executionVariables);
|
||||
List<String> history = new ArrayList<>();
|
||||
|
||||
for (int turn = 1; turn <= MAX_SIMULATED_INTERACTIONS; turn++) {
|
||||
String simulatedMessage = generateSimulatorMessage(simulatorProvider, simulatorDescriptor, simulatorAuthorization,
|
||||
resolvedGoal, inputs, history, turn);
|
||||
history.add(formatConversationLine(ChatMessage.Role.USER, simulatedMessage));
|
||||
|
||||
List<ChatMessage> messages = history.stream().map(this::parseHistoryLine).collect(Collectors.toList());
|
||||
String assistantResponse = chatAuthorization == null
|
||||
? chatProvider.chat(chatDescriptor.model(), List.copyOf(messages))
|
||||
: chatProvider.chat(chatDescriptor.model(), List.copyOf(messages), chatAuthorization);
|
||||
history.add(formatConversationLine(ChatMessage.Role.ASSISTANT, assistantResponse));
|
||||
}
|
||||
|
||||
String finalResponse = generateSimulatorFinalResponse(simulatorProvider, simulatorDescriptor, simulatorAuthorization,
|
||||
resolvedGoal, inputs, history);
|
||||
return Map.of(
|
||||
ChatInteractionBlockFactory.RESPONSE_OUTPUT, finalResponse,
|
||||
ChatInteractionBlockFactory.HISTORY_OUTPUT, List.copyOf(history));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -45,7 +93,8 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
|
||||
@Override
|
||||
public InteractionResult interact(Block<ChatInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> interaction, Map<String, Object> partialResults, Map<String, Object> authorizations) {
|
||||
Map<String, Object> interaction, Map<String, Object> partialResults, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
ChatInteractionBlockConfiguration configuration =
|
||||
(ChatInteractionBlockConfiguration) block.getSpecificConfiguration();
|
||||
LLMDescriptor llmDescriptor = configuration.getLlmDescriptor();
|
||||
|
|
@ -63,7 +112,7 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
+ ChatInteractionBlockFactory.INTERACTION_FIELD);
|
||||
}
|
||||
|
||||
String resolvedMessage = resolvePlaceholders(message, inputs);
|
||||
String resolvedMessage = resolvePlaceholders(message, inputs, executionVariables);
|
||||
List<String> history = existingHistory(partialResults);
|
||||
List<ChatMessage> messages = history.stream()
|
||||
.map(this::parseHistoryLine)
|
||||
|
|
@ -112,6 +161,75 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
return llmProvider;
|
||||
}
|
||||
|
||||
private String resolveAuthorization(LLMProvider provider, LLMDescriptor descriptor, Map<String, Object> authorizations) {
|
||||
if (!provider.requiresAuthorization()) {
|
||||
return null;
|
||||
}
|
||||
String authKey = provider.authorizationKey();
|
||||
if (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey)))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + descriptor.provider());
|
||||
}
|
||||
return String.valueOf(authorizations.get(authKey));
|
||||
}
|
||||
|
||||
private String generateSimulatorMessage(LLMProvider simulatorProvider, LLMDescriptor simulatorDescriptor,
|
||||
String simulatorAuthorization, String goalDescription, List<Input> inputs, List<String> history, int turn) {
|
||||
String prompt = """
|
||||
###SIMULATED_CHAT_MESSAGE###
|
||||
Goal:
|
||||
%s
|
||||
|
||||
Turn: %d/%d
|
||||
|
||||
Inputs:
|
||||
%s
|
||||
|
||||
Conversation so far:
|
||||
%s
|
||||
|
||||
Return only the next user message for the conversation.
|
||||
""".formatted(goalDescription, turn, MAX_SIMULATED_INTERACTIONS, formatInputs(inputs), formatHistory(history));
|
||||
String response = simulatorAuthorization == null
|
||||
? simulatorProvider.generate(simulatorDescriptor.model(), prompt)
|
||||
: simulatorProvider.generate(simulatorDescriptor.model(), prompt, simulatorAuthorization);
|
||||
if (!StringUtils.hasText(response)) {
|
||||
throw new IllegalArgumentException("Simulated ChatInteraction produced an empty message");
|
||||
}
|
||||
return stripDirective(response, "MESSAGE:");
|
||||
}
|
||||
|
||||
private String generateSimulatorFinalResponse(LLMProvider simulatorProvider, LLMDescriptor simulatorDescriptor,
|
||||
String simulatorAuthorization, String goalDescription, List<Input> inputs, List<String> history) {
|
||||
String prompt = """
|
||||
###SIMULATED_CHAT_FINAL###
|
||||
Goal:
|
||||
%s
|
||||
|
||||
Inputs:
|
||||
%s
|
||||
|
||||
Final conversation:
|
||||
%s
|
||||
|
||||
Return only the final response value that the simulated user would submit.
|
||||
""".formatted(goalDescription, formatInputs(inputs), formatHistory(history));
|
||||
String response = simulatorAuthorization == null
|
||||
? simulatorProvider.generate(simulatorDescriptor.model(), prompt)
|
||||
: simulatorProvider.generate(simulatorDescriptor.model(), prompt, simulatorAuthorization);
|
||||
if (!StringUtils.hasText(response)) {
|
||||
throw new IllegalArgumentException("Simulated ChatInteraction produced an empty final response");
|
||||
}
|
||||
return stripDirective(response, "FINAL:");
|
||||
}
|
||||
|
||||
private String stripDirective(String value, String directive) {
|
||||
String normalized = value == null ? "" : value.strip();
|
||||
if (normalized.startsWith(directive)) {
|
||||
return normalized.substring(directive.length()).strip();
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String formatConversationLine(ChatMessage.Role role, String content) {
|
||||
return "[" + role.name() + "] " + content;
|
||||
}
|
||||
|
|
@ -142,27 +260,24 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
return new ChatMessage(ChatMessage.Role.USER, line);
|
||||
}
|
||||
|
||||
private String resolvePlaceholders(String template, List<Input> inputs) {
|
||||
if (!StringUtils.hasText(template)) {
|
||||
return template;
|
||||
private String resolvePlaceholders(String template, List<Input> inputs, Map<String, Object> executionVariables) {
|
||||
return ExecutionTemplateResolver.resolve(template, inputs, executionVariables);
|
||||
}
|
||||
|
||||
private String formatInputs(List<Input> inputs) {
|
||||
if (inputs == null || inputs.isEmpty()) {
|
||||
return "(none)";
|
||||
}
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
for (Input input : inputs) {
|
||||
values.put(input.getDescriptor().getName(), formatInputValue(input.getValue()));
|
||||
}
|
||||
String resolved = template;
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
resolved = resolved.replace("${{" + entry.getKey() + "}}", entry.getValue());
|
||||
}
|
||||
return resolved;
|
||||
return inputs.stream()
|
||||
.map(input -> input.getDescriptor().getName() + "=" + formatInputValue(input.getValue()))
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
}
|
||||
|
||||
private String formatHistory(List<String> history) {
|
||||
return history == null || history.isEmpty() ? "(empty)" : String.join(System.lineSeparator(), history);
|
||||
}
|
||||
|
||||
private String formatInputValue(Object value) {
|
||||
if (value instanceof Collection<?> collection) {
|
||||
return collection.stream()
|
||||
.map(item -> item == null ? "null" : item.toString())
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
}
|
||||
return value == null ? "null" : value.toString();
|
||||
return ExecutionTemplateResolver.formatValue(value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import it.cnr.isti.workflow.manager.blocks.Block;
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionTemplateResolver;
|
||||
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;
|
||||
|
|
@ -44,13 +46,14 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<ConditionalBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
ConditionalBlockConfiguration config = (ConditionalBlockConfiguration) block.getSpecificConfiguration();
|
||||
Map<String, Object> inputValues = collectInputValues(inputs);
|
||||
boolean matches = config.isUseLlm()
|
||||
? evaluateWithLlm(config, inputValues, authorizations)
|
||||
: evaluateWithExpression(config.getCondition(), inputValues);
|
||||
String payload = resolvePlaceholders(config.getOutputTemplate(), inputValues);
|
||||
? evaluateWithLlm(config, inputValues, authorizations, executionVariables)
|
||||
: evaluateWithExpression(config.getCondition(), inputValues, executionVariables);
|
||||
String payload = resolvePlaceholders(config.getOutputTemplate(), inputValues, executionVariables);
|
||||
return matches
|
||||
? Map.of(ConditionalBlockFactory.TRUE_OUTPUT, payload)
|
||||
: Map.of(ConditionalBlockFactory.FALSE_OUTPUT, payload);
|
||||
|
|
@ -69,10 +72,12 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
return values;
|
||||
}
|
||||
|
||||
private boolean evaluateWithExpression(String expression, Map<String, Object> inputValues) {
|
||||
private boolean evaluateWithExpression(String expression, Map<String, Object> inputValues,
|
||||
Map<String, Object> executionVariables) {
|
||||
String normalizedExpression = normalizeExpression(expression);
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(inputValues);
|
||||
inputValues.forEach(context::setVariable);
|
||||
context.setVariable("vars", executionVariables == null ? Map.of() : executionVariables);
|
||||
Boolean result = expressionParser.parseExpression(normalizedExpression).getValue(context, Boolean.class);
|
||||
if (result == null) {
|
||||
throw new IllegalArgumentException("Checker expression did not resolve to a boolean value");
|
||||
|
|
@ -81,7 +86,7 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
}
|
||||
|
||||
private boolean evaluateWithLlm(ConditionalBlockConfiguration config, Map<String, Object> inputValues,
|
||||
Map<String, Object> authorizations) {
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables) {
|
||||
LLMDescriptor llmDescriptor = config.getLlmDescriptor();
|
||||
LLMProvider llmProvider = resolveProvider(llmDescriptor.provider());
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
|
|
@ -90,7 +95,7 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
|
||||
String prompt = buildLlmPrompt(config, inputValues);
|
||||
String prompt = buildLlmPrompt(config, inputValues, executionVariables);
|
||||
String response = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt, authorizations.get(authKey).toString())
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
|
|
@ -108,10 +113,11 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
.orElseThrow(() -> new IllegalArgumentException("Provider not found: " + providerName));
|
||||
}
|
||||
|
||||
private String buildLlmPrompt(ConditionalBlockConfiguration config, Map<String, Object> inputValues) {
|
||||
private String buildLlmPrompt(ConditionalBlockConfiguration config, Map<String, Object> inputValues,
|
||||
Map<String, Object> executionVariables) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(LLM_SYSTEM_PROMPT).append("\n");
|
||||
builder.append("Decision prompt: ").append(resolvePlaceholders(config.getPrompt(), inputValues)).append("\n");
|
||||
builder.append("Decision prompt: ").append(resolvePlaceholders(config.getPrompt(), inputValues, executionVariables)).append("\n");
|
||||
builder.append("Inputs: ").append(inputValues).append("\n");
|
||||
return builder.toString();
|
||||
}
|
||||
|
|
@ -126,12 +132,8 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
return buffer.toString();
|
||||
}
|
||||
|
||||
private String resolvePlaceholders(String template, Map<String, Object> inputValues) {
|
||||
String resolved = template;
|
||||
for (Map.Entry<String, Object> entry : inputValues.entrySet()) {
|
||||
resolved = resolved.replace("${{" + entry.getKey() + "}}", entry.getValue() == null ? "null" : entry.getValue().toString());
|
||||
}
|
||||
return resolved;
|
||||
private String resolvePlaceholders(String template, Map<String, Object> inputValues, Map<String, Object> executionVariables) {
|
||||
return ExecutionTemplateResolver.resolve(template, inputValues, executionVariables);
|
||||
}
|
||||
|
||||
private boolean parseBooleanResponse(String response) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import it.cnr.isti.workflow.manager.blocks.Block;
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HTTPServerCallBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionTemplateResolver;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.http.HTTPServerCallService;
|
||||
|
||||
|
|
@ -22,10 +24,11 @@ public class HTTPServerCallExecutor implements BlockExecutor<HTTPServerCallBlock
|
|||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<HTTPServerCallBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
HTTPServerCallBlockConfiguration config = (HTTPServerCallBlockConfiguration) block.getSpecificConfiguration();
|
||||
String resolvedUrl = resolveTemplate(config.getUrl(), inputs);
|
||||
String resolvedRequestBody = resolveTemplate(config.getRequestBody(), inputs);
|
||||
String resolvedUrl = resolveTemplate(config.getUrl(), inputs, executionVariables);
|
||||
String resolvedRequestBody = resolveTemplate(config.getRequestBody(), inputs, executionVariables);
|
||||
String authorizationValue = null;
|
||||
if (Boolean.TRUE.equals(config.getRequiresAuthorization())) {
|
||||
String authorizationKey = config.authorizationKey(block.getId());
|
||||
|
|
@ -44,16 +47,7 @@ public class HTTPServerCallExecutor implements BlockExecutor<HTTPServerCallBlock
|
|||
return HTTPServerCallBlockType.class;
|
||||
}
|
||||
|
||||
private String resolveTemplate(String template, List<Input> inputs) {
|
||||
if (!StringUtils.hasText(template)) {
|
||||
return template;
|
||||
}
|
||||
String resolved = template;
|
||||
for (Input input : inputs) {
|
||||
Object value = input.getValue();
|
||||
resolved = resolved.replace("${{" + input.getDescriptor().getName() + "}}",
|
||||
value == null ? "null" : value.toString());
|
||||
}
|
||||
return resolved;
|
||||
private String resolveTemplate(String template, List<Input> inputs, Map<String, Object> executionVariables) {
|
||||
return ExecutionTemplateResolver.resolve(template, inputs, executionVariables);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import org.springframework.util.StringUtils;
|
|||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
|
||||
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;
|
||||
|
|
@ -25,16 +26,28 @@ public class HumanInteractionExecutor implements BlockExecutor<HumanInteractionB
|
|||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<HumanInteractionBlockType> block, List<Input> inputs, Map<String, Object> authorizations) {
|
||||
public Map<String, Object> execute(Block<HumanInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
throw new UnsupportedOperationException("HumanInteraction blocks require user interaction unless execution simulation is enabled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> simulate(Block<HumanInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
LLMDescriptor simulatorDescriptor) {
|
||||
HumanInteractiveBlockConfiguration config = (HumanInteractiveBlockConfiguration) block.getSpecificConfiguration();
|
||||
//TODO: consider multi input cases
|
||||
String context = inputs.stream()
|
||||
.map(input -> "%s= %s".formatted(input.getDescriptor().getName(), input.getValue()))
|
||||
.collect(Collectors.joining(", "));
|
||||
String prompt = "giving the following context as input { %s } perform the Task : '%s'"
|
||||
.formatted(context, config.getActionDescription());
|
||||
logger.debug("Generated prompt for Human Interaction Block '{}': {}", block.getName(), prompt);
|
||||
LLMDescriptor llmDescriptor = config.getSimulateWith();
|
||||
LLMDescriptor llmDescriptor = simulatorDescriptor;
|
||||
if (llmDescriptor == null) {
|
||||
throw new IllegalArgumentException("Missing simulation descriptor for HumanInteraction execution");
|
||||
}
|
||||
LLMProvider llmProvider = llmProviders.get(llmDescriptor.provider());
|
||||
if (llmProvider == null) {
|
||||
llmProvider = llmProviders.values().stream()
|
||||
|
|
@ -71,14 +84,10 @@ public class HumanInteractionExecutor implements BlockExecutor<HumanInteractionB
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> simulate(Block<HumanInteractionBlockType> block, List<Input> inputs, Map<String, Object> authorizations) {
|
||||
throw new UnsupportedOperationException("HumanInteractionExecutor does not support simulation.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult interact(Block<HumanInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> interaction, Map<String, Object> partialResults, Map<String, Object> authorizations) {
|
||||
Map<String, Object> interaction, Map<String, Object> partialResults, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
Object value = interaction.get("output");
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("Missing interaction value for field: output");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package it.cnr.isti.workflow.manager.executions.executors.blocks;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -15,6 +13,8 @@ import it.cnr.isti.workflow.manager.blocks.Block;
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.LLMBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionTemplateResolver;
|
||||
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;
|
||||
|
|
@ -28,21 +28,19 @@ public class LLMExecutor implements BlockExecutor<LLMBlockType> {
|
|||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<LLMBlockType> block, List<Input> inputs, Map<String, Object> authorizations) {
|
||||
public Map<String, Object> execute(Block<LLMBlockType> block, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
logger.info("Executing LLM Block: {}", block.getName());
|
||||
LLMBlockConfiguration config = (LLMBlockConfiguration) block.getSpecificConfiguration();
|
||||
String prompt = config.getPrompt();
|
||||
|
||||
if (prompt != null)
|
||||
for (Input input : inputs) {
|
||||
prompt = prompt.replaceAll("\\$\\{\\{" + input.getDescriptor().getName() + "\\}\\}",
|
||||
formatInputValue(input.getValue()));
|
||||
}
|
||||
else {
|
||||
if (prompt != null) {
|
||||
prompt = ExecutionTemplateResolver.resolve(prompt, inputs, executionVariables);
|
||||
} else {
|
||||
// If no prompt is defined in the configuration, use the first input's value as the prompt
|
||||
if (inputs.isEmpty())
|
||||
throw new IllegalArgumentException("No inputs provided for LLM block and no prompt defined in configuration");
|
||||
prompt = formatInputValue(inputs.getFirst().getValue());
|
||||
prompt = ExecutionTemplateResolver.formatValue(inputs.getFirst().getValue());
|
||||
}
|
||||
|
||||
LLMDescriptor llmDescriptor = config.getLlmDescriptor();
|
||||
|
|
@ -76,13 +74,4 @@ public class LLMExecutor implements BlockExecutor<LLMBlockType> {
|
|||
return LLMBlockType.class;
|
||||
}
|
||||
|
||||
private String formatInputValue(Object value) {
|
||||
if (value instanceof Collection<?> collection) {
|
||||
return collection.stream()
|
||||
.map(item -> item == null ? "null" : item.toString())
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
}
|
||||
return value == null ? "null" : value.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,22 +16,86 @@ import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfigura
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentChatBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.MCPAgentChatBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentChatBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionTemplateResolver;
|
||||
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;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPAgentService;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPSharedSessionRegistry;
|
||||
|
||||
@Component
|
||||
public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType> {
|
||||
|
||||
private static final String SESSION_ID_STATE = "__sessionId";
|
||||
private static final int MAX_SIMULATED_INTERACTIONS = 5;
|
||||
|
||||
@Autowired
|
||||
private MCPAgentService mcpAgentService;
|
||||
|
||||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<MCPAgentChatBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
throw new UnsupportedOperationException("MCPAgentChat blocks require user interaction.");
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
throw new UnsupportedOperationException("MCPAgentChat blocks require user interaction unless execution simulation is enabled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> simulate(Block<MCPAgentChatBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
LLMDescriptor simulatorDescriptor) {
|
||||
MCPAgentChatBlockConfiguration configuration =
|
||||
(MCPAgentChatBlockConfiguration) block.getSpecificConfiguration();
|
||||
if (!StringUtils.hasText(configuration.getGoalDescription())) {
|
||||
throw new IllegalArgumentException("Simulated MCPAgentChat requires goalDescription");
|
||||
}
|
||||
|
||||
String model = Boolean.TRUE.equals(configuration.getUseSharedSession())
|
||||
? null
|
||||
: resolveConfigurableInput(MCPAgentChatBlockFactory.MODEL_INPUT, configuration.getModel(), inputs);
|
||||
|
||||
if (simulatorDescriptor == null) {
|
||||
throw new IllegalArgumentException("Missing simulation descriptor for MCPAgentChat execution");
|
||||
}
|
||||
LLMProvider simulatorProvider = resolveProvider(simulatorDescriptor.provider());
|
||||
String simulatorAuthorization = resolveAuthorization(simulatorProvider, simulatorDescriptor, authorizations);
|
||||
String resolvedGoal = resolvePlaceholders(configuration.getGoalDescription(), inputs, executionVariables);
|
||||
List<String> history = new ArrayList<>();
|
||||
String sharedSessionKey = normalize(Boolean.TRUE.equals(configuration.getUseSharedSession())
|
||||
? configuration.getSharedSessionRef()
|
||||
: null);
|
||||
String shareSessionAs = normalize(Boolean.TRUE.equals(configuration.getShareSession())
|
||||
? configuration.getSharedSessionName()
|
||||
: null);
|
||||
String sessionId = resolveOrCreateSession(block, configuration, executionVariables, executionVariableDescriptors, model,
|
||||
sharedSessionKey, shareSessionAs);
|
||||
boolean managedSharedSession = isManagedSharedSession(configuration, executionVariableDescriptors);
|
||||
|
||||
try {
|
||||
for (int turn = 1; turn <= MAX_SIMULATED_INTERACTIONS; turn++) {
|
||||
String simulatedMessage = generateSimulatorMessage(simulatorProvider, simulatorDescriptor, simulatorAuthorization,
|
||||
resolvedGoal, inputs, history, turn);
|
||||
history.add(formatConversationLine("USER", simulatedMessage));
|
||||
|
||||
String assistantResponse = mcpAgentService.querySession(sessionId, simulatedMessage);
|
||||
history.add(formatConversationLine("ASSISTANT", assistantResponse));
|
||||
}
|
||||
|
||||
String finalResponse = generateSimulatorFinalResponse(simulatorProvider, simulatorDescriptor, simulatorAuthorization,
|
||||
resolvedGoal, inputs, history);
|
||||
return Map.of(
|
||||
MCPAgentChatBlockFactory.RESPONSE_OUTPUT, finalResponse,
|
||||
MCPAgentChatBlockFactory.HISTORY_OUTPUT, List.copyOf(history));
|
||||
} finally {
|
||||
if (!managedSharedSession) {
|
||||
mcpAgentService.closeSessionQuietly(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -46,7 +110,8 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
|
||||
@Override
|
||||
public InteractionResult interact(Block<MCPAgentChatBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> interaction, Map<String, Object> partialResults, Map<String, Object> authorizations) {
|
||||
Map<String, Object> interaction, Map<String, Object> partialResults, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
MCPAgentChatBlockConfiguration configuration =
|
||||
(MCPAgentChatBlockConfiguration) block.getSpecificConfiguration();
|
||||
|
||||
|
|
@ -57,16 +122,22 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
+ MCPAgentChatBlockFactory.INTERACTION_FIELD);
|
||||
}
|
||||
|
||||
String resolvedMessage = resolvePlaceholders(message, inputs);
|
||||
String model = resolveConfigurableInput(MCPAgentChatBlockFactory.MODEL_INPUT, configuration.getModel(), inputs);
|
||||
if (!StringUtils.hasText(model)) {
|
||||
throw new IllegalArgumentException("MCPAgentChat requires a model either from configuration or runtime input");
|
||||
}
|
||||
String resolvedMessage = resolvePlaceholders(message, inputs, executionVariables);
|
||||
String model = Boolean.TRUE.equals(configuration.getUseSharedSession())
|
||||
? null
|
||||
: resolveConfigurableInput(MCPAgentChatBlockFactory.MODEL_INPUT, configuration.getModel(), inputs);
|
||||
|
||||
String existingSessionId = existingSessionId(partialResults);
|
||||
boolean createdSession = false;
|
||||
if (!StringUtils.hasText(existingSessionId)) {
|
||||
existingSessionId = mcpAgentService.openSession(model, mapServers(configuration.getMcpServers()));
|
||||
existingSessionId = resolveOrCreateSession(block, configuration, executionVariables, executionVariableDescriptors,
|
||||
model,
|
||||
normalize(Boolean.TRUE.equals(configuration.getUseSharedSession())
|
||||
? configuration.getSharedSessionRef()
|
||||
: null),
|
||||
normalize(Boolean.TRUE.equals(configuration.getShareSession())
|
||||
? configuration.getSharedSessionName()
|
||||
: null));
|
||||
createdSession = true;
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +154,7 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
MCPAgentChatBlockFactory.HISTORY_OUTPUT, List.copyOf(updatedHistory),
|
||||
SESSION_ID_STATE, existingSessionId));
|
||||
} catch (RuntimeException ex) {
|
||||
if (createdSession) {
|
||||
if (createdSession && !isManagedSharedSession(configuration, executionVariableDescriptors)) {
|
||||
mcpAgentService.closeSessionQuietly(existingSessionId);
|
||||
}
|
||||
throw ex;
|
||||
|
|
@ -96,7 +167,9 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
throw new IllegalArgumentException("Missing interaction value for field: "
|
||||
+ MCPAgentChatBlockFactory.FINAL_RESPONSE_FIELD);
|
||||
}
|
||||
mcpAgentService.closeSessionQuietly(existingSessionId(partialResults));
|
||||
if (!isManagedSharedSession(configuration, executionVariableDescriptors)) {
|
||||
mcpAgentService.closeSessionQuietly(existingSessionId(partialResults));
|
||||
}
|
||||
List<String> history = existingHistory(partialResults);
|
||||
return InteractionResult.completed(Map.of(
|
||||
MCPAgentChatBlockFactory.RESPONSE_OUTPUT, response,
|
||||
|
|
@ -108,8 +181,93 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
|
||||
@Override
|
||||
public void cancel(Block<MCPAgentChatBlockType> block, List<Input> inputs, Map<String, Object> partialResults,
|
||||
Map<String, Object> authorizations) {
|
||||
mcpAgentService.closeSessionQuietly(existingSessionId(partialResults));
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
MCPAgentChatBlockConfiguration configuration =
|
||||
(MCPAgentChatBlockConfiguration) block.getSpecificConfiguration();
|
||||
if (!isManagedSharedSession(configuration, executionVariableDescriptors)) {
|
||||
mcpAgentService.closeSessionQuietly(existingSessionId(partialResults));
|
||||
}
|
||||
}
|
||||
|
||||
private LLMProvider resolveProvider(String providerName) {
|
||||
LLMProvider provider = llmProviders.get(providerName);
|
||||
if (provider != null) {
|
||||
return provider;
|
||||
}
|
||||
return llmProviders.values().stream()
|
||||
.filter(candidate -> candidate.getName().equals(providerName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Provider not found: " + providerName));
|
||||
}
|
||||
|
||||
private String resolveAuthorization(LLMProvider provider, LLMDescriptor descriptor, Map<String, Object> authorizations) {
|
||||
if (!provider.requiresAuthorization()) {
|
||||
return null;
|
||||
}
|
||||
String authKey = provider.authorizationKey();
|
||||
if (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey)))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + descriptor.provider());
|
||||
}
|
||||
return String.valueOf(authorizations.get(authKey));
|
||||
}
|
||||
|
||||
private String generateSimulatorMessage(LLMProvider simulatorProvider, LLMDescriptor simulatorDescriptor,
|
||||
String simulatorAuthorization, String goalDescription, List<Input> inputs, List<String> history, int turn) {
|
||||
String prompt = """
|
||||
###SIMULATED_CHAT_MESSAGE###
|
||||
Goal:
|
||||
%s
|
||||
|
||||
Turn: %d/%d
|
||||
|
||||
Inputs:
|
||||
%s
|
||||
|
||||
Conversation so far:
|
||||
%s
|
||||
|
||||
Return only the next user message for the conversation.
|
||||
""".formatted(goalDescription, turn, MAX_SIMULATED_INTERACTIONS, formatInputs(inputs), formatHistory(history));
|
||||
String response = simulatorAuthorization == null
|
||||
? simulatorProvider.generate(simulatorDescriptor.model(), prompt)
|
||||
: simulatorProvider.generate(simulatorDescriptor.model(), prompt, simulatorAuthorization);
|
||||
if (!StringUtils.hasText(response)) {
|
||||
throw new IllegalArgumentException("Simulated MCPAgentChat produced an empty message");
|
||||
}
|
||||
return stripDirective(response, "MESSAGE:");
|
||||
}
|
||||
|
||||
private String generateSimulatorFinalResponse(LLMProvider simulatorProvider, LLMDescriptor simulatorDescriptor,
|
||||
String simulatorAuthorization, String goalDescription, List<Input> inputs, List<String> history) {
|
||||
String prompt = """
|
||||
###SIMULATED_CHAT_FINAL###
|
||||
Goal:
|
||||
%s
|
||||
|
||||
Inputs:
|
||||
%s
|
||||
|
||||
Final conversation:
|
||||
%s
|
||||
|
||||
Return only the final response value that the simulated user would submit.
|
||||
""".formatted(goalDescription, formatInputs(inputs), formatHistory(history));
|
||||
String response = simulatorAuthorization == null
|
||||
? simulatorProvider.generate(simulatorDescriptor.model(), prompt)
|
||||
: simulatorProvider.generate(simulatorDescriptor.model(), prompt, simulatorAuthorization);
|
||||
if (!StringUtils.hasText(response)) {
|
||||
throw new IllegalArgumentException("Simulated MCPAgentChat produced an empty final response");
|
||||
}
|
||||
return stripDirective(response, "FINAL:");
|
||||
}
|
||||
|
||||
private String stripDirective(String value, String directive) {
|
||||
String normalized = value == null ? "" : value.strip();
|
||||
if (normalized.startsWith(directive)) {
|
||||
return normalized.substring(directive.length()).strip();
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
@ -126,10 +284,62 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private String resolveOrCreateSession(Block<MCPAgentChatBlockType> block, MCPAgentChatBlockConfiguration configuration,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
String model, String sharedSessionKey, String shareSessionAs) {
|
||||
String existingSharedSessionId = MCPSharedSessionRegistry.getSessionId(executionVariableDescriptors, sharedSessionKey);
|
||||
if (StringUtils.hasText(existingSharedSessionId)) {
|
||||
if (StringUtils.hasText(shareSessionAs)) {
|
||||
MCPSharedSessionRegistry.registerSharedSession(executionVariableDescriptors, shareSessionAs, existingSharedSessionId,
|
||||
block.getId());
|
||||
executionVariables.put(shareSessionAs, existingSharedSessionId);
|
||||
}
|
||||
return existingSharedSessionId;
|
||||
}
|
||||
if (StringUtils.hasText(sharedSessionKey)) {
|
||||
throw new IllegalArgumentException("Shared MCP session not found: " + sharedSessionKey);
|
||||
}
|
||||
String sessionId = mcpAgentService.openSession(model, mapServers(configuration.getMcpServers()));
|
||||
if (StringUtils.hasText(shareSessionAs)) {
|
||||
MCPSharedSessionRegistry.registerSharedSession(executionVariableDescriptors, shareSessionAs, sessionId, block.getId());
|
||||
executionVariables.put(shareSessionAs, sessionId);
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
private boolean isManagedSharedSession(MCPAgentChatBlockConfiguration configuration,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
String sharedSessionKey = normalize(Boolean.TRUE.equals(configuration.getUseSharedSession())
|
||||
? configuration.getSharedSessionRef()
|
||||
: null);
|
||||
String shareSessionAs = normalize(Boolean.TRUE.equals(configuration.getShareSession())
|
||||
? configuration.getSharedSessionName()
|
||||
: null);
|
||||
return MCPSharedSessionRegistry.isManagedSharedSession(executionVariableDescriptors, sharedSessionKey)
|
||||
|| MCPSharedSessionRegistry.isManagedSharedSession(executionVariableDescriptors, shareSessionAs);
|
||||
}
|
||||
|
||||
private String formatConversationLine(String role, String content) {
|
||||
return "[" + role + "] " + content;
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
private String formatInputs(List<Input> inputs) {
|
||||
if (inputs == null || inputs.isEmpty()) {
|
||||
return "(none)";
|
||||
}
|
||||
return inputs.stream()
|
||||
.map(input -> input.getDescriptor().getName() + "=" + formatInputValue(input.getValue()))
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
}
|
||||
|
||||
private String formatHistory(List<String> history) {
|
||||
return history == null || history.isEmpty() ? "(empty)" : String.join(System.lineSeparator(), history);
|
||||
}
|
||||
|
||||
private String resolveConfigurableInput(String inputName, String configuredValue, List<Input> inputs) {
|
||||
return inputs.stream()
|
||||
.filter(input -> inputName.equals(input.getDescriptor().getName()))
|
||||
|
|
@ -154,27 +364,11 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
.toList();
|
||||
}
|
||||
|
||||
private String resolvePlaceholders(String template, List<Input> inputs) {
|
||||
if (!StringUtils.hasText(template)) {
|
||||
return template;
|
||||
}
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
for (Input input : inputs) {
|
||||
values.put(input.getDescriptor().getName(), formatInputValue(input.getValue()));
|
||||
}
|
||||
String resolved = template;
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
resolved = resolved.replace("${{" + entry.getKey() + "}}", entry.getValue());
|
||||
}
|
||||
return resolved;
|
||||
private String resolvePlaceholders(String template, List<Input> inputs, Map<String, Object> executionVariables) {
|
||||
return ExecutionTemplateResolver.resolve(template, inputs, executionVariables);
|
||||
}
|
||||
|
||||
private String formatInputValue(Object value) {
|
||||
if (value instanceof Collection<?> collection) {
|
||||
return collection.stream()
|
||||
.map(item -> item == null ? "null" : item.toString())
|
||||
.collect(Collectors.joining(System.lineSeparator()));
|
||||
}
|
||||
return value == null ? "null" : value.toString();
|
||||
return ExecutionTemplateResolver.formatValue(value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import it.cnr.isti.workflow.manager.blocks.Block;
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.MCPAgentBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionTemplateResolver;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPAgentService;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPSharedSessionRegistry;
|
||||
|
||||
@Component
|
||||
public class MCPBridgeExecutor implements BlockExecutor<MCPAgentBlockType> {
|
||||
|
|
@ -22,14 +25,50 @@ public class MCPBridgeExecutor implements BlockExecutor<MCPAgentBlockType> {
|
|||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<MCPAgentBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
MCPAgentBlockConfiguration config = (MCPAgentBlockConfiguration) block.getSpecificConfiguration();
|
||||
String model = resolveConfigurableInput("model", config.getModel(), inputs);
|
||||
String prompt = resolvePrompt(config.getPrompt(), inputs);
|
||||
if (!StringUtils.hasText(model)) {
|
||||
throw new IllegalArgumentException("MCPAgent requires a model either from configuration or runtime input");
|
||||
String prompt = resolvePrompt(config.getPrompt(), inputs, executionVariables);
|
||||
String model = Boolean.TRUE.equals(config.getUseSharedSession())
|
||||
? null
|
||||
: resolveConfigurableInput("model", config.getModel(), inputs);
|
||||
String configuredSharedSessionKey = normalize(Boolean.TRUE.equals(config.getUseSharedSession())
|
||||
? config.getSharedSessionRef()
|
||||
: null);
|
||||
String shareSessionAs = normalize(Boolean.TRUE.equals(config.getShareSession())
|
||||
? config.getSharedSessionName()
|
||||
: null);
|
||||
String existingSharedSessionId = MCPSharedSessionRegistry.getSessionId(executionVariableDescriptors, configuredSharedSessionKey);
|
||||
|
||||
String response;
|
||||
if (StringUtils.hasText(existingSharedSessionId)) {
|
||||
response = mcpAgentService.querySession(existingSharedSessionId, prompt);
|
||||
if (StringUtils.hasText(shareSessionAs)) {
|
||||
MCPSharedSessionRegistry.registerSharedSession(executionVariableDescriptors, shareSessionAs, existingSharedSessionId,
|
||||
block.getId());
|
||||
executionVariables.put(shareSessionAs, existingSharedSessionId);
|
||||
}
|
||||
} else if (StringUtils.hasText(configuredSharedSessionKey)) {
|
||||
throw new IllegalArgumentException("Shared MCP session not found: " + configuredSharedSessionKey);
|
||||
} else if (StringUtils.hasText(shareSessionAs)) {
|
||||
if (!StringUtils.hasText(model)) {
|
||||
throw new IllegalArgumentException("MCPAgent requires a model either from configuration or runtime input");
|
||||
}
|
||||
String sessionId = mcpAgentService.openSession(model, config.getMcpServers());
|
||||
try {
|
||||
response = mcpAgentService.querySession(sessionId, prompt);
|
||||
MCPSharedSessionRegistry.registerSharedSession(executionVariableDescriptors, shareSessionAs, sessionId, block.getId());
|
||||
executionVariables.put(shareSessionAs, sessionId);
|
||||
} catch (RuntimeException ex) {
|
||||
mcpAgentService.closeSessionQuietly(sessionId);
|
||||
throw ex;
|
||||
}
|
||||
} else {
|
||||
if (!StringUtils.hasText(model)) {
|
||||
throw new IllegalArgumentException("MCPAgent requires a model either from configuration or runtime input");
|
||||
}
|
||||
response = mcpAgentService.execute(model, prompt, config.getMcpServers());
|
||||
}
|
||||
String response = mcpAgentService.execute(model, prompt, config.getMcpServers());
|
||||
return Map.of(MCPAgentBlockFactory.OUTPUT_NAME, response);
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +77,7 @@ public class MCPBridgeExecutor implements BlockExecutor<MCPAgentBlockType> {
|
|||
return MCPAgentBlockType.class;
|
||||
}
|
||||
|
||||
private String resolvePrompt(String template, List<Input> inputs) {
|
||||
private String resolvePrompt(String template, List<Input> inputs, Map<String, Object> executionVariables) {
|
||||
if (!StringUtils.hasText(template)) {
|
||||
return inputs.stream()
|
||||
.filter(input -> MCPAgentBlockFactory.INPUT_NAME.equals(input.getDescriptor().getName()))
|
||||
|
|
@ -49,13 +88,7 @@ public class MCPBridgeExecutor implements BlockExecutor<MCPAgentBlockType> {
|
|||
? null
|
||||
: inputs.getFirst().getValue().toString());
|
||||
}
|
||||
String resolved = template;
|
||||
for (Input input : inputs) {
|
||||
Object value = input.getValue();
|
||||
resolved = resolved.replace("${{" + input.getDescriptor().getName() + "}}",
|
||||
value == null ? "null" : value.toString());
|
||||
}
|
||||
return resolved;
|
||||
return ExecutionTemplateResolver.resolve(template, inputs, executionVariables);
|
||||
}
|
||||
|
||||
private String resolveConfigurableInput(String inputName, String configuredValue, List<Input> inputs) {
|
||||
|
|
@ -67,4 +100,9 @@ public class MCPBridgeExecutor implements BlockExecutor<MCPAgentBlockType> {
|
|||
.filter(StringUtils::hasText)
|
||||
.orElse(configuredValue);
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import java.util.Map;
|
|||
|
||||
import it.cnr.isti.workflow.manager.containers.Container;
|
||||
import it.cnr.isti.workflow.manager.containers.types.ContainerType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
|
||||
public interface ContainerExecutor<T extends ContainerType> {
|
||||
|
||||
Map<String, Object> execute(Container<T> container, List<Input> inputs, Map<String, Object> authorizations);
|
||||
Map<String, Object> execute(Container<T> container, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors);
|
||||
|
||||
Class<T> getContainerType();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import it.cnr.isti.workflow.manager.containers.iresolvers.ContainerFlowInterface
|
|||
import it.cnr.isti.workflow.manager.containers.types.GenericContainerType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionObject;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionStatus;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionsService;
|
||||
import it.cnr.isti.workflow.manager.executions.FieldKey;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
|
|
@ -24,7 +25,9 @@ public class GenericContainerExecutor implements ContainerExecutor<GenericContai
|
|||
private ExecutionsService executionsService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Container<GenericContainerType> container, List<Input> inputs, Map<String, Object> authorizations) {
|
||||
public Map<String, Object> execute(Container<GenericContainerType> container, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
GenericContainerConfiguration configuration = (GenericContainerConfiguration) container.getSpecificConfiguration();
|
||||
if (configuration.getSubFlow() == null
|
||||
|| ((configuration.getSubFlow().getBlocks() == null || configuration.getSubFlow().getBlocks().isEmpty())
|
||||
|
|
@ -35,6 +38,7 @@ public class GenericContainerExecutor implements ContainerExecutor<GenericContai
|
|||
ExecutionObject innerExecution = executionsService.createExecution(container.getName() + " subflow", configuration.getSubFlow());
|
||||
|
||||
propagateAuthorizations(innerExecution, authorizations);
|
||||
executionsService.setExecutionVariableDescriptors(innerExecution.getId(), executionVariableDescriptors);
|
||||
|
||||
Map<String, ContainerFlowInterfaceResolver.ExposedHandle> inputPortsByName = ContainerFlowInterfaceResolver
|
||||
.getExposedInputs(configuration.getSubFlow()).stream()
|
||||
|
|
@ -68,6 +72,10 @@ public class GenericContainerExecutor implements ContainerExecutor<GenericContai
|
|||
if (innerExecution.getContext().getStatus() != ExecutionStatus.SUCCESS) {
|
||||
throw new IllegalStateException("GenericContainer subflow ended in unexpected status: " + innerExecution.getContext().getStatus());
|
||||
}
|
||||
executionVariables.clear();
|
||||
executionVariables.putAll(innerExecution.getContext().getExecutionVariables());
|
||||
executionVariableDescriptors.clear();
|
||||
executionVariableDescriptors.putAll(innerExecution.getContext().getExecutionVariableDescriptors());
|
||||
|
||||
Map<String, Object> outputs = new java.util.LinkedHashMap<>();
|
||||
for (ContainerFlowInterfaceResolver.ExposedHandle exposedHandle : ContainerFlowInterfaceResolver
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import it.cnr.isti.workflow.manager.containers.iresolvers.IteratorContainerInter
|
|||
import it.cnr.isti.workflow.manager.containers.types.IteratorContainerType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionObject;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionStatus;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionsService;
|
||||
import it.cnr.isti.workflow.manager.executions.FieldKey;
|
||||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
|
|
@ -28,7 +29,8 @@ public class IteratorContainerExecutor implements ContainerExecutor<IteratorCont
|
|||
|
||||
@Override
|
||||
public Map<String, Object> execute(Container<IteratorContainerType> container, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
Map<String, ExecutionVariableDescriptor> executionVariableDescriptors) {
|
||||
IteratorContainerConfiguration configuration = (IteratorContainerConfiguration) container.getSpecificConfiguration();
|
||||
IteratorContainerInterfaceResolver.Resolution resolution = IteratorContainerInterfaceResolver.resolvePorts(configuration);
|
||||
IteratorContainerInterfaceResolver.ResolvedInput iteratedInput = resolution.inputsByPublicName()
|
||||
|
|
@ -56,6 +58,7 @@ public class IteratorContainerExecutor implements ContainerExecutor<IteratorCont
|
|||
configuration.getSubFlow());
|
||||
|
||||
propagateAuthorizations(innerExecution, authorizations);
|
||||
executionsService.setExecutionVariableDescriptors(innerExecution.getId(), executionVariableDescriptors);
|
||||
|
||||
for (IteratorContainerInterfaceResolver.ResolvedInput resolvedInput : resolution.resolvedInputs()) {
|
||||
Object value = resolvedInput.iterated()
|
||||
|
|
@ -69,6 +72,10 @@ public class IteratorContainerExecutor implements ContainerExecutor<IteratorCont
|
|||
}
|
||||
|
||||
innerExecution = startAndWait(innerExecution);
|
||||
executionVariables.clear();
|
||||
executionVariables.putAll(innerExecution.getContext().getExecutionVariables());
|
||||
executionVariableDescriptors.clear();
|
||||
executionVariableDescriptors.putAll(innerExecution.getContext().getExecutionVariableDescriptors());
|
||||
|
||||
for (IteratorContainerInterfaceResolver.ResolvedOutput resolvedOutput : resolution.resolvedOutputs()) {
|
||||
Object value = innerExecution.getContext().getResult()
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package it.cnr.isti.workflow.manager.executions.persistence;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionStatus;
|
||||
import it.cnr.isti.workflow.manager.executions.FieldKey;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
|
@ -19,6 +21,10 @@ public class ExecutionSnapshot {
|
|||
private ExecutionStatus status;
|
||||
private Long startTime;
|
||||
private Long endTime;
|
||||
private boolean interactionSimulationEnabled;
|
||||
private LLMDescriptor interactionSimulationDescriptor;
|
||||
private Map<String, Object> executionVariables;
|
||||
private Map<String, ExecutionVariableDescriptor> executionVariableDescriptors;
|
||||
private Map<String, Object> providedAuthorizations;
|
||||
private Map<FieldKey, Object> inputs;
|
||||
private Map<FieldKey, Object> result;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import it.cnr.isti.workflow.manager.executions.persistence.ExecutionInputSnapsho
|
|||
import it.cnr.isti.workflow.manager.executions.persistence.ExecutionStepSnapshot;
|
||||
import it.cnr.isti.workflow.manager.executions.executors.NodeExecutors;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowNode;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
|
@ -66,6 +67,18 @@ public class Step<N extends FlowNode> implements InputListener {
|
|||
@Getter
|
||||
private Map<String, Object> authorizations = Map.of();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private Map<String, Object> executionVariables = Map.of();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private Map<String, it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor> executionVariableDescriptors = Map.of();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private LLMDescriptor interactionSimulationDescriptor;
|
||||
|
||||
@Builder
|
||||
public Step(@NonNull N node) {
|
||||
// Initialize the step with the provided block
|
||||
|
|
@ -130,7 +143,11 @@ public class Step<N extends FlowNode> implements InputListener {
|
|||
this.status = StepStatus.RUNNING;
|
||||
listener.started(this.id);
|
||||
try {
|
||||
Map<String, Object> outputs = NodeExecutors.execute(this.node, this.inputs, authorizations);
|
||||
Map<String, Object> outputs = isSimulated() && this.node.isUserInteractive()
|
||||
? NodeExecutors.simulate(this.node, this.inputs, authorizations, executionVariables,
|
||||
executionVariableDescriptors, this.interactionSimulationDescriptor)
|
||||
: NodeExecutors.execute(this.node, this.inputs, authorizations, executionVariables,
|
||||
executionVariableDescriptors);
|
||||
for (Output output : this.outputs) {
|
||||
if (outputs.containsKey(output.getDescriptor().getName())) {
|
||||
output.setValue(outputs.get(output.getDescriptor().getName()));
|
||||
|
|
@ -157,7 +174,8 @@ public class Step<N extends FlowNode> implements InputListener {
|
|||
logger.info("Resuming step " + this.id + " of node " + this.node.getName());
|
||||
this.status = StepStatus.RUNNING;
|
||||
listener.resumed(this.id);
|
||||
var interactionResult = NodeExecutors.interact(this.node, this.inputs, providedOutputs, Map.copyOf(this.partialResults), authorizations);
|
||||
var interactionResult = NodeExecutors.interact(this.node, this.inputs, providedOutputs,
|
||||
Map.copyOf(this.partialResults), authorizations, executionVariables, executionVariableDescriptors);
|
||||
this.partialResults.clear();
|
||||
this.partialResults.putAll(interactionResult.partialResults());
|
||||
listener.partialUpdated(this.id, this.partialResults);
|
||||
|
|
@ -224,7 +242,8 @@ public class Step<N extends FlowNode> implements InputListener {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
NodeExecutors.cancel(this.node, this.inputs, Map.copyOf(this.partialResults), this.authorizations);
|
||||
NodeExecutors.cancel(this.node, this.inputs, Map.copyOf(this.partialResults), this.authorizations,
|
||||
this.executionVariables, this.executionVariableDescriptors);
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Failed to cancel resources for step {}", this.id, ex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package it.cnr.isti.workflow.manager.flows.validation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
|
@ -10,8 +13,11 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentChatBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.containers.Container;
|
||||
import it.cnr.isti.workflow.manager.containers.configurations.ContainerConfiguration;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Connection;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowNode;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
|
|
@ -108,6 +114,122 @@ public class FlowExecutionValidator {
|
|||
}
|
||||
}
|
||||
|
||||
if (flowData != null) {
|
||||
errors.addAll(validateSharedExecutionVariableOrdering(flowData));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private List<ValidationError> validateSharedExecutionVariableOrdering(FlowData flowData) {
|
||||
List<ValidationError> errors = new ArrayList<>();
|
||||
Map<String, List<String>> producerIdsByVariable = new HashMap<>();
|
||||
|
||||
for (FlowNode node : flowData.getNodes()) {
|
||||
if (!(node instanceof Block<?> block) || block.getSpecificConfiguration() == null) {
|
||||
continue;
|
||||
}
|
||||
String producedName = sharedSessionProducedName(block);
|
||||
if (producedName != null) {
|
||||
producerIdsByVariable.computeIfAbsent(producedName, ignored -> new ArrayList<>())
|
||||
.add(block.getId());
|
||||
}
|
||||
}
|
||||
|
||||
producerIdsByVariable.forEach((variableName, producerIds) -> {
|
||||
if (producerIds.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
producerIds.forEach(producerId -> errors.add(new ValidationError(
|
||||
"block",
|
||||
producerId,
|
||||
"specificConfiguration.sharedSessionName",
|
||||
"Shared MCP session name '" + variableName + "' must be unique within the flow")));
|
||||
});
|
||||
|
||||
if (producerIdsByVariable.isEmpty()) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
Map<String, List<String>> outgoing = buildOutgoingGraph(flowData.getConnections());
|
||||
for (FlowNode node : flowData.getNodes()) {
|
||||
if (!(node instanceof Block<?> block) || block.getSpecificConfiguration() == null) {
|
||||
continue;
|
||||
}
|
||||
String referencedName = sharedSessionReferencedName(block);
|
||||
if (referencedName == null) {
|
||||
continue;
|
||||
}
|
||||
List<String> producerIds = producerIdsByVariable.getOrDefault(referencedName, List.of());
|
||||
boolean reachable = producerIds.stream()
|
||||
.anyMatch(producerId -> !producerId.equals(block.getId())
|
||||
&& isReachable(producerId, block.getId(), outgoing, new HashSet<>()));
|
||||
if (!reachable) {
|
||||
errors.add(new ValidationError(
|
||||
"block",
|
||||
block.getId(),
|
||||
"specificConfiguration.sharedSessionRef",
|
||||
"Shared MCP session '" + referencedName + "' must be produced by a block that executes before this block"));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private Map<String, List<String>> buildOutgoingGraph(List<Connection> connections) {
|
||||
Map<String, List<String>> outgoing = new HashMap<>();
|
||||
if (connections == null) {
|
||||
return outgoing;
|
||||
}
|
||||
for (Connection connection : connections) {
|
||||
if (connection == null || connection.getSourceId() == null || connection.getTargetId() == null) {
|
||||
continue;
|
||||
}
|
||||
outgoing.computeIfAbsent(connection.getSourceId(), ignored -> new ArrayList<>())
|
||||
.add(connection.getTargetId());
|
||||
}
|
||||
return outgoing;
|
||||
}
|
||||
|
||||
private boolean isReachable(String sourceId, String targetId, Map<String, List<String>> outgoing, Set<String> visited) {
|
||||
if (!visited.add(sourceId)) {
|
||||
return false;
|
||||
}
|
||||
for (String nextId : outgoing.getOrDefault(sourceId, List.of())) {
|
||||
if (targetId.equals(nextId) || isReachable(nextId, targetId, outgoing, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String sharedSessionProducedName(Block<?> block) {
|
||||
if (block.getSpecificConfiguration() instanceof MCPAgentBlockConfiguration configuration) {
|
||||
if (Boolean.TRUE.equals(configuration.getShareSession()) && hasText(configuration.getSharedSessionName())) {
|
||||
return configuration.getSharedSessionName().trim();
|
||||
}
|
||||
} else if (block.getSpecificConfiguration() instanceof MCPAgentChatBlockConfiguration configuration) {
|
||||
if (Boolean.TRUE.equals(configuration.getShareSession()) && hasText(configuration.getSharedSessionName())) {
|
||||
return configuration.getSharedSessionName().trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String sharedSessionReferencedName(Block<?> block) {
|
||||
if (block.getSpecificConfiguration() instanceof MCPAgentBlockConfiguration configuration) {
|
||||
if (Boolean.TRUE.equals(configuration.getUseSharedSession()) && hasText(configuration.getSharedSessionRef())) {
|
||||
return configuration.getSharedSessionRef().trim();
|
||||
}
|
||||
} else if (block.getSpecificConfiguration() instanceof MCPAgentChatBlockConfiguration configuration) {
|
||||
if (Boolean.TRUE.equals(configuration.getUseSharedSession()) && hasText(configuration.getSharedSessionRef())) {
|
||||
return configuration.getSharedSessionRef().trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package it.cnr.isti.workflow.manager.mcp;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableCleanupPolicy;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableDescriptor;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableKind;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableRegistry;
|
||||
|
||||
public final class MCPSharedSessionRegistry {
|
||||
|
||||
private MCPSharedSessionRegistry() {
|
||||
}
|
||||
|
||||
public static String getSessionId(Map<String, ExecutionVariableDescriptor> descriptors, String key) {
|
||||
ExecutionVariableDescriptor descriptor = getSessionDescriptor(descriptors, key);
|
||||
if (descriptor == null || descriptor.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
String sessionId = String.valueOf(descriptor.getValue()).trim();
|
||||
return sessionId.isEmpty() ? null : sessionId;
|
||||
}
|
||||
|
||||
public static ExecutionVariableDescriptor getSessionDescriptor(Map<String, ExecutionVariableDescriptor> descriptors, String key) {
|
||||
if (!StringUtils.hasText(key) || descriptors == null) {
|
||||
return null;
|
||||
}
|
||||
ExecutionVariableDescriptor descriptor = descriptors.get(key.trim());
|
||||
if (descriptor == null) {
|
||||
return null;
|
||||
}
|
||||
ExecutionVariableDescriptor normalized = ExecutionVariableRegistry.normalize(descriptor);
|
||||
return normalized.getKind() == ExecutionVariableKind.MCP_SESSION ? normalized : null;
|
||||
}
|
||||
|
||||
public static void registerSharedSession(Map<String, ExecutionVariableDescriptor> descriptors, String key, String sessionId,
|
||||
String producerBlockId) {
|
||||
if (!StringUtils.hasText(key) || !StringUtils.hasText(sessionId) || descriptors == null) {
|
||||
return;
|
||||
}
|
||||
ExecutionVariableDescriptor descriptor = ExecutionVariableDescriptor.builder()
|
||||
.name(key.trim())
|
||||
.kind(ExecutionVariableKind.MCP_SESSION)
|
||||
.value(sessionId)
|
||||
.producerBlockId(producerBlockId)
|
||||
.description("Shared MCP session")
|
||||
.cleanupPolicy(ExecutionVariableCleanupPolicy.CLOSE_RESOURCE)
|
||||
.build();
|
||||
descriptors.put(descriptor.getName(), descriptor);
|
||||
}
|
||||
|
||||
public static boolean isManagedSharedSession(Map<String, ExecutionVariableDescriptor> descriptors, String key) {
|
||||
return getSessionDescriptor(descriptors, key) != null;
|
||||
}
|
||||
|
||||
public static Set<String> sharedKeys(Map<String, ExecutionVariableDescriptor> descriptors) {
|
||||
if (descriptors == null || descriptors.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
descriptors.values().stream()
|
||||
.map(ExecutionVariableRegistry::normalize)
|
||||
.filter(descriptor -> descriptor.getKind() == ExecutionVariableKind.MCP_SESSION)
|
||||
.forEach(descriptor -> keys.add(descriptor.getName()));
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
}
|
||||
|
|
@ -105,6 +105,7 @@ public class BlockTest {
|
|||
MCPAgentChatBlockConfiguration config = MCPAgentChatBlockConfiguration.builder()
|
||||
.name("agent-chat-1")
|
||||
.model("llama3.1:8b")
|
||||
.goalDescription("Assess the candidate")
|
||||
.inputs(List.of(new ChatInteractionInput("candidate", IOType.TEXT, false)))
|
||||
.mcpServers(List.of(
|
||||
MCPAgentChatBlockConfiguration.MCPServerBinding.builder()
|
||||
|
|
|
|||
|
|
@ -340,7 +340,6 @@ public class AssistantControllerTest {
|
|||
HumanInteractiveBlockConfiguration reviewConfiguration = HumanInteractiveBlockConfiguration.builder()
|
||||
.name("Human review")
|
||||
.actionDescription("Review high-risk tickets")
|
||||
.simulateWith(llmDescriptor)
|
||||
.build();
|
||||
|
||||
Block<HumanInteractionBlockType> reviewBlock = Block.<HumanInteractionBlockType>builder()
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ import java.util.List;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.IOCapabilityType;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ChatInteractionBlockConfiguration;
|
||||
|
|
@ -32,6 +34,11 @@ import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
|
|||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentChatBlockType;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiContextKeys;
|
||||
import it.cnr.isti.workflow.manager.configurations.retrievers.ExecutionVariablesFieldRetriever;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowView;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
|
||||
|
|
@ -43,6 +50,19 @@ public class BlocksControllerTest {
|
|||
@Autowired
|
||||
private BlocksController blocksController;
|
||||
|
||||
@Autowired
|
||||
private FlowController flowController;
|
||||
|
||||
@Autowired
|
||||
private ExecutionVariablesFieldRetriever executionVariablesFieldRetriever;
|
||||
|
||||
private LLMDescriptor simulatorDescriptor() {
|
||||
return LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("simulateModel")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTypes(){
|
||||
List<BlockConfigurationDescriptor> types = blocksController.getTypes();
|
||||
|
|
@ -92,6 +112,7 @@ public class BlocksControllerTest {
|
|||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build())
|
||||
.goalDescription("Assess the candidate")
|
||||
.inputs(List.of(new ChatInteractionInput("candidate", it.cnr.isti.workflow.manager.ios.IOType.TEXT)))
|
||||
.build();
|
||||
|
||||
|
|
@ -124,6 +145,7 @@ public class BlocksControllerTest {
|
|||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build())
|
||||
.goalDescription("Assess the candidate")
|
||||
.inputs(List.of(new ChatInteractionInput("candidate", null)))
|
||||
.build();
|
||||
|
||||
|
|
@ -148,6 +170,7 @@ public class BlocksControllerTest {
|
|||
"provider": "testProvider",
|
||||
"model": "testModel"
|
||||
},
|
||||
"goalDescription": "Assess the candidate",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "candidate",
|
||||
|
|
@ -184,6 +207,7 @@ public class BlocksControllerTest {
|
|||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build())
|
||||
.goalDescription("Assess the candidate")
|
||||
.inputs(List.of(
|
||||
new ChatInteractionInput("candidate", it.cnr.isti.workflow.manager.ios.IOType.TEXT),
|
||||
new ChatInteractionInput("candidate", it.cnr.isti.workflow.manager.ios.IOType.TEXT, true)))
|
||||
|
|
@ -202,6 +226,7 @@ public class BlocksControllerTest {
|
|||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build())
|
||||
.goalDescription("Assess the candidate")
|
||||
.inputs(List.of(new ChatInteractionInput("candidate", it.cnr.isti.workflow.manager.ios.IOType.FILE)))
|
||||
.build();
|
||||
|
||||
|
|
@ -215,6 +240,7 @@ public class BlocksControllerTest {
|
|||
MCPAgentChatBlockConfiguration config = MCPAgentChatBlockConfiguration.builder()
|
||||
.name("Agent chat")
|
||||
.model("llama3.1:8b")
|
||||
.goalDescription("Assess the candidate")
|
||||
.inputs(List.of(new ChatInteractionInput("candidate", it.cnr.isti.workflow.manager.ios.IOType.TEXT)))
|
||||
.mcpServers(List.of(MCPAgentChatBlockConfiguration.MCPServerBinding.builder()
|
||||
.serverName("filesystem")
|
||||
|
|
@ -231,6 +257,179 @@ public class BlocksControllerTest {
|
|||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("history") && output.isMultiple()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMcpAgentChatBlockAllowsPartialConfigurationForUiDraft() {
|
||||
MCPAgentChatBlockConfiguration config = MCPAgentChatBlockConfiguration.builder()
|
||||
.name("Agent chat")
|
||||
.model("llama3.1:8b")
|
||||
.build();
|
||||
|
||||
Block<MCPAgentChatBlockType> block = blocksController.create(config);
|
||||
|
||||
assertNotNull(block);
|
||||
assertEquals(MCPAgentChatBlockType.TYPE, block.getType().getName());
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response") && !output.isMultiple()));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("history") && output.isMultiple()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionVariablesRetrieverReturnsSharedMcpSessionsForFlow() {
|
||||
Block<MCPAgentBlockType> block = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Shared agent")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Find data for ${{candidate}}")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(
|
||||
new FlowCreateRequest(
|
||||
"Shared Session Flow",
|
||||
"Flow with MCP shared session",
|
||||
FlowData.builder().block(block).build()),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
List<it.cnr.isti.workflow.manager.configurations.retrievers.SecureDynamicFieldRetriever.RetrieverItem> items =
|
||||
executionVariablesFieldRetriever.retrieve(
|
||||
"shared",
|
||||
java.util.Map.of("flowId", createdFlow.getBody().id(), "kind", "MCP_SESSION"),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
assertEquals(1, items.size());
|
||||
assertEquals("candidateResearchSession", items.getFirst().data());
|
||||
assertEquals("MCP_SESSION", items.getFirst().descriptor().meta().get("kind"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionVariablesRetrieverUsesDraftReplacementForUnsavedBlockUpdates() {
|
||||
Block<MCPAgentBlockType> initialBlock = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Shared agent")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Find data for ${{candidate}}")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(
|
||||
new FlowCreateRequest(
|
||||
"Shared Session Flow",
|
||||
"Flow with MCP shared session",
|
||||
FlowData.builder().block(initialBlock).build()),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
Block<MCPAgentBlockType> updatedBlock = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Shared agent")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Find data for ${{candidate}}")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("renamedSharedSession")
|
||||
.build(),
|
||||
createdFlow.getBody().id(),
|
||||
initialBlock.getId());
|
||||
|
||||
List<it.cnr.isti.workflow.manager.configurations.retrievers.SecureDynamicFieldRetriever.RetrieverItem> items =
|
||||
executionVariablesFieldRetriever.retrieve(
|
||||
"shared",
|
||||
java.util.Map.of("flowId", createdFlow.getBody().id(), "kind", "MCP_SESSION"),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
assertEquals(1, items.size());
|
||||
assertEquals(updatedBlock.getId(), items.getFirst().descriptor().meta().get("producerBlockId"));
|
||||
assertEquals("renamedSharedSession", items.getFirst().data());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionVariablesRetrieverDeduplicatesSharedSessionNamesPreferringLatestDraft() {
|
||||
Block<MCPAgentBlockType> persistedBlock = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Persisted agent")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Find data for ${{candidate}}")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(
|
||||
new FlowCreateRequest(
|
||||
"Shared Session Flow",
|
||||
"Flow with MCP shared session",
|
||||
FlowData.builder().block(persistedBlock).build()),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
Block<MCPAgentBlockType> draftBlock = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Draft agent")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Find more data for ${{candidate}}")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build(),
|
||||
createdFlow.getBody().id(),
|
||||
null);
|
||||
|
||||
List<it.cnr.isti.workflow.manager.configurations.retrievers.SecureDynamicFieldRetriever.RetrieverItem> items =
|
||||
executionVariablesFieldRetriever.retrieve(
|
||||
"shared",
|
||||
java.util.Map.of("flowId", createdFlow.getBody().id(), "kind", "MCP_SESSION"),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
assertEquals(1, items.size());
|
||||
assertEquals("candidateResearchSession", items.getFirst().data());
|
||||
assertEquals(draftBlock.getId(), items.getFirst().descriptor().meta().get("producerBlockId"));
|
||||
assertEquals("Draft agent", items.getFirst().descriptor().meta().get("producerBlockName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMcpAgentBlockAllowsPartialConfigurationForUiDraft() {
|
||||
MCPAgentBlockConfiguration config = MCPAgentBlockConfiguration.builder()
|
||||
.name("Agent")
|
||||
.shareSession(true)
|
||||
.build();
|
||||
|
||||
Block<MCPAgentBlockType> block = blocksController.create(config);
|
||||
|
||||
assertNotNull(block);
|
||||
assertEquals(MCPAgentBlockType.TYPE, block.getType().getName());
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response") && !output.isMultiple()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcpAgentSchemaMarksSharedSessionRetrieverAsAuthenticated() {
|
||||
BlockConfigurationDescriptor descriptor = blocksController
|
||||
.getConfigurationDescriptorForType(MCPAgentBlockType.TYPE);
|
||||
|
||||
JsonNode schema = (JsonNode) descriptor.schema();
|
||||
JsonNode properties = schema.path("properties");
|
||||
JsonNode shareSession = properties.path("shareSession");
|
||||
JsonNode sharedSessionName = properties.path("sharedSessionName");
|
||||
JsonNode useSharedSession = properties.path("useSharedSession");
|
||||
JsonNode sharedSessionRef = schema.path("properties").path("sharedSessionRef");
|
||||
|
||||
assertEquals("boolean", shareSession.path("type").asText());
|
||||
assertTrue(shareSession.path("x-ui-structural").asBoolean());
|
||||
assertTrue(sharedSessionName.path("x-ui-structural").asBoolean());
|
||||
assertTrue(useSharedSession.path("x-ui-structural").asBoolean());
|
||||
assertTrue(sharedSessionRef.path("x-ui-structural").asBoolean());
|
||||
assertEquals("shareSession", sharedSessionName.path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("true", sharedSessionName.path("x-ui-enabled-when").path("equals").asText());
|
||||
assertEquals("shareSession", sharedSessionName.path("x-ui-required-when").path("field").asText());
|
||||
assertEquals("true", sharedSessionName.path("x-ui-required-when").path("equals").asText());
|
||||
assertEquals("shareSession", useSharedSession.path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("false", useSharedSession.path("x-ui-enabled-when").path("equals").asText());
|
||||
assertEquals("useSharedSession", sharedSessionRef.path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("true", sharedSessionRef.path("x-ui-enabled-when").path("equals").asText());
|
||||
assertEquals("useSharedSession", sharedSessionRef.path("x-ui-required-when").path("field").asText());
|
||||
assertEquals("true", sharedSessionRef.path("x-ui-required-when").path("equals").asText());
|
||||
assertEquals("useSharedSession", properties.path("model").path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("false", properties.path("model").path("x-ui-enabled-when").path("equals").asText());
|
||||
assertFalse(properties.path("prompt").has("x-ui-enabled-when"));
|
||||
assertEquals("useSharedSession", properties.path("mcpServers").path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("false", properties.path("mcpServers").path("x-ui-enabled-when").path("equals").asText());
|
||||
assertEquals("/secure-retriever/ExecutionVariables/shared/items?kind=MCP_SESSION",
|
||||
sharedSessionRef.path("x-retriever-url").asText());
|
||||
assertEquals(UiContextKeys.FLOW_ID,
|
||||
sharedSessionRef.path("x-retriever-depends-on").get(0).asText());
|
||||
assertTrue(sharedSessionRef.path("x-retriever-requires-auth").asBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcpAgentChatDescriptorExposesInteractionContract() {
|
||||
BlockConfigurationDescriptor descriptor = blocksController
|
||||
|
|
@ -446,6 +645,37 @@ public class BlocksControllerTest {
|
|||
assertEquals("filesystem", ((MCPAgentBlockConfiguration) block.getSpecificConfiguration()).getMcpServers().getFirst().serverName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMcpAgentBlockUsingSharedSessionDoesNotExposeModelInput() {
|
||||
MCPAgentBlockConfiguration config = MCPAgentBlockConfiguration.builder()
|
||||
.name("Bridge")
|
||||
.prompt("Bridge prompt for ${{candidate}}")
|
||||
.useSharedSession(true)
|
||||
.sharedSessionRef("sharedSession")
|
||||
.build();
|
||||
|
||||
Block<MCPAgentBlockType> block = blocksController.create(config);
|
||||
|
||||
assertNotNull(block);
|
||||
assertFalse(block.getInputs().stream().anyMatch(input -> input.getName().equals("model")));
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("candidate")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMcpAgentChatBlockUsingSharedSessionDoesNotExposeModelInput() {
|
||||
MCPAgentChatBlockConfiguration config = MCPAgentChatBlockConfiguration.builder()
|
||||
.name("Agent chat")
|
||||
.goalDescription("Assess the candidate")
|
||||
.useSharedSession(true)
|
||||
.sharedSessionRef("sharedSession")
|
||||
.build();
|
||||
|
||||
Block<MCPAgentChatBlockType> block = blocksController.create(config);
|
||||
|
||||
assertNotNull(block);
|
||||
assertFalse(block.getInputs().stream().anyMatch(input -> input.getName().equals("model")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHTTPServerCallBlock() {
|
||||
HTTPServerCallBlockConfiguration config = HTTPServerCallBlockConfiguration.builder()
|
||||
|
|
@ -491,12 +721,12 @@ public class BlocksControllerTest {
|
|||
assertTrue(method.has("enum"));
|
||||
assertEquals("textarea", requestBody.path("x-ui-widget").asText());
|
||||
assertTrue(requestBody.path("x-ui-structural").asBoolean());
|
||||
assertEquals("method", requestBody.path("x-ui-visible-when").path("field").asText());
|
||||
assertTrue(requestBody.path("x-ui-visible-when").path("in").isArray());
|
||||
assertTrue(requestBody.path("x-ui-visible-when").path("in").toString().contains("POST"));
|
||||
assertTrue(requestBody.path("x-ui-visible-when").path("in").toString().contains("PUT"));
|
||||
assertEquals("requiresAuthorization", authType.path("x-ui-visible-when").path("field").asText());
|
||||
assertEquals("authorizationType", authHeader.path("x-ui-visible-when").path("field").asText());
|
||||
assertEquals("method", requestBody.path("x-ui-enabled-when").path("field").asText());
|
||||
assertTrue(requestBody.path("x-ui-enabled-when").path("in").isArray());
|
||||
assertTrue(requestBody.path("x-ui-enabled-when").path("in").toString().contains("POST"));
|
||||
assertTrue(requestBody.path("x-ui-enabled-when").path("in").toString().contains("PUT"));
|
||||
assertEquals("requiresAuthorization", authType.path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("authorizationType", authHeader.path("x-ui-enabled-when").path("field").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package it.cnr.isti.workflow.manager.controllers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
|
|
@ -15,11 +19,15 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
|||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ChatInteractionBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ChatInteractionInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ChatInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionObject;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionSimulationRequest;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionStatus;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionAuthorizationValueRequest;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionsService;
|
||||
|
|
@ -28,7 +36,9 @@ import it.cnr.isti.workflow.manager.flows.model.Flow;
|
|||
import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowView;
|
||||
import it.cnr.isti.workflow.manager.llms.ChatMessage;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
|
|
@ -36,6 +46,41 @@ public class ExecutionControllerTest {
|
|||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExecutionControllerTest.class);
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public LLMProvider testProvider() {
|
||||
return new LLMProvider() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "testProvider";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRegisteredModels() {
|
||||
return List.of("testModel", "simulateModel");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String model, String prompt) {
|
||||
if (model.contains("simulate") && prompt.contains("###SIMULATED_CHAT_MESSAGE###")) {
|
||||
return "MESSAGE: Please continue the interview";
|
||||
}
|
||||
if (model.contains("simulate") && prompt.contains("###SIMULATED_CHAT_FINAL###")) {
|
||||
return "FINAL: Candidate approved";
|
||||
}
|
||||
return "Hello, " + prompt + "!";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(String model, List<ChatMessage> messages) {
|
||||
return "Chat, " + model + "!";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private BlocksController blocksController;
|
||||
|
||||
|
|
@ -159,7 +204,6 @@ public class ExecutionControllerTest {
|
|||
Block<HumanInteractionBlockType> reviewBlock = blocksController.create(HumanInteractiveBlockConfiguration.builder()
|
||||
.name("Recruiter review")
|
||||
.actionDescription("Validate the CV assessment")
|
||||
.simulateWith(llmDescriptor)
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
|
|
@ -233,7 +277,6 @@ public class ExecutionControllerTest {
|
|||
Block<HumanInteractionBlockType> reviewBlock = blocksController.create(HumanInteractiveBlockConfiguration.builder()
|
||||
.name("Recruiter review")
|
||||
.actionDescription("Validate candidate")
|
||||
.simulateWith(llmDescriptor)
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
|
|
@ -270,7 +313,6 @@ public class ExecutionControllerTest {
|
|||
Block<HumanInteractionBlockType> reviewBlock = blocksController.create(HumanInteractiveBlockConfiguration.builder()
|
||||
.name("Recruiter review")
|
||||
.actionDescription("Validate candidate")
|
||||
.simulateWith(llmDescriptor)
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
|
|
@ -299,6 +341,79 @@ public class ExecutionControllerTest {
|
|||
waitForExecutionStatus(resumed, ExecutionStatus.SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simulateExecutionStartsInteractiveNodesInSimulationMode() {
|
||||
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build();
|
||||
LLMDescriptor simulatorDescriptor = LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("simulateModel")
|
||||
.build();
|
||||
|
||||
Block<ChatInteractionBlockType> chatBlock = blocksController.create(ChatInteractionBlockConfiguration.builder()
|
||||
.name("Recruiter chat")
|
||||
.llmDescriptor(llmDescriptor)
|
||||
.goalDescription("Assess ${{cand}} and produce a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", it.cnr.isti.workflow.manager.ios.IOType.TEXT)))
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
"Simulated chat flow",
|
||||
"Flow to test simulation endpoint",
|
||||
FlowData.builder().block(chatBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionObject executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(executionObject.isSimulationAvailable());
|
||||
|
||||
executionsController.prepareStringInputs(executionObject.getId(), chatBlock.getId(),
|
||||
chatBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
|
||||
executionObject = executionsController.simulate(executionObject.getId(),
|
||||
new ExecutionSimulationRequest(simulatorDescriptor));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(executionObject.isInteractionSimulationEnabled());
|
||||
waitForExecutionStatus(executionObject, ExecutionStatus.SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simulateExecutionRejectsFlowsWithoutSimulableSteps() {
|
||||
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build();
|
||||
|
||||
Block<LLMBlockType> llmBlock = blocksController.create(LLMBlockConfiguration.builder()
|
||||
.name("Analysis")
|
||||
.llmDescriptor(llmDescriptor)
|
||||
.prompt("Analyze ${{candidate}}")
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
"Non simulable flow",
|
||||
"Flow without interactive steps",
|
||||
FlowData.builder().block(llmBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionObject executionObject = executionsController.create(createdFlow.getBody().id());
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertFalse(executionObject.isSimulationAvailable());
|
||||
|
||||
executionsController.prepareStringInputs(executionObject.getId(), llmBlock.getId(),
|
||||
llmBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
|
||||
ResponseStatusException exception = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> executionsController.simulate(executionObject.getId(),
|
||||
new ExecutionSimulationRequest(LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("simulateModel")
|
||||
.build())));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.READY, executionObject.getContext().getStatus());
|
||||
}
|
||||
|
||||
private void waitForExecutionStatus(ExecutionObject executionObject, ExecutionStatus expectedStatus) {
|
||||
long deadline = System.currentTimeMillis() + 5_000;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
|||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ConditionalBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ConditionalBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ConditionalBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.containers.Container;
|
||||
import it.cnr.isti.workflow.manager.containers.configurations.IteratorContainerConfiguration;
|
||||
import it.cnr.isti.workflow.manager.containers.types.IteratorContainerType;
|
||||
|
|
@ -587,6 +589,75 @@ public class FlowControllerTest {
|
|||
assertEquals(FlowViewStatus.DRAFT, createResponse.getBody().status());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFlowWithSharedSessionConsumerBeforeProducerReturnsDraftStatus() {
|
||||
Block<MCPAgentBlockType> producer = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Producer")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Start MCP research")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
Block<MCPAgentBlockType> consumer = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Consumer")
|
||||
.prompt("Continue with the shared session")
|
||||
.useSharedSession(true)
|
||||
.sharedSessionRef("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
"Invalid shared session ordering",
|
||||
"Consumer is not reachable after the producer",
|
||||
FlowData.builder()
|
||||
.block(producer)
|
||||
.block(consumer)
|
||||
.build());
|
||||
|
||||
ResponseEntity<FlowView> createResponse = flowController.createFlow(
|
||||
request,
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
assertTrue(createResponse.getStatusCode().is2xxSuccessful());
|
||||
assertNotNull(createResponse.getBody());
|
||||
assertEquals(FlowViewStatus.DRAFT, createResponse.getBody().status());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFlowWithDuplicateSharedSessionProducerNamesReturnsDraftStatus() {
|
||||
Block<MCPAgentBlockType> firstProducer = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Producer 1")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Start MCP research")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
Block<MCPAgentBlockType> secondProducer = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Producer 2")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Start MCP research again")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
"Duplicate shared session names",
|
||||
"Two MCP producers must not expose the same shared session name",
|
||||
FlowData.builder()
|
||||
.block(firstProducer)
|
||||
.block(secondProducer)
|
||||
.build());
|
||||
|
||||
ResponseEntity<FlowView> createResponse = flowController.createFlow(
|
||||
request,
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
assertTrue(createResponse.getStatusCode().is2xxSuccessful());
|
||||
assertNotNull(createResponse.getBody());
|
||||
assertEquals(FlowViewStatus.DRAFT, createResponse.getBody().status());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFlowRejectsConditionalBranchMerge() {
|
||||
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
|
|||
import it.cnr.isti.workflow.manager.blocks.configurations.ChatInteractionBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.ChatInteractionInput;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentChatBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.containers.Container;
|
||||
import it.cnr.isti.workflow.manager.containers.configurations.ContainerConfiguration;
|
||||
|
|
@ -48,25 +49,35 @@ 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.LLMBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.ChatInteractionBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HumanInteractiveBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HTTPServerCallBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.LLMBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.MCPAgentBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.MCPAgentChatBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.ChatInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentChatBlockType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionVariableKind;
|
||||
import it.cnr.isti.workflow.manager.ios.IODescriptor;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
import it.cnr.isti.workflow.manager.llms.ChatMessage;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPAgentService;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Connection;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
public class ExecutionTest {
|
||||
|
||||
private static final AtomicReference<List<ChatMessage>> lastChatMessages = new AtomicReference<>(List.of());
|
||||
private static final LLMDescriptor SIMULATOR_DESCRIPTOR = LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("simulateModel")
|
||||
.build();
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
|
@ -85,7 +96,13 @@ public class ExecutionTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String generate(String prompt, String model) {
|
||||
public String generate(String model, String prompt) {
|
||||
if (model.contains("simulate") && prompt.contains("###SIMULATED_CHAT_MESSAGE###")) {
|
||||
return "MESSAGE: Please continue the interview";
|
||||
}
|
||||
if (model.contains("simulate") && prompt.contains("###SIMULATED_CHAT_FINAL###")) {
|
||||
return "FINAL: Candidate approved";
|
||||
}
|
||||
return "Hello, " + prompt.replace("Hello, ", "").replace("!", "") + "!";
|
||||
|
||||
}
|
||||
|
|
@ -114,6 +131,12 @@ public class ExecutionTest {
|
|||
@Autowired
|
||||
MCPAgentChatBlockFactory mcpAgentChatBlockFactory;
|
||||
|
||||
@Autowired
|
||||
MCPAgentBlockFactory mcpAgentBlockFactory;
|
||||
|
||||
@Autowired
|
||||
HumanInteractiveBlockFactory humanInteractiveBlockFactory;
|
||||
|
||||
@Autowired
|
||||
LLMBlockType llmBlockType;
|
||||
|
||||
|
|
@ -171,6 +194,7 @@ public class ExecutionTest {
|
|||
Block<ChatInteractionBlockType> chatBlock = chatInteractionBlockFactory.create(ChatInteractionBlockConfiguration.builder()
|
||||
.name("Recruiter Chat")
|
||||
.llmDescriptor(llmBrick)
|
||||
.goalDescription("Assess ${{cand}} and reach a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", IOType.TEXT, false)))
|
||||
.build());
|
||||
|
||||
|
|
@ -222,11 +246,43 @@ public class ExecutionTest {
|
|||
assertTrue(execObject.getContext().getPartialResult().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simulatedChatInteractionExecutionCompletesAfterFiveTurns() {
|
||||
Block<ChatInteractionBlockType> chatBlock = chatInteractionBlockFactory.create(ChatInteractionBlockConfiguration.builder()
|
||||
.name("Recruiter Chat")
|
||||
.llmDescriptor(llmBrick)
|
||||
.goalDescription("Assess ${{cand}} and reach a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", IOType.TEXT, false)))
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("Simulated Chat flow")
|
||||
.description("Single simulated chat block")
|
||||
.block(chatBlock)
|
||||
.build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
execObject = executionsService.prepareInput(execObject.getId(), chatBlock.getId(), "cand", "John Doe");
|
||||
execObject = executionsService.startSimulationExecution(execObject.getId(), SIMULATOR_DESCRIPTOR);
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertEquals("Candidate approved",
|
||||
execObject.getContext().getResult().get(new FieldKey(chatBlock.getId(), ChatInteractionBlockFactory.RESPONSE_OUTPUT)));
|
||||
Object history = execObject.getContext().getResult()
|
||||
.get(new FieldKey(chatBlock.getId(), ChatInteractionBlockFactory.HISTORY_OUTPUT));
|
||||
assertTrue(history instanceof List<?>);
|
||||
assertEquals(10, ((List<?>) history).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatInteractionExecutionKeepsConversationHistoryAcrossMessages() {
|
||||
Block<ChatInteractionBlockType> chatBlock = chatInteractionBlockFactory.create(ChatInteractionBlockConfiguration.builder()
|
||||
.name("Recruiter Chat")
|
||||
.llmDescriptor(llmBrick)
|
||||
.goalDescription("Assess ${{cand}} and reach a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", IOType.TEXT, false)))
|
||||
.build());
|
||||
|
||||
|
|
@ -300,6 +356,7 @@ public class ExecutionTest {
|
|||
Block<MCPAgentChatBlockType> chatBlock = mcpAgentChatBlockFactory.create(MCPAgentChatBlockConfiguration.builder()
|
||||
.name("MCP Chat")
|
||||
.model("llama3.1:8b")
|
||||
.goalDescription("Assess ${{cand}} and reach a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", IOType.TEXT, false)))
|
||||
.mcpServers(List.of())
|
||||
.build());
|
||||
|
|
@ -362,6 +419,7 @@ public class ExecutionTest {
|
|||
Block<MCPAgentChatBlockType> chatBlock = mcpAgentChatBlockFactory.create(MCPAgentChatBlockConfiguration.builder()
|
||||
.name("MCP Chat")
|
||||
.model("llama3.1:8b")
|
||||
.goalDescription("Assess ${{cand}} and reach a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", IOType.TEXT, false)))
|
||||
.mcpServers(List.of())
|
||||
.build());
|
||||
|
|
@ -392,11 +450,112 @@ public class ExecutionTest {
|
|||
Mockito.verify(mcpAgentService, Mockito.times(1)).closeSessionQuietly("session-cancel");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simulatedMcpAgentChatExecutionCompletesAfterFiveTurns() {
|
||||
AtomicInteger queryCount = new AtomicInteger();
|
||||
Mockito.when(mcpAgentService.openSession(Mockito.eq("llama3.1:8b"), Mockito.anyList()))
|
||||
.thenReturn("session-simulated");
|
||||
Mockito.when(mcpAgentService.querySession(Mockito.eq("session-simulated"), Mockito.anyString()))
|
||||
.thenAnswer(invocation -> "MCP answer " + queryCount.incrementAndGet());
|
||||
|
||||
Block<MCPAgentChatBlockType> chatBlock = mcpAgentChatBlockFactory.create(MCPAgentChatBlockConfiguration.builder()
|
||||
.name("MCP Chat")
|
||||
.model("llama3.1:8b")
|
||||
.goalDescription("Assess ${{cand}} and reach a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", IOType.TEXT, false)))
|
||||
.mcpServers(List.of())
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("Simulated MCP Chat flow")
|
||||
.description("Single simulated MCP chat block")
|
||||
.block(chatBlock)
|
||||
.build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
execObject = executionsService.prepareInput(execObject.getId(), chatBlock.getId(), "cand", "John Doe");
|
||||
execObject = executionsService.startSimulationExecution(execObject.getId(), SIMULATOR_DESCRIPTOR);
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertEquals("Candidate approved",
|
||||
execObject.getContext().getResult().get(new FieldKey(chatBlock.getId(), MCPAgentChatBlockFactory.RESPONSE_OUTPUT)));
|
||||
Object history = execObject.getContext().getResult()
|
||||
.get(new FieldKey(chatBlock.getId(), MCPAgentChatBlockFactory.HISTORY_OUTPUT));
|
||||
assertTrue(history instanceof List<?>);
|
||||
assertEquals(10, ((List<?>) history).size());
|
||||
Mockito.verify(mcpAgentService, Mockito.times(1)).closeSessionQuietly("session-simulated");
|
||||
assertEquals(5, queryCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionVariablesResolveInLlmPromptAndPersistAcrossReload() {
|
||||
Block<LLMBlockType> llmBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
.name("Greeter")
|
||||
.prompt("Hello, ${{vars.person}} from ${{city}}!")
|
||||
.llmDescriptor(llmBrick)
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("Execution vars flow")
|
||||
.description("Single LLM block using execution variables")
|
||||
.block(llmBlock)
|
||||
.build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
executionsService.setExecutionVariable(execObject.getId(), "person", "Ada Lovelace");
|
||||
executionsService.prepareInput(execObject.getId(), llmBlock.getId(), "city", "Pisa");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertEquals("Hello, Ada Lovelace from Pisa!",
|
||||
execObject.getContext().getResult().get(new FieldKey(llmBlock.getId(), LLMBlockFactory.OUTPUT_NAME)));
|
||||
assertEquals("Ada Lovelace", execObject.getContext().getExecutionVariables().get("person"));
|
||||
|
||||
String executionId = execObject.getId();
|
||||
executionsService.clearInMemoryExecutions();
|
||||
ExecutionObject restored = executionsService.getExecution(executionId);
|
||||
assertEquals("Ada Lovelace", restored.getContext().getExecutionVariables().get("person"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelExecutionClearsExecutionVariables() {
|
||||
Block<HumanInteractionBlockType> reviewBlock = humanInteractiveBlockFactory.create(HumanInteractiveBlockConfiguration.builder()
|
||||
.name("Recruiter review")
|
||||
.actionDescription("Validate candidate")
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("Execution vars cancel flow")
|
||||
.description("Flow to test execution variables cleanup")
|
||||
.block(reviewBlock)
|
||||
.build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
executionsService.setExecutionVariable(execObject.getId(), "sharedSessionId", "session-123");
|
||||
executionsService.prepareInput(execObject.getId(), reviewBlock.getId(),
|
||||
reviewBlock.getInputs().getFirst().getName(), "Ada Lovelace");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.WAITING, execObject.getContext().getStatus());
|
||||
executionsService.cancelExecution(execObject.getId());
|
||||
assertTrue(execObject.getContext().getExecutionVariables().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistedWaitingExecutionCanBeRestoredAfterRestart() {
|
||||
Block<MCPAgentChatBlockType> chatBlock = mcpAgentChatBlockFactory.create(MCPAgentChatBlockConfiguration.builder()
|
||||
.name("MCP Chat")
|
||||
.model("llama3.1:8b")
|
||||
.goalDescription("Assess ${{cand}} and reach a final decision")
|
||||
.inputs(List.of(new ChatInteractionInput("cand", IOType.TEXT, false)))
|
||||
.mcpServers(List.of())
|
||||
.build());
|
||||
|
|
@ -446,6 +605,64 @@ public class ExecutionTest {
|
|||
Mockito.verify(mcpAgentService).querySession("session-resume", "Continue with John Doe");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcpAgentBlockCanShareSessionWithFollowingMcpAgentBlock() {
|
||||
Block<MCPAgentBlockType> producer = mcpAgentBlockFactory.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Research session")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Find data for ${{cand}}")
|
||||
.shareSession(true)
|
||||
.sharedSessionName("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
Block<MCPAgentBlockType> consumer = mcpAgentBlockFactory.create(MCPAgentBlockConfiguration.builder()
|
||||
.name("Reuse session")
|
||||
.model("llama3.1:8b")
|
||||
.prompt("Summarize next steps for ${{cand}}")
|
||||
.useSharedSession(true)
|
||||
.sharedSessionRef("candidateResearchSession")
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("Shared MCP session flow")
|
||||
.description("Two MCP agent blocks sharing one MCP session")
|
||||
.block(producer)
|
||||
.block(consumer)
|
||||
.connection(Connection.builder()
|
||||
.sourceId(producer.getId())
|
||||
.sourceName(MCPAgentBlockFactory.OUTPUT_NAME)
|
||||
.targetId(consumer.getId())
|
||||
.targetName("cand")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Mockito.when(mcpAgentService.openSession(Mockito.eq("llama3.1:8b"), Mockito.anyList()))
|
||||
.thenReturn("shared-session-1");
|
||||
Mockito.when(mcpAgentService.querySession("shared-session-1", "Find data for Ada"))
|
||||
.thenReturn("Ada");
|
||||
Mockito.when(mcpAgentService.querySession("shared-session-1", "Summarize next steps for Ada"))
|
||||
.thenReturn("Summary for Ada");
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
execObject = executionsService.prepareInput(execObject.getId(), producer.getId(), "cand", "Ada");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertEquals("shared-session-1", execObject.getContext().getExecutionVariables().get("candidateResearchSession"));
|
||||
assertNotNull(execObject.getContext().getExecutionVariableDescriptors().get("candidateResearchSession"));
|
||||
assertEquals(ExecutionVariableKind.MCP_SESSION,
|
||||
execObject.getContext().getExecutionVariableDescriptors().get("candidateResearchSession").getKind());
|
||||
assertEquals("Summary for Ada", execObject.getContext().getResult()
|
||||
.get(new FieldKey(consumer.getId(), MCPAgentBlockFactory.OUTPUT_NAME)));
|
||||
Mockito.verify(mcpAgentService).openSession(Mockito.eq("llama3.1:8b"), Mockito.anyList());
|
||||
Mockito.verify(mcpAgentService).querySession("shared-session-1", "Find data for Ada");
|
||||
Mockito.verify(mcpAgentService).querySession("shared-session-1", "Summarize next steps for Ada");
|
||||
Mockito.verify(mcpAgentService, Mockito.timeout(1000)).closeSessionQuietly("shared-session-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createInteractiveExecutionSetInputAndStart() {
|
||||
ExecutionObject eo = createInteractiveExecutionAndSetInputInternally();
|
||||
|
|
@ -514,7 +731,9 @@ public class ExecutionTest {
|
|||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertTrue(execObject.getContext().getResult().values().contains("Hello, testModel!"));
|
||||
Object finalResult = execObject.getContext().getResult().values().stream().findFirst().orElseThrow();
|
||||
assertTrue(finalResult instanceof String);
|
||||
assertTrue(((String) finalResult).contains("Senior Java engineer with AI workflow experience"));
|
||||
}
|
||||
|
||||
private void normalizeSeedFlowProviders(FlowData flowData) {
|
||||
|
|
@ -526,9 +745,11 @@ public class ExecutionTest {
|
|||
if (configuration instanceof LLMBlockConfiguration) {
|
||||
overwriteField(configuration, "llmDescriptor", llmBrick);
|
||||
} else if (configuration instanceof HumanInteractiveBlockConfiguration humanConfiguration) {
|
||||
if (humanConfiguration.getSimulateWith() != null) {
|
||||
overwriteField(configuration, "simulateWith", llmBrick);
|
||||
} else if (configuration instanceof ChatInteractionBlockConfiguration chatConfiguration) {
|
||||
if (chatConfiguration.getLlmDescriptor() != null) {
|
||||
overwriteField(configuration, "llmDescriptor", llmBrick);
|
||||
}
|
||||
} else if (configuration instanceof MCPAgentChatBlockConfiguration mcpChatConfiguration) {
|
||||
}
|
||||
}
|
||||
for (Container<?> container : flowData.getContainers() == null ? List.<Container<?>>of() : flowData.getContainers()) {
|
||||
|
|
@ -642,7 +863,7 @@ public class ExecutionTest {
|
|||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertEquals("Hello, testModel!", execObject.getContext().getResult().values().stream().findFirst().orElseThrow());
|
||||
assertEquals("Hello, Frank!", execObject.getContext().getResult().values().stream().findFirst().orElseThrow());
|
||||
assertTrue(execObject.getContext().getResult().keySet().stream()
|
||||
.anyMatch(key -> key.toString().equals(container.getId() + ":" + exposedOutputName)));
|
||||
}
|
||||
|
|
@ -679,7 +900,7 @@ public class ExecutionTest {
|
|||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
Object output = execObject.getContext().getResult().values().stream().findFirst().orElseThrow();
|
||||
assertTrue(output instanceof List<?>);
|
||||
assertEquals(List.of("Hello, testModel!", "Hello, testModel!"), output);
|
||||
assertEquals(List.of("Hello, Alice!", "Hello, Bob!"), output);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -136,11 +136,8 @@ public class ExecutionWithContainer {
|
|||
}
|
||||
}
|
||||
|
||||
execObject.getContext().getSteps().values().stream().filter(s -> s.getBlock().getType().isUserInteractive())
|
||||
.forEach(s -> s.setSimulated(true));
|
||||
|
||||
assertEquals(ExecutionStatus.READY, execObject.getContext().getStatus());
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
execObject = executionsService.startSimulationExecution(execObject.getId(), llmBrick);
|
||||
assertEquals(ExecutionStatus.RUNNING, execObject.getContext().getStatus());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ public class FlowTestCreator {
|
|||
Block<HumanInteractionBlockType> block2 = humanInteractiveBlockFactory.create(HumanInteractiveBlockConfiguration.builder()
|
||||
.actionDescription("Answer the question in input")
|
||||
.name("interactive")
|
||||
.simulateWith(llmBrick)
|
||||
.build());
|
||||
|
||||
Connection connection = Connection.builder()
|
||||
|
|
|
|||
Loading…
Reference in New Issue