chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+240
@@ -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 @@
|
||||
|
||||
@@ -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))
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user