Skip to content

Commit bac6cde

Browse files
cristipufuclaude
andcommitted
feat: add web server with React frontend for dev console
Adds a FastAPI-based web server with WebSocket support and a React frontend that provides a browser-based UI for running and debugging UiPath workflows. Backend: - FastAPI app serving static frontend build + REST API + WebSocket - Routes for entrypoints, runs, and graph data - WebSocket handler for real-time run updates (traces, logs, chat) - Serializers for converting internal models to API format Frontend (Vite + React 19 + React Flow + Tailwind v4 + Zustand): - Run management: create runs, view history, inspect details - Graph visualization: ELK-based layered layout with custom node types - Trace tree: hierarchical span view with connector lines and span details - Log panel: zebra-striped rows with colored level badges - Dark/light theme toggle - Real-time updates via WebSocket Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 66f6d7d commit bac6cde

77 files changed

Lines changed: 9285 additions & 403 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ wheels/
2828

2929
.cache/*
3030
.cache
31+
32+
# Node
33+
node_modules/
34+
35+
# Claude Code
36+
.claude/

CLAUDE.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# CLAUDE.md
2+
3+
## Project Structure
4+
- Monorepo: Python backend (`src/uipath/dev/`) + React frontend (`src/uipath/dev/server/frontend/`)
5+
- Frontend: Vite + React 19 + React Flow v11 + Tailwind v4 + Zustand
6+
- Backend: FastAPI + WebSocket, serves static frontend build
7+
- Graph layout: ELK (elkjs) for layered/hierarchical graph visualization
8+
- Build: `npm run build` in `frontend/` outputs to `../static/`
9+
10+
## React Flow + ELK Integration
11+
12+
### Node Types
13+
- Never use `"default"` as a custom node type name — React Flow's built-in CSS (`.react-flow__node-default`) renders a white box behind it. Use `"defaultNode"` instead.
14+
- All fallback types in `runElkLayout` must match registered `nodeTypes` keys exactly.
15+
16+
### Handles
17+
- Handles are hidden via CSS (`opacity: 0, width: 0, height: 0`). This breaks `smoothstep`/`bezier` edge types.
18+
- Always use the custom `"elk"` edge type that draws from explicit coordinates. Never fall back to `"smoothstep"`.
19+
20+
### Edge Rendering
21+
- ELK provides `sections` (startPoint, bendPoints, endPoint) for edge routes.
22+
- For compound node edges, section coords are relative to the compound node — add parent `(x, y)` offset.
23+
- Build a `nodeRects` lookup after layout for fallback: source bottom-center → target top-center when ELK doesn't provide sections.
24+
- Don't mix ELK bendPoints with manually computed endpoints — use ELK sections as-is or compute everything from scratch.
25+
26+
### Subgraph / Compound Nodes
27+
- ELK child IDs: `${parentId}/${childId}` to avoid collisions. Same for edge IDs.
28+
- React Flow: children use `parentNode` + `extent: "parent"`, positions relative to parent.
29+
- Group node must come before its children in the nodes array.
30+
- Delete `width`/`height` on compound ELK nodes — let ELK compute from children + padding.
31+
32+
### Tailwind CSS Conflicts
33+
- Tailwind preflight sets `svg { overflow: hidden }` which clips React Flow's edge SVG layer.
34+
- Fix: `.react-flow__edges { overflow: visible !important }`.
35+
36+
### Node Sizing
37+
- Width: `Math.max(80, label.length * 8 + 32)` — no max cap.
38+
- All custom nodes must use `overflow-hidden` + `text-ellipsis` on the outer div.
39+
40+
### ELK Options
41+
```
42+
algorithm: layered, direction: DOWN, edgeRouting: ORTHOGONAL
43+
crossingMinimization: LAYER_SWEEP, nodePlacement: NETWORK_SIMPLEX
44+
portAlignment: CENTER, considerModelOrder: NODES_AND_EDGES
45+
```

demo/mock_greeting_runtime.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
UiPathRuntimeStatus,
1313
UiPathStreamOptions,
1414
)
15-
from uipath.runtime.schema import UiPathRuntimeSchema
15+
from uipath.runtime.schema import (
16+
UiPathRuntimeEdge,
17+
UiPathRuntimeGraph,
18+
UiPathRuntimeNode,
19+
UiPathRuntimeSchema,
20+
)
1621

1722
ENTRYPOINT_GREETING = "agent/greeting.py:main"
1823

@@ -59,6 +64,34 @@ async def get_schema(self) -> UiPathRuntimeSchema:
5964
},
6065
"required": ["greeting"],
6166
},
67+
graph=UiPathRuntimeGraph(
68+
nodes=[
69+
UiPathRuntimeNode(
70+
id="__start__", name="__start__", type="__start__"
71+
),
72+
UiPathRuntimeNode(
73+
id="normalize_name", name="normalize_name", type="node"
74+
),
75+
UiPathRuntimeNode(
76+
id="build_message",
77+
name="build_message",
78+
type="model",
79+
metadata={"model_name": "greeting-builder"},
80+
),
81+
UiPathRuntimeNode(
82+
id="compute_metadata", name="compute_metadata", type="node"
83+
),
84+
UiPathRuntimeNode(id="__end__", name="__end__", type="__end__"),
85+
],
86+
edges=[
87+
UiPathRuntimeEdge(source="__start__", target="normalize_name"),
88+
UiPathRuntimeEdge(source="normalize_name", target="build_message"),
89+
UiPathRuntimeEdge(
90+
source="build_message", target="compute_metadata"
91+
),
92+
UiPathRuntimeEdge(source="compute_metadata", target="__end__"),
93+
],
94+
),
6295
)
6396

6497
async def execute(

demo/mock_numbers_runtime.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
UiPathRuntimeStatus,
1313
UiPathStreamOptions,
1414
)
15-
from uipath.runtime.schema import UiPathRuntimeSchema
15+
from uipath.runtime.schema import (
16+
UiPathRuntimeEdge,
17+
UiPathRuntimeGraph,
18+
UiPathRuntimeNode,
19+
UiPathRuntimeSchema,
20+
)
1621

1722
ENTRYPOINT_ANALYZE_NUMBERS = "agent/numbers.py:analyze"
1823

@@ -58,6 +63,27 @@ async def get_schema(self) -> UiPathRuntimeSchema:
5863
},
5964
"required": ["operation", "result"],
6065
},
66+
graph=UiPathRuntimeGraph(
67+
nodes=[
68+
UiPathRuntimeNode(
69+
id="__start__", name="__start__", type="__start__"
70+
),
71+
UiPathRuntimeNode(
72+
id="validate_input", name="validate_input", type="node"
73+
),
74+
UiPathRuntimeNode(id="compute", name="compute", type="node"),
75+
UiPathRuntimeNode(
76+
id="postprocess", name="postprocess", type="node"
77+
),
78+
UiPathRuntimeNode(id="__end__", name="__end__", type="__end__"),
79+
],
80+
edges=[
81+
UiPathRuntimeEdge(source="__start__", target="validate_input"),
82+
UiPathRuntimeEdge(source="validate_input", target="compute"),
83+
UiPathRuntimeEdge(source="compute", target="postprocess"),
84+
UiPathRuntimeEdge(source="postprocess", target="__end__"),
85+
],
86+
),
6187
)
6288

6389
async def execute(

demo/mock_support_runtime.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
UiPathRuntimeStatus,
1313
UiPathStreamOptions,
1414
)
15-
from uipath.runtime.schema import UiPathRuntimeSchema
15+
from uipath.runtime.schema import (
16+
UiPathRuntimeEdge,
17+
UiPathRuntimeGraph,
18+
UiPathRuntimeNode,
19+
UiPathRuntimeSchema,
20+
)
1621

1722
ENTRYPOINT_SUPPORT_CHAT = "agent/support.py:main"
1823

@@ -57,6 +62,85 @@ async def get_schema(self) -> UiPathRuntimeSchema:
5762
},
5863
"required": ["reply"],
5964
},
65+
graph=UiPathRuntimeGraph(
66+
nodes=[
67+
UiPathRuntimeNode(id="__start__", name="__start__", type="node"),
68+
UiPathRuntimeNode(
69+
id="model",
70+
name="model",
71+
type="model",
72+
metadata={
73+
"model_name": "claude-haiku-4-5",
74+
"max_tokens": 64000,
75+
},
76+
),
77+
UiPathRuntimeNode(
78+
id="tools",
79+
name="tools",
80+
type="tool",
81+
metadata={
82+
"tool_names": [
83+
"write_todos",
84+
"ls",
85+
"read_file",
86+
"write_file",
87+
"edit_file",
88+
"glob",
89+
"grep",
90+
"execute",
91+
"task",
92+
"tavily_search",
93+
],
94+
"tool_count": 10,
95+
},
96+
),
97+
UiPathRuntimeNode(
98+
id="TodoListMiddleware.after_model",
99+
name="TodoListMiddleware.after_model",
100+
type="node",
101+
),
102+
UiPathRuntimeNode(
103+
id="SummarizationMiddleware.before_model",
104+
name="SummarizationMiddleware.before_model",
105+
type="node",
106+
),
107+
UiPathRuntimeNode(
108+
id="PatchToolCallsMiddleware.before_agent",
109+
name="PatchToolCallsMiddleware.before_agent",
110+
type="node",
111+
),
112+
UiPathRuntimeNode(id="__end__", name="__end__", type="node"),
113+
],
114+
edges=[
115+
UiPathRuntimeEdge(
116+
source="PatchToolCallsMiddleware.before_agent",
117+
target="SummarizationMiddleware.before_model",
118+
),
119+
UiPathRuntimeEdge(
120+
source="SummarizationMiddleware.before_model", target="model"
121+
),
122+
UiPathRuntimeEdge(
123+
source="TodoListMiddleware.after_model",
124+
target="SummarizationMiddleware.before_model",
125+
),
126+
UiPathRuntimeEdge(
127+
source="TodoListMiddleware.after_model", target="__end__"
128+
),
129+
UiPathRuntimeEdge(
130+
source="TodoListMiddleware.after_model", target="tools"
131+
),
132+
UiPathRuntimeEdge(
133+
source="__start__",
134+
target="PatchToolCallsMiddleware.before_agent",
135+
),
136+
UiPathRuntimeEdge(
137+
source="model", target="TodoListMiddleware.after_model"
138+
),
139+
UiPathRuntimeEdge(
140+
source="tools", target="SummarizationMiddleware.before_model"
141+
),
142+
],
143+
),
60144
)
61145

62146
async def execute(

demo/mock_telemetry_runtime.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
UiPathStreamOptions,
1414
)
1515
from uipath.runtime.debug import UiPathBreakpointResult
16-
from uipath.runtime.schema import UiPathRuntimeSchema
16+
from uipath.runtime.schema import (
17+
UiPathRuntimeEdge,
18+
UiPathRuntimeGraph,
19+
UiPathRuntimeNode,
20+
UiPathRuntimeSchema,
21+
)
1722

1823
ENTRYPOINT_TELEMETRY = "agent/telemetry.py:run"
1924

@@ -58,6 +63,113 @@ async def get_schema(self) -> UiPathRuntimeSchema:
5863
"properties": {"result": {"type": "string"}},
5964
"required": ["result"],
6065
},
66+
graph=UiPathRuntimeGraph(
67+
nodes=[
68+
UiPathRuntimeNode(id="__start__", name="__start__", type="node"),
69+
UiPathRuntimeNode(id="input", name="input", type="node"),
70+
UiPathRuntimeNode(
71+
id="supervisor",
72+
name="supervisor",
73+
type="model",
74+
metadata={
75+
"model_name": "claude-3-7-sonnet-latest",
76+
"max_tokens": 64000,
77+
},
78+
),
79+
UiPathRuntimeNode(
80+
id="researcher",
81+
name="researcher",
82+
type="node",
83+
subgraph=UiPathRuntimeGraph(
84+
nodes=[
85+
UiPathRuntimeNode(
86+
id="__start__", name="__start__", type="node"
87+
),
88+
UiPathRuntimeNode(
89+
id="model",
90+
name="model",
91+
type="model",
92+
metadata={
93+
"model_name": "claude-3-7-sonnet-latest",
94+
"max_tokens": 64000,
95+
},
96+
),
97+
UiPathRuntimeNode(
98+
id="tools",
99+
name="tools",
100+
type="tool",
101+
metadata={
102+
"tool_names": ["tavily_search"],
103+
"tool_count": 1,
104+
},
105+
),
106+
UiPathRuntimeNode(
107+
id="__end__", name="__end__", type="node"
108+
),
109+
],
110+
edges=[
111+
UiPathRuntimeEdge(source="__start__", target="model"),
112+
UiPathRuntimeEdge(source="model", target="__end__"),
113+
UiPathRuntimeEdge(source="model", target="tools"),
114+
UiPathRuntimeEdge(source="tools", target="model"),
115+
],
116+
),
117+
),
118+
UiPathRuntimeNode(
119+
id="coder",
120+
name="coder",
121+
type="node",
122+
subgraph=UiPathRuntimeGraph(
123+
nodes=[
124+
UiPathRuntimeNode(
125+
id="__start__", name="__start__", type="node"
126+
),
127+
UiPathRuntimeNode(
128+
id="model",
129+
name="model",
130+
type="model",
131+
metadata={
132+
"model_name": "claude-3-7-sonnet-latest",
133+
"max_tokens": 64000,
134+
},
135+
),
136+
UiPathRuntimeNode(
137+
id="tools",
138+
name="tools",
139+
type="tool",
140+
metadata={
141+
"tool_names": ["python_repl_tool"],
142+
"tool_count": 1,
143+
},
144+
),
145+
UiPathRuntimeNode(
146+
id="__end__", name="__end__", type="node"
147+
),
148+
],
149+
edges=[
150+
UiPathRuntimeEdge(source="__start__", target="model"),
151+
UiPathRuntimeEdge(source="model", target="__end__"),
152+
UiPathRuntimeEdge(source="model", target="tools"),
153+
UiPathRuntimeEdge(source="tools", target="model"),
154+
],
155+
),
156+
),
157+
UiPathRuntimeNode(id="output", name="output", type="node"),
158+
UiPathRuntimeNode(id="__end__", name="__end__", type="__end__"),
159+
],
160+
edges=[
161+
UiPathRuntimeEdge(source="__start__", target="input"),
162+
UiPathRuntimeEdge(source="coder", target="supervisor"),
163+
UiPathRuntimeEdge(source="input", target="supervisor"),
164+
UiPathRuntimeEdge(source="researcher", target="supervisor"),
165+
UiPathRuntimeEdge(source="supervisor", target="coder"),
166+
UiPathRuntimeEdge(
167+
source="supervisor", target="output", label="__end__"
168+
),
169+
UiPathRuntimeEdge(source="supervisor", target="researcher"),
170+
UiPathRuntimeEdge(source="output", target="__end__"),
171+
],
172+
),
61173
)
62174

63175
async def execute(

justfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,18 @@ install:
2323
# Test the custom linter
2424
test-lint-httpx:
2525
python scripts/test_httpx_linter.py
26+
27+
# Validate server module (imports, serializers, app factory)
28+
validate-server:
29+
python scripts/validate_server.py
30+
31+
# Validate frontend structure (no build)
32+
validate-frontend:
33+
python scripts/validate_frontend.py
34+
35+
# Validate frontend structure + build
36+
validate-frontend-build:
37+
python scripts/validate_frontend.py --build
38+
39+
# Run all validations (no frontend build)
40+
validate-all: validate validate-server validate-frontend

0 commit comments

Comments
 (0)