Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const environment = {
production: false,
langGraphApiUrl: 'http://localhost:4511/api',
a2uiAssistantId: 'c-a2ui',
a2uiAssistantId: 'a2ui_form',
};
2 changes: 1 addition & 1 deletion cockpit/chat/a2ui/angular/src/environments/environment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const environment = {
production: true,
langGraphApiUrl: '/api',
a2uiAssistantId: 'c-a2ui',
a2uiAssistantId: 'a2ui_form',
};
123 changes: 64 additions & 59 deletions cockpit/langgraph/streaming/python/src/a2ui_graph.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""
A2UI Contact Form Graph

Demonstrates the A2UI (Agent-to-UI) protocol by streaming JSONL that
builds an interactive contact form on the Angular frontend.
Demonstrates the A2UI (Agent-to-UI) protocol by emitting hardcoded JSONL
that builds an interactive contact form on the Angular frontend.
Uses the v0.9 envelope format: {"createSurface": {...}}.

The graph does NOT use an LLM for UI generation — A2UI JSONL requires
exact format adherence that LLMs cannot reliably provide. The LLM is
only used for conversational responses to form submission events.
"""

import json
Expand All @@ -11,83 +16,83 @@

A2UI_PREFIX = "---a2ui_JSON---"

# v0.9 envelope format: each message is {"<type>": {<payload>}}
CONTACT_FORM_JSONL = A2UI_PREFIX + "\n" + "\n".join([
json.dumps({"type": "createSurface", "surfaceId": "contact", "catalogId": "basic"}),
json.dumps({"type": "updateDataModel", "surfaceId": "contact", "value": {
"name": "", "email": "", "department": "Engineering", "consent": False,
json.dumps({"createSurface": {
"surfaceId": "contact", "catalogId": "basic", "sendDataModel": True,
}}),
json.dumps({"updateDataModel": {
"surfaceId": "contact",
"value": {"name": "", "email": "", "department": "Engineering", "consent": False},
}}),
json.dumps({"updateComponents": {
"surfaceId": "contact",
"components": [
{"id": "root", "component": "Column", "children": ["card"]},
{"id": "card", "component": "Card", "title": "Contact Us", "children": [
"name_field", "email_field", "dept_picker", "consent_check", "divider", "submit_btn",
]},
{"id": "name_field", "component": "TextField",
"label": "Name", "value": {"path": "/name"}, "placeholder": "Your full name",
"checks": [
{"condition": {"call": "required", "args": {"value": {"path": "/name"}}},
"message": "Name is required"},
]},
{"id": "email_field", "component": "TextField",
"label": "Email", "value": {"path": "/email"}, "placeholder": "you@company.com",
"checks": [
{"condition": {"call": "required", "args": {"value": {"path": "/email"}}},
"message": "Email is required"},
{"condition": {"call": "email", "args": {"value": {"path": "/email"}}},
"message": "Must be a valid email address"},
]},
{"id": "dept_picker", "component": "ChoicePicker",
"label": "Department",
"options": ["Engineering", "Sales", "Support", "Marketing"],
"selected": {"path": "/department"}},
{"id": "consent_check", "component": "CheckBox",
"label": "I agree to be contacted", "checked": {"path": "/consent"}},
{"id": "divider", "component": "Divider"},
{"id": "submit_btn", "component": "Button",
"label": "Submit",
"checks": [
{"condition": {"call": "and", "args": {"values": [
{"call": "required", "args": {"value": {"path": "/name"}}},
{"call": "email", "args": {"value": {"path": "/email"}}},
{"path": "/consent"},
]}},
"message": "Complete all required fields and agree to be contacted"},
],
"action": {"event": {"name": "formSubmit", "context": {"formId": "contact"}}}},
],
}}),
json.dumps({"type": "updateComponents", "surfaceId": "contact", "components": [
{"id": "root", "component": "Column", "children": ["card"]},
{"id": "card", "component": "Card", "title": "Contact Us", "children": [
"name_field", "email_field", "dept_picker", "consent_check", "divider", "submit_btn",
]},
{"id": "name_field", "component": "TextField",
"label": "Name", "value": {"path": "/name"}, "placeholder": "Your full name",
"_bindings": {"value": "/name"},
"checks": [
{"condition": {"call": "required", "args": {"value": {"path": "/name"}}},
"message": "Name is required"},
]},
{"id": "email_field", "component": "TextField",
"label": "Email", "value": {"path": "/email"}, "placeholder": "you@company.com",
"_bindings": {"value": "/email"},
"checks": [
{"condition": {"call": "required", "args": {"value": {"path": "/email"}}},
"message": "Email is required"},
{"condition": {"call": "email", "args": {"value": {"path": "/email"}}},
"message": "Must be a valid email address"},
]},
{"id": "dept_picker", "component": "ChoicePicker",
"label": "Department",
"options": ["Engineering", "Sales", "Support", "Marketing"],
"selected": {"path": "/department"},
"_bindings": {"selected": "/department"}},
{"id": "consent_check", "component": "CheckBox",
"label": "I agree to be contacted", "checked": {"path": "/consent"},
"_bindings": {"checked": "/consent"}},
{"id": "divider", "component": "Divider"},
{"id": "submit_btn", "component": "Button",
"label": "Submit",
"checks": [
{"condition": {"call": "and", "args": {"values": [
{"call": "required", "args": {"value": {"path": "/name"}}},
{"call": "email", "args": {"value": {"path": "/email"}}},
{"path": "/consent"},
]}},
"message": "Complete all required fields and agree to be contacted"},
],
"action": {"event": {"name": "formSubmit", "context": {"formId": "contact"}}}},
]}),
])


def build_a2ui_graph():
"""
Two-node graph:
- create_form: emits the A2UI contact form surface
- handle_event: responds to form submission events
Single-node graph:
- On first message: emits hardcoded A2UI JSONL for the contact form
- On a2ui_event messages: responds with a confirmation message
"""

async def create_form(state: MessagesState) -> dict:
last = state["messages"][-1]

# If this is an a2ui_event, route to event handling
# Check if this is a form submission event from the A2UI surface
try:
payload = json.loads(last.content)
if isinstance(payload, dict) and payload.get("type") == "a2ui_event":
return await handle_event(state, payload)
name = payload.get("context", {}).get("formId", "unknown")
return {"messages": [AIMessage(
content=f"Thanks for submitting the **{name}** form! We'll be in touch soon.",
)]}
except (json.JSONDecodeError, AttributeError):
pass

# First message — emit the contact form
# Any other message — emit the contact form
return {"messages": [AIMessage(content=CONTACT_FORM_JSONL)]}

async def handle_event(state: MessagesState, payload: dict) -> dict:
name = payload.get("context", {}).get("formId", "unknown")
return {"messages": [AIMessage(
content=f"Thanks for submitting the **{name}** form! We'll be in touch soon.",
)]}

graph = StateGraph(MessagesState)
graph.add_node("create_form", create_form)
graph.set_entry_point("create_form")
Expand Down
25 changes: 13 additions & 12 deletions scripts/examples-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,19 @@ const PATH_TO_KEY: Record<string, string> = {
'deep-agents/memory': 'da-memory',
'deep-agents/skills': 'skills',
'deep-agents/sandboxes': 'sandboxes',
// Chat capabilities
'chat/a2ui': 'c-a2ui',
'chat/debug': 'c-debug',
'chat/generative-ui': 'c-generative-ui',
'chat/input': 'c-input',
'chat/interrupts': 'c-interrupts',
'chat/messages': 'c-messages',
'chat/subagents': 'c-subagents',
'chat/theming': 'c-theming',
'chat/threads': 'c-threads',
'chat/timeline': 'c-timeline',
'chat/tool-calls': 'c-tool-calls',
// Chat capabilities — routed through the streaming deployment which has
// all chat graphs consolidated in its langgraph.json (PR #113).
'chat/a2ui': 'streaming',
'chat/debug': 'streaming',
'chat/generative-ui': 'streaming',
'chat/input': 'streaming',
'chat/interrupts': 'streaming',
'chat/messages': 'streaming',
'chat/subagents': 'streaming',
'chat/theming': 'streaming',
'chat/threads': 'streaming',
'chat/timeline': 'streaming',
'chat/tool-calls': 'streaming',
// Render capabilities
'render/computed-functions': 'r-computed-functions',
'render/element-rendering': 'r-element-rendering',
Expand Down
Loading