From 5478277b762d85bd4b81347de30b6ca945e4b354 Mon Sep 17 00:00:00 2001 From: a2a-platform Date: Sun, 10 May 2026 00:44:29 +0000 Subject: [PATCH] deploy --- .dockerignore | 5 ++ .gitea/workflows/build.yml | 35 ++++++++++++++ Dockerfile | 14 ++++++ a2a.yaml | 5 ++ agent.py | 97 ++++++++++++++++++++++++++++++++++++++ deploy/20-deployment.yaml | 70 +++++++++++++++++++++++++++ requirements.txt | 1 + 7 files changed, 227 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/build.yml create mode 100644 Dockerfile create mode 100644 a2a.yaml create mode 100644 agent.py create mode 100644 deploy/20-deployment.yaml create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8d9d89e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +.venv +.git +.pytest_cache diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..48e13ad --- /dev/null +++ b/.gitea/workflows/build.yml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e1cc07a --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/a2a.yaml b/a2a.yaml new file mode 100644 index 0000000..dd8aead --- /dev/null +++ b/a2a.yaml @@ -0,0 +1,5 @@ +name: graph-agent +version: 0.1.0 +entrypoint: agent:GraphAgent +expose: + public: true diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..b93b244 --- /dev/null +++ b/agent.py @@ -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), + } diff --git a/deploy/20-deployment.yaml b/deploy/20-deployment.yaml new file mode 100644 index 0000000..fc5ebaa --- /dev/null +++ b/deploy/20-deployment.yaml @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6ecf620 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +httpx>=0.27