flock-core 0.5.11__py3-none-any.whl → 0.5.21__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 (94) hide show
  1. flock/__init__.py +1 -1
  2. flock/agent/__init__.py +30 -0
  3. flock/agent/builder_helpers.py +192 -0
  4. flock/agent/builder_validator.py +169 -0
  5. flock/agent/component_lifecycle.py +325 -0
  6. flock/agent/context_resolver.py +141 -0
  7. flock/agent/mcp_integration.py +212 -0
  8. flock/agent/output_processor.py +304 -0
  9. flock/api/__init__.py +20 -0
  10. flock/{api_models.py → api/models.py} +0 -2
  11. flock/{service.py → api/service.py} +3 -3
  12. flock/cli.py +2 -2
  13. flock/components/__init__.py +41 -0
  14. flock/components/agent/__init__.py +22 -0
  15. flock/{components.py → components/agent/base.py} +4 -3
  16. flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
  17. flock/components/orchestrator/__init__.py +22 -0
  18. flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
  19. flock/components/orchestrator/circuit_breaker.py +95 -0
  20. flock/components/orchestrator/collection.py +143 -0
  21. flock/components/orchestrator/deduplication.py +78 -0
  22. flock/core/__init__.py +30 -0
  23. flock/core/agent.py +953 -0
  24. flock/{artifacts.py → core/artifacts.py} +1 -1
  25. flock/{context_provider.py → core/context_provider.py} +3 -3
  26. flock/core/orchestrator.py +1102 -0
  27. flock/{store.py → core/store.py} +99 -454
  28. flock/{subscription.py → core/subscription.py} +1 -1
  29. flock/dashboard/collector.py +5 -5
  30. flock/dashboard/events.py +1 -1
  31. flock/dashboard/graph_builder.py +7 -7
  32. flock/dashboard/routes/__init__.py +21 -0
  33. flock/dashboard/routes/control.py +327 -0
  34. flock/dashboard/routes/helpers.py +340 -0
  35. flock/dashboard/routes/themes.py +76 -0
  36. flock/dashboard/routes/traces.py +521 -0
  37. flock/dashboard/routes/websocket.py +108 -0
  38. flock/dashboard/service.py +43 -1316
  39. flock/engines/dspy/__init__.py +20 -0
  40. flock/engines/dspy/artifact_materializer.py +216 -0
  41. flock/engines/dspy/signature_builder.py +474 -0
  42. flock/engines/dspy/streaming_executor.py +812 -0
  43. flock/engines/dspy_engine.py +45 -1330
  44. flock/engines/examples/simple_batch_engine.py +2 -2
  45. flock/engines/streaming/__init__.py +3 -0
  46. flock/engines/streaming/sinks.py +489 -0
  47. flock/examples.py +7 -7
  48. flock/logging/logging.py +1 -16
  49. flock/models/__init__.py +10 -0
  50. flock/orchestrator/__init__.py +45 -0
  51. flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
  52. flock/orchestrator/artifact_manager.py +168 -0
  53. flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
  54. flock/orchestrator/component_runner.py +389 -0
  55. flock/orchestrator/context_builder.py +167 -0
  56. flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
  57. flock/orchestrator/event_emitter.py +167 -0
  58. flock/orchestrator/initialization.py +184 -0
  59. flock/orchestrator/lifecycle_manager.py +226 -0
  60. flock/orchestrator/mcp_manager.py +202 -0
  61. flock/orchestrator/scheduler.py +189 -0
  62. flock/orchestrator/server_manager.py +234 -0
  63. flock/orchestrator/tracing.py +147 -0
  64. flock/storage/__init__.py +10 -0
  65. flock/storage/artifact_aggregator.py +158 -0
  66. flock/storage/in_memory/__init__.py +6 -0
  67. flock/storage/in_memory/artifact_filter.py +114 -0
  68. flock/storage/in_memory/history_aggregator.py +115 -0
  69. flock/storage/sqlite/__init__.py +10 -0
  70. flock/storage/sqlite/agent_history_queries.py +154 -0
  71. flock/storage/sqlite/consumption_loader.py +100 -0
  72. flock/storage/sqlite/query_builder.py +112 -0
  73. flock/storage/sqlite/query_params_builder.py +91 -0
  74. flock/storage/sqlite/schema_manager.py +168 -0
  75. flock/storage/sqlite/summary_queries.py +194 -0
  76. flock/utils/__init__.py +14 -0
  77. flock/utils/async_utils.py +67 -0
  78. flock/{runtime.py → utils/runtime.py} +3 -3
  79. flock/utils/time_utils.py +53 -0
  80. flock/utils/type_resolution.py +38 -0
  81. flock/{utilities.py → utils/utilities.py} +2 -2
  82. flock/utils/validation.py +57 -0
  83. flock/utils/visibility.py +79 -0
  84. flock/utils/visibility_utils.py +134 -0
  85. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/METADATA +19 -5
  86. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/RECORD +92 -34
  87. flock/agent.py +0 -1578
  88. flock/orchestrator.py +0 -1983
  89. /flock/{visibility.py → core/visibility.py} +0 -0
  90. /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
  91. /flock/{helper → utils}/cli_helper.py +0 -0
  92. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/WHEEL +0 -0
  93. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/entry_points.txt +0 -0
  94. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,812 @@
1
+ """DSPy streaming execution with Rich display and WebSocket support.
2
+
3
+ Phase 6: Extracted from dspy_engine.py to reduce file size and improve modularity.
4
+
5
+ This module handles all streaming-related logic for DSPy program execution,
6
+ including two modes:
7
+ - CLI mode: Rich Live display with terminal formatting (agents.run())
8
+ - Dashboard mode: WebSocket-only streaming for parallel execution (no Rich overhead)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ from collections import OrderedDict, defaultdict
15
+ from contextlib import nullcontext
16
+ from datetime import UTC, datetime
17
+ from typing import Any, Awaitable, Callable, Sequence
18
+
19
+ from pydantic import BaseModel
20
+
21
+ from flock.dashboard.events import StreamingOutputEvent
22
+ from flock.engines.streaming.sinks import RichSink, StreamSink, WebSocketSink
23
+ from flock.logging.logging import get_logger
24
+
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ class DSPyStreamingExecutor:
30
+ """Executes DSPy programs in streaming mode with Rich or WebSocket output.
31
+
32
+ Responsibilities:
33
+ - Standard (non-streaming) execution
34
+ - WebSocket-only streaming (dashboard mode, no Rich overhead)
35
+ - Rich CLI streaming with formatted tables
36
+ - Stream formatter setup (themes, styles)
37
+ - Final display rendering with artifact metadata
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ *,
43
+ status_output_field: str,
44
+ stream_vertical_overflow: str,
45
+ theme: str,
46
+ no_output: bool,
47
+ ):
48
+ """Initialize streaming executor with configuration.
49
+
50
+ Args:
51
+ status_output_field: Field name for status output
52
+ stream_vertical_overflow: Rich Live vertical overflow strategy
53
+ theme: Theme name for Rich output formatting
54
+ no_output: Whether to disable output
55
+ """
56
+ self.status_output_field = status_output_field
57
+ self.stream_vertical_overflow = stream_vertical_overflow
58
+ self.theme = theme
59
+ self.no_output = no_output
60
+ self._model_stream_cls: Any | None = None
61
+
62
+ def _make_listeners(self, dspy_mod, signature) -> list[Any]:
63
+ """Create DSPy stream listeners for string output fields."""
64
+ streaming_mod = getattr(dspy_mod, "streaming", None)
65
+ if not streaming_mod or not hasattr(streaming_mod, "StreamListener"):
66
+ return []
67
+
68
+ listeners: list[Any] = []
69
+ try:
70
+ for name, field in getattr(signature, "output_fields", {}).items():
71
+ if getattr(field, "annotation", None) is str:
72
+ listeners.append(
73
+ streaming_mod.StreamListener(signature_field_name=name)
74
+ )
75
+ except Exception:
76
+ return []
77
+ return listeners
78
+
79
+ def _payload_kwargs(self, *, payload: Any, description: str) -> dict[str, Any]:
80
+ """Normalize payload variations into kwargs for streamify."""
81
+ if isinstance(payload, dict) and "description" in payload:
82
+ return payload
83
+
84
+ if isinstance(payload, dict) and "input" in payload:
85
+ return {
86
+ "description": description,
87
+ "input": payload["input"],
88
+ "context": payload.get("context", []),
89
+ }
90
+
91
+ # Legacy fallback: treat payload as the primary input.
92
+ return {"description": description, "input": payload, "context": []}
93
+
94
+ def _artifact_type_label(self, agent: Any, output_group: Any) -> str:
95
+ """Derive user-facing artifact label for streaming events."""
96
+ outputs_to_display = (
97
+ getattr(output_group, "outputs", None)
98
+ if output_group and hasattr(output_group, "outputs")
99
+ else getattr(agent, "outputs", [])
100
+ if hasattr(agent, "outputs")
101
+ else []
102
+ )
103
+
104
+ if not outputs_to_display:
105
+ return "output"
106
+
107
+ # Preserve ordering while avoiding duplicates.
108
+ seen: set[str] = set()
109
+ segments: list[str] = []
110
+ for output in outputs_to_display:
111
+ type_name = getattr(getattr(output, "spec", None), "type_name", None)
112
+ if type_name and type_name not in seen:
113
+ seen.add(type_name)
114
+ segments.append(type_name)
115
+
116
+ return ", ".join(segments) if segments else "output"
117
+
118
+ def _streaming_classes_for(self, dspy_mod: Any) -> tuple[type | None, type | None]:
119
+ streaming_mod = getattr(dspy_mod, "streaming", None)
120
+ if not streaming_mod:
121
+ return None, None
122
+ status_cls = getattr(streaming_mod, "StatusMessage", None)
123
+ stream_cls = getattr(streaming_mod, "StreamResponse", None)
124
+ return status_cls, stream_cls
125
+
126
+ def _resolve_model_stream_cls(self) -> Any | None:
127
+ if self._model_stream_cls is None:
128
+ try:
129
+ from litellm import ModelResponseStream # type: ignore
130
+ except Exception: # pragma: no cover - litellm optional at runtime
131
+ self._model_stream_cls = False
132
+ else:
133
+ self._model_stream_cls = ModelResponseStream
134
+ return self._model_stream_cls or None
135
+
136
+ @staticmethod
137
+ def _normalize_status_message(
138
+ value: Any,
139
+ ) -> tuple[str, str | None, str | None, Any | None]:
140
+ message = getattr(value, "message", "")
141
+ return "status", str(message), None, None
142
+
143
+ @staticmethod
144
+ def _normalize_stream_response(
145
+ value: Any,
146
+ ) -> tuple[str, str | None, str | None, Any | None]:
147
+ chunk = getattr(value, "chunk", None)
148
+ signature_field = getattr(value, "signature_field_name", None)
149
+ return "token", ("" if chunk is None else str(chunk)), signature_field, None
150
+
151
+ @staticmethod
152
+ def _normalize_model_stream(
153
+ value: Any,
154
+ ) -> tuple[str, str | None, str | None, Any | None]:
155
+ token_text = ""
156
+ try:
157
+ token_text = value.choices[0].delta.content or ""
158
+ except Exception: # pragma: no cover - defensive parity with legacy path
159
+ token_text = ""
160
+ signature_field = getattr(value, "signature_field_name", None)
161
+ return "token", str(token_text), signature_field, None
162
+
163
+ def _initialize_display_data(
164
+ self,
165
+ *,
166
+ signature_order: Sequence[str],
167
+ agent: Any,
168
+ ctx: Any,
169
+ pre_generated_artifact_id: Any,
170
+ output_group: Any,
171
+ status_field: str,
172
+ ) -> tuple[OrderedDict[str, Any], str]:
173
+ """Build the initial Rich display structure for CLI streaming."""
174
+ display_data: OrderedDict[str, Any] = OrderedDict()
175
+ display_data["id"] = str(pre_generated_artifact_id)
176
+
177
+ artifact_type_name = self._artifact_type_label(agent, output_group)
178
+ display_data["type"] = artifact_type_name
179
+
180
+ payload_section: OrderedDict[str, Any] = OrderedDict()
181
+ for field_name in signature_order:
182
+ if field_name != "description":
183
+ payload_section[field_name] = ""
184
+ display_data["payload"] = payload_section
185
+
186
+ display_data["produced_by"] = getattr(agent, "name", "")
187
+ correlation_id = None
188
+ if ctx and getattr(ctx, "correlation_id", None):
189
+ correlation_id = str(ctx.correlation_id)
190
+ display_data["correlation_id"] = correlation_id
191
+ display_data["partition_key"] = None
192
+ display_data["tags"] = "set()"
193
+ display_data["visibility"] = OrderedDict([("kind", "Public")])
194
+ display_data["created_at"] = "streaming..."
195
+ display_data["version"] = 1
196
+ display_data["status"] = status_field
197
+
198
+ return display_data, artifact_type_name
199
+
200
+ def _prepare_rich_env(
201
+ self,
202
+ *,
203
+ console,
204
+ display_data: OrderedDict[str, Any],
205
+ agent: Any,
206
+ overflow_mode: str,
207
+ ) -> tuple[Any, dict[str, Any], dict[str, Any], str, Any]:
208
+ """Create formatter metadata and Live context for Rich output."""
209
+ from rich.live import Live
210
+
211
+ from flock.engines.dspy_engine import _ensure_live_crop_above
212
+
213
+ _ensure_live_crop_above()
214
+ formatter, theme_dict, styles, agent_label = self.prepare_stream_formatter(
215
+ agent
216
+ )
217
+ initial_panel = formatter.format_result(
218
+ display_data, agent_label, theme_dict, styles
219
+ )
220
+ live_cm = Live(
221
+ initial_panel,
222
+ console=console,
223
+ refresh_per_second=4,
224
+ transient=False,
225
+ vertical_overflow=overflow_mode,
226
+ )
227
+ return formatter, theme_dict, styles, agent_label, live_cm
228
+
229
+ def _build_rich_sink(
230
+ self,
231
+ *,
232
+ live: Any,
233
+ formatter: Any | None,
234
+ display_data: OrderedDict[str, Any],
235
+ agent_label: str | None,
236
+ theme_dict: dict[str, Any] | None,
237
+ styles: dict[str, Any] | None,
238
+ status_field: str,
239
+ signature_order: Sequence[str],
240
+ stream_buffers: defaultdict[str, list[str]],
241
+ timestamp_factory: Callable[[], str],
242
+ ) -> RichSink | None:
243
+ if formatter is None or live is None:
244
+ return None
245
+
246
+ def refresh_panel() -> None:
247
+ live.update(
248
+ formatter.format_result(display_data, agent_label, theme_dict, styles)
249
+ )
250
+
251
+ return RichSink(
252
+ display_data=display_data,
253
+ stream_buffers=stream_buffers,
254
+ status_field=status_field,
255
+ signature_order=signature_order,
256
+ formatter=formatter,
257
+ theme_dict=theme_dict,
258
+ styles=styles,
259
+ agent_label=agent_label,
260
+ refresh_panel=refresh_panel,
261
+ timestamp_factory=timestamp_factory,
262
+ )
263
+
264
+ def _build_websocket_sink(
265
+ self,
266
+ *,
267
+ ws_broadcast: Callable[[StreamingOutputEvent], Awaitable[None]] | None,
268
+ ctx: Any,
269
+ agent: Any,
270
+ pre_generated_artifact_id: Any,
271
+ artifact_type_name: str,
272
+ ) -> WebSocketSink | None:
273
+ if not ws_broadcast:
274
+ return None
275
+
276
+ def event_factory(
277
+ output_type: str, content: str, sequence: int, is_final: bool
278
+ ) -> StreamingOutputEvent:
279
+ return self._build_event(
280
+ ctx=ctx,
281
+ agent=agent,
282
+ artifact_id=pre_generated_artifact_id,
283
+ artifact_type=artifact_type_name,
284
+ output_type=output_type,
285
+ content=content,
286
+ sequence=sequence,
287
+ is_final=is_final,
288
+ )
289
+
290
+ return WebSocketSink(ws_broadcast=ws_broadcast, event_factory=event_factory)
291
+
292
+ def _collect_sinks(
293
+ self,
294
+ *,
295
+ rich_sink: RichSink | None,
296
+ ws_sink: WebSocketSink | None,
297
+ ) -> list[StreamSink]:
298
+ sinks: list[StreamSink] = []
299
+ if rich_sink:
300
+ sinks.append(rich_sink)
301
+ if ws_sink:
302
+ sinks.append(ws_sink)
303
+ return sinks
304
+
305
+ async def _dispatch_to_sinks(
306
+ self, sinks: Sequence[StreamSink], method: str, *args: Any
307
+ ) -> None:
308
+ for sink in sinks:
309
+ await getattr(sink, method)(*args)
310
+
311
+ async def _consume_stream(
312
+ self,
313
+ stream_generator: Any,
314
+ sinks: Sequence[StreamSink],
315
+ dspy_mod: Any,
316
+ ) -> tuple[Any | None, int]:
317
+ tokens_emitted = 0
318
+ final_result: Any | None = None
319
+
320
+ async for value in stream_generator:
321
+ kind, text, signature_field, prediction = self._normalize_value(
322
+ value, dspy_mod
323
+ )
324
+
325
+ if kind == "status" and text:
326
+ await self._dispatch_to_sinks(sinks, "on_status", text)
327
+ continue
328
+
329
+ if kind == "token" and text:
330
+ tokens_emitted += 1
331
+ await self._dispatch_to_sinks(sinks, "on_token", text, signature_field)
332
+ continue
333
+
334
+ if kind == "prediction":
335
+ final_result = prediction
336
+ await self._dispatch_to_sinks(
337
+ sinks, "on_final", prediction, tokens_emitted
338
+ )
339
+ await self._close_stream_generator(stream_generator)
340
+ return final_result, tokens_emitted
341
+
342
+ return final_result, tokens_emitted
343
+
344
+ async def _flush_sinks(self, sinks: Sequence[StreamSink]) -> None:
345
+ for sink in sinks:
346
+ await sink.flush()
347
+
348
+ def _finalize_stream_display(
349
+ self,
350
+ *,
351
+ rich_sink: RichSink | None,
352
+ formatter: Any | None,
353
+ display_data: OrderedDict[str, Any],
354
+ theme_dict: dict[str, Any] | None,
355
+ styles: dict[str, Any] | None,
356
+ agent_label: str | None,
357
+ ) -> tuple[Any, OrderedDict, dict | None, dict | None, str | None]:
358
+ if rich_sink:
359
+ return rich_sink.final_display_data
360
+ return formatter, display_data, theme_dict, styles, agent_label
361
+
362
+ @staticmethod
363
+ async def _close_stream_generator(stream_generator: Any) -> None:
364
+ aclose = getattr(stream_generator, "aclose", None)
365
+ if callable(aclose):
366
+ try:
367
+ await aclose()
368
+ except GeneratorExit:
369
+ pass
370
+ except BaseExceptionGroup as exc: # pragma: no cover - defensive logging
371
+ remaining = [
372
+ err
373
+ for err in getattr(exc, "exceptions", [])
374
+ if not isinstance(err, GeneratorExit)
375
+ ]
376
+ if remaining:
377
+ logger.debug("Error closing stream generator", exc_info=True)
378
+ except Exception:
379
+ logger.debug("Error closing stream generator", exc_info=True)
380
+
381
+ def _build_event(
382
+ self,
383
+ *,
384
+ ctx: Any,
385
+ agent: Any,
386
+ artifact_id: Any,
387
+ artifact_type: str,
388
+ output_type: str,
389
+ content: str,
390
+ sequence: int,
391
+ is_final: bool,
392
+ ) -> StreamingOutputEvent:
393
+ """Construct a StreamingOutputEvent with consistent metadata."""
394
+ correlation_id = ""
395
+ run_id = ""
396
+ if ctx:
397
+ correlation_id = str(getattr(ctx, "correlation_id", "") or "")
398
+ run_id = str(getattr(ctx, "task_id", "") or "")
399
+
400
+ return StreamingOutputEvent(
401
+ correlation_id=correlation_id,
402
+ agent_name=getattr(agent, "name", ""),
403
+ run_id=run_id,
404
+ output_type=output_type,
405
+ content=content,
406
+ sequence=sequence,
407
+ is_final=is_final,
408
+ artifact_id=str(artifact_id) if artifact_id is not None else "",
409
+ artifact_type=artifact_type,
410
+ )
411
+
412
+ def _normalize_value(
413
+ self, value: Any, dspy_mod: Any
414
+ ) -> tuple[str, str | None, str | None, Any | None]:
415
+ """Normalize raw DSPy streaming values into (kind, text, field, final)."""
416
+ status_cls, stream_cls = self._streaming_classes_for(dspy_mod)
417
+ model_stream_cls = self._resolve_model_stream_cls()
418
+ prediction_cls = getattr(dspy_mod, "Prediction", None)
419
+
420
+ if status_cls and isinstance(value, status_cls):
421
+ return self._normalize_status_message(value)
422
+
423
+ if stream_cls and isinstance(value, stream_cls):
424
+ return self._normalize_stream_response(value)
425
+
426
+ if model_stream_cls and isinstance(value, model_stream_cls):
427
+ return self._normalize_model_stream(value)
428
+
429
+ if prediction_cls and isinstance(value, prediction_cls):
430
+ return "prediction", None, None, value
431
+
432
+ return "unknown", None, None, None
433
+
434
+ async def execute_standard(
435
+ self, dspy_mod, program, *, description: str, payload: dict[str, Any]
436
+ ) -> Any:
437
+ """Execute DSPy program in standard mode (no streaming).
438
+
439
+ Args:
440
+ dspy_mod: DSPy module
441
+ program: DSPy program (Predict or ReAct)
442
+ description: System description
443
+ payload: Execution payload with semantic field names
444
+
445
+ Returns:
446
+ DSPy Prediction result
447
+ """
448
+ # Handle semantic fields format: {"description": ..., "task": ..., "report": ...}
449
+ if isinstance(payload, dict) and "description" in payload:
450
+ # Semantic fields: pass all fields as kwargs
451
+ return program(**payload)
452
+
453
+ # Fallback for unexpected payload format
454
+ raise ValueError(
455
+ f"Invalid payload format: expected dict with 'description' key, got {type(payload).__name__}"
456
+ )
457
+
458
+ async def execute_streaming_websocket_only(
459
+ self,
460
+ dspy_mod,
461
+ program,
462
+ signature,
463
+ *,
464
+ description: str,
465
+ payload: dict[str, Any],
466
+ agent: Any,
467
+ ctx: Any = None,
468
+ pre_generated_artifact_id: Any = None,
469
+ output_group=None,
470
+ ) -> tuple[Any, None]:
471
+ """Execute streaming for WebSocket only (no Rich display).
472
+
473
+ Optimized path for dashboard mode that skips all Rich formatting overhead.
474
+ Used when multiple agents stream in parallel to avoid terminal conflicts
475
+ and deadlocks with MCP tools.
476
+
477
+ This method eliminates the Rich Live context that can cause deadlocks when
478
+ combined with MCP tool execution and parallel agent streaming.
479
+
480
+ Args:
481
+ dspy_mod: DSPy module
482
+ program: DSPy program (Predict or ReAct)
483
+ signature: DSPy Signature
484
+ description: System description
485
+ payload: Execution payload with semantic field names
486
+ agent: Agent instance
487
+ ctx: Execution context
488
+ pre_generated_artifact_id: Pre-generated artifact ID for streaming
489
+ output_group: OutputGroup defining expected outputs
490
+
491
+ Returns:
492
+ Tuple of (DSPy Prediction result, None)
493
+ """
494
+ logger.info(
495
+ f"Agent {agent.name}: Starting WebSocket-only streaming (dashboard mode)"
496
+ )
497
+
498
+ # Get WebSocket broadcast function (security: wrapper prevents object traversal)
499
+ # Phase 6+7 Security Fix: Use broadcast wrapper from Agent class variable (prevents GOD MODE restoration)
500
+ from flock.core import Agent
501
+
502
+ ws_broadcast = Agent._websocket_broadcast_global
503
+
504
+ if not ws_broadcast:
505
+ logger.warning(
506
+ f"Agent {agent.name}: No WebSocket manager, falling back to standard execution"
507
+ )
508
+ result = await self.execute_standard(
509
+ dspy_mod, program, description=description, payload=payload
510
+ )
511
+ return result, None
512
+
513
+ artifact_type_name = self._artifact_type_label(agent, output_group)
514
+ listeners = self._make_listeners(dspy_mod, signature)
515
+
516
+ # Create streaming task
517
+ streaming_task = dspy_mod.streamify(
518
+ program,
519
+ is_async_program=True,
520
+ stream_listeners=listeners if listeners else None,
521
+ )
522
+
523
+ stream_kwargs = self._payload_kwargs(payload=payload, description=description)
524
+ stream_generator = streaming_task(**stream_kwargs)
525
+
526
+ def event_factory(
527
+ output_type: str, content: str, sequence: int, is_final: bool
528
+ ) -> StreamingOutputEvent:
529
+ return self._build_event(
530
+ ctx=ctx,
531
+ agent=agent,
532
+ artifact_id=pre_generated_artifact_id,
533
+ artifact_type=artifact_type_name,
534
+ output_type=output_type,
535
+ content=content,
536
+ sequence=sequence,
537
+ is_final=is_final,
538
+ )
539
+
540
+ sink: StreamSink = WebSocketSink(
541
+ ws_broadcast=ws_broadcast,
542
+ event_factory=event_factory,
543
+ )
544
+
545
+ final_result = None
546
+ tokens_emitted = 0
547
+
548
+ async for value in stream_generator:
549
+ kind, text, signature_field, prediction = self._normalize_value(
550
+ value, dspy_mod
551
+ )
552
+
553
+ if kind == "status" and text:
554
+ await sink.on_status(text)
555
+ continue
556
+
557
+ if kind == "token" and text:
558
+ tokens_emitted += 1
559
+ await sink.on_token(text, signature_field)
560
+ continue
561
+
562
+ if kind == "prediction":
563
+ final_result = prediction
564
+ await sink.on_final(prediction, tokens_emitted)
565
+ await self._close_stream_generator(stream_generator)
566
+ break
567
+
568
+ await sink.flush()
569
+
570
+ if final_result is None:
571
+ raise RuntimeError(
572
+ f"Agent {agent.name}: Streaming did not yield a final prediction"
573
+ )
574
+
575
+ logger.info(
576
+ f"Agent {agent.name}: WebSocket streaming completed ({tokens_emitted} tokens)"
577
+ )
578
+ return final_result, None
579
+
580
+ async def execute_streaming(
581
+ self,
582
+ dspy_mod,
583
+ program,
584
+ signature,
585
+ *,
586
+ description: str,
587
+ payload: dict[str, Any],
588
+ agent: Any,
589
+ ctx: Any = None,
590
+ pre_generated_artifact_id: Any = None,
591
+ output_group=None,
592
+ ) -> Any:
593
+ """Execute DSPy program in streaming mode with Rich table updates."""
594
+
595
+ from rich.console import Console
596
+
597
+ console = Console()
598
+
599
+ # Get WebSocket broadcast function (security: wrapper prevents object traversal)
600
+ from flock.core import Agent
601
+
602
+ ws_broadcast = Agent._websocket_broadcast_global
603
+
604
+ listeners = self._make_listeners(dspy_mod, signature)
605
+ streaming_task = dspy_mod.streamify(
606
+ program,
607
+ is_async_program=True,
608
+ stream_listeners=listeners if listeners else None,
609
+ )
610
+
611
+ stream_kwargs = self._payload_kwargs(payload=payload, description=description)
612
+ stream_generator = streaming_task(**stream_kwargs)
613
+
614
+ status_field = self.status_output_field
615
+ try:
616
+ signature_order = list(signature.output_fields.keys())
617
+ except Exception:
618
+ signature_order = []
619
+
620
+ display_data, artifact_type_name = self._initialize_display_data(
621
+ signature_order=signature_order,
622
+ agent=agent,
623
+ ctx=ctx,
624
+ pre_generated_artifact_id=pre_generated_artifact_id,
625
+ output_group=output_group,
626
+ status_field=status_field,
627
+ )
628
+
629
+ stream_buffers: defaultdict[str, list[str]] = defaultdict(list)
630
+ overflow_mode = self.stream_vertical_overflow
631
+
632
+ if not self.no_output:
633
+ (
634
+ formatter,
635
+ theme_dict,
636
+ styles,
637
+ agent_label,
638
+ live_cm,
639
+ ) = self._prepare_rich_env(
640
+ console=console,
641
+ display_data=display_data,
642
+ agent=agent,
643
+ overflow_mode=overflow_mode,
644
+ )
645
+ else:
646
+ formatter = theme_dict = styles = agent_label = None
647
+ live_cm = nullcontext()
648
+
649
+ timestamp_factory = lambda: datetime.now(UTC).isoformat()
650
+
651
+ final_result: Any = None
652
+ tokens_emitted = 0
653
+ sinks: list[StreamSink] = []
654
+ rich_sink: RichSink | None = None
655
+
656
+ with live_cm as live:
657
+ rich_sink = self._build_rich_sink(
658
+ live=live,
659
+ formatter=formatter,
660
+ display_data=display_data,
661
+ agent_label=agent_label,
662
+ theme_dict=theme_dict,
663
+ styles=styles,
664
+ status_field=status_field,
665
+ signature_order=signature_order,
666
+ stream_buffers=stream_buffers,
667
+ timestamp_factory=timestamp_factory,
668
+ )
669
+
670
+ ws_sink = self._build_websocket_sink(
671
+ ws_broadcast=ws_broadcast,
672
+ ctx=ctx,
673
+ agent=agent,
674
+ pre_generated_artifact_id=pre_generated_artifact_id,
675
+ artifact_type_name=artifact_type_name,
676
+ )
677
+
678
+ sinks = self._collect_sinks(rich_sink=rich_sink, ws_sink=ws_sink)
679
+ final_result, tokens_emitted = await self._consume_stream(
680
+ stream_generator, sinks, dspy_mod
681
+ )
682
+
683
+ await self._flush_sinks(sinks)
684
+
685
+ if final_result is None:
686
+ raise RuntimeError("Streaming did not yield a final prediction.")
687
+
688
+ stream_display = self._finalize_stream_display(
689
+ rich_sink=rich_sink,
690
+ formatter=formatter,
691
+ display_data=display_data,
692
+ theme_dict=theme_dict,
693
+ styles=styles,
694
+ agent_label=agent_label,
695
+ )
696
+
697
+ logger.info(
698
+ f"Agent {agent.name}: Rich streaming completed ({tokens_emitted} tokens)"
699
+ )
700
+
701
+ return final_result, stream_display
702
+
703
+ def prepare_stream_formatter(
704
+ self, agent: Any
705
+ ) -> tuple[Any, dict[str, Any], dict[str, Any], str]:
706
+ """Build formatter + theme metadata for streaming tables.
707
+
708
+ Args:
709
+ agent: Agent instance
710
+
711
+ Returns:
712
+ Tuple of (formatter, theme_dict, styles, agent_label)
713
+ """
714
+ import pathlib
715
+
716
+ # Import model from local context since we're in a separate module
717
+ from flock.engines.dspy_engine import DSPyEngine
718
+ from flock.logging.formatters.themed_formatter import (
719
+ ThemedAgentResultFormatter,
720
+ create_pygments_syntax_theme,
721
+ get_default_styles,
722
+ load_syntax_theme_from_file,
723
+ load_theme_from_file,
724
+ )
725
+
726
+ # Get themes directory relative to engine module
727
+ themes_dir = (
728
+ pathlib.Path(DSPyEngine.__module__.replace(".", "/")).parent.parent
729
+ / "themes"
730
+ )
731
+ # Fallback: use __file__ if module path doesn't work
732
+ if not themes_dir.exists():
733
+ import flock.engines.dspy_engine as engine_mod
734
+
735
+ themes_dir = (
736
+ pathlib.Path(engine_mod.__file__).resolve().parents[1] / "themes"
737
+ )
738
+
739
+ theme_filename = self.theme
740
+ if not theme_filename.endswith(".toml"):
741
+ theme_filename = f"{theme_filename}.toml"
742
+ theme_path = themes_dir / theme_filename
743
+
744
+ try:
745
+ theme_dict = load_theme_from_file(theme_path)
746
+ except Exception:
747
+ fallback_path = themes_dir / "afterglow.toml"
748
+ theme_dict = load_theme_from_file(fallback_path)
749
+ theme_path = fallback_path
750
+
751
+ from flock.logging.formatters.themes import OutputTheme
752
+
753
+ formatter = ThemedAgentResultFormatter(theme=OutputTheme.afterglow)
754
+ styles = get_default_styles(theme_dict)
755
+ formatter.styles = styles
756
+
757
+ try:
758
+ syntax_theme = load_syntax_theme_from_file(theme_path)
759
+ formatter.syntax_style = create_pygments_syntax_theme(syntax_theme)
760
+ except Exception:
761
+ formatter.syntax_style = None
762
+
763
+ # Get model label from agent if available
764
+ model_label = getattr(agent, "engine", None)
765
+ if model_label and hasattr(model_label, "model"):
766
+ model_label = model_label.model or ""
767
+ else:
768
+ model_label = ""
769
+
770
+ agent_label = agent.name if not model_label else f"{agent.name} - {model_label}"
771
+
772
+ return formatter, theme_dict, styles, agent_label
773
+
774
+ def print_final_stream_display(
775
+ self,
776
+ stream_display_data: tuple[Any, OrderedDict, dict, dict, str],
777
+ artifact_id: str,
778
+ artifact,
779
+ ) -> None:
780
+ """Print the final streaming display with the real artifact ID.
781
+
782
+ Args:
783
+ stream_display_data: Tuple of (formatter, display_data, theme_dict, styles, agent_label)
784
+ artifact_id: Final artifact ID
785
+ artifact: Artifact instance with metadata
786
+ """
787
+ from rich.console import Console
788
+
789
+ formatter, display_data, theme_dict, styles, agent_label = stream_display_data
790
+
791
+ # Update display_data with the real artifact information
792
+ display_data["id"] = artifact_id
793
+ display_data["created_at"] = artifact.created_at.isoformat()
794
+
795
+ # Update all artifact metadata
796
+ display_data["correlation_id"] = (
797
+ str(artifact.correlation_id) if artifact.correlation_id else None
798
+ )
799
+ display_data["partition_key"] = artifact.partition_key
800
+ display_data["tags"] = (
801
+ "set()" if not artifact.tags else f"set({list(artifact.tags)})"
802
+ )
803
+
804
+ # Print the final panel
805
+ console = Console()
806
+ final_panel = formatter.format_result(
807
+ display_data, agent_label, theme_dict, styles
808
+ )
809
+ console.print(final_panel)
810
+
811
+
812
+ __all__ = ["DSPyStreamingExecutor"]