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
82 changes: 65 additions & 17 deletions src/main/java/com/google/genai/AfcUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.google.genai;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Arrays.stream;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
Expand All @@ -28,9 +27,12 @@
import com.google.genai.types.Part;
import com.google.genai.types.Tool;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;

Expand Down Expand Up @@ -95,6 +97,15 @@ static ImmutableMap<String, Method> getFunctionMap(GenerateContentConfig config)
for (Tool tool : config.tools().get()) {
if (tool.functions().isPresent() && !tool.functions().get().isEmpty()) {
for (Method method : tool.functions().get()) {
if (!Modifier.isStatic(method.getModifiers())
&& (!tool.functionInstances().isPresent()
|| !tool.functionInstances().get().containsKey(method)
|| tool.functionInstances().get().get(method) == null)) {
throw new IllegalArgumentException(
"Instance methods are not supported without an instance. Please provide an"
+ " instance for method: "
+ method.getName());
}
functionMapBuilder.put(method.getName(), method);
}
}
Expand All @@ -103,8 +114,26 @@ static ImmutableMap<String, Method> getFunctionMap(GenerateContentConfig config)
return functionMapBuilder.buildOrThrow();
}

static ImmutableMap<String, Object> getFunctionInstanceMap(GenerateContentConfig config) {
ImmutableMap.Builder<String, Object> functionInstanceMapBuilder = ImmutableMap.builder();
if (config != null && config.tools().isPresent() && !config.tools().get().isEmpty()) {
for (Tool tool : config.tools().get()) {
if (tool.functionInstances().isPresent() && !tool.functionInstances().get().isEmpty()) {
for (Map.Entry<Method, Object> entry : tool.functionInstances().get().entrySet()) {
if (entry.getValue() != null) {
functionInstanceMapBuilder.put(entry.getKey().getName(), entry.getValue());
}
}
}
}
}
return functionInstanceMapBuilder.buildOrThrow();
}

static ImmutableList<Part> getFunctionResponseParts(
GenerateContentResponse response, ImmutableMap<String, Method> functionMap) {
GenerateContentResponse response,
ImmutableMap<String, Method> functionMap,
ImmutableMap<String, Object> functionInstanceMap) {
ImmutableList.Builder<Part> functionResponsePartsBuilder = ImmutableList.builder();
ImmutableList<Part> responseParts = response.parts();
ImmutableList<FunctionCall> functionCalls = response.functionCalls();
Expand All @@ -115,14 +144,21 @@ static ImmutableList<Part> getFunctionResponseParts(
return functionResponsePartsBuilder.build();
}
for (FunctionCall functionCall : functionCalls) {
if (!functionCall.name().isPresent() || functionCall.name().get().isEmpty()) {
continue;
}
String funcName = functionCall.name().get();
if (funcName == null || !functionMap.containsKey(funcName)) {
if (!functionMap.containsKey(funcName)) {
continue;
}
Method method = functionMap.get(funcName);
ImmutableMap<String, Object> args = ImmutableMap.copyOf(functionCall.args().get());
Object instance = functionInstanceMap != null ? functionInstanceMap.get(funcName) : null;
Map<String, Object> args = functionCall.args().orElse(Collections.emptyMap());
if (args == null) {
args = Collections.emptyMap();
}
try {
Object funcResponse = getFunctionResponse(method, args);
Object funcResponse = getFunctionResponse(method, args, instance);
if (funcResponse == null) {
functionResponsePartsBuilder.add(
Part.fromFunctionResponse(funcName, ImmutableMap.of("result", "")));
Expand All @@ -138,6 +174,11 @@ static ImmutableList<Part> getFunctionResponseParts(
return functionResponsePartsBuilder.build();
}

static ImmutableList<Part> getFunctionResponseParts(
GenerateContentResponse response, ImmutableMap<String, Method> functionMap) {
return getFunctionResponseParts(response, functionMap, ImmutableMap.of());
}

static boolean shouldDisableAfc(GenerateContentConfig config) {
if (config == null) {
return false;
Expand Down Expand Up @@ -206,11 +247,10 @@ static boolean shouldAppendAfcHistory(GenerateContentConfig config) {
}

private static Object getFunctionResponse(
Method method, ImmutableMap<String, Object> argsFromModel) throws Exception {
Method method, Map<String, Object> argsFromModel, Object instance) throws Exception {
List<Object> argsListFromModel = new ArrayList<>();
ImmutableList<String> methodParameterNames =
stream(method.getParameters()).map(Parameter::getName).collect(toImmutableList());
for (String parameterName : methodParameterNames) {
for (Parameter parameter : method.getParameters()) {
String parameterName = parameter.getName();
if (!argsFromModel.containsKey(parameterName)) {
throw new IllegalArgumentException(
"The parameter \""
Expand All @@ -220,17 +260,25 @@ private static Object getFunctionResponse(
+ argsFromModel);
}
Object argValueFromModel = argsFromModel.get(parameterName);
String className = argValueFromModel.getClass().getName();

if (className.equals("java.lang.String")) {
if (argValueFromModel == null) {
if (parameter.getType().isPrimitive()) {
throw new IllegalArgumentException(
"The parameter \""
+ parameterName
+ "\" is a primitive type "
+ parameter.getType().getName()
+ " but received null from the model.");
}
argsListFromModel.add(null);
} else if (argValueFromModel instanceof String) {
argsListFromModel.add(argValueFromModel);
} else if (className.equals("java.lang.Integer")) {
} else if (argValueFromModel instanceof Integer) {
argsListFromModel.add(Integer.parseInt(argValueFromModel.toString()));
} else if (className.equals("java.lang.Double")) {
} else if (argValueFromModel instanceof Double) {
argsListFromModel.add(Double.parseDouble(argValueFromModel.toString()));
} else if (className.equals("java.lang.Float")) {
} else if (argValueFromModel instanceof Float) {
argsListFromModel.add(Float.parseFloat(argValueFromModel.toString()));
} else if (className.equals("java.lang.Boolean")) {
} else if (argValueFromModel instanceof Boolean) {
argsListFromModel.add(Boolean.parseBoolean(argValueFromModel.toString()));
} else if (argValueFromModel instanceof List) {
argsListFromModel.add(argValueFromModel);
Expand All @@ -243,7 +291,7 @@ private static Object getFunctionResponse(
}
}

return method.invoke(null, argsListFromModel.toArray());
return method.invoke(instance, argsListFromModel.toArray());
}

private AfcUtil() {}
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/google/genai/AsyncModels.java
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ private CompletableFuture<AfcLoopResult> privateGenerateContentLoopAsync(
List<Content> contents,
GenerateContentConfig transformedConfig,
ImmutableMap<String, Method> functionMap,
ImmutableMap<String, Object> functionInstanceMap,
List<Content> automaticFunctionCallingHistory,
int remainingRemoteCalls,
int initialMaxCalls) {
Expand Down Expand Up @@ -652,7 +653,7 @@ private CompletableFuture<AfcLoopResult> privateGenerateContentLoopAsync(
}

ImmutableList<Part> functionResponseParts =
AfcUtil.getFunctionResponseParts(response, functionMap);
AfcUtil.getFunctionResponseParts(response, functionMap, functionInstanceMap);
if (functionResponseParts.isEmpty()) {
return CompletableFuture.completedFuture(
new AfcLoopResult(response, automaticFunctionCallingHistory));
Expand All @@ -671,6 +672,7 @@ private CompletableFuture<AfcLoopResult> privateGenerateContentLoopAsync(
newHistory,
transformedConfig,
functionMap,
functionInstanceMap,
newHistory,
remainingRemoteCalls - 1,
initialMaxCalls);
Expand Down Expand Up @@ -710,6 +712,7 @@ public CompletableFuture<GenerateContentResponse> generateContent(
return privateGenerateContent(model, contents, transformedConfig);
}
ImmutableMap<String, Method> functionMap = AfcUtil.getFunctionMap(config);
ImmutableMap<String, Object> functionInstanceMap = AfcUtil.getFunctionInstanceMap(config);
if (functionMap.isEmpty()) {
return privateGenerateContent(model, contents, transformedConfig);
}
Expand All @@ -725,6 +728,7 @@ public CompletableFuture<GenerateContentResponse> generateContent(
contents,
transformedConfig,
functionMap,
functionInstanceMap,
automaticFunctionCallingHistory,
maxRemoteCalls,
maxRemoteCalls)
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/google/genai/Models.java
Original file line number Diff line number Diff line change
Expand Up @@ -7227,6 +7227,7 @@ public GenerateContentResponse generateContent(
return privateGenerateContent(model, contents, transformedConfig);
}
ImmutableMap<String, Method> functionMap = AfcUtil.getFunctionMap(config);
ImmutableMap<String, Object> functionInstanceMap = AfcUtil.getFunctionInstanceMap(config);
if (functionMap.isEmpty()) {
return privateGenerateContent(model, contents, transformedConfig);
}
Expand Down Expand Up @@ -7254,7 +7255,7 @@ public GenerateContentResponse generateContent(
break;
}
ImmutableList<Part> functionResponseParts =
AfcUtil.getFunctionResponseParts(response, functionMap);
AfcUtil.getFunctionResponseParts(response, functionMap, functionInstanceMap);
if (functionResponseParts.isEmpty()) {
break;
}
Expand Down
8 changes: 0 additions & 8 deletions src/main/java/com/google/genai/types/FunctionDeclaration.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.genai.JsonSerializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
Expand Down Expand Up @@ -332,7 +331,6 @@ public static FunctionDeclaration fromJson(String jsonString) {
* Creates a FunctionDeclaration instance from a {@link Method} instance.
*
* @param method The {@link Method} instance to be parsed into the FunctionDeclaration instance.
* Only static method is supported.
* @param orderedParameterNames Optional ordered parameter names. If not provided, parameter names
* will be retrieved via reflection.
* @return A FunctionDeclaration instance.
Expand All @@ -346,18 +344,12 @@ public static FunctionDeclaration fromMethod(Method method, String... orderedPar
*
* @param functionDescription Description of the function.
* @param method The {@link Method} instance to be parsed into the FunctionDeclaration instance.
* Only static method is supported.
* @param orderedParameterNames Optional ordered parameter names. If not provided, parameter names
* will be retrieved via reflection.
* @return A FunctionDeclaration instance.
*/
public static FunctionDeclaration fromMethod(
String functionDescription, Method method, String... orderedParameterNames) {
if (!Modifier.isStatic(method.getModifiers())) {
throw new IllegalArgumentException(
"Instance methods are not supported. Please use static methods.");
}

Schema.Builder parametersBuilder = Schema.builder().type("OBJECT");

Parameter[] parameters = method.getParameters();
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/com/google/genai/types/Tool.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.auto.value.AutoValue;
import com.google.common.base.VerifyException;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.genai.JsonSerializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/** Tool details of a tool that the model may use to generate a response. */
Expand Down Expand Up @@ -127,6 +132,13 @@ public abstract class Tool extends JsonSerializable {
@JsonProperty("exaAiSearch")
public abstract Optional<ToolExaAiSearch> exaAiSearch();

/**
* The java.lang.reflect.Method instance to target instance mapping. If provided, it will be used
* to invoke instance methods during automatic function calling.
*/
@JsonIgnore
public abstract Optional<Map<Method, Object>> functionInstances();

/** Instantiates a builder for Tool. */
@ExcludeFromGeneratedCoverageReport
public static Builder builder() {
Expand Down Expand Up @@ -608,6 +620,56 @@ public Builder clearExaAiSearch() {
return exaAiSearch(Optional.empty());
}

/**
* Setter for functionInstances.
*
* <p>functionInstances: The java.lang.reflect.Method instance to target instance mapping. If
* provided, it will be used to invoke instance methods during automatic function calling.
*/
@JsonIgnore
public abstract Builder functionInstances(Map<Method, Object> functionInstances);

@ExcludeFromGeneratedCoverageReport
abstract Builder functionInstances(Optional<Map<Method, Object>> functionInstances);

/** Clears the value of functionInstances field. */
@ExcludeFromGeneratedCoverageReport
@CanIgnoreReturnValue
public Builder clearFunctionInstances() {
return functionInstances(Optional.empty());
}

public Builder functionWithInstance(Method method, Object instance) {
try {
Field fFunctions = this.getClass().getDeclaredField("functions");
fFunctions.setAccessible(true);
@SuppressWarnings("unchecked")
Optional<List<Method>> optFunctions = (Optional<List<Method>>) fFunctions.get(this);
List<Method> currentFunctions =
new ArrayList<>(
optFunctions != null && optFunctions.isPresent()
? optFunctions.get()
: new ArrayList<>());
currentFunctions.add(method);
functions(currentFunctions);

Field fInstances = this.getClass().getDeclaredField("functionInstances");
fInstances.setAccessible(true);
@SuppressWarnings("unchecked")
Optional<Map<Method, Object>> optInstances =
(Optional<Map<Method, Object>>) fInstances.get(this);
Map<Method, Object> currentInstances =
new HashMap<>(
optInstances != null && optInstances.isPresent()
? optInstances.get()
: new HashMap<>());
currentInstances.put(method, instance);
return functionInstances(currentInstances);
} catch (Exception e) {
throw new VerifyException("Failed to add function with instance", e);
}
}

public abstract Tool build();
}

Expand Down
Loading
Loading