ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
198 lines
6.9 KiB
Python
198 lines
6.9 KiB
Python
# Copyright 2026 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.
|
|
|
|
import asyncio
|
|
import contextlib
|
|
from typing import AsyncGenerator
|
|
|
|
from google.adk.agents.live_request_queue import LiveRequestQueue
|
|
from google.adk.agents.llm_agent import Agent
|
|
from google.adk.models.llm_response import LlmResponse
|
|
from google.genai import types
|
|
import pytest
|
|
from typing_extensions import override # <-- FIX: Add this import
|
|
from websockets import frames # <-- FIX 1: Import the frames module
|
|
from websockets.exceptions import ConnectionClosed
|
|
|
|
from .. import testing_utils
|
|
|
|
|
|
def test_live_streaming_multi_agent_single_tool():
|
|
"""Test live streaming with multi-agent delegation for a single tool call."""
|
|
# --- 1. Mock LLM Responses ---
|
|
|
|
# Mock response for the root_agent to delegate the task to the roll_agent.
|
|
# FIX: Use from_function_call to represent delegation to a sub-agent.
|
|
delegation_to_roll_agent = types.Part.from_function_call(
|
|
name='transfer_to_agent', args={'agent_name': 'roll_agent'}
|
|
)
|
|
|
|
root_response1 = LlmResponse(
|
|
content=types.Content(role='model', parts=[delegation_to_roll_agent]),
|
|
turn_complete=False,
|
|
)
|
|
root_response2 = LlmResponse(turn_complete=True)
|
|
mock_root_model = testing_utils.MockModel.create(
|
|
[root_response1, root_response2]
|
|
)
|
|
|
|
# Mock response for the roll_agent to call its `roll_die` tool.
|
|
function_call = types.Part.from_function_call(
|
|
name='roll_die', args={'sides': 20}
|
|
)
|
|
roll_agent_response1 = LlmResponse(
|
|
content=types.Content(role='model', parts=[function_call]),
|
|
turn_complete=False,
|
|
)
|
|
roll_agent_response2 = LlmResponse(turn_complete=True)
|
|
mock_roll_model = testing_utils.MockModel.create(
|
|
[roll_agent_response1, roll_agent_response2]
|
|
)
|
|
|
|
# --- 2. Mock Tools and Agents ---
|
|
|
|
def roll_die(sides: int) -> int:
|
|
"""Rolls a die and returns a fixed result for testing."""
|
|
return 15
|
|
|
|
mock_roll_sub_agent = Agent(
|
|
name='roll_agent',
|
|
model=mock_roll_model,
|
|
tools=[roll_die],
|
|
)
|
|
|
|
main_agent = Agent(
|
|
name='root_agent',
|
|
model=mock_root_model,
|
|
sub_agents=[mock_roll_sub_agent],
|
|
)
|
|
|
|
# --- 3. Test Runner Setup ---
|
|
class CustomTestRunner(testing_utils.InMemoryRunner):
|
|
|
|
def run_live(
|
|
self,
|
|
live_request_queue: LiveRequestQueue,
|
|
run_config: testing_utils.RunConfig = None,
|
|
) -> list[testing_utils.Event]:
|
|
collected_responses = []
|
|
|
|
async def consume_responses(session: testing_utils.Session):
|
|
run_res = self.runner.run_live(
|
|
session=session,
|
|
live_request_queue=live_request_queue,
|
|
run_config=run_config or testing_utils.RunConfig(),
|
|
)
|
|
from contextlib import aclosing
|
|
|
|
async with aclosing(run_res) as agen:
|
|
async for response in agen:
|
|
collected_responses.append(response)
|
|
if len(collected_responses) >= 5:
|
|
return
|
|
|
|
try:
|
|
session = self.session
|
|
asyncio.run(asyncio.wait_for(consume_responses(session), timeout=5.0))
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
pass
|
|
return collected_responses
|
|
|
|
runner = CustomTestRunner(root_agent=main_agent)
|
|
live_request_queue = LiveRequestQueue()
|
|
live_request_queue.send_realtime(
|
|
blob=types.Blob(data=b'Roll a 20-sided die', mime_type='audio/pcm')
|
|
)
|
|
|
|
# --- 4. Run and Assert ---
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
assert res_events is not None, 'Expected a list of events, but got None.'
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
delegation_found = False
|
|
tool_call_found = False
|
|
tool_response_found = False
|
|
|
|
for event in res_events:
|
|
if event.content and event.content.parts:
|
|
for part in event.content.parts:
|
|
if part.function_call:
|
|
# FIX: Check for the function call that represents delegation.
|
|
if part.function_call.name == 'transfer_to_agent':
|
|
delegation_found = True
|
|
assert part.function_call.args == {'agent_name': 'roll_agent'}
|
|
|
|
# Check for the function call made by the roll_agent.
|
|
if part.function_call.name == 'roll_die':
|
|
tool_call_found = True
|
|
assert part.function_call.args['sides'] == 20
|
|
|
|
# Check for the result from the executed function.
|
|
if part.function_response and part.function_response.name == 'roll_die':
|
|
tool_response_found = True
|
|
assert part.function_response.response['result'] == 15
|
|
|
|
assert delegation_found, 'A function_call event for delegation was not found.'
|
|
assert tool_call_found, 'A function_call event for roll_die was not found.'
|
|
assert tool_response_found, 'A function_response for roll_die was not found.'
|
|
|
|
|
|
def test_live_streaming_connection_error_on_connect():
|
|
"""
|
|
Tests that the runner correctly handles a ConnectionClosed exception
|
|
raised from the model's `connect` method during a live run.
|
|
"""
|
|
|
|
# 1. Create a mock model that fails during the connection phase.
|
|
class MockModelThatFailsToConnect(testing_utils.MockModel):
|
|
|
|
@contextlib.asynccontextmanager
|
|
@override
|
|
async def connect(self, llm_request: testing_utils.LlmRequest):
|
|
"""Override connect to simulate an immediate connection failure."""
|
|
|
|
# FIX 2: Create a proper `Close` frame object first.
|
|
close_frame = frames.Close(
|
|
1007,
|
|
'gemini-live-2.5-flash-preview is not supported in the live api.',
|
|
)
|
|
|
|
# FIX 3: Pass the frame object to the `rcvd` parameter of the exception.
|
|
raise ConnectionClosed(rcvd=close_frame, sent=None)
|
|
|
|
yield # pragma: no cover
|
|
|
|
# 2. Instantiate the custom mock model.
|
|
mock_model = MockModelThatFailsToConnect(responses=[])
|
|
|
|
# 3. Set up the agent and runner.
|
|
agent = Agent(name='test_agent_for_connection_failure', model=mock_model)
|
|
runner = testing_utils.InMemoryRunner(root_agent=agent)
|
|
live_request_queue = LiveRequestQueue()
|
|
live_request_queue.send_realtime(
|
|
blob=types.Blob(data=b'Initial audio chunk', mime_type='audio/pcm')
|
|
)
|
|
|
|
# 4. Assert that `run_live` raises `ConnectionClosed`.
|
|
with pytest.raises(ConnectionClosed) as excinfo:
|
|
runner.run_live(live_request_queue)
|
|
|
|
# 5. Verify the details of the exception. The `code` and `reason` are
|
|
# attributes of the received frame (`rcvd`), not the exception itself.
|
|
assert excinfo.value.rcvd.code == 1007
|
|
assert (
|
|
'is not supported in the live api' in excinfo.value.rcvd.reason
|
|
), 'The exception reason should match the simulated server error.'
|