BYOC — JSON Render
Bring your own component library. Have the agent emit a JSON spec and let json-render render it against a Zod-validated catalog of React components.
"""PydanticAI agent backing the BYOC json-render demo.Mirrors showcase/integrations/langgraph-python/src/agents/byoc_json_render_agent.pyso the PydanticAI port is functionally equivalent. Emits a single JSONobject shaped like `@json-render/react`'s flat spec format(`{ root, elements }`) so the frontend can feed it directly into`<Renderer />` against a Zod-validated catalog of three components —MetricCard, BarChart, PieChart.PydanticAI-specific note------------------------Unlike langgraph-python (which uses `create_agent` with a`CopilotKitMiddleware`), the PydanticAI `Agent` emits plain streamingtext by default. The system prompt here instructs the model to emitonly a JSON object; the frontend (`json-render-renderer.tsx`) alreadytolerates partial streams and falls back to the default bubble untilthe content parses."""from __future__ import annotationsfrom textwrap import dedentfrom pydantic_ai import Agentfrom pydantic_ai.models.openai import OpenAIResponsesModelSYSTEM_PROMPT = dedent( """ You are a sales-dashboard UI generator for a BYOC json-render demo. When the user asks for a UI, respond with **exactly one JSON object** and nothing else — no prose, no markdown fences, no leading explanation. The object must match this schema (the "flat element map" format consumed by `@json-render/react`): { "root": "<id of the root element>", "elements": { "<id>": { "type": "<component name>", "props": { ... component-specific props ... }, "children": [ "<id>", ... ] }, ... } } Available components (use each name verbatim as "type"): - MetricCard props: { "label": string, "value": string, "trend": string | null } Example trend strings: "+12% vs last quarter", "-3% vs last month", null. - BarChart props: { "title": string, "description": string | null, "data": [ { "label": string, "value": number }, ... ] } - PieChart props: { "title": string, "description": string | null, "data": [ { "label": string, "value": number }, ... ] } Rules: 1. Output **only** valid JSON. No markdown code fences. No text outside the object. 2. Every id referenced in `root` or any `children` array must be a key in `elements`. 3. For a multi-component dashboard, use a root MetricCard and list the charts in its `children` array, OR pick any element as root and list the others as its children. Do not emit orphan elements. 4. Use realistic sales-domain values (revenue, pipeline, conversion, categories, months) — the demo is a sales dashboard. 5. `children` is optional but when present must be an array of strings. 6. Never invent component types outside the three listed above. ### Worked example — "Show me the sales dashboard with metrics and a revenue chart" { "root": "revenue-metric", "elements": { "revenue-metric": { "type": "MetricCard", "props": { "label": "Revenue (Q3)", "value": "$1.24M", "trend": "+18% vs Q2" }, "children": ["revenue-bar"] }, "revenue-bar": { "type": "BarChart", "props": { "title": "Monthly revenue", "description": "Revenue by month across Q3", "data": [ { "label": "Jul", "value": 380000 }, { "label": "Aug", "value": 410000 }, { "label": "Sep", "value": 450000 } ] } } } } ### Worked example — "Break down revenue by category as a pie chart" { "root": "category-pie", "elements": { "category-pie": { "type": "PieChart", "props": { "title": "Revenue by category", "description": "Share of total revenue by product category", "data": [ { "label": "Enterprise", "value": 540000 }, { "label": "SMB", "value": 310000 }, { "label": "Self-serve", "value": 220000 }, { "label": "Partner", "value": 170000 } ] } } } } ### Worked example — "Show me monthly expenses as a bar chart" { "root": "expense-bar", "elements": { "expense-bar": { "type": "BarChart", "props": { "title": "Monthly expenses", "description": "Operating expenses by month", "data": [ { "label": "Jul", "value": 210000 }, { "label": "Aug", "value": 225000 }, { "label": "Sep", "value": 240000 } ] } } } } Respond with the JSON object only. """).strip()agent = Agent( model=OpenAIResponsesModel("gpt-4o-mini"), system_prompt=SYSTEM_PROMPT,)You have a chat surface and you want the agent to draw a dashboard from a typed JSON spec. By the end of this guide, the agent will emit a { root, elements } object, @json-render/react will validate it against a Zod-described catalog, and the user sees the dashboard render as a single React tree.
When to use this#
- Structured UI with a typed contract where the agent's output is validated against a known schema before it touches the DOM.
- Tolerance for prose preamble + code fences in the agent's output (json-render's parser handles them).
- Cases where you already use json-render elsewhere or prefer Zod-validated catalogs.
If you'd rather have a streaming progressive render rather than a one-shot validated render, see the sibling page BYOC — Hashbrown for the same scenario with @hashbrownai/react.
Frontend#
The integration point is <CopilotChat>'s messageView.assistantMessage slot. Swap the default renderer for a json-render-backed one:
import {
CopilotKit,
CopilotChat,
useConfigureSuggestions,
} from "@copilotkit/react-core/v2";
import { JsonRenderAssistantMessage } from "./json-render-renderer";
export default function ByocJsonRenderDemo() {
useConfigureSuggestions({
suggestions: [
{ title: "Sales dashboard", message: "Show me a sales dashboard." },
{ title: "Region breakdown", message: "Break down sales by region." },
],
available: "always",
});
return (
<CopilotKit runtimeUrl="/api/copilotkit-byoc-json-render" agent="byoc_json_render">
<CopilotChat
messageView={{ assistantMessage: JsonRenderAssistantMessage }}
/>
</CopilotKit>
);
}The custom renderer parses the streaming assistant content (tolerating partial tokens, code fences, and prose preamble), validates each element against a Zod-typed catalog, and feeds the resulting spec into <Renderer />:
import { Renderer } from "@json-render/react";
import { catalog } from "./registry";
export function JsonRenderAssistantMessage({ message }: { message: AssistantMessage }) {
const spec = parseSpec(message.content ?? "");
if (!spec) return null;
return <Renderer spec={spec} catalog={catalog} />;
}
function parseSpec(content: string) {
const cleaned = stripCodeFencesAndPrelude(content);
const partial = tolerantJsonParse(cleaned);
return validateAgainstCatalog(partial);
}The catalog lives next to the renderer and pairs each component with a Zod schema describing its props:
import { z } from "zod";
import { MetricCard } from "./metric-card";
import { BarChart } from "./charts/bar-chart";
import { PieChart } from "./charts/pie-chart";
export const catalog = {
MetricCard: {
component: MetricCard,
propsSchema: z.object({
title: z.string(),
value: z.number(),
delta: z.number().optional(),
}),
},
BarChart: {
component: BarChart,
propsSchema: z.object({
data: z.array(z.object({ label: z.string(), value: z.number() })),
}),
},
PieChart: {
component: PieChart,
propsSchema: z.object({
data: z.array(z.object({ label: z.string(), value: z.number() })),
}),
},
};Validation is the safety net: anything the agent emits that doesn't match a registered schema is rejected before it hits React, so the chat can't render arbitrary garbage.
Backend#
The agent emits a { root, elements } JSON object as the assistant message content. root references a top-level element id; elements maps each id to a { type, props, children } triple matching the catalog.
{
"root": "dashboard",
"elements": {
"dashboard": {
"type": "Stack",
"children": ["revenue-card", "by-region"]
},
"revenue-card": {
"type": "MetricCard",
"props": { "title": "Total revenue", "value": 184302 }
},
"by-region": {
"type": "BarChart",
"props": { "data": [...] }
}
}
}Anything else (free-form text, code fences around the JSON, a "Here's your dashboard:" preamble) is stripped by the renderer's tolerant parser before validation. The agent doesn't need to be perfectly clean.
Comparing the two patterns#
Both byoc-json-render and byoc-hashbrown solve the same problem with two different rendering libraries. The agent contract is similar; the React glue, validation strategy, and rendering behaviour differ.
