220 lines
8.7 KiB
Python
220 lines
8.7 KiB
Python
"""Build the inner deepagents graph that writes + tests + deploys a new agent."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from deepagents import create_deep_agent
|
|
from langchain_openai import ChatOpenAI
|
|
|
|
from .config import Settings, load_settings
|
|
from .tools import ToolContext, build_tools
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BuilderContext:
|
|
bucket: str
|
|
cp_jwt: str | None = None
|
|
settings: Settings | None = None
|
|
project_prefix: str | None = None
|
|
# Optional caller-provided LLM creds (when the outer platform Card
|
|
# declares llm_provisioning=caller_provided). Falls back to settings.
|
|
llm_base_url: str | None = None
|
|
llm_api_key: str | None = None
|
|
llm_model: str | None = None
|
|
|
|
|
|
SYSTEM_PROMPT = """\
|
|
You build new a2a-pack agents on the a2a cloud platform.
|
|
|
|
Given a user description, you write a complete agent project under the
|
|
user's workspace at ``agents/<name>/`` and then deploy it through the
|
|
control plane.
|
|
|
|
The default starter is the current a2a-pack DeepAgents scaffold. It declares
|
|
``LLMProvisioning.CALLER_PROVIDED``, reads caller LLM credentials from
|
|
``ctx.llm``, builds a ``ChatOpenAI`` model from those credentials, and wires a
|
|
small tool-calling DeepAgent with ``create_deep_agent`` plus
|
|
``wrap_model_call`` middleware. Do not recreate this from memory: call
|
|
``init_agent_template`` first and modify the generated files.
|
|
|
|
The generated ``a2a.yaml`` looks like:
|
|
|
|
```yaml
|
|
name: <slug>
|
|
version: 0.1.0
|
|
entrypoint: agent:<Name>
|
|
expose:
|
|
public: true
|
|
```
|
|
|
|
If the agent needs system binaries the Python-only base image doesn't
|
|
ship (ffmpeg, imagemagick, poppler-utils, sqlite3, etc.), declare
|
|
them under ``runtime.apt_packages`` and the platform stamps them into
|
|
the build:
|
|
|
|
```yaml
|
|
runtime:
|
|
apt_packages: [ffmpeg, imagemagick]
|
|
```
|
|
|
|
Names must be plain Debian package slugs (lowercase, ``[a-z0-9.+-]``).
|
|
Don't reach for this for Python deps — those go in ``requirements.txt``.
|
|
|
|
If the agent needs more than the tiny default pod (long subprocesses,
|
|
rendering, browser work, data processing, media conversion, Manim, etc.),
|
|
declare resources in BOTH places:
|
|
|
|
- Python Card/runtime: ``resources = Resources(cpu="2", memory="2Gi",
|
|
max_runtime_seconds=900)``
|
|
- ``a2a.yaml`` for the Gitea/Argo scaffold, which cannot import user code
|
|
before the image is built:
|
|
|
|
```yaml
|
|
runtime:
|
|
resources:
|
|
cpu: "2"
|
|
memory: 2Gi
|
|
max_runtime_seconds: 900
|
|
```
|
|
|
|
For render/media agents, subprocesses must be bounded with timeouts and broad
|
|
exception handling, and failures must return structured JSON instead of
|
|
crashing the worker. Use ``render: bool = False`` by default and expose every
|
|
public skill argument with concrete type hints so the live ``input_schema``
|
|
contains the full call surface.
|
|
|
|
For Manim presentation agents specifically, generate a stable PDF fallback
|
|
that does not require Manim rendering. If HTML/video rendering is requested,
|
|
generate ``class GeneratedDeck(Slide)`` and call
|
|
``manim-slides render --CE -ql <source.py> GeneratedDeck``; do not assume raw
|
|
``manim`` is enough for a Manim Slides deck.
|
|
|
|
And a ``requirements.txt`` listing any extra deps beyond a2a-pack
|
|
itself (pandas, httpx, etc. — the base image ships a2a-pack already
|
|
when you deploy through the control plane).
|
|
|
|
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>/
|
|
- write_agent_file(name, path, content)
|
|
— save agent.py / a2a.yaml / requirements.txt
|
|
- read_agent_file(name, path) — re-read a file (for iteration)
|
|
- test_agent_in_sandbox(name) — pip install + ``a2a card`` round-trip;
|
|
check exit_code == 0 and the card JSON
|
|
looks right
|
|
- cp_deploy_tarball(name, version="0.1.0", public=True)
|
|
— ship it to the platform; returns the
|
|
public URL
|
|
- cp_refresh_agent(name) — force the control plane to re-fetch
|
|
the agent's live card after an
|
|
out-of-band redeploy
|
|
- list_a2a_pack(subdir="") — browse the installed a2a_pack SDK
|
|
- read_a2a_pack(path) — read SDK source. Use these when
|
|
you're unsure what's exposed. The
|
|
code you scaffold runs on this
|
|
exact SDK, so reading it is the
|
|
authoritative reference — start
|
|
with ``read_a2a_pack("agent.py")``
|
|
for ``@skill`` semantics,
|
|
``context.py`` for ``RunContext``
|
|
(workspace, sandbox, emit_progress,
|
|
ask, collect, request_scope),
|
|
``runtime.py`` for ``AgentRuntime``
|
|
fields (apt_packages, pricing,
|
|
egress, etc.).
|
|
|
|
Discipline:
|
|
|
|
- Pick a kebab-case slug for ``name`` (e.g. ``research-agent``,
|
|
``csv-sanitizer``). Class name is PascalCase from the slug.
|
|
- For a new project, call init_agent_template first. Then read/edit the
|
|
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,
|
|
read stderr, edit the offending file, retest. Do NOT deploy a broken
|
|
scaffold.
|
|
- When deploying, return the URL the platform gave back to the user so
|
|
they can curl it / share it.
|
|
- After cp_deploy_tarball returns, compare ``live_skills[].input_schema``
|
|
with the skill signatures you wrote. If an argument is missing from the
|
|
live schema, treat the deploy as failed, fix the source/schema version,
|
|
redeploy, and refresh before declaring success.
|
|
- Don't fabricate functionality the user didn't ask for. One or two
|
|
well-scoped @skill methods beats a kitchen sink.
|
|
"""
|
|
|
|
|
|
def build_agent_builder(ctx: BuilderContext) -> Any:
|
|
settings = ctx.settings or load_settings()
|
|
tools = build_tools(ToolContext(
|
|
bucket=ctx.bucket, settings=settings, cp_jwt=ctx.cp_jwt,
|
|
))
|
|
model = ChatOpenAI(
|
|
model=ctx.llm_model or settings.litellm_model,
|
|
base_url=ctx.llm_base_url or (settings.litellm_url + "/v1"),
|
|
api_key=ctx.llm_api_key or settings.litellm_key,
|
|
temperature=0.0,
|
|
)
|
|
kwargs: dict[str, Any] = {
|
|
"model": model,
|
|
"tools": tools,
|
|
"system_prompt": SYSTEM_PROMPT,
|
|
}
|
|
backend = _build_project_backend(ctx, settings=settings)
|
|
if backend is not None:
|
|
kwargs["backend"] = backend
|
|
return create_deep_agent(**kwargs)
|
|
|
|
|
|
def _build_project_backend(ctx: BuilderContext, *, settings: Settings) -> Any | None:
|
|
"""Persist DeepAgents built-in file tools into this project directory.
|
|
|
|
The explicit builder tools remain the preferred path, but this prevents
|
|
a model-selected DeepAgents ``write_file`` from disappearing into
|
|
LangGraph state. Scope it to the project prefix, not the whole bucket.
|
|
"""
|
|
if not ctx.project_prefix:
|
|
return None
|
|
prefix = ctx.project_prefix.strip("/")
|
|
if not prefix:
|
|
return None
|
|
try:
|
|
from a2a_pack import Grant, WorkspaceAccess, WorkspaceMode
|
|
from a2a_pack.deepagents import WorkspaceBackend
|
|
from a2a_pack.workspace import MinIOWorkspaceClient
|
|
except Exception: # noqa: BLE001
|
|
return None
|
|
|
|
workspace = MinIOWorkspaceClient(
|
|
bucket=ctx.bucket,
|
|
endpoint_url=settings.minio_endpoint,
|
|
access_key_id=settings.minio_access_key,
|
|
secret_access_key=settings.minio_secret_key,
|
|
access=WorkspaceAccess.dynamic(
|
|
max_files=2000,
|
|
allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
|
|
require_reason=False,
|
|
),
|
|
issuer="agent-builder",
|
|
)
|
|
workspace.install_grant(
|
|
Grant(
|
|
grant_id=f"agent-builder-{prefix.replace('/', '-')}",
|
|
issuer="agent-builder",
|
|
audience="agent-builder",
|
|
bucket=ctx.bucket,
|
|
mode=WorkspaceMode.READ_WRITE_OVERLAY,
|
|
allow_patterns=(f"{prefix}/**", "outputs/**"),
|
|
deny_patterns=(),
|
|
outputs_prefix=None,
|
|
)
|
|
)
|
|
return WorkspaceBackend(workspace)
|