flock-core 0.5.0b71__py3-none-any.whl → 0.5.0b75__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.

Files changed (62) hide show
  1. flock/agent.py +39 -1
  2. flock/artifacts.py +17 -10
  3. flock/cli.py +1 -1
  4. flock/dashboard/__init__.py +2 -0
  5. flock/dashboard/collector.py +282 -6
  6. flock/dashboard/events.py +6 -0
  7. flock/dashboard/graph_builder.py +563 -0
  8. flock/dashboard/launcher.py +11 -6
  9. flock/dashboard/models/graph.py +156 -0
  10. flock/dashboard/service.py +175 -14
  11. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
  12. flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
  13. flock/dashboard/static_v2/index.html +13 -0
  14. flock/dashboard/websocket.py +2 -2
  15. flock/engines/dspy_engine.py +27 -8
  16. flock/frontend/README.md +6 -6
  17. flock/frontend/src/App.tsx +23 -31
  18. flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
  19. flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
  20. flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
  21. flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
  22. flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
  23. flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
  24. flock/frontend/src/components/graph/AgentNode.tsx +8 -6
  25. flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
  26. flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
  27. flock/frontend/src/components/graph/MessageNode.tsx +16 -3
  28. flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
  29. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
  30. flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
  31. flock/frontend/src/hooks/useModules.ts +12 -4
  32. flock/frontend/src/hooks/usePersistence.ts +5 -3
  33. flock/frontend/src/services/api.ts +3 -19
  34. flock/frontend/src/services/graphService.test.ts +330 -0
  35. flock/frontend/src/services/graphService.ts +75 -0
  36. flock/frontend/src/services/websocket.ts +104 -268
  37. flock/frontend/src/store/filterStore.test.ts +89 -1
  38. flock/frontend/src/store/filterStore.ts +38 -16
  39. flock/frontend/src/store/graphStore.test.ts +538 -173
  40. flock/frontend/src/store/graphStore.ts +374 -465
  41. flock/frontend/src/store/moduleStore.ts +51 -33
  42. flock/frontend/src/store/uiStore.ts +23 -11
  43. flock/frontend/src/types/graph.ts +77 -44
  44. flock/frontend/src/utils/mockData.ts +16 -3
  45. flock/frontend/vite.config.ts +2 -2
  46. flock/orchestrator.py +24 -6
  47. flock/service.py +2 -2
  48. flock/store.py +169 -4
  49. flock/themes/darkmatrix.toml +2 -2
  50. flock/themes/deep.toml +2 -2
  51. flock/themes/neopolitan.toml +4 -4
  52. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
  53. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/RECORD +56 -53
  54. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
  55. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
  56. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
  57. flock/frontend/src/services/websocket.test.ts +0 -595
  58. flock/frontend/src/utils/transforms.test.ts +0 -860
  59. flock/frontend/src/utils/transforms.ts +0 -323
  60. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
  61. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
  62. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,330 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { fetchGraphSnapshot, mergeNodePositions, overlayWebSocketState } from './graphService';
3
+ import { GraphRequest, GraphSnapshot } from '../types/graph';
4
+ import { Node } from '@xyflow/react';
5
+
6
+ /**
7
+ * Graph Service Tests - UI Optimization Migration (Spec 002)
8
+ *
9
+ * Tests the NEW graph service layer that replaces client-side graph construction
10
+ * with backend snapshot consumption.
11
+ *
12
+ * SPECIFICATION: docs/internal/ui-optimization/03-migration-implementation-guide.md
13
+ * FOCUS: Backend integration, position merging, WebSocket state overlay, error handling
14
+ */
15
+
16
+ describe('graphService', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ afterEach(() => {
22
+ vi.restoreAllMocks();
23
+ });
24
+
25
+ describe('fetchGraphSnapshot', () => {
26
+ it('should fetch agent view graph from backend with correct request format', async () => {
27
+ const mockSnapshot: GraphSnapshot = {
28
+ nodes: [
29
+ { id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 0, y: 0 }, hidden: false },
30
+ ],
31
+ edges: [
32
+ { id: 'edge1', source: 'agent1', target: 'agent2', type: 'message_flow', hidden: false, data: {} },
33
+ ],
34
+ statistics: null,
35
+ viewMode: 'agent',
36
+ filters: {
37
+ correlation_id: null,
38
+ time_range: { preset: 'last10min' },
39
+ artifactTypes: [],
40
+ producers: [],
41
+ tags: [],
42
+ visibility: [],
43
+ },
44
+ generatedAt: '2025-10-11T00:00:00Z',
45
+ totalArtifacts: 1,
46
+ truncated: false,
47
+ };
48
+
49
+ globalThis.fetch = vi.fn().mockResolvedValue({
50
+ ok: true,
51
+ json: async () => mockSnapshot,
52
+ }) as any;
53
+
54
+ const request: GraphRequest = {
55
+ viewMode: 'agent',
56
+ filters: {
57
+ correlation_id: null,
58
+ time_range: { preset: 'last10min' },
59
+ artifactTypes: ['Pizza'],
60
+ producers: ['pizza_master'],
61
+ tags: [],
62
+ visibility: [],
63
+ },
64
+ options: { include_statistics: true },
65
+ };
66
+
67
+ const result = await fetchGraphSnapshot(request);
68
+
69
+ expect(globalThis.fetch).toHaveBeenCalledWith('/api/dashboard/graph', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify(request),
73
+ });
74
+
75
+ expect(result).toEqual(mockSnapshot);
76
+ expect(result.nodes).toHaveLength(1);
77
+ expect(result.edges).toHaveLength(1);
78
+ });
79
+
80
+ it('should throw error when API call fails', async () => {
81
+ globalThis.fetch = vi.fn().mockResolvedValue({
82
+ ok: false,
83
+ statusText: 'Internal Server Error',
84
+ });
85
+
86
+ const request: GraphRequest = {
87
+ viewMode: 'agent',
88
+ filters: {
89
+ correlation_id: null,
90
+ time_range: { preset: 'last10min' },
91
+ artifactTypes: [],
92
+ producers: [],
93
+ tags: [],
94
+ visibility: [],
95
+ },
96
+ };
97
+
98
+ await expect(fetchGraphSnapshot(request)).rejects.toThrow('Graph API error: Internal Server Error');
99
+ });
100
+
101
+ it('should handle network errors gracefully', async () => {
102
+ globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
103
+
104
+ const request: GraphRequest = {
105
+ viewMode: 'blackboard',
106
+ filters: {
107
+ correlation_id: null,
108
+ time_range: { preset: 'last5min' },
109
+ artifactTypes: [],
110
+ producers: [],
111
+ tags: [],
112
+ visibility: [],
113
+ },
114
+ };
115
+
116
+ await expect(fetchGraphSnapshot(request)).rejects.toThrow('Network error');
117
+ });
118
+ });
119
+
120
+ describe('mergeNodePositions - Priority Logic', () => {
121
+ it('should prioritize saved positions over all others', () => {
122
+ const backendNodes = [
123
+ { id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
124
+ ];
125
+
126
+ const savedPositions = new Map([['agent1', { x: 500, y: 500 }]]);
127
+
128
+ const currentNodes: Node[] = [
129
+ { id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 300, y: 300 } },
130
+ ];
131
+
132
+ const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
133
+
134
+ // Saved position (500, 500) wins
135
+ expect(result[0]?.position).toEqual({ x: 500, y: 500 });
136
+ });
137
+
138
+ it('should prioritize current positions when no saved position exists', () => {
139
+ const backendNodes = [
140
+ { id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
141
+ ];
142
+
143
+ const savedPositions = new Map(); // No saved position
144
+
145
+ const currentNodes: Node[] = [
146
+ { id: 'agent1', type: 'agent', data: { name: 'agent1' }, position: { x: 300, y: 300 } },
147
+ ];
148
+
149
+ const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
150
+
151
+ // Current position (300, 300) wins
152
+ expect(result[0]!.position).toEqual({ x: 300, y: 300 });
153
+ });
154
+
155
+ it('should use backend position when saved and current are unavailable', () => {
156
+ const backendNodes = [
157
+ { id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
158
+ ];
159
+
160
+ const savedPositions = new Map(); // No saved position
161
+ const currentNodes: Node[] = []; // No current position
162
+
163
+ const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
164
+
165
+ // Backend position (100, 100) wins
166
+ expect(result[0]!.position).toEqual({ x: 100, y: 100 });
167
+ });
168
+
169
+ it('should generate random position when backend has zero coordinates', () => {
170
+ const backendNodes = [
171
+ { id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 0, y: 0 }, hidden: false },
172
+ ];
173
+
174
+ const savedPositions = new Map(); // No saved position
175
+ const currentNodes: Node[] = []; // No current position
176
+
177
+ const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
178
+
179
+ // Random position should be generated (not 0, 0)
180
+ expect(result[0]!.position.x).toBeGreaterThan(0);
181
+ expect(result[0]!.position.y).toBeGreaterThan(0);
182
+ // Random position range: x in [400, 600], y in [300, 500]
183
+ expect(result[0]!.position.x).toBeGreaterThanOrEqual(400);
184
+ expect(result[0]!.position.x).toBeLessThanOrEqual(600);
185
+ expect(result[0]!.position.y).toBeGreaterThanOrEqual(300);
186
+ expect(result[0]!.position.y).toBeLessThanOrEqual(500);
187
+ });
188
+
189
+ it('should handle multiple nodes with mixed position sources', () => {
190
+ const backendNodes = [
191
+ { id: 'agent1', type: 'agent' as const, data: { name: 'agent1' }, position: { x: 100, y: 100 }, hidden: false },
192
+ { id: 'agent2', type: 'agent' as const, data: { name: 'agent2' }, position: { x: 200, y: 200 }, hidden: false },
193
+ { id: 'agent3', type: 'agent' as const, data: { name: 'agent3' }, position: { x: 0, y: 0 }, hidden: false },
194
+ ];
195
+
196
+ const savedPositions = new Map([['agent1', { x: 500, y: 500 }]]); // Only agent1
197
+
198
+ const currentNodes: Node[] = [
199
+ { id: 'agent2', type: 'agent', data: { name: 'agent2' }, position: { x: 300, y: 300 } },
200
+ ];
201
+
202
+ const result = mergeNodePositions(backendNodes, savedPositions, currentNodes);
203
+
204
+ // agent1: saved position wins
205
+ expect(result[0]!.position).toEqual({ x: 500, y: 500 });
206
+ // agent2: current position wins
207
+ expect(result[1]!.position).toEqual({ x: 300, y: 300 });
208
+ // agent3: random position (backend is 0,0)
209
+ expect(result[2]!.position.x).toBeGreaterThan(0);
210
+ expect(result[2]!.position.y).toBeGreaterThan(0);
211
+ });
212
+ });
213
+
214
+ describe('overlayWebSocketState', () => {
215
+ it('should overlay agent status from WebSocket state', () => {
216
+ const nodes: Node[] = [
217
+ {
218
+ id: 'agent1',
219
+ type: 'agent',
220
+ data: { name: 'agent1', status: 'idle' },
221
+ position: { x: 100, y: 100 },
222
+ },
223
+ ];
224
+
225
+ const agentStatus = new Map([['agent1', 'running']]);
226
+ const streamingTokens = new Map<string, string[]>();
227
+
228
+ const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
229
+
230
+ expect(result[0]!.data.status).toBe('running');
231
+ });
232
+
233
+ it('should overlay streaming tokens from WebSocket state', () => {
234
+ const nodes: Node[] = [
235
+ {
236
+ id: 'agent1',
237
+ type: 'agent',
238
+ data: { name: 'agent1', status: 'idle' },
239
+ position: { x: 100, y: 100 },
240
+ },
241
+ ];
242
+
243
+ const agentStatus = new Map<string, string>();
244
+ const streamingTokens = new Map([['agent1', ['token1', 'token2', 'token3']]]);
245
+
246
+ const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
247
+
248
+ expect(result[0]!.data.streamingTokens).toEqual(['token1', 'token2', 'token3']);
249
+ });
250
+
251
+ it('should use backend status when WebSocket state is unavailable', () => {
252
+ const nodes: Node[] = [
253
+ {
254
+ id: 'agent1',
255
+ type: 'agent',
256
+ data: { name: 'agent1', status: 'idle' },
257
+ position: { x: 100, y: 100 },
258
+ },
259
+ ];
260
+
261
+ const agentStatus = new Map<string, string>(); // Empty
262
+ const streamingTokens = new Map<string, string[]>(); // Empty
263
+
264
+ const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
265
+
266
+ // Backend status is preserved
267
+ expect(result[0]!.data.status).toBe('idle');
268
+ // Empty array for tokens
269
+ expect(result[0]!.data.streamingTokens).toEqual([]);
270
+ });
271
+
272
+ it('should not modify non-agent nodes', () => {
273
+ const nodes: Node[] = [
274
+ {
275
+ id: 'message1',
276
+ type: 'message',
277
+ data: { type: 'Pizza', payload: {} },
278
+ position: { x: 100, y: 100 },
279
+ },
280
+ ];
281
+
282
+ const agentStatus = new Map([['message1', 'running']]);
283
+ const streamingTokens = new Map([['message1', ['token1']]]);
284
+
285
+ const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
286
+
287
+ // Message node should not be modified
288
+ expect(result[0]!.data).toEqual({ type: 'Pizza', payload: {} });
289
+ expect(result[0]!.data.status).toBeUndefined();
290
+ expect(result[0]!.data.streamingTokens).toBeUndefined();
291
+ });
292
+
293
+ it('should handle multiple agent nodes with mixed WebSocket state', () => {
294
+ const nodes: Node[] = [
295
+ {
296
+ id: 'agent1',
297
+ type: 'agent',
298
+ data: { name: 'agent1', status: 'idle' },
299
+ position: { x: 100, y: 100 },
300
+ },
301
+ {
302
+ id: 'agent2',
303
+ type: 'agent',
304
+ data: { name: 'agent2', status: 'idle' },
305
+ position: { x: 200, y: 200 },
306
+ },
307
+ {
308
+ id: 'message1',
309
+ type: 'message',
310
+ data: { type: 'Pizza' },
311
+ position: { x: 300, y: 300 },
312
+ },
313
+ ];
314
+
315
+ const agentStatus = new Map([['agent1', 'running']]); // Only agent1
316
+ const streamingTokens = new Map([['agent2', ['token1', 'token2']]]); // Only agent2
317
+
318
+ const result = overlayWebSocketState(nodes, agentStatus, streamingTokens);
319
+
320
+ // agent1: status updated, no tokens
321
+ expect(result[0]!.data.status).toBe('running');
322
+ expect(result[0]!.data.streamingTokens).toEqual([]);
323
+ // agent2: status from backend, tokens updated
324
+ expect(result[1]!.data.status).toBe('idle');
325
+ expect(result[1]!.data.streamingTokens).toEqual(['token1', 'token2']);
326
+ // message1: unchanged
327
+ expect(result[2]!.data).toEqual({ type: 'Pizza' });
328
+ });
329
+ });
330
+ });
@@ -0,0 +1,75 @@
1
+ import { GraphRequest, GraphSnapshot, GraphNode } from '../types/graph';
2
+ import { Node } from '@xyflow/react';
3
+
4
+ /**
5
+ * Fetch graph snapshot from backend
6
+ */
7
+ export async function fetchGraphSnapshot(
8
+ request: GraphRequest
9
+ ): Promise<GraphSnapshot> {
10
+ const response = await fetch('/api/dashboard/graph', {
11
+ method: 'POST',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify(request),
14
+ });
15
+
16
+ if (!response.ok) {
17
+ throw new Error(`Graph API error: ${response.statusText}`);
18
+ }
19
+
20
+ return response.json();
21
+ }
22
+
23
+ /**
24
+ * Merge backend node positions with saved/current positions
25
+ * Priority: saved > current > backend > random
26
+ */
27
+ export function mergeNodePositions(
28
+ backendNodes: GraphNode[],
29
+ savedPositions: Map<string, { x: number; y: number }>,
30
+ currentNodes: Node[]
31
+ ): Node[] {
32
+ const currentPositions = new Map(
33
+ currentNodes.map(n => [n.id, n.position])
34
+ );
35
+
36
+ return backendNodes.map(node => {
37
+ const position =
38
+ savedPositions.get(node.id) ||
39
+ currentPositions.get(node.id) ||
40
+ (node.position.x !== 0 || node.position.y !== 0 ? node.position : null) ||
41
+ randomPosition();
42
+
43
+ return { ...node, position };
44
+ });
45
+ }
46
+
47
+ function randomPosition() {
48
+ return {
49
+ x: 400 + Math.random() * 200,
50
+ y: 300 + Math.random() * 200,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Overlay real-time WebSocket state on backend nodes
56
+ */
57
+ export function overlayWebSocketState(
58
+ nodes: Node[],
59
+ agentStatus: Map<string, string>,
60
+ streamingTokens: Map<string, string[]>
61
+ ): Node[] {
62
+ return nodes.map(node => {
63
+ if (node.type === 'agent') {
64
+ return {
65
+ ...node,
66
+ data: {
67
+ ...node.data,
68
+ status: agentStatus.get(node.id) || node.data.status,
69
+ streamingTokens: streamingTokens.get(node.id) || [],
70
+ },
71
+ };
72
+ }
73
+ return node;
74
+ });
75
+ }