Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1662ae7
Tool call full e2e unverified
rpanackal Mar 14, 2025
9ef8e6f
CI
rpanackal Mar 14, 2025
ef9bdc4
Reuse tool
rpanackal Mar 17, 2025
fd6acdc
Remove unnecessary line inserts
rpanackal Mar 17, 2025
a6a209b
Create schema with jackson
rpanackal Mar 24, 2025
9ad7225
Merge remote-tracking branch 'refs/remotes/origin/main' into test/ope…
rpanackal Mar 25, 2025
9a5aec3
First version
rpanackal Mar 26, 2025
275eee5
Formatting
bot-sdk-js Mar 26, 2025
ad9ef65
Testing and javadocs
rpanackal Mar 27, 2025
e70eb29
improve test clarity
rpanackal Mar 27, 2025
a13b183
Merge remote-tracking branch 'refs/remotes/origin/main' into test/ope…
rpanackal Mar 27, 2025
03a14a4
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 27, 2025
bf2ce11
Remove strict tool invocation config
rpanackal Mar 27, 2025
f74a0a7
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 27, 2025
698eab8
Fix maven dependency scope issue
rpanackal Mar 28, 2025
f694f40
Fix maven dependency scope issue + 1
rpanackal Mar 28, 2025
4e388e2
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 31, 2025
78aa10e
Refactor. toolCalls get dedicated field in assistant message
rpanackal Mar 31, 2025
49b8be4
minor fixes
rpanackal Mar 31, 2025
78c2909
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 31, 2025
05d6218
minor fixes
rpanackal Mar 31, 2025
23a247a
Test message list in request externally unmodifiable
rpanackal Mar 31, 2025
4642de0
Test message list in request externally unmodifiable
rpanackal Mar 31, 2025
96647f5
minor javadoc update
rpanackal Mar 31, 2025
f8ee9d4
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/ope…
rpanackal Mar 31, 2025
bf7d86a
Remove sample app changes
rpanackal Mar 31, 2025
1a6d21a
All e2e test code
rpanackal Mar 31, 2025
26b7dae
@beta annotation
rpanackal Mar 31, 2025
f616263
Update foundation-models/openai/src/main/java/com/sap/ai/sdk/foundati…
rpanackal Apr 1, 2025
cd11f37
Apply suggestions from code review
rpanackal Apr 1, 2025
b95b960
update getMessage method impl readability
rpanackal Apr 1, 2025
9062e3a
Move test to generated client test class
rpanackal Apr 1, 2025
aa04e61
Merge branch 'refs/heads/feat/openai/tool-call-execute' into test/ope…
rpanackal Apr 1, 2025
69b49ea
Extend tests
rpanackal Apr 1, 2025
d26ecbf
Reduce test
rpanackal Apr 1, 2025
40f1e22
Refactor test
rpanackal Apr 1, 2025
bd61ce2
Formatting
bot-sdk-js Apr 1, 2025
d440c28
import statement
rpanackal Apr 1, 2025
c6969f4
Merge branch 'refs/heads/main' into feat/openai/tool-call-execute
rpanackal Apr 1, 2025
a36bef0
Merge branch 'feat/openai/tool-call-execute' of https://github.com/SA…
rpanackal Apr 1, 2025
3e2efdc
Merge branch 'refs/heads/feat/openai/tool-call-execute' into test/ope…
rpanackal Apr 1, 2025
a27294f
refactor: remove unused chatCompletionTools method and
rpanackal Apr 2, 2025
c752e52
pom dependency order and controller method params
rpanackal Apr 3, 2025
750e73b
pom dependency order and controller method params
rpanackal Apr 3, 2025
0e22166
update index file
rpanackal Apr 3, 2025
00f93cc
Merge remote-tracking branch 'refs/remotes/origin/main' into test/ope…
rpanackal Apr 3, 2025
4aa87e6
Improve sample code
rpanackal Apr 3, 2025
ddae356
Move the service class out of test
rpanackal Apr 3, 2025
f1c6c50
PMD
rpanackal Apr 3, 2025
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
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<mockito.version>5.16.1</mockito.version>
<javaparser.version>3.26.3</javaparser.version>
<jsonschema-generator.version>4.38.0</jsonschema-generator.version>
<jackson.version>2.18.3</jackson.version>
<!-- conflicts resolution -->
<micrometer.version>1.14.2</micrometer.version>
<json.version>20250107</json.version>
Expand Down Expand Up @@ -132,7 +133,12 @@
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.18.3</version>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jsonSchema</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
Expand Down
16 changes: 12 additions & 4 deletions sample-code/spring-app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>cloudplatform-core</artifactId>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>cloudplatform-connectivity</artifactId>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk.datamodel</groupId>
<artifactId>openapi-core</artifactId>
Expand Down Expand Up @@ -131,6 +135,14 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jsonSchema</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- scope "runtime" -->
<dependency>
<groupId>ch.qos.logback</groupId>
Expand Down Expand Up @@ -177,10 +189,6 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>cloudplatform-connectivity</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,13 @@ Object chatCompletionImage(
return response.getChoices().get(0).getMessage();
}

@GetMapping("/chatCompletionTool")
@GetMapping("/chatCompletionToolExecution")
@Nonnull
Object chatCompletionTools(
@Nullable @RequestParam(value = "format", required = false) final String format) {
final var response = service.chatCompletionTools(12);
Object chatCompletionToolExecution(
@Nullable @RequestParam(value = "format", required = false) final String format,
@Nonnull @RequestParam(value = "location", defaultValue = "Dubai") final String location,
@Nonnull @RequestParam(value = "unit", defaultValue = "°C") final String unit) {
final var response = service.chatCompletionToolExecution(location, unit);
if ("json".equals(format)) {
return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.TEXT_EMBEDDING_3_SMALL;
import static com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionTool.ToolType.FUNCTION;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
import com.sap.ai.sdk.core.AiCoreService;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta;
Expand All @@ -13,8 +18,10 @@
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionParameters;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionTool;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatMessage;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatToolCall;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingOutput;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingParameters;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
Expand All @@ -26,6 +33,7 @@
@Service
@Slf4j
public class OpenAiService {
private static final ObjectMapper JACKSON = new ObjectMapper();

/**
* Chat request to OpenAI
Expand Down Expand Up @@ -86,30 +94,79 @@ public OpenAiChatCompletionOutput chatCompletionImage(@Nonnull final String link
}

/**
* Chat request to OpenAI with a tool.
* Executes a chat completion request to OpenAI with a tool that calculates the weather.
*
* @param months The number of months to be inferred in the tool
* @return the assistant message response
* @param location The location to get the weather for.
* @param unit The unit of temperature to use.
* @return The assistant message response.
*/
@Nonnull
public OpenAiChatCompletionOutput chatCompletionTools(final int months) {
final var question =
"A pair of rabbits is placed in a field. Each month, every pair produces one new pair, starting from the second month. How many rabbits will there be after %s months?"
.formatted(months);
final var par = Map.of("type", "object", "properties", Map.of("N", Map.of("type", "integer")));
public OpenAiChatCompletionOutput chatCompletionToolExecution(
@Nonnull final String location, @Nonnull final String unit) {

// 1. Define the function
final Map<String, Object> schemaMap = generateSchema(WeatherMethod.Request.class);
final var function =
new OpenAiChatCompletionFunction()
.setName("fibonacci")
.setDescription("Calculate the Fibonacci number for given sequence index.")
.setParameters(par);
.setName("weather")
.setDescription("Get the weather for the given location")
.setParameters(schemaMap);
final var tool = new OpenAiChatCompletionTool().setType(FUNCTION).setFunction(function);

final var messages = new ArrayList<OpenAiChatMessage>();
messages.add(
new OpenAiChatMessage.OpenAiChatUserMessage()
.addText("What's the weather in %s in %s?".formatted(location, unit)));

// Assistant will call the function
final var request =
new OpenAiChatCompletionParameters()
.addMessages(new OpenAiChatMessage.OpenAiChatUserMessage().addText(question))
.setTools(List.of(tool))
.setToolChoiceFunction("fibonacci");
.addMessages(messages.toArray(OpenAiChatMessage[]::new))
.setTools(List.of(tool));

final OpenAiChatCompletionOutput response =
OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(request);

// 2. Optionally, execute the function.
final OpenAiChatToolCall toolCall =
response.getChoices().get(0).getMessage().getToolCalls().get(0);
final WeatherMethod.Request arguments =
parseJson(toolCall.getFunction().getArguments(), WeatherMethod.Request.class);
final WeatherMethod.Response currentWeather = WeatherMethod.getCurrentWeather(arguments);

final OpenAiChatMessage.OpenAiChatAssistantMessage assistantMessage =
response.getChoices().get(0).getMessage();
messages.add(assistantMessage);

final var toolMessage =
new OpenAiChatMessage.OpenAiChatToolMessage()
.setToolCallId(toolCall.getId())
.setContent(currentWeather.toString());
messages.add(toolMessage);

final var finalRequest =
new OpenAiChatCompletionParameters()
.addMessages(messages.toArray(OpenAiChatMessage[]::new));

return OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(finalRequest);
}

private static <T> T parseJson(@Nonnull final String rawJson, @Nonnull final Class<T> clazz) {
try {
return JACKSON.readValue(rawJson, clazz);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse tool call arguments: " + rawJson, e);
}
}

return OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(request);
private static Map<String, Object> generateSchema(@Nonnull final Class<?> clazz) {
final var jsonSchemaGenerator = new JsonSchemaGenerator(JACKSON);
try {
final var schema = jsonSchemaGenerator.generateSchema(clazz);
return JACKSON.convertValue(schema, new TypeReference<>() {});
} catch (JsonMappingException e) {
throw new IllegalArgumentException("Could not generate schema for " + clazz.getName(), e);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,26 @@
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.TEXT_EMBEDDING_3_SMALL;
import static com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionTool.TypeEnum.FUNCTION;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
import com.sap.ai.sdk.core.AiCoreService;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiAssistantMessage;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionDelta;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionRequest;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionResponse;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiEmbeddingRequest;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiEmbeddingResponse;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiFunctionCall;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiImageItem;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiMessage;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiToolChoice;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiToolCall;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionTool;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.FunctionObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
Expand All @@ -28,6 +36,7 @@
@Service
@Slf4j
public class OpenAiServiceV2 {
private static final ObjectMapper JACKSON = new ObjectMapper();

/**
* Chat request to OpenAI
Expand Down Expand Up @@ -84,30 +93,71 @@ public OpenAiChatCompletionResponse chatCompletionImage(@Nonnull final String li
}

/**
* Chat request to OpenAI with a tool.
* Executes a chat completion request to OpenAI with a tool that calculates the weather.
*
* @param months The number of months to be inferred in the tool
* @return the assistant message response
* @param location The location to get the weather for.
* @param unit The unit of temperature to use.
* @return The assistant message response.
*/
@Nonnull
public OpenAiChatCompletionResponse chatCompletionTools(final int months) {
public OpenAiChatCompletionResponse chatCompletionToolExecution(
@Nonnull final String location, @Nonnull final String unit) {

// 1. Define the function
final Map<String, Object> schemaMap = generateSchema(WeatherMethod.Request.class);
final var function =
new FunctionObject()
.name("fibonacci")
.description("Calculate the Fibonacci number for given sequence index.")
.parameters(
Map.of("type", "object", "properties", Map.of("N", Map.of("type", "integer"))));

.name("weather")
.description("Get the weather for the given location")
.parameters(schemaMap);
final var tool = new ChatCompletionTool().type(FUNCTION).function(function);

final var request =
new OpenAiChatCompletionRequest(
"A pair of rabbits is placed in a field. Each month, every pair produces one new pair, starting from the second month. How many rabbits will there be after %s months?"
.formatted(months))
.withTools(List.of(tool))
.withToolChoice(OpenAiToolChoice.function("fibonacci"));
final var messages = new ArrayList<OpenAiMessage>();
messages.add(OpenAiMessage.user("What's the weather in %s in %s?".formatted(location, unit)));

// Assistant will call the function
final var request = new OpenAiChatCompletionRequest(messages).withTools(List.of(tool));
final OpenAiChatCompletionResponse response =
OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(request);

// 2. Optionally, execute the function.
final OpenAiAssistantMessage assistantMessage = response.getMessage();
messages.add(assistantMessage);

final OpenAiToolCall toolCall = assistantMessage.toolCalls().get(0);
if (!(toolCall instanceof OpenAiFunctionCall functionCall)) {
throw new IllegalArgumentException(
"Expected a function call, but got: %s".formatted(assistantMessage));
}

final WeatherMethod.Request arguments =
parseJson(functionCall.getArguments(), WeatherMethod.Request.class);
final WeatherMethod.Response weatherMethod = WeatherMethod.getCurrentWeather(arguments);

return OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(request);
messages.add(OpenAiMessage.tool(weatherMethod.toString(), functionCall.getId()));

// Send back the results, and the model will incorporate them into its final response.
return OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(request.withMessages(messages));
}

@Nonnull
private static <T> T parseJson(@Nonnull final String rawJson, @Nonnull final Class<T> clazz) {
try {
return JACKSON.readValue(rawJson, clazz);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse tool call arguments: " + rawJson, e);
}
}

@Nonnull
private static Map<String, Object> generateSchema(@Nonnull final Class<?> clazz) {
final var jsonSchemaGenerator = new JsonSchemaGenerator(JACKSON);
try {
final var schema = jsonSchemaGenerator.generateSchema(clazz);
return JACKSON.convertValue(schema, new TypeReference<>() {});
} catch (JsonMappingException e) {
throw new IllegalArgumentException("Could not generate schema for " + clazz.getName(), e);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ record Response(double temp, Unit unit) {}
@Nonnull
@SuppressWarnings("unused")
@Tool(description = "Get the weather in location")
Response getCurrentWeather(@ToolParam @Nonnull final Request request) {
static Response getCurrentWeather(@ToolParam @Nonnull final Request request) {
final int temperature = request.location.hashCode() % 30;
return new Response(temperature, request.unit);
}
Expand Down
6 changes: 3 additions & 3 deletions sample-code/spring-app/src/main/resources/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -569,12 +569,12 @@ <h5 class="mb-1">OpenAI</h5>
</li>
<li class="list-group-item">
<div class="info-tooltip">
<button type="submit" formaction="/chatCompletionTool"
<button type="submit" formaction="/chatCompletionToolExecution"
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
<code>/chatCompletionTool</code>
<code>/chatCompletionToolExecution</code>
</button>
<div class="tooltip-content">
Chat request to OpenAI with a tool.
Chat request to OpenAI with an executed tool call.
</div>
</div>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,6 @@ void streamChatCompletion() {
assertThat(totalOutput.getChoices().get(0).getContentFilterResults()).isNotNull();
}

@Test
void chatCompletionTools() {
final var completion = service.chatCompletionTools(12);

final var message = completion.getChoices().get(0).getMessage();
assertThat(message.getRole()).isEqualTo("assistant");
assertThat(message.getToolCalls()).isNotNull();
assertThat(message.getToolCalls().get(0).getFunction().getName()).isEqualTo("fibonacci");
}

@Test
void embedding() {
final var embedding = service.embedding("Hello world");
Expand All @@ -101,4 +91,13 @@ void chatCompletionWithResource() {
assertThat(message.getRole()).isEqualTo("assistant");
assertThat(message.getContent()).isNotEmpty();
}

@Test
void chatCompletionToolExecution() {
final var completion = service.chatCompletionToolExecution("Dubai", "°C");

String content = completion.getContent();

assertThat(content).contains("°C");
}
}
Loading