Teach agent builder packed frontends
All checks were successful
build / build (push) Successful in 25s
All checks were successful
build / build (push) Successful in 25s
This commit is contained in:
@@ -28,6 +28,9 @@ if TYPE_CHECKING:
|
||||
from .config import Settings
|
||||
|
||||
|
||||
A2A_PACK_MIN_VERSION = "0.1.19"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolContext:
|
||||
bucket: str
|
||||
@@ -132,17 +135,25 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
def init_agent_template(
|
||||
name: str,
|
||||
description: str = "A new A2A agent",
|
||||
frontend: str = "none",
|
||||
) -> str:
|
||||
"""Initialize ``agents/<name>/`` from the installed a2a-pack template.
|
||||
|
||||
Use this FIRST for a new project. It writes the same baseline files
|
||||
as ``a2a init`` for the SDK version installed in this runtime:
|
||||
``agent.py``, ``a2a.yaml``, and ``requirements.txt``. After this,
|
||||
edit those files with ``read_agent_file`` + ``write_agent_file`` to
|
||||
implement the user's requested behavior.
|
||||
``agent.py``, ``a2a.yaml``, and ``requirements.txt``. Set
|
||||
``frontend`` to ``"react"`` for a Vite packed app, ``"static"`` for a
|
||||
minimal bundled HTML app, or ``"none"`` for a headless agent. After
|
||||
this, edit those files with ``read_agent_file`` + ``write_agent_file``
|
||||
to implement the user's requested behavior.
|
||||
"""
|
||||
try:
|
||||
files = _render_a2a_init_template(name, description=description)
|
||||
frontend_kind = _normalize_frontend_kind(frontend)
|
||||
files = _render_a2a_init_template(
|
||||
name,
|
||||
description=description,
|
||||
frontend=frontend_kind,
|
||||
)
|
||||
prefix = _agent_prefix(name)
|
||||
except (OSError, RuntimeError, ValueError) as exc:
|
||||
return json.dumps({"error": str(exc)})
|
||||
@@ -156,7 +167,17 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
ContentType="text/plain; charset=utf-8",
|
||||
)
|
||||
written.append({"path": key, "size": len(content)})
|
||||
return json.dumps({"ok": True, "agent": name, "files": written})
|
||||
return json.dumps({
|
||||
"ok": True,
|
||||
"agent": name,
|
||||
"frontend": frontend_kind,
|
||||
"files": written,
|
||||
"docs": (
|
||||
"https://docs.a2acloud.io/concepts/packed-frontends"
|
||||
if frontend_kind != "none"
|
||||
else "https://docs.a2acloud.io/quickstart"
|
||||
),
|
||||
})
|
||||
|
||||
@tool
|
||||
def list_agent_files(name: str) -> str:
|
||||
@@ -186,7 +207,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
"""Write a file under ``agents/<name>/<path>`` in the user's workspace.
|
||||
|
||||
Use this for ``agent.py``, ``a2a.yaml``, ``requirements.txt``, and
|
||||
any auxiliary modules the agent needs.
|
||||
any auxiliary modules or packed frontend files the agent needs.
|
||||
"""
|
||||
try:
|
||||
prefix = _agent_prefix(name)
|
||||
@@ -277,7 +298,8 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
async def test_agent_in_sandbox(name: str) -> str:
|
||||
"""Spin a microVM, ``pip install a2a-pack`` + the agent's own
|
||||
requirements.txt, then run ``a2a card`` to verify the scaffold
|
||||
produces a valid Card. Returns stdout/stderr.
|
||||
produces a valid Card. When a packed frontend is declared, it also
|
||||
prints ``a2a frontend info`` metadata. Returns stdout/stderr.
|
||||
"""
|
||||
try:
|
||||
prefix = _agent_prefix(name)
|
||||
@@ -294,7 +316,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
b64 = base64.b64encode(bundle_bytes).decode("ascii")
|
||||
script = (
|
||||
"set -e\n"
|
||||
"pip install --quiet a2a-pack >/dev/null\n"
|
||||
f"pip install --quiet 'a2a-pack>={A2A_PACK_MIN_VERSION}' >/dev/null\n"
|
||||
"mkdir -p /tmp/agent\n"
|
||||
f"echo '{b64}' | base64 -d | tar -xzf - -C /tmp/agent\n"
|
||||
"cd /tmp/agent\n"
|
||||
@@ -304,6 +326,10 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
"fi\n"
|
||||
"echo '--- card ---'\n"
|
||||
"a2a card --project .\n"
|
||||
"if grep -q '^frontend:' a2a.yaml 2>/dev/null; then\n"
|
||||
" echo '--- frontend ---'\n"
|
||||
" a2a frontend info --project . || true\n"
|
||||
"fi\n"
|
||||
)
|
||||
headers = (
|
||||
{"authorization": f"Bearer {settings.sandbox_token}"}
|
||||
@@ -911,12 +937,64 @@ def _class_name(slug: str) -> str:
|
||||
return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent"
|
||||
|
||||
|
||||
_FRONTEND_TEMPLATE_FILES: dict[str, dict[str, str]] = {
|
||||
"static": {
|
||||
"frontend/dist/index.html": "frontend/static-index.html.tmpl",
|
||||
},
|
||||
"react": {
|
||||
"frontend/package.json": "frontend/react-package.json.tmpl",
|
||||
"frontend/index.html": "frontend/react-index.html.tmpl",
|
||||
"frontend/vite.config.js": "frontend/react-vite.config.js.tmpl",
|
||||
"frontend/src/main.jsx": "frontend/react-main.jsx.tmpl",
|
||||
"frontend/src/App.jsx": "frontend/react-app.jsx.tmpl",
|
||||
"frontend/src/a2a.js": "frontend/react-a2a.js.tmpl",
|
||||
"frontend/src/style.css": "frontend/react-style.css.tmpl",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_frontend_kind(frontend: str | None) -> str:
|
||||
kind = (frontend or "none").strip().lower()
|
||||
if kind in {"", "no", "off", "false"}:
|
||||
kind = "none"
|
||||
if kind not in {"none", "static", "react"}:
|
||||
raise ValueError("frontend must be one of: none, static, react")
|
||||
return kind
|
||||
|
||||
|
||||
def _frontend_block(frontend: str) -> str:
|
||||
kind = _normalize_frontend_kind(frontend)
|
||||
if kind == "none":
|
||||
return ""
|
||||
build = "\n build: npm run build" if kind == "react" else ""
|
||||
return (
|
||||
"frontend:\n"
|
||||
" path: frontend"
|
||||
f"{build}\n"
|
||||
" dist: dist\n"
|
||||
" mount: /app\n"
|
||||
" auth: inherit"
|
||||
)
|
||||
|
||||
|
||||
def _render_frontend_files(frontend: str, *, name: str) -> dict[str, str]:
|
||||
kind = _normalize_frontend_kind(frontend)
|
||||
if kind == "none":
|
||||
return {}
|
||||
return {
|
||||
path: _render_sdk_template(template, name=name)
|
||||
for path, template in _FRONTEND_TEMPLATE_FILES[kind].items()
|
||||
}
|
||||
|
||||
|
||||
def _render_a2a_init_template(
|
||||
name: str,
|
||||
*,
|
||||
description: str = "A new A2A agent",
|
||||
frontend: str = "none",
|
||||
) -> dict[str, str]:
|
||||
_validate_name(name)
|
||||
frontend_kind = _normalize_frontend_kind(frontend)
|
||||
class_name = _class_name(name)
|
||||
files = {
|
||||
"agent.py": _render_sdk_template(
|
||||
@@ -929,13 +1007,16 @@ def _render_a2a_init_template(
|
||||
"a2a.yaml.tmpl",
|
||||
name=name,
|
||||
class_name=class_name,
|
||||
frontend_block=_frontend_block(frontend_kind),
|
||||
),
|
||||
"requirements.txt": _render_sdk_template("requirements.txt.tmpl"),
|
||||
}
|
||||
files.update(_render_frontend_files(frontend_kind, name=name))
|
||||
return files
|
||||
|
||||
|
||||
def _render_sdk_template(template: str, /, **vars: str) -> str:
|
||||
parts = template.split("/")
|
||||
local_templates = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "a2a"
|
||||
@@ -944,13 +1025,12 @@ def _render_sdk_template(template: str, /, **vars: str) -> str:
|
||||
/ "templates"
|
||||
)
|
||||
if local_templates.exists():
|
||||
text = local_templates.joinpath(template).read_text(encoding="utf-8")
|
||||
text = local_templates.joinpath(*parts).read_text(encoding="utf-8")
|
||||
else:
|
||||
text = (
|
||||
resources.files("a2a_pack.cli.templates")
|
||||
.joinpath(template)
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
resource = resources.files("a2a_pack.cli.templates")
|
||||
for part in parts:
|
||||
resource = resource.joinpath(part)
|
||||
text = resource.read_text(encoding="utf-8")
|
||||
for key, value in vars.items():
|
||||
text = text.replace("{{ " + key + " }}", value)
|
||||
return text
|
||||
|
||||
Reference in New Issue
Block a user