Skip to content

Commit 27fa975

Browse files
committed
test: OpenAiToolExecutor and improve error messages
1 parent c53b04e commit 27fa975

File tree

5 files changed

+119
-63
lines changed

5 files changed

+119
-63
lines changed

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,19 @@
3737
public class OpenAiTool<I> {
3838

3939
/** The name of the function. */
40-
@Nonnull String name;
40+
private @Nonnull String name;
4141

4242
/** The model class for function request. */
43-
@Nonnull Class<I> requestClass;
43+
private @Nonnull Class<I> requestClass;
4444

4545
/** An optional description of the function. */
46-
@Nullable String description;
46+
private @Nullable String description;
4747

4848
/** An optional flag indicating whether the function parameters should be treated strictly. */
49-
@Nullable Boolean strict;
49+
private @Nullable Boolean strict;
5050

5151
/** The function to be called. */
52-
@Nullable Function<I, ?> function;
52+
private @Nullable Function<I, ?> function;
5353

5454
/**
5555
* Constructs an {@code OpenAiFunctionTool} with the specified name and a model class that
@@ -65,7 +65,7 @@ public OpenAiTool(@Nonnull final String name, @Nonnull final Class<I> requestCla
6565
@Nonnull
6666
Object execute(@Nonnull final I argument) {
6767
if (getFunction() == null) {
68-
throw new IllegalStateException("Callback function is not set");
68+
throw new IllegalStateException("Function must not be set to execute the tool.");
6969
}
7070
return getFunction().apply(argument);
7171
}

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiToolExecutor.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,17 @@ public class OpenAiToolExecutor {
2323

2424
/**
2525
* Executes the given tool calls with the provided tools and returns the results as a list of
26-
* {@link OpenAiToolMessage}.
26+
* {@link OpenAiToolMessage} containing execution results encoded as JSON string.
2727
*
2828
* @param tools the list of tools to execute
2929
* @param toolCalls the list of tool calls with arguments
3030
* @return the list of tool messages with the results
31+
* @throws IllegalArgumentException if the tool results cannot be serialized to JSON
3132
*/
3233
@Nonnull
3334
public static List<OpenAiToolMessage> executeTools(
34-
@Nonnull final List<OpenAiTool<?>> tools, @Nonnull final List<OpenAiToolCall> toolCalls) {
35+
@Nonnull final List<OpenAiTool<?>> tools, @Nonnull final List<OpenAiToolCall> toolCalls)
36+
throws IllegalArgumentException {
3537

3638
final var toolMap = tools.stream().collect(Collectors.toMap(OpenAiTool::getName, tool -> tool));
3739

@@ -56,11 +58,11 @@ private static <I> Object executeFunction(
5658
}
5759

5860
@Nonnull
59-
private static String serializeObject(@Nonnull final Object obj) {
61+
private static String serializeObject(@Nonnull final Object obj) throws IllegalArgumentException {
6062
try {
6163
return JACKSON.writeValueAsString(obj);
6264
} catch (JsonProcessingException e) {
63-
throw new IllegalArgumentException(e);
65+
throw new IllegalArgumentException("Failed to serialize object to JSON", e);
6466
}
6567
}
6668
}

foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiToolCallTest.java

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.sap.ai.sdk.foundationmodels.openai;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import java.util.List;
7+
import java.util.function.Function;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
class OpenAiToolTest {
12+
private final OpenAiFunctionCall functionCallA =
13+
new OpenAiFunctionCall("1", "functionA", "{\"key\":\"value\"}");
14+
private final OpenAiFunctionCall functionCallB =
15+
new OpenAiFunctionCall("2", "functionB", "{\"key\":\"value\"}");
16+
private final OpenAiFunctionCall invalidFunctionCallA =
17+
new OpenAiFunctionCall("3", "functionA", "{invalid-json}");
18+
19+
private static class Dummy {
20+
record Request(String key) {}
21+
22+
record Response(String result) {}
23+
24+
static final Function<Dummy.Request, Dummy.Response> conCat =
25+
request -> new Dummy.Response(request.key());
26+
}
27+
28+
private OpenAiTool<Dummy.Request> toolA;
29+
30+
@BeforeEach
31+
void setUp() {
32+
toolA = new OpenAiTool<>("functionA", Dummy.Request.class);
33+
}
34+
35+
@Test
36+
void getArgumentsAsMapValid() {
37+
final var result = functionCallA.getArgumentsAsMap();
38+
assertThat(result).containsEntry("key", "value");
39+
}
40+
41+
@Test
42+
void getArgumentsAsMapInvalid() {
43+
assertThatThrownBy(invalidFunctionCallA::getArgumentsAsMap)
44+
.isInstanceOf(IllegalArgumentException.class)
45+
.hasMessageContaining("Failed to parse JSON string");
46+
}
47+
48+
@Test
49+
void getArgumentsAsObjectValid() {
50+
final Dummy.Request result = functionCallA.getArgumentsAsObject(toolA);
51+
assertThat(result).isInstanceOf(Dummy.Request.class);
52+
assertThat(result.key()).isEqualTo("value");
53+
}
54+
55+
@Test
56+
void getArgumentsAsObjectInvalid() {
57+
assertThatThrownBy(() -> invalidFunctionCallA.getArgumentsAsObject(toolA))
58+
.isInstanceOf(IllegalArgumentException.class)
59+
.hasMessageContaining("Failed to parse JSON string");
60+
}
61+
62+
@Test
63+
void executeToolsValid() {
64+
toolA.setFunction(Dummy.conCat);
65+
final var result = OpenAiToolExecutor.executeTools(List.of(toolA), List.of(functionCallA));
66+
67+
assertThat(result).hasSize(1);
68+
assertThat(result.get(0).toolCallId()).isEqualTo("1");
69+
assertThat(((OpenAiTextItem) result.get(0).content().items().get(0)).text())
70+
.isEqualTo("{\"result\":\"value\"}");
71+
}
72+
73+
@Test
74+
void executeToolsThrowsOnNoFunction() {
75+
assertThatThrownBy(
76+
() -> OpenAiToolExecutor.executeTools(List.of(toolA), List.of(functionCallA)))
77+
.isInstanceOf(IllegalStateException.class)
78+
.hasMessageContaining("Function must not be set to execute the tool");
79+
}
80+
81+
@Test
82+
void executeToolsNoMatchingCall() {
83+
final var toolAWithFunction = toolA.setFunction(Dummy.conCat);
84+
final var result =
85+
OpenAiToolExecutor.executeTools(List.of(toolAWithFunction), List.of(functionCallB));
86+
assertThat(result).isEmpty();
87+
}
88+
89+
@Test
90+
void executeToolsThrowsOnSerializationError() {
91+
class NonSerializableResponse {
92+
private String result;
93+
94+
NonSerializableResponse(String result) {
95+
this.result = result;
96+
}
97+
}
98+
99+
toolA.setFunction(request -> new NonSerializableResponse(request.key()));
100+
101+
assertThatThrownBy(
102+
() -> OpenAiToolExecutor.executeTools(List.of(toolA), List.of(functionCallA)))
103+
.isInstanceOf(IllegalArgumentException.class)
104+
.hasMessageContaining("Failed to serialize object to JSON");
105+
}
106+
}

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OpenAiServiceV2.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public OpenAiChatCompletionResponse chatCompletionToolExecution(
112112
final OpenAiAssistantMessage assistantMessage = response.getMessage();
113113

114114
// 3. Execute the tool call for given tools
115-
List<OpenAiToolMessage> toolMessages =
115+
final List<OpenAiToolMessage> toolMessages =
116116
OpenAiToolExecutor.executeTools(tools, assistantMessage.toolCalls());
117117

118118
// 4. Send back the results for model will incorporate them into its final response.

0 commit comments

Comments
 (0)