diff --git a/cockpit/chat/a2ui/python/src/graph.py b/cockpit/chat/a2ui/python/src/graph.py index 8db85fc66..2d3b0616c 100644 --- a/cockpit/chat/a2ui/python/src/graph.py +++ b/cockpit/chat/a2ui/python/src/graph.py @@ -1,36 +1,102 @@ """ -A2UI Chat Graph +A2UI Contact Form Graph -A LangGraph StateGraph that generates A2UI JSONL responses using an LLM. -The Angular frontend detects the ---a2ui_JSON--- prefix and renders -interactive surfaces from the streamed component definitions. +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. """ -from pathlib import Path +import json from langgraph.graph import StateGraph, MessagesState, END -from langchain_openai import ChatOpenAI -from langchain_core.messages import SystemMessage +from langchain_core.messages import AIMessage + +A2UI_PREFIX = "---a2ui_JSON---" -PROMPTS_DIR = Path(__file__).parent.parent / "prompts" +# v0.9 envelope format: each message is {"": {}} +CONTACT_FORM_JSONL = A2UI_PREFIX + "\n" + "\n".join([ + 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"}}}}, + ], + }}), +]) def build_a2ui_graph(): """ - Single-node graph that invokes an LLM with the A2UI system prompt. - The LLM generates A2UI JSONL that builds interactive surfaces. + Single-node graph: + - On first message: emits hardcoded A2UI JSONL for the contact form + - On a2ui_event messages: responds with a confirmation message """ - llm = ChatOpenAI(model="gpt-5-mini", streaming=True) - async def generate(state: MessagesState) -> dict: - system_prompt = (PROMPTS_DIR / "a2ui.md").read_text() - messages = [SystemMessage(content=system_prompt)] + state["messages"] - response = await llm.ainvoke(messages) - return {"messages": [response]} + async def create_form(state: MessagesState) -> dict: + last = state["messages"][-1] + + # 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": + 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 + + # Any other message — emit the contact form + return {"messages": [AIMessage(content=CONTACT_FORM_JSONL)]} graph = StateGraph(MessagesState) - graph.add_node("generate", generate) - graph.set_entry_point("generate") - graph.add_edge("generate", END) + graph.add_node("create_form", create_form) + graph.set_entry_point("create_form") + graph.add_edge("create_form", END) return graph.compile()