chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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))
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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))
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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
|
||||
```
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user