Python: Fix agent option merge to support dict-defined tools#4311
Python: Fix agent option merge to support dict-defined tools#4311moonbox3 wants to merge 1 commit intomicrosoft:mainfrom
Conversation
_merge_options used getattr(tool, 'name', None) to deduplicate tools, which returns None for dict-defined tools. This caused all dict tools from the override to be silently dropped as duplicates. Add _get_tool_name() helper that extracts names from both object tools (via attribute) and dict tools (via ['function']['name']). Also discard None from the existing_names set so unnamed tools are never treated as duplicates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 90%
✓ Correctness
The diff introduces a
_get_tool_namehelper to support both dict-style and object-style tool definitions in_merge_options, and correctly discardsNonefrom the existing names set so that unnamed tools are not erroneously deduplicated. The logic is sound, the tests cover dict tools, object tools, deduplication, and the no-name case. No correctness bugs found.
✓ Security Reliability
The diff introduces a helper
_get_tool_nameand updates_merge_optionsto handle dict-format tools. The changes are generally clean. One minor reliability concern: if a dict tool has a"function"key mapped to a non-dict value (e.g.,None), the chained.get("name")call will raise anAttributeError. This is unlikely in practice but violates the defensive style the rest of the function aims for. No security issues found.
✗ Test Coverage
The new tests cover the core dict-tool scenarios well (_get_tool_name extraction, dict tool merging, deduplication). However, there is a meaningful gap: the
existing_names.discard(None)on line 108 of _agents.py is a deliberate behavior change (nameless tools in the override are no longer silently dropped when a nameless tool exists in base), yet no test exercises this path. Additionally, there is no test for mixed tool types (object tools in base + dict tools in override), which is the cross-format scenario this change is specifically enabling.
Blocking Issues
- No test covers the
existing_names.discard(None)behavior (line 108). Without this, a regression that removes the discard call would silently drop nameless override tools, and the test suite would not catch it.
Suggestions
- In
_get_tool_name, if a dict has a"function"key whose value is not itself a dict (e.g., a string), the chained.get("name")call will raise anAttributeError. Consider guarding with anisinstancecheck if malformed tool dicts are a realistic input. - In
_get_tool_name, guard againsttool["function"]being a non-dict value (e.g.,None) which would cause anAttributeErroron the chained.get("name")call. Using(tool.get("function") or {}).get("name")or an explicit isinstance check would make it more robust. - Add a test for mixed tool types (e.g., object-based tool in base, dict-based tool in override) to verify cross-format deduplication works correctly.
- Consider testing
_get_tool_namewith a dict that has afunctionkey but nonamekey (e.g.,{"function": {}}) to explicitly cover that branch.
Automated review by moonbox3's agents
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical bug where dict-defined tools (OpenAI-compatible {"type": "function", "function": {"name": ...}} format) were being silently dropped during agent option merging. The root cause was that _merge_options used getattr(t, "name", None) to extract tool names, which always returned None for dict tools, causing all runtime dict tools to be incorrectly treated as duplicates.
Changes:
- Introduced
_get_tool_name()helper function that extracts names from both object-based tools (via.nameattribute) and dict-based tools (via["function"]["name"]path) - Updated
_merge_options()to use the new helper and discardNonefrom the existing-names set to prevent spurious deduplication - Added comprehensive test coverage for dict tools, object tools, and edge cases where tools have no name
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| python/packages/core/agent_framework/_agents.py | Added _get_tool_name() helper and updated _merge_options() to properly handle dict-defined tools |
| python/packages/core/tests/core/test_agents.py | Added tests for dict tool merging, deduplication, and _get_tool_name() helper with both dict and object formats |
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
|
Closing due to agent review errors. |
Motivation and Context
When tools were defined as plain dicts (the
{"type": "function", "function": {"name": ...}}format accepted by OpenAI-compatible APIs),_merge_optionsusedgetattr(t, "name", None)to extract tool names for deduplication, which always returnedNonefor dicts — causing all override tools to be silently dropped as false-positive duplicates.Fixes #4303
Description
The root cause was that
_merge_optionsassumed tools were always objects with a.nameattribute. For dict-defined tools, the name lives attool["function"]["name"], sogetattrreturnedNonefor every tool, and theNone-populatedexisting_namesset incorrectly matched all incoming tools. The fix introduces a_get_tool_namehelper that handles both object and dict tool formats, and discardsNonefrom the existing-names set so that unnamed tools never cause spurious deduplication.Contribution Checklist