Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion deepeval/test_case/llm_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,16 @@ def _make_hashable(obj):
# Handle frozenset that might contain unhashable elements
return frozenset(_make_hashable(item) for item in obj)
else:
# For primitive hashable types (str, int, float, bool, etc.)
# Primitive hashable types (str, int, float, bool, etc.) pass through
# unchanged. Some objects reach here while being unhashable, e.g. a
# LangChain ToolMessage or a pydantic model that defines __eq__ without
# __hash__. Returning those as-is makes ToolCall.__hash__ raise
# ``TypeError: unhashable type`` (#2815), so fall back to a stable
# string representation for anything that is not hashable.
try:
hash(obj)
except TypeError:
return repr(obj)
return obj


Expand Down
37 changes: 37 additions & 0 deletions tests/test_core/test_test_case/test_single_turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,43 @@ def test_tool_call_hashing_with_complex_types(self):
hash_value = hash(tool_call)
assert isinstance(hash_value, int)

def test_tool_call_hashing_with_unhashable_types(self):
"""Regression test for #2815.

ToolCall must stay hashable even when its output (or a value nested in
input_parameters) is an arbitrary unhashable object, such as a
LangChain ToolMessage or a pydantic model that defines __eq__ without
__hash__. ToolCorrectnessMetric puts ToolCall instances into a set, so a
crash here breaks component-level evaluation of agent traces.
"""

class _Unhashable:
# Mirrors objects that define __eq__ without __hash__, which Python
# then makes unhashable.
__hash__ = None

def __init__(self, value):
self.value = value

def __repr__(self):
return f"_Unhashable({self.value!r})"

# Sanity check: the helper object really is unhashable.
with pytest.raises(TypeError):
hash(_Unhashable("x"))

# Unhashable object as the output.
tool_output = ToolCall(name="tool", output=_Unhashable("x"))
assert isinstance(hash(tool_output), int)
assert len({tool_output}) == 1 # usable in a set

# Unhashable object nested inside input_parameters.
tool_input = ToolCall(
name="tool", input_parameters={"arg": _Unhashable("y")}
)
assert isinstance(hash(tool_input), int)
assert len({tool_input}) == 1

def test_tool_call_repr(self):
tool_call = ToolCall(
name="test_tool",
Expand Down
Loading