flock-core 0.5.0b65__py3-none-any.whl → 0.5.0b70__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/cli.py +74 -2
- flock/engines/dspy_engine.py +40 -4
- flock/examples.py +4 -1
- flock/frontend/README.md +15 -1
- flock/frontend/package-lock.json +2 -2
- flock/frontend/package.json +1 -1
- flock/frontend/src/App.tsx +74 -6
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +4 -5
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +7 -3
- flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
- flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
- flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
- flock/frontend/src/components/filters/FilterPills.module.css +186 -45
- flock/frontend/src/components/filters/FilterPills.test.tsx +115 -99
- flock/frontend/src/components/filters/FilterPills.tsx +120 -44
- flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
- flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
- flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
- flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
- flock/frontend/src/components/filters/TagFilter.tsx +21 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +24 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +6 -1
- flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +24 -0
- flock/frontend/src/components/layout/DashboardLayout.css +13 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +8 -24
- flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +460 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +7 -1
- flock/frontend/src/components/modules/registerModules.ts +9 -10
- flock/frontend/src/hooks/useModules.ts +11 -1
- flock/frontend/src/services/api.ts +140 -0
- flock/frontend/src/services/indexeddb.ts +56 -2
- flock/frontend/src/services/websocket.ts +129 -0
- flock/frontend/src/store/filterStore.test.ts +105 -185
- flock/frontend/src/store/filterStore.ts +173 -26
- flock/frontend/src/store/graphStore.test.ts +19 -0
- flock/frontend/src/store/graphStore.ts +166 -27
- flock/frontend/src/types/filters.ts +34 -1
- flock/frontend/src/types/graph.ts +7 -0
- flock/frontend/src/utils/artifacts.ts +24 -0
- flock/orchestrator.py +23 -1
- flock/service.py +146 -9
- flock/store.py +971 -24
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/METADATA +26 -1
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/RECORD +50 -43
- flock/frontend/src/components/filters/FilterBar.module.css +0 -29
- flock/frontend/src/components/filters/FilterBar.test.tsx +0 -133
- flock/frontend/src/components/filters/FilterBar.tsx +0 -33
- flock/frontend/src/components/modules/EventLogModule.test.tsx +0 -401
- flock/frontend/src/components/modules/EventLogModule.tsx +0 -396
- flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +0 -17
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b70.dist-info}/licenses/LICENSE +0 -0
|
@@ -76,14 +76,79 @@ function toDashboardState(
|
|
|
76
76
|
consumptions: Map<string, string[]>
|
|
77
77
|
): DashboardState {
|
|
78
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
|
+
});
|
|
79
93
|
|
|
80
94
|
messages.forEach((message) => {
|
|
81
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
|
+
}
|
|
82
147
|
});
|
|
83
148
|
|
|
84
149
|
return {
|
|
85
150
|
artifacts,
|
|
86
|
-
runs,
|
|
151
|
+
runs: syntheticRuns,
|
|
87
152
|
consumptions, // Phase 11: Pass actual consumption data for filtered count calculation
|
|
88
153
|
};
|
|
89
154
|
}
|
|
@@ -251,6 +316,8 @@ export const useGraphStore = create<GraphState>()(
|
|
|
251
316
|
const edges = deriveAgentViewEdges(dashboardState);
|
|
252
317
|
|
|
253
318
|
set({ nodes, edges });
|
|
319
|
+
// Re-apply active filters so newly generated nodes respect current selections
|
|
320
|
+
useGraphStore.getState().applyFilters();
|
|
254
321
|
},
|
|
255
322
|
|
|
256
323
|
generateBlackboardViewGraph: () => {
|
|
@@ -289,6 +356,8 @@ export const useGraphStore = create<GraphState>()(
|
|
|
289
356
|
timestamp: message.timestamp,
|
|
290
357
|
isStreaming: message.isStreaming || false,
|
|
291
358
|
streamingText: message.streamingText || '',
|
|
359
|
+
tags: message.tags || [],
|
|
360
|
+
visibilityKind: message.visibilityKind || 'Unknown',
|
|
292
361
|
},
|
|
293
362
|
});
|
|
294
363
|
});
|
|
@@ -298,6 +367,8 @@ export const useGraphStore = create<GraphState>()(
|
|
|
298
367
|
const edges = deriveBlackboardViewEdges(dashboardState);
|
|
299
368
|
|
|
300
369
|
set({ nodes, edges });
|
|
370
|
+
// Ensure filters are reapplied after regeneration
|
|
371
|
+
useGraphStore.getState().applyFilters();
|
|
301
372
|
},
|
|
302
373
|
|
|
303
374
|
batchUpdate: (update) =>
|
|
@@ -312,9 +383,16 @@ export const useGraphStore = create<GraphState>()(
|
|
|
312
383
|
|
|
313
384
|
if (update.messages) {
|
|
314
385
|
const messages = new Map(state.messages);
|
|
315
|
-
|
|
386
|
+
const consumptions = new Map(state.consumptions);
|
|
387
|
+
update.messages.forEach((m) => {
|
|
388
|
+
messages.set(m.id, m);
|
|
389
|
+
if (m.consumedBy && m.consumedBy.length > 0) {
|
|
390
|
+
consumptions.set(m.id, Array.from(new Set(m.consumedBy)));
|
|
391
|
+
}
|
|
392
|
+
});
|
|
316
393
|
newState.messages = messages;
|
|
317
394
|
newState.events = [...update.messages, ...state.events].slice(0, 100);
|
|
395
|
+
newState.consumptions = consumptions;
|
|
318
396
|
}
|
|
319
397
|
|
|
320
398
|
if (update.runs) {
|
|
@@ -327,8 +405,15 @@ export const useGraphStore = create<GraphState>()(
|
|
|
327
405
|
}),
|
|
328
406
|
|
|
329
407
|
applyFilters: () => {
|
|
330
|
-
const { nodes, edges, messages } = get();
|
|
331
|
-
const {
|
|
408
|
+
const { nodes, edges, messages, consumptions } = get();
|
|
409
|
+
const {
|
|
410
|
+
correlationId,
|
|
411
|
+
timeRange,
|
|
412
|
+
selectedArtifactTypes,
|
|
413
|
+
selectedProducers,
|
|
414
|
+
selectedTags,
|
|
415
|
+
selectedVisibility,
|
|
416
|
+
} = useFilterStore.getState();
|
|
332
417
|
|
|
333
418
|
// Helper to calculate time range boundaries
|
|
334
419
|
const getTimeRangeBoundaries = (): { start: number; end: number } => {
|
|
@@ -342,64 +427,118 @@ export const useGraphStore = create<GraphState>()(
|
|
|
342
427
|
} else if (timeRange.preset === 'custom' && timeRange.start && timeRange.end) {
|
|
343
428
|
return { start: timeRange.start, end: timeRange.end };
|
|
344
429
|
}
|
|
345
|
-
return { start:
|
|
430
|
+
return { start: Number.NEGATIVE_INFINITY, end: Number.POSITIVE_INFINITY };
|
|
346
431
|
};
|
|
347
432
|
|
|
348
433
|
const { start: timeStart, end: timeEnd } = getTimeRangeBoundaries();
|
|
349
434
|
|
|
350
|
-
// Filter messages based on correlation ID and time range
|
|
351
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: {} });
|
|
446
|
+
}
|
|
447
|
+
const entry = map.get(key)!;
|
|
448
|
+
entry.total += 1;
|
|
449
|
+
entry.byType[type] = (entry.byType[type] || 0) + 1;
|
|
450
|
+
};
|
|
451
|
+
|
|
352
452
|
messages.forEach((message) => {
|
|
353
453
|
let visible = true;
|
|
354
454
|
|
|
355
|
-
// Apply correlation ID filter (selective)
|
|
356
455
|
if (correlationId && message.correlationId !== correlationId) {
|
|
357
456
|
visible = false;
|
|
358
457
|
}
|
|
359
458
|
|
|
360
|
-
// Apply time range filter (in-memory)
|
|
361
459
|
if (visible && (message.timestamp < timeStart || message.timestamp > timeEnd)) {
|
|
362
460
|
visible = false;
|
|
363
461
|
}
|
|
364
462
|
|
|
463
|
+
if (
|
|
464
|
+
visible &&
|
|
465
|
+
selectedArtifactTypes.length > 0 &&
|
|
466
|
+
!selectedArtifactTypes.includes(message.type)
|
|
467
|
+
) {
|
|
468
|
+
visible = false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (
|
|
472
|
+
visible &&
|
|
473
|
+
selectedProducers.length > 0 &&
|
|
474
|
+
!selectedProducers.includes(message.producedBy)
|
|
475
|
+
) {
|
|
476
|
+
visible = false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (
|
|
480
|
+
visible &&
|
|
481
|
+
selectedVisibility.length > 0 &&
|
|
482
|
+
!selectedVisibility.includes(message.visibilityKind || 'Unknown')
|
|
483
|
+
) {
|
|
484
|
+
visible = false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (visible && selectedTags.length > 0) {
|
|
488
|
+
const messageTags = message.tags || [];
|
|
489
|
+
const hasAllTags = selectedTags.every((tag) => messageTags.includes(tag));
|
|
490
|
+
if (!hasAllTags) {
|
|
491
|
+
visible = false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
365
495
|
if (visible) {
|
|
366
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
|
+
});
|
|
367
503
|
}
|
|
368
504
|
});
|
|
369
505
|
|
|
370
|
-
// Update nodes visibility
|
|
371
506
|
const updatedNodes = nodes.map((node) => {
|
|
372
507
|
if (node.type === 'message') {
|
|
373
|
-
// For message nodes, check if message is visible
|
|
374
508
|
return {
|
|
375
509
|
...node,
|
|
376
510
|
hidden: !visibleMessageIds.has(node.id),
|
|
377
511
|
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (message.producedBy === node.id) {
|
|
384
|
-
hasVisibleMessages = true;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
});
|
|
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;
|
|
388
517
|
return {
|
|
389
518
|
...node,
|
|
390
|
-
hidden:
|
|
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
|
+
},
|
|
391
527
|
};
|
|
392
528
|
}
|
|
393
529
|
return node;
|
|
394
530
|
});
|
|
395
531
|
|
|
396
|
-
// Update edges visibility
|
|
397
532
|
const updatedEdges = edges.map((edge) => {
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
533
|
+
let hidden = edge.hidden ?? false;
|
|
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));
|
|
537
|
+
} else {
|
|
538
|
+
const sourceNode = updatedNodes.find((n) => n.id === edge.source);
|
|
539
|
+
const targetNode = updatedNodes.find((n) => n.id === edge.target);
|
|
540
|
+
hidden = !!(sourceNode?.hidden || targetNode?.hidden);
|
|
541
|
+
}
|
|
403
542
|
return {
|
|
404
543
|
...edge,
|
|
405
544
|
hidden,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TimeRangePreset = 'last5min' | 'last10min' | 'last1hour' | 'custom';
|
|
1
|
+
export type TimeRangePreset = 'last5min' | 'last10min' | 'last1hour' | 'custom' | 'all';
|
|
2
2
|
|
|
3
3
|
export interface TimeRange {
|
|
4
4
|
preset: TimeRangePreset;
|
|
@@ -12,3 +12,36 @@ export interface CorrelationIdMetadata {
|
|
|
12
12
|
artifact_count: number;
|
|
13
13
|
run_count: number;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export interface FilterFacets {
|
|
17
|
+
artifactTypes: string[];
|
|
18
|
+
producers: string[];
|
|
19
|
+
tags: string[];
|
|
20
|
+
visibilities: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ArtifactSummary {
|
|
24
|
+
total: number;
|
|
25
|
+
by_type: Record<string, number>;
|
|
26
|
+
by_producer: Record<string, number>;
|
|
27
|
+
by_visibility: Record<string, number>;
|
|
28
|
+
tag_counts: Record<string, number>;
|
|
29
|
+
earliest_created_at: string | null;
|
|
30
|
+
latest_created_at: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FilterSnapshot {
|
|
34
|
+
correlationId: string | null;
|
|
35
|
+
timeRange: TimeRange;
|
|
36
|
+
artifactTypes: string[];
|
|
37
|
+
producers: string[];
|
|
38
|
+
tags: string[];
|
|
39
|
+
visibility: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SavedFilterMeta {
|
|
43
|
+
filter_id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
created_at: number;
|
|
46
|
+
filters: FilterSnapshot;
|
|
47
|
+
}
|
|
@@ -22,8 +22,13 @@ export interface Message {
|
|
|
22
22
|
timestamp: number;
|
|
23
23
|
correlationId: string;
|
|
24
24
|
producedBy: string;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
visibilityKind?: string;
|
|
27
|
+
partitionKey?: string | null;
|
|
28
|
+
version?: number;
|
|
25
29
|
isStreaming?: boolean; // True while streaming, false when complete
|
|
26
30
|
streamingText?: string; // Accumulated streaming text (raw)
|
|
31
|
+
consumedBy?: string[];
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export interface AgentNodeData extends Record<string, unknown> {
|
|
@@ -47,6 +52,8 @@ export interface MessageNodeData extends Record<string, unknown> {
|
|
|
47
52
|
timestamp: number;
|
|
48
53
|
isStreaming?: boolean; // True while streaming tokens
|
|
49
54
|
streamingText?: string; // Raw streaming text
|
|
55
|
+
tags?: string[];
|
|
56
|
+
visibilityKind?: string;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export type AgentViewNode = Node<AgentNodeData, 'agent'>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ArtifactListItem } from '../services/api';
|
|
2
|
+
import type { Message } from '../types/graph';
|
|
3
|
+
|
|
4
|
+
export function mapArtifactToMessage(item: ArtifactListItem): Message {
|
|
5
|
+
const consumedBy =
|
|
6
|
+
item.consumed_by ??
|
|
7
|
+
(item.consumptions ? Array.from(new Set(item.consumptions.map((record) => record.consumer))) : []);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
id: item.id,
|
|
11
|
+
type: item.type,
|
|
12
|
+
payload: item.payload,
|
|
13
|
+
timestamp: new Date(item.created_at).getTime(),
|
|
14
|
+
correlationId: item.correlation_id ?? '',
|
|
15
|
+
producedBy: item.produced_by,
|
|
16
|
+
tags: item.tags || [],
|
|
17
|
+
visibilityKind: item.visibility_kind || item.visibility?.kind || 'Unknown',
|
|
18
|
+
partitionKey: item.partition_key ?? null,
|
|
19
|
+
version: item.version ?? 1,
|
|
20
|
+
isStreaming: false,
|
|
21
|
+
streamingText: '',
|
|
22
|
+
consumedBy,
|
|
23
|
+
};
|
|
24
|
+
}
|
flock/orchestrator.py
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import logging
|
|
6
7
|
import os
|
|
7
8
|
from asyncio import Task
|
|
8
9
|
from collections.abc import Iterable, Mapping, Sequence
|
|
9
10
|
from contextlib import asynccontextmanager
|
|
11
|
+
from datetime import datetime, timezone
|
|
10
12
|
from typing import TYPE_CHECKING, Any, AsyncGenerator
|
|
11
13
|
from uuid import uuid4
|
|
12
14
|
|
|
@@ -27,7 +29,7 @@ from flock.mcp import (
|
|
|
27
29
|
)
|
|
28
30
|
from flock.registry import type_registry
|
|
29
31
|
from flock.runtime import Context
|
|
30
|
-
from flock.store import BlackboardStore, InMemoryBlackboardStore
|
|
32
|
+
from flock.store import BlackboardStore, ConsumptionRecord, InMemoryBlackboardStore
|
|
31
33
|
from flock.visibility import AgentIdentity, PublicVisibility, Visibility
|
|
32
34
|
|
|
33
35
|
|
|
@@ -110,6 +112,7 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
110
112
|
... )
|
|
111
113
|
"""
|
|
112
114
|
self._patch_litellm_proxy_imports()
|
|
115
|
+
self._logger = logging.getLogger(__name__)
|
|
113
116
|
self.model = model
|
|
114
117
|
self.store: BlackboardStore = store or InMemoryBlackboardStore()
|
|
115
118
|
self._agents: dict[str, Agent] = {}
|
|
@@ -891,6 +894,25 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
891
894
|
self._record_agent_run(agent)
|
|
892
895
|
await agent.execute(ctx, artifacts)
|
|
893
896
|
|
|
897
|
+
if artifacts:
|
|
898
|
+
try:
|
|
899
|
+
timestamp = datetime.now(timezone.utc)
|
|
900
|
+
records = [
|
|
901
|
+
ConsumptionRecord(
|
|
902
|
+
artifact_id=artifact.id,
|
|
903
|
+
consumer=agent.name,
|
|
904
|
+
run_id=ctx.task_id,
|
|
905
|
+
correlation_id=str(correlation_id) if correlation_id else None,
|
|
906
|
+
consumed_at=timestamp,
|
|
907
|
+
)
|
|
908
|
+
for artifact in artifacts
|
|
909
|
+
]
|
|
910
|
+
await self.store.record_consumptions(records)
|
|
911
|
+
except NotImplementedError:
|
|
912
|
+
pass
|
|
913
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
914
|
+
self._logger.exception("Failed to record artifact consumption: %s", exc)
|
|
915
|
+
|
|
894
916
|
# Helpers --------------------------------------------------------------
|
|
895
917
|
|
|
896
918
|
def _normalize_input(
|
flock/service.py
CHANGED
|
@@ -3,13 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
"""HTTP control plane for the blackboard orchestrator."""
|
|
5
5
|
|
|
6
|
+
from datetime import datetime
|
|
6
7
|
from typing import TYPE_CHECKING, Any
|
|
7
8
|
from uuid import UUID
|
|
8
9
|
|
|
9
|
-
from fastapi import FastAPI, HTTPException
|
|
10
|
+
from fastapi import FastAPI, HTTPException, Query
|
|
10
11
|
from fastapi.responses import PlainTextResponse
|
|
11
12
|
|
|
12
13
|
from flock.registry import type_registry
|
|
14
|
+
from flock.store import ArtifactEnvelope, ConsumptionRecord, FilterConfig
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
@@ -26,6 +28,64 @@ class BlackboardHTTPService:
|
|
|
26
28
|
app = self.app
|
|
27
29
|
orchestrator = self.orchestrator
|
|
28
30
|
|
|
31
|
+
def _serialize_artifact(
|
|
32
|
+
artifact,
|
|
33
|
+
consumptions: list[ConsumptionRecord] | None = None,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
data = {
|
|
36
|
+
"id": str(artifact.id),
|
|
37
|
+
"type": artifact.type,
|
|
38
|
+
"payload": artifact.payload,
|
|
39
|
+
"produced_by": artifact.produced_by,
|
|
40
|
+
"visibility": artifact.visibility.model_dump(mode="json"),
|
|
41
|
+
"visibility_kind": getattr(artifact.visibility, "kind", "Unknown"),
|
|
42
|
+
"created_at": artifact.created_at.isoformat(),
|
|
43
|
+
"correlation_id": str(artifact.correlation_id) if artifact.correlation_id else None,
|
|
44
|
+
"partition_key": artifact.partition_key,
|
|
45
|
+
"tags": sorted(artifact.tags),
|
|
46
|
+
"version": artifact.version,
|
|
47
|
+
}
|
|
48
|
+
if consumptions is not None:
|
|
49
|
+
data["consumptions"] = [
|
|
50
|
+
{
|
|
51
|
+
"artifact_id": str(record.artifact_id),
|
|
52
|
+
"consumer": record.consumer,
|
|
53
|
+
"run_id": record.run_id,
|
|
54
|
+
"correlation_id": record.correlation_id,
|
|
55
|
+
"consumed_at": record.consumed_at.isoformat(),
|
|
56
|
+
}
|
|
57
|
+
for record in consumptions
|
|
58
|
+
]
|
|
59
|
+
data["consumed_by"] = sorted({record.consumer for record in consumptions})
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
def _parse_datetime(value: str | None, label: str) -> datetime | None:
|
|
63
|
+
if value is None:
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
return datetime.fromisoformat(value)
|
|
67
|
+
except ValueError as exc: # pragma: no cover - FastAPI converts
|
|
68
|
+
raise HTTPException(status_code=400, detail=f"Invalid {label}: {value}") from exc
|
|
69
|
+
|
|
70
|
+
def _make_filter_config(
|
|
71
|
+
type_names: list[str] | None,
|
|
72
|
+
produced_by: list[str] | None,
|
|
73
|
+
correlation_id: str | None,
|
|
74
|
+
tags: list[str] | None,
|
|
75
|
+
visibility: list[str] | None,
|
|
76
|
+
start: str | None,
|
|
77
|
+
end: str | None,
|
|
78
|
+
) -> FilterConfig:
|
|
79
|
+
return FilterConfig(
|
|
80
|
+
type_names=set(type_names) if type_names else None,
|
|
81
|
+
produced_by=set(produced_by) if produced_by else None,
|
|
82
|
+
correlation_id=correlation_id,
|
|
83
|
+
tags=set(tags) if tags else None,
|
|
84
|
+
visibility=set(visibility) if visibility else None,
|
|
85
|
+
start=_parse_datetime(start, "from"),
|
|
86
|
+
end=_parse_datetime(end, "to"),
|
|
87
|
+
)
|
|
88
|
+
|
|
29
89
|
@app.post("/api/v1/artifacts")
|
|
30
90
|
async def publish_artifact(body: dict[str, Any]) -> dict[str, str]:
|
|
31
91
|
type_name = body.get("type")
|
|
@@ -38,19 +98,73 @@ class BlackboardHTTPService:
|
|
|
38
98
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
39
99
|
return {"status": "accepted"}
|
|
40
100
|
|
|
101
|
+
@app.get("/api/v1/artifacts")
|
|
102
|
+
async def list_artifacts(
|
|
103
|
+
type_names: list[str] | None = Query(None, alias="type"),
|
|
104
|
+
produced_by: list[str] | None = Query(None),
|
|
105
|
+
correlation_id: str | None = None,
|
|
106
|
+
tag: list[str] | None = Query(None),
|
|
107
|
+
start: str | None = Query(None, alias="from"),
|
|
108
|
+
end: str | None = Query(None, alias="to"),
|
|
109
|
+
visibility: list[str] | None = Query(None),
|
|
110
|
+
limit: int = Query(50, ge=1, le=500),
|
|
111
|
+
offset: int = Query(0, ge=0),
|
|
112
|
+
embed_meta: bool = Query(False, alias="embed_meta"),
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
filters = _make_filter_config(
|
|
115
|
+
type_names,
|
|
116
|
+
produced_by,
|
|
117
|
+
correlation_id,
|
|
118
|
+
tag,
|
|
119
|
+
visibility,
|
|
120
|
+
start,
|
|
121
|
+
end,
|
|
122
|
+
)
|
|
123
|
+
artifacts, total = await orchestrator.store.query_artifacts(
|
|
124
|
+
filters,
|
|
125
|
+
limit=limit,
|
|
126
|
+
offset=offset,
|
|
127
|
+
embed_meta=embed_meta,
|
|
128
|
+
)
|
|
129
|
+
items: list[dict[str, Any]] = []
|
|
130
|
+
for artifact in artifacts:
|
|
131
|
+
if isinstance(artifact, ArtifactEnvelope):
|
|
132
|
+
items.append(_serialize_artifact(artifact.artifact, artifact.consumptions))
|
|
133
|
+
else:
|
|
134
|
+
items.append(_serialize_artifact(artifact))
|
|
135
|
+
return {
|
|
136
|
+
"items": items,
|
|
137
|
+
"pagination": {"limit": limit, "offset": offset, "total": total},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@app.get("/api/v1/artifacts/summary")
|
|
141
|
+
async def summarize_artifacts(
|
|
142
|
+
type_names: list[str] | None = Query(None, alias="type"),
|
|
143
|
+
produced_by: list[str] | None = Query(None),
|
|
144
|
+
correlation_id: str | None = None,
|
|
145
|
+
tag: list[str] | None = Query(None),
|
|
146
|
+
start: str | None = Query(None, alias="from"),
|
|
147
|
+
end: str | None = Query(None, alias="to"),
|
|
148
|
+
visibility: list[str] | None = Query(None),
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
filters = _make_filter_config(
|
|
151
|
+
type_names,
|
|
152
|
+
produced_by,
|
|
153
|
+
correlation_id,
|
|
154
|
+
tag,
|
|
155
|
+
visibility,
|
|
156
|
+
start,
|
|
157
|
+
end,
|
|
158
|
+
)
|
|
159
|
+
summary = await orchestrator.store.summarize_artifacts(filters)
|
|
160
|
+
return {"summary": summary}
|
|
161
|
+
|
|
41
162
|
@app.get("/api/v1/artifacts/{artifact_id}")
|
|
42
163
|
async def get_artifact(artifact_id: UUID) -> dict[str, Any]:
|
|
43
164
|
artifact = await orchestrator.store.get(artifact_id)
|
|
44
165
|
if artifact is None:
|
|
45
166
|
raise HTTPException(status_code=404, detail="artifact not found")
|
|
46
|
-
return
|
|
47
|
-
"id": str(artifact.id),
|
|
48
|
-
"type": artifact.type,
|
|
49
|
-
"payload": artifact.payload,
|
|
50
|
-
"produced_by": artifact.produced_by,
|
|
51
|
-
"visibility": artifact.visibility.model_dump(mode="json"),
|
|
52
|
-
"created_at": artifact.created_at.isoformat(),
|
|
53
|
-
}
|
|
167
|
+
return _serialize_artifact(artifact)
|
|
54
168
|
|
|
55
169
|
@app.post("/api/v1/agents/{name}/run")
|
|
56
170
|
async def run_agent(name: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -110,6 +224,29 @@ class BlackboardHTTPService:
|
|
|
110
224
|
]
|
|
111
225
|
}
|
|
112
226
|
|
|
227
|
+
@app.get("/api/v1/agents/{agent_id}/history-summary")
|
|
228
|
+
async def agent_history(
|
|
229
|
+
agent_id: str,
|
|
230
|
+
type_names: list[str] | None = Query(None, alias="type"),
|
|
231
|
+
produced_by: list[str] | None = Query(None),
|
|
232
|
+
correlation_id: str | None = None,
|
|
233
|
+
tag: list[str] | None = Query(None),
|
|
234
|
+
start: str | None = Query(None, alias="from"),
|
|
235
|
+
end: str | None = Query(None, alias="to"),
|
|
236
|
+
visibility: list[str] | None = Query(None),
|
|
237
|
+
) -> dict[str, Any]:
|
|
238
|
+
filters = _make_filter_config(
|
|
239
|
+
type_names,
|
|
240
|
+
produced_by,
|
|
241
|
+
correlation_id,
|
|
242
|
+
tag,
|
|
243
|
+
visibility,
|
|
244
|
+
start,
|
|
245
|
+
end,
|
|
246
|
+
)
|
|
247
|
+
summary = await orchestrator.store.agent_history_summary(agent_id, filters)
|
|
248
|
+
return {"agent_id": agent_id, "summary": summary}
|
|
249
|
+
|
|
113
250
|
@app.get("/health")
|
|
114
251
|
async def health() -> dict[str, str]: # pragma: no cover - trivial
|
|
115
252
|
return {"status": "ok"}
|