Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Environments
.env**
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Git
.git
.gitignore

# Misc
.DS_Store
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# syntax=docker/dockerfile:1.3
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/

# Install system dependencies
RUN apt-get update && apt-get install -y \
htop \
vim \
curl \
tar \
python3-dev \
postgresql-client \
build-essential \
libpq-dev \
gcc \
cmake \
netcat-openbsd \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN uv pip install --system --upgrade pip setuptools wheel

ENV UV_HTTP_TIMEOUT=1000

# Copy pyproject.toml and README.md to install dependencies
COPY 00_sync/050_openai_agents_local_sandbox/pyproject.toml /app/050_openai_agents_local_sandbox/pyproject.toml
COPY 00_sync/050_openai_agents_local_sandbox/README.md /app/050_openai_agents_local_sandbox/README.md

WORKDIR /app/050_openai_agents_local_sandbox

# Copy the project code
COPY 00_sync/050_openai_agents_local_sandbox/project /app/050_openai_agents_local_sandbox/project

# Copy the test files
COPY 00_sync/050_openai_agents_local_sandbox/tests /app/050_openai_agents_local_sandbox/tests

# Copy shared test utilities
COPY test_utils /app/test_utils

# Install the required Python packages with dev dependencies
RUN uv pip install --system .[dev]

# Set environment variables
ENV PYTHONPATH=/app

# Set test environment variables
ENV AGENT_NAME=s050-openai-agents-local-sandbox

# Run the agent using uvicorn
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
113 changes: 113 additions & 0 deletions examples/tutorials/00_sync/050_openai_agents_local_sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Tutorial 050: Sync OpenAI Agents SDK with a Local Sandbox

This tutorial demonstrates how to build a **synchronous** agent on AgentEx using the
[OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents) and its
**sandbox** runtime, running with the **local** (`unix_local`) backend.

The agent is a "local sandbox assistant": it answers questions by actually running
real shell commands (e.g. `python3 --version`, `ls /tmp`, `python3 -c "..."`)
instead of guessing.

## Key Concepts

### Sync ACP
The sync ACP model uses HTTP request/response for communication. The
`@acp.on_message_send` handler receives a message, runs the agent, and returns the
agent's final answer as a `TextContent`.

### OpenAI Agents SDK Sandbox
The OpenAI Agents SDK ships `agents.sandbox`, which lets you give an agent
**capabilities** (instead of hand-written tools) that the runtime turns into real
tools backed by a sandbox:

- **`SandboxAgent`**: an `Agent` that is granted sandbox capabilities.
- **Capabilities** (`from agents.sandbox.capabilities import Shell, Filesystem, Memory`):
each capability expands into a set of real tools. This tutorial uses `Shell`, which
lets the model run real shell commands.
- **`SandboxRunConfig`** + a sandbox **client**: tells the runtime *where* the tools
actually execute.

### The LOCAL sandbox (`UnixLocalSandboxClient`)
This tutorial uses the local backend
(`from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient, UnixLocalSandboxClientOptions`),
`backend_id="unix_local"`. The local sandbox runs shell commands **ON THE HOST** —
the agent's own container/process. There is **no Docker, no Temporal, and no remote
sandbox infrastructure** involved. This makes it the simplest way to give an agent a
real shell.

The sandbox is wired up through the SDK's `RunConfig`:

```python
from agents import Runner, set_tracing_disabled
from agents.run_config import RunConfig
from agents.sandbox import SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.sandboxes.unix_local import (
UnixLocalSandboxClient,
UnixLocalSandboxClientOptions,
)

set_tracing_disabled(True) # avoid api.openai.com tracing 401 behind a gateway

agent = SandboxAgent(
name="Local Sandbox Assistant",
instructions="...use the shell tools to actually run commands...",
capabilities=[Shell()],
)
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=UnixLocalSandboxClient(),
options=UnixLocalSandboxClientOptions(),
)
)
result = await Runner.run(agent, input="what's the python version?", run_config=run_config)
print(result.final_output)
```

`Runner.run` drives the full tool-call loop internally: the model issues shell
commands, the local sandbox runs them on the host, the output is fed back, and the
loop continues until the model produces a final answer.

## Files

| File | Description |
|------|-------------|
| `project/acp.py` | ACP server and message handler (runs the sandbox agent) |
| `project/agent.py` | `SandboxAgent` + `RunConfig(sandbox=...)` wiring + `run_agent` |
| `project/tools.py` | Sandbox capability factory (`Shell`) |
| `tests/test_agent.py` | Integration tests |
| `manifest.yaml` | Agent configuration |

## Running Locally

```bash
# From this directory
agentex agents run
```

Set `OPENAI_API_KEY` (or `LITELLM_API_KEY` if you're behind the Scale LiteLLM
gateway) in your environment or in a `.env` file in `project/` so the agent can call
the model.

## Running Tests

```bash
pytest tests/test_agent.py -v
```

## Notes

- **No infra required.** Because this uses the `unix_local` backend, the shell tools
run directly in the agent's process — no Docker daemon, no Temporal, no remote
sandbox. Swap the client for a remote/containerized backend to isolate execution.
- **Tracing.** `set_tracing_disabled(True)` turns off the OpenAI Agents SDK's native
tracer (which would otherwise try to ship traces to `api.openai.com`). The manifest
also sets `OPENAI_AGENTS_DISABLE_TRACING=1`. AgentEx/SGP tracing still runs via the
tracing manager configured in `acp.py` when SGP credentials are present.
- **Capabilities are the tools.** To let the agent do more, add capabilities in
`project/tools.py` (e.g. `Filesystem()`, `Memory()`).

## Further Reading

- OpenAI Agents SDK guide: https://developers.openai.com/api/docs/guides/agents
- The next evolution of the Agents SDK: https://openai.com/index/the-next-evolution-of-the-agents-sdk/
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
build:
context:
root: ../../
include_paths:
- 00_sync/050_openai_agents_local_sandbox
- test_utils
dockerfile: 00_sync/050_openai_agents_local_sandbox/Dockerfile
dockerignore: 00_sync/050_openai_agents_local_sandbox/.dockerignore

local_development:
agent:
port: 8000
host_address: host.docker.internal
paths:
acp: project/acp.py

agent:
acp_type: sync
name: s050-openai-agents-local-sandbox
description: A sync OpenAI Agents SDK agent using a local (unix_local) sandbox

temporal:
enabled: false

credentials:
- env_var_name: OPENAI_API_KEY
secret_name: openai-api-key
secret_key: api-key
- env_var_name: REDIS_URL
secret_name: redis-url-secret
secret_key: url
- env_var_name: SGP_API_KEY
secret_name: sgp-api-key
secret_key: api-key
- env_var_name: SGP_ACCOUNT_ID
secret_name: sgp-account-id
secret_key: account-id
- env_var_name: SGP_CLIENT_BASE_URL
secret_name: sgp-client-base-url
secret_key: url

env:
OPENAI_AGENTS_DISABLE_TRACING: "1"

deployment:
image:
repository: ""
tag: "latest"

global:
agent:
name: "s050-openai-agents-local-sandbox"
description: "A sync OpenAI Agents SDK agent using a local (unix_local) sandbox"
replicaCount: 1
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1000m"
memory: "2Gi"
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""ACP (Agent Communication Protocol) handler for Agentex.

This is the API layer — it owns the agent lifecycle and runs the OpenAI Agents
SDK *sandbox* agent for each incoming message, returning the agent's final
answer to the Agentex frontend.

The agent uses the LOCAL sandbox backend (``UnixLocalSandboxClient``), which runs
shell commands on the host (this process/container). The OpenAI Agents SDK runs
its tool-call loop internally via ``Runner.run`` and returns the final output, so
this sync handler returns a single ``TextContent`` rather than streaming tokens.
"""

from __future__ import annotations

import os

from dotenv import load_dotenv

load_dotenv()

from agentex.lib import adk
from project.agent import run_agent
from agentex.lib.types.acp import SendMessageParams
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.sdk.fastacp.fastacp import FastACP
from agentex.types.task_message_content import TaskMessageContent
from agentex.lib.core.tracing.tracing_processor_manager import (
add_tracing_processor_config,
)

logger = make_logger(__name__)

# LiteLLM proxy auth: copy LITELLM_API_KEY to OPENAI_API_KEY for OpenAI client
# compatibility, so the same example works behind the Scale LiteLLM gateway.
_litellm_key = os.environ.get("LITELLM_API_KEY")
if _litellm_key and not os.environ.get("OPENAI_API_KEY"):
os.environ["OPENAI_API_KEY"] = _litellm_key
Comment thread
greptile-apps[bot] marked this conversation as resolved.

SGP_API_KEY = os.environ.get("SGP_API_KEY", "")
SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "")
SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "")

if SGP_API_KEY and SGP_ACCOUNT_ID:
add_tracing_processor_config(
SGPTracingProcessorConfig(
sgp_api_key=SGP_API_KEY,
sgp_account_id=SGP_ACCOUNT_ID,
sgp_base_url=SGP_CLIENT_BASE_URL,
)
)

acp = FastACP.create(acp_type="sync")


@acp.on_message_send
async def handle_message_send(
params: SendMessageParams,
) -> TaskMessageContent:
"""Handle incoming messages by running the local-sandbox agent."""
task_id = params.task.id
user_message = params.content.content
logger.info(f"Processing message for task {task_id}")

async with adk.tracing.span(
trace_id=task_id,
task_id=task_id,
name="message",
input={"message": user_message},
data={"__span_type__": "AGENT_WORKFLOW"},
) as turn_span:
final_output = await run_agent(user_message)
if turn_span:
turn_span.output = {"final_output": final_output}

return TextContent(author="agent", content=final_output)
Loading
Loading