Add HTTP server call block and retire MCP agent
This commit is contained in:
parent
8c11dbb191
commit
9e475ca09b
|
|
@ -0,0 +1,115 @@
|
|||
package it.cnr.isti.workflow.manager.blocks.configurations;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.annotations.UiDependency;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class HTTPServerCallBlockConfiguration extends BlockConfiguration<HTTPServerCallBlockType> {
|
||||
|
||||
@NotBlank
|
||||
@JsonProperty(required = true)
|
||||
private String url;
|
||||
|
||||
@NotNull
|
||||
@JsonProperty(required = true)
|
||||
private HTTPMethod method;
|
||||
|
||||
@Structural
|
||||
@UiDependency(field = "method", equalsAny = { "POST", "PUT" })
|
||||
@LongText(
|
||||
placeholder = "Optional HTTP request body.",
|
||||
tip = "Use ${{}} placeholders to inject workflow variables into the URL or request body.",
|
||||
acceptVariableAsPlaceholder = true)
|
||||
@JsonProperty(required = false)
|
||||
private String requestBody;
|
||||
|
||||
@NotNull
|
||||
@JsonProperty(required = true)
|
||||
private Boolean requiresAuthorization;
|
||||
|
||||
@NotNull
|
||||
@UiDependency(field = "requiresAuthorization", equals = "true", group = "authorization")
|
||||
@JsonProperty(required = true)
|
||||
private HTTPAuthorizationType authorizationType;
|
||||
|
||||
@UiDependency(field = "authorizationType", equals = "API_KEY", group = "authorization")
|
||||
@JsonProperty(required = false)
|
||||
private String authorizationHeaderName;
|
||||
|
||||
@Builder
|
||||
public HTTPServerCallBlockConfiguration(@NonNull String name,
|
||||
@NonNull String url,
|
||||
@NonNull HTTPMethod method,
|
||||
String requestBody,
|
||||
@NonNull Boolean requiresAuthorization,
|
||||
@NonNull HTTPAuthorizationType authorizationType,
|
||||
String authorizationHeaderName) {
|
||||
super(name);
|
||||
this.url = url;
|
||||
this.method = method;
|
||||
this.requestBody = requestBody;
|
||||
this.requiresAuthorization = requiresAuthorization;
|
||||
this.authorizationType = authorizationType;
|
||||
this.authorizationHeaderName = authorizationHeaderName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<HTTPServerCallBlockType> getBlockType() {
|
||||
return HTTPServerCallBlockType.class;
|
||||
}
|
||||
|
||||
public static HTTPServerCallBlockConfiguration empty() {
|
||||
HTTPServerCallBlockConfiguration configuration = new HTTPServerCallBlockConfiguration();
|
||||
configuration.name = HTTPServerCallBlockType.TYPE;
|
||||
configuration.method = HTTPMethod.GET;
|
||||
configuration.requiresAuthorization = false;
|
||||
configuration.authorizationType = HTTPAuthorizationType.API_KEY;
|
||||
configuration.authorizationHeaderName = "X-API-Key";
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public String authorizationKey(String blockId) {
|
||||
return "HTTPServerCall::" + blockId + "::" + authorizationFieldName();
|
||||
}
|
||||
|
||||
public String authorizationFieldName() {
|
||||
return switch (authorizationType) {
|
||||
case API_KEY -> "apiKey";
|
||||
case CREDENTIALS -> "credentials";
|
||||
};
|
||||
}
|
||||
|
||||
public String authorizationDescription() {
|
||||
return switch (authorizationType) {
|
||||
case API_KEY -> "HTTP API key value for " + url
|
||||
+ (authorizationHeaderName == null || authorizationHeaderName.isBlank()
|
||||
? ""
|
||||
: " using header " + authorizationHeaderName);
|
||||
case CREDENTIALS -> "HTTP Basic credentials for " + url + " in the format username:password";
|
||||
};
|
||||
}
|
||||
|
||||
public enum HTTPAuthorizationType {
|
||||
API_KEY,
|
||||
CREDENTIALS
|
||||
}
|
||||
|
||||
public enum HTTPMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT
|
||||
}
|
||||
}
|
||||
|
|
@ -411,7 +411,15 @@ public class JsonSchemaProducer {
|
|||
}
|
||||
ObjectNode visibleWhen = propertySchema.putObject("x-ui-visible-when");
|
||||
visibleWhen.put("field", dependency.field());
|
||||
visibleWhen.put("equals", dependency.equals());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
package it.cnr.isti.workflow.manager.blocks.configurations;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.annotations.LongText;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.annotations.Structural;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MCPAgentBlockConfiguration extends BlockConfiguration<MCPAgentBlockType> {
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty(required = true)
|
||||
private LLMDescriptor llmDescriptor;
|
||||
|
||||
@NotBlank
|
||||
@Structural
|
||||
@LongText(
|
||||
placeholder = "Define the system-level behavior of the MCP agent",
|
||||
tip = "Use this to constrain style, goals, and safety behavior for the agent.")
|
||||
@JsonProperty(required = true)
|
||||
private String systemPrompt;
|
||||
|
||||
@NotBlank
|
||||
@Structural
|
||||
@LongText(
|
||||
placeholder = "Describe the task the agent must solve. Use ${{}} for workflow variables.",
|
||||
tip = "Inputs are derived automatically from placeholders in the task prompt.",
|
||||
acceptVariableAsPlaceholder = true)
|
||||
@JsonProperty(required = true)
|
||||
private String taskPrompt;
|
||||
|
||||
@NotBlank
|
||||
@LongText(
|
||||
placeholder = "Describe available MCP servers and tools as JSON or structured text",
|
||||
tip = "This MVP passes MCP capabilities as context to the model; it does not execute tools directly.")
|
||||
@JsonProperty(required = true)
|
||||
private String mcpContext;
|
||||
|
||||
@LongText(
|
||||
placeholder = "Executable command used to start an MCP server over stdio, for example npx",
|
||||
tip = "Optional. When present, the MCP agent will try to start and use a real MCP server.")
|
||||
private String mcpServerCommand;
|
||||
|
||||
@LongText(
|
||||
placeholder = "Command arguments as JSON array or space separated text, for example [\"-y\",\"@modelcontextprotocol/server-filesystem\",\"/tmp\"]",
|
||||
tip = "Optional. Used together with mcpServerCommand for real MCP execution.")
|
||||
private String mcpServerArgs;
|
||||
|
||||
@NotNull
|
||||
@Min(1)
|
||||
@JsonProperty(required = true)
|
||||
private Integer maxIterations;
|
||||
|
||||
@Builder
|
||||
public MCPAgentBlockConfiguration(@NonNull String name, @NonNull LLMDescriptor llmDescriptor,
|
||||
@NonNull String systemPrompt, @NonNull String taskPrompt, @NonNull String mcpContext,
|
||||
String mcpServerCommand, String mcpServerArgs, @NonNull Integer maxIterations) {
|
||||
super(name);
|
||||
this.llmDescriptor = llmDescriptor;
|
||||
this.systemPrompt = systemPrompt;
|
||||
this.taskPrompt = taskPrompt;
|
||||
this.mcpContext = mcpContext;
|
||||
this.mcpServerCommand = mcpServerCommand;
|
||||
this.mcpServerArgs = mcpServerArgs;
|
||||
this.maxIterations = maxIterations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<MCPAgentBlockType> getBlockType() {
|
||||
return MCPAgentBlockType.class;
|
||||
}
|
||||
|
||||
public static MCPAgentBlockConfiguration empty() {
|
||||
MCPAgentBlockConfiguration configuration = new MCPAgentBlockConfiguration();
|
||||
configuration.name = MCPAgentBlockType.TYPE;
|
||||
configuration.maxIterations = 3;
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,9 @@ import java.lang.annotation.Target;
|
|||
public @interface UiDependency {
|
||||
String field();
|
||||
|
||||
String equals();
|
||||
String equals() default "";
|
||||
|
||||
String[] equalsAny() default {};
|
||||
|
||||
String group() default "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package it.cnr.isti.workflow.manager.blocks.factories;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
|
||||
import it.cnr.isti.workflow.manager.ios.IODescriptor;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
|
||||
@Component
|
||||
public class HTTPServerCallBlockFactory implements BlockFactory<HTTPServerCallBlockType, HTTPServerCallBlockConfiguration> {
|
||||
|
||||
public static final String OUTPUT_NAME = "response";
|
||||
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{\\{(.*?)}}");
|
||||
|
||||
@Autowired
|
||||
private HTTPServerCallBlockType blockType;
|
||||
|
||||
@Override
|
||||
public Block<HTTPServerCallBlockType> create(HTTPServerCallBlockConfiguration configuration) {
|
||||
return Block.<HTTPServerCallBlockType>builder()
|
||||
.inputs(retrieveInputs(configuration))
|
||||
.output(IODescriptor.of(OUTPUT_NAME, IOType.TEXT))
|
||||
.specificConfiguration(configuration)
|
||||
.type(blockType)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block<HTTPServerCallBlockType> createEmpty() {
|
||||
return create(HTTPServerCallBlockConfiguration.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<HTTPServerCallBlockType> getBlockType() {
|
||||
return HTTPServerCallBlockType.class;
|
||||
}
|
||||
|
||||
private List<IODescriptor> retrieveInputs(HTTPServerCallBlockConfiguration configuration) {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
collectPlaceholders(names, configuration.getUrl());
|
||||
collectPlaceholders(names, configuration.getRequestBody());
|
||||
return names.stream().map(name -> IODescriptor.of(name, IOType.TEXT)).toList();
|
||||
}
|
||||
|
||||
private void collectPlaceholders(Set<String> names, String template) {
|
||||
if (template == null || template.isBlank()) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
|
||||
while (matcher.find()) {
|
||||
names.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
package it.cnr.isti.workflow.manager.blocks.factories;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.ios.IODescriptor;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
|
||||
@Component
|
||||
public class MCPAgentBlockFactory implements BlockFactory<MCPAgentBlockType, MCPAgentBlockConfiguration> {
|
||||
|
||||
public static final String RESPONSE_OUTPUT = "response";
|
||||
public static final String TRACE_OUTPUT = "trace";
|
||||
public static final String INPUT_NAME = "task";
|
||||
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{\\{(.*?)}}");
|
||||
|
||||
@Autowired
|
||||
private MCPAgentBlockType blockType;
|
||||
|
||||
@Override
|
||||
public Block<MCPAgentBlockType> create(MCPAgentBlockConfiguration configuration) {
|
||||
List<IODescriptor> inputs = retrieveInputs(configuration);
|
||||
return Block.<MCPAgentBlockType>builder()
|
||||
.inputs(inputs)
|
||||
.output(IODescriptor.of(RESPONSE_OUTPUT, IOType.TEXT))
|
||||
.output(IODescriptor.of(TRACE_OUTPUT, IOType.TEXT))
|
||||
.specificConfiguration(configuration)
|
||||
.type(blockType)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block<MCPAgentBlockType> createEmpty() {
|
||||
return create(MCPAgentBlockConfiguration.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<MCPAgentBlockType> getBlockType() {
|
||||
return MCPAgentBlockType.class;
|
||||
}
|
||||
|
||||
private List<IODescriptor> retrieveInputs(MCPAgentBlockConfiguration configuration) {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
collectPlaceholders(configuration.getSystemPrompt(), names);
|
||||
collectPlaceholders(configuration.getTaskPrompt(), names);
|
||||
if (names.isEmpty()) {
|
||||
return List.of(IODescriptor.of(INPUT_NAME, IOType.TEXT));
|
||||
}
|
||||
return names.stream().map(name -> IODescriptor.of(name, IOType.TEXT)).toList();
|
||||
}
|
||||
|
||||
private void collectPlaceholders(String value, Set<String> names) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
|
||||
while (matcher.find()) {
|
||||
names.add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,12 @@ package it.cnr.isti.workflow.manager.blocks.types;
|
|||
import org.springframework.stereotype.Component;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.BlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
|
||||
@Component(MCPAgentBlockType.TYPE)
|
||||
public class MCPAgentBlockType implements BlockType {
|
||||
@Component(HTTPServerCallBlockType.TYPE)
|
||||
public class HTTPServerCallBlockType implements BlockType {
|
||||
|
||||
public static final String TYPE = "MCPAgentBlock";
|
||||
public static final String TYPE = "HTTPServerCall";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
|
|
@ -17,7 +17,7 @@ public class MCPAgentBlockType implements BlockType {
|
|||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "An agent-style block that uses an LLM with declared MCP tool context";
|
||||
return "Calls an external HTTP endpoint using a configured URL, optional request body, and optional runtime authorization";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -32,6 +32,6 @@ public class MCPAgentBlockType implements BlockType {
|
|||
|
||||
@Override
|
||||
public Class<? extends BlockConfiguration<?>> getBlockConfigurationClass() {
|
||||
return MCPAgentBlockConfiguration.class;
|
||||
return HTTPServerCallBlockConfiguration.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@ import org.springframework.stereotype.Service;
|
|||
|
||||
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.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.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Flow;
|
||||
import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
||||
import it.cnr.isti.workflow.manager.flows.validation.FlowExecutionValidator;
|
||||
|
|
@ -108,6 +108,7 @@ public class ExecutionsService {
|
|||
Map<String, RequirementAccumulator> requirements = new LinkedHashMap<>();
|
||||
for (Block<?> block : flow.getBlocks()) {
|
||||
collectRequirement(requirements, block, resolveDescriptor(block), block == null ? null : block.getName());
|
||||
collectHttpRequirement(requirements, block);
|
||||
}
|
||||
return requirements.values().stream()
|
||||
.map(RequirementAccumulator::toRequirement)
|
||||
|
|
@ -129,9 +130,6 @@ public class ExecutionsService {
|
|||
&& conditionalConfiguration.isUseLlm()) {
|
||||
return conditionalConfiguration.getLlmDescriptor();
|
||||
}
|
||||
if (configuration instanceof MCPAgentBlockConfiguration mcpAgentConfiguration) {
|
||||
return mcpAgentConfiguration.getLlmDescriptor();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +149,20 @@ public class ExecutionsService {
|
|||
.addStepReference(block == null ? null : block.getId(), stepName);
|
||||
}
|
||||
|
||||
private void collectHttpRequirement(Map<String, RequirementAccumulator> requirements, Block<?> block) {
|
||||
if (block == null || !(block.getSpecificConfiguration() instanceof HTTPServerCallBlockConfiguration configuration)) {
|
||||
return;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(configuration.getRequiresAuthorization())) {
|
||||
return;
|
||||
}
|
||||
String key = configuration.authorizationKey(block.getId());
|
||||
requirements.computeIfAbsent(key,
|
||||
ignored -> new RequirementAccumulator(key, "HTTP", configuration.authorizationFieldName(),
|
||||
configuration.authorizationDescription()))
|
||||
.addStepReference(block.getId(), block.getName());
|
||||
}
|
||||
|
||||
private LLMProvider resolveProvider(String providerName) {
|
||||
LLMProvider provider = llmProviders.get(providerName);
|
||||
if (provider != null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package it.cnr.isti.workflow.manager.executions.executors;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.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.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.http.HTTPServerCallService;
|
||||
|
||||
@Component
|
||||
public class HTTPServerCallExecutor implements BlockExecutor<HTTPServerCallBlockType> {
|
||||
|
||||
@Autowired
|
||||
private HTTPServerCallService httpServerCallService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<HTTPServerCallBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
HTTPServerCallBlockConfiguration config = (HTTPServerCallBlockConfiguration) block.getSpecificConfiguration();
|
||||
String resolvedUrl = resolveTemplate(config.getUrl(), inputs);
|
||||
String resolvedRequestBody = resolveTemplate(config.getRequestBody(), inputs);
|
||||
String authorizationValue = null;
|
||||
if (Boolean.TRUE.equals(config.getRequiresAuthorization())) {
|
||||
String authorizationKey = config.authorizationKey(block.getId());
|
||||
Object rawAuthorization = authorizations.get(authorizationKey);
|
||||
authorizationValue = rawAuthorization == null ? null : rawAuthorization.toString();
|
||||
if (!StringUtils.hasText(authorizationValue)) {
|
||||
throw new IllegalArgumentException("Missing authorization for HTTP server call block " + block.getName());
|
||||
}
|
||||
}
|
||||
String response = httpServerCallService.execute(config, resolvedUrl, resolvedRequestBody, authorizationValue);
|
||||
return Map.of(HTTPServerCallBlockFactory.OUTPUT_NAME, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<HTTPServerCallBlockType> getBlockType() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
package it.cnr.isti.workflow.manager.executions.executors;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
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.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.McpStdioClient;
|
||||
|
||||
@Component
|
||||
public class MCPAgentExecutor implements BlockExecutor<MCPAgentBlockType> {
|
||||
|
||||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<MCPAgentBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations) {
|
||||
MCPAgentBlockConfiguration config = (MCPAgentBlockConfiguration) block.getSpecificConfiguration();
|
||||
LLMDescriptor llmDescriptor = config.getLlmDescriptor();
|
||||
LLMProvider llmProvider = resolveProvider(llmDescriptor.provider());
|
||||
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(authorizations.get(authKey).toString()))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
|
||||
String response;
|
||||
String trace;
|
||||
if (StringUtils.hasText(config.getMcpServerCommand())) {
|
||||
AgentExecutionResult result = executeWithMcp(config, inputs, llmProvider, llmDescriptor, authorizations, authKey);
|
||||
response = result.response();
|
||||
trace = result.trace();
|
||||
} else {
|
||||
String prompt = buildPrompt(config, inputs);
|
||||
response = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt, authorizations.get(authKey).toString())
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
trace = "MCP agent executed in single-shot mode with declared MCP context and maxIterations="
|
||||
+ config.getMaxIterations();
|
||||
}
|
||||
|
||||
return Map.of(
|
||||
MCPAgentBlockFactory.RESPONSE_OUTPUT, response,
|
||||
MCPAgentBlockFactory.TRACE_OUTPUT, trace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<MCPAgentBlockType> getBlockType() {
|
||||
return MCPAgentBlockType.class;
|
||||
}
|
||||
|
||||
private LLMProvider resolveProvider(String providerName) {
|
||||
LLMProvider llmProvider = llmProviders.get(providerName);
|
||||
if (llmProvider != null) {
|
||||
return llmProvider;
|
||||
}
|
||||
return llmProviders.values().stream()
|
||||
.filter(provider -> provider.getName().equals(providerName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Provider not found: " + providerName));
|
||||
}
|
||||
|
||||
private String buildPrompt(MCPAgentBlockConfiguration config, List<Input> inputs) {
|
||||
String systemPrompt = resolvePlaceholders(config.getSystemPrompt(), inputs);
|
||||
String taskPrompt = resolvePlaceholders(config.getTaskPrompt(), inputs);
|
||||
return """
|
||||
You are operating as an MCP-enabled workflow agent.
|
||||
This implementation is single-shot: you cannot actually call tools, but you must reason using the declared MCP context.
|
||||
|
||||
System prompt:
|
||||
%s
|
||||
|
||||
Declared MCP context:
|
||||
%s
|
||||
|
||||
Max iterations budget:
|
||||
%s
|
||||
|
||||
Task:
|
||||
%s
|
||||
|
||||
Return the best final answer for the task.
|
||||
Do not describe internal chain-of-thought.
|
||||
""".formatted(systemPrompt, config.getMcpContext(), config.getMaxIterations(), taskPrompt);
|
||||
}
|
||||
|
||||
private AgentExecutionResult executeWithMcp(MCPAgentBlockConfiguration config, List<Input> inputs, LLMProvider llmProvider,
|
||||
LLMDescriptor llmDescriptor, Map<String, Object> authorizations, String authKey) {
|
||||
try (McpStdioClient client = new McpStdioClient(config.getMcpServerCommand(), parseCommandArgs(config.getMcpServerArgs()))) {
|
||||
client.initialize();
|
||||
JsonNode tools = client.listTools();
|
||||
StringBuilder trace = new StringBuilder();
|
||||
trace.append("MCP stdio server started: ").append(config.getMcpServerCommand()).append("\n");
|
||||
String scratchpad = "";
|
||||
for (int iteration = 1; iteration <= config.getMaxIterations(); iteration++) {
|
||||
AgentDecision decision = decideNextStep(config, inputs, tools, scratchpad, llmProvider, llmDescriptor,
|
||||
authorizations, authKey, iteration);
|
||||
if ("final".equalsIgnoreCase(decision.action())) {
|
||||
trace.append("Iteration ").append(iteration).append(": final answer produced\n");
|
||||
return new AgentExecutionResult(decision.answer(), trace.toString().trim());
|
||||
}
|
||||
if (!"tool".equalsIgnoreCase(decision.action())) {
|
||||
throw new IllegalArgumentException("Unsupported MCP agent action: " + decision.action());
|
||||
}
|
||||
|
||||
JsonNode toolResult = client.callTool(decision.toolName(),
|
||||
decision.arguments() == null ? ObjectMapperHolder.mapper.createObjectNode() : decision.arguments());
|
||||
String toolResultJson = ObjectMapperHolder.mapper.writeValueAsString(toolResult);
|
||||
trace.append("Iteration ").append(iteration).append(": called tool ")
|
||||
.append(decision.toolName()).append(" with result ").append(toolResultJson).append("\n");
|
||||
scratchpad = scratchpad + "\nTool " + decision.toolName() + " result: " + toolResultJson;
|
||||
}
|
||||
|
||||
String finalPrompt = """
|
||||
You are an MCP-enabled workflow agent.
|
||||
Provide the final answer using the information already gathered.
|
||||
|
||||
System prompt:
|
||||
%s
|
||||
|
||||
Task:
|
||||
%s
|
||||
|
||||
Scratchpad:
|
||||
%s
|
||||
""".formatted(
|
||||
resolvePlaceholders(config.getSystemPrompt(), inputs),
|
||||
resolvePlaceholders(config.getTaskPrompt(), inputs),
|
||||
scratchpad);
|
||||
|
||||
String answer = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), finalPrompt, authorizations.get(authKey).toString())
|
||||
: llmProvider.generate(llmDescriptor.model(), finalPrompt);
|
||||
trace.append("Reached max iterations and requested final answer");
|
||||
return new AgentExecutionResult(answer, trace.toString().trim());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("MCP agent execution failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private AgentDecision decideNextStep(MCPAgentBlockConfiguration config, List<Input> inputs, JsonNode tools,
|
||||
String scratchpad, LLMProvider llmProvider, LLMDescriptor llmDescriptor, Map<String, Object> authorizations,
|
||||
String authKey, int iteration) throws Exception {
|
||||
String prompt = """
|
||||
You are an MCP-enabled workflow agent.
|
||||
You may either produce a final answer or request one MCP tool call.
|
||||
Return only JSON with one of these shapes:
|
||||
{"action":"final","answer":"..."}
|
||||
{"action":"tool","toolName":"...","arguments":{...}}
|
||||
|
||||
System prompt:
|
||||
%s
|
||||
|
||||
Task:
|
||||
%s
|
||||
|
||||
Available tools:
|
||||
%s
|
||||
|
||||
Scratchpad:
|
||||
%s
|
||||
|
||||
Current iteration:
|
||||
%s
|
||||
""".formatted(
|
||||
resolvePlaceholders(config.getSystemPrompt(), inputs),
|
||||
resolvePlaceholders(config.getTaskPrompt(), inputs),
|
||||
ObjectMapperHolder.mapper.writeValueAsString(tools),
|
||||
scratchpad == null || scratchpad.isBlank() ? "(empty)" : scratchpad,
|
||||
iteration);
|
||||
|
||||
String rawResponse = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt, authorizations.get(authKey).toString())
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
String json = extractJsonObject(rawResponse);
|
||||
JsonNode node = ObjectMapperHolder.mapper.readTree(json);
|
||||
String action = node.path("action").asText();
|
||||
String answer = node.path("answer").asText(null);
|
||||
String toolName = node.path("toolName").asText(null);
|
||||
JsonNode arguments = node.path("arguments");
|
||||
if (arguments.isMissingNode() || arguments.isNull()) {
|
||||
arguments = ObjectMapperHolder.mapper.createObjectNode();
|
||||
}
|
||||
return new AgentDecision(action, toolName, arguments, answer);
|
||||
}
|
||||
|
||||
private List<String> parseCommandArgs(String rawArgs) {
|
||||
if (!StringUtils.hasText(rawArgs)) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
JsonNode node = ObjectMapperHolder.mapper.readTree(rawArgs);
|
||||
if (node.isArray()) {
|
||||
List<String> args = new ArrayList<>();
|
||||
for (JsonNode item : node) {
|
||||
args.add(item.asText());
|
||||
}
|
||||
return args;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return List.of(rawArgs.trim().split("\\s+"));
|
||||
}
|
||||
|
||||
private String resolvePlaceholders(String template, List<Input> inputs) {
|
||||
String resolved = template;
|
||||
if (resolved == null) {
|
||||
return null;
|
||||
}
|
||||
for (Input input : inputs) {
|
||||
Object value = input.getValue();
|
||||
resolved = resolved.replace("${{" + input.getDescriptor().getName() + "}}",
|
||||
value == null ? "null" : value.toString());
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String extractJsonObject(String rawResponse) {
|
||||
if (!StringUtils.hasText(rawResponse)) {
|
||||
throw new IllegalArgumentException("Empty LLM response for MCP agent decision");
|
||||
}
|
||||
String trimmed = rawResponse.trim();
|
||||
int start = trimmed.indexOf('{');
|
||||
int end = trimmed.lastIndexOf('}');
|
||||
if (start < 0 || end < start) {
|
||||
throw new IllegalArgumentException("No JSON object found in MCP agent decision");
|
||||
}
|
||||
return trimmed.substring(start, end + 1);
|
||||
}
|
||||
|
||||
private record AgentDecision(String action, String toolName, JsonNode arguments, String answer) {
|
||||
}
|
||||
|
||||
private record AgentExecutionResult(String response, String trace) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package it.cnr.isti.workflow.manager.http;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
|
||||
@Component
|
||||
public class HTTPServerCallService {
|
||||
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
|
||||
public HTTPServerCallService(WebClient.Builder webClientBuilder) {
|
||||
this.webClientBuilder = Objects.requireNonNull(webClientBuilder, "webClientBuilder cannot be null");
|
||||
}
|
||||
|
||||
public String execute(HTTPServerCallBlockConfiguration configuration, String resolvedUrl, String resolvedRequestBody,
|
||||
String authorizationValue) {
|
||||
WebClient.RequestHeadersSpec<?> requestSpec;
|
||||
WebClient webClient = webClientBuilder.build();
|
||||
|
||||
requestSpec = switch (configuration.getMethod()) {
|
||||
case GET -> {
|
||||
WebClient.RequestHeadersUriSpec<?> getSpec = webClient.get();
|
||||
WebClient.RequestHeadersSpec<?> headersSpec = getSpec.uri(resolvedUrl);
|
||||
applyAuthorization(configuration, headersSpec, authorizationValue);
|
||||
yield headersSpec;
|
||||
}
|
||||
case POST -> {
|
||||
WebClient.RequestBodySpec bodySpec = webClient.post()
|
||||
.uri(resolvedUrl)
|
||||
.contentType(MediaType.TEXT_PLAIN);
|
||||
applyAuthorization(configuration, bodySpec, authorizationValue);
|
||||
yield bodySpec.bodyValue(resolvedRequestBody == null ? "" : resolvedRequestBody);
|
||||
}
|
||||
case PUT -> {
|
||||
WebClient.RequestBodySpec bodySpec = webClient.put()
|
||||
.uri(resolvedUrl)
|
||||
.contentType(MediaType.TEXT_PLAIN);
|
||||
applyAuthorization(configuration, bodySpec, authorizationValue);
|
||||
yield bodySpec.bodyValue(resolvedRequestBody == null ? "" : resolvedRequestBody);
|
||||
}
|
||||
};
|
||||
|
||||
return requestSpec.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
}
|
||||
|
||||
private void applyAuthorization(HTTPServerCallBlockConfiguration configuration,
|
||||
WebClient.RequestHeadersSpec<?> requestSpec, String authorizationValue) {
|
||||
if (!Boolean.TRUE.equals(configuration.getRequiresAuthorization())) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasText(authorizationValue)) {
|
||||
throw new IllegalArgumentException("Missing authorization for HTTP server call block");
|
||||
}
|
||||
switch (configuration.getAuthorizationType()) {
|
||||
case API_KEY -> requestSpec.header(resolveApiKeyHeader(configuration), authorizationValue);
|
||||
case CREDENTIALS -> requestSpec.header(HttpHeaders.AUTHORIZATION, toBasicAuthorization(authorizationValue));
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveApiKeyHeader(HTTPServerCallBlockConfiguration configuration) {
|
||||
return StringUtils.hasText(configuration.getAuthorizationHeaderName())
|
||||
? configuration.getAuthorizationHeaderName()
|
||||
: "X-API-Key";
|
||||
}
|
||||
|
||||
private String toBasicAuthorization(String credentials) {
|
||||
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
|
||||
return "Basic " + encoded;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,16 +10,16 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPBridgeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.BlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.HTTPServerCallBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.LLMBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.MCPBridgeBlockFactory;
|
||||
import it.cnr.isti.workflow.manager.blocks.factories.MCPAgentBlockFactory;
|
||||
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.MCPBridgeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
|
||||
@SpringBootTest
|
||||
|
|
@ -33,10 +33,10 @@ public class BlockTest {
|
|||
LLMBlockType llmBlockType;
|
||||
|
||||
@Autowired
|
||||
MCPAgentBlockFactory mcpAgentBlockFactory;
|
||||
MCPBridgeBlockFactory mcpBridgeBlockFactory;
|
||||
|
||||
@Autowired
|
||||
MCPBridgeBlockFactory mcpBridgeBlockFactory;
|
||||
HTTPServerCallBlockFactory httpServerCallBlockFactory;
|
||||
|
||||
|
||||
@Test
|
||||
|
|
@ -62,35 +62,6 @@ public class BlockTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
void createMCPAgentBlock() {
|
||||
BlockFactory<MCPAgentBlockType, MCPAgentBlockConfiguration> factory = mcpAgentBlockFactory;
|
||||
|
||||
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build();
|
||||
|
||||
MCPAgentBlockConfiguration config = MCPAgentBlockConfiguration.builder()
|
||||
.name("agent")
|
||||
.llmDescriptor(llmDescriptor)
|
||||
.systemPrompt("You are an expert recruiter.")
|
||||
.taskPrompt("Analyze candidate ${{candidate}} for role ${{role}}")
|
||||
.mcpContext("""
|
||||
{
|
||||
"servers": ["cv-parser", "scoring-engine"]
|
||||
}
|
||||
""")
|
||||
.maxIterations(3)
|
||||
.build();
|
||||
|
||||
Block<MCPAgentBlockType> block = factory.create(config);
|
||||
assertNotNull(block);
|
||||
assertNotNull(block.getSpecificConfiguration());
|
||||
assertNotNull(block.getInputs());
|
||||
assertNotNull(block.getOutputs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createMCPBridgeBlock() {
|
||||
BlockFactory<MCPBridgeBlockType, MCPBridgeBlockConfiguration> factory = mcpBridgeBlockFactory;
|
||||
|
|
@ -118,4 +89,25 @@ public class BlockTest {
|
|||
assertNotNull(((MCPBridgeBlockConfiguration) block.getSpecificConfiguration()).getMcpServers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createHTTPServerCallBlock() {
|
||||
BlockFactory<HTTPServerCallBlockType, HTTPServerCallBlockConfiguration> factory = httpServerCallBlockFactory;
|
||||
|
||||
HTTPServerCallBlockConfiguration config = HTTPServerCallBlockConfiguration.builder()
|
||||
.name("Remote call")
|
||||
.url("https://example.com/users/${{userId}}")
|
||||
.method(HTTPServerCallBlockConfiguration.HTTPMethod.POST)
|
||||
.requestBody("{\"message\":\"Hello ${{name}}\"}")
|
||||
.requiresAuthorization(true)
|
||||
.authorizationType(HTTPServerCallBlockConfiguration.HTTPAuthorizationType.API_KEY)
|
||||
.authorizationHeaderName("X-API-Key")
|
||||
.build();
|
||||
|
||||
Block<HTTPServerCallBlockType> block = factory.create(config);
|
||||
assertNotNull(block);
|
||||
assertNotNull(block.getSpecificConfiguration());
|
||||
assertNotNull(block.getInputs());
|
||||
assertNotNull(block.getOutputs());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HumanInteractionBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPBridgeBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.MCPAgentBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.controllers.BlocksController.BlockConfigurationDescriptor;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.HTTPServerCallBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPBridgeBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.MCPAgentBlockType;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.app.ObjectMapperHolder;
|
||||
|
||||
|
|
@ -138,42 +138,6 @@ public class BlocksControllerTest {
|
|||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("output")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMCPAgentBlock() {
|
||||
MCPAgentBlockConfiguration config = MCPAgentBlockConfiguration.builder()
|
||||
.name("CV agent")
|
||||
.llmDescriptor(LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
.build())
|
||||
.systemPrompt("You are an HR assistant.")
|
||||
.taskPrompt("Analyze CV for ${{candidate}} against role ${{role}}")
|
||||
.mcpContext("servers: cv-parser, recruiter-db")
|
||||
.maxIterations(4)
|
||||
.build();
|
||||
|
||||
Block<MCPAgentBlockType> block = blocksController.create(config);
|
||||
assertNotNull(block);
|
||||
assertEquals(MCPAgentBlockType.TYPE, block.getType().getName());
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("candidate")));
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("role")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("trace")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMCPAgentExampleForType() {
|
||||
Block<MCPAgentBlockType> block = blocksController.getExampleForType(MCPAgentBlockType.TYPE);
|
||||
|
||||
assertNotNull(block);
|
||||
assertEquals(MCPAgentBlockType.TYPE, block.getType().getName());
|
||||
assertEquals(MCPAgentBlockType.TYPE, block.getName());
|
||||
assertNotNull(block.getSpecificConfiguration());
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("task")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("trace")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMCPBridgeBlock() {
|
||||
MCPBridgeBlockConfiguration config = MCPBridgeBlockConfiguration.builder()
|
||||
|
|
@ -194,6 +158,59 @@ public class BlocksControllerTest {
|
|||
assertEquals("filesystem", ((MCPBridgeBlockConfiguration) block.getSpecificConfiguration()).getMcpServers().getFirst().serverName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHTTPServerCallBlock() {
|
||||
HTTPServerCallBlockConfiguration config = HTTPServerCallBlockConfiguration.builder()
|
||||
.name("External API")
|
||||
.url("https://example.com/users/${{userId}}")
|
||||
.method(HTTPServerCallBlockConfiguration.HTTPMethod.POST)
|
||||
.requestBody("{\"message\":\"Hello ${{name}}\"}")
|
||||
.requiresAuthorization(true)
|
||||
.authorizationType(HTTPServerCallBlockConfiguration.HTTPAuthorizationType.CREDENTIALS)
|
||||
.build();
|
||||
|
||||
Block<HTTPServerCallBlockType> block = blocksController.create(config);
|
||||
assertNotNull(block);
|
||||
assertEquals(HTTPServerCallBlockType.TYPE, block.getType().getName());
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("userId")));
|
||||
assertTrue(block.getInputs().stream().anyMatch(input -> input.getName().equals("name")));
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHTTPServerCallExampleForType() {
|
||||
Block<HTTPServerCallBlockType> block = blocksController.getExampleForType(HTTPServerCallBlockType.TYPE);
|
||||
|
||||
assertNotNull(block);
|
||||
assertEquals(HTTPServerCallBlockType.TYPE, block.getType().getName());
|
||||
assertEquals(HTTPServerCallBlockType.TYPE, block.getName());
|
||||
assertNotNull(block.getSpecificConfiguration());
|
||||
assertTrue(block.getInputs().isEmpty());
|
||||
assertTrue(block.getOutputs().stream().anyMatch(output -> output.getName().equals("response")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void httpServerCallSchemaContainsAuthorizationHints() {
|
||||
BlockConfigurationDescriptor descriptor = blocksController
|
||||
.getConfigurationDescriptorForType(HTTPServerCallBlockType.TYPE);
|
||||
|
||||
JsonNode schema = (JsonNode) descriptor.schema();
|
||||
JsonNode method = schema.path("properties").path("method");
|
||||
JsonNode requestBody = schema.path("properties").path("requestBody");
|
||||
JsonNode authType = schema.path("properties").path("authorizationType");
|
||||
JsonNode authHeader = schema.path("properties").path("authorizationHeaderName");
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMCPBridgeExampleForType() {
|
||||
Block<MCPBridgeBlockType> block = blocksController.getExampleForType(MCPBridgeBlockType.TYPE);
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ import it.cnr.isti.workflow.manager.flows.model.FlowData;
|
|||
import it.cnr.isti.workflow.manager.flows.FlowTestCreator;
|
||||
import it.cnr.isti.workflow.manager.flows.model.Flow;
|
||||
import it.cnr.isti.workflow.manager.blocks.Block;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.HTTPServerCallBlockConfiguration;
|
||||
import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration;
|
||||
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.types.HTTPServerCallBlockType;
|
||||
import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
|
@ -68,6 +71,9 @@ public class ExecutionTest {
|
|||
@Autowired
|
||||
LLMBlockFactory llmBlockFactory;
|
||||
|
||||
@Autowired
|
||||
HTTPServerCallBlockFactory httpServerCallBlockFactory;
|
||||
|
||||
LLMDescriptor llmBrick = LLMDescriptor.builder()
|
||||
.provider("testProvider")
|
||||
.model("testModel")
|
||||
|
|
@ -156,6 +162,27 @@ public class ExecutionTest {
|
|||
.allMatch(step -> step.getStatus() == StepStatus.READY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createHttpServerCallExecutionRequiresAuthorization() {
|
||||
Block<HTTPServerCallBlockType> block = httpServerCallBlockFactory.create(HTTPServerCallBlockConfiguration.builder()
|
||||
.name("Protected endpoint")
|
||||
.url("https://example.com/ping")
|
||||
.method(HTTPServerCallBlockConfiguration.HTTPMethod.GET)
|
||||
.requiresAuthorization(true)
|
||||
.authorizationType(HTTPServerCallBlockConfiguration.HTTPAuthorizationType.API_KEY)
|
||||
.authorizationHeaderName("X-API-Key")
|
||||
.build());
|
||||
FlowData flow = FlowData.builder().block(block).build();
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution("Protected HTTP Call", flow);
|
||||
|
||||
assertEquals(ExecutionStatus.CREATED, execObject.getContext().getStatus());
|
||||
assertEquals(1, execObject.getRequiredAuthorizations().size());
|
||||
assertEquals("apiKey", execObject.getRequiredAuthorizations().getFirst().fieldName());
|
||||
assertTrue(execObject.getMissingAuthorizationKeys().contains(
|
||||
((HTTPServerCallBlockConfiguration) block.getSpecificConfiguration()).authorizationKey(block.getId())));
|
||||
}
|
||||
|
||||
private ExecutionObject createExecutionAndSetInputInternally() {
|
||||
Flow flow = flowTestCreator.createFlowWithConnection(llmBrick);
|
||||
ExecutionObject execObject = executionsService.createExecution(flow);
|
||||
|
|
|
|||
Loading…
Reference in New Issue