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
110 changes: 110 additions & 0 deletions examples/analytics-tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# AI Analytics with agent-sandbox
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a small overview of what this example does before going into the implementation details.


## Getting Started

### Prerequisites
- Running **GKE** cluster (**Standard** or **Autopilot**))
- `kubectl` access to a Kubernetes **GKE Standard** or **GKE Autopilot** cluster
- Agent-sandbox installed on GKE. Here is the ([Installation Guide](../../getting_started/))

## Deploy analytics tools

This section describes how to build Docker image that defines analytics tool for an ADK agent, push the Docker image to a Artifact Registry repository and deploy the pushed image.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no ADK agent in this example ??


Run the following commands:

```bash
cd analytics-tool
```

Create a repository in Artifact Registry:

```bash
gcloud artifacts repositories create analytics \
--project=${PROJECT_ID} \
--repository-format=docker \
--location=us \
--description="Analytics Repo"
```

Create a repository in Artifact Registry.

```bash
gcloud builds submit .
```

After build is completed, change `$PROJECT_ID` in `sandbox-python.yaml` and apply it:

```bash
kubectl apply -f sandbox-python.yaml
kubectl apply -f analytics-svc.yaml
```

## Deploy jupyter lab

Deploy a jupyter lab to make some data analytics:

```bash
kubectl apply -f ../jupyterlab.yaml
```

Once it's running, port-forward the jupyterlab and access on `http://127.0.0.1:8888` by running this command:

```bash
kubectl port-forward "pod/jupyterlab-sandbox" 8888:8888
```

Follow the `welcome.ipynb` notebook (defined in `jupyterlab.yaml`).

## Analytics example

In the `Download the data` is described the dataset that will be used in the example. In the `Data analytics` is described the actual data analytics. Function `analyze_movies` with the `tool` decorator is defined in this section. In the docstring is described the instruction for the LLM how to use it.

The example query looks like this:

```log
Load /my-data/shopping_behavior_updated.csv. This data has 'Purchase Amount (USD)' column. Create a bar chart showing a sum of 'Purchase Amount (USD)' per column 'Location'.
```

The agent will be able to generate code that will be executed in the agent-sandbox pod. For example, the code might look like this:

```python
import pandas as pd
import matplotlib.pyplot as plt
import io
import base64

# Load the data
df = pd.read_csv('/my-data/shopping_behavior_updated.csv')

# Group by 'Location' and sum 'Purchase Amount (USD)'
purchase_amount_by_location = df.groupby('Location')['Purchase Amount (USD)'].sum()

# Create a bar chart
plt.figure(figsize=(10, 6))
purchase_amount_by_location.plot(kind='bar')
plt.title('Total Purchase Amount (USD) by Location')
plt.xlabel('Location')
plt.ylabel('Total Purchase Amount (USD)')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()

# Save to buffer
buf = io.BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
img_str = base64.b64encode(buf.read()).decode('utf-8')
print(f"<IMG>{img_str}</IMG>")
```

In the end the code prints an encoded image. Inside the tool definition the regex expression is used to extract this string, decode, and plot it.

![](imgs/analytics-output.png)

## Cleanup

```bash
gcloud artifacts repositories delete analytics \
--project=${PROJECT_ID} \
--location=us
```
25 changes: 25 additions & 0 deletions examples/analytics-tool/analytics-tool/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use the official Python image from the Docker Hub as the base image.
FROM python:3.11-slim

WORKDIR /app

RUN apt update && apt install curl zip unzip -y && apt clean

# Installation of dependencies for python runtime sandbox.
COPY requirements.txt .

RUN pip install --no-cache-dir --require-hashes -r requirements.txt

COPY main.py .

# Change ownership of the /app directory to the non-root user 1000.
RUN chown -R 1000:1000 /app
USER 1000

# Expose the port that the Uvicorn server will run on.
# This must match the port in the CMD instruction below.
EXPOSE 8888

# The command to run when the container starts.
# This starts the Uvicorn server, making our API available.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8888", "--log-level", "trace"]
13 changes: 13 additions & 0 deletions examples/analytics-tool/analytics-tool/analytics-svc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: python-sandbox-service
namespace: default
spec:
type: ClusterIP
selector:
sandbox: my-python-sandbox
ports:
- protocol: TCP
port: 8888
targetPort: 8888
5 changes: 5 additions & 0 deletions examples/analytics-tool/analytics-tool/cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-t', 'us-docker.pkg.dev/$PROJECT_ID/analytics/analytics-tool:1.0.0', '.' ]
images:
- 'us-docker.pkg.dev/$PROJECT_ID/analytics/analytics-tool:1.0.0'
130 changes: 130 additions & 0 deletions examples/analytics-tool/analytics-tool/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright 2025 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import subprocess
import os
import shlex
import logging

from fastapi import FastAPI, UploadFile, File
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel

class ExecuteRequest(BaseModel):
"""Request model for the /execute endpoint."""
command: str

class ExecuteResponse(BaseModel):
"""Response model for the /execute endpoint."""
stdout: str
stderr: str
exit_code: int


ALLOWED_COMMANDS = {"ls", "echo", "cat", "grep", "pwd", "zip", "unzip", "mv", "curl", "python"}

app = FastAPI(
title="Agentic Sandbox Runtime",
description="An API server for executing commands and managing files in a secure sandbox.",
version="1.0.0",
)

@app.get("/", summary="Health Check")
async def health_check():
"""A simple health check endpoint to confirm the server is running."""
return {"status": "ok", "message": "Sandbox Runtime is active."}

@app.post("/execute", summary="Execute a shell command", response_model=ExecuteResponse)
async def execute_command(request: ExecuteRequest):
"""
Executes a shell command inside the sandbox and returns its output.
Uses shlex.split for security to prevent shell injection.
"""
try:
# Syntax Validation: shlex.split raises ValueError on malformed quotes
try:
args = shlex.split(request.command)
except ValueError as e:
return ExecuteResponse(
stdout="",
stderr=f"Malformed command syntax: {str(e)}",
exit_code=1
)
# Structural Validation: Ensure the command isn't empty
if not args:
return ExecuteResponse(
stdout="",
stderr="No command provided",
exit_code=1
)

# Security Validation: Check against an Allow-list
executable = args[0]
if executable not in ALLOWED_COMMANDS:
return ExecuteResponse(
stdout="",
stderr=f"Forbidden command: '{executable}'. Only {list(ALLOWED_COMMANDS)} are allowed.",
exit_code=1
)

# Execute the command, always from the /app directory
process = subprocess.run(
args,
capture_output=True,
text=True,
cwd="/app",
timeout=30,
)
return ExecuteResponse(
stdout=process.stdout,
stderr=process.stderr,
exit_code=process.returncode
)
except subprocess.TimeoutExpired:
return ExecuteResponse(stdout="", stderr="Command timed out", exit_code=124)
except Exception as e:
return ExecuteResponse(stdout="", stderr=str(e), exit_code=1)

@app.post("/upload", summary="Upload a file to the sandbox")
async def upload_file(file: UploadFile = File(...)):
"""
Receives a file and saves it to the /app directory in the sandbox.
"""
try:
logging.info(f"--- UPLOAD_FILE CALLED: Attempting to save '{file.filename}' ---")
file_path = os.path.join("/app", file.filename)

with open(file_path, "wb") as f:
f.write(await file.read())

return JSONResponse(
status_code=200,
content={"message": f"File '{file.filename}' uploaded successfully."}
)
except Exception as e:
logging.exception("An error occurred during file upload.")
return JSONResponse(
status_code=500,
content={"message": f"File upload failed: {str(e)}"}
)

@app.get("/download/{file_path:path}", summary="Download a file from the sandbox")
async def download_file(file_path: str):
"""
Downloads a specified file from the /app directory in the sandbox.
"""
full_path = os.path.join("/app", file_path)
if os.path.isfile(full_path):
return FileResponse(path=full_path, media_type='application/octet-stream', filename=file_path)
return JSONResponse(status_code=404, content={"message": "File not found"})
10 changes: 10 additions & 0 deletions examples/analytics-tool/analytics-tool/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# For the API server
fastapi
uvicorn
python-multipart

# For the ML agent's tasks
pandas
scikit-learn
lightgbm
matplotlib
Loading