ag-ui-a2ui-toolkit 0.0.1a0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ag-ui-a2ui-toolkit
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: Framework-agnostic helpers for building A2UI subagent tools — op builders, prompt assembly, history walkers, and validation against Google's a2ui-agent-sdk.
|
|
5
|
+
Author: Ran Shem Tov
|
|
6
|
+
Author-email: Ran Shem Tov <ran@copilotkit.ai>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.10, <3.15
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# ag-ui-a2ui-toolkit
|
|
12
|
+
|
|
13
|
+
Framework-agnostic helpers for building A2UI subagent tools.
|
|
14
|
+
|
|
15
|
+
Each per-framework adapter (LangGraph, ADK, Mastra, …) composes these helpers
|
|
16
|
+
with its own framework-specific glue: tool decorator, runtime accessor, model
|
|
17
|
+
binding + invoke. Nothing in this package depends on any agent framework.
|
|
18
|
+
|
|
19
|
+
## Surface
|
|
20
|
+
|
|
21
|
+
- Constants: `A2UI_OPERATIONS_KEY`, `BASIC_CATALOG_ID`
|
|
22
|
+
- Op builders: `create_surface`, `update_components`, `update_data_model`
|
|
23
|
+
- `RENDER_A2UI_TOOL_DEF`
|
|
24
|
+
- State + history helpers: `build_context_prompt`, `find_prior_surface`
|
|
25
|
+
- Prompt composer: `build_subagent_prompt`
|
|
26
|
+
- Output: `assemble_ops`, `wrap_as_operations_envelope`
|
|
27
|
+
|
|
28
|
+
## See also
|
|
29
|
+
|
|
30
|
+
The TypeScript counterpart lives in
|
|
31
|
+
[`@ag-ui/a2ui-toolkit`](../../typescript/packages/a2ui-toolkit) and exposes the
|
|
32
|
+
same surface in camelCase.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ag-ui-a2ui-toolkit
|
|
2
|
+
|
|
3
|
+
Framework-agnostic helpers for building A2UI subagent tools.
|
|
4
|
+
|
|
5
|
+
Each per-framework adapter (LangGraph, ADK, Mastra, …) composes these helpers
|
|
6
|
+
with its own framework-specific glue: tool decorator, runtime accessor, model
|
|
7
|
+
binding + invoke. Nothing in this package depends on any agent framework.
|
|
8
|
+
|
|
9
|
+
## Surface
|
|
10
|
+
|
|
11
|
+
- Constants: `A2UI_OPERATIONS_KEY`, `BASIC_CATALOG_ID`
|
|
12
|
+
- Op builders: `create_surface`, `update_components`, `update_data_model`
|
|
13
|
+
- `RENDER_A2UI_TOOL_DEF`
|
|
14
|
+
- State + history helpers: `build_context_prompt`, `find_prior_surface`
|
|
15
|
+
- Prompt composer: `build_subagent_prompt`
|
|
16
|
+
- Output: `assemble_ops`, `wrap_as_operations_envelope`
|
|
17
|
+
|
|
18
|
+
## See also
|
|
19
|
+
|
|
20
|
+
The TypeScript counterpart lives in
|
|
21
|
+
[`@ag-ui/a2ui-toolkit`](../../typescript/packages/a2ui-toolkit) and exposes the
|
|
22
|
+
same surface in camelCase.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ag-ui-a2ui-toolkit
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
Framework-agnostic building blocks for A2UI subagent tools. Each per-
|
|
6
|
+
framework adapter (LangGraph, ADK, Mastra, …) composes these helpers with its
|
|
7
|
+
framework-specific glue (tool decorator, runtime accessor, model binding +
|
|
8
|
+
invoke). Nothing in this package depends on any agent framework.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any, Optional, TypedDict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"A2UI_OPERATIONS_KEY",
|
|
19
|
+
"BASIC_CATALOG_ID",
|
|
20
|
+
"RENDER_A2UI_TOOL_DEF",
|
|
21
|
+
"create_surface",
|
|
22
|
+
"update_components",
|
|
23
|
+
"update_data_model",
|
|
24
|
+
"build_context_prompt",
|
|
25
|
+
"find_prior_surface",
|
|
26
|
+
"build_subagent_prompt",
|
|
27
|
+
"assemble_ops",
|
|
28
|
+
"wrap_as_operations_envelope",
|
|
29
|
+
"PriorSurface",
|
|
30
|
+
"EditContext",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
A2UI_OPERATIONS_KEY = "a2ui_operations"
|
|
35
|
+
"""Container key the A2UI middleware looks for in tool results."""
|
|
36
|
+
|
|
37
|
+
BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/basic_catalog.json"
|
|
38
|
+
"""Default catalog id used when the subagent does not specify one."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Op builders
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_surface(surface_id: str, catalog_id: str) -> dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
"version": "v0.9",
|
|
49
|
+
"createSurface": {"surfaceId": surface_id, "catalogId": catalog_id},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def update_components(
|
|
54
|
+
surface_id: str, components: list[dict[str, Any]]
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
return {
|
|
57
|
+
"version": "v0.9",
|
|
58
|
+
"updateComponents": {"surfaceId": surface_id, "components": components},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def update_data_model(
|
|
63
|
+
surface_id: str, data: Any, path: str = "/"
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
"version": "v0.9",
|
|
67
|
+
"updateDataModel": {"surfaceId": surface_id, "path": path, "value": data},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Inner render_a2ui tool definition
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
RENDER_A2UI_TOOL_DEF: dict[str, Any] = {
|
|
76
|
+
"type": "function",
|
|
77
|
+
"function": {
|
|
78
|
+
"name": "render_a2ui",
|
|
79
|
+
"description": (
|
|
80
|
+
"Render a dynamic A2UI v0.9 surface. The root component must have "
|
|
81
|
+
"id 'root'. Use components from the available catalog only."
|
|
82
|
+
),
|
|
83
|
+
"parameters": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"surfaceId": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Unique surface identifier.",
|
|
89
|
+
},
|
|
90
|
+
"catalogId": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "The catalog id for the component catalog.",
|
|
93
|
+
},
|
|
94
|
+
"components": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"description": (
|
|
97
|
+
"A2UI v0.9 component array (flat format). The root "
|
|
98
|
+
"component must have id 'root'."
|
|
99
|
+
),
|
|
100
|
+
"items": {"type": "object"},
|
|
101
|
+
},
|
|
102
|
+
"data": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"description": (
|
|
105
|
+
"Optional initial data model for the surface (form "
|
|
106
|
+
"values, list items for data-bound components, etc.)."
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
"required": ["surfaceId", "components"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
"""JSON schema for the inner ``render_a2ui`` tool the subagent is forced to call."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# State helpers
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def build_context_prompt(state: dict) -> str:
|
|
123
|
+
"""Assemble the prompt prefix from AG-UI state context entries + the A2UI
|
|
124
|
+
component catalog.
|
|
125
|
+
|
|
126
|
+
Framework integrations conventionally extract the catalog into
|
|
127
|
+
``state["ag-ui"]["a2ui_schema"]`` and forward other context entries
|
|
128
|
+
(generation guidelines, design guidelines) under
|
|
129
|
+
``state["ag-ui"]["context"]``.
|
|
130
|
+
"""
|
|
131
|
+
ag_ui = state.get("ag-ui", {}) or {}
|
|
132
|
+
parts: list[str] = []
|
|
133
|
+
|
|
134
|
+
for entry in ag_ui.get("context", []) or []:
|
|
135
|
+
if isinstance(entry, dict):
|
|
136
|
+
desc = entry.get("description")
|
|
137
|
+
value = entry.get("value")
|
|
138
|
+
else:
|
|
139
|
+
desc = getattr(entry, "description", None)
|
|
140
|
+
value = getattr(entry, "value", None)
|
|
141
|
+
if desc:
|
|
142
|
+
parts.append(f"## {desc}\n{value}\n")
|
|
143
|
+
elif value:
|
|
144
|
+
parts.append(f"{value}\n")
|
|
145
|
+
|
|
146
|
+
a2ui_schema = ag_ui.get("a2ui_schema")
|
|
147
|
+
if a2ui_schema:
|
|
148
|
+
parts.append(f"## Available Components\n{a2ui_schema}\n")
|
|
149
|
+
|
|
150
|
+
return "\n".join(parts)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Prior surface lookup (used for intent="update")
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class PriorSurface(TypedDict, total=False):
|
|
159
|
+
components: list[dict[str, Any]]
|
|
160
|
+
data: Any
|
|
161
|
+
catalogId: Optional[str]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def find_prior_surface(
|
|
165
|
+
messages: list[Any], surface_id: str
|
|
166
|
+
) -> Optional[PriorSurface]:
|
|
167
|
+
"""Locate the most recent rendered state for ``surface_id`` in message history.
|
|
168
|
+
|
|
169
|
+
Walks backwards looking for a ``ToolMessage``-shaped entry whose content is
|
|
170
|
+
a JSON string containing ``a2ui_operations`` for the given surface.
|
|
171
|
+
Returns the reconstructed ``{"components": [...], "data": ..., "catalogId": ...}``
|
|
172
|
+
or ``None`` if no matching surface is found.
|
|
173
|
+
"""
|
|
174
|
+
for msg in reversed(messages):
|
|
175
|
+
role = getattr(msg, "type", None) or getattr(msg, "role", None)
|
|
176
|
+
if role not in ("tool", "ToolMessage"):
|
|
177
|
+
continue
|
|
178
|
+
content = getattr(msg, "content", None)
|
|
179
|
+
if not isinstance(content, str):
|
|
180
|
+
continue
|
|
181
|
+
try:
|
|
182
|
+
parsed = json.loads(content)
|
|
183
|
+
except (ValueError, TypeError):
|
|
184
|
+
continue
|
|
185
|
+
if not isinstance(parsed, dict):
|
|
186
|
+
continue
|
|
187
|
+
ops = parsed.get(A2UI_OPERATIONS_KEY)
|
|
188
|
+
if not isinstance(ops, list):
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
components: Optional[list[dict[str, Any]]] = None
|
|
192
|
+
data: Any = None
|
|
193
|
+
catalog_id: Optional[str] = None
|
|
194
|
+
matched = False
|
|
195
|
+
for op in ops:
|
|
196
|
+
if not isinstance(op, dict):
|
|
197
|
+
continue
|
|
198
|
+
if "createSurface" in op:
|
|
199
|
+
cs = op["createSurface"]
|
|
200
|
+
if isinstance(cs, dict) and cs.get("surfaceId") == surface_id:
|
|
201
|
+
matched = True
|
|
202
|
+
catalog_id = cs.get("catalogId") or catalog_id
|
|
203
|
+
if "updateComponents" in op:
|
|
204
|
+
uc = op["updateComponents"]
|
|
205
|
+
if isinstance(uc, dict) and uc.get("surfaceId") == surface_id:
|
|
206
|
+
matched = True
|
|
207
|
+
if isinstance(uc.get("components"), list):
|
|
208
|
+
components = uc["components"]
|
|
209
|
+
if "updateDataModel" in op:
|
|
210
|
+
ud = op["updateDataModel"]
|
|
211
|
+
if isinstance(ud, dict) and ud.get("surfaceId") == surface_id:
|
|
212
|
+
matched = True
|
|
213
|
+
data = ud.get("value")
|
|
214
|
+
if matched:
|
|
215
|
+
return {
|
|
216
|
+
"components": components or [],
|
|
217
|
+
"data": data,
|
|
218
|
+
"catalogId": catalog_id,
|
|
219
|
+
}
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# Prompt assembly
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class EditContext(TypedDict, total=False):
|
|
229
|
+
surfaceId: str
|
|
230
|
+
prior: PriorSurface
|
|
231
|
+
changes: Optional[str]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def build_subagent_prompt(
|
|
235
|
+
*,
|
|
236
|
+
context_prompt: str,
|
|
237
|
+
composition_guide: Optional[str] = None,
|
|
238
|
+
edit_context: Optional[EditContext] = None,
|
|
239
|
+
) -> str:
|
|
240
|
+
"""Compose the full subagent system prompt.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
context_prompt: Output of ``build_context_prompt(state)``.
|
|
244
|
+
composition_guide: Project-specific composition rules to append.
|
|
245
|
+
edit_context: When set, instructs the subagent to edit a prior surface
|
|
246
|
+
in place (used by ``intent="update"``).
|
|
247
|
+
"""
|
|
248
|
+
parts: list[str] = []
|
|
249
|
+
if context_prompt:
|
|
250
|
+
parts.append(context_prompt)
|
|
251
|
+
if composition_guide:
|
|
252
|
+
parts.append(composition_guide)
|
|
253
|
+
|
|
254
|
+
if edit_context:
|
|
255
|
+
surface_id = edit_context.get("surfaceId")
|
|
256
|
+
prior = edit_context.get("prior") or {}
|
|
257
|
+
changes = edit_context.get("changes")
|
|
258
|
+
edit_block = (
|
|
259
|
+
"## Editing an existing surface\n"
|
|
260
|
+
f"You are editing surface '{surface_id}'. Produce the FULL "
|
|
261
|
+
f"updated components array and data model — not just a diff. "
|
|
262
|
+
f"Preserve component ids that the user has not asked to change so "
|
|
263
|
+
f"the renderer can reconcile them. Reuse the same catalogId.\n\n"
|
|
264
|
+
f"### Previous components\n"
|
|
265
|
+
f"{json.dumps(prior.get('components', []), indent=2)}\n\n"
|
|
266
|
+
f"### Previous data\n"
|
|
267
|
+
f"{json.dumps(prior.get('data'), indent=2)}\n"
|
|
268
|
+
)
|
|
269
|
+
if changes:
|
|
270
|
+
edit_block += f"\n### Requested changes\n{changes}\n"
|
|
271
|
+
parts.append(edit_block)
|
|
272
|
+
|
|
273
|
+
return "\n".join(p for p in parts if p)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
# Operations envelope
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def assemble_ops(
|
|
282
|
+
*,
|
|
283
|
+
intent: str,
|
|
284
|
+
surface_id: str,
|
|
285
|
+
catalog_id: str,
|
|
286
|
+
components: list[dict[str, Any]],
|
|
287
|
+
data: Optional[dict[str, Any]] = None,
|
|
288
|
+
) -> list[dict[str, Any]]:
|
|
289
|
+
"""Produce the final A2UI v0.9 operation list for a render result.
|
|
290
|
+
|
|
291
|
+
``intent="create"`` emits ``[createSurface, updateComponents, updateDataModel?]``.
|
|
292
|
+
Any other intent (e.g. ``"update"``) skips ``createSurface`` so the
|
|
293
|
+
frontend reconciles the existing surface in place rather than erroring
|
|
294
|
+
(per v0.9 spec, ``createSurface`` on an existing id is invalid).
|
|
295
|
+
"""
|
|
296
|
+
ops: list[dict[str, Any]] = []
|
|
297
|
+
if intent != "update":
|
|
298
|
+
ops.append(create_surface(surface_id, catalog_id))
|
|
299
|
+
ops.append(update_components(surface_id, components))
|
|
300
|
+
if data:
|
|
301
|
+
ops.append(update_data_model(surface_id, data))
|
|
302
|
+
return ops
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def wrap_as_operations_envelope(ops: list[dict[str, Any]]) -> str:
|
|
306
|
+
"""Wrap a list of A2UI operations as the JSON envelope the A2UI middleware
|
|
307
|
+
looks for in tool results."""
|
|
308
|
+
return json.dumps({A2UI_OPERATIONS_KEY: ops})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ag-ui-a2ui-toolkit"
|
|
3
|
+
version = "0.0.1-alpha.0"
|
|
4
|
+
description = "Framework-agnostic helpers for building A2UI subagent tools — op builders, prompt assembly, history walkers, and validation against Google's a2ui-agent-sdk."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Ran Shem Tov", email = "ran@copilotkit.ai" }
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10,<3.15"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["uv_build>=0.8.0,<0.9"]
|
|
15
|
+
build-backend = "uv_build"
|
|
16
|
+
|
|
17
|
+
[tool.uv.build-backend]
|
|
18
|
+
module-root = ""
|
|
19
|
+
module-name = "ag_ui_a2ui_toolkit"
|