page.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import Link from "next/link";
  2. import { Code } from "@/components/code";
  3. export const metadata = {
  4. title: "AG-UI Integration | json-render",
  5. };
  6. export default function AGUIPage() {
  7. return (
  8. <article>
  9. <h1 className="text-3xl font-bold mb-4">AG-UI Integration</h1>
  10. <p className="text-muted-foreground mb-8">
  11. Use json-render to support{" "}
  12. <a
  13. href="https://docs.copilotkit.ai/ag-ui"
  14. target="_blank"
  15. rel="noopener noreferrer"
  16. className="text-foreground hover:underline"
  17. >
  18. AG-UI
  19. </a>{" "}
  20. (Agent User Interaction Protocol) from CopilotKit.
  21. </p>
  22. <div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
  23. <p className="text-sm text-amber-700 dark:text-amber-300">
  24. <strong>Concept:</strong> This page demonstrates how json-render can
  25. support AG-UI. The examples are illustrative and may require
  26. adaptation for production use.
  27. </p>
  28. </div>
  29. <h2 className="text-xl font-semibold mt-12 mb-4">What is AG-UI?</h2>
  30. <p className="text-sm text-muted-foreground mb-4">
  31. AG-UI is an open protocol for connecting AI agents to user interfaces.
  32. It provides a standardized way for agents to render UI components,
  33. handle user input, and manage state. The protocol uses events streamed
  34. over HTTP to update the UI in real-time.
  35. </p>
  36. <h2 className="text-xl font-semibold mt-12 mb-4">AG-UI Event Types</h2>
  37. <p className="text-sm text-muted-foreground mb-4">
  38. AG-UI defines several event types for agent-UI communication:
  39. </p>
  40. <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
  41. <li>
  42. <code className="text-foreground">TEXT_MESSAGE_START</code> /{" "}
  43. <code className="text-foreground">TEXT_MESSAGE_CONTENT</code> /{" "}
  44. <code className="text-foreground">TEXT_MESSAGE_END</code> — Streaming
  45. text messages
  46. </li>
  47. <li>
  48. <code className="text-foreground">TOOL_CALL_START</code> /{" "}
  49. <code className="text-foreground">TOOL_CALL_ARGS</code> /{" "}
  50. <code className="text-foreground">TOOL_CALL_END</code> — Tool/function
  51. calls
  52. </li>
  53. <li>
  54. <code className="text-foreground">STATE_SNAPSHOT</code> /{" "}
  55. <code className="text-foreground">STATE_DELTA</code> — State updates
  56. </li>
  57. <li>
  58. <code className="text-foreground">CUSTOM</code> — Custom events for UI
  59. rendering
  60. </li>
  61. </ul>
  62. <h3 className="text-lg font-medium mt-8 mb-3">
  63. Example AG-UI Event Stream
  64. </h3>
  65. <Code lang="json">{`{"type": "RUN_STARTED", "threadId": "thread-123", "runId": "run-456"}
  66. {"type": "TEXT_MESSAGE_START", "messageId": "msg-1", "role": "assistant"}
  67. {"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg-1", "delta": "Here's a dashboard for you:"}
  68. {"type": "TEXT_MESSAGE_END", "messageId": "msg-1"}
  69. {"type": "TOOL_CALL_START", "toolCallId": "tc-1", "toolCallName": "render_ui"}
  70. {"type": "TOOL_CALL_ARGS", "toolCallId": "tc-1", "delta": "{\\"component\\": \\"Dashboard\\", \\"props\\": {\\"title\\": \\"Sales\\"}}"}
  71. {"type": "TOOL_CALL_END", "toolCallId": "tc-1"}
  72. {"type": "RUN_FINISHED"}`}</Code>
  73. <h2 className="text-xl font-semibold mt-12 mb-4">
  74. Define the AG-UI Schema
  75. </h2>
  76. <p className="text-sm text-muted-foreground mb-4">
  77. Define schemas for AG-UI event types:
  78. </p>
  79. <Code lang="typescript">{`import { z } from 'zod';
  80. // Base event schema
  81. const BaseEvent = z.object({
  82. type: z.string(),
  83. timestamp: z.number().optional(),
  84. });
  85. // Text message events
  86. const TextMessageStart = BaseEvent.extend({
  87. type: z.literal('TEXT_MESSAGE_START'),
  88. messageId: z.string(),
  89. role: z.enum(['user', 'assistant']),
  90. });
  91. const TextMessageContent = BaseEvent.extend({
  92. type: z.literal('TEXT_MESSAGE_CONTENT'),
  93. messageId: z.string(),
  94. delta: z.string(),
  95. });
  96. const TextMessageEnd = BaseEvent.extend({
  97. type: z.literal('TEXT_MESSAGE_END'),
  98. messageId: z.string(),
  99. });
  100. // Tool call events
  101. const ToolCallStart = BaseEvent.extend({
  102. type: z.literal('TOOL_CALL_START'),
  103. toolCallId: z.string(),
  104. toolCallName: z.string(),
  105. parentMessageId: z.string().optional(),
  106. });
  107. const ToolCallArgs = BaseEvent.extend({
  108. type: z.literal('TOOL_CALL_ARGS'),
  109. toolCallId: z.string(),
  110. delta: z.string(),
  111. });
  112. const ToolCallEnd = BaseEvent.extend({
  113. type: z.literal('TOOL_CALL_END'),
  114. toolCallId: z.string(),
  115. });
  116. // State events
  117. const StateSnapshot = BaseEvent.extend({
  118. type: z.literal('STATE_SNAPSHOT'),
  119. snapshot: z.record(z.unknown()),
  120. });
  121. const StateDelta = BaseEvent.extend({
  122. type: z.literal('STATE_DELTA'),
  123. delta: z.array(z.object({
  124. op: z.enum(['add', 'remove', 'replace']),
  125. path: z.string(),
  126. value: z.unknown().optional(),
  127. })),
  128. });
  129. // Custom event for UI components
  130. const CustomEvent = BaseEvent.extend({
  131. type: z.literal('CUSTOM'),
  132. name: z.string(),
  133. value: z.unknown(),
  134. });
  135. // Run lifecycle events
  136. const RunStarted = BaseEvent.extend({
  137. type: z.literal('RUN_STARTED'),
  138. threadId: z.string(),
  139. runId: z.string(),
  140. });
  141. const RunFinished = BaseEvent.extend({
  142. type: z.literal('RUN_FINISHED'),
  143. });
  144. const RunError = BaseEvent.extend({
  145. type: z.literal('RUN_ERROR'),
  146. message: z.string(),
  147. code: z.string().optional(),
  148. });
  149. // Union of all events
  150. export const AGUIEvent = z.discriminatedUnion('type', [
  151. TextMessageStart,
  152. TextMessageContent,
  153. TextMessageEnd,
  154. ToolCallStart,
  155. ToolCallArgs,
  156. ToolCallEnd,
  157. StateSnapshot,
  158. StateDelta,
  159. CustomEvent,
  160. RunStarted,
  161. RunFinished,
  162. RunError,
  163. ]);
  164. export type AGUIEvent = z.infer<typeof AGUIEvent>;`}</Code>
  165. <h2 className="text-xl font-semibold mt-12 mb-4">
  166. Define the AG-UI Catalog
  167. </h2>
  168. <p className="text-sm text-muted-foreground mb-4">
  169. Create a catalog for UI components that agents can render:
  170. </p>
  171. <Code lang="typescript">{`import { createCatalog } from '@json-render/core';
  172. import { z } from 'zod';
  173. export const aguiCatalog = createCatalog({
  174. components: {
  175. // Layout components
  176. Container: {
  177. description: 'A container for grouping elements',
  178. props: z.object({
  179. direction: z.enum(['row', 'column']).optional(),
  180. gap: z.enum(['none', 'sm', 'md', 'lg']).optional(),
  181. padding: z.enum(['none', 'sm', 'md', 'lg']).optional(),
  182. }),
  183. },
  184. Card: {
  185. description: 'A card with optional title',
  186. props: z.object({
  187. title: z.string().optional(),
  188. description: z.string().optional(),
  189. }),
  190. },
  191. // Content components
  192. Text: {
  193. description: 'Text content',
  194. props: z.object({
  195. content: z.string(),
  196. variant: z.enum(['body', 'heading', 'caption', 'code']).optional(),
  197. }),
  198. },
  199. Markdown: {
  200. description: 'Renders markdown content',
  201. props: z.object({
  202. content: z.string(),
  203. }),
  204. },
  205. Image: {
  206. description: 'Displays an image',
  207. props: z.object({
  208. src: z.string(),
  209. alt: z.string().optional(),
  210. width: z.number().optional(),
  211. height: z.number().optional(),
  212. }),
  213. },
  214. // Data display
  215. Table: {
  216. description: 'Displays tabular data',
  217. props: z.object({
  218. columns: z.array(z.object({
  219. key: z.string(),
  220. header: z.string(),
  221. width: z.string().optional(),
  222. })),
  223. data: z.array(z.record(z.unknown())),
  224. }),
  225. },
  226. Chart: {
  227. description: 'Renders a chart',
  228. props: z.object({
  229. type: z.enum(['line', 'bar', 'pie', 'area']),
  230. data: z.array(z.record(z.unknown())),
  231. xKey: z.string(),
  232. yKey: z.string(),
  233. title: z.string().optional(),
  234. }),
  235. },
  236. Metric: {
  237. description: 'Displays a metric value',
  238. props: z.object({
  239. label: z.string(),
  240. value: z.union([z.string(), z.number()]),
  241. change: z.number().optional(),
  242. format: z.enum(['number', 'currency', 'percent']).optional(),
  243. }),
  244. },
  245. // Input components
  246. Button: {
  247. description: 'Interactive button',
  248. props: z.object({
  249. label: z.string(),
  250. variant: z.enum(['primary', 'secondary', 'outline', 'ghost']).optional(),
  251. disabled: z.boolean().optional(),
  252. }),
  253. },
  254. Input: {
  255. description: 'Text input field',
  256. props: z.object({
  257. name: z.string(),
  258. label: z.string().optional(),
  259. placeholder: z.string().optional(),
  260. type: z.enum(['text', 'email', 'password', 'number']).optional(),
  261. required: z.boolean().optional(),
  262. }),
  263. },
  264. Select: {
  265. description: 'Dropdown select',
  266. props: z.object({
  267. name: z.string(),
  268. label: z.string().optional(),
  269. options: z.array(z.object({
  270. value: z.string(),
  271. label: z.string(),
  272. })),
  273. placeholder: z.string().optional(),
  274. }),
  275. },
  276. Form: {
  277. description: 'Form container',
  278. props: z.object({
  279. id: z.string(),
  280. submitLabel: z.string().optional(),
  281. }),
  282. },
  283. // Feedback components
  284. Alert: {
  285. description: 'Alert message',
  286. props: z.object({
  287. message: z.string(),
  288. type: z.enum(['info', 'success', 'warning', 'error']).optional(),
  289. }),
  290. },
  291. Progress: {
  292. description: 'Progress indicator',
  293. props: z.object({
  294. value: z.number(),
  295. max: z.number().optional(),
  296. label: z.string().optional(),
  297. }),
  298. },
  299. Skeleton: {
  300. description: 'Loading placeholder',
  301. props: z.object({
  302. width: z.string().optional(),
  303. height: z.string().optional(),
  304. variant: z.enum(['text', 'circular', 'rectangular']).optional(),
  305. }),
  306. },
  307. },
  308. actions: {
  309. submit: {
  310. description: 'Submit form data',
  311. params: z.object({
  312. formId: z.string(),
  313. }),
  314. },
  315. navigate: {
  316. description: 'Navigate to a URL',
  317. params: z.object({
  318. url: z.string(),
  319. }),
  320. },
  321. callback: {
  322. description: 'Trigger a callback to the agent',
  323. params: z.object({
  324. name: z.string(),
  325. data: z.record(z.unknown()).optional(),
  326. }),
  327. },
  328. },
  329. });`}</Code>
  330. <h2 className="text-xl font-semibold mt-12 mb-4">
  331. Build an AG-UI Event Processor
  332. </h2>
  333. <p className="text-sm text-muted-foreground mb-4">
  334. Process AG-UI events and render UI components:
  335. </p>
  336. <Code lang="tsx">{`'use client';
  337. import React, { useState, useCallback } from 'react';
  338. import { AGUIEvent } from './schema';
  339. interface AGUIState {
  340. messages: Array<{
  341. id: string;
  342. role: 'user' | 'assistant';
  343. content: string;
  344. }>;
  345. toolCalls: Map<string, {
  346. name: string;
  347. args: string;
  348. result?: unknown;
  349. }>;
  350. state: Record<string, unknown>;
  351. isRunning: boolean;
  352. }
  353. export function useAGUI() {
  354. const [aguiState, setAGUIState] = useState<AGUIState>({
  355. messages: [],
  356. toolCalls: new Map(),
  357. state: {},
  358. isRunning: false,
  359. });
  360. const processEvent = useCallback((event: AGUIEvent) => {
  361. switch (event.type) {
  362. case 'RUN_STARTED':
  363. setAGUIState(prev => ({ ...prev, isRunning: true }));
  364. break;
  365. case 'RUN_FINISHED':
  366. setAGUIState(prev => ({ ...prev, isRunning: false }));
  367. break;
  368. case 'TEXT_MESSAGE_START':
  369. setAGUIState(prev => ({
  370. ...prev,
  371. messages: [...prev.messages, {
  372. id: event.messageId,
  373. role: event.role,
  374. content: '',
  375. }],
  376. }));
  377. break;
  378. case 'TEXT_MESSAGE_CONTENT':
  379. setAGUIState(prev => ({
  380. ...prev,
  381. messages: prev.messages.map(msg =>
  382. msg.id === event.messageId
  383. ? { ...msg, content: msg.content + event.delta }
  384. : msg
  385. ),
  386. }));
  387. break;
  388. case 'TOOL_CALL_START':
  389. setAGUIState(prev => {
  390. const toolCalls = new Map(prev.toolCalls);
  391. toolCalls.set(event.toolCallId, { name: event.toolCallName, args: '' });
  392. return { ...prev, toolCalls };
  393. });
  394. break;
  395. case 'TOOL_CALL_ARGS':
  396. setAGUIState(prev => {
  397. const toolCalls = new Map(prev.toolCalls);
  398. const tc = toolCalls.get(event.toolCallId);
  399. if (tc) {
  400. toolCalls.set(event.toolCallId, { ...tc, args: tc.args + event.delta });
  401. }
  402. return { ...prev, toolCalls };
  403. });
  404. break;
  405. case 'STATE_SNAPSHOT':
  406. setAGUIState(prev => ({ ...prev, state: event.snapshot }));
  407. break;
  408. case 'STATE_DELTA':
  409. setAGUIState(prev => {
  410. const newState = { ...prev.state };
  411. for (const op of event.delta) {
  412. const parts = op.path.split('/').filter(Boolean);
  413. if (op.op === 'replace' || op.op === 'add') {
  414. let obj: any = newState;
  415. for (let i = 0; i < parts.length - 1; i++) {
  416. obj = obj[parts[i]] = obj[parts[i]] || {};
  417. }
  418. obj[parts[parts.length - 1]] = op.value;
  419. } else if (op.op === 'remove') {
  420. let obj: any = newState;
  421. for (let i = 0; i < parts.length - 1; i++) {
  422. obj = obj[parts[i]];
  423. if (!obj) break;
  424. }
  425. if (obj) delete obj[parts[parts.length - 1]];
  426. }
  427. }
  428. return { ...prev, state: newState };
  429. });
  430. break;
  431. }
  432. }, []);
  433. return { state: aguiState, processEvent };
  434. }`}</Code>
  435. <h2 className="text-xl font-semibold mt-12 mb-4">
  436. Rendering Tool Call Results as UI
  437. </h2>
  438. <p className="text-sm text-muted-foreground mb-4">
  439. When an agent calls a <code className="text-foreground">render_ui</code>{" "}
  440. tool, parse the arguments and render the component:
  441. </p>
  442. <Code lang="tsx">{`import { aguiCatalog } from './catalog';
  443. // Component registry
  444. const components: Record<string, React.FC<any>> = {
  445. Container: ({ direction = 'column', gap = 'md', children }) => (
  446. <div className={\`flex flex-\${direction} gap-\${gap}\`}>{children}</div>
  447. ),
  448. Card: ({ title, description, children }) => (
  449. <div className="border rounded-lg p-4">
  450. {title && <h3 className="font-semibold">{title}</h3>}
  451. {description && <p className="text-muted-foreground text-sm">{description}</p>}
  452. {children}
  453. </div>
  454. ),
  455. Text: ({ content, variant = 'body' }) => {
  456. const styles = {
  457. body: 'text-sm',
  458. heading: 'text-lg font-semibold',
  459. caption: 'text-xs text-muted-foreground',
  460. code: 'font-mono text-sm bg-muted px-1 rounded',
  461. };
  462. return <p className={styles[variant]}>{content}</p>;
  463. },
  464. Button: ({ label, variant = 'primary', onClick }) => (
  465. <button
  466. className={\`px-4 py-2 rounded text-sm \${
  467. variant === 'primary' ? 'bg-primary text-primary-foreground' :
  468. variant === 'secondary' ? 'bg-secondary text-secondary-foreground' :
  469. 'border'
  470. }\`}
  471. onClick={onClick}
  472. >
  473. {label}
  474. </button>
  475. ),
  476. Metric: ({ label, value, change, format }) => {
  477. const formatted = format === 'currency' ? \`$\${value.toLocaleString()}\` :
  478. format === 'percent' ? \`\${value}%\` :
  479. value.toLocaleString();
  480. return (
  481. <div className="p-4 border rounded">
  482. <p className="text-sm text-muted-foreground">{label}</p>
  483. <p className="text-2xl font-bold">{formatted}</p>
  484. {change !== undefined && (
  485. <p className={\`text-sm \${change >= 0 ? 'text-green-600' : 'text-red-600'}\`}>
  486. {change >= 0 ? '+' : ''}{change}%
  487. </p>
  488. )}
  489. </div>
  490. );
  491. },
  492. Alert: ({ message, type = 'info' }) => {
  493. const styles = {
  494. info: 'bg-blue-50 text-blue-800 border-blue-200',
  495. success: 'bg-green-50 text-green-800 border-green-200',
  496. warning: 'bg-yellow-50 text-yellow-800 border-yellow-200',
  497. error: 'bg-red-50 text-red-800 border-red-200',
  498. };
  499. return <div className={\`p-3 rounded border \${styles[type]}\`}>{message}</div>;
  500. },
  501. // Add more components...
  502. };
  503. // Render tool call result
  504. export function renderToolCallUI(toolCall: { name: string; args: string }) {
  505. if (toolCall.name !== 'render_ui') return null;
  506. try {
  507. const parsed = JSON.parse(toolCall.args);
  508. const Component = components[parsed.component];
  509. if (!Component) return null;
  510. return <Component {...parsed.props} />;
  511. } catch {
  512. return null;
  513. }
  514. }`}</Code>
  515. <h2 className="text-xl font-semibold mt-12 mb-4">Usage Example</h2>
  516. <Code lang="tsx">{`'use client';
  517. import { useAGUI } from './use-agui';
  518. import { renderToolCallUI } from './renderer';
  519. export function AGUIChat() {
  520. const { state, processEvent } = useAGUI();
  521. // Connect to AG-UI event stream
  522. async function startRun(prompt: string) {
  523. const response = await fetch('/api/agent', {
  524. method: 'POST',
  525. headers: { 'Content-Type': 'application/json' },
  526. body: JSON.stringify({ prompt }),
  527. });
  528. const reader = response.body?.getReader();
  529. const decoder = new TextDecoder();
  530. while (reader) {
  531. const { done, value } = await reader.read();
  532. if (done) break;
  533. const lines = decoder.decode(value).split('\\n').filter(Boolean);
  534. for (const line of lines) {
  535. const event = JSON.parse(line);
  536. processEvent(event);
  537. }
  538. }
  539. }
  540. return (
  541. <div className="space-y-4">
  542. {/* Messages */}
  543. {state.messages.map(msg => (
  544. <div key={msg.id} className={\`p-3 rounded \${
  545. msg.role === 'assistant' ? 'bg-muted' : 'bg-primary/10'
  546. }\`}>
  547. {msg.content}
  548. </div>
  549. ))}
  550. {/* Rendered UI from tool calls */}
  551. {Array.from(state.toolCalls.values()).map((tc, i) => (
  552. <div key={i}>{renderToolCallUI(tc)}</div>
  553. ))}
  554. {/* Input */}
  555. <form onSubmit={(e) => {
  556. e.preventDefault();
  557. const input = e.currentTarget.querySelector('input');
  558. if (input?.value) {
  559. startRun(input.value);
  560. input.value = '';
  561. }
  562. }}>
  563. <input
  564. type="text"
  565. placeholder="Ask the agent..."
  566. className="w-full px-4 py-2 border rounded"
  567. disabled={state.isRunning}
  568. />
  569. </form>
  570. </div>
  571. );
  572. }`}</Code>
  573. <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
  574. <p className="text-sm text-muted-foreground">
  575. Learn about{" "}
  576. <Link href="/docs/openapi" className="text-foreground hover:underline">
  577. OpenAPI integration
  578. </Link>{" "}
  579. for rendering forms from API schemas.
  580. </p>
  581. </article>
  582. );
  583. }