Skip to content
Draft
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: 9 additions & 2 deletions python/semantic_kernel/contents/utils/hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ def make_hashable(input: Any, visited=None) -> Any:
visited[unique_obj_id] = items
return items

# Convert lists, sets, and tuples to tuples
if isinstance(input, (list, set, tuple)):
# Preserve unordered equality for sets, including equality with frozensets.
if isinstance(input, (set, frozenset)):
visited[unique_obj_id] = None
set_items = frozenset(make_hashable(item, visited) for item in input)
visited[unique_obj_id] = set_items
return set_items

# Convert ordered sequences to tuples.
if isinstance(input, (list, tuple)):
visited[unique_obj_id] = None
items = tuple(make_hashable(item, visited) for item in input)
visited[unique_obj_id] = items
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,27 @@ async def test_receive_skips_file_references(chat_history_channel):
assert len(channel.messages[2].items) == 2
assert channel.messages[2].items[0].result == "normal content 2"
assert channel.messages[2].items[1].text == "Normal message"


async def test_invoke_deduplicates_equal_set_results(chat_history_channel):
"""Equivalent set results must not be appended twice by the channel."""
channel = chat_history_channel
channel.add_user_message("Look up the values")
stored = ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(id="call", name="lookup", result=set([0, 8]))],
)
returned = ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(id="call", name="lookup", result=set([8, 0]))],
)
assert stored == returned

class AgentWithHistoryMutation:
async def invoke(self, **kwargs):
channel.messages.append(stored)
yield AgentResponseItem(message=returned, thread=channel.thread)

received = [message async for _, message in channel.invoke(AgentWithHistoryMutation())]
assert received == [stored]
assert channel.messages == [channel.messages[0], stored]
33 changes: 33 additions & 0 deletions python/tests/unit/contents/test_hashing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,36 @@ def test_hash_function_call_content():
)

_ = hash(content)


def test_equal_sets_have_equal_hashes():
"""Set iteration order must not affect function-result hashes."""
first = FunctionResultContent(id="call", name="lookup", result=set([0, 8]))
second = FunctionResultContent(id="call", name="lookup", result=set([8, 0]))
assert first == second
assert hash(first) == hash(second)
assert second in {first}


def test_set_and_frozenset_have_equal_hashes():
"""Python treats a set and a frozenset with equal elements as equal."""
first = FunctionResultContent(id="call", name="lookup", result={0, 8})
second = FunctionResultContent(id="call", name="lookup", result=frozenset({0, 8}))
assert first == second
assert hash(first) == hash(second)


def test_nested_equal_sets_have_equal_hashes():
"""Sets nested in function results retain unordered equality."""
first = FunctionResultContent(id="call", name="lookup", result={"values": [set([0, 8])]})
second = FunctionResultContent(id="call", name="lookup", result={"values": [set([8, 0])]})
assert first == second
assert hash(first) == hash(second)


def test_mixed_set_elements_can_be_hashed():
"""Unordered hashing must not require ordering unlike element types."""
first = FunctionResultContent(id="call", name="lookup", result={1, "one", (2, 3)})
second = FunctionResultContent(id="call", name="lookup", result=frozenset({1, "one", (2, 3)}))
assert first == second
assert hash(first) == hash(second)
Loading