From 338bb62ad503431f20277cf329e9b203f2e3f7ad Mon Sep 17 00:00:00 2001 From: Ronit Raj Date: Mon, 3 Nov 2025 03:54:00 +0530 Subject: [PATCH] feat: implement visual agent builder with node-based interface - Add visual agent builder using React Flow for drag-and-drop workflow creation - Implement 5 core node types: BasicInfo, LLM, SystemPrompt, Tool, Output - Add bidirectional conversion between form data and visual graph - Create node library, canvas, and properties panel components - Implement real-time validation with error/warning display - Add mode toggle to switch between form and visual builder - Support declarative agents with model and tool configuration - Integrate with existing agent creation workflow and validation Resolves #916 Signed-off-by: Ronit Raj --- ui/package-lock.json | 230 ++++++++++++ ui/package.json | 1 + ui/src/app/actions/agents.ts | 3 +- ui/src/app/agents/new/page.tsx | 108 +++++- ui/src/app/globals.css | 5 + ui/src/app/layout.tsx | 2 +- ui/src/components/AgentsProvider.tsx | 26 +- .../agent-builder/BuilderModeToggle.tsx | 41 +++ ui/src/components/agent-builder/Canvas.tsx | 175 +++++++++ .../agent-builder/ErrorBoundary.tsx | 90 +++++ .../components/agent-builder/NodeLibrary.tsx | 111 ++++++ .../agent-builder/VisualAgentBuilder.tsx | 335 ++++++++++++++++++ .../agent-builder/nodes/BasicInfoNode.tsx | 45 +++ .../agent-builder/nodes/LLMNode.tsx | 41 +++ .../agent-builder/nodes/OutputNode.tsx | 41 +++ .../agent-builder/nodes/SystemPromptNode.tsx | 48 +++ .../agent-builder/nodes/ToolNode.tsx | 73 ++++ .../properties/BasicInfoPropertyEditor.tsx | 87 +++++ .../properties/LLMPropertyEditor.tsx | 143 ++++++++ .../properties/NodeProperties.tsx | 161 +++++++++ .../properties/OutputPropertyEditor.tsx | 51 +++ .../properties/PropertyEditor.tsx | 48 +++ .../properties/SystemPromptPropertyEditor.tsx | 35 ++ .../properties/ToolPropertyEditor.tsx | 258 ++++++++++++++ .../onboarding/OnboardingWizard.tsx | 4 +- ui/src/lib/agent-builder/formConverter.ts | 170 +++++++++ ui/src/lib/agent-builder/graphConverter.ts | 264 ++++++++++++++ ui/src/lib/agent-builder/utils.ts | 268 ++++++++++++++ ui/src/lib/agent-builder/validation.ts | 255 +++++++++++++ ui/src/types/index.ts | 207 +++++++++++ 30 files changed, 3277 insertions(+), 49 deletions(-) create mode 100644 ui/src/components/agent-builder/BuilderModeToggle.tsx create mode 100644 ui/src/components/agent-builder/Canvas.tsx create mode 100644 ui/src/components/agent-builder/ErrorBoundary.tsx create mode 100644 ui/src/components/agent-builder/NodeLibrary.tsx create mode 100644 ui/src/components/agent-builder/VisualAgentBuilder.tsx create mode 100644 ui/src/components/agent-builder/nodes/BasicInfoNode.tsx create mode 100644 ui/src/components/agent-builder/nodes/LLMNode.tsx create mode 100644 ui/src/components/agent-builder/nodes/OutputNode.tsx create mode 100644 ui/src/components/agent-builder/nodes/SystemPromptNode.tsx create mode 100644 ui/src/components/agent-builder/nodes/ToolNode.tsx create mode 100644 ui/src/components/agent-builder/properties/BasicInfoPropertyEditor.tsx create mode 100644 ui/src/components/agent-builder/properties/LLMPropertyEditor.tsx create mode 100644 ui/src/components/agent-builder/properties/NodeProperties.tsx create mode 100644 ui/src/components/agent-builder/properties/OutputPropertyEditor.tsx create mode 100644 ui/src/components/agent-builder/properties/PropertyEditor.tsx create mode 100644 ui/src/components/agent-builder/properties/SystemPromptPropertyEditor.tsx create mode 100644 ui/src/components/agent-builder/properties/ToolPropertyEditor.tsx create mode 100644 ui/src/lib/agent-builder/formConverter.ts create mode 100644 ui/src/lib/agent-builder/graphConverter.ts create mode 100644 ui/src/lib/agent-builder/utils.ts create mode 100644 ui/src/lib/agent-builder/validation.ts diff --git a/ui/package-lock.json b/ui/package-lock.json index bc603d81a..29661532c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -28,6 +28,7 @@ "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/typography": "^0.5.16", "@types/uuid": "^10.0.0", + "@xyflow/react": "^12.9.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -4269,6 +4270,55 @@ "integrity": "sha512-FYKQLvCsRYxZ3fp+XsoCiJZ1aK3x17RmaZjHI4Ou43khFkXPycrQaXo9b1J07PNlEfWnRtUc9loxHXzKjSsbYg==", "dev": true }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4872,6 +4922,66 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@xyflow/react": { + "version": "12.9.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.9.2.tgz", + "integrity": "sha512-Xr+LFcysHCCoc5KRHaw+FwbqbWYxp9tWtk1mshNcqy25OAPuaKzXSdqIMNOA82TIXF/gFKo0Wgpa6PU7wUUVqw==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.72", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.72", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.72.tgz", + "integrity": "sha512-WBI5Aau0fXTXwxHPzceLNS6QdXggSWnGjDtj/gG669crApN8+SCmEtkBth1m7r6pStNo/5fI9McEi7Dk0ymCLA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -6009,6 +6119,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -6609,6 +6725,111 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -16172,6 +16393,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 82a3d9b4c..f1fece9d8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/typography": "^0.5.16", "@types/uuid": "^10.0.0", + "@xyflow/react": "^12.9.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/ui/src/app/actions/agents.ts b/ui/src/app/actions/agents.ts index 8ef821799..bb96cb146 100644 --- a/ui/src/app/actions/agents.ts +++ b/ui/src/app/actions/agents.ts @@ -1,10 +1,9 @@ "use server"; -import { AgentSpec, BaseResponse } from "@/types"; +import { AgentSpec, BaseResponse, AgentFormData } from "@/types"; import { Agent, AgentResponse, Tool } from "@/types"; import { revalidatePath } from "next/cache"; import { fetchApi, createErrorResponse } from "./utils"; -import { AgentFormData } from "@/components/AgentsProvider"; import { isMcpTool } from "@/lib/toolUtils"; import { k8sRefUtils } from "@/lib/k8sUtils"; diff --git a/ui/src/app/agents/new/page.tsx b/ui/src/app/agents/new/page.tsx index a7571b06b..f01f66b2f 100644 --- a/ui/src/app/agents/new/page.tsx +++ b/ui/src/app/agents/new/page.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Loader2, Settings2, PlusCircle, Trash2 } from "lucide-react"; -import { ModelConfig, AgentType } from "@/types"; +import { ModelConfig, AgentType, AgentFormData } from "@/types"; import { SystemPromptSection } from "@/components/create/SystemPromptSection"; import { ModelSelectionSection } from "@/components/create/ModelSelectionSection"; import { ToolsSection } from "@/components/create/ToolsSection"; @@ -14,13 +14,15 @@ import { useAgents } from "@/components/AgentsProvider"; import { LoadingState } from "@/components/LoadingState"; import { ErrorState } from "@/components/ErrorState"; import KagentLogo from "@/components/kagent-logo"; -import { AgentFormData } from "@/components/AgentsProvider"; import { Tool, EnvVar } from "@/types"; import { toast } from "sonner"; import { NamespaceCombobox } from "@/components/NamespaceCombobox"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { VisualAgentBuilder } from "@/components/agent-builder/VisualAgentBuilder"; +import { BuilderModeToggle } from "@/components/agent-builder/BuilderModeToggle"; +import { VisualBuilderErrorBoundary } from "@/components/agent-builder/ErrorBoundary"; interface ValidationErrors { name?: string; @@ -99,6 +101,23 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo errors: {}, }); + // Builder mode state (only for non-edit mode) + const [builderMode, setBuilderMode] = useState<'form' | 'visual'>('form'); + + // Hide footer when in visual builder mode + useEffect(() => { + if (builderMode === 'visual' && !isEditMode) { + document.body.classList.add('hide-footer'); + } else { + document.body.classList.remove('hide-footer'); + } + + // Cleanup on unmount + return () => { + document.body.classList.remove('hide-footer'); + }; + }, [builderMode, isEditMode]); + // Fetch existing agent data if in edit mode useEffect(() => { const fetchAgentData = async () => { @@ -305,10 +324,56 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo } return ( -
-
-

{isEditMode ? "Edit Agent" : "Create New Agent"}

+
+
+ {/* Header with title and builder mode toggle */} +
+

{isEditMode ? "Edit Agent" : "Create New Agent"}

+ {!isEditMode && ( + + )} +
+ {builderMode === 'visual' && !isEditMode ? ( + + { + setState(prev => ({ ...prev, errors })); + }} + onGraphDataChange={(graphData) => { + // Sync visual builder changes to form state + setState(prev => ({ + ...prev, + name: graphData.name, + namespace: graphData.namespace, + description: graphData.description, + systemPrompt: graphData.systemPrompt || '', + selectedModel: graphData.modelName ? { + ref: graphData.modelName, + model: graphData.modelName, + } : null, + selectedTools: graphData.tools || [], + })); + }} + initialFormData={{ + name: state.name, + namespace: state.namespace, + description: state.description, + type: state.agentType, + systemPrompt: state.systemPrompt, + modelName: state.selectedModel?.ref, + tools: state.selectedTools, + stream: true, + }} + onCreateAgent={handleSaveAgent} + isSubmitting={state.isSubmitting} + /> + + ) : (
@@ -578,21 +643,26 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo )} -
- -
+ )} + + {/* Submit button visible only in form mode */} + {builderMode === 'form' && ( +
+ +
+ )}
); diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index a6987249f..610f2ed6e 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -89,4 +89,9 @@ body { body { @apply bg-background text-foreground; } +} + +/* Hide footer when in visual builder mode */ +body.hide-footer footer { + display: none !important; } \ No newline at end of file diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 43f9c75e0..1ca5226d3 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -27,7 +27,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
{children}
+
{children}