Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 8 additions & 1 deletion src/google/adk/flows/llm_flows/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,10 +1151,17 @@ def __build_response_event(


def deep_merge_dicts(d1: dict, d2: dict) -> dict:
"""Recursively merges d2 into d1."""
"""Recursively merges d2 into d1.

For dict values, merges recursively. For list values, concatenates instead of
overwriting so that parallel tool calls don't silently drop list entries
(e.g. state_delta lists from concurrent function responses).
"""
for key, value in d2.items():
if key in d1 and isinstance(d1[key], dict) and isinstance(value, dict):
d1[key] = deep_merge_dicts(d1[key], value)
elif key in d1 and isinstance(d1[key], list) and isinstance(value, list):
d1[key] = d1[key] + value
else:
d1[key] = value
return d1
Expand Down
71 changes: 71 additions & 0 deletions tests/unittests/flows/llm_flows/test_functions_simple.py
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the tests to test_functions_parallel.py

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from google.adk.flows.llm_flows.functions import find_matching_function_call
from google.adk.flows.llm_flows.functions import handle_function_calls_async
from google.adk.flows.llm_flows.functions import handle_function_calls_live
from google.adk.flows.llm_flows.functions import deep_merge_dicts
from google.adk.flows.llm_flows.functions import merge_parallel_function_response_events
from google.adk.tools.computer_use.computer_use_tool import ComputerUseTool
from google.adk.tools.function_tool import FunctionTool
Expand Down Expand Up @@ -1236,3 +1237,73 @@ async def mock_run(*args, **kwargs):
# Verify the image was converted to a blob
assert len(response_part.parts) == 1
assert response_part.parts[0].inline_data is not None


def test_deep_merge_dicts_concatenates_lists():
"""Test that deep_merge_dicts concatenates list values instead of overwriting."""
d1 = {"state_delta": {"items": ["a"]}}
d2 = {"state_delta": {"items": ["b"]}}
result = deep_merge_dicts(d1, d2)
assert result["state_delta"]["items"] == ["a", "b"]


def test_deep_merge_dicts_overwrites_non_list_non_dict():
"""Test that deep_merge_dicts still overwrites scalar values."""
d1 = {"key": "old"}
d2 = {"key": "new"}
result = deep_merge_dicts(d1, d2)
assert result["key"] == "new"


def test_deep_merge_dicts_merges_nested_dicts():
"""Test that deep_merge_dicts recursively merges nested dicts."""
d1 = {"a": {"b": 1, "c": 2}}
d2 = {"a": {"b": 3, "d": 4}}
result = deep_merge_dicts(d1, d2)
assert result == {"a": {"b": 3, "c": 2, "d": 4}}


def test_deep_merge_dicts_handles_mixed_list_and_non_list():
"""Test that deep_merge_dicts overwrites when types differ (list vs non-list)."""
d1 = {"key": "not_a_list"}
d2 = {"key": ["a", "b"]}
result = deep_merge_dicts(d1, d2)
assert result["key"] == ["a", "b"]

d1 = {"key": ["a", "b"]}
d2 = {"key": "not_a_list"}
result = deep_merge_dicts(d1, d2)
assert result["key"] == "not_a_list"


def test_merge_parallel_function_response_events_merges_state_delta_lists():
"""Test that parallel events with list state_delta values are concatenated, not overwritten."""
invocation_id = "base_invocation_123"

event1 = Event(
invocation_id=invocation_id,
author="tool",
content=types.Content(
role="user",
parts=[types.Part(function_response=types.FunctionResponse(
name="func_1", response={"result": "ok"},
))],
),
actions=EventActions(state_delta={"items": ["a"]}),
)

event2 = Event(
invocation_id=invocation_id,
author="tool",
content=types.Content(
role="user",
parts=[types.Part(function_response=types.FunctionResponse(
name="func_2", response={"result": "ok"},
))],
),
actions=EventActions(state_delta={"items": ["b"]}),
)

merged_event = merge_parallel_function_response_events([event1, event2])

assert merged_event.actions.state_delta == {"items": ["a", "b"]}
Loading