|
36 | 36 | import com.google.adk.flows.llmflows.RequestProcessor.RequestProcessingResult; |
37 | 37 | import com.google.adk.flows.llmflows.ResponseProcessor.ResponseProcessingResult; |
38 | 38 | import com.google.adk.models.CacheMetadata; |
| 39 | +import com.google.adk.models.LlmCallsLimitExceededException; |
39 | 40 | import com.google.adk.models.LlmRequest; |
40 | 41 | import com.google.adk.models.LlmResponse; |
| 42 | +import com.google.adk.runner.InMemoryRunner; |
| 43 | +import com.google.adk.sessions.Session; |
41 | 44 | import com.google.adk.testing.TestLlm; |
42 | 45 | import com.google.adk.tools.BaseTool; |
43 | 46 | import com.google.adk.tools.BaseToolset; |
44 | 47 | import com.google.adk.tools.ToolContext; |
45 | 48 | import com.google.common.collect.ImmutableList; |
46 | 49 | import com.google.common.collect.ImmutableMap; |
| 50 | +import com.google.common.collect.ImmutableSet; |
47 | 51 | import com.google.genai.types.Content; |
48 | 52 | import com.google.genai.types.FinishReason; |
49 | 53 | import com.google.genai.types.FunctionCall; |
|
60 | 64 | import io.reactivex.rxjava3.core.Maybe; |
61 | 65 | import io.reactivex.rxjava3.core.Single; |
62 | 66 | import io.reactivex.rxjava3.schedulers.Schedulers; |
| 67 | +import io.reactivex.rxjava3.subscribers.TestSubscriber; |
63 | 68 | import java.time.Instant; |
| 69 | +import java.util.ArrayList; |
64 | 70 | import java.util.List; |
65 | 71 | import java.util.Map; |
66 | 72 | import java.util.Optional; |
67 | 73 | import java.util.concurrent.atomic.AtomicInteger; |
68 | 74 | import java.util.function.Function; |
| 75 | +import java.util.stream.Stream; |
69 | 76 | import org.junit.Test; |
70 | 77 | import org.junit.runner.RunWith; |
71 | 78 | import org.junit.runners.JUnit4; |
@@ -218,6 +225,47 @@ public void run_withFunctionCallsAndMaxSteps_stopsAfterMaxSteps() { |
218 | 225 | Content.fromParts(Part.fromFunctionResponse("my_function", testResponse))); |
219 | 226 | } |
220 | 227 |
|
| 228 | + @Test |
| 229 | + public void run_modelRespondingOnSubscribingThread_reachesMaxLlmCallsWithoutStackOverflow() { |
| 230 | + int maxLlmCalls = 2_000; |
| 231 | + List<Long> stackDepths = new ArrayList<>(); |
| 232 | + TestLlm testLlm = createTestLlmAlwaysCallingMyFunction(stackDepths); |
| 233 | + InvocationContext invocationContext = |
| 234 | + createInvocationContext( |
| 235 | + createTestAgentWithMyFunction(testLlm), |
| 236 | + RunConfig.builder().maxLlmCalls(maxLlmCalls).build()); |
| 237 | + BaseLlmFlow baseLlmFlow = createBaseLlmFlowWithoutProcessors(); |
| 238 | + |
| 239 | + TestSubscriber<Event> subscriber = baseLlmFlow.run(invocationContext).test(); |
| 240 | + |
| 241 | + subscriber.assertError(LlmCallsLimitExceededException.class); |
| 242 | + assertThat(testLlm.getRequests()).hasSize(maxLlmCalls); |
| 243 | + assertThat(ImmutableSet.copyOf(stackDepths)).hasSize(1); |
| 244 | + } |
| 245 | + |
| 246 | + @Test |
| 247 | + public void run_throughInMemoryRunner_reachesMaxLlmCallsWithoutStackOverflow() { |
| 248 | + // Unlike the test above, each step also waits for the Runner to persist its events. |
| 249 | + int maxLlmCalls = 500; |
| 250 | + List<Long> stackDepths = new ArrayList<>(); |
| 251 | + TestLlm testLlm = createTestLlmAlwaysCallingMyFunction(stackDepths); |
| 252 | + InMemoryRunner runner = new InMemoryRunner(createTestAgentWithMyFunction(testLlm)); |
| 253 | + Session session = runner.sessionService().createSession(runner.appName(), "user").blockingGet(); |
| 254 | + |
| 255 | + TestSubscriber<Event> subscriber = |
| 256 | + runner |
| 257 | + .runAsync( |
| 258 | + "user", |
| 259 | + session.id(), |
| 260 | + Content.builder().role("user").parts(Part.fromText("call my_function")).build(), |
| 261 | + RunConfig.builder().maxLlmCalls(maxLlmCalls).build()) |
| 262 | + .test(); |
| 263 | + |
| 264 | + subscriber.assertError(LlmCallsLimitExceededException.class); |
| 265 | + assertThat(testLlm.getRequests()).hasSize(maxLlmCalls); |
| 266 | + assertThat(ImmutableSet.copyOf(stackDepths)).hasSize(1); |
| 267 | + } |
| 268 | + |
221 | 269 | @Test |
222 | 270 | public void run_withLongRunningFunctionCall_returnsCorrectEventsWithLongRunningToolIds() { |
223 | 271 | Content firstContent = |
@@ -671,6 +719,26 @@ private static ResponseProcessor createResponseProcessor( |
671 | 719 | ResponseProcessingResult.create(responseUpdater.apply(response), ImmutableList.of())); |
672 | 720 | } |
673 | 721 |
|
| 722 | + /** Returns a model that always calls {@code my_function} and records its stack depth per call. */ |
| 723 | + private static TestLlm createTestLlmAlwaysCallingMyFunction(List<Long> stackDepths) { |
| 724 | + Content contentWithFunctionCall = |
| 725 | + Content.fromParts(Part.fromFunctionCall("my_function", ImmutableMap.of("arg1", "value1"))); |
| 726 | + return createTestLlm( |
| 727 | + () -> { |
| 728 | + stackDepths.add(StackWalker.getInstance().walk(Stream::count)); |
| 729 | + // Flowable.just answers on the subscribing thread, like the built-in models. |
| 730 | + return Flowable.just(createLlmResponse(contentWithFunctionCall)); |
| 731 | + }); |
| 732 | + } |
| 733 | + |
| 734 | + private static LlmAgent createTestAgentWithMyFunction(TestLlm testLlm) { |
| 735 | + ImmutableMap<String, Object> testResponse = |
| 736 | + ImmutableMap.<String, Object>of("response", "response for my_function"); |
| 737 | + return createTestAgentBuilder(testLlm) |
| 738 | + .tools(ImmutableList.of(new TestTool("my_function", testResponse))) |
| 739 | + .build(); |
| 740 | + } |
| 741 | + |
674 | 742 | private static class TestTool extends BaseTool { |
675 | 743 | private final Map<String, Object> response; |
676 | 744 |
|
|
0 commit comments