flock-core 0.5.0b56__py3-none-any.whl → 0.5.0b58__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/dashboard/websocket.py +5 -5
- flock/frontend/package-lock.json +2 -2
- flock/frontend/package.json +1 -1
- flock/frontend/src/components/graph/GraphCanvas.tsx +212 -34
- flock/frontend/src/services/layout.ts +227 -16
- {flock_core-0.5.0b56.dist-info → flock_core-0.5.0b58.dist-info}/METADATA +55 -6
- {flock_core-0.5.0b56.dist-info → flock_core-0.5.0b58.dist-info}/RECORD +10 -10
- {flock_core-0.5.0b56.dist-info → flock_core-0.5.0b58.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b56.dist-info → flock_core-0.5.0b58.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b56.dist-info → flock_core-0.5.0b58.dist-info}/licenses/LICENSE +0 -0
flock/dashboard/websocket.py
CHANGED
|
@@ -103,9 +103,9 @@ class WebSocketManager:
|
|
|
103
103
|
# Store streaming output events for history (always, even if no clients)
|
|
104
104
|
if isinstance(event, StreamingOutputEvent):
|
|
105
105
|
self._streaming_history[event.agent_name].append(event)
|
|
106
|
-
logger.debug(
|
|
107
|
-
|
|
108
|
-
)
|
|
106
|
+
# logger.debug(
|
|
107
|
+
# f"Stored streaming event for {event.agent_name}, history size: {len(self._streaming_history[event.agent_name])}"
|
|
108
|
+
# )
|
|
109
109
|
|
|
110
110
|
# If no clients, still log but don't broadcast
|
|
111
111
|
if not self.clients:
|
|
@@ -115,11 +115,11 @@ class WebSocketManager:
|
|
|
115
115
|
return
|
|
116
116
|
|
|
117
117
|
# Log broadcast attempt
|
|
118
|
-
logger.debug(f"Broadcasting {type(event).__name__} to {len(self.clients)} client(s)")
|
|
118
|
+
# logger.debug(f"Broadcasting {type(event).__name__} to {len(self.clients)} client(s)")
|
|
119
119
|
|
|
120
120
|
# Serialize event to JSON using Pydantic's model_dump_json
|
|
121
121
|
message = event.model_dump_json()
|
|
122
|
-
logger.debug(f"Event JSON: {message[:200]}...") # Log first 200 chars
|
|
122
|
+
# logger.debug(f"Event JSON: {message[:200]}...") # Log first 200 chars
|
|
123
123
|
|
|
124
124
|
# Broadcast to all clients concurrently
|
|
125
125
|
# Use return_exceptions=True to handle client failures gracefully
|
flock/frontend/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flock-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "flock-ui",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.3",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@types/dagre": "^0.7.53",
|
flock/frontend/package.json
CHANGED
|
@@ -21,16 +21,20 @@ import { useUIStore } from '../../store/uiStore';
|
|
|
21
21
|
import { useModuleStore } from '../../store/moduleStore';
|
|
22
22
|
import { useSettingsStore } from '../../store/settingsStore';
|
|
23
23
|
import { moduleRegistry } from '../modules/ModuleRegistry';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
applyHierarchicalLayout,
|
|
26
|
+
applyCircularLayout,
|
|
27
|
+
applyGridLayout,
|
|
28
|
+
applyRandomLayout
|
|
29
|
+
} from '../../services/layout';
|
|
25
30
|
import { usePersistence } from '../../hooks/usePersistence';
|
|
26
31
|
import { v4 as uuidv4 } from 'uuid';
|
|
27
32
|
|
|
28
33
|
const GraphCanvas: React.FC = () => {
|
|
29
|
-
const { fitView, getIntersectingNodes } = useReactFlow();
|
|
34
|
+
const { fitView, getIntersectingNodes, screenToFlowPosition } = useReactFlow();
|
|
30
35
|
|
|
31
36
|
const mode = useUIStore((state) => state.mode);
|
|
32
37
|
const openDetailWindow = useUIStore((state) => state.openDetailWindow);
|
|
33
|
-
const layoutDirection = useSettingsStore((state) => state.advanced.layoutDirection);
|
|
34
38
|
const nodes = useGraphStore((state) => state.nodes);
|
|
35
39
|
const edges = useGraphStore((state) => state.edges);
|
|
36
40
|
const agents = useGraphStore((state) => state.agents);
|
|
@@ -48,6 +52,7 @@ const GraphCanvas: React.FC = () => {
|
|
|
48
52
|
|
|
49
53
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
50
54
|
const [showModuleSubmenu, setShowModuleSubmenu] = useState(false);
|
|
55
|
+
const [showLayoutSubmenu, setShowLayoutSubmenu] = useState(false);
|
|
51
56
|
|
|
52
57
|
// Persistence hook - loads positions on mount and handles saves
|
|
53
58
|
const { saveNodePosition } = usePersistence();
|
|
@@ -111,21 +116,52 @@ const GraphCanvas: React.FC = () => {
|
|
|
111
116
|
[edges]
|
|
112
117
|
);
|
|
113
118
|
|
|
114
|
-
//
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
+
// Generic layout handler
|
|
120
|
+
const applyLayout = useCallback((layoutType: string) => {
|
|
121
|
+
// Get the React Flow pane element to find its actual center
|
|
122
|
+
const pane = document.querySelector('.react-flow__pane');
|
|
123
|
+
let viewportCenter = { x: 0, y: 0 };
|
|
124
|
+
|
|
125
|
+
if (pane) {
|
|
126
|
+
const rect = pane.getBoundingClientRect();
|
|
127
|
+
// Convert screen center of the pane to flow coordinates
|
|
128
|
+
viewportCenter = screenToFlowPosition({
|
|
129
|
+
x: rect.left + rect.width / 2,
|
|
130
|
+
y: rect.top + rect.height / 2,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let result;
|
|
135
|
+
switch (layoutType) {
|
|
136
|
+
case 'hierarchical-vertical':
|
|
137
|
+
result = applyHierarchicalLayout(nodes, edges, { direction: 'TB', center: viewportCenter });
|
|
138
|
+
break;
|
|
139
|
+
case 'hierarchical-horizontal':
|
|
140
|
+
result = applyHierarchicalLayout(nodes, edges, { direction: 'LR', center: viewportCenter });
|
|
141
|
+
break;
|
|
142
|
+
case 'circular':
|
|
143
|
+
result = applyCircularLayout(nodes, edges, { center: viewportCenter });
|
|
144
|
+
break;
|
|
145
|
+
case 'grid':
|
|
146
|
+
result = applyGridLayout(nodes, edges, { center: viewportCenter });
|
|
147
|
+
break;
|
|
148
|
+
case 'random':
|
|
149
|
+
result = applyRandomLayout(nodes, edges, { center: viewportCenter });
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
result = applyHierarchicalLayout(nodes, edges, { direction: 'TB', center: viewportCenter });
|
|
153
|
+
}
|
|
119
154
|
|
|
120
155
|
// Update nodes with new positions
|
|
121
|
-
|
|
156
|
+
result.nodes.forEach((node) => {
|
|
122
157
|
updateNodePosition(node.id, node.position);
|
|
123
158
|
});
|
|
124
159
|
|
|
125
|
-
useGraphStore.setState({ nodes:
|
|
160
|
+
useGraphStore.setState({ nodes: result.nodes });
|
|
126
161
|
setContextMenu(null);
|
|
127
162
|
setShowModuleSubmenu(false);
|
|
128
|
-
|
|
163
|
+
setShowLayoutSubmenu(false);
|
|
164
|
+
}, [nodes, edges, updateNodePosition, screenToFlowPosition]);
|
|
129
165
|
|
|
130
166
|
// Auto-zoom handler
|
|
131
167
|
const handleAutoZoom = useCallback(() => {
|
|
@@ -163,6 +199,7 @@ const GraphCanvas: React.FC = () => {
|
|
|
163
199
|
const onPaneClick = useCallback(() => {
|
|
164
200
|
setContextMenu(null);
|
|
165
201
|
setShowModuleSubmenu(false);
|
|
202
|
+
setShowLayoutSubmenu(false);
|
|
166
203
|
}, []);
|
|
167
204
|
|
|
168
205
|
// Node drag handler - prevent overlaps with collision detection
|
|
@@ -270,29 +307,170 @@ const GraphCanvas: React.FC = () => {
|
|
|
270
307
|
minWidth: 180,
|
|
271
308
|
}}
|
|
272
309
|
>
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
<div style={{ position: 'relative' }}>
|
|
311
|
+
<button
|
|
312
|
+
onMouseEnter={() => setShowLayoutSubmenu(true)}
|
|
313
|
+
onMouseLeave={(e) => {
|
|
314
|
+
const relatedTarget = e.relatedTarget as HTMLElement;
|
|
315
|
+
if (!relatedTarget || !relatedTarget.closest('.layout-submenu')) {
|
|
316
|
+
setShowLayoutSubmenu(false);
|
|
317
|
+
}
|
|
318
|
+
}}
|
|
319
|
+
style={{
|
|
320
|
+
display: 'flex',
|
|
321
|
+
alignItems: 'center',
|
|
322
|
+
justifyContent: 'space-between',
|
|
323
|
+
width: '100%',
|
|
324
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
325
|
+
border: 'none',
|
|
326
|
+
background: showLayoutSubmenu ? 'var(--color-bg-overlay)' : 'transparent',
|
|
327
|
+
cursor: 'pointer',
|
|
328
|
+
textAlign: 'left',
|
|
329
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
330
|
+
color: 'var(--color-text-primary)',
|
|
331
|
+
transition: 'var(--transition-colors)',
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<span>Auto Layout</span>
|
|
335
|
+
<span style={{ marginLeft: 'var(--spacing-2)' }}>▶</span>
|
|
336
|
+
</button>
|
|
337
|
+
|
|
338
|
+
{/* Layout Submenu */}
|
|
339
|
+
{showLayoutSubmenu && (
|
|
340
|
+
<div
|
|
341
|
+
className="layout-submenu"
|
|
342
|
+
onMouseEnter={() => setShowLayoutSubmenu(true)}
|
|
343
|
+
onMouseLeave={() => setShowLayoutSubmenu(false)}
|
|
344
|
+
style={{
|
|
345
|
+
position: 'absolute',
|
|
346
|
+
top: 0,
|
|
347
|
+
left: '100%',
|
|
348
|
+
background: 'var(--color-bg-surface)',
|
|
349
|
+
border: 'var(--border-default)',
|
|
350
|
+
borderRadius: 'var(--radius-md)',
|
|
351
|
+
boxShadow: 'var(--shadow-lg)',
|
|
352
|
+
zIndex: 1001,
|
|
353
|
+
minWidth: 180,
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
<button
|
|
357
|
+
onClick={() => applyLayout('hierarchical-vertical')}
|
|
358
|
+
style={{
|
|
359
|
+
display: 'block',
|
|
360
|
+
width: '100%',
|
|
361
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
362
|
+
border: 'none',
|
|
363
|
+
background: 'transparent',
|
|
364
|
+
cursor: 'pointer',
|
|
365
|
+
textAlign: 'left',
|
|
366
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
367
|
+
color: 'var(--color-text-primary)',
|
|
368
|
+
transition: 'var(--transition-colors)',
|
|
369
|
+
}}
|
|
370
|
+
onMouseEnter={(e) => {
|
|
371
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
372
|
+
}}
|
|
373
|
+
onMouseLeave={(e) => {
|
|
374
|
+
e.currentTarget.style.background = 'transparent';
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
Hierarchical (Vertical)
|
|
378
|
+
</button>
|
|
379
|
+
<button
|
|
380
|
+
onClick={() => applyLayout('hierarchical-horizontal')}
|
|
381
|
+
style={{
|
|
382
|
+
display: 'block',
|
|
383
|
+
width: '100%',
|
|
384
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
385
|
+
border: 'none',
|
|
386
|
+
background: 'transparent',
|
|
387
|
+
cursor: 'pointer',
|
|
388
|
+
textAlign: 'left',
|
|
389
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
390
|
+
color: 'var(--color-text-primary)',
|
|
391
|
+
transition: 'var(--transition-colors)',
|
|
392
|
+
}}
|
|
393
|
+
onMouseEnter={(e) => {
|
|
394
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
395
|
+
}}
|
|
396
|
+
onMouseLeave={(e) => {
|
|
397
|
+
e.currentTarget.style.background = 'transparent';
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
Hierarchical (Horizontal)
|
|
401
|
+
</button>
|
|
402
|
+
<button
|
|
403
|
+
onClick={() => applyLayout('circular')}
|
|
404
|
+
style={{
|
|
405
|
+
display: 'block',
|
|
406
|
+
width: '100%',
|
|
407
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
408
|
+
border: 'none',
|
|
409
|
+
background: 'transparent',
|
|
410
|
+
cursor: 'pointer',
|
|
411
|
+
textAlign: 'left',
|
|
412
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
413
|
+
color: 'var(--color-text-primary)',
|
|
414
|
+
transition: 'var(--transition-colors)',
|
|
415
|
+
}}
|
|
416
|
+
onMouseEnter={(e) => {
|
|
417
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
418
|
+
}}
|
|
419
|
+
onMouseLeave={(e) => {
|
|
420
|
+
e.currentTarget.style.background = 'transparent';
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
Circular
|
|
424
|
+
</button>
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => applyLayout('grid')}
|
|
427
|
+
style={{
|
|
428
|
+
display: 'block',
|
|
429
|
+
width: '100%',
|
|
430
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
431
|
+
border: 'none',
|
|
432
|
+
background: 'transparent',
|
|
433
|
+
cursor: 'pointer',
|
|
434
|
+
textAlign: 'left',
|
|
435
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
436
|
+
color: 'var(--color-text-primary)',
|
|
437
|
+
transition: 'var(--transition-colors)',
|
|
438
|
+
}}
|
|
439
|
+
onMouseEnter={(e) => {
|
|
440
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
441
|
+
}}
|
|
442
|
+
onMouseLeave={(e) => {
|
|
443
|
+
e.currentTarget.style.background = 'transparent';
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
Grid
|
|
447
|
+
</button>
|
|
448
|
+
<button
|
|
449
|
+
onClick={() => applyLayout('random')}
|
|
450
|
+
style={{
|
|
451
|
+
display: 'block',
|
|
452
|
+
width: '100%',
|
|
453
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
454
|
+
border: 'none',
|
|
455
|
+
background: 'transparent',
|
|
456
|
+
cursor: 'pointer',
|
|
457
|
+
textAlign: 'left',
|
|
458
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
459
|
+
color: 'var(--color-text-primary)',
|
|
460
|
+
transition: 'var(--transition-colors)',
|
|
461
|
+
}}
|
|
462
|
+
onMouseEnter={(e) => {
|
|
463
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
464
|
+
}}
|
|
465
|
+
onMouseLeave={(e) => {
|
|
466
|
+
e.currentTarget.style.background = 'transparent';
|
|
467
|
+
}}
|
|
468
|
+
>
|
|
469
|
+
Random
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
296
474
|
|
|
297
475
|
<button
|
|
298
476
|
onClick={handleAutoZoom}
|
|
@@ -15,6 +15,7 @@ export interface LayoutOptions {
|
|
|
15
15
|
direction?: 'TB' | 'LR' | 'BT' | 'RL';
|
|
16
16
|
nodeSpacing?: number;
|
|
17
17
|
rankSpacing?: number;
|
|
18
|
+
center?: { x: number; y: number }; // Optional center point for layout
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface LayoutResult {
|
|
@@ -30,10 +31,6 @@ const DEFAULT_NODE_HEIGHT = 80;
|
|
|
30
31
|
const MESSAGE_NODE_WIDTH = 150;
|
|
31
32
|
const MESSAGE_NODE_HEIGHT = 60;
|
|
32
33
|
|
|
33
|
-
// Default spacing (increased by 50% for better label visibility)
|
|
34
|
-
const DEFAULT_NODE_SPACING = 75; // Was 50
|
|
35
|
-
const DEFAULT_RANK_SPACING = 150; // Was 100
|
|
36
|
-
|
|
37
34
|
/**
|
|
38
35
|
* Get node dimensions based on node type
|
|
39
36
|
*/
|
|
@@ -59,8 +56,7 @@ export function applyHierarchicalLayout(
|
|
|
59
56
|
): LayoutResult {
|
|
60
57
|
const {
|
|
61
58
|
direction = 'TB',
|
|
62
|
-
|
|
63
|
-
rankSpacing = DEFAULT_RANK_SPACING,
|
|
59
|
+
center,
|
|
64
60
|
} = options;
|
|
65
61
|
|
|
66
62
|
// Handle empty graph
|
|
@@ -68,6 +64,21 @@ export function applyHierarchicalLayout(
|
|
|
68
64
|
return { nodes: [], edges, width: 0, height: 0 };
|
|
69
65
|
}
|
|
70
66
|
|
|
67
|
+
// Calculate dynamic spacing based on actual node sizes
|
|
68
|
+
// This ensures 200px minimum clearance regardless of node dimensions
|
|
69
|
+
let maxWidth = 0;
|
|
70
|
+
let maxHeight = 0;
|
|
71
|
+
|
|
72
|
+
nodes.forEach((node) => {
|
|
73
|
+
const { width, height } = getNodeDimensions(node);
|
|
74
|
+
maxWidth = Math.max(maxWidth, width);
|
|
75
|
+
maxHeight = Math.max(maxHeight, height);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Spacing = half of max node size + 200px minimum clearance
|
|
79
|
+
const nodeSpacing = options.nodeSpacing ?? (maxWidth / 2 + 200);
|
|
80
|
+
const rankSpacing = options.rankSpacing ?? (maxHeight / 2 + 200);
|
|
81
|
+
|
|
71
82
|
// Create a new directed graph
|
|
72
83
|
const graph = new dagre.graphlib.Graph();
|
|
73
84
|
|
|
@@ -97,6 +108,15 @@ export function applyHierarchicalLayout(
|
|
|
97
108
|
// Run the layout algorithm
|
|
98
109
|
dagre.layout(graph);
|
|
99
110
|
|
|
111
|
+
// Get graph dimensions first to calculate offset
|
|
112
|
+
const graphConfig = graph.graph();
|
|
113
|
+
const graphWidth = (graphConfig.width || 0) + 40; // Add margin
|
|
114
|
+
const graphHeight = (graphConfig.height || 0) + 40; // Add margin
|
|
115
|
+
|
|
116
|
+
// Calculate offset to center the layout around viewport center (or 0,0 if no center provided)
|
|
117
|
+
const offsetX = center ? center.x - graphWidth / 2 : 0;
|
|
118
|
+
const offsetY = center ? center.y - graphHeight / 2 : 0;
|
|
119
|
+
|
|
100
120
|
// Extract positioned nodes
|
|
101
121
|
const layoutedNodes = nodes.map((node) => {
|
|
102
122
|
const nodeWithPosition = graph.node(node.id);
|
|
@@ -107,22 +127,211 @@ export function applyHierarchicalLayout(
|
|
|
107
127
|
return {
|
|
108
128
|
...node,
|
|
109
129
|
position: {
|
|
110
|
-
x: nodeWithPosition.x - width / 2,
|
|
111
|
-
y: nodeWithPosition.y - height / 2,
|
|
130
|
+
x: nodeWithPosition.x - width / 2 + offsetX,
|
|
131
|
+
y: nodeWithPosition.y - height / 2 + offsetY,
|
|
112
132
|
},
|
|
113
133
|
};
|
|
114
134
|
});
|
|
115
135
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
return {
|
|
137
|
+
nodes: layoutedNodes,
|
|
138
|
+
edges,
|
|
139
|
+
width: graphWidth,
|
|
140
|
+
height: graphHeight,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Apply circular layout - nodes arranged in a circle
|
|
146
|
+
*/
|
|
147
|
+
export function applyCircularLayout(
|
|
148
|
+
nodes: Node[],
|
|
149
|
+
edges: Edge[],
|
|
150
|
+
options: LayoutOptions = {}
|
|
151
|
+
): LayoutResult {
|
|
152
|
+
const { center } = options;
|
|
153
|
+
|
|
154
|
+
if (nodes.length === 0) {
|
|
155
|
+
return { nodes: [], edges, width: 0, height: 0 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Calculate radius based on number of nodes and their sizes
|
|
159
|
+
let maxWidth = 0;
|
|
160
|
+
let maxHeight = 0;
|
|
161
|
+
nodes.forEach((node) => {
|
|
162
|
+
const { width, height } = getNodeDimensions(node);
|
|
163
|
+
maxWidth = Math.max(maxWidth, width);
|
|
164
|
+
maxHeight = Math.max(maxHeight, height);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const minSpacing = 200; // 200px minimum clearance
|
|
168
|
+
const nodeSize = Math.max(maxWidth, maxHeight);
|
|
169
|
+
const circumference = nodes.length * (nodeSize + minSpacing);
|
|
170
|
+
const radius = circumference / (2 * Math.PI);
|
|
171
|
+
|
|
172
|
+
const centerX = center?.x ?? 0;
|
|
173
|
+
const centerY = center?.y ?? 0;
|
|
174
|
+
|
|
175
|
+
const layoutedNodes = nodes.map((node, index) => {
|
|
176
|
+
const angle = (2 * Math.PI * index) / nodes.length;
|
|
177
|
+
const { width, height } = getNodeDimensions(node);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...node,
|
|
181
|
+
position: {
|
|
182
|
+
x: centerX + radius * Math.cos(angle) - width / 2,
|
|
183
|
+
y: centerY + radius * Math.sin(angle) - height / 2,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const graphWidth = radius * 2 + maxWidth;
|
|
189
|
+
const graphHeight = radius * 2 + maxHeight;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
nodes: layoutedNodes,
|
|
193
|
+
edges,
|
|
194
|
+
width: graphWidth,
|
|
195
|
+
height: graphHeight,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Apply grid layout - nodes arranged in a grid
|
|
201
|
+
*/
|
|
202
|
+
export function applyGridLayout(
|
|
203
|
+
nodes: Node[],
|
|
204
|
+
edges: Edge[],
|
|
205
|
+
options: LayoutOptions = {}
|
|
206
|
+
): LayoutResult {
|
|
207
|
+
const { center } = options;
|
|
208
|
+
|
|
209
|
+
if (nodes.length === 0) {
|
|
210
|
+
return { nodes: [], edges, width: 0, height: 0 };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Calculate grid dimensions
|
|
214
|
+
const cols = Math.ceil(Math.sqrt(nodes.length));
|
|
215
|
+
const rows = Math.ceil(nodes.length / cols);
|
|
216
|
+
|
|
217
|
+
let maxWidth = 0;
|
|
218
|
+
let maxHeight = 0;
|
|
219
|
+
nodes.forEach((node) => {
|
|
220
|
+
const { width, height } = getNodeDimensions(node);
|
|
221
|
+
maxWidth = Math.max(maxWidth, width);
|
|
222
|
+
maxHeight = Math.max(maxHeight, height);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const minSpacing = 200;
|
|
226
|
+
const cellWidth = maxWidth + minSpacing;
|
|
227
|
+
const cellHeight = maxHeight + minSpacing;
|
|
228
|
+
|
|
229
|
+
const graphWidth = cols * cellWidth;
|
|
230
|
+
const graphHeight = rows * cellHeight;
|
|
231
|
+
|
|
232
|
+
const startX = center ? center.x - graphWidth / 2 : 0;
|
|
233
|
+
const startY = center ? center.y - graphHeight / 2 : 0;
|
|
234
|
+
|
|
235
|
+
const layoutedNodes = nodes.map((node, index) => {
|
|
236
|
+
const col = index % cols;
|
|
237
|
+
const row = Math.floor(index / cols);
|
|
238
|
+
const { width, height } = getNodeDimensions(node);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
...node,
|
|
242
|
+
position: {
|
|
243
|
+
x: startX + col * cellWidth + (cellWidth - width) / 2,
|
|
244
|
+
y: startY + row * cellHeight + (cellHeight - height) / 2,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
nodes: layoutedNodes,
|
|
251
|
+
edges,
|
|
252
|
+
width: graphWidth,
|
|
253
|
+
height: graphHeight,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Apply random layout - nodes placed randomly with minimum spacing
|
|
259
|
+
*/
|
|
260
|
+
export function applyRandomLayout(
|
|
261
|
+
nodes: Node[],
|
|
262
|
+
edges: Edge[],
|
|
263
|
+
options: LayoutOptions = {}
|
|
264
|
+
): LayoutResult {
|
|
265
|
+
const { center } = options;
|
|
266
|
+
|
|
267
|
+
if (nodes.length === 0) {
|
|
268
|
+
return { nodes: [], edges, width: 0, height: 0 };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let maxWidth = 0;
|
|
272
|
+
let maxHeight = 0;
|
|
273
|
+
nodes.forEach((node) => {
|
|
274
|
+
const { width, height } = getNodeDimensions(node);
|
|
275
|
+
maxWidth = Math.max(maxWidth, width);
|
|
276
|
+
maxHeight = Math.max(maxHeight, height);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const minSpacing = 200;
|
|
280
|
+
const spreadFactor = 1.5; // How much to spread nodes apart
|
|
281
|
+
const areaSize = Math.sqrt(nodes.length) * (maxWidth + maxHeight + minSpacing) * spreadFactor;
|
|
282
|
+
|
|
283
|
+
const centerX = center?.x ?? 0;
|
|
284
|
+
const centerY = center?.y ?? 0;
|
|
285
|
+
|
|
286
|
+
// Place nodes randomly, checking for collisions
|
|
287
|
+
const layoutedNodes: Node[] = [];
|
|
288
|
+
const maxAttempts = 100;
|
|
289
|
+
|
|
290
|
+
nodes.forEach((node) => {
|
|
291
|
+
const { width, height } = getNodeDimensions(node);
|
|
292
|
+
let placed = false;
|
|
293
|
+
let attempts = 0;
|
|
294
|
+
|
|
295
|
+
while (!placed && attempts < maxAttempts) {
|
|
296
|
+
const x = centerX + (Math.random() - 0.5) * areaSize - width / 2;
|
|
297
|
+
const y = centerY + (Math.random() - 0.5) * areaSize - height / 2;
|
|
298
|
+
|
|
299
|
+
// Check if this position collides with existing nodes
|
|
300
|
+
const collides = layoutedNodes.some((existingNode) => {
|
|
301
|
+
const exDims = getNodeDimensions(existingNode);
|
|
302
|
+
const dx = Math.abs(x - existingNode.position.x);
|
|
303
|
+
const dy = Math.abs(y - existingNode.position.y);
|
|
304
|
+
return dx < (width + exDims.width) / 2 + minSpacing &&
|
|
305
|
+
dy < (height + exDims.height) / 2 + minSpacing;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!collides) {
|
|
309
|
+
layoutedNodes.push({
|
|
310
|
+
...node,
|
|
311
|
+
position: { x, y },
|
|
312
|
+
});
|
|
313
|
+
placed = true;
|
|
314
|
+
}
|
|
315
|
+
attempts++;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If we couldn't place it without collision, just place it anyway
|
|
319
|
+
if (!placed) {
|
|
320
|
+
layoutedNodes.push({
|
|
321
|
+
...node,
|
|
322
|
+
position: {
|
|
323
|
+
x: centerX + (Math.random() - 0.5) * areaSize - width / 2,
|
|
324
|
+
y: centerY + (Math.random() - 0.5) * areaSize - height / 2,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
});
|
|
120
329
|
|
|
121
330
|
return {
|
|
122
331
|
nodes: layoutedNodes,
|
|
123
332
|
edges,
|
|
124
|
-
width,
|
|
125
|
-
height,
|
|
333
|
+
width: areaSize,
|
|
334
|
+
height: areaSize,
|
|
126
335
|
};
|
|
127
336
|
}
|
|
128
337
|
|
|
@@ -135,12 +344,14 @@ export function applyDagreLayout(
|
|
|
135
344
|
edges: Edge[],
|
|
136
345
|
direction: 'TB' | 'LR' = 'TB',
|
|
137
346
|
nodeSpacing?: number,
|
|
138
|
-
rankSpacing?: number
|
|
347
|
+
rankSpacing?: number,
|
|
348
|
+
center?: { x: number; y: number }
|
|
139
349
|
): Node[] {
|
|
140
350
|
const result = applyHierarchicalLayout(nodes, edges, {
|
|
141
351
|
direction,
|
|
142
352
|
nodeSpacing,
|
|
143
|
-
rankSpacing
|
|
353
|
+
rankSpacing,
|
|
354
|
+
center
|
|
144
355
|
});
|
|
145
356
|
return result.nodes;
|
|
146
357
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flock-core
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.0b58
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -62,7 +62,8 @@ prompt = """You are an expert code reviewer. When you receive code, you should..
|
|
|
62
62
|
|
|
63
63
|
# 500-line prompt that breaks when models update
|
|
64
64
|
|
|
65
|
-
# How do I know that there isn't an even better prompt (you don't)
|
|
65
|
+
# How do I know that there isn't an even better prompt (you don't)
|
|
66
|
+
# -> proof of 'best possible performane' impossible
|
|
66
67
|
```
|
|
67
68
|
|
|
68
69
|
**🧪 Testing Nightmares**
|
|
@@ -88,7 +89,8 @@ workflow.add_edge("agent_b", "agent_c")
|
|
|
88
89
|
**🧠 God object anti-pattern:**
|
|
89
90
|
```python
|
|
90
91
|
# One orchestrator needs domain knowledge of 20+ agents to route correctly
|
|
91
|
-
# Orchestrator 'guesses' next agent based on a natural language description.
|
|
92
|
+
# Orchestrator 'guesses' next agent based on a natural language description.
|
|
93
|
+
# Hardly fit for critical systems.
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
These aren't framework limitations, they're **architectural choices** that don't scale.
|
|
@@ -105,8 +107,51 @@ Flock takes a different path, combining two proven patterns:
|
|
|
105
107
|
|
|
106
108
|
**Traditional approach:**
|
|
107
109
|
```python
|
|
108
|
-
prompt = "
|
|
109
|
-
|
|
110
|
+
prompt = """You are an expert software engineer and bug analyst. Your task is to analyze bug reports and provide structured diagnostic information.
|
|
111
|
+
|
|
112
|
+
INSTRUCTIONS:
|
|
113
|
+
1. Read the bug report carefully
|
|
114
|
+
2. Determine the severity level (must be exactly one of: Critical, High, Medium, Low)
|
|
115
|
+
3. Classify the bug category (e.g., "performance", "security", "UI", "data corruption")
|
|
116
|
+
4. Formulate a root cause hypothesis (minimum 50 characters)
|
|
117
|
+
5. Assign a confidence score between 0.0 and 1.0
|
|
118
|
+
|
|
119
|
+
OUTPUT FORMAT:
|
|
120
|
+
You MUST return valid JSON with this exact structure:
|
|
121
|
+
{
|
|
122
|
+
"severity": "string (Critical|High|Medium|Low)",
|
|
123
|
+
"category": "string",
|
|
124
|
+
"root_cause_hypothesis": "string (minimum 50 characters)",
|
|
125
|
+
"confidence_score": "number (0.0 to 1.0)"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
VALIDATION RULES:
|
|
129
|
+
- severity: Must be exactly one of: Critical, High, Medium, Low (case-sensitive)
|
|
130
|
+
- category: Must be a single word or short phrase describing the bug type
|
|
131
|
+
- root_cause_hypothesis: Must be at least 50 characters long and explain the likely cause
|
|
132
|
+
- confidence_score: Must be a decimal number between 0.0 and 1.0 inclusive
|
|
133
|
+
|
|
134
|
+
EXAMPLES:
|
|
135
|
+
Input: "App crashes when user clicks submit button"
|
|
136
|
+
Output: {"severity": "Critical", "category": "crash", "root_cause_hypothesis": "Null pointer exception in form validation logic when required fields are empty", "confidence_score": 0.85}
|
|
137
|
+
|
|
138
|
+
Input: "Login button has wrong color"
|
|
139
|
+
Output: {"severity": "Low", "category": "UI", "root_cause_hypothesis": "CSS class override not applied correctly in the theme configuration", "confidence_score": 0.9}
|
|
140
|
+
|
|
141
|
+
IMPORTANT:
|
|
142
|
+
- Do NOT include any explanatory text before or after the JSON
|
|
143
|
+
- Do NOT use markdown code blocks (no ```json```)
|
|
144
|
+
- Do NOT add comments in the JSON
|
|
145
|
+
- Ensure the JSON is valid and parseable
|
|
146
|
+
- If you cannot determine something, use your best judgment
|
|
147
|
+
- Never return null values
|
|
148
|
+
|
|
149
|
+
Now analyze this bug report:
|
|
150
|
+
{bug_report_text}"""
|
|
151
|
+
|
|
152
|
+
result = llm.invoke(prompt) # 500-line prompt that breaks when models update
|
|
153
|
+
# Then parse and hope it's valid JSON
|
|
154
|
+
data = json.loads(result.content) # Crashes in production 🔥
|
|
110
155
|
```
|
|
111
156
|
|
|
112
157
|
**The Flock way:**
|
|
@@ -405,7 +450,11 @@ The dashboard provides comprehensive real-time visibility into your agent system
|
|
|
405
450
|
- **Interactive Graph:**
|
|
406
451
|
- Drag nodes, zoom, pan, and explore topology
|
|
407
452
|
- Double-click nodes to open detail windows
|
|
408
|
-
- Right-click for context menu
|
|
453
|
+
- Right-click for context menu with auto-layout options:
|
|
454
|
+
- **5 Layout Algorithms**: Hierarchical (Vertical/Horizontal), Circular, Grid, and Random
|
|
455
|
+
- **Smart Spacing**: Dynamic 200px minimum clearance based on node dimensions
|
|
456
|
+
- **Viewport Centering**: Layouts always center around current viewport
|
|
457
|
+
- Add modules dynamically from context menu
|
|
409
458
|
|
|
410
459
|
- **Advanced Filtering:**
|
|
411
460
|
- Correlation ID tracking for workflow tracing
|
|
@@ -18,13 +18,13 @@ flock/dashboard/collector.py,sha256=dF8uddDMpOSdxGkhDSAvRNNaABo-TfOceipf1SQmLSU,
|
|
|
18
18
|
flock/dashboard/events.py,sha256=ujdmRJK-GQubrv43qfQ73dnrTj7g39VzBkWfmskJ0j8,5234
|
|
19
19
|
flock/dashboard/launcher.py,sha256=zXWVpyLNxCIu6fJ2L2j2sJ4oDWTvkxhT4FWz7K6eooM,8122
|
|
20
20
|
flock/dashboard/service.py,sha256=5QGPT2xSbMx6Zd9_GSKJ8QtxOnBucx9BZAQhpWyUfgw,32097
|
|
21
|
-
flock/dashboard/websocket.py,sha256=
|
|
21
|
+
flock/dashboard/websocket.py,sha256=RdJ7fhjNYJR8WHJ19wWdf9GEQtuKE14NmUpqm-QsLnA,9013
|
|
22
22
|
flock/engines/__init__.py,sha256=waNyObJ8PKCLFZL3WUFynxSK-V47m559P3Px-vl_OSc,124
|
|
23
23
|
flock/engines/dspy_engine.py,sha256=Q2gPYLW_f8f-JuBYOMtjtoCZH8Fc447zWF8cHpJl4ew,34538
|
|
24
24
|
flock/frontend/README.md,sha256=OFdOItV8FGifmUDb694rV2xLC0vl1HlR5KBEtYv5AB0,25054
|
|
25
25
|
flock/frontend/index.html,sha256=BFg1VR_YVAJ_MGN16xa7sT6wTGwtFYUhfJhGuKv89VM,312
|
|
26
|
-
flock/frontend/package-lock.json,sha256=
|
|
27
|
-
flock/frontend/package.json,sha256=
|
|
26
|
+
flock/frontend/package-lock.json,sha256=F-KmPhq6IbHXzxtmHL0S7dt6DGs4fWrOkrfKeXaSx6U,150998
|
|
27
|
+
flock/frontend/package.json,sha256=X5mMewghkW5K03Y4CI6unF2GIGbV0QoS-kZ04RNtQ3k,1258
|
|
28
28
|
flock/frontend/tsconfig.json,sha256=B9p9jXohg_jrCZAq5_yIHvznpeXHiHQkwUZrVE2oMRA,705
|
|
29
29
|
flock/frontend/tsconfig.node.json,sha256=u5_YWSqeNkZBRBIZ8Q2E2q6bospcyF23mO-taRO7glc,233
|
|
30
30
|
flock/frontend/vite.config.ts,sha256=OQOr6Hl75iW7EvEm2_GXFdicYWthgdLhp4lz3d7RkJA,566
|
|
@@ -71,7 +71,7 @@ flock/frontend/src/components/filters/TimeRangeFilter.test.tsx,sha256=4vkGYNecXc
|
|
|
71
71
|
flock/frontend/src/components/filters/TimeRangeFilter.tsx,sha256=xwTPRl-NbSEvSrX4krvLPb40GvCjdoUp-FqRbVZBzbU,3148
|
|
72
72
|
flock/frontend/src/components/graph/AgentNode.test.tsx,sha256=Wd3AGe1E9GJ26wyRU1AUakki9AQ9euYsKx5lhi1xAfg,1966
|
|
73
73
|
flock/frontend/src/components/graph/AgentNode.tsx,sha256=JOSMiR_YXq6MgME2DfhqHldS_4zZEPySEIEM6t4fg48,12447
|
|
74
|
-
flock/frontend/src/components/graph/GraphCanvas.tsx,sha256=
|
|
74
|
+
flock/frontend/src/components/graph/GraphCanvas.tsx,sha256=vMLbZie9Isnlr2pflQTRGLgME8a8Q8zXRg4qLGW7Vw4,20809
|
|
75
75
|
flock/frontend/src/components/graph/MessageFlowEdge.tsx,sha256=OWYmmlS5P9yIxfCddEleEd27MA-djMd_3fcQmyT22r4,3914
|
|
76
76
|
flock/frontend/src/components/graph/MessageNode.test.tsx,sha256=S0vmtk2r4HGm8844Pb65syim-ZsCqvia24CECGgq5SY,1828
|
|
77
77
|
flock/frontend/src/components/graph/MessageNode.tsx,sha256=axwYof1z3UtLO0QDPfTKnZX6w_TNFlZvVJsun1aDD_k,3645
|
|
@@ -108,7 +108,7 @@ flock/frontend/src/services/api.ts,sha256=hGkImiimpSOX9xjmX5flI1Ug6YVh7EbRr7l0rq
|
|
|
108
108
|
flock/frontend/src/services/indexeddb.test.ts,sha256=lY1JzyKRd4Kaw135JlO7njy5rpaAsRRXLM8SwYxDhAc,27452
|
|
109
109
|
flock/frontend/src/services/indexeddb.ts,sha256=Eadllc6tPh09RhJPjCd6MO0tdh8PWX5Hoxy8HX9HQS0,26915
|
|
110
110
|
flock/frontend/src/services/layout.test.ts,sha256=-KwUxWCum_Rsyc5NIpk99UB3prfAkMO5ksJULhjOiwA,16174
|
|
111
|
-
flock/frontend/src/services/layout.ts,sha256=
|
|
111
|
+
flock/frontend/src/services/layout.ts,sha256=5WzlOv7OBlQXiUxrv4l1JwaAfHbUK1C99JOT0fQCNRY,9503
|
|
112
112
|
flock/frontend/src/services/themeApplicator.ts,sha256=utRFw-45e1IEOrI6lHkB_E_-5kc2kFKbN-veAUdXiOM,5802
|
|
113
113
|
flock/frontend/src/services/themeService.ts,sha256=ltDAE30KzzVFDQJGm8awN0Go-l16NgTWYOxlvgxvx0E,1743
|
|
114
114
|
flock/frontend/src/services/websocket.test.ts,sha256=Ix5dQI3lzKJPyFPcTs5yXxu8ZZoJ05f1CWgcQEpCOtg,17062
|
|
@@ -508,8 +508,8 @@ flock/themes/zenburned.toml,sha256=UEmquBbcAO3Zj652XKUwCsNoC2iQSlIh-q5c6DH-7Kc,1
|
|
|
508
508
|
flock/themes/zenwritten-dark.toml,sha256=-dgaUfg1iCr5Dv4UEeHv_cN4GrPUCWAiHSxWK20X1kI,1663
|
|
509
509
|
flock/themes/zenwritten-light.toml,sha256=G1iEheCPfBNsMTGaVpEVpDzYBHA_T-MV27rolUYolmE,1666
|
|
510
510
|
flock/utility/output_utility_component.py,sha256=yVHhlIIIoYKziI5UyT_zvQb4G-NsxCTgLwA1wXXTTj4,9047
|
|
511
|
-
flock_core-0.5.
|
|
512
|
-
flock_core-0.5.
|
|
513
|
-
flock_core-0.5.
|
|
514
|
-
flock_core-0.5.
|
|
515
|
-
flock_core-0.5.
|
|
511
|
+
flock_core-0.5.0b58.dist-info/METADATA,sha256=1frs-b0VPrAk3XwsqCa6pG03VeE0cDID7WtFsMeeezQ,34719
|
|
512
|
+
flock_core-0.5.0b58.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
513
|
+
flock_core-0.5.0b58.dist-info/entry_points.txt,sha256=UQdPmtHd97gSA_IdLt9MOd-1rrf_WO-qsQeIiHWVrp4,42
|
|
514
|
+
flock_core-0.5.0b58.dist-info/licenses/LICENSE,sha256=U3IZuTbC0yLj7huwJdldLBipSOHF4cPf6cUOodFiaBE,1072
|
|
515
|
+
flock_core-0.5.0b58.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|