diff --git a/pom.xml b/pom.xml index 88e796f..5f337c2 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,11 @@ spring-boot-starter-test test + + com.h2database + h2 + test + diff --git a/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java b/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java index 16744c3..d678784 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/TransformerService.java @@ -31,27 +31,53 @@ public class TransformerService { @Autowired NodeDefinitionRepository nodeDefinitionRepository; - private ExecutionObject execObject; - public ExecutionObject transform(Flow flow) { - execObject = new ExecutionObject(flow); - ExecutionStep startStep = createStep(flow, null, execObject.getContext(), flow.getStartNode(), null); - execObject.setStartStep(startStep); + + Map steps = new HashMap<>(); + Map inputs = new HashMap<>(); + Map outputs = new HashMap<>(); + + ExecutionObject execObject = new ExecutionObject(flow); + + for (Node node : flow.getNodes()) { + ExecutionStep step = getStep(node, execObject.getContext()); + steps.put(node.getKey(), step); + + if (node.getKey().equals(flow.getStartNode())) { + Map mappingInputName = new HashMap<>(); + step.getNodeDefinition().getInputs().forEach((k, v) -> mappingInputName.put(k, k)); + log.debug("setting {} to step {}", mappingInputName, step.getId()); + step.setParentOutputsToInputMapping(mappingInputName); + execObject.setStartStep(step); + node.getOutputs().forEach(o -> outputs.put(o.getKey(), new IDPair(node.getKey(), o.getName()))); + } else if (node.getKey().equals(flow.getEndNode())) { + execObject.setEndStep(step); + node.getInputs().forEach(i -> inputs.put(i.getKey(), new IDPair(node.getKey(), i.getName()))); + } else { + node.getOutputs().forEach(o -> outputs.put(o.getKey(), new IDPair(node.getKey(), o.getName()))); + node.getInputs().forEach(i -> inputs.put(i.getKey(), new IDPair(node.getKey(), i.getName()))); + } + } + + for (Connection connection : flow.getConnections()) { + String from = connection.getFrom(); + String to = connection.getTo(); + if (!inputs.containsKey(to)) + throw new RuntimeException("Input " + to + " not found"); + if (!outputs.containsKey(from)) + throw new RuntimeException("Output " + from + " not found"); + IDPair inputPair = inputs.get(to); + IDPair outputPair = outputs.get(from); + ExecutionStep outputNode = steps.get(outputPair.nodeId); + ExecutionStep inputNode = steps.get(inputPair.nodeId); + outputNode.getNextSteps().add(inputNode); + inputNode.getParentOutputsToInputMapping().put(outputPair.name, inputPair.name); + } + return execObject; } - private ExecutionStep createStep(Flow flow, ExecutionStep parent, ExecutionContext context, String nodeId, - Map mappingNodeOutputNameToChildInputName) { - // TODO: cosa succede se ho input da più nodi in un solo nodo ? devo tenere - // traccia dei nodi già visitati e non ricreare lo step ma aggiungerlo solamente - // ai previous steps - - Node node = flow.getNodes().stream().filter(n -> n.getKey().equals(nodeId)).findFirst() - .orElseThrow(() -> new RuntimeException("node " + nodeId + " not found")); - - // prendo tutte le gli id in uscita dal nodo - List outputs = node.getOutputs(); - + private ExecutionStep getStep(Node node, ExecutionContext context) { // prende l'executor per il tipo di nodo NodeDefinition nodeDefinition = nodeDefinitionRepository.findById(node.getType()) .orElseThrow(() -> new RuntimeException("Node definition not found for node type: " + node.getType())); @@ -63,48 +89,16 @@ public class TransformerService { .executor(executors.get(nodeDefinition.getExecutor())) .runtimeParameters(node.getParameters()).nodeDefinition(nodeDefinition).context(context).build(); - if (parent == null) { // se è il primo step devo settare il mapping degli input - Map mappingInputName = new HashMap<>(); - nodeDefinition.getInputs().forEach((k, v) -> mappingInputName.put(k, k)); - log.debug("setting {} to step {}", mappingInputName, step.getId()); - step.setParentOutputsToInputMapping(mappingInputName); - } else { - step.setParentOutputsToInputMapping(mappingNodeOutputNameToChildInputName); - } - - if (!nodeId.equals(flow.getEndNode())) { - - // mantiene il mapping tra nome output del nodo inviante e nome input del nodo - // ricevente - Map currentMappingNodeOutputNameToChildInputName = new HashMap<>(); - - List childNodes = new ArrayList<>(); - - for (IOModel output : outputs) { - for (Connection connection : flow.getConnections()) { - if (!connection.getFrom().equals(output.getKey())) - continue; - String inputId = connection.getTo(); - Node nodeFound = flow.getNodes().stream() - .filter(n -> n.getInputs().stream().anyMatch(i -> i.getKey().equals(inputId))).findFirst() - .orElseThrow(() -> new RuntimeException("node " + inputId + " not found")); - - // prendo il nome dell'input e lo mappo con l'output - IOModel input = nodeFound.getInputs().stream().filter(i -> i.getKey().equals(inputId)).findFirst() - .orElseThrow(() -> new RuntimeException("input " + inputId + " not found")); - currentMappingNodeOutputNameToChildInputName.put(output.getName(), input.getName()); - childNodes.add(nodeFound); - } - } - // creo ricorsivamente i prossimi step - step.getNextSteps().addAll(childNodes.stream() - .map(n -> createStep(flow, step, context, n.getKey(), currentMappingNodeOutputNameToChildInputName)) - .toList()); - } else - execObject.setEndStep(step); - return step; - } + private static class IDPair { + public String nodeId; + public String name; + + public IDPair(String nodeId, String name) { + this.nodeId = nodeId; + this.name = name; + } + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java b/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java index 9833f98..8907b7f 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java +++ b/src/main/java/it/cnr/isti/workflow/manager/WorkflowManagerApplication.java @@ -8,6 +8,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; +import org.springframework.util.ResourceUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; import it.cnr.isti.workflow.manager.executors.ai.AIModel; import it.cnr.isti.workflow.manager.model.types.IOType; @@ -16,7 +19,9 @@ import it.cnr.isti.workflow.manager.model.types.NodeDefinition; import it.cnr.isti.workflow.manager.model.types.ParameterDefinition; import it.cnr.isti.workflow.manager.model.types.ParameterType; import it.cnr.isti.workflow.manager.model.types.Translator; -import it.cnr.isti.workflow.manager.model.types.Validation; +import it.cnr.isti.workflow.manager.model.ui.Flow; +import it.cnr.isti.workflow.manager.model.ui.FlowEntity; +import it.cnr.isti.workflow.manager.repositories.FlowRepository; import it.cnr.isti.workflow.manager.repositories.NodeDefinitionRepository; @SpringBootApplication @@ -31,7 +36,7 @@ public class WorkflowManagerApplication { @Bean @ConditionalOnProperty(prefix = "app", name = "db.init.enabled", havingValue = "true") - CommandLineRunner init(NodeDefinitionRepository repository) { + CommandLineRunner init(NodeDefinitionRepository repository, FlowRepository flowRepository) { return args -> { JSONObject rPars = new JSONObject().put("options", @@ -39,18 +44,16 @@ public class WorkflowManagerApplication { ParameterDefinition rgParams = ParameterDefinition.builder().name("LLM").type(ParameterType.Select) .label("Select the LLM to use").description("The LLM to use for generating requirements") - .validation(Validation.builder().name("required").validator("required").build()) - .specificAttributes(rPars).build(); + .required(true).specificAttributes(rPars).build(); JSONObject languageOptions = new JSONObject().put("options", List.of("Franch", "English", "Italian", "spanish", "German", "Chinese", "Japanese")); ParameterDefinition langParam = ParameterDefinition.builder().name("language").type(ParameterType.Select) - .label("Language").description("The language to translate to") - .validation(Validation.builder().name("required").validator("required").build()) + .label("Language").description("The language to translate to").required(true) .specificAttributes(languageOptions).build(); - Translator inputTranslator = Translator.builder().usedKey(Key.inputKey("phrase")).usedKey(Key.runtimeKey("language")) + Translator inputTranslator = Translator.builder() .translation("translate the following phrase ${{phrase}} to ${{language}} returning only the translated").build(); NodeDefinition translator = NodeDefinition.builder().type("Translator") @@ -63,9 +66,39 @@ public class WorkflowManagerApplication { repository.save(translator); + loadNodeDefinitionAndSave(repository); + + loadFlowsAndSave(flowRepository); }; } + private void loadNodeDefinitionAndSave(NodeDefinitionRepository repository) { + ObjectMapper objectMapper = new ObjectMapper(); + + try{ + + List nodes = objectMapper.readValue(ResourceUtils.getFile("classpath:saved-node-type.json"), objectMapper.getTypeFactory().constructCollectionType(List.class,NodeDefinition.class)); + for (NodeDefinition node: nodes) + repository.save(node); + } catch (Exception e) { + e.printStackTrace(); + } + } + private void loadFlowsAndSave(FlowRepository repository) { + ObjectMapper objectMapper = new ObjectMapper(); + + try{ + + List flows = objectMapper.readValue(ResourceUtils.getFile("classpath:flows.json"), objectMapper.getTypeFactory().constructCollectionType(List.class,FlowEntity.class)); + for (FlowEntity flow: flows){ + FlowEntity savedFlow = repository.save(flow); + savedFlow.getFlow().setId(savedFlow.getId()); + repository.save(savedFlow); + } + } catch (Exception e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionController.java index 9341962..97fbfba 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionController.java +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/ExecutionController.java @@ -6,7 +6,6 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.server.WebServerException; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -27,6 +26,8 @@ import org.springframework.web.bind.annotation.PathVariable; @RequestMapping("/executions") public class ExecutionController { + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ExecutionController.class); + @Autowired FlowRepository flowRepository; @@ -35,10 +36,16 @@ public class ExecutionController { @PostMapping() public ExecutionObject create(@RequestBody String flowId) { + logger.info("Creating execution for flow {}", flowId); FlowEntity flow = flowRepository.findById(flowId) .orElseThrow(() -> new IllegalArgumentException("Flow with id " + flowId + " not found")); - ExecutionObject eo = executionService.createExecution(flow); - return eo; + try{ + ExecutionObject eo = executionService.createExecution(flow); + return eo; + } catch (Throwable e) { + logger.error("Error creating execution for flow {}", flowId, e); + throw new WebServerException("Error while creating execution", e); + } } @GetMapping() diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/NodeDefinitionController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/NodeDefinitionController.java index 006a0de..7774995 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/controllers/NodeDefinitionController.java +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/NodeDefinitionController.java @@ -1,11 +1,9 @@ package it.cnr.isti.workflow.manager.controllers; import java.util.List; -import java.util.Map; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,7 +25,7 @@ public class NodeDefinitionController { private NodeDefinitionRepository repository; @Autowired - private Map executors; + private List executors; @GetMapping("/nodes") public List getNodes() { @@ -51,8 +49,7 @@ public class NodeDefinitionController { @GetMapping("/executors") public List getAvailableExecutors() { - return executors.entrySet().stream() - .map((entry) -> new ExecutorDescriptor(entry.getKey(), entry.getValue())).toList(); + return executors.stream().map(entry -> entry.getDescriptor(null)).toList(); } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/exceptions/ExecutionException.java b/src/main/java/it/cnr/isti/workflow/manager/exceptions/ExecutionException.java new file mode 100644 index 0000000..8cccd6b --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/exceptions/ExecutionException.java @@ -0,0 +1,24 @@ +package it.cnr.isti.workflow.manager.exceptions; + +public class ExecutionException extends Exception { + + private static final long serialVersionUID = 1L; + + public ExecutionException(String message) { + super(message); + } + + public ExecutionException(String message, Throwable cause) { + super(message, cause); + } + + public ExecutionException(Throwable cause) { + super(cause); + } + + public ExecutionException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/executors/Executor.java b/src/main/java/it/cnr/isti/workflow/manager/executors/Executor.java index f9f9146..d83ceb2 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executors/Executor.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executors/Executor.java @@ -1,21 +1,15 @@ package it.cnr.isti.workflow.manager.executors; -import java.util.List; import java.util.Map; -import it.cnr.isti.workflow.manager.model.types.ParameterDefinition; +import it.cnr.isti.workflow.manager.exceptions.ExecutionException; +import it.cnr.isti.workflow.manager.model.ExecutorDescriptor; public interface Executor { - Map execute(Map userParameters, Map preExecutionParameters, Map inputsFromParent); + Map execute(Map userParameters, Map preExecutionParameters, Map inputsFromParent) throws ExecutionException; - Map> getRequiredPreExecutionParameters(Map userParameters); - - List declaredOutputNames(); - - List declaredInputNames(); - - List getMandatoryParameters(); - + ExecutorDescriptor getDescriptor(Map executorDescriptorParameters); + String getName(); } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executors/NoOpExecutor.java b/src/main/java/it/cnr/isti/workflow/manager/executors/NoOpExecutor.java index 3bb596a..d037d75 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executors/NoOpExecutor.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executors/NoOpExecutor.java @@ -1,21 +1,34 @@ package it.cnr.isti.workflow.manager.executors; import org.springframework.stereotype.Service; + +import java.lang.reflect.Parameter; import java.util.List; import java.util.Map; + +import it.cnr.isti.workflow.manager.model.ExecutorDescriptor; +import it.cnr.isti.workflow.manager.model.Validations; import it.cnr.isti.workflow.manager.model.types.ParameterDefinition; +import it.cnr.isti.workflow.manager.model.types.ParameterType; import lombok.ToString; @Service("NoOp") @ToString(of = {"name"}) public class NoOpExecutor implements Executor { - private static final String name = "NoOp"; + + private static final String name = "No Operation Executor"; @Override public Map execute(Map userParameters, Map preExecutionParameters, Map inputsFromParent) { - return inputsFromParent; + return Map.of("output", inputsFromParent.get("input")); + } + + @Override + public ExecutorDescriptor getDescriptor(Map executorDescriptorParameters) { + return ExecutorDescriptor.builder().identifier("NoOp").name(name).inputName("input").outputName("output") + .description("No operation executor").build(); } @Override @@ -23,24 +36,19 @@ public class NoOpExecutor implements Executor { return name; } - @Override - public List getMandatoryParameters() { - return List.of(); - } - - @Override public Map> getRequiredPreExecutionParameters(Map userParameters) { return Map.of(); } - @Override - public List declaredOutputNames() { - return List.of(); - } - - @Override - public List declaredInputNames() { - return List.of(); + public List getDynamicDescriptorParameters() { + ParameterDefinition parameterDefinition = ParameterDefinition.builder() + .name("IONumber") + .description("The Input/Output Number") + .type(ParameterType.Number).required(true) + .validation(Validations.minValidator(1)) + .validation(Validations.maxValidator(4)) + .build(); + return List.of(parameterDefinition); } } \ No newline at end of file diff --git a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/AIModel.java b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/AIModel.java index c1f7f9b..d4a5c92 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/AIModel.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/AIModel.java @@ -8,7 +8,7 @@ public interface AIModel { String getDescription(); - String executePrompt(Map parameters, String prompt); + String executePrompt(Map parameters, String prompt) throws Throwable; public Map> getRequiredPreExecutionParameters(); diff --git a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java index 3218871..7501394 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GeminiModel.java @@ -1,5 +1,6 @@ package it.cnr.isti.workflow.manager.executors.ai; +import java.io.IOException; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -8,6 +9,9 @@ import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; + import it.cnr.isti.workflow.manager.executors.ai.services.GeminiService; @Service("google-gemini") @@ -31,33 +35,27 @@ public class GeminiModel implements AIModel { return "Gemini is an LLM model provided by Google"; } - @Override - public String executePrompt(Map parameters, String prompt) { + public String executePrompt(Map parameters, String prompt) throws Throwable { geminiService.setApiKey("***REMOVED-API-KEY***"); + //log.info("------- REQUEST ------------"); + //log.info(prompt); + //log.info("-------------------"); String result = geminiService.getResponse(prompt).block(); - log.debug("google gemini called with result {}",result); + ObjectMapper objectMapper = new ObjectMapper(); - //TODO: Remove, delay only for testing - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - log.error("Error during sleep", e); - } + // Deserializzazione della risposta JSON in un oggetto GeminiResponse + GeminiResponse response = objectMapper.readValue(result, GeminiResponse.class); - result = parseResponse(result); - log.debug("post parsing: {}",result); - return result; - - } + // Estrazione del testo dal primo candidato + String candidateText = response.getCandidates().get(0).getContent().getParts().get(0).getText(); + + //log.info("------- RESPONSE ------------"); + //log.info(candidateText); + //log.info("-------------------"); + + return candidateText; - private String parseResponse(String response) { - Pattern pattern = Pattern.compile("\"text\"\\s*:\\s*\"(.*?)\""); - Matcher matcher = pattern.matcher(response); - if (matcher.find()) { - return matcher.group(1); // Estratto il testo - } - return response; } @@ -66,12 +64,56 @@ public class GeminiModel implements AIModel { return Map.of("API-KEY", String.class); } - /* - @Override - public Map execute(Map userParameters, Map preExecutionParameters, - Map inputsFromParent) { - String returnString = (String)executePrompt(preExecutionParameters, (String) inputsFromParent.get("prompt")); - return Map.of("response", returnString); - }*/ - +} + +@JsonIgnoreProperties(ignoreUnknown = true) +class GeminiResponse { + private java.util.List candidates; + + public java.util.List getCandidates() { + return candidates; + } + + public void setCandidates(java.util.List candidates) { + this.candidates = candidates; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Candidate { + private Content content; + + public Content getContent() { + return content; + } + + public void setContent(Content content) { + this.content = content; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Content { + private java.util.List parts; + + public java.util.List getParts() { + return parts; + } + + public void setParts(java.util.List parts) { + this.parts = parts; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Part { + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GenericAIExecutor.java b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GenericAIExecutor.java index 81742a0..1d9a3df 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GenericAIExecutor.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/GenericAIExecutor.java @@ -3,14 +3,18 @@ package it.cnr.isti.workflow.manager.executors.ai; import java.util.List; import java.util.Map; import java.util.Objects; + import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import it.cnr.isti.workflow.manager.exceptions.ExecutionException; import it.cnr.isti.workflow.manager.executors.Executor; +import it.cnr.isti.workflow.manager.model.ExecutorDescriptor; import it.cnr.isti.workflow.manager.model.types.ParameterDefinition; import it.cnr.isti.workflow.manager.model.types.ParameterType; -import it.cnr.isti.workflow.manager.model.types.Validation; import lombok.ToString; @Service("GENERIC-AI") @@ -18,6 +22,7 @@ import lombok.ToString; public class GenericAIExecutor implements Executor { //EVERY executor in ai model must return a Map with the key "response" and the value of the response + private static final Logger log = LoggerFactory.getLogger(GenericAIExecutor.class); private static final String name = "Generic AI Executor"; @@ -30,13 +35,18 @@ public class GenericAIExecutor implements Executor { @Override public Map execute(Map userParameters, Map preExecutionParameters, - Map inputsFromParent) { + Map inputsFromParent) throws ExecutionException { Objects.requireNonNull(userParameters.get("LLM"), "'LLM' parameter is required for 'GENERIC-AI' executor"); Objects.requireNonNull(aiModels.get(userParameters.get("LLM")), "AI model not found: " + userParameters.get("LLM")); AIModel aiModel = aiModels.get(userParameters.get("LLM")); String prompt = (String) inputsFromParent.get("prompt"); - String result = aiModel.executePrompt(userParameters, prompt); - return Map.of("response", result); + try{ + String result = aiModel.executePrompt(userParameters, prompt); + return Map.of("response", result); + }catch (Throwable e) { + log.error("Error executing AI model: {}", e.getMessage(), e); + throw new ExecutionException("Error executing AI model: " + e.getMessage(), e); + } } @@ -47,37 +57,29 @@ public class GenericAIExecutor implements Executor { } @Override + public ExecutorDescriptor getDescriptor(Map executorDescriptorParameters) { + return ExecutorDescriptor.builder().identifier("GENERIC-AI").name(name).inputName("prompt").outputName("response") + .description("Generic AI executor").mandatoryParameters(getMandatoryParameters()).build(); + } + public Map> getRequiredPreExecutionParameters(Map userParameters) { Objects.requireNonNull(userParameters.get("LLM"), "'LLM' parameter is required for 'GENERIC-AI' executor"); Objects.requireNonNull(aiModels.get(userParameters.get("LLM")), "AI model not found: " + userParameters.get("LLM")); return aiModels.get(userParameters.get("LLM")).getRequiredPreExecutionParameters(); } - @Override - public List declaredOutputNames() { - return List.of("response"); - } - @Override - public List declaredInputNames() { - return List.of("prompt"); - } - - @Override public List getMandatoryParameters() { JSONObject rPars = new JSONObject().put("options", aiModels.keySet()); - ParameterDefinition rgParams = ParameterDefinition.builder().name("LLM").type(ParameterType.Select) .label("Select the LLM to use").description("The LLM to use for generating requirements") - .validation(Validation.builder().name("required").validator("required").build()) + .required(true) .specificAttributes(rPars).build(); return List.of(rgParams); } - - - + } diff --git a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/services/GeminiService.java b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/services/GeminiService.java index 987e00f..2da3633 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executors/ai/services/GeminiService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executors/ai/services/GeminiService.java @@ -4,7 +4,9 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Objects; @@ -34,16 +36,27 @@ public class GeminiService { ))); WebClient webClient = webClientBuilder.baseUrl(GEMINI_URL).build(); - Mono result = webClient.post() + Mono result = webClient.post() .uri(uriBuilder -> uriBuilder.path("v1beta/models/gemini-2.0-flash:generateContent") .queryParam("key", apiKey) .build()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() - .bodyToMono(String.class); - - - return result; + .onStatus( + status -> status.is5xxServerError(), + clientResponse -> clientResponse.bodyToMono(String.class) + .defaultIfEmpty("Error: server without body") + .flatMap(body -> Mono.error(new RuntimeException("Errore 5xx: " + body)))) + .bodyToMono(String.class) + .timeout(Duration.ofMinutes(2)) + .retryWhen( + Retry.backoff(10, Duration.ofSeconds(30)) + .filter(throwable -> throwable instanceof RuntimeException + || throwable instanceof java.util.concurrent.TimeoutException) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> new RuntimeException( + "Error contacting Gemini API", retrySignal.failure()))); + + return result; } } \ No newline at end of file diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java b/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java index 067dbf5..f793c47 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/ExecutionStep.java @@ -9,9 +9,9 @@ import java.util.Set; import org.slf4j.Logger; +import it.cnr.isti.workflow.manager.exceptions.ExecutionException; import it.cnr.isti.workflow.manager.executors.Executor; import it.cnr.isti.workflow.manager.model.ExecutionContext.Status; -import it.cnr.isti.workflow.manager.model.types.Key; import it.cnr.isti.workflow.manager.model.types.NodeDefinition; import it.cnr.isti.workflow.manager.model.types.Translator; import lombok.AllArgsConstructor; @@ -37,13 +37,13 @@ public class ExecutionStep implements InputReadyListener { boolean startAutomatically = true; Executor executor; - Map parentOutputsToInputMapping; + Map parentOutputsToInputMapping = new HashMap<>(); Map inputs = new HashMap<>(); List nextSteps = new ArrayList<>(); - List previouSteps = new ArrayList<>(); + //List previouSteps = new ArrayList<>(); @NonNull NodeDefinition nodeDefinition; @@ -63,8 +63,6 @@ public class ExecutionStep implements InputReadyListener { @Override public void inputReady(String inputKey, Object inputValue) { - log.debug("Input ready for step {} with key {} and value {}", id, inputKey, inputValue); - parentOutputsToInputMapping.forEach((k, v) -> log.debug("mapping {} -> {}", k, v)); if (parentOutputsToInputMapping.containsKey(inputKey)) { inputs.put(parentOutputsToInputMapping.get(inputKey), inputValue); if (startAutomatically) startExecution(); @@ -72,6 +70,7 @@ public class ExecutionStep implements InputReadyListener { }; public boolean areAllInputsReady() { + log.debug("checking inputs {} - {} ({},{})", inputs.keySet(), parentOutputsToInputMapping.keySet(), inputs.size(), parentOutputsToInputMapping.size()); return inputs.size() == parentOutputsToInputMapping.size(); } @@ -81,26 +80,22 @@ public class ExecutionStep implements InputReadyListener { this.context.getStepsUnderExecution().add(this.id); try { Map realInputs = prepareInputs(); - log.debug("input translation for step {} is {}", id, realInputs); + //log.debug("input translation for step {} is {}", id, realInputs); Map returned = executor.execute(this.runtimeParameters, null, realInputs); - log.debug("returned from executor {} for step {} is {}", executor.getClass().getName(), id, - returned); this.context.getNodeResult().put(this.id, returned); for (InputReadyListener nextStep : nextSteps) for (Map.Entry entry : returned.entrySet()) nextStep.inputReady(!nodeDefinition.getExecutorToOutputTranslationMappings().isEmpty() ? nodeDefinition.getExecutorToOutputTranslationMappings().get(entry.getKey()) : entry.getKey(), entry.getValue()); - } catch (Throwable e) { + } catch (ExecutionException e) { log.error("error during execution of step {}: {}", id, e.getMessage()); this.context.getErrors().add(String.format("[%S] %s", this.id, e.getMessage())); this.context.setStatus(Status.ERROR); return; } finally { this.context.getStepsUnderExecution().remove(this.id); - } - - + } }).start(); else log.debug("not all inputs are ready for step {}", id); @@ -111,23 +106,21 @@ public class ExecutionStep implements InputReadyListener { Map realInputs = new HashMap<>(); Set keys = new HashSet<>(inputs.keySet()); if (nodeDefinition.getInputTranslators() != null) { - log.debug("translators {}", nodeDefinition.getInputTranslators().size()); + //log.debug("translators {}", nodeDefinition.getInputTranslators().size()); for (Map.Entry entry : nodeDefinition.getInputTranslators().entrySet()) { String translation = entry.getValue().getTranslation(); - - for (Key usedKey : entry.getValue().getUsedKeys()) { + List usedKeys = entry.getValue().getUsedKeys(); + log.debug("used keys", usedKeys.toString()); + for (String usedKey : usedKeys) { String valueToReplace = ""; - switch (usedKey.getType()) { - case Key.Type.RUNTIME: - valueToReplace = (String) this.runtimeParameters.get(usedKey.getKey()); - break; - default: - valueToReplace = (String) inputs.get(usedKey.getKey()); - keys.remove(usedKey.getKey()); - break; - } - translation = translation.replace(String.format("${{%s}}", usedKey.getKey()), valueToReplace); - log.debug("replacing used key {} value {}", usedKey.getKey(), valueToReplace); + if (this.runtimeParameters.containsKey(usedKey)) + valueToReplace = (String) this.runtimeParameters.get(usedKey); + else if (inputs.containsKey(usedKey)){ + valueToReplace = (String) inputs.get(usedKey); + keys.remove(usedKey); + } + translation = translation.replace(String.format("${{%s}}", usedKey), valueToReplace); + //log.debug("replacing used key {} value {}", usedKey, valueToReplace); } realInputs.put(entry.getKey(), translation); } diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/ExecutorDescriptor.java b/src/main/java/it/cnr/isti/workflow/manager/model/ExecutorDescriptor.java index 95c437b..305e123 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/ExecutorDescriptor.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/ExecutorDescriptor.java @@ -4,27 +4,33 @@ import java.util.List; import it.cnr.isti.workflow.manager.executors.Executor; import it.cnr.isti.workflow.manager.model.types.ParameterDefinition; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NonNull; +import lombok.Singular; @Data +@AllArgsConstructor +@Builder public class ExecutorDescriptor { - public ExecutorDescriptor(String id, Executor executor){ - this.identifier = id; - this.name = executor.getName(); - this.inputNames = executor.declaredInputNames(); - this.outputNames = executor.declaredOutputNames(); - this.mandatoryParameters = executor.getMandatoryParameters(); - } - + @NonNull String identifier; + @NonNull String name; + String description; + @Singular List inputNames; + @Singular List outputNames; + @Singular List mandatoryParameters; + List dynamicDescriptorParameter; + //List optionalParameters; } diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/Validations.java b/src/main/java/it/cnr/isti/workflow/manager/model/Validations.java new file mode 100644 index 0000000..cca68ba --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/model/Validations.java @@ -0,0 +1,36 @@ +package it.cnr.isti.workflow.manager.model; + + +import it.cnr.isti.workflow.manager.model.types.ParameterDefinition; +import it.cnr.isti.workflow.manager.model.types.Validation; + +public class Validations { + + + public static Validation maxValidator(int value){ + return Validation.builder().name("max").value(value).message("max value is " + value).with((o) -> ((Integer) o)<=value ).build(); + } + public static Validation minValidator(int value){ + return Validation.builder().name("min").value(value).message("min value is " + value).with((o) -> ((Integer) o)>=value).build(); + } + public static Validation maxLengthValidator(int value){ + return Validation.builder().name("maxLength").value(value).message("max length is " + value).with((o) -> o.toString().length() o.toString().length()>value).build(); + } + + public static Validation regexValidator(String regex){ + return Validation.builder().name("pattern").value(regex).message("regex is " + regex).with((o) -> o.toString().matches(regex)).build(); + } + + + public static boolean isValid(ParameterDefinition parameter, Object value) { + for (Validation validation : parameter.getValidations()) { + if (!validation.getWith().test(value)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/types/IOType.java b/src/main/java/it/cnr/isti/workflow/manager/model/types/IOType.java index 1f03f2f..9ed2d2b 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/types/IOType.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/types/IOType.java @@ -1,6 +1,7 @@ package it.cnr.isti.workflow.manager.model.types; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; public enum IOType { Text, @@ -10,4 +11,9 @@ public enum IOType { public static IOType fromString(String key) { return IOType.valueOf(key); } + + @JsonValue + public String toString() { + return name(); + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/types/NodeDefinition.java b/src/main/java/it/cnr/isti/workflow/manager/model/types/NodeDefinition.java index 58c7fc0..39b44b6 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/types/NodeDefinition.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/types/NodeDefinition.java @@ -36,6 +36,9 @@ public class NodeDefinition { @NonNull private String executor; + @Builder.Default + private String color = "black"; + @Singular @Convert(converter = MapsConverter.class) @Column(columnDefinition = "TEXT") diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterDefinition.java b/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterDefinition.java index a92775f..dc89061 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterDefinition.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterDefinition.java @@ -38,6 +38,9 @@ public class ParameterDefinition { @NonNull private ParameterType type; + @Builder.Default + private boolean required = false; + @Singular private List validations; diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterType.java b/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterType.java index 5b40713..a4d1605 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterType.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/types/ParameterType.java @@ -3,5 +3,7 @@ package it.cnr.isti.workflow.manager.model.types; public enum ParameterType { Select, - Input + Text, + Boolean, + Number } diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/types/Translator.java b/src/main/java/it/cnr/isti/workflow/manager/model/types/Translator.java index 6245bf3..ec9b1cb 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/types/Translator.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/types/Translator.java @@ -1,13 +1,18 @@ package it.cnr.isti.workflow.manager.model.types; +import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.slf4j.Logger; + +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.NonNull; -import lombok.Singular; @Data @@ -16,17 +21,32 @@ import lombok.Singular; @Builder public class Translator { - @Singular - @NonNull - List usedKeys; + private static final Logger log = org.slf4j.LoggerFactory.getLogger(Translator.class); + + private static final Pattern pattern = Pattern.compile("\\$\\{\\{(.*?)\\}\\}"); @NonNull String translation; public String toString() { - return "Translator(usedKeys=" + this.getUsedKeys() + ", translation=" + this.getTranslation() + ")"; + return "Translator(translation=" + this.getTranslation() + ")"; } + @JsonIgnore + public List getUsedKeys() { + Matcher matcher = pattern.matcher(translation); + + List variables = new ArrayList<>(); + + // Cicla tutti i match trovati + while (matcher.find()) { + String found =matcher.group(1); + log.info("found: {} ",found); + variables.add(found); // prende solo il nome della variabile (senza ${{}}) + } + + return variables; + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/model/types/Validation.java b/src/main/java/it/cnr/isti/workflow/manager/model/types/Validation.java index 372f122..98dbf48 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/model/types/Validation.java +++ b/src/main/java/it/cnr/isti/workflow/manager/model/types/Validation.java @@ -1,8 +1,11 @@ package it.cnr.isti.workflow.manager.model.types; +import java.util.function.Predicate; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.NonNull; @@ -10,6 +13,7 @@ import lombok.NonNull; @AllArgsConstructor @Data @NoArgsConstructor +@EqualsAndHashCode public class Validation { @NonNull @@ -18,5 +22,12 @@ public class Validation { @NonNull private String validator; + private Object value; + + @EqualsAndHashCode.Exclude private String message; + + @NonNull + @EqualsAndHashCode.Exclude + private Predicate with; } diff --git a/src/main/resources/flows.json b/src/main/resources/flows.json new file mode 100644 index 0000000..4a1a0d3 --- /dev/null +++ b/src/main/resources/flows.json @@ -0,0 +1,289 @@ +[ + { + "flow": { + "name": "testgen", + "description": null, + "nodes": [ + { + "key": "bbc98e6d-ea70-46d8-93d9-9cd8333b7565", + "name": "newNode", + "outputs": [ + { + "key": "19f50259-89cf-4dca-aba0-34b7620cb555", + "type": "text", + "name": "requirements" + } + ], + "inputs": [ + { + "key": "0dc3195a-2682-4a2f-8466-862f29797d6b", + "type": "text", + "name": "interview" + } + ], + "color": "#A8E6CF", + "position": { + "x": -197, + "y": 206 + }, + "parameters": { + "LLM": "google-gemini" + }, + "description": null, + "type": "Requirement Generator" + }, + { + "key": "3b0b2489-61a5-4024-98fb-42cdfca2dafa", + "name": "newNode", + "outputs": [ + { + "key": "b38c0c63-4ac5-4225-b5b6-d1e1b7434842", + "type": "text", + "name": "requirement-smells" + } + ], + "inputs": [ + { + "key": "b254b9d1-71cf-4c87-9875-eeddf47b89a5", + "type": "text", + "name": "requirements" + } + ], + "color": "#A8E6CF", + "position": { + "x": -199, + "y": 562 + }, + "parameters": { + "LLM": "google-gemini" + }, + "description": null, + "type": "Requirements Analyzer" + }, + { + "key": "2bfc55bf-1e15-4e14-8e75-0f1c1cb305f5", + "name": "newNode", + "outputs": [ + { + "key": "e0fe597b-712d-496e-9780-d3c4a6eb729b", + "type": "text", + "name": "corrected-requirements" + } + ], + "inputs": [ + { + "key": "0e7016a9-43ef-4ce5-9e54-403811061e2c", + "type": "text", + "name": "requirement-smells" + } + ], + "color": "#A8E6CF", + "position": { + "x": 398, + "y": -113 + }, + "parameters": { + "LLM": "google-gemini" + }, + "description": null, + "type": "Requirements Corrector" + }, + { + "key": "a1412e6e-0c32-4615-af3e-c318f8b609f1", + "name": "newNode", + "outputs": [ + { + "key": "a7eaf186-5fc9-48b0-b76f-fdcad73e4d7a", + "type": "text", + "name": "output" + } + ], + "inputs": [ + { + "key": "11a14440-8d94-420d-99dc-79bc6fcbab3f", + "type": "text", + "name": "input" + } + ], + "color": "#A8E6CF", + "position": { + "x": -198, + "y": -147 + }, + "parameters": null, + "description": null, + "type": "InputDispatcher" + }, + { + "key": "8d3e4cf7-5dc0-48ef-b94e-98d8f0a8ccc7", + "name": "newNode", + "outputs": [ + { + "key": "caaa6d9a-af43-4501-968e-1bba415818d4", + "type": "text", + "name": "plant-uml" + } + ], + "inputs": [ + { + "key": "b31f2183-c35e-4a7b-871c-a6fe553687c0", + "type": "text", + "name": "corrected-requirements" + }, + { + "key": "296d4535-8082-4b90-9814-306201887cae", + "type": "text", + "name": "interview-script" + } + ], + "color": "#A8E6CF", + "position": { + "x": 541, + "y": 270 + }, + "parameters": { + "LLM": "google-gemini" + }, + "description": null, + "type": "Class Modeller (UML)" + }, + { + "key": "252071ea-2a74-46af-b9e7-08f02771c6c6", + "name": "newNode", + "outputs": [ + { + "key": "6c9465a6-e1f6-4c76-94fe-eddaf3671250", + "type": "text", + "name": "plantuml-sequence-diagram" + } + ], + "inputs": [ + { + "key": "94064079-0d19-4cec-9391-6ab1ac6e1e18", + "type": "text", + "name": "interview" + }, + { + "key": "a5bf84d5-7a15-422a-820a-a8490943404c", + "type": "text", + "name": "plantuml-class-diagram" + }, + { + "key": "83a47422-033a-45c9-aac6-ecd4de4b2b9c", + "type": "text", + "name": "corrected-requirements" + } + ], + "color": "#A8E6CF", + "position": { + "x": 999, + "y": 461 + }, + "parameters": { + "LLM": "google-gemini" + }, + "description": null, + "type": "Sequence Modeler" + }, + { + "key": "d44d8b30-ab97-4a9c-9475-1f7b2c54a581", + "name": "newNode", + "outputs": [ + { + "key": "6d37eb52-3bc2-4dd9-8985-0b17f8db92ba", + "type": "text", + "name": "source-code" + } + ], + "inputs": [ + { + "key": "87c429c4-c876-48df-a1f8-c772e3da63f1", + "type": "text", + "name": "corrected-requirements" + }, + { + "key": "edf43f01-fbe9-4bc3-ae8b-43c5ac3ba0c8", + "type": "text", + "name": "class-diagram" + }, + { + "key": "f0546984-491e-420e-97c2-a54a330c8ba2", + "type": "text", + "name": "sequence-diagram" + } + ], + "color": "#A8E6CF", + "position": { + "x": 313, + "y": 564 + }, + "parameters": { + "LLM": "google-gemini" + }, + "description": null, + "type": "Code Generator" + } + ], + "connections": [ + { + "key": "cbe021cf-76af-4675-b2cb-dad023dd0f7f", + "from": "19f50259-89cf-4dca-aba0-34b7620cb555", + "to": "b254b9d1-71cf-4c87-9875-eeddf47b89a5" + }, + { + "key": "8829a219-4985-40e4-8453-c725e2f3c608", + "from": "b38c0c63-4ac5-4225-b5b6-d1e1b7434842", + "to": "0e7016a9-43ef-4ce5-9e54-403811061e2c" + }, + { + "key": "12ce4604-f383-41a6-9292-a0adb04351f5", + "from": "a7eaf186-5fc9-48b0-b76f-fdcad73e4d7a", + "to": "0dc3195a-2682-4a2f-8466-862f29797d6b" + }, + { + "key": "359be251-dfad-4c0f-8759-251fd36b0005", + "from": "e0fe597b-712d-496e-9780-d3c4a6eb729b", + "to": "b31f2183-c35e-4a7b-871c-a6fe553687c0" + }, + { + "key": "9aa48a8c-d56d-49db-bf5f-9ebec0d750ea", + "from": "a7eaf186-5fc9-48b0-b76f-fdcad73e4d7a", + "to": "296d4535-8082-4b90-9814-306201887cae" + }, + { + "key": "4923b0de-5885-4e17-bd8f-ccf3177278a9", + "from": "e0fe597b-712d-496e-9780-d3c4a6eb729b", + "to": "83a47422-033a-45c9-aac6-ecd4de4b2b9c" + }, + { + "key": "a16d869b-7eb6-45d5-8549-53b2c74e9378", + "from": "caaa6d9a-af43-4501-968e-1bba415818d4", + "to": "a5bf84d5-7a15-422a-820a-a8490943404c" + }, + { + "key": "80e8947e-fc2e-4e35-84a0-f4810504bcaa", + "from": "a7eaf186-5fc9-48b0-b76f-fdcad73e4d7a", + "to": "94064079-0d19-4cec-9391-6ab1ac6e1e18" + }, + { + "key": "13861a56-c812-47d1-85d6-9edb55918d6e", + "from": "e0fe597b-712d-496e-9780-d3c4a6eb729b", + "to": "87c429c4-c876-48df-a1f8-c772e3da63f1" + }, + { + "key": "a81ea345-7e44-435b-b9a3-3e875e0305ff", + "from": "caaa6d9a-af43-4501-968e-1bba415818d4", + "to": "edf43f01-fbe9-4bc3-ae8b-43c5ac3ba0c8" + }, + { + "key": "d2ac98b4-66be-4734-919b-753fcd04dd17", + "from": "6c9465a6-e1f6-4c76-94fe-eddaf3671250", + "to": "f0546984-491e-420e-97c2-a54a330c8ba2" + } + ], + "startNode": "a1412e6e-0c32-4615-af3e-c318f8b609f1", + "endNode": "d44d8b30-ab97-4a9c-9475-1f7b2c54a581" + + } + } +] \ No newline at end of file diff --git a/src/main/resources/saved-node-type.json b/src/main/resources/saved-node-type.json new file mode 100644 index 0000000..338bbba --- /dev/null +++ b/src/main/resources/saved-node-type.json @@ -0,0 +1,263 @@ +[ + { + "type": "Requirement Generator", + "name": "req gen", + "category": "Generators", + "executor": "GENERIC-AI", + "color": "black", + "fixedParameters": {}, + "runtimeParameters": { + "LLM": { + "name": "LLM", + "label": "Select the LLM to use", + "description": "The LLM to use for generating requirements", + "type": "Select", + "required": true, + "validations": [], + "specificAttributes": { + "options": [ + "google-gemini", + "open-router" + ] + } + } + }, + "inputs": { + "interview": "Text" + }, + "outputs": { + "requirements": "Text" + }, + "executorToOutputTranslationMappings": { + "response": "requirements" + }, + "inputToExecutorTranslationMappings": {}, + "inputTranslators": { + "prompt": { + "translation": "Analyze the following interview transcript and extract a list of functional requirements. \n\n${{interview}}\n\nFormat the output as a CSV with the following columns: \n1. **ID**: Unique identifier (e.g., F1, F2, F3…). \n2. **Requirement Text**: Clear description of what the system must do (in plain language). \n3. **Customer Quote**: Exact phrase from the transcript that justifies the requirement. \n\n**Rules:** \n- Include **only functional requirements** (no qualities or non-functional requirements). \n- Keep requirements concise and actionable. \n- List requirements in logical order.\n- Make sure requirements are free of smells.\n- Output only the CSV, do not add any text.\n\n**CSV Format Example:**\n\nF1; The system shall ask users to select the arithmetic operation to perform. The possible arithmetic operations are addition, subtraction, multiplication, division; “I need some support to perform different arithmetic operations”.\nF2: The system shall ask users to select the first operand of the operation; “I want to express the entire operation verbally, and get the results”.\nF3: The system shall ask users to select the second operand of the operation; “I want to express the entire operation verbally, and get the results”.\n" + } + } + }, + { + "type": "Requirements Analyzer", + "name": "req analyzer", + "category": "Analyzers", + "executor": "GENERIC-AI", + "color": "black", + "fixedParameters": {}, + "runtimeParameters": { + "LLM": { + "name": "LLM", + "label": "Select the LLM to use", + "description": "The LLM to use for generating requirements", + "type": "Select", + "required": true, + "validations": [], + "specificAttributes": { + "options": [ + "google-gemini", + "open-router" + ] + } + } + }, + "inputs": { + "requirements": "Text" + }, + "outputs": { + "requirement-smells": "Text" + }, + "executorToOutputTranslationMappings": { + "response": "requirement-smells" + }, + "inputToExecutorTranslationMappings": {}, + "inputTranslators": { + "prompt": { + "translation": "giving the following requirements as csv:\n${{requirements}}\nAct as a **Requirements Analyst** tasked with reviewing a set of functional requirements derived from a customer interview. Analyze each requirement for potential **requirement smells** (e.g., ambiguity, incompleteness, inconsistency, or vagueness) and generate a CSV report with the following columns: \n\n1. **ID**: Unique identifier of the requirement (e.g., F1, F2). \n2. **Requirement**: The exact text of the requirement. \n3. **Customer Quote**: Supporting quote from the stakeholder. \n4. **Smell Description**: \n - If a smell is detected, briefly describe it (e.g., \"Ambiguous: 'user-friendly' is subjective\"). \n - If no smell is found, leave this field empty (null). \n\n**Rules:** \n- Read the requirement carefully, and think whether it can be tested\n- If it cannot be tested, understand what the smell is that makes it not testable. \n- Typical smells are: \nAmbiguity: Subjective terms (e.g., \"easy,\" \"fast\").\nIncompleteness: Missing steps/triggers (e.g., \"remind users\" but no timing specified).\nInconsistency: Conflicts with other requirements.\nVagueness: Unquantifiable metrics (e.g., \"often,\" \"sometimes\").\n- Output only the CSV, do not add any text.\n\n**Output CSV Format Example:** \n\nF1; The system shall ask users to select the arithmetic operation to perform. The possible arithmetic operations are addition, subtraction, multiplication, division; “I need some support to perform different arithmetic operations”; null\nF2; The system shall ask users to select the first operand of the operation; “I want to express the entire operation verbally, and get the results”; users is plural, can be ambiguous\n" + } + } + }, + { + "type": "Requirements Corrector", + "name": "Req corrector", + "category": "Correctors", + "executor": "GENERIC-AI", + "color": "black", + "fixedParameters": { }, + "runtimeParameters": { + "LLM": { + "name": "LLM", + "label": "Select the LLM to use", + "description": "The LLM to use for generating requirements", + "type": "Select", + "required": true, + "validations": [ ], + "specificAttributes": { + "options": [ + "google-gemini", + "open-router" + ] + } + } + }, + "inputs": { + "requirement-smells": "Text" + }, + "outputs": { + "corrected-requirements": "Text" + }, + "executorToOutputTranslationMappings": { + "response": "corrected-requirements" + }, + "inputToExecutorTranslationMappings": { }, + "inputTranslators": { + "prompt": { + "translation": "giving the input:\n\n${{requirement-smells}}\n\nthat is a CSV of requirements with identified smells (columns: `ID`, `Requirement`, `Customer Quote`, `Smell Description`). \n\nAct as a **Requirements Corrector**. Your task is to **revise smelly requirements** (ambiguity, vagueness, inconsistency, incompleteness) into **clear, actionable, and testable** functional requirements. \n\n**Output Rules:** \n- Generate a **new CSV** with columns: \n 1. **ID**: Original requirement identifier (e.g., `F1`, `F2`). \n 2. **Corrected Requirement**: Rewritten version that resolves the smell. \n 3. **Customer Quote**: Original supporting quote from the transcript. \n- **Remove the \"Smell Description\" column** (corrections should speak for themselves). \n- **Preserve intent**: Ensure the Corrected requirement aligns with the stakeholder’s original need. \n- **For non-smelly requirements**: Copy them verbatim to the new CSV.\n- Output only the CSV, do not add any text.\n\n**Output CSV Format Example:** \n\nF1; The system shall ask users to select the arithmetic operation to perform. The possible arithmetic operations are addition, subtraction, multiplication, division; “I need some support to perform different arithmetic operations”\nF2; The system shall ask the user to select the first operand of the operation; “I want to express the entire operation verbally, and get the results”;\n" + } + } + }, + { + "type": "Class Modeller (UML)", + "name": "req modeller (class)", + "category": "Modellers", + "executor": "GENERIC-AI", + "color": "black", + "fixedParameters": { }, + "runtimeParameters": { + "LLM": { + "name": "LLM", + "label": "Select the LLM to use", + "description": "The LLM to use for generating requirements", + "type": "Select", + "required": true, + "validations": [ ], + "specificAttributes": { + "options": [ + "google-gemini", + "open-router" + ] + } + } + }, + "inputs": { + "corrected-requirements": "Text", + "interview-script": "Text" + }, + "outputs": { + "plant-uml": "Text" + }, + "executorToOutputTranslationMappings": { + "response": "plant-uml" + }, + "inputToExecutorTranslationMappings": { }, + "inputTranslators": { + "prompt": { + "translation": "Act as a **Requirements Modeler** specializing in structural design. Create a PlantUML class diagram that captures the core domain model implied by the functional requirements and interview transcript.\n\n**Input:** \n1. Corrected requirements (CSV with `ID`, `Corrected Requirement`, `Customer Quote`) :\n\n${{corrected-requirements}}\n \n2. Interview transcript for context :\n\n${{interview-script}}\n\n**Output Rules:** \n1. Generate **one concise class diagram** as a textual output in PlantUML syntax \n2. Focus on: \n - Key entities (classes) and their attributes \n - Critical relationships (associations, compositions) \n - Basic operations/methods implied by requirements \n3. Output only the model, do not add any text.\n\n**Example Output (PlantUML):**\n\n@startuml Calculator_Class_Diagram\n\nclass Calculator {\n + currentInput: Number\n + storedValue: Number\n + selectedOperation: OperationType\n + calculate(): Number\n + clear(): void\n}\n\nclass InputHandler {\n + handleDigitPress(digit: Integer): void\n + handleOperator(op: OperationType): void\n + handleEquals(): void\n}\n\nclass Display {\n + updateScreen(value: String): void\n}\n\nenum OperationType <> {\n ADD\n SUBTRACT\n MULTIPLY\n DIVIDE\n}\n\nCalculator \"1\" *-- \"1\" OperationType\nCalculator --> Display : updates\nInputHandler --> Calculator : commands\n\nnote top of Calculator : \"Must support basic arithmetic\\noperations (F2)\"\nnote right of InputHandler : \"User quote: 'I need buttons for\\nnumbers and functions...'\"\n\n@enduml\n" + } + } + }, + { + "type": "InputDispatcher", + "name": "Input", + "category": "Dispachers", + "executor": "NoOp", + "color": "black", + "fixedParameters": { }, + "runtimeParameters": { }, + "inputs": { + "input": "Text" + }, + "outputs": { + "output": "Text" + }, + "executorToOutputTranslationMappings": { + "output": "output" + }, + "inputToExecutorTranslationMappings": { + "input": "input" + }, + "inputTranslators": { } + }, + { + "type": "Sequence Modeler", + "name": "Seq Req modeller", + "category": "Modelers", + "executor": "GENERIC-AI", + "color": "black", + "fixedParameters": { }, + "runtimeParameters": { + "LLM": { + "name": "LLM", + "label": "Select the LLM to use", + "description": "The LLM to use for generating requirements", + "type": "Select", + "required": true, + "validations": [ ], + "specificAttributes": { + "options": [ + "google-gemini", + "open-router" + ] + } + } + }, + "inputs": { + "interview": "Text", + "plantuml-class-diagram": "Text", + "corrected-requirements": "Text" + }, + "outputs": { + "plantuml-sequence-diagram": "Text" + }, + "executorToOutputTranslationMappings": { + "response": "plantuml-sequence-diagram" + }, + "inputToExecutorTranslationMappings": { }, + "inputTranslators": { + "prompt": { + "translation": "Act as a **Requirements Modeler** specializing in interaction design. Create a PlantUML sequence diagram that visually captures the key system interactions described in the functional requirements and interview transcript.\n\n**Input:**\n1. Cleaned requirements (CSV format with ID, Corrected Requirement, Customer Quote):\n\n${{corrected-requirements}}\n\n2. Original interview transcript for context:\n\n${{interview}}\n\n3. Class diagram:\n\n\n\n**Output Rules:**\n1. Generate **one comprehensive sequence diagram** in PlantUML syntax\n2. Consider only the classes from the input class diagram\n3. Focus on showing:\n - Primary user-to-system interactions\n - Critical system responses\n - Important timing constraints (where specified)\n4. Include only the most essential interactions (5-10 steps max)\n5. Output only the model, do not add any text.\n\n**Example Output (PlantUML):**\n\n@startuml Calculator_Sequence_Diagram \n\nactor User \nparticipant InputHandler \nparticipant Calculator \nparticipant Display \n\nUser -> InputHandler : Press digit \"5\" \nnote right of User: \"I need buttons for numbers...\" (Requirement F1) \nInputHandler -> Calculator : Store input (currentInput = 5) \nCalculator -> Display : Update screen (\"5\") \n\nUser -> InputHandler : Press operator \"+\" \nnote right of User: \"Basic arithmetic is essential\" (F3) \nInputHandler -> Calculator : Set operation (ADD) \n\nUser -> InputHandler : Press digit \"3\" \nInputHandler -> Calculator : Store input (currentInput = 3) \nCalculator -> Display : Update screen (\"3\") \n\nUser -> InputHandler : Press \"=\" \nInputHandler -> Calculator : calculate() \nCalculator -> Calculator : Compute 5 + 3 \nCalculator -> Display : Update screen (\"8\") \n\n@enduml \n" + } + } + }, + { + "type": "Code Generator", + "name": "Code Generator", + "category": "Generators", + "executor": "GENERIC-AI", + "color": "black", + "fixedParameters": { }, + "runtimeParameters": { + "LLM": { + "name": "LLM", + "label": "Select the LLM to use", + "description": "The LLM to use for generating requirements", + "type": "Select", + "required": true, + "validations": [ ], + "specificAttributes": { + "options": [ + "google-gemini", + "open-router" + ] + } + } + }, + "inputs": { + "corrected-requirements": "Text", + "class-diagram": "Text", + "sequence-diagram": "Text" + }, + "outputs": { + "source-code": "Text" + }, + "executorToOutputTranslationMappings": { + "response": "source-code" + }, + "inputToExecutorTranslationMappings": { }, + "inputTranslators": { + "prompt": { + "translation": "Act as a **Senior Software Engineer** specializing in **model-driven development**. Generate clean, maintainable code that:\n1. Implements all validated requirements (from CSV) \n2. Matches the structure of the provided class diagram \n3. Follows the interaction flows from the sequence diagram \n\n**Input:** \n1. requirements (ID, Corrected Requirement, Customer Quote):\n\n${{corrected-requirements}}\n\n2. class diagram (PlantUML) :\n\n${{class-diagram}}\n\n3. sequence diagram (PlantUML) :\n\n\n\n\n**Output Rules:** \n- **Language**: Python\n- **Architecture**: \n - 1:1 mapping of PlantUML classes to code \n - Sequence diagram steps → method logic \n- **Quality**: \n - Type hints (Python)\n - Docstrings referencing requirement IDs (e.g., `# F1`) \n - Appropriate error handling \n- **Format**:\n - Output only executable code, do not add any text, besides comments\n\n**Example Output (Python Calculator):** \n\nfrom enum import Enum, auto\n\nclass OperationType(Enum):\n \"\"\"Supported operations (matches class diagram <>)\"\"\"\n ADD = auto() # F3: Basic arithmetic\n SUBTRACT = auto() # F3\n MULTIPLY = auto() # F3\n DIVIDE = auto() # F3\n\nclass Calculator:\n \"\"\"Core calculator logic (from class diagram)\"\"\"\n def __init__(self):\n self.current_input = 0 # F1: Digit entry\n self.stored_value = None # For multi-step operations\n self.selected_op = None # F3: Operation selection\n \n def calculate(self) -> float:\n \"\"\"Execute the current operation (sequence diagram '=' press)\"\"\"\n if not self.selected_op:\n return self.current_input\n \n # Sequence diagram compute step\n match self.selected_op:\n case OperationType.ADD:\n result = self.stored_value + self.current_input\n case OperationType.SUBTRACT:\n result = self.stored_value - self.current_input\n # ... other operations (F3)\n \n self.current_input = result\n return result\n\nclass InputHandler:\n \"\"\"Manages user input flow (sequence diagram interactions)\"\"\"\n def __init__(self, calculator: Calculator, display: Display):\n self.calc = calculator\n self.display = display\n \n def handle_digit(self, digit: int): # F1\n \"\"\"Process digit press (sequence diagram step 1)\"\"\"\n self.calc.current_input = self.calc.current_input * 10 + digit\n self.display.update(str(self.calc.current_input))\n\n def handle_operator(self, op: OperationType): # F3\n \"\"\"Process +,-,*,/ press (sequence diagram step 2)\"\"\"\n self.calc.stored_value = self.calc.current_input\n self.calc.selected_op = op\n self.calc.current_input = 0\n" + } + } + } +] \ No newline at end of file diff --git a/src/test/java/it/cnr/isti/workflow/manager/FlowTest.java b/src/test/java/it/cnr/isti/workflow/manager/FlowTest.java index 17bc420..a748754 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/FlowTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/FlowTest.java @@ -8,17 +8,18 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; import com.fasterxml.jackson.databind.ObjectMapper; import it.cnr.isti.workflow.manager.executors.ExecutionObject; -import it.cnr.isti.workflow.manager.model.ExecutionStep; import it.cnr.isti.workflow.manager.model.InputReadyListener; import it.cnr.isti.workflow.manager.model.ui.Flow; import it.cnr.isti.workflow.manager.model.ui.FlowEntity; import it.cnr.isti.workflow.manager.repositories.FlowRepository; @SpringBootTest +@TestPropertySource(locations="classpath:test.properties") public class FlowTest { private static final Logger log = org.slf4j.LoggerFactory.getLogger(FlowTest.class); diff --git a/src/test/java/it/cnr/isti/workflow/manager/NodeDefinitionControllerTest.java b/src/test/java/it/cnr/isti/workflow/manager/NodeDefinitionControllerTest.java index 81af3d4..3aef681 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/NodeDefinitionControllerTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/NodeDefinitionControllerTest.java @@ -2,8 +2,10 @@ package it.cnr.isti.workflow.manager; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; @SpringBootTest +@TestPropertySource(locations="classpath:test.properties") class NodeDefinitionControllerTest { diff --git a/src/test/java/it/cnr/isti/workflow/manager/repositories/NodeDefRepoTest.java b/src/test/java/it/cnr/isti/workflow/manager/repositories/NodeDefRepoTest.java index 5a0d85c..461f51a 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/repositories/NodeDefRepoTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/repositories/NodeDefRepoTest.java @@ -18,19 +18,80 @@ import static org.junit.jupiter.api.Assertions.*; import java.io.StringWriter; import java.util.stream.StreamSupport; - @SpringBootTest +@TestPropertySource(locations="classpath:test.properties") class NodeDefRepoTest { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(NodeDefRepoTest.class); + private static final String NODE_EXAMPLE_JSON = """ + { + "type": "Translator", + "name": "translator_copy", + "category": "TRANSLATOR", + "color": "black", + "executor": "Generic AI Executor", + "executorToOutputTranslationMappings": { + "response": "translated" + }, + "inputToExecutorTranslationMappings": {}, + "inputTranslators": { + "prompt": { + "usedKeys": [ + { + "key": null, + "type": "STANDARD" + } + ], + "translation": "translate the following phrase ${{phrase}} to ${{language}} returning only the translated" + } + }, + "outputs": { + "translated": "Text" + }, + "inputs": { + "phrase": "Text" + }, + "fixedParameters": {}, + "runtimeParameters": { + "LLM": { + "name": "LLM", + "label": "Select the LLM to use", + "description": "The LLM to use for generating requirements", + "type": "Select", + "required": true, + "validations": [], + "specificAttributes": { + "options": [ + "google-gemini", + "open-router" + ] + } + }, + "language": { + "name": "language", + "type": "Number", + "description": "The language to translate to", + "label": "Language", + "required": true, + "specificAttributes": { + "options": [] + } + } + } + } + """; + @Autowired private NodeDefinitionRepository nodeRepository; @Test void testStoreAndFindNode() { - NodeDefinition node = NodeDefinition.builder().type("DefectDetection").name("defect detection").executor("GENERIC-AI").category("DefectDetection").input("file",IOType.Csv) - .fixedParameter("LLM", ParameterDefinition.builder().name("param").label("param").type(ParameterType.Input).build()) .output("file",IOType.Csv).build(); + NodeDefinition node = NodeDefinition.builder().type("DefectDetection").name("defect detection") + .executor("GENERIC-AI").category("DefectDetection").input("file", IOType.Csv) + .fixedParameter("LLM", + ParameterDefinition.builder().name("param").label("param").type(ParameterType.Text).build()) + .output("file", IOType.Csv).build(); nodeRepository.save(node); NodeDefinition foundNode = nodeRepository.findById(node.getType()).orElse(null); assertNotNull(foundNode); @@ -40,25 +101,51 @@ class NodeDefRepoTest { @Test void testStoreAndFindAll() { - NodeDefinition node = NodeDefinition.builder().type("DefectDetection").name("defect detection").executor("GENERIC-AI").category("DefectDetection").input("file",IOType.Csv) .output("file",IOType.Csv).build(); + NodeDefinition node = NodeDefinition.builder().type("DefectDetection").name("defect detection") + .executor("GENERIC-AI").category("DefectDetection").input("file", IOType.Csv).output("file", IOType.Csv) + .build(); nodeRepository.save(node); - NodeDefinition returnNode = StreamSupport.stream(nodeRepository.findAll().spliterator(), false).filter(n->n.getType().equals(node.getType())).findFirst().orElse(null) ; + NodeDefinition returnNode = StreamSupport.stream(nodeRepository.findAll().spliterator(), false) + .filter(n -> n.getType().equals(node.getType())).findFirst().orElse(null); assertNotNull(returnNode); } - + @Test void testStoreAndSerializeAsJson() throws Exception { ObjectMapper mapper = new ObjectMapper(); - NodeDefinition node = NodeDefinition.builder().type("DefectDetection").name("defect detection").executor("GENERIC-AI").category("DefectDetection").input("file",IOType.Csv) - .fixedParameter("LLM", ParameterDefinition.builder().name("param").label("param").type(ParameterType.Input).build()) .output("file",IOType.Csv).build(); + NodeDefinition node = NodeDefinition.builder().type("DefectDetection").name("defect detection") + .executor("GENERIC-AI").category("DefectDetection").input("file", IOType.Csv) + .fixedParameter("LLM", + ParameterDefinition.builder().name("param").label("param").type(ParameterType.Text).build()) + .output("file", IOType.Csv).build(); nodeRepository.save(node); NodeDefinition foundNode = nodeRepository.findById(node.getType()).orElse(null); assertNotNull(foundNode); - foundNode.getInputs().forEach((k,v) -> logger.info("input: {} {}", k, v )); - StringWriter sw= new StringWriter(); + foundNode.getInputs().forEach((k, v) -> logger.info("input: {} {}", k, v)); + StringWriter sw = new StringWriter(); mapper.writeValue(sw, foundNode); String json = sw.toString(); assertNotNull(json); } + @Test + public void nodeTypeDeserialization(){ + ObjectMapper mapper = new ObjectMapper(); + try { + NodeDefinition node = mapper.readValue(NODE_EXAMPLE_JSON, NodeDefinition.class); + assertNotNull(node); + assertEquals("Translator", node.getType()); + assertEquals("translator_copy", node.getName()); + assertEquals("TRANSLATOR", node.getCategory()); + assertEquals("Generic AI Executor", node.getExecutor()); + assertNotNull(node.getInputs()); + assertNotNull(node.getOutputs()); + assertNotNull(node.getRuntimeParameters()); + assertNotNull(node.getFixedParameters()); + + } catch (Exception e) { + fail("Deserialization failed: " + e.getMessage()); + } + } + } diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties new file mode 100644 index 0000000..bffaec9 --- /dev/null +++ b/src/test/resources/test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL; +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username= +spring.datasource.password= +# We add the MySQL Dialect so that it understands and generates the query based on MySQL +spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect