2d665c9a67
### Sandbox Agents This release adds **Sandbox Agents**, a beta SDK surface for running agents with a persistent, isolated workspace. Sandbox agents keep the normal `Agent` and `Runner` flow, but add workspace manifests, sandbox-native capabilities, sandbox clients, snapshots, and resume support so agents can work over real files, run commands, edit repositories, generate artifacts, and continue work across runs. Key pieces: - `SandboxAgent`: an `Agent` with sandbox defaults such as `default_manifest`, sandbox instructions, capabilities, and `run_as`. - `Manifest`: a fresh-workspace contract for files, directories, local files, local directories, Git repos, environment, users, groups, and mounts. - `SandboxRunConfig`: per-run sandbox wiring for client creation, live session injection, serialized session resume, manifest overrides, snapshots, and materialization concurrency limits. - Built-in capabilities for shell access, filesystem editing and image inspection, skills, memory, and compaction. - Workspace snapshots and serialized sandbox session state for reconnecting to existing work or seeding a fresh sandbox from saved contents. ### Sandbox clients and hosted providers Sandbox agents now support local, containerized, and hosted execution backends: - `UnixLocalSandboxClient` for fast local development. - `DockerSandboxClient` for container isolation and image parity. - Hosted sandbox clients for Blaxel, Cloudflare, Daytona, E2B, Modal, Runloop, and Vercel through optional extras. The release also adds provider-specific examples and mount strategies for common storage backends, including S3, Cloudflare R2, Google Cloud Storage, Azure Blob Storage, and S3 Files where supported by the selected backend. ### Sandbox memory Adds a sandbox memory capability that lets future sandbox-agent runs learn from prior runs. Memory stores extracted lessons in the sandbox workspace, injects a concise summary into later runs, and uses progressive disclosure so agents can search deeper rollout summaries only when useful. Memory supports: - Read-only or generate-only modes. - Live updates when the agent discovers stale memory. - Multi-turn grouping through `conversation_id`, SDK `Session`, `RunConfig.group_id`, or generated run IDs. - Separate memory layouts for isolating memory across agents or workflows. - S3-backed examples for persisted memory across runs. ### Workspace mounts, snapshots, and resume This release adds a full workspace entry and mount model for sandbox sessions: - Local files and directories. - Synthetic files and directories. - Git repository entries. - Remote storage mounts for S3, R2, GCS, Azure Blob Storage, and S3 Files. - Provider-specific mount strategies across Docker, Modal, Cloudflare, Blaxel, Daytona, E2B, and Runloop. - Portable snapshots with path normalization, symlink preservation, mount-safe snapshotting, and remote snapshot support. - Resume paths through runner-managed `RunState`, explicit `SandboxSessionState`, or saved snapshots. ### Examples and tutorials Adds a large `examples/sandbox/` suite covering: - Local Unix and Docker sandbox runners. - Docker mount smoke tests for S3, GCS, Azure Blob Storage, and S3 Files. - Sandbox coding tasks with skills. - Sandbox agents as tools and handoff patterns. - Memory examples, including multi-agent/multi-turn memory and S3-backed memory. - Tax-prep and healthcare-support workflows. - Dataroom QA and metric extraction tutorials. - Repository code review tutorial. - Vision website clone tutorial. - Provider examples for Blaxel, Cloudflare, Daytona, E2B, Modal, Runloop, Temporal, and Vercel. ### Runtime, tracing, and model plumbing The release includes the runtime plumbing needed to make sandbox agents work naturally inside the existing SDK: - Runner-managed sandbox preparation, capability binding, session lifecycle, state serialization, and resume behavior. - Sandbox-aware `RunState` serialization. - Unified sandbox tracing with SDK spans. - Token usage on tracing spans. - Runner-managed prompt cache key defaults. - OpenAI agent registration and harness ID configuration. - Safer redaction of sensitive MCP tool outputs when sensitive tracing is disabled. - Additional OpenAI client/model utilities and Chat Completions coverage. ## Documentation & Other Changes - docs: add Asqav to external tracing processors list. - docs: update translated document pages. Co-authored-by: Abdulrahman Alfozan <alfozan@openai.com> Co-authored-by: Aditya Singh <60082699+adityasingh2400@users.noreply.github.com> Co-authored-by: Andi Liu <andi@openai.com> Co-authored-by: Aron <263346377+aron-cf@users.noreply.github.com> Co-authored-by: ashwinnathan-openai <ashwinnathan@openai.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: cploujoux <cploujoux@blaxel.ai> Co-authored-by: elainegan-openai <168589666+elainegan-openai@users.noreply.github.com> Co-authored-by: Elias Freider <freider@users.noreply.github.com> Co-authored-by: Erik Dunteman <erik@erikds-macbook-air.local> Co-authored-by: Jason Liu <jasonliu@openai.com> Co-authored-by: Jason Steving <32336750+jasonsteving99@users.noreply.github.com> Co-authored-by: Kazuhiro Sera <seratch@openai.com> Co-authored-by: Lovre Pešut <lovre.pesut@gmail.com> Co-authored-by: Lucas Wang <lucas_wang@lucas-futures.com> Co-authored-by: Matt Brockman <matt.brockman@e2b.dev> Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com> Co-authored-by: Naresh <ghostwriternr@gmail.com> Co-authored-by: nicholasclark-openai <nicholasclark@openai.com> Co-authored-by: qiyaoq-oai <qiyaoq@openai.com> Co-authored-by: Scott Trinh <scott@scotttrinh.com> Co-authored-by: tode-rl <tony@runloop.ai> Co-authored-by: Wendy Jiao <wendyjiao@openai.com>
505 lines
17 KiB
Python
505 lines
17 KiB
Python
"""NASA spending text-to-SQL agent.
|
|
|
|
Multi-turn conversational agent that translates natural-language questions
|
|
about NASA federal spending into SQL queries, executes them against a
|
|
USAspending SQLite database, and returns structured results.
|
|
|
|
Usage:
|
|
uv run python -m examples.sandbox.extensions.daytona.usaspending_text2sql.agent
|
|
|
|
The database is built automatically inside the sandbox on first run by
|
|
executing setup_db.py (requires internet access). Subsequent runs reuse the
|
|
existing database.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from openai.types.responses import ResponseTextDeltaEvent
|
|
|
|
from agents import Runner
|
|
from agents.run import RunConfig
|
|
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
|
|
from agents.sandbox.capabilities.compaction import Compaction
|
|
from agents.sandbox.capabilities.memory import Memory
|
|
from agents.sandbox.capabilities.shell import Shell
|
|
from agents.sandbox.config import MemoryGenerateConfig, MemoryReadConfig
|
|
from agents.sandbox.entries import Dir, File, LocalDir, LocalFile
|
|
from agents.sandbox.session import (
|
|
EventPayloadPolicy,
|
|
Instrumentation,
|
|
JsonlOutboxSink,
|
|
)
|
|
from examples.sandbox.extensions.daytona.usaspending_text2sql.sql_capability import (
|
|
SqlCapability,
|
|
)
|
|
|
|
try:
|
|
from agents.extensions.sandbox import (
|
|
DEFAULT_DAYTONA_WORKSPACE_ROOT,
|
|
DaytonaSandboxClient,
|
|
DaytonaSandboxClientOptions,
|
|
DaytonaSandboxSessionState,
|
|
)
|
|
except Exception as exc: # pragma: no cover
|
|
raise SystemExit(
|
|
"Daytona sandbox examples require the optional repo extra.\n"
|
|
"Install it with: uv sync --extra daytona"
|
|
) from exc
|
|
|
|
EXAMPLE_DIR = Path(__file__).parent
|
|
SCHEMA_DIR = EXAMPLE_DIR / "schema"
|
|
SETUP_DB_PATH = EXAMPLE_DIR / "setup_db.py"
|
|
SESSION_STATE_PATH = EXAMPLE_DIR / ".session_state.json"
|
|
AUDIT_LOG_PATH = EXAMPLE_DIR / ".audit_log.jsonl"
|
|
|
|
# Set at runtime once the exposed port is resolved.
|
|
_downloads_base_url: str = ""
|
|
|
|
DEVELOPER_INSTRUCTIONS = (
|
|
(SCHEMA_DIR / "overview.md").read_text()
|
|
+ """
|
|
|
|
## Instructions
|
|
|
|
- Always use the `run_sql` tool to query the database. Never attempt to run sqlite3 directly.
|
|
- Read schema documentation from schema/tables/ if you need detailed column information.
|
|
- Read schema/glossary.md for official USAspending term definitions (e.g., what "obligation" vs "outlay" means).
|
|
- Prefer aggregations (GROUP BY, SUM, COUNT, AVG) over returning many raw rows.
|
|
- Format monetary values with dollar signs and commas in your final answers (e.g., $1,234,567).
|
|
- When the user asks a follow-up question, use conversation context to understand references
|
|
like "break that down by year" or "just the top 5".
|
|
- If a query fails, read the error message and try to fix the SQL.
|
|
- Explain your query logic briefly so the user can verify correctness.
|
|
|
|
## Data caveats
|
|
|
|
- The database contains **obligations** (money legally committed), not outlays (money actually paid).
|
|
When the user asks about "spending", clarify that these are obligation amounts.
|
|
- Amounts are tied to the **action_date** (when the obligation was signed), not when the work happens.
|
|
A multi-year contract may appear entirely in the fiscal year it was obligated.
|
|
- Some recipients are masked as "MULTIPLE RECIPIENTS" or "REDACTED DUE TO PII" for privacy reasons.
|
|
Mention this if recipient-level analysis looks incomplete.
|
|
"""
|
|
)
|
|
|
|
DB_PATH = "data/usaspending.db"
|
|
|
|
WORKSPACE_ROOT = DEFAULT_DAYTONA_WORKSPACE_ROOT
|
|
|
|
|
|
def build_agent() -> SandboxAgent:
|
|
"""Build the agent blueprint."""
|
|
manifest = Manifest(
|
|
root=WORKSPACE_ROOT,
|
|
entries={
|
|
"setup_db.py": LocalFile(src=SETUP_DB_PATH),
|
|
"schema": LocalDir(src=SCHEMA_DIR),
|
|
"data": Dir(ephemeral=True),
|
|
"memory/memory_summary.md": File(content=b""),
|
|
"memory/phase_two_selection.json": File(content=b""),
|
|
},
|
|
)
|
|
|
|
return SandboxAgent(
|
|
name="NASA Spending Q&A",
|
|
default_manifest=manifest,
|
|
model="gpt-5.4",
|
|
instructions=(
|
|
"You are a helpful data analyst that answers questions about NASA federal spending "
|
|
"by writing and executing SQL queries.\n\n" + DEVELOPER_INSTRUCTIONS
|
|
),
|
|
capabilities=[
|
|
SqlCapability(db_path=DB_PATH),
|
|
Shell(),
|
|
Compaction(),
|
|
Memory(
|
|
read=MemoryReadConfig(live_update=False),
|
|
generate=MemoryGenerateConfig(
|
|
extra_prompt=(
|
|
"Pay attention to which SQL patterns work best for the USAspending data, "
|
|
"column quirks (e.g. recipient_parent_name vs recipient_name for grouping), "
|
|
"and data caveats the user discovers (e.g. negative obligations, masked "
|
|
"recipients)."
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Terminal formatting helpers (unchanged from universal_computer version)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DIM = "\033[2;39m"
|
|
DIM_CYAN = "\033[2;36m"
|
|
DIM_BLUE = "\033[2;34m"
|
|
DIM_YELLOW = "\033[2;33m"
|
|
DIM_GREEN = "\033[2;32m"
|
|
RESET = "\033[0m"
|
|
|
|
_SQL_KEYWORDS = (
|
|
r"\b(?:SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|CROSS|FULL|NATURAL|ON|AND|OR"
|
|
r"|NOT|IN|IS|NULL|AS|WITH|GROUP\s+BY|ORDER\s+BY|HAVING|LIMIT|OFFSET|UNION"
|
|
r"|ALL|DISTINCT|CASE|WHEN|THEN|ELSE|END|EXISTS|BETWEEN|LIKE|INSERT|UPDATE"
|
|
r"|DELETE|CREATE|DROP|ALTER|SET|VALUES|INTO|TABLE|INDEX|VIEW|ASC|DESC|BY"
|
|
r"|OVER|PARTITION\s+BY)\b"
|
|
)
|
|
|
|
_SQL_FUNCTIONS = (
|
|
r"\b(?:COUNT|SUM|AVG|MIN|MAX|COALESCE|CAST|SUBSTR|LENGTH|ROUND|ABS|IFNULL"
|
|
r"|NULLIF|REPLACE|TRIM|UPPER|LOWER|DATE|DATETIME|STRFTIME|TYPEOF|TOTAL"
|
|
r"|GROUP_CONCAT|PRINTF|ROW_NUMBER|RANK|DENSE_RANK)(?=\s*\()"
|
|
)
|
|
|
|
_SQL_STRING = r"'(?:''|[^'])*'"
|
|
|
|
|
|
def _highlight_sql(sql: str) -> str:
|
|
"""Apply ANSI syntax highlighting to a SQL string."""
|
|
placeholders: list[str] = []
|
|
|
|
def _stash_string(m: re.Match[str]) -> str:
|
|
placeholders.append(m.group(0))
|
|
return f"\x00STR{len(placeholders) - 1}\x00"
|
|
|
|
result = re.sub(_SQL_STRING, _stash_string, sql)
|
|
|
|
result = re.sub(
|
|
_SQL_KEYWORDS,
|
|
lambda m: f"{DIM_BLUE}{m.group(0)}{DIM}",
|
|
result,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
result = re.sub(
|
|
_SQL_FUNCTIONS,
|
|
lambda m: f"{DIM_YELLOW}{m.group(0)}{DIM}",
|
|
result,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
|
|
def _restore_string(m: re.Match[str]) -> str:
|
|
idx = int(m.group(1))
|
|
return f"{DIM_GREEN}{placeholders[idx]}{DIM}"
|
|
|
|
result = re.sub(r"\x00STR(\d+)\x00", _restore_string, result)
|
|
return result
|
|
|
|
|
|
def _format_tool_args(name: str, arguments: str) -> str:
|
|
"""Format a tool call for display, pretty-printing SQL queries."""
|
|
if name == "run_sql":
|
|
try:
|
|
args = json.loads(arguments)
|
|
query = args.get("query", "")
|
|
limit = args.get("limit")
|
|
header = f" {DIM}[SQL]"
|
|
if limit is not None:
|
|
header += f" (limit {limit})"
|
|
header += RESET
|
|
highlighted = _highlight_sql(query)
|
|
sql = textwrap.indent(highlighted, " ")
|
|
return f"{header}\n{DIM}{sql}{RESET}"
|
|
except Exception:
|
|
pass
|
|
return f" {DIM}[tool] {name}({arguments}){RESET}"
|
|
|
|
|
|
def _format_tool_result(output: str) -> str | None:
|
|
"""Format a tool result for display. Returns None for non-SQL results."""
|
|
try:
|
|
data = json.loads(output)
|
|
except (json.JSONDecodeError, TypeError):
|
|
if output.strip():
|
|
return f" {DIM}{output.strip()}{RESET}"
|
|
return None
|
|
|
|
columns = data.get("columns")
|
|
rows = data.get("rows")
|
|
if not isinstance(columns, list) or not isinstance(rows, list):
|
|
return None
|
|
|
|
row_count = data.get("row_count", len(rows))
|
|
display_count = data.get("display_count", len(rows))
|
|
truncated = data.get("truncated", False)
|
|
|
|
if not columns:
|
|
return f" {DIM_CYAN}\u2192 Result (0 rows){RESET}"
|
|
|
|
# Build the summary line.
|
|
parts = []
|
|
if display_count < row_count:
|
|
parts.append(f"showing {display_count} of {row_count}")
|
|
else:
|
|
parts.append(f"{row_count} rows")
|
|
if truncated:
|
|
parts.append("CSV truncated at limit")
|
|
|
|
csv_file = data.get("csv_file")
|
|
download_line = ""
|
|
if csv_file and _downloads_base_url:
|
|
download_line = f"\n {DIM}\u2193 {_downloads_base_url}{csv_file}{RESET}"
|
|
|
|
# Try to fit the table in the terminal. If too wide, skip it —
|
|
# the model's prose summary + download link are enough.
|
|
try:
|
|
term_width = os.get_terminal_size().columns
|
|
except OSError:
|
|
term_width = 120
|
|
|
|
widths = [len(str(c)) for c in columns]
|
|
for row in rows:
|
|
for i, val in enumerate(row):
|
|
widths[i] = max(widths[i], len(str(val) if val is not None else "NULL"))
|
|
|
|
# 4 leading spaces + "| " between each col + trailing " |"
|
|
table_width = 4 + sum(widths) + 3 * len(widths) + 1
|
|
|
|
if table_width > term_width:
|
|
header = f" {DIM_CYAN}\u2192 Result ({row_count} rows) \u2014 too wide to print in terminal, download below{RESET}"
|
|
return f"{header}{download_line}"
|
|
|
|
def fmt_row(vals: list[Any]) -> str:
|
|
cells = []
|
|
for v, w in zip(vals, widths, strict=False):
|
|
cells.append(str(v if v is not None else "NULL").ljust(w))
|
|
return " | " + " | ".join(cells) + " |"
|
|
|
|
lines = [fmt_row(columns)]
|
|
lines.append(" |" + "|".join("-" * (w + 2) for w in widths) + "|")
|
|
for row in rows:
|
|
lines.append(fmt_row(row))
|
|
|
|
header = f" {DIM_CYAN}\u2192 Result ({', '.join(parts)})"
|
|
table = "\n".join(lines)
|
|
return f"{header}\n{table}{RESET}{download_line}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Multi-turn REPL using Runner.run_streamed()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def run_turn(
|
|
agent: SandboxAgent,
|
|
conversation: list[Any],
|
|
question: str,
|
|
run_config: RunConfig,
|
|
) -> list[Any]:
|
|
"""Run one conversational turn and return the updated conversation history."""
|
|
input_items = conversation + [{"role": "user", "content": question}]
|
|
|
|
result = Runner.run_streamed(agent, input_items, run_config=run_config)
|
|
|
|
async for event in result.stream_events():
|
|
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
|
|
print(event.data.delta, end="", flush=True)
|
|
continue
|
|
|
|
if event.type != "run_item_stream_event":
|
|
continue
|
|
|
|
if event.name == "tool_called":
|
|
item = event.item
|
|
raw = getattr(item, "raw_item", None)
|
|
if raw is not None:
|
|
name = getattr(raw, "name", "")
|
|
arguments = getattr(raw, "arguments", "")
|
|
print()
|
|
print(_format_tool_args(name, arguments))
|
|
continue
|
|
|
|
if event.name == "tool_output":
|
|
item = event.item
|
|
output = getattr(item, "output", "")
|
|
if isinstance(output, str):
|
|
formatted = _format_tool_result(output)
|
|
if formatted is not None:
|
|
print(formatted)
|
|
print()
|
|
continue
|
|
|
|
print()
|
|
|
|
# Build the full conversation history for the next turn using the SDK's
|
|
# built-in conversion, which correctly serializes all item types.
|
|
return result.to_input_list()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Session state persistence for pause/resume
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _load_session_state() -> DaytonaSandboxSessionState | None:
|
|
"""Load saved session state from disk, or return None."""
|
|
if not SESSION_STATE_PATH.exists():
|
|
return None
|
|
try:
|
|
return DaytonaSandboxSessionState.model_validate_json(SESSION_STATE_PATH.read_text())
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _save_session_state(state: DaytonaSandboxSessionState) -> None:
|
|
"""Persist session state to disk so the sandbox can be reused next run."""
|
|
SESSION_STATE_PATH.write_text(state.model_dump_json(indent=2))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main entrypoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def main() -> None:
|
|
agent = build_agent()
|
|
|
|
instrumentation = Instrumentation(
|
|
sinks=[JsonlOutboxSink(AUDIT_LOG_PATH)],
|
|
payload_policy=EventPayloadPolicy(include_exec_output=True),
|
|
)
|
|
RESULTS_PORT = 8080
|
|
|
|
client = DaytonaSandboxClient(instrumentation=instrumentation)
|
|
client_options = DaytonaSandboxClientOptions(
|
|
pause_on_exit=True,
|
|
exposed_ports=(RESULTS_PORT,),
|
|
)
|
|
|
|
# Try to resume a previously paused sandbox.
|
|
saved_state = _load_session_state()
|
|
sandbox = None
|
|
destroy = False
|
|
|
|
try:
|
|
if saved_state is not None:
|
|
old_sandbox_id = saved_state.sandbox_id
|
|
try:
|
|
sandbox = await client.resume(saved_state)
|
|
assert isinstance(sandbox.state, DaytonaSandboxSessionState)
|
|
if sandbox.state.sandbox_id == old_sandbox_id:
|
|
print("Reconnected to existing sandbox.")
|
|
else:
|
|
print("Previous sandbox no longer exists. Created a new one.")
|
|
except Exception as e:
|
|
print(f"Could not resume previous sandbox: {e}")
|
|
saved_state = None
|
|
sandbox = None
|
|
|
|
if sandbox is None:
|
|
sandbox = await client.create(manifest=agent.default_manifest, options=client_options)
|
|
|
|
await sandbox.start()
|
|
|
|
# Persist state immediately so crashes don't orphan the sandbox.
|
|
assert isinstance(sandbox.state, DaytonaSandboxSessionState)
|
|
_save_session_state(sandbox.state)
|
|
|
|
# Build database inside sandbox (idempotent — skips if DB already exists).
|
|
print("Setting up database (may take a few minutes on first run)...")
|
|
result = await sandbox.exec("python3", "setup_db.py", timeout=1800.0)
|
|
stdout = result.stdout.decode("utf-8", errors="replace")
|
|
if stdout.strip():
|
|
print(stdout)
|
|
if not result.ok():
|
|
stderr = result.stderr.decode("utf-8", errors="replace")
|
|
print(f"Database setup failed:\n{stderr}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Start a file server in the sandbox so query results can be downloaded.
|
|
await sandbox.exec("mkdir -p results", timeout=5.0)
|
|
await sandbox.exec(
|
|
f"nohup python3 -m http.server {RESULTS_PORT} --directory results > /dev/null 2>&1 &",
|
|
timeout=5.0,
|
|
)
|
|
|
|
# Resolve the Daytona signed URL for the file server.
|
|
global _downloads_base_url
|
|
try:
|
|
endpoint = await sandbox.resolve_exposed_port(RESULTS_PORT)
|
|
_downloads_base_url = endpoint.url_for("http")
|
|
except Exception as e:
|
|
print(f" Warning: could not resolve download URL: {e}")
|
|
|
|
run_config = RunConfig(
|
|
sandbox=SandboxRunConfig(session=sandbox),
|
|
workflow_name="NASA Spending Q&A",
|
|
)
|
|
|
|
downloads_line = ""
|
|
if _downloads_base_url:
|
|
downloads_line = f"\n Browse results: {DIM_CYAN}{_downloads_base_url}{RESET}"
|
|
|
|
print(f"""
|
|
{DIM}{"=" * 60}{RESET}
|
|
NASA Spending Q&A (FY2021\u2013FY2025)
|
|
|
|
Data from USAspending.gov \u2014 contracts, grants, and IDVs
|
|
awarded by NASA. Each row is a transaction (obligation).
|
|
|
|
Includes: amounts, award descriptions, recipients, recipient
|
|
locations, places of performance, industry and product
|
|
categories, sub-agencies, and fiscal years.
|
|
{downloads_line}
|
|
Type {DIM_CYAN}'exit'{RESET} to pause sandbox, {DIM_CYAN}'destroy'{RESET} to delete it.
|
|
{DIM}{"=" * 60}{RESET}
|
|
""")
|
|
|
|
conversation: list[Any] = []
|
|
|
|
while True:
|
|
try:
|
|
question = input("> ")
|
|
except (EOFError, KeyboardInterrupt):
|
|
print()
|
|
break
|
|
|
|
cmd = question.strip().lower()
|
|
if cmd == "exit":
|
|
break
|
|
if cmd == "destroy":
|
|
destroy = True
|
|
break
|
|
|
|
if not question.strip():
|
|
continue
|
|
|
|
try:
|
|
conversation = await run_turn(agent, conversation, question, run_config)
|
|
except Exception as e:
|
|
print(f"\nError: {e}")
|
|
print()
|
|
|
|
if destroy:
|
|
assert isinstance(sandbox.state, DaytonaSandboxSessionState)
|
|
sandbox.state.pause_on_exit = False
|
|
SESSION_STATE_PATH.unlink(missing_ok=True)
|
|
print("Deleting sandbox...")
|
|
else:
|
|
assert isinstance(sandbox.state, DaytonaSandboxSessionState)
|
|
_save_session_state(sandbox.state)
|
|
print("Saving memory and pausing sandbox (can take a couple of minutes)...")
|
|
|
|
finally:
|
|
if sandbox is not None:
|
|
if destroy:
|
|
# Skip memory flush — sandbox is being deleted.
|
|
await sandbox.stop()
|
|
await sandbox.shutdown()
|
|
else:
|
|
await sandbox.aclose()
|
|
await client.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|