Metadata-Version: 2.5
Name: gmi-agentbox-sdk
Version: 0.1.0b6
Summary: GMI AgentBox SDK for Python.
Project-URL: Homepage, https://www.gmicloud.ai/
Author: GMI
License: MIT
License-File: LICENSE
Keywords: agentbox,gmi,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Requires-Dist: certifi>=2024.0.0
Requires-Dist: websocket-client>=1.8
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

# Usage

GMI AgentBox SDK (`gmi-agentbox-sdk`) is a thin Python client for AgentBox.

## Install

```bash
python -m pip install gmi-agentbox-sdk
```

Requires Python 3.9+. TLS uses [certifi](https://pypi.org/project/certifi/).

## Authenticate

To create an AgentBox API key, open
[API keys](https://console.gmicloud.ai/user-setting/ce/access/api-keys), then
click **Create API key**. Store the key securely; it
authenticates requests for your organization.

```bash
export GMI_AGENTBOX_API_KEY="your-api-key"
```

`GMI_AGENTBOX_API_KEY` is required when constructing `AgentBoxClient()` with no
`api_key=` argument.

Or pass it to the client:

```python
from agentbox_sdk import AgentBoxClient

client = AgentBoxClient(api_key="your-api-key")
```

The default API root is production (used when `GMI_AGENTBOX_BASE_URL` is unset):

`https://api.gmi-serving.com/v1/agents`

Resource paths such as `/deployments` and `/tasks` are joined onto that root.
Override the full root with `GMI_AGENTBOX_BASE_URL` or `AgentBoxClient(base_url=...)`.

## Quickstart

```python
import time

from agentbox_sdk import AgentBoxClient, SandboxFailed, SandboxWaitTimeout

client = AgentBoxClient()

# Available values vary by organization. Use the catalogue to select an IDC
# and a SKU for the Sandbox runtime.
idcs = client.idcs.list(runtime="sandbox")
# Choose an `idc_id` returned above. This is the production example value.
idc_id = "us-central-iowa16"
products = client.products.list(idc_name=idc_id, runtime="sandbox")
# Choose an `instance_type` returned above.
instance_type = "gmi.sandbox.small"


def wait_for_template(agent, timeout=600, poll_interval=3):
    deadline = time.monotonic() + timeout
    while True:
        agent.refresh()
        status = agent.template_build_status
        if status == "ready" or status is None:
            return
        if status == "error":
            raise RuntimeError(agent.template_build_error or "Sandbox image build failed")
        if time.monotonic() >= deadline:
            raise TimeoutError(f"Sandbox image is still {status!r} after {timeout}s")
        print("...")
        time.sleep(poll_interval)

agent = None
sandbox = None
try:
    agent = client.agents.create(
        title="agentbox-demo",  # choose a unique name
        image_url="docker.io/library/alpine:3.20",
        idc=idc_id,
        instance_type=instance_type,
        runtime="sandbox",
    )
    print("Building Sandbox image; a new image can take several minutes.")
    wait_for_template(agent)
    sandbox = agent.launch(instance_type=instance_type)
    sandbox.wait_until_running()  # default timeout 300s
    print(sandbox.status, sandbox.endpoint_url)
    execution = sandbox.execute("echo hello")
    print(execution.status, execution.exit_code, execution.data.get("stdout"))
except SandboxFailed as exc:
    print("failed", exc.status, exc.message)
except SandboxWaitTimeout:
    print("still not running", sandbox.status, sandbox.last_error)
finally:
    if sandbox is not None:
        sandbox.delete()
    if agent is not None:
        agent.delete()
```

Use `client.idcs.list(runtime="sandbox")` and
`client.products.list(idc_name=..., runtime="sandbox")` to discover available
Sandbox data centers and SKUs.

## Register an Agent

```python
agent = client.agents.create(
    title="agentbox-demo",
    image_url="docker.io/library/alpine:3.20",
    idc=idc_id,
    runtime="sandbox",
    env=[{"name": "GMI_MODELS", "value": "llama", "secret": False}],
)
print(agent.slug, agent.launchable)
```

- `generated_api_key` is plaintext only on create (and rare patch retries).
- `agent.launchable` is false when the upstream template is missing.

Later:

```python
agent = client.agents.get("agentbox-demo")
page = client.agents.list(page=1, page_size=20)
agent.update(title="renamed")
agent.delete()
```

## Launch a Sandbox

```python
sandbox = agent.launch(instance_type=instance_type)
# or: client.sandboxes.launch(agent.slug, instance_type=..., idc_name=agent.idc)

sandbox.wait_until_running(timeout=300, poll_interval=2.0)
sandbox.refresh()
print(sandbox.status, sandbox.last_error, sandbox.endpoint_url)
```

`wait_until_running` polls until `running`. It raises:

- `SandboxFailed` if status is `error`, `deleted`, `stopped`, or `stopping`
- `SandboxWaitTimeout` if the timeout elapses first

`endpoint_url` is empty until the sandbox is running, and can still be empty
after `running`. Do not assume an HTTP URL is always present.

## Metadata and expiry

Both are sandbox-runtime only. `metadata` is your own labels.
`timeout_seconds` is the lifetime in seconds from now. Omit it to keep the
platform TTL and auto-renew. A positive value pins the expiry. Read the
effective timestamp from `sandbox.expires_at` after `refresh()` or
`set_timeout()`.

```python
sandbox = agent.launch(
    instance_type=instance_type,
    metadata={"session": "abc"},
    timeout_seconds=3600,
)
sandbox.refresh()
print(sandbox.metadata, sandbox.expires_at, sandbox.expiry_pinned)

sandbox.set_timeout(6 * 3600)
page = client.sandboxes.list(metadata={"session": "abc"})
```

`set_timeout` works while the sandbox is running or paused. It can shorten or
extend the remaining lifetime, and it pins the sandbox so later reads do not
auto-renew it.

`pause()` pauses a running sandbox. Disk files are kept (except `/tmp`, which
is cleared on resume); memory and running processes are not. A paused sandbox
stops consuming run time. The call returns when the pause is accepted;
`status` is usually `pausing`. Poll with `refresh()` until it is `paused`.
`paused_expire_at` is the deadline to resume before the sandbox and its disk
are reclaimed.

```python
sandbox.pause()
while sandbox.status != "paused":
    time.sleep(2)
    sandbox.refresh()
print(sandbox.paused_expire_at)
```

`resume()` wakes a paused sandbox. Omit `timeout_seconds` to keep the run time
that was left at pause. Set it (>= 60) to reset that run time and pin the
expiry. The call returns when the resume is accepted; `status` is usually
`resuming`. Poll with `wait_until_running()`. Resume is a cold start: disk
files come back except `/tmp`, processes are not restored, and the start
command is not re-run.

```python
sandbox.resume()
sandbox.wait_until_running()

sandbox.resume(timeout_seconds=3600)
```

## Snapshots

A snapshot saves a running sandbox so new sandboxes of the same Agent can
start from it. Creation is asynchronous: the snapshot starts `creating`; wait
for `ready`. Launching from a snapshot inherits its sizing and IDC, so
`instance_type` can be omitted.

```python
snapshot = sandbox.create_snapshot(name="base")
snapshot.wait_until_ready(timeout=600)

clone = agent.launch(snapshot_id=snapshot.id)
clone.wait_until_running()

snapshots = agent.list_snapshots()  # newest first
snapshot.rename("base-v2")          # only when ready or error
snapshot.delete()                   # async; status becomes deleting
```

`wait_until_ready()` raises `SnapshotFailed` if the status becomes `error`,
`deleting` or `delete_failed`, and `SnapshotWaitTimeout` if the timeout
elapses first. A snapshot from `client.sandboxes.create_snapshot()` does not
know its Agent; poll it with `client.agents.list_snapshots()` instead.

## Run commands and transfer files

```python
# Finished command
result = sandbox.execute("echo hello")
print(result.status, result.exit_code, result.data.get("stdout"))

# Start a long-running command, then cancel it.
execution = sandbox.execute("sleep 120", wait=False)
if execution.accepted:
    sandbox.cancel_execution(execution.id)

# File round trip
sandbox.upload_file(path="/home/user/input.txt", file=b"hello\n")
download = sandbox.download_file(path="/home/user/input.txt")
print(download.filename, download.content)
```

`wait=False` returns an accepted execution; retrieve its latest state with
`sandbox.get_execution(execution.id)`. `wait_timeout_seconds` accepts values
from 1 through 25 and is omitted when `wait=False`. File paths must be absolute
and cannot contain `.` or `..` segments.

## List and filter sandboxes

```python
page = client.sandboxes.list()  # omits stopped and deleted
page = client.sandboxes.list(status=["running", "creating", "error"])
page = client.sandboxes.list(agent_id=agent.id, status="running")
page = client.sandboxes.list(metadata={"session": "abc"})
page = agent.sandboxes(metadata={"session": "abc"})
```

The default Sandbox list **excludes** `stopped` and `deleted`.
Pass `status` when you need those.

## Delete

```python
sandbox.delete()  # one-way; the sandbox cannot be restarted
agent.delete()     # does not stop or delete remaining sandboxes
```

Always delete sandboxes you launched in tests. They bill the Console account.

## Log stream

The SDK supports log streaming when the Sandbox provides the `logs_stream`
capability:

```python
if sandbox.capabilities.get("logs_stream"):
    for event in sandbox.stream():
        print(event.event, event.data)
        if event.event == "status" and isinstance(event.data, dict):
            break
```

When unavailable, this call raises an unsupported-runtime error. A backend
can enable the capability without requiring an SDK update.

## Template build logs

Sandbox Agents expose paged template build logs. Container Agents raise
`NotSupportedError` (HTTP 501). GMI does not persist these logs.

```python
import time

from agentbox_sdk import GoneError

builds = agent.list_builds()
build_id = builds[0].id
offset = 0
while True:
    try:
        page = agent.build_logs(build_id, offset=offset)
    except GoneError:
        print("logs no longer available")
        break
    for entry in page.entries:
        print(entry.level, entry.message)
    offset = page.next_offset
    if page.has_more:
        continue
    build = agent.get_build(build_id)
    if build.status in ("ready", "error"):
        try:
            final = agent.build_logs(build_id, offset=offset)
        except GoneError:
            print("logs no longer available")
            break
        for entry in final.entries:
            print(entry.level, entry.message)
        if build.status == "error" and build.failure is not None:
            raise RuntimeError(build.failure.message)
        break
    time.sleep(2)
```

`has_more` false only means nothing more is available right now. Keep polling
from `next_offset` while the build is `waiting` or `building`, then read one
last page after it reaches `ready` or `error`. `410` means the logs are gone,
including for a successful build.

## Eligibility and metrics

```python
entitlement = client.eligibility()
print(entitlement.eligible, entitlement.data_centers)

if sandbox.capabilities.get("metrics"):
    batch = sandbox.metrics(start=start, end=end, kinds=["cpu", "memory"], step=60)
    series = sandbox.metrics_timeseries(kind="cpu", start=start, end=end)
    print(series.empty_reason, series.points)
```

`kinds` omitted means the API default (all 8 charts): `gpu_util`, `gpu_mem`,
`cpu`, `memory`, `disk_read`, `disk_write`, `net_rx`, `net_tx`. `start` /
`end` are unix seconds. Metrics are callable only when the Sandbox provides
the `metrics` capability; otherwise the service reports that the runtime does
not support them.

## Concepts

| Name | Meaning |
|---|---|
| `idc` / `idc_name` | Data center identifier, not a region label. |
| `instance_type` | Billing SKU, not the product object name. |

## Errors

```python
from agentbox_sdk import (
    AuthenticationError,
    SandboxFailed,
    SandboxWaitTimeout,
    NotFoundError,
    UnprocessableError,
)

try:
    client.agents.get("missing")
except NotFoundError as exc:
    print(exc.status_code, exc.message)
```

`401` is `AuthenticationError` (for example, when the API key is invalid).
`422` is `UnprocessableError` (locked field / template mismatch). `410` is
`GoneError` (template build logs reclaimed). `501` is `NotSupportedError`
(template build APIs on a container Agent) and is not retried as a server
error. See
[Reference](https://docs.gmicloud.ai/api-reference/agentbox-sdk/reference).
