modified the step creation to resolve some bugs

This commit is contained in:
Lucio Lelii 2025-04-29 20:58:00 +02:00
parent c519dab98c
commit 13a61aea03
27 changed files with 1052 additions and 205 deletions

View File

@ -85,6 +85,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -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<String, ExecutionStep> steps = new HashMap<>();
Map<String, IDPair> inputs = new HashMap<>();
Map<String, IDPair> 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<String, String> 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<String, String> 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<IOModel> 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<String, String> 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<String, String> currentMappingNodeOutputNameToChildInputName = new HashMap<>();
List<Node> 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;
}
}
}

View File

@ -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<NodeDefinition> 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<FlowEntity> 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();
}
}
}

View File

@ -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()

View File

@ -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<String, Executor> executors;
private List<Executor> executors;
@GetMapping("/nodes")
public List<NodeDefinition> getNodes() {
@ -51,8 +49,7 @@ public class NodeDefinitionController {
@GetMapping("/executors")
public List<ExecutorDescriptor> getAvailableExecutors() {
return executors.entrySet().stream()
.map((entry) -> new ExecutorDescriptor(entry.getKey(), entry.getValue())).toList();
return executors.stream().map(entry -> entry.getDescriptor(null)).toList();
}
}

View File

@ -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);
}
}

View File

@ -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<String,Object> execute(Map<String, Object> userParameters, Map<String,Object> preExecutionParameters, Map<String, Object> inputsFromParent);
Map<String,Object> execute(Map<String, Object> userParameters, Map<String,Object> preExecutionParameters, Map<String, Object> inputsFromParent) throws ExecutionException;
Map<String, Class<?>> getRequiredPreExecutionParameters(Map<String, Object> userParameters);
List<String> declaredOutputNames();
List<String> declaredInputNames();
List<ParameterDefinition> getMandatoryParameters();
ExecutorDescriptor getDescriptor(Map<String,Object> executorDescriptorParameters);
String getName();
}

View File

@ -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<String, Object> execute(Map<String, Object> userParameters, Map<String, Object> preExecutionParameters,
Map<String, Object> inputsFromParent) {
return inputsFromParent;
return Map.of("output", inputsFromParent.get("input"));
}
@Override
public ExecutorDescriptor getDescriptor(Map<String, Object> 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<ParameterDefinition> getMandatoryParameters() {
return List.of();
}
@Override
public Map<String, Class<?>> getRequiredPreExecutionParameters(Map<String, Object> userParameters) {
return Map.of();
}
@Override
public List<String> declaredOutputNames() {
return List.of();
}
@Override
public List<String> declaredInputNames() {
return List.of();
public List<ParameterDefinition> 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);
}
}

View File

@ -8,7 +8,7 @@ public interface AIModel {
String getDescription();
String executePrompt(Map<String, Object> parameters, String prompt);
String executePrompt(Map<String, Object> parameters, String prompt) throws Throwable;
public Map<String, Class<?>> getRequiredPreExecutionParameters();

View File

@ -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<String, Object> parameters, String prompt) {
public String executePrompt(Map<String, Object> 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<String, Object> execute(Map<String, Object> userParameters, Map<String, Object> preExecutionParameters,
Map<String, Object> inputsFromParent) {
String returnString = (String)executePrompt(preExecutionParameters, (String) inputsFromParent.get("prompt"));
return Map.of("response", returnString);
}*/
}
@JsonIgnoreProperties(ignoreUnknown = true)
class GeminiResponse {
private java.util.List<Candidate> candidates;
public java.util.List<Candidate> getCandidates() {
return candidates;
}
public void setCandidates(java.util.List<Candidate> 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<Part> parts;
public java.util.List<Part> getParts() {
return parts;
}
public void setParts(java.util.List<Part> 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;
}
}
}

View File

@ -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<String, Object> 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<String, Object> execute(Map<String, Object> userParameters, Map<String, Object> preExecutionParameters,
Map<String, Object> inputsFromParent) {
Map<String, Object> 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<String, Object> executorDescriptorParameters) {
return ExecutorDescriptor.builder().identifier("GENERIC-AI").name(name).inputName("prompt").outputName("response")
.description("Generic AI executor").mandatoryParameters(getMandatoryParameters()).build();
}
public Map<String, Class<?>> getRequiredPreExecutionParameters(Map<String, Object> 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<String> declaredOutputNames() {
return List.of("response");
}
@Override
public List<String> declaredInputNames() {
return List.of("prompt");
}
@Override
public List<ParameterDefinition> 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);
}
}

View File

@ -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<String> result = webClient.post()
Mono<String> 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;
}
}

View File

@ -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<String, String> parentOutputsToInputMapping;
Map<String, String> parentOutputsToInputMapping = new HashMap<>();
Map<String, Object> inputs = new HashMap<>();
List<InputReadyListener> nextSteps = new ArrayList<>();
List<ExecutionStep> previouSteps = new ArrayList<>();
//List<ExecutionStep> 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<String, Object> realInputs = prepareInputs();
log.debug("input translation for step {} is {}", id, realInputs);
//log.debug("input translation for step {} is {}", id, realInputs);
Map<String, Object> 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<String, Object> 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<String, Object> realInputs = new HashMap<>();
Set<String> keys = new HashSet<>(inputs.keySet());
if (nodeDefinition.getInputTranslators() != null) {
log.debug("translators {}", nodeDefinition.getInputTranslators().size());
//log.debug("translators {}", nodeDefinition.getInputTranslators().size());
for (Map.Entry<String, Translator> entry : nodeDefinition.getInputTranslators().entrySet()) {
String translation = entry.getValue().getTranslation();
for (Key usedKey : entry.getValue().getUsedKeys()) {
List<String> 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);
}

View File

@ -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<String> inputNames;
@Singular
List<String> outputNames;
@Singular
List<ParameterDefinition> mandatoryParameters;
List<ParameterDefinition> dynamicDescriptorParameter;
//List<ParameterDescriptor> optionalParameters;
}

View File

@ -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()<value).build();
}
public static Validation minLengthValidator(int value){
return Validation.builder().name("minLength").value(value).message("min length is " + value).with((o) -> 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;
}
}

View File

@ -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();
}
}

View File

@ -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")

View File

@ -38,6 +38,9 @@ public class ParameterDefinition {
@NonNull
private ParameterType type;
@Builder.Default
private boolean required = false;
@Singular
private List<Validation> validations;

View File

@ -3,5 +3,7 @@ package it.cnr.isti.workflow.manager.model.types;
public enum ParameterType {
Select,
Input
Text,
Boolean,
Number
}

View File

@ -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<Key> 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<String> getUsedKeys() {
Matcher matcher = pattern.matcher(translation);
List<String> 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;
}
}

View File

@ -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<Object> with;
}

View File

@ -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"
}
}
]

View File

@ -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 <<enum>> {\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 <<enum>>)\"\"\"\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"
}
}
}
]

View File

@ -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);

View File

@ -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 {

View File

@ -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());
}
}
}

View File

@ -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