Repository navigation
fix(agent_tool): wrap input_schema payload in ReAct prompt and propagate tool_choice to LiteLLM - #5924
fix(agent_tool): wrap input_schema payload in ReAct prompt and propagate tool_choice to LiteLLM#5924ecanlar wants to merge 13 commits into
Conversation
…ol_choice to LiteLLM When AgentTool uses input_schema, the inner agent receives a raw JSON blob that causes Claude models to skip the tool-calling loop (ReAct). Fix by wrapping the payload in a natural-language instruction. Also propagate tool_config.function_calling_config.mode to LiteLLM's tool_choice parameter so callers can enforce tool_choice='required'. Addresses google#773. Fixes: google#5926
ded18ca to
6168c49
Compare
|
Response from ADK Triaging Agent Hello @ecanlar, thank you for creating this PR! We appreciate your contribution to fixing the LiteLLM/Claude Currently, the following items are missing:
Providing this information helps ensure high code quality, prevents future regressions, and makes the review process smoother. Thank you! |
…tool_choice propagation - Update existing _get_completion_inputs call sites to handle new 5-tuple return value (adds tool_choice as 5th element) - Add 3 tests for AgentTool.run_async: verifies message is passed verbatim without input_schema, and is wrapped in a natural-language instruction with input_schema (PR google#5924 fix) - Add 8 tests for LiteLLM tool_choice propagation: covers _get_completion_inputs returning correct tool_choice for ANY/NONE/AUTO modes and None when no tool_config; covers generate_content_async correctly including/omitting tool_choice in completion_args
- Fix: only apply the ReAct wrapper in agent_tool.py when output_schema is not set on the inner agent, preventing breaking of single-shot structured output mode - Add regression test test_run_async_with_input_and_output_schema_passes_raw_json documenting that raw JSON is passed when both input_schema and output_schema are set - Apply pre-commit formatting fixes (isort + pyink)
23d582a to
67d087a
Compare
…st HTTP call Move the ValueError from module import time to the lazy _get_session() factory so that importing the package (e.g. pytest collection) does not fail in environments where GITHUB_TOKEN is not set.
f8c61e3 to
7c23d83
Compare
|
Good catch — confirmed the guard was not in place. Fixed in the latest commit:
Two new tests cover both paths; 10 |
|
Hi @ecanlar , Thank you for your contribution! We appreciate you taking the time to submit this pull request. |
|
/adk-pr-analyze |
|
Hi @ecanlar , Your PR has been received by the team and is currently under review. We will provide feedback as soon as we have an update to share. |
|
Hi @GWeale , can you please review this. |
|
Hi @ecanlar , can you please fix formatting errors. |
LiteLLM rejects tool_choice="required"/"none" when the tools list is falsy. tool_choice and tools were derived from independent config fields with no cross-check, so a caller setting mode=ANY with no function_declarations would reach acompletion() with tools=None and tool_choice="required". Add a guard in _get_completion_inputs to coerce tool_choice to None when tools is falsy, and mirror the same coercion in generate_content_async after the "functions" additional-arg override that already nulls tools. Add two tests covering both paths; update four existing tool_choice tests to include a FunctionDeclaration so they remain valid under the new guard.
b680ca5 to
9cd8f31
Compare
|
i fixed the formatting erros. @rohityan |
…ice to LiteLLM Merge #5924 END_PUBLIC ## Summary Fixes two issues that prevent Claude-family models from entering the ReAct tool-calling loop when used via LiteLLM inside a nested `AgentTool`: 1. **`AgentTool` with `input_schema`** — the serialized JSON payload sent as the first message causes Claude to interpret the request as already complete and respond directly without calling any tools. 2. **`tool_choice` not propagated** — `llm_request.config.tool_config.function_calling_config.mode` was not forwarded to LiteLLM's `completion_args`, so callers could not enforce tool use at the request level. ## Changes ### `src/google/adk/tools/agent_tool.py` Wrap the serialized `input_schema` JSON in a natural-language instruction that explicitly asks the inner agent to use its available tools before producing a response. This keeps Claude in ReAct mode regardless of the message content format. ### `src/google/adk/models/lite_llm.py` Read `llm_request.config.tool_config.function_calling_config.mode` and map it to LiteLLM's `tool_choice` parameter: - `ANY` → `"required"` - `NONE` → `"none"` - `AUTO` → provider default (unchanged, key omitted from `completion_args`) `_get_completion_inputs` now returns a 5-tuple `(messages, tools, response_format, generation_params, tool_choice)`. ## Unit Tests Added ### `tests/unittests/tools/test_agent_tool.py` - `test_run_async_no_input_schema_passes_request_unchanged`: without `input_schema`, the content passed to the inner runner is `args['request']` verbatim. - `test_run_async_with_input_schema_wraps_in_natural_language`: with `input_schema`, the text begins with `"Process the following structured request"`, contains `"Request:\n"` followed by the JSON payload, and is not a bare JSON blob. - `test_run_async_with_input_schema_text_not_raw_json`: asserts the text does not start with `{`. ### `tests/unittests/models/test_litellm.py` - `test_get_completion_inputs_tool_choice_none_without_tool_config`: `tool_choice` is `None` with no `tool_config`. - `test_get_completion_inputs_tool_choice_required_for_any_mode`: returns `"required"` for `ANY` mode. - `test_get_completion_inputs_tool_choice_none_for_none_mode`: returns `"none"` for `NONE` mode. - `test_get_completion_inputs_tool_choice_none_for_auto_mode`: returns `None` for `AUTO` mode. - `test_generate_content_async_propagates_tool_choice_required`: `acompletion` receives `tool_choice="required"` for `ANY`. - `test_generate_content_async_propagates_tool_choice_none_mode`: `acompletion` receives `tool_choice="none"` for `NONE`. - `test_generate_content_async_omits_tool_choice_for_auto_mode`: `tool_choice` key absent from `completion_args` for `AUTO`. - `test_generate_content_async_omits_tool_choice_without_tool_config`: `tool_choice` key absent when no `tool_config`. Also updated all existing `_get_completion_inputs` call sites (10 occurrences) to unpack the new 5-tuple. ## Pytest Results ``` tests/unittests/tools/test_agent_tool.py + tests/unittests/models/test_litellm.py 1 failed (pre-existing: test_custom_schema[GOOGLE_AI] — unrelated to this PR), 302 passed, 1 skipped in 2.98s New tests: 11 passed (3 agent_tool + 8 litellm) ``` The 1 pre-existing failure (`test_custom_schema[GOOGLE_AI]`) is a `pydantic.ValidationError` that reproduces on the base branch before any of these changes and is unrelated to this fix. ## Related - Fixes #5926 - Addresses #773 (expose tool_choice to callers via FunctionCallingConfig) - Related #1063 (fixed FunctionDeclaration description in v1.20, different issue) Co-authored-by: George Weale <gweale@google.com> COPYBARA_INTEGRATE_REVIEW=#5924 from ecanlar:fix/agent-tool-input-schema-tool-choice-litellm 9cd8f31 PiperOrigin-RevId: 955433983
…ice to LiteLLM Merge google#5924 END_PUBLIC ## Summary Fixes two issues that prevent Claude-family models from entering the ReAct tool-calling loop when used via LiteLLM inside a nested `AgentTool`: 1. **`AgentTool` with `input_schema`** — the serialized JSON payload sent as the first message causes Claude to interpret the request as already complete and respond directly without calling any tools. 2. **`tool_choice` not propagated** — `llm_request.config.tool_config.function_calling_config.mode` was not forwarded to LiteLLM's `completion_args`, so callers could not enforce tool use at the request level. ## Changes ### `src/google/adk/tools/agent_tool.py` Wrap the serialized `input_schema` JSON in a natural-language instruction that explicitly asks the inner agent to use its available tools before producing a response. This keeps Claude in ReAct mode regardless of the message content format. ### `src/google/adk/models/lite_llm.py` Read `llm_request.config.tool_config.function_calling_config.mode` and map it to LiteLLM's `tool_choice` parameter: - `ANY` → `"required"` - `NONE` → `"none"` - `AUTO` → provider default (unchanged, key omitted from `completion_args`) `_get_completion_inputs` now returns a 5-tuple `(messages, tools, response_format, generation_params, tool_choice)`. ## Unit Tests Added ### `tests/unittests/tools/test_agent_tool.py` - `test_run_async_no_input_schema_passes_request_unchanged`: without `input_schema`, the content passed to the inner runner is `args['request']` verbatim. - `test_run_async_with_input_schema_wraps_in_natural_language`: with `input_schema`, the text begins with `"Process the following structured request"`, contains `"Request:\n"` followed by the JSON payload, and is not a bare JSON blob. - `test_run_async_with_input_schema_text_not_raw_json`: asserts the text does not start with `{`. ### `tests/unittests/models/test_litellm.py` - `test_get_completion_inputs_tool_choice_none_without_tool_config`: `tool_choice` is `None` with no `tool_config`. - `test_get_completion_inputs_tool_choice_required_for_any_mode`: returns `"required"` for `ANY` mode. - `test_get_completion_inputs_tool_choice_none_for_none_mode`: returns `"none"` for `NONE` mode. - `test_get_completion_inputs_tool_choice_none_for_auto_mode`: returns `None` for `AUTO` mode. - `test_generate_content_async_propagates_tool_choice_required`: `acompletion` receives `tool_choice="required"` for `ANY`. - `test_generate_content_async_propagates_tool_choice_none_mode`: `acompletion` receives `tool_choice="none"` for `NONE`. - `test_generate_content_async_omits_tool_choice_for_auto_mode`: `tool_choice` key absent from `completion_args` for `AUTO`. - `test_generate_content_async_omits_tool_choice_without_tool_config`: `tool_choice` key absent when no `tool_config`. Also updated all existing `_get_completion_inputs` call sites (10 occurrences) to unpack the new 5-tuple. ## Pytest Results ``` tests/unittests/tools/test_agent_tool.py + tests/unittests/models/test_litellm.py 1 failed (pre-existing: test_custom_schema[GOOGLE_AI] — unrelated to this PR), 302 passed, 1 skipped in 2.98s New tests: 11 passed (3 agent_tool + 8 litellm) ``` The 1 pre-existing failure (`test_custom_schema[GOOGLE_AI]`) is a `pydantic.ValidationError` that reproduces on the base branch before any of these changes and is unrelated to this fix. ## Related - Fixes google#5926 - Addresses google#773 (expose tool_choice to callers via FunctionCallingConfig) - Related google#1063 (fixed FunctionDeclaration description in v1.20, different issue) Co-authored-by: George Weale <gweale@google.com> COPYBARA_INTEGRATE_REVIEW=google#5924 from ecanlar:fix/agent-tool-input-schema-tool-choice-litellm 9cd8f31 PiperOrigin-RevId: 955433983
Summary
Fixes two issues that prevent Claude-family models from entering the ReAct tool-calling loop when used via LiteLLM inside a nested
AgentTool:AgentToolwithinput_schema— the serialized JSON payload sent as the first message causes Claude to interpret the request as already complete and respond directly without calling any tools.tool_choicenot propagated —llm_request.config.tool_config.function_calling_config.modewas not forwarded to LiteLLM'scompletion_args, so callers could not enforce tool use at the request level.Changes
src/google/adk/tools/agent_tool.pyWrap the serialized
input_schemaJSON in a natural-language instruction that explicitly asks the inner agent to use its available tools before producing a response. This keeps Claude in ReAct mode regardless of the message content format.src/google/adk/models/lite_llm.pyRead
llm_request.config.tool_config.function_calling_config.modeand map it to LiteLLM'stool_choiceparameter:ANY→"required"NONE→"none"AUTO→ provider default (unchanged, key omitted fromcompletion_args)_get_completion_inputsnow returns a 5-tuple(messages, tools, response_format, generation_params, tool_choice).Unit Tests Added
tests/unittests/tools/test_agent_tool.pytest_run_async_no_input_schema_passes_request_unchanged: withoutinput_schema, the content passed to the inner runner isargs['request']verbatim.test_run_async_with_input_schema_wraps_in_natural_language: withinput_schema, the text begins with"Process the following structured request", contains"Request:\n"followed by the JSON payload, and is not a bare JSON blob.test_run_async_with_input_schema_text_not_raw_json: asserts the text does not start with{.tests/unittests/models/test_litellm.pytest_get_completion_inputs_tool_choice_none_without_tool_config:tool_choiceisNonewith notool_config.test_get_completion_inputs_tool_choice_required_for_any_mode: returns"required"forANYmode.test_get_completion_inputs_tool_choice_none_for_none_mode: returns"none"forNONEmode.test_get_completion_inputs_tool_choice_none_for_auto_mode: returnsNoneforAUTOmode.test_generate_content_async_propagates_tool_choice_required:acompletionreceivestool_choice="required"forANY.test_generate_content_async_propagates_tool_choice_none_mode:acompletionreceivestool_choice="none"forNONE.test_generate_content_async_omits_tool_choice_for_auto_mode:tool_choicekey absent fromcompletion_argsforAUTO.test_generate_content_async_omits_tool_choice_without_tool_config:tool_choicekey absent when notool_config.Also updated all existing
_get_completion_inputscall sites (10 occurrences) to unpack the new 5-tuple.Pytest Results
The 1 pre-existing failure (
test_custom_schema[GOOGLE_AI]) is apydantic.ValidationErrorthat reproduces on the base branch before any of these changes and is unrelated to this fix.Related