page.mdx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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 { createCatalog } from '@json-render/core';
  122. import { z } from 'zod';
  123. export const aguiCatalog = createCatalog({
  124. components: {
  125. Container: {
  126. description: 'A container for grouping elements',
  127. props: z.object({
  128. direction: z.enum(['row', 'column']).optional(),
  129. gap: z.enum(['none', 'sm', 'md', 'lg']).optional(),
  130. padding: z.enum(['none', 'sm', 'md', 'lg']).optional(),
  131. }),
  132. },
  133. Card: {
  134. description: 'A card with optional title',
  135. props: z.object({
  136. title: z.string().optional(),
  137. description: z.string().optional(),
  138. }),
  139. },
  140. Text: {
  141. description: 'Text content',
  142. props: z.object({
  143. content: z.string(),
  144. variant: z.enum(['body', 'heading', 'caption', 'code']).optional(),
  145. }),
  146. },
  147. Metric: {
  148. description: 'Displays a metric value',
  149. props: z.object({
  150. label: z.string(),
  151. value: z.union([z.string(), z.number()]),
  152. change: z.number().optional(),
  153. format: z.enum(['number', 'currency', 'percent']).optional(),
  154. }),
  155. },
  156. Button: {
  157. description: 'Interactive button',
  158. props: z.object({
  159. label: z.string(),
  160. variant: z.enum(['primary', 'secondary', 'outline', 'ghost']).optional(),
  161. disabled: z.boolean().optional(),
  162. }),
  163. },
  164. Alert: {
  165. description: 'Alert message',
  166. props: z.object({
  167. message: z.string(),
  168. type: z.enum(['info', 'success', 'warning', 'error']).optional(),
  169. }),
  170. },
  171. // Add more components...
  172. },
  173. actions: {
  174. submit: {
  175. description: 'Submit form data',
  176. params: z.object({ formId: z.string() }),
  177. },
  178. navigate: {
  179. description: 'Navigate to a URL',
  180. params: z.object({ url: z.string() }),
  181. },
  182. callback: {
  183. description: 'Trigger a callback to the agent',
  184. params: z.object({
  185. name: z.string(),
  186. data: z.record(z.unknown()).optional(),
  187. }),
  188. },
  189. },
  190. });
  191. ```
  192. ## Build an AG-UI Event Processor
  193. Process AG-UI events and render UI components:
  194. ```tsx
  195. 'use client';
  196. import React, { useState, useCallback } from 'react';
  197. import { AGUIEvent } from './schema';
  198. interface AGUIState {
  199. messages: Array<{
  200. id: string;
  201. role: 'user' | 'assistant';
  202. content: string;
  203. }>;
  204. toolCalls: Map<string, {
  205. name: string;
  206. args: string;
  207. result?: unknown;
  208. }>;
  209. state: Record<string, unknown>;
  210. isRunning: boolean;
  211. }
  212. export function useAGUI() {
  213. const [aguiState, setAGUIState] = useState<AGUIState>({
  214. messages: [],
  215. toolCalls: new Map(),
  216. state: {},
  217. isRunning: false,
  218. });
  219. const processEvent = useCallback((event: AGUIEvent) => {
  220. switch (event.type) {
  221. case 'RUN_STARTED':
  222. setAGUIState(prev => ({ ...prev, isRunning: true }));
  223. break;
  224. case 'RUN_FINISHED':
  225. setAGUIState(prev => ({ ...prev, isRunning: false }));
  226. break;
  227. case 'TEXT_MESSAGE_START':
  228. setAGUIState(prev => ({
  229. ...prev,
  230. messages: [...prev.messages, {
  231. id: event.messageId,
  232. role: event.role,
  233. content: '',
  234. }],
  235. }));
  236. break;
  237. case 'TEXT_MESSAGE_CONTENT':
  238. setAGUIState(prev => ({
  239. ...prev,
  240. messages: prev.messages.map(msg =>
  241. msg.id === event.messageId
  242. ? { ...msg, content: msg.content + event.delta }
  243. : msg
  244. ),
  245. }));
  246. break;
  247. case 'TOOL_CALL_START':
  248. setAGUIState(prev => {
  249. const toolCalls = new Map(prev.toolCalls);
  250. toolCalls.set(event.toolCallId, { name: event.toolCallName, args: '' });
  251. return { ...prev, toolCalls };
  252. });
  253. break;
  254. case 'TOOL_CALL_ARGS':
  255. setAGUIState(prev => {
  256. const toolCalls = new Map(prev.toolCalls);
  257. const tc = toolCalls.get(event.toolCallId);
  258. if (tc) {
  259. toolCalls.set(event.toolCallId, { ...tc, args: tc.args + event.delta });
  260. }
  261. return { ...prev, toolCalls };
  262. });
  263. break;
  264. case 'STATE_SNAPSHOT':
  265. setAGUIState(prev => ({ ...prev, state: event.snapshot }));
  266. break;
  267. }
  268. }, []);
  269. return { state: aguiState, processEvent };
  270. }
  271. ```
  272. ## Usage Example
  273. ```tsx
  274. 'use client';
  275. import { useAGUI } from './use-agui';
  276. import { renderToolCallUI } from './renderer';
  277. export function AGUIChat() {
  278. const { state, processEvent } = useAGUI();
  279. async function startRun(prompt: string) {
  280. const response = await fetch('/api/agent', {
  281. method: 'POST',
  282. headers: { 'Content-Type': 'application/json' },
  283. body: JSON.stringify({ prompt }),
  284. });
  285. const reader = response.body?.getReader();
  286. const decoder = new TextDecoder();
  287. while (reader) {
  288. const { done, value } = await reader.read();
  289. if (done) break;
  290. const lines = decoder.decode(value).split('\n').filter(Boolean);
  291. for (const line of lines) {
  292. const event = JSON.parse(line);
  293. processEvent(event);
  294. }
  295. }
  296. }
  297. return (
  298. <div className="space-y-4">
  299. {state.messages.map(msg => (
  300. <div key={msg.id} className={`p-3 rounded ${
  301. msg.role === 'assistant' ? 'bg-muted' : 'bg-primary/10'
  302. }`}>
  303. {msg.content}
  304. </div>
  305. ))}
  306. {Array.from(state.toolCalls.values()).map((tc, i) => (
  307. <div key={i}>{renderToolCallUI(tc)}</div>
  308. ))}
  309. <form onSubmit={(e) => {
  310. e.preventDefault();
  311. const input = e.currentTarget.querySelector('input');
  312. if (input?.value) {
  313. startRun(input.value);
  314. input.value = '';
  315. }
  316. }}>
  317. <input
  318. type="text"
  319. placeholder="Ask the agent..."
  320. className="w-full px-4 py-2 border rounded"
  321. disabled={state.isRunning}
  322. />
  323. </form>
  324. </div>
  325. );
  326. }
  327. ```
  328. ## Next
  329. Learn about [OpenAPI integration](/docs/openapi) for rendering forms from API schemas.