Add JsonSchemaProducer, UiOrder annotation, and enhance LoopContainer and block configurations
This commit is contained in:
parent
a50c6d7802
commit
c88875c3ba
|
|
@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
|||
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.types.BlockType;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
|
@ -24,6 +25,7 @@ import lombok.NoArgsConstructor;
|
|||
public abstract class BlockConfiguration<T extends BlockType> {
|
||||
|
||||
@NotBlank
|
||||
@UiOrder(10)
|
||||
@JsonProperty(required = true)
|
||||
String name;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ public class ChatInteractionBlockConfiguration extends BlockConfiguration<ChatIn
|
|||
@Valid
|
||||
private List<ChatInteractionInput> inputs = List.of();
|
||||
|
||||
@Structural
|
||||
@JsonProperty(required = true)
|
||||
private boolean exposeHistory = true;
|
||||
|
||||
@Override
|
||||
public Class<ChatInteractionBlockType> getBlockType() {
|
||||
return ChatInteractionBlockType.class;
|
||||
|
|
@ -47,17 +51,20 @@ public class ChatInteractionBlockConfiguration extends BlockConfiguration<ChatIn
|
|||
@Builder
|
||||
public ChatInteractionBlockConfiguration(@NonNull String name, LLMDescriptor llmDescriptor,
|
||||
String goalDescription,
|
||||
List<ChatInteractionInput> inputs) {
|
||||
List<ChatInteractionInput> inputs,
|
||||
Boolean exposeHistory) {
|
||||
super(name);
|
||||
this.llmDescriptor = llmDescriptor;
|
||||
this.goalDescription = goalDescription;
|
||||
this.inputs = inputs == null ? List.of() : List.copyOf(inputs);
|
||||
this.exposeHistory = exposeHistory == null ? true : exposeHistory;
|
||||
}
|
||||
|
||||
public static ChatInteractionBlockConfiguration empty() {
|
||||
ChatInteractionBlockConfiguration configuration = new ChatInteractionBlockConfiguration();
|
||||
configuration.name = ChatInteractionBlockType.TYPE;
|
||||
configuration.inputs = List.of();
|
||||
configuration.exposeHistory = true;
|
||||
return configuration;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,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.UiEnabledWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;
|
||||
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,6 +23,12 @@ import lombok.NonNull;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ConditionalBlockConfiguration extends BlockConfiguration<ConditionalBlockType> {
|
||||
|
||||
@UiOrder(30)
|
||||
@JsonProperty(required = true)
|
||||
@Structural
|
||||
boolean useLlm;
|
||||
|
||||
@UiOrder(40)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "false")
|
||||
@Structural
|
||||
@LongText(
|
||||
|
|
@ -30,15 +37,13 @@ public class ConditionalBlockConfiguration extends BlockConfiguration<Conditiona
|
|||
acceptVariableAsPlaceholder = true)
|
||||
String condition;
|
||||
|
||||
@JsonProperty(required = true)
|
||||
@Structural
|
||||
boolean useLlm;
|
||||
|
||||
@Valid
|
||||
@UiOrder(50)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
LLMDescriptor llmDescriptor;
|
||||
|
||||
@UiOrder(60)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
@Structural
|
||||
|
|
@ -48,6 +53,7 @@ public class ConditionalBlockConfiguration extends BlockConfiguration<Conditiona
|
|||
acceptVariableAsPlaceholder = true)
|
||||
String prompt;
|
||||
|
||||
@UiOrder(70)
|
||||
@Structural
|
||||
@LongText(
|
||||
placeholder = "Define the output to propagate, for example: Approved request for ${{customer}}",
|
||||
|
|
|
|||
|
|
@ -5,9 +5,13 @@ import java.lang.reflect.ParameterizedType;
|
|||
import java.lang.reflect.RecordComponent;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Queue;
|
||||
|
|
@ -33,6 +37,7 @@ import it.cnr.isti.workflow.manager.configurations.annotations.UiDescription;
|
|||
import it.cnr.isti.workflow.manager.configurations.annotations.UiRequiredWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiEnabledWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiLabel;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOptionsFromNode;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiUniqueItemsBy;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
|
@ -57,6 +62,7 @@ public class JsonSchemaProducer {
|
|||
Map<Class<?>, Map<String, LongText>> longTextMap = collectLongTextMetadata(type);
|
||||
Map<Class<?>, Map<String, Structural>> structuralMap = collectStructuralMetadata(type);
|
||||
Map<Class<?>, Map<String, UiEnabledWhen>> uiEnabledWhenMap = collectUiEnabledWhenMetadata(type);
|
||||
Map<Class<?>, Map<String, UiOrder>> uiOrderMap = collectUiOrderMetadata(type);
|
||||
Map<Class<?>, Map<String, UiOptionsFromNode>> uiOptionsFromNodeMap = collectUiOptionsFromNodeMetadata(type);
|
||||
Map<Class<?>, Map<String, UiRequiredWhen>> uiRequiredWhenMap = collectUiRequiredWhenMetadata(type);
|
||||
Map<Class<?>, Map<String, UiUniqueItemsBy>> uiUniqueItemsByMap = collectUiUniqueItemsByMetadata(type);
|
||||
|
|
@ -70,6 +76,7 @@ public class JsonSchemaProducer {
|
|||
applyLongTextMetadata(root, getMergedMetadata(longTextMap, type));
|
||||
applyStructuralMetadata(root, getMergedMetadata(structuralMap, type));
|
||||
applyUiEnabledWhenMetadata(root, getMergedMetadata(uiEnabledWhenMap, type));
|
||||
applyUiOrderMetadata(root, type, getMergedMetadata(uiOrderMap, type));
|
||||
applyUiOptionsFromNodeMetadata(root, getMergedMetadata(uiOptionsFromNodeMap, type));
|
||||
applyUiRequiredWhenMetadata(root, getMergedMetadata(uiRequiredWhenMap, type));
|
||||
applyUiUniqueItemsByMetadata(root, getMergedMetadata(uiUniqueItemsByMap, type));
|
||||
|
|
@ -86,6 +93,7 @@ public class JsonSchemaProducer {
|
|||
metadataClasses.addAll(longTextMap.keySet());
|
||||
metadataClasses.addAll(structuralMap.keySet());
|
||||
metadataClasses.addAll(uiEnabledWhenMap.keySet());
|
||||
metadataClasses.addAll(uiOrderMap.keySet());
|
||||
metadataClasses.addAll(uiOptionsFromNodeMap.keySet());
|
||||
metadataClasses.addAll(uiRequiredWhenMap.keySet());
|
||||
metadataClasses.addAll(uiUniqueItemsByMap.keySet());
|
||||
|
|
@ -105,6 +113,7 @@ public class JsonSchemaProducer {
|
|||
applyLongTextMetadata(classSchema, getMergedMetadata(longTextMap, matchedClass));
|
||||
applyStructuralMetadata(classSchema, getMergedMetadata(structuralMap, matchedClass));
|
||||
applyUiEnabledWhenMetadata(classSchema, getMergedMetadata(uiEnabledWhenMap, matchedClass));
|
||||
applyUiOrderMetadata(classSchema, matchedClass, getMergedMetadata(uiOrderMap, matchedClass));
|
||||
applyUiOptionsFromNodeMetadata(classSchema, getMergedMetadata(uiOptionsFromNodeMap, matchedClass));
|
||||
applyUiRequiredWhenMetadata(classSchema, getMergedMetadata(uiRequiredWhenMap, matchedClass));
|
||||
applyUiUniqueItemsByMetadata(classSchema, getMergedMetadata(uiUniqueItemsByMap, matchedClass));
|
||||
|
|
@ -859,6 +868,90 @@ public class JsonSchemaProducer {
|
|||
return result;
|
||||
}
|
||||
|
||||
private Map<Class<?>, Map<String, UiOrder>> collectUiOrderMetadata(Class<?> rootClass) {
|
||||
Map<Class<?>, Map<String, UiOrder>> 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, UiOrder> metadata = new LinkedHashMap<>();
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
UiOrder annotation = field.getAnnotation(UiOrder.class);
|
||||
if (annotation != null) {
|
||||
metadata.put(field.getName(), annotation);
|
||||
}
|
||||
enqueueRelatedTypes(queue, field.getGenericType(), field.getType());
|
||||
}
|
||||
|
||||
if (current.isRecord()) {
|
||||
for (RecordComponent component : current.getRecordComponents()) {
|
||||
UiOrder annotation = component.getAnnotation(UiOrder.class);
|
||||
if (annotation != null) {
|
||||
metadata.put(component.getName(), annotation);
|
||||
}
|
||||
enqueueRelatedTypes(queue, component.getGenericType(), component.getType());
|
||||
}
|
||||
}
|
||||
|
||||
if (current.getSuperclass() != null) {
|
||||
queue.add(current.getSuperclass());
|
||||
}
|
||||
|
||||
if (!metadata.isEmpty()) {
|
||||
result.put(current, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void applyUiOrderMetadata(ObjectNode classSchema, Class<?> ownerClass, Map<String, UiOrder> metadata) {
|
||||
JsonNode propsNode = classSchema.get("properties");
|
||||
if (!(propsNode instanceof ObjectNode properties)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> propertyNames = new ArrayList<>();
|
||||
for (String propertyName : iterable(properties.fieldNames())) {
|
||||
propertyNames.add(propertyName);
|
||||
}
|
||||
if (propertyNames.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Integer> declarationIndex = new LinkedHashMap<>();
|
||||
List<String> declaredNames = collectDeclaredPropertyNames(ownerClass);
|
||||
for (int i = 0; i < declaredNames.size(); i++) {
|
||||
declarationIndex.put(declaredNames.get(i), i);
|
||||
}
|
||||
|
||||
propertyNames.sort(Comparator
|
||||
.comparingInt((String name) -> metadata != null && metadata.containsKey(name)
|
||||
? metadata.get(name).value()
|
||||
: Integer.MAX_VALUE)
|
||||
.thenComparingInt(name -> declarationIndex.getOrDefault(name, Integer.MAX_VALUE))
|
||||
.thenComparing(Comparator.naturalOrder()));
|
||||
|
||||
ObjectNode reorderedProperties = properties.objectNode();
|
||||
ArrayNode propertyOrder = classSchema.putArray("x-ui-property-order");
|
||||
for (int i = 0; i < propertyNames.size(); i++) {
|
||||
String propertyName = propertyNames.get(i);
|
||||
JsonNode propertyNode = properties.get(propertyName);
|
||||
if (propertyNode instanceof ObjectNode propertySchema) {
|
||||
propertySchema.put("x-ui-order", i + 1);
|
||||
}
|
||||
reorderedProperties.set(propertyName, propertyNode);
|
||||
propertyOrder.add(propertyName);
|
||||
}
|
||||
classSchema.set("properties", reorderedProperties);
|
||||
}
|
||||
|
||||
private void applyUiEnabledWhenMetadata(ObjectNode classSchema, Map<String, UiEnabledWhen> metadata) {
|
||||
if (metadata == null || metadata.isEmpty()) {
|
||||
return;
|
||||
|
|
@ -895,6 +988,32 @@ public class JsonSchemaProducer {
|
|||
}
|
||||
}
|
||||
|
||||
private List<String> collectDeclaredPropertyNames(Class<?> type) {
|
||||
if (type == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Deque<Class<?>> hierarchy = new ArrayDeque<>();
|
||||
Class<?> current = type;
|
||||
while (current != null && current != Object.class) {
|
||||
hierarchy.addFirst(current);
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
|
||||
Set<String> ordered = new LinkedHashSet<>();
|
||||
for (Class<?> clazz : hierarchy) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
ordered.add(field.getName());
|
||||
}
|
||||
if (clazz.isRecord()) {
|
||||
for (RecordComponent component : clazz.getRecordComponents()) {
|
||||
ordered.add(component.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(ordered);
|
||||
}
|
||||
|
||||
private void applyUiRequiredWhenMetadata(ObjectNode classSchema, Map<String, UiRequiredWhen> metadata) {
|
||||
if (metadata == null || metadata.isEmpty()) {
|
||||
return;
|
||||
|
|
@ -922,6 +1041,9 @@ public class JsonSchemaProducer {
|
|||
equalsAny.add(value);
|
||||
}
|
||||
}
|
||||
if (dependency.present()) {
|
||||
requiredWhen.put("present", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -981,6 +1103,8 @@ public class JsonSchemaProducer {
|
|||
ConfigurableAsInput annotation = entry.getValue();
|
||||
propertySchema.put("x-ui-bindable-as-input", true);
|
||||
propertySchema.put("x-ui-structural", true);
|
||||
propertySchema.put("x-ui-binding-static-label", "static value");
|
||||
propertySchema.put("x-ui-binding-input-label", "node input");
|
||||
propertySchema.put("x-ui-input-name", annotation.inputName().isBlank() ? entry.getKey() : annotation.inputName());
|
||||
propertySchema.put("x-ui-input-type", annotation.type().name());
|
||||
propertySchema.put("x-ui-input-multiple", annotation.multiple());
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
package it.cnr.isti.workflow.manager.blocks.configurations;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiUniqueItemsBy;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.skills.SkillBinding;
|
||||
import it.cnr.isti.workflow.manager.skills.SkillsCatalogService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
|
@ -31,11 +38,20 @@ public class LLMBlockConfiguration extends BlockConfiguration<LLMBlockType> {
|
|||
@JsonProperty(required = false)
|
||||
String prompt;
|
||||
|
||||
@Structural
|
||||
@Valid
|
||||
@Size(max = SkillsCatalogService.MAX_SKILLS_PER_BLOCK)
|
||||
@UiUniqueItemsBy("skillId")
|
||||
@JsonProperty(required = false)
|
||||
List<SkillBinding> skills = List.of();
|
||||
|
||||
@Builder
|
||||
public LLMBlockConfiguration(@NonNull String name, @NonNull LLMDescriptor llmDescriptor, String prompt) {
|
||||
public LLMBlockConfiguration(@NonNull String name, @NonNull LLMDescriptor llmDescriptor, String prompt,
|
||||
List<SkillBinding> skills) {
|
||||
super(name);
|
||||
this.llmDescriptor = llmDescriptor;
|
||||
this.prompt = prompt;
|
||||
this.skills = skills == null ? List.of() : List.copyOf(skills);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -46,7 +62,23 @@ public class LLMBlockConfiguration extends BlockConfiguration<LLMBlockType> {
|
|||
public static LLMBlockConfiguration empty() {
|
||||
LLMBlockConfiguration configuration = new LLMBlockConfiguration();
|
||||
configuration.name = LLMBlockType.TYPE;
|
||||
configuration.skills = List.of();
|
||||
return configuration;
|
||||
}
|
||||
|
||||
@AssertTrue(message = "skills must have unique skillId values")
|
||||
boolean areSkillIdsUnique() {
|
||||
if (skills == null || skills.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return skills.stream()
|
||||
.map(SkillBinding::skillId)
|
||||
.filter(skillId -> skillId != null && !skillId.isBlank())
|
||||
.distinct()
|
||||
.count() == skills.stream()
|
||||
.map(SkillBinding::skillId)
|
||||
.filter(skillId -> skillId != null && !skillId.isBlank())
|
||||
.count();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public class MCPAgentBlockConfiguration extends BlockConfiguration<MCPAgentBlock
|
|||
@Structural
|
||||
@ConfigurableAsInput
|
||||
@UiEnabledWhen(field = "useSharedSession", equals = "false")
|
||||
@UiRequiredWhen(field = "useSharedSession", equals = "false")
|
||||
@FieldRetriever(name = "LLM", url = "/retriever/LLM/models?provider=InternalOllama")
|
||||
@JsonProperty(required = false)
|
||||
private String model;
|
||||
|
|
@ -96,6 +97,7 @@ public class MCPAgentBlockConfiguration extends BlockConfiguration<MCPAgentBlock
|
|||
public static MCPAgentBlockConfiguration empty() {
|
||||
MCPAgentBlockConfiguration configuration = new MCPAgentBlockConfiguration();
|
||||
configuration.name = MCPAgentBlockType.TYPE;
|
||||
configuration.model = "";
|
||||
configuration.mcpServers = List.of();
|
||||
configuration.shareSession = false;
|
||||
configuration.useSharedSession = false;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ public class MCPAgentChatBlockConfiguration extends BlockConfiguration<MCPAgentC
|
|||
@Valid
|
||||
private List<ChatInteractionInput> inputs = List.of();
|
||||
|
||||
@Structural
|
||||
@JsonProperty(required = true)
|
||||
private boolean exposeHistory = true;
|
||||
|
||||
@NotBlank
|
||||
@LongText(placeholder = "Describe the goal of the simulated chat")
|
||||
@JsonProperty(required = true)
|
||||
|
|
@ -78,12 +82,14 @@ public class MCPAgentChatBlockConfiguration extends BlockConfiguration<MCPAgentC
|
|||
|
||||
@Builder
|
||||
public MCPAgentChatBlockConfiguration(@NonNull String name, String model, List<ChatInteractionInput> inputs,
|
||||
Boolean exposeHistory,
|
||||
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.exposeHistory = exposeHistory == null ? true : exposeHistory;
|
||||
this.goalDescription = goalDescription;
|
||||
this.mcpServers = mcpServers == null ? List.of() : List.copyOf(mcpServers);
|
||||
this.shareSession = shareSession;
|
||||
|
|
@ -100,7 +106,9 @@ public class MCPAgentChatBlockConfiguration extends BlockConfiguration<MCPAgentC
|
|||
public static MCPAgentChatBlockConfiguration empty() {
|
||||
MCPAgentChatBlockConfiguration configuration = new MCPAgentChatBlockConfiguration();
|
||||
configuration.name = MCPAgentChatBlockType.TYPE;
|
||||
configuration.model = "";
|
||||
configuration.inputs = List.of();
|
||||
configuration.exposeHistory = true;
|
||||
configuration.mcpServers = List.of();
|
||||
configuration.shareSession = false;
|
||||
configuration.useSharedSession = false;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import it.cnr.isti.workflow.manager.blocks.types.SwitchBlockType;
|
|||
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.UiOrder;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiRequiredWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiUniqueItemsBy;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
|
|
@ -26,6 +27,7 @@ import lombok.NonNull;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SwitchBlockConfiguration extends BlockConfiguration<SwitchBlockType> {
|
||||
|
||||
@UiOrder(30)
|
||||
@Structural
|
||||
@UiUniqueItemsBy("name")
|
||||
@Size(max = 10)
|
||||
|
|
@ -33,6 +35,12 @@ public class SwitchBlockConfiguration extends BlockConfiguration<SwitchBlockType
|
|||
@JsonProperty(required = false)
|
||||
private List<SwitchCase> cases = List.of();
|
||||
|
||||
@UiOrder(40)
|
||||
@JsonProperty(required = true)
|
||||
@Structural
|
||||
private boolean useLlm;
|
||||
|
||||
@UiOrder(50)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "false")
|
||||
@Structural
|
||||
@LongText(
|
||||
|
|
@ -41,15 +49,13 @@ public class SwitchBlockConfiguration extends BlockConfiguration<SwitchBlockType
|
|||
acceptVariableAsPlaceholder = true)
|
||||
private String condition;
|
||||
|
||||
@JsonProperty(required = true)
|
||||
@Structural
|
||||
private boolean useLlm;
|
||||
|
||||
@Valid
|
||||
@UiOrder(60)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
private LLMDescriptor llmDescriptor;
|
||||
|
||||
@UiOrder(70)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
@Structural
|
||||
|
|
@ -59,6 +65,7 @@ public class SwitchBlockConfiguration extends BlockConfiguration<SwitchBlockType
|
|||
acceptVariableAsPlaceholder = true)
|
||||
private String prompt;
|
||||
|
||||
@UiOrder(80)
|
||||
@Structural
|
||||
@LongText(
|
||||
placeholder = "Define the output payload to propagate on the selected branch",
|
||||
|
|
|
|||
|
|
@ -36,13 +36,15 @@ public class ChatInteractionBlockFactory
|
|||
@Override
|
||||
public Block<ChatInteractionBlockType> create(ChatInteractionBlockConfiguration configuration) {
|
||||
validateConfiguration(configuration);
|
||||
return Block.<ChatInteractionBlockType>builder()
|
||||
Block.BlockBuilder<ChatInteractionBlockType> builder = Block.<ChatInteractionBlockType>builder()
|
||||
.inputs(resolveInputs(configuration))
|
||||
.output(IODescriptor.output(RESPONSE_OUTPUT, IOType.TEXT, false, RESPONSE_CAPABILITIES))
|
||||
.output(IODescriptor.output(HISTORY_OUTPUT, IOType.TEXT, true, HISTORY_CAPABILITIES))
|
||||
.specificConfiguration(configuration)
|
||||
.type(blockType)
|
||||
.build();
|
||||
.type(blockType);
|
||||
if (configuration.isExposeHistory()) {
|
||||
builder.output(IODescriptor.output(HISTORY_OUTPUT, IOType.TEXT, true, HISTORY_CAPABILITIES));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -52,7 +52,13 @@ public class MCPAgentBlockFactory implements BlockFactory<MCPAgentBlockType, MCP
|
|||
|
||||
@Override
|
||||
public Block<MCPAgentBlockType> createEmpty() {
|
||||
return create(MCPAgentBlockConfiguration.empty());
|
||||
MCPAgentBlockConfiguration configuration = MCPAgentBlockConfiguration.empty();
|
||||
return Block.<MCPAgentBlockType>builder()
|
||||
.inputs(retrieveInputs(configuration.getPrompt()))
|
||||
.output(IODescriptor.output(OUTPUT_NAME, IOType.TEXT, false, OUTPUT_CAPABILITIES))
|
||||
.specificConfiguration(configuration)
|
||||
.type(blockType)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -37,18 +37,29 @@ public class MCPAgentChatBlockFactory
|
|||
@Override
|
||||
public Block<MCPAgentChatBlockType> create(MCPAgentChatBlockConfiguration configuration) {
|
||||
validateConfiguration(configuration);
|
||||
return Block.<MCPAgentChatBlockType>builder()
|
||||
Block.BlockBuilder<MCPAgentChatBlockType> builder = Block.<MCPAgentChatBlockType>builder()
|
||||
.inputs(resolveInputs(configuration))
|
||||
.output(IODescriptor.output(RESPONSE_OUTPUT, IOType.TEXT, false, RESPONSE_CAPABILITIES))
|
||||
.output(IODescriptor.output(HISTORY_OUTPUT, IOType.TEXT, true, HISTORY_CAPABILITIES))
|
||||
.specificConfiguration(configuration)
|
||||
.type(blockType)
|
||||
.build();
|
||||
.type(blockType);
|
||||
if (configuration.isExposeHistory()) {
|
||||
builder.output(IODescriptor.output(HISTORY_OUTPUT, IOType.TEXT, true, HISTORY_CAPABILITIES));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block<MCPAgentChatBlockType> createEmpty() {
|
||||
return create(MCPAgentChatBlockConfiguration.empty());
|
||||
MCPAgentChatBlockConfiguration configuration = MCPAgentChatBlockConfiguration.empty();
|
||||
Block.BlockBuilder<MCPAgentChatBlockType> builder = Block.<MCPAgentChatBlockType>builder()
|
||||
.inputs(List.of())
|
||||
.output(IODescriptor.output(RESPONSE_OUTPUT, IOType.TEXT, false, RESPONSE_CAPABILITIES))
|
||||
.specificConfiguration(configuration)
|
||||
.type(blockType);
|
||||
if (configuration.isExposeHistory()) {
|
||||
builder.output(IODescriptor.output(HISTORY_OUTPUT, IOType.TEXT, true, HISTORY_CAPABILITIES));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
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 UiOrder {
|
||||
int value();
|
||||
}
|
||||
|
|
@ -13,4 +13,6 @@ public @interface UiRequiredWhen {
|
|||
String equals() default "";
|
||||
|
||||
String[] equalsAny() default {};
|
||||
|
||||
boolean present() default false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
|
|||
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;
|
||||
import it.cnr.isti.workflow.manager.containers.types.ContainerType;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import jakarta.validation.Valid;
|
||||
|
|
@ -26,9 +27,11 @@ import lombok.NoArgsConstructor;
|
|||
public abstract class ContainerConfiguration<T extends ContainerType> {
|
||||
|
||||
@NotBlank
|
||||
@UiOrder(10)
|
||||
@JsonProperty(required = true)
|
||||
String name;
|
||||
|
||||
@UiOrder(20)
|
||||
@Structural
|
||||
@Valid
|
||||
@FieldRetriever(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package it.cnr.isti.workflow.manager.containers.configurations;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
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.UiEnabledWhen;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOrder;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiOptionsFromNode;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiRequiredWhen;
|
||||
import it.cnr.isti.workflow.manager.containers.iresolvers.ContainerFlowInterfaceResolver;
|
||||
import it.cnr.isti.workflow.manager.containers.types.LoopContainerType;
|
||||
|
|
@ -25,50 +28,89 @@ import lombok.NonNull;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LoopContainerConfiguration extends ContainerConfiguration<LoopContainerType> {
|
||||
|
||||
@UiEnabledWhen(field = "useLlm", equals = "false")
|
||||
@Structural
|
||||
@LongText(
|
||||
placeholder = "Add a deterministic stop condition, for example: ${{response}} == 'done' or #iteration >= 3",
|
||||
tip = "Supports SpEL. Inputs and latest exposed outputs are available through placeholders ${{}}. The variable #iteration contains the current 1-based iteration number.",
|
||||
acceptVariableAsPlaceholder = true)
|
||||
private String condition;
|
||||
|
||||
@JsonProperty(required = true)
|
||||
@Structural
|
||||
private boolean useLlm;
|
||||
|
||||
@Valid
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
private LLMDescriptor llmDescriptor;
|
||||
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
@Structural
|
||||
@LongText(
|
||||
placeholder = "Add the prompt the LLM should use to decide whether the loop should stop",
|
||||
tip = "The LLM must answer true or false. Inputs, outputs and iteration are included in the evaluation context.",
|
||||
acceptVariableAsPlaceholder = true)
|
||||
private String prompt;
|
||||
|
||||
@UiOrder(30)
|
||||
@Structural
|
||||
@Min(1)
|
||||
@JsonProperty(required = true)
|
||||
private Integer maxIterations;
|
||||
|
||||
@UiOrder(40)
|
||||
@JsonProperty(required = true)
|
||||
@Structural
|
||||
private boolean useLlm;
|
||||
|
||||
@UiOrder(50)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "false")
|
||||
@Structural
|
||||
@JsonAlias("condition")
|
||||
@JsonProperty("guardCondition")
|
||||
@LongText(
|
||||
placeholder = "Add a deterministic guard condition, for example: ${{response}} == 'done' or ${{outputs.response}} == 'done' or #iteration >= 3",
|
||||
tip = "Supports SpEL. Inputs and latest exposed outputs are available through placeholders ${{}}. Use outputs.* to reference exposed subflow outputs explicitly. The variable #iteration contains the current 1-based iteration number.",
|
||||
acceptVariableAsPlaceholder = true)
|
||||
private String guardCondition;
|
||||
|
||||
@Valid
|
||||
@UiOrder(60)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
private LLMDescriptor llmDescriptor;
|
||||
|
||||
@UiOrder(70)
|
||||
@UiEnabledWhen(field = "useLlm", equals = "true", group = "llm")
|
||||
@UiRequiredWhen(field = "useLlm", equals = "true")
|
||||
@Structural
|
||||
@JsonAlias("prompt")
|
||||
@JsonProperty("guardPrompt")
|
||||
@LongText(
|
||||
placeholder = "Add the guard prompt the LLM should use to decide whether the loop should stop",
|
||||
tip = "The LLM must answer true or false. Inputs, exposed outputs and iteration are included in the evaluation context. Use outputs.* to reference exposed subflow outputs explicitly.",
|
||||
acceptVariableAsPlaceholder = true)
|
||||
private String guardPrompt;
|
||||
|
||||
@UiOrder(80)
|
||||
@Structural
|
||||
@UiEnabledWhen(field = "subFlow", present = true)
|
||||
@UiOptionsFromNode(collection = "inputs", valueField = "name", labelField = "name")
|
||||
@JsonProperty(required = false)
|
||||
private String feedbackInput;
|
||||
|
||||
@Valid
|
||||
@UiOrder(90)
|
||||
@UiEnabledWhen(field = "feedbackPrompt", present = true, group = "feedback")
|
||||
@UiRequiredWhen(field = "feedbackPrompt", present = true)
|
||||
private LLMDescriptor feedbackLlmDescriptor;
|
||||
|
||||
@UiOrder(100)
|
||||
@UiEnabledWhen(field = "feedbackInput", present = true, group = "feedback")
|
||||
@UiRequiredWhen(field = "feedbackInput", present = true)
|
||||
@Structural
|
||||
@JsonProperty(required = false)
|
||||
@LongText(
|
||||
placeholder = "Add the prompt that constructs the next value for the selected input when the guard is false",
|
||||
tip = "Executed only when the loop continues. Use inputs.*, outputs.* and iteration to build the next input value. The LLM response becomes the next value of the selected input.",
|
||||
acceptVariableAsPlaceholder = true)
|
||||
private String feedbackPrompt;
|
||||
|
||||
@Builder
|
||||
public LoopContainerConfiguration(@NonNull String name,
|
||||
FlowData subFlow,
|
||||
String condition,
|
||||
String guardCondition,
|
||||
boolean useLlm,
|
||||
LLMDescriptor llmDescriptor,
|
||||
String prompt,
|
||||
String guardPrompt,
|
||||
String feedbackInput,
|
||||
LLMDescriptor feedbackLlmDescriptor,
|
||||
String feedbackPrompt,
|
||||
Integer maxIterations) {
|
||||
super(name, subFlow);
|
||||
this.condition = condition;
|
||||
this.guardCondition = guardCondition;
|
||||
this.useLlm = useLlm;
|
||||
this.llmDescriptor = llmDescriptor;
|
||||
this.prompt = prompt;
|
||||
this.guardPrompt = guardPrompt;
|
||||
this.feedbackInput = feedbackInput;
|
||||
this.feedbackLlmDescriptor = feedbackLlmDescriptor;
|
||||
this.feedbackPrompt = feedbackPrompt;
|
||||
this.maxIterations = maxIterations == null ? 10 : maxIterations;
|
||||
}
|
||||
|
||||
|
|
@ -85,13 +127,16 @@ public class LoopContainerConfiguration extends ContainerConfiguration<LoopConta
|
|||
false,
|
||||
null,
|
||||
"",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
10);
|
||||
}
|
||||
|
||||
@AssertTrue(message = "condition is required when useLlm is false")
|
||||
@AssertTrue(message = "guardCondition is required when useLlm is false")
|
||||
@JsonIgnore
|
||||
boolean isDeterministicConfigurationValid() {
|
||||
return useLlm || (condition != null && !condition.isBlank());
|
||||
return useLlm || (guardCondition != null && !guardCondition.isBlank());
|
||||
}
|
||||
|
||||
@AssertTrue(message = "llmDescriptor is required when useLlm is true")
|
||||
|
|
@ -100,10 +145,40 @@ public class LoopContainerConfiguration extends ContainerConfiguration<LoopConta
|
|||
return !useLlm || llmDescriptor != null;
|
||||
}
|
||||
|
||||
@AssertTrue(message = "prompt is required when useLlm is true")
|
||||
@AssertTrue(message = "guardPrompt is required when useLlm is true")
|
||||
@JsonIgnore
|
||||
boolean isLlmPromptConfigurationValid() {
|
||||
return !useLlm || (prompt != null && !prompt.isBlank());
|
||||
return !useLlm || (guardPrompt != null && !guardPrompt.isBlank());
|
||||
}
|
||||
|
||||
@AssertTrue(message = "feedbackPrompt requires feedbackInput")
|
||||
@JsonIgnore
|
||||
boolean isFeedbackInputPresentWhenFeedbackPromptConfigured() {
|
||||
return feedbackPrompt == null || feedbackPrompt.isBlank()
|
||||
|| (feedbackInput != null && !feedbackInput.isBlank());
|
||||
}
|
||||
|
||||
@AssertTrue(message = "feedbackInput requires feedbackPrompt")
|
||||
@JsonIgnore
|
||||
boolean isFeedbackPromptPresentWhenFeedbackInputConfigured() {
|
||||
return feedbackInput == null || feedbackInput.isBlank()
|
||||
|| (feedbackPrompt != null && !feedbackPrompt.isBlank());
|
||||
}
|
||||
|
||||
@AssertTrue(message = "feedbackLlmDescriptor is required when feedbackPrompt is configured")
|
||||
@JsonIgnore
|
||||
boolean isFeedbackLlmConfigurationValid() {
|
||||
return feedbackPrompt == null || feedbackPrompt.isBlank() || feedbackLlmDescriptor != null;
|
||||
}
|
||||
|
||||
@AssertTrue(message = "feedbackInput must target an open non-multiple input of the subFlow")
|
||||
@JsonIgnore
|
||||
boolean isFeedbackInputValid() {
|
||||
if (!hasSubFlowNodes() || feedbackInput == null || feedbackInput.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
return ContainerFlowInterfaceResolver.getExposedInputs(getSubFlow()).stream()
|
||||
.anyMatch(handle -> handle.publicName().equals(feedbackInput) && !handle.handle().io().isMultiple());
|
||||
}
|
||||
|
||||
@AssertTrue(message = "maxIterations must be greater than zero")
|
||||
|
|
@ -121,4 +196,16 @@ public class LoopContainerConfiguration extends ContainerConfiguration<LoopConta
|
|||
private boolean hasSubFlowNodes() {
|
||||
return getSubFlow() != null && !getSubFlow().getNodes().isEmpty();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public String getCondition() {
|
||||
return guardCondition;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@JsonIgnore
|
||||
public String getPrompt() {
|
||||
return guardPrompt;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class LoopContainerType implements ContainerType {
|
|||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "A container node that repeatedly executes a subflow until a stop condition becomes true. The stop condition can be deterministic (SpEL) or evaluated by an LLM.";
|
||||
return "A container node that repeatedly executes a subflow until its guard evaluates to true. The guard can be deterministic (SpEL) or evaluated by an LLM.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -83,9 +83,7 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
|
||||
String finalResponse = generateSimulatorFinalResponse(simulatorProvider, simulatorDescriptor, simulatorAuthorization,
|
||||
resolvedGoal, inputs, history);
|
||||
return Map.of(
|
||||
ChatInteractionBlockFactory.RESPONSE_OUTPUT, finalResponse,
|
||||
ChatInteractionBlockFactory.HISTORY_OUTPUT, List.copyOf(history));
|
||||
return buildResult(configuration, finalResponse, List.copyOf(history));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -140,9 +138,7 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
updatedHistory.add(formatConversationLine(ChatMessage.Role.USER, resolvedMessage));
|
||||
updatedHistory.add(formatConversationLine(ChatMessage.Role.ASSISTANT, assistantResponse));
|
||||
|
||||
return InteractionResult.partial(Map.of(
|
||||
ChatInteractionBlockFactory.RESPONSE_OUTPUT, assistantResponse,
|
||||
ChatInteractionBlockFactory.HISTORY_OUTPUT, List.copyOf(updatedHistory)));
|
||||
return InteractionResult.partial(buildResult(configuration, assistantResponse, List.copyOf(updatedHistory)));
|
||||
}
|
||||
|
||||
if (interaction.containsKey(ChatInteractionBlockFactory.FINAL_RESPONSE_FIELD)) {
|
||||
|
|
@ -152,9 +148,7 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
+ ChatInteractionBlockFactory.FINAL_RESPONSE_FIELD);
|
||||
}
|
||||
List<String> history = existingHistory(partialResults);
|
||||
return InteractionResult.completed(Map.of(
|
||||
ChatInteractionBlockFactory.RESPONSE_OUTPUT, response,
|
||||
ChatInteractionBlockFactory.HISTORY_OUTPUT, List.copyOf(history)));
|
||||
return InteractionResult.completed(buildResult(configuration, response, List.copyOf(history)));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported interaction field for ChatInteraction");
|
||||
|
|
@ -247,6 +241,16 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
return "[" + role.name() + "] " + content;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResult(ChatInteractionBlockConfiguration configuration, String response,
|
||||
List<String> history) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put(ChatInteractionBlockFactory.RESPONSE_OUTPUT, response);
|
||||
if (configuration.isExposeHistory()) {
|
||||
result.put(ChatInteractionBlockFactory.HISTORY_OUTPUT, history);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> existingHistory(Map<String, Object> partialResults) {
|
||||
Object value = partialResults.get(ChatInteractionBlockFactory.HISTORY_OUTPUT);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ 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;
|
||||
import it.cnr.isti.workflow.manager.skills.SkillPromptService;
|
||||
|
||||
@Component
|
||||
public class LLMExecutor implements BlockExecutor<LLMBlockType> {
|
||||
|
|
@ -29,6 +30,9 @@ public class LLMExecutor implements BlockExecutor<LLMBlockType> {
|
|||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Autowired
|
||||
private SkillPromptService skillPromptService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<LLMBlockType> block, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
|
|
@ -46,6 +50,11 @@ public class LLMExecutor implements BlockExecutor<LLMBlockType> {
|
|||
prompt = ExecutionTemplateResolver.formatValue(inputs.getFirst().getValue());
|
||||
}
|
||||
|
||||
String skillsInstructions = skillPromptService.renderSkillsInstructions(config.getSkills());
|
||||
if (StringUtils.hasText(skillsInstructions)) {
|
||||
prompt = skillsInstructions + System.lineSeparator() + System.lineSeparator() + prompt;
|
||||
}
|
||||
|
||||
LLMDescriptor llmDescriptor = config.getLlmDescriptor();
|
||||
LLMProvider llmProvider = llmProviders.get(llmDescriptor.provider());
|
||||
if (llmProvider == null) {
|
||||
|
|
|
|||
|
|
@ -90,9 +90,7 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
|
||||
String finalResponse = generateSimulatorFinalResponse(simulatorProvider, simulatorDescriptor, simulatorAuthorization,
|
||||
resolvedGoal, inputs, history);
|
||||
return Map.of(
|
||||
MCPAgentChatBlockFactory.RESPONSE_OUTPUT, finalResponse,
|
||||
MCPAgentChatBlockFactory.HISTORY_OUTPUT, List.copyOf(history));
|
||||
return buildResult(configuration, finalResponse, List.copyOf(history), null);
|
||||
} finally {
|
||||
if (!managedSharedSession) {
|
||||
mcpAgentService.closeSessionQuietly(sessionId);
|
||||
|
|
@ -157,10 +155,8 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
List<String> updatedHistory = new ArrayList<>(history);
|
||||
updatedHistory.add(formatConversationLine("USER", resolvedMessage));
|
||||
updatedHistory.add(formatConversationLine("ASSISTANT", assistantResponse));
|
||||
return InteractionResult.partial(Map.of(
|
||||
MCPAgentChatBlockFactory.RESPONSE_OUTPUT, assistantResponse,
|
||||
MCPAgentChatBlockFactory.HISTORY_OUTPUT, List.copyOf(updatedHistory),
|
||||
SESSION_ID_STATE, existingSessionId));
|
||||
return InteractionResult.partial(buildResult(configuration, assistantResponse, List.copyOf(updatedHistory),
|
||||
existingSessionId));
|
||||
} catch (RuntimeException ex) {
|
||||
if (createdSession && !isManagedSharedSession(configuration, executionVariableDescriptors)) {
|
||||
mcpAgentService.closeSessionQuietly(existingSessionId);
|
||||
|
|
@ -188,9 +184,7 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
}
|
||||
}
|
||||
List<String> history = existingHistory(partialResults);
|
||||
return InteractionResult.completed(Map.of(
|
||||
MCPAgentChatBlockFactory.RESPONSE_OUTPUT, response,
|
||||
MCPAgentChatBlockFactory.HISTORY_OUTPUT, List.copyOf(history)));
|
||||
return InteractionResult.completed(buildResult(configuration, response, List.copyOf(history), null));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported interaction field for MCPAgentChat");
|
||||
|
|
@ -291,6 +285,19 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
return normalized;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResult(MCPAgentChatBlockConfiguration configuration, String response,
|
||||
List<String> history, String sessionId) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put(MCPAgentChatBlockFactory.RESPONSE_OUTPUT, response);
|
||||
if (configuration.isExposeHistory()) {
|
||||
result.put(MCPAgentChatBlockFactory.HISTORY_OUTPUT, history);
|
||||
}
|
||||
if (StringUtils.hasText(sessionId)) {
|
||||
result.put(SESSION_ID_STATE, sessionId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> existingHistory(Map<String, Object> partialResults) {
|
||||
Object value = partialResults.get(MCPAgentChatBlockFactory.HISTORY_OUTPUT);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ public class LoopContainerExecutor implements ContainerExecutor<LoopContainerTyp
|
|||
Reply strictly as JSON in the form {"result":true} or {"result":false}.
|
||||
Do not add any extra text.
|
||||
""";
|
||||
private static final String FEEDBACK_SYSTEM_PROMPT = """
|
||||
You are a workflow loop feedback constructor.
|
||||
Generate the next value for the requested input so the next loop iteration can improve the previous result.
|
||||
Reply only with the raw replacement value for that input.
|
||||
Do not add markdown, labels, or explanations.
|
||||
""";
|
||||
|
||||
private final ExecutionsService executionsService;
|
||||
private final Map<String, LLMProvider> llmProviders;
|
||||
|
|
@ -127,12 +133,13 @@ public class LoopContainerExecutor implements ContainerExecutor<LoopContainerTyp
|
|||
}
|
||||
}
|
||||
|
||||
Map<String, Object> iterationInputs = new LinkedHashMap<>(currentInputs);
|
||||
updateInputsForNextIteration(currentInputs, latestOutputs, inputPortsByName);
|
||||
|
||||
boolean shouldStop = shouldStop(configuration, currentInputs, latestOutputs, authorizations, executionVariables, iteration);
|
||||
if (eventLogger != null) {
|
||||
eventLogger.info(ExecutionEventType.CONTAINER_CONDITION_EVALUATED,
|
||||
"Evaluated loop stop condition",
|
||||
"Evaluated loop guard",
|
||||
Map.of("containerType", LoopContainerType.TYPE, "iteration", iteration, "stop", shouldStop));
|
||||
eventLogger.info(ExecutionEventType.CONTAINER_ITERATION_COMPLETED,
|
||||
shouldStop ? "Loop completed at iteration " + iteration : "Completed loop iteration " + iteration,
|
||||
|
|
@ -141,10 +148,13 @@ public class LoopContainerExecutor implements ContainerExecutor<LoopContainerTyp
|
|||
if (shouldStop) {
|
||||
return new LinkedHashMap<>(latestOutputs);
|
||||
}
|
||||
|
||||
applyFeedbackInputForNextIteration(configuration, currentInputs, iterationInputs, latestOutputs,
|
||||
authorizations, executionVariables, iteration, eventLogger);
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"LoopContainer condition was not satisfied within maxIterations=" + configuration.getMaxIterations());
|
||||
"LoopContainer guard was not satisfied within maxIterations=" + configuration.getMaxIterations());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -164,20 +174,23 @@ public class LoopContainerExecutor implements ContainerExecutor<LoopContainerTyp
|
|||
private boolean shouldStop(LoopContainerConfiguration configuration, Map<String, Object> currentInputs,
|
||||
Map<String, Object> latestOutputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, int iteration) {
|
||||
Map<String, Object> contextValues = new LinkedHashMap<>(currentInputs);
|
||||
contextValues.putAll(latestOutputs);
|
||||
contextValues.put("iteration", iteration);
|
||||
Map<String, Object> contextValues = buildGuardTemplateValues(currentInputs, latestOutputs, iteration);
|
||||
return configuration.isUseLlm()
|
||||
? evaluateWithLlm(configuration, contextValues, authorizations, executionVariables, iteration)
|
||||
: evaluateWithExpression(configuration.getCondition(), contextValues, executionVariables, iteration);
|
||||
? evaluateWithLlm(configuration, contextValues, currentInputs, latestOutputs, authorizations,
|
||||
executionVariables, iteration)
|
||||
: evaluateWithExpression(configuration.getCondition(), contextValues, currentInputs, latestOutputs,
|
||||
executionVariables, iteration);
|
||||
}
|
||||
|
||||
private boolean evaluateWithExpression(String expression, Map<String, Object> values,
|
||||
Map<String, Object> currentInputs, Map<String, Object> latestOutputs,
|
||||
Map<String, Object> executionVariables, int iteration) {
|
||||
String normalizedExpression = normalizeExpression(expression);
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(values);
|
||||
context.addPropertyAccessor(new MapAccessor());
|
||||
values.forEach(context::setVariable);
|
||||
context.setVariable("inputs", currentInputs == null ? Map.of() : currentInputs);
|
||||
context.setVariable("outputs", latestOutputs == null ? Map.of() : latestOutputs);
|
||||
context.setVariable("vars", executionVariables == null ? Map.of() : executionVariables);
|
||||
context.setVariable("global", ExecutionRuntimeContextSupport.globalView(executionVariables));
|
||||
context.setVariable("context", ExecutionRuntimeContextSupport.contextView(executionVariables));
|
||||
|
|
@ -190,37 +203,100 @@ public class LoopContainerExecutor implements ContainerExecutor<LoopContainerTyp
|
|||
}
|
||||
|
||||
private boolean evaluateWithLlm(LoopContainerConfiguration configuration, Map<String, Object> values,
|
||||
Map<String, Object> currentInputs, Map<String, Object> latestOutputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables, int iteration) {
|
||||
LLMDescriptor llmDescriptor = configuration.getLlmDescriptor();
|
||||
LLMProvider llmProvider = resolveProvider(llmDescriptor.provider());
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey))))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
ensureAuthorization(llmProvider, llmDescriptor.provider(), authorizations);
|
||||
|
||||
String prompt = buildLlmPrompt(configuration, values, executionVariables, iteration);
|
||||
String response = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt, String.valueOf(authorizations.get(authKey)))
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
String prompt = buildLlmPrompt(configuration, values, currentInputs, latestOutputs, executionVariables, iteration);
|
||||
String response = callLlmProvider(llmProvider, llmDescriptor, prompt, authorizations);
|
||||
return parseBooleanResponse(response);
|
||||
}
|
||||
|
||||
private void applyFeedbackInputForNextIteration(LoopContainerConfiguration configuration, Map<String, Object> nextInputs,
|
||||
Map<String, Object> iterationInputs, Map<String, Object> latestOutputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, int iteration, ExecutionEventLogger eventLogger) {
|
||||
if (!StringUtils.hasText(configuration.getFeedbackPrompt()) || !StringUtils.hasText(configuration.getFeedbackInput())) {
|
||||
return;
|
||||
}
|
||||
|
||||
LLMDescriptor llmDescriptor = configuration.getFeedbackLlmDescriptor();
|
||||
if (llmDescriptor == null) {
|
||||
throw new IllegalArgumentException("feedbackLlmDescriptor is required when feedbackPrompt is configured");
|
||||
}
|
||||
|
||||
Map<String, Object> values = buildGuardTemplateValues(iterationInputs, latestOutputs, iteration);
|
||||
String prompt = buildFeedbackPrompt(configuration, values, iterationInputs, latestOutputs, executionVariables, iteration);
|
||||
LLMProvider llmProvider = resolveProvider(llmDescriptor.provider());
|
||||
ensureAuthorization(llmProvider, llmDescriptor.provider(), authorizations);
|
||||
String response = callLlmProvider(llmProvider, llmDescriptor, prompt, authorizations);
|
||||
nextInputs.put(configuration.getFeedbackInput(), response);
|
||||
|
||||
if (eventLogger != null) {
|
||||
eventLogger.info(ExecutionEventType.LLM_REQUEST,
|
||||
"Constructed next loop input " + configuration.getFeedbackInput(),
|
||||
Map.of(
|
||||
"containerType", LoopContainerType.TYPE,
|
||||
"iteration", iteration,
|
||||
"purpose", "feedbackInput",
|
||||
"inputName", configuration.getFeedbackInput(),
|
||||
"provider", llmDescriptor.provider(),
|
||||
"model", llmDescriptor.model()));
|
||||
}
|
||||
}
|
||||
|
||||
private String buildLlmPrompt(LoopContainerConfiguration configuration, Map<String, Object> values,
|
||||
Map<String, Object> currentInputs, Map<String, Object> latestOutputs,
|
||||
Map<String, Object> executionVariables, int iteration) {
|
||||
String resolvedPrompt = ExecutionTemplateResolver.resolve(
|
||||
configuration.getPrompt(),
|
||||
configuration.getGuardPrompt(),
|
||||
values,
|
||||
executionVariables == null ? Map.of() : executionVariables);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(LLM_SYSTEM_PROMPT).append("\n");
|
||||
builder.append("Decision prompt: ").append(resolvedPrompt).append("\n");
|
||||
builder.append("Guard prompt: ").append(resolvedPrompt).append("\n");
|
||||
builder.append("Inputs: ").append(currentInputs == null ? Map.of() : currentInputs).append("\n");
|
||||
builder.append("Exposed outputs: ").append(latestOutputs == null ? Map.of() : latestOutputs).append("\n");
|
||||
builder.append("Current values: ").append(values).append("\n");
|
||||
builder.append("Execution variables: ").append(executionVariables == null ? Map.of() : executionVariables).append("\n");
|
||||
builder.append("Iteration: ").append(iteration).append("\n");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String buildFeedbackPrompt(LoopContainerConfiguration configuration, Map<String, Object> values,
|
||||
Map<String, Object> iterationInputs, Map<String, Object> latestOutputs,
|
||||
Map<String, Object> executionVariables, int iteration) {
|
||||
String resolvedPrompt = ExecutionTemplateResolver.resolve(
|
||||
configuration.getFeedbackPrompt(),
|
||||
values,
|
||||
executionVariables == null ? Map.of() : executionVariables);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(FEEDBACK_SYSTEM_PROMPT).append("\n");
|
||||
builder.append("Target input: ").append(configuration.getFeedbackInput()).append("\n");
|
||||
builder.append("Feedback prompt: ").append(resolvedPrompt).append("\n");
|
||||
builder.append("Current iteration inputs: ").append(iterationInputs == null ? Map.of() : iterationInputs).append("\n");
|
||||
builder.append("Current iteration exposed outputs: ").append(latestOutputs == null ? Map.of() : latestOutputs).append("\n");
|
||||
builder.append("Execution variables: ").append(executionVariables == null ? Map.of() : executionVariables).append("\n");
|
||||
builder.append("Iteration: ").append(iteration).append("\n");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private Map<String, Object> buildGuardTemplateValues(Map<String, Object> currentInputs,
|
||||
Map<String, Object> latestOutputs, int iteration) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
if (currentInputs != null) {
|
||||
values.putAll(currentInputs);
|
||||
currentInputs.forEach((key, value) -> values.put("inputs." + key, value));
|
||||
}
|
||||
if (latestOutputs != null) {
|
||||
values.putAll(latestOutputs);
|
||||
latestOutputs.forEach((key, value) -> values.put("outputs." + key, value));
|
||||
}
|
||||
values.put("iteration", iteration);
|
||||
return values;
|
||||
}
|
||||
|
||||
private LLMProvider resolveProvider(String providerName) {
|
||||
LLMProvider provider = llmProviders.get(providerName);
|
||||
if (provider != null) {
|
||||
|
|
@ -232,6 +308,22 @@ public class LoopContainerExecutor implements ContainerExecutor<LoopContainerTyp
|
|||
.orElseThrow(() -> new IllegalArgumentException("Provider not found: " + providerName));
|
||||
}
|
||||
|
||||
private void ensureAuthorization(LLMProvider llmProvider, String providerName, Map<String, Object> authorizations) {
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey))))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + providerName);
|
||||
}
|
||||
}
|
||||
|
||||
private String callLlmProvider(LLMProvider llmProvider, LLMDescriptor llmDescriptor, String prompt,
|
||||
Map<String, Object> authorizations) {
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
return llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt, String.valueOf(authorizations.get(authKey)))
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
}
|
||||
|
||||
private String normalizeExpression(String expression) {
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(expression);
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@
|
|||
"writeOnly": true,
|
||||
"minLength": 1,
|
||||
"x-ui-bindable-as-input": true,
|
||||
"x-ui-binding-static-label": "static value",
|
||||
"x-ui-binding-input-label": "node input",
|
||||
"x-ui-input-name": "key",
|
||||
"x-ui-input-type": "TEXT",
|
||||
"x-ui-input-multiple": false
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: java-springboot
|
||||
name: springboot-development
|
||||
description: 'Get best practices for developing applications with Spring Boot.'
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -173,6 +173,25 @@ public class BlocksControllerTest {
|
|||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("history")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createChatInteractionBlockCanHideHistoryOutput() {
|
||||
ChatInteractionBlockConfiguration config = ChatInteractionBlockConfiguration.builder()
|
||||
.name("Recruiter chat")
|
||||
.llmDescriptor(LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build())
|
||||
.goalDescription("Assess the candidate")
|
||||
.exposeHistory(false)
|
||||
.build();
|
||||
|
||||
Block<ChatInteractionBlockType> block = blocksController.create(config);
|
||||
|
||||
assertNotNull(block);
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
assertFalse(block.getOutputs().stream().anyMatch(output -> output.getName().equals("history")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createChatInteractionBlockDefaultsMissingInputTypeToText() {
|
||||
ChatInteractionBlockConfiguration config = ChatInteractionBlockConfiguration.builder()
|
||||
|
|
@ -308,6 +327,22 @@ public class BlocksControllerTest {
|
|||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("history") && output.isMultiple()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMcpAgentChatBlockCanHideHistoryOutput() {
|
||||
MCPAgentChatBlockConfiguration config = MCPAgentChatBlockConfiguration.builder()
|
||||
.name("Agent chat")
|
||||
.model("llama3.1:8b")
|
||||
.goalDescription("Assess the candidate")
|
||||
.exposeHistory(false)
|
||||
.build();
|
||||
|
||||
Block<MCPAgentChatBlockType> block = blocksController.create(config);
|
||||
|
||||
assertNotNull(block);
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response") && !output.isMultiple()));
|
||||
assertFalse(block.getOutputs().stream().anyMatch(output -> output.getName().equals("history")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionVariablesRetrieverReturnsSharedMcpSessionsForFlow() {
|
||||
Block<MCPAgentBlockType> block = blocksController.create(MCPAgentBlockConfiguration.builder()
|
||||
|
|
@ -456,6 +491,8 @@ public class BlocksControllerTest {
|
|||
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());
|
||||
assertEquals("useSharedSession", properties.path("model").path("x-ui-required-when").path("field").asText());
|
||||
assertEquals("false", properties.path("model").path("x-ui-required-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());
|
||||
|
|
@ -623,7 +660,8 @@ public class BlocksControllerTest {
|
|||
assertEquals(MCPAgentChatBlockType.TYPE, block.getName());
|
||||
assertNotNull(block.getSpecificConfiguration());
|
||||
assertTrue(((MCPAgentChatBlockConfiguration) block.getSpecificConfiguration()).getMcpServers().isEmpty());
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("model")));
|
||||
assertEquals("", ((MCPAgentChatBlockConfiguration) block.getSpecificConfiguration()).getModel());
|
||||
assertFalse(block.getInputs().stream().anyMatch(input -> input.getName().equals("model")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("history") && output.isMultiple()));
|
||||
}
|
||||
|
|
@ -783,6 +821,24 @@ public class BlocksControllerTest {
|
|||
assertEquals("true", prompt.path("x-ui-required-when").path("equals").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void conditionalBlockSchemaContainsLogicalPropertyOrderMetadata() {
|
||||
BlockConfigurationDescriptor descriptor = blocksController
|
||||
.getConfigurationDescriptorForType(ConditionalBlockType.TYPE);
|
||||
|
||||
assertNotNull(descriptor.schema());
|
||||
JsonNode schema = (JsonNode) descriptor.schema();
|
||||
|
||||
assertEquals(
|
||||
List.of("name", "useLlm", "condition", "llmDescriptor", "prompt", "outputTemplate", "type"),
|
||||
propertyNames(schema.path("properties")));
|
||||
assertEquals(
|
||||
List.of("name", "useLlm", "condition", "llmDescriptor", "prompt", "outputTemplate", "type"),
|
||||
arrayValues(schema.path("x-ui-property-order")));
|
||||
assertEquals(2, schema.path("properties").path("useLlm").path("x-ui-order").asInt());
|
||||
assertEquals(3, schema.path("properties").path("condition").path("x-ui-order").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void switchBlockSchemaContainsConditionalRequiredMetadata() {
|
||||
BlockConfigurationDescriptor descriptor = blocksController
|
||||
|
|
@ -826,7 +882,8 @@ public class BlocksControllerTest {
|
|||
assertEquals(MCPAgentBlockType.TYPE, block.getName());
|
||||
assertNotNull(block.getSpecificConfiguration());
|
||||
assertTrue(((MCPAgentBlockConfiguration) block.getSpecificConfiguration()).getMcpServers().isEmpty());
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("model")));
|
||||
assertEquals("", ((MCPAgentBlockConfiguration) block.getSpecificConfiguration()).getModel());
|
||||
assertFalse(block.getInputs().stream().anyMatch(input -> input.getName().equals("model")));
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("prompt")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
}
|
||||
|
|
@ -856,10 +913,24 @@ public class BlocksControllerTest {
|
|||
JsonNode model = schema.path("properties").path("model");
|
||||
assertTrue(model.path("x-ui-bindable-as-input").asBoolean());
|
||||
assertTrue(model.path("x-ui-structural").asBoolean());
|
||||
assertEquals("static value", model.path("x-ui-binding-static-label").asText());
|
||||
assertEquals("node input", model.path("x-ui-binding-input-label").asText());
|
||||
assertEquals("model", model.path("x-ui-input-name").asText());
|
||||
assertEquals("TEXT", model.path("x-ui-input-type").asText());
|
||||
assertFalse(model.path("x-ui-input-multiple").asBoolean());
|
||||
assertFalse(model.has("x-ui-input-description"));
|
||||
}
|
||||
|
||||
private List<String> propertyNames(JsonNode objectNode) {
|
||||
List<String> names = new java.util.ArrayList<>();
|
||||
objectNode.fieldNames().forEachRemaining(names::add);
|
||||
return names;
|
||||
}
|
||||
|
||||
private List<String> arrayValues(JsonNode arrayNode) {
|
||||
List<String> values = new java.util.ArrayList<>();
|
||||
arrayNode.forEach(node -> values.add(node.asText()));
|
||||
return values;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
|
|||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.Position;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
|
|
@ -201,9 +202,27 @@ public class ContainersControllerTest {
|
|||
JsonNode schema = (JsonNode) descriptor.schema();
|
||||
JsonNode useLlm = schema.path("properties").path("useLlm");
|
||||
JsonNode llmDescriptor = schema.path("properties").path("llmDescriptor");
|
||||
JsonNode prompt = schema.path("properties").path("prompt");
|
||||
JsonNode guardPrompt = schema.path("properties").path("guardPrompt");
|
||||
JsonNode guardCondition = schema.path("properties").path("guardCondition");
|
||||
JsonNode feedbackInput = schema.path("properties").path("feedbackInput");
|
||||
JsonNode feedbackPrompt = schema.path("properties").path("feedbackPrompt");
|
||||
JsonNode feedbackLlmDescriptor = schema.path("properties").path("feedbackLlmDescriptor");
|
||||
|
||||
assertEquals(
|
||||
List.of("name", "subFlow", "maxIterations", "useLlm", "guardCondition", "llmDescriptor",
|
||||
"guardPrompt", "feedbackInput", "feedbackLlmDescriptor", "feedbackPrompt", "containerType",
|
||||
"type"),
|
||||
propertyNames(schema.path("properties")));
|
||||
assertEquals(
|
||||
List.of("name", "subFlow", "maxIterations", "useLlm", "guardCondition", "llmDescriptor",
|
||||
"guardPrompt", "feedbackInput", "feedbackLlmDescriptor", "feedbackPrompt", "containerType",
|
||||
"type"),
|
||||
arrayValues(schema.path("x-ui-property-order")));
|
||||
assertEquals(4, useLlm.path("x-ui-order").asInt());
|
||||
assertEquals(5, guardCondition.path("x-ui-order").asInt());
|
||||
|
||||
assertTrue(useLlm.path("x-ui-structural").asBoolean());
|
||||
assertEquals("textarea", guardCondition.path("x-ui-widget").asText());
|
||||
|
||||
assertEquals("useLlm", llmDescriptor.path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("true", llmDescriptor.path("x-ui-enabled-when").path("equals").asText());
|
||||
|
|
@ -211,13 +230,50 @@ public class ContainersControllerTest {
|
|||
assertEquals("true", llmDescriptor.path("x-ui-required-when").path("equals").asText());
|
||||
assertEquals("llm", llmDescriptor.path("x-ui-group").asText());
|
||||
|
||||
assertTrue(prompt.path("x-ui-structural").asBoolean());
|
||||
assertEquals("textarea", prompt.path("x-ui-widget").asText());
|
||||
assertEquals("useLlm", prompt.path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("true", prompt.path("x-ui-enabled-when").path("equals").asText());
|
||||
assertEquals("useLlm", prompt.path("x-ui-required-when").path("field").asText());
|
||||
assertEquals("true", prompt.path("x-ui-required-when").path("equals").asText());
|
||||
assertEquals("llm", prompt.path("x-ui-group").asText());
|
||||
assertTrue(guardPrompt.path("x-ui-structural").asBoolean());
|
||||
assertEquals("textarea", guardPrompt.path("x-ui-widget").asText());
|
||||
assertEquals("useLlm", guardPrompt.path("x-ui-enabled-when").path("field").asText());
|
||||
assertEquals("true", guardPrompt.path("x-ui-enabled-when").path("equals").asText());
|
||||
assertEquals("useLlm", guardPrompt.path("x-ui-required-when").path("field").asText());
|
||||
assertEquals("true", guardPrompt.path("x-ui-required-when").path("equals").asText());
|
||||
assertEquals("llm", guardPrompt.path("x-ui-group").asText());
|
||||
|
||||
assertEquals("inputs", feedbackInput.path("x-ui-options-from-node").path("collection").asText());
|
||||
assertEquals("name", feedbackInput.path("x-ui-options-from-node").path("valueField").asText());
|
||||
assertEquals("name", feedbackInput.path("x-ui-options-from-node").path("labelField").asText());
|
||||
|
||||
assertEquals("feedbackInput", feedbackPrompt.path("x-ui-enabled-when").path("field").asText());
|
||||
assertTrue(feedbackPrompt.path("x-ui-required-when").path("present").asBoolean());
|
||||
assertEquals("feedback", feedbackPrompt.path("x-ui-group").asText());
|
||||
|
||||
assertEquals("feedbackPrompt", feedbackLlmDescriptor.path("x-ui-enabled-when").path("field").asText());
|
||||
assertTrue(feedbackLlmDescriptor.path("x-ui-required-when").path("present").asBoolean());
|
||||
assertEquals("feedback", feedbackLlmDescriptor.path("x-ui-group").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loopContainerAcceptsLegacyConditionAndPromptAliases() throws Exception {
|
||||
String payload = """
|
||||
{
|
||||
"type": "LoopContainerConfiguration",
|
||||
"name": "Loop",
|
||||
"subFlow": {},
|
||||
"condition": "${{response}} == 'done'",
|
||||
"useLlm": true,
|
||||
"llmDescriptor": {
|
||||
"provider": "testProvider",
|
||||
"model": "testModel"
|
||||
},
|
||||
"prompt": "Stop when ${{outputs.response}} is done",
|
||||
"maxIterations": 3
|
||||
}
|
||||
""";
|
||||
|
||||
LoopContainerConfiguration configuration = ObjectMapperHolder.mapper.readValue(payload,
|
||||
LoopContainerConfiguration.class);
|
||||
|
||||
assertEquals("${{response}} == 'done'", configuration.getGuardCondition());
|
||||
assertEquals("Stop when ${{outputs.response}} is done", configuration.getGuardPrompt());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -460,7 +516,7 @@ public class ContainersControllerTest {
|
|||
Container<LoopContainerType> container = containersController.create(LoopContainerConfiguration.builder()
|
||||
.name("Loop")
|
||||
.subFlow(FlowData.builder().block(internalBlock).build())
|
||||
.condition("${{response}} == 'done'")
|
||||
.guardCondition("${{response}} == 'done'")
|
||||
.useLlm(false)
|
||||
.maxIterations(3)
|
||||
.build());
|
||||
|
|
@ -469,4 +525,16 @@ public class ContainersControllerTest {
|
|||
assertTrue(container.getInputs().stream().anyMatch(input -> input.getName().equals("candidate")));
|
||||
assertTrue(container.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
}
|
||||
|
||||
private List<String> propertyNames(JsonNode objectNode) {
|
||||
List<String> names = new java.util.ArrayList<>();
|
||||
objectNode.fieldNames().forEachRemaining(names::add);
|
||||
return names;
|
||||
}
|
||||
|
||||
private List<String> arrayValues(JsonNode arrayNode) {
|
||||
List<String> values = new java.util.ArrayList<>();
|
||||
arrayNode.forEach(node -> values.add(node.asText()));
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ 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;
|
||||
import it.cnr.isti.workflow.manager.skills.SkillBinding;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
|
|
@ -109,6 +110,15 @@ public class ExecutionTest {
|
|||
if (model.contains("simulate") && prompt.contains("###SIMULATED_CHAT_FINAL###")) {
|
||||
return "FINAL: Candidate approved";
|
||||
}
|
||||
if (prompt.contains("Build function for request:")) {
|
||||
return prompt.contains("add null validation") ? "function v2" : "function v1";
|
||||
}
|
||||
if (prompt.contains("Construct the next request for the function loop")) {
|
||||
return "Implement the same function and add null validation";
|
||||
}
|
||||
if (prompt.contains("Loop guard via exposed outputs")) {
|
||||
return prompt.contains("Hello, Alice!") ? "{\"result\":true}" : "{\"result\":false}";
|
||||
}
|
||||
if (prompt.contains("You are a workflow switch evaluator.")) {
|
||||
return "{\"result\":\"approved\"}";
|
||||
}
|
||||
|
|
@ -393,6 +403,45 @@ public class ExecutionTest {
|
|||
assertEquals(4, ((List<?>) partialConversation).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatInteractionExecutionCanHideHistoryOutput() {
|
||||
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)))
|
||||
.exposeHistory(false)
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("Chat flow")
|
||||
.description("Single chat block without history output")
|
||||
.block(chatBlock)
|
||||
.build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
execObject = executionsService.prepareInput(execObject.getId(), chatBlock.getId(), "cand", "John Doe");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
execObject.setInteractionValue(chatBlock.getId(), ChatInteractionBlockFactory.INTERACTION_FIELD, "Hello ${{cand}}");
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
|
||||
assertFalse(execObject.getContext().getPartialResult()
|
||||
.containsKey(new FieldKey(chatBlock.getId(), ChatInteractionBlockFactory.HISTORY_OUTPUT)));
|
||||
|
||||
execObject.setInteractionValue(chatBlock.getId(), ChatInteractionBlockFactory.FINAL_RESPONSE_FIELD,
|
||||
"Candidate approved");
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertFalse(execObject.getContext().getResult()
|
||||
.containsKey(new FieldKey(chatBlock.getId(), ChatInteractionBlockFactory.HISTORY_OUTPUT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createExecutionRejectsChatInteractionWithoutLlmDescriptor() {
|
||||
Block<ChatInteractionBlockType> chatBlock = chatInteractionBlockFactory
|
||||
|
|
@ -556,6 +605,51 @@ public class ExecutionTest {
|
|||
assertEquals(5, queryCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mcpAgentChatExecutionCanHideHistoryOutput() {
|
||||
Mockito.when(mcpAgentService.openSession(Mockito.eq("llama3.1:8b"), Mockito.anyList(), Mockito.anyMap()))
|
||||
.thenReturn("session-no-history");
|
||||
Mockito.when(mcpAgentService.querySession(Mockito.eq("session-no-history"), Mockito.anyString()))
|
||||
.thenReturn("MCP answer");
|
||||
|
||||
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())
|
||||
.exposeHistory(false)
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("MCP Chat flow")
|
||||
.description("Single MCP chat block without history output")
|
||||
.block(chatBlock)
|
||||
.build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
execObject = executionsService.prepareInput(execObject.getId(), chatBlock.getId(), "cand", "John Doe");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
execObject.setInteractionValue(chatBlock.getId(), MCPAgentChatBlockFactory.INTERACTION_FIELD, "Hello ${{cand}}");
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
|
||||
assertFalse(execObject.getContext().getPartialResult()
|
||||
.containsKey(new FieldKey(chatBlock.getId(), MCPAgentChatBlockFactory.HISTORY_OUTPUT)));
|
||||
|
||||
execObject.setInteractionValue(chatBlock.getId(), MCPAgentChatBlockFactory.FINAL_RESPONSE_FIELD,
|
||||
"Candidate approved");
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
assertFalse(execObject.getContext().getResult()
|
||||
.containsKey(new FieldKey(chatBlock.getId(), MCPAgentChatBlockFactory.HISTORY_OUTPUT)));
|
||||
Mockito.verify(mcpAgentService).closeSessionQuietly("session-no-history");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionVariablesResolveInLlmPromptAndPersistAcrossReload() {
|
||||
Block<LLMBlockType> llmBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
|
|
@ -589,6 +683,38 @@ public class ExecutionTest {
|
|||
assertEquals("Ada Lovelace", restored.getContext().getExecutionVariables().get("person"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void llmExecutionPrependsSelectedSkillsToPrompt() {
|
||||
Block<LLMBlockType> llmBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
.name("Skillful prompt")
|
||||
.prompt("Implement a REST endpoint for ${{resource}}")
|
||||
.llmDescriptor(llmBrick)
|
||||
.skills(List.of(SkillBinding.builder()
|
||||
.skillId("springboot-development")
|
||||
.build()))
|
||||
.build());
|
||||
|
||||
Flow flow = Flow.builder()
|
||||
.name("Skill flow")
|
||||
.description("Single LLM block with skills")
|
||||
.block(llmBlock)
|
||||
.build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
executionsService.prepareInput(execObject.getId(), llmBlock.getId(), "resource", "orders");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
String response = (String) execObject.getContext().getResult()
|
||||
.get(new FieldKey(llmBlock.getId(), LLMBlockFactory.OUTPUT_NAME));
|
||||
assertTrue(response.contains("Apply the following local skills as mandatory operating instructions."));
|
||||
assertTrue(response.contains("[SKILL springboot-development]"));
|
||||
assertTrue(response.contains("Implement a REST endpoint for orders"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void switchExecutionCanReadGlobalVariableInSpelContext() {
|
||||
Block<SwitchBlockType> switchBlock = switchBlockFactory.create(SwitchBlockConfiguration.builder()
|
||||
|
|
@ -1157,7 +1283,7 @@ public class ExecutionTest {
|
|||
Container<LoopContainerType> container = loopContainerFactory.create(LoopContainerConfiguration.builder()
|
||||
.name("Loop")
|
||||
.subFlow(FlowData.builder().block(internalBlock).build())
|
||||
.condition("${{response}} == 'Hello, Alice!'")
|
||||
.guardCondition("${{response}} == 'Hello, Alice!'")
|
||||
.useLlm(false)
|
||||
.maxIterations(3)
|
||||
.build());
|
||||
|
|
@ -1188,6 +1314,106 @@ public class ExecutionTest {
|
|||
&& event.getDetails().get("iterationIndex") != null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loopConditionCanReferenceExposedOutputsExplicitly() {
|
||||
Block<LLMBlockType> internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
.name("Internal LLM")
|
||||
.llmDescriptor(llmBrick)
|
||||
.prompt("Hello, ${{name}}!")
|
||||
.build());
|
||||
|
||||
Container<LoopContainerType> container = loopContainerFactory.create(LoopContainerConfiguration.builder()
|
||||
.name("Loop")
|
||||
.subFlow(FlowData.builder().block(internalBlock).build())
|
||||
.guardCondition("${{outputs.response}} == 'Hello, Alice!'")
|
||||
.useLlm(false)
|
||||
.maxIterations(3)
|
||||
.build());
|
||||
|
||||
FlowData flow = FlowData.builder().container(container).build();
|
||||
ExecutionObject execObject = executionsService.createExecution("Loop outputs flow", flow);
|
||||
|
||||
executionsService.prepareInput(execObject.getId(), container.getId(), "name", "Alice");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
Object output = execObject.getContext().getResult().values().stream().findFirst().orElseThrow();
|
||||
assertEquals("Hello, Alice!", output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loopLlmGuardPromptCanReferenceExposedOutputsExplicitly() {
|
||||
Block<LLMBlockType> internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
.name("Internal LLM")
|
||||
.llmDescriptor(llmBrick)
|
||||
.prompt("Hello, ${{name}}!")
|
||||
.build());
|
||||
|
||||
Container<LoopContainerType> container = loopContainerFactory.create(LoopContainerConfiguration.builder()
|
||||
.name("Loop")
|
||||
.subFlow(FlowData.builder().block(internalBlock).build())
|
||||
.useLlm(true)
|
||||
.llmDescriptor(llmBrick)
|
||||
.guardPrompt("Loop guard via exposed outputs: stop when ${{outputs.response}} is exactly Hello, Alice!")
|
||||
.maxIterations(3)
|
||||
.build());
|
||||
|
||||
FlowData flow = FlowData.builder().container(container).build();
|
||||
ExecutionObject execObject = executionsService.createExecution("Loop llm outputs flow", flow);
|
||||
|
||||
executionsService.prepareInput(execObject.getId(), container.getId(), "name", "Alice");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
Object output = execObject.getContext().getResult().values().stream().findFirst().orElseThrow();
|
||||
assertEquals("Hello, Alice!", output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loopCanInjectFeedbackIntoNextIterationInput() {
|
||||
Block<LLMBlockType> internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
.name("Function Builder")
|
||||
.llmDescriptor(llmBrick)
|
||||
.prompt("Build function for request: ${{request}}")
|
||||
.build());
|
||||
|
||||
Container<LoopContainerType> container = loopContainerFactory.create(LoopContainerConfiguration.builder()
|
||||
.name("Loop")
|
||||
.subFlow(FlowData.builder().block(internalBlock).build())
|
||||
.guardCondition("${{outputs.response}} == 'function v2'")
|
||||
.feedbackInput("request")
|
||||
.feedbackLlmDescriptor(llmBrick)
|
||||
.feedbackPrompt(
|
||||
"Construct the next request for the function loop. Previous request: ${{inputs.request}} Previous output: ${{outputs.response}} Iteration: ${{iteration}}")
|
||||
.useLlm(false)
|
||||
.maxIterations(3)
|
||||
.build());
|
||||
|
||||
FlowData flow = FlowData.builder().container(container).build();
|
||||
ExecutionObject execObject = executionsService.createExecution("Loop feedback flow", flow);
|
||||
|
||||
executionsService.prepareInput(execObject.getId(), container.getId(), "request", "Implement the same function");
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
while (execObject.getContext().getStatus() == ExecutionStatus.RUNNING) {
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
assertEquals(ExecutionStatus.SUCCESS, execObject.getContext().getStatus());
|
||||
Object output = execObject.getContext().getResult().values().stream().findFirst().orElseThrow();
|
||||
assertEquals("function v2", output);
|
||||
assertTrue(execObject.getContext().getEvents().stream()
|
||||
.anyMatch(event -> event.getType() == ExecutionEventType.LLM_REQUEST
|
||||
&& "LoopContainer".equals(event.getDetails().get("containerType"))
|
||||
&& "feedbackInput".equals(event.getDetails().get("purpose"))
|
||||
&& "request".equals(event.getDetails().get("inputName"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTextInputRejectsMultipleValues() {
|
||||
Flow flow = flowTestCreator.createFlowwithLLMUnpromptedWithConnection(llmBrick);
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ class MCPAgentServiceTest {
|
|||
"format": "password",
|
||||
"writeOnly": true,
|
||||
"x-ui-bindable-as-input": true,
|
||||
"x-ui-binding-static-label": "static value",
|
||||
"x-ui-binding-input-label": "node input",
|
||||
"x-ui-input-name": "key",
|
||||
"x-ui-input-type": "TEXT",
|
||||
"x-ui-input-multiple": false
|
||||
|
|
@ -298,6 +300,8 @@ class MCPAgentServiceTest {
|
|||
"format": "password",
|
||||
"writeOnly": true,
|
||||
"x-ui-bindable-as-input": true,
|
||||
"x-ui-binding-static-label": "static value",
|
||||
"x-ui-binding-input-label": "node input",
|
||||
"x-ui-input-name": "key",
|
||||
"x-ui-input-type": "TEXT",
|
||||
"x-ui-input-multiple": false
|
||||
|
|
@ -318,6 +322,10 @@ class MCPAgentServiceTest {
|
|||
assertEquals("password", definition.configurationSchema().path("properties").path("key").path("format").asText());
|
||||
assertTrue(definition.configurationSchema().path("properties").path("key").path("writeOnly").asBoolean());
|
||||
assertTrue(definition.configurationSchema().path("properties").path("key").path("x-ui-bindable-as-input").asBoolean());
|
||||
assertEquals("static value",
|
||||
definition.configurationSchema().path("properties").path("key").path("x-ui-binding-static-label").asText());
|
||||
assertEquals("node input",
|
||||
definition.configurationSchema().path("properties").path("key").path("x-ui-binding-input-label").asText());
|
||||
}
|
||||
|
||||
private MCPServersProvider providerFor(String json) throws Exception {
|
||||
|
|
|
|||
Loading…
Reference in New Issue