feat(gemini) Support Safety Settings for VertexAiGeminiChatModel
feat(gemini) not null checks on safetySettings feat(gemini) check safety settings not empty instead of not null feat(gemini) Add VertexAiGeminiSafetySetting wrapper class feat(gemini) Update documentation with new property feat(gemini) Update equals, hashCode, and toString
This commit is contained in:
committed by
Christian Tzolov
parent
7bbd3ef593
commit
c14618fe9a
@@ -36,6 +36,7 @@ import com.google.cloud.vertexai.api.GenerateContentResponse;
|
||||
import com.google.cloud.vertexai.api.GenerationConfig;
|
||||
import com.google.cloud.vertexai.api.GoogleSearchRetrieval;
|
||||
import com.google.cloud.vertexai.api.Part;
|
||||
import com.google.cloud.vertexai.api.SafetySetting;
|
||||
import com.google.cloud.vertexai.api.Schema;
|
||||
import com.google.cloud.vertexai.api.Tool;
|
||||
import com.google.cloud.vertexai.generativeai.GenerativeModel;
|
||||
@@ -46,6 +47,7 @@ import com.google.protobuf.util.JsonFormat;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
import org.springframework.ai.vertexai.gemini.common.VertexAiGeminiSafetySetting;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
@@ -455,7 +457,8 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
|
||||
GenerationConfig generationConfig = this.generationConfig;
|
||||
|
||||
var generativeModelBuilder = new GenerativeModel.Builder().setModelName(this.defaultOptions.getModel())
|
||||
.setVertexAi(this.vertexAI);
|
||||
.setVertexAi(this.vertexAI)
|
||||
.setSafetySettings(toGeminiSafetySettings(this.defaultOptions.getSafetySettings()));
|
||||
|
||||
if (prompt.getOptions() != null) {
|
||||
if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
|
||||
@@ -501,6 +504,11 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
|
||||
generativeModelBuilder.setTools(tools);
|
||||
}
|
||||
|
||||
if (prompt.getOptions() instanceof VertexAiGeminiChatOptions options
|
||||
&& !CollectionUtils.isEmpty(options.getSafetySettings())) {
|
||||
generativeModelBuilder.setSafetySettings(toGeminiSafetySettings(options.getSafetySettings()));
|
||||
}
|
||||
|
||||
generativeModelBuilder.setGenerationConfig(generationConfig);
|
||||
|
||||
GenerativeModel generativeModel = generativeModelBuilder.build();
|
||||
@@ -559,6 +567,16 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements
|
||||
return contents;
|
||||
}
|
||||
|
||||
private List<SafetySetting> toGeminiSafetySettings(List<VertexAiGeminiSafetySetting> safetySettings) {
|
||||
return safetySettings.stream()
|
||||
.map(safetySetting -> SafetySetting.newBuilder()
|
||||
.setCategoryValue(safetySetting.getCategory().getValue())
|
||||
.setThresholdValue(safetySetting.getThreshold().getValue())
|
||||
.setMethodValue(safetySetting.getMethod().getValue())
|
||||
.build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Tool> getFunctionTools(Set<String> functionNames) {
|
||||
|
||||
final var tool = Tool.newBuilder();
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel.ChatModel;
|
||||
import org.springframework.ai.vertexai.gemini.common.VertexAiGeminiSafetySetting;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -117,6 +118,9 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
|
||||
@JsonIgnore
|
||||
private boolean googleSearchRetrieval = false;
|
||||
|
||||
@JsonIgnore
|
||||
private List<VertexAiGeminiSafetySetting> safetySettings = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
private Boolean proxyToolCalls;
|
||||
|
||||
@@ -143,6 +147,7 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
|
||||
options.setFunctions(fromOptions.getFunctions());
|
||||
options.setResponseMimeType(fromOptions.getResponseMimeType());
|
||||
options.setGoogleSearchRetrieval(fromOptions.getGoogleSearchRetrieval());
|
||||
options.setSafetySettings(fromOptions.getSafetySettings());
|
||||
options.setProxyToolCalls(fromOptions.getProxyToolCalls());
|
||||
options.setToolContext(fromOptions.getToolContext());
|
||||
return options;
|
||||
@@ -269,6 +274,15 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
|
||||
this.googleSearchRetrieval = googleSearchRetrieval;
|
||||
}
|
||||
|
||||
public List<VertexAiGeminiSafetySetting> getSafetySettings() {
|
||||
return safetySettings;
|
||||
}
|
||||
|
||||
public void setSafetySettings(List<VertexAiGeminiSafetySetting> safetySettings) {
|
||||
Assert.notNull(safetySettings, "safetySettings must not be null");
|
||||
this.safetySettings = safetySettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getProxyToolCalls() {
|
||||
return this.proxyToolCalls;
|
||||
@@ -304,6 +318,7 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
|
||||
&& Objects.equals(this.responseMimeType, that.responseMimeType)
|
||||
&& Objects.equals(this.functionCallbacks, that.functionCallbacks)
|
||||
&& Objects.equals(this.functions, that.functions)
|
||||
&& Objects.equals(this.safetySettings, that.safetySettings)
|
||||
&& Objects.equals(this.proxyToolCalls, that.proxyToolCalls)
|
||||
&& Objects.equals(this.toolContext, that.toolContext);
|
||||
}
|
||||
@@ -312,7 +327,7 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
|
||||
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.proxyToolCalls, this.toolContext);
|
||||
this.googleSearchRetrieval, this.safetySettings, this.proxyToolCalls, this.toolContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -322,7 +337,7 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
|
||||
+ this.candidateCount + ", maxOutputTokens=" + this.maxOutputTokens + ", model='" + this.model + '\''
|
||||
+ ", responseMimeType='" + this.responseMimeType + '\'' + ", functionCallbacks="
|
||||
+ this.functionCallbacks + ", functions=" + this.functions + ", googleSearchRetrieval="
|
||||
+ this.googleSearchRetrieval + '}';
|
||||
+ this.googleSearchRetrieval + ", safetySettings=" + this.safetySettings + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -408,6 +423,12 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSafetySettings(List<VertexAiGeminiSafetySetting> safetySettings) {
|
||||
Assert.notNull(safetySettings, "safetySettings must not be null");
|
||||
this.options.safetySettings = safetySettings;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withProxyToolCalls(boolean proxyToolCalls) {
|
||||
this.options.proxyToolCalls = proxyToolCalls;
|
||||
return this;
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.springframework.ai.vertexai.gemini.common;
|
||||
|
||||
public class VertexAiGeminiSafetySetting {
|
||||
|
||||
/**
|
||||
* Enum representing different threshold levels for blocking harmful content.
|
||||
*/
|
||||
public enum HarmBlockThreshold {
|
||||
|
||||
HARM_BLOCK_THRESHOLD_UNSPECIFIED(0), BLOCK_LOW_AND_ABOVE(1), BLOCK_MEDIUM_AND_ABOVE(2), BLOCK_ONLY_HIGH(3),
|
||||
BLOCK_NONE(4), OFF(5);
|
||||
|
||||
private final int value;
|
||||
|
||||
HarmBlockThreshold(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing methods for evaluating harmful content.
|
||||
*/
|
||||
public enum HarmBlockMethod {
|
||||
|
||||
HARM_BLOCK_METHOD_UNSPECIFIED(0), SEVERITY(1), PROBABILITY(2);
|
||||
|
||||
private final int value;
|
||||
|
||||
HarmBlockMethod(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing different categories of harmful content.
|
||||
*/
|
||||
public enum HarmCategory {
|
||||
|
||||
HARM_CATEGORY_UNSPECIFIED(0), HARM_CATEGORY_HATE_SPEECH(1), HARM_CATEGORY_DANGEROUS_CONTENT(2),
|
||||
HARM_CATEGORY_HARASSMENT(3), HARM_CATEGORY_SEXUALLY_EXPLICIT(4);
|
||||
|
||||
private final int value;
|
||||
|
||||
HarmCategory(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private HarmCategory category;
|
||||
|
||||
private HarmBlockThreshold threshold;
|
||||
|
||||
private HarmBlockMethod method;
|
||||
|
||||
// Default constructor
|
||||
public VertexAiGeminiSafetySetting() {
|
||||
this.category = HarmCategory.HARM_CATEGORY_UNSPECIFIED;
|
||||
this.threshold = HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED;
|
||||
this.method = HarmBlockMethod.HARM_BLOCK_METHOD_UNSPECIFIED;
|
||||
}
|
||||
|
||||
// Constructor with all fields
|
||||
public VertexAiGeminiSafetySetting(HarmCategory category, HarmBlockThreshold threshold, HarmBlockMethod method) {
|
||||
this.category = category;
|
||||
this.threshold = threshold;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public HarmCategory getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(HarmCategory category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public HarmBlockThreshold getThreshold() {
|
||||
return threshold;
|
||||
}
|
||||
|
||||
public void setThreshold(HarmBlockThreshold threshold) {
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public HarmBlockMethod getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(HarmBlockMethod method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SafetySetting{" + "category=" + category + ", threshold=" + threshold + ", method=" + method + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
VertexAiGeminiSafetySetting that = (VertexAiGeminiSafetySetting) o;
|
||||
|
||||
if (category != that.category)
|
||||
return false;
|
||||
if (threshold != that.threshold)
|
||||
return false;
|
||||
return method == that.method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = category != null ? category.hashCode() : 0;
|
||||
result = 31 * result + (threshold != null ? threshold.hashCode() : 0);
|
||||
result = 31 * result + (method != null ? method.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private HarmCategory category = HarmCategory.HARM_CATEGORY_UNSPECIFIED;
|
||||
|
||||
private HarmBlockThreshold threshold = HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED;
|
||||
|
||||
private HarmBlockMethod method = HarmBlockMethod.HARM_BLOCK_METHOD_UNSPECIFIED;
|
||||
|
||||
public Builder withCategory(HarmCategory category) {
|
||||
this.category = category;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withThreshold(HarmBlockThreshold threshold) {
|
||||
this.threshold = threshold;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMethod(HarmBlockMethod method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public VertexAiGeminiSafetySetting build() {
|
||||
return new VertexAiGeminiSafetySetting(category, threshold, method);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,6 +41,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.common.VertexAiGeminiSafetySetting;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
@@ -92,6 +93,18 @@ class VertexAiGeminiChatModelIT {
|
||||
assertThat(response.getResult().getOutput().getText()).containsAnyOf("Blackbeard", "Bartholomew");
|
||||
}
|
||||
|
||||
@Test
|
||||
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().withSafetySettings(safetySettings).build());
|
||||
ChatResponse response = this.chatModel.call(prompt);
|
||||
assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("SAFETY");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Prompt createPrompt(VertexAiGeminiChatOptions chatOptions) {
|
||||
String request = "Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.";
|
||||
|
||||
@@ -84,6 +84,7 @@ The prefix `spring.ai.vertex.ai.gemini.chat` is the property prefix that lets yo
|
||||
| spring.ai.vertex.ai.gemini.chat.options.presencePenalty | | -
|
||||
| spring.ai.vertex.ai.gemini.chat.options.functions | List of functions, identified by their names, to enable for function calling in a single prompt requests. Functions with those names must exist in the functionCallbacks registry. | -
|
||||
| spring.ai.vertex.ai.gemini.chat.options.proxy-tool-calls | If true, the Spring AI will not handle the function calls internally, but will proxy them to the client. Then is the client's responsibility to handle the function calls, dispatch them to the appropriate function, and return the results. If false (the default), the Spring AI will handle the function calls internally. Applicable only for chat models with function calling support | false
|
||||
| spring.ai.vertex.ai.gemini.chat.options.safetySettings | List of safety settings to control safety filters, as defined by https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters[Vertex AI Safety Filters]. Each safety setting can have a method, threshold, and category. | -
|
||||
|
||||
|====
|
||||
|
||||
|
||||
Reference in New Issue
Block a user