Skip to content

Commit 47e6639

Browse files
committed
[BAEL-9419] Introduce the evaluator-optimizer-workflow pattern
1 parent 9ed9d9b commit 47e6639

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.baeldung.springai.agenticpatterns.aimodels;
2+
3+
import org.springframework.ai.chat.client.ChatClient;
4+
5+
public interface CodeReviewClient extends ChatClient {
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.baeldung.springai.agenticpatterns.aimodels;
2+
3+
public final class CodeReviewClientPrompts {
4+
5+
private CodeReviewClientPrompts() {
6+
}
7+
8+
/**
9+
* Prompt for the code review of a given PR
10+
*/
11+
public static final String CODE_REVIEW_PROMPT = """
12+
Given a PR link -> generate a map with proposed code improvements.
13+
The key should be {class-name}:{line-number}:{short-description}.
14+
The value should be the code in one line. For example, 1 proposed improvement could be for the line 'int x = 0, y = 0;':
15+
{"Client:23:'no multiple variables defined in 1 line'", "int x = 0;\\n int y = 0;"}
16+
Rules are, to follow the checkstyle and spotless rules set to the repo. Keep java code clear, readable and extensible.
17+
18+
Finally, if it is not your first attempt, there might feedback provided to you, including your previous suggestion.
19+
You should reflect on it and improve the previous suggestions, or even add more.""";
20+
21+
/**
22+
* Prompt for the evaluation of the result
23+
*/
24+
public static final String EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT = """
25+
Evaluate the suggested code improvements for correctness, time complexity, and best practices.
26+
27+
Return a Map with one entry. The key is the value the evaluation. The value will be your feedback.
28+
29+
The evaluation field must be one of: "PASS", "NEEDS_IMPROVEMENT", "FAIL"
30+
Use "PASS" only if all criteria are met with no improvements needed.
31+
""";
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.baeldung.springai.agenticpatterns.aimodels;
2+
3+
import org.springframework.ai.chat.prompt.Prompt;
4+
import org.springframework.lang.NonNull;
5+
import org.springframework.stereotype.Component;
6+
7+
@Component
8+
public class DummyCodeReviewClient implements CodeReviewClient {
9+
10+
@Override
11+
@NonNull
12+
public ChatClientRequestSpec prompt() {
13+
throw new UnsupportedOperationException("Not supported yet.");
14+
}
15+
16+
@Override
17+
@NonNull
18+
public ChatClientRequestSpec prompt(@NonNull String input) {
19+
throw new UnsupportedOperationException("Not supported yet.");
20+
}
21+
22+
@Override
23+
@NonNull
24+
public ChatClientRequestSpec prompt(@NonNull Prompt prompt) {
25+
throw new UnsupportedOperationException("Not supported yet.");
26+
}
27+
28+
@Override
29+
@NonNull
30+
public Builder mutate() {
31+
throw new UnsupportedOperationException("Not supported yet.");
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.baeldung.springai.agenticpatterns.workflows.evaluator;
2+
3+
import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.CODE_REVIEW_PROMPT;
4+
import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
import org.springframework.ai.chat.client.ChatClient;
10+
import org.springframework.core.ParameterizedTypeReference;
11+
import org.springframework.stereotype.Component;
12+
13+
import com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClient;
14+
15+
@Component
16+
public class EvaluatorOptimizerWorkflow {
17+
18+
private final CodeReviewClient codeReviewClient;
19+
static final ParameterizedTypeReference<Map<String, String>> mapClass = new ParameterizedTypeReference<>() {};
20+
21+
public EvaluatorOptimizerWorkflow(CodeReviewClient codeReviewClient) {
22+
this.codeReviewClient = codeReviewClient;
23+
}
24+
25+
public Map<String, String> evaluate(String task) {
26+
return loop(task, new HashMap<>(), "");
27+
}
28+
29+
private Map<String, String> loop(String task, Map<String, String> latestSuggestions, String evaluation) {
30+
latestSuggestions = generate(task, latestSuggestions, evaluation);
31+
Map<String, String> evaluationResponse = evaluate(latestSuggestions, task);
32+
String outcome = evaluationResponse.keySet().iterator().next();
33+
evaluation = evaluationResponse.values().iterator().next();
34+
35+
if ("PASS".equals(outcome)) {
36+
System.out.println("Accepted RE Review Suggestions:\n" + latestSuggestions);
37+
return latestSuggestions;
38+
}
39+
40+
return loop(task, latestSuggestions, evaluation);
41+
}
42+
43+
private Map<String, String> generate(String task, Map<String, String> previousSuggestions, String evaluation) {
44+
String request = CODE_REVIEW_PROMPT +
45+
"\n PR: " + task +
46+
"\n previous suggestions: " + previousSuggestions +
47+
"\n evaluation on previous suggestions: " + evaluation;
48+
System.out.println("PR REVIEW PROMPT: " + request);
49+
50+
ChatClient.ChatClientRequestSpec requestSpec = codeReviewClient.prompt(request);
51+
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
52+
Map<String, String> response = responseSpec.entity(mapClass);
53+
54+
System.out.println("PR REVIEW OUTCOME: " + response);
55+
56+
return response;
57+
}
58+
59+
private Map<String, String> evaluate(Map<String, String> latestSuggestions, String task) {
60+
String request = EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT +
61+
"\n PR: " + task +
62+
"\n proposed suggestions: " + latestSuggestions;
63+
System.out.println("EVALUATION PROMPT: " + request);
64+
65+
ChatClient.ChatClientRequestSpec requestSpec = codeReviewClient.prompt(request);
66+
ChatClient.CallResponseSpec responseSpec = requestSpec.call();
67+
Map<String, String> response = responseSpec.entity(mapClass);
68+
System.out.println("EVALUATION OUTCOME: " + response);
69+
70+
return response;
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.baeldung.springai.agenticpatterns.workflows.evaluator;
2+
3+
import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.CODE_REVIEW_PROMPT;
4+
import static com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClientPrompts.EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT;
5+
import static com.baeldung.springai.agenticpatterns.workflows.evaluator.EvaluatorOptimizerWorkflow.mapClass;
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
import static org.mockito.ArgumentMatchers.any;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.when;
10+
11+
import java.util.Map;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.mockito.InjectMocks;
16+
import org.mockito.Mock;
17+
import org.mockito.junit.jupiter.MockitoExtension;
18+
import org.springframework.ai.chat.client.DefaultChatClient;
19+
import org.springframework.core.ParameterizedTypeReference;
20+
21+
import com.baeldung.springai.agenticpatterns.aimodels.CodeReviewClient;
22+
23+
@ExtendWith(MockitoExtension.class)
24+
class EvaluatorOptimizerWorkflowTest {
25+
26+
@Mock
27+
private CodeReviewClient codeReviewClient;
28+
@InjectMocks
29+
private EvaluatorOptimizerWorkflow evaluatorOptimizerWorkflow;
30+
31+
@Test
32+
void opsPipeline_whenAllStepsAreSuccessful_thenSuccess() {
33+
String prLink = "https://github.com/org/repo/pull/70";
34+
String firstGenerationRequest = CODE_REVIEW_PROMPT + "\n PR: " + prLink + "\n previous suggestions: {}" + "\n evaluation on previous suggestions: ";
35+
Map<String, String> firstSuggestion = Map.of("Client:23:'no multiple variables in 1 line'", "int x = 0;\\n int y = 0;");
36+
mockCodeReviewClient(firstGenerationRequest, firstSuggestion);
37+
String firstEvaluationRequest = EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT + "\n PR: " + prLink + "\n proposed suggestions: " + firstSuggestion;
38+
Map<String, String> firstEvaluation = Map.of("FAIL", "method names should be more descriptive");
39+
mockCodeReviewClient(firstEvaluationRequest, firstEvaluation);
40+
String secondGenerationRequest =
41+
CODE_REVIEW_PROMPT + "\n PR: " + prLink + "\n previous suggestions: " + firstSuggestion + "\n evaluation on previous suggestions: " + firstEvaluation.values().iterator().next();
42+
Map<String, String> secondSuggestion = Map.of("Client:23:'no multiple variables in 1 line & improved names'", "int readTimeout = 0;\\n int connectTimeout = 0;");
43+
mockCodeReviewClient(secondGenerationRequest, secondSuggestion);
44+
String secondEvaluationRequest = EVALUATE_PROPOSED_IMPROVEMENTS_PROMPT + "\n PR: " + prLink + "\n proposed suggestions: " + secondSuggestion;
45+
Map<String, String> secondEvaluation = Map.of("PASS", "");
46+
mockCodeReviewClient(secondEvaluationRequest, secondEvaluation);
47+
48+
Map<String, String> response = evaluatorOptimizerWorkflow.evaluate(prLink);
49+
50+
assertThat(response).isEqualTo(secondSuggestion);
51+
}
52+
53+
private void mockCodeReviewClient(String prompt, Map<String, String> response) {
54+
DefaultChatClient.DefaultChatClientRequestSpec requestSpec = mock(DefaultChatClient.DefaultChatClientRequestSpec.class);
55+
DefaultChatClient.DefaultCallResponseSpec responseSpec = mock(DefaultChatClient.DefaultCallResponseSpec.class);
56+
57+
when(codeReviewClient.prompt(prompt)).thenReturn(requestSpec);
58+
when(requestSpec.call()).thenReturn(responseSpec);
59+
when(responseSpec.entity(mapClass)).thenReturn(response);
60+
}
61+
}

0 commit comments

Comments
 (0)