Skip to content

Commit 6b40faf

Browse files
Jonas-Isrnewtork
andauthored
feat: [OpenAI] Sample code for Agentic Workflow tutorial (#466)
* first draft * Align with docs * Codestyle * Fix tool callbacks; Update SpringAi API usage; Add integration test --------- Co-authored-by: Alexander Dümont <[email protected]> Co-authored-by: Alexander Dümont <[email protected]>
1 parent b9150cb commit 6b40faf

File tree

6 files changed

+206
-1
lines changed

6 files changed

+206
-1
lines changed

orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,10 @@ public <T extends ChatOptions> T copy() {
167167
.withLlmConfig(config.getLlmConfig())
168168
.withMaskingConfig(config.getMaskingConfig())
169169
.withGroundingConfig(config.getGroundingConfig());
170-
return (T) new OrchestrationChatOptions(copyConfig);
170+
val result = new OrchestrationChatOptions(copyConfig);
171+
result.setToolCallbacks(toolCallbacks);
172+
result.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
173+
return (T) result;
171174
}
172175

173176
@SuppressWarnings("unchecked")
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.sap.ai.sdk.app.controllers;
2+
3+
import com.sap.ai.sdk.app.services.SpringAiAgenticWorkflowService;
4+
import com.sap.ai.sdk.orchestration.spring.OrchestrationSpringChatResponse;
5+
import javax.annotation.Nullable;
6+
import lombok.extern.slf4j.Slf4j;
7+
import lombok.val;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
/** Endpoints for the AgenticWorkflow Service */
15+
@SuppressWarnings("unused")
16+
@RestController
17+
@Slf4j
18+
@RequestMapping("/spring-ai-agentic")
19+
public class SpringAiAgenticWorkflowController {
20+
21+
@Autowired private SpringAiAgenticWorkflowService service;
22+
23+
@GetMapping("/chain")
24+
Object completion(
25+
@Nullable @RequestParam(value = "format", required = false) final String format) {
26+
val response =
27+
service.runAgent("I want to do a one-day trip to Paris. Help me make an itinerary, please");
28+
29+
if ("json".equals(format)) {
30+
return ((OrchestrationSpringChatResponse) response)
31+
.getOrchestrationResponse()
32+
.getOriginalResponse();
33+
}
34+
return response.getResult().getOutput().getText();
35+
}
36+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.sap.ai.sdk.app.services;
2+
3+
import java.util.List;
4+
import java.util.Locale;
5+
import java.util.Map;
6+
import javax.annotation.Nonnull;
7+
import lombok.val;
8+
import org.springframework.ai.tool.annotation.Tool;
9+
import org.springframework.ai.tool.annotation.ToolParam;
10+
11+
/** Mock tool for agentic workflow */
12+
class RestaurantMethod {
13+
14+
/**
15+
* Request for list of restaurants
16+
*
17+
* @param location the city
18+
*/
19+
record Request(String location) {}
20+
21+
/**
22+
* Response for restaurant recommendations
23+
*
24+
* @param restaurants the list of restaurants
25+
*/
26+
record Response(List<String> restaurants) {}
27+
28+
@Nonnull
29+
@SuppressWarnings("unused")
30+
@Tool(description = "Get recommended restaurants for a location")
31+
static RestaurantMethod.Response getRestaurants(
32+
@ToolParam @Nonnull final RestaurantMethod.Request request) {
33+
val recommendations =
34+
Map.of(
35+
"paris",
36+
List.of("Le Comptoir du Relais", "L'As du Fallafel", "Breizh Café"),
37+
"reykjavik",
38+
List.of("Dill Restaurant", "Fish Market", "Grillmarkaðurinn"));
39+
return new RestaurantMethod.Response(
40+
recommendations.getOrDefault(
41+
request.location.toLowerCase(Locale.ROOT),
42+
List.of("No recommendations for this city.")));
43+
}
44+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.sap.ai.sdk.app.services;
2+
3+
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI;
4+
5+
import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig;
6+
import com.sap.ai.sdk.orchestration.spring.OrchestrationChatModel;
7+
import com.sap.ai.sdk.orchestration.spring.OrchestrationChatOptions;
8+
import java.util.List;
9+
import java.util.Objects;
10+
import javax.annotation.Nonnull;
11+
import lombok.extern.slf4j.Slf4j;
12+
import lombok.val;
13+
import org.springframework.ai.chat.client.ChatClient;
14+
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
15+
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
16+
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
17+
import org.springframework.ai.chat.model.ChatModel;
18+
import org.springframework.ai.chat.model.ChatResponse;
19+
import org.springframework.ai.chat.prompt.Prompt;
20+
import org.springframework.ai.support.ToolCallbacks;
21+
import org.springframework.stereotype.Service;
22+
23+
/** Service class for the AgenticWorkflow service */
24+
@Service
25+
@Slf4j
26+
public class SpringAiAgenticWorkflowService {
27+
private final ChatModel client = new OrchestrationChatModel();
28+
private final OrchestrationModuleConfig config =
29+
new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI);
30+
31+
/**
32+
* Simple agentic workflow using chain-like structure. The agent is generating a travel itinerary
33+
* for a given city.
34+
*
35+
* @param userInput the user input including the target city
36+
* @return a short travel itinerary
37+
*/
38+
@Nonnull
39+
public ChatResponse runAgent(@Nonnull final String userInput) {
40+
41+
// Configure chat memory
42+
val repository = new InMemoryChatMemoryRepository();
43+
val memory = MessageWindowChatMemory.builder().chatMemoryRepository(repository).build();
44+
val advisor = MessageChatMemoryAdvisor.builder(memory).build();
45+
val cl = ChatClient.builder(client).defaultAdvisors(advisor).build();
46+
47+
// Add (mocked) tools
48+
val options = new OrchestrationChatOptions(config);
49+
options.setToolCallbacks(
50+
List.of(ToolCallbacks.from(new WeatherMethod(), new RestaurantMethod())));
51+
options.setInternalToolExecutionEnabled(true);
52+
53+
// Prompts for the chain workflow
54+
final List<String> systemPrompts =
55+
List.of(
56+
"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.",
57+
"Now, check the whether for the city.",
58+
"Finally, combine the suggested itinerary from this conversation into a short, one-sentence plan for the day trip.");
59+
60+
// Perform the chain workflow
61+
String responseText = userInput;
62+
ChatResponse response = null;
63+
64+
for (final String systemPrompt : systemPrompts) {
65+
66+
// Combine the pre-defined prompt with the previous answer to get the new input
67+
val input = String.format("{%s}\n {%s}", systemPrompt, responseText);
68+
val prompt = new Prompt(input, options);
69+
70+
// Make a call to the LLM with the new input
71+
response =
72+
Objects.requireNonNull(cl.prompt(prompt).call().chatResponse(), "Chat response is null.");
73+
responseText = response.getResult().getOutput().getText();
74+
}
75+
76+
return response;
77+
}
78+
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,26 @@ <h5 class="mb-1">Orchestration Integration</h5>
801801
inquiring about France.
802802
</div>
803803
</div>
804+
</li>
805+
</ul>
806+
</div>
807+
<div class="card-body">
808+
<div class="d-flex align-items-center">
809+
<h5 class="mb-1">🤖 Agentic Workflows</h5>
810+
</div>
811+
<ul class="list-group">
812+
<li class="list-group-item">
813+
<div class="info-tooltip">
814+
<button type="submit"
815+
formaction="/spring-ai-agentic/chain"
816+
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
817+
<code>/spring-ai-agentic/chain</code>
818+
</button>
819+
<div class="tooltip-content">
820+
Make a call to a simple chain-like agentic workflow.
821+
</div>
822+
</div>
823+
</li>
804824
</ul>
805825
</div>
806826
<div class="card-body">
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.sap.ai.sdk.app.controllers;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.sap.ai.sdk.app.services.SpringAiAgenticWorkflowService;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.junit.jupiter.api.Test;
8+
9+
@Slf4j
10+
public class SpringAiAgenticWorkflowTest {
11+
12+
SpringAiAgenticWorkflowService service = new SpringAiAgenticWorkflowService();
13+
14+
@Test
15+
void testCompletion() {
16+
var response = service.runAgent("I want to visit Paris for a day. What can I do there?");
17+
assertThat(response).isNotNull();
18+
assertThat(response.getResult().getOutput().getText())
19+
.contains("Le Comptoir du Relais")
20+
.contains("L'As du Fallafel")
21+
.contains("Breizh Café")
22+
.contains("1°C");
23+
}
24+
}

0 commit comments

Comments
 (0)