Skip to content

Commit 4565996

Browse files
authored
Add a run-samples.py script that runs all of the samples (#68)
Maybe eventually this should be "test", but currently they don't do a very good job testing, since some exit succesfully despite not working.
1 parent 54513bc commit 4565996

4 files changed

Lines changed: 226 additions & 23 deletions

File tree

examples/run-samples.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env python3
2+
"""Run example samples and report results.
3+
4+
Usage (from repo root):
5+
uv run examples/run-samples.py # text-only samples
6+
uv run examples/run-samples.py --image # also run image samples
7+
uv run examples/run-samples.py --video # also run video samples
8+
uv run examples/run-samples.py --all # run everything
9+
uv run examples/run-samples.py --parallel # run in parallel
10+
"""
11+
12+
import argparse
13+
import concurrent.futures
14+
import dataclasses
15+
import os
16+
import subprocess
17+
import sys
18+
from pathlib import Path
19+
20+
REPO = Path(__file__).resolve().parent.parent
21+
SAMPLES = REPO / "examples" / "samples"
22+
23+
24+
@dataclasses.dataclass
25+
class Sample:
26+
name: str
27+
_: dataclasses.KW_ONLY
28+
stdin: str | None = None
29+
30+
31+
TEXT_SAMPLES = [
32+
Sample("stream.py"),
33+
Sample("stream_all.py"),
34+
Sample("structured_output.py"),
35+
Sample("tools_schema.py"),
36+
Sample("agent_simple.py"),
37+
Sample("agent_custom_loop.py"),
38+
Sample("agent_nested.py"),
39+
Sample("streaming_tool.py"),
40+
Sample("explicit_client.py"),
41+
Sample("middleware_simple.py"),
42+
Sample("multimodal_input.py"),
43+
Sample("check_connection.py"),
44+
]
45+
46+
IMAGE_SAMPLES = [
47+
Sample("image_generation.py"),
48+
Sample("image_edit.py"),
49+
Sample("inline_image.py"),
50+
]
51+
52+
VIDEO_SAMPLES = [
53+
Sample("video_generation.py"),
54+
]
55+
56+
# Broken!
57+
HOOKS_SAMPLES = [
58+
Sample("agent_hooks.py", stdin="y\n"),
59+
Sample("agent_hooks_serverless.py"),
60+
]
61+
62+
# Broken!
63+
MCP_SAMPLES = [
64+
Sample("mcp_tools.py"),
65+
]
66+
67+
68+
def _sample_cmd(sample: Sample) -> list[str]:
69+
return [
70+
"uv",
71+
"run",
72+
"--frozen",
73+
"--with-editable",
74+
str(REPO),
75+
"python",
76+
str(SAMPLES / sample.name),
77+
]
78+
79+
80+
_env = {k: v for k, v in os.environ.items() if k != "VIRTUAL_ENV"}
81+
82+
83+
def run_sample(sample: Sample) -> bool:
84+
print(f"{'=' * 20} {sample.name} {'=' * 20}")
85+
sys.stdout.flush()
86+
result = subprocess.run(
87+
_sample_cmd(sample),
88+
env=_env,
89+
timeout=120,
90+
input=sample.stdin,
91+
text=True,
92+
)
93+
print()
94+
sys.stdout.flush()
95+
return result.returncode == 0
96+
97+
98+
def run_sample_quiet(sample: Sample) -> tuple[str, bool, str]:
99+
try:
100+
result = subprocess.run(
101+
_sample_cmd(sample),
102+
env=_env,
103+
timeout=120,
104+
capture_output=True,
105+
text=True,
106+
input=sample.stdin,
107+
)
108+
output = result.stdout + result.stderr
109+
return sample.name, result.returncode == 0, output
110+
except subprocess.TimeoutExpired:
111+
return sample.name, False, "TIMEOUT after 120s"
112+
113+
114+
def main() -> None:
115+
parser = argparse.ArgumentParser(description="Run example samples.")
116+
parser.add_argument("--text", action="store_true", help="include text samples")
117+
parser.add_argument("--image", action="store_true", help="include image samples")
118+
parser.add_argument("--video", action="store_true", help="include video samples")
119+
parser.add_argument("--hooks", action="store_true", help="include hook samples")
120+
parser.add_argument("--mcp", action="store_true", help="include MCP samples")
121+
parser.add_argument("--all", action="store_true", help="run all samples")
122+
parser.add_argument(
123+
"--parallel", action="store_true", help="run samples in parallel"
124+
)
125+
args = parser.parse_args()
126+
127+
has_category = args.text or args.image or args.video or args.hooks or args.mcp
128+
129+
samples: list[Sample] = []
130+
if args.text or args.all or not has_category:
131+
samples.extend(TEXT_SAMPLES)
132+
if args.image or args.all:
133+
samples.extend(IMAGE_SAMPLES)
134+
if args.video or args.all:
135+
samples.extend(VIDEO_SAMPLES)
136+
if args.hooks or args.all:
137+
samples.extend(HOOKS_SAMPLES)
138+
if args.mcp or args.all:
139+
samples.extend(MCP_SAMPLES)
140+
141+
results: list[tuple[str, bool]] = []
142+
143+
if args.parallel:
144+
outputs: dict[str, str] = {}
145+
with concurrent.futures.ThreadPoolExecutor() as pool:
146+
futures = {pool.submit(run_sample_quiet, s): s for s in samples}
147+
for future in concurrent.futures.as_completed(futures):
148+
name, ok, output = future.result()
149+
status = "PASS" if ok else "FAIL"
150+
print(f" {status} {name}")
151+
sys.stdout.flush()
152+
outputs[name] = output
153+
results.append((name, ok))
154+
155+
passed = sorted(name for name, ok in results if ok)
156+
failed = sorted(name for name, ok in results if not ok)
157+
158+
print()
159+
for name in [*passed, *failed]:
160+
print(f"{'=' * 20} {name} {'=' * 20}")
161+
if outputs[name].strip():
162+
print(outputs[name].rstrip())
163+
print()
164+
165+
if failed:
166+
sys.exit(1)
167+
else:
168+
for sample in samples:
169+
try:
170+
ok = run_sample(sample)
171+
except subprocess.TimeoutExpired:
172+
print(" TIMEOUT after 120s\n")
173+
ok = False
174+
results.append((sample.name, ok))
175+
176+
print("=" * 60)
177+
print("Summary:")
178+
any_failed = False
179+
for name, ok in results:
180+
status = "PASS" if ok else "FAIL"
181+
print(f" {status} {name}")
182+
if not ok:
183+
any_failed = True
184+
print()
185+
186+
if any_failed:
187+
sys.exit(1)
188+
189+
190+
if __name__ == "__main__":
191+
main()

examples/samples/check_connection.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,62 @@
11
"""Check connection and list models — verify credentials and model availability."""
22

33
import asyncio
4+
import sys
45

56
import ai
67

7-
MODELS = [
8-
ai.ai_gateway("anthropic/claude-sonnet-4"),
9-
ai.anthropic("claude-sonnet-4-20250514"),
10-
ai.openai("gpt-5.4-mini"),
8+
PROVIDERS: list[tuple[str, ai.Provider, str]] = [
9+
("ai_gateway", ai.ai_gateway, "anthropic/claude-sonnet-4"),
10+
("anthropic", ai.anthropic, "claude-sonnet-4-20250514"),
11+
("openai", ai.openai, "gpt-5.4-mini"),
1112
]
1213

13-
PROVIDERS = [
14-
("ai_gateway", ai.ai_gateway),
15-
("anthropic", ai.anthropic),
16-
("openai", ai.openai),
17-
]
14+
_failed = False
15+
16+
17+
def _fail(msg: str) -> None:
18+
global _failed # noqa: PLW0603
19+
_failed = True
20+
print(msg)
1821

1922

20-
async def _check(model: ai.Model) -> None:
23+
async def _check(name: str, provider: ai.Provider, model_id: str) -> None:
24+
if provider.client().api_key is None:
25+
print(f" [SKIP] {provider.api_key_env} not set")
26+
return
27+
model = provider(model_id)
2128
try:
2229
ok = await ai.check_connection(model)
23-
status = "[OK] " if ok else "[FAIL]"
30+
if ok:
31+
print(f" [OK] {name}/{model_id}")
32+
else:
33+
_fail(f" [FAIL] {name}/{model_id}")
2434
except Exception as exc:
25-
status = f"[ERR] {exc}"
26-
print(f" {status} {model.provider}/{model.id}")
35+
_fail(f" [ERR] {name}/{model_id}: {exc}")
2736

2837

29-
async def _list_models(name: str, provider: object) -> None:
38+
async def _list_models(name: str, provider: ai.Provider) -> None:
39+
if provider.client().api_key is None:
40+
return
3041
try:
31-
ids: list[str] = await provider.list() # type: ignore[attr-defined]
32-
print(f" {name}: {len(ids)} models")
33-
for mid in ids:
34-
print(f" - {mid}")
42+
ids: list[str] = await provider.list()
43+
print(f" {name}: {len(ids)} models (last: {ids[-1]})")
3544
except Exception as exc:
36-
print(f" {name}: [ERR] {exc}")
45+
_fail(f" {name}: [ERR] {exc}")
3746

3847

3948
async def main() -> None:
4049
print("Checking connections...\n")
41-
await asyncio.gather(*[_check(m) for m in MODELS])
50+
for name, provider, model_id in PROVIDERS:
51+
await _check(name, provider, model_id)
4252

4353
print("\nListing models...\n")
44-
await asyncio.gather(*[_list_models(n, p) for n, p in PROVIDERS])
54+
for name, provider, _ in PROVIDERS:
55+
await _list_models(name, provider)
4556

4657
print()
58+
if _failed:
59+
sys.exit(1)
4760

4861

4962
if __name__ == "__main__":

examples/samples/multimodal_input.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
model = ai.ai_gateway("anthropic/claude-sonnet-4")
99

10-
# Load a local image file (replace with your own path).
11-
image_path = pathlib.Path("sample_image.jpg")
10+
image_path = pathlib.Path(__file__).parent / "sample_image.jpg"
1211
image_data = image_path.read_bytes()
1312

1413
messages = [

examples/samples/sample_image.jpg

3.83 KB
Loading

0 commit comments

Comments
 (0)