chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:17 +08:00
commit 4ed4e9ff99
1368 changed files with 334957 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
import argparse
import asyncio
import hashlib
import os
import tempfile
from pathlib import Path
from agents import Agent, ApplyPatchTool, ModelSettings, Runner, apply_diff, trace
from agents.editor import ApplyPatchOperation, ApplyPatchResult
from examples.auto_mode import confirm_with_fallback, is_auto_mode
class ApprovalTracker:
def __init__(self) -> None:
self._approved: set[str] = set()
def fingerprint(self, operation: ApplyPatchOperation, relative_path: str) -> str:
hasher = hashlib.sha256()
hasher.update(operation.type.encode("utf-8"))
hasher.update(b"\0")
hasher.update(relative_path.encode("utf-8"))
hasher.update(b"\0")
hasher.update((operation.diff or "").encode("utf-8"))
return hasher.hexdigest()
def remember(self, fingerprint: str) -> None:
self._approved.add(fingerprint)
def is_approved(self, fingerprint: str) -> bool:
return fingerprint in self._approved
class WorkspaceEditor:
def __init__(self, root: Path, approvals: ApprovalTracker, auto_approve: bool) -> None:
self._root = root.resolve()
self._approvals = approvals
self._auto_approve = auto_approve or os.environ.get("APPLY_PATCH_AUTO_APPROVE") == "1"
def create_file(self, operation: ApplyPatchOperation) -> ApplyPatchResult:
relative = self._relative_path(operation.path)
self._require_approval(operation, relative)
target = self._resolve(operation.path, ensure_parent=True)
diff = operation.diff or ""
content = apply_diff("", diff, mode="create")
target.write_text(content, encoding="utf-8")
return ApplyPatchResult(output=f"Created {relative}")
def update_file(self, operation: ApplyPatchOperation) -> ApplyPatchResult:
relative = self._relative_path(operation.path)
self._require_approval(operation, relative)
target = self._resolve(operation.path)
original = target.read_text(encoding="utf-8")
diff = operation.diff or ""
patched = apply_diff(original, diff)
target.write_text(patched, encoding="utf-8")
return ApplyPatchResult(output=f"Updated {relative}")
def delete_file(self, operation: ApplyPatchOperation) -> ApplyPatchResult:
relative = self._relative_path(operation.path)
self._require_approval(operation, relative)
target = self._resolve(operation.path)
target.unlink(missing_ok=True)
return ApplyPatchResult(output=f"Deleted {relative}")
def _relative_path(self, value: str) -> str:
resolved = self._resolve(value)
return resolved.relative_to(self._root).as_posix()
def _resolve(self, relative: str, ensure_parent: bool = False) -> Path:
candidate = Path(relative)
target = candidate if candidate.is_absolute() else (self._root / candidate)
target = target.resolve()
try:
target.relative_to(self._root)
except ValueError:
raise RuntimeError(f"Operation outside workspace: {relative}") from None
if ensure_parent:
target.parent.mkdir(parents=True, exist_ok=True)
return target
def _require_approval(self, operation: ApplyPatchOperation, display_path: str) -> None:
fingerprint = self._approvals.fingerprint(operation, display_path)
if self._auto_approve or self._approvals.is_approved(fingerprint):
self._approvals.remember(fingerprint)
return
print("\n[apply_patch] approval required")
print(f"- type: {operation.type}")
print(f"- path: {display_path}")
if operation.diff:
preview = operation.diff if len(operation.diff) < 400 else f"{operation.diff[:400]}"
print("- diff preview:\n", preview)
approved = confirm_with_fallback("Proceed? [y/N] ", default=is_auto_mode())
if not approved:
raise RuntimeError("Apply patch operation rejected by user.")
self._approvals.remember(fingerprint)
async def main(auto_approve: bool, model: str) -> None:
with trace("apply_patch_example"):
with tempfile.TemporaryDirectory(prefix="apply-patch-example-") as workspace:
workspace_path = Path(workspace).resolve()
approvals = ApprovalTracker()
editor = WorkspaceEditor(workspace_path, approvals, auto_approve)
tool = ApplyPatchTool(editor=editor)
previous_response_id: str | None = None
agent = Agent(
name="Patch Assistant",
model=model,
instructions=(
f"You can edit files inside {workspace_path} using the apply_patch tool. "
"When modifying an existing file, include the file contents between "
"<BEGIN_FILES> and <END_FILES> in your prompt."
),
tools=[tool],
model_settings=ModelSettings(tool_choice="required"),
)
print(f"[info] Workspace root: {workspace_path}")
print(f"[info] Using model: {model}")
print("[run] Creating tasks.md")
result = await Runner.run(
agent,
"Create tasks.md with a shopping checklist of 5 entries.",
previous_response_id=previous_response_id,
)
previous_response_id = result.last_response_id
print(f"[run] Final response #1:\n{result.final_output}\n")
notes_path = workspace_path / "tasks.md"
if not notes_path.exists():
raise RuntimeError(f"{notes_path} was not created by the apply_patch tool.")
updated_notes = notes_path.read_text(encoding="utf-8")
print("[file] tasks.md after creation:\n")
print(updated_notes)
prompt = (
"<BEGIN_FILES>\n"
f"===== tasks.md\n{updated_notes}\n"
"<END_FILES>\n"
"Check off the last two items from the file."
)
print("\n[run] Updating tasks.md")
result2 = await Runner.run(
agent,
prompt,
previous_response_id=previous_response_id,
)
print(f"[run] Final response #2:\n{result2.final_output}\n")
if not notes_path.exists():
raise RuntimeError("tasks.md vanished unexpectedly before the second read.")
print("[file] Final tasks.md:\n")
print(notes_path.read_text(encoding="utf-8"))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--auto-approve",
action="store_true",
default=False,
help="Skip manual confirmations for apply_patch operations.",
)
parser.add_argument(
"--model",
default="gpt-5.6-sol",
help="Model ID to use for the agent.",
)
args = parser.parse_args()
asyncio.run(main(args.auto_approve, args.model))
+63
View File
@@ -0,0 +1,63 @@
import asyncio
from collections.abc import Mapping
from typing import Any
from agents import Agent, CodeInterpreterTool, Runner, trace
def _get_field(obj: Any, key: str) -> Any:
if isinstance(obj, Mapping):
return obj.get(key)
return getattr(obj, key, None)
async def main():
agent = Agent(
name="Code interpreter",
# Note: using gpt-5-class models with streaming for this tool may require org verification.
# Code interpreter does not support gpt-5 minimal reasoning effort; use default effort.
model="gpt-5.6-sol",
instructions=(
"Always use the code interpreter tool to solve numeric problems, and show the code "
"you ran when possible."
),
tools=[
CodeInterpreterTool(
tool_config={"type": "code_interpreter", "container": {"type": "auto"}},
)
],
)
with trace("Code interpreter example"):
print("Solving math problem with the code interpreter...")
result = Runner.run_streamed(
agent,
(
"Use the code interpreter tool to calculate the square root of 273 * 312821 + "
"1782. Show the Python code you ran and then provide the numeric answer."
),
)
saw_code_interpreter_call = False
async for event in result.stream_events():
if event.type != "run_item_stream_event":
continue
item = event.item
if item.type == "tool_call_item":
raw_call = item.raw_item
if _get_field(raw_call, "type") == "code_interpreter_call":
saw_code_interpreter_call = True
code = _get_field(raw_call, "code")
if isinstance(code, str):
print(f"Code interpreter code:\n```\n{code}\n```\n")
continue
print(f"Other event: {event.item.type}")
if not saw_code_interpreter_call:
print("No code_interpreter_call item was emitted.")
print(f"Final output: {result.final_output}")
if __name__ == "__main__":
asyncio.run(main())
+165
View File
@@ -0,0 +1,165 @@
import asyncio
from datetime import datetime
from agents import Agent, Runner, gen_trace_id, trace
# This tool is still in experimental phase and the details could be changed until being GAed.
from agents.extensions.experimental.codex import (
CodexToolStreamEvent,
CommandExecutionItem,
ErrorItem,
FileChangeItem,
ItemCompletedEvent,
ItemStartedEvent,
ItemUpdatedEvent,
McpToolCallItem,
ReasoningItem,
ThreadErrorEvent,
ThreadOptions,
ThreadStartedEvent,
TodoListItem,
TurnCompletedEvent,
TurnFailedEvent,
TurnOptions,
TurnStartedEvent,
WebSearchItem,
codex_tool,
)
# This example runs the Codex CLI via the Codex tool wrapper.
# You can configure the CLI path with CODEX_PATH or CodexOptions(codex_path_override="...").
# codex_tool accepts options as keyword arguments or a plain dict.
# For example: codex_tool(sandbox_mode="read-only") or codex_tool({"sandbox_mode": "read-only"}).
async def on_codex_stream(payload: CodexToolStreamEvent) -> None:
event = payload.event
if isinstance(event, ThreadStartedEvent):
log(f"codex thread started: {event.thread_id}")
return
if isinstance(event, TurnStartedEvent):
log("codex turn started")
return
if isinstance(event, TurnCompletedEvent):
usage = event.usage
log(f"codex turn completed, usage: {usage}")
return
if isinstance(event, TurnFailedEvent):
error = event.error.message
log(f"codex turn failed: {error}")
return
if isinstance(event, ThreadErrorEvent):
log(f"codex stream error: {event.message}")
return
if not isinstance(event, ItemStartedEvent | ItemUpdatedEvent | ItemCompletedEvent):
return
item = event.item
if isinstance(item, ReasoningItem):
text = item.text
log(f"codex reasoning ({event.type}): {text}")
return
if isinstance(item, CommandExecutionItem):
command = item.command
output = item.aggregated_output
output_preview = output[-200:] if isinstance(output, str) else ""
status = item.status
log(f"codex command {event.type}: {command} | status={status} | output={output_preview}")
return
if isinstance(item, McpToolCallItem):
server = item.server
tool = item.tool
status = item.status
log(f"codex mcp {event.type}: {server}.{tool} | status={status}")
return
if isinstance(item, FileChangeItem):
changes = item.changes
status = item.status
log(f"codex file change {event.type}: {status} | {changes}")
return
if isinstance(item, WebSearchItem):
log(f"codex web search {event.type}: {item.query}")
return
if isinstance(item, TodoListItem):
items = item.items
log(f"codex todo list {event.type}: {len(items)} items")
return
if isinstance(item, ErrorItem):
log(f"codex error {event.type}: {item.message}")
def _timestamp() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log(message: str) -> None:
timestamp = _timestamp()
lines = str(message).splitlines() or [""]
for line in lines:
print(f"{timestamp} {line}")
async def main() -> None:
agent = Agent(
name="Codex Agent",
instructions=(
"Use the codex tool to inspect the workspace in read-only mode and answer the question. "
"When skill names, which usually starts with `$`, are mentioned, "
"you must rely on the codex tool to use the skill and answer the question.\n\n"
"When you send the final answer, you must include the following info at the end:\n\n"
"Run `codex resume <thread_id>` to continue the codex session."
),
tools=[
# Run local Codex CLI as a sub process
codex_tool(
sandbox_mode="read-only",
default_thread_options=ThreadOptions(
# You can pass a Codex instance to customize CLI details
# codex=Codex(executable_path="/path/to/codex", base_url="..."),
model="gpt-5.5",
model_reasoning_effort="low",
network_access_enabled=True,
web_search_enabled=False,
approval_policy="never", # We'll update this example once the HITL is implemented
),
default_turn_options=TurnOptions(
# Abort Codex CLI if no events arrive within this many seconds.
idle_timeout_seconds=60,
),
on_stream=on_codex_stream,
)
],
)
trace_id = gen_trace_id()
log(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}")
with trace("Codex tool example", trace_id=trace_id):
log("Using the Codex tool to inspect pyproject.toml and summarize Python requirements...")
result = await Runner.run(
agent,
(
"Inspect pyproject.toml in this repository and summarize the supported Python "
"version plus the main local test command. Do not modify any files."
),
)
log(result.final_output)
# Use local inspection in read-only mode.
log(
"Using the Codex tool to inspect AGENTS.md and summarize the local verification workflow..."
)
result = await Runner.run(
agent,
(
"Inspect AGENTS.md and summarize the mandatory local verification commands for this "
"repository. Do not modify any files or suggest code changes."
),
)
log(result.final_output)
# (A read-only summary of the local verification workflow will be displayed.)
if __name__ == "__main__":
asyncio.run(main())
+132
View File
@@ -0,0 +1,132 @@
import asyncio
from collections.abc import Mapping
from datetime import datetime
from pydantic import BaseModel
from agents import Agent, ModelSettings, Runner, gen_trace_id, trace
# This tool is still in experimental phase and the details could be changed until being GAed.
from agents.extensions.experimental.codex import (
CodexToolStreamEvent,
ThreadErrorEvent,
ThreadOptions,
ThreadStartedEvent,
TurnCompletedEvent,
TurnFailedEvent,
TurnStartedEvent,
codex_tool,
)
# Derived from codex_tool(name="codex_engineer") when run_context_thread_id_key is omitted.
THREAD_ID_KEY = "codex_thread_id_engineer"
async def on_codex_stream(payload: CodexToolStreamEvent) -> None:
event = payload.event
if isinstance(event, ThreadStartedEvent):
log(f"codex thread started: {event.thread_id}")
return
if isinstance(event, TurnStartedEvent):
log("codex turn started")
return
if isinstance(event, TurnCompletedEvent):
log(f"codex turn completed, usage: {event.usage}")
return
if isinstance(event, TurnFailedEvent):
log(f"codex turn failed: {event.error.message}")
return
if isinstance(event, ThreadErrorEvent):
log(f"codex stream error: {event.message}")
def _timestamp() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log(message: str) -> None:
timestamp = _timestamp()
lines = str(message).splitlines() or [""]
for line in lines:
print(f"{timestamp} {line}")
def read_context_value(context: Mapping[str, str] | BaseModel, key: str) -> str | None:
# either dict or pydantic model
if isinstance(context, Mapping):
return context.get(key)
return getattr(context, key, None)
async def main() -> None:
agent = Agent(
name="Codex Agent (same thread)",
instructions=(
"Always use the Codex tool to inspect the local workspace and answer the user's "
"question. Treat the workspace as read-only and answer concisely."
),
tools=[
codex_tool(
# Give each Codex tool a unique `codex_` name when you run multiple tools in one agent.
# Name-based defaults keep their run-context thread IDs separated.
name="codex_engineer",
sandbox_mode="read-only",
default_thread_options=ThreadOptions(
model="gpt-5.5",
model_reasoning_effort="low",
network_access_enabled=True,
web_search_enabled=False,
approval_policy="never",
),
on_stream=on_codex_stream,
# Reuse the same Codex thread across runs that share this context object.
use_run_context_thread_id=True,
)
],
model_settings=ModelSettings(tool_choice="required"),
)
class MyContext(BaseModel):
something: str | None = None
# the default is "codex_thread_id"; missing this works as well
codex_thread_id_engineer: str | None = None # aligns with run_context_thread_id_key
context = MyContext()
# Simple dict object works as well:
# context: dict[str, str] = {}
trace_id = gen_trace_id()
log(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}")
with trace("Codex same thread example", trace_id=trace_id):
log("Turn 1: inspect AGENTS.md with the Codex tool.")
first_prompt = (
"Use the Codex tool to inspect AGENTS.md in this repository and list the mandatory "
"local verification commands. Do not modify any files."
)
first_result = await Runner.run(agent, first_prompt, context=context)
first_thread_id = read_context_value(context, THREAD_ID_KEY)
log(first_result.final_output)
log(f"thread id after turn 1: {first_thread_id}")
if first_thread_id is None:
log("thread id after turn 1 is unavailable; turn 2 may start a new Codex thread.")
log("Turn 2: continue with the same Codex thread.")
second_prompt = (
"Continue from the same Codex thread. Rewrite that verification workflow as a single "
"short sentence. Do not modify any files."
)
second_result = await Runner.run(agent, second_prompt, context=context)
second_thread_id = read_context_value(context, THREAD_ID_KEY)
log(second_result.final_output)
log(f"thread id after turn 2: {second_thread_id}")
log(
"same thread reused: "
+ str(first_thread_id is not None and first_thread_id == second_thread_id)
)
if __name__ == "__main__":
asyncio.run(main())
+311
View File
@@ -0,0 +1,311 @@
# How to run this example:
# uv run python -m playwright install chromium
# uv run -m examples.tools.computer_use
import asyncio
import base64
import os
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any, Literal
from playwright.async_api import Browser, Page, Playwright, async_playwright
from agents import (
Agent,
AsyncComputer,
Button,
ComputerProvider,
ComputerTool,
ModelSettings,
RunContextWrapper,
Runner,
trace,
)
# Uncomment to see very verbose logs
# import logging
# logging.getLogger("openai.agents").setLevel(logging.DEBUG)
# logging.getLogger("openai.agents").addHandler(logging.StreamHandler())
HEADLESS = os.environ.get("COMPUTER_USE_HEADLESS") != "0"
START_URL = os.environ.get("COMPUTER_USE_START_URL")
BROWSER_CHANNEL = os.environ.get("COMPUTER_USE_BROWSER_CHANNEL", "chromium")
DEMO_PAGE_HTML = """<!doctype html>
<html>
<head>
<title>Tokyo Weather Demo</title>
<style>
body {
font-family: system-ui, sans-serif;
margin: 40px;
}
section {
max-width: 520px;
}
button {
font: inherit;
padding: 8px 12px;
}
</style>
<script>
function refreshForecast() {
document.querySelector('[data-testid="status"]').textContent =
'Forecast refreshed at demo time.';
document.querySelector('[data-testid="current"]').textContent =
'Current conditions: partly cloudy, 22C.';
document.querySelector('[data-testid="details"]').textContent =
'Wind: 37 km/h. Visibility: 10 km. Precipitation: 0.1 mm.';
document.querySelector('[data-testid="outlook"]').hidden = false;
}
</script>
</head>
<body>
<section>
<h1>Tokyo Weather Demo</h1>
<p data-testid="status">Forecast pending.</p>
<button type="button" onclick="refreshForecast()">Refresh forecast</button>
<p data-testid="current">Current conditions: not loaded.</p>
<p data-testid="details">Details: not loaded.</p>
<div data-testid="outlook" hidden>
<h2>Today</h2>
<ul>
<li>Morning: partly cloudy, 19C.</li>
<li>Noon: sunny, 20C.</li>
<li>Evening: partly cloudy, 20C.</li>
<li>Night: clear, 19C.</li>
</ul>
</div>
</section>
</body>
</html>"""
AGENT_INSTRUCTIONS = "You are a helpful agent. Use the browser computer tool to inspect web pages."
WEATHER_PROMPT = (
"Use the browser computer tool to click the Refresh forecast button, then summarize "
"the Tokyo weather shown on the page."
)
CUA_KEY_TO_PLAYWRIGHT_KEY = {
"/": "Divide",
"\\": "Backslash",
"alt": "Alt",
"arrowdown": "ArrowDown",
"arrowleft": "ArrowLeft",
"arrowright": "ArrowRight",
"arrowup": "ArrowUp",
"backspace": "Backspace",
"capslock": "CapsLock",
"cmd": "Meta",
"ctrl": "Control",
"delete": "Delete",
"end": "End",
"enter": "Enter",
"esc": "Escape",
"home": "Home",
"insert": "Insert",
"option": "Alt",
"pagedown": "PageDown",
"pageup": "PageUp",
"shift": "Shift",
"space": " ",
"super": "Meta",
"tab": "Tab",
"win": "Meta",
}
class LocalPlaywrightComputer(AsyncComputer):
"""A computer, implemented using a local Playwright browser."""
def __init__(self):
self._playwright: Playwright | None = None
self._browser: Browser | None = None
self._page: Page | None = None
async def _get_browser_and_page(self) -> tuple[Browser, Page]:
width, height = self.dimensions
launch_args = [f"--window-size={width},{height}"]
browser = await self.playwright.chromium.launch(
channel=BROWSER_CHANNEL,
headless=HEADLESS,
args=launch_args,
)
page = await browser.new_page()
await page.set_viewport_size({"width": width, "height": height})
if START_URL:
await page.goto(START_URL, wait_until="domcontentloaded")
else:
await page.set_content(DEMO_PAGE_HTML, wait_until="domcontentloaded")
return browser, page
async def __aenter__(self):
# Start Playwright and call the subclass hook for getting browser/page
self._playwright = await async_playwright().start()
self._browser, self._page = await self._get_browser_and_page()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._browser:
await self._browser.close()
if self._playwright:
await self._playwright.stop()
return None
async def open(self) -> "LocalPlaywrightComputer":
"""Open resources without using a context manager."""
await self.__aenter__()
return self
async def close(self) -> None:
"""Close resources without using a context manager."""
await self.__aexit__(None, None, None)
@property
def playwright(self) -> Playwright:
assert self._playwright is not None
return self._playwright
@property
def browser(self) -> Browser:
assert self._browser is not None
return self._browser
@property
def page(self) -> Page:
assert self._page is not None
return self._page
@property
def dimensions(self) -> tuple[int, int]:
return (1024, 768)
async def screenshot(self) -> str:
"""Capture only the viewport (not full_page)."""
png_bytes = await self.page.screenshot(full_page=False)
return base64.b64encode(png_bytes).decode("utf-8")
def _normalize_keys(self, keys: list[str] | None) -> list[str]:
if not keys:
return []
return [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
@asynccontextmanager
async def _hold_keys(self, keys: list[str] | None) -> AsyncIterator[None]:
mapped_keys = self._normalize_keys(keys)
try:
for key in mapped_keys:
await self.page.keyboard.down(key)
yield
finally:
for key in reversed(mapped_keys):
await self.page.keyboard.up(key)
async def click(
self, x: int, y: int, button: Button = "left", *, keys: list[str] | None = None
) -> None:
playwright_button: Literal["left", "middle", "right"] = "left"
# Playwright only supports left, middle, right buttons
if button in ("left", "right", "middle"):
playwright_button = button # type: ignore
async with self._hold_keys(keys):
await self.page.mouse.click(x, y, button=playwright_button)
async def double_click(self, x: int, y: int, *, keys: list[str] | None = None) -> None:
async with self._hold_keys(keys):
await self.page.mouse.dblclick(x, y)
async def scroll(
self,
x: int,
y: int,
scroll_x: int,
scroll_y: int,
*,
keys: list[str] | None = None,
) -> None:
async with self._hold_keys(keys):
await self.page.mouse.move(x, y)
await self.page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
async def type(self, text: str) -> None:
await self.page.keyboard.type(text)
async def wait(self) -> None:
await asyncio.sleep(1)
async def move(self, x: int, y: int, *, keys: list[str] | None = None) -> None:
async with self._hold_keys(keys):
await self.page.mouse.move(x, y)
async def keypress(self, keys: list[str]) -> None:
mapped_keys = self._normalize_keys(keys)
for key in mapped_keys:
await self.page.keyboard.down(key)
for key in reversed(mapped_keys):
await self.page.keyboard.up(key)
async def drag(self, path: list[tuple[int, int]], *, keys: list[str] | None = None) -> None:
if not path:
return
async with self._hold_keys(keys):
await self.page.mouse.move(path[0][0], path[0][1])
await self.page.mouse.down()
for px, py in path[1:]:
await self.page.mouse.move(px, py)
await self.page.mouse.up()
async def run_agent(
computer_config: ComputerProvider[LocalPlaywrightComputer] | AsyncComputer,
) -> None:
with trace("Computer use example"):
agent = Agent(
name="Browser user",
instructions=AGENT_INSTRUCTIONS,
tools=[ComputerTool(computer=computer_config)],
# GPT-5.4 uses the built-in Responses API computer tool.
model="gpt-5.5",
model_settings=ModelSettings(tool_choice="required"),
)
result = await Runner.run(agent, WEATHER_PROMPT)
print(result.final_output)
async def singleton_computer() -> None:
# Use a shared computer when you do not expect to run multiple agents concurrently.
async with LocalPlaywrightComputer() as computer:
await run_agent(computer)
async def computer_per_request() -> None:
# Initialize a new computer per request to avoid sharing state between runs.
async def create_computer(*, run_context: RunContextWrapper[Any]) -> LocalPlaywrightComputer:
print(f"Creating computer for run context: {run_context}")
return await LocalPlaywrightComputer().open()
async def dispose_computer(
*,
run_context: RunContextWrapper[Any],
computer: LocalPlaywrightComputer,
) -> None:
print(f"Disposing computer for run context: {run_context}")
await computer.close()
await run_agent(
ComputerProvider[LocalPlaywrightComputer](
create=create_computer,
dispose=dispose_computer,
)
)
if __name__ == "__main__":
mode = (sys.argv[1] if len(sys.argv) > 1 else "").lower()
if mode == "singleton":
asyncio.run(singleton_computer())
else:
asyncio.run(computer_per_request())
@@ -0,0 +1,117 @@
import argparse
import asyncio
import base64
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZIP_DEFLATED, ZipFile
from openai.types.responses import ResponseFunctionShellToolCall
from openai.types.responses.response_container_reference import ResponseContainerReference
from agents import Agent, Runner, ShellTool, ShellToolInlineSkill, trace
from agents.items import ModelResponse
SKILL_NAME = "csv-workbench"
SKILL_DIR = Path(__file__).resolve().parent / "skills" / SKILL_NAME
def build_skill_zip_bundle() -> bytes:
with TemporaryDirectory(prefix="agents-inline-skill-") as temp_dir:
zip_path = Path(temp_dir) / f"{SKILL_NAME}.zip"
with ZipFile(zip_path, "w", compression=ZIP_DEFLATED) as archive:
for path in sorted(SKILL_DIR.rglob("*")):
if path.is_file():
archive.write(path, f"{SKILL_NAME}/{path.relative_to(SKILL_DIR)}")
return zip_path.read_bytes()
def build_inline_skill() -> ShellToolInlineSkill:
bundle = build_skill_zip_bundle()
return {
"type": "inline",
"name": SKILL_NAME,
"description": "Analyze CSV files in /mnt/data and return concise numeric summaries.",
"source": {
"type": "base64",
"media_type": "application/zip",
"data": base64.b64encode(bundle).decode("ascii"),
},
}
def extract_container_id(raw_responses: list[ModelResponse]) -> str | None:
for response in raw_responses:
for item in response.output:
if isinstance(item, ResponseFunctionShellToolCall) and isinstance(
item.environment, ResponseContainerReference
):
return item.environment.container_id
return None
async def main(model: str) -> None:
inline_skill = build_inline_skill()
with trace("container_shell_inline_skill_example"):
agent1 = Agent(
name="Container Shell Agent (Inline Skill)",
model=model,
instructions="Use the available container skill to answer user requests.",
tools=[
ShellTool(
environment={
"type": "container_auto",
"network_policy": {"type": "disabled"},
"skills": [inline_skill],
}
)
],
)
result1 = await Runner.run(
agent1,
(
"Use the csv-workbench skill. Create /mnt/data/orders.csv with columns "
"id,region,amount,status and at least 6 rows. Then report total amount by "
"region and count failed orders."
),
)
print(f"Agent: {result1.final_output}")
container_id = extract_container_id(result1.raw_responses)
if not container_id:
raise RuntimeError("Container ID was not returned in shell call output.")
print(f"[info] Reusing container_id={container_id}")
agent2 = Agent(
name="Container Reference Shell Agent",
model=model,
instructions="Reuse the existing shell container and answer concisely.",
tools=[
ShellTool(
environment={
"type": "container_reference",
"container_id": container_id,
}
)
],
)
result2 = await Runner.run(
agent2,
"Run `ls -la /mnt/data`, then summarize in one sentence.",
)
print(f"Agent (container reuse): {result2.final_output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="gpt-5.6-sol",
help="Model name to use.",
)
args = parser.parse_args()
asyncio.run(main(args.model))
@@ -0,0 +1,112 @@
import argparse
import asyncio
import os
from openai.types.responses import ResponseFunctionShellToolCall
from openai.types.responses.response_container_reference import ResponseContainerReference
from agents import Agent, Runner, ShellTool, ShellToolSkillReference, trace
from agents.items import ModelResponse
SHELL_SKILL_ID_ENV = "OPENAI_SHELL_SKILL_ID"
SHELL_SKILL_VERSION_ENV = "OPENAI_SHELL_SKILL_VERSION"
DEFAULT_SKILL_REFERENCE: ShellToolSkillReference = {
"type": "skill_reference",
"skill_id": "skill_698bbe879adc81918725cbc69dcae7960bc5613dadaed377",
"version": "1",
}
def resolve_skill_reference() -> ShellToolSkillReference:
skill_id = os.environ.get(SHELL_SKILL_ID_ENV)
if not skill_id:
return DEFAULT_SKILL_REFERENCE
reference: ShellToolSkillReference = {"type": "skill_reference", "skill_id": skill_id}
skill_version = os.environ.get(SHELL_SKILL_VERSION_ENV)
if skill_version:
reference["version"] = skill_version
return reference
def extract_container_id(raw_responses: list[ModelResponse]) -> str | None:
for response in raw_responses:
for item in response.output:
if isinstance(item, ResponseFunctionShellToolCall) and isinstance(
item.environment, ResponseContainerReference
):
return item.environment.container_id
return None
async def main(model: str) -> None:
skill_reference = resolve_skill_reference()
print(
"[info] Using skill reference:",
skill_reference["skill_id"],
f"(version {skill_reference.get('version', 'default')})",
)
with trace("container_shell_skill_reference_example"):
agent1 = Agent(
name="Container Shell Agent (Skill Reference)",
model=model,
instructions="Use the available container skill to answer user requests.",
tools=[
ShellTool(
environment={
"type": "container_auto",
"network_policy": {"type": "disabled"},
"skills": [skill_reference],
}
)
],
)
result1 = await Runner.run(
agent1,
(
"Use the csv-workbench skill. Create /mnt/data/orders.csv with columns "
"id,region,amount,status and at least 6 rows. Then report total amount by "
"region and count failed orders."
),
)
print(f"Agent: {result1.final_output}")
container_id = extract_container_id(result1.raw_responses)
if not container_id:
raise RuntimeError("Container ID was not returned in shell call output.")
print(f"[info] Reusing container_id={container_id}")
agent2 = Agent(
name="Container Reference Shell Agent",
model=model,
instructions="Reuse the existing shell container and answer concisely.",
tools=[
ShellTool(
environment={
"type": "container_reference",
"container_id": container_id,
}
)
],
)
result2 = await Runner.run(
agent2,
"Run `ls -la /mnt/data`, then summarize in one sentence.",
)
print(f"Agent (container reuse): {result2.final_output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="gpt-5.6-sol",
help="Model name to use.",
)
args = parser.parse_args()
asyncio.run(main(args.model))
+65
View File
@@ -0,0 +1,65 @@
import asyncio
from openai import OpenAI
from agents import Agent, FileSearchTool, Runner, trace
async def main():
vector_store_id: str | None = None
if vector_store_id is None:
print("### Preparing vector store:\n")
# Create a new vector store and index a file
client = OpenAI()
text = "Arrakis, the desert planet in Frank Herbert's 'Dune,' was inspired by the scarcity of water as a metaphor for oil and other finite resources."
file_upload = client.files.create(
file=("example.txt", text.encode("utf-8")),
purpose="assistants",
)
print(f"File uploaded: {file_upload.to_dict()}")
vector_store = client.vector_stores.create(name="example-vector-store")
print(f"Vector store created: {vector_store.to_dict()}")
indexed = client.vector_stores.files.create_and_poll(
vector_store_id=vector_store.id,
file_id=file_upload.id,
)
print(f"Stored files in vector store: {indexed.to_dict()}")
vector_store_id = vector_store.id
# Create an agent that can search the vector store
agent = Agent(
name="File searcher",
instructions="You are a helpful agent. You answer only based on the information in the vector store.",
tools=[
FileSearchTool(
max_num_results=3,
vector_store_ids=[vector_store_id],
include_search_results=True,
)
],
)
with trace("File search example"):
result = await Runner.run(
agent, "Be concise, and tell me 1 sentence about Arrakis I might not know."
)
print("\n### Final output:\n")
print(result.final_output)
"""
Arrakis, the desert planet in Frank Herbert's "Dune," was inspired by the scarcity of water
as a metaphor for oil and other finite resources.
"""
print("\n### Output items:\n")
print("\n".join([str(out.raw_item) + "\n" for out in result.new_items]))
"""
{"id":"...", "queries":["Arrakis"], "results":[...]}
"""
if __name__ == "__main__":
asyncio.run(main())
+78
View File
@@ -0,0 +1,78 @@
import asyncio
import base64
import os
import subprocess
import sys
import tempfile
from collections.abc import Mapping
from typing import Any
from agents import Agent, ImageGenerationTool, Runner, trace
from examples.auto_mode import is_auto_mode
def _get_field(obj: Any, key: str) -> Any:
if isinstance(obj, Mapping):
return obj.get(key)
return getattr(obj, key, None)
def open_file(path: str) -> None:
if sys.platform.startswith("darwin"):
subprocess.run(["open", path], check=False) # macOS
elif os.name == "nt": # Windows
os.startfile(path) # type: ignore
elif os.name == "posix":
subprocess.run(["xdg-open", path], check=False) # Linux/Unix
else:
print(f"Don't know how to open files on this platform: {sys.platform}")
async def main():
agent = Agent(
name="Image generator",
instructions="Always use the image generation tool when the user asks for a new image.",
tools=[
ImageGenerationTool(
tool_config={"type": "image_generation", "quality": "low"},
)
],
)
with trace("Image generation example"):
print("Generating image, this may take a while...")
result = await Runner.run(
agent, "Create an image of a frog eating a pizza, comic book style."
)
print(result.final_output)
generated_image = False
for item in result.new_items:
if item.type != "tool_call_item":
continue
raw_call = item.raw_item
call_type = _get_field(raw_call, "type")
if call_type != "image_generation_call":
continue
img_result = _get_field(raw_call, "result")
if not isinstance(img_result, str):
continue
generated_image = True
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
tmp.write(base64.b64decode(img_result))
temp_path = tmp.name
print(f"Saved generated image to: {temp_path}")
if is_auto_mode():
print("Auto mode leaves the image on disk instead of opening it.")
else:
open_file(temp_path)
if not generated_image:
print("No image_generation_call item was returned.")
if __name__ == "__main__":
asyncio.run(main())
+78
View File
@@ -0,0 +1,78 @@
import argparse
import asyncio
from pathlib import Path
from agents import Agent, Runner, ShellTool, ShellToolLocalSkill, trace
from examples.tools.shell import ShellExecutor
SKILL_NAME = "csv-workbench"
SKILL_DIR = Path(__file__).resolve().parent / "skills" / SKILL_NAME
def build_local_skill() -> ShellToolLocalSkill:
return {
"name": SKILL_NAME,
"description": "Analyze CSV files and return concise numeric summaries.",
"path": str(SKILL_DIR),
}
async def main(model: str) -> None:
local_skill = build_local_skill()
with trace("local_shell_skill_example"):
agent1 = Agent(
name="Local Shell Agent (Local Skill)",
model=model,
instructions="Use the available local skill to answer user requests.",
tools=[
ShellTool(
environment={
"type": "local",
"skills": [local_skill],
},
executor=ShellExecutor(),
)
],
)
result1 = await Runner.run(
agent1,
(
"Use the csv-workbench skill. Create /tmp/test_orders.csv with columns "
"id,region,amount,status and at least 6 rows. Then report total amount by "
"region and count failed orders."
),
)
print(f"Agent: {result1.final_output}")
agent2 = Agent(
name="Local Shell Agent (Reuse)",
model=model,
instructions="Reuse the existing local shell and answer concisely.",
tools=[
ShellTool(
environment={
"type": "local",
},
executor=ShellExecutor(),
)
],
)
result2 = await Runner.run(
agent2,
"Run `ls -la /tmp/test_orders.csv`, then summarize in one sentence.",
)
print(f"Agent (reuse): {result2.final_output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="gpt-5.6-sol",
help="Model name to use.",
)
args = parser.parse_args()
asyncio.run(main(args.model))
+141
View File
@@ -0,0 +1,141 @@
import argparse
import asyncio
import os
from collections.abc import Sequence
from pathlib import Path
from agents import (
Agent,
ModelSettings,
Runner,
ShellCallOutcome,
ShellCommandOutput,
ShellCommandRequest,
ShellResult,
ShellTool,
trace,
)
from agents.items import ToolApprovalItem
from agents.run_context import RunContextWrapper
from agents.tool import ShellOnApprovalFunctionResult
SHELL_AUTO_APPROVE = os.environ.get("SHELL_AUTO_APPROVE") == "1"
class ShellExecutor:
"""Executes shell commands; approval is handled via ShellTool."""
def __init__(self, cwd: Path | None = None):
self.cwd = Path(cwd or Path.cwd())
async def __call__(self, request: ShellCommandRequest) -> ShellResult:
action = request.data.action
outputs: list[ShellCommandOutput] = []
for command in action.commands:
proc = await asyncio.create_subprocess_shell(
command,
cwd=self.cwd,
env=os.environ.copy(),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
timed_out = False
try:
timeout = (action.timeout_ms or 0) / 1000 or None
stdout_bytes, stderr_bytes = await asyncio.wait_for(
proc.communicate(), timeout=timeout
)
except asyncio.TimeoutError:
proc.kill()
stdout_bytes, stderr_bytes = await proc.communicate()
timed_out = True
stdout = stdout_bytes.decode("utf-8", errors="ignore")
stderr = stderr_bytes.decode("utf-8", errors="ignore")
outputs.append(
ShellCommandOutput(
command=command,
stdout=stdout,
stderr=stderr,
outcome=ShellCallOutcome(
type="timeout" if timed_out else "exit",
exit_code=getattr(proc, "returncode", None),
),
)
)
if timed_out:
break
return ShellResult(
output=outputs,
provider_data={"working_directory": str(self.cwd)},
)
async def prompt_shell_approval(commands: Sequence[str]) -> bool:
"""Simple CLI prompt for shell approvals."""
if SHELL_AUTO_APPROVE:
return True
print("Shell command approval required:")
for entry in commands:
print(" ", entry)
response = input("Proceed? [y/N] ").strip().lower()
return response in {"y", "yes"}
async def main(prompt: str, model: str) -> None:
with trace("shell_example"):
print(f"[info] Using model: {model}")
async def on_shell_approval(
_context: RunContextWrapper, approval_item: ToolApprovalItem
) -> ShellOnApprovalFunctionResult:
raw = approval_item.raw_item
commands: Sequence[str] = ()
if isinstance(raw, dict):
action = raw.get("action", {})
if isinstance(action, dict):
commands = action.get("commands", [])
else:
action_obj = getattr(raw, "action", None)
if action_obj and hasattr(action_obj, "commands"):
commands = action_obj.commands
approved = await prompt_shell_approval(commands)
return {"approve": approved, "reason": "user rejected" if not approved else "approved"}
agent = Agent(
name="Shell Assistant",
model=model,
instructions=(
"You can run shell commands using the shell tool. "
"Keep responses concise and include command output when helpful."
),
tools=[
ShellTool(
executor=ShellExecutor(),
needs_approval=True,
on_approval=on_shell_approval,
)
],
model_settings=ModelSettings(tool_choice="required"),
)
result = await Runner.run(agent, prompt)
print(f"\nFinal response:\n{result.final_output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--prompt",
default="Show the list of files in the current directory.",
help="Instruction to send to the agent.",
)
parser.add_argument(
"--model",
default="gpt-5.6-sol",
)
args = parser.parse_args()
asyncio.run(main(args.prompt, args.model))
+154
View File
@@ -0,0 +1,154 @@
import argparse
import asyncio
import os
from collections.abc import Sequence
from pathlib import Path
from agents import (
Agent,
ModelSettings,
Runner,
ShellCallOutcome,
ShellCommandOutput,
ShellCommandRequest,
ShellResult,
ShellTool,
trace,
)
from agents.items import ToolApprovalItem
from examples.auto_mode import confirm_with_fallback, is_auto_mode
class ShellExecutor:
"""Executes shell commands; approvals are handled manually via interruptions."""
def __init__(self, cwd: Path | None = None):
self.cwd = Path(cwd or Path.cwd())
async def __call__(self, request: ShellCommandRequest) -> ShellResult:
action = request.data.action
outputs: list[ShellCommandOutput] = []
for command in action.commands:
proc = await asyncio.create_subprocess_shell(
command,
cwd=self.cwd,
env=os.environ.copy(),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
timed_out = False
try:
timeout = (action.timeout_ms or 0) / 1000 or None
stdout_bytes, stderr_bytes = await asyncio.wait_for(
proc.communicate(), timeout=timeout
)
except asyncio.TimeoutError:
proc.kill()
stdout_bytes, stderr_bytes = await proc.communicate()
timed_out = True
stdout = stdout_bytes.decode("utf-8", errors="ignore")
stderr = stderr_bytes.decode("utf-8", errors="ignore")
outputs.append(
ShellCommandOutput(
command=command,
stdout=stdout,
stderr=stderr,
outcome=ShellCallOutcome(
type="timeout" if timed_out else "exit",
exit_code=getattr(proc, "returncode", None),
),
)
)
if timed_out:
break
return ShellResult(
output=outputs,
provider_data={"working_directory": str(self.cwd)},
)
async def prompt_shell_approval(commands: Sequence[str]) -> tuple[bool, bool]:
"""Prompt for approval and optional always-approve choice."""
print("Shell command approval required:")
for entry in commands:
print(f" {entry}")
auto_mode = is_auto_mode()
decision = confirm_with_fallback("Approve? [y/N]: ", default=auto_mode)
always = False
if decision:
always = confirm_with_fallback(
"Approve all future shell calls? [y/N]: ",
default=auto_mode,
)
return decision, always
def _extract_commands(approval_item: ToolApprovalItem) -> Sequence[str]:
raw = approval_item.raw_item
if isinstance(raw, dict):
action = raw.get("action", {})
if isinstance(action, dict):
commands = action.get("commands", [])
if isinstance(commands, Sequence):
return [str(cmd) for cmd in commands]
action_obj = getattr(raw, "action", None)
if action_obj and hasattr(action_obj, "commands"):
return list(action_obj.commands)
return ()
async def main(prompt: str, model: str) -> None:
with trace("shell_hitl_example"):
print(f"[info] Using model: {model}")
agent = Agent(
name="Shell HITL Assistant",
model=model,
instructions=(
"You can run shell commands using the shell tool. "
"Ask for approval before running commands."
),
tools=[
ShellTool(
executor=ShellExecutor(),
needs_approval=True,
)
],
model_settings=ModelSettings(tool_choice="required"),
)
result = await Runner.run(agent, prompt)
while result.interruptions:
print("\n== Pending approvals ==")
state = result.to_state()
for interruption in result.interruptions:
commands = _extract_commands(interruption)
approved, always = await prompt_shell_approval(commands)
if approved:
state.approve(interruption, always_approve=always)
else:
state.reject(interruption, always_reject=always)
result = await Runner.run(agent, state)
print(f"\nFinal response:\n{result.final_output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--prompt",
default="List the files in the current directory and show the current working directory.",
help="Instruction to send to the agent.",
)
parser.add_argument(
"--model",
default="gpt-5.6-sol",
)
args = parser.parse_args()
asyncio.run(main(args.prompt, args.model))
@@ -0,0 +1,20 @@
---
name: csv-workbench
description: Analyze CSV files in /mnt/data and return concise numeric summaries.
---
# CSV Workbench
Use this skill when the user asks for quick analysis of tabular data.
## Workflow
1. Inspect the CSV schema first (`head`, `python csv.DictReader`, or both).
2. Compute requested aggregates with a short Python script.
3. Return concise results with concrete numbers and units when available.
## Constraints
- Prefer Python stdlib for portability.
- If data is missing or malformed, state assumptions clearly.
- Keep the final answer short and actionable.
@@ -0,0 +1,32 @@
# CSV Playbook
## Quick checks
- Preview rows: `head -n 10 /mnt/data/your-file.csv`.
- Count rows:
```bash
python - <<'PY'
import csv
with open('/mnt/data/your-file.csv', newline='') as f:
print(sum(1 for _ in csv.DictReader(f)))
PY
```
## Grouped totals template
```bash
python - <<'PY'
import csv
from collections import defaultdict
totals = defaultdict(float)
with open('/mnt/data/your-file.csv', newline='') as f:
for row in csv.DictReader(f):
totals[row['region']] += float(row['amount'])
for region in sorted(totals):
print(region, round(totals[region], 2))
PY
```
+219
View File
@@ -0,0 +1,219 @@
import asyncio
import json
import sys
from collections.abc import Mapping
from typing import Annotated, Any
from agents import (
Agent,
ModelSettings,
Runner,
ToolSearchTool,
function_tool,
tool_namespace,
trace,
)
CUSTOMER_PROFILES = {
"customer_42": {
"customer_id": "customer_42",
"full_name": "Avery Chen",
"tier": "enterprise",
}
}
OPEN_ORDERS = {
"customer_42": [
{"order_id": "ord_1042", "status": "awaiting fulfillment"},
{"order_id": "ord_1049", "status": "pending approval"},
]
}
INVOICE_STATUSES = {
"inv_2001": "paid",
}
SHIPPING_ETAS = {
"ZX-123": "2026-03-06 14:00 JST",
}
SHIPPING_CREDIT_BALANCES = {
"customer_42": "$125.00",
}
@function_tool(defer_loading=True)
def get_customer_profile(
customer_id: Annotated[str, "The CRM customer identifier to look up."],
) -> str:
"""Fetch a CRM customer profile."""
return json.dumps(CUSTOMER_PROFILES[customer_id], indent=2)
@function_tool(defer_loading=True)
def list_open_orders(
customer_id: Annotated[str, "The CRM customer identifier to look up."],
) -> str:
"""List open orders for a customer."""
return json.dumps(OPEN_ORDERS.get(customer_id, []), indent=2)
@function_tool(defer_loading=True)
def get_invoice_status(
invoice_id: Annotated[str, "The invoice identifier to look up."],
) -> str:
"""Look up the status of an invoice."""
return INVOICE_STATUSES.get(invoice_id, "unknown")
@function_tool(defer_loading=True)
def get_shipping_eta(
tracking_number: Annotated[str, "The shipment tracking number to look up."],
) -> str:
"""Look up a shipment ETA by tracking number."""
return SHIPPING_ETAS.get(tracking_number, "unavailable")
@function_tool(defer_loading=True)
def get_shipping_credit_balance(
customer_id: Annotated[str, "The customer account identifier to look up."],
) -> str:
"""Look up the available shipping credit balance for a customer."""
return SHIPPING_CREDIT_BALANCES.get(customer_id, "$0.00")
crm_tools = tool_namespace(
name="crm",
description="CRM tools for customer lookups.",
tools=[get_customer_profile, list_open_orders],
)
billing_tools = tool_namespace(
name="billing",
description="Billing tools for invoice lookups.",
tools=[get_invoice_status],
)
namespaced_agent = Agent(
name="Operations assistant",
model="gpt-5.6-sol",
instructions=(
"For customer questions in this example, load the full `crm` namespace with no query "
"filter before calling tools. "
"Do not search `billing` unless the user asks about invoices."
),
model_settings=ModelSettings(parallel_tool_calls=False),
tools=[*crm_tools, *billing_tools, ToolSearchTool()],
)
top_level_agent = Agent(
name="Shipping assistant",
model="gpt-5.6-sol",
instructions=(
"For ETA questions in this example, search `get_shipping_eta` before calling tools. "
"Do not search `get_shipping_credit_balance` unless the user asks about shipping credits."
),
model_settings=ModelSettings(parallel_tool_calls=False),
tools=[get_shipping_eta, get_shipping_credit_balance, ToolSearchTool()],
)
def loaded_paths(result: Any) -> list[str]:
paths: set[str] = set()
for item in result.new_items:
if item.type != "tool_search_output_item":
continue
raw_tools = (
item.raw_item.get("tools")
if isinstance(item.raw_item, Mapping)
else getattr(item.raw_item, "tools", None)
)
if not isinstance(raw_tools, list):
continue
for raw_tool in raw_tools:
tool_payload = (
raw_tool
if isinstance(raw_tool, Mapping)
else (
raw_tool.model_dump(exclude_unset=True)
if callable(getattr(raw_tool, "model_dump", None))
else None
)
)
if not isinstance(tool_payload, Mapping):
continue
tool_type = tool_payload.get("type")
if tool_type == "namespace":
path = tool_payload.get("name")
elif tool_type == "function":
path = tool_payload.get("name")
else:
path = tool_payload.get("server_label")
if isinstance(path, str) and path:
paths.add(path)
return sorted(paths)
def print_result(title: str, result: Any, registered_paths: list[str]) -> None:
loaded = loaded_paths(result)
untouched = [path for path in registered_paths if path not in loaded]
print(f"## {title}")
print("### Final output")
print(result.final_output)
print("\n### Loaded paths")
print(f"- registered: {', '.join(registered_paths)}")
print(f"- loaded: {', '.join(loaded) if loaded else 'none'}")
print(f"- untouched: {', '.join(untouched) if untouched else 'none'}")
print("\n### Relevant items")
for item in result.new_items:
if item.type in {"tool_search_call_item", "tool_search_output_item", "tool_call_item"}:
print(f"- {item.type}: {item.raw_item}")
print()
async def run_namespaced_example() -> None:
result = await Runner.run(
namespaced_agent,
"Look up customer_42 and list their open orders.",
)
print_result(
"Tool search with namespaces",
result,
registered_paths=["crm", "billing"],
)
async def run_top_level_example() -> None:
result = await Runner.run(
top_level_agent,
"Can you get my ETA for tracking number ZX-123?",
)
print_result(
"Tool search with top-level deferred tools",
result,
registered_paths=["get_shipping_eta", "get_shipping_credit_balance"],
)
async def main() -> None:
mode = sys.argv[1] if len(sys.argv) > 1 else "all"
if mode not in {"all", "namespace", "top-level"}:
raise SystemExit(f"Unknown mode: {mode}. Expected one of: all, namespace, top-level.")
with trace("Tool search example"):
if mode in {"all", "namespace"}:
await run_namespaced_example()
if mode in {"all", "top-level"}:
await run_top_level_example()
if __name__ == "__main__":
asyncio.run(main())
+23
View File
@@ -0,0 +1,23 @@
import asyncio
from agents import Agent, Runner, WebSearchTool, trace
async def main():
agent = Agent(
name="Web searcher",
instructions="You are a helpful agent.",
tools=[WebSearchTool(user_location={"type": "approximate", "city": "New York"})],
)
with trace("Web search example"):
result = await Runner.run(
agent,
"search the web for 'local sports news' and give me 1 interesting update in a sentence.",
)
print(result.final_output)
# The New York Giants are reportedly pursuing quarterback Aaron Rodgers after his ...
if __name__ == "__main__":
asyncio.run(main())
+147
View File
@@ -0,0 +1,147 @@
import asyncio
from urllib.parse import unquote, urlsplit, urlunsplit
from openai.types.responses.web_search_tool import Filters
from openai.types.shared.reasoning import Reasoning
from agents import Agent, ModelSettings, Runner, WebSearchTool, trace
from examples.web_search_utils import extract_url_citations, extract_web_search_source_urls
ALLOWED_DOMAINS = ["developers.openai.com"]
# import logging
# logging.basicConfig(level=logging.DEBUG)
def _normalize_source_url(url: str) -> str | None:
allowed_domains = {domain.lower().rstrip(".") for domain in ALLOWED_DOMAINS}
blocked_suffixes = (
".css",
".eot",
".gif",
".ico",
".jpeg",
".jpg",
".js",
".png",
".svg",
".svgz",
".tar",
".tgz",
".woff",
".woff2",
".zip",
".gz",
)
try:
parsed = urlsplit(url)
port = parsed.port
except ValueError:
return None
hostname = parsed.hostname.lower().rstrip(".") if parsed.hostname else None
if (
parsed.scheme not in {"http", "https"}
or hostname is None
or parsed.username is not None
or parsed.password is not None
or port is not None
or not any(
hostname == domain or hostname.endswith(f".{domain}") for domain in allowed_domains
)
):
return None
path = parsed.path.rstrip("/")
decoded_path = unquote(path)
if (
not path
or any(character in decoded_path for character in "?#")
or any(ord(character) < 32 or ord(character) == 127 for character in decoded_path)
or decoded_path.lower().endswith(blocked_suffixes)
):
return None
return urlunsplit((parsed.scheme, hostname, path, "", ""))
def _normalized_source_urls(urls: list[str]) -> list[str]:
normalized_urls: list[str] = []
seen: set[str] = set()
for url in urls:
normalized = _normalize_source_url(url)
if normalized is None or normalized in seen:
continue
seen.add(normalized)
normalized_urls.append(normalized)
return normalized_urls
async def main():
agent = Agent(
name="WebOAI website searcher",
model="gpt-5.6",
instructions=(
"You are a helpful agent that searches OpenAI developer documentation. Answer only "
"from the allowed official documentation sources and include inline citations. Cite "
"the official page for each model when comparing multiple models."
),
tools=[
WebSearchTool(
# https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#domain-filtering
filters=Filters(allowed_domains=ALLOWED_DOMAINS),
search_context_size="medium",
)
],
model_settings=ModelSettings(
reasoning=Reasoning(effort="low"),
tool_choice="required",
verbosity="low",
# https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#sources
response_include=["web_search_call.action.sources"],
),
)
with trace("Web search example"):
query = (
"Using only official OpenAI developer documentation, compare GPT-5.6 Sol and "
"GPT-5.6 Terra in three concise bullets and explain when to use each model."
)
result = await Runner.run(agent, query)
citations = extract_url_citations(result.new_items)
cited_urls = _normalized_source_urls([citation.url for citation in citations])
retrieved_urls = _normalized_source_urls(extract_web_search_source_urls(result.new_items))
model_documentation_urls = [
url for url in retrieved_urls if "/api/docs/models/gpt-5.6-" in url
]
if not cited_urls:
raise RuntimeError("Expected at least one official inline citation in the final answer")
if not model_documentation_urls:
raise RuntimeError(
f"Expected GPT-5.6 model documentation in retrieved sources, got {retrieved_urls}"
)
print()
print("### Cited sources ###")
print()
for url in cited_urls:
print(f"- {url}")
print()
print("### Retrieved model documentation ###")
print()
for url in model_documentation_urls:
print(f"- {url}")
print()
print("### Final output ###")
print()
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())