Skip to content

feat: [OpenAI] Sample code for Agentic Workflow tutorial #466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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;

/** Endpoints for the AgenticWorkflow Service */
@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.runAgent("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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.sap.ai.sdk.app.services;

import java.util.List;
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;

/** 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<String> restaurants) {}

@Nonnull
@SuppressWarnings("unused")
@Tool(description = "Get recommended restaurants for a location")
static RestaurantMethod.Response getRestaurants(
@ToolParam @Nonnull final RestaurantMethod.Request request) {
val 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.")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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.extern.slf4j.Slf4j;
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 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(@Nonnull final 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
final List<String> 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
String responseText = userInput;
ChatResponse response = null;

for (final String systemPrompt : systemPrompts) {

// Combine the pre-defined prompt with the previous answer to get the new input
val input = String.format("{%s}\n {%s}", systemPrompt, responseText);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Preference)

Looks like here we are mixing system prompt with user prompt, which should not be encouraged. I am assuming this is a simplification for the sake of demonstration. But, I would prefer if we distinguish the usage of user and system message correctly.

That said, SpringAi docs also seems to simply concatenate the messages, which is sus to me. Let me know if I missed something.

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.");
responseText = response.getResult().getOutput().getText();
}

return response;
}
}
20 changes: 20 additions & 0 deletions sample-code/spring-app/src/main/resources/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,26 @@ <h5 class="mb-1">Orchestration Integration</h5>
inquiring about France.
</div>
</div>
</li>
</ul>
</div>
<div class="card-body">
<div class="d-flex align-items-center">
<h5 class="mb-1">🤖 Agentic Workflows</h5>
</div>
<ul class="list-group">
<li class="list-group-item">
<div class="info-tooltip">
<button type="submit"
formaction="/spring-ai-agentic/chain"
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
<code>/spring-ai-agentic/chain</code>
</button>
<div class="tooltip-content">
Make a call to a simple chain-like agentic workflow.
</div>
</div>
</li>
</ul>
</div>
<div class="card-body">
Expand Down