From 9ff262be3aa534cd7ac5bc7faa28a486b123798a Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Mon, 16 Jun 2025 17:44:44 +0200 Subject: [PATCH 1/3] first draft --- .../SpringAiAgenticWorkflowController.java | 35 +++++++++ .../ai/sdk/app/services/RestaurantMethod.java | 43 +++++++++++ .../SpringAiAgenticWorkflowService.java | 72 +++++++++++++++++++ .../src/main/resources/static/index.html | 20 ++++++ 4 files changed, 170 insertions(+) create mode 100644 sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java create mode 100644 sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java create mode 100644 sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiAgenticWorkflowService.java diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java new file mode 100644 index 000000000..ff1f839aa --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java @@ -0,0 +1,35 @@ +package com.sap.ai.sdk.app.controllers; + +import com.sap.ai.sdk.app.services.SpringAiAgenticWorkflowService; +import com.sap.ai.sdk.orchestration.spring.OrchestrationSpringChatResponse; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@SuppressWarnings("unused") +@RestController +@Slf4j +@RequestMapping("/spring-ai-agentic") +public class SpringAiAgenticWorkflowController { + + @Autowired private SpringAiAgenticWorkflowService service; + + @GetMapping("/chain") + Object completion( + @Nullable @RequestParam(value = "format", required = false) final String format) { + val response = + service.chain("I want to do a one-day trip to Paris. Help me make an itinerary, please"); + + if ("json".equals(format)) { + return ((OrchestrationSpringChatResponse) response) + .getOrchestrationResponse() + .getOriginalResponse(); + } + return response.getResult().getOutput().getText(); + } +} diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java new file mode 100644 index 000000000..fb8377c21 --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java @@ -0,0 +1,43 @@ +package com.sap.ai.sdk.app.services; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import javax.annotation.Nonnull; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; + +/** Mock tool for agentic workflow */ +class RestaurantMethod { + + /** + * Request for list of restaurants + * + * @param location the city + */ + record Request(String location) {} + + /** + * Response for restaurant recommendations + * + * @param restaurants the list of restaurants + */ + record Response(List restaurants) {} + + @Nonnull + @SuppressWarnings("unused") + @Tool(description = "Get recommended restaurants for a location") + static RestaurantMethod.Response getRestaurants( + @ToolParam @Nonnull final RestaurantMethod.Request request) { + var recommendations = + Map.of( + "paris", + List.of("Le Comptoir du Relais", "L'As du Fallafel", "Breizh Café"), + "reykjavik", + List.of("Dill Restaurant", "Fish Market", "Grillmarkaðurinn")); + return new RestaurantMethod.Response( + recommendations.getOrDefault( + request.location.toLowerCase(Locale.ROOT), + List.of("No recommendations for this city."))); + } +} 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 new file mode 100644 index 000000000..f5851d48e --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiAgenticWorkflowService.java @@ -0,0 +1,72 @@ +package com.sap.ai.sdk.app.services; + +import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI; + +import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; +import com.sap.ai.sdk.orchestration.spring.OrchestrationChatModel; +import com.sap.ai.sdk.orchestration.spring.OrchestrationChatOptions; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import lombok.val; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.InMemoryChatMemory; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.tool.ToolCallbacks; +import org.springframework.stereotype.Service; + +@Service +public class SpringAiAgenticWorkflowService { + private final ChatModel client = new OrchestrationChatModel(); + private final OrchestrationModuleConfig config = + new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); + + @Nonnull + public ChatResponse chain(String userInput) { + + // Configure chat memory + val memory = new InMemoryChatMemory(); + val advisor = new MessageChatMemoryAdvisor(memory); + 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()))); + options.setInternalToolExecutionEnabled(true); + + // Prompts for the chain workflow + List systemPrompts = + List.of( + "You are a traveling planning agent for a single day trip. Where appropriate, use the provided tools. First, start by suggesting some restaurants for the mentioned city.", + "Now, check the whether for the city.", + "Finally, combine the suggested itinerary from this conversation into a short, one-sentence plan for the day trip."); + + // Perform the chain workflow + int step = 0; + String responseText = userInput; + ChatResponse response = null; + + System.out.printf("\nSTEP %s:\n %s%n", step++, responseText); + + for (String systemPrompt : systemPrompts) { + + // 1. Compose the input using the response from the previous step. + String input = String.format("{%s}\n {%s}", systemPrompt, responseText); + val prompt = new Prompt(input, options); + + // 2. Call the chat client with the new input and get the new response. + response = + Objects.requireNonNull( + cl.prompt(prompt).call().chatResponse(), "Chat response is null in step " + step); + responseText = response.getResult().getOutput().getText(); + + System.out.printf("\nSTEP %s:\n %s%n", step++, responseText); + } + + return response; + } +} 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 17e216c78..fe73b02e5 100644 --- a/sample-code/spring-app/src/main/resources/static/index.html +++ b/sample-code/spring-app/src/main/resources/static/index.html @@ -789,6 +789,26 @@
Orchestration Integration
inquiring about France. + + + +
+
+
🤖 Agentic Workflows
+
+
    +
  • +
    + +
    + Make a call to a simple chain-like agentic workflow. +
    +
    +
From b14d70ca372f9d61e66c80b0416498dd88b7ac2f Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Tue, 17 Jun 2025 10:34:38 +0200 Subject: [PATCH 2/3] Align with docs --- .../app/controllers/SpringAiAgenticWorkflowController.java | 2 +- .../ai/sdk/app/services/SpringAiAgenticWorkflowService.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java index ff1f839aa..32f051c34 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java @@ -23,7 +23,7 @@ public class SpringAiAgenticWorkflowController { Object completion( @Nullable @RequestParam(value = "format", required = false) final String format) { val response = - service.chain("I want to do a one-day trip to Paris. Help me make an itinerary, please"); + service.runAgent("I want to do a one-day trip to Paris. Help me make an itinerary, please"); if ("json".equals(format)) { return ((OrchestrationSpringChatResponse) response) 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 f5851d48e..cf74b3184 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 @@ -25,7 +25,7 @@ public class SpringAiAgenticWorkflowService { new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); @Nonnull - public ChatResponse chain(String userInput) { + public ChatResponse runAgent(String userInput) { // Configure chat memory val memory = new InMemoryChatMemory(); @@ -54,11 +54,11 @@ public ChatResponse chain(String userInput) { for (String systemPrompt : systemPrompts) { - // 1. Compose the input using the response from the previous step. + // Combine the pre-defined prompt with the previous answer to get the new input String input = String.format("{%s}\n {%s}", systemPrompt, responseText); val prompt = new Prompt(input, options); - // 2. Call the chat client with the new input and get the new response. + // Make a call to the LLM with the new input response = Objects.requireNonNull( cl.prompt(prompt).call().chatResponse(), "Chat response is null in step " + step); From b32b999dd8a27ef0c77f02939c6ff23a3d13e5ea Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Tue, 17 Jun 2025 10:51:49 +0200 Subject: [PATCH 3/3] Codestyle --- .../SpringAiAgenticWorkflowController.java | 1 + .../ai/sdk/app/services/RestaurantMethod.java | 3 ++- .../SpringAiAgenticWorkflowService.java | 26 +++++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java index 32f051c34..ecc5c5a79 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/SpringAiAgenticWorkflowController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +/** Endpoints for the AgenticWorkflow Service */ @SuppressWarnings("unused") @RestController @Slf4j diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java index fb8377c21..4927cdcef 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/RestaurantMethod.java @@ -4,6 +4,7 @@ import java.util.Locale; import java.util.Map; import javax.annotation.Nonnull; +import lombok.val; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; @@ -29,7 +30,7 @@ record Response(List restaurants) {} @Tool(description = "Get recommended restaurants for a location") static RestaurantMethod.Response getRestaurants( @ToolParam @Nonnull final RestaurantMethod.Request request) { - var recommendations = + val recommendations = Map.of( "paris", List.of("Le Comptoir du Relais", "L'As du Fallafel", "Breizh Café"), 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 cf74b3184..08c354bce 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 @@ -8,6 +8,7 @@ import java.util.List; import java.util.Objects; import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; import lombok.val; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; @@ -18,14 +19,23 @@ import org.springframework.ai.tool.ToolCallbacks; import org.springframework.stereotype.Service; +/** Service class for the AgenticWorkflow service */ @Service +@Slf4j public class SpringAiAgenticWorkflowService { private final ChatModel client = new OrchestrationChatModel(); private final OrchestrationModuleConfig config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); + /** + * Simple agentic workflow using chain-like structure. The agent is generating a travel itinerary + * for a given city. + * + * @param userInput the user input including the target city + * @return a short travel itinerary + */ @Nonnull - public ChatResponse runAgent(String userInput) { + public ChatResponse runAgent(@Nonnull final String userInput) { // Configure chat memory val memory = new InMemoryChatMemory(); @@ -39,32 +49,26 @@ public ChatResponse runAgent(String userInput) { options.setInternalToolExecutionEnabled(true); // Prompts for the chain workflow - List systemPrompts = + final List systemPrompts = List.of( "You are a traveling planning agent for a single day trip. Where appropriate, use the provided tools. First, start by suggesting some restaurants for the mentioned city.", "Now, check the whether for the city.", "Finally, combine the suggested itinerary from this conversation into a short, one-sentence plan for the day trip."); // Perform the chain workflow - int step = 0; String responseText = userInput; ChatResponse response = null; - System.out.printf("\nSTEP %s:\n %s%n", step++, responseText); - - for (String systemPrompt : systemPrompts) { + for (final String systemPrompt : systemPrompts) { // Combine the pre-defined prompt with the previous answer to get the new input - String input = String.format("{%s}\n {%s}", systemPrompt, responseText); + val input = String.format("{%s}\n {%s}", systemPrompt, responseText); val prompt = new Prompt(input, options); // Make a call to the LLM with the new input response = - Objects.requireNonNull( - cl.prompt(prompt).call().chatResponse(), "Chat response is null in step " + step); + Objects.requireNonNull(cl.prompt(prompt).call().chatResponse(), "Chat response is null."); responseText = response.getResult().getOutput().getText(); - - System.out.printf("\nSTEP %s:\n %s%n", step++, responseText); } return response;