page.mdx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import { pageMetadata } from "@/lib/page-metadata"
  2. export const metadata = pageMetadata("docs/ag-ui")
  3. # AG-UI Integration
  4. Use json-render to support [AG-UI](https://docs.copilotkit.ai/ag-ui) (Agent User Interaction Protocol) from CopilotKit.
  5. <div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
  6. <p className="text-sm text-amber-700 dark:text-amber-300">
  7. <strong>Concept:</strong> This page demonstrates how json-render can support AG-UI. The examples are illustrative and may require adaptation for production use.
  8. </p>
  9. </div>
  10. ## What is AG-UI?
  11. 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.
  12. ## AG-UI Event Types
  13. AG-UI defines several event types for agent-UI communication:
  14. - `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` / `TEXT_MESSAGE_END` — Streaming text messages
  15. - `TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END` — Tool/function calls
  16. - `STATE_SNAPSHOT` / `STATE_DELTA` — State updates
  17. - `CUSTOM` — Custom events for UI rendering
  18. ### Example AG-UI Event Stream
  19. ```json
  20. {"type": "RUN_STARTED", "threadId": "thread-123", "runId": "run-456"}
  21. {"type": "TEXT_MESSAGE_START", "messageId": "msg-1", "role": "assistant"}
  22. {"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg-1", "delta": "Here's a dashboard for you:"}
  23. {"type": "TEXT_MESSAGE_END", "messageId": "msg-1"}
  24. {"type": "TOOL_CALL_START", "toolCallId": "tc-1", "toolCallName": "render_ui"}
  25. {"type": "TOOL_CALL_ARGS", "toolCallId": "tc-1", "delta": "{\"component\": \"Dashboard\", \"props\": {\"title\": \"Sales\"}}"}
  26. {"type": "TOOL_CALL_END", "toolCallId": "tc-1"}
  27. {"type": "RUN_FINISHED"}
  28. ```
  29. ## Define the AG-UI Schema
  30. Define schemas for AG-UI event types:
  31. ```typescript
  32. import { z } from 'zod';
  33. // Base event schema
  34. const BaseEvent = z.object({
  35. type: z.string(),
  36. timestamp: z.number().optional(),
  37. });
  38. // Text message events
  39. const TextMessageStart = BaseEvent.extend({
  40. type: z.literal('TEXT_MESSAGE_START'),
  41. messageId: z.string(),
  42. role: z.enum(['user', 'assistant']),
  43. });
  44. const TextMessageContent = BaseEvent.extend({
  45. type: z.literal('TEXT_MESSAGE_CONTENT'),
  46. messageId: z.string(),
  47. delta: z.string(),
  48. });
  49. const TextMessageEnd = BaseEvent.extend({
  50. type: z.literal('TEXT_MESSAGE_END'),
  51. messageId: z.string(),
  52. });
  53. // Tool call events
  54. const ToolCallStart = BaseEvent.extend({
  55. type: z.literal('TOOL_CALL_START'),
  56. toolCallId: z.string(),
  57. toolCallName: z.string(),
  58. parentMessageId: z.string().optional(),
  59. });
  60. const ToolCallArgs = BaseEvent.extend({
  61. type: z.literal('TOOL_CALL_ARGS'),
  62. toolCallId: z.string(),
  63. delta: z.string(),
  64. });
  65. const ToolCallEnd = BaseEvent.extend({
  66. type: z.literal('TOOL_CALL_END'),
  67. toolCallId: z.string(),
  68. });
  69. // State events
  70. const StateSnapshot = BaseEvent.extend({
  71. type: z.literal('STATE_SNAPSHOT'),
  72. snapshot: z.record(z.unknown()),
  73. });
  74. const StateDelta = BaseEvent.extend({
  75. type: z.literal('STATE_DELTA'),
  76. delta: z.array(z.object({
  77. op: z.enum(['add', 'remove', 'replace']),
  78. path: z.string(),
  79. value: z.unknown().optional(),
  80. })),
  81. });
  82. // Custom event for UI components
  83. const CustomEvent = BaseEvent.extend({
  84. type: z.literal('CUSTOM'),
  85. name: z.string(),
  86. value: z.unknown(),
  87. });
  88. // Run lifecycle events
  89. const RunStarted = BaseEvent.extend({
  90. type: z.literal('RUN_STARTED'),
  91. threadId: z.string(),
  92. runId: z.string(),
  93. });
  94. const RunFinished = BaseEvent.extend({
  95. type: z.literal('RUN_FINISHED'),
  96. });
  97. const RunError = BaseEvent.extend({
  98. type: z.literal('RUN_ERROR'),
  99. message: z.string(),
  100. code: z.string().optional(),
  101. });
  102. // Union of all events
  103. export const AGUIEvent = z.discriminatedUnion('type', [
  104. TextMessageStart,
  105. TextMessageContent,
  106. TextMessageEnd,
  107. ToolCallStart,
  108. ToolCallArgs,
  109. ToolCallEnd,
  110. StateSnapshot,
  111. StateDelta,
  112. CustomEvent,
  113. RunStarted,
  114. RunFinished,
  115. RunError,
  116. ]);
  117. export type AGUIEvent = z.infer<typeof AGUIEvent>;
  118. ```
  119. ## Define the AG-UI Catalog
  120. Create a catalog for UI components that agents can render:
  121. ```typescript
  122. import { defineCatalog } from '@json-render/core';
  123. import { schema } from '@json-render/react/schema';
  124. import { z } from 'zod';
  125. export const aguiCatalog = defineCatalog(schema, {
  126. components: {
  127. Container: {
  128. description: 'A container for grouping elements',
  129. props: z.object({
  130. direction: z.enum(['row', 'column']).optional(),
  131. gap: z.enum(['none', 'sm', 'md', 'lg']).optional(),
  132. padding: z.enum(['none', 'sm', 'md', 'lg']).optional(),
  133. }),
  134. },
  135. Card: {
  136. description: 'A card with optional title',
  137. props: z.object({
  138. title: z.string().optional(),
  139. description: z.string().optional(),
  140. }),
  141. },
  142. Text: {
  143. description: 'Text content',
  144. props: z.object({
  145. content: z.string(),
  146. variant: z.enum(['body', 'heading', 'caption', 'code']).optional(),
  147. }),
  148. },
  149. Metric: {
  150. description: 'Displays a metric value',
  151. props: z.object({
  152. label: z.string(),
  153. value: z.union([z.string(), z.number()]),
  154. change: z.number().optional(),
  155. format: z.enum(['number', 'currency', 'percent']).optional(),
  156. }),
  157. },
  158. Button: {
  159. description: 'Interactive button',
  160. props: z.object({
  161. label: z.string(),
  162. variant: z.enum(['primary', 'secondary', 'outline', 'ghost']).optional(),
  163. disabled: z.boolean().optional(),
  164. }),
  165. },
  166. Alert: {
  167. description: 'Alert message',
  168. props: z.object({
  169. message: z.string(),
  170. type: z.enum(['info', 'success', 'warning', 'error']).optional(),
  171. }),
  172. },
  173. // Add more components...
  174. },
  175. actions: {
  176. submit: {
  177. description: 'Submit form data',
  178. params: z.object({ formId: z.string() }),
  179. },
  180. navigate: {
  181. description: 'Navigate to a URL',
  182. params: z.object({ url: z.string() }),
  183. },
  184. callback: {
  185. description: 'Trigger a callback to the agent',
  186. params: z.object({
  187. name: z.string(),
  188. data: z.record(z.unknown()).optional(),
  189. }),
  190. },
  191. },
  192. });
  193. ```
  194. ## Build an AG-UI Event Processor
  195. Process AG-UI events and render UI components:
  196. ```tsx
  197. 'use client';
  198. import React, { useState, useCallback } from 'react';
  199. import { AGUIEvent } from './schema';
  200. interface AGUIState {
  201. messages: Array<{
  202. id: string;
  203. role: 'user' | 'assistant';
  204. content: string;
  205. }>;
  206. toolCalls: Map<string, {
  207. name: string;
  208. args: string;
  209. result?: unknown;
  210. }>;
  211. state: Record<string, unknown>;
  212. isRunning: boolean;
  213. }
  214. export function useAGUI() {
  215. const [aguiState, setAGUIState] = useState<AGUIState>({
  216. messages: [],
  217. toolCalls: new Map(),
  218. state: {},
  219. isRunning: false,
  220. });
  221. const processEvent = useCallback((event: AGUIEvent) => {
  222. switch (event.type) {
  223. case 'RUN_STARTED':
  224. setAGUIState(prev => ({ ...prev, isRunning: true }));
  225. break;
  226. case 'RUN_FINISHED':
  227. setAGUIState(prev => ({ ...prev, isRunning: false }));
  228. break;
  229. case 'TEXT_MESSAGE_START':
  230. setAGUIState(prev => ({
  231. ...prev,
  232. messages: [...prev.messages, {
  233. id: event.messageId,
  234. role: event.role,
  235. content: '',
  236. }],
  237. }));
  238. break;
  239. case 'TEXT_MESSAGE_CONTENT':
  240. setAGUIState(prev => ({
  241. ...prev,
  242. messages: prev.messages.map(msg =>
  243. msg.id === event.messageId
  244. ? { ...msg, content: msg.content + event.delta }
  245. : msg
  246. ),
  247. }));
  248. break;
  249. case 'TOOL_CALL_START':
  250. setAGUIState(prev => {
  251. const toolCalls = new Map(prev.toolCalls);
  252. toolCalls.set(event.toolCallId, { name: event.toolCallName, args: '' });
  253. return { ...prev, toolCalls };
  254. });
  255. break;
  256. case 'TOOL_CALL_ARGS':
  257. setAGUIState(prev => {
  258. const toolCalls = new Map(prev.toolCalls);
  259. const tc = toolCalls.get(event.toolCallId);
  260. if (tc) {
  261. toolCalls.set(event.toolCallId, { ...tc, args: tc.args + event.delta });
  262. }
  263. return { ...prev, toolCalls };
  264. });
  265. break;
  266. case 'STATE_SNAPSHOT':
  267. setAGUIState(prev => ({ ...prev, state: event.snapshot }));
  268. break;
  269. }
  270. }, []);
  271. return { state: aguiState, processEvent };
  272. }
  273. ```
  274. ## Usage Example
  275. ```tsx
  276. 'use client';
  277. import { useAGUI } from './use-agui';
  278. import { renderToolCallUI } from './renderer';
  279. export function AGUIChat() {
  280. const { state, processEvent } = useAGUI();
  281. async function startRun(prompt: string) {
  282. const response = await fetch('/api/agent', {
  283. method: 'POST',
  284. headers: { 'Content-Type': 'application/json' },
  285. body: JSON.stringify({ prompt }),
  286. });
  287. const reader = response.body?.getReader();
  288. const decoder = new TextDecoder();
  289. while (reader) {
  290. const { done, value } = await reader.read();
  291. if (done) break;
  292. const lines = decoder.decode(value).split('\n').filter(Boolean);
  293. for (const line of lines) {
  294. const event = JSON.parse(line);
  295. processEvent(event);
  296. }
  297. }
  298. }
  299. return (
  300. <div className="space-y-4">
  301. {state.messages.map(msg => (
  302. <div key={msg.id} className={`p-3 rounded ${
  303. msg.role === 'assistant' ? 'bg-muted' : 'bg-primary/10'
  304. }`}>
  305. {msg.content}
  306. </div>
  307. ))}
  308. {Array.from(state.toolCalls.values()).map((tc, i) => (
  309. <div key={i}>{renderToolCallUI(tc)}</div>
  310. ))}
  311. <form onSubmit={(e) => {
  312. e.preventDefault();
  313. const input = e.currentTarget.querySelector('input');
  314. if (input?.value) {
  315. startRun(input.value);
  316. input.value = '';
  317. }
  318. }}>
  319. <input
  320. type="text"
  321. placeholder="Ask the agent..."
  322. className="w-full px-4 py-2 border rounded"
  323. disabled={state.isRunning}
  324. />
  325. </form>
  326. </div>
  327. );
  328. }
  329. ```
  330. ## Next
  331. Learn about [OpenAPI integration](/docs/openapi) for rendering forms from API schemas.