deploy
All checks were successful
build / build (push) Successful in 6s

This commit is contained in:
a2a-platform
2026-05-10 00:44:29 +00:00
commit 5478277b76
7 changed files with 227 additions and 0 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
__pycache__
*.pyc
.venv
.git
.pytest_cache

View File

@@ -0,0 +1,35 @@
name: build
on:
push:
branches: [main]
paths-ignore:
- 'deploy/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: build image
run: |
IMG=registry.88-99-219-120.nip.io/agents/graph-agent
docker build -t "$IMG:$GITHUB_SHA" -t "$IMG:latest" .
docker push "$IMG:$GITHUB_SHA"
docker push "$IMG:latest"
- name: bump deploy manifest
run: |
IMG=registry.88-99-219-120.nip.io/agents/graph-agent
sed -i "s|image: $IMG:.*|image: $IMG:$GITHUB_SHA|" deploy/20-deployment.yaml
git config user.email "ci@a2a.local"
git config user.name "ci"
git add deploy/20-deployment.yaml
if git diff --staged --quiet; then
echo "no manifest changes"
else
git commit -m "ci: bump image to $GITHUB_SHA"
git push "http://gitea_admin:gitea_admin@gitea-http.gitea.svc.cluster.local:3000/gitea_admin/graph-agent.git" HEAD:main
fi

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM registry.88-99-219-120.nip.io/a2a/a2a-pack-base:latest
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV A2A_ENTRYPOINT=agent:GraphAgent
ENV PORT=8000
EXPOSE 8000
CMD a2a run --entrypoint "$A2A_ENTRYPOINT" --host 0.0.0.0 --port 8000

5
a2a.yaml Normal file
View File

@@ -0,0 +1,5 @@
name: graph-agent
version: 0.1.0
entrypoint: agent:GraphAgent
expose:
public: true

97
agent.py Normal file
View File

@@ -0,0 +1,97 @@
"""Graph agent: turns prompts into PNG charts inside a microsandbox VM.
Receives an a2a grant from the caller, derives the user's MinIO bucket
from the grant, asks the cluster sandbox runtime to spin a microVM with
that bucket FUSE/bridge-mounted at /workspace, runs matplotlib inside,
writes the PNG back into the bucket.
"""
from __future__ import annotations
import os
import httpx
from pydantic import BaseModel
from a2a_pack import A2AAgent, NoAuth, RunContext, skill
SANDBOX_URL = os.environ.get(
"SANDBOX_URL", "http://sandbox.sandbox.svc.cluster.local:8000"
)
class GraphConfig(BaseModel):
image: str = "python:3.11-slim"
class GraphAgent(A2AAgent[GraphConfig, NoAuth]):
name = "graph-agent"
description = "Generate matplotlib charts in an isolated microVM, write PNG to /workspace/outputs/"
version = "0.1.0"
config_model = GraphConfig
auth_model = NoAuth
tools_used = ("microsandbox", "matplotlib")
@skill(
description="Render a chart and save it as a PNG in the caller's workspace.",
tags=["visualization", "chart", "spreadsheet"],
)
async def generate_chart(
self, ctx: RunContext[NoAuth], prompt: str
) -> dict:
# The grant we received gives us a workspace bound to the caller's
# bucket. We can ONLY see that bucket; nothing else.
bucket = getattr(ctx.workspace, "bucket", None)
if not bucket:
return {"error": "no workspace grant; refusing to run"}
await ctx.emit_progress(f"rendering '{prompt}' for bucket {bucket}")
# Embed the prompt as the chart title via an env var so we don't have
# to escape it inside a heredoc'd Python script.
script = (
"set -e\n"
"pip install -q --no-cache-dir matplotlib >/dev/null\n"
'python - <<\'PY\'\n'
"import os, matplotlib\n"
"matplotlib.use('Agg')\n"
"import matplotlib.pyplot as plt\n"
"os.makedirs('/workspace/outputs', exist_ok=True)\n"
"title = os.environ.get('CHART_TITLE', 'chart')\n"
"data = {'Q1': 12, 'Q2': 19, 'Q3': 15, 'Q4': 27}\n"
"fig, ax = plt.subplots(figsize=(8, 5))\n"
"ax.bar(list(data.keys()), list(data.values()), color='#4f46e5')\n"
"ax.set_title(title)\n"
"ax.set_ylabel('value')\n"
"fig.tight_layout()\n"
"fig.savefig('/workspace/outputs/chart.png', dpi=120)\n"
"print('wrote /workspace/outputs/chart.png')\n"
"PY\n"
)
async with httpx.AsyncClient(timeout=180.0) as c:
r = await c.post(
f"{SANDBOX_URL}/v1/run_shell",
json={
"bucket": bucket,
"script": f"export CHART_TITLE={prompt!r}; {script}",
"image": self.config.image,
"memory_mib": 1024,
"timeout_seconds": 150,
},
)
if r.status_code >= 400:
return {
"error": f"sandbox {r.status_code}",
"detail": r.text[:1000],
}
out = r.json()
await ctx.emit_progress("chart rendered")
return {
"prompt": prompt,
"bucket": bucket,
"chart_path": "outputs/chart.png",
"stdout": out.get("stdout", ""),
"exit_code": out.get("exit_code", -1),
}

70
deploy/20-deployment.yaml Normal file
View File

@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: graph-agent
namespace: agents
labels:
app: graph-agent
a2a/managed-by: control-plane
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: graph-agent
template:
metadata:
labels:
app: graph-agent
spec:
containers:
- name: agent
image: registry.88-99-219-120.nip.io/agents/graph-agent:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
readinessProbe:
httpGet: {path: /healthz, port: 8000}
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet: {path: /healthz, port: 8000}
initialDelaySeconds: 10
periodSeconds: 15
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: 200m, memory: 256Mi}
---
apiVersion: v1
kind: Service
metadata:
name: graph-agent
namespace: agents
spec:
type: ClusterIP
selector:
app: graph-agent
ports:
- name: http
port: 80
targetPort: 8000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: graph-agent
namespace: agents
spec:
rules:
- host: graph-agent.88-99-219-120.nip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: graph-agent
port:
number: 80

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
httpx>=0.27