mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
fix: Add a NOTE to agent transfer instructions listing available agents
The system instructions for agent transfer now include a NOTE section that lists all agents available for the `transfer_to_agent` function. This also has the target agents and, if there is one that applies, the parent agent. New unit tests are added to verify the correct generation of this NOTE. PiperOrigin-RevId: 804569691
This commit is contained in:
committed by
Copybara-Service
parent
5b465fd71b
commit
43eec82f84
@@ -82,6 +82,18 @@ line_break = '\n'
|
||||
def _build_target_agents_instructions(
|
||||
agent: LlmAgent, target_agents: list[BaseAgent]
|
||||
) -> str:
|
||||
# Build list of available agent names for the NOTE
|
||||
# target_agents already includes parent agent if applicable, so no need to add it again
|
||||
available_agent_names = [target_agent.name for target_agent in target_agents]
|
||||
|
||||
# Sort for consistency
|
||||
available_agent_names.sort()
|
||||
|
||||
# Format agent names with backticks for clarity
|
||||
formatted_agent_names = ', '.join(
|
||||
f'`{name}`' for name in available_agent_names
|
||||
)
|
||||
|
||||
si = f"""
|
||||
You have a list of other agents to transfer to:
|
||||
|
||||
@@ -96,13 +108,13 @@ If another agent is better for answering the question according to its
|
||||
description, call `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function to transfer the
|
||||
question to that agent. When transferring, do not generate any text other than
|
||||
the function call.
|
||||
|
||||
**NOTE**: the only available agents for `{_TRANSFER_TO_AGENT_FUNCTION_NAME}` function are {formatted_agent_names}.
|
||||
"""
|
||||
|
||||
if agent.parent_agent and not agent.disallow_transfer_to_parent:
|
||||
si += f"""
|
||||
Your parent agent is {agent.parent_agent.name}. If neither the other agents nor
|
||||
you are best for answering the question according to the descriptions, transfer
|
||||
to your parent agent.
|
||||
If neither you nor the other agents are best for the question, transfer to your parent agent {agent.parent_agent.name}.
|
||||
"""
|
||||
return si
|
||||
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
# 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 agent transfer system instructions.
|
||||
|
||||
These tests verify the behavior of the agent transfer system by calling
|
||||
the request processor and checking the resulting system instructions not just
|
||||
implementation.
|
||||
"""
|
||||
|
||||
from google.adk.agents.invocation_context import InvocationContext
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.flows.llm_flows import agent_transfer
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.models.llm_request import LlmRequest
|
||||
from google.adk.plugins.plugin_manager import PluginManager
|
||||
from google.adk.runners import RunConfig
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.genai import types
|
||||
import pytest
|
||||
|
||||
from ... import testing_utils
|
||||
|
||||
|
||||
async def create_test_invocation_context(agent: Agent) -> InvocationContext:
|
||||
"""Helper to create constructed InvocationContext."""
|
||||
session_service = InMemorySessionService()
|
||||
memory_service = InMemoryMemoryService()
|
||||
session = await session_service.create_session(
|
||||
app_name='test_app', user_id='test_user'
|
||||
)
|
||||
|
||||
return InvocationContext(
|
||||
artifact_service=InMemoryArtifactService(),
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
plugin_manager=PluginManager(plugins=[]),
|
||||
invocation_id='test_invocation_id',
|
||||
agent=agent,
|
||||
session=session,
|
||||
user_content=types.Content(
|
||||
role='user', parts=[types.Part.from_text(text='test')]
|
||||
),
|
||||
run_config=RunConfig(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_transfer_includes_sorted_agent_names_in_system_instructions():
|
||||
"""Test that agent transfer adds NOTE with sorted agent names to system instructions."""
|
||||
mockModel = testing_utils.MockModel.create(responses=[])
|
||||
|
||||
# Create agents with names that will test alphabetical sorting
|
||||
z_agent = Agent(name='z_agent', model=mockModel, description='Last agent')
|
||||
a_agent = Agent(name='a_agent', model=mockModel, description='First agent')
|
||||
m_agent = Agent(name='m_agent', model=mockModel, description='Middle agent')
|
||||
peer_agent = Agent(
|
||||
name='peer_agent', model=mockModel, description='Peer agent'
|
||||
)
|
||||
|
||||
# Create parent agent with a peer agent
|
||||
parent_agent = Agent(
|
||||
name='parent_agent',
|
||||
model=mockModel,
|
||||
sub_agents=[peer_agent],
|
||||
description='Parent agent',
|
||||
)
|
||||
|
||||
# Create main agent with sub-agents and parent (intentionally unsorted order)
|
||||
main_agent = Agent(
|
||||
name='main_agent',
|
||||
model=mockModel,
|
||||
sub_agents=[z_agent, a_agent, m_agent], # Unsorted input
|
||||
parent_agent=parent_agent,
|
||||
description='Main coordinating agent',
|
||||
)
|
||||
|
||||
# Create test context and LLM request
|
||||
invocation_context = await create_test_invocation_context(main_agent)
|
||||
llm_request = LlmRequest()
|
||||
|
||||
# Call the actual agent transfer request processor (this behavior we're testing)
|
||||
async for _ in agent_transfer.request_processor.run_async(
|
||||
invocation_context, llm_request
|
||||
):
|
||||
pass
|
||||
|
||||
# Check on the behavior: verify system instructions contain sorted agent names
|
||||
instructions = llm_request.config.system_instruction
|
||||
|
||||
# The NOTE should contain agents in alphabetical order: sub-agents + parent + peers
|
||||
expected_content = """\
|
||||
|
||||
You have a list of other agents to transfer to:
|
||||
|
||||
|
||||
Agent name: z_agent
|
||||
Agent description: Last agent
|
||||
|
||||
|
||||
Agent name: a_agent
|
||||
Agent description: First agent
|
||||
|
||||
|
||||
Agent name: m_agent
|
||||
Agent description: Middle agent
|
||||
|
||||
|
||||
Agent name: parent_agent
|
||||
Agent description: Parent agent
|
||||
|
||||
|
||||
Agent name: peer_agent
|
||||
Agent description: Peer agent
|
||||
|
||||
|
||||
If you are the best to answer the question according to your description, you
|
||||
can answer it.
|
||||
|
||||
If another agent is better for answering the question according to its
|
||||
description, call `transfer_to_agent` function to transfer the
|
||||
question to that agent. When transferring, do not generate any text other than
|
||||
the function call.
|
||||
|
||||
**NOTE**: the only available agents for `transfer_to_agent` function are `a_agent`, `m_agent`, `parent_agent`, `peer_agent`, `z_agent`.
|
||||
|
||||
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
|
||||
|
||||
assert expected_content in instructions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_transfer_system_instructions_without_parent():
|
||||
"""Test system instructions when agent has no parent."""
|
||||
mockModel = testing_utils.MockModel.create(responses=[])
|
||||
|
||||
# Create agents without parent
|
||||
sub_agent_1 = Agent(
|
||||
name='agent1', model=mockModel, description='First sub-agent'
|
||||
)
|
||||
sub_agent_2 = Agent(
|
||||
name='agent2', model=mockModel, description='Second sub-agent'
|
||||
)
|
||||
|
||||
main_agent = Agent(
|
||||
name='main_agent',
|
||||
model=mockModel,
|
||||
sub_agents=[sub_agent_1, sub_agent_2],
|
||||
# No parent_agent
|
||||
description='Main agent without parent',
|
||||
)
|
||||
|
||||
# Create test context and LLM request
|
||||
invocation_context = await create_test_invocation_context(main_agent)
|
||||
llm_request = LlmRequest()
|
||||
|
||||
# Call the agent transfer request processor
|
||||
async for _ in agent_transfer.request_processor.run_async(
|
||||
invocation_context, llm_request
|
||||
):
|
||||
pass
|
||||
|
||||
# Assert behavior: should only include sub-agents in NOTE, no parent
|
||||
instructions = llm_request.config.system_instruction
|
||||
|
||||
# Direct multiline string assertion showing the exact expected content
|
||||
expected_content = """\
|
||||
|
||||
You have a list of other agents to transfer to:
|
||||
|
||||
|
||||
Agent name: agent1
|
||||
Agent description: First sub-agent
|
||||
|
||||
|
||||
Agent name: agent2
|
||||
Agent description: Second sub-agent
|
||||
|
||||
|
||||
If you are the best to answer the question according to your description, you
|
||||
can answer it.
|
||||
|
||||
If another agent is better for answering the question according to its
|
||||
description, call `transfer_to_agent` function to transfer the
|
||||
question to that agent. When transferring, do not generate any text other than
|
||||
the function call.
|
||||
|
||||
**NOTE**: the only available agents for `transfer_to_agent` function are `agent1`, `agent2`."""
|
||||
|
||||
assert expected_content in instructions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_transfer_simplified_parent_instructions():
|
||||
"""Test that parent agent instructions are simplified and not verbose."""
|
||||
mockModel = testing_utils.MockModel.create(responses=[])
|
||||
|
||||
# Create agent with parent
|
||||
sub_agent = Agent(name='sub_agent', model=mockModel, description='Sub agent')
|
||||
parent_agent = Agent(
|
||||
name='parent_agent', model=mockModel, description='Parent agent'
|
||||
)
|
||||
|
||||
main_agent = Agent(
|
||||
name='main_agent',
|
||||
model=mockModel,
|
||||
sub_agents=[sub_agent],
|
||||
parent_agent=parent_agent,
|
||||
description='Main agent with parent',
|
||||
)
|
||||
|
||||
# Create test context and LLM request
|
||||
invocation_context = await create_test_invocation_context(main_agent)
|
||||
llm_request = LlmRequest()
|
||||
|
||||
# Call the agent transfer request processor
|
||||
async for _ in agent_transfer.request_processor.run_async(
|
||||
invocation_context, llm_request
|
||||
):
|
||||
pass
|
||||
|
||||
# Assert behavior: parent instructions should be simplified
|
||||
instructions = llm_request.config.system_instruction
|
||||
|
||||
# Direct multiline string assertion showing the exact expected content
|
||||
expected_content = """\
|
||||
|
||||
You have a list of other agents to transfer to:
|
||||
|
||||
|
||||
Agent name: sub_agent
|
||||
Agent description: Sub agent
|
||||
|
||||
|
||||
Agent name: parent_agent
|
||||
Agent description: Parent agent
|
||||
|
||||
|
||||
If you are the best to answer the question according to your description, you
|
||||
can answer it.
|
||||
|
||||
If another agent is better for answering the question according to its
|
||||
description, call `transfer_to_agent` function to transfer the
|
||||
question to that agent. When transferring, do not generate any text other than
|
||||
the function call.
|
||||
|
||||
**NOTE**: the only available agents for `transfer_to_agent` function are `parent_agent`, `sub_agent`.
|
||||
|
||||
If neither you nor the other agents are best for the question, transfer to your parent agent parent_agent."""
|
||||
|
||||
assert expected_content in instructions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_transfer_no_instructions_when_no_transfer_targets():
|
||||
"""Test that no instructions are added when there are no transfer targets."""
|
||||
mockModel = testing_utils.MockModel.create(responses=[])
|
||||
|
||||
# Create agent with no sub-agents and no parent
|
||||
main_agent = Agent(
|
||||
name='main_agent',
|
||||
model=mockModel,
|
||||
# No sub_agents, no parent_agent
|
||||
description='Isolated agent',
|
||||
)
|
||||
|
||||
# Create test context and LLM request
|
||||
invocation_context = await create_test_invocation_context(main_agent)
|
||||
llm_request = LlmRequest()
|
||||
original_system_instruction = llm_request.config.system_instruction
|
||||
|
||||
# Call the agent transfer request processor
|
||||
async for _ in agent_transfer.request_processor.run_async(
|
||||
invocation_context, llm_request
|
||||
):
|
||||
pass
|
||||
|
||||
# Assert behavior: no instructions should be added
|
||||
assert llm_request.config.system_instruction == original_system_instruction
|
||||
|
||||
instructions = llm_request.config.system_instruction or ''
|
||||
assert '**NOTE**:' not in instructions
|
||||
assert 'transfer_to_agent' not in instructions
|
||||
Reference in New Issue
Block a user