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
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.14-slim
COPY --from=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a /uv /bin/uv
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
git \
poppler-utils \
ripgrep \
&& rm -rf /var/lib/apt/lists/*
RUN uv pip install --system --no-cache-dir --index-strategy first-index --exclude-newer "7 days" pypdf
WORKDIR /workspace
+1
View File
@@ -0,0 +1 @@
+240
View File
@@ -0,0 +1,240 @@
"""Generate the synthetic dataroom fixture files."""
from pathlib import Path
def pdf_escape(text: str) -> str:
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
def write_plain_pdf(path: Path, lines: list[str]) -> None:
content_lines = ["BT", "/F1 11 Tf", "50 760 Td", "14 TL"]
for index, line in enumerate(lines):
operator = "Tj" if index == 0 else "T* Tj"
content_lines.append(f"({pdf_escape(line)}) {operator}")
content_lines.append("ET")
stream = "\n".join(content_lines).encode("utf-8")
objects = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
b"/Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
b"<< /Length "
+ str(len(stream)).encode("ascii")
+ b" >>\nstream\n"
+ stream
+ b"\nendstream",
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
]
pdf = bytearray(b"%PDF-1.4\n")
offsets = [0]
for index, body in enumerate(objects, start=1):
offsets.append(len(pdf))
pdf.extend(f"{index} 0 obj\n".encode("ascii"))
pdf.extend(body)
pdf.extend(b"\nendobj\n")
xref_offset = len(pdf)
pdf.extend(f"xref\n0 {len(objects) + 1}\n".encode("ascii"))
pdf.extend(b"0000000000 65535 f \n")
for offset in offsets[1:]:
pdf.extend(f"{offset:010d} 00000 n \n".encode("ascii"))
pdf.extend(
(
"trailer\n"
f"<< /Size {len(objects) + 1} /Root 1 0 R >>\n"
"startxref\n"
f"{xref_offset}\n"
"%%EOF\n"
).encode("ascii")
)
path.write_bytes(pdf)
def write_financial_pdf(path: Path, title: str, lines: list[str], rows: list[list[str]]) -> None:
write_plain_pdf(path, [title, *lines, *(" | ".join(row) for row in rows)])
def write_fixture_text(data_dir: Path, filename: str, content: str) -> None:
(data_dir / filename).write_text(content.strip() + "\n", encoding="utf-8")
def main() -> None:
data_dir = Path(__file__).resolve().parent
write_fixture_text(
data_dir,
"10-k-mdna-overview.txt",
"""
UNITED STATES
SECURITIES AND EXCHANGE COMMISSION
Washington, D.C. 20549
FORM 10-K
ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934
For the fiscal year ended December 31, 2025
HelioCart, Inc.
PART II
Item 7. Management's Discussion and Analysis of Financial Condition and Results of Operations
Revenue for fiscal 2025 was $1,284 million, compared with $1,008 million in fiscal 2024.
The increase was driven primarily by Platform revenue growth from merchant fraud
decisioning and payment orchestration workloads.
Gross margin improved to 71.4% in fiscal 2025 from 68.2% in fiscal 2024 because a higher
mix of transaction volume ran on lower-cost model serving infrastructure.
Operating income was $186 million in fiscal 2025, compared with $118 million in fiscal 2024.
Management uses "net revenue" and "revenue" interchangeably in this MD&A section.
""",
)
write_fixture_text(
data_dir,
"10-k-mdna-liquidity.txt",
"""
UNITED STATES
SECURITIES AND EXCHANGE COMMISSION
Washington, D.C. 20549
FORM 10-K
ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934
For the fiscal year ended December 31, 2025
HelioCart, Inc.
PART II
Item 7. Management's Discussion and Analysis of Financial Condition and Results of Operations
Liquidity and capital resources. Net cash provided by operating activities was $248 million
in fiscal 2025, compared with $192 million in fiscal 2024, primarily because of higher
cash collections and improved operating margins.
Capital expenditures were $86 million in fiscal 2025 and $73 million in fiscal 2024.
Free cash flow, a non-GAAP measure defined as operating cash flow less capital
expenditures, was $162 million in fiscal 2025 and $119 million in fiscal 2024.
""",
)
write_fixture_text(
data_dir,
"10-k-note-segments.txt",
"""
UNITED STATES
SECURITIES AND EXCHANGE COMMISSION
Washington, D.C. 20549
FORM 10-K
ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934
For the fiscal year ended December 31, 2025
HelioCart, Inc.
PART II
Item 8. Financial Statements and Supplementary Data
Note 4. Revenue by reportable segment
Platform segment revenue was $942 million in fiscal 2025 and $711 million in fiscal 2024.
Services segment revenue was $342 million in fiscal 2025 and $297 million in fiscal 2024.
Management refers to Platform revenue as "Subscription and transaction platform revenue"
in some tables; treat that label as the same Platform segment revenue metric.
""",
)
write_fixture_text(
data_dir,
"10-k-note-geography.txt",
"""
UNITED STATES
SECURITIES AND EXCHANGE COMMISSION
Washington, D.C. 20549
FORM 10-K
ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934
For the fiscal year ended December 31, 2025
HelioCart, Inc.
PART II
Item 8. Financial Statements and Supplementary Data
Note 5. Revenue by geography
Americas revenue was $764 million in fiscal 2025, EMEA revenue was $343 million,
and APAC revenue was $177 million. Those regional line items reconcile to the
company-wide revenue figure disclosed in MD&A.
""",
)
write_fixture_text(
data_dir,
"10-k-note-balance-sheet.txt",
"""
UNITED STATES
SECURITIES AND EXCHANGE COMMISSION
Washington, D.C. 20549
FORM 10-K
ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(d) OF THE SECURITIES EXCHANGE ACT OF 1934
For the fiscal year ended December 31, 2025
HelioCart, Inc.
PART II
Item 8. Financial Statements and Supplementary Data
Note 7. Selected balance sheet metrics
Cash and cash equivalents were $422 million as of December 31, 2025, compared with
$351 million as of December 31, 2024. Deferred revenue was $402 million as of
December 31, 2025, compared with $337 million as of December 31, 2024.
""",
)
write_financial_pdf(
data_dir / "10-k-statements-of-operations.pdf",
"Consolidated Statements of Operations",
[
"The table below presents annual operating results for fiscal 2025 and fiscal 2024.",
"Revenue and net revenue refer to the same top-line measure for this synthetic filing.",
],
[
["Metric", "FY2025", "FY2024"],
["Net revenue", "1,284", "1,008"],
["Gross profit", "917", "687"],
["Operating income", "186", "118"],
],
)
write_financial_pdf(
data_dir / "10-k-balance-sheets.pdf",
"Consolidated Balance Sheets",
[
"The table below presents selected balance sheet amounts as of December 31, 2025 and 2024.",
"Amounts are shown in USD millions.",
],
[
["Metric", "2025", "2024"],
["Cash and cash equivalents", "422", "351"],
["Accounts receivable", "211", "187"],
["Deferred revenue", "402", "337"],
],
)
write_financial_pdf(
data_dir / "10-k-statements-of-cash-flows.pdf",
"Consolidated Statements of Cash Flows",
[
"The table below presents selected annual cash flow metrics for fiscal 2025 and 2024.",
"Net cash provided by operating activities is also described as operating cash flow in MD&A.",
],
[
["Metric", "FY2025", "FY2024"],
["Net cash provided by operating activities", "248", "192"],
["Capital expenditures", "86", "73"],
["Free cash flow", "162", "119"],
],
)
if __name__ == "__main__":
main()
@@ -0,0 +1,44 @@
# Dataroom metric extract
## Goal
Extract financial metrics from a synthetic 10-K packet, write the resulting table as CSV or JSONL, then validate the generated artifact with a deterministic eval script.
The packet uses synthetic company data, but the source docs are formatted as annual-report excerpts with 10-K `Part II, Item 7` MD&A sections and `Part II, Item 8` financial statement sections.
## Why this is valuable
This demo shows a single-pass structured extraction pattern: a sandbox agent reads messy filing documents and emits typed financial rows, then a separate host-side eval script checks the artifact. The wrapper does not repair or deduplicate model output after the fact; if the row set is wrong, `evals.py` fails and you iterate on the prompt or fixture data instead.
## Setup
Run the fixture generator and then the Unix-local example from the repository root. Set `OPENAI_API_KEY` in your shell environment before running the example.
```bash
uv run python examples/sandbox/tutorials/data/dataroom/setup.py
uv run python examples/sandbox/tutorials/dataroom_metric_extract/main.py --output-format csv
uv run python examples/sandbox/tutorials/dataroom_metric_extract/evals.py --artifact-path examples/sandbox/tutorials/dataroom_metric_extract/output/financial_metrics.csv
```
After the initial extraction, the demo keeps the sandbox session open for Rich-rendered follow-up prompts before writing the final artifact. Pass `--no-interactive` for a one-shot run.
To run extraction in Docker, build the shared tutorial image once and add `--docker`
to `main.py`:
```bash
docker build --tag sandbox-tutorials:latest examples/sandbox/tutorials
uv run python examples/sandbox/tutorials/dataroom_metric_extract/main.py --docker --output-format csv
uv run python examples/sandbox/tutorials/dataroom_metric_extract/evals.py --artifact-path examples/sandbox/tutorials/dataroom_metric_extract/output/financial_metrics.csv
```
## Expected artifacts
- `output/financial_metrics.csv`
- `output/financial_metrics.jsonl`
## Demo shape
- Inputs: the shared SEC fixture packet in `examples/sandbox/tutorials/data/dataroom/`.
- Runtime primitives: sandbox-local bash/file search plus typed agent outputs.
- Workflow: a fixed single-step pipeline where the sandbox extractor emits `FinancialMetricBatch`; no handoff is needed. `main.py` writes the selected artifact format, and `evals.py` validates that artifact in a separate step.
- Scratch space: the extractor may use `scratchpad/` for interim notes, but only the selected `output/financial_metrics.*` artifact is part of the final contract.
@@ -0,0 +1,315 @@
from __future__ import annotations
import argparse
import csv
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, TypeAlias
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parent))
if TYPE_CHECKING or __package__:
from .schemas import FinancialMetric, FinancialMetricBatch
else:
from schemas import FinancialMetric, FinancialMetricBatch
MetricKey: TypeAlias = tuple[str, str, str, str | None]
EXPECTED_SOURCE_METADATA: dict[str, str] = {
"data/10-k-mdna-overview.txt": (
"Part II, Item 7. Management's Discussion and Analysis of Financial Condition and "
"Results of Operations"
),
"data/10-k-mdna-liquidity.txt": (
"Part II, Item 7. Management's Discussion and Analysis of Financial Condition and "
"Results of Operations"
),
"data/10-k-note-segments.txt": ("Part II, Item 8. Financial Statements and Supplementary Data"),
"data/10-k-note-geography.txt": (
"Part II, Item 8. Financial Statements and Supplementary Data"
),
"data/10-k-note-balance-sheet.txt": (
"Part II, Item 8. Financial Statements and Supplementary Data"
),
"data/10-k-statements-of-operations.pdf": (
"Part II, Item 8. Financial Statements and Supplementary Data"
),
"data/10-k-balance-sheets.pdf": (
"Part II, Item 8. Financial Statements and Supplementary Data"
),
"data/10-k-statements-of-cash-flows.pdf": (
"Part II, Item 8. Financial Statements and Supplementary Data"
),
}
EXPECTED_ROWS: dict[MetricKey, tuple[float, str]] = {
("data/10-k-mdna-overview.txt", "Revenue", "FY2025", None): (1284.0, "USD millions"),
("data/10-k-mdna-overview.txt", "Revenue", "FY2024", None): (1008.0, "USD millions"),
("data/10-k-mdna-overview.txt", "Gross margin", "FY2025", None): (71.4, "percent"),
("data/10-k-mdna-overview.txt", "Gross margin", "FY2024", None): (68.2, "percent"),
("data/10-k-mdna-overview.txt", "Operating income", "FY2025", None): (186.0, "USD millions"),
("data/10-k-mdna-overview.txt", "Operating income", "FY2024", None): (118.0, "USD millions"),
(
"data/10-k-mdna-liquidity.txt",
"Net cash provided by operating activities",
"FY2025",
None,
): (248.0, "USD millions"),
(
"data/10-k-mdna-liquidity.txt",
"Net cash provided by operating activities",
"FY2024",
None,
): (192.0, "USD millions"),
("data/10-k-mdna-liquidity.txt", "Capital expenditures", "FY2025", None): (
86.0,
"USD millions",
),
("data/10-k-mdna-liquidity.txt", "Capital expenditures", "FY2024", None): (
73.0,
"USD millions",
),
("data/10-k-mdna-liquidity.txt", "Free cash flow", "FY2025", None): (
162.0,
"USD millions",
),
("data/10-k-mdna-liquidity.txt", "Free cash flow", "FY2024", None): (
119.0,
"USD millions",
),
("data/10-k-note-segments.txt", "Platform segment revenue", "FY2025", "Platform"): (
942.0,
"USD millions",
),
("data/10-k-note-segments.txt", "Platform segment revenue", "FY2024", "Platform"): (
711.0,
"USD millions",
),
("data/10-k-note-segments.txt", "Services segment revenue", "FY2025", "Services"): (
342.0,
"USD millions",
),
("data/10-k-note-segments.txt", "Services segment revenue", "FY2024", "Services"): (
297.0,
"USD millions",
),
("data/10-k-note-geography.txt", "Americas revenue", "FY2025", "Americas"): (
764.0,
"USD millions",
),
("data/10-k-note-geography.txt", "EMEA revenue", "FY2025", "EMEA"): (
343.0,
"USD millions",
),
("data/10-k-note-geography.txt", "APAC revenue", "FY2025", "APAC"): (
177.0,
"USD millions",
),
(
"data/10-k-note-balance-sheet.txt",
"Cash and cash equivalents",
"2025-12-31",
None,
): (422.0, "USD millions"),
(
"data/10-k-note-balance-sheet.txt",
"Cash and cash equivalents",
"2024-12-31",
None,
): (351.0, "USD millions"),
("data/10-k-note-balance-sheet.txt", "Deferred revenue", "2025-12-31", None): (
402.0,
"USD millions",
),
("data/10-k-note-balance-sheet.txt", "Deferred revenue", "2024-12-31", None): (
337.0,
"USD millions",
),
("data/10-k-statements-of-operations.pdf", "Net revenue", "FY2025", None): (
1284.0,
"USD millions",
),
("data/10-k-statements-of-operations.pdf", "Net revenue", "FY2024", None): (
1008.0,
"USD millions",
),
("data/10-k-statements-of-operations.pdf", "Gross profit", "FY2025", None): (
917.0,
"USD millions",
),
("data/10-k-statements-of-operations.pdf", "Gross profit", "FY2024", None): (
687.0,
"USD millions",
),
("data/10-k-statements-of-operations.pdf", "Operating income", "FY2025", None): (
186.0,
"USD millions",
),
("data/10-k-statements-of-operations.pdf", "Operating income", "FY2024", None): (
118.0,
"USD millions",
),
(
"data/10-k-balance-sheets.pdf",
"Cash and cash equivalents",
"2025-12-31",
None,
): (422.0, "USD millions"),
(
"data/10-k-balance-sheets.pdf",
"Cash and cash equivalents",
"2024-12-31",
None,
): (351.0, "USD millions"),
("data/10-k-balance-sheets.pdf", "Accounts receivable", "2025-12-31", None): (
211.0,
"USD millions",
),
("data/10-k-balance-sheets.pdf", "Accounts receivable", "2024-12-31", None): (
187.0,
"USD millions",
),
("data/10-k-balance-sheets.pdf", "Deferred revenue", "2025-12-31", None): (
402.0,
"USD millions",
),
("data/10-k-balance-sheets.pdf", "Deferred revenue", "2024-12-31", None): (
337.0,
"USD millions",
),
(
"data/10-k-statements-of-cash-flows.pdf",
"Net cash provided by operating activities",
"FY2025",
None,
): (248.0, "USD millions"),
(
"data/10-k-statements-of-cash-flows.pdf",
"Net cash provided by operating activities",
"FY2024",
None,
): (192.0, "USD millions"),
("data/10-k-statements-of-cash-flows.pdf", "Capital expenditures", "FY2025", None): (
86.0,
"USD millions",
),
("data/10-k-statements-of-cash-flows.pdf", "Capital expenditures", "FY2024", None): (
73.0,
"USD millions",
),
("data/10-k-statements-of-cash-flows.pdf", "Free cash flow", "FY2025", None): (
162.0,
"USD millions",
),
("data/10-k-statements-of-cash-flows.pdf", "Free cash flow", "FY2024", None): (
119.0,
"USD millions",
),
}
@dataclass(frozen=True)
class EvalSummary:
row_count: int
def load_metrics(artifact_path: Path) -> FinancialMetricBatch:
if artifact_path.suffix == ".jsonl":
metrics = [
FinancialMetric.model_validate_json(line)
for line in artifact_path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
return FinancialMetricBatch(metrics=metrics)
if artifact_path.suffix == ".csv":
with artifact_path.open(encoding="utf-8", newline="") as input_file:
reader = csv.DictReader(input_file)
metrics = []
for row in reader:
row["segment"] = row["segment"] or None
row["value"] = float(row["value"])
metrics.append(FinancialMetric.model_validate(row))
return FinancialMetricBatch(metrics=metrics)
raise ValueError(f"Unsupported artifact type: {artifact_path}")
def validate_outputs(metrics: FinancialMetricBatch) -> EvalSummary:
rows = metrics.metrics
duplicate_keys: list[MetricKey] = []
seen_keys: set[MetricKey] = set()
rows_by_key: dict[MetricKey, FinancialMetric] = {
(
row.source_file.strip(),
row.metric_name.strip(),
row.fiscal_period,
row.segment.strip() if row.segment else None,
): row
for row in rows
}
for row in rows:
row_key = (
row.source_file.strip(),
row.metric_name.strip(),
row.fiscal_period,
row.segment.strip() if row.segment else None,
)
if row_key in seen_keys:
duplicate_keys.append(row_key)
seen_keys.add(row_key)
if duplicate_keys:
raise AssertionError(f"Duplicate metric rows found: {sorted(set(duplicate_keys))}.")
if len(rows) != len(EXPECTED_ROWS):
raise AssertionError(
f"Expected exactly {len(EXPECTED_ROWS)} metric rows, found {len(rows)}."
)
for source_file, expected_section in EXPECTED_SOURCE_METADATA.items():
source_rows = [row for row in rows if row.source_file.strip() == source_file]
if not source_rows:
raise AssertionError(f"Missing rows from {source_file}.")
bad_sections = {
row.filing_section for row in source_rows if row.filing_section != expected_section
}
if bad_sections:
raise AssertionError(
f"{source_file} filing_section mismatch. Expected {expected_section}, found {bad_sections}."
)
missing_rows = [
key
for key, (expected_value, expected_unit) in EXPECTED_ROWS.items()
if key not in rows_by_key
or rows_by_key[key].value != expected_value
or rows_by_key[key].unit != expected_unit
]
if missing_rows:
observed = sorted(rows_by_key)
raise AssertionError(
f"Missing or mismatched expected metric rows: {missing_rows}. Observed keys: {observed}."
)
unexpected_rows = sorted(set(rows_by_key) - set(EXPECTED_ROWS))
if unexpected_rows:
raise AssertionError(f"Unexpected metric rows found: {unexpected_rows}.")
return EvalSummary(row_count=len(rows))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--artifact-path",
default=str(Path(__file__).resolve().parent / "output" / "financial_metrics.jsonl"),
help="Path to the generated JSONL or CSV artifact.",
)
args = parser.parse_args()
summary = validate_outputs(load_metrics(Path(args.artifact_path)))
print(f"Eval checks passed for {summary.row_count} metric row(s).")
@@ -0,0 +1,274 @@
"""
Extract structured financial metrics from a synthetic 10-K dataroom and write a
JSONL or CSV artifact.
"""
import argparse
import asyncio
import csv
import json
import sys
from collections.abc import Sequence
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING, Literal, cast
from openai.types.shared.reasoning import Reasoning
from pydantic import BaseModel
from agents import ModelSettings, Runner, RunResultStreaming, TResponseInputItem
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File, LocalDir
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
if TYPE_CHECKING or __package__:
from .schemas import FinancialMetric, FinancialMetricBatch
else:
from schemas import FinancialMetric, FinancialMetricBatch
from examples.sandbox.tutorials.misc import (
DEFAULT_SANDBOX_IMAGE,
console,
create_sandbox_client_and_session,
load_env_defaults,
print_event,
run_interactive_loop,
)
DEMO_DIR = Path(__file__).resolve().parent
DATAROOM_DATA_DIR = DEMO_DIR.parent / "data" / "dataroom"
DEFAULT_QUESTION = (
"Extract revenue, gross margin, operating income, cash flow, balance-sheet, segment, "
"and geography metrics from the 10-K packet into one row per metric-period-source. "
"For each table, include every explicit line item in the source, even when it is "
"similar to a line item in another source."
)
AGENTS_MD = dedent(
"""\
# AGENTS.md
Extract structured financial metrics from the synthetic 10-K packet under `data/`.
## Output (one row per metric-value occurrence)
Required fields: `source_file`, `filing_section`, `metric_name`, `fiscal_period`, `value`,
`unit` (`USD millions` or `percent`).
Optional field: `segment` (segment/geography if explicitly stated, else null).
## Rules
- Review all `.txt` and `.pdf` under `data/` (these PDFs contain searchable text).
- Use shell tools (`rg`, `sed`) for discovery/inspection; do not run Python from the sandbox shell.
- Do not read `data/setup.py`.
- Emit a separate row for each metric-period pair in each source file (do not dedupe across files).
- For tables, include every explicit table line item in that source. For example, the
statements-of-operations PDF has separate Net revenue, Gross profit, and Operating income rows.
- Only extract explicit source line items / table rows. Do not invent rollups or “cleaned up” metrics.
- Do not treat Gross profit and Gross margin as duplicates; they are distinct source metrics.
- Preserve labels as written (e.g., `Revenue` vs `Net revenue`).
## Completeness checklist
Before final output, verify the batch has exactly 41 rows from these source-level line items:
- `data/10-k-mdna-overview.txt`: Revenue, Gross margin, and Operating income for FY2025 and FY2024.
- `data/10-k-mdna-liquidity.txt`: Net cash provided by operating activities, Capital expenditures,
and Free cash flow for FY2025 and FY2024.
- `data/10-k-note-segments.txt`: Platform segment revenue and Services segment revenue for FY2025
and FY2024, with the matching segment names.
- `data/10-k-note-geography.txt`: Americas revenue, EMEA revenue, and APAC revenue for FY2025, with
the matching geography names as segments.
- `data/10-k-note-balance-sheet.txt`: Cash and cash equivalents and Deferred revenue for 2025-12-31
and 2024-12-31.
- `data/10-k-statements-of-operations.pdf`: Net revenue, Gross profit, and Operating income for
FY2025 and FY2024.
- `data/10-k-balance-sheets.pdf`: Cash and cash equivalents, Accounts receivable, and Deferred revenue
for 2025-12-31 and 2024-12-31.
- `data/10-k-statements-of-cash-flows.pdf`: Net cash provided by operating activities, Capital
expenditures, and Free cash flow for FY2025 and FY2024.
Return the structured rows directly in your final output.
"""
)
async def print_streamed_result(result: RunResultStreaming) -> BaseModel:
async for event in result.stream_events():
print_event(event)
if result.final_output is None:
raise RuntimeError("10-K Metric Extractor returned no structured metric output.")
print_event(str(result.final_output).strip())
return cast(BaseModel, result.final_output)
def write_jsonl(path: Path, metrics: Sequence[BaseModel]) -> None:
path.write_text(
"\n".join(metric.model_dump_json() for metric in metrics) + "\n",
encoding="utf-8",
)
def write_csv(path: Path, metrics: list[FinancialMetric]) -> None:
with path.open("w", encoding="utf-8", newline="") as output_file:
writer = csv.DictWriter(
output_file,
fieldnames=[
"source_file",
"filing_section",
"metric_name",
"fiscal_period",
"value",
"unit",
"segment",
],
)
writer.writeheader()
for metric in metrics:
writer.writerow(json.loads(metric.model_dump_json()))
def write_final_artifact(
output_dir: Path,
output_format: Literal["jsonl", "csv"],
metrics: list[FinancialMetric],
) -> Path:
output_path = output_dir / f"financial_metrics.{output_format}"
if output_format == "jsonl":
write_jsonl(output_path, metrics)
else:
write_csv(output_path, metrics)
return output_path
async def main(
model: str,
question: str,
output_format: Literal["jsonl", "csv"],
use_docker: bool,
image: str,
no_interactive: bool,
) -> None:
if not (DATAROOM_DATA_DIR / "10-k-mdna-overview.txt").exists():
raise SystemExit(
"Run `uv run python examples/sandbox/tutorials/data/dataroom/setup.py` "
"before starting this demo."
)
manifest = Manifest(
entries={
"AGENTS.md": File(content=AGENTS_MD.encode("utf-8")),
"data": LocalDir(src=DATAROOM_DATA_DIR),
}
)
agent = SandboxAgent(
name="10-K Metric Extractor",
model=model,
instructions=AGENTS_MD,
capabilities=[Shell()],
model_settings=ModelSettings(
reasoning=Reasoning(effort="high"),
tool_choice="required",
),
output_type=FinancialMetricBatch,
)
client, sandbox = await create_sandbox_client_and_session(
manifest=manifest,
use_docker=use_docker,
image=image,
)
try:
async with sandbox:
extracted_metrics: FinancialMetricBatch | None = None
async def run_turn(
conversation: list[TResponseInputItem],
) -> list[TResponseInputItem]:
nonlocal extracted_metrics
result = Runner.run_streamed(
agent,
conversation,
max_turns=25,
run_config=RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
tracing_disabled=True,
workflow_name="Dataroom extraction example",
),
)
extracted_metrics = cast(FinancialMetricBatch, await print_streamed_result(result))
return result.to_input_list()
conversation: list[TResponseInputItem] = [{"role": "user", "content": question}]
conversation = await run_turn(conversation)
await run_interactive_loop(
conversation=conversation,
no_interactive=no_interactive,
run_turn=run_turn,
)
finally:
await client.delete(sandbox)
if extracted_metrics is None:
raise RuntimeError("10-K Metric Extractor returned no structured metric output.")
output_dir = DEMO_DIR / "output"
output_dir.mkdir(exist_ok=True)
artifact_path = write_final_artifact(output_dir, output_format, extracted_metrics.metrics)
console.print(
f"[green]Wrote {len(extracted_metrics.metrics)} metric row(s) to {artifact_path}[/green]"
)
if __name__ == "__main__":
load_env_defaults(DEMO_DIR / ".env")
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="gpt-5.4-mini",
help="Model name to use.",
)
parser.add_argument(
"--question",
default=DEFAULT_QUESTION,
help="Prompt to send to the agent.",
)
parser.add_argument(
"--output-format",
choices=("jsonl", "csv"),
default="csv",
help="Artifact format.",
)
parser.add_argument(
"--docker",
action="store_true",
help="Run this example in Docker instead of Unix-local.",
)
parser.add_argument(
"--image",
default=DEFAULT_SANDBOX_IMAGE,
help="Docker image to use when --docker is set.",
)
parser.add_argument(
"--no-interactive",
action="store_true",
help="Run the scripted turn and skip follow-up terminal input.",
)
args = parser.parse_args()
asyncio.run(
main(
args.model,
args.question,
args.output_format,
args.docker,
args.image,
args.no_interactive,
)
)
@@ -0,0 +1,33 @@
from typing import Literal
from pydantic import BaseModel, Field
class FinancialMetric(BaseModel):
source_file: str = Field(
description="Workspace-relative source path under data/, such as data/10-k-mdna-overview.txt."
)
filing_section: Literal[
"Part II, Item 7. Management's Discussion and Analysis of Financial Condition and Results of Operations",
"Part II, Item 8. Financial Statements and Supplementary Data",
] = Field(description="Normalized 10-K filing section for the source document.")
metric_name: str = Field(
description="Metric label exactly as written in the source document or table."
)
fiscal_period: Literal["FY2025", "FY2024", "2025-12-31", "2024-12-31"] = Field(
description="Annual period label for statement rows, or balance-sheet date for point-in-time rows."
)
value: float = Field(description="Numeric value from the source row.")
unit: Literal["USD millions", "percent"] = Field(
description="Unit for `value`; use USD millions for dollar amounts and percent for margins."
)
segment: str | None = Field(
default=None,
description="Reportable segment or geography when the row is segment-specific, otherwise null.",
)
class FinancialMetricBatch(BaseModel):
metrics: list[FinancialMetric] = Field(
description="One row per metric-period pair extracted from each source document."
)
@@ -0,0 +1,44 @@
# Dataroom Q&A
## Goal
Answer grounded financial questions over a synthetic 10-K packet.
The packet uses synthetic company data, but the documents are shaped like annual report excerpts: MD&A text uses 10-K `Part II, Item 7`, while statement PDFs and footnote text use `Part II, Item 8`.
## Why this is valuable
This demo shows a retrieval-first agent pattern over a bounded financial corpus where each metric and explanation should stay tied to source files.
## Setup
Run the fixture generator and then the Unix-local example from the repository root. Set `OPENAI_API_KEY` in your shell environment before running the example.
```bash
uv run python examples/sandbox/tutorials/data/dataroom/setup.py
uv run python examples/sandbox/tutorials/dataroom_qa/main.py
```
After the initial answer, the demo keeps the sandbox session open for Rich-rendered follow-up prompts. Pass `--no-interactive` for a one-shot run.
To run the same manifest in Docker, build the shared tutorial image once and pass
`--docker`:
```bash
docker build --tag sandbox-tutorials:latest examples/sandbox/tutorials
uv run python examples/sandbox/tutorials/dataroom_qa/main.py --docker
```
## Expected artifacts
- A direct cited answer in the streamed agent response.
- Citations use `[n](data/source-file.txt:line:14)` for text excerpts and `[n](data/source-file.pdf:page:1)` for the one-page synthetic PDFs.
## Demo shape
- Inputs: 5 synthetic filing text docs and 3 simple filing PDFs from `examples/sandbox/tutorials/data/dataroom/`.
- Runtime primitives: sandbox-local bash/file search.
## How instructions are loaded
At startup, the wrapper loads this folder's `AGENTS.md` into the agent instructions and builds a hard-coded manifest that maps the shared SEC packet from `examples/sandbox/tutorials/data/dataroom/` into the sandbox as `data/...`.
@@ -0,0 +1 @@
@@ -0,0 +1,146 @@
"""
Answer questions over a synthetic dataroom.
"""
import argparse
import asyncio
import sys
from pathlib import Path
from textwrap import dedent
from agents import Runner, RunResultStreaming, TResponseInputItem
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File, LocalDir
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
from examples.sandbox.tutorials.misc import (
DEFAULT_SANDBOX_IMAGE,
create_sandbox_client_and_session,
load_env_defaults,
print_event,
run_interactive_loop,
)
DEMO_DIR = Path(__file__).resolve().parent
DATAROOM_DATA_DIR = DEMO_DIR.parent / "data" / "dataroom"
DEFAULT_QUESTION = (
"How did revenue, gross margin, operating income, and operating cash flow change in "
"FY2025 versus FY2024, and which segment contributed the most revenue?"
)
AGENTS_MD = dedent(
"""\
# AGENTS.md
Answer the user's financial question using only the synthetic 10-K packet in `data/`.
## Evidence & citations
- Cite every material claim with markdown links in these formats (no bare links):
- `[1](data/source-file.txt:line:14)` for text sources
- `[2](data/source-file.pdf:page:1)` for PDF sources (each synthetic PDF is one page)
- Use `rg` and `sed` to find and quote exact evidence; do not use `data/setup.py`.
Keep the final answer direct and finance-oriented.
"""
)
async def print_streamed_result(result: RunResultStreaming) -> list[TResponseInputItem]:
async for event in result.stream_events():
print_event(event)
print_event(str(result.final_output).strip())
return result.to_input_list()
async def main(
model: str, question: str, use_docker: bool, image: str, no_interactive: bool
) -> None:
if not (DATAROOM_DATA_DIR / "10-k-mdna-overview.txt").exists():
raise SystemExit(
"Run `uv run python examples/sandbox/tutorials/data/dataroom/setup.py` "
"before starting this demo."
)
manifest = Manifest(
entries={
"AGENTS.md": File(content=AGENTS_MD.encode("utf-8")),
"data": LocalDir(src=DATAROOM_DATA_DIR),
}
)
agent = SandboxAgent(
name="Dataroom Analyst",
model=model,
instructions=AGENTS_MD,
capabilities=[Shell()],
)
client, sandbox = await create_sandbox_client_and_session(
manifest=manifest,
use_docker=use_docker,
image=image,
)
try:
async with sandbox:
async def run_turn(
conversation: list[TResponseInputItem],
) -> list[TResponseInputItem]:
result = Runner.run_streamed(
agent,
conversation,
max_turns=20,
run_config=RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
tracing_disabled=True,
workflow_name="Dataroom Q&A example",
),
)
return await print_streamed_result(result)
conversation: list[TResponseInputItem] = [{"role": "user", "content": question}]
conversation = await run_turn(conversation)
await run_interactive_loop(
conversation=conversation,
no_interactive=no_interactive,
run_turn=run_turn,
)
finally:
await client.delete(sandbox)
if __name__ == "__main__":
load_env_defaults(DEMO_DIR / ".env")
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="gpt-5.4-mini",
help="Model name to use.",
)
parser.add_argument(
"--question",
default=DEFAULT_QUESTION,
help="Prompt to send to the agent.",
)
parser.add_argument(
"--docker",
action="store_true",
help="Run this example in Docker instead of Unix-local.",
)
parser.add_argument(
"--image",
default=DEFAULT_SANDBOX_IMAGE,
help="Docker image to use when --docker is set.",
)
parser.add_argument(
"--no-interactive",
action="store_true",
help="Run the scripted turn and skip follow-up terminal input.",
)
args = parser.parse_args()
asyncio.run(main(args.model, args.question, args.docker, args.image, args.no_interactive))
+397
View File
@@ -0,0 +1,397 @@
import json
import os
import subprocess
from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import Any, Literal, TypeAlias, cast
from openai.types.responses import (
ResponseComputerToolCall,
ResponseFileSearchToolCall,
ResponseFunctionToolCall,
ResponseFunctionWebSearch,
)
from openai.types.responses.response_code_interpreter_tool_call import (
ResponseCodeInterpreterToolCall,
)
from openai.types.responses.response_output_item import ImageGenerationCall, LocalShellCall, McpCall
from pydantic import BaseModel, Field
from rich import box
from rich.console import Console, Group
from rich.markdown import Markdown
from rich.panel import Panel
from rich.pretty import Pretty
from rich.prompt import Prompt
from rich.syntax import Syntax
from rich.text import Text
from typing_extensions import TypedDict
from agents import ItemHelpers, TResponseInputItem
from agents.items import (
CompactionItem,
HandoffCallItem,
HandoffOutputItem,
MCPApprovalRequestItem,
MCPApprovalResponseItem,
MCPListToolsItem,
MessageOutputItem,
ReasoningItem,
ToolApprovalItem,
ToolCallItem,
ToolCallOutputItem,
ToolSearchCallItem,
ToolSearchOutputItem,
)
from agents.sandbox import Manifest
from agents.sandbox.sandboxes.docker import DockerSandboxClient, DockerSandboxClientOptions
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from agents.sandbox.session import BaseSandboxClient, SandboxSession
from agents.stream_events import (
AgentUpdatedStreamEvent,
RawResponsesStreamEvent,
StreamEvent,
)
from examples.auto_mode import input_with_fallback, is_auto_mode
DEFAULT_SANDBOX_IMAGE = "sandbox-tutorials:latest"
console = Console()
PanelBody = Group | Pretty | Text
PrintableEvent: TypeAlias = StreamEvent | str
SandboxClient: TypeAlias = BaseSandboxClient[Any]
InteractiveTurnRunner: TypeAlias = Callable[
[list[TResponseInputItem]], Awaitable[list[TResponseInputItem]]
]
class ApplyPatchOperationPayload(TypedDict):
path: str
type: Literal["create_file", "update_file", "delete_file"]
diff: str
class ApplyPatchCallPayload(TypedDict):
type: Literal["apply_patch_call"]
call_id: str
operation: ApplyPatchOperationPayload
class Question(BaseModel):
query: str = Field(description="User-facing question to ask.")
options: list[str] = Field(
default_factory=list,
description="Suggested answer options. The UI always adds a custom free-text choice.",
)
class QuestionAnswer(BaseModel):
question: str = Field(description="The question that was asked.")
answer: str = Field(description="The user's selected or free-text answer.")
def load_env_defaults(env_path: Path) -> None:
if not env_path.exists():
return
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
normalized_key = key.strip()
normalized_value = value.strip().strip('"').strip("'")
if normalized_key:
os.environ.setdefault(normalized_key, normalized_value)
async def create_sandbox_client_and_session(
*,
manifest: Manifest,
use_docker: bool,
image: str = DEFAULT_SANDBOX_IMAGE,
) -> tuple[SandboxClient, SandboxSession]:
if use_docker:
try:
from docker import from_env as docker_from_env # type: ignore[import-untyped]
except ImportError as exc:
raise SystemExit(
"Docker-backed runs require the Docker SDK. Install repo dependencies with `make sync`."
) from exc
client: SandboxClient = DockerSandboxClient(
docker_from_env(environment=build_docker_environment())
)
sandbox = await client.create(
manifest=manifest,
options=DockerSandboxClientOptions(image=image),
)
return client, sandbox
client = UnixLocalSandboxClient()
sandbox = await client.create(manifest=manifest)
return client, sandbox
def build_docker_environment() -> dict[str, str]:
environment = os.environ.copy()
if environment.get("DOCKER_HOST") or environment.get("DOCKER_CONTEXT"):
return environment
# Respect whichever Docker context the CLI is currently using, including Docker Desktop
# and Colima, without taking a direct dependency on a specific daemon provider.
try:
result = subprocess.run(
["docker", "context", "inspect", "--format", "{{json .Endpoints.docker.Host}}"],
capture_output=True,
check=True,
text=True,
)
docker_host = json.loads(result.stdout.strip() or "null")
except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
return environment
if isinstance(docker_host, str) and docker_host:
environment["DOCKER_HOST"] = docker_host
return environment
def prompt_with_fallback(prompt: str, fallback: str) -> str:
if is_auto_mode():
return input_with_fallback(prompt, fallback).strip()
try:
return Prompt.ask(prompt).strip()
except (EOFError, KeyboardInterrupt):
return fallback
def ask_user_questions(questions: list[Question]) -> list[QuestionAnswer]:
answers: list[QuestionAnswer] = []
for question_index, question in enumerate(questions, start=1):
suggested_options = [option.strip() for option in question.options if option.strip()]
custom_choice_index = len(suggested_options) + 1
options_text = Text.from_markup(
"\n".join(
[
*(
f"[cyan]{index}.[/cyan] {option}"
for index, option in enumerate(
suggested_options,
start=1,
)
),
f"[cyan]{custom_choice_index}.[/cyan] Use your own text",
]
)
)
console.print(
Panel(
Group(
Text(question.query),
options_text,
),
title=f"Question {question_index}",
border_style="magenta",
box=box.ROUNDED,
expand=False,
)
)
while True:
choice = prompt_with_fallback(
f"[bold cyan]Select[/bold cyan] 1-{custom_choice_index}",
"1" if suggested_options else str(custom_choice_index),
)
if choice.isdigit() and 1 <= int(choice) <= len(suggested_options):
answer = suggested_options[int(choice) - 1]
break
if choice.isdigit() and int(choice) == custom_choice_index:
answer = prompt_with_fallback(
"[bold cyan]Your answer[/bold cyan]",
suggested_options[0] if suggested_options else "Use a conservative assumption.",
)
if answer:
break
continue
if choice and not choice.isdigit():
answer = choice
break
console.print(
f"[red]Please enter a number from 1 to {custom_choice_index}, or custom text.[/red]"
)
answers.append(QuestionAnswer(question=question.query, answer=answer))
console.print(
Panel(
Pretty([answer.model_dump(mode="json") for answer in answers], expand_all=True),
title="Question answers",
border_style="magenta",
box=box.ROUNDED,
expand=False,
)
)
return answers
async def run_interactive_loop(
*,
conversation: list[TResponseInputItem],
no_interactive: bool,
run_turn: InteractiveTurnRunner,
) -> list[TResponseInputItem]:
if no_interactive or is_auto_mode():
return conversation
console.print("[dim]Enter follow-up prompts. Press Ctrl-D or Ctrl-C to finish.[/dim]")
while True:
try:
next_message = Prompt.ask("[bold cyan]user[/bold cyan]").strip()
except (EOFError, KeyboardInterrupt):
break
if not next_message:
continue
conversation.append({"role": "user", "content": next_message})
conversation = await run_turn(conversation)
return conversation
def print_event(event: PrintableEvent) -> None:
if isinstance(event, str):
console.print()
console.rule("[bold green]Final output[/bold green]", style="green")
console.print(
Panel(
Markdown(event or "_No final output returned._"),
border_style="green",
box=box.ROUNDED,
expand=False,
)
)
return
if isinstance(event, AgentUpdatedStreamEvent):
console.print(
Panel(
Pretty(event.new_agent.name, expand_all=True),
title="Agent updated",
border_style="cyan",
box=box.ROUNDED,
expand=False,
)
)
return
if isinstance(event, RawResponsesStreamEvent):
return
body: PanelBody
match event.item:
case ReasoningItem() as item:
body = Pretty(item, expand_all=True)
title = f"Reasoning item: {event.name.replace('_', ' ')}"
case ToolCallItem() as item:
tool_name = "tool"
body = Pretty(item.raw_item, expand_all=True)
match item.raw_item:
case ResponseFunctionToolCall() as raw_item:
tool_name = raw_item.name
payload = json.loads(raw_item.arguments) if raw_item.arguments else {}
if tool_name == "exec_command":
command = payload["cmd"]
if "\\n" in command and "\n" not in command:
command = command.replace("\\n", "\n")
body = Group(
Pretty(
{key: value for key, value in payload.items() if key != "cmd"},
expand_all=True,
),
Syntax(command, "bash", theme="ansi_dark", word_wrap=True),
)
else:
body = Pretty(payload, expand_all=True)
case ResponseComputerToolCall() as raw_item:
tool_name = "computer"
body = Pretty(raw_item, expand_all=True)
case ResponseFileSearchToolCall() as raw_item:
tool_name = "file_search"
body = Pretty(raw_item, expand_all=True)
case ResponseFunctionWebSearch() as raw_item:
tool_name = "web_search"
body = Pretty(raw_item, expand_all=True)
case McpCall() as raw_item:
tool_name = "mcp"
body = Pretty(raw_item, expand_all=True)
case ResponseCodeInterpreterToolCall() as raw_item:
tool_name = "code_interpreter"
body = Pretty(raw_item, expand_all=True)
case ImageGenerationCall() as raw_item:
tool_name = "image_generation"
body = Pretty(raw_item, expand_all=True)
case LocalShellCall() as raw_item:
tool_name = "local_shell"
body = Pretty(raw_item, expand_all=True)
case dict() as raw_item:
tool_name = "apply_patch"
payload = cast(ApplyPatchCallPayload, raw_item)["operation"]
body = Group(
Pretty(
{
"path": payload["path"],
"type": payload["type"],
},
expand_all=True,
),
Syntax(payload["diff"], "diff", theme="ansi_dark", word_wrap=True),
)
title = f"Tool call: {tool_name}"
case ToolCallOutputItem() as item:
body = Text(item.output) if isinstance(item.output, str) else Pretty(item.output)
title = "Tool output"
case MessageOutputItem() as item:
output = ItemHelpers.text_message_output(item)
body = Text(output) if isinstance(output, str) else Pretty(output, expand_all=True)
title = "Message output"
case ToolSearchCallItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "Tool search call"
case ToolSearchOutputItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "Tool search output"
case HandoffCallItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "Handoff call"
case HandoffOutputItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "Handoff output"
case MCPListToolsItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "MCP list tools"
case MCPApprovalRequestItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "MCP approval request"
case MCPApprovalResponseItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "MCP approval response"
case CompactionItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "Compaction"
case ToolApprovalItem() as item:
body = Pretty(item.raw_item, expand_all=True)
title = "Tool approval"
console.print(
Panel(
body,
title=title,
border_style="cyan",
box=box.ROUNDED,
expand=False,
)
)
@@ -0,0 +1,43 @@
# Repo code review
## Goal
Review a small public git repository, run its tests, leave line-level review comments in the structured output, and write a patch-oriented review artifact.
## Why this is valuable
This demo shows a coding-agent workflow where the sandbox can inspect a real git worktree, run tests, reason over a diff, and produce review artifacts that a developer can act on. The manifest mounts `pypa/sampleproject` at a pinned ref with `GitRepo(...)`. The review contract is intentionally narrow: one finding should target the CI workflow, and one should target the missing type hints in `src/sample/simple.py`.
## Setup
Run the Unix-local example from the repository root:
```bash
uv run python examples/sandbox/tutorials/repo_code_review/main.py
uv run python examples/sandbox/tutorials/repo_code_review/evals.py
```
This demo exits after the scripted review so the generated artifacts and eval contract stay deterministic.
To run the same review in Docker, build the shared tutorial image once and pass
`--docker`:
```bash
docker build -t sandbox-tutorials:latest -f examples/sandbox/tutorials/Dockerfile .
uv run python examples/sandbox/tutorials/repo_code_review/main.py --docker
uv run python examples/sandbox/tutorials/repo_code_review/evals.py
```
## Expected artifacts
- `output/review.md`
- `output/findings.jsonl`
- Optional `output/fix.patch`
## Demo shape
- Inputs: `pypa/sampleproject` at a pinned git ref, mounted into the workspace as `repo/`.
- Runtime primitives: sandbox-local bash, optional file edits, and a typed `RepoReviewResult` final output.
- Workflow: one sandbox reviewer agent is enough here; there is no handoff because the task is a linear inspect -> test -> patch -> summarize loop.
- Scratch space: the reviewer can use `scratchpad/` for notes or draft diffs, then return the final review object for the wrapper to persist.
- Evals: `evals.py` checks that the two findings stay focused on `uv` in the test workflow and type hints in `src/sample/simple.py`, and that the patch only edits `simple.py`.
@@ -0,0 +1 @@
@@ -0,0 +1,79 @@
"""Evaluate the repo code-review demo outputs."""
import argparse
import json
from pathlib import Path
EXPECTED_FINDING_PATHS = {
"repo/.github/workflows/test.yml",
"repo/src/sample/simple.py",
}
def load_findings(findings_path: Path) -> list[dict[str, object]]:
return [
json.loads(line)
for line in findings_path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def validate_findings(findings: list[dict[str, object]]) -> None:
if len(findings) != 2:
raise ValueError(f"Expected 2 review findings, got {len(findings)}.")
finding_paths = {str(finding["file"]) for finding in findings}
if finding_paths != EXPECTED_FINDING_PATHS:
raise ValueError(
f"Expected findings for {sorted(EXPECTED_FINDING_PATHS)}, got {sorted(finding_paths)}."
)
workflow_comment = next(
str(finding["comment"])
for finding in findings
if finding["file"] == "repo/.github/workflows/test.yml"
)
workflow_words = {word.strip("`.,:;()[]{}").lower() for word in workflow_comment.split()}
if "nox" not in workflow_words:
raise ValueError("Expected the workflow review comment to mention nox.")
if not ({"uv", "pip", "install", "project", "test"} & workflow_words):
raise ValueError(
"Expected the workflow review comment to describe a concrete test-tooling concern."
)
simple_comment = next(
str(finding["comment"])
for finding in findings
if finding["file"] == "repo/src/sample/simple.py"
)
if "add_one" not in simple_comment or "-> int" not in simple_comment:
raise ValueError("Expected the simple.py review comment to suggest type hints for add_one.")
def validate_patch(patch_path: Path) -> None:
patch_text = patch_path.read_text(encoding="utf-8")
if "src/sample/simple.py" not in patch_text:
raise ValueError("Expected the patch to modify src/sample/simple.py.")
if ".github/workflows/test.yml" in patch_text or "noxfile.py" in patch_text:
raise ValueError("Expected the patch to avoid CI and noxfile changes.")
if "def add_one(number: int) -> int:" not in patch_text:
raise ValueError("Expected the patch to add type hints to add_one.")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--output-dir",
type=Path,
default=Path(__file__).resolve().parent / "output",
help="Directory containing findings.jsonl and fix.patch.",
)
args = parser.parse_args()
validate_findings(load_findings(args.output_dir / "findings.jsonl"))
validate_patch(args.output_dir / "fix.patch")
print("Repo review eval checks passed.")
if __name__ == "__main__":
main()
@@ -0,0 +1,173 @@
"""
Review a small GitHub repository and produce sandbox-generated findings artifacts.
"""
import argparse
import asyncio
import json
import sys
from pathlib import Path
from textwrap import dedent
from typing import cast
from pydantic import BaseModel, Field
from agents import ModelSettings, Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Filesystem, Shell
from agents.sandbox.entries import File, GitRepo
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
from examples.sandbox.tutorials.misc import (
DEFAULT_SANDBOX_IMAGE,
console,
create_sandbox_client_and_session,
load_env_defaults,
print_event,
)
DEMO_DIR = Path(__file__).resolve().parent
REPO_NAME = "pypa/sampleproject"
REPO_REF = "621e4974ca25ce531773def586ba3ed8e736b3fc"
DEFAULT_QUESTION = (
"Review this small Python repository as a maintainer. Run the tests, inspect the "
"project layout, and return exactly two concise line-level findings: one for "
"`repo/.github/workflows/test.yml` about concrete nox/test installation reliability, "
"and one for `repo/src/sample/simple.py` about adding explicit type hints to "
"`add_one`. Return a patch artifact for the obvious `simple.py` type-hint fix."
)
AGENTS_MD = dedent(
"""\
# AGENTS.md
Review the mounted repository under `repo/` like a maintainer.
- Run `uv run python -m unittest discover -s tests` from `repo/` and report a short result summary.
- Return exactly two findings, using these exact file paths:
- `repo/.github/workflows/test.yml`: mention nox and a concrete test-tooling/install concern.
- `repo/src/sample/simple.py`: mention `add_one` and suggest `-> int` type hints.
- Do not return findings for `pyproject.toml`, `noxfile.py`, README files, or tests.
- Do not edit the mounted repository. Return the suggested patch text in `fix_patch`.
- Set `fix_patch` to a minimal git diff that only edits `repo/src/sample/simple.py` by changing
`def add_one(number):` to `def add_one(number: int) -> int:`.
- If you inspect files with shell commands, use paths under `repo/`; use `rg`.
"""
)
class ReviewFinding(BaseModel):
file: str = Field(
description=(
"Exact workspace-relative path under repo/. Preserve casing from the workspace file listing."
)
)
line_number: int = Field(description="1-based line number for the review comment.")
comment: str = Field(
description=(
"Concrete review comment for that line. Include a tiny git-diff-style "
"suggestion in the comment when the fix is obvious."
)
)
class RepoReviewResult(BaseModel):
test_command: str = Field(description="Exact test command that was run.")
test_result: str = Field(description="Short summary of the test outcome.")
findings: list[ReviewFinding] = Field(description="Review findings ordered by severity.")
review_markdown: str = Field(description="Human-readable review summary in Markdown.")
fix_patch: str | None = Field(
description="A minimal git diff patch if a fix was made, otherwise null."
)
def write_review_artifacts(output_dir: Path, review: RepoReviewResult) -> None:
output_dir.mkdir(exist_ok=True)
(output_dir / "review.md").write_text(review.review_markdown.strip() + "\n", encoding="utf-8")
(output_dir / "findings.jsonl").write_text(
"\n".join(
json.dumps(finding.model_dump(mode="json"), sort_keys=True)
for finding in review.findings
)
+ "\n",
encoding="utf-8",
)
if review.fix_patch:
(output_dir / "fix.patch").write_text(review.fix_patch.strip() + "\n", encoding="utf-8")
async def main(model: str, question: str, use_docker: bool, image: str) -> None:
manifest = Manifest(
entries={
"AGENTS.md": File(content=AGENTS_MD.encode("utf-8")),
"repo": GitRepo(repo=REPO_NAME, ref=REPO_REF),
}
)
agent = SandboxAgent(
name="Code Reviewer",
model=model,
instructions=AGENTS_MD,
capabilities=[Shell(), Filesystem()],
model_settings=ModelSettings(tool_choice="required"),
output_type=RepoReviewResult,
)
client, sandbox = await create_sandbox_client_and_session(
manifest=manifest,
use_docker=use_docker,
image=image,
)
try:
async with sandbox:
result = Runner.run_streamed(
agent,
[{"role": "user", "content": question}],
max_turns=25,
run_config=RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
tracing_disabled=True,
workflow_name="Repo Review example",
),
)
async for event in result.stream_events():
print_event(event)
if result.final_output is None:
raise RuntimeError("Code Reviewer returned no structured review output.")
print_event(str(result.final_output).strip())
review = cast(RepoReviewResult, result.final_output)
finally:
await client.delete(sandbox)
write_review_artifacts(DEMO_DIR / "output", review)
console.print(f"[green]Wrote review artifacts to {DEMO_DIR / 'output'}[/green]")
if __name__ == "__main__":
load_env_defaults(DEMO_DIR / ".env")
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="gpt-5.4-mini",
help="Model name to use.",
)
parser.add_argument(
"--question",
default=DEFAULT_QUESTION,
help="Prompt to send to the agent.",
)
parser.add_argument(
"--docker",
action="store_true",
help="Run this example in Docker instead of Unix-local.",
)
parser.add_argument(
"--image",
default=DEFAULT_SANDBOX_IMAGE,
help="Docker image to use when --docker is set.",
)
args = parser.parse_args()
asyncio.run(main(args.model, args.question, args.docker, args.image))
@@ -0,0 +1,32 @@
# Sandbox resume
This example shows a small sandbox resume flow with `AGENTS.md` mounted in the sandbox and loaded into the agent instructions. It runs in two
steps: first it builds the app and smoke tests it, then it serializes the
sandbox session state, resumes the sandbox, and adds pytest coverage.
By default the agent builds a tiny warehouse-robot status API, smoke-tests it, then resumes the same sandbox to add tests. The sandbox workspace starts with
one instruction file:
- `AGENTS.md` with instructions to build FastAPI apps, use type hints and Pydantic, install dependencies with `uv`, run Python commands through `uv run python`, and test locally before finishing.
Run the example from the repository root:
```bash
uv run python examples/sandbox/tutorials/sandbox_resume/main.py
```
This demo exits after the scripted resume flow so the serialized session state and resume step stay easy to follow.
You can override the model or prompt:
```bash
uv run python examples/sandbox/tutorials/sandbox_resume/main.py --model gpt-5.6-sol --question "Build a FastAPI service that exposes a warehouse robot's maintenance status."
```
To run the same flow in Docker, build the shared tutorial image once and pass
`--docker`:
```bash
docker build --tag sandbox-tutorials:latest examples/sandbox/tutorials
uv run python examples/sandbox/tutorials/sandbox_resume/main.py --docker
```
@@ -0,0 +1 @@
@@ -0,0 +1,145 @@
"""
Show the smallest Unix-local sandbox flow with workspace instructions.
The manifest includes an AGENTS.md file that tells the agent how to build the
app, and the prompt asks for a tiny FastAPI operations status API with a health
check.
"""
import argparse
import asyncio
import sys
from pathlib import Path
from textwrap import dedent
from agents import Runner, RunResultStreaming, TResponseInputItem
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Filesystem, Shell
from agents.sandbox.entries import File
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
from examples.sandbox.tutorials.misc import (
DEFAULT_SANDBOX_IMAGE,
create_sandbox_client_and_session,
load_env_defaults,
print_event,
)
DEFAULT_QUESTION = (
"Build a small warehouse-robot operations status API with FastAPI. Include a health "
"check, a typed `/robots/{robot_id}/status` endpoint backed by a tiny in-memory "
"fixture, and clear 404 behavior. Install dependencies with uv, smoke test it locally "
"with `uv run python` and `urllib.request`, and summarize what you built."
)
DEMO_DIR = Path(__file__).resolve().parent
RESUME_QUESTION = (
"Now add pytest coverage for the health check, robot status success case, and unknown "
"robot 404 case. Install any missing dependencies with uv, run the tests locally, and "
"summarize the files you changed."
)
AGENTS_MD = dedent(
"""\
# AGENTS.md
- When asked to build an app, make it a FastAPI app.
- Use type hints and Pydantic models.
- Use `uv` when installing dependencies.
- Run Python commands as `uv run python ...`, not bare `python`.
- Smoke test local HTTP endpoints with `uv run python` and `urllib.request`, not `curl`.
- Test the app locally before finishing.
"""
)
async def run_step(result: RunResultStreaming) -> list[TResponseInputItem]:
async for event in result.stream_events():
print_event(event)
print_event(str(result.final_output).strip())
return result.to_input_list()
async def main(model: str, question: str, use_docker: bool, image: str) -> None:
manifest = Manifest(entries={"AGENTS.md": File(content=AGENTS_MD.encode("utf-8"))})
agent = SandboxAgent(
name="Vibe Coder",
model=model,
instructions=AGENTS_MD,
capabilities=[Shell(), Filesystem()],
)
client, sandbox = await create_sandbox_client_and_session(
manifest=manifest,
use_docker=use_docker,
image=image,
)
conversation: list[TResponseInputItem] = [{"role": "user", "content": question}]
try:
async with sandbox:
result = Runner.run_streamed(
agent,
conversation,
max_turns=20,
run_config=RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
tracing_disabled=True,
workflow_name="Sandbox resume example",
),
)
conversation = await run_step(result)
frozen_session_state = client.deserialize_session_state(
client.serialize_session_state(sandbox.state)
)
conversation.append({"role": "user", "content": RESUME_QUESTION})
resumed_sandbox = await client.resume(frozen_session_state)
try:
async with resumed_sandbox:
resumed_result = Runner.run_streamed(
agent,
conversation,
max_turns=20,
run_config=RunConfig(
sandbox=SandboxRunConfig(session=resumed_sandbox),
tracing_disabled=True,
workflow_name="Sandbox resume example",
),
)
conversation = await run_step(resumed_result)
finally:
await client.delete(resumed_sandbox)
finally:
await client.delete(sandbox)
if __name__ == "__main__":
load_env_defaults(DEMO_DIR / ".env")
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="gpt-5.4-mini",
help="Model name to use.",
)
parser.add_argument(
"--question",
default=DEFAULT_QUESTION,
help="Prompt to send to the agent.",
)
parser.add_argument(
"--docker",
action="store_true",
help="Run this example in Docker instead of Unix-local.",
)
parser.add_argument(
"--image",
default=DEFAULT_SANDBOX_IMAGE,
help="Docker image to use when --docker is set.",
)
args = parser.parse_args()
asyncio.run(main(args.model, args.question, args.docker, args.image))
@@ -0,0 +1,41 @@
# Vision UI reproduction
## Goal
Use the sandbox `view_image` tool to inspect a reference app screenshot, then reproduce the visible screen as a static HTML/CSS artifact. This is a narrow UI repro target for vision and screenshot-debugging; it is not a web-app scaffold.
This demo is intentionally file-only: no FastAPI, no exposed port, and no local browser server. The agent calls `view_image`, lazy-loads the `playwright` skill, writes the site under `output/site/`, captures browser screenshots for visual revision, and the host copies the generated site plus the visual-review artifacts back to this example's `output/` directory.
## Setup
Run the Unix-local example from the repository root:
```bash
uv run python examples/sandbox/tutorials/vision_website_clone/main.py
```
To run the same manifest in Docker, build the shared tutorial image once and pass
`--docker`:
```bash
docker build -t sandbox-tutorials:latest -f examples/sandbox/tutorials/Dockerfile .
uv run python examples/sandbox/tutorials/vision_website_clone/main.py --docker
```
## Expected artifact
- `output/index.html`
- `output/styles.css`
- `output/screenshots/draft-1.png`
- `output/screenshots/draft-2.png`
- `output/visual-notes.md`
Open `output/index.html` locally after the run to inspect the generated clone. Open the copied draft screenshots to inspect the agent's visual-debug loop.
## Demo shape
- Inputs: one checked-in PNG reference screenshot mounted under `reference/`.
- Runtime primitives: sandbox-local shell/edit tools, `view_image`, and the lazy-loaded `playwright` skill.
- Required vision call: `view_image("reference/reference-site.png")`.
- Required debug loop: capture `output/screenshots/draft-1.png`, view it with `view_image`, revise, then repeat with `output/screenshots/draft-2.png`.
- Artifact path: the sandbox agent writes `output/site/`, `output/screenshots/`, and `output/visual-notes.md`; `main.py` copies the site files and review artifacts to this example's `output/`.
@@ -0,0 +1 @@
@@ -0,0 +1,253 @@
"""
Clone a reference app screenshot as static HTML/CSS with the sandbox filesystem tools.
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
from pathlib import Path
from textwrap import dedent
from agents import ModelSettings, Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig, WorkspaceReadNotFoundError
from agents.sandbox.capabilities import (
Filesystem,
LocalDirLazySkillSource,
Shell,
Skills,
)
from agents.sandbox.entries import Dir, File, LocalDir, LocalFile
from agents.sandbox.session import BaseSandboxSession
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
from examples.sandbox.tutorials.misc import (
DEFAULT_SANDBOX_IMAGE,
console,
create_sandbox_client_and_session,
load_env_defaults,
print_event,
)
DEMO_DIR = Path(__file__).resolve().parent
REFERENCE_IMAGE = DEMO_DIR / "reference-site.png"
SKILLS_SOURCE_DIR = DEMO_DIR / "skills"
SANDBOX_SITE_DIR = Path("output") / "site"
REMOTE_REVIEW_ARTIFACTS = (
Path("output") / "screenshots" / "draft-1.png",
Path("output") / "screenshots" / "draft-2.png",
Path("output") / "visual-notes.md",
)
DEFAULT_MODEL = "gpt-5.4-mini"
DEFAULT_QUESTION = (
"Inspect the reference screenshot and build a static HTML/CSS reproduction of the "
"screen. Write output/site/index.html and output/site/styles.css, then capture "
"browser screenshots, inspect them, and revise the site."
)
AGENTS_MD = dedent(
"""\
# Vision UI Reproduction Instructions
Create a static HTML/CSS reproduction of the provided reference screenshot.
Build only the single screen shown in the reference.
## Required workflow (must do)
- First call `view_image` on `reference/reference-site.png`.
- Before writing code, write `output/visual-notes.md` with brief layout + typography notes.
- Write the site to `output/site/index.html` and `output/site/styles.css`.
- Before taking screenshots, call `load_skill("playwright")` and read `skills/playwright/SKILL.md`.
- Capture `output/screenshots/draft-1.png`, inspect it, revise, then capture `output/screenshots/draft-2.png`.
- Do not finish without the screenshots.
"""
)
def default_output_dir() -> Path:
"""Return the local directory for copied example artifacts."""
artifacts_dir = os.environ.get("EXAMPLES_ARTIFACTS_DIR")
if artifacts_dir:
return Path(artifacts_dir)
return DEMO_DIR / "output"
def build_manifest() -> Manifest:
return Manifest(
entries={
"AGENTS.md": File(content=AGENTS_MD.encode("utf-8")),
"reference": Dir(
children={
"reference-site.png": LocalFile(src=REFERENCE_IMAGE),
},
description="Reference app screenshot to clone.",
),
"output": Dir(description="Write generated website files here."),
}
)
def build_agent(model: str) -> SandboxAgent:
return SandboxAgent(
name="Vision Website Clone Builder",
model=model,
instructions=AGENTS_MD,
capabilities=[
Shell(),
Filesystem(),
Skills(
lazy_from=LocalDirLazySkillSource(
# This is a host path read by the SDK process.
# Requested skills are copied into `skills_path` in the sandbox.
source=LocalDir(src=SKILLS_SOURCE_DIR),
),
skills_path="skills",
),
],
model_settings=ModelSettings(tool_choice="required"),
)
async def copy_site_output_dir(
*,
session: BaseSandboxSession,
output_dir: Path,
) -> list[Path]:
output_dir.mkdir(parents=True, exist_ok=True)
remote_site_dir = session.normalize_path(SANDBOX_SITE_DIR)
pending_dirs = [remote_site_dir]
copied_files: list[Path] = []
while pending_dirs:
current_dir = pending_dirs.pop()
for entry in await session.ls(current_dir):
entry_path = Path(entry.path)
if entry.is_dir():
pending_dirs.append(entry_path)
continue
relative_path = entry_path.relative_to(remote_site_dir)
local_path = output_dir / relative_path
local_path.parent.mkdir(parents=True, exist_ok=True)
handle = await session.read(entry_path)
try:
payload = handle.read()
finally:
handle.close()
if isinstance(payload, str):
local_path.write_text(payload, encoding="utf-8")
else:
local_path.write_bytes(bytes(payload))
copied_files.append(local_path)
return copied_files
async def copy_review_artifacts(
*,
session: BaseSandboxSession,
output_dir: Path,
remote_artifacts: tuple[Path, ...] = REMOTE_REVIEW_ARTIFACTS,
) -> list[Path]:
output_dir.mkdir(parents=True, exist_ok=True)
copied_files: list[Path] = []
for remote_artifact in remote_artifacts:
remote_path = session.normalize_path(remote_artifact)
relative_artifact = remote_artifact.relative_to(Path("output"))
local_path = output_dir / relative_artifact
local_path.parent.mkdir(parents=True, exist_ok=True)
try:
handle = await session.read(remote_path)
except WorkspaceReadNotFoundError:
continue
try:
payload = handle.read()
finally:
handle.close()
if isinstance(payload, str):
local_path.write_text(payload, encoding="utf-8")
else:
local_path.write_bytes(bytes(payload))
copied_files.append(local_path)
return copied_files
async def main(model: str, question: str, use_docker: bool, image: str, output_dir: Path) -> None:
client, sandbox = await create_sandbox_client_and_session(
manifest=build_manifest(),
use_docker=use_docker,
image=image,
)
try:
async with sandbox:
result = Runner.run_streamed(
build_agent(model),
[{"role": "user", "content": question}],
max_turns=30,
run_config=RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
tracing_disabled=True,
workflow_name="Vision Website Clone example",
),
)
async for event in result.stream_events():
print_event(event)
if result.final_output is None:
raise RuntimeError("Vision Website Clone Builder returned no final message.")
print_event(str(result.final_output).strip())
copied_files = await copy_site_output_dir(session=sandbox, output_dir=output_dir)
copied_review_files = await copy_review_artifacts(
session=sandbox,
output_dir=output_dir,
)
finally:
await client.delete(sandbox)
expected_files = {output_dir / "index.html", output_dir / "styles.css"}
if not expected_files <= set(copied_files):
raise RuntimeError(
"Vision Website Clone Builder must write output/site/index.html and "
"output/site/styles.css."
)
console.print(f"[green]Copied static site to {output_dir / 'index.html'}[/green]")
for path in copied_review_files:
console.print(f"[green]Copied review artifact to {path}[/green]")
if __name__ == "__main__":
load_env_defaults(DEMO_DIR / ".env")
parser = argparse.ArgumentParser()
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
parser.add_argument(
"--docker",
action="store_true",
help="Run this example in Docker instead of Unix-local.",
)
parser.add_argument(
"--image",
default=DEFAULT_SANDBOX_IMAGE,
help="Docker image to use when --docker is set.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=default_output_dir(),
help="Directory for copied website files.",
)
args = parser.parse_args()
asyncio.run(main(args.model, args.question, args.docker, args.image, args.output_dir))
Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

@@ -0,0 +1,23 @@
---
name: "playwright"
description: "Use when the task requires capturing or automating a real browser from the terminal."
---
# Playwright
Use Playwright to capture the static site directly. Do not start a server for this example.
```sh
mkdir -p output/screenshots output/playwright/.tmp
export TMPDIR="$PWD/output/playwright/.tmp"
export TEMP="$TMPDIR"
export TMP="$TMPDIR"
npx --yes --package playwright@1.50.0 playwright install chromium
npx --yes --package playwright@1.50.0 playwright screenshot \
--browser=chromium \
--viewport-size=2048,1152 \
"file://$PWD/output/site/index.html" \
output/screenshots/draft-1.png
```
Change the final path to `output/screenshots/draft-2.png` for the second pass.