Repository navigation
Python: fix FunctionCallContent hash for list and nested dict arguments - #14765
Anubhav Lal (anubhavlal07) wants to merge 1 commit into
Conversation
Hash nested dict or list arguments via make_hashable instead of frozenset.
|
Anubhav Lal (@anubhavlal07) please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
1 similar comment
|
Anubhav Lal (@anubhavlal07) please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
Muhammad Abdullah Butt (1AbdullahButt)
left a comment
There was a problem hiding this comment.
Looks correct. Here's what I checked.
- Hash/eq consistency.
FunctionCallContenthas no__eq__of its own, so equality is pydantic's field-by-field comparison. Two calls with equalargumentsdicts now hash the same:make_hashablesorts dict items by key, so key order doesn't matter, and it turns nested lists and dicts into tuples. Equal objects therefore always have equal hashes, which is the propertyset/dictmembership needs. - Shapes from connectors. JSON-decoded arguments only contain dicts, lists, strings, numbers, bools and
None, andmake_hashablehandles all of them. String arguments are now hashed as-is instead of collapsing toNone, which removes a guaranteed collision between calls that differ only in their argument string. - Consistency. This matches what
FunctionResultContentandChatMessageContentalready do. git merge-treeagainstmainis clean.
I didn't run the tests.
One gap I noticed but which shouldn't block: make_hashable turns a set into a tuple in iteration order, so two equal sets inside arguments could hash differently. JSON can't produce a set, so this only matters for arguments built in code.
The CLA bot is still waiting on this PR. You signed on #14764, which may already cover it once the bot re-checks.
Reviewed with help from Claude Code.
Title: Python: fix FunctionCallContent.hash crash for nested arguments
Motivation and Context
FunctionCallContent.__hash__builds the hash of dict arguments withfrozenset(self.arguments.items()). That only works when every argument value is hashable. Tool arguments often contain lists or nested objects, such as{"cities": ["Paris", "Rome"]}or{"options": {"units": "metric"}}. Several connectors (Anthropic, Google AI, Vertex AI, Bedrock, Mistral) also store arguments as a dict. In those caseshash()raises:FunctionResultContent.__hash__had the same problem and was fixed in #9977 by switching tomake_hashable.ChatMessageContentandStreamingChatMessageContentalso usemake_hashable.FunctionCallContentwas the only one still using the frozenset approach.Description
__hash__now usesmake_hashable(dict(self.arguments))for mapping arguments. This recursively turns nested dicts and lists into hashable tuples, with dict keys sorted so that equal arguments hash the same regardless of key order.None, so function calls that differed only in their string arguments always hashed the same.Test added in
tests/unit/contents/test_function_call_content.py:test_hash_with_nested_dict_argumentshashes calls with list and nested dict arguments. It checks that equal arguments in a different key order hash the same, that different arguments hash differently, and that the objects can be put in a set.Without the fix the test fails with
TypeError: unhashable type: 'list'.Tests:
tests/unit/contents/test_function_call_content.pypasses (25 tests). The newtest_hash_with_nested_dict_argumentsfails onmainwithTypeError: unhashable type: 'list'and passes with the change.ruff checkandmypyare clean on the changed file.Contribution Checklist
This change was prepared with the help of an AI assistant (Claude), and I reviewed and tested it.