Skip to content

EventActions does not capture all the action when there are multiple function calls in an Event #1938

Description

@iwknow

Describe the bug
EventActions does not capture all the action when there are multiple function calls in an Event.

For example, if an Event generated from LLM contains two functionCall

content:
  parts:
    0:
      functionCall:
        id: "adk-645f8032-712b-41a7-8e27-64c49f977522"
      args:
        query: "arg1"
      name: "state_update_tool1"
    1:
      functionCall:
        id: "adk-645f8032-712b-41a7-8e27-64c49f977522"
      args:
        query: "arg2"
      name: "state_update_tool2"

and both of the tools will update he session state, respectively. then, only the last state update will be horned.

To Reproduce
the following unit test will fail with the current implementation

async def test_parallel_function_calls_with_state_change():
  function_calls = [
      types.Part.from_function_call(
          name='update_session_state',
          args={'key': 'test_key1', 'value': 'test_value1'},
      ),
      types.Part.from_function_call(
          name='update_session_state',
          args={'key': 'test_key2', 'value': 'test_value2'},
      ),
      types.Part.from_function_call(
          name='transfer_to_agent', args={'agent_name': 'test_sub_agent'}
      ),
  ]
  function_responses = [
      types.Part.from_function_response(
          name='update_session_state', response={'result': None}
      ),
      types.Part.from_function_response(
          name='update_session_state', response={'result': None}
      ),
      types.Part.from_function_response(
          name='transfer_to_agent', response={'result': None}
      ),
  ]

  responses: list[types.Content] = [
      function_calls,
      'response1',
  ]
  function_called = 0
  mock_model = testing_utils.MockModel.create(responses=responses)

  async def update_session_state(
      key: str, value: str, tool_context: ToolContext
  ) -> None:
    nonlocal function_called
    function_called += 1
    tool_context.state.update({key: value})
    return

  async def transfer_to_agent(
      agent_name: str, tool_context: ToolContext
  ) -> None:
    nonlocal function_called
    function_called += 1
    tool_context.actions.transfer_to_agent = agent_name
    return

  test_sub_agent = Agent(
      name='test_sub_agent',
  )

  agent = Agent(
      name='root_agent',
      model=mock_model,
      tools=[update_session_state, transfer_to_agent],
      sub_agents=[test_sub_agent],
  )
  runner = testing_utils.TestInMemoryRunner(agent)
  events = await runner.run_async_with_new_session('test')

  # Notice that the following assertion only checks the "contents" part of the events.
  # The "actions" part will be checked later.
  assert testing_utils.simplify_events(events) == [
      ('root_agent', function_calls),
      ('root_agent', function_responses),
      ('test_sub_agent', 'response1'),
  ]

  # Asserts the function calls.
  assert function_called == 3

  # Asserts the actions in response event.
  response_event = events[1]

  assert response_event.actions == EventActions(
      state_delta={
          'test_key1': 'test_value1',
          'test_key2': 'test_value2',
      },
      transfer_to_agent='test_sub_agent',
  )

Expected behavior
EventAction from multiple function calls will be properly merged.

Desktop (please complete the following information):

  • OS: Linux
  • Python version(python -V): 3.11.2
  • ADK version(pip show google-adk): 1.6.1

Additional Information:
After carefully inspect the code, i found the root cause of the issue. When there are multiple functions calls in the same event, ADK tries to merge the EventAction updates resulting from all the function calls (see: https://github.com/google/adk-python/blob/main/src/google/adk/flows/llm_flows/functions.py#L537). However, the merging logic is wrong. it replaces the value instead of merging it when the type of the action field is dict. The handling of requested_auth_configs is correct though.

Also, it seems that #345 and #1119 both related to this.

I will create a PR that fix this properly that merging logic changes based on the data type.

For project owner: you can assign this issue to me.

Metadata

Metadata

Labels

tools[Component] This issue is related to tools

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions