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.

Files changed (65) 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/__init__.py +1 -0
  10. flock/dashboard/models/graph.py +156 -0
  11. flock/dashboard/service.py +175 -14
  12. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
  13. flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
  14. flock/dashboard/static_v2/index.html +13 -0
  15. flock/dashboard/websocket.py +2 -2
  16. flock/engines/dspy_engine.py +294 -20
  17. flock/frontend/README.md +6 -6
  18. flock/frontend/src/App.tsx +23 -31
  19. flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
  20. flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
  21. flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
  22. flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
  23. flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
  24. flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
  25. flock/frontend/src/components/graph/AgentNode.tsx +8 -6
  26. flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
  27. flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
  28. flock/frontend/src/components/graph/MessageNode.tsx +16 -3
  29. flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
  30. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
  31. flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
  32. flock/frontend/src/hooks/useModules.ts +12 -4
  33. flock/frontend/src/hooks/usePersistence.ts +5 -3
  34. flock/frontend/src/services/api.ts +3 -19
  35. flock/frontend/src/services/graphService.test.ts +330 -0
  36. flock/frontend/src/services/graphService.ts +75 -0
  37. flock/frontend/src/services/websocket.ts +104 -268
  38. flock/frontend/src/store/filterStore.test.ts +89 -1
  39. flock/frontend/src/store/filterStore.ts +38 -16
  40. flock/frontend/src/store/graphStore.test.ts +538 -173
  41. flock/frontend/src/store/graphStore.ts +374 -465
  42. flock/frontend/src/store/moduleStore.ts +51 -33
  43. flock/frontend/src/store/uiStore.ts +23 -11
  44. flock/frontend/src/types/graph.ts +77 -44
  45. flock/frontend/src/utils/mockData.ts +16 -3
  46. flock/frontend/vite.config.ts +2 -2
  47. flock/orchestrator.py +27 -7
  48. flock/patches/__init__.py +5 -0
  49. flock/patches/dspy_streaming_patch.py +82 -0
  50. flock/service.py +2 -2
  51. flock/store.py +169 -4
  52. flock/themes/darkmatrix.toml +2 -2
  53. flock/themes/deep.toml +2 -2
  54. flock/themes/neopolitan.toml +4 -4
  55. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/METADATA +20 -13
  56. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/RECORD +59 -53
  57. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
  58. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
  59. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
  60. flock/frontend/src/services/websocket.test.ts +0 -595
  61. flock/frontend/src/utils/transforms.test.ts +0 -860
  62. flock/frontend/src/utils/transforms.ts +0 -323
  63. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/WHEEL +0 -0
  64. {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/entry_points.txt +0 -0
  65. {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
- (set) => ({
19
- instances: new Map(),
18
+ persist<ModuleState>(
19
+ (set) => ({
20
+ instances: new Map(),
20
21
 
21
- addModule: (module) =>
22
- set((state) => {
23
- const instances = new Map(state.instances);
24
- instances.set(module.id, module);
25
- return { instances };
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
- updateModule: (id, updates) =>
29
- set((state) => {
30
- const instances = new Map(state.instances);
31
- const existing = instances.get(id);
32
- if (existing) {
33
- instances.set(id, { ...existing, ...updates });
34
- }
35
- return { instances };
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
- removeModule: (id) =>
39
- set((state) => {
40
- const instances = new Map(state.instances);
41
- instances.delete(id);
42
- return { instances };
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
- toggleVisibility: (id) =>
46
- set((state) => {
47
- const instances = new Map(state.instances);
48
- const existing = instances.get(id);
49
- if (existing) {
50
- instances.set(id, { ...existing, visible: !existing.visible });
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 { instances };
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
- import { Node, Edge } from '@xyflow/react';
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; // True while streaming, false when complete
30
- streamingText?: string; // Accumulated streaming text (raw)
13
+ isStreaming?: boolean;
14
+ streamingText?: string;
31
15
  consumedBy?: string[];
32
16
  }
33
17
 
34
- export interface AgentNodeData extends Record<string, unknown> {
35
- name: string;
36
- status: 'idle' | 'running' | 'error';
37
- subscriptions: string[];
38
- outputTypes?: string[];
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 MessageNodeData extends Record<string, unknown> {
47
- artifactType: string;
48
- payloadPreview: string;
49
- payload: any; // Full payload for display
50
- producedBy: string;
51
- consumedBy: string[];
52
- timestamp: number;
53
- isStreaming?: boolean; // True while streaming tokens
54
- streamingText?: string; // Raw streaming text
55
- tags?: string[];
56
- visibilityKind?: string;
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 type AgentViewNode = Node<AgentNodeData, 'agent'>;
60
- export type MessageViewNode = Node<MessageNodeData, 'message'>;
61
- export type GraphNode = AgentViewNode | MessageViewNode;
62
- export type GraphEdge = Edge;
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 { Agent, Message } from '../types/graph';
1
+ import { Message } from '../types/graph';
2
2
 
3
- export const mockAgents: Agent[] = [
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() - 8000,
62
+ timestamp: Date.now() - 8344,
50
63
  correlationId: 'corr-123',
51
64
  producedBy: 'movie',
52
65
  },
@@ -13,11 +13,11 @@ export default defineConfig({
13
13
  port: 5173,
14
14
  proxy: {
15
15
  '/api': {
16
- target: 'http://localhost:8000',
16
+ target: 'http://localhost:8344',
17
17
  changeOrigin: true,
18
18
  },
19
19
  '/ws': {
20
- target: 'ws://localhost:8000',
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 typing import TYPE_CHECKING, Any, AsyncGenerator
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, *, dashboard: bool = False, host: str = "127.0.0.1", port: int = 8000
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: 8000)
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
- launcher = DashboardLauncher(port=port)
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,5 @@
1
+ """Monkey-patches for third-party libraries to fix known issues."""
2
+
3
+ from flock.patches.dspy_streaming_patch import apply_patch, restore_original
4
+
5
+ __all__ = ["apply_patch", "restore_original"]
@@ -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 = 8000
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 = 8000
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