This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.venv
|
||||||
|
.git
|
||||||
|
.pytest_cache
|
||||||
35
.gitea/workflows/build.yml
Normal file
35
.gitea/workflows/build.yml
Normal 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
14
Dockerfile
Normal 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
5
a2a.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
name: graph-agent
|
||||||
|
version: 0.1.0
|
||||||
|
entrypoint: agent:GraphAgent
|
||||||
|
expose:
|
||||||
|
public: true
|
||||||
97
agent.py
Normal file
97
agent.py
Normal 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
70
deploy/20-deployment.yaml
Normal 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
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
httpx>=0.27
|
||||||
Reference in New Issue
Block a user