mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
fix: Fixes thought handling in contents.py and refactors its unit tests
Before this change, other agent's reply with thought will still be inserted in the outgoing LlmRequest due to the wrong `else` statement for calling all other type of part. This commit also refactors test_contents.py to be behavior-oriented tests, instead of implementation-oriented, and add more test cases to cover expected scenarios. The tests are divided into the following files with different focus: - test_contents.py: covers the basic logic of event filter; - test_contents_branch.py: covers the behavior related to branch, which takes effect when ParallelAgent is used. - test_contents_other_agent.py: covers the retelling behavior to include other agents' reply as context for the current agent. - test_contents_function.py: covers the function_call/function_response rearrangement logic mainly for `LongRunningFunctionTool`. PiperOrigin-RevId: 802759821
This commit is contained in:
committed by
Copybara-Service
parent
fe8b37b0d3
commit
a30851ee16
@@ -63,8 +63,8 @@ from ..a2a.logs.log_utils import build_a2a_request_log
|
||||
from ..a2a.logs.log_utils import build_a2a_response_log
|
||||
from ..agents.invocation_context import InvocationContext
|
||||
from ..events.event import Event
|
||||
from ..flows.llm_flows.contents import _convert_foreign_event
|
||||
from ..flows.llm_flows.contents import _is_other_agent_reply
|
||||
from ..flows.llm_flows.contents import _present_other_agent_message
|
||||
from ..flows.llm_flows.functions import find_matching_function_call
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
@@ -338,7 +338,7 @@ class RemoteA2aAgent(BaseAgent):
|
||||
context_id = None
|
||||
for event in reversed(ctx.session.events):
|
||||
if _is_other_agent_reply(self.name, event):
|
||||
event = _convert_foreign_event(event)
|
||||
event = _present_other_agent_message(event)
|
||||
elif event.author == self.name:
|
||||
# stop on content generated by current a2a agent given it should already
|
||||
# be in remote session
|
||||
|
||||
@@ -16,7 +16,6 @@ from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import AsyncGenerator
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
|
||||
from google.genai import types
|
||||
@@ -203,6 +202,25 @@ def _rearrange_events_for_latest_function_response(
|
||||
return result_events
|
||||
|
||||
|
||||
def _contains_empty_content(event: Event) -> bool:
|
||||
"""Check if an event should be skipped due to missing or empty content.
|
||||
|
||||
This can happen to the evnets that only changed session state.
|
||||
|
||||
Args:
|
||||
event: The event to check.
|
||||
|
||||
Returns:
|
||||
True if the event should be skipped, False otherwise.
|
||||
"""
|
||||
return (
|
||||
not event.content
|
||||
or not event.content.role
|
||||
or not event.content.parts
|
||||
or event.content.parts[0].text == ''
|
||||
)
|
||||
|
||||
|
||||
def _get_contents(
|
||||
current_branch: Optional[str], events: list[Event], agent_name: str = ''
|
||||
) -> list[types.Content]:
|
||||
@@ -222,16 +240,7 @@ def _get_contents(
|
||||
# Parse the events, leaving the contents and the function calls and
|
||||
# responses from the current agent.
|
||||
for event in events:
|
||||
if (
|
||||
not event.content
|
||||
or not event.content.role
|
||||
or not event.content.parts
|
||||
or event.content.parts[0].text == ''
|
||||
):
|
||||
# Skip events without content, or generated neither by user nor by model
|
||||
# or has empty text.
|
||||
# E.g. events purely for mutating session states.
|
||||
|
||||
if _contains_empty_content(event):
|
||||
continue
|
||||
if not _is_event_belongs_to_branch(current_branch, event):
|
||||
# Skip events not belong to current branch.
|
||||
@@ -242,11 +251,12 @@ def _get_contents(
|
||||
if _is_request_confirmation_event(event):
|
||||
# Skip request confirmation events.
|
||||
continue
|
||||
filtered_events.append(
|
||||
_convert_foreign_event(event)
|
||||
if _is_other_agent_reply(agent_name, event)
|
||||
else event
|
||||
)
|
||||
|
||||
if _is_other_agent_reply(agent_name, event):
|
||||
if converted_event := _present_other_agent_message(event):
|
||||
filtered_events.append(converted_event)
|
||||
else:
|
||||
filtered_events.append(event)
|
||||
|
||||
# Rearrange events for proper function call/response pairing
|
||||
result_events = _rearrange_events_for_latest_function_response(
|
||||
@@ -305,19 +315,18 @@ def _is_other_agent_reply(current_agent_name: str, event: Event) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _convert_foreign_event(event: Event) -> Event:
|
||||
"""Converts an event authored by another agent as a user-content event.
|
||||
def _present_other_agent_message(event: Event) -> Optional[Event]:
|
||||
"""Presents another agent's message as user context for the current agent.
|
||||
|
||||
This is to provide another agent's output as context to the current agent, so
|
||||
that current agent can continue to respond, such as summarizing previous
|
||||
agent's reply, etc.
|
||||
Reformats the event with role='user' and adds '[agent_name] said:' prefix
|
||||
to provide context without confusion about authorship.
|
||||
|
||||
Args:
|
||||
event: The event to convert.
|
||||
event: The event from another agent to present as context.
|
||||
|
||||
Returns:
|
||||
The converted event.
|
||||
|
||||
Event reformatted as user-role context with agent attribution, or None
|
||||
if no meaningful content remains after filtering.
|
||||
"""
|
||||
if not event.content or not event.content.parts:
|
||||
return event
|
||||
@@ -326,8 +335,10 @@ def _convert_foreign_event(event: Event) -> Event:
|
||||
content.role = 'user'
|
||||
content.parts = [types.Part(text='For context:')]
|
||||
for part in event.content.parts:
|
||||
# Exclude thoughts from the context.
|
||||
if part.text and not part.thought:
|
||||
if part.thought:
|
||||
# Exclude thoughts from the context.
|
||||
continue
|
||||
elif part.text:
|
||||
content.parts.append(
|
||||
types.Part(text=f'[{event.author}] said: {part.text}')
|
||||
)
|
||||
@@ -354,6 +365,10 @@ def _convert_foreign_event(event: Event) -> Event:
|
||||
else:
|
||||
content.parts.append(part)
|
||||
|
||||
# If no meaningful parts were added (only "For context:" remains), return None
|
||||
if len(content.parts) == 1:
|
||||
return None
|
||||
|
||||
return Event(
|
||||
timestamp=event.timestamp,
|
||||
author='user',
|
||||
@@ -429,7 +444,11 @@ def _merge_function_response_events(
|
||||
def _is_event_belongs_to_branch(
|
||||
invocation_branch: Optional[str], event: Event
|
||||
) -> bool:
|
||||
"""Event belongs to a branch, when event.branch is prefix of the invocation branch."""
|
||||
"""Check if an event belongs to the current branch.
|
||||
|
||||
This is for event context segration between agents. E.g. agent A shouldn't
|
||||
see output of agent B.
|
||||
"""
|
||||
if not invocation_branch or not event.branch:
|
||||
return True
|
||||
return invocation_branch.startswith(event.branch)
|
||||
|
||||
@@ -65,7 +65,15 @@ def populate_client_function_call_id(model_response_event: Event) -> None:
|
||||
function_call.id = generate_client_function_call_id()
|
||||
|
||||
|
||||
def remove_client_function_call_id(content: types.Content) -> None:
|
||||
def remove_client_function_call_id(content: Optional[types.Content]) -> None:
|
||||
"""Removes ADK-generated function call IDs from content before sending to LLM.
|
||||
|
||||
Strips client-side function call/response IDs that start with 'adk-' prefix
|
||||
to avoid sending internal tracking IDs to the model.
|
||||
|
||||
Args:
|
||||
content: Content containing function calls/responses to clean.
|
||||
"""
|
||||
if content and content.parts:
|
||||
for part in content.parts:
|
||||
if (
|
||||
|
||||
@@ -515,7 +515,7 @@ class TestRemoteA2aAgentMessageHandling:
|
||||
self.mock_session.events = [mock_event]
|
||||
|
||||
with patch(
|
||||
"google.adk.agents.remote_a2a_agent._convert_foreign_event"
|
||||
"google.adk.agents.remote_a2a_agent._present_other_agent_message"
|
||||
) as mock_convert:
|
||||
mock_convert.return_value = mock_event
|
||||
|
||||
@@ -937,7 +937,7 @@ class TestRemoteA2aAgentIntegration:
|
||||
|
||||
# Mock dependencies
|
||||
with patch(
|
||||
"google.adk.agents.remote_a2a_agent._convert_foreign_event"
|
||||
"google.adk.agents.remote_a2a_agent._present_other_agent_message"
|
||||
) as mock_convert:
|
||||
mock_convert.return_value = mock_event
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,288 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for branch filtering in contents module.
|
||||
|
||||
Branch format: agent_1.agent_2.agent_3 (parent.child.grandchild)
|
||||
Child agents can see parent agents' events, but not sibling agents' events.
|
||||
"""
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.flows.llm_flows.contents import request_processor
|
||||
from google.adk.models.llm_request import LlmRequest
|
||||
from google.genai import types
|
||||
import pytest
|
||||
|
||||
from ... import testing_utils
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_branch_filtering_child_sees_parent():
|
||||
"""Test that child agents can see parent agents' events."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="child_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Set current branch as child of "parent_agent"
|
||||
invocation_context.branch = "parent_agent.child_agent"
|
||||
|
||||
# Add events from parent and child levels
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("User message"),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="parent_agent",
|
||||
content=types.ModelContent("Parent agent response"),
|
||||
branch="parent_agent", # Parent branch - should be included
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="child_agent",
|
||||
content=types.ModelContent("Child agent response"),
|
||||
branch="parent_agent.child_agent", # Current branch - should be included
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify child can see user message and parent events, but not sibling events
|
||||
assert len(llm_request.contents) == 3
|
||||
assert llm_request.contents[0] == types.UserContent("User message")
|
||||
assert llm_request.contents[1].role == "user"
|
||||
assert llm_request.contents[1].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[parent_agent] said: Parent agent response"),
|
||||
]
|
||||
assert llm_request.contents[2] == types.ModelContent("Child agent response")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_branch_filtering_excludes_sibling_agents():
|
||||
"""Test that sibling agents cannot see each other's events."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="child_agent1")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Set current branch as first child
|
||||
invocation_context.branch = "parent_agent.child_agent1"
|
||||
|
||||
# Add events from parent, current child, and sibling child
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("User message"),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="parent_agent",
|
||||
content=types.ModelContent("Parent response"),
|
||||
branch="parent_agent", # Parent - should be included
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="child_agent1",
|
||||
content=types.ModelContent("Child1 response"),
|
||||
branch="parent_agent.child_agent1", # Current - should be included
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv4",
|
||||
author="child_agent2",
|
||||
content=types.ModelContent("Sibling response"),
|
||||
branch="parent_agent.child_agent2", # Sibling - should be excluded
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify sibling events are excluded, but parent and current agent events included
|
||||
assert len(llm_request.contents) == 3
|
||||
assert llm_request.contents[0] == types.UserContent("User message")
|
||||
assert llm_request.contents[1].role == "user"
|
||||
assert llm_request.contents[1].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[parent_agent] said: Parent response"),
|
||||
]
|
||||
assert llm_request.contents[2] == types.ModelContent("Child1 response")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_branch_filtering_no_branch_allows_all():
|
||||
"""Test that events are included when no branches are set."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# No current branch set (None)
|
||||
invocation_context.branch = None
|
||||
|
||||
# Add events with and without branches
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("No branch message"),
|
||||
branch=None,
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="agent1",
|
||||
content=types.ModelContent("Agent with branch"),
|
||||
branch="agent1",
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="user",
|
||||
content=types.UserContent("Another no branch"),
|
||||
branch=None,
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify all events are included when no current branch
|
||||
assert len(llm_request.contents) == 3
|
||||
assert llm_request.contents[0] == types.UserContent("No branch message")
|
||||
assert llm_request.contents[1].role == "user"
|
||||
assert llm_request.contents[1].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[agent1] said: Agent with branch"),
|
||||
]
|
||||
assert llm_request.contents[2] == types.UserContent("Another no branch")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_branch_filtering_grandchild_sees_grandparent():
|
||||
"""Test that deeply nested child agents can see all ancestor events."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="grandchild_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Set deeply nested branch: grandparent.parent.grandchild
|
||||
invocation_context.branch = "grandparent_agent.parent_agent.grandchild_agent"
|
||||
|
||||
# Add events from all levels of hierarchy
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="grandparent_agent",
|
||||
content=types.ModelContent("Grandparent response"),
|
||||
branch="grandparent_agent",
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="parent_agent",
|
||||
content=types.ModelContent("Parent response"),
|
||||
branch="grandparent_agent.parent_agent",
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="grandchild_agent",
|
||||
content=types.ModelContent("Grandchild response"),
|
||||
branch="grandparent_agent.parent_agent.grandchild_agent",
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv4",
|
||||
author="sibling_agent",
|
||||
content=types.ModelContent("Sibling response"),
|
||||
branch="grandparent_agent.parent_agent.sibling_agent",
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify only ancestors and current level are included
|
||||
assert len(llm_request.contents) == 3
|
||||
assert llm_request.contents[0].role == "user"
|
||||
assert llm_request.contents[0].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[grandparent_agent] said: Grandparent response"),
|
||||
]
|
||||
assert llm_request.contents[1].role == "user"
|
||||
assert llm_request.contents[1].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[parent_agent] said: Parent response"),
|
||||
]
|
||||
assert llm_request.contents[2] == types.ModelContent("Grandchild response")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_branch_filtering_parent_cannot_see_child():
|
||||
"""Test that parent agents cannot see child agents' events."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="parent_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Set current branch as parent
|
||||
invocation_context.branch = "parent_agent"
|
||||
|
||||
# Add events from parent and its children
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("User message"),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="parent_agent",
|
||||
content=types.ModelContent("Parent response"),
|
||||
branch="parent_agent",
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="child_agent",
|
||||
content=types.ModelContent("Child response"),
|
||||
branch="parent_agent.child_agent",
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv4",
|
||||
author="grandchild_agent",
|
||||
content=types.ModelContent("Grandchild response"),
|
||||
branch="parent_agent.child_agent.grandchild_agent",
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify parent cannot see child or grandchild events
|
||||
assert llm_request.contents == [
|
||||
types.UserContent("User message"),
|
||||
types.ModelContent("Parent response"),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,388 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Behavioral tests for other agent message processing in contents module."""
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.flows.llm_flows.contents import request_processor
|
||||
from google.adk.models.llm_request import LlmRequest
|
||||
from google.genai import types
|
||||
import pytest
|
||||
|
||||
from ... import testing_utils
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_agent_message_appears_as_user_context():
|
||||
"""Test that messages from other agents appear as user context."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Add event from another agent
|
||||
other_agent_event = Event(
|
||||
invocation_id="test_inv",
|
||||
author="other_agent",
|
||||
content=types.ModelContent("Hello from other agent"),
|
||||
)
|
||||
invocation_context.session.events = [other_agent_event]
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify the other agent's message is presented as user context
|
||||
assert llm_request.contents[0].role == "user"
|
||||
assert llm_request.contents[0].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[other_agent] said: Hello from other agent"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_agent_thoughts_are_excluded():
|
||||
"""Test that thoughts from other agents are excluded from context."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Add event from other agent with both regular text and thoughts
|
||||
other_agent_event = Event(
|
||||
invocation_id="test_inv",
|
||||
author="other_agent",
|
||||
content=types.ModelContent([
|
||||
types.Part(text="Public message", thought=False),
|
||||
types.Part(text="Private thought", thought=True),
|
||||
types.Part(text="Another public message"),
|
||||
]),
|
||||
)
|
||||
invocation_context.session.events = [other_agent_event]
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify only non-thought parts are included (thoughts excluded)
|
||||
assert llm_request.contents[0].role == "user"
|
||||
assert llm_request.contents[0].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[other_agent] said: Public message"),
|
||||
types.Part(text="[other_agent] said: Another public message"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_agent_function_calls():
|
||||
"""Test that function calls from other agents are preserved in context."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Add event from other agent with function call
|
||||
function_call = types.FunctionCall(
|
||||
id="func_123", name="search_tool", args={"query": "test query"}
|
||||
)
|
||||
other_agent_event = Event(
|
||||
invocation_id="test_inv",
|
||||
author="other_agent",
|
||||
content=types.ModelContent([types.Part(function_call=function_call)]),
|
||||
)
|
||||
invocation_context.session.events = [other_agent_event]
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify function call is presented as context
|
||||
assert llm_request.contents[0].role == "user"
|
||||
assert llm_request.contents[0].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(
|
||||
text="""\
|
||||
[other_agent] called tool `search_tool` with parameters: {'query': 'test query'}"""
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_agent_function_responses():
|
||||
"""Test that function responses from other agents are properly formatted."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Add event from other agent with function response
|
||||
function_response = types.FunctionResponse(
|
||||
id="func_123",
|
||||
name="search_tool",
|
||||
response={"results": ["item1", "item2"]},
|
||||
)
|
||||
other_agent_event = Event(
|
||||
invocation_id="test_inv",
|
||||
author="other_agent",
|
||||
content=types.Content(
|
||||
role="user", parts=[types.Part(function_response=function_response)]
|
||||
),
|
||||
)
|
||||
invocation_context.session.events = [other_agent_event]
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify function response is presented as context
|
||||
assert llm_request.contents[0].role == "user"
|
||||
assert llm_request.contents[0].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(
|
||||
text=(
|
||||
"[other_agent] `search_tool` tool returned result: {'results':"
|
||||
" ['item1', 'item2']}"
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_agent_function_call_response():
|
||||
"""Test function call and response sequence from other agents."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Add function call event from other agent
|
||||
function_call = types.FunctionCall(
|
||||
id="func_123", name="calc_tool", args={"query": "6x7"}
|
||||
)
|
||||
call_event = Event(
|
||||
invocation_id="test_inv1",
|
||||
author="other_agent",
|
||||
content=types.ModelContent([
|
||||
types.Part(text="Let me calculate this"),
|
||||
types.Part(function_call=function_call),
|
||||
]),
|
||||
)
|
||||
# Add function response event
|
||||
function_response = types.FunctionResponse(
|
||||
id="func_123", name="calc_tool", response={"result": 42}
|
||||
)
|
||||
response_event = Event(
|
||||
invocation_id="test_inv2",
|
||||
author="other_agent",
|
||||
content=types.UserContent(
|
||||
parts=[types.Part(function_response=function_response)]
|
||||
),
|
||||
)
|
||||
invocation_context.session.events = [call_event, response_event]
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify function call and response are properly formatted
|
||||
assert len(llm_request.contents) == 2
|
||||
|
||||
# Function call from other agent
|
||||
assert llm_request.contents[0].role == "user"
|
||||
assert llm_request.contents[0].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[other_agent] said: Let me calculate this"),
|
||||
types.Part(
|
||||
text=(
|
||||
"[other_agent] called tool `calc_tool` with parameters: {'query':"
|
||||
" '6x7'}"
|
||||
)
|
||||
),
|
||||
]
|
||||
# Function response from other agent
|
||||
assert llm_request.contents[1].role == "user"
|
||||
assert llm_request.contents[1].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(
|
||||
text="[other_agent] `calc_tool` tool returned result: {'result': 42}"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_agent_empty_content():
|
||||
"""Test that other agent messages with only thoughts or empty content are filtered out."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Add events: user message, other agents with empty content, user message
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("Hello"),
|
||||
),
|
||||
# Other agent with only thoughts
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="other_agent1",
|
||||
content=types.ModelContent([
|
||||
types.Part(text="This is a private thought", thought=True),
|
||||
types.Part(text="Another private thought", thought=True),
|
||||
]),
|
||||
),
|
||||
# Other agent with empty text and thoughts
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="other_agent2",
|
||||
content=types.ModelContent([
|
||||
types.Part(text="", thought=False),
|
||||
types.Part(text="Secret thought", thought=True),
|
||||
]),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv4",
|
||||
author="user",
|
||||
content=types.UserContent("World"),
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify empty content events are completely filtered out
|
||||
assert llm_request.contents == [
|
||||
types.UserContent("Hello"),
|
||||
types.UserContent("World"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_agents_in_conversation():
|
||||
"""Test handling multiple agents in a conversation flow."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
|
||||
# Create a multi-agent conversation
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("Hello everyone"),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="agent1",
|
||||
content=types.ModelContent("Hi from agent1"),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="agent2",
|
||||
content=types.ModelContent("Hi from agent2"),
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify all messages are properly processed
|
||||
assert len(llm_request.contents) == 3
|
||||
|
||||
# User message should remain as user
|
||||
assert llm_request.contents[0] == types.UserContent("Hello everyone")
|
||||
# Other agents' messages should be converted to user context
|
||||
assert llm_request.contents[1].role == "user"
|
||||
assert llm_request.contents[1].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[agent1] said: Hi from agent1"),
|
||||
]
|
||||
assert llm_request.contents[2].role == "user"
|
||||
assert llm_request.contents[2].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[agent2] said: Hi from agent2"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_current_agent_messages_not_converted():
|
||||
"""Test that the current agent's own messages are not converted."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Add events from both current agent and other agent
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="current_agent",
|
||||
content=types.ModelContent("My own message"),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="other_agent",
|
||||
content=types.ModelContent("Other agent message"),
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify current agent's message stays as model role
|
||||
# and other agent's message is converted to user context
|
||||
assert len(llm_request.contents) == 2
|
||||
assert llm_request.contents[0] == types.ModelContent("My own message")
|
||||
assert llm_request.contents[1].role == "user"
|
||||
assert llm_request.contents[1].parts == [
|
||||
types.Part(text="For context:"),
|
||||
types.Part(text="[other_agent] said: Other agent message"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_messages_preserved():
|
||||
"""Test that user messages are preserved as-is."""
|
||||
agent = Agent(model="gemini-2.5-flash", name="current_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
# Add user message
|
||||
user_event = Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("User message"),
|
||||
)
|
||||
invocation_context.session.events = [user_event]
|
||||
|
||||
# Process the request
|
||||
async for _ in request_processor.run_async(invocation_context, llm_request):
|
||||
pass
|
||||
|
||||
# Verify user message is preserved exactly
|
||||
assert len(llm_request.contents) == 1
|
||||
assert llm_request.contents[0] == types.UserContent("User message")
|
||||
Reference in New Issue
Block a user