+ {/* Left sidebar with help & instructions */}
+
+
+ {leftOpen ? (
+
+ {/* Intro */}
+
+
Graph Builder
+
+ Visually design Burr application graphs, then export as Python code or JSON.
+
+
+
+ {/* Quick Start */}
+
+
+ Quick Start
+
+
+ Add action nodes to the canvas
+ Connect them by dragging between handles
+
+ Switch to the Python tab to see generated
+ code
+
+
+
+
+ {/* Creating */}
+
+
+ Creating
+
+
+
+
+
+ {navigator.platform?.includes('Mac') ? '\u2318' : 'Ctrl'}+Click
+
+
+
+ Add an action node at that position
+
+
+
+
+
+ {navigator.platform?.includes('Mac') ? '\u2318' : 'Ctrl'}+Right-click
+
+
+
Add an input node
+
+
+
+
+ Drag
+
+
+
+ From bottom handle to top handle to create an edge (transition)
+
+
+
+
+
+ +
+
+
+
+ Use the button at the bottom-right to add a node via dialog
+
+
+
+
+
+ {/* Editing */}
+
+
+ Editing
+
+
+
+
+ Click label
+
+
Edit a node's name inline
+
+
+
+ Click edge label
+
+
+ Edit a conditional edge's condition text
+
+
+
+
+ Select node
+
+
+ Toggle action/streaming type via the badge in the top-right corner
+
+
+
+
+ Click edge
+
+
+ Pick a color or toggle conditional/default
+
+
+
+
+
+ {/* Deleting */}
+
+
+ Deleting
+
+
+
+
+ {navigator.platform?.includes('Mac') ? '\u232b' : 'Backspace'}
+
+
+
Delete the selected node or edge
+
+
+
+ {/* Concepts */}
+
+
+ Node Types
+
+
+
+
+
+
Action
+
+ A step decorated with{' '}
+ @action
+
+
+
+
+
+
+
Input
+
+ External input passed into an action at runtime
+
+
+
+
+
+
+ {/* Node Flags */}
+
+
+ Action Flags
+
+
+ Select a node to toggle these independently:
+
+
+
+
+ async
+
+
+ Makes the function async def
+
+
+
+
+ stream
+
+
+ Uses @streaming_action and
+ yields results
+
+
+
+
+
+ {/* Edge Types */}
+
+
+ Edge Types
+
+
+
+
+
+
Default
+
+ Unconditional transition (uses{' '}
+ default)
+
+
+
+
+
+
+
Conditional
+
+ Guarded transition (uses{' '}
+ when()). Created
+ automatically when a node has multiple outgoing edges.
+
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+ setLeftOpen(!leftOpen)}
+ className="p-1 rounded hover:bg-gray-100"
+ title={leftOpen ? 'Collapse help panel' : 'Expand help panel'}
+ >
+ {leftOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Main content area */}
+
+ {/* Tab navigation */}
+
+
+ {[
+ { label: 'Canvas', title: 'Visual graph editor' },
+ { label: 'Python', title: 'Generated Burr Python code' },
+ { label: 'JSON', title: 'Graph data as JSON (importable)' }
+ ].map((tab, idx) => (
+ setTabIndex(idx)}
+ title={tab.title}
+ className={`py-2 px-1 border-b-2 font-medium text-sm ${
+ tabIndex === idx
+ ? 'border-blue-500 text-blue-600'
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
+ }`}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+ {/* Tab content */}
+
+ {tabIndex === 0 && (
+
+
+
+
+
+
+
+ {/* Empty state overlay */}
+ {nodes.length === 0 && (
+
+
+
+ Design your Burr graph
+
+
+ Build application graphs visually and export as Python code.
+
+
+
+
+
+ {navigator.platform?.includes('Mac') ? '\u2318' : 'Ctrl'}+Click
+
+ Add an action node
+
+
+
+ Drag handle
+
+ Connect nodes with edges
+
+
+
+
+
+
+ Add First Node
+
+
setShowExamplePicker(true)}
+ >
+ Load Example
+
+
+
+
+ )}
+
+
+ {hasExistingContent && (
+
setConfirmClearOpen(true)}
+ title="Clear canvas"
+ aria-label="Clear canvas"
+ >
+
+
+ )}
+
+
+
+
+
+ )}
+ {tabIndex === 1 && (
+
+
+ {
+ navigator.clipboard
+ .writeText(pythonCode)
+ .then(() => {
+ setCopied('python');
+ setTimeout(() => setCopied(null), 1200);
+ })
+ .catch(() => {
+ /* clipboard not available */
+ });
+ }}
+ title={copied === 'python' ? 'Copied!' : 'Copy Python code to clipboard'}
+ aria-label="Copy Python code"
+ >
+
+
+
+
+
+ {pythonCode}
+
+
+
+ )}
+ {tabIndex === 2 && (
+
+
+ {
+ navigator.clipboard
+ .writeText(jsonCode)
+ .then(() => {
+ setCopied('json');
+ setTimeout(() => setCopied(null), 1200);
+ })
+ .catch(() => {
+ /* clipboard not available */
+ });
+ }}
+ title={copied === 'json' ? 'Copied!' : 'Copy JSON to clipboard'}
+ aria-label="Copy JSON code"
+ >
+
+
+
+
+
+ {jsonCode}
+
+
+
+ )}
+
+
+
+ {/* Right panel: ExampleGallery */}
+
+
+ {rightOpen ? (
+
+
+
+ ) : (
+
+ )}
+
+ setRightOpen(!rightOpen)}
+ className="p-1 rounded hover:bg-gray-100"
+ title={rightOpen ? 'Collapse examples panel' : 'Expand examples panel'}
+ >
+ {rightOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Add Node Dialog */}
+ {nodeDialog && (
+
setNodeDialog(false)}
+ >
+
e.stopPropagation()}
+ >
+
Add New Node
+
+
+ Node Label
+ setNodeDialogData({ ...nodeDialogData, label: e.target.value })}
+ className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+ Description
+
+
+ Node Type
+
+ setNodeDialogData({ ...nodeDialogData, nodeType: e.target.value })
+ }
+ className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ {nodeTemplates.map((template) => (
+
+ {template.label}
+
+ ))}
+
+
+
+
+ setNodeDialog(false)} outline>
+ Cancel
+
+ Create Node
+
+
+
+ )}
+
+ {/* Color Picker Popover for Edges */}
+ {colorPickerOpen && colorPickerAnchor && (
+
+
Select Edge Color
+
+ {edgeColors.map((color) => (
+ handleEdgeColorChange(color)}
+ />
+ ))}
+
+ {(() => {
+ if (!selectedEdge) return null;
+ const selected = edges.find((e) => e.id === selectedEdge);
+ if (!selected) return null;
+ const groupEdges = edges.filter((e) => e.source === selected.source);
+ if (groupEdges.length > 1) {
+ return (
+
+ {selected.data?.isConditional ? (
+
+ Make Default
+
+ ) : (
+
+ Make Conditional
+
+ )}
+
+ );
+ }
+ return null;
+ })()}
+
+ )}
+
+ {/* Example Picker Modal (from empty state) */}
+ {showExamplePicker && (
+
setShowExamplePicker(false)}
+ >
+
e.stopPropagation()}
+ >
+
Load an Example
+
+ Pick a pre-built graph to explore the builder.
+
+
+ {examples.map((example) => (
+
{
+ setShowExamplePicker(false);
+ handleLoadExample(example);
+ }}
+ >
+ {example.title}
+ {example.description}
+
+
+ {example.nodes.length} nodes
+
+
+ {example.edges.length} edges
+
+
+
+ ))}
+
+
+ setShowExamplePicker(false)}
+ >
+ Cancel
+
+
+
+
+ )}
+
+ {/* Confirm Clear Dialog */}
+ {confirmClearOpen && (
+
setConfirmClearOpen(false)}
+ >
+
e.stopPropagation()}
+ >
+
Clear canvas?
+
+ This will remove all nodes and edges. This cannot be undone.
+
+
+ setConfirmClearOpen(false)}>
+ Cancel
+
+
+ Clear
+
+
+
+
+ )}
+
+ {/* Confirm Load Example Dialog */}
+
+
+ );
+};
+
+export default GraphBuilder;
diff --git a/telemetry/ui/src/components/routes/graph-builder/data/examples.ts b/telemetry/ui/src/components/routes/graph-builder/data/examples.ts
new file mode 100644
index 000000000..528600dc9
--- /dev/null
+++ b/telemetry/ui/src/components/routes/graph-builder/data/examples.ts
@@ -0,0 +1,564 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface ExampleGraph {
+ id: string;
+ title: string;
+ description: string;
+ nodes: Array<{
+ id: string;
+ label: string;
+ nodeType: 'input' | 'action';
+ isAsync?: boolean;
+ isStreaming?: boolean;
+ position: { x: number; y: number };
+ description?: string;
+ }>;
+ edges: Array<{
+ id: string;
+ source: string;
+ target: string;
+ condition?: string;
+ isConditional: boolean;
+ }>;
+}
+
+export const multiModalChatbotWorkflow: ExampleGraph = {
+ id: 'multi-modal-chatbot',
+ title: 'MultiModal Chatbot',
+ description: 'A ChatGPT-like bot which supports multiple response modes, with regular actions.',
+ nodes: [
+ {
+ id: 'node_prompt',
+ label: 'prompt',
+ nodeType: 'action',
+ position: { x: 761, y: 91 }
+ },
+ {
+ id: 'node_check_openai_key',
+ label: 'check_openai_key',
+ nodeType: 'action',
+ position: { x: 591, y: 193 }
+ },
+ {
+ id: 'node_check_safety',
+ label: 'check_safety',
+ nodeType: 'action',
+ position: { x: 384, y: 342 }
+ },
+ {
+ id: 'node_decide_mode',
+ label: 'decide_mode',
+ nodeType: 'action',
+ position: { x: 245, y: 441 }
+ },
+ {
+ id: 'node_prompt_for_more',
+ label: 'prompt_for_more',
+ nodeType: 'action',
+ position: { x: 25, y: 640 }
+ },
+ {
+ id: 'node_generate_image',
+ label: 'generate_image',
+ nodeType: 'action',
+ position: { x: 237, y: 673 }
+ },
+ {
+ id: 'node_generate_code',
+ label: 'generate_code',
+ nodeType: 'action',
+ position: { x: 470, y: 747 }
+ },
+ {
+ id: 'node_answer_question',
+ label: 'answer_question',
+ nodeType: 'action',
+ position: { x: 1019, y: 724 }
+ },
+ {
+ id: 'node_response',
+ label: 'response',
+ nodeType: 'action',
+ position: { x: 883, y: 909 }
+ },
+ {
+ id: 'input_prompt',
+ label: 'input: prompt',
+ nodeType: 'input',
+ position: { x: 790, y: -29 }
+ },
+ {
+ id: 'input_model',
+ label: 'input: model',
+ nodeType: 'input',
+ position: { x: 884, y: 549 }
+ },
+ {
+ id: 'input_display_type',
+ label: 'input: display_type',
+ nodeType: 'input',
+ position: { x: 1086, y: 551 }
+ }
+ ],
+ edges: [
+ {
+ id: 'e-input_prompt-node_prompt',
+ source: 'input_prompt',
+ target: 'node_prompt',
+ isConditional: false
+ },
+ {
+ id: 'e-node_prompt-node_check_openai_key',
+ source: 'node_prompt',
+ target: 'node_check_openai_key',
+ isConditional: false
+ },
+ {
+ id: 'e-node_check_openai_key-node_response',
+ source: 'node_check_openai_key',
+ target: 'node_response',
+ isConditional: false
+ },
+ {
+ id: 'e-node_check_openai_key-node_check_safety',
+ source: 'node_check_openai_key',
+ target: 'node_check_safety',
+ condition: 'has_openai_key=True',
+ isConditional: true
+ },
+ {
+ id: 'e-node_check_safety-node_decide_mode',
+ source: 'node_check_safety',
+ target: 'node_decide_mode',
+ condition: 'safe=True',
+ isConditional: true
+ },
+ {
+ id: 'e-node_check_safety-node_response',
+ source: 'node_check_safety',
+ target: 'node_response',
+ isConditional: false
+ },
+ {
+ id: 'e-node_decide_mode-node_prompt_for_more',
+ source: 'node_decide_mode',
+ target: 'node_prompt_for_more',
+ isConditional: false
+ },
+ {
+ id: 'e-node_decide_mode-node_generate_image',
+ source: 'node_decide_mode',
+ target: 'node_generate_image',
+ condition: 'mode="generate_image"',
+ isConditional: true
+ },
+ {
+ id: 'e-node_decide_mode-node_generate_code',
+ source: 'node_decide_mode',
+ target: 'node_generate_code',
+ condition: 'mode="generate_code"',
+ isConditional: true
+ },
+ {
+ id: 'e-node_decide_mode-node_answer_question',
+ source: 'node_decide_mode',
+ target: 'node_answer_question',
+ condition: 'mode="answer_question"',
+ isConditional: true
+ },
+ {
+ id: 'e-node_answer_question-node_response',
+ source: 'node_answer_question',
+ target: 'node_response',
+ isConditional: false
+ },
+ {
+ id: 'e-node_generate_code-node_response',
+ source: 'node_generate_code',
+ target: 'node_response',
+ isConditional: false
+ },
+ {
+ id: 'e-node_generate_image-node_response',
+ source: 'node_generate_image',
+ target: 'node_response',
+ isConditional: false
+ },
+ {
+ id: 'e-node_prompt_for_more-node_response',
+ source: 'node_prompt_for_more',
+ target: 'node_response',
+ isConditional: false
+ },
+ {
+ id: 'e-node_response-node_prompt',
+ source: 'node_response',
+ target: 'node_prompt',
+ isConditional: false
+ },
+ {
+ id: 'e-input_model-node_generate_code',
+ source: 'input_model',
+ target: 'node_generate_code',
+ isConditional: false
+ },
+ {
+ id: 'e-input_model-node_answer_question',
+ source: 'input_model',
+ target: 'node_answer_question',
+ isConditional: false
+ },
+ {
+ id: 'e-input_model-node_generate_image',
+ source: 'input_model',
+ target: 'node_generate_image',
+ isConditional: false
+ },
+ {
+ id: 'e-input_display_type-node_answer_question',
+ source: 'input_display_type',
+ target: 'node_answer_question',
+ isConditional: false
+ },
+ {
+ id: 'e-input_display_type-node_generate_code',
+ source: 'input_display_type',
+ target: 'node_generate_code',
+ isConditional: false
+ }
+ ]
+};
+
+export const streamingChatbotWorkflow: ExampleGraph = {
+ id: 'streaming-chatbot',
+ title: 'Streaming Chatbot',
+ description: 'A ChatGPT-like bot which supports multiple response modes, with streaming actions.',
+ nodes: [
+ {
+ id: 'input_prompt',
+ label: 'input: prompt',
+ nodeType: 'input',
+ position: { x: 500, y: 50 }
+ },
+ {
+ id: 'input_model',
+ label: 'input: model',
+ nodeType: 'input',
+ position: { x: 84, y: 436 }
+ },
+ {
+ id: 'prompt',
+ label: 'prompt',
+ nodeType: 'action',
+ position: { x: 500, y: 200 }
+ },
+ {
+ id: 'check_safety',
+ label: 'check_safety',
+ nodeType: 'action',
+ position: { x: 500, y: 350 }
+ },
+ {
+ id: 'decide_mode',
+ label: 'decide_mode',
+ nodeType: 'action',
+ position: { x: 400, y: 500 }
+ },
+ {
+ id: 'unsafe_response',
+ label: 'unsafe_response',
+ nodeType: 'action',
+ isAsync: true,
+ isStreaming: true,
+ position: { x: 761, y: 496 }
+ },
+ {
+ id: 'generate_code',
+ label: 'generate_code',
+ nodeType: 'action',
+ isAsync: true,
+ isStreaming: true,
+ position: { x: 90, y: 635 }
+ },
+ {
+ id: 'answer_question',
+ label: 'answer_question',
+ nodeType: 'action',
+ isAsync: true,
+ isStreaming: true,
+ position: { x: 299, y: 713 }
+ },
+ {
+ id: 'generate_poem',
+ label: 'generate_poem',
+ nodeType: 'action',
+ isAsync: true,
+ isStreaming: true,
+ position: { x: 565, y: 780 }
+ },
+ {
+ id: 'prompt_for_more',
+ label: 'prompt_for_more',
+ nodeType: 'action',
+ isAsync: true,
+ isStreaming: true,
+ position: { x: 730, y: 677 }
+ }
+ ],
+ edges: [
+ {
+ id: 'e-input_prompt-prompt',
+ source: 'input_prompt',
+ target: 'prompt',
+ isConditional: false
+ },
+ {
+ id: 'e-input_model-generate_code',
+ source: 'input_model',
+ target: 'generate_code',
+ isConditional: false
+ },
+ {
+ id: 'e-input_model-answer_question',
+ source: 'input_model',
+ target: 'answer_question',
+ isConditional: false
+ },
+ {
+ id: 'e-input_model-generate_poem',
+ source: 'input_model',
+ target: 'generate_poem',
+ isConditional: false
+ },
+ {
+ id: 'e-prompt-check_safety',
+ source: 'prompt',
+ target: 'check_safety',
+ isConditional: false
+ },
+ {
+ id: 'e-check_safety-decide_mode',
+ source: 'check_safety',
+ target: 'decide_mode',
+ condition: 'safe=True',
+ isConditional: true
+ },
+ {
+ id: 'e-check_safety-unsafe_response',
+ source: 'check_safety',
+ target: 'unsafe_response',
+ isConditional: false
+ },
+ {
+ id: 'e-decide_mode-generate_code',
+ source: 'decide_mode',
+ target: 'generate_code',
+ condition: 'mode="generate_code"',
+ isConditional: true
+ },
+ {
+ id: 'e-decide_mode-answer_question',
+ source: 'decide_mode',
+ target: 'answer_question',
+ condition: 'mode="answer_question"',
+ isConditional: true
+ },
+ {
+ id: 'e-decide_mode-generate_poem',
+ source: 'decide_mode',
+ target: 'generate_poem',
+ condition: 'mode="generate_poem"',
+ isConditional: true
+ },
+ {
+ id: 'e-decide_mode-prompt_for_more',
+ source: 'decide_mode',
+ target: 'prompt_for_more',
+ isConditional: false
+ },
+ {
+ id: 'e-generate_code-prompt',
+ source: 'generate_code',
+ target: 'prompt',
+ isConditional: false
+ },
+ {
+ id: 'e-answer_question-prompt',
+ source: 'answer_question',
+ target: 'prompt',
+ isConditional: false
+ },
+ {
+ id: 'e-generate_poem-prompt',
+ source: 'generate_poem',
+ target: 'prompt',
+ isConditional: false
+ },
+ {
+ id: 'e-unsafe_response-prompt',
+ source: 'unsafe_response',
+ target: 'prompt',
+ isConditional: false
+ },
+ {
+ id: 'e-prompt_for_more-prompt',
+ source: 'prompt_for_more',
+ target: 'prompt',
+ isConditional: false
+ }
+ ]
+};
+
+export const adaptiveCRAGWorkflow: ExampleGraph = {
+ id: 'adaptive-crag',
+ title: 'Adaptive CRAG',
+ description:
+ 'A system that dynamically selects the most suitable route for a given user query and self-reflects on retrieved documents to improve response quality.',
+ nodes: [
+ {
+ id: 'node_router',
+ label: 'router',
+ nodeType: 'action',
+ position: { x: 821, y: 177 }
+ },
+ {
+ id: 'node_terminate',
+ label: 'terminate',
+ nodeType: 'action',
+ position: { x: 1115, y: 339 }
+ },
+ {
+ id: 'node_rewrite_query',
+ label: 'rewrite_query_for_lancedb',
+ nodeType: 'action',
+ position: { x: 570, y: 343 }
+ },
+ {
+ id: 'node_search_lancedb',
+ label: 'search_lancedb',
+ nodeType: 'action',
+ position: { x: 395, y: 474 }
+ },
+ {
+ id: 'node_remove_irrelevant',
+ label: 'remove_irrelevant_lancedb_results',
+ nodeType: 'action',
+ position: { x: 222, y: 598 }
+ },
+ {
+ id: 'node_extract_keywords',
+ label: 'extract_keywords_for_exa_search',
+ nodeType: 'action',
+ position: { x: 862, y: 755 }
+ },
+ {
+ id: 'node_search_exa',
+ label: 'search_exa',
+ nodeType: 'action',
+ position: { x: 1014, y: 900 }
+ },
+ {
+ id: 'node_ask_assistant',
+ label: 'ask_assistant',
+ nodeType: 'action',
+ position: { x: 664, y: 1050 }
+ },
+ {
+ id: 'input_query',
+ label: 'input: query',
+ nodeType: 'input',
+ position: { x: 822, y: 26 }
+ }
+ ],
+ edges: [
+ {
+ id: 'e-input_query-node_router',
+ source: 'input_query',
+ target: 'node_router',
+ isConditional: false
+ },
+ {
+ id: 'e-node_router-node_rewrite_query',
+ source: 'node_router',
+ target: 'node_rewrite_query',
+ isConditional: false
+ },
+ {
+ id: 'e-node_rewrite_query-node_search_lancedb',
+ source: 'node_rewrite_query',
+ target: 'node_search_lancedb',
+ isConditional: false
+ },
+ {
+ id: 'e-node_search_lancedb-node_remove_irrelevant',
+ source: 'node_search_lancedb',
+ target: 'node_remove_irrelevant',
+ isConditional: false
+ },
+ {
+ id: 'e-node_remove_irrelevant-node_ask_assistant',
+ source: 'node_remove_irrelevant',
+ target: 'node_ask_assistant',
+ isConditional: false
+ },
+ {
+ id: 'e-node_remove_irrelevant-node_extract_keywords',
+ source: 'node_remove_irrelevant',
+ target: 'node_extract_keywords',
+ condition: 'len(lancedb_results) < docs_limit',
+ isConditional: true
+ },
+ {
+ id: 'e-node_extract_keywords-node_search_exa',
+ source: 'node_extract_keywords',
+ target: 'node_search_exa',
+ isConditional: false
+ },
+ {
+ id: 'e-node_search_exa-node_ask_assistant',
+ source: 'node_search_exa',
+ target: 'node_ask_assistant',
+ isConditional: false
+ },
+ {
+ id: 'e-node_router-node_ask_assistant',
+ source: 'node_router',
+ target: 'node_ask_assistant',
+ condition: 'route="assistant"',
+ isConditional: true
+ },
+ {
+ id: 'e-node_router-node_extract_keywords',
+ source: 'node_router',
+ target: 'node_extract_keywords',
+ condition: 'route="web_search"',
+ isConditional: true
+ },
+ {
+ id: 'e-node_router-node_terminate',
+ source: 'node_router',
+ target: 'node_terminate',
+ condition: 'route="terminate"',
+ isConditional: true
+ }
+ ]
+};
+
+export const examples = [multiModalChatbotWorkflow, adaptiveCRAGWorkflow, streamingChatbotWorkflow];
diff --git a/telemetry/ui/src/components/routes/graph-builder/utils/BurrCodeGenerator.ts b/telemetry/ui/src/components/routes/graph-builder/utils/BurrCodeGenerator.ts
new file mode 100644
index 000000000..16004ce42
--- /dev/null
+++ b/telemetry/ui/src/components/routes/graph-builder/utils/BurrCodeGenerator.ts
@@ -0,0 +1,321 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { BurrGraphJSON } from './GraphExporter';
+
+type NodeRef = BurrGraphJSON['nodes'][0];
+
+export class BurrGraphCodeGenerator {
+ static generatePythonCode(graphData: BurrGraphJSON): string {
+ const actionNodes = graphData.nodes.filter((n) => n.nodeType !== 'input');
+ const hasAsync = actionNodes.some((n) => n.isAsync);
+ const hasStreaming = actionNodes.some((n) => n.isStreaming);
+
+ const imports = this.generateImports(actionNodes, hasAsync, hasStreaming);
+ const actions = this.generateActions(graphData.nodes, graphData.edges);
+ const graphFunction = this.generateGraphFunction(graphData);
+ const main = this.generateMain(hasAsync, hasStreaming);
+
+ return [imports, actions, graphFunction, main].join('\n\n');
+ }
+
+ private static generateImports(
+ actionNodes: NodeRef[],
+ hasAsync: boolean,
+ hasStreaming: boolean
+ ): string {
+ // Decorator imports
+ const decorators: string[] = ['action'];
+ if (hasStreaming) decorators.push('streaming_action');
+ const actionImports = `from burr.core.action import ${decorators.join(', ')}`;
+
+ // Typing imports
+ const typingParts: string[] = ['Tuple'];
+ if (hasStreaming) {
+ typingParts.push('Optional');
+ if (hasAsync) typingParts.push('AsyncGenerator');
+ else typingParts.push('Generator');
+ }
+ const typingImports = `from typing import ${typingParts.join(', ')}`;
+
+ const asyncioImport = hasAsync ? '\nimport asyncio' : '';
+
+ return `${typingImports}
+from burr.core import State, default, when
+${actionImports}
+from burr.core.graph import GraphBuilder${asyncioImport}`;
+ }
+
+ private static generateActions(
+ nodes: BurrGraphJSON['nodes'],
+ edges: BurrGraphJSON['edges']
+ ): string {
+ const processNodes = nodes.filter((node) => node.nodeType !== 'input');
+
+ const actionFunctions = processNodes.map((node) => {
+ return this.generateAction(node, nodes, edges);
+ });
+
+ return actionFunctions.join('\n\n');
+ }
+
+ private static generateAction(
+ node: NodeRef,
+ nodes: BurrGraphJSON['nodes'],
+ edges: BurrGraphJSON['edges']
+ ): string {
+ const functionName = this.sanitizeNodeName(node.label);
+ const inputParams = this.getInputParameters(node.id, nodes, edges);
+ const paramString =
+ inputParams.length > 0 ? `state: State, ${inputParams.join(', ')}` : 'state: State';
+
+ const isAsync = node.isAsync || false;
+ const isStreaming = node.isStreaming || false;
+
+ const decorator = isStreaming
+ ? '@streaming_action(reads=[], writes=[])'
+ : '@action(reads=[], writes=[])';
+ const asyncKeyword = isAsync ? 'async ' : '';
+
+ let returnType: string;
+ let body: string;
+ let docKind: string;
+
+ if (isStreaming && isAsync) {
+ returnType = 'AsyncGenerator[Tuple[dict, Optional[State]], None]';
+ body = ' yield {}, state';
+ docKind = 'async streaming action';
+ } else if (isStreaming) {
+ returnType = 'Generator[Tuple[dict, Optional[State]], None, None]';
+ body = ' yield {}, state';
+ docKind = 'streaming action';
+ } else if (isAsync) {
+ returnType = 'Tuple[dict, State]';
+ body = ' return {}, state';
+ docKind = 'async action';
+ } else {
+ returnType = 'Tuple[dict, State]';
+ body = ' return {}, state';
+ docKind = 'action';
+ }
+
+ const docstring = node.description
+ ? `\n """${node.description}\n\n This is a stub ${docKind}. Please complete with your business logic.\n """`
+ : `\n """Stub ${docKind}. Please complete with your business logic."""`;
+
+ return `${decorator}
+${asyncKeyword}def ${functionName}(${paramString}) -> ${returnType}:${docstring}
+${body}`;
+ }
+
+ private static getInputParameters(
+ nodeId: string,
+ nodes: BurrGraphJSON['nodes'],
+ edges: BurrGraphJSON['edges']
+ ): string[] {
+ const inputParams: string[] = [];
+
+ edges.forEach((edge) => {
+ if (edge.target === nodeId) {
+ const sourceNode = nodes.find((n) => n.id === edge.source);
+ if (sourceNode && sourceNode.nodeType === 'input') {
+ const paramName = sourceNode.label.replace(/^input:\s*/, '').trim();
+ const sanitizedParam = this.sanitizeParameterName(paramName);
+ inputParams.push(`${sanitizedParam}: str`);
+ }
+ }
+ });
+
+ return inputParams;
+ }
+
+ private static sanitizeParameterName(name: string): string {
+ return (
+ name
+ .toLowerCase()
+ .replace(/[^a-z0-9]/g, '_')
+ .replace(/_+/g, '_')
+ .replace(/^_|_$/g, '') || 'param'
+ );
+ }
+
+ private static deduplicateNames(names: string[]): string[] {
+ const seen = new Map