diff --git a/spring-ai-modules/pom.xml b/spring-ai-modules/pom.xml index a8a9b050cb30..6220718d90a2 100644 --- a/spring-ai-modules/pom.xml +++ b/spring-ai-modules/pom.xml @@ -20,6 +20,6 @@ spring-ai-mcp spring-ai-text-to-sql spring-ai-vector-stores + spring-ai-agentic-patterns - - \ No newline at end of file + diff --git a/spring-ai-modules/spring-ai-agentic-patterns/pom.xml b/spring-ai-modules/spring-ai-agentic-patterns/pom.xml new file mode 100644 index 000000000000..1b97120497fd --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + com.baeldung + spring-ai-modules + 0.0.1 + ../pom.xml + + + spring-ai-agentic-patterns + spring-ai-agentic-patterns + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.ai + spring-ai-model + + + org.springframework.ai + spring-ai-client-chat + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + 21 + 1.0.2 + 3.5.5 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/Application.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/Application.java new file mode 100644 index 000000000000..e120d55e6573 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/Application.java @@ -0,0 +1,12 @@ +package com.baeldung.springai.agenticpatterns; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/CodeReviewClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/CodeReviewClient.java new file mode 100644 index 000000000000..07e556a3c390 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/CodeReviewClient.java @@ -0,0 +1,6 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.client.ChatClient; + +public interface CodeReviewClient extends ChatClient { +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/CodeReviewClientPrompts.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/CodeReviewClientPrompts.java new file mode 100644 index 000000000000..f2d9d2e667ae --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/CodeReviewClientPrompts.java @@ -0,0 +1,32 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +public final class CodeReviewClientPrompts { + + private CodeReviewClientPrompts() { + } + + /** + * Prompt for the code review of a given PR + */ + public static final String CODE_REVIEW_PROMPT = """ + Given a PR link -> generate a map with proposed code improvements. + The key should be {class-name}:{line-number}:{short-description}. + The value should be the code in one line. For example, 1 proposed improvement could be for the line 'int x = 0, y = 0;': + {"Client:23:'no multiple variables defined in 1 line'", "int x = 0;\\n int y = 0;"} + Rules are, to follow the checkstyle and spotless rules set to the repo. Keep java code clear, readable and extensible. + + Finally, if it is not your first attempt, there might feedback provided to you, including your previous suggestion. + You should reflect on it and improve the previous suggestions, or even add more."""; + + /** + * Prompt for the evaluation of the result + */ + public static final String EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT = """ + Evaluate the suggested code improvements for correctness, time complexity, and best practices. + + Return a Map with one entry. The key is the value the evaluation. The value will be your feedback. + + The evaluation field must be one of: "PASS", "NEEDS_IMPROVEMENT", "FAIL" + Use "PASS" only if all criteria are met with no improvements needed. + """; +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyCodeReviewClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyCodeReviewClient.java new file mode 100644 index 000000000000..47d0a3d72653 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyCodeReviewClient.java @@ -0,0 +1,33 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class DummyCodeReviewClient implements CodeReviewClient { + + @Override + @NonNull + public ChatClientRequestSpec prompt() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull String input) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull Prompt prompt) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public Builder mutate() { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsClient.java new file mode 100644 index 000000000000..e18793781168 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsClient.java @@ -0,0 +1,33 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class DummyOpsClient implements OpsClient { + + @Override + @NonNull + public ChatClientRequestSpec prompt() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull String input) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull Prompt prompt) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public Builder mutate() { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsOrchestratorClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsOrchestratorClient.java new file mode 100644 index 000000000000..e73c0e93070b --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsOrchestratorClient.java @@ -0,0 +1,33 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class DummyOpsOrchestratorClient implements OpsOrchestratorClient { + + @Override + @NonNull + public ChatClientRequestSpec prompt() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull String request) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull Prompt prompt) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public Builder mutate() { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsRouterClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsRouterClient.java new file mode 100644 index 000000000000..abcfade2acac --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/DummyOpsRouterClient.java @@ -0,0 +1,33 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Component +public class DummyOpsRouterClient implements OpsRouterClient { + + @Override + @NonNull + public ChatClientRequestSpec prompt() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull String request) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public ChatClientRequestSpec prompt(@NonNull Prompt prompt) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + @NonNull + public Builder mutate() { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsClient.java new file mode 100644 index 000000000000..1556262eccdd --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsClient.java @@ -0,0 +1,6 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.client.ChatClient; + +public interface OpsClient extends ChatClient { +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsClientPrompts.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsClientPrompts.java new file mode 100644 index 000000000000..e53369de7835 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsClientPrompts.java @@ -0,0 +1,65 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +public final class OpsClientPrompts { + + private OpsClientPrompts() { + } + + /** + * Array of steps to be taken for the dev pipeline + */ + public static final String[] DEV_PIPELINE_STEPS = { + // Checkout from VCS + """ + Checkout the PR from the link. + If error occurs, return the error, + else return the path of the checked-out code""", + // Build the code and package + """ + Identify the build tool and build the code of the given path. + If error occurs, return the error, + else return the path of the input""", + // Containerize and push to docker repo + """ + On the given path, create the docker container. Then push to our private repo. + If error occurs, return the error, + else return the link of the container and the path to the code""", + // Deploy to test environment + """ + Deploy the given docker image to test. + If error occurs, return the error, + else return the path of the input""", + // Run integration tests + """ + From the PR code, execute the integration tests against test environment. + If error occurs, return the error, + else return success""" }; + + /** + * Prompt for the deployment of a container to one or many environments + */ + public static final String NON_PROD_DEPLOYMENT_PROMPT = + // Prompt For Deployment + """ + Deploy the given container to the given environment. + If any prod environment is requested, fail. + If error occurs, return the error, + else return success message."""; + + /** + * Array of steps to be taken for deployment and test execution against this environment + */ + public static final String[] EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS = { + // Prompt For Deployment. If successful, pass to the next step the PR link and the environment. + """ + Deploy the container associated to the given PR, on the given environment. + If any prod environment is requested, fail. + If error occurs, return the error, + else return an array of strings: '{the PR link] on {the environment}'.""", + // Continue with running the tests from the code base that are related to this env. + // Which means, run the Functional Tests on 'test' env, the Integration Tests on 'int', etc. + """ + Execute the tests from the codebase version provided, that are related to the environment provided. + If error occurs, return the error, + else return the environment as title and then the test outcome.""" }; +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsOrchestratorClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsOrchestratorClient.java new file mode 100644 index 000000000000..4c2b2b4a5555 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsOrchestratorClient.java @@ -0,0 +1,7 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.client.ChatClient; + +public interface OpsOrchestratorClient extends ChatClient { + +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsOrchestratorClientPrompts.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsOrchestratorClientPrompts.java new file mode 100644 index 000000000000..b24b73676488 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsOrchestratorClientPrompts.java @@ -0,0 +1,15 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +public final class OpsOrchestratorClientPrompts { + + private OpsOrchestratorClientPrompts() { + } + + /** + * Prompt to identify the environments which the given PR need to be tested on + */ + public static final String REMOTE_TESTING_ORCHESTRATION_PROMPT = """ + The user should provide a PR link. From the changes of each PR, you need to decide on which environments + these changes should be tested against. The outcome should be an array of the the PR link and then all the environments. + User input:\s"""; +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsRouterClient.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsRouterClient.java new file mode 100644 index 000000000000..803064f641f7 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsRouterClient.java @@ -0,0 +1,7 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import org.springframework.ai.chat.client.ChatClient; + +public interface OpsRouterClient extends ChatClient { + +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsRouterClientPrompts.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsRouterClientPrompts.java new file mode 100644 index 000000000000..1591bdc76121 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/aimodels/OpsRouterClientPrompts.java @@ -0,0 +1,23 @@ +package com.baeldung.springai.agenticpatterns.aimodels; + +import java.util.Map; + +public final class OpsRouterClientPrompts { + + private OpsRouterClientPrompts() { + } + + /** + * Array of available routing options of Ops Model + */ + public static final Map OPS_ROUTING_OPTIONS = Map.of( + // option 1: route to run pipeline + "pipeline", """ + We'll need make a request to ChainWorkflow. Return only the PR link the user provided""", + // option 2: route to deploy an image in 1 or more envs + "deployment", """ + We'll need make a request to ParallelizationWorkflow. Return 3 lines. + First: the container link the user provided, eg 'host/service/img-name/repo:1.12.1'. + Second: the environments, separated with comma, no spaces. eg: 'test,dev,int' + Third: the max concurent workers the client asked for, eg: '3'."""); +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/chain/ChainWorkflow.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/chain/ChainWorkflow.java new file mode 100644 index 000000000000..87576d42939e --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/chain/ChainWorkflow.java @@ -0,0 +1,41 @@ +package com.baeldung.springai.agenticpatterns.workflows.chain; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Component; + +import com.baeldung.springai.agenticpatterns.aimodels.OpsClient; +import com.baeldung.springai.agenticpatterns.aimodels.OpsClientPrompts; + +@Component +public class ChainWorkflow { + + private final OpsClient opsClient; + + public ChainWorkflow(OpsClient opsClient) { + this.opsClient = opsClient; + } + + public String opsPipeline(String userInput) { + String response = userInput; + System.out.printf("User input: [%s]\n", response); + + for (String prompt : OpsClientPrompts.DEV_PIPELINE_STEPS) { + // Compose the request using the response from the previous step. + String request = String.format("{%s}\n {%s}", prompt, response); + System.out.printf("PROMPT: %s:\n", request); + + // Call the ops client with the new request and get the new response. + ChatClient.ChatClientRequestSpec requestSpec = opsClient.prompt(request); + ChatClient.CallResponseSpec responseSpec = requestSpec.call(); + response = responseSpec.content(); + System.out.printf("OUTCOME: %s:\n", response); + + // If there is an error, print the error and break + if (response.startsWith("ERROR:")) { + break; + } + } + + return response; + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/evaluator/EvaluatorOptimizerWorkflow.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/evaluator/EvaluatorOptimizerWorkflow.java new file mode 100644 index 000000000000..18a7c6106080 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/evaluator/EvaluatorOptimizerWorkflow.java @@ -0,0 +1,72 @@ +package com.baeldung.springai.agenticpatterns.workflows.evaluator; + +import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.CODE_REVIEW_PROMPT; +import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Component; + +import com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClient; + +@Component +public class EvaluatorOptimizerWorkflow { + + private final CodeReviewClient codeReviewClient; + static final ParameterizedTypeReference> mapClass = new ParameterizedTypeReference<>() {}; + + public EvaluatorOptimizerWorkflow(CodeReviewClient codeReviewClient) { + this.codeReviewClient = codeReviewClient; + } + + public Map evaluate(String task) { + return loop(task, new HashMap<>(), ""); + } + + private Map loop(String task, Map latestSuggestions, String evaluation) { + latestSuggestions = generate(task, latestSuggestions, evaluation); + Map evaluationResponse = evaluate(latestSuggestions, task); + String outcome = evaluationResponse.keySet().iterator().next(); + evaluation = evaluationResponse.values().iterator().next(); + + if ("PASS".equals(outcome)) { + System.out.println("Accepted RE Review Suggestions:\n" + latestSuggestions); + return latestSuggestions; + } + + return loop(task, latestSuggestions, evaluation); + } + + private Map generate(String task, Map previousSuggestions, String evaluation) { + String request = CODE_REVIEW_PROMPT + + "\n PR: " + task + + "\n previous suggestions: " + previousSuggestions + + "\n evaluation on previous suggestions: " + evaluation; + System.out.println("PR REVIEW PROMPT: " + request); + + ChatClient.ChatClientRequestSpec requestSpec = codeReviewClient.prompt(request); + ChatClient.CallResponseSpec responseSpec = requestSpec.call(); + Map response = responseSpec.entity(mapClass); + + System.out.println("PR REVIEW OUTCOME: " + response); + + return response; + } + + private Map evaluate(Map latestSuggestions, String task) { + String request = EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT + + "\n PR: " + task + + "\n proposed suggestions: " + latestSuggestions; + System.out.println("EVALUATION PROMPT: " + request); + + ChatClient.ChatClientRequestSpec requestSpec = codeReviewClient.prompt(request); + ChatClient.CallResponseSpec responseSpec = requestSpec.call(); + Map response = responseSpec.entity(mapClass); + System.out.println("EVALUATION OUTCOME: " + response); + + return response; + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/orchestrator/OrchestratorWorkersWorkflow.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/orchestrator/OrchestratorWorkersWorkflow.java new file mode 100644 index 000000000000..0c2e1fef3e13 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/orchestrator/OrchestratorWorkersWorkflow.java @@ -0,0 +1,55 @@ +package com.baeldung.springai.agenticpatterns.workflows.orchestrator; + +import static com.baeldung.springai.agenticpatterns.aimodels.OpsOrchestratorClientPrompts.REMOTE_TESTING_ORCHESTRATION_PROMPT; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Component; + +import com.baeldung.springai.agenticpatterns.aimodels.OpsClient; +import com.baeldung.springai.agenticpatterns.aimodels.OpsClientPrompts; +import com.baeldung.springai.agenticpatterns.aimodels.OpsOrchestratorClient; + +@Component +public class OrchestratorWorkersWorkflow { + + private final OpsOrchestratorClient opsOrchestratorClient; + private final OpsClient opsClient; + + public OrchestratorWorkersWorkflow(OpsOrchestratorClient opsOrchestratorClient, OpsClient opsClient) { + this.opsOrchestratorClient = opsOrchestratorClient; + this.opsClient = opsClient; + } + + public String remoteTestingExecution(String userInput) { + System.out.printf("User input: [%s]\n", userInput); + String orchestratorRequest = REMOTE_TESTING_ORCHESTRATION_PROMPT + userInput; + System.out.println("The prompt to orchestrator: " + orchestratorRequest); + + ChatClient.ChatClientRequestSpec orchestratorRequestSpec = opsOrchestratorClient.prompt(orchestratorRequest); + ChatClient.CallResponseSpec orchestratorResponseSpec = orchestratorRequestSpec.call(); + String[] orchestratorResponse = orchestratorResponseSpec.entity(String[].class); + String prLink = orchestratorResponse[0]; + StringBuilder response = new StringBuilder(); + + // for each environment that we need to test on + for (int i = 1; i < orchestratorResponse.length; i++) { + // execute the chain steps for 1) deployment and 2) test execution + String testExecutionChainInput = prLink + " on " + orchestratorResponse[i]; + for (String prompt : OpsClientPrompts.EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS) { + // Compose the request for the next step + String testExecutionChainRequest = + String.format("%s\n PR: [%s] environment", prompt, testExecutionChainInput); + System.out.printf("PROMPT: %s:\n", testExecutionChainRequest); + + // Call the ops client with the new request and set the result as the next step input. + ChatClient.ChatClientRequestSpec requestSpec = opsClient.prompt(testExecutionChainRequest); + ChatClient.CallResponseSpec responseSpec = requestSpec.call(); + testExecutionChainInput = responseSpec.content(); + System.out.printf("OUTCOME: %s\n", testExecutionChainInput); + } + response.append(testExecutionChainInput).append("\n"); + } + + return response.toString(); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/parallel/ParallelizationWorkflow.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/parallel/ParallelizationWorkflow.java new file mode 100644 index 000000000000..7ba9a315b137 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/parallel/ParallelizationWorkflow.java @@ -0,0 +1,50 @@ +package com.baeldung.springai.agenticpatterns.workflows.parallel; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Component; + +import com.baeldung.springai.agenticpatterns.aimodels.OpsClient; +import com.baeldung.springai.agenticpatterns.aimodels.OpsClientPrompts; + +@Component +public class ParallelizationWorkflow { + + private final OpsClient opsClient; + + public ParallelizationWorkflow(OpsClient opsClient) { + this.opsClient = opsClient; + } + + public List opsDeployments(String containerLink, List environments, int maxConcurentWorkers) { + try (ExecutorService executor = Executors.newFixedThreadPool(maxConcurentWorkers)) { + List> futures = environments.stream() + .map(env -> CompletableFuture.supplyAsync(() -> { + try { + String request = OpsClientPrompts.NON_PROD_DEPLOYMENT_PROMPT + "\n Image:" + containerLink + " to environment: " + env; + System.out.println("Request: " + request); + + ChatClient.ChatClientRequestSpec requestSpec = opsClient.prompt(request); + ChatClient.CallResponseSpec responseSpec = requestSpec.call(); + return responseSpec.content(); + } catch (Exception e) { + throw new RuntimeException("Failed to deploy to env: " + env, e); + } + }, executor)) + .toList(); + + // Wait for all tasks to complete + CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + allFutures.join(); + + return futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()); + } + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/routing/RoutingWorkflow.java b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/routing/RoutingWorkflow.java new file mode 100644 index 000000000000..76dd2c6b625b --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/java/com/baeldung/springai/agenticpatterns/workflows/routing/RoutingWorkflow.java @@ -0,0 +1,72 @@ +package com.baeldung.springai.agenticpatterns.workflows.routing; + +import static com.baeldung.springai.agenticpatterns.aimodels.OpsRouterClientPrompts.OPS_ROUTING_OPTIONS; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Component; + +import com.baeldung.springai.agenticpatterns.aimodels.OpsRouterClient; +import com.baeldung.springai.agenticpatterns.workflows.chain.ChainWorkflow; +import com.baeldung.springai.agenticpatterns.workflows.parallel.ParallelizationWorkflow; + +@Component +public class RoutingWorkflow { + + private final OpsRouterClient opsRouterClient; + private final ChainWorkflow chainWorkflow; + private final ParallelizationWorkflow parallelizationWorkflow; + + public RoutingWorkflow(OpsRouterClient opsRouterClient, ChainWorkflow chainWorkflow, ParallelizationWorkflow parallelizationWorkflow) { + this.opsRouterClient = opsRouterClient; + this.chainWorkflow = chainWorkflow; + this.parallelizationWorkflow = parallelizationWorkflow; + } + + public String route(String input) { + // Determine the appropriate route for the input + String[] route = determineRoute(input, OPS_ROUTING_OPTIONS); + String opsOperation = route[0]; + List requestValues = route[1].lines() + .toList(); + + // Get the selected operation from the router and send the request + // (the outcome is already printed out in the relevant model) + return switch (opsOperation) { + case "pipeline" -> chainWorkflow.opsPipeline(requestValues.getFirst()); + case "deployment" -> executeDeployment(requestValues); + default -> throw new IllegalStateException("Unexpected value: " + opsOperation); + }; + } + + @SuppressWarnings("SameParameterValue") + private String[] determineRoute(String input, Map availableRoutes) { + String request = String.format(""" + Given this map that provides the ops operation as key and the description for you to build the operation value, as value: %s. + Analyze the input and select the most appropriate operation. + Return an array of two strings. First string is the operations decided and second is the value you built based on the operation. + + Input: %s""", availableRoutes, input); + + ChatClient.ChatClientRequestSpec requestSpec = opsRouterClient.prompt(request); + ChatClient.CallResponseSpec responseSpec = requestSpec.call(); + String[] routingResponse = responseSpec.entity(String[].class); + System.out.printf("Routing Decision: Operation is: %s\n, Operation value: %s%n", routingResponse[0], routingResponse[1]); + + return routingResponse; + } + + private String executeDeployment(List requestValues) { + String containerLink = requestValues.getFirst(); + List environments = Arrays.asList(requestValues.get(1) + .split(",")); + int maxWorkers = Integer.parseInt(requestValues.getLast()); + + List results = parallelizationWorkflow.opsDeployments(containerLink, environments, maxWorkers); + + return String.join(", ", results); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/main/resources/application.yml b/spring-ai-modules/spring-ai-agentic-patterns/src/main/resources/application.yml new file mode 100644 index 000000000000..42e0f6b41049 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + application: + name: agentic-patterns + main: + web-application-type: none diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/chain/ChainWorkflowTest.java b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/chain/ChainWorkflowTest.java new file mode 100644 index 000000000000..56bc09ba06cd --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/chain/ChainWorkflowTest.java @@ -0,0 +1,58 @@ +package com.baeldung.springai.agenticpatterns.workflows.chain; + +import static com.baeldung.springai.agenticpatterns.aimodels.OpsClientPrompts.DEV_PIPELINE_STEPS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.client.DefaultChatClient; + +import com.baeldung.springai.agenticpatterns.aimodels.DummyOpsClient; + +@ExtendWith(MockitoExtension.class) +class ChainWorkflowTest { + + @Mock + private DummyOpsClient opsClient; + @InjectMocks + private ChainWorkflow chainWorkflow; + + @SuppressWarnings("UnnecessaryLocalVariable") + @Test + void opsPipeline_whenAllStepsAreSuccessful_thenSuccess() { + String prText = "https://github.com/org/repo/pull/70"; + String prompt1 = String.format("{%s}\n {%s}", DEV_PIPELINE_STEPS[0], prText); + String response1 = "internal/code/path"; + mockClient(prompt1, response1); + String prompt2 = String.format("{%s}\n {%s}", DEV_PIPELINE_STEPS[1], response1); + String response2 = response1; + mockClient(prompt2, response2); + String prompt3 = String.format("{%s}\n {%s}", DEV_PIPELINE_STEPS[2], response2); + String response3 = response2 + ". Container link: [hub.docker.com/org/repo:PR-70.1]"; + mockClient(prompt3, response3); + String prompt4 = String.format("{%s}\n {%s}", DEV_PIPELINE_STEPS[3], response3); + String response4 = response1; + mockClient(prompt4, response4); + String prompt5 = String.format("{%s}\n {%s}", DEV_PIPELINE_STEPS[4], response4); + String response5 = "success!"; + mockClient(prompt5, response5); + + String result = chainWorkflow.opsPipeline(prText); + + assertThat("success!").isEqualTo(result); + } + + private void mockClient(String prompt, String response) { + DefaultChatClient.DefaultChatClientRequestSpec requestSpec = mock(DefaultChatClient.DefaultChatClientRequestSpec.class); + DefaultChatClient.DefaultCallResponseSpec responseSpec = mock(DefaultChatClient.DefaultCallResponseSpec.class); + + when(opsClient.prompt(prompt)).thenReturn(requestSpec); + when(requestSpec.call()).thenReturn(responseSpec); + when(responseSpec.content()).thenReturn(response); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/evaluator/EvaluatorOptimizerWorkflowTest.java b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/evaluator/EvaluatorOptimizerWorkflowTest.java new file mode 100644 index 000000000000..d2b4444a6ff6 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/evaluator/EvaluatorOptimizerWorkflowTest.java @@ -0,0 +1,63 @@ +package com.baeldung.springai.agenticpatterns.workflows.evaluator; + +import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.CODE_REVIEW_PROMPT; +import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT; +import static com.baeldung.springai.agenticpatterns.workflows.evaluator.EvaluatorOptimizerWorkflow.mapClass; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.client.DefaultChatClient; + +import com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClient; + +@ExtendWith(MockitoExtension.class) +class EvaluatorOptimizerWorkflowTest { + + @Mock + private CodeReviewClient codeReviewClient; + @InjectMocks + private EvaluatorOptimizerWorkflow evaluatorOptimizerWorkflow; + + @Test + void opsPipeline_whenAllStepsAreSuccessful_thenSuccess() { + String prLink = "https://github.com/org/repo/pull/70"; + String firstGenerationRequest = CODE_REVIEW_PROMPT + "\n PR: " + prLink + "\n previous suggestions: {}" + "\n evaluation on previous suggestions: "; + Map firstSuggestion = Map.of("Client:23:'no multiple variables in 1 line'", "int x = 0;\\n int y = 0;"); + mockCodeReviewClient(firstGenerationRequest, firstSuggestion); + String firstEvaluationRequest = EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT + "\n PR: " + prLink + "\n proposed suggestions: " + firstSuggestion; + Map firstEvaluation = Map.of("FAIL", "method names should be more descriptive"); + mockCodeReviewClient(firstEvaluationRequest, firstEvaluation); + String secondGenerationRequest = + CODE_REVIEW_PROMPT + "\n PR: " + prLink + "\n previous suggestions: " + firstSuggestion + "\n evaluation on previous suggestions: " + + firstEvaluation.values() + .iterator() + .next(); + Map secondSuggestion = Map.of("Client:23:'no multiple variables in 1 line & improved names'", + "int readTimeout = 0;\\n int connectTimeout = 0;"); + mockCodeReviewClient(secondGenerationRequest, secondSuggestion); + String secondEvaluationRequest = EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT + "\n PR: " + prLink + "\n proposed suggestions: " + secondSuggestion; + Map secondEvaluation = Map.of("PASS", ""); + mockCodeReviewClient(secondEvaluationRequest, secondEvaluation); + + Map response = evaluatorOptimizerWorkflow.evaluate(prLink); + + assertThat(response).isEqualTo(secondSuggestion); + } + + private void mockCodeReviewClient(String prompt, Map response) { + DefaultChatClient.DefaultChatClientRequestSpec requestSpec = mock(DefaultChatClient.DefaultChatClientRequestSpec.class); + DefaultChatClient.DefaultCallResponseSpec responseSpec = mock(DefaultChatClient.DefaultCallResponseSpec.class); + + when(codeReviewClient.prompt(prompt)).thenReturn(requestSpec); + when(requestSpec.call()).thenReturn(responseSpec); + when(responseSpec.entity(mapClass)).thenReturn(response); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/orchestrator/OrchestratorWorkersWorkflowTest.java b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/orchestrator/OrchestratorWorkersWorkflowTest.java new file mode 100644 index 000000000000..35a0ae594823 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/orchestrator/OrchestratorWorkersWorkflowTest.java @@ -0,0 +1,68 @@ +package com.baeldung.springai.agenticpatterns.workflows.orchestrator; + +import static com.baeldung.springai.agenticpatterns.aimodels.OpsClientPrompts.EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS; +import static com.baeldung.springai.agenticpatterns.aimodels.OpsOrchestratorClientPrompts.REMOTE_TESTING_ORCHESTRATION_PROMPT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.client.DefaultChatClient; + +import com.baeldung.springai.agenticpatterns.aimodels.OpsClient; +import com.baeldung.springai.agenticpatterns.aimodels.OpsOrchestratorClient; + +@ExtendWith(MockitoExtension.class) +class OrchestratorWorkersWorkflowTest { + + @Mock + private OpsOrchestratorClient opsOrchestratorClient; + @Mock + private OpsClient opsClient; + @InjectMocks + private OrchestratorWorkersWorkflow orchestratorWorkersWorkflow; + + @Test + void remoteTestingExecution_whenEnvsToTestAreDevAndIntAndAllStepsAreSuccessful_thenSuccess() { + String prText = "https://github.com/org/repo/pull/70"; + mockOrchestratorClient(REMOTE_TESTING_ORCHESTRATION_PROMPT + prText, new String[] { prText, "dev", "int" }); + String devPrompt1 = String.format("%s\n PR: [%s] environment", EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS[0], prText + " on dev"); + String devResponse1 = prText + " on dev"; + mockOpsClient(devPrompt1, devResponse1); + String devPrompt2 = String.format("%s\n PR: [%s] environment", EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS[1], devResponse1); + String devResponse2 = "DEV\nTest executed: 10, successful: 10."; + mockOpsClient(devPrompt2, devResponse2); + String intPrompt1 = String.format("%s\n PR: [%s] environment", EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS[0], prText + " on int"); + String intResponse1 = prText + " on int"; + mockOpsClient(intPrompt1, intResponse1); + String intPrompt2 = String.format("%s\n PR: [%s] environment", EXECUTE_TEST_ON_DEPLOYED_ENV_STEPS[1], intResponse1); + String intResponse2 = "INT\nTest executed: 5, successful: 5."; + mockOpsClient(intPrompt2, intResponse2); + + String result = orchestratorWorkersWorkflow.remoteTestingExecution(prText); + + assertThat(result).isEqualTo("DEV\nTest executed: 10, successful: 10.\nINT\nTest executed: 5, successful: 5.\n"); + } + + private void mockOrchestratorClient(String prompt, String[] response) { + DefaultChatClient.DefaultChatClientRequestSpec requestSpec = mock(DefaultChatClient.DefaultChatClientRequestSpec.class); + DefaultChatClient.DefaultCallResponseSpec responseSpec = mock(DefaultChatClient.DefaultCallResponseSpec.class); + + when(opsOrchestratorClient.prompt(prompt)).thenReturn(requestSpec); + when(requestSpec.call()).thenReturn(responseSpec); + when(responseSpec.entity(String[].class)).thenReturn(response); + } + + private void mockOpsClient(String prompt, String response) { + DefaultChatClient.DefaultChatClientRequestSpec requestSpec = mock(DefaultChatClient.DefaultChatClientRequestSpec.class); + DefaultChatClient.DefaultCallResponseSpec responseSpec = mock(DefaultChatClient.DefaultCallResponseSpec.class); + + when(opsClient.prompt(prompt)).thenReturn(requestSpec); + when(requestSpec.call()).thenReturn(responseSpec); + when(responseSpec.content()).thenReturn(response); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/parallel/ParallelizationWorkflowTest.java b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/parallel/ParallelizationWorkflowTest.java new file mode 100644 index 000000000000..49b62706b4fb --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/parallel/ParallelizationWorkflowTest.java @@ -0,0 +1,52 @@ +package com.baeldung.springai.agenticpatterns.workflows.parallel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.client.DefaultChatClient; + +import com.baeldung.springai.agenticpatterns.aimodels.DummyOpsClient; +import com.baeldung.springai.agenticpatterns.aimodels.OpsClientPrompts; + +@ExtendWith(MockitoExtension.class) +class ParallelizationWorkflowTest { + + @Mock + private DummyOpsClient opsClient; + @InjectMocks + private ParallelizationWorkflow parallelizationWorkflow; + + @Test + void opsPipeline_whenAllStepsAreSuccessful_thenSuccess() { + String containerLink = "hub.docker.com/org/repo:PR-70.1"; + String successResponse = "success!"; + String prompt1 = OpsClientPrompts.NON_PROD_DEPLOYMENT_PROMPT + "\n Image:" + containerLink + " to environment: dev"; + mockClient(prompt1, successResponse); + String prompt2 = OpsClientPrompts.NON_PROD_DEPLOYMENT_PROMPT + "\n Image:" + containerLink + " to environment: test"; + mockClient(prompt2, successResponse); + String prompt3 = OpsClientPrompts.NON_PROD_DEPLOYMENT_PROMPT + "\n Image:" + containerLink + " to environment: demo"; + mockClient(prompt3, successResponse); + + List results = parallelizationWorkflow.opsDeployments(containerLink, List.of("dev", "test", "demo"), 2); + + assertThat(results).hasSize(3); + assertThat(results).containsExactly("success!", "success!", "success!"); + } + + private void mockClient(String prompt, String response) { + DefaultChatClient.DefaultChatClientRequestSpec requestSpec = mock(DefaultChatClient.DefaultChatClientRequestSpec.class); + DefaultChatClient.DefaultCallResponseSpec responseSpec = mock(DefaultChatClient.DefaultCallResponseSpec.class); + + when(opsClient.prompt(prompt)).thenReturn(requestSpec); + when(requestSpec.call()).thenReturn(responseSpec); + when(responseSpec.content()).thenReturn(response); + } +} diff --git a/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/routing/RoutingWorkflowTest.java b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/routing/RoutingWorkflowTest.java new file mode 100644 index 000000000000..6615133c1ac2 --- /dev/null +++ b/spring-ai-modules/spring-ai-agentic-patterns/src/test/java/com/baeldung/springai/agenticpatterns/workflows/routing/RoutingWorkflowTest.java @@ -0,0 +1,55 @@ +package com.baeldung.springai.agenticpatterns.workflows.routing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.client.DefaultChatClient; + +import com.baeldung.springai.agenticpatterns.aimodels.DummyOpsRouterClient; +import com.baeldung.springai.agenticpatterns.workflows.chain.ChainWorkflow; +import com.baeldung.springai.agenticpatterns.workflows.parallel.ParallelizationWorkflow; + +@ExtendWith(MockitoExtension.class) +class RoutingWorkflowTest { + + @Mock + private DummyOpsRouterClient routerClient; + @SuppressWarnings("unused") + @Mock + private ChainWorkflow chainWorkflow; + @Mock + private ParallelizationWorkflow parallelizationWorkflow; + @InjectMocks + private RoutingWorkflow routingWorkflow; + + @Test + void opsPipeline_whenAllStepsAreSuccessful_thenSuccess() { + String input = "please deploy hub.docker.com/org/repo:PR-70.1 to dev and test"; + String successResponse = "success!"; + mockRouterClient(new String[] { "deployment", "hub.docker.com/org/repo:PR-70.1\ndev,test\n3" }); + when(parallelizationWorkflow.opsDeployments("hub.docker.com/org/repo:PR-70.1", List.of("dev", "test"), 3)).thenReturn( + List.of(successResponse, successResponse)); + + String response = routingWorkflow.route(input); + + assertThat(response).isEqualTo("success!, success!"); + } + + private void mockRouterClient(String[] response) { + DefaultChatClient.DefaultChatClientRequestSpec requestSpec = mock(DefaultChatClient.DefaultChatClientRequestSpec.class); + DefaultChatClient.DefaultCallResponseSpec responseSpec = mock(DefaultChatClient.DefaultCallResponseSpec.class); + + when(routerClient.prompt(anyString())).thenReturn(requestSpec); + when(requestSpec.call()).thenReturn(responseSpec); + when(responseSpec.entity(String[].class)).thenReturn(response); + } +}