deploy
This commit is contained in:
142
.a2a/agent.dsl.json
Normal file
142
.a2a/agent.dsl.json
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "2026-06-04",
|
||||||
|
"language": "python",
|
||||||
|
"name": "quick-demo",
|
||||||
|
"description": "A new A2A agent",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"entrypoint": {
|
||||||
|
"module": "agent",
|
||||||
|
"class_name": "QuickDemo",
|
||||||
|
"function": null,
|
||||||
|
"command": []
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "ask",
|
||||||
|
"description": "Ask the starter DeepAgent to answer with tool calls when useful",
|
||||||
|
"handler": "ask",
|
||||||
|
"tags": [],
|
||||||
|
"scopes": [],
|
||||||
|
"stream": false,
|
||||||
|
"policy": {
|
||||||
|
"timeout_seconds": null,
|
||||||
|
"idempotent": false,
|
||||||
|
"max_retries": 0,
|
||||||
|
"cost_class": null,
|
||||||
|
"allow_scope_expansion": false,
|
||||||
|
"grant_mode": null,
|
||||||
|
"grant_allow_patterns": [],
|
||||||
|
"grant_deny_patterns": [],
|
||||||
|
"grant_outputs_prefix": null,
|
||||||
|
"grant_write_prefixes": [],
|
||||||
|
"grant_ttl_seconds": null,
|
||||||
|
"grant_run_timeout_seconds": null,
|
||||||
|
"grant_approval_timeout_seconds": null,
|
||||||
|
"grant_scope_approval_timeout_seconds": null
|
||||||
|
},
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"prompt": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"prompt"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": {},
|
||||||
|
"input_modes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"output_modes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"required_secrets": [],
|
||||||
|
"required_env": [],
|
||||||
|
"consumer_setup": {
|
||||||
|
"fields": []
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lifecycle": "ephemeral",
|
||||||
|
"availability": "on_demand",
|
||||||
|
"state": "none",
|
||||||
|
"sandbox": "microsandbox",
|
||||||
|
"resources": {
|
||||||
|
"cpu": "100m",
|
||||||
|
"memory": "256Mi",
|
||||||
|
"gpu": 0,
|
||||||
|
"max_runtime_seconds": 600
|
||||||
|
},
|
||||||
|
"concurrency": 1,
|
||||||
|
"egress": {
|
||||||
|
"allow_hosts": [],
|
||||||
|
"allow_internal_services": [],
|
||||||
|
"deny_internet_by_default": true
|
||||||
|
},
|
||||||
|
"tools_used": [
|
||||||
|
"deepagents",
|
||||||
|
"langchain"
|
||||||
|
],
|
||||||
|
"llm_provisioning": "platform",
|
||||||
|
"pricing": {
|
||||||
|
"price_per_call_usd": 0.0,
|
||||||
|
"caller_pays_llm": true,
|
||||||
|
"notes": "Starter agent uses the caller's saved LLM credential via ctx.llm."
|
||||||
|
},
|
||||||
|
"wants_cp_jwt": false,
|
||||||
|
"platform_resources": {
|
||||||
|
"memory": null,
|
||||||
|
"databases": []
|
||||||
|
},
|
||||||
|
"endpoints": [],
|
||||||
|
"apt_packages": []
|
||||||
|
},
|
||||||
|
"template_lineage": null,
|
||||||
|
"meta_agent_manifest": null,
|
||||||
|
"state_schema": null,
|
||||||
|
"workspace_access": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_files": 64,
|
||||||
|
"allowed_modes": [
|
||||||
|
"read_only",
|
||||||
|
"read_write_overlay"
|
||||||
|
],
|
||||||
|
"require_reason": false,
|
||||||
|
"deny_patterns": [],
|
||||||
|
"require_human_approval": false,
|
||||||
|
"max_total_size_bytes": 104857600
|
||||||
|
},
|
||||||
|
"config_schema": {
|
||||||
|
"properties": {},
|
||||||
|
"title": "QuickDemoConfig",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"model": "a2a_pack.auth.NoAuth",
|
||||||
|
"strategy": "public",
|
||||||
|
"principal_schema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Public agent: no caller identity required.",
|
||||||
|
"properties": {},
|
||||||
|
"title": "NoAuth",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"resolver": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"source": "python-a2a-pack",
|
||||||
|
"project_manifest": {
|
||||||
|
"name": "quick-demo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"entrypoint": "agent:QuickDemo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
README.md
Normal file
121
README.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# quick-demo
|
||||||
|
|
||||||
|
A new A2A agent
|
||||||
|
|
||||||
|
This project was scaffolded with `a2a init`. It starts as a DeepAgents-backed
|
||||||
|
A2A agent that uses the caller's saved LLM credential from `ctx.llm`.
|
||||||
|
|
||||||
|
`LLMProvisioning.PLATFORM` and `LLMProvisioning.CALLER_PROVIDED` both read that
|
||||||
|
same `ctx.llm` path. Do not read provider keys, LiteLLM master keys,
|
||||||
|
`OPENAI_API_KEY`, or `A2A_LITELLM_KEY` directly in agent code, and do not
|
||||||
|
substitute fake fallback API keys when `ctx.llm.api_key` is empty.
|
||||||
|
|
||||||
|
## Durable Files
|
||||||
|
|
||||||
|
A2A Cloud workspaces are grant-scoped and backed by MinIO. Files become durable
|
||||||
|
when your agent uses one of the platform-owned file paths:
|
||||||
|
|
||||||
|
- `ctx.workspace` for direct workspace reads and writes
|
||||||
|
- `ctx.write_artifact(...)` for explicit output artifacts
|
||||||
|
- `ctx.sandbox` for commands that read or write files in a sandbox
|
||||||
|
- `ctx.workspace_backend()` for framework file tools such as DeepAgents
|
||||||
|
|
||||||
|
DeepAgents has its own built-in file tools (`write_file`, `read_file`,
|
||||||
|
`edit_file`). Without an A2A backend, those tools write into LangGraph state
|
||||||
|
only, so files can appear to the agent but never reach MinIO or `/workspace`.
|
||||||
|
|
||||||
|
Keep this line when building DeepAgents graphs:
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend = ctx.workspace_backend()
|
||||||
|
return create_a2a_deep_agent(ctx, backend=backend, tools=[...])
|
||||||
|
```
|
||||||
|
|
||||||
|
Invoke DeepAgents graphs with the starter recursion budget:
|
||||||
|
|
||||||
|
```python
|
||||||
|
state = await graph.ainvoke(
|
||||||
|
{"messages": [{"role": "user", "content": prompt}]},
|
||||||
|
config={"recursion_limit": 500},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend respects the caller's grant. In handoffs, generated files should go
|
||||||
|
through `ctx.workspace_backend()`, `ctx.write_artifact(...)`, or a sandbox helper
|
||||||
|
so they are mirrored to the caller workspace instead of becoming private virtual
|
||||||
|
files.
|
||||||
|
|
||||||
|
For stable, human-readable paths, write intentional outputs to
|
||||||
|
`/workspace/outputs/...` or `ctx.write_artifact(...)`. If sandboxed code writes
|
||||||
|
to process-local paths such as `/tmp/result.csv`, `/root`, or `/app`, the
|
||||||
|
platform captures changed rootfs files under `outputs/rootfs-captures/...` so
|
||||||
|
the caller can still download and inspect them.
|
||||||
|
|
||||||
|
When a skill needs to run real code, render media, convert files, or call a
|
||||||
|
CLI that writes outputs, use the workspace-mounted sandbox helpers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await ctx.workspace_shell(
|
||||||
|
"python script.py --out /tmp/result.txt",
|
||||||
|
image="python:3.11-slim",
|
||||||
|
timeout_seconds=120,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not rely on `asyncio.create_subprocess_exec(...)` for durable outputs. A
|
||||||
|
plain subprocess runs in the agent container, which is not mounted to the
|
||||||
|
caller workspace; files it creates in `/tmp` or the image filesystem can vanish
|
||||||
|
after the request. Use the sandbox helpers for any file-producing toolchain.
|
||||||
|
|
||||||
|
## DeepAgents Skills
|
||||||
|
|
||||||
|
If this project grows reusable workflow knowledge, add source-controlled skill
|
||||||
|
folders under `skills/<skill-name>/SKILL.md`. The starter's `_seed_runtime_skills`
|
||||||
|
helper copies those packaged skills into the invocation workspace and passes the
|
||||||
|
resulting source path to `create_a2a_deep_agent(..., skills=[...])`, which is
|
||||||
|
how DeepAgents discovers skills with progressive disclosure.
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
a2a dev
|
||||||
|
a2a test
|
||||||
|
a2a test --invoke --skill summarize --args-json '{"text":"hello"}'
|
||||||
|
a2a card
|
||||||
|
```
|
||||||
|
|
||||||
|
`a2a dev` loads `.env.local`, creates `.a2a/workspace/{inputs,outputs}`, serves
|
||||||
|
the same HTTP invoke/card/MCP endpoints as production, and hot reloads local
|
||||||
|
code. Files written through `ctx.workspace_backend()` land under
|
||||||
|
`.a2a/workspace/outputs` before you deploy.
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
The template is public by default (`auth_model = NoAuth`). To require the
|
||||||
|
caller's app login, declare a typed auth model and resolver in `agent.py`.
|
||||||
|
Resolvers receive the inbound bearer token and return the principal exposed as
|
||||||
|
`ctx.auth`.
|
||||||
|
|
||||||
|
Hosted browser/direct invokes that use `LLMProvisioning.PLATFORM` still need an
|
||||||
|
A2A Cloud session so the runtime can mint a short-lived LLM/workspace grant for
|
||||||
|
that user. Agent-to-agent handoffs pass that grant explicitly.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from a2a_pack import JWTAuth, OIDCUserInfoAuthResolver
|
||||||
|
|
||||||
|
auth_model = JWTAuth
|
||||||
|
auth_resolver = OIDCUserInfoAuthResolver(
|
||||||
|
"https://auth.example.com/oauth2/userinfo",
|
||||||
|
auth_model=JWTAuth,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For homegrown auth or SAML-backed apps, expose a bearer-token `/me` or
|
||||||
|
`/introspect` endpoint and use the same resolver contract.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
a2a deploy
|
||||||
|
```
|
||||||
20
a2a.yaml
Normal file
20
a2a.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Project identity for `a2a deploy`. Most metadata (resources, scopes,
|
||||||
|
# secrets, workspace, etc.) lives on the Python class — this file only
|
||||||
|
# tells the CLI how to find it.
|
||||||
|
name: quick-demo
|
||||||
|
version: 0.1.0
|
||||||
|
entrypoint: agent:QuickDemo
|
||||||
|
expose:
|
||||||
|
public: true
|
||||||
|
# Optional platform resources:
|
||||||
|
# resources:
|
||||||
|
# memory:
|
||||||
|
# tiers: [kv, vector]
|
||||||
|
# namespace: quick-demo
|
||||||
|
# databases:
|
||||||
|
# - name: app
|
||||||
|
# provider: neon
|
||||||
|
# engine: postgres
|
||||||
|
# env:
|
||||||
|
# url: DATABASE_URL
|
||||||
|
|
||||||
189
agent.py
Normal file
189
agent.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"""quick-demo agent.
|
||||||
|
|
||||||
|
Starter stack:
|
||||||
|
- DeepAgents for tool-calling orchestration
|
||||||
|
- Caller-provided LLM credentials via ctx.llm
|
||||||
|
- A tiny model-call middleware hook you can replace with tracing,
|
||||||
|
routing, rate limits, or policy checks
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from a2a_pack import (
|
||||||
|
A2AAgent,
|
||||||
|
LLMProvisioning,
|
||||||
|
NoAuth,
|
||||||
|
Pricing,
|
||||||
|
RunContext,
|
||||||
|
WorkspaceAccess,
|
||||||
|
WorkspaceMode,
|
||||||
|
skill,
|
||||||
|
)
|
||||||
|
from a2a_pack.context import LLMCreds
|
||||||
|
|
||||||
|
|
||||||
|
class QuickDemoConfig(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """\
|
||||||
|
You are a compact tool-calling agent.
|
||||||
|
|
||||||
|
Use the text_stats tool when the user asks about text, counts, summaries,
|
||||||
|
or anything where exact length/word numbers would help. Mention tool results
|
||||||
|
briefly instead of dumping raw JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RUNTIME_SKILLS_DIR = "quick-demo/.deepagents/skills/"
|
||||||
|
DEEPAGENTS_RECURSION_LIMIT = 500
|
||||||
|
|
||||||
|
|
||||||
|
class QuickDemo(A2AAgent[QuickDemoConfig, NoAuth]):
|
||||||
|
name = "quick-demo"
|
||||||
|
description = "A new A2A agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
config_model = QuickDemoConfig
|
||||||
|
auth_model = NoAuth
|
||||||
|
|
||||||
|
# Hosted generated agents read the caller's saved LLM credential through
|
||||||
|
# ctx.llm. The platform may proxy that credential through LiteLLM, but agent
|
||||||
|
# code never reads provider keys, LiteLLM master keys, or OPENAI_API_KEY
|
||||||
|
# directly.
|
||||||
|
llm_provisioning = LLMProvisioning.PLATFORM
|
||||||
|
pricing = Pricing(
|
||||||
|
price_per_call_usd=0.0,
|
||||||
|
caller_pays_llm=True,
|
||||||
|
notes="Starter agent uses the caller's saved LLM credential via ctx.llm.",
|
||||||
|
)
|
||||||
|
workspace_access = WorkspaceAccess.dynamic(
|
||||||
|
max_files=64,
|
||||||
|
allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
|
||||||
|
require_reason=False,
|
||||||
|
)
|
||||||
|
tools_used = ("deepagents", "langchain")
|
||||||
|
|
||||||
|
@skill(description="Ask the starter DeepAgent to answer with tool calls when useful")
|
||||||
|
async def ask(self, ctx: RunContext[NoAuth], prompt: str) -> str:
|
||||||
|
creds = ctx.llm
|
||||||
|
await ctx.emit_progress(f"llm: {creds.model} via {creds.source}")
|
||||||
|
if not creds.api_key:
|
||||||
|
return (
|
||||||
|
"LLM key required. Add an LLM credential in Settings > LLM "
|
||||||
|
"credentials before running this agent; for local --invoke "
|
||||||
|
"runs set AGENT_LLM_KEY."
|
||||||
|
)
|
||||||
|
graph = self._build_deep_agent(ctx=ctx, creds=creds)
|
||||||
|
state = await graph.ainvoke(
|
||||||
|
{"messages": [{"role": "user", "content": prompt}]},
|
||||||
|
config={"recursion_limit": DEEPAGENTS_RECURSION_LIMIT},
|
||||||
|
)
|
||||||
|
await ctx.emit_progress("deepagent finished")
|
||||||
|
return _last_message_text(state)
|
||||||
|
|
||||||
|
def _build_deep_agent(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
ctx: RunContext[NoAuth],
|
||||||
|
creds: LLMCreds,
|
||||||
|
) -> Any:
|
||||||
|
# Lazy imports keep `a2a card` usable before local dependencies are
|
||||||
|
# installed. `a2a deploy` installs requirements.txt during the build.
|
||||||
|
from a2a_pack.deepagents import create_a2a_deep_agent
|
||||||
|
from langchain.agents.middleware import wrap_model_call
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def text_stats(text: str) -> str:
|
||||||
|
"""Return exact word, character, and line counts for text."""
|
||||||
|
words = [part for part in text.split() if part.strip()]
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"characters": len(text),
|
||||||
|
"words": len(words),
|
||||||
|
"lines": len(text.splitlines()) or 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@wrap_model_call
|
||||||
|
async def log_model_call(request: Any, handler: Any) -> Any:
|
||||||
|
messages = request.state.get("messages", [])
|
||||||
|
print(
|
||||||
|
"[middleware] model_call "
|
||||||
|
f"model={creds.model} source={creds.source} messages={len(messages)}"
|
||||||
|
)
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
backend = ctx.workspace_backend()
|
||||||
|
skill_sources = _seed_runtime_skills(backend, ctx)
|
||||||
|
# create_a2a_deep_agent resolves provider:model strings with
|
||||||
|
# langchain.init_chat_model from ctx.llm, preserving LiteLLM routing,
|
||||||
|
# provider-specific extra body, and runtime model overrides.
|
||||||
|
return create_a2a_deep_agent(
|
||||||
|
ctx,
|
||||||
|
creds=creds,
|
||||||
|
backend=backend,
|
||||||
|
skills=skill_sources or None,
|
||||||
|
tools=[text_stats],
|
||||||
|
middleware=[log_model_call],
|
||||||
|
system_prompt=SYSTEM_PROMPT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_skills_root(ctx: RunContext[Any]) -> str:
|
||||||
|
workspace = getattr(ctx, "_workspace", None)
|
||||||
|
prefixes = tuple(getattr(workspace, "write_prefixes", ()) or ())
|
||||||
|
if not prefixes:
|
||||||
|
outputs_prefix = getattr(workspace, "outputs_prefix", None)
|
||||||
|
prefixes = (outputs_prefix or "outputs/",)
|
||||||
|
prefix = str(prefixes[0]).strip("/")
|
||||||
|
return f"/{prefix}/{RUNTIME_SKILLS_DIR}" if prefix else f"/{RUNTIME_SKILLS_DIR}"
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_runtime_skills(backend: Any, ctx: RunContext[Any]) -> list[str]:
|
||||||
|
"""Copy packaged DeepAgents skills into the invocation workspace.
|
||||||
|
|
||||||
|
DeepAgents loads skills from its backend, while source-controlled
|
||||||
|
``skills/`` folders live in the image. This bridge lets generated agents
|
||||||
|
ship reusable SKILL.md bundles without giving up durable A2A workspace
|
||||||
|
files.
|
||||||
|
"""
|
||||||
|
root = Path(__file__).parent / "skills"
|
||||||
|
if not root.exists():
|
||||||
|
return []
|
||||||
|
runtime_skills_root = _runtime_skills_root(ctx)
|
||||||
|
uploads: list[tuple[str, bytes]] = []
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if path.is_file():
|
||||||
|
rel = path.relative_to(root).as_posix()
|
||||||
|
uploads.append((runtime_skills_root + rel, path.read_bytes()))
|
||||||
|
if uploads:
|
||||||
|
backend.upload_files(uploads)
|
||||||
|
return [runtime_skills_root]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _last_message_text(state: dict[str, Any]) -> str:
|
||||||
|
messages = state.get("messages") or []
|
||||||
|
if not messages:
|
||||||
|
return json.dumps(state, default=str)
|
||||||
|
|
||||||
|
content = getattr(messages[-1], "content", None)
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts: list[str] = []
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
text = item.get("text") or item.get("content")
|
||||||
|
if text:
|
||||||
|
parts.append(str(text))
|
||||||
|
elif item:
|
||||||
|
parts.append(str(item))
|
||||||
|
return "\n".join(parts) if parts else json.dumps(content, default=str)
|
||||||
|
return str(content or messages[-1])
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# a2a-pack is auto-installed by the deploy build.
|
||||||
|
# These starter deps power the DeepAgents tool-calling example in agent.py.
|
||||||
|
deepagents>=0.5.0
|
||||||
|
langchain>=0.3
|
||||||
|
langchain-openai>=0.2
|
||||||
|
langgraph>=0.6
|
||||||
Reference in New Issue
Block a user