streamChatCompletion() {
+ val prompt = new Prompt("Can you give me the first 100 numbers of the Fibonacci sequence?");
+ return chatClient.stream(prompt);
+ }
+
+ /**
+ * Turn a method into a tool by annotating it with @Tool. Spring AI
+ * Tool Method Declarative Specification
+ *
+ * @param internalToolExecutionEnabled whether the internal tool execution is enabled
+ * @return the assistant response object
+ */
+ @Nonnull
+ public ChatResponse toolCalling(final boolean internalToolExecutionEnabled) {
+ val options = new DefaultToolCallingChatOptions();
+ options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod())));
+ options.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
+
+ val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
+ return chatClient.call(prompt);
+ }
+
+ /**
+ * Chat request to OpenAI through the OpenAI service using chat memory.
+ *
+ * @return the assistant response object
+ */
+ @Nonnull
+ public ChatResponse chatMemory() {
+ val repository = new InMemoryChatMemoryRepository();
+ val memory = MessageWindowChatMemory.builder().chatMemoryRepository(repository).build();
+ val advisor = MessageChatMemoryAdvisor.builder(memory).build();
+ val cl = ChatClient.builder(chatClient).defaultAdvisors(advisor).build();
+ val prompt1 = new Prompt("What is the capital of France?");
+ val prompt2 = new Prompt("And what is the typical food there?");
+
+ cl.prompt(prompt1).call().content();
+ return Objects.requireNonNull(
+ cl.prompt(prompt2).call().chatResponse(), "Chat response is null");
}
}
diff --git a/sample-code/spring-app/src/main/resources/static/index.html b/sample-code/spring-app/src/main/resources/static/index.html
index 0e4ac11d6..61e2a9f68 100644
--- a/sample-code/spring-app/src/main/resources/static/index.html
+++ b/sample-code/spring-app/src/main/resources/static/index.html
@@ -784,7 +784,8 @@ Orchestration Integration
/spring-ai-orchestration/mcp
- Use an MCP file system server as tool to answer questions about the SDK itself. ⚠️ Only works if the server is started with the "mcp" Spring profile ⚠️.
+ Use an MCP file system server as tool to answer questions about the SDK itself.
+ ⚠️ Only works if the server is started with the "mcp" Spring profile ⚠️.
@@ -837,11 +838,61 @@ OpenAI
/spring-ai-openai/embed/strings
- Get the embedding for a given string using SpringAI from
+ Get the embedding for a given string using OpenAI.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOpenAiTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOpenAiTest.java
index 7d3ea42fd..285b31a70 100644
--- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOpenAiTest.java
+++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/SpringAiOpenAiTest.java
@@ -4,11 +4,17 @@
import com.sap.ai.sdk.app.services.SpringAiOpenAiService;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.model.ChatResponse;
class SpringAiOpenAiTest {
private final SpringAiOpenAiService service = new SpringAiOpenAiService();
+ private static final org.slf4j.Logger log =
+ org.slf4j.LoggerFactory.getLogger(SpringAiOrchestrationTest.class);
@Test
void testEmbedStrings() {
@@ -23,4 +29,66 @@ void testEmbedStrings() {
assertThat(response.getMetadata().getModel())
.isEqualTo(OpenAiModel.TEXT_EMBEDDING_3_SMALL.name());
}
+
+ @Test
+ void testCompletion() {
+ ChatResponse response = service.completion();
+ assertThat(response).isNotNull();
+ assertThat(response.getResult().getOutput().getText()).contains("Paris");
+ }
+
+ @Test
+ void testStreamChatCompletion() {
+ final var stream = service.streamChatCompletion().toStream();
+
+ final var filledDeltaCount = new AtomicInteger(0);
+ stream
+ // foreach consumes all elements, closing the stream at the end
+ .forEach(
+ delta -> {
+ log.info("delta: {}", delta);
+ String text = delta.getResult().getOutput().getText();
+ if (text != null && !text.isEmpty()) {
+ filledDeltaCount.incrementAndGet();
+ }
+ });
+
+ // the first two and the last delta don't have any content
+ // see OpenAiChatCompletionDelta#getDeltaContent
+ assertThat(filledDeltaCount.get()).isGreaterThan(0);
+ }
+
+ @Test
+ void testToolCallingWithExecution() {
+ ChatResponse response = service.toolCalling(true);
+ assertThat(response.getResult().getOutput().getText()).contains("Potsdam", "Toulouse", "°C");
+ }
+
+ @Test
+ void testToolCallingWithoutExecution() {
+ ChatResponse response = service.toolCalling(false);
+ List toolCalls = response.getResult().getOutput().getToolCalls();
+ assertThat(toolCalls).hasSize(2);
+ AssistantMessage.ToolCall toolCall1 = toolCalls.get(0);
+ AssistantMessage.ToolCall toolCall2 = toolCalls.get(1);
+ assertThat(toolCall1.type()).isEqualTo("function");
+ assertThat(toolCall2.type()).isEqualTo("function");
+ assertThat(toolCall1.name()).isEqualTo("getCurrentWeather");
+ assertThat(toolCall2.name()).isEqualTo("getCurrentWeather");
+ assertThat(toolCall1.arguments())
+ .isEqualTo("{\"arg0\": {\"location\": \"Potsdam\", \"unit\": \"C\"}}");
+ assertThat(toolCall2.arguments())
+ .isEqualTo("{\"arg0\": {\"location\": \"Toulouse\", \"unit\": \"C\"}}");
+ }
+
+ @Test
+ void testChatMemory() {
+ ChatResponse response = service.chatMemory();
+ assertThat(response).isNotNull();
+ String text = response.getResult().getOutput().getText();
+ log.info(text);
+ assertThat(text)
+ .containsAnyOf(
+ "French", "onion", "pastries", "cheese", "baguette", "coq au vin", "foie gras");
+ }
}