|
18 | 18 | from unittest import mock |
19 | 19 |
|
20 | 20 | from fastapi.openapi.models import HTTPBearer |
| 21 | +from google.adk.agents.live_request_queue import LiveRequestQueue |
21 | 22 | from google.adk.agents.llm_agent import Agent |
22 | 23 | from google.adk.auth.auth_tool import AuthConfig |
23 | 24 | from google.adk.events.event import Event |
@@ -1559,3 +1560,151 @@ async def test_detection_exception_does_not_break_tool_call( |
1559 | 1560 | assert len(recorded_calls) == 1 |
1560 | 1561 | assert recorded_calls[0]['error_type'] is None |
1561 | 1562 | assert recorded_calls[0]['error'] is None |
| 1563 | + |
| 1564 | + |
| 1565 | +@pytest.mark.asyncio |
| 1566 | +async def test_response_scheduling_applied_to_function_response(): |
| 1567 | + """response_scheduling on a tool is stamped onto the FunctionResponse part.""" |
| 1568 | + |
| 1569 | + def simple_fn(**kwargs) -> dict: |
| 1570 | + return {'result': 'test'} |
| 1571 | + |
| 1572 | + tool = FunctionTool(simple_fn) |
| 1573 | + tool.response_scheduling = types.FunctionResponseScheduling.SILENT |
| 1574 | + model = testing_utils.MockModel.create(responses=[]) |
| 1575 | + agent = Agent(name='test_agent', model=model, tools=[tool]) |
| 1576 | + invocation_context = await testing_utils.create_invocation_context( |
| 1577 | + agent=agent, user_content='' |
| 1578 | + ) |
| 1579 | + |
| 1580 | + function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test') |
| 1581 | + event = Event( |
| 1582 | + invocation_id=invocation_context.invocation_id, |
| 1583 | + author=agent.name, |
| 1584 | + content=types.Content(parts=[types.Part(function_call=function_call)]), |
| 1585 | + ) |
| 1586 | + |
| 1587 | + result_event = await handle_function_calls_async( |
| 1588 | + invocation_context, event, {tool.name: tool} |
| 1589 | + ) |
| 1590 | + |
| 1591 | + assert result_event is not None |
| 1592 | + function_response = result_event.content.parts[0].function_response |
| 1593 | + assert function_response.scheduling is types.FunctionResponseScheduling.SILENT |
| 1594 | + |
| 1595 | + |
| 1596 | +@pytest.mark.asyncio |
| 1597 | +async def test_response_scheduling_unset_by_default(): |
| 1598 | + """Without response_scheduling, the FunctionResponse part leaves it unset.""" |
| 1599 | + |
| 1600 | + def simple_fn(**kwargs) -> dict: |
| 1601 | + return {'result': 'test'} |
| 1602 | + |
| 1603 | + tool = FunctionTool(simple_fn) |
| 1604 | + model = testing_utils.MockModel.create(responses=[]) |
| 1605 | + agent = Agent(name='test_agent', model=model, tools=[tool]) |
| 1606 | + invocation_context = await testing_utils.create_invocation_context( |
| 1607 | + agent=agent, user_content='' |
| 1608 | + ) |
| 1609 | + |
| 1610 | + function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test') |
| 1611 | + event = Event( |
| 1612 | + invocation_id=invocation_context.invocation_id, |
| 1613 | + author=agent.name, |
| 1614 | + content=types.Content(parts=[types.Part(function_call=function_call)]), |
| 1615 | + ) |
| 1616 | + |
| 1617 | + result_event = await handle_function_calls_async( |
| 1618 | + invocation_context, event, {tool.name: tool} |
| 1619 | + ) |
| 1620 | + |
| 1621 | + assert result_event is not None |
| 1622 | + function_response = result_event.content.parts[0].function_response |
| 1623 | + assert function_response.scheduling is None |
| 1624 | + |
| 1625 | + |
| 1626 | +async def _drain_live_function_responses( |
| 1627 | + live_request_queue: LiveRequestQueue, |
| 1628 | + count: int, |
| 1629 | +) -> list[types.Content]: |
| 1630 | + """Drains ``count`` contents from a live request queue, ignoring acks.""" |
| 1631 | + contents = [] |
| 1632 | + while len(contents) < count: |
| 1633 | + request = await asyncio.wait_for(live_request_queue._queue.get(), timeout=5) |
| 1634 | + if request.content is not None: |
| 1635 | + contents.append(request.content) |
| 1636 | + return contents |
| 1637 | + |
| 1638 | + |
| 1639 | +@pytest.mark.asyncio |
| 1640 | +async def test_streaming_tool_with_scheduling_emits_function_response(): |
| 1641 | + """A streaming tool with response_scheduling relays yields as FunctionResponses.""" |
| 1642 | + |
| 1643 | + async def streaming_fn(**kwargs): |
| 1644 | + yield 'first' |
| 1645 | + yield 'second' |
| 1646 | + |
| 1647 | + tool = FunctionTool(streaming_fn) |
| 1648 | + tool.response_scheduling = types.FunctionResponseScheduling.SILENT |
| 1649 | + model = testing_utils.MockModel.create(responses=[]) |
| 1650 | + agent = Agent(name='test_agent', model=model, tools=[tool]) |
| 1651 | + invocation_context = await testing_utils.create_invocation_context( |
| 1652 | + agent=agent, user_content='' |
| 1653 | + ) |
| 1654 | + invocation_context.live_request_queue = LiveRequestQueue() |
| 1655 | + |
| 1656 | + function_call = types.FunctionCall(name=tool.name, args={}, id='fc_stream') |
| 1657 | + event = Event( |
| 1658 | + invocation_id=invocation_context.invocation_id, |
| 1659 | + author=agent.name, |
| 1660 | + content=types.Content(parts=[types.Part(function_call=function_call)]), |
| 1661 | + ) |
| 1662 | + |
| 1663 | + await handle_function_calls_live(invocation_context, event, {tool.name: tool}) |
| 1664 | + contents = await _drain_live_function_responses( |
| 1665 | + invocation_context.live_request_queue, count=2 |
| 1666 | + ) |
| 1667 | + |
| 1668 | + responses = [content.parts[0].function_response for content in contents] |
| 1669 | + assert [r.response for r in responses] == [ |
| 1670 | + {'result': 'first'}, |
| 1671 | + {'result': 'second'}, |
| 1672 | + ] |
| 1673 | + assert all(r.id == 'fc_stream' for r in responses) |
| 1674 | + assert all( |
| 1675 | + r.scheduling is types.FunctionResponseScheduling.SILENT for r in responses |
| 1676 | + ) |
| 1677 | + |
| 1678 | + |
| 1679 | +@pytest.mark.asyncio |
| 1680 | +async def test_streaming_tool_without_scheduling_emits_function_response(): |
| 1681 | + """A streaming tool without response_scheduling still relays yields as |
| 1682 | + FunctionResponses, leaving scheduling unset.""" |
| 1683 | + |
| 1684 | + async def streaming_fn(**kwargs): |
| 1685 | + yield 'hello' |
| 1686 | + |
| 1687 | + tool = FunctionTool(streaming_fn) |
| 1688 | + model = testing_utils.MockModel.create(responses=[]) |
| 1689 | + agent = Agent(name='test_agent', model=model, tools=[tool]) |
| 1690 | + invocation_context = await testing_utils.create_invocation_context( |
| 1691 | + agent=agent, user_content='' |
| 1692 | + ) |
| 1693 | + invocation_context.live_request_queue = LiveRequestQueue() |
| 1694 | + |
| 1695 | + function_call = types.FunctionCall(name=tool.name, args={}, id='fc_stream') |
| 1696 | + event = Event( |
| 1697 | + invocation_id=invocation_context.invocation_id, |
| 1698 | + author=agent.name, |
| 1699 | + content=types.Content(parts=[types.Part(function_call=function_call)]), |
| 1700 | + ) |
| 1701 | + |
| 1702 | + await handle_function_calls_live(invocation_context, event, {tool.name: tool}) |
| 1703 | + contents = await _drain_live_function_responses( |
| 1704 | + invocation_context.live_request_queue, count=1 |
| 1705 | + ) |
| 1706 | + |
| 1707 | + function_response = contents[0].parts[0].function_response |
| 1708 | + assert function_response.response == {'result': 'hello'} |
| 1709 | + assert function_response.id == 'fc_stream' |
| 1710 | + assert function_response.scheduling is None |
0 commit comments