page.mdx 10.0 KB

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