This commit is contained in:
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/charts/"
|
||||
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/charts', 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/charts/chart.png', dpi=120)\n"
|
||||
"print('wrote /workspace/charts/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": "charts/chart.png",
|
||||
"stdout": out.get("stdout", ""),
|
||||
"exit_code": out.get("exit_code", -1),
|
||||
}
|
||||
Reference in New Issue
Block a user