From 5c5edb84e369c024c0ecd703e685fd77b8ce7ed4 Mon Sep 17 00:00:00 2001 From: Anish Mehta Date: Wed, 23 Sep 2026 16:03:51 +0530 Subject: [PATCH] Python: fix(core): keep booleans distinct from numbers inside sets filter_values_equal distinguishes True from 1 for scalars, sequences and mappings, but sets fell through to plain `==`, where `{1} == {True}` is true in Python. require_filter_collection accepts sets, and _evaluate_filter uses this for eq/ne/in/contains, so a record holding {1} matched a filter looking for {True}. Compare set members by a key that marks bools, recursing through the hashable containers a set can hold. Ints and floats still compare equal, and a set still equals an equal frozenset. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WyCUJcDPnCnQMmuDwsnDmD --- .../core/agent_framework/_vector_filters.py | 31 ++++++++++++++++++- .../packages/core/tests/core/test_vectors.py | 14 +++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_vector_filters.py b/python/packages/core/agent_framework/_vector_filters.py index 3f410e2ebc1..cb8fff19cfb 100644 --- a/python/packages/core/agent_framework/_vector_filters.py +++ b/python/packages/core/agent_framework/_vector_filters.py @@ -24,6 +24,7 @@ import math import re from collections.abc import Collection, Mapping, Sequence +from collections.abc import Set as AbstractSet from copy import deepcopy from dataclasses import dataclass from decimal import Decimal @@ -115,10 +116,38 @@ def require_filter_string(value: Any) -> str: return value +def _set_member_key(value: Any) -> Any: + """Key a set member so equality keeps bools distinct from numbers. + + A set cannot be walked pairwise the way a sequence can, and ``{1} == {True}`` + is true in Python because ``True`` hashes and compares equal to ``1``. Keying + each member by whether it is a bool restores the distinction, recursing + through the hashable containers a set can actually hold. Ints and floats + still compare equal, matching the scalar behaviour. + """ + if isinstance(value, bool): + return ("bool", value) + if isinstance(value, tuple): + return ("tuple", tuple(_set_member_key(item) for item in cast(tuple[Any, ...], value))) + if isinstance(value, frozenset): + return ("frozenset", frozenset(_set_member_key(item) for item in cast(AbstractSet[Any], value))) + return ("value", value) + + def filter_values_equal(left: Any, right: Any) -> bool: - """Compare validated scalars, sequences, and mappings without equating booleans to numbers.""" + """Compare validated scalars, sequences, sets, and mappings without equating booleans to numbers.""" if isinstance(left, bool) or isinstance(right, bool): return isinstance(left, bool) and isinstance(right, bool) and left == right + if isinstance(left, AbstractSet) and isinstance(right, AbstractSet): + left_set = cast(AbstractSet[Any], left) + right_set = cast(AbstractSet[Any], right) + if len(left_set) != len(right_set): + return False + return {_set_member_key(item) for item in left_set} == {_set_member_key(item) for item in right_set} + if isinstance(left, AbstractSet) or isinstance(right, AbstractSet): + # Only one side is a set. Preserve native semantics, which never equate + # a set with a list or a mapping. + return left == right if _is_non_string_sequence(left) and _is_non_string_sequence(right): # Preserve native container semantics, such as lists not equaling tuples. return left == right and all( diff --git a/python/packages/core/tests/core/test_vectors.py b/python/packages/core/tests/core/test_vectors.py index 03e42414ac1..353fc71827d 100644 --- a/python/packages/core/tests/core/test_vectors.py +++ b/python/packages/core/tests/core/test_vectors.py @@ -1815,6 +1815,20 @@ async def test_scoreless_results_remain_when_threshold_cannot_be_applied() -> No ({"one": True, "two": False}, {"two": False, "one": True}, True), ({"one": True}, {"two": True}, False), ({"value": True}, {}, False), + # Sets: `{1} == {True}` is true in Python, so they need the same + # bool/number distinction the sequence and mapping cases get. + ({1}, {True}, False), + ({0}, {False}, False), + (frozenset({1}), frozenset({True}), False), + ({(1,)}, {(True,)}, False), + ({1}, {1.0}, True), + ({1, 2}, {2, 1}, True), + ({1}, {1, 2}, False), + (set(), set(), True), + # set and frozenset compare equal by contents in Python; keep that. + ({1}, frozenset({1}), True), + ({True}, frozenset({1}), False), + ({1}, [1], False), ("1", 1, False), ("text", "text", True), (b"\x01", b"\x01", True),