chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
# Copyright
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from openai.types.responses.response_input_item_param import FunctionCallOutput
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
FunctionToolResult,
|
||||
RunContextWrapper,
|
||||
ToolCallOutputItem,
|
||||
ToolsToFinalOutputResult,
|
||||
UserError,
|
||||
function_tool,
|
||||
tool_namespace,
|
||||
)
|
||||
from agents.run_internal import run_loop
|
||||
|
||||
from .test_responses import get_function_tool
|
||||
|
||||
|
||||
def _make_function_tool_result(
|
||||
agent: Agent,
|
||||
output: str,
|
||||
tool_name: str | None = None,
|
||||
*,
|
||||
tool: Any | None = None,
|
||||
) -> FunctionToolResult:
|
||||
# Construct a FunctionToolResult with the given output using a simple function tool.
|
||||
tool = tool or get_function_tool(tool_name or "dummy", return_value=output)
|
||||
raw_item: FunctionCallOutput = cast(
|
||||
FunctionCallOutput,
|
||||
{
|
||||
"call_id": "1",
|
||||
"output": output,
|
||||
"type": "function_call_output",
|
||||
},
|
||||
)
|
||||
# For this test we don't care about the specific RunItem subclass, only the output field
|
||||
run_item = ToolCallOutputItem(agent=agent, raw_item=raw_item, output=output)
|
||||
return FunctionToolResult(tool=tool, output=output, run_item=run_item)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_tool_results_returns_not_final_output() -> None:
|
||||
# If there are no tool results at all, tool_use_behavior should not produce a final output.
|
||||
agent = Agent(name="test")
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=[],
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is False
|
||||
assert result.final_output is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_llm_again_behavior() -> None:
|
||||
# With the default run_llm_again behavior, even with tools we still expect to keep running.
|
||||
agent = Agent(name="test", tool_use_behavior="run_llm_again")
|
||||
tool_results = [_make_function_tool_result(agent, "ignored")]
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is False
|
||||
assert result.final_output is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_on_first_tool_behavior() -> None:
|
||||
# When tool_use_behavior is stop_on_first_tool, we should surface first tool output as final.
|
||||
agent = Agent(name="test", tool_use_behavior="stop_on_first_tool")
|
||||
tool_results = [
|
||||
_make_function_tool_result(agent, "first_tool_output"),
|
||||
_make_function_tool_result(agent, "ignored"),
|
||||
]
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is True
|
||||
assert result.final_output == "first_tool_output"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_tool_use_behavior_sync() -> None:
|
||||
"""If tool_use_behavior is a sync function, we should call it and propagate its return."""
|
||||
|
||||
def behavior(
|
||||
context: RunContextWrapper, results: list[FunctionToolResult]
|
||||
) -> ToolsToFinalOutputResult:
|
||||
assert len(results) == 3
|
||||
return ToolsToFinalOutputResult(is_final_output=True, final_output="custom")
|
||||
|
||||
agent = Agent(name="test", tool_use_behavior=behavior)
|
||||
tool_results = [
|
||||
_make_function_tool_result(agent, "ignored1"),
|
||||
_make_function_tool_result(agent, "ignored2"),
|
||||
_make_function_tool_result(agent, "ignored3"),
|
||||
]
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is True
|
||||
assert result.final_output == "custom"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_tool_use_behavior_async() -> None:
|
||||
"""If tool_use_behavior is an async function, we should await it and propagate its return."""
|
||||
|
||||
async def behavior(
|
||||
context: RunContextWrapper, results: list[FunctionToolResult]
|
||||
) -> ToolsToFinalOutputResult:
|
||||
assert len(results) == 3
|
||||
return ToolsToFinalOutputResult(is_final_output=True, final_output="async_custom")
|
||||
|
||||
agent = Agent(name="test", tool_use_behavior=behavior)
|
||||
tool_results = [
|
||||
_make_function_tool_result(agent, "ignored1"),
|
||||
_make_function_tool_result(agent, "ignored2"),
|
||||
_make_function_tool_result(agent, "ignored3"),
|
||||
]
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is True
|
||||
assert result.final_output == "async_custom"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_tool_use_behavior_raises() -> None:
|
||||
"""If tool_use_behavior is invalid, we should raise a UserError."""
|
||||
agent = Agent(name="test")
|
||||
# Force an invalid value; mypy will complain, so ignore the type here.
|
||||
agent.tool_use_behavior = "bad_value" # type: ignore[assignment]
|
||||
tool_results = [_make_function_tool_result(agent, "ignored")]
|
||||
with pytest.raises(UserError):
|
||||
await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_names_to_stop_at_behavior() -> None:
|
||||
agent = Agent(
|
||||
name="test",
|
||||
tools=[
|
||||
get_function_tool("tool1", return_value="tool1_output"),
|
||||
get_function_tool("tool2", return_value="tool2_output"),
|
||||
get_function_tool("tool3", return_value="tool3_output"),
|
||||
],
|
||||
tool_use_behavior={"stop_at_tool_names": ["tool1"]},
|
||||
)
|
||||
|
||||
tool_results = [
|
||||
_make_function_tool_result(agent, "ignored1", "tool2"),
|
||||
_make_function_tool_result(agent, "ignored3", "tool3"),
|
||||
]
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is False, "We should not have stopped at tool1"
|
||||
|
||||
# Now test with a tool that matches the list
|
||||
tool_results = [
|
||||
_make_function_tool_result(agent, "output1", "tool1"),
|
||||
_make_function_tool_result(agent, "ignored2", "tool2"),
|
||||
_make_function_tool_result(agent, "ignored3", "tool3"),
|
||||
]
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is True, "We should have stopped at tool1"
|
||||
assert result.final_output == "output1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_at_tool_names_supports_public_and_qualified_names_for_namespaced_tools() -> (
|
||||
None
|
||||
):
|
||||
namespaced_tool = tool_namespace(
|
||||
name="billing",
|
||||
description="Billing tools",
|
||||
tools=[function_tool(lambda account_id: account_id, name_override="lookup_account")],
|
||||
)[0]
|
||||
agent = Agent(
|
||||
name="test",
|
||||
tools=[namespaced_tool],
|
||||
tool_use_behavior={"stop_at_tool_names": ["lookup_account"]},
|
||||
)
|
||||
|
||||
tool_results = [
|
||||
_make_function_tool_result(agent, "billing-output", tool=namespaced_tool),
|
||||
]
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is True
|
||||
assert result.final_output == "billing-output"
|
||||
|
||||
agent.tool_use_behavior = {"stop_at_tool_names": ["billing.lookup_account"]}
|
||||
result = await run_loop.check_for_final_output_from_tools(
|
||||
agent=agent,
|
||||
tool_results=tool_results,
|
||||
context_wrapper=RunContextWrapper(context=None),
|
||||
)
|
||||
assert result.is_final_output is True
|
||||
Reference in New Issue
Block a user