Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/main/java/com/google/genai/Models.java
Original file line number Diff line number Diff line change
Expand Up @@ -7245,7 +7245,12 @@ public GenerateContentResponse generateContent(
logger.info(String.format("Automatic function calling remote call %d is done", i));
remainingRemoteCalls--;
if (remainingRemoteCalls == 0) {
/*
* No request is left to send a result with, so the functions are not called at all. The
* turn ends on the model's function call, which the caller can run and answer themselves.
*/
logger.info("Reached max remote calls for automatic function calling.");
break;
}
if (!response.candidates().isPresent()
|| response.candidates().get().isEmpty()
Expand Down
54 changes: 54 additions & 0 deletions src/test/java/com/google/genai/ChatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static org.mockito.Mockito.when;

import com.google.common.collect.ImmutableMap;
import com.google.genai.types.AutomaticFunctionCallingConfig;
import com.google.genai.types.Candidate;
import com.google.genai.types.Content;
import com.google.genai.types.FinishReason;
Expand Down Expand Up @@ -61,7 +62,10 @@ public class ChatTest {
private static final String STREAMING_RESPONSE_CHUNK_3 = " far, far away...";
private static final String NON_STREAMING_RESPONSE = "This is a non-streaming response.";

static int findTheatersCallCount = 0;

public static String findTheaters(String movie, String location, String time) {
findTheatersCallCount++;
return "AMC Metreon 16, AMC Kabuki 8, AMC Theater 11";
}

Expand Down Expand Up @@ -245,6 +249,56 @@ public void testGetHistoryWithAfc() throws Exception {
assert chatSession.getHistory(true).size() == 4;
}

@Test
public void testSpentAfcBudgetLeavesTheFunctionCallUnanswered() throws Exception {
findTheatersCallCount = 0;
String userMessage = "Find theaters for Oppenheimer.";
Content functionCallContent =
Content.fromParts(
Part.fromFunctionCall(
"findTheaters",
ImmutableMap.of(
"movie", "Oppenheimer", "location", "New York, NY", "time", "10:00 PM")));

GenerateContentResponse functionResponse =
GenerateContentResponse.builder()
.candidates(
Candidate.builder()
.content(functionCallContent)
.finishReason(FinishReason.Known.STOP))
.build();

when(mockedClient.request(anyString(), anyString(), anyString(), any()))
.thenReturn(mockedResponse1);
ResponseBody functionResponseBody =
ResponseBody.create(functionResponse.toJson(), MediaType.get("application/json"));
when(mockedResponse1.getBody()).thenReturn(functionResponseBody);

Field apiClientField = Chats.class.getDeclaredField("apiClient");
apiClientField.setAccessible(true);
apiClientField.set(client.chats, mockedClient);
Method method =
ChatTest.class.getDeclaredMethod("findTheaters", String.class, String.class, String.class);
GenerateContentConfig config =
GenerateContentConfig.builder()
.tools(Tool.builder().functions(method))
.automaticFunctionCalling(
AutomaticFunctionCallingConfig.builder().maximumRemoteCalls(1))
.build();
Chat chatSession = client.chats.create(MODEL_ID, config);

GenerateContentResponse response = chatSession.sendMessage(userMessage, null);

// The one request the budget allows is spent being asked, leaving nothing to send a result
// with, so the function is never called.
assert findTheatersCallCount == 0;
// The model's function call is recorded once, not twice, and the turn ends on it so the
// caller can answer it themselves.
assert chatSession.getHistory(false).size() == 2; // user input, function call
assertNotNull(response.functionCalls());
assert response.functionCalls().size() == 1;
}

@Test
public void testMultiTurnChat() throws Exception {

Expand Down
Loading