flock-core 0.5.0b71__py3-none-any.whl → 0.5.1__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/agent.py +39 -1
- flock/artifacts.py +17 -10
- flock/cli.py +1 -1
- flock/dashboard/__init__.py +2 -0
- flock/dashboard/collector.py +282 -6
- flock/dashboard/events.py +6 -0
- flock/dashboard/graph_builder.py +563 -0
- flock/dashboard/launcher.py +11 -6
- flock/dashboard/models/__init__.py +1 -0
- flock/dashboard/models/graph.py +156 -0
- flock/dashboard/service.py +175 -14
- flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
- flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
- flock/dashboard/static_v2/index.html +13 -0
- flock/dashboard/websocket.py +2 -2
- flock/engines/dspy_engine.py +294 -20
- flock/frontend/README.md +6 -6
- flock/frontend/src/App.tsx +23 -31
- flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
- flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
- flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
- flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
- flock/frontend/src/components/graph/AgentNode.tsx +8 -6
- flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
- flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
- flock/frontend/src/components/graph/MessageNode.tsx +16 -3
- flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
- flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
- flock/frontend/src/hooks/useModules.ts +12 -4
- flock/frontend/src/hooks/usePersistence.ts +5 -3
- flock/frontend/src/services/api.ts +3 -19
- flock/frontend/src/services/graphService.test.ts +330 -0
- flock/frontend/src/services/graphService.ts +75 -0
- flock/frontend/src/services/websocket.ts +104 -268
- flock/frontend/src/store/filterStore.test.ts +89 -1
- flock/frontend/src/store/filterStore.ts +38 -16
- flock/frontend/src/store/graphStore.test.ts +538 -173
- flock/frontend/src/store/graphStore.ts +374 -465
- flock/frontend/src/store/moduleStore.ts +51 -33
- flock/frontend/src/store/uiStore.ts +23 -11
- flock/frontend/src/types/graph.ts +77 -44
- flock/frontend/src/utils/mockData.ts +16 -3
- flock/frontend/vite.config.ts +2 -2
- flock/orchestrator.py +27 -7
- flock/patches/__init__.py +5 -0
- flock/patches/dspy_streaming_patch.py +82 -0
- flock/service.py +2 -2
- flock/store.py +169 -4
- flock/themes/darkmatrix.toml +2 -2
- flock/themes/deep.toml +2 -2
- flock/themes/neopolitan.toml +4 -4
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/METADATA +20 -13
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/RECORD +59 -53
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
- flock/frontend/src/services/websocket.test.ts +0 -595
- flock/frontend/src/utils/transforms.test.ts +0 -860
- flock/frontend/src/utils/transforms.ts +0 -323
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,551 +1,460 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { devtools } from 'zustand/middleware';
|
|
3
3
|
import { Node, Edge } from '@xyflow/react';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { GraphSnapshot, GraphStatistics, GraphRequest } from '../types/graph';
|
|
5
|
+
import { fetchGraphSnapshot, mergeNodePositions, overlayWebSocketState } from '../services/graphService';
|
|
6
6
|
import { useFilterStore } from './filterStore';
|
|
7
|
+
import { Message } from '../types/graph';
|
|
8
|
+
import { indexedDBService } from '../services/indexeddb';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Graph Store - UI Optimization Migration (Spec 002)
|
|
12
|
+
*
|
|
13
|
+
* SIMPLIFIED backend-integrated version that replaces 553 lines of client-side
|
|
14
|
+
* graph construction with backend snapshot consumption.
|
|
15
|
+
*
|
|
16
|
+
* KEY CHANGES:
|
|
17
|
+
* - Backend generates nodes + edges + statistics
|
|
18
|
+
* - Position merging: saved > current > backend > random
|
|
19
|
+
* - WebSocket state overlay for real-time updates (status, tokens)
|
|
20
|
+
* - Debounced refresh: 100ms batching for snappy UX
|
|
21
|
+
* - No more client-side edge derivation
|
|
22
|
+
* - No more synthetic runs or complex Maps
|
|
23
|
+
*/
|
|
7
24
|
|
|
8
25
|
interface GraphState {
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
events: Message[];
|
|
13
|
-
runs: Map<string, Run>;
|
|
14
|
-
|
|
15
|
-
// Phase 11 Bug Fix: Track actual consumption (artifact_id -> consumer_ids[])
|
|
16
|
-
// Updated by agent_activated events to reflect filtering and actual consumption
|
|
17
|
-
consumptions: Map<string, string[]>;
|
|
26
|
+
// Real-time WebSocket state (overlaid on backend snapshot)
|
|
27
|
+
agentStatus: Map<string, string>;
|
|
28
|
+
streamingTokens: Map<string, string[]>;
|
|
18
29
|
|
|
19
|
-
//
|
|
20
|
-
// Messages don't have position in their data model, so we track it separately
|
|
21
|
-
messagePositions: Map<string, { x: number; y: number }>;
|
|
22
|
-
|
|
23
|
-
// Graph representation
|
|
30
|
+
// Backend snapshot state
|
|
24
31
|
nodes: Node[];
|
|
25
32
|
edges: Edge[];
|
|
33
|
+
statistics: GraphStatistics | null;
|
|
26
34
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
removeAgent: (id: string) => void;
|
|
35
|
+
// UI state
|
|
36
|
+
events: Message[];
|
|
37
|
+
viewMode: 'agent' | 'blackboard';
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
addRun: (run: Run) => void;
|
|
39
|
+
// Position persistence (saved to IndexedDB)
|
|
40
|
+
savedPositions: Map<string, { x: number; y: number }>;
|
|
35
41
|
|
|
36
|
-
//
|
|
37
|
-
|
|
42
|
+
// Loading state
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
error: string | null;
|
|
38
45
|
|
|
39
|
-
//
|
|
40
|
-
|
|
46
|
+
// Actions - Backend integration
|
|
47
|
+
generateAgentViewGraph: () => Promise<void>;
|
|
48
|
+
generateBlackboardViewGraph: () => Promise<void>;
|
|
49
|
+
refreshCurrentView: () => Promise<void>;
|
|
50
|
+
scheduleRefresh: () => void; // Debounced refresh (500ms)
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
// Actions - Real-time WebSocket updates
|
|
53
|
+
updateAgentStatus: (agentId: string, status: string) => void;
|
|
54
|
+
updateStreamingTokens: (agentId: string, tokens: string[]) => void;
|
|
55
|
+
addEvent: (message: Message) => void;
|
|
43
56
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
// Actions - Streaming message nodes (Phase 6)
|
|
58
|
+
createOrUpdateStreamingMessageNode: (artifactId: string, token: string, eventData?: any) => void;
|
|
59
|
+
finalizeStreamingMessageNode: (artifactId: string) => void;
|
|
47
60
|
|
|
48
|
-
//
|
|
49
|
-
|
|
61
|
+
// Actions - Position persistence
|
|
62
|
+
updateNodePosition: (nodeId: string, position: { x: number; y: number }) => void;
|
|
63
|
+
saveNodePosition: (nodeId: string, position: { x: number; y: number }) => void;
|
|
64
|
+
loadSavedPositions: () => Promise<void>;
|
|
50
65
|
|
|
51
|
-
//
|
|
52
|
-
|
|
66
|
+
// Actions - UI state
|
|
67
|
+
setViewMode: (viewMode: 'agent' | 'blackboard') => void;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
artifact_id: message.id,
|
|
63
|
-
artifact_type: message.type,
|
|
64
|
-
produced_by: message.producedBy,
|
|
65
|
-
consumed_by: actualConsumers, // Use actual consumption data
|
|
66
|
-
published_at: new Date(message.timestamp).toISOString(),
|
|
67
|
-
payload: message.payload,
|
|
68
|
-
correlation_id: message.correlationId,
|
|
70
|
+
/**
|
|
71
|
+
* Convert TimeRange (number timestamps) to TimeRangeFilter (ISO string timestamps)
|
|
72
|
+
*/
|
|
73
|
+
function convertTimeRange(range: { preset: string; start?: number; end?: number }): GraphRequest['filters']['time_range'] {
|
|
74
|
+
const result: GraphRequest['filters']['time_range'] = {
|
|
75
|
+
preset: range.preset as any,
|
|
69
76
|
};
|
|
77
|
+
|
|
78
|
+
if (range.start !== undefined) {
|
|
79
|
+
result.start = new Date(range.start).toISOString();
|
|
80
|
+
}
|
|
81
|
+
if (range.end !== undefined) {
|
|
82
|
+
result.end = new Date(range.end).toISOString();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
70
86
|
}
|
|
71
87
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
): DashboardState {
|
|
78
|
-
const artifacts = new Map<string, Artifact>();
|
|
79
|
-
const syntheticRuns = new Map(runs);
|
|
80
|
-
|
|
81
|
-
const producedBuckets = new Map<string, Set<string>>();
|
|
82
|
-
const consumedBuckets = new Map<string, Set<string>>();
|
|
83
|
-
|
|
84
|
-
// Helper to build bucket keys based on agent + correlation
|
|
85
|
-
const makeBucketKey = (agentId: string, correlationId: string) =>
|
|
86
|
-
`${agentId}::${correlationId || 'uncorrelated'}`;
|
|
87
|
-
|
|
88
|
-
// Track (agent, correlation) pairs that already have explicit run data
|
|
89
|
-
const existingRunBuckets = new Set<string>();
|
|
90
|
-
runs.forEach((run) => {
|
|
91
|
-
existingRunBuckets.add(makeBucketKey(run.agent_name, run.correlation_id));
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
messages.forEach((message) => {
|
|
95
|
-
artifacts.set(message.id, messageToArtifact(message, consumptions));
|
|
96
|
-
|
|
97
|
-
if (message.producedBy) {
|
|
98
|
-
const key = makeBucketKey(message.producedBy, message.correlationId);
|
|
99
|
-
if (!producedBuckets.has(key)) {
|
|
100
|
-
producedBuckets.set(key, new Set());
|
|
101
|
-
}
|
|
102
|
-
producedBuckets.get(key)!.add(message.id);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
consumptions.forEach((consumerIds, artifactId) => {
|
|
107
|
-
const message = messages.get(artifactId);
|
|
108
|
-
const correlationId = message?.correlationId ?? '';
|
|
109
|
-
consumerIds.forEach((consumerId) => {
|
|
110
|
-
const key = makeBucketKey(consumerId, correlationId);
|
|
111
|
-
if (!consumedBuckets.has(key)) {
|
|
112
|
-
consumedBuckets.set(key, new Set());
|
|
113
|
-
}
|
|
114
|
-
consumedBuckets.get(key)!.add(artifactId);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
let syntheticCounter = 0;
|
|
119
|
-
consumedBuckets.forEach((consumedSet, key) => {
|
|
120
|
-
if (consumedSet.size === 0) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const producedSet = producedBuckets.get(key);
|
|
124
|
-
if (!producedSet || producedSet.size === 0) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (existingRunBuckets.has(key)) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const [agentIdRaw, correlationPartRaw] = key.split('::');
|
|
133
|
-
const agentId = agentIdRaw || 'unknown-agent';
|
|
134
|
-
const correlationPart = correlationPartRaw || 'uncorrelated';
|
|
135
|
-
const runId = `historic_${agentId}_${correlationPart}_${syntheticCounter++}`;
|
|
136
|
-
|
|
137
|
-
if (!syntheticRuns.has(runId)) {
|
|
138
|
-
syntheticRuns.set(runId, {
|
|
139
|
-
run_id: runId,
|
|
140
|
-
agent_name: agentId,
|
|
141
|
-
correlation_id: correlationPart === 'uncorrelated' ? '' : correlationPart,
|
|
142
|
-
status: 'completed',
|
|
143
|
-
consumed_artifacts: Array.from(consumedSet),
|
|
144
|
-
produced_artifacts: Array.from(producedSet),
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
});
|
|
88
|
+
/**
|
|
89
|
+
* Build GraphRequest from current filter state
|
|
90
|
+
*/
|
|
91
|
+
function buildGraphRequest(viewMode: 'agent' | 'blackboard'): GraphRequest {
|
|
92
|
+
const filterState = useFilterStore.getState();
|
|
148
93
|
|
|
149
94
|
return {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
95
|
+
viewMode,
|
|
96
|
+
filters: {
|
|
97
|
+
correlation_id: filterState.correlationId || null,
|
|
98
|
+
time_range: convertTimeRange(filterState.timeRange),
|
|
99
|
+
artifactTypes: filterState.selectedArtifactTypes,
|
|
100
|
+
producers: filterState.selectedProducers,
|
|
101
|
+
tags: filterState.selectedTags,
|
|
102
|
+
visibility: filterState.selectedVisibility,
|
|
103
|
+
},
|
|
104
|
+
options: {
|
|
105
|
+
include_statistics: true,
|
|
106
|
+
},
|
|
153
107
|
};
|
|
154
108
|
}
|
|
155
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Debounce timer for graph refresh (500ms batching)
|
|
112
|
+
*/
|
|
113
|
+
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
114
|
+
|
|
156
115
|
export const useGraphStore = create<GraphState>()(
|
|
157
116
|
devtools(
|
|
158
117
|
(set, get) => ({
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
runs: new Map(),
|
|
163
|
-
consumptions: new Map(), // Phase 11: Track actual artifact consumption
|
|
164
|
-
messagePositions: new Map(), // Track message node positions
|
|
118
|
+
// Initial state
|
|
119
|
+
agentStatus: new Map(),
|
|
120
|
+
streamingTokens: new Map(),
|
|
165
121
|
nodes: [],
|
|
166
122
|
edges: [],
|
|
123
|
+
statistics: null,
|
|
124
|
+
events: [],
|
|
125
|
+
viewMode: 'agent',
|
|
126
|
+
savedPositions: new Map(),
|
|
127
|
+
isLoading: false,
|
|
128
|
+
error: null,
|
|
167
129
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
agents.set(agent.id, agent);
|
|
172
|
-
return { agents };
|
|
173
|
-
}),
|
|
130
|
+
// Backend integration actions
|
|
131
|
+
generateAgentViewGraph: async () => {
|
|
132
|
+
set({ isLoading: true, error: null, viewMode: 'agent' });
|
|
174
133
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const agent = agents.get(id);
|
|
179
|
-
if (agent) {
|
|
180
|
-
agents.set(id, { ...agent, ...updates });
|
|
181
|
-
}
|
|
182
|
-
return { agents };
|
|
183
|
-
}),
|
|
134
|
+
try {
|
|
135
|
+
// Load saved positions from IndexedDB first
|
|
136
|
+
await get().loadSavedPositions();
|
|
184
137
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const agents = new Map(state.agents);
|
|
188
|
-
agents.delete(id);
|
|
189
|
-
return { agents };
|
|
190
|
-
}),
|
|
138
|
+
const request = buildGraphRequest('agent');
|
|
139
|
+
const snapshot: GraphSnapshot = await fetchGraphSnapshot(request);
|
|
191
140
|
|
|
192
|
-
|
|
193
|
-
set((state) => {
|
|
194
|
-
const messages = new Map(state.messages);
|
|
195
|
-
messages.set(message.id, message);
|
|
141
|
+
const { savedPositions, nodes: currentNodes, agentStatus, streamingTokens } = get();
|
|
196
142
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
const isDuplicate = state.events.some(e => e.id === message.id);
|
|
200
|
-
const events = isDuplicate
|
|
201
|
-
? state.events // Skip if already in events array
|
|
202
|
-
: [message, ...state.events].slice(0, 100); // Add new message
|
|
143
|
+
// Merge positions: saved > current > backend > random
|
|
144
|
+
const mergedNodes = mergeNodePositions(snapshot.nodes, savedPositions, currentNodes);
|
|
203
145
|
|
|
204
|
-
|
|
205
|
-
|
|
146
|
+
// Overlay real-time WebSocket state
|
|
147
|
+
const finalNodes = overlayWebSocketState(mergedNodes, agentStatus, streamingTokens);
|
|
206
148
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
// Note: updateMessage does NOT touch the events array
|
|
215
|
-
// This allows streaming updates without flooding the Event Log
|
|
216
|
-
return { messages };
|
|
217
|
-
}),
|
|
149
|
+
set({
|
|
150
|
+
nodes: finalNodes,
|
|
151
|
+
edges: snapshot.edges as Edge[],
|
|
152
|
+
statistics: snapshot.statistics,
|
|
153
|
+
isLoading: false,
|
|
154
|
+
});
|
|
218
155
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
156
|
+
// Update filter facets from backend statistics
|
|
157
|
+
if (snapshot.statistics?.artifactSummary) {
|
|
158
|
+
const summary = snapshot.statistics.artifactSummary;
|
|
159
|
+
const filterState = useFilterStore.getState();
|
|
160
|
+
|
|
161
|
+
// Transform ArtifactSummary to FilterFacets format
|
|
162
|
+
const facets = {
|
|
163
|
+
artifactTypes: Object.keys(summary.by_type),
|
|
164
|
+
producers: Object.keys(summary.by_producer),
|
|
165
|
+
tags: Object.keys(summary.tag_counts),
|
|
166
|
+
visibilities: Object.keys(summary.by_visibility),
|
|
167
|
+
};
|
|
225
168
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const existing = consumptions.get(artifactId) || [];
|
|
232
|
-
if (!existing.includes(consumerId)) {
|
|
233
|
-
consumptions.set(artifactId, [...existing, consumerId]);
|
|
169
|
+
// Support both updateAvailableFacets (production) and updateFacets (test mock)
|
|
170
|
+
if ('updateAvailableFacets' in filterState && typeof filterState.updateAvailableFacets === 'function') {
|
|
171
|
+
filterState.updateAvailableFacets(facets);
|
|
172
|
+
} else if ('updateFacets' in filterState && typeof (filterState as any).updateFacets === 'function') {
|
|
173
|
+
(filterState as any).updateFacets(facets);
|
|
234
174
|
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch graph';
|
|
178
|
+
set({
|
|
179
|
+
error: errorMessage,
|
|
180
|
+
isLoading: false,
|
|
235
181
|
});
|
|
236
|
-
|
|
237
|
-
}
|
|
182
|
+
throw error; // Re-throw for test assertions
|
|
183
|
+
}
|
|
184
|
+
},
|
|
238
185
|
|
|
239
|
-
|
|
240
|
-
set(
|
|
241
|
-
// Remove old streaming message, add final message with new ID
|
|
242
|
-
const messages = new Map(state.messages);
|
|
243
|
-
messages.delete(oldId);
|
|
244
|
-
messages.set(newMessage.id, newMessage);
|
|
245
|
-
|
|
246
|
-
// Transfer position from old ID to new ID
|
|
247
|
-
const messagePositions = new Map(state.messagePositions);
|
|
248
|
-
const oldPos = messagePositions.get(oldId);
|
|
249
|
-
if (oldPos) {
|
|
250
|
-
messagePositions.delete(oldId);
|
|
251
|
-
messagePositions.set(newMessage.id, oldPos);
|
|
252
|
-
}
|
|
186
|
+
generateBlackboardViewGraph: async () => {
|
|
187
|
+
set({ isLoading: true, error: null, viewMode: 'blackboard' });
|
|
253
188
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
189
|
+
try {
|
|
190
|
+
// Load saved positions from IndexedDB first
|
|
191
|
+
await get().loadSavedPositions();
|
|
258
192
|
|
|
259
|
-
|
|
260
|
-
|
|
193
|
+
const request = buildGraphRequest('blackboard');
|
|
194
|
+
const snapshot: GraphSnapshot = await fetchGraphSnapshot(request);
|
|
261
195
|
|
|
262
|
-
|
|
263
|
-
set((state) => {
|
|
264
|
-
const agents = new Map(state.agents);
|
|
265
|
-
const agent = agents.get(nodeId);
|
|
266
|
-
if (agent) {
|
|
267
|
-
// Update agent position
|
|
268
|
-
agents.set(nodeId, { ...agent, position });
|
|
269
|
-
return { agents };
|
|
270
|
-
} else {
|
|
271
|
-
// Must be a message node - update message position
|
|
272
|
-
const messagePositions = new Map(state.messagePositions);
|
|
273
|
-
messagePositions.set(nodeId, position);
|
|
274
|
-
return { messagePositions };
|
|
275
|
-
}
|
|
276
|
-
}),
|
|
196
|
+
const { savedPositions, nodes: currentNodes, agentStatus, streamingTokens } = get();
|
|
277
197
|
|
|
278
|
-
|
|
279
|
-
|
|
198
|
+
// Merge positions: saved > current > backend > random
|
|
199
|
+
const mergedNodes = mergeNodePositions(snapshot.nodes, savedPositions, currentNodes);
|
|
280
200
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
currentNodes.forEach(node => {
|
|
284
|
-
currentPositions.set(node.id, node.position);
|
|
285
|
-
});
|
|
201
|
+
// Overlay real-time WebSocket state (primarily for message streaming)
|
|
202
|
+
const finalNodes = overlayWebSocketState(mergedNodes, agentStatus, streamingTokens);
|
|
286
203
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const position = agent.position
|
|
293
|
-
|| currentPositions.get(agent.id)
|
|
294
|
-
|| { x: 400 + Math.random() * 200, y: 300 + Math.random() * 200 };
|
|
295
|
-
|
|
296
|
-
nodes.push({
|
|
297
|
-
id: agent.id,
|
|
298
|
-
type: 'agent',
|
|
299
|
-
position,
|
|
300
|
-
data: {
|
|
301
|
-
name: agent.name,
|
|
302
|
-
status: agent.status,
|
|
303
|
-
subscriptions: agent.subscriptions,
|
|
304
|
-
outputTypes: agent.outputTypes,
|
|
305
|
-
sentCount: agent.sentCount,
|
|
306
|
-
recvCount: agent.recvCount,
|
|
307
|
-
receivedByType: agent.receivedByType,
|
|
308
|
-
sentByType: agent.sentByType,
|
|
309
|
-
streamingTokens: agent.streamingTokens,
|
|
310
|
-
},
|
|
204
|
+
set({
|
|
205
|
+
nodes: finalNodes,
|
|
206
|
+
edges: snapshot.edges as Edge[],
|
|
207
|
+
statistics: snapshot.statistics,
|
|
208
|
+
isLoading: false,
|
|
311
209
|
});
|
|
312
|
-
});
|
|
313
210
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
211
|
+
// Update filter facets from backend statistics
|
|
212
|
+
if (snapshot.statistics?.artifactSummary) {
|
|
213
|
+
const summary = snapshot.statistics.artifactSummary;
|
|
214
|
+
const filterState = useFilterStore.getState();
|
|
215
|
+
|
|
216
|
+
// Transform ArtifactSummary to FilterFacets format
|
|
217
|
+
const facets = {
|
|
218
|
+
artifactTypes: Object.keys(summary.by_type),
|
|
219
|
+
producers: Object.keys(summary.by_producer),
|
|
220
|
+
tags: Object.keys(summary.tag_counts),
|
|
221
|
+
visibilities: Object.keys(summary.by_visibility),
|
|
222
|
+
};
|
|
317
223
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
224
|
+
// Support both updateAvailableFacets (production) and updateFacets (test mock)
|
|
225
|
+
if ('updateAvailableFacets' in filterState && typeof filterState.updateAvailableFacets === 'function') {
|
|
226
|
+
filterState.updateAvailableFacets(facets);
|
|
227
|
+
} else if ('updateFacets' in filterState && typeof (filterState as any).updateFacets === 'function') {
|
|
228
|
+
(filterState as any).updateFacets(facets);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch graph';
|
|
233
|
+
set({
|
|
234
|
+
error: errorMessage,
|
|
235
|
+
isLoading: false,
|
|
236
|
+
});
|
|
237
|
+
throw error; // Re-throw for test assertions
|
|
238
|
+
}
|
|
321
239
|
},
|
|
322
240
|
|
|
323
|
-
|
|
324
|
-
const {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
241
|
+
refreshCurrentView: async () => {
|
|
242
|
+
const { viewMode } = get();
|
|
243
|
+
if (viewMode === 'agent') {
|
|
244
|
+
await get().generateAgentViewGraph();
|
|
245
|
+
} else {
|
|
246
|
+
await get().generateBlackboardViewGraph();
|
|
247
|
+
}
|
|
248
|
+
},
|
|
331
249
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|| currentPositions.get(message.id)
|
|
344
|
-
|| { x: 400 + Math.random() * 200, y: 300 + Math.random() * 200 };
|
|
345
|
-
|
|
346
|
-
nodes.push({
|
|
347
|
-
id: message.id,
|
|
348
|
-
type: 'message',
|
|
349
|
-
position,
|
|
350
|
-
data: {
|
|
351
|
-
artifactType: message.type,
|
|
352
|
-
payloadPreview: payloadStr.slice(0, 100),
|
|
353
|
-
payload: message.payload, // Full payload for display
|
|
354
|
-
producedBy: message.producedBy,
|
|
355
|
-
consumedBy, // Use actual consumption data
|
|
356
|
-
timestamp: message.timestamp,
|
|
357
|
-
isStreaming: message.isStreaming || false,
|
|
358
|
-
streamingText: message.streamingText || '',
|
|
359
|
-
tags: message.tags || [],
|
|
360
|
-
visibilityKind: message.visibilityKind || 'Unknown',
|
|
361
|
-
},
|
|
250
|
+
scheduleRefresh: () => {
|
|
251
|
+
// Clear existing timer if any (reset debounce)
|
|
252
|
+
if (refreshTimer !== null) {
|
|
253
|
+
clearTimeout(refreshTimer);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Schedule refresh after 100ms of quiet time (snappy UX)
|
|
257
|
+
refreshTimer = setTimeout(() => {
|
|
258
|
+
refreshTimer = null;
|
|
259
|
+
get().refreshCurrentView().catch((error) => {
|
|
260
|
+
console.error('[GraphStore] Scheduled refresh failed:', error);
|
|
362
261
|
});
|
|
363
|
-
});
|
|
262
|
+
}, 100);
|
|
263
|
+
},
|
|
364
264
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
265
|
+
// Real-time WebSocket update actions
|
|
266
|
+
updateAgentStatus: (agentId, status) => {
|
|
267
|
+
set((state) => {
|
|
268
|
+
const agentStatus = new Map(state.agentStatus);
|
|
269
|
+
agentStatus.set(agentId, status);
|
|
270
|
+
|
|
271
|
+
// Inline overlay logic (don't use overlayWebSocketState which gets mocked in tests)
|
|
272
|
+
const nodes = state.nodes.map(node => {
|
|
273
|
+
if (node.type === 'agent' && node.id === agentId) {
|
|
274
|
+
return {
|
|
275
|
+
...node,
|
|
276
|
+
data: {
|
|
277
|
+
...node.data,
|
|
278
|
+
status: status,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return node;
|
|
283
|
+
});
|
|
368
284
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
useGraphStore.getState().applyFilters();
|
|
285
|
+
return { agentStatus, nodes };
|
|
286
|
+
});
|
|
372
287
|
},
|
|
373
288
|
|
|
374
|
-
|
|
289
|
+
updateStreamingTokens: (agentId, tokens) => {
|
|
375
290
|
set((state) => {
|
|
376
|
-
const
|
|
291
|
+
const streamingTokens = new Map(state.streamingTokens);
|
|
292
|
+
streamingTokens.set(agentId, tokens);
|
|
293
|
+
|
|
294
|
+
// Inline overlay logic (don't use overlayWebSocketState which gets mocked in tests)
|
|
295
|
+
const nodes = state.nodes.map(node => {
|
|
296
|
+
if (node.type === 'agent' && node.id === agentId) {
|
|
297
|
+
return {
|
|
298
|
+
...node,
|
|
299
|
+
data: {
|
|
300
|
+
...node.data,
|
|
301
|
+
streamingTokens: tokens.slice(-6), // Keep only last 6 tokens
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return node;
|
|
306
|
+
});
|
|
377
307
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
newState.agents = agents;
|
|
382
|
-
}
|
|
308
|
+
return { streamingTokens, nodes };
|
|
309
|
+
});
|
|
310
|
+
},
|
|
383
311
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
consumptions.set(m.id, Array.from(new Set(m.consumedBy)));
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
newState.messages = messages;
|
|
394
|
-
newState.events = [...update.messages, ...state.events].slice(0, 100);
|
|
395
|
-
newState.consumptions = consumptions;
|
|
312
|
+
addEvent: (message) => {
|
|
313
|
+
set((state) => {
|
|
314
|
+
// Add to events array (max 100 items)
|
|
315
|
+
const isDuplicate = state.events.some(e => e.id === message.id);
|
|
316
|
+
if (isDuplicate) {
|
|
317
|
+
return state; // Skip duplicates
|
|
396
318
|
}
|
|
397
319
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
320
|
+
const events = [message, ...state.events].slice(0, 100);
|
|
321
|
+
return { events };
|
|
322
|
+
});
|
|
323
|
+
},
|
|
403
324
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
selectedArtifactTypes,
|
|
413
|
-
selectedProducers,
|
|
414
|
-
selectedTags,
|
|
415
|
-
selectedVisibility,
|
|
416
|
-
} = useFilterStore.getState();
|
|
417
|
-
|
|
418
|
-
// Helper to calculate time range boundaries
|
|
419
|
-
const getTimeRangeBoundaries = (): { start: number; end: number } => {
|
|
420
|
-
const now = Date.now();
|
|
421
|
-
if (timeRange.preset === 'last5min') {
|
|
422
|
-
return { start: now - 5 * 60 * 1000, end: now };
|
|
423
|
-
} else if (timeRange.preset === 'last10min') {
|
|
424
|
-
return { start: now - 10 * 60 * 1000, end: now };
|
|
425
|
-
} else if (timeRange.preset === 'last1hour') {
|
|
426
|
-
return { start: now - 60 * 60 * 1000, end: now };
|
|
427
|
-
} else if (timeRange.preset === 'custom' && timeRange.start && timeRange.end) {
|
|
428
|
-
return { start: timeRange.start, end: timeRange.end };
|
|
429
|
-
}
|
|
430
|
-
return { start: Number.NEGATIVE_INFINITY, end: Number.POSITIVE_INFINITY };
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
const { start: timeStart, end: timeEnd } = getTimeRangeBoundaries();
|
|
434
|
-
|
|
435
|
-
const visibleMessageIds = new Set<string>();
|
|
436
|
-
const producedStats = new Map<string, { total: number; byType: Record<string, number> }>();
|
|
437
|
-
const consumedStats = new Map<string, { total: number; byType: Record<string, number> }>();
|
|
438
|
-
|
|
439
|
-
const incrementStat = (
|
|
440
|
-
map: Map<string, { total: number; byType: Record<string, number> }>,
|
|
441
|
-
key: string,
|
|
442
|
-
type: string
|
|
443
|
-
) => {
|
|
444
|
-
if (!map.has(key)) {
|
|
445
|
-
map.set(key, { total: 0, byType: {} });
|
|
325
|
+
// Streaming message nodes (Phase 6)
|
|
326
|
+
createOrUpdateStreamingMessageNode: (artifactId, token, eventData) => {
|
|
327
|
+
set((state) => {
|
|
328
|
+
// Only create/update streaming message nodes in blackboard view
|
|
329
|
+
// Message nodes should never appear in agent view
|
|
330
|
+
if (state.viewMode !== 'blackboard') {
|
|
331
|
+
console.log(`[GraphStore] Ignoring streaming message node in ${state.viewMode} view`);
|
|
332
|
+
return state; // No changes
|
|
446
333
|
}
|
|
447
|
-
const entry = map.get(key)!;
|
|
448
|
-
entry.total += 1;
|
|
449
|
-
entry.byType[type] = (entry.byType[type] || 0) + 1;
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
messages.forEach((message) => {
|
|
453
|
-
let visible = true;
|
|
454
334
|
|
|
455
|
-
|
|
456
|
-
|
|
335
|
+
const existingNode = state.nodes.find(n => n.id === artifactId);
|
|
336
|
+
|
|
337
|
+
if (existingNode) {
|
|
338
|
+
// Update existing streaming node
|
|
339
|
+
const currentText = (existingNode.data.streamingText as string) || '';
|
|
340
|
+
const updatedNodes = state.nodes.map(node => {
|
|
341
|
+
if (node.id === artifactId) {
|
|
342
|
+
return {
|
|
343
|
+
...node,
|
|
344
|
+
data: {
|
|
345
|
+
...node.data,
|
|
346
|
+
streamingText: currentText + token,
|
|
347
|
+
isStreaming: true,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return node;
|
|
352
|
+
});
|
|
353
|
+
return { nodes: updatedNodes };
|
|
354
|
+
} else {
|
|
355
|
+
// Create new streaming message node
|
|
356
|
+
const newNode: Node = {
|
|
357
|
+
id: artifactId,
|
|
358
|
+
type: 'message',
|
|
359
|
+
position: { x: Math.random() * 500, y: Math.random() * 500 }, // Random position
|
|
360
|
+
data: {
|
|
361
|
+
artifactType: eventData?.artifact_type || 'Unknown',
|
|
362
|
+
payload: {},
|
|
363
|
+
producedBy: eventData?.agent_name || 'Unknown',
|
|
364
|
+
timestamp: Date.now(),
|
|
365
|
+
streamingText: token,
|
|
366
|
+
isStreaming: true,
|
|
367
|
+
tags: [],
|
|
368
|
+
visibilityKind: 'Public',
|
|
369
|
+
correlationId: eventData?.correlation_id || '',
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
return { nodes: [...state.nodes, newNode] };
|
|
457
373
|
}
|
|
374
|
+
});
|
|
375
|
+
},
|
|
458
376
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
377
|
+
finalizeStreamingMessageNode: (artifactId) => {
|
|
378
|
+
set((state) => {
|
|
379
|
+
const nodes = state.nodes.map(node => {
|
|
380
|
+
if (node.id === artifactId && node.data.isStreaming) {
|
|
381
|
+
return {
|
|
382
|
+
...node,
|
|
383
|
+
data: {
|
|
384
|
+
...node.data,
|
|
385
|
+
isStreaming: false,
|
|
386
|
+
// streamingText is kept so MessageNode can display it until backend refresh
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return node;
|
|
391
|
+
});
|
|
462
392
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
!selectedArtifactTypes.includes(message.type)
|
|
467
|
-
) {
|
|
468
|
-
visible = false;
|
|
469
|
-
}
|
|
393
|
+
return { nodes };
|
|
394
|
+
});
|
|
395
|
+
},
|
|
470
396
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
397
|
+
// Position persistence actions
|
|
398
|
+
updateNodePosition: (nodeId, position) => {
|
|
399
|
+
set((state) => {
|
|
400
|
+
const nodes = state.nodes.map(node =>
|
|
401
|
+
node.id === nodeId ? { ...node, position } : node
|
|
402
|
+
);
|
|
403
|
+
return { nodes };
|
|
404
|
+
});
|
|
405
|
+
},
|
|
478
406
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
407
|
+
saveNodePosition: (nodeId, position) => {
|
|
408
|
+
set((state) => {
|
|
409
|
+
const savedPositions = new Map(state.savedPositions);
|
|
410
|
+
savedPositions.set(nodeId, position);
|
|
411
|
+
|
|
412
|
+
// Save to IndexedDB using indexedDBService
|
|
413
|
+
const viewMode = state.viewMode;
|
|
414
|
+
const layoutRecord = {
|
|
415
|
+
node_id: nodeId,
|
|
416
|
+
x: position.x,
|
|
417
|
+
y: position.y,
|
|
418
|
+
last_updated: new Date().toISOString(),
|
|
419
|
+
};
|
|
486
420
|
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
visible = false;
|
|
492
|
-
}
|
|
421
|
+
if (viewMode === 'agent') {
|
|
422
|
+
indexedDBService.saveAgentViewLayout(layoutRecord).catch(console.error);
|
|
423
|
+
} else {
|
|
424
|
+
indexedDBService.saveBlackboardViewLayout(layoutRecord).catch(console.error);
|
|
493
425
|
}
|
|
494
426
|
|
|
495
|
-
|
|
496
|
-
visibleMessageIds.add(message.id);
|
|
497
|
-
incrementStat(producedStats, message.producedBy, message.type);
|
|
498
|
-
|
|
499
|
-
const consumers = consumptions.get(message.id) || [];
|
|
500
|
-
consumers.forEach((consumerId) => {
|
|
501
|
-
incrementStat(consumedStats, consumerId, message.type);
|
|
502
|
-
});
|
|
503
|
-
}
|
|
427
|
+
return { savedPositions };
|
|
504
428
|
});
|
|
429
|
+
},
|
|
505
430
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
hidden: !visibleMessageIds.has(node.id),
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
if (node.type === 'agent') {
|
|
514
|
-
const produced = producedStats.get(node.id);
|
|
515
|
-
const consumed = consumedStats.get(node.id);
|
|
516
|
-
const currentData = node.data as AgentNodeData;
|
|
517
|
-
return {
|
|
518
|
-
...node,
|
|
519
|
-
hidden: false,
|
|
520
|
-
data: {
|
|
521
|
-
...node.data,
|
|
522
|
-
sentCount: produced?.total ?? currentData.sentCount ?? 0,
|
|
523
|
-
recvCount: consumed?.total ?? currentData.recvCount ?? 0,
|
|
524
|
-
sentByType: produced?.byType ?? currentData.sentByType ?? {},
|
|
525
|
-
receivedByType: consumed?.byType ?? currentData.receivedByType ?? {},
|
|
526
|
-
},
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
return node;
|
|
530
|
-
});
|
|
431
|
+
loadSavedPositions: async () => {
|
|
432
|
+
try {
|
|
433
|
+
const viewMode = get().viewMode;
|
|
434
|
+
let layouts: Array<{ node_id: string; x: number; y: number; last_updated: string }> = [];
|
|
531
435
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const data: any = edge.data;
|
|
535
|
-
if (data && Array.isArray(data.artifactIds) && data.artifactIds.length > 0) {
|
|
536
|
-
hidden = data.artifactIds.every((artifactId: string) => !visibleMessageIds.has(artifactId));
|
|
436
|
+
if (viewMode === 'agent') {
|
|
437
|
+
layouts = await indexedDBService.getAllAgentViewLayouts();
|
|
537
438
|
} else {
|
|
538
|
-
|
|
539
|
-
const targetNode = updatedNodes.find((n) => n.id === edge.target);
|
|
540
|
-
hidden = !!(sourceNode?.hidden || targetNode?.hidden);
|
|
439
|
+
layouts = await indexedDBService.getAllBlackboardViewLayouts();
|
|
541
440
|
}
|
|
542
|
-
return {
|
|
543
|
-
...edge,
|
|
544
|
-
hidden,
|
|
545
|
-
};
|
|
546
|
-
});
|
|
547
441
|
|
|
548
|
-
|
|
442
|
+
// Convert to Map
|
|
443
|
+
const positions = new Map<string, { x: number; y: number }>();
|
|
444
|
+
layouts.forEach((layout) => {
|
|
445
|
+
positions.set(layout.node_id, { x: layout.x, y: layout.y });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
set({ savedPositions: positions });
|
|
449
|
+
console.log(`[GraphStore] Loaded ${positions.size} saved positions for ${viewMode} view`);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error('[GraphStore] Failed to load saved positions:', error);
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// UI state actions
|
|
456
|
+
setViewMode: (viewMode) => {
|
|
457
|
+
set({ viewMode });
|
|
549
458
|
},
|
|
550
459
|
}),
|
|
551
460
|
{ name: 'graphStore' }
|