Skip to content

fix(tools): take the first non-null union member in _remove_any_of - #7989

Open
PratikDhanave wants to merge 1 commit into
google:mainfrom
PratikDhanaveFork:fix-remove-any-of-first-non-null
Open

PratikDhanave wants to merge 1 commit into
google:mainfrom
PratikDhanaveFork:fix-remove-any-of-first-non-null

Conversation

@PratikDhanave

Copy link
Copy Markdown

Link to Issue or Description of Change

Describe the change:

Problem:

_remove_any_of in src/google/adk/tools/_automatic_function_calling_util.py collapses a parameter's anyOf into a single schema on the Gemini (non-Vertex) function-declaration path used by build_function_declaration_for_langchain and build_function_declaration_for_params_for_crewai (i.e. LangChain / CrewAI tools).

Its comment says "Take the first non-null type", but the loop has no break and merges every non-null branch with property_schema.update(type_):

# Take the first non-null type.
if union_types:
  for type_ in union_types:
    if type_.get('type') != 'null':
      property_schema.update(type_)

For a heterogeneous union this has two wrong effects:

  1. the last non-null branch wins instead of the first;
  2. update() merges rather than replaces, so keywords from an earlier branch leak into the result, producing an invalid schema.

Example — a tool argument typed Union[List[str], int]:

# actual (before fix) — invalid: INTEGER type carrying the array-only `items`
type=INTEGER, items=Schema(type=STRING)

# expected — the first non-null member (list[str])
type=ARRAY, items=Schema(type=STRING)

Union[int, str] similarly yields STRING (the last member) instead of the documented first member INTEGER. Optional[T] is unaffected, because after _annotate_nullable_fields it has a single non-null branch.

Solution:

Add break after the first non-null branch is applied, so only the first non-null member is taken. This matches the function's own documented contract and produces a valid single-type schema. The common Optional[T] path is unchanged (single branch).

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added two regression tests to tests/unittests/tools/test_build_function_declaration.py:

  • test_for_langchain_collapses_a_union_to_its_first_non_null_member
  • test_for_crewai_collapses_a_union_to_its_first_non_null_member

Both fail on main (the first collapses to INTEGER with a stray items; the second collapses to the last member STRING) and pass with this change.

$ python -m pytest tests/unittests/tools/test_build_function_declaration.py \
    tests/unittests/tools/test_from_function_with_options.py \
    tests/unittests/tools/test_function_tool_with_import_annotations.py -q
131 passed in 0.53s

Fail-pre / pass-post for the new tests (fix reverted vs applied):

# fix reverted
2 failed      assert <Type.STRING> == 'INTEGER'
# fix applied
2 passed

Manual End-to-End (E2E) Tests:

Not run — this is a pure schema-generation fix with no runtime/UI surface; it is covered by the unit tests above.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

`_remove_any_of` collapses a parameter's `anyOf` to a single schema for
the Gemini (non-Vertex) function declaration path used by LangChain and
CrewAI tools. Its comment says "Take the first non-null type", but the
loop had no `break` and merged every non-null branch with `.update()`.

For a heterogeneous union this had two wrong effects: the last non-null
branch won instead of the first, and `.update()` left keywords from the
earlier branch behind. For example `Union[List[str], int]` produced a
schema typed `integer` that still carried the array-only `items` keyword
(an invalid schema), and picked `int` rather than the first member.
Optional[T] is unaffected because it has a single non-null branch.

Add `break` so only the first non-null member is taken, matching the
documented contract and producing a valid single-type schema.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants