Python: Add DevUI to AgentFramework (#781)

* add initial backend service code for devui

* add tests

* add frontendcode

* ui updates

* update readme

* ui updates and tweaks

* update ui bundle

* improve ui, add react flow base

* add react flow ui, fix background

* update ui, fix introspection bug

* update readme

* update ui build

* add support for multimodal input - both backend and frontend

* update ui build

* refactor as main framework package

* backend and tests refactor

* ui build update

* ui build update and refactor

* update pyproject.toml, update uv.lock

* update ui build

* ui update to fit oai responses types

* add backend updat and readme update

* mypy and other fixes

* add intial dev guide

* update ui and fix workflow bug

* update ui build, add thread support

* type fixes

* update workflow view

* update uv.lock

* fix workflow iport errors

* lint and other fixes

* mypy fixes

* minor update

* update ui build

* refactor to use oai dependencies directly, update examples to samples, improve typing

* readme update

* update ui and ui build

* fix workflow pyright error

* update ui, fix issues with run workflow placement, miniamp menu, etc

* make samples integrate serve

---------

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
Victor Dibia
2025-09-22 16:30:08 -07:00
committed by GitHub
Unverified
parent adb6dcd2af
commit 1ef24d3e91
98 changed files with 18045 additions and 4 deletions
@@ -0,0 +1,139 @@
import type { Node, Edge } from "@xyflow/react";
import type { ExecutorNodeData } from "@/components/workflow/executor-node";
/**
* Lightweight auto-layout algorithm to replace dagre
* Handles fan-out nodes properly by spacing siblings
*/
export function applySimpleLayout(
nodes: Node<ExecutorNodeData>[],
edges: Edge[],
direction: "TB" | "LR" = "LR"
): Node<ExecutorNodeData>[] {
if (nodes.length === 0) return nodes;
if (nodes.length === 1) {
return nodes.map((node) => ({
...node,
position: { x: 0, y: 0 },
}));
}
// Create adjacency maps
const outgoingEdges = new Map<string, string[]>();
const incomingEdges = new Map<string, string[]>();
nodes.forEach((node) => {
outgoingEdges.set(node.id, []);
incomingEdges.set(node.id, []);
});
edges.forEach((edge) => {
outgoingEdges.get(edge.source)?.push(edge.target);
incomingEdges.get(edge.target)?.push(edge.source);
});
// Find root nodes (nodes with no incoming edges)
const rootNodes = nodes.filter(
(node) => (incomingEdges.get(node.id) || []).length === 0
);
if (rootNodes.length === 0) {
// Fallback: use first node as root if no clear root
rootNodes.push(nodes[0]);
}
// Constants for spacing
const NODE_WIDTH = 220;
const NODE_HEIGHT = 120;
const HORIZONTAL_SPACING = direction === "LR" ? 350 : 200;
const VERTICAL_SPACING = direction === "TB" ? 250 : 180;
// Track positioned nodes and level information
const positioned = new Map<string, { x: number; y: number; level: number }>();
const levelGroups = new Map<number, string[]>();
// Build level groups using BFS
const queue: Array<{ nodeId: string; level: number }> = [];
const visited = new Set<string>();
// Start with root nodes at level 0
rootNodes.forEach((node) => {
queue.push({ nodeId: node.id, level: 0 });
});
// BFS to assign levels
while (queue.length > 0) {
const { nodeId, level } = queue.shift()!;
if (visited.has(nodeId)) continue;
visited.add(nodeId);
// Add to level group
if (!levelGroups.has(level)) {
levelGroups.set(level, []);
}
levelGroups.get(level)!.push(nodeId);
// Add children to next level
const children = outgoingEdges.get(nodeId) || [];
children.forEach((childId) => {
if (!visited.has(childId)) {
queue.push({ nodeId: childId, level: level + 1 });
}
});
}
// Handle orphaned nodes (not connected to root)
nodes.forEach((node) => {
if (!visited.has(node.id)) {
const maxLevel = Math.max(...Array.from(levelGroups.keys()), -1);
const orphanLevel = maxLevel + 1;
if (!levelGroups.has(orphanLevel)) {
levelGroups.set(orphanLevel, []);
}
levelGroups.get(orphanLevel)!.push(node.id);
}
});
// Position nodes level by level
levelGroups.forEach((nodeIds, level) => {
const nodeCount = nodeIds.length;
nodeIds.forEach((nodeId, index) => {
let x: number, y: number;
if (direction === "LR") {
// Horizontal layout: X increases with level, Y centers siblings
x = level * HORIZONTAL_SPACING;
// Center siblings vertically
const totalHeight = (nodeCount - 1) * VERTICAL_SPACING;
const startY = -totalHeight / 2;
y = startY + index * VERTICAL_SPACING;
} else {
// Vertical layout: Y increases with level, X centers siblings
y = level * VERTICAL_SPACING;
// Center siblings horizontally
const totalWidth = (nodeCount - 1) * HORIZONTAL_SPACING;
const startX = -totalWidth / 2;
x = startX + index * HORIZONTAL_SPACING;
}
positioned.set(nodeId, { x, y, level });
});
});
// Apply positions to nodes (centering them on their calculated positions)
return nodes.map((node) => {
const pos = positioned.get(node.id) || { x: 0, y: 0 };
return {
...node,
position: {
x: pos.x - NODE_WIDTH / 2, // Center the node
y: pos.y - NODE_HEIGHT / 2,
},
};
});
}