Skip to content

Commit 8bc4264

Browse files
authored
Add runner script for examples and refresh sample outputs (#2289)
1 parent a459e36 commit 8bc4264

5 files changed

Lines changed: 235 additions & 16 deletions

File tree

examples/basic/hello_world_gpt_oss.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import asyncio
2-
import logging
32

43
from openai import AsyncOpenAI
54

65
from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled
76

87
set_tracing_disabled(True)
9-
logging.basicConfig(level=logging.DEBUG)
8+
9+
# import logging
10+
# logging.basicConfig(level=logging.DEBUG)
1011

1112
# This is an example of how to use gpt-oss with Ollama.
1213
# Refer to https://cookbook.openai.com/articles/gpt-oss/run-locally-ollama for more details.

examples/basic/image_tool_output.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@
44

55
return_typed_dict = True
66

7+
URL = "https://images.unsplash.com/photo-1505761671935-60b3a7427bad?auto=format&fit=crop&w=400&q=80"
8+
79

810
@function_tool
911
def fetch_random_image() -> ToolOutputImage | ToolOutputImageDict:
1012
"""Fetch a random image."""
1113

1214
print("Image tool called")
1315
if return_typed_dict:
14-
return {
15-
"type": "image",
16-
"image_url": "https://upload.wikimedia.org/wikipedia/commons/0/0c/GoldenGateBridge-001.jpg",
17-
"detail": "auto",
18-
}
19-
20-
return ToolOutputImage(
21-
image_url="https://upload.wikimedia.org/wikipedia/commons/0/0c/GoldenGateBridge-001.jpg",
22-
detail="auto",
23-
)
16+
return {"type": "image", "image_url": URL, "detail": "auto"}
17+
18+
return ToolOutputImage(image_url=URL, detail="auto")
2419

2520

2621
async def main():
@@ -35,8 +30,7 @@ async def main():
3530
input="Fetch an image using the random_image tool, then describe it",
3631
)
3732
print(result.final_output)
38-
"""The image shows the iconic Golden Gate Bridge, a large suspension bridge painted in a
39-
bright reddish-orange color..."""
33+
"""This image features the famous clock tower, commonly known as Big Ben, ..."""
4034

4135

4236
if __name__ == "__main__":

examples/basic/remote_image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from agents import Agent, Runner
44

5-
URL = "https://upload.wikimedia.org/wikipedia/commons/0/0c/GoldenGateBridge-001.jpg"
5+
URL = "https://images.unsplash.com/photo-1505761671935-60b3a7427bad?auto=format&fit=crop&w=400&q=80"
66

77

88
async def main():

examples/reasoning_content/runner_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from agents import Agent, ModelSettings, Runner, trace
1818
from agents.items import ReasoningItem
1919

20-
MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or "gpt-5"
20+
MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or "gpt-5.2"
2121

2222

2323
async def main():

examples/run_examples.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""Run multiple example entry points in this repository.
2+
3+
This script locates Python files under ``examples/`` that contain a
4+
``__main__`` guard and executes them one by one. By default it skips
5+
interactive, server-like, audio-heavy, and external-service examples so
6+
that automated validation does not hang waiting for input or require
7+
hardware. Use flags to opt into those categories when you want to run
8+
them.
9+
10+
Usage examples:
11+
12+
uv run examples/run_examples.py --dry-run
13+
uv run examples/run_examples.py --filter basic
14+
uv run examples/run_examples.py --include-interactive --include-server
15+
16+
By default the script keeps running even if an example fails; use
17+
``--fail-fast`` to stop on the first failure.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import argparse
23+
import re
24+
import shlex
25+
import subprocess
26+
import sys
27+
from collections.abc import Iterable, Sequence
28+
from dataclasses import dataclass, field
29+
from pathlib import Path
30+
31+
ROOT_DIR = Path(__file__).resolve().parent.parent
32+
EXAMPLES_DIR = ROOT_DIR / "examples"
33+
MAIN_PATTERN = re.compile(r"__name__\s*==\s*['\"]__main__['\"]")
34+
35+
36+
@dataclass
37+
class ExampleScript:
38+
path: Path
39+
tags: set[str] = field(default_factory=set)
40+
41+
@property
42+
def relpath(self) -> str:
43+
return str(self.path.relative_to(ROOT_DIR))
44+
45+
@property
46+
def module(self) -> str:
47+
relative = self.path.relative_to(ROOT_DIR).with_suffix("")
48+
return ".".join(relative.parts)
49+
50+
@property
51+
def command(self) -> list[str]:
52+
# Run via module path so relative imports inside examples work.
53+
return ["uv", "run", "python", "-m", self.module]
54+
55+
56+
def parse_args() -> argparse.Namespace:
57+
parser = argparse.ArgumentParser(description="Run example scripts sequentially.")
58+
parser.add_argument(
59+
"--filter",
60+
"-f",
61+
action="append",
62+
default=[],
63+
help="Case-insensitive substring filter applied to the relative path.",
64+
)
65+
parser.add_argument(
66+
"--dry-run", action="store_true", help="List commands without running them."
67+
)
68+
parser.add_argument(
69+
"--include-interactive",
70+
action="store_true",
71+
help="Include examples that prompt for user input or human-in-the-loop approvals.",
72+
)
73+
parser.add_argument(
74+
"--include-server",
75+
action="store_true",
76+
help="Include long-running server-style examples (HTTP servers, background services).",
77+
)
78+
parser.add_argument(
79+
"--include-audio",
80+
action="store_true",
81+
help="Include voice or realtime audio examples that require a microphone/speaker.",
82+
)
83+
parser.add_argument(
84+
"--include-external",
85+
action="store_true",
86+
help="Include examples that rely on extra services like Redis, Dapr, Twilio, or Playwright.",
87+
)
88+
parser.add_argument(
89+
"--fail-fast",
90+
action="store_true",
91+
help="Stop after the first failing example.",
92+
)
93+
parser.add_argument(
94+
"--verbose",
95+
action="store_true",
96+
help="Show detected tags for each example entry.",
97+
)
98+
return parser.parse_args()
99+
100+
101+
def detect_tags(path: Path, source: str) -> set[str]:
102+
tags: set[str] = set()
103+
lower_source = source.lower()
104+
lower_parts = [part.lower() for part in path.parts]
105+
106+
if re.search(r"\binput\s*\(", source):
107+
tags.add("interactive")
108+
if "prompt_toolkit" in lower_source or "questionary" in lower_source:
109+
tags.add("interactive")
110+
if "human_in_the_loop" in lower_source or "hitl" in lower_source:
111+
tags.add("interactive")
112+
113+
if any("server" in part for part in lower_parts):
114+
tags.add("server")
115+
if any(keyword in lower_source for keyword in ("uvicorn", "fastapi", "websocket")):
116+
tags.add("server")
117+
118+
if any(part in {"voice", "realtime"} for part in lower_parts):
119+
tags.add("audio")
120+
if any(keyword in lower_source for keyword in ("sounddevice", "microphone", "audioinput")):
121+
tags.add("audio")
122+
123+
if any(keyword in lower_source for keyword in ("redis", "dapr", "twilio", "playwright")):
124+
tags.add("external")
125+
126+
return tags
127+
128+
129+
def discover_examples(filters: Iterable[str]) -> list[ExampleScript]:
130+
filters_lower = [f.lower() for f in filters]
131+
examples: list[ExampleScript] = []
132+
133+
for path in EXAMPLES_DIR.rglob("*.py"):
134+
if "__pycache__" in path.parts or path.name.startswith("__"):
135+
continue
136+
137+
try:
138+
source = path.read_text(encoding="utf-8")
139+
except OSError:
140+
continue
141+
142+
if not MAIN_PATTERN.search(source):
143+
continue
144+
145+
if filters_lower and not any(
146+
f in str(path.relative_to(ROOT_DIR)).lower() for f in filters_lower
147+
):
148+
continue
149+
150+
tags = detect_tags(path, source)
151+
examples.append(ExampleScript(path=path, tags=tags))
152+
153+
return sorted(examples, key=lambda item: item.relpath)
154+
155+
156+
def should_skip(tags: set[str], allowed_overrides: set[str]) -> tuple[bool, set[str]]:
157+
blocked = {"interactive", "server", "audio", "external"} - allowed_overrides
158+
active_blockers = tags & blocked
159+
return (len(active_blockers) > 0, active_blockers)
160+
161+
162+
def format_command(cmd: Sequence[str]) -> str:
163+
return shlex.join(cmd)
164+
165+
166+
def run_examples(examples: Sequence[ExampleScript], args: argparse.Namespace) -> int:
167+
overrides: set[str] = set()
168+
if args.include_interactive:
169+
overrides.add("interactive")
170+
if args.include_server:
171+
overrides.add("server")
172+
if args.include_audio:
173+
overrides.add("audio")
174+
if args.include_external:
175+
overrides.add("external")
176+
177+
if not examples:
178+
print("No example entry points found that match the filters.")
179+
return 0
180+
181+
print(f"Found {len(examples)} example entry points under examples/.")
182+
183+
executed = 0
184+
skipped = 0
185+
failed = 0
186+
187+
for example in examples:
188+
skip, reasons = should_skip(example.tags, overrides)
189+
tag_label = f" [tags: {', '.join(sorted(example.tags))}]" if args.verbose else ""
190+
191+
if skip:
192+
reason_label = f" (skipped: {', '.join(sorted(reasons))})" if reasons else ""
193+
print(f"- SKIP {example.relpath}{tag_label}{reason_label}")
194+
skipped += 1
195+
continue
196+
197+
print(f"- RUN {example.relpath}{tag_label}")
198+
print(f" cmd: {format_command(example.command)}")
199+
200+
if args.dry_run:
201+
continue
202+
203+
result = subprocess.run(example.command, cwd=ROOT_DIR)
204+
if result.returncode != 0:
205+
print(f" !! {example.relpath} exited with {result.returncode}")
206+
failed += 1
207+
if args.fail_fast:
208+
return result.returncode
209+
continue
210+
211+
executed += 1
212+
213+
print(f"Done. Ran {executed} example(s), skipped {skipped}, failed {failed}.")
214+
return 0 if failed == 0 else 1
215+
216+
217+
def main() -> int:
218+
args = parse_args()
219+
examples = discover_examples(args.filter)
220+
return run_examples(examples, args)
221+
222+
223+
if __name__ == "__main__":
224+
sys.exit(main())

0 commit comments

Comments
 (0)