Class: AgUi::Middleware::A2ui

Inherits:
Object
  • Object
show all
Defined in:
lib/ag_ui/middleware/a2ui.rb

Overview

A2UI (Tier-3 generative UI) — the Ruby port of CopilotKit's @ag-ui/a2ui-middleware transform (doc 09 §6).

Way in:

- injects the `render_a2ui` tool into env[:tools] (STATIC schema —
the catalog does NOT shape it)
- appends the catalog's component schema to the conversation as a
system message (that's how the model learns the vocabulary)

Way out, for each render_a2ui call the model made:

- converts the call args into an ACTIVITY_SNAPSHOT carrying
`a2ui_operations` (createSurface once per surface, then
updateComponents / updateDataModel), flat wire shape,
activityType "a2ui-surface", replace: true
- emits a synthetic TOOL_CALL_RESULT ({"status":"rendered"}) so the
next run's history shows the model its call completed

The TOOL_CALL_* events themselves pass through (ToolRouter emits them) and the run still ends — same multi-run model as client tools; the activity events are what the canvas renders.

Sits OUTSIDE ToolRouter in the pipeline:

use SystemPrompt; use A2ui, catalog: catalog; use ToolRouter

Constant Summary collapse

TOOL_NAME =
"render_a2ui"
TOOL_DEFINITION =
{
  "name" => TOOL_NAME,
  "description" =>
    "Render a dynamic A2UI v0.9 surface with structured parameters. " \
    "Follow the A2UI render tool usage guide provided in context.",
  "parameters" => {
    "type" => "object",
    "properties" => {
      "surfaceId" => {
        "type" => "string",
        "description" => "Unique surface identifier.",
      },
      "components" => {
        "type" => "array",
        "description" =>
          "A2UI v0.9 component array (flat format). The root component " \
          "must have id \"root\".",
        "items" => { "type" => "object" },
      },
      "data" => {
        "type" => "object",
        "description" =>
          "Initial data model for the surface. Written to the root path. " \
          "Use for pre-filling form values (e.g. {\"form\": {\"name\": \"Alice\"}}) " \
          "or providing data for components bound to data model paths.",
      },
    },
    "required" => %w[surfaceId components],
  },
}.freeze
SCHEMA_CONTEXT_PREAMBLE =
"A2UI Component Schema — available components for generating UI " \
"surfaces. Use these component names and properties when creating " \
"A2UI operations."
BASIC_CATALOG_ID =
"https://a2ui.org/specification/v0_9/basic_catalog.json"
MAX_ATTEMPTS =

Attempt cap for in-run regeneration (initial try + retries), matching the toolkit recovery default.

::AgUi::A2ui::Recovery::MAX_A2UI_ATTEMPTS

Instance Method Summary collapse

Constructor Details

#initialize(app, catalog: nil, default_catalog_id: nil) ⇒ A2ui

Returns a new instance of A2ui.



73
74
75
76
77
# File 'lib/ag_ui/middleware/a2ui.rb', line 73

def initialize(app, catalog: nil, default_catalog_id: nil)
  @app = app
  @catalog = catalog
  @default_catalog_id = default_catalog_id || catalog&.catalog_id
end

Instance Method Details

#call(env) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/ag_ui/middleware/a2ui.rb', line 83

def call(env)
  advertise(env)
  inject_catalog_schema(env)

  before = env[:messages].length
  @app.call(env)

  # Only this iteration's assistant message — seeded history can
  # carry old tool-call turns that must not re-render.
  appended = env[:messages][before..] || []
  assistant = appended.reverse.find { |m| m.respond_to?(:tool_call?) && m.tool_call? }
  if assistant
    assistant.tool_calls.each do |tool_call|
      if tool_call.name == TOOL_NAME
        render(env, tool_call)
      end
    end
  end

  env
end