feat(vertex-ai): Refactor Gemini for new tool calling API

- Migrate from function calling to tool calling API
- Add support for Gemini 2.0 models (flash, flash-lite)
- Implement JSON schema to OpenAPI schema conversion
- Add builder pattern for improved configuration
- Deprecate legacy function calling constructors and methods
- Update default model to GEMINI_2_0_FLASH
- Add comprehensive test coverage for tool calling
- Upgrade victools dependency to 4.37.0
- Update the Vertex Tool calling docs

Part of the #2207 epic

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov
2025-02-13 00:46:52 +01:00
committed by Mark Pollack
parent 4d692a542b
commit 710bb05f19
28 changed files with 1850 additions and 541 deletions

View File

@@ -16,7 +16,8 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
@@ -53,6 +54,17 @@
<dependencies>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
<version>${victools.version}</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
<version>${victools.version}</version>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-vertexai</artifactId>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,10 +18,8 @@ package org.springframework.ai.vertexai.gemini;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -47,7 +45,10 @@ import com.google.protobuf.util.JsonFormat;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
@@ -75,9 +76,15 @@ import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.LegacyToolCallingManager;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.ToolExecutionResult;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.vertexai.gemini.common.VertexAiGeminiConstants;
import org.springframework.ai.vertexai.gemini.common.VertexAiGeminiSafetySetting;
import org.springframework.ai.vertexai.gemini.schema.VertexToolCallingManager;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.lang.NonNull;
import org.springframework.retry.support.RetryTemplate;
@@ -102,6 +109,10 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
private static final ToolCallingManager DEFAULT_TOOL_CALLING_MANAGER = ToolCallingManager.builder().build();
private final Logger logger = LoggerFactory.getLogger(getClass());
private final VertexAI vertexAI;
private final VertexAiGeminiChatOptions defaultOptions;
@@ -118,29 +129,54 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
*/
private final ObservationRegistry observationRegistry;
/**
* Tool calling manager used to call tools.
*/
private final ToolCallingManager toolCallingManager;
/**
* Conventions to use for generating observations.
*/
private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
/**
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
*/
@Deprecated
public VertexAiGeminiChatModel(VertexAI vertexAI) {
this(vertexAI, VertexAiGeminiChatOptions.builder().model(ChatModel.GEMINI_1_5_PRO).temperature(0.8).build());
}
/**
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
*/
@Deprecated
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options) {
this(vertexAI, options, null);
}
/**
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
*/
@Deprecated
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
FunctionCallbackResolver functionCallbackResolver) {
this(vertexAI, options, functionCallbackResolver, List.of());
}
/**
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
*/
@Deprecated
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks) {
this(vertexAI, options, functionCallbackResolver, toolFunctionCallbacks, RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
/**
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
*/
@Deprecated
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks,
RetryTemplate retryTemplate) {
@@ -148,22 +184,49 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
ObservationRegistry.NOOP);
}
/**
* @deprecated Use {@link VertexAiGeminiChatModel.Builder}.
*/
@Deprecated
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options,
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks,
RetryTemplate retryTemplate, ObservationRegistry observationRegistry) {
super(functionCallbackResolver, options, toolFunctionCallbacks);
this(vertexAI, options,
LegacyToolCallingManager.builder()
.functionCallbackResolver(functionCallbackResolver)
.functionCallbacks(toolFunctionCallbacks)
.build(),
retryTemplate, observationRegistry);
logger.warn("This constructor is deprecated and will be removed in the next milestone. "
+ "Please use the new constructor accepting ToolCallingManager instead.");
}
public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions defaultOptions,
ToolCallingManager toolCallingManager, RetryTemplate retryTemplate,
ObservationRegistry observationRegistry) {
super(null, VertexAiGeminiChatOptions.builder().build(), List.of());
Assert.notNull(vertexAI, "VertexAI must not be null");
Assert.notNull(options, "VertexAiGeminiChatOptions must not be null");
Assert.notNull(options.getModel(), "VertexAiGeminiChatOptions.modelName must not be null");
Assert.notNull(defaultOptions, "VertexAiGeminiChatOptions must not be null");
Assert.notNull(defaultOptions.getModel(), "VertexAiGeminiChatOptions.modelName must not be null");
Assert.notNull(retryTemplate, "RetryTemplate must not be null");
Assert.notNull(toolCallingManager, "ToolCallingManager must not be null");
this.vertexAI = vertexAI;
this.defaultOptions = options;
this.generationConfig = toGenerationConfig(options);
this.defaultOptions = defaultOptions;
this.generationConfig = toGenerationConfig(defaultOptions);
this.retryTemplate = retryTemplate;
this.observationRegistry = observationRegistry;
if (toolCallingManager instanceof VertexToolCallingManager) {
this.toolCallingManager = toolCallingManager;
}
else {
this.toolCallingManager = new VertexToolCallingManager(toolCallingManager);
}
}
private static GeminiMessageType toGeminiMessageType(@NonNull MessageType type) {
@@ -287,13 +350,16 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
// https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini
@Override
public ChatResponse call(Prompt prompt) {
var requestPrompt = this.buildRequestPrompt(prompt);
return this.internalCall(requestPrompt);
}
VertexAiGeminiChatOptions vertexAiGeminiChatOptions = vertexAiGeminiChatOptions(prompt);
private ChatResponse internalCall(Prompt prompt) {
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(VertexAiGeminiConstants.PROVIDER_NAME)
.requestOptions(vertexAiGeminiChatOptions)
.requestOptions(prompt.getOptions())
.build();
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
@@ -301,7 +367,7 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
this.observationRegistry)
.observe(() -> this.retryTemplate.execute(context -> {
var geminiRequest = createGeminiRequest(prompt, vertexAiGeminiChatOptions);
var geminiRequest = createGeminiRequest(prompt);
GenerateContentResponse generateContentResponse = this.getContentResponse(geminiRequest);
@@ -318,26 +384,94 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
return chatResponse;
}));
if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(FinishReason.STOP.name()))) {
var toolCallConversation = handleToolCalls(prompt, response);
// Recursively call the call method with the tool call message
// conversation that contains the call responses.
return this.call(new Prompt(toolCallConversation, prompt.getOptions()));
if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) && response != null
&& response.hasToolCalls()) {
var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);
if (toolExecutionResult.returnDirect()) {
// Return tool execution result directly to the client.
return ChatResponse.builder()
.from(response)
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
.build();
}
else {
// Send the tool execution result back to the model.
return this.internalCall(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()));
}
}
return response;
}
Prompt buildRequestPrompt(Prompt prompt) {
// Process runtime options
VertexAiGeminiChatOptions runtimeOptions = null;
if (prompt.getOptions() != null) {
if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {
runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class,
VertexAiGeminiChatOptions.class);
}
else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
runtimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions, FunctionCallingOptions.class,
VertexAiGeminiChatOptions.class);
}
else {
runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
VertexAiGeminiChatOptions.class);
}
}
// Define request options by merging runtime options and default options
VertexAiGeminiChatOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
VertexAiGeminiChatOptions.class);
// Merge @JsonIgnore-annotated options explicitly since they are ignored by
// Jackson, used by ModelOptionsUtils.
if (runtimeOptions != null) {
requestOptions.setInternalToolExecutionEnabled(
ModelOptionsUtils.mergeOption(runtimeOptions.isInternalToolExecutionEnabled(),
this.defaultOptions.isInternalToolExecutionEnabled()));
requestOptions.setToolNames(ToolCallingChatOptions.mergeToolNames(runtimeOptions.getToolNames(),
this.defaultOptions.getToolNames()));
requestOptions.setToolCallbacks(ToolCallingChatOptions.mergeToolCallbacks(runtimeOptions.getToolCallbacks(),
this.defaultOptions.getToolCallbacks()));
requestOptions.setToolContext(ToolCallingChatOptions.mergeToolContext(runtimeOptions.getToolContext(),
this.defaultOptions.getToolContext()));
requestOptions.setGoogleSearchRetrieval(ModelOptionsUtils.mergeOption(
runtimeOptions.getGoogleSearchRetrieval(), this.defaultOptions.getGoogleSearchRetrieval()));
requestOptions.setSafetySettings(ModelOptionsUtils.mergeOption(runtimeOptions.getSafetySettings(),
this.defaultOptions.getSafetySettings()));
}
else {
requestOptions.setInternalToolExecutionEnabled(this.defaultOptions.isInternalToolExecutionEnabled());
requestOptions.setToolNames(this.defaultOptions.getToolNames());
requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());
requestOptions.setToolContext(this.defaultOptions.getToolContext());
requestOptions.setGoogleSearchRetrieval(this.defaultOptions.getGoogleSearchRetrieval());
requestOptions.setSafetySettings(this.defaultOptions.getSafetySettings());
}
ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks());
return new Prompt(prompt.getInstructions(), requestOptions);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
var requestPrompt = this.buildRequestPrompt(prompt);
return this.internalStream(requestPrompt);
}
public Flux<ChatResponse> internalStream(Prompt prompt) {
return Flux.deferContextual(contextView -> {
VertexAiGeminiChatOptions vertexAiGeminiChatOptions = vertexAiGeminiChatOptions(prompt);
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(VertexAiGeminiConstants.PROVIDER_NAME)
.requestOptions(vertexAiGeminiChatOptions)
.requestOptions(prompt.getOptions())
.build();
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
@@ -345,41 +479,56 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
this.observationRegistry);
observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
var request = createGeminiRequest(prompt, vertexAiGeminiChatOptions);
var request = createGeminiRequest(prompt);
try {
ResponseStream<GenerateContentResponse> responseStream = request.model
.generateContentStream(request.contents);
return Flux.fromStream(responseStream.stream()).switchMap(response -> {
Flux<ChatResponse> chatResponse1 = Flux.fromStream(responseStream.stream())
.switchMap(response2 -> Mono.just(response2).map(response -> {
List<Generation> generations = response.getCandidatesList()
.stream()
.map(this::responseCandidateToGeneration)
.flatMap(List::stream)
.toList();
List<Generation> generations = response.getCandidatesList()
.stream()
.map(this::responseCandidateToGeneration)
.flatMap(List::stream)
.toList();
ChatResponse chatResponse = new ChatResponse(generations, toChatResponseMetadata(response));
return new ChatResponse(generations, toChatResponseMetadata(response));
if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(chatResponse,
Set.of(FinishReason.STOP.name(), FinishReason.FINISH_REASON_UNSPECIFIED.name()))) {
var toolCallConversation = handleToolCalls(prompt, chatResponse);
// Recursively call the stream method with the tool call message
// conversation that contains the call responses.
return this.stream(new Prompt(toolCallConversation, prompt.getOptions()));
}));
// @formatter:off
Flux<ChatResponse> chatResponseFlux = chatResponse1.flatMap(response -> {
if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) && response.hasToolCalls()) {
var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);
if (toolExecutionResult.returnDirect()) {
// Return tool execution result directly to the client.
return Flux.just(ChatResponse.builder().from(response)
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
.build());
} else {
// Send the tool execution result back to the model.
return this.internalStream(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()));
}
}
else {
return Flux.just(response);
}
})
.doOnError(observation::error)
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
// @formatter:on;
Flux<ChatResponse> chatResponseFlux = Flux.just(chatResponse)
.doOnError(observation::error)
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
return new MessageAggregator().aggregate(chatResponseFlux, observationContext::setResponse);
return new MessageAggregator().aggregate(chatResponseFlux, observationContext::setResponse);
});
}
catch (Exception e) {
throw new RuntimeException("Failed to generate content", e);
}
});
}
@@ -448,54 +597,40 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
VertexAiGeminiChatOptions.class);
return updatedRuntimeOptions;
}
/**
* Tests access to the {@link #createGeminiRequest(Prompt, VertexAiGeminiChatOptions)}
* method.
*/
GeminiRequest createGeminiRequest(Prompt prompt, VertexAiGeminiChatOptions updatedRuntimeOptions) {
GeminiRequest createGeminiRequest(Prompt prompt) {
Set<String> functionsForThisRequest = new HashSet<>();
VertexAiGeminiChatOptions requestOptions = (VertexAiGeminiChatOptions) prompt.getOptions();
var generativeModelBuilder = new GenerativeModel.Builder().setVertexAi(this.vertexAI)
.setSafetySettings(toGeminiSafetySettings(requestOptions.getSafetySettings()));
if (requestOptions.getModel() != null) {
generativeModelBuilder.setModelName(requestOptions.getModel());
}
else {
generativeModelBuilder.setModelName(this.defaultOptions.getModel());
}
GenerationConfig generationConfig = this.generationConfig;
var generativeModelBuilder = new GenerativeModel.Builder().setModelName(this.defaultOptions.getModel())
.setVertexAi(this.vertexAI)
.setSafetySettings(toGeminiSafetySettings(this.defaultOptions.getSafetySettings()));
if (prompt.getOptions() != null) {
if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions,
FunctionCallingOptions.class, VertexAiGeminiChatOptions.class);
}
else {
updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
VertexAiGeminiChatOptions.class);
}
functionsForThisRequest.addAll(runtimeFunctionCallbackConfigurations(updatedRuntimeOptions));
}
if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) {
functionsForThisRequest.addAll(this.defaultOptions.getFunctions());
}
if (updatedRuntimeOptions != null) {
if (StringUtils.hasText(updatedRuntimeOptions.getModel())
&& !updatedRuntimeOptions.getModel().equals(this.defaultOptions.getModel())) {
// Override model name
generativeModelBuilder.setModelName(updatedRuntimeOptions.getModel());
}
generationConfig = toGenerationConfig(updatedRuntimeOptions);
if (requestOptions != null) {
generationConfig = toGenerationConfig(requestOptions);
}
// Add the enabled functions definitions to the request's tools parameter.
List<Tool> tools = new ArrayList<>();
if (!CollectionUtils.isEmpty(functionsForThisRequest)) {
tools.addAll(this.getFunctionTools(functionsForThisRequest));
List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);
if (!CollectionUtils.isEmpty(toolDefinitions)) {
final List<FunctionDeclaration> functionDeclarations = toolDefinitions.stream()
.map(toolDefinition -> FunctionDeclaration.newBuilder()
.setName(toolDefinition.name())
.setDescription(toolDefinition.description())
.setParameters(jsonToSchema(toolDefinition.inputSchema()))
.build())
.toList();
tools.add(Tool.newBuilder().addAllFunctionDeclarations(functionDeclarations).build());
}
if (prompt.getOptions() instanceof VertexAiGeminiChatOptions options && options.getGoogleSearchRetrieval()) {
@@ -505,13 +640,13 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
.build();
tools.add(googleSearchRetrievalTool);
}
if (!CollectionUtils.isEmpty(tools)) {
generativeModelBuilder.setTools(tools);
}
if (prompt.getOptions() instanceof VertexAiGeminiChatOptions options
&& !CollectionUtils.isEmpty(options.getSafetySettings())) {
generativeModelBuilder.setSafetySettings(toGeminiSafetySettings(options.getSafetySettings()));
if (!CollectionUtils.isEmpty(requestOptions.getSafetySettings())) {
generativeModelBuilder.setSafetySettings(toGeminiSafetySettings(requestOptions.getSafetySettings()));
}
generativeModelBuilder.setGenerationConfig(generationConfig);
@@ -582,22 +717,6 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
.toList();
}
private List<Tool> getFunctionTools(Set<String> functionNames) {
final var tool = Tool.newBuilder();
final List<FunctionDeclaration> functionDeclarations = this.resolveFunctionCallbacks(functionNames)
.stream()
.map(functionCallback -> FunctionDeclaration.newBuilder()
.setName(functionCallback.getName())
.setDescription(functionCallback.getDescription())
.setParameters(jsonToSchema(functionCallback.getInputTypeSchema()))
.build())
.toList();
tool.addAllFunctionDeclarations(functionDeclarations);
return List.of(tool.build());
}
/**
* Generates the content response based on the provided Gemini request. Package
* protected for testing purposes.
@@ -662,9 +781,15 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
GEMINI_PRO("gemini-pro"),
GEMINI_1_5_PRO("gemini-1.5-pro-001"),
GEMINI_1_5_PRO("gemini-1.5-pro-002"),
GEMINI_1_5_FLASH("gemini-1.5-flash-001");
GEMINI_1_5_FLASH("gemini-1.5-flash-002"),
GEMINI_1_5_FLASH_8B("gemini-1.5-flash-8b-001"),
GEMINI_2_0_FLASH("gemini-2.0-flash"),
GEMINI_2_0_FLASH_LIGHT("gemini-2.0-flash-lite-preview-02-05");
public final String value;
@@ -688,4 +813,95 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private VertexAI vertexAI;
private VertexAiGeminiChatOptions defaultOptions = VertexAiGeminiChatOptions.builder()
.temperature(0.7)
.topP(1.0)
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.build();
private ToolCallingManager toolCallingManager;
private FunctionCallbackResolver functionCallbackResolver;
private List<FunctionCallback> toolFunctionCallbacks;
private RetryTemplate retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE;
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
private Builder() {
}
public Builder vertexAI(VertexAI vertexAI) {
this.vertexAI = vertexAI;
return this;
}
public Builder defaultOptions(VertexAiGeminiChatOptions defaultOptions) {
this.defaultOptions = defaultOptions;
return this;
}
public Builder toolCallingManager(ToolCallingManager toolCallingManager) {
this.toolCallingManager = toolCallingManager;
return this;
}
@Deprecated
public Builder functionCallbackResolver(FunctionCallbackResolver functionCallbackResolver) {
this.functionCallbackResolver = functionCallbackResolver;
return this;
}
@Deprecated
public Builder toolFunctionCallbacks(List<FunctionCallback> toolFunctionCallbacks) {
this.toolFunctionCallbacks = toolFunctionCallbacks;
return this;
}
public Builder retryTemplate(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
return this;
}
public Builder observationRegistry(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
return this;
}
public VertexAiGeminiChatModel build() {
if (toolCallingManager != null) {
Assert.isNull(functionCallbackResolver,
"functionCallbackResolver cannot be set when toolCallingManager is set");
Assert.isNull(toolFunctionCallbacks,
"toolFunctionCallbacks cannot be set when toolCallingManager is set");
return new VertexAiGeminiChatModel(vertexAI, defaultOptions, toolCallingManager, retryTemplate,
observationRegistry);
}
if (functionCallbackResolver != null) {
Assert.isNull(toolCallingManager,
"toolCallingManager cannot be set when functionCallbackResolver is set");
List<FunctionCallback> toolCallbacks = this.toolFunctionCallbacks != null ? this.toolFunctionCallbacks
: List.of();
return new VertexAiGeminiChatModel(vertexAI, defaultOptions, functionCallbackResolver, toolCallbacks,
retryTemplate, observationRegistry);
}
return new VertexAiGeminiChatModel(vertexAI, defaultOptions, DEFAULT_TOOL_CALLING_MANAGER, retryTemplate,
observationRegistry);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,11 +29,12 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel.ChatModel;
import org.springframework.ai.vertexai.gemini.common.VertexAiGeminiSafetySetting;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -46,7 +47,7 @@ import org.springframework.util.Assert;
* @since 1.0.0
*/
@JsonInclude(Include.NON_NULL)
public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
public class VertexAiGeminiChatOptions implements ToolCallingChatOptions {
// https://cloud.google.com/vertex-ai/docs/reference/rest/v1/GenerationConfig
@@ -95,41 +96,37 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
private @JsonProperty("responseMimeType") String responseMimeType;
/**
* Tool Function Callbacks to register with the ChatModel.
* For Prompt Options the functionCallbacks are automatically enabled for the duration of the prompt execution.
* For Default Options the functionCallbacks are registered but disabled by default. Use the enableFunctions to set the functions
* from the registry to be used by the ChatModel chat completion requests.
* Collection of {@link ToolCallback}s to be used for tool calling in the chat
* completion requests.
*/
@JsonIgnore
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
/**
* List of functions, identified by their names, to configure for function calling in
* the chat completion requests.
* Functions with those names must exist in the functionCallbacks registry.
* The {@link #functionCallbacks} from the PromptOptions are automatically enabled for the duration of the prompt execution.
*
* Note that function enabled with the default options are enabled for all chat completion requests. This could impact the token count and the billing.
* If the functions is set in a prompt options, then the enabled functions are only active for the duration of this prompt execution.
* Collection of tool names to be resolved at runtime and used for tool calling in the
* chat completion requests.
*/
@JsonIgnore
private Set<String> functions = new HashSet<>();
private Set<String> toolNames = new HashSet<>();
/**
* Whether to enable the tool execution lifecycle internally in ChatModel.
*/
@JsonIgnore
private Boolean internalToolExecutionEnabled;
@JsonIgnore
private Map<String, Object> toolContext = new HashMap<>();
/**
* Use Google search Grounding feature
*/
@JsonIgnore
private boolean googleSearchRetrieval = false;
private Boolean googleSearchRetrieval = false;
@JsonIgnore
private List<VertexAiGeminiSafetySetting> safetySettings = new ArrayList<>();
@JsonIgnore
private Boolean proxyToolCalls;
@JsonIgnore
private Map<String, Object> toolContext;
public static Builder builder() {
return new Builder();
}
@@ -145,13 +142,13 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
options.setCandidateCount(fromOptions.getCandidateCount());
options.setMaxOutputTokens(fromOptions.getMaxOutputTokens());
options.setModel(fromOptions.getModel());
options.setFunctionCallbacks(fromOptions.getFunctionCallbacks());
options.setToolCallbacks(fromOptions.getToolCallbacks());
options.setResponseMimeType(fromOptions.getResponseMimeType());
options.setFunctions(fromOptions.getFunctions());
options.setToolNames(fromOptions.getToolNames());
options.setResponseMimeType(fromOptions.getResponseMimeType());
options.setGoogleSearchRetrieval(fromOptions.getGoogleSearchRetrieval());
options.setSafetySettings(fromOptions.getSafetySettings());
options.setProxyToolCalls(fromOptions.getProxyToolCalls());
options.setInternalToolExecutionEnabled(fromOptions.isInternalToolExecutionEnabled());
options.setToolContext(fromOptions.getToolContext());
return options;
}
@@ -236,20 +233,67 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
this.responseMimeType = mimeType;
}
@Override
@JsonIgnore
@Deprecated
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
return this.getToolCallbacks();
}
@Override
@JsonIgnore
@Deprecated
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.functionCallbacks = functionCallbacks;
this.setToolCallbacks(functionCallbacks);
}
@Override
public List<FunctionCallback> getToolCallbacks() {
return this.toolCallbacks;
}
@Override
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
this.toolCallbacks = toolCallbacks;
}
@Override
@JsonIgnore
@Deprecated
public Set<String> getFunctions() {
return this.functions;
return this.getToolNames();
}
@JsonIgnore
@Deprecated
public void setFunctions(Set<String> functions) {
this.functions = functions;
this.setToolNames(functions);
}
@Override
public Set<String> getToolNames() {
return this.toolNames;
}
@Override
public void setToolNames(Set<String> toolNames) {
Assert.notNull(toolNames, "toolNames cannot be null");
Assert.noNullElements(toolNames, "toolNames cannot contain null elements");
toolNames.forEach(tool -> Assert.hasText(tool, "toolNames cannot contain empty elements"));
this.toolNames = toolNames;
}
@Override
@Nullable
public Boolean isInternalToolExecutionEnabled() {
return internalToolExecutionEnabled;
}
@Override
public void setInternalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) {
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
}
@Override
@@ -264,11 +308,11 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
return null;
}
public boolean getGoogleSearchRetrieval() {
public Boolean getGoogleSearchRetrieval() {
return this.googleSearchRetrieval;
}
public void setGoogleSearchRetrieval(boolean googleSearchRetrieval) {
public void setGoogleSearchRetrieval(Boolean googleSearchRetrieval) {
this.googleSearchRetrieval = googleSearchRetrieval;
}
@@ -281,13 +325,17 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
this.safetySettings = safetySettings;
}
@Deprecated
@Override
@JsonIgnore
public Boolean getProxyToolCalls() {
return this.proxyToolCalls;
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
}
@Deprecated
@JsonIgnore
public void setProxyToolCalls(Boolean proxyToolCalls) {
this.proxyToolCalls = proxyToolCalls;
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
}
@Override
@@ -314,18 +362,18 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
&& Objects.equals(this.topK, that.topK) && Objects.equals(this.candidateCount, that.candidateCount)
&& Objects.equals(this.maxOutputTokens, that.maxOutputTokens) && Objects.equals(this.model, that.model)
&& Objects.equals(this.responseMimeType, that.responseMimeType)
&& Objects.equals(this.functionCallbacks, that.functionCallbacks)
&& Objects.equals(this.functions, that.functions)
&& Objects.equals(this.toolCallbacks, that.toolCallbacks)
&& Objects.equals(this.toolNames, that.toolNames)
&& Objects.equals(this.safetySettings, that.safetySettings)
&& Objects.equals(this.proxyToolCalls, that.proxyToolCalls)
&& Objects.equals(this.internalToolExecutionEnabled, that.internalToolExecutionEnabled)
&& Objects.equals(this.toolContext, that.toolContext);
}
@Override
public int hashCode() {
return Objects.hash(this.stopSequences, this.temperature, this.topP, this.topK, this.candidateCount,
this.maxOutputTokens, this.model, this.responseMimeType, this.functionCallbacks, this.functions,
this.googleSearchRetrieval, this.safetySettings, this.proxyToolCalls, this.toolContext);
this.maxOutputTokens, this.model, this.responseMimeType, this.toolCallbacks, this.toolNames,
this.googleSearchRetrieval, this.safetySettings, this.internalToolExecutionEnabled, this.toolContext);
}
@Override
@@ -333,9 +381,9 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
return "VertexAiGeminiChatOptions{" + "stopSequences=" + this.stopSequences + ", temperature="
+ this.temperature + ", topP=" + this.topP + ", topK=" + this.topK + ", candidateCount="
+ this.candidateCount + ", maxOutputTokens=" + this.maxOutputTokens + ", model='" + this.model + '\''
+ ", responseMimeType='" + this.responseMimeType + '\'' + ", functionCallbacks="
+ this.functionCallbacks + ", functions=" + this.functions + ", googleSearchRetrieval="
+ this.googleSearchRetrieval + ", safetySettings=" + this.safetySettings + '}';
+ ", responseMimeType='" + this.responseMimeType + '\'' + ", toolCallbacks=" + this.toolCallbacks
+ ", toolNames=" + this.toolNames + ", googleSearchRetrieval=" + this.googleSearchRetrieval
+ ", safetySettings=" + this.safetySettings + '}';
}
@Override
@@ -343,67 +391,6 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
return fromOptions(this);
}
public FunctionCallingOptions merge(ChatOptions options) {
VertexAiGeminiChatOptions.Builder builder = VertexAiGeminiChatOptions.builder();
// Merge chat-specific options
builder.model(options.getModel() != null ? options.getModel() : this.getModel())
.maxOutputTokens(options.getMaxTokens() != null ? options.getMaxTokens() : this.getMaxOutputTokens())
.stopSequences(options.getStopSequences() != null ? options.getStopSequences() : this.getStopSequences())
.temperature(options.getTemperature() != null ? options.getTemperature() : this.getTemperature())
.topP(options.getTopP() != null ? options.getTopP() : this.getTopP())
.topK(options.getTopK() != null ? options.getTopK() : this.getTopK());
// Try to get function-specific properties if options is a FunctionCallingOptions
if (options instanceof FunctionCallingOptions functionOptions) {
builder.proxyToolCalls(functionOptions.getProxyToolCalls() != null ? functionOptions.getProxyToolCalls()
: this.proxyToolCalls);
Set<String> functions = new HashSet<>();
if (this.functions != null) {
functions.addAll(this.functions);
}
if (functionOptions.getFunctions() != null) {
functions.addAll(functionOptions.getFunctions());
}
builder.functions(functions);
List<FunctionCallback> functionCallbacks = new ArrayList<>();
if (this.functionCallbacks != null) {
functionCallbacks.addAll(this.functionCallbacks);
}
if (functionOptions.getFunctionCallbacks() != null) {
functionCallbacks.addAll(functionOptions.getFunctionCallbacks());
}
builder.functionCallbacks(functionCallbacks);
Map<String, Object> context = new HashMap<>();
if (this.toolContext != null) {
context.putAll(this.toolContext);
}
if (functionOptions.getToolContext() != null) {
context.putAll(functionOptions.getToolContext());
}
builder.toolContext(context);
}
else {
// If not a FunctionCallingOptions, preserve current function-specific
// properties
builder.proxyToolCalls(this.proxyToolCalls);
builder.functions(this.functions != null ? new HashSet<>(this.functions) : null);
builder.functionCallbacks(this.functionCallbacks != null ? new ArrayList<>(this.functionCallbacks) : null);
builder.toolContext(this.toolContext != null ? new HashMap<>(this.toolContext) : null);
}
// Preserve Vertex AI Gemini-specific properties
builder.candidateCount(this.candidateCount)
.responseMimeType(this.responseMimeType)
.googleSearchRetrieval(this.googleSearchRetrieval)
.safetySettings(this.safetySettings != null ? new ArrayList<>(this.safetySettings) : null);
return builder.build();
}
public enum TransportType {
GRPC, REST
@@ -460,20 +447,35 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
return this;
}
@Deprecated
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return toolCallbacks(functionCallbacks);
}
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
this.options.toolCallbacks = toolCallbacks;
return this;
}
@Deprecated
public Builder functions(Set<String> functionNames) {
Assert.notNull(functionNames, "Function names must not be null");
this.options.functions = functionNames;
return this.toolNames(functionNames);
}
public Builder toolNames(Set<String> toolNames) {
Assert.notNull(toolNames, "Function names must not be null");
this.options.toolNames = toolNames;
return this;
}
@Deprecated
public Builder function(String functionName) {
Assert.hasText(functionName, "Function name must not be empty");
this.options.functions.add(functionName);
return this.toolName(functionName);
}
public Builder toolName(String toolName) {
Assert.hasText(toolName, "Function name must not be empty");
this.options.toolNames.add(toolName);
return this;
}
@@ -488,8 +490,13 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
return this;
}
@Deprecated
public Builder proxyToolCalls(boolean proxyToolCalls) {
this.options.proxyToolCalls = proxyToolCalls;
return this.internalToolExecutionEnabled(proxyToolCalls);
}
public Builder internalToolExecutionEnabled(boolean internalToolExecutionEnabled) {
this.options.internalToolExecutionEnabled = internalToolExecutionEnabled;
return this;
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2025 - 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.schema;
/**
* @author Christian Tzolov
* @since 1.0.0
*/
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.util.Assert;
/**
* Utility class for converting JSON Schema to OpenAPI schema format.
*/
public final class JsonSchemaConverter {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private JsonSchemaConverter() {
// Prevent instantiation
}
public static ObjectNode fromJson(String jsonString) {
try {
return (ObjectNode) OBJECT_MAPPER.readTree(jsonString);
}
catch (Exception e) {
throw new RuntimeException("Failed to parse JSON: " + jsonString, e);
}
}
/**
* Converts a JSON Schema ObjectNode to OpenAPI schema format.
* @param jsonSchemaNode The input JSON Schema as ObjectNode
* @return ObjectNode containing the OpenAPI schema
* @throws IllegalArgumentException if jsonSchemaNode is null
*/
public static ObjectNode convertToOpenApiSchema(ObjectNode jsonSchemaNode) {
Assert.notNull(jsonSchemaNode, "JSON Schema node must not be null");
try {
// Convert to OpenAPI schema using our custom conversion logic
ObjectNode openApiSchema = convertSchema(jsonSchemaNode, OBJECT_MAPPER.getNodeFactory());
// Add OpenAPI-specific metadata
if (!openApiSchema.has("openapi")) {
openApiSchema.put("openapi", "3.0.0");
}
return openApiSchema;
}
catch (Exception e) {
throw new IllegalStateException("Failed to convert JSON Schema to OpenAPI format: " + e.getMessage(), e);
}
}
/**
* Copies common properties from source to target node.
* @param source The source ObjectNode containing JSON Schema properties
* @param target The target ObjectNode to copy properties to
*/
private static void copyCommonProperties(ObjectNode source, ObjectNode target) {
Assert.notNull(source, "Source node must not be null");
Assert.notNull(target, "Target node must not be null");
String[] commonProperties = {
// Core schema properties
"type", "format", "description", "default", "maximum", "minimum", "maxLength", "minLength", "pattern",
"enum", "multipleOf", "uniqueItems",
// OpenAPI specific properties
"example", "deprecated", "readOnly", "writeOnly", "nullable", "discriminator", "xml", "externalDocs" };
for (String prop : commonProperties) {
if (source.has(prop)) {
target.set(prop, source.get(prop));
}
}
}
/**
* Handles JSON Schema specific attributes and converts them to OpenAPI format.
* @param source The source ObjectNode containing JSON Schema
* @param target The target ObjectNode to store OpenAPI schema
*/
private static void handleJsonSchemaSpecifics(ObjectNode source, ObjectNode target) {
Assert.notNull(source, "Source node must not be null");
Assert.notNull(target, "Target node must not be null");
if (source.has("properties")) {
ObjectNode properties = target.putObject("properties");
source.get("properties").fields().forEachRemaining(entry -> {
if (entry.getValue() instanceof ObjectNode) {
properties.set(entry.getKey(),
convertSchema((ObjectNode) entry.getValue(), OBJECT_MAPPER.getNodeFactory()));
}
});
}
// Handle required array
if (source.has("required")) {
target.set("required", source.get("required"));
}
// Convert JSON Schema specific attributes to OpenAPI equivalents
if (source.has("additionalProperties")) {
JsonNode additionalProps = source.get("additionalProperties");
if (additionalProps.isBoolean()) {
target.put("additionalProperties", additionalProps.asBoolean());
}
else if (additionalProps.isObject()) {
target.set("additionalProperties",
convertSchema((ObjectNode) additionalProps, OBJECT_MAPPER.getNodeFactory()));
}
}
// Handle arrays
if (source.has("items")) {
JsonNode items = source.get("items");
if (items.isObject()) {
target.set("items", convertSchema((ObjectNode) items, OBJECT_MAPPER.getNodeFactory()));
}
}
// Handle allOf, anyOf, oneOf
String[] combiners = { "allOf", "anyOf", "oneOf" };
for (String combiner : combiners) {
if (source.has(combiner)) {
JsonNode combinerNode = source.get(combiner);
if (combinerNode.isArray()) {
target.putArray(combiner).addAll((com.fasterxml.jackson.databind.node.ArrayNode) combinerNode);
}
}
}
}
/**
* Recursively converts a JSON Schema node to OpenAPI format.
* @param source The source ObjectNode containing JSON Schema
* @param factory The JsonNodeFactory to create new nodes
* @return The converted OpenAPI schema as ObjectNode
*/
private static ObjectNode convertSchema(ObjectNode source,
com.fasterxml.jackson.databind.node.JsonNodeFactory factory) {
Assert.notNull(source, "Source node must not be null");
Assert.notNull(factory, "JsonNodeFactory must not be null");
ObjectNode converted = factory.objectNode();
copyCommonProperties(source, converted);
handleJsonSchemaSpecifics(source, converted);
return converted;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 - 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.schema;
import java.util.List;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.ToolExecutionResult;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.util.json.schema.JsonSchemaGenerator;
/**
* @author Christian Tzolov
* @since 1.0.0
*/
public class VertexToolCallingManager implements ToolCallingManager {
private final ToolCallingManager delegateToolCallingManager;
public VertexToolCallingManager(ToolCallingManager delegateToolCallingManager) {
this.delegateToolCallingManager = delegateToolCallingManager;
}
@Override
public List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions) {
List<ToolDefinition> toolDefinitions = delegateToolCallingManager.resolveToolDefinitions(chatOptions);
return toolDefinitions.stream().map(td -> {
ObjectNode jsonSchema = JsonSchemaConverter.fromJson(td.inputSchema());
ObjectNode openApiSchema = JsonSchemaConverter.convertToOpenApiSchema(jsonSchema);
JsonSchemaGenerator.convertTypeValuesToUpperCase(openApiSchema);
return ToolDefinition.builder()
.name(td.name())
.description(td.description())
.inputSchema(openApiSchema.toPrettyString())
.build();
}).toList();
}
@Override
public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse) {
return this.delegateToolCallingManager.executeToolCalls(prompt, chatResponse);
}
}

View File

@@ -33,8 +33,12 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel.GeminiRequest;
import org.springframework.ai.vertexai.gemini.function.MockWeatherService;
import org.springframework.ai.vertexai.gemini.tool.MockWeatherService;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -51,10 +55,13 @@ public class CreateGeminiRequestTests {
@Test
public void createRequestWithChatOptions() {
var client = new VertexAiGeminiChatModel(this.vertexAI,
VertexAiGeminiChatOptions.builder().model("DEFAULT_MODEL").temperature(66.6).build());
var client = VertexAiGeminiChatModel.builder()
.vertexAI(this.vertexAI)
.defaultOptions(VertexAiGeminiChatOptions.builder().model("DEFAULT_MODEL").temperature(66.6).build())
.build();
GeminiRequest request = client.createGeminiRequest(new Prompt("Test message content"), null);
GeminiRequest request = client.createGeminiRequest(client
.buildRequestPrompt(new Prompt("Test message content", VertexAiGeminiChatOptions.builder().build())));
assertThat(request.contents()).hasSize(1);
@@ -62,8 +69,8 @@ public class CreateGeminiRequestTests {
assertThat(request.model().getModelName()).isEqualTo("DEFAULT_MODEL");
assertThat(request.model().getGenerationConfig().getTemperature()).isEqualTo(66.6f);
request = client.createGeminiRequest(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder().model("PROMPT_MODEL").temperature(99.9).build()), null);
request = client.createGeminiRequest(client.buildRequestPrompt(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder().model("PROMPT_MODEL").temperature(99.9).build())));
assertThat(request.contents()).hasSize(1);
@@ -80,10 +87,13 @@ public class CreateGeminiRequestTests {
var userMessage = new UserMessage("User Message Text",
List.of(Media.builder().mimeType(MimeTypeUtils.IMAGE_PNG).data(new URL("http://example.com")).build()));
var client = new VertexAiGeminiChatModel(this.vertexAI,
VertexAiGeminiChatOptions.builder().model("DEFAULT_MODEL").temperature(66.6).build());
var client = VertexAiGeminiChatModel.builder()
.vertexAI(this.vertexAI)
.defaultOptions(VertexAiGeminiChatOptions.builder().model("DEFAULT_MODEL").temperature(66.6).build())
.build();
GeminiRequest request = client.createGeminiRequest(new Prompt(List.of(systemMessage, userMessage)), null);
GeminiRequest request = client
.createGeminiRequest(client.buildRequestPrompt(new Prompt(List.of(systemMessage, userMessage))));
assertThat(request.model().getModelName()).isEqualTo("DEFAULT_MODEL");
assertThat(request.model().getGenerationConfig().getTemperature()).isEqualTo(66.6f);
@@ -109,22 +119,30 @@ public class CreateGeminiRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new VertexAiGeminiChatModel(this.vertexAI,
VertexAiGeminiChatOptions.builder().model("DEFAULT_MODEL").build());
var toolCallingManager = ToolCallingManager.builder().build();
var request = client.createGeminiRequest(new Prompt("Test message content",
var client = VertexAiGeminiChatModel.builder()
.vertexAI(this.vertexAI)
.defaultOptions(VertexAiGeminiChatOptions.builder().model("DEFAULT_MODEL").build())
.toolCallingManager(toolCallingManager)
.build();
var requestPrompt = client.buildRequestPrompt(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder()
.model("PROMPT_MODEL")
.functionCallbacks(List.of(FunctionCallback.builder()
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.toolCallbacks(List.of(FunctionToolCallback.builder(TOOL_FUNCTION_NAME, new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
null);
.build()));
assertThat(client.getFunctionCallbackRegister()).hasSize(1);
assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
var request = client.createGeminiRequest(requestPrompt);
List<ToolDefinition> toolDefinitions = toolCallingManager
.resolveToolDefinitions((ToolCallingChatOptions) requestPrompt.getOptions());
assertThat(toolDefinitions).hasSize(1);
assertThat(toolDefinitions.get(0).name()).isSameAs(TOOL_FUNCTION_NAME);
assertThat(request.contents()).hasSize(1);
assertThat(request.model().getSystemInstruction()).isNotPresent();
@@ -140,33 +158,44 @@ public class CreateGeminiRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new VertexAiGeminiChatModel(this.vertexAI,
VertexAiGeminiChatOptions.builder()
.model("DEFAULT_MODEL")
.functionCallbacks(List.of(FunctionCallback.builder()
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()))
.build());
var toolCallingManager = ToolCallingManager.builder().build();
var request = client.createGeminiRequest(new Prompt("Test message content"), null);
var client = VertexAiGeminiChatModel.builder()
.vertexAI(this.vertexAI)
.toolCallingManager(toolCallingManager)
.defaultOptions(VertexAiGeminiChatOptions.builder()
.model("DEFAULT_MODEL")
.functionCallbacks(List.of(FunctionCallback.builder()
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()))
.build())
.build();
assertThat(client.getFunctionCallbackRegister()).hasSize(1);
assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(client.getFunctionCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription())
.isEqualTo("Get the weather in location");
var requestPrompt = client.buildRequestPrompt(new Prompt("Test message content"));
var request = client.createGeminiRequest(requestPrompt);
List<ToolDefinition> toolDefinitions = toolCallingManager
.resolveToolDefinitions((ToolCallingChatOptions) requestPrompt.getOptions());
assertThat(toolDefinitions).hasSize(1);
assertThat(toolDefinitions.get(0).name()).isSameAs(TOOL_FUNCTION_NAME);
assertThat(toolDefinitions.get(0).description()).isEqualTo("Get the weather in location");
assertThat(request.contents()).hasSize(1);
assertThat(request.model().getSystemInstruction()).isNotPresent();
assertThat(request.model().getModelName()).isEqualTo("DEFAULT_MODEL");
assertThat(request.model().getTools()).as("Default Options callback functions are not automatically enabled!")
.isNullOrEmpty();
assertThat(request.model().getTools()).hasSize(1);
// Explicitly enable the function
request = client.createGeminiRequest(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder().function(TOOL_FUNCTION_NAME).build()), null);
requestPrompt = client.buildRequestPrompt(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder().toolName(TOOL_FUNCTION_NAME).build()));
request = client.createGeminiRequest(requestPrompt);
assertThat(request.model().getTools()).hasSize(1);
assertThat(request.model().getTools().get(0).getFunctionDeclarations(0).getName())
@@ -174,43 +203,48 @@ public class CreateGeminiRequestTests {
.isEqualTo(TOOL_FUNCTION_NAME);
// Override the default options function with one from the prompt
request = client.createGeminiRequest(new Prompt("Test message content",
requestPrompt = client.buildRequestPrompt(new Prompt("Test message content",
VertexAiGeminiChatOptions.builder()
.functionCallbacks(List.of(FunctionCallback.builder()
.function(TOOL_FUNCTION_NAME, new MockWeatherService())
.description("Overridden function description")
.inputType(MockWeatherService.Request.class)
.build()))
.build()),
null);
.build()));
request = client.createGeminiRequest(requestPrompt);
assertThat(request.model().getTools()).hasSize(1);
assertThat(request.model().getTools().get(0).getFunctionDeclarations(0).getName())
.as("Explicitly enabled function")
.isEqualTo(TOOL_FUNCTION_NAME);
assertThat(client.getFunctionCallbackRegister()).hasSize(1);
assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(client.getFunctionCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription())
.isEqualTo("Overridden function description");
toolDefinitions = toolCallingManager
.resolveToolDefinitions((ToolCallingChatOptions) requestPrompt.getOptions());
assertThat(toolDefinitions).hasSize(1);
assertThat(toolDefinitions.get(0).name()).isSameAs(TOOL_FUNCTION_NAME);
assertThat(toolDefinitions.get(0).description()).isEqualTo("Overridden function description");
}
@Test
public void createRequestWithGenerationConfigOptions() {
var client = new VertexAiGeminiChatModel(this.vertexAI,
VertexAiGeminiChatOptions.builder()
.model("DEFAULT_MODEL")
.temperature(66.6)
.maxOutputTokens(100)
.topK(10)
.topP(5.0)
.stopSequences(List.of("stop1", "stop2"))
.candidateCount(1)
.responseMimeType("application/json")
.build());
var client = VertexAiGeminiChatModel.builder()
.vertexAI(this.vertexAI)
.defaultOptions(VertexAiGeminiChatOptions.builder()
.model("DEFAULT_MODEL")
.temperature(66.6)
.maxOutputTokens(100)
.topK(10)
.topP(5.0)
.stopSequences(List.of("stop1", "stop2"))
.candidateCount(1)
.responseMimeType("application/json")
.build())
.build();
GeminiRequest request = client.createGeminiRequest(new Prompt("Test message content"), null);
GeminiRequest request = client
.createGeminiRequest(client.buildRequestPrompt(new Prompt("Test message content")));
assertThat(request.contents()).hasSize(1);

View File

@@ -39,7 +39,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
@@ -66,7 +65,7 @@ public class VertexAiChatModelObservationIT {
void observationForChatOperation() {
var options = VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_PRO.getValue())
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
.temperature(0.7)
.stopSequences(List.of("this-is-the-end"))
.maxOutputTokens(2048)
@@ -88,7 +87,7 @@ public class VertexAiChatModelObservationIT {
void observationForStreamingOperation() {
var options = VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_PRO.getValue())
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
.temperature(0.7)
.stopSequences(List.of("this-is-the-end"))
.maxOutputTokens(2048)
@@ -128,7 +127,7 @@ public class VertexAiChatModelObservationIT {
AiProvider.VERTEX_AI.value())
.hasLowCardinalityKeyValue(
ChatModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL.asString(),
VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_PRO.getValue())
VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
.hasHighCardinalityKeyValue(
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), "2048")
.hasHighCardinalityKeyValue(
@@ -177,9 +176,14 @@ public class VertexAiChatModelObservationIT {
@Bean
public VertexAiGeminiChatModel vertexAiEmbedding(VertexAI vertexAi,
TestObservationRegistry observationRegistry) {
return new VertexAiGeminiChatModel(vertexAi,
VertexAiGeminiChatOptions.builder().model(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_PRO).build(),
null, List.of(), RetryTemplate.defaultInstance(), observationRegistry);
return VertexAiGeminiChatModel.builder()
.vertexAI(vertexAi)
.observationRegistry(observationRegistry)
.defaultOptions(VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.build())
.build();
}
}

View File

@@ -26,6 +26,7 @@ import java.util.stream.Stream;
import com.google.cloud.vertexai.Transport;
import com.google.cloud.vertexai.VertexAI;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
@@ -41,6 +42,7 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel.ChatModel;
import org.springframework.ai.vertexai.gemini.common.VertexAiGeminiSafetySetting;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -88,19 +90,24 @@ class VertexAiGeminiChatModelIT {
@Test
void googleSearchTool() {
Prompt prompt = createPrompt(VertexAiGeminiChatOptions.builder().googleSearchRetrieval(true).build());
Prompt prompt = createPrompt(VertexAiGeminiChatOptions.builder()
.model(ChatModel.GEMINI_1_5_PRO) // Only the pro model supports the google
// search tool
.googleSearchRetrieval(true)
.build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Blackbeard", "Bartholomew");
}
@Test
@Disabled
void testSafetySettings() {
List<VertexAiGeminiSafetySetting> safetySettings = List.of(new VertexAiGeminiSafetySetting.Builder()
.withCategory(VertexAiGeminiSafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT)
.withThreshold(VertexAiGeminiSafetySetting.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE)
.build());
Prompt prompt = new Prompt("What are common digital attack vectors?",
VertexAiGeminiChatOptions.builder().safetySettings(safetySettings).build());
Prompt prompt = new Prompt("How to make cocktail Molotov bomb at home?",
VertexAiGeminiChatOptions.builder().model(ChatModel.GEMINI_PRO).safetySettings(safetySettings).build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("SAFETY");
}
@@ -235,7 +242,8 @@ class VertexAiGeminiChatModelIT {
// Response should contain something like:
// I see a bunch of bananas in a golden basket. The bananas are ripe and yellow.
// There are also some red apples in the basket. The basket is sitting on a table.
// There are also some red apples in the basket. The basket is sitting on a
// table.
// The background is a blurred light blue color.'
assertThat(response.getResult().getOutput().getText()).satisfies(content -> {
long count = Stream.of("bananas", "apple", "basket").filter(content::contains).count();
@@ -293,10 +301,12 @@ class VertexAiGeminiChatModelIT {
@Bean
public VertexAiGeminiChatModel vertexAiEmbedding(VertexAI vertexAi) {
return new VertexAiGeminiChatModel(vertexAi,
VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_PRO)
.build());
return VertexAiGeminiChatModel.builder()
.vertexAI(vertexAi)
.defaultOptions(VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.build())
.build();
}
}

View File

@@ -75,7 +75,7 @@ public class VertexAiGeminiRetryTests {
VertexAiGeminiChatOptions.builder()
.temperature(0.7)
.topP(1.0)
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_PRO.getValue())
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
.build(),
null, Collections.emptyList(), this.retryTemplate);

View File

@@ -0,0 +1,210 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.schema;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Nested;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link JsonSchemaConverter}.
*
* @author Christian Tzolov
*/
class JsonSchemaConverterTests {
@Test
void fromJsonShouldParseValidJson() {
String json = "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}";
ObjectNode result = JsonSchemaConverter.fromJson(json);
assertThat(result.get("type").asText()).isEqualTo("object");
assertThat(result.get("properties").get("name").get("type").asText()).isEqualTo("string");
}
@Test
void fromJsonShouldThrowOnInvalidJson() {
String invalidJson = "{invalid:json}";
assertThatThrownBy(() -> JsonSchemaConverter.fromJson(invalidJson)).isInstanceOf(RuntimeException.class)
.hasMessageContaining("Failed to parse JSON");
}
@Test
void convertToOpenApiSchemaShouldThrowOnNullInput() {
assertThatThrownBy(() -> JsonSchemaConverter.convertToOpenApiSchema(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("JSON Schema node must not be null");
}
@Nested
class SchemaConversionTests {
@Test
void shouldConvertBasicSchema() {
String json = """
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name property"
}
},
"required": ["name"]
}
""";
ObjectNode result = JsonSchemaConverter.convertToOpenApiSchema(JsonSchemaConverter.fromJson(json));
assertThat(result.get("openapi").asText()).isEqualTo("3.0.0");
assertThat(result.get("type").asText()).isEqualTo("object");
assertThat(result.get("properties").get("name").get("type").asText()).isEqualTo("string");
assertThat(result.get("properties").get("name").get("description").asText()).isEqualTo("The name property");
assertThat(result.get("required").get(0).asText()).isEqualTo("name");
}
@Test
void shouldHandleArrayTypes() {
String json = """
{
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
""";
ObjectNode result = JsonSchemaConverter.convertToOpenApiSchema(JsonSchemaConverter.fromJson(json));
assertThat(result.get("properties").get("tags").get("type").asText()).isEqualTo("array");
assertThat(result.get("properties").get("tags").get("items").get("type").asText()).isEqualTo("string");
}
@Test
void shouldHandleAdditionalProperties() {
String json = """
{
"type": "object",
"additionalProperties": {
"type": "string"
}
}
""";
ObjectNode result = JsonSchemaConverter.convertToOpenApiSchema(JsonSchemaConverter.fromJson(json));
assertThat(result.get("additionalProperties").get("type").asText()).isEqualTo("string");
}
@Test
void shouldHandleCombiningSchemas() {
String json = """
{
"type": "object",
"allOf": [
{"type": "object", "properties": {"name": {"type": "string"}}},
{"type": "object", "properties": {"age": {"type": "integer"}}}
]
}
""";
ObjectNode result = JsonSchemaConverter.convertToOpenApiSchema(JsonSchemaConverter.fromJson(json));
assertThat(result.get("allOf")).isNotNull();
assertThat(result.get("allOf").isArray()).isTrue();
assertThat(result.get("allOf").size()).isEqualTo(2);
}
@Test
void shouldCopyCommonProperties() {
String json = """
{
"type": "string",
"format": "email",
"description": "Email address",
"minLength": 5,
"maxLength": 100,
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$",
"example": "user@example.com",
"deprecated": false
}
""";
ObjectNode result = JsonSchemaConverter.convertToOpenApiSchema(JsonSchemaConverter.fromJson(json));
assertThat(result.get("type").asText()).isEqualTo("string");
assertThat(result.get("format").asText()).isEqualTo("email");
assertThat(result.get("description").asText()).isEqualTo("Email address");
assertThat(result.get("minLength").asInt()).isEqualTo(5);
assertThat(result.get("maxLength").asInt()).isEqualTo(100);
assertThat(result.get("pattern").asText()).isEqualTo("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
assertThat(result.get("example").asText()).isEqualTo("user@example.com");
assertThat(result.get("deprecated").asBoolean()).isFalse();
}
@Test
void shouldHandleNestedObjects() {
String json = """
{
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
}
}
}
}
}
}
""";
ObjectNode result = JsonSchemaConverter.convertToOpenApiSchema(JsonSchemaConverter.fromJson(json));
assertThat(result.get("properties")
.get("user")
.get("properties")
.get("address")
.get("properties")
.get("street")
.get("type")
.asText()).isEqualTo("string");
assertThat(result.get("properties")
.get("user")
.get("properties")
.get("address")
.get("properties")
.get("city")
.get("type")
.asText()).isEqualTo("string");
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.function;
package org.springframework.ai.vertexai.gemini.tool;
import java.util.function.Function;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.function;
package org.springframework.ai.vertexai.gemini.tool;
import java.util.ArrayList;
import java.util.List;
@@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
@Deprecated
public class VertexAiGeminiChatModelFunctionCallingIT {
private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiChatModelFunctionCallingIT.class);
@@ -106,7 +107,7 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_FLASH)
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.functionCallbacks(List.of(
FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
.inputSchema(JsonSchemaGenerator.generateForType(MockWeatherService.Request.class,
@@ -138,6 +139,43 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
}
@Test
public void functionCallTestInferredOpenApiSchema2() {
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.functionCallbacks(List.of(
FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
.description("Get the current weather in a given location.")
.inputType(MockWeatherService.Request.class)
.build(),
FunctionToolCallback.builder("get_payment_status", new PaymentStatus())
.description(
"Retrieves the payment status for transaction. For example what is the payment status for transaction 700?")
.inputType(PaymentInfoRequest.class)
.build()))
.build();
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
ChatResponse response2 = this.chatModel
.call(new Prompt("What is the payment status for transaction 696?", promptOptions));
logger.info("Response: {}", response2);
assertThat(response2.getResult().getOutput().getText()).containsIgnoringCase("transaction 696 is PAYED");
}
@Test
public void functionCallTestInferredOpenApiSchemaStream() {
@@ -147,7 +185,7 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_FLASH)
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.functionCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
.inputSchema(JsonSchemaGenerator.generateForType(MockWeatherService.Request.class,
JsonSchemaGenerator.SchemaOption.UPPER_CASE_TYPE_VALUES))
@@ -207,7 +245,7 @@ public class VertexAiGeminiChatModelFunctionCallingIT {
public VertexAiGeminiChatModel vertexAiEmbedding(VertexAI vertexAi) {
return new VertexAiGeminiChatModel(vertexAi,
VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_1_5_PRO)
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.temperature(0.9)
.build());
}

View File

@@ -0,0 +1,212 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.tool;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.cloud.vertexai.Transport;
import com.google.cloud.vertexai.VertexAI;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
public class VertexAiGeminiChatModelToolCallingIT {
private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiChatModelToolCallingIT.class);
@Autowired
private VertexAiGeminiChatModel chatModel;
@Test
public void functionCallExplicitOpenApiSchema() {
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
List<Message> messages = new ArrayList<>(List.of(userMessage));
String openApiSchema = """
{
"type": "OBJECT",
"properties": {
"location": {
"type": "STRING",
"description": "The city and state e.g. San Francisco, CA"
},
"unit" : {
"type" : "STRING",
"enum" : [ "C", "F" ],
"description" : "Temperature unit"
}
},
"required": ["location", "unit"]
}
""";
var promptOptions = VertexAiGeminiChatOptions.builder()
.toolCallbacks(List.of(FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
.description("Get the current weather in a given location")
.inputSchema(openApiSchema)
.inputType(MockWeatherService.Request.class)
.build()))
.build();
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
}
@Test
public void functionCallTestInferredOpenApiSchema() {
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.toolCallbacks(List.of(
FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
.description("Get the current weather in a given location.")
.inputType(MockWeatherService.Request.class)
.build(),
FunctionToolCallback.builder("get_payment_status", new PaymentStatus())
.description(
"Retrieves the payment status for transaction. For example what is the payment status for transaction 700?")
.inputType(PaymentInfoRequest.class)
.build()))
.build();
ChatResponse response = this.chatModel.call(new Prompt(messages, promptOptions));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
ChatResponse response2 = this.chatModel
.call(new Prompt("What is the payment status for transaction 696?", promptOptions));
logger.info("Response: {}", response2);
assertThat(response2.getResult().getOutput().getText()).containsIgnoringCase("transaction 696 is PAYED");
}
@Test
public void functionCallTestInferredOpenApiSchemaStream() {
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Paris and in Tokyo? Return the temperature in Celsius.");
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
.description("Get the current weather in a given location")
.inputType(MockWeatherService.Request.class)
.build()))
.build();
Flux<ChatResponse> response = this.chatModel.stream(new Prompt(messages, promptOptions));
String responseString = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.collect(Collectors.joining());
logger.info("Response: {}", responseString);
assertThat(responseString).contains("30", "10", "15");
}
public record PaymentInfoRequest(String id) {
}
public record TransactionStatus(String status) {
}
public static class PaymentStatus implements Function<PaymentInfoRequest, TransactionStatus> {
@Override
public TransactionStatus apply(PaymentInfoRequest paymentInfoRequest) {
return new TransactionStatus("Transaction " + paymentInfoRequest.id() + " is PAYED");
}
}
@SpringBootConfiguration
public static class TestConfiguration {
@Bean
public VertexAI vertexAiApi() {
String projectId = System.getenv("VERTEX_AI_GEMINI_PROJECT_ID");
String location = System.getenv("VERTEX_AI_GEMINI_LOCATION");
return new VertexAI.Builder().setLocation(location)
.setProjectId(projectId)
.setTransport(Transport.REST)
.build();
}
@Bean
public VertexAiGeminiChatModel vertexAiEmbedding(VertexAI vertexAi) {
return VertexAiGeminiChatModel.builder()
.vertexAI(vertexAi)
.defaultOptions(VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.temperature(0.9)
.build())
.build();
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.function;
package org.springframework.ai.vertexai.gemini.tool;
import java.util.List;
import java.util.Map;
@@ -55,9 +55,10 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
public class VertexAiGeminiPaymentTransactionIT {
@Deprecated
public class VertexAiGeminiPaymentTransactionDeprecatedIT {
private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiPaymentTransactionIT.class);
private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiPaymentTransactionDeprecatedIT.class);
private static final Map<Transaction, Status> DATASET = Map.of(new Transaction("001"), new Status("pending"),
new Transaction("002"), new Status("approved"), new Transaction("003"), new Status("rejected"));

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.tool;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.cloud.vertexai.Transport;
import com.google.cloud.vertexai.VertexAI;
import io.micrometer.observation.ObservationRegistry;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;
import org.springframework.context.support.GenericApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@SpringBootTest
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
public class VertexAiGeminiPaymentTransactionIT {
private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiPaymentTransactionIT.class);
private static final Map<Transaction, Status> DATASET = Map.of(new Transaction("001"), new Status("pending"),
new Transaction("002"), new Status("approved"), new Transaction("003"), new Status("rejected"));
@Autowired
ChatClient chatClient;
@Test
public void paymentStatuses() {
// @formatter:off
String content = this.chatClient.prompt()
.advisors(new LoggingAdvisor())
.tools("paymentStatus")
.user("""
What is the status of my payment transactions 001, 002 and 003?
If requred invoke the function per transaction.
""").call().content();
// @formatter:on
logger.info("" + content);
assertThat(content).contains("001", "002", "003");
assertThat(content).contains("pending", "approved", "rejected");
}
@RepeatedTest(5)
public void streamingPaymentStatuses() {
Flux<String> streamContent = this.chatClient.prompt()
.advisors(new LoggingAdvisor())
.tools("paymentStatus")
.user("""
What is the status of my payment transactions 001, 002 and 003?
If requred invoke the function per transaction.
""")
.stream()
.content();
String content = streamContent.collectList().block().stream().collect(Collectors.joining());
logger.info(content);
assertThat(content).contains("001", "002", "003");
assertThat(content).contains("pending", "approved", "rejected");
// Quota rate
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
}
}
record TransactionStatusResponse(String id, String status) {
}
private static class LoggingAdvisor implements CallAroundAdvisor {
private final Logger logger = LoggerFactory.getLogger(LoggingAdvisor.class);
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
return 0;
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
var response = chain.nextAroundCall(before(advisedRequest));
observeAfter(response);
return response;
}
private AdvisedRequest before(AdvisedRequest request) {
logger.info("System text: \n" + request.systemText());
logger.info("System params: " + request.systemParams());
logger.info("User text: \n" + request.userText());
logger.info("User params:" + request.userParams());
logger.info("Function names: " + request.functionNames());
logger.info("Options: " + request.chatOptions().toString());
return request;
}
private void observeAfter(AdvisedResponse advisedResponse) {
logger.info("Response: " + advisedResponse.response());
}
}
record Transaction(String id) {
}
record Status(String name) {
}
record Transactions(List<Transaction> transactions) {
}
record Statuses(List<Status> statuses) {
}
@SpringBootConfiguration
public static class TestConfiguration {
@Bean
@Description("Get the status of a single payment transaction")
public Function<Transaction, Status> paymentStatus() {
return transaction -> {
logger.info("Single Transaction: " + transaction);
return DATASET.get(transaction);
};
}
@Bean
@Description("Get the list statuses of a list of payment transactions")
public Function<Transactions, Statuses> paymentStatuses() {
return transactions -> {
logger.info("Transactions: " + transactions);
return new Statuses(transactions.transactions().stream().map(t -> DATASET.get(t)).toList());
};
}
@Bean
public ChatClient chatClient(VertexAiGeminiChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
@Bean
public VertexAI vertexAiApi() {
String projectId = System.getenv("VERTEX_AI_GEMINI_PROJECT_ID");
String location = System.getenv("VERTEX_AI_GEMINI_LOCATION");
return new VertexAI.Builder().setLocation(location)
.setProjectId(projectId)
.setTransport(Transport.REST)
// .setTransport(Transport.GRPC)
.build();
}
@Bean
public VertexAiGeminiChatModel vertexAiChatModel(VertexAI vertexAi, ToolCallingManager toolCallingManager) {
return VertexAiGeminiChatModel.builder()
.vertexAI(vertexAi)
.toolCallingManager(toolCallingManager)
.defaultOptions(VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.temperature(0.1)
.build())
.build();
}
@Bean
ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
List<FunctionCallback> toolCallbacks, ObjectProvider<ObservationRegistry> observationRegistry) {
var staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks);
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
.applicationContext(applicationContext)
.build();
ToolCallbackResolver toolCallbackResolver = new DelegatingToolCallbackResolver(
List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
return ToolCallingManager.builder()
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.toolCallbackResolver(toolCallbackResolver)
.toolExecutionExceptionProcessor(new DefaultToolExecutionExceptionProcessor(false))
.build();
}
}
}

View File

@@ -0,0 +1,248 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vertexai.gemini.tool;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.cloud.vertexai.Transport;
import com.google.cloud.vertexai.VertexAI;
import io.micrometer.observation.ObservationRegistry;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.GenericApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@SpringBootTest
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
public class VertexAiGeminiPaymentTransactionMethodIT {
private static final Logger logger = LoggerFactory.getLogger(VertexAiGeminiPaymentTransactionMethodIT.class);
private static final Map<Transaction, Status> DATASET = Map.of(new Transaction("001"), new Status("pending"),
new Transaction("002"), new Status("approved"), new Transaction("003"), new Status("rejected"));
@Autowired
ChatClient chatClient;
@Test
public void paymentStatuses() {
String content = this.chatClient.prompt().advisors(new LoggingAdvisor()).tools("paymentStatus").user("""
What is the status of my payment transactions 001, 002 and 003?
If requred invoke the function per transaction.
""").call().content();
logger.info("" + content);
assertThat(content).contains("001", "002", "003");
assertThat(content).contains("pending", "approved", "rejected");
}
@RepeatedTest(5)
public void streamingPaymentStatuses() {
Flux<String> streamContent = this.chatClient.prompt()
.advisors(new LoggingAdvisor())
.tools("paymentStatus")
.user("""
What is the status of my payment transactions 001, 002 and 003?
If requred invoke the function per transaction.
""")
.stream()
.content();
String content = streamContent.collectList().block().stream().collect(Collectors.joining());
logger.info(content);
assertThat(content).contains("001", "002", "003");
assertThat(content).contains("pending", "approved", "rejected");
// Quota rate
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
}
}
record TransactionStatusResponse(String id, String status) {
}
private static class LoggingAdvisor implements CallAroundAdvisor {
private final Logger logger = LoggerFactory.getLogger(LoggingAdvisor.class);
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
return 0;
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
var response = chain.nextAroundCall(before(advisedRequest));
observeAfter(response);
return response;
}
private AdvisedRequest before(AdvisedRequest request) {
logger.info("System text: \n" + request.systemText());
logger.info("System params: " + request.systemParams());
logger.info("User text: \n" + request.userText());
logger.info("User params:" + request.userParams());
logger.info("Function names: " + request.functionNames());
logger.info("Options: " + request.chatOptions().toString());
return request;
}
private void observeAfter(AdvisedResponse advisedResponse) {
logger.info("Response: " + advisedResponse.response());
}
}
record Transaction(String id) {
}
record Status(String name) {
}
public static class PaymentService {
@Tool(description = "Get the status of a single payment transaction")
public Status paymentStatus(Transaction transaction) {
logger.info("Single Transaction: " + transaction);
return DATASET.get(transaction);
}
@Tool(description = "Get the list statuses of a list of payment transactions")
public List<Status> statusespaymentStatuses(List<Transaction> transactions) {
logger.info("Transactions: " + transactions);
return transactions.stream().map(t -> DATASET.get(t)).toList();
}
}
@SpringBootConfiguration
public static class TestConfiguration {
@Bean
public List<ToolCallback> paymentServiceTools() {
var tools = List.of(ToolCallbacks.from(new PaymentService()));
return tools;
}
@Bean
public ChatClient chatClient(VertexAiGeminiChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
@Bean
public VertexAI vertexAiApi() {
String projectId = System.getenv("VERTEX_AI_GEMINI_PROJECT_ID");
String location = System.getenv("VERTEX_AI_GEMINI_LOCATION");
return new VertexAI.Builder().setLocation(location)
.setProjectId(projectId)
.setTransport(Transport.REST)
// .setTransport(Transport.GRPC)
.build();
}
@Bean
public VertexAiGeminiChatModel vertexAiChatModel(VertexAI vertexAi, ToolCallingManager toolCallingManager) {
return VertexAiGeminiChatModel.builder()
.vertexAI(vertexAi)
.toolCallingManager(toolCallingManager)
.defaultOptions(VertexAiGeminiChatOptions.builder()
.model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH)
.temperature(0.1)
.build())
.build();
}
@Bean
ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
List<ToolCallback> toolCallbacks, List<FunctionCallback> functionCallbacks,
ObjectProvider<ObservationRegistry> observationRegistry) {
List<FunctionCallback> allFunctionCallbacks = new ArrayList(functionCallbacks);
allFunctionCallbacks.addAll(toolCallbacks.stream().map(tc -> (FunctionCallback) tc).toList());
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionCallbacks);
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
.applicationContext(applicationContext)
.build();
ToolCallbackResolver toolCallbackResolver = new DelegatingToolCallbackResolver(
List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
return ToolCallingManager.builder()
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
.toolCallbackResolver(toolCallbackResolver)
.toolExecutionExceptionProcessor(new DefaultToolExecutionExceptionProcessor(false))
.build();
}
}
}