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"