Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.GenerateContentResponse;
import com.google.genai.types.GoogleSearch;
import com.google.genai.types.GoogleMaps;
import com.google.genai.types.LatLng;
import com.google.genai.types.Part;
import com.google.genai.types.RetrievalConfig;
import com.google.genai.types.SafetySetting;
import com.google.genai.types.Schema;
import com.google.genai.types.ThinkingConfig;
import com.google.genai.types.ThinkingLevel;
import com.google.genai.types.Tool;
import com.google.genai.types.ToolConfig;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
Expand Down Expand Up @@ -515,6 +519,14 @@ Prompt buildRequestPrompt(Prompt prompt) {

requestOptions.setGoogleSearchRetrieval(ModelOptionsUtils.mergeOption(
runtimeOptions.getGoogleSearchRetrieval(), this.defaultOptions.getGoogleSearchRetrieval()));
requestOptions.setGoogleMaps(
ModelOptionsUtils.mergeOption(runtimeOptions.getGoogleMaps(), this.defaultOptions.getGoogleMaps()));
requestOptions.setGoogleMapsWidget(ModelOptionsUtils.mergeOption(runtimeOptions.getGoogleMapsWidget(),
this.defaultOptions.getGoogleMapsWidget()));
requestOptions.setLatitude(
ModelOptionsUtils.mergeOption(runtimeOptions.getLatitude(), this.defaultOptions.getLatitude()));
requestOptions.setLongitude(
ModelOptionsUtils.mergeOption(runtimeOptions.getLongitude(), this.defaultOptions.getLongitude()));
requestOptions.setSafetySettings(ModelOptionsUtils.mergeOption(runtimeOptions.getSafetySettings(),
this.defaultOptions.getSafetySettings()));
requestOptions
Expand All @@ -527,6 +539,10 @@ Prompt buildRequestPrompt(Prompt prompt) {
requestOptions.setToolContext(this.defaultOptions.getToolContext());

requestOptions.setGoogleSearchRetrieval(this.defaultOptions.getGoogleSearchRetrieval());
requestOptions.setGoogleMaps(this.defaultOptions.getGoogleMaps());
requestOptions.setGoogleMapsWidget(this.defaultOptions.getGoogleMapsWidget());
requestOptions.setLatitude(this.defaultOptions.getLatitude());
requestOptions.setLongitude(this.defaultOptions.getLongitude());
requestOptions.setSafetySettings(this.defaultOptions.getSafetySettings());
requestOptions.setLabels(this.defaultOptions.getLabels());
}
Expand Down Expand Up @@ -648,9 +664,10 @@ protected List<Generation> responseCandidateToGeneration(Candidate candidate) {
}
}

ChatGenerationMetadata chatGenerationMetadata = ChatGenerationMetadata.builder()
.finishReason(candidateFinishReason.toString())
.build();
var generationMetadataBuilder = ChatGenerationMetadata.builder().finishReason(candidateFinishReason.toString());
candidate.groundingMetadata()
.ifPresent(grounding -> generationMetadataBuilder.metadata("groundingMetadata", grounding));
ChatGenerationMetadata chatGenerationMetadata = generationMetadataBuilder.build();

boolean isFunctionCall = candidate.content().isPresent() && candidate.content().get().parts().isPresent()
&& candidate.content().get().parts().get().stream().allMatch(part -> part.functionCall().isPresent());
Expand Down Expand Up @@ -725,6 +742,7 @@ GeminiRequest createGeminiRequest(Prompt prompt) {

// Build GenerateContentConfig
GenerateContentConfig.Builder configBuilder = GenerateContentConfig.builder();
RetrievalConfig.Builder retrievalConfigBuilder = RetrievalConfig.builder();

String modelName = requestOptions.getModel() != null ? requestOptions.getModel()
: this.defaultOptions.getModel();
Expand Down Expand Up @@ -806,6 +824,23 @@ GeminiRequest createGeminiRequest(Prompt prompt) {
tools.add(googleSearchRetrievalTool);
}

if (requestOptions.getGoogleMaps()) {
var googleMapsBuilder = GoogleMaps.builder();
if (requestOptions.getGoogleMapsWidget()) {
googleMapsBuilder.enableWidget(true);
}
tools.add(Tool.builder().googleMaps(googleMapsBuilder.build()).build());
}

if (requestOptions.getLatitude() != null && requestOptions.getLongitude() != null) {
retrievalConfigBuilder.latLng(LatLng.builder()
.latitude(requestOptions.getLatitude())
.longitude(requestOptions.getLongitude())
.build());
}

configBuilder.toolConfig(ToolConfig.builder().retrievalConfig(retrievalConfigBuilder).build());

if (!CollectionUtils.isEmpty(tools)) {
configBuilder.tools(tools);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ public class GoogleGenAiChatOptions implements ToolCallingChatOptions, Structure
@JsonIgnore
private Boolean googleSearchRetrieval = false;

@JsonIgnore
private Boolean googleMaps = false;

@JsonIgnore
private Boolean googleMapsWidget = false;

@JsonIgnore
private Double latitude;

@JsonIgnore
private Double longitude;

@JsonIgnore
private List<GoogleGenAiSafetySetting> safetySettings = new ArrayList<>();

Expand Down Expand Up @@ -241,6 +253,10 @@ public static GoogleGenAiChatOptions fromOptions(GoogleGenAiChatOptions fromOpti
options.setUseCachedContent(fromOptions.getUseCachedContent());
options.setAutoCacheThreshold(fromOptions.getAutoCacheThreshold());
options.setAutoCacheTtl(fromOptions.getAutoCacheTtl());
options.setGoogleMaps(fromOptions.getGoogleMaps());
options.setGoogleMapsWidget(fromOptions.getGoogleMapsWidget());
options.setLatitude(fromOptions.getLatitude());
options.setLongitude(fromOptions.getLongitude());
return options;
}

Expand Down Expand Up @@ -458,6 +474,38 @@ public void setGoogleSearchRetrieval(Boolean googleSearchRetrieval) {
this.googleSearchRetrieval = googleSearchRetrieval;
}

public Boolean getGoogleMaps() {
return this.googleMaps;
}

public void setGoogleMaps(Boolean googleMaps) {
this.googleMaps = googleMaps;
}

public Boolean getGoogleMapsWidget() {
return this.googleMapsWidget;
}

public void setGoogleMapsWidget(Boolean googleMapsWidget) {
this.googleMapsWidget = googleMapsWidget;
}

public Double getLatitude() {
return this.latitude;
}

public void setLatitude(Double latitude) {
this.latitude = latitude;
}

public Double getLongitude() {
return this.longitude;
}

public void setLongitude(Double longitude) {
this.longitude = longitude;
}

public List<GoogleGenAiSafetySetting> getSafetySettings() {
return this.safetySettings;
}
Expand Down Expand Up @@ -522,7 +570,10 @@ public boolean equals(Object o) {
&& Objects.equals(this.toolNames, that.toolNames)
&& Objects.equals(this.safetySettings, that.safetySettings)
&& Objects.equals(this.internalToolExecutionEnabled, that.internalToolExecutionEnabled)
&& Objects.equals(this.toolContext, that.toolContext) && Objects.equals(this.labels, that.labels);
&& Objects.equals(this.toolContext, that.toolContext) && Objects.equals(this.labels, that.labels)
&& Objects.equals(this.googleMaps, that.googleMaps)
&& Objects.equals(this.googleMapsWidget, that.googleMapsWidget)
&& Objects.equals(this.latitude, that.latitude) && Objects.equals(this.longitude, that.longitude);
}

@Override
Expand All @@ -531,7 +582,8 @@ public int hashCode() {
this.frequencyPenalty, this.presencePenalty, this.thinkingBudget, this.includeThoughts,
this.thinkingLevel, this.maxOutputTokens, this.model, this.responseMimeType, this.responseSchema,
this.toolCallbacks, this.toolNames, this.googleSearchRetrieval, this.safetySettings,
this.internalToolExecutionEnabled, this.toolContext, this.labels);
this.internalToolExecutionEnabled, this.toolContext, this.labels, this.googleMaps,
this.googleMapsWidget, this.latitude, this.longitude);
}

@Override
Expand All @@ -544,7 +596,8 @@ public String toString() {
+ this.model + '\'' + ", responseMimeType='" + this.responseMimeType + '\'' + ", toolCallbacks="
+ this.toolCallbacks + ", toolNames=" + this.toolNames + ", googleSearchRetrieval="
+ this.googleSearchRetrieval + ", safetySettings=" + this.safetySettings + ", labels=" + this.labels
+ '}';
+ ", googleMaps=" + this.googleMaps + ", googleMapsWidget=" + this.googleMapsWidget + ", latitude="
+ this.latitude + ", longitude=" + this.longitude + '}';
}

@Override
Expand Down Expand Up @@ -656,6 +709,26 @@ public Builder googleSearchRetrieval(boolean googleSearch) {
return this;
}

public Builder googleMaps(boolean googleMaps) {
this.options.googleMaps = googleMaps;
return this;
}

public Builder googleMapsWidget(boolean googleMapsWidget) {
this.options.googleMapsWidget = googleMapsWidget;
return this;
}

public Builder latitude(Double latitude) {
this.options.latitude = latitude;
return this;
}

public Builder longitude(Double longitude) {
this.options.longitude = longitude;
return this;
}

public Builder safetySettings(List<GoogleGenAiSafetySetting> safetySettings) {
Assert.notNull(safetySettings, "safetySettings must not be null");
this.options.safetySettings = safetySettings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.stream.Stream;

import com.google.genai.Client;
import com.google.genai.types.GroundingMetadata;
import io.micrometer.observation.ObservationRegistry;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -118,6 +119,113 @@ void googleSearchToolFlash() {
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Blackbeard", "Bartholomew", "Bob");
}

@Test
void googleMapsToolPro() {
Prompt prompt = new Prompt("Could you recommend some tourist spots around the White House?",
GoogleGenAiChatOptions.builder().model(ChatModel.GEMINI_2_5_PRO).googleMaps(true).build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Washington Monument", "Lincoln Memorial");
assertThat(response.getResult().getMetadata().containsKey("groundingMetadata")).isTrue();

GroundingMetadata groundingMetadata = response.getResult().getMetadata().get("groundingMetadata");
assertThat(groundingMetadata.groundingChunks()).isNotEmpty();
assertThat(groundingMetadata.groundingSupports()).isNotEmpty();
assertThat(groundingMetadata.retrievalQueries()).isNotEmpty();
assertThat(groundingMetadata.googleMapsWidgetContextToken()).isNotPresent();
}

@Test
void googleMapsToolFlash() {
Prompt prompt = new Prompt("Could you recommend some tourist spots around the White House?",
GoogleGenAiChatOptions.builder().model(ChatModel.GEMINI_2_0_FLASH).googleMaps(true).build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getMetadata().containsKey("groundingMetadata")).isTrue();

GroundingMetadata groundingMetadata = response.getResult().getMetadata().get("groundingMetadata");
assertThat(groundingMetadata.groundingChunks()).isNotEmpty();
assertThat(groundingMetadata.groundingSupports()).isNotEmpty();
assertThat(groundingMetadata.retrievalQueries()).isNotEmpty();
assertThat(groundingMetadata.googleMapsWidgetContextToken()).isNotPresent();
}

@Test
void googleMapsToolProWithWidget() {
Prompt prompt = new Prompt("Could you recommend some tourist spots around the White House?",
GoogleGenAiChatOptions.builder()
.model(ChatModel.GEMINI_2_5_PRO)
.googleMaps(true)
.googleMapsWidget(true)
.build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Washington Monument", "Lincoln Memorial");
assertThat(response.getResult().getMetadata().containsKey("groundingMetadata")).isTrue();

GroundingMetadata groundingMetadata = response.getResult().getMetadata().get("groundingMetadata");
assertThat(groundingMetadata.groundingChunks()).isNotEmpty();
assertThat(groundingMetadata.groundingSupports()).isNotEmpty();
assertThat(groundingMetadata.retrievalQueries()).isNotEmpty();
assertThat(groundingMetadata.googleMapsWidgetContextToken()).isPresent();
}

@Test
void googleMapsToolFlashWithWidget() {
Prompt prompt = new Prompt("Could you recommend some tourist spots around the White House?",
GoogleGenAiChatOptions.builder()
.model(ChatModel.GEMINI_2_0_FLASH)
.googleMaps(true)
.googleMapsWidget(true)
.build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Washington Monument", "Lincoln Memorial");
assertThat(response.getResult().getMetadata().containsKey("groundingMetadata")).isTrue();

GroundingMetadata groundingMetadata = response.getResult().getMetadata().get("groundingMetadata");
assertThat(groundingMetadata.groundingChunks()).isNotEmpty();
assertThat(groundingMetadata.groundingSupports()).isNotEmpty();
assertThat(groundingMetadata.retrievalQueries()).isNotEmpty();
assertThat(groundingMetadata.googleMapsWidgetContextToken()).isPresent();
}

@Test
void googleMapsToolProWithLatLng() {
Prompt prompt = new Prompt("Please tell me about some tourist spots near my current location",
GoogleGenAiChatOptions.builder()
.model(ChatModel.GEMINI_2_5_PRO)
.googleMaps(true)
.latitude(38.890307)
.longitude(-77.036256)
.build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Washington Monument", "Lincoln Memorial");
assertThat(response.getResult().getMetadata().containsKey("groundingMetadata")).isTrue();

GroundingMetadata groundingMetadata = response.getResult().getMetadata().get("groundingMetadata");
assertThat(groundingMetadata.groundingChunks()).isNotEmpty();
assertThat(groundingMetadata.groundingSupports()).isNotEmpty();
assertThat(groundingMetadata.retrievalQueries()).isNotEmpty();
assertThat(groundingMetadata.googleMapsWidgetContextToken()).isNotPresent();
}

@Test
void googleMapsToolFlashWithLatLng() {
Prompt prompt = new Prompt("Please tell me about some tourist spots near my current location",
GoogleGenAiChatOptions.builder()
.model(ChatModel.GEMINI_2_0_FLASH)
.googleMaps(true)
.latitude(38.890307)
.longitude(-77.036256)
.build());
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Washington Monument", "Lincoln Memorial");
assertThat(response.getResult().getMetadata().containsKey("groundingMetadata")).isTrue();

GroundingMetadata groundingMetadata = response.getResult().getMetadata().get("groundingMetadata");
assertThat(groundingMetadata.groundingChunks()).isNotEmpty();
assertThat(groundingMetadata.groundingSupports()).isNotEmpty();
assertThat(groundingMetadata.retrievalQueries()).isNotEmpty();
assertThat(groundingMetadata.googleMapsWidgetContextToken()).isNotPresent();
}

@Test
@Disabled
void testSafetySettings() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ The prefix `spring.ai.google.genai.chat` is the property prefix that lets you co
| spring.ai.google.genai.chat.options.model | Supported https://ai.google.dev/gemini-api/docs/models[Google GenAI Chat models] to use include `gemini-2.0-flash`, `gemini-2.0-flash-lite`, `gemini-pro`, and `gemini-1.5-flash`. | gemini-2.0-flash
| spring.ai.google.genai.chat.options.response-mime-type | Output response mimetype of the generated candidate text. | `text/plain`: (default) Text output or `application/json`: JSON response.
| spring.ai.google.genai.chat.options.google-search-retrieval | Use Google search Grounding feature | `true` or `false`, default `false`.
| spring.ai.google.genai.chat.options.google-maps | Use Grounding with Google Maps tool | `true` or `false`, default `false`.
| spring.ai.google.genai.chat.options.google-maps-widget | Request Google Maps widget context token in the response (requires `google-maps=true`) | `true` or `false`, default `false`.
| spring.ai.google.genai.chat.options.latitude | Latitude for Google Maps grounding (must be set together with `longitude` when provided) | -
| spring.ai.google.genai.chat.options.longitude | Longitude for Google Maps grounding (must be set together with `latitude` when provided) | -
| spring.ai.google.genai.chat.options.temperature | Controls the randomness of the output. Values can range over [0.0,1.0], inclusive. A value closer to 1.0 will produce responses that are more varied, while a value closer to 0.0 will typically result in less surprising responses from the generative. | -
| spring.ai.google.genai.chat.options.top-k | The maximum number of tokens to consider when sampling. The generative uses combined Top-k and nucleus sampling. Top-k sampling considers the set of topK most probable tokens. | -
| spring.ai.google.genai.chat.options.top-p | The maximum cumulative probability of tokens to consider when sampling. The generative uses combined Top-k and nucleus sampling. Nucleus sampling considers the smallest set of tokens whose probability sum is at least topP. | -
Expand Down