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,5 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
-
import { devtools } from 'zustand/middleware';
|
|
2
|
+
import { devtools, persist } from 'zustand/middleware';
|
|
3
3
|
import type { ModuleInstance } from '../types/modules';
|
|
4
4
|
|
|
5
5
|
interface ModuleState {
|
|
@@ -15,43 +15,61 @@ interface ModuleState {
|
|
|
15
15
|
|
|
16
16
|
export const useModuleStore = create<ModuleState>()(
|
|
17
17
|
devtools(
|
|
18
|
-
(
|
|
19
|
-
|
|
18
|
+
persist<ModuleState>(
|
|
19
|
+
(set) => ({
|
|
20
|
+
instances: new Map(),
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
addModule: (module: ModuleInstance) =>
|
|
23
|
+
set((state) => {
|
|
24
|
+
const instances = new Map(state.instances);
|
|
25
|
+
instances.set(module.id, module);
|
|
26
|
+
return { instances };
|
|
27
|
+
}),
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
updateModule: (id: string, updates: Partial<Omit<ModuleInstance, 'id' | 'type'>>) =>
|
|
30
|
+
set((state) => {
|
|
31
|
+
const instances = new Map(state.instances);
|
|
32
|
+
const existing = instances.get(id);
|
|
33
|
+
if (existing) {
|
|
34
|
+
instances.set(id, { ...existing, ...updates });
|
|
35
|
+
}
|
|
36
|
+
return { instances };
|
|
37
|
+
}),
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
removeModule: (id: string) =>
|
|
40
|
+
set((state) => {
|
|
41
|
+
const instances = new Map(state.instances);
|
|
42
|
+
instances.delete(id);
|
|
43
|
+
return { instances };
|
|
44
|
+
}),
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
toggleVisibility: (id: string) =>
|
|
47
|
+
set((state) => {
|
|
48
|
+
const instances = new Map(state.instances);
|
|
49
|
+
const existing = instances.get(id);
|
|
50
|
+
if (existing) {
|
|
51
|
+
instances.set(id, { ...existing, visible: !existing.visible });
|
|
52
|
+
}
|
|
53
|
+
return { instances };
|
|
54
|
+
}),
|
|
55
|
+
}),
|
|
56
|
+
{
|
|
57
|
+
name: 'flock-module-state',
|
|
58
|
+
partialize: (state) => ({
|
|
59
|
+
instances: Array.from(state.instances.entries()),
|
|
60
|
+
}) as any,
|
|
61
|
+
merge: (persistedState: any, currentState: any) => {
|
|
62
|
+
// Convert instances array back to Map
|
|
63
|
+
if (persistedState?.instances && Array.isArray(persistedState.instances)) {
|
|
64
|
+
persistedState.instances = new Map(persistedState.instances);
|
|
51
65
|
}
|
|
52
|
-
return {
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
return {
|
|
67
|
+
...currentState,
|
|
68
|
+
...persistedState,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
),
|
|
55
73
|
{ name: 'moduleStore' }
|
|
56
74
|
)
|
|
57
75
|
);
|
|
@@ -40,17 +40,17 @@ interface UIState {
|
|
|
40
40
|
|
|
41
41
|
export const useUIStore = create<UIState>()(
|
|
42
42
|
devtools(
|
|
43
|
-
persist(
|
|
43
|
+
persist<UIState>(
|
|
44
44
|
(set) => ({
|
|
45
45
|
mode: 'agent',
|
|
46
|
-
setMode: (mode) => set({ mode }),
|
|
46
|
+
setMode: (mode: VisualizationMode) => set({ mode }),
|
|
47
47
|
|
|
48
48
|
selectedNodeIds: new Set(),
|
|
49
|
-
selectNode: (nodeId) =>
|
|
49
|
+
selectNode: (nodeId: string) =>
|
|
50
50
|
set((state) => ({
|
|
51
51
|
selectedNodeIds: new Set(state.selectedNodeIds).add(nodeId),
|
|
52
52
|
})),
|
|
53
|
-
deselectNode: (nodeId) =>
|
|
53
|
+
deselectNode: (nodeId: string) =>
|
|
54
54
|
set((state) => {
|
|
55
55
|
const ids = new Set(state.selectedNodeIds);
|
|
56
56
|
ids.delete(nodeId);
|
|
@@ -59,7 +59,7 @@ export const useUIStore = create<UIState>()(
|
|
|
59
59
|
clearSelection: () => set({ selectedNodeIds: new Set() }),
|
|
60
60
|
|
|
61
61
|
detailWindows: new Map(),
|
|
62
|
-
openDetailWindow: (nodeId) =>
|
|
62
|
+
openDetailWindow: (nodeId: string) =>
|
|
63
63
|
set((state) => {
|
|
64
64
|
const windows = new Map(state.detailWindows);
|
|
65
65
|
if (!windows.has(nodeId)) {
|
|
@@ -72,13 +72,13 @@ export const useUIStore = create<UIState>()(
|
|
|
72
72
|
}
|
|
73
73
|
return { detailWindows: windows };
|
|
74
74
|
}),
|
|
75
|
-
closeDetailWindow: (nodeId) =>
|
|
75
|
+
closeDetailWindow: (nodeId: string) =>
|
|
76
76
|
set((state) => {
|
|
77
77
|
const windows = new Map(state.detailWindows);
|
|
78
78
|
windows.delete(nodeId);
|
|
79
79
|
return { detailWindows: windows };
|
|
80
80
|
}),
|
|
81
|
-
updateDetailWindow: (nodeId, updates) =>
|
|
81
|
+
updateDetailWindow: (nodeId: string, updates: Partial<DetailWindow>) =>
|
|
82
82
|
set((state) => {
|
|
83
83
|
const windows = new Map(state.detailWindows);
|
|
84
84
|
const existing = windows.get(nodeId);
|
|
@@ -89,20 +89,32 @@ export const useUIStore = create<UIState>()(
|
|
|
89
89
|
}),
|
|
90
90
|
|
|
91
91
|
layoutDirection: 'TB',
|
|
92
|
-
setLayoutDirection: (direction) => set({ layoutDirection: direction }),
|
|
92
|
+
setLayoutDirection: (direction: 'TB' | 'LR') => set({ layoutDirection: direction }),
|
|
93
93
|
autoLayoutEnabled: true,
|
|
94
|
-
setAutoLayoutEnabled: (enabled) => set({ autoLayoutEnabled: enabled }),
|
|
94
|
+
setAutoLayoutEnabled: (enabled: boolean) => set({ autoLayoutEnabled: enabled }),
|
|
95
95
|
|
|
96
96
|
defaultTab: 'liveOutput',
|
|
97
|
-
setDefaultTab: (tab) => set({ defaultTab: tab }),
|
|
97
|
+
setDefaultTab: (tab: 'liveOutput' | 'messageHistory' | 'runStatus') => set({ defaultTab: tab }),
|
|
98
98
|
}),
|
|
99
99
|
{
|
|
100
100
|
name: 'ui-storage',
|
|
101
101
|
partialize: (state) => ({
|
|
102
|
+
mode: state.mode,
|
|
103
|
+
detailWindows: Array.from(state.detailWindows.entries()),
|
|
102
104
|
defaultTab: state.defaultTab,
|
|
103
105
|
layoutDirection: state.layoutDirection,
|
|
104
106
|
autoLayoutEnabled: state.autoLayoutEnabled,
|
|
105
|
-
}),
|
|
107
|
+
}) as any,
|
|
108
|
+
merge: (persistedState: any, currentState: any) => {
|
|
109
|
+
// Convert detailWindows array back to Map
|
|
110
|
+
if (persistedState?.detailWindows && Array.isArray(persistedState.detailWindows)) {
|
|
111
|
+
persistedState.detailWindows = new Map(persistedState.detailWindows);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
...currentState,
|
|
115
|
+
...persistedState,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
106
118
|
}
|
|
107
119
|
),
|
|
108
120
|
{ name: 'uiStore' }
|
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export interface Agent {
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
status: 'idle' | 'running' | 'error';
|
|
7
|
-
subscriptions: string[];
|
|
8
|
-
lastActive: number;
|
|
9
|
-
sentCount: number;
|
|
10
|
-
recvCount: number;
|
|
11
|
-
position?: { x: number; y: number };
|
|
12
|
-
outputTypes?: string[]; // Artifact types this agent produces
|
|
13
|
-
receivedByType?: Record<string, number>; // Count of messages received per type
|
|
14
|
-
sentByType?: Record<string, number>; // Count of messages sent per type
|
|
15
|
-
streamingTokens?: string[]; // Last 6 streaming tokens for news ticker effect
|
|
16
|
-
}
|
|
17
|
-
|
|
1
|
+
// Legacy types (still used during migration for events, WebSocket handlers)
|
|
18
2
|
export interface Message {
|
|
19
3
|
id: string;
|
|
20
4
|
type: string;
|
|
@@ -26,37 +10,86 @@ export interface Message {
|
|
|
26
10
|
visibilityKind?: string;
|
|
27
11
|
partitionKey?: string | null;
|
|
28
12
|
version?: number;
|
|
29
|
-
isStreaming?: boolean;
|
|
30
|
-
streamingText?: string;
|
|
13
|
+
isStreaming?: boolean;
|
|
14
|
+
streamingText?: string;
|
|
31
15
|
consumedBy?: string[];
|
|
32
16
|
}
|
|
33
17
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
sentCount: number;
|
|
40
|
-
recvCount: number;
|
|
41
|
-
receivedByType?: Record<string, number>;
|
|
42
|
-
sentByType?: Record<string, number>;
|
|
43
|
-
streamingTokens?: string[]; // Last 6 streaming tokens for news ticker effect
|
|
18
|
+
// New backend API types (Phase 1 - Spec 002)
|
|
19
|
+
export interface GraphRequest {
|
|
20
|
+
viewMode: 'agent' | 'blackboard';
|
|
21
|
+
filters: GraphFilters;
|
|
22
|
+
options?: GraphRequestOptions;
|
|
44
23
|
}
|
|
45
24
|
|
|
46
|
-
export interface
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
25
|
+
export interface GraphFilters {
|
|
26
|
+
correlation_id?: string | null;
|
|
27
|
+
time_range: TimeRangeFilter;
|
|
28
|
+
artifactTypes: string[];
|
|
29
|
+
producers: string[];
|
|
30
|
+
tags: string[];
|
|
31
|
+
visibility: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TimeRangeFilter {
|
|
35
|
+
preset: 'last10min' | 'last5min' | 'last1hour' | 'all' | 'custom';
|
|
36
|
+
start?: string | null;
|
|
37
|
+
end?: string | null;
|
|
57
38
|
}
|
|
58
39
|
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
export interface GraphRequestOptions {
|
|
41
|
+
include_statistics?: boolean;
|
|
42
|
+
label_offset_strategy?: 'stack' | 'none';
|
|
43
|
+
limit?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GraphSnapshot {
|
|
47
|
+
generatedAt: string;
|
|
48
|
+
viewMode: 'agent' | 'blackboard';
|
|
49
|
+
filters: GraphFilters;
|
|
50
|
+
nodes: GraphNode[];
|
|
51
|
+
edges: GraphEdge[];
|
|
52
|
+
statistics: GraphStatistics | null;
|
|
53
|
+
totalArtifacts: number;
|
|
54
|
+
truncated: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GraphNode {
|
|
58
|
+
id: string;
|
|
59
|
+
type: 'agent' | 'message';
|
|
60
|
+
data: Record<string, any>;
|
|
61
|
+
position: { x: number; y: number };
|
|
62
|
+
hidden: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface GraphEdge {
|
|
66
|
+
id: string;
|
|
67
|
+
source: string;
|
|
68
|
+
target: string;
|
|
69
|
+
type: 'message_flow' | 'transformation';
|
|
70
|
+
label?: string | null;
|
|
71
|
+
data: Record<string, any>;
|
|
72
|
+
markerEnd?: { type: string; width: number; height: number };
|
|
73
|
+
hidden: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface GraphStatistics {
|
|
77
|
+
producedByAgent: Record<string, GraphAgentMetrics>;
|
|
78
|
+
consumedByAgent: Record<string, GraphAgentMetrics>;
|
|
79
|
+
artifactSummary: ArtifactSummary;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GraphAgentMetrics {
|
|
83
|
+
total: number;
|
|
84
|
+
byType: Record<string, number>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ArtifactSummary {
|
|
88
|
+
total: number;
|
|
89
|
+
by_type: Record<string, number>;
|
|
90
|
+
by_producer: Record<string, number>;
|
|
91
|
+
by_visibility: Record<string, number>;
|
|
92
|
+
tag_counts: Record<string, number>;
|
|
93
|
+
earliest_created_at: string;
|
|
94
|
+
latest_created_at: string;
|
|
95
|
+
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Message } from '../types/graph';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// UI Optimization Migration (Phase 4.1 - Spec 002): OLD Phase 1 mock data (unused)
|
|
4
|
+
// Kept for potential future testing, but Agent type no longer exists
|
|
5
|
+
type LegacyAgent = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
status: string;
|
|
9
|
+
subscriptions: string[];
|
|
10
|
+
lastActive: number;
|
|
11
|
+
sentCount: number;
|
|
12
|
+
recvCount: number;
|
|
13
|
+
position: { x: number; y: number };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const mockAgents: LegacyAgent[] = [
|
|
4
17
|
{
|
|
5
18
|
id: 'movie',
|
|
6
19
|
name: 'movie',
|
|
@@ -46,7 +59,7 @@ export const mockMessages: Message[] = [
|
|
|
46
59
|
id: 'msg-2',
|
|
47
60
|
type: 'Movie',
|
|
48
61
|
payload: { title: 'Inception', year: 2010, genre: 'Sci-Fi' },
|
|
49
|
-
timestamp: Date.now() -
|
|
62
|
+
timestamp: Date.now() - 8344,
|
|
50
63
|
correlationId: 'corr-123',
|
|
51
64
|
producedBy: 'movie',
|
|
52
65
|
},
|
flock/frontend/vite.config.ts
CHANGED
|
@@ -13,11 +13,11 @@ export default defineConfig({
|
|
|
13
13
|
port: 5173,
|
|
14
14
|
proxy: {
|
|
15
15
|
'/api': {
|
|
16
|
-
target: 'http://localhost:
|
|
16
|
+
target: 'http://localhost:8344',
|
|
17
17
|
changeOrigin: true,
|
|
18
18
|
},
|
|
19
19
|
'/ws': {
|
|
20
|
-
target: 'ws://localhost:
|
|
20
|
+
target: 'ws://localhost:8344',
|
|
21
21
|
ws: true,
|
|
22
22
|
},
|
|
23
23
|
},
|
flock/orchestrator.py
CHANGED
|
@@ -6,10 +6,11 @@ import asyncio
|
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
8
|
from asyncio import Task
|
|
9
|
-
from collections.abc import Iterable, Mapping, Sequence
|
|
9
|
+
from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence
|
|
10
10
|
from contextlib import asynccontextmanager
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
|
-
from
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
13
14
|
from uuid import uuid4
|
|
14
15
|
|
|
15
16
|
from opentelemetry import trace
|
|
@@ -545,14 +546,20 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
545
546
|
return self
|
|
546
547
|
|
|
547
548
|
async def serve(
|
|
548
|
-
self,
|
|
549
|
+
self,
|
|
550
|
+
*,
|
|
551
|
+
dashboard: bool = False,
|
|
552
|
+
dashboard_v2: bool = False,
|
|
553
|
+
host: str = "127.0.0.1",
|
|
554
|
+
port: int = 8344,
|
|
549
555
|
) -> None:
|
|
550
556
|
"""Start HTTP service for the orchestrator (blocking).
|
|
551
557
|
|
|
552
558
|
Args:
|
|
553
559
|
dashboard: Enable real-time dashboard with WebSocket support (default: False)
|
|
560
|
+
dashboard_v2: Launch the new dashboard v2 frontend (implies dashboard=True)
|
|
554
561
|
host: Host to bind to (default: "127.0.0.1")
|
|
555
|
-
port: Port to bind to (default:
|
|
562
|
+
port: Port to bind to (default: 8344)
|
|
556
563
|
|
|
557
564
|
Examples:
|
|
558
565
|
# Basic HTTP API (no dashboard) - runs until interrupted
|
|
@@ -561,6 +568,9 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
561
568
|
# With dashboard (WebSocket + browser launch) - runs until interrupted
|
|
562
569
|
await orchestrator.serve(dashboard=True)
|
|
563
570
|
"""
|
|
571
|
+
if dashboard_v2:
|
|
572
|
+
dashboard = True
|
|
573
|
+
|
|
564
574
|
if not dashboard:
|
|
565
575
|
# Standard service without dashboard
|
|
566
576
|
from flock.service import BlackboardHTTPService
|
|
@@ -577,8 +587,9 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
577
587
|
|
|
578
588
|
# Create dashboard components
|
|
579
589
|
websocket_manager = WebSocketManager()
|
|
580
|
-
event_collector = DashboardEventCollector()
|
|
590
|
+
event_collector = DashboardEventCollector(store=self.store)
|
|
581
591
|
event_collector.set_websocket_manager(websocket_manager)
|
|
592
|
+
await event_collector.load_persistent_snapshots()
|
|
582
593
|
|
|
583
594
|
# Store collector reference for agents added later
|
|
584
595
|
self._dashboard_collector = event_collector
|
|
@@ -589,7 +600,13 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
589
600
|
agent.utilities.insert(0, event_collector)
|
|
590
601
|
|
|
591
602
|
# Start dashboard launcher (npm process + browser)
|
|
592
|
-
|
|
603
|
+
launcher_kwargs: dict[str, Any] = {"port": port}
|
|
604
|
+
if dashboard_v2:
|
|
605
|
+
dashboard_pkg_dir = Path(__file__).parent / "dashboard"
|
|
606
|
+
launcher_kwargs["frontend_dir"] = dashboard_pkg_dir.parent / "frontend_v2"
|
|
607
|
+
launcher_kwargs["static_dir"] = dashboard_pkg_dir / "static_v2"
|
|
608
|
+
|
|
609
|
+
launcher = DashboardLauncher(**launcher_kwargs)
|
|
593
610
|
launcher.start()
|
|
594
611
|
|
|
595
612
|
# Create dashboard HTTP service
|
|
@@ -597,6 +614,7 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
597
614
|
orchestrator=self,
|
|
598
615
|
websocket_manager=websocket_manager,
|
|
599
616
|
event_collector=event_collector,
|
|
617
|
+
use_v2=dashboard_v2,
|
|
600
618
|
)
|
|
601
619
|
|
|
602
620
|
# Store launcher for cleanup
|
|
@@ -650,8 +668,10 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
650
668
|
>>> # Publish with tags for channel routing
|
|
651
669
|
>>> await orchestrator.publish(task, tags={"urgent", "backend"})
|
|
652
670
|
"""
|
|
653
|
-
init_console(clear_screen=True, show_banner=True, model=self.model)
|
|
654
671
|
self.is_dashboard = is_dashboard
|
|
672
|
+
# Only show banner in CLI mode, not dashboard mode
|
|
673
|
+
if not self.is_dashboard:
|
|
674
|
+
init_console(clear_screen=True, show_banner=True, model=self.model)
|
|
655
675
|
# Handle different input types
|
|
656
676
|
if isinstance(obj, Artifact):
|
|
657
677
|
# Already an artifact - publish as-is
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monkey-patch for DSPy's sync_send_to_stream function.
|
|
3
|
+
|
|
4
|
+
The original DSPy implementation blocks the event loop with future.result(),
|
|
5
|
+
causing deadlocks when using MCP tools with dashboard streaming.
|
|
6
|
+
|
|
7
|
+
This patch replaces it with a non-blocking fire-and-forget approach.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def patched_sync_send_to_stream(stream, message):
|
|
17
|
+
"""Non-blocking replacement for DSPy's sync_send_to_stream.
|
|
18
|
+
|
|
19
|
+
Instead of blocking with future.result(), this version:
|
|
20
|
+
1. Schedules the send as a background task (fire-and-forget)
|
|
21
|
+
2. Never blocks the calling thread
|
|
22
|
+
3. Logs errors but doesn't raise them
|
|
23
|
+
|
|
24
|
+
This allows MCP tool callbacks to complete without deadlocking.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
async def _send():
|
|
28
|
+
try:
|
|
29
|
+
await stream.send(message)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logger.debug(f"DSPy status message send failed (non-critical): {e}")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# Try to get the running event loop
|
|
35
|
+
loop = asyncio.get_running_loop()
|
|
36
|
+
|
|
37
|
+
# Schedule as a background task (fire-and-forget)
|
|
38
|
+
# This won't block - the task runs independently
|
|
39
|
+
loop.create_task(_send())
|
|
40
|
+
|
|
41
|
+
except RuntimeError:
|
|
42
|
+
# No event loop running - this is a sync context
|
|
43
|
+
# We can safely create a new loop and run the task
|
|
44
|
+
try:
|
|
45
|
+
asyncio.run(_send())
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.debug(f"DSPy status message send failed in sync context (non-critical): {e}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def apply_patch():
|
|
51
|
+
"""Apply the monkey-patch to DSPy's streaming module."""
|
|
52
|
+
try:
|
|
53
|
+
import dspy.streaming.messages as dspy_messages
|
|
54
|
+
|
|
55
|
+
# Store original for reference (in case we need to restore)
|
|
56
|
+
if not hasattr(dspy_messages, "_original_sync_send_to_stream"):
|
|
57
|
+
dspy_messages._original_sync_send_to_stream = dspy_messages.sync_send_to_stream
|
|
58
|
+
|
|
59
|
+
# Replace with our non-blocking version
|
|
60
|
+
dspy_messages.sync_send_to_stream = patched_sync_send_to_stream
|
|
61
|
+
|
|
62
|
+
logger.info("Applied DSPy streaming patch - status messages are now non-blocking")
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.warning(f"Failed to apply DSPy streaming patch: {e}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def restore_original():
|
|
71
|
+
"""Restore the original DSPy function (for testing/debugging)."""
|
|
72
|
+
try:
|
|
73
|
+
import dspy.streaming.messages as dspy_messages
|
|
74
|
+
|
|
75
|
+
if hasattr(dspy_messages, "_original_sync_send_to_stream"):
|
|
76
|
+
dspy_messages.sync_send_to_stream = dspy_messages._original_sync_send_to_stream
|
|
77
|
+
logger.info("Restored original DSPy streaming function")
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(f"Failed to restore original DSPy function: {e}")
|
|
82
|
+
return False
|
flock/service.py
CHANGED
|
@@ -257,14 +257,14 @@ class BlackboardHTTPService:
|
|
|
257
257
|
return PlainTextResponse("\n".join(lines))
|
|
258
258
|
|
|
259
259
|
def run(
|
|
260
|
-
self, host: str = "127.0.0.1", port: int =
|
|
260
|
+
self, host: str = "127.0.0.1", port: int = 8344
|
|
261
261
|
) -> None: # pragma: no cover - manual execution
|
|
262
262
|
import uvicorn
|
|
263
263
|
|
|
264
264
|
uvicorn.run(self.app, host=host, port=port)
|
|
265
265
|
|
|
266
266
|
async def run_async(
|
|
267
|
-
self, host: str = "127.0.0.1", port: int =
|
|
267
|
+
self, host: str = "127.0.0.1", port: int = 8344
|
|
268
268
|
) -> None: # pragma: no cover - manual execution
|
|
269
269
|
"""Run the service asynchronously (for use within async context)."""
|
|
270
270
|
import uvicorn
|