|
| 1 | +# Copyright 2025 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +"""Behavioral tests for agent transfer system instructions. |
| 16 | +
|
| 17 | +These tests verify the behavior of the agent transfer system by calling |
| 18 | +the request processor and checking the resulting system instructions not just |
| 19 | +implementation. |
| 20 | +""" |
| 21 | + |
| 22 | +from google.adk.agents.invocation_context import InvocationContext |
| 23 | +from google.adk.agents.llm_agent import Agent |
| 24 | +from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService |
| 25 | +from google.adk.flows.llm_flows import agent_transfer |
| 26 | +from google.adk.memory.in_memory_memory_service import InMemoryMemoryService |
| 27 | +from google.adk.models.llm_request import LlmRequest |
| 28 | +from google.adk.plugins.plugin_manager import PluginManager |
| 29 | +from google.adk.runners import RunConfig |
| 30 | +from google.adk.sessions.in_memory_session_service import InMemorySessionService |
| 31 | +from google.genai import types |
| 32 | +import pytest |
| 33 | + |
| 34 | +from ... import testing_utils |
| 35 | + |
| 36 | + |
| 37 | +async def create_test_invocation_context(agent: Agent) -> InvocationContext: |
| 38 | + """Helper to create constructed InvocationContext.""" |
| 39 | + session_service = InMemorySessionService() |
| 40 | + memory_service = InMemoryMemoryService() |
| 41 | + session = await session_service.create_session( |
| 42 | + app_name='test_app', user_id='test_user' |
| 43 | + ) |
| 44 | + |
| 45 | + return InvocationContext( |
| 46 | + artifact_service=InMemoryArtifactService(), |
| 47 | + session_service=session_service, |
| 48 | + memory_service=memory_service, |
| 49 | + plugin_manager=PluginManager(plugins=[]), |
| 50 | + invocation_id='test_invocation_id', |
| 51 | + agent=agent, |
| 52 | + session=session, |
| 53 | + user_content=types.Content( |
| 54 | + role='user', parts=[types.Part.from_text(text='test')] |
| 55 | + ), |
| 56 | + run_config=RunConfig(), |
| 57 | + ) |
| 58 | + |
| 59 | + |
| 60 | +@pytest.mark.asyncio |
| 61 | +async def test_agent_transfer_includes_sorted_agent_names_in_system_instructions(): |
| 62 | + """Test that agent transfer adds NOTE with sorted agent names to system instructions.""" |
| 63 | + mockModel = testing_utils.MockModel.create(responses=[]) |
| 64 | + |
| 65 | + # Create agents with names that will test alphabetical sorting |
| 66 | + z_agent = Agent(name='z_agent', model=mockModel, description='Last agent') |
| 67 | + a_agent = Agent(name='a_agent', model=mockModel, description='First agent') |
| 68 | + m_agent = Agent(name='m_agent', model=mockModel, description='Middle agent') |
| 69 | + peer_agent = Agent( |
| 70 | + name='peer_agent', model=mockModel, description='Peer agent' |
| 71 | + ) |
| 72 | + |
| 73 | + # Create parent agent with a peer agent |
| 74 | + parent_agent = Agent( |
| 75 | + name='parent_agent', |
| 76 | + model=mockModel, |
| 77 | + sub_agents=[peer_agent], |
| 78 | + description='Parent agent', |
| 79 | + ) |
| 80 | + |
| 81 | + # Create main agent with sub-agents and parent (intentionally unsorted order) |
| 82 | + main_agent = Agent( |
| 83 | + name='main_agent', |
| 84 | + model=mockModel, |
| 85 | + sub_agents=[z_agent, a_agent, m_agent], # Unsorted input |
| 86 | + parent_agent=parent_agent, |
| 87 | + description='Main coordinating agent', |
| 88 | + ) |
| 89 | + |
| 90 | + # Create test context and LLM request |
| 91 | + invocation_context = await create_test_invocation_context(main_agent) |
| 92 | + llm_request = LlmRequest() |
| 93 | + |
| 94 | + # Call the actual agent transfer request processor (this behavior we're testing) |
| 95 | + async for _ in agent_transfer.request_processor.run_async( |
| 96 | + invocation_context, llm_request |
| 97 | + ): |
| 98 | + pass |
| 99 | + |
| 100 | + # Check on the behavior: verify system instructions contain sorted agent names |
| 101 | + instructions = llm_request.config.system_instruction |
| 102 | + |
| 103 | + # The NOTE should contain agents in alphabetical order: sub-agents + parent + peers |
| 104 | + expected_content = """\ |
| 105 | +
|
| 106 | +You have a list of other agents to transfer to: |
| 107 | +
|
| 108 | +
|
| 109 | +Agent name: z_agent |
| 110 | +Agent description: Last agent |
| 111 | +
|
| 112 | +
|
| 113 | +Agent name: a_agent |
| 114 | +Agent description: First agent |
| 115 | +
|
| 116 | +
|
| 117 | +Agent name: m_agent |
| 118 | +Agent description: Middle agent |
| 119 | +
|
| 120 | +
|
| 121 | +Agent name: parent_agent |
| 122 | +Agent description: Parent agent |
| 123 | +
|
| 124 | +
|
| 125 | +Agent name: peer_agent |
| 126 | +Agent description: Peer agent |
| 127 | +
|
| 128 | +
|
| 129 | +If you are the best to answer the question according to your description, you |
| 130 | +can answer it. |
| 131 | +
|
| 132 | +If another agent is better for answering the question according to its |
| 133 | +description, call `transfer_to_agent` function to transfer the |
| 134 | +question to that agent. When transferring, do not generate any text other than |
| 135 | +the function call. |
| 136 | +
|
| 137 | +**NOTE**: the only available agents for `transfer_to_agent` function are `a_agent`, `m_agent`, `parent_agent`, `peer_agent`, `z_agent`. |
| 138 | +
|
| 139 | +If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent.""" |
| 140 | + |
| 141 | + assert expected_content in instructions |
| 142 | + |
| 143 | + |
| 144 | +@pytest.mark.asyncio |
| 145 | +async def test_agent_transfer_system_instructions_without_parent(): |
| 146 | + """Test system instructions when agent has no parent.""" |
| 147 | + mockModel = testing_utils.MockModel.create(responses=[]) |
| 148 | + |
| 149 | + # Create agents without parent |
| 150 | + sub_agent_1 = Agent( |
| 151 | + name='agent1', model=mockModel, description='First sub-agent' |
| 152 | + ) |
| 153 | + sub_agent_2 = Agent( |
| 154 | + name='agent2', model=mockModel, description='Second sub-agent' |
| 155 | + ) |
| 156 | + |
| 157 | + main_agent = Agent( |
| 158 | + name='main_agent', |
| 159 | + model=mockModel, |
| 160 | + sub_agents=[sub_agent_1, sub_agent_2], |
| 161 | + # No parent_agent |
| 162 | + description='Main agent without parent', |
| 163 | + ) |
| 164 | + |
| 165 | + # Create test context and LLM request |
| 166 | + invocation_context = await create_test_invocation_context(main_agent) |
| 167 | + llm_request = LlmRequest() |
| 168 | + |
| 169 | + # Call the agent transfer request processor |
| 170 | + async for _ in agent_transfer.request_processor.run_async( |
| 171 | + invocation_context, llm_request |
| 172 | + ): |
| 173 | + pass |
| 174 | + |
| 175 | + # Assert behavior: should only include sub-agents in NOTE, no parent |
| 176 | + instructions = llm_request.config.system_instruction |
| 177 | + |
| 178 | + # Direct multiline string assertion showing the exact expected content |
| 179 | + expected_content = """\ |
| 180 | +
|
| 181 | +You have a list of other agents to transfer to: |
| 182 | +
|
| 183 | +
|
| 184 | +Agent name: agent1 |
| 185 | +Agent description: First sub-agent |
| 186 | +
|
| 187 | +
|
| 188 | +Agent name: agent2 |
| 189 | +Agent description: Second sub-agent |
| 190 | +
|
| 191 | +
|
| 192 | +If you are the best to answer the question according to your description, you |
| 193 | +can answer it. |
| 194 | +
|
| 195 | +If another agent is better for answering the question according to its |
| 196 | +description, call `transfer_to_agent` function to transfer the |
| 197 | +question to that agent. When transferring, do not generate any text other than |
| 198 | +the function call. |
| 199 | +
|
| 200 | +**NOTE**: the only available agents for `transfer_to_agent` function are `agent1`, `agent2`.""" |
| 201 | + |
| 202 | + assert expected_content in instructions |
| 203 | + |
| 204 | + |
| 205 | +@pytest.mark.asyncio |
| 206 | +async def test_agent_transfer_simplified_parent_instructions(): |
| 207 | + """Test that parent agent instructions are simplified and not verbose.""" |
| 208 | + mockModel = testing_utils.MockModel.create(responses=[]) |
| 209 | + |
| 210 | + # Create agent with parent |
| 211 | + sub_agent = Agent(name='sub_agent', model=mockModel, description='Sub agent') |
| 212 | + parent_agent = Agent( |
| 213 | + name='parent_agent', model=mockModel, description='Parent agent' |
| 214 | + ) |
| 215 | + |
| 216 | + main_agent = Agent( |
| 217 | + name='main_agent', |
| 218 | + model=mockModel, |
| 219 | + sub_agents=[sub_agent], |
| 220 | + parent_agent=parent_agent, |
| 221 | + description='Main agent with parent', |
| 222 | + ) |
| 223 | + |
| 224 | + # Create test context and LLM request |
| 225 | + invocation_context = await create_test_invocation_context(main_agent) |
| 226 | + llm_request = LlmRequest() |
| 227 | + |
| 228 | + # Call the agent transfer request processor |
| 229 | + async for _ in agent_transfer.request_processor.run_async( |
| 230 | + invocation_context, llm_request |
| 231 | + ): |
| 232 | + pass |
| 233 | + |
| 234 | + # Assert behavior: parent instructions should be simplified |
| 235 | + instructions = llm_request.config.system_instruction |
| 236 | + |
| 237 | + # Direct multiline string assertion showing the exact expected content |
| 238 | + expected_content = """\ |
| 239 | +
|
| 240 | +You have a list of other agents to transfer to: |
| 241 | +
|
| 242 | +
|
| 243 | +Agent name: sub_agent |
| 244 | +Agent description: Sub agent |
| 245 | +
|
| 246 | +
|
| 247 | +Agent name: parent_agent |
| 248 | +Agent description: Parent agent |
| 249 | +
|
| 250 | +
|
| 251 | +If you are the best to answer the question according to your description, you |
| 252 | +can answer it. |
| 253 | +
|
| 254 | +If another agent is better for answering the question according to its |
| 255 | +description, call `transfer_to_agent` function to transfer the |
| 256 | +question to that agent. When transferring, do not generate any text other than |
| 257 | +the function call. |
| 258 | +
|
| 259 | +**NOTE**: the only available agents for `transfer_to_agent` function are `parent_agent`, `sub_agent`. |
| 260 | +
|
| 261 | +If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent.""" |
| 262 | + |
| 263 | + assert expected_content in instructions |
| 264 | + |
| 265 | + |
| 266 | +@pytest.mark.asyncio |
| 267 | +async def test_agent_transfer_no_instructions_when_no_transfer_targets(): |
| 268 | + """Test that no instructions are added when there are no transfer targets.""" |
| 269 | + mockModel = testing_utils.MockModel.create(responses=[]) |
| 270 | + |
| 271 | + # Create agent with no sub-agents and no parent |
| 272 | + main_agent = Agent( |
| 273 | + name='main_agent', |
| 274 | + model=mockModel, |
| 275 | + # No sub_agents, no parent_agent |
| 276 | + description='Isolated agent', |
| 277 | + ) |
| 278 | + |
| 279 | + # Create test context and LLM request |
| 280 | + invocation_context = await create_test_invocation_context(main_agent) |
| 281 | + llm_request = LlmRequest() |
| 282 | + original_system_instruction = llm_request.config.system_instruction |
| 283 | + |
| 284 | + # Call the agent transfer request processor |
| 285 | + async for _ in agent_transfer.request_processor.run_async( |
| 286 | + invocation_context, llm_request |
| 287 | + ): |
| 288 | + pass |
| 289 | + |
| 290 | + # Assert behavior: no instructions should be added |
| 291 | + assert llm_request.config.system_instruction == original_system_instruction |
| 292 | + |
| 293 | + instructions = llm_request.config.system_instruction or '' |
| 294 | + assert '**NOTE**:' not in instructions |
| 295 | + assert 'transfer_to_agent' not in instructions |
0 commit comments