Skip to content

Commit c6f90cf

Browse files
authored
docs: sync tool docs and add coverage for internal helper branches (#2485)
1 parent 1526c28 commit c6f90cf

6 files changed

Lines changed: 449 additions & 1 deletion

File tree

docs/examples.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Check out a variety of sample implementations of the SDK in the examples section
6969
- Web applications
7070
- Command-line interfaces
7171
- Twilio integration
72+
- Twilio SIP integration
7273

7374
- **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):**
7475
Examples demonstrating how to work with reasoning content and structured outputs.

docs/tools.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ OpenAI offers a few built-in tools when using the [`OpenAIResponsesModel`][agent
1818
- The [`HostedMCPTool`][agents.tool.HostedMCPTool] exposes a remote MCP server's tools to the model.
1919
- The [`ImageGenerationTool`][agents.tool.ImageGenerationTool] generates images from a prompt.
2020

21+
Advanced hosted search options:
22+
23+
- `FileSearchTool` supports `filters`, `ranking_options`, and `include_search_results` in addition to `vector_store_ids` and `max_num_results`.
24+
- `WebSearchTool` supports `filters`, `user_location`, and `search_context_size`.
25+
2126
```python
2227
from agents import Agent, FileSearchTool, Runner, WebSearchTool
2328

@@ -78,9 +83,11 @@ What to know:
7883

7984
- Hosted shell is available through the Responses API shell tool.
8085
- `container_auto` provisions a container for the request; `container_reference` reuses an existing one.
86+
- `container_auto` can also include `file_ids` and `memory_limit`.
8187
- `environment.skills` accepts skill references and inline skill bundles.
8288
- With hosted environments, do not set `executor`, `needs_approval`, or `on_approval` on `ShellTool`.
8389
- `network_policy` supports `disabled` and `allowlist` modes.
90+
- In allowlist mode, `network_policy.domain_secrets` can inject domain-scoped secrets by name.
8491
- See `examples/tools/container_shell_skill_reference.py` and `examples/tools/container_shell_inline_skill.py` for complete examples.
8592
- OpenAI platform guides: [Shell](https://platform.openai.com/docs/guides/tools-shell) and [Skills](https://platform.openai.com/docs/guides/tools-skills).
8693

@@ -92,6 +99,7 @@ Local runtime tools execute in your environment and require you to supply implem
9299
- [`ShellTool`][agents.tool.ShellTool]: the latest shell tool for both local execution and hosted container execution.
93100
- [`LocalShellTool`][agents.tool.LocalShellTool]: legacy local-shell integration.
94101
- [`ApplyPatchTool`][agents.tool.ApplyPatchTool]: implement [`ApplyPatchEditor`][agents.editor.ApplyPatchEditor] to apply diffs locally.
102+
- Local shell skills are available with `ShellTool(environment={"type": "local", "skills": [...]})`.
95103

96104
```python
97105
from agents import Agent, ApplyPatchTool, ShellTool
@@ -284,7 +292,7 @@ Sometimes, you don't want to use a Python function as a tool. You can directly c
284292
- `name`
285293
- `description`
286294
- `params_json_schema`, which is the JSON schema for the arguments
287-
- `on_invoke_tool`, which is an async function that receives a [`ToolContext`][agents.tool_context.ToolContext] and the arguments as a JSON string, and must return the tool output as a string.
295+
- `on_invoke_tool`, which is an async function that receives a [`ToolContext`][agents.tool_context.ToolContext] and the arguments as a JSON string, and returns tool output (for example, text, structured tool output objects, or a list of outputs).
288296

289297
```python
290298
from typing import Any

tests/test_agent_tool_input.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
from agents.agent_tool_input import (
99
AgentAsToolInput,
1010
StructuredInputSchemaInfo,
11+
_build_schema_summary,
12+
_describe_json_schema_field,
13+
_format_enum_label,
14+
_format_literal_label,
15+
_read_schema_description,
16+
build_structured_input_schema_info,
1117
resolve_agent_tool_input,
1218
)
1319

@@ -57,3 +63,65 @@ async def builder(_options):
5763

5864
result = await resolve_agent_tool_input(params={"input": "ignored"}, input_builder=builder)
5965
assert result == items
66+
67+
68+
def test_build_structured_input_schema_info_handles_empty_schema() -> None:
69+
info = build_structured_input_schema_info(None, include_json_schema=False)
70+
assert info.summary is None
71+
assert info.json_schema is None
72+
73+
74+
def test_build_structured_input_schema_info_generates_summary_for_simple_fields() -> None:
75+
schema = {
76+
"type": "object",
77+
"description": "Tool arguments.",
78+
"properties": {
79+
"mode": {"enum": ["fast", "safe"], "description": "Execution mode."},
80+
"status": {"const": "ok", "description": "Status marker."},
81+
"count": {"type": ["integer", "null"], "description": "Optional count."},
82+
"enabled": {"type": "boolean", "description": "Feature toggle."},
83+
},
84+
"required": ["mode", "status"],
85+
}
86+
87+
info = build_structured_input_schema_info(schema, include_json_schema=True)
88+
89+
assert info.summary is not None
90+
assert "Description: Tool arguments." in info.summary
91+
assert '- mode (enum("fast" | "safe"), required) - Execution mode.' in info.summary
92+
assert '- status (literal("ok"), required) - Status marker.' in info.summary
93+
assert "- count (integer | null, optional) - Optional count." in info.summary
94+
assert "- enabled (boolean, optional) - Feature toggle." in info.summary
95+
assert info.json_schema == schema
96+
97+
98+
def test_schema_summary_returns_none_for_unsupported_shapes() -> None:
99+
assert _build_schema_summary({"type": "array"}) is None
100+
assert _build_schema_summary({"type": "object", "properties": []}) is None
101+
assert (
102+
_build_schema_summary(
103+
{
104+
"type": "object",
105+
"properties": {
106+
"nested": {
107+
"type": "object",
108+
"properties": {"x": {"type": "string"}},
109+
}
110+
},
111+
}
112+
)
113+
is None
114+
)
115+
116+
117+
def test_private_schema_helper_edge_cases() -> None:
118+
assert _describe_json_schema_field("not-a-dict") is None
119+
assert _describe_json_schema_field({"type": ["integer", "string"]}) is None
120+
assert _describe_json_schema_field({"type": "array"}) is None
121+
assert _describe_json_schema_field({}) is None
122+
123+
assert _read_schema_description("not-a-dict") is None
124+
125+
assert _format_enum_label([]) == "enum"
126+
assert "..." in _format_enum_label([1, 2, 3, 4, 5, 6])
127+
assert _format_literal_label({}) == "literal"
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from typing import Any
5+
6+
import pytest
7+
8+
from agents import Agent
9+
from agents.agent_output import AgentOutputSchemaBase
10+
from agents.exceptions import MaxTurnsExceeded, UserError
11+
from agents.run_context import RunContextWrapper
12+
from agents.run_error_handlers import RunErrorData
13+
from agents.run_internal import error_handlers as run_error_handlers
14+
15+
16+
class _CustomSchema(AgentOutputSchemaBase):
17+
def is_plain_text(self) -> bool:
18+
return False
19+
20+
def name(self) -> str:
21+
return "CustomSchema"
22+
23+
def json_schema(self) -> dict[str, Any]:
24+
return {"type": "object"}
25+
26+
def is_strict_json_schema(self) -> bool:
27+
return True
28+
29+
def validate_json(self, json_str: str) -> Any:
30+
return json.loads(json_str)
31+
32+
33+
def _make_run_data(agent: Agent[Any]) -> RunErrorData:
34+
return RunErrorData(
35+
input="hello",
36+
new_items=[],
37+
history=[],
38+
output=[],
39+
raw_responses=[],
40+
last_agent=agent,
41+
)
42+
43+
44+
def test_format_final_output_text_handles_wrapped_payload() -> None:
45+
agent = Agent(name="wrapped-output", output_type=list[str])
46+
output = {"response": ["a", "b"]}
47+
48+
rendered = run_error_handlers.format_final_output_text(agent, output)
49+
assert json.loads(rendered) == output
50+
51+
52+
def test_validate_handler_final_output_accepts_wrapped_payload() -> None:
53+
agent = Agent(name="wrapped-validate", output_type=list[str])
54+
output = {"response": ["ok"]}
55+
56+
validated = run_error_handlers.validate_handler_final_output(agent, output)
57+
assert validated == ["ok"]
58+
59+
60+
def test_format_final_output_text_uses_custom_schema_and_fallback(
61+
monkeypatch: pytest.MonkeyPatch,
62+
) -> None:
63+
agent = Agent(name="custom-format")
64+
custom_schema = _CustomSchema()
65+
monkeypatch.setattr(run_error_handlers, "get_output_schema", lambda _agent: custom_schema)
66+
67+
rendered = run_error_handlers.format_final_output_text(agent, {"ok": True})
68+
assert json.loads(rendered) == {"ok": True}
69+
70+
value = object()
71+
fallback = run_error_handlers.format_final_output_text(agent, value)
72+
assert fallback == str(value)
73+
74+
75+
def test_validate_handler_final_output_raises_for_unserializable_data(
76+
monkeypatch: pytest.MonkeyPatch,
77+
) -> None:
78+
agent = Agent(name="custom-validate")
79+
custom_schema = _CustomSchema()
80+
monkeypatch.setattr(run_error_handlers, "get_output_schema", lambda _agent: custom_schema)
81+
82+
with pytest.raises(UserError, match="Invalid run error handler final_output"):
83+
run_error_handlers.validate_handler_final_output(agent, {"bad": {1, 2}})
84+
85+
86+
@pytest.mark.asyncio
87+
async def test_resolve_run_error_handler_result_covers_async_and_validation_paths() -> None:
88+
agent = Agent(name="max-turns")
89+
context_wrapper: RunContextWrapper[dict[str, Any]] = RunContextWrapper(context={})
90+
run_data = _make_run_data(agent)
91+
error = MaxTurnsExceeded("too many turns")
92+
93+
no_handler = await run_error_handlers.resolve_run_error_handler_result(
94+
error_handlers={},
95+
error=error,
96+
context_wrapper=context_wrapper,
97+
run_data=run_data,
98+
)
99+
assert no_handler is None
100+
101+
async def async_handler(_handler_input: Any) -> None:
102+
return None
103+
104+
async_none = await run_error_handlers.resolve_run_error_handler_result(
105+
error_handlers={"max_turns": async_handler},
106+
error=error,
107+
context_wrapper=context_wrapper,
108+
run_data=run_data,
109+
)
110+
assert async_none is None
111+
112+
with pytest.raises(UserError, match="Invalid run error handler result"):
113+
await run_error_handlers.resolve_run_error_handler_result(
114+
error_handlers={
115+
"max_turns": lambda _handler_input: {"final_output": "x", "extra": "y"}
116+
},
117+
error=error,
118+
context_wrapper=context_wrapper,
119+
run_data=run_data,
120+
)

0 commit comments

Comments
 (0)