Skip to content

Commit f6c7de0

Browse files
Added tests
1 parent 7f84ab8 commit f6c7de0

File tree

7 files changed

+92
-21
lines changed

7 files changed

+92
-21
lines changed

docs/guides/SPRING_AI_INTEGRATION.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- [Orchestration Chat Completion](#orchestration-chat-completion)
77
- [Orchestration Masking](#orchestration-masking)
88
- [Stream chat completion](#stream-chat-completion)
9-
- [Function Calling](#function-calling)
9+
- [Tool Calling](#tool-calling)
1010

1111
## Introduction
1212

@@ -34,7 +34,7 @@ First, add the Spring AI dependency to your `pom.xml`:
3434

3535
:::note Spring AI Milestone Version
3636
Note that currently no stable version of Spring AI exists just yet.
37-
The AI SDK currently uses the [M5 milestone](https://spring.io/blog/2024/12/23/spring-ai-1-0-0-m5-released).
37+
The AI SDK currently uses the [M6 milestone](https://spring.io/blog/2025/02/14/spring-ai-1-0-0-m6-released).
3838

3939
Please be aware that future versions of the AI SDK may increase the Spring AI version.
4040
:::
@@ -102,7 +102,7 @@ _Note: A Spring endpoint can return `Flux` instead of `ResponseEntity`._
102102

103103
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java).
104104

105-
## Function Calling
105+
## Tool Calling
106106

107107
First define a function that will be called by the LLM:
108108

@@ -124,12 +124,12 @@ Then add your function to the options:
124124
OrchestrationChatOptions options = new OrchestrationChatOptions(config);
125125
options.setToolCallbacks(
126126
List.of(
127-
ToolCallback.builder()
128-
.function(
129-
"CurrentWeather", new MockWeatherService()) // (1) function name and instance
127+
FunctionToolCallback.builder(
128+
"CurrentWeather", new MockWeatherService()) // (1) function name and instance
130129
.description("Get the weather in location") // (2) function description
131130
.inputType(MockWeatherService.Request.class) // (3) function input type
132131
.build()));
132+
options.setInternalToolExecutionEnabled(false);// tool execution is not yet available in orchestration
133133
Prompt prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
134134

135135
ChatResponse response = client.call(prompt);

orchestration/pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
</developers>
3232
<properties>
3333
<project.rootdir>${project.basedir}/../</project.rootdir>
34-
<coverage.complexity>79%</coverage.complexity>
34+
<coverage.complexity>81%</coverage.complexity>
3535
<coverage.line>92%</coverage.line>
36-
<coverage.instruction>92%</coverage.instruction>
37-
<coverage.branch>73%</coverage.branch>
38-
<coverage.method>90%</coverage.method>
36+
<coverage.instruction>93%</coverage.instruction>
37+
<coverage.branch>74%</coverage.branch>
38+
<coverage.method>93%</coverage.method>
3939
<coverage.class>100%</coverage.class>
4040
</properties>
4141

orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModelTest.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.junit.jupiter.api.BeforeEach;
4040
import org.junit.jupiter.api.Test;
4141
import org.mockito.Mockito;
42+
import org.springframework.ai.chat.messages.AssistantMessage.ToolCall;
4243
import org.springframework.ai.chat.model.ChatResponse;
4344
import org.springframework.ai.chat.prompt.Prompt;
4445
import org.springframework.ai.tool.function.FunctionToolCallback;
@@ -143,7 +144,44 @@ void testStreamCompletion() throws IOException {
143144
}
144145

145146
@Test
146-
void testToolCalls() throws IOException {
147+
void testToolCallsWithoutExecution() throws IOException {
148+
stubFor(
149+
post(urlPathEqualTo("/completion"))
150+
.willReturn(
151+
aResponse()
152+
.withBodyFile("toolCallsResponse.json")
153+
.withHeader("Content-Type", "application/json")));
154+
155+
defaultOptions.setToolCallbacks(
156+
List.of(
157+
FunctionToolCallback.builder(
158+
"CurrentWeather", new MockWeatherService()) // (1) function name and instance
159+
.description("Get the weather in location") // (2) function description
160+
.inputType(MockWeatherService.Request.class) // (3) function input type
161+
.build()));
162+
defaultOptions.setInternalToolExecutionEnabled(false);
163+
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", defaultOptions);
164+
val result = client.call(prompt);
165+
166+
List<ToolCall> toolCalls = result.getResult().getOutput().getToolCalls();
167+
assertThat(toolCalls).hasSize(2);
168+
ToolCall toolCall1 = toolCalls.get(0);
169+
ToolCall toolCall2 = toolCalls.get(1);
170+
assertThat(toolCall1.type()).isEqualTo("function");
171+
assertThat(toolCall2.type()).isEqualTo("function");
172+
assertThat(toolCall1.name()).isEqualTo("CurrentWeather");
173+
assertThat(toolCall2.name()).isEqualTo("CurrentWeather");
174+
assertThat(toolCall1.arguments()).isEqualTo("{\"location\": \"Potsdam\", \"unit\": \"C\"}");
175+
assertThat(toolCall2.arguments()).isEqualTo("{\"location\": \"Toulouse\", \"unit\": \"C\"}");
176+
177+
try (var request1InputStream = fileLoader.apply("toolCallsRequest.json")) {
178+
final String request1 = new String(request1InputStream.readAllBytes());
179+
verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request1)));
180+
}
181+
}
182+
183+
@Test
184+
void testToolCallsWithExecution() throws IOException {
147185
// https://platform.openai.com/docs/guides/function-calling
148186
stubFor(
149187
post(urlPathEqualTo("/completion"))

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiOrchestrationController.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import javax.annotation.Nonnull;
66
import javax.annotation.Nullable;
77
import lombok.val;
8+
import org.springframework.ai.chat.messages.AssistantMessage;
89
import org.springframework.beans.factory.annotation.Autowired;
910
import org.springframework.web.bind.annotation.GetMapping;
1011
import org.springframework.web.bind.annotation.RequestMapping;
@@ -63,16 +64,19 @@ Object masking(@Nullable @RequestParam(value = "format", required = false) final
6364
return response.getResult().getOutput().getText();
6465
}
6566

66-
@GetMapping("/functionCalling")
67-
Object functionCalling(
67+
@GetMapping("/toolCalling")
68+
Object toolCalling(
6869
@Nullable @RequestParam(value = "format", required = false) final String format) {
69-
val response = service.functionCalling();
70+
// tool execution broken on orchestration https://jira.tools.sap/browse/AI-86627
71+
val response = service.toolCalling(false);
7072

7173
if ("json".equals(format)) {
7274
return ((OrchestrationSpringChatResponse) response)
7375
.getOrchestrationResponse()
7476
.getOriginalResponse();
7577
}
76-
return response.getResult().getOutput().getText();
78+
final AssistantMessage message = response.getResult().getOutput();
79+
final String text = message.getText();
80+
return text.isEmpty() ? message.getToolCalls().toString() : text;
7781
}
7882
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,24 @@ public ChatResponse masking() {
9292

9393
/**
9494
* Register a function that will be called when the user asks for the weather. <a
95-
* href="https://docs.spring.io/spring-ai/reference/api/chat/functions/openai-chat-functions.html#_registercall_functions_with_prompt_options">Spring
95+
* href="https://docs.spring.io/spring-ai/reference/api/tools.html#_programmatic_specification_functiontoolcallback">Spring
9696
* AI Function Calling</a>
9797
*
9898
* @return the assistant response object
9999
*/
100100
@Nonnull
101-
public ChatResponse functionCalling() {
101+
public ChatResponse toolCalling(final boolean internalToolExecutionEnabled) {
102102
final OrchestrationChatOptions options = new OrchestrationChatOptions(config);
103103
options.setToolCallbacks(
104104
List.of(
105-
FunctionToolCallback.builder("CurrentWeather", new MockWeatherService())
105+
FunctionToolCallback.builder(
106+
"CurrentWeather", new MockWeatherService()) // (1) function name and instance
106107
.description("Get the weather in location") // (2) function description
107108
.inputType(MockWeatherService.Request.class) // (3) function input type
108109
.build()));
109-
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
110+
options.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
110111

112+
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
111113
return client.call(prompt);
112114
}
113115
}

sample-code/spring-app/src/main/resources/static/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,8 +566,8 @@ <h5 class="mb-1">Orchestration Integration</h5>
566566
</li>
567567
<li class="list-group-item">
568568
<div class="info-tooltip">
569-
<button type="submit" formaction="/spring-ai-orchestration/functionCalling" class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
570-
<code>/spring-ai-orchestration/functionCalling</code>
569+
<button type="submit" formaction="/spring-ai-orchestration/toolCalling" class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
570+
<code>/spring-ai-orchestration/toolCalling</code>
571571
</button>
572572
<div class="tooltip-content">
573573
Register a function that will be called when the user asks for the weather.

sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOrchestrationTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.sap.ai.sdk.app.controllers;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
45

56
import com.sap.ai.sdk.app.services.SpringAiOrchestrationService;
7+
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
8+
import java.util.List;
69
import java.util.concurrent.atomic.AtomicInteger;
710
import lombok.extern.slf4j.Slf4j;
811
import org.junit.jupiter.api.Test;
12+
import org.springframework.ai.chat.messages.AssistantMessage.ToolCall;
913
import org.springframework.ai.chat.model.ChatResponse;
1014

1115
@Slf4j
@@ -53,4 +57,27 @@ void testMasking() {
5357
assertThat(response).isNotNull();
5458
assertThat(response.getResult().getOutput().getText()).isNotEmpty();
5559
}
60+
61+
@Test
62+
void testToolCallingWithoutExecution() {
63+
ChatResponse response = service.toolCalling(false);
64+
List<ToolCall> toolCalls = response.getResult().getOutput().getToolCalls();
65+
assertThat(toolCalls).hasSize(2);
66+
ToolCall toolCall1 = toolCalls.get(0);
67+
ToolCall toolCall2 = toolCalls.get(1);
68+
assertThat(toolCall1.type()).isEqualTo("function");
69+
assertThat(toolCall2.type()).isEqualTo("function");
70+
assertThat(toolCall1.name()).isEqualTo("CurrentWeather");
71+
assertThat(toolCall2.name()).isEqualTo("CurrentWeather");
72+
assertThat(toolCall1.arguments()).isEqualTo("{\"location\": \"Potsdam\", \"unit\": \"C\"}");
73+
assertThat(toolCall2.arguments()).isEqualTo("{\"location\": \"Toulouse\", \"unit\": \"C\"}");
74+
}
75+
76+
@Test
77+
void testToolCallingWithExecution() {
78+
// tool execution broken on orchestration https://jira.tools.sap/browse/AI-86627
79+
assertThatThrownBy(() -> service.toolCalling(true))
80+
.isExactlyInstanceOf(OrchestrationClientException.class)
81+
.hasMessageContaining("Request failed with status 400 Bad Request");
82+
}
5683
}

0 commit comments

Comments
 (0)