| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- export const metadata = { title: "AG-UI Integration" }
- # AG-UI Integration
- Use json-render to support [AG-UI](https://docs.copilotkit.ai/ag-ui) (Agent User Interaction Protocol) from CopilotKit.
- <div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
- <p className="text-sm text-amber-700 dark:text-amber-300">
- <strong>Concept:</strong> This page demonstrates how json-render can support AG-UI. The examples are illustrative and may require adaptation for production use.
- </p>
- </div>
- ## What is AG-UI?
- AG-UI is an open protocol for connecting AI agents to user interfaces. It provides a standardized way for agents to render UI components, handle user input, and manage state. The protocol uses events streamed over HTTP to update the UI in real-time.
- ## AG-UI Event Types
- AG-UI defines several event types for agent-UI communication:
- - `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` / `TEXT_MESSAGE_END` — Streaming text messages
- - `TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END` — Tool/function calls
- - `STATE_SNAPSHOT` / `STATE_DELTA` — State updates
- - `CUSTOM` — Custom events for UI rendering
- ### Example AG-UI Event Stream
- ```json
- {"type": "RUN_STARTED", "threadId": "thread-123", "runId": "run-456"}
- {"type": "TEXT_MESSAGE_START", "messageId": "msg-1", "role": "assistant"}
- {"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg-1", "delta": "Here's a dashboard for you:"}
- {"type": "TEXT_MESSAGE_END", "messageId": "msg-1"}
- {"type": "TOOL_CALL_START", "toolCallId": "tc-1", "toolCallName": "render_ui"}
- {"type": "TOOL_CALL_ARGS", "toolCallId": "tc-1", "delta": "{\"component\": \"Dashboard\", \"props\": {\"title\": \"Sales\"}}"}
- {"type": "TOOL_CALL_END", "toolCallId": "tc-1"}
- {"type": "RUN_FINISHED"}
- ```
- ## Define the AG-UI Schema
- Define schemas for AG-UI event types:
- ```typescript
- import { z } from 'zod';
- // Base event schema
- const BaseEvent = z.object({
- type: z.string(),
- timestamp: z.number().optional(),
- });
- // Text message events
- const TextMessageStart = BaseEvent.extend({
- type: z.literal('TEXT_MESSAGE_START'),
- messageId: z.string(),
- role: z.enum(['user', 'assistant']),
- });
- const TextMessageContent = BaseEvent.extend({
- type: z.literal('TEXT_MESSAGE_CONTENT'),
- messageId: z.string(),
- delta: z.string(),
- });
- const TextMessageEnd = BaseEvent.extend({
- type: z.literal('TEXT_MESSAGE_END'),
- messageId: z.string(),
- });
- // Tool call events
- const ToolCallStart = BaseEvent.extend({
- type: z.literal('TOOL_CALL_START'),
- toolCallId: z.string(),
- toolCallName: z.string(),
- parentMessageId: z.string().optional(),
- });
- const ToolCallArgs = BaseEvent.extend({
- type: z.literal('TOOL_CALL_ARGS'),
- toolCallId: z.string(),
- delta: z.string(),
- });
- const ToolCallEnd = BaseEvent.extend({
- type: z.literal('TOOL_CALL_END'),
- toolCallId: z.string(),
- });
- // State events
- const StateSnapshot = BaseEvent.extend({
- type: z.literal('STATE_SNAPSHOT'),
- snapshot: z.record(z.unknown()),
- });
- const StateDelta = BaseEvent.extend({
- type: z.literal('STATE_DELTA'),
- delta: z.array(z.object({
- op: z.enum(['add', 'remove', 'replace']),
- path: z.string(),
- value: z.unknown().optional(),
- })),
- });
- // Custom event for UI components
- const CustomEvent = BaseEvent.extend({
- type: z.literal('CUSTOM'),
- name: z.string(),
- value: z.unknown(),
- });
- // Run lifecycle events
- const RunStarted = BaseEvent.extend({
- type: z.literal('RUN_STARTED'),
- threadId: z.string(),
- runId: z.string(),
- });
- const RunFinished = BaseEvent.extend({
- type: z.literal('RUN_FINISHED'),
- });
- const RunError = BaseEvent.extend({
- type: z.literal('RUN_ERROR'),
- message: z.string(),
- code: z.string().optional(),
- });
- // Union of all events
- export const AGUIEvent = z.discriminatedUnion('type', [
- TextMessageStart,
- TextMessageContent,
- TextMessageEnd,
- ToolCallStart,
- ToolCallArgs,
- ToolCallEnd,
- StateSnapshot,
- StateDelta,
- CustomEvent,
- RunStarted,
- RunFinished,
- RunError,
- ]);
- export type AGUIEvent = z.infer<typeof AGUIEvent>;
- ```
- ## Define the AG-UI Catalog
- Create a catalog for UI components that agents can render:
- ```typescript
- import { createCatalog } from '@json-render/core';
- import { z } from 'zod';
- export const aguiCatalog = createCatalog({
- components: {
- Container: {
- description: 'A container for grouping elements',
- props: z.object({
- direction: z.enum(['row', 'column']).optional(),
- gap: z.enum(['none', 'sm', 'md', 'lg']).optional(),
- padding: z.enum(['none', 'sm', 'md', 'lg']).optional(),
- }),
- },
- Card: {
- description: 'A card with optional title',
- props: z.object({
- title: z.string().optional(),
- description: z.string().optional(),
- }),
- },
- Text: {
- description: 'Text content',
- props: z.object({
- content: z.string(),
- variant: z.enum(['body', 'heading', 'caption', 'code']).optional(),
- }),
- },
- Metric: {
- description: 'Displays a metric value',
- props: z.object({
- label: z.string(),
- value: z.union([z.string(), z.number()]),
- change: z.number().optional(),
- format: z.enum(['number', 'currency', 'percent']).optional(),
- }),
- },
- Button: {
- description: 'Interactive button',
- props: z.object({
- label: z.string(),
- variant: z.enum(['primary', 'secondary', 'outline', 'ghost']).optional(),
- disabled: z.boolean().optional(),
- }),
- },
- Alert: {
- description: 'Alert message',
- props: z.object({
- message: z.string(),
- type: z.enum(['info', 'success', 'warning', 'error']).optional(),
- }),
- },
- // Add more components...
- },
- actions: {
- submit: {
- description: 'Submit form data',
- params: z.object({ formId: z.string() }),
- },
- navigate: {
- description: 'Navigate to a URL',
- params: z.object({ url: z.string() }),
- },
- callback: {
- description: 'Trigger a callback to the agent',
- params: z.object({
- name: z.string(),
- data: z.record(z.unknown()).optional(),
- }),
- },
- },
- });
- ```
- ## Build an AG-UI Event Processor
- Process AG-UI events and render UI components:
- ```tsx
- 'use client';
- import React, { useState, useCallback } from 'react';
- import { AGUIEvent } from './schema';
- interface AGUIState {
- messages: Array<{
- id: string;
- role: 'user' | 'assistant';
- content: string;
- }>;
- toolCalls: Map<string, {
- name: string;
- args: string;
- result?: unknown;
- }>;
- state: Record<string, unknown>;
- isRunning: boolean;
- }
- export function useAGUI() {
- const [aguiState, setAGUIState] = useState<AGUIState>({
- messages: [],
- toolCalls: new Map(),
- state: {},
- isRunning: false,
- });
- const processEvent = useCallback((event: AGUIEvent) => {
- switch (event.type) {
- case 'RUN_STARTED':
- setAGUIState(prev => ({ ...prev, isRunning: true }));
- break;
- case 'RUN_FINISHED':
- setAGUIState(prev => ({ ...prev, isRunning: false }));
- break;
- case 'TEXT_MESSAGE_START':
- setAGUIState(prev => ({
- ...prev,
- messages: [...prev.messages, {
- id: event.messageId,
- role: event.role,
- content: '',
- }],
- }));
- break;
- case 'TEXT_MESSAGE_CONTENT':
- setAGUIState(prev => ({
- ...prev,
- messages: prev.messages.map(msg =>
- msg.id === event.messageId
- ? { ...msg, content: msg.content + event.delta }
- : msg
- ),
- }));
- break;
- case 'TOOL_CALL_START':
- setAGUIState(prev => {
- const toolCalls = new Map(prev.toolCalls);
- toolCalls.set(event.toolCallId, { name: event.toolCallName, args: '' });
- return { ...prev, toolCalls };
- });
- break;
- case 'TOOL_CALL_ARGS':
- setAGUIState(prev => {
- const toolCalls = new Map(prev.toolCalls);
- const tc = toolCalls.get(event.toolCallId);
- if (tc) {
- toolCalls.set(event.toolCallId, { ...tc, args: tc.args + event.delta });
- }
- return { ...prev, toolCalls };
- });
- break;
- case 'STATE_SNAPSHOT':
- setAGUIState(prev => ({ ...prev, state: event.snapshot }));
- break;
- }
- }, []);
- return { state: aguiState, processEvent };
- }
- ```
- ## Usage Example
- ```tsx
- 'use client';
- import { useAGUI } from './use-agui';
- import { renderToolCallUI } from './renderer';
- export function AGUIChat() {
- const { state, processEvent } = useAGUI();
- async function startRun(prompt: string) {
- const response = await fetch('/api/agent', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ prompt }),
- });
- const reader = response.body?.getReader();
- const decoder = new TextDecoder();
- while (reader) {
- const { done, value } = await reader.read();
- if (done) break;
- const lines = decoder.decode(value).split('\n').filter(Boolean);
- for (const line of lines) {
- const event = JSON.parse(line);
- processEvent(event);
- }
- }
- }
- return (
- <div className="space-y-4">
- {state.messages.map(msg => (
- <div key={msg.id} className={`p-3 rounded ${
- msg.role === 'assistant' ? 'bg-muted' : 'bg-primary/10'
- }`}>
- {msg.content}
- </div>
- ))}
- {Array.from(state.toolCalls.values()).map((tc, i) => (
- <div key={i}>{renderToolCallUI(tc)}</div>
- ))}
- <form onSubmit={(e) => {
- e.preventDefault();
- const input = e.currentTarget.querySelector('input');
- if (input?.value) {
- startRun(input.value);
- input.value = '';
- }
- }}>
- <input
- type="text"
- placeholder="Ask the agent..."
- className="w-full px-4 py-2 border rounded"
- disabled={state.isRunning}
- />
- </form>
- </div>
- );
- }
- ```
- ## Next
- Learn about [OpenAPI integration](/docs/openapi) for rendering forms from API schemas.
|