2025-04-08 17:22:09 +00:00
|
|
|
# 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.
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
import asyncio
|
|
|
|
|
from typing import AsyncGenerator
|
|
|
|
|
|
2025-07-23 10:47:47 -07:00
|
|
|
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
|
2025-04-08 17:22:09 +00:00
|
|
|
from google.genai import types
|
|
|
|
|
import pytest
|
|
|
|
|
|
2025-05-21 16:34:28 -07:00
|
|
|
from .. import testing_utils
|
2025-04-08 17:22:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_streaming():
|
|
|
|
|
response1 = LlmResponse(
|
|
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-21 16:34:28 -07:00
|
|
|
mock_model = testing_utils.MockModel.create([response1])
|
2025-04-08 17:22:09 +00:00
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
|
|
|
|
tools=[],
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-21 16:34:28 -07:00
|
|
|
runner = testing_utils.InMemoryRunner(
|
2025-04-08 17:22:09 +00:00
|
|
|
root_agent=root_agent, response_modalities=['AUDIO']
|
|
|
|
|
)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(data=b'\x00\xFF', mime_type='audio/pcm')
|
|
|
|
|
)
|
|
|
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
|
|
|
|
assert (
|
|
|
|
|
len(res_events) > 0
|
|
|
|
|
), 'Expected at least one response, but got an empty list.'
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
def test_live_streaming_function_call_single():
|
|
|
|
|
"""Test live streaming with a single function call response."""
|
|
|
|
|
# Create a function call response
|
|
|
|
|
function_call = types.Part.from_function_call(
|
|
|
|
|
name='get_weather', args={'location': 'San Francisco', 'unit': 'celsius'}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses: function call followed by turn completion
|
2025-07-07 11:00:12 -07:00
|
|
|
response1 = LlmResponse(
|
2025-07-09 19:40:56 -07:00
|
|
|
content=types.Content(role='model', parts=[function_call]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
2025-07-07 11:00:12 -07:00
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2])
|
|
|
|
|
|
|
|
|
|
# Mock function that would be called
|
|
|
|
|
def get_weather(location: str, unit: str = 'celsius') -> dict:
|
|
|
|
|
return {
|
|
|
|
|
'temperature': 22,
|
|
|
|
|
'condition': 'sunny',
|
|
|
|
|
'location': location,
|
|
|
|
|
'unit': unit,
|
|
|
|
|
}
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
2025-07-09 19:40:56 -07:00
|
|
|
tools=[get_weather],
|
2025-07-07 11:00:12 -07:00
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
# Create a custom runner class that collects all events
|
|
|
|
|
class CustomTestRunner(testing_utils.InMemoryRunner):
|
2025-07-07 11:00:12 -07:00
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
def run_live(
|
|
|
|
|
self,
|
|
|
|
|
live_request_queue: LiveRequestQueue,
|
|
|
|
|
run_config: testing_utils.RunConfig = None,
|
|
|
|
|
) -> list[testing_utils.Event]:
|
|
|
|
|
collected_responses = []
|
2025-07-07 11:00:12 -07:00
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
# Collect a reasonable number of events, don't wait for too many
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
# Return whatever we collected so far
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
2025-07-07 11:00:12 -07:00
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
2025-07-09 19:40:56 -07:00
|
|
|
blob=types.Blob(
|
|
|
|
|
data=b'What is the weather in San Francisco?', mime_type='audio/pcm'
|
2025-07-07 11:00:12 -07:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
res_events = runner.run_live(live_request_queue)
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
2025-07-09 19:40:56 -07:00
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check that we got a function call event
|
|
|
|
|
function_call_found = False
|
|
|
|
|
function_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 and part.function_call.name == 'get_weather':
|
|
|
|
|
function_call_found = True
|
|
|
|
|
assert part.function_call.args['location'] == 'San Francisco'
|
|
|
|
|
assert part.function_call.args['unit'] == 'celsius'
|
|
|
|
|
elif (
|
|
|
|
|
part.function_response
|
|
|
|
|
and part.function_response.name == 'get_weather'
|
|
|
|
|
):
|
|
|
|
|
function_response_found = True
|
|
|
|
|
assert part.function_response.response['temperature'] == 22
|
|
|
|
|
assert part.function_response.response['condition'] == 'sunny'
|
|
|
|
|
|
|
|
|
|
assert function_call_found, 'Expected a function call event.'
|
|
|
|
|
# Note: In live streaming, function responses might be handled differently,
|
|
|
|
|
# so we check for the function call which is the primary indicator of function calling working
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
def test_live_streaming_function_call_multiple():
|
|
|
|
|
"""Test live streaming with multiple function calls in sequence."""
|
|
|
|
|
# Create multiple function call responses
|
|
|
|
|
function_call1 = types.Part.from_function_call(
|
|
|
|
|
name='get_weather', args={'location': 'San Francisco'}
|
|
|
|
|
)
|
|
|
|
|
function_call2 = types.Part.from_function_call(
|
|
|
|
|
name='get_time', args={'timezone': 'PST'}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses: two function calls followed by turn completion
|
2025-07-07 11:00:12 -07:00
|
|
|
response1 = LlmResponse(
|
2025-07-09 19:40:56 -07:00
|
|
|
content=types.Content(role='model', parts=[function_call1]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
|
|
|
|
content=types.Content(role='model', parts=[function_call2]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response3 = LlmResponse(
|
2025-07-07 11:00:12 -07:00
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2, response3])
|
|
|
|
|
|
|
|
|
|
# Mock functions
|
|
|
|
|
def get_weather(location: str) -> dict:
|
|
|
|
|
return {'temperature': 22, 'condition': 'sunny', 'location': location}
|
|
|
|
|
|
|
|
|
|
def get_time(timezone: str) -> dict:
|
|
|
|
|
return {'time': '14:30', 'timezone': timezone}
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
2025-07-09 19:40:56 -07:00
|
|
|
tools=[get_weather, get_time],
|
2025-07-07 11:00:12 -07:00
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
# Use the custom runner
|
|
|
|
|
class CustomTestRunner(testing_utils.InMemoryRunner):
|
2025-07-07 11:00:12 -07:00
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(
|
|
|
|
|
data=b'What is the weather and current time?', mime_type='audio/pcm'
|
2025-07-07 11:00:12 -07:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
res_events = runner.run_live(live_request_queue)
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
2025-07-09 19:40:56 -07:00
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check function calls
|
|
|
|
|
weather_call_found = False
|
|
|
|
|
time_call_found = False
|
|
|
|
|
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if part.function_call:
|
|
|
|
|
if part.function_call.name == 'get_weather':
|
|
|
|
|
weather_call_found = True
|
|
|
|
|
assert part.function_call.args['location'] == 'San Francisco'
|
|
|
|
|
elif part.function_call.name == 'get_time':
|
|
|
|
|
time_call_found = True
|
|
|
|
|
assert part.function_call.args['timezone'] == 'PST'
|
|
|
|
|
|
|
|
|
|
# In live streaming, we primarily check that function calls are generated correctly
|
2025-07-07 11:00:12 -07:00
|
|
|
assert (
|
2025-07-09 19:40:56 -07:00
|
|
|
weather_call_found or time_call_found
|
|
|
|
|
), 'Expected at least one function call.'
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
def test_live_streaming_function_call_parallel():
|
|
|
|
|
"""Test live streaming with parallel function calls."""
|
|
|
|
|
# Create parallel function calls in the same response
|
|
|
|
|
function_call1 = types.Part.from_function_call(
|
|
|
|
|
name='get_weather', args={'location': 'San Francisco'}
|
|
|
|
|
)
|
|
|
|
|
function_call2 = types.Part.from_function_call(
|
|
|
|
|
name='get_weather', args={'location': 'New York'}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM response with parallel function calls
|
2025-07-07 11:00:12 -07:00
|
|
|
response1 = LlmResponse(
|
2025-07-09 19:40:56 -07:00
|
|
|
content=types.Content(
|
|
|
|
|
role='model', parts=[function_call1, function_call2]
|
2025-07-07 11:00:12 -07:00
|
|
|
),
|
2025-07-09 19:40:56 -07:00
|
|
|
turn_complete=False,
|
2025-07-07 11:00:12 -07:00
|
|
|
)
|
2025-07-09 19:40:56 -07:00
|
|
|
response2 = LlmResponse(
|
2025-07-07 11:00:12 -07:00
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2])
|
|
|
|
|
|
|
|
|
|
# Mock function
|
|
|
|
|
def get_weather(location: str) -> dict:
|
|
|
|
|
temperatures = {'San Francisco': 22, 'New York': 15}
|
|
|
|
|
return {'temperature': temperatures.get(location, 20), 'location': location}
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
2025-07-09 19:40:56 -07:00
|
|
|
tools=[get_weather],
|
2025-07-07 11:00:12 -07:00
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
# Use the custom runner
|
|
|
|
|
class CustomTestRunner(testing_utils.InMemoryRunner):
|
2025-07-07 11:00:12 -07:00
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
def run_live(
|
|
|
|
|
self,
|
|
|
|
|
live_request_queue: LiveRequestQueue,
|
|
|
|
|
run_config: testing_utils.RunConfig = None,
|
|
|
|
|
) -> list[testing_utils.Event]:
|
|
|
|
|
collected_responses = []
|
2025-07-07 11:00:12 -07:00
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
2025-07-07 11:00:12 -07:00
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
2025-07-09 19:40:56 -07:00
|
|
|
blob=types.Blob(
|
|
|
|
|
data=b'Compare weather in SF and NYC', mime_type='audio/pcm'
|
|
|
|
|
)
|
2025-07-07 11:00:12 -07:00
|
|
|
)
|
|
|
|
|
|
2025-07-09 19:40:56 -07:00
|
|
|
res_events = runner.run_live(live_request_queue)
|
2025-07-07 11:00:12 -07:00
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
2025-07-09 19:40:56 -07:00
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check parallel function calls
|
|
|
|
|
sf_call_found = False
|
|
|
|
|
nyc_call_found = False
|
|
|
|
|
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if part.function_call and part.function_call.name == 'get_weather':
|
|
|
|
|
location = part.function_call.args['location']
|
|
|
|
|
if location == 'San Francisco':
|
|
|
|
|
sf_call_found = True
|
|
|
|
|
elif location == 'New York':
|
|
|
|
|
nyc_call_found = True
|
|
|
|
|
|
2025-07-07 11:00:12 -07:00
|
|
|
assert (
|
2025-07-09 19:40:56 -07:00
|
|
|
sf_call_found and nyc_call_found
|
|
|
|
|
), 'Expected both location function calls.'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_streaming_function_call_with_error():
|
|
|
|
|
"""Test live streaming with function call that returns an error."""
|
|
|
|
|
# Create a function call response
|
|
|
|
|
function_call = types.Part.from_function_call(
|
|
|
|
|
name='get_weather', args={'location': 'Invalid Location'}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses
|
|
|
|
|
response1 = LlmResponse(
|
|
|
|
|
content=types.Content(role='model', parts=[function_call]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
|
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2])
|
|
|
|
|
|
|
|
|
|
# Mock function that returns an error for invalid locations
|
|
|
|
|
def get_weather(location: str) -> dict:
|
|
|
|
|
if location == 'Invalid Location':
|
|
|
|
|
return {'error': 'Location not found'}
|
|
|
|
|
return {'temperature': 22, 'condition': 'sunny', 'location': location}
|
|
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
|
|
|
|
tools=[get_weather],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Use the custom runner
|
|
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(
|
|
|
|
|
data=b'What is weather in Invalid Location?', mime_type='audio/pcm'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
|
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check that we got the function call (error handling happens at execution time)
|
|
|
|
|
function_call_found = False
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if part.function_call and part.function_call.name == 'get_weather':
|
|
|
|
|
function_call_found = True
|
|
|
|
|
assert part.function_call.args['location'] == 'Invalid Location'
|
|
|
|
|
|
|
|
|
|
assert function_call_found, 'Expected function call event with error case.'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_streaming_function_call_sync_tool():
|
|
|
|
|
"""Test live streaming with synchronous function call."""
|
|
|
|
|
# Create a function call response
|
|
|
|
|
function_call = types.Part.from_function_call(
|
|
|
|
|
name='calculate', args={'x': 5, 'y': 3}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses
|
|
|
|
|
response1 = LlmResponse(
|
|
|
|
|
content=types.Content(role='model', parts=[function_call]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
|
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2])
|
|
|
|
|
|
|
|
|
|
# Mock sync function
|
|
|
|
|
def calculate(x: int, y: int) -> dict:
|
|
|
|
|
return {'result': x + y, 'operation': 'addition'}
|
|
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
|
|
|
|
tools=[calculate],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Use the custom runner
|
|
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(data=b'Calculate 5 plus 3', mime_type='audio/pcm')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
|
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check function call
|
|
|
|
|
function_call_found = False
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if part.function_call and part.function_call.name == 'calculate':
|
|
|
|
|
function_call_found = True
|
|
|
|
|
assert part.function_call.args['x'] == 5
|
|
|
|
|
assert part.function_call.args['y'] == 3
|
|
|
|
|
|
|
|
|
|
assert function_call_found, 'Expected calculate function call event.'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_streaming_simple_streaming_tool():
|
|
|
|
|
"""Test live streaming with a simple streaming tool (non-video)."""
|
|
|
|
|
# Create a function call response for the streaming tool
|
|
|
|
|
function_call = types.Part.from_function_call(
|
|
|
|
|
name='monitor_stock_price', args={'stock_symbol': 'AAPL'}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses
|
|
|
|
|
response1 = LlmResponse(
|
|
|
|
|
content=types.Content(role='model', parts=[function_call]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
|
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2])
|
|
|
|
|
|
|
|
|
|
# Mock simple streaming tool (without return type annotation to avoid parsing issues)
|
|
|
|
|
async def monitor_stock_price(stock_symbol: str):
|
|
|
|
|
"""Mock streaming tool that monitors stock prices."""
|
|
|
|
|
# Simulate some streaming updates
|
|
|
|
|
yield f'Stock {stock_symbol} price: $150'
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
yield f'Stock {stock_symbol} price: $155'
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
yield f'Stock {stock_symbol} price: $160'
|
|
|
|
|
|
|
|
|
|
def stop_streaming(function_name: str):
|
|
|
|
|
"""Stop the streaming tool."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
|
|
|
|
tools=[monitor_stock_price, stop_streaming],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Use the custom runner
|
|
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(data=b'Monitor AAPL stock price', mime_type='audio/pcm')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
|
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check that we got the streaming tool function call
|
|
|
|
|
function_call_found = False
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if (
|
|
|
|
|
part.function_call
|
|
|
|
|
and part.function_call.name == 'monitor_stock_price'
|
|
|
|
|
):
|
|
|
|
|
function_call_found = True
|
|
|
|
|
assert part.function_call.args['stock_symbol'] == 'AAPL'
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
function_call_found
|
|
|
|
|
), 'Expected monitor_stock_price function call event.'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_streaming_video_streaming_tool():
|
|
|
|
|
"""Test live streaming with a video streaming tool."""
|
|
|
|
|
# Create a function call response for the video streaming tool
|
|
|
|
|
function_call = types.Part.from_function_call(
|
|
|
|
|
name='monitor_video_stream', args={}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses
|
|
|
|
|
response1 = LlmResponse(
|
|
|
|
|
content=types.Content(role='model', parts=[function_call]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
|
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2])
|
|
|
|
|
|
|
|
|
|
# Mock video streaming tool (without return type annotation to avoid parsing issues)
|
|
|
|
|
async def monitor_video_stream(input_stream: LiveRequestQueue):
|
|
|
|
|
"""Mock video streaming tool that processes video frames."""
|
|
|
|
|
# Simulate processing a few frames from the input stream
|
|
|
|
|
frame_count = 0
|
|
|
|
|
while frame_count < 3: # Process a few frames
|
|
|
|
|
try:
|
|
|
|
|
# Try to get a frame from the queue with timeout
|
|
|
|
|
live_req = await asyncio.wait_for(input_stream.get(), timeout=0.1)
|
|
|
|
|
if live_req.blob and live_req.blob.mime_type == 'image/jpeg':
|
|
|
|
|
frame_count += 1
|
|
|
|
|
yield f'Processed frame {frame_count}: detected 2 people'
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
# No more frames, simulate detection anyway for testing
|
|
|
|
|
frame_count += 1
|
|
|
|
|
yield f'Simulated frame {frame_count}: detected 1 person'
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
def stop_streaming(function_name: str):
|
|
|
|
|
"""Stop the streaming tool."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
|
|
|
|
tools=[monitor_video_stream, stop_streaming],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Use the custom runner
|
|
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
|
|
|
|
|
# Send some mock video frames
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(data=b'fake_jpeg_data_1', mime_type='image/jpeg')
|
|
|
|
|
)
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(data=b'fake_jpeg_data_2', mime_type='image/jpeg')
|
|
|
|
|
)
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(data=b'Monitor video stream', mime_type='audio/pcm')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
|
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check that we got the video streaming tool function call
|
|
|
|
|
function_call_found = False
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if (
|
|
|
|
|
part.function_call
|
|
|
|
|
and part.function_call.name == 'monitor_video_stream'
|
|
|
|
|
):
|
|
|
|
|
function_call_found = True
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
function_call_found
|
|
|
|
|
), 'Expected monitor_video_stream function call event.'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_streaming_stop_streaming_tool():
|
|
|
|
|
"""Test live streaming with stop_streaming functionality."""
|
|
|
|
|
# Create function calls for starting and stopping a streaming tool
|
|
|
|
|
start_function_call = types.Part.from_function_call(
|
|
|
|
|
name='monitor_stock_price', args={'stock_symbol': 'TSLA'}
|
|
|
|
|
)
|
|
|
|
|
stop_function_call = types.Part.from_function_call(
|
|
|
|
|
name='stop_streaming', args={'function_name': 'monitor_stock_price'}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses: start streaming, then stop streaming
|
|
|
|
|
response1 = LlmResponse(
|
|
|
|
|
content=types.Content(role='model', parts=[start_function_call]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
|
|
|
|
content=types.Content(role='model', parts=[stop_function_call]),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response3 = LlmResponse(
|
|
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2, response3])
|
|
|
|
|
|
|
|
|
|
# Mock streaming tool and stop function
|
|
|
|
|
async def monitor_stock_price(stock_symbol: str):
|
|
|
|
|
"""Mock streaming tool that monitors stock prices."""
|
|
|
|
|
yield f'Started monitoring {stock_symbol}'
|
|
|
|
|
while True: # Infinite stream (would be stopped by stop_streaming)
|
|
|
|
|
yield f'Stock {stock_symbol} price update'
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
def stop_streaming(function_name: str):
|
|
|
|
|
"""Stop the streaming tool."""
|
|
|
|
|
return f'Stopped streaming for {function_name}'
|
|
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
|
|
|
|
tools=[monitor_stock_price, stop_streaming],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Use the custom runner
|
|
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(data=b'Monitor TSLA and then stop', mime_type='audio/pcm')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
|
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check that we got both function calls
|
|
|
|
|
monitor_call_found = False
|
|
|
|
|
stop_call_found = False
|
|
|
|
|
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if part.function_call:
|
|
|
|
|
if part.function_call.name == 'monitor_stock_price':
|
|
|
|
|
monitor_call_found = True
|
|
|
|
|
assert part.function_call.args['stock_symbol'] == 'TSLA'
|
|
|
|
|
elif part.function_call.name == 'stop_streaming':
|
|
|
|
|
stop_call_found = True
|
|
|
|
|
assert (
|
|
|
|
|
part.function_call.args['function_name']
|
|
|
|
|
== 'monitor_stock_price'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert monitor_call_found, 'Expected monitor_stock_price function call event.'
|
|
|
|
|
assert stop_call_found, 'Expected stop_streaming function call event.'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_live_streaming_multiple_streaming_tools():
|
|
|
|
|
"""Test live streaming with multiple streaming tools running simultaneously."""
|
|
|
|
|
# Create function calls for multiple streaming tools
|
|
|
|
|
stock_function_call = types.Part.from_function_call(
|
|
|
|
|
name='monitor_stock_price', args={'stock_symbol': 'NVDA'}
|
|
|
|
|
)
|
|
|
|
|
video_function_call = types.Part.from_function_call(
|
|
|
|
|
name='monitor_video_stream', args={}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create LLM responses: start both streaming tools
|
|
|
|
|
response1 = LlmResponse(
|
|
|
|
|
content=types.Content(
|
|
|
|
|
role='model', parts=[stock_function_call, video_function_call]
|
|
|
|
|
),
|
|
|
|
|
turn_complete=False,
|
|
|
|
|
)
|
|
|
|
|
response2 = LlmResponse(
|
|
|
|
|
turn_complete=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mock_model = testing_utils.MockModel.create([response1, response2])
|
|
|
|
|
|
|
|
|
|
# Mock streaming tools
|
|
|
|
|
async def monitor_stock_price(stock_symbol: str):
|
|
|
|
|
"""Mock streaming tool that monitors stock prices."""
|
|
|
|
|
yield f'Stock {stock_symbol} price: $800'
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
yield f'Stock {stock_symbol} price: $805'
|
|
|
|
|
|
|
|
|
|
async def monitor_video_stream(input_stream: LiveRequestQueue):
|
|
|
|
|
"""Mock video streaming tool."""
|
|
|
|
|
yield 'Video monitoring started'
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
yield 'Detected motion in video stream'
|
|
|
|
|
|
|
|
|
|
def stop_streaming(function_name: str):
|
|
|
|
|
"""Stop the streaming tool."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
root_agent = Agent(
|
|
|
|
|
name='root_agent',
|
|
|
|
|
model=mock_model,
|
|
|
|
|
tools=[monitor_stock_price, monitor_video_stream, stop_streaming],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Use the custom runner
|
|
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async for response in run_res:
|
|
|
|
|
collected_responses.append(response)
|
|
|
|
|
if len(collected_responses) >= 3:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
session = self.session
|
2025-08-02 12:27:38 -07:00
|
|
|
# Create a new event loop to avoid nested event loop issues
|
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
|
try:
|
|
|
|
|
loop.run_until_complete(
|
|
|
|
|
asyncio.wait_for(consume_responses(session), timeout=5.0)
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
loop.close()
|
2025-07-09 19:40:56 -07:00
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return collected_responses
|
|
|
|
|
|
|
|
|
|
runner = CustomTestRunner(root_agent=root_agent)
|
|
|
|
|
live_request_queue = LiveRequestQueue()
|
|
|
|
|
live_request_queue.send_realtime(
|
|
|
|
|
blob=types.Blob(
|
|
|
|
|
data=b'Monitor both stock and video', mime_type='audio/pcm'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
res_events = runner.run_live(live_request_queue)
|
|
|
|
|
|
|
|
|
|
assert res_events is not None, 'Expected a list of events, got None.'
|
|
|
|
|
assert len(res_events) >= 1, 'Expected at least one event.'
|
|
|
|
|
|
|
|
|
|
# Check that we got both streaming tool function calls
|
|
|
|
|
stock_call_found = False
|
|
|
|
|
video_call_found = False
|
|
|
|
|
|
|
|
|
|
for event in res_events:
|
|
|
|
|
if event.content and event.content.parts:
|
|
|
|
|
for part in event.content.parts:
|
|
|
|
|
if part.function_call:
|
|
|
|
|
if part.function_call.name == 'monitor_stock_price':
|
|
|
|
|
stock_call_found = True
|
|
|
|
|
assert part.function_call.args['stock_symbol'] == 'NVDA'
|
|
|
|
|
elif part.function_call.name == 'monitor_video_stream':
|
|
|
|
|
video_call_found = True
|
|
|
|
|
|
|
|
|
|
assert stock_call_found, 'Expected monitor_stock_price function call event.'
|
|
|
|
|
assert video_call_found, 'Expected monitor_video_stream function call event.'
|