diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index 0e7e61203..7bffc6829 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -13,17 +13,6 @@ updates:
prefix: 'chore: [DevOps] '
cooldown:
default-days: 4
- ignore:
- - dependency-name: "com.fasterxml.jackson.*:*"
- versions: [ ">=3.0.0" ]
- - dependency-name: "tools.jackson.*:*"
- versions: [ ">=3.0.0" ]
- - dependency-name: "com.github.victools:jsonschema-generator"
- versions: [ ">=5.0.0" ]
- - dependency-name: "com.github.victools:jsonschema-module-jackson"
- versions: [ ">=5.0.0" ]
- - dependency-name: "org.springframework.ai:spring-ai-bom"
- versions: [ ">=2.0.0" ]
groups:
production-minor-patch:
dependency-type: "production"
diff --git a/.github/workflows/e2e-test.yaml b/.github/workflows/e2e-test.yaml
index 3213825d4..f7d8a615d 100644
--- a/.github/workflows/e2e-test.yaml
+++ b/.github/workflows/e2e-test.yaml
@@ -105,7 +105,7 @@ jobs:
run: wget -qO- -S localhost:8080
- name: "Slack Notification"
- if: failure()
+ if: github.ref_name == 'main' && failure()
uses: slackapi/slack-github-action@v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
diff --git a/docs/release_notes.md b/docs/release_notes.md
index b60c091dc..5df2e0dd1 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -9,6 +9,7 @@
### 🔧 Compatibility Notes
- [Foundation models] SAP-RPT was updated to the newer 1.6.0 API
+- [Orchestration] Spring AI support was upgraded to version `2.0.1`
### ✨ New Functionality
diff --git a/foundation-models/openai/pom.xml b/foundation-models/openai/pom.xml
index 18b4ea321..f80c6f41a 100644
--- a/foundation-models/openai/pom.xml
+++ b/foundation-models/openai/pom.xml
@@ -94,6 +94,10 @@
com.github.victools
jsonschema-module-jackson
+
+ tools.jackson.core
+ jackson-databind
+
io.vavr
vavr
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java
index af14b1be5..84f7f9962 100644
--- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java
@@ -86,7 +86,13 @@ public static Builder1 forFunction(@Nonnull final Function {
final Function exec =
s -> function.apply(deserializeArgument(inputClass, s));
- final var schema = GENERATOR.generateSchema(inputClass);
+ final var jackson3Schema = GENERATOR.generateSchema(inputClass);
+ final ObjectNode schema;
+ try {
+ schema = (ObjectNode) JACKSON.readTree(jackson3Schema.toString());
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Failed to parse generated JSON schema", e);
+ }
return new OpenAiTool(name, exec, schema, null, null);
};
}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java
index a38d99ff7..22a998ad4 100644
--- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java
@@ -1,7 +1,5 @@
package com.sap.ai.sdk.foundationmodels.openai.spring;
-import static org.springframework.ai.model.tool.ToolCallingChatOptions.isInternalToolExecutionEnabled;
-
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,7 +33,7 @@
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
-import org.springframework.ai.model.tool.DefaultToolCallingManager;
+import org.springframework.ai.model.tool.DefaultToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import reactor.core.publisher.Flux;
@@ -49,8 +47,10 @@ public class OpenAiChatModel implements ChatModel {
private final OpenAiClient client;
@Nonnull
- private final DefaultToolCallingManager toolCallingManager =
- DefaultToolCallingManager.builder().build();
+ @Override
+ public ChatOptions getOptions() {
+ return DefaultToolCallingChatOptions.builder().toolCallbacks(List.of()).build();
+ }
@Override
@Nonnull
@@ -66,18 +66,7 @@ public ChatResponse call(@Nonnull final Prompt prompt) {
}
val result = client.chatCompletion(request);
- val response = new ChatResponse(toGenerations(result));
-
- if (options != null && isInternalToolExecutionEnabled(options) && response.hasToolCalls()) {
- val toolCalls =
- response.getResult().getOutput().getToolCalls().stream().map(ToolCall::name).toList();
- log.info("Executing {} tool call(s) - {}.", toolCalls.size(), toolCalls);
- val toolExecutionResult = toolCallingManager.executeToolCalls(prompt, response);
- // Send the tool execution result back to the model.
- log.debug("Re-invoking model with tool execution results.");
- return call(new Prompt(toolExecutionResult.conversationHistory(), options));
- }
- return response;
+ return new ChatResponse(toGenerations(result));
}
@Override
@@ -129,14 +118,15 @@ private static List extractMessages(final Prompt prompt) {
private static void addAssistantMessage(
final List result, final AssistantMessage message) {
- if (message.getText() != null) {
- result.add(OpenAiMessage.assistant(message.getText()));
+ final var toolCalls = message.getToolCalls();
+ if (toolCalls != null && !toolCalls.isEmpty()) {
+ final Function callTranslate =
+ toolCall -> OpenAiToolCall.function(toolCall.id(), toolCall.name(), toolCall.arguments());
+ val calls = toolCalls.stream().map(callTranslate).toList();
+ result.add(OpenAiMessage.assistant(calls));
return;
}
- final Function callTranslate =
- toolCall -> OpenAiToolCall.function(toolCall.id(), toolCall.name(), toolCall.arguments());
- val calls = message.getToolCalls().stream().map(callTranslate).toList();
- result.add(OpenAiMessage.assistant(calls));
+ Option.of(message.getText()).peek(t -> result.add(OpenAiMessage.assistant(t)));
}
private static void addToolMessages(
diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java
index 3f92e35ee..6fb1478c7 100644
--- a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java
+++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java
@@ -134,9 +134,10 @@ void testToolCallsWithoutExecution() throws IOException {
.withHeader("Content-Type", "application/json")
.withBodyFile("weatherToolResponse.json")));
- var options = new DefaultToolCallingChatOptions();
- options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod())));
- options.setInternalToolExecutionEnabled(false);
+ var options =
+ DefaultToolCallingChatOptions.builder()
+ .toolCallbacks(ToolCallbacks.from(new WeatherMethod()))
+ .build();
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
val result = client.call(prompt);
@@ -178,10 +179,16 @@ void testToolCallsWithExecution() throws IOException {
.withBodyFile("weatherToolResponse2.json")
.withHeader("Content-Type", "application/json")));
- var options = new DefaultToolCallingChatOptions();
- options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod())));
- val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
- val result = client.call(prompt);
+ var options =
+ DefaultToolCallingChatOptions.builder()
+ .toolCallbacks(ToolCallbacks.from(new WeatherMethod()))
+ .build();
+ val chatClient = ChatClient.builder(client).build();
+ val result =
+ chatClient
+ .prompt(new Prompt("What is the weather in Potsdam and in Toulouse?", options))
+ .call()
+ .chatResponse();
assertThat(result.getResult().getOutput().getText())
.isEqualTo("The current temperature in Potsdam is 30°C and in Toulouse 30°C.");
diff --git a/orchestration/pom.xml b/orchestration/pom.xml
index e5d998154..e1fa08555 100644
--- a/orchestration/pom.xml
+++ b/orchestration/pom.xml
@@ -118,6 +118,10 @@
com.github.victools
jsonschema-module-jackson
+
+ tools.jackson.core
+ jackson-databind
+
com.fasterxml.jackson.dataformat
jackson-dataformat-yaml
diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java
index 718db8755..0ba8b8cde 100644
--- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java
+++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java
@@ -1,5 +1,6 @@
package com.sap.ai.sdk.orchestration;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.victools.jsonschema.generator.Option;
@@ -7,8 +8,8 @@
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
-import com.github.victools.jsonschema.module.jackson.JacksonModule;
import com.github.victools.jsonschema.module.jackson.JacksonOption;
+import com.github.victools.jsonschema.module.jackson.JacksonSchemaModule;
import java.lang.reflect.Type;
import java.util.Map;
import javax.annotation.Nonnull;
@@ -63,7 +64,7 @@ public static ResponseJsonSchema fromMap(
@Nonnull
public static ResponseJsonSchema fromType(@Nonnull final Type classType) {
val module =
- new JacksonModule(
+ new JacksonSchemaModule(
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER);
val generator =
new SchemaGenerator(
@@ -73,8 +74,12 @@ public static ResponseJsonSchema fromType(@Nonnull final Type classType) {
.with(module)
.build());
val jsonSchema = generator.generateSchema(classType);
- val mapper = new ObjectMapper();
- val schemaMap = mapper.convertValue(jsonSchema, new TypeReference
+
+ com.networknt
+ json-schema-validator
+ ${json-schema-validator.version}
+
com.fasterxml.jackson.datatype
jackson-datatype-jsr310
@@ -195,6 +202,11 @@
jsonschema-module-jackson
${jsonschema-generator.version}
+
+ com.github.victools
+ jsonschema-module-swagger-2
+ ${jsonschema-module-swagger.version}
+
com.fasterxml.jackson.dataformat
jackson-dataformat-yaml
diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml
index 47b2083a1..4591f827b 100644
--- a/sample-code/spring-app/pom.xml
+++ b/sample-code/spring-app/pom.xml
@@ -36,7 +36,6 @@
4.1.1
4.2.0
11.0.25
- 2.0.1
2.22
true
@@ -59,12 +58,6 @@
tomcat-embed-websocket
${apache-tomcat-embed.version}
-
-
- io.modelcontextprotocol.sdk
- mcp-core
- ${mcp-core.version}
-
org.junit
@@ -169,25 +162,6 @@
-
- io.modelcontextprotocol.sdk
- mcp-core
- runtime
-
-
- org.springframework.ai
- spring-ai-autoconfigure-mcp-client
- 1.0.9
- runtime
-
-
-
- org.springframework.boot
- spring-boot-starter
-
-
-
org.springframework.boot
spring-boot-autoconfigure
diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiAgenticWorkflowService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiAgenticWorkflowService.java
index 9cb0fd3d7..ac288fbff 100644
--- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiAgenticWorkflowService.java
+++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiAgenticWorkflowService.java
@@ -46,9 +46,11 @@ public ChatResponse runAgent(@Nonnull final String userInput) {
val cl = ChatClient.builder(client).defaultAdvisors(advisor).build();
// Add (mocked) tools
- val options = new OrchestrationChatOptions(config);
- options.setToolCallbacks(
- List.of(ToolCallbacks.from(new WeatherMethod(), new RestaurantMethod())));
+ val options =
+ new OrchestrationChatOptions(config)
+ .mutate()
+ .toolCallbacks(ToolCallbacks.from(new WeatherMethod(), new RestaurantMethod()))
+ .build();
// Prompts for the chain workflow
final List systemPrompts =
diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java
index 48796e833..94d912200 100644
--- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java
+++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java
@@ -91,11 +91,16 @@ public Flux streamChatCompletion() {
*/
@Nonnull
public ChatResponse toolCalling(final boolean internalToolExecutionEnabled) {
- val options = new DefaultToolCallingChatOptions();
- options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod())));
- options.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
-
+ val options =
+ DefaultToolCallingChatOptions.builder()
+ .toolCallbacks(ToolCallbacks.from(new WeatherMethod()))
+ .build();
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
+ if (internalToolExecutionEnabled) {
+ return Objects.requireNonNull(
+ ChatClient.builder(chatClient).build().prompt(prompt).call().chatResponse(),
+ "Chat response is null");
+ }
return chatClient.call(prompt);
}
diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java
index 62c9cdc8a..cd896326a 100644
--- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java
+++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java
@@ -176,11 +176,18 @@ public ChatResponse outputFiltering(@Nonnull final AzureFilterThreshold policy)
*/
@Nonnull
public ChatResponse toolCalling(final boolean internalToolExecutionEnabled) {
- val options = new OrchestrationChatOptions(config);
- options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod())));
- options.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
+ val options =
+ new OrchestrationChatOptions(config)
+ .mutate()
+ .toolCallbacks(ToolCallbacks.from(new WeatherMethod()))
+ .build();
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
+ if (internalToolExecutionEnabled) {
+ return Objects.requireNonNull(
+ ChatClient.builder(client).build().prompt(prompt).call().chatResponse(),
+ "Chat response is null");
+ }
return client.call(prompt);
}
@@ -202,8 +209,11 @@ public ChatResponse toolCallingMcp() {
"No MCP clients were found. Ensure that you configured the clients correctly in the application.yaml file.");
}
// GPT-4o-mini doesn't work too well with the file system tool, so we use 4o here
- val options = new OrchestrationChatOptions(config.withLlmConfig(GPT_4O));
- options.setToolCallbacks(List.of(toolCallbackProvider.getToolCallbacks()));
+ val options =
+ new OrchestrationChatOptions(config.withLlmConfig(GPT_4O))
+ .mutate()
+ .toolCallbacks(toolCallbackProvider.getToolCallbacks())
+ .build();
val sys =
new SystemMessage(