Skip to main content

Generative User Interfaces

Summary

Problem Statement

Currently, creating custom user interfaces for agent interactions requires programmers to define specific tool renderers. This limits the flexibility and adaptability of agent-driven applications.

Motivation

This draft describes an AG-UI extension that addresses generative user interfaces—interfaces produced directly by artificial intelligence without requiring a programmer to define custom tool renderers. The key idea is to leverage our ability to send client-side tools to the agent, thereby enabling this capability across all agent frameworks supported by AG-UI.

Status

Challenges and Limitations

Tool Description Length

OpenAI enforces a limit of 1024 characters for tool descriptions. Gemini and Anthropic impose no such limit.

Arguments JSON Schema Constraints

Classes, nesting, $ref, and oneOf are not reliably supported across LLM providers.

Context Window Considerations

Injecting a large UI description language into an agent may reduce its performance. Agents dedicated solely to UI generation perform better than agents combining UI generation with other tasks.

Detailed Specification

Two-Step Generation Process

Step 1: What to Generate?

Inject a lightweight tool into the agent: Tool Definition:
  • Name: generateUserInterface
  • Arguments:
    • description: A high-level description of the UI (e.g., “A form for entering the user’s address”)
    • data: Arbitrary pre-populated data for the generated UI
    • output: A description or schema of the data the agent expects the user to submit back (fields, required/optional, types, constraints)
Example Tool Call:
{
  "tool": "generateUserInterface",
  "arguments": {
    "description": "A form that collects a user's shipping address.",
    "data": {
      "firstName": "Ada",
      "lastName": "Lovelace",
      "city": "London"
    },
    "output": {
      "type": "object",
      "required": [
        "firstName",
        "lastName",
        "street",
        "city",
        "postalCode",
        "country"
      ],
      "properties": {
        "firstName": { "type": "string", "title": "First Name" },
        "lastName": { "type": "string", "title": "Last Name" },
        "street": { "type": "string", "title": "Street Address" },
        "city": { "type": "string", "title": "City" },
        "postalCode": { "type": "string", "title": "Postal Code" },
        "country": {
          "type": "string",
          "title": "Country",
          "enum": ["GB", "US", "DE", "AT"]
        }
      }
    }
  }
}

Step 2: How to Generate?

Delegate UI generation to a secondary LLM or agent:
  • The CopilotKit user stays in control: Can make their own generators, add custom libraries, include additional prompts etc.
  • On tool invocation, the secondary model consumes description, data, and output to generate the user interface
  • This model is focused solely on UI generation, ensuring maximum fidelity and consistency
  • The generation method can be swapped as needed (e.g., JSON, HTML, or other renderable formats)
  • The UI format description is not subject to structural or length constraints, allowing arbitrarily complex specifications

Implementation Examples

Example Output: UISchemaGenerator

{
  "jsonSchema": {
    "title": "Shipping Address",
    "type": "object",
    "required": [
      "firstName",
      "lastName",
      "street",
      "city",
      "postalCode",
      "country"
    ],
    "properties": {
      "firstName": { "type": "string", "title": "First name" },
      "lastName": { "type": "string", "title": "Last name" },
      "street": { "type": "string", "title": "Street address" },
      "city": { "type": "string", "title": "City" },
      "postalCode": { "type": "string", "title": "Postal code" },
      "country": {
        "type": "string",
        "title": "Country",
        "enum": ["GB", "US", "DE", "AT"]
      }
    }
  },
  "uiSchema": {
    "type": "VerticalLayout",
    "elements": [
      {
        "type": "Group",
        "label": "Personal Information",
        "elements": [
          { "type": "Control", "scope": "#/properties/firstName" },
          { "type": "Control", "scope": "#/properties/lastName" }
        ]
      },
      {
        "type": "Group",
        "label": "Address",
        "elements": [
          { "type": "Control", "scope": "#/properties/street" },
          { "type": "Control", "scope": "#/properties/city" },
          { "type": "Control", "scope": "#/properties/postalCode" },
          { "type": "Control", "scope": "#/properties/country" }
        ]
      }
    ]
  },
  "initialData": {
    "firstName": "Ada",
    "lastName": "Lovelace",
    "city": "London",
    "country": "GB"
  }
}

Example Output: ReactFormHookGenerator

import React from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"

// ----- Schema (contract) -----
const AddressSchema = z.object({
  firstName: z.string().min(1, "Required"),
  lastName: z.string().min(1, "Required"),
  street: z.string().min(1, "Required"),
  city: z.string().min(1, "Required"),
  postalCode: z.string().regex(/^[A-Za-z0-9\\-\\s]{3,10}$/, "3–10 chars"),
  country: z.enum(["GB", "US", "DE", "AT", "FR", "IT", "ES"]),
})
export type Address = z.infer<typeof AddressSchema>

type Props = {
  initialData?: Partial<Address>
  meta?: { title?: string; submitLabel?: string }
  respond: (data: Address) => void // <-- called on successful submit
}

const COUNTRIES: Address["country"][] = [
  "GB",
  "US",
  "DE",
  "AT",
  "FR",
  "IT",
  "ES",
]

export default function AddressForm({ initialData, meta, respond }: Props) {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Address>({
    resolver: zodResolver(AddressSchema),
    defaultValues: {
      firstName: "",
      lastName: "",
      street: "",
      city: "",
      postalCode: "",
      country: "GB",
      ...initialData,
    },
  })

  const onSubmit = (data: Address) => {
    // Guaranteed to match AddressSchema
    respond(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {meta?.title && <h2>{meta.title}</h2>}

      {/* Section: Personal Information */}
      <fieldset>
        <legend>Personal Information</legend>

        <div>
          <label>First name</label>
          <input {...register("firstName")} placeholder="Ada" autoFocus />
          {errors.firstName && <small>{errors.firstName.message}</small>}
        </div>

        <div>
          <label>Last name</label>
          <input {...register("lastName")} placeholder="Lovelace" />
          {errors.lastName && <small>{errors.lastName.message}</small>}
        </div>
      </fieldset>

      {/* Section: Address */}
      <fieldset>
        <legend>Address</legend>

        <div>
          <label>Street address</label>
          <input {...register("street")} />
          {errors.street && <small>{errors.street.message}</small>}
        </div>

        <div>
          <label>City</label>
          <input {...register("city")} />
          {errors.city && <small>{errors.city.message}</small>}
        </div>

        <div>
          <label>Postal code</label>
          <input {...register("postalCode")} />
          {errors.postalCode && <small>{errors.postalCode.message}</small>}
        </div>

        <div>
          <label>Country</label>
          <select {...register("country")}>
            {COUNTRIES.map((c) => (
              <option key={c} value={c}>
                {c}
              </option>
            ))}
          </select>
          {errors.country && <small>{errors.country.message}</small>}
        </div>
      </fieldset>

      <div>
        <button type="submit">{meta?.submitLabel ?? "Submit"}</button>
      </div>
    </form>
  )
}

Implementation Considerations

Client SDK Changes

TypeScript SDK additions:
  • New generateUserInterface tool type
  • UI generator registry for pluggable generators
  • Validation layer for generated UI schemas
  • Response handler for user-submitted data
Python SDK additions:
  • Support for UI generation tool invocation
  • Schema validation utilities
  • Serialization for UI definitions

Integration Impact

  • All AG-UI integrations can leverage this capability without modification
  • Frameworks emit standard tool calls; client handles UI generation
  • Backward compatible with existing tool-based UI approaches

Use Cases

Dynamic Forms

Agents can generate forms on-the-fly based on conversation context without pre-defined schemas.

Data Visualization

Generate charts, graphs, or tables appropriate to the data being discussed.

Interactive Workflows

Create multi-step wizards or guided processes tailored to user needs.

Adaptive Interfaces

Generate different UI layouts based on user preferences or device capabilities.

Testing Strategy

  • Unit tests for tool injection and invocation
  • Integration tests with multiple UI generators
  • E2E tests demonstrating various UI types
  • Performance benchmarks comparing single vs. two-step generation
  • Cross-provider compatibility testing

References

I