feat: initialize agents from a2a template
All checks were successful
build / build (push) Successful in 2s
All checks were successful
build / build (push) Successful in 2s
This commit is contained in:
@@ -84,6 +84,11 @@ when you deploy through the control plane).
|
|||||||
|
|
||||||
Your tools:
|
Your tools:
|
||||||
|
|
||||||
|
- init_agent_template(name, description)
|
||||||
|
— initialize agents/<name>/ from the
|
||||||
|
installed a2a-pack `a2a init` template.
|
||||||
|
Use this FIRST for a new project, then
|
||||||
|
edit the generated files.
|
||||||
- list_agent_files(name) — see what's already at agents/<name>/
|
- list_agent_files(name) — see what's already at agents/<name>/
|
||||||
- write_agent_file(name, path, content)
|
- write_agent_file(name, path, content)
|
||||||
— save agent.py / a2a.yaml / requirements.txt
|
— save agent.py / a2a.yaml / requirements.txt
|
||||||
@@ -116,8 +121,10 @@ Discipline:
|
|||||||
|
|
||||||
- Pick a kebab-case slug for ``name`` (e.g. ``research-agent``,
|
- Pick a kebab-case slug for ``name`` (e.g. ``research-agent``,
|
||||||
``csv-sanitizer``). Class name is PascalCase from the slug.
|
``csv-sanitizer``). Class name is PascalCase from the slug.
|
||||||
- Write ALL THREE files (agent.py, a2a.yaml, requirements.txt) before
|
- For a new project, call init_agent_template first. Then read/edit the
|
||||||
testing — partial scaffolds break ``a2a card``.
|
generated files instead of inventing boilerplate from memory.
|
||||||
|
- Ensure all three core files (agent.py, a2a.yaml, requirements.txt)
|
||||||
|
exist before testing — partial scaffolds break ``a2a card``.
|
||||||
- Always run test_agent_in_sandbox before deploying. If exit_code != 0,
|
- Always run test_agent_in_sandbox before deploying. If exit_code != 0,
|
||||||
read stderr, edit the offending file, retest. Do NOT deploy a broken
|
read stderr, edit the offending file, retest. Do NOT deploy a broken
|
||||||
scaffold.
|
scaffold.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import re
|
|||||||
import tarfile
|
import tarfile
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from importlib import resources
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
@@ -111,6 +112,36 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
s3 = _s3(ctx)
|
s3 = _s3(ctx)
|
||||||
_ensure_bucket(s3, bucket)
|
_ensure_bucket(s3, bucket)
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def init_agent_template(
|
||||||
|
name: str,
|
||||||
|
description: str = "A new A2A agent",
|
||||||
|
) -> 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.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
files = _render_a2a_init_template(name, description=description)
|
||||||
|
prefix = _agent_prefix(name)
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
return json.dumps({"error": str(exc)})
|
||||||
|
written: list[dict[str, Any]] = []
|
||||||
|
for path, content in files.items():
|
||||||
|
key = prefix + path
|
||||||
|
s3.put_object(
|
||||||
|
Bucket=bucket,
|
||||||
|
Key=key,
|
||||||
|
Body=content.encode("utf-8"),
|
||||||
|
ContentType="text/plain; charset=utf-8",
|
||||||
|
)
|
||||||
|
written.append({"path": key, "size": len(content)})
|
||||||
|
return json.dumps({"ok": True, "agent": name, "files": written})
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def list_agent_files(name: str) -> str:
|
def list_agent_files(name: str) -> str:
|
||||||
"""List every file under ``agents/<name>/`` in the user's workspace.
|
"""List every file under ``agents/<name>/`` in the user's workspace.
|
||||||
@@ -434,7 +465,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
})
|
})
|
||||||
|
|
||||||
return [
|
return [
|
||||||
list_agent_files, write_agent_file, read_agent_file,
|
init_agent_template, list_agent_files, write_agent_file, read_agent_file,
|
||||||
test_agent_in_sandbox, cp_deploy_tarball, cp_refresh_agent,
|
test_agent_in_sandbox, cp_deploy_tarball, cp_refresh_agent,
|
||||||
list_a2a_pack, read_a2a_pack,
|
list_a2a_pack, read_a2a_pack,
|
||||||
]
|
]
|
||||||
@@ -479,3 +510,37 @@ def _read_entrypoint(bundle: bytes) -> str | None:
|
|||||||
|
|
||||||
def _class_name(slug: str) -> str:
|
def _class_name(slug: str) -> str:
|
||||||
return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent"
|
return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent"
|
||||||
|
|
||||||
|
|
||||||
|
def _render_a2a_init_template(
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
description: str = "A new A2A agent",
|
||||||
|
) -> dict[str, str]:
|
||||||
|
_validate_name(name)
|
||||||
|
class_name = _class_name(name)
|
||||||
|
return {
|
||||||
|
"agent.py": _render_sdk_template(
|
||||||
|
"agent.py.tmpl",
|
||||||
|
name=name,
|
||||||
|
class_name=class_name,
|
||||||
|
description=description,
|
||||||
|
),
|
||||||
|
"a2a.yaml": _render_sdk_template(
|
||||||
|
"a2a.yaml.tmpl",
|
||||||
|
name=name,
|
||||||
|
class_name=class_name,
|
||||||
|
),
|
||||||
|
"requirements.txt": _render_sdk_template("requirements.txt.tmpl"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _render_sdk_template(template: str, /, **vars: str) -> str:
|
||||||
|
text = (
|
||||||
|
resources.files("a2a_pack.cli.templates")
|
||||||
|
.joinpath(template)
|
||||||
|
.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
for key, value in vars.items():
|
||||||
|
text = text.replace("{{ " + key + " }}", value)
|
||||||
|
return text
|
||||||
|
|||||||
27
tests/test_template_init.py
Normal file
27
tests/test_template_init.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from agent_builder.tools import _render_a2a_init_template
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateInitTests(unittest.TestCase):
|
||||||
|
def test_render_a2a_init_template_matches_sdk_starter(self) -> None:
|
||||||
|
files = _render_a2a_init_template(
|
||||||
|
"research-agent",
|
||||||
|
description="Research helper",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(set(files), {"agent.py", "a2a.yaml", "requirements.txt"})
|
||||||
|
self.assertIn('name = "research-agent"', files["agent.py"])
|
||||||
|
self.assertIn('description = "Research helper"', files["agent.py"])
|
||||||
|
self.assertIn("LLMProvisioning.CALLER_PROVIDED", files["agent.py"])
|
||||||
|
self.assertIn("ctx.llm", files["agent.py"])
|
||||||
|
self.assertIn("create_deep_agent", files["agent.py"])
|
||||||
|
self.assertIn("name: research-agent", files["a2a.yaml"])
|
||||||
|
self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"])
|
||||||
|
self.assertIn("deepagents>=0.5.0", files["requirements.txt"])
|
||||||
|
|
||||||
|
def test_render_a2a_init_template_rejects_invalid_names(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_render_a2a_init_template("Bad Name")
|
||||||
Reference in New Issue
Block a user