flock-core 0.5.20__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.
- flock/dashboard/events.py +1 -1
- flock/engines/dspy/streaming_executor.py +483 -529
- flock/engines/streaming/__init__.py +3 -0
- flock/engines/streaming/sinks.py +489 -0
- {flock_core-0.5.20.dist-info → flock_core-0.5.21.dist-info}/METADATA +2 -2
- {flock_core-0.5.20.dist-info → flock_core-0.5.21.dist-info}/RECORD +9 -7
- {flock_core-0.5.20.dist-info → flock_core-0.5.21.dist-info}/WHEEL +0 -0
- {flock_core-0.5.20.dist-info → flock_core-0.5.21.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.20.dist-info → flock_core-0.5.21.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""Shared streaming sink implementations for DSPy execution.
|
|
2
|
+
|
|
3
|
+
This module provides a composable sink pattern for consuming streaming output
|
|
4
|
+
from DSPy programs and routing it to different presentation layers (Rich terminal,
|
|
5
|
+
WebSocket dashboard, etc.).
|
|
6
|
+
|
|
7
|
+
Architecture
|
|
8
|
+
------------
|
|
9
|
+
The StreamSink protocol defines a minimal interface that all sinks must implement.
|
|
10
|
+
Sinks receive normalized streaming events (status messages, tokens, final predictions)
|
|
11
|
+
and handle presentation-specific logic (Rich display updates, WebSocket broadcasts, etc.).
|
|
12
|
+
|
|
13
|
+
Sinks are designed to be:
|
|
14
|
+
- Composable: Multiple sinks can consume the same stream in parallel
|
|
15
|
+
- Isolated: Each sink maintains its own state and error handling
|
|
16
|
+
- Testable: Sinks can be tested independently with mock dependencies
|
|
17
|
+
|
|
18
|
+
Error Handling Contract
|
|
19
|
+
-----------------------
|
|
20
|
+
Sinks SHOULD NOT raise exceptions during normal streaming operations. Instead:
|
|
21
|
+
- Log errors and continue processing remaining events
|
|
22
|
+
- Use defensive programming (null checks, try/except where appropriate)
|
|
23
|
+
- Only raise exceptions for unrecoverable errors (e.g., invalid configuration)
|
|
24
|
+
|
|
25
|
+
The streaming loop treats sink exceptions as fatal and will abort the stream.
|
|
26
|
+
For fault tolerance, sinks should catch and log their own errors.
|
|
27
|
+
|
|
28
|
+
Example Usage
|
|
29
|
+
-------------
|
|
30
|
+
Basic WebSocket-only streaming:
|
|
31
|
+
|
|
32
|
+
async def ws_broadcast(event: StreamingOutputEvent) -> None:
|
|
33
|
+
await websocket_manager.broadcast(event)
|
|
34
|
+
|
|
35
|
+
def event_factory(output_type, content, seq, is_final):
|
|
36
|
+
return StreamingOutputEvent(
|
|
37
|
+
correlation_id="123",
|
|
38
|
+
agent_name="agent",
|
|
39
|
+
run_id="run-1",
|
|
40
|
+
output_type=output_type,
|
|
41
|
+
content=content,
|
|
42
|
+
sequence=seq,
|
|
43
|
+
is_final=is_final,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
sink = WebSocketSink(ws_broadcast=ws_broadcast, event_factory=event_factory)
|
|
47
|
+
|
|
48
|
+
async for value in stream:
|
|
49
|
+
kind, text, field, final = normalize_value(value)
|
|
50
|
+
if kind == "status":
|
|
51
|
+
await sink.on_status(text)
|
|
52
|
+
elif kind == "token":
|
|
53
|
+
await sink.on_token(text, field)
|
|
54
|
+
elif kind == "prediction":
|
|
55
|
+
await sink.on_final(final, token_count)
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
await sink.flush()
|
|
59
|
+
|
|
60
|
+
Dual-sink composition (CLI with WebSocket):
|
|
61
|
+
|
|
62
|
+
sinks = []
|
|
63
|
+
if rich_enabled:
|
|
64
|
+
sinks.append(RichSink(...))
|
|
65
|
+
if ws_enabled:
|
|
66
|
+
sinks.append(WebSocketSink(...))
|
|
67
|
+
|
|
68
|
+
# Dispatch to all sinks
|
|
69
|
+
for sink in sinks:
|
|
70
|
+
await sink.on_token(text, field)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
from __future__ import annotations
|
|
74
|
+
|
|
75
|
+
import asyncio
|
|
76
|
+
from collections.abc import Awaitable, Callable, MutableMapping, Sequence
|
|
77
|
+
from typing import (
|
|
78
|
+
Any,
|
|
79
|
+
Protocol,
|
|
80
|
+
runtime_checkable,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
from pydantic import BaseModel
|
|
84
|
+
|
|
85
|
+
from flock.dashboard.events import StreamingOutputEvent
|
|
86
|
+
from flock.logging.logging import get_logger
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
logger = get_logger(__name__)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@runtime_checkable
|
|
93
|
+
class StreamSink(Protocol):
|
|
94
|
+
"""Minimal sink protocol for consuming normalized stream events.
|
|
95
|
+
|
|
96
|
+
Sinks receive streaming events from DSPy execution and handle
|
|
97
|
+
presentation-specific logic (Rich display, WebSocket broadcast, etc.).
|
|
98
|
+
|
|
99
|
+
Implementations must be idempotent for on_final() to handle edge cases
|
|
100
|
+
where the stream loop might call it multiple times.
|
|
101
|
+
|
|
102
|
+
Error Handling
|
|
103
|
+
--------------
|
|
104
|
+
Implementations SHOULD catch and log their own errors rather than raising,
|
|
105
|
+
to prevent one sink failure from aborting the entire stream. Only raise
|
|
106
|
+
exceptions for unrecoverable errors during initialization/configuration.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
async def on_status(self, text: str) -> None:
|
|
110
|
+
"""Process a status message from the LLM.
|
|
111
|
+
|
|
112
|
+
Status messages are typically intermediate reasoning steps or
|
|
113
|
+
progress indicators (e.g., "Analyzing input...", "Generating response...").
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
text: Status message text (may include newlines)
|
|
117
|
+
|
|
118
|
+
Note:
|
|
119
|
+
Empty text should be ignored. Implementations should handle
|
|
120
|
+
this gracefully without raising.
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
async def on_token(self, text: str, signature_field: str | None) -> None:
|
|
125
|
+
"""Process a single token from the LLM output stream.
|
|
126
|
+
|
|
127
|
+
Tokens are emitted as the LLM generates text. signature_field indicates
|
|
128
|
+
which output field this token belongs to (for multi-field signatures).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
text: Token text (typically a single word or word fragment)
|
|
132
|
+
signature_field: Name of the signature field being streamed,
|
|
133
|
+
or None if the token doesn't belong to a specific field
|
|
134
|
+
|
|
135
|
+
Note:
|
|
136
|
+
Empty text should be ignored. The field "description" is typically
|
|
137
|
+
skipped as it's the input prompt, not output.
|
|
138
|
+
"""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
async def on_final(self, result: Any, tokens_emitted: int) -> None:
|
|
142
|
+
"""Process the final prediction result.
|
|
143
|
+
|
|
144
|
+
Called once when streaming completes successfully. Contains the
|
|
145
|
+
complete DSPy Prediction object with all output fields populated.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
result: DSPy Prediction object with output fields
|
|
149
|
+
tokens_emitted: Total number of tokens emitted during streaming
|
|
150
|
+
|
|
151
|
+
Note:
|
|
152
|
+
Implementations MUST be idempotent - this may be called multiple
|
|
153
|
+
times in edge cases. Use a finalization guard flag if necessary.
|
|
154
|
+
"""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
async def flush(self) -> None:
|
|
158
|
+
"""Flush any pending async operations.
|
|
159
|
+
|
|
160
|
+
Called after streaming completes to ensure all async tasks
|
|
161
|
+
(e.g., WebSocket broadcasts) complete before returning.
|
|
162
|
+
|
|
163
|
+
Implementations should await any background tasks and handle
|
|
164
|
+
errors gracefully (log but don't raise).
|
|
165
|
+
|
|
166
|
+
Note:
|
|
167
|
+
For synchronous sinks (e.g., Rich terminal), this is a no-op.
|
|
168
|
+
"""
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class RichSink(StreamSink):
|
|
173
|
+
"""Rich terminal sink responsible for mutating live display data.
|
|
174
|
+
|
|
175
|
+
This sink updates a mutable display_data dictionary that represents
|
|
176
|
+
the artifact being streamed. It accumulates status messages and tokens
|
|
177
|
+
in buffers, then replaces them with final structured data when streaming
|
|
178
|
+
completes.
|
|
179
|
+
|
|
180
|
+
The sink integrates with Rich's Live display context, calling a refresh
|
|
181
|
+
callback after each update to trigger terminal re-rendering.
|
|
182
|
+
|
|
183
|
+
Display Data Flow
|
|
184
|
+
-----------------
|
|
185
|
+
1. Initialization: display_data contains empty payload fields and "streaming..." timestamp
|
|
186
|
+
2. on_status(): Accumulates status messages in a buffer, updates display_data["status"]
|
|
187
|
+
3. on_token(): Accumulates tokens in field-specific buffers, updates display_data["payload"]["_streaming"]
|
|
188
|
+
4. on_final(): Replaces streaming buffers with final Prediction fields, removes "status", adds real timestamp
|
|
189
|
+
5. flush(): No-op (Rich rendering is synchronous)
|
|
190
|
+
|
|
191
|
+
Error Handling
|
|
192
|
+
--------------
|
|
193
|
+
- refresh_panel() errors are caught and logged, never raised
|
|
194
|
+
- Idempotent: on_final() checks _finalized flag to prevent double-finalization
|
|
195
|
+
- Defensive: Uses setdefault() and get() to handle missing dictionary keys
|
|
196
|
+
|
|
197
|
+
Thread Safety
|
|
198
|
+
-------------
|
|
199
|
+
NOT thread-safe. Assumes single-threaded async execution within a single
|
|
200
|
+
Rich Live context. Multiple concurrent streams should use separate RichSink instances.
|
|
201
|
+
|
|
202
|
+
Example
|
|
203
|
+
-------
|
|
204
|
+
display_data = OrderedDict([("id", "artifact-123"), ("payload", {}), ...])
|
|
205
|
+
stream_buffers = defaultdict(list)
|
|
206
|
+
|
|
207
|
+
def refresh():
|
|
208
|
+
live.update(formatter.format_result(display_data, ...))
|
|
209
|
+
|
|
210
|
+
sink = RichSink(
|
|
211
|
+
display_data=display_data,
|
|
212
|
+
stream_buffers=stream_buffers,
|
|
213
|
+
status_field="_status",
|
|
214
|
+
signature_order=["output", "summary"],
|
|
215
|
+
formatter=formatter,
|
|
216
|
+
theme_dict=theme,
|
|
217
|
+
styles=styles,
|
|
218
|
+
agent_label="Agent - gpt-4",
|
|
219
|
+
refresh_panel=refresh,
|
|
220
|
+
timestamp_factory=lambda: datetime.now(UTC).isoformat(),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
await sink.on_status("Processing...")
|
|
224
|
+
await sink.on_token("Hello", "output")
|
|
225
|
+
await sink.on_final(prediction, tokens_emitted=5)
|
|
226
|
+
await sink.flush()
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
display_data: MutableMapping[str, Any],
|
|
233
|
+
stream_buffers: MutableMapping[str, list[str]],
|
|
234
|
+
status_field: str,
|
|
235
|
+
signature_order: Sequence[str],
|
|
236
|
+
formatter: Any | None,
|
|
237
|
+
theme_dict: dict[str, Any] | None,
|
|
238
|
+
styles: dict[str, Any] | None,
|
|
239
|
+
agent_label: str | None,
|
|
240
|
+
refresh_panel: Callable[[], None],
|
|
241
|
+
timestamp_factory: Callable[[], str],
|
|
242
|
+
) -> None:
|
|
243
|
+
self._display_data = display_data
|
|
244
|
+
self._stream_buffers = stream_buffers
|
|
245
|
+
self._status_field = status_field
|
|
246
|
+
self._signature_order = list(signature_order)
|
|
247
|
+
self._formatter = formatter
|
|
248
|
+
self._theme_dict = theme_dict
|
|
249
|
+
self._styles = styles
|
|
250
|
+
self._agent_label = agent_label
|
|
251
|
+
self._refresh_panel = refresh_panel
|
|
252
|
+
self._timestamp_factory = timestamp_factory
|
|
253
|
+
self._final_display = (
|
|
254
|
+
formatter,
|
|
255
|
+
display_data,
|
|
256
|
+
theme_dict,
|
|
257
|
+
styles,
|
|
258
|
+
agent_label,
|
|
259
|
+
)
|
|
260
|
+
# Ensure buffers exist for status updates
|
|
261
|
+
self._stream_buffers.setdefault(status_field, [])
|
|
262
|
+
self._finalized = False
|
|
263
|
+
|
|
264
|
+
def _refresh(self) -> None:
|
|
265
|
+
try:
|
|
266
|
+
self._refresh_panel()
|
|
267
|
+
except Exception:
|
|
268
|
+
logger.debug("Rich sink refresh panel callable failed", exc_info=True)
|
|
269
|
+
|
|
270
|
+
async def on_status(self, text: str) -> None:
|
|
271
|
+
if not text:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
buffer = self._stream_buffers.setdefault(self._status_field, [])
|
|
275
|
+
buffer.append(f"{text}\n")
|
|
276
|
+
self._display_data["status"] = "".join(buffer)
|
|
277
|
+
self._refresh()
|
|
278
|
+
|
|
279
|
+
async def on_token(self, text: str, signature_field: str | None) -> None:
|
|
280
|
+
if not text:
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
if signature_field and signature_field != "description":
|
|
284
|
+
buffer_key = f"_stream_{signature_field}"
|
|
285
|
+
buffer = self._stream_buffers.setdefault(buffer_key, [])
|
|
286
|
+
buffer.append(str(text))
|
|
287
|
+
payload = self._display_data.setdefault("payload", {})
|
|
288
|
+
payload["_streaming"] = "".join(buffer)
|
|
289
|
+
else:
|
|
290
|
+
buffer = self._stream_buffers.setdefault(self._status_field, [])
|
|
291
|
+
buffer.append(str(text))
|
|
292
|
+
self._display_data["status"] = "".join(buffer)
|
|
293
|
+
|
|
294
|
+
self._refresh()
|
|
295
|
+
|
|
296
|
+
async def on_final(self, result: Any, tokens_emitted: int) -> None: # noqa: ARG002
|
|
297
|
+
if self._finalized:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
payload_section: MutableMapping[str, Any] = self._display_data.setdefault(
|
|
301
|
+
"payload", {}
|
|
302
|
+
)
|
|
303
|
+
payload_section.clear()
|
|
304
|
+
|
|
305
|
+
for field_name in self._signature_order:
|
|
306
|
+
if field_name == "description":
|
|
307
|
+
continue
|
|
308
|
+
if not hasattr(result, field_name):
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
value = getattr(result, field_name)
|
|
312
|
+
if isinstance(value, list):
|
|
313
|
+
payload_section[field_name] = [
|
|
314
|
+
item.model_dump() if isinstance(item, BaseModel) else item
|
|
315
|
+
for item in value
|
|
316
|
+
]
|
|
317
|
+
elif isinstance(value, BaseModel):
|
|
318
|
+
payload_section[field_name] = value.model_dump()
|
|
319
|
+
else:
|
|
320
|
+
payload_section[field_name] = value
|
|
321
|
+
|
|
322
|
+
self._display_data["created_at"] = self._timestamp_factory()
|
|
323
|
+
self._display_data.pop("status", None)
|
|
324
|
+
payload_section.pop("_streaming", None)
|
|
325
|
+
self._refresh()
|
|
326
|
+
self._finalized = True
|
|
327
|
+
|
|
328
|
+
async def flush(self) -> None:
|
|
329
|
+
# Rich sink has no async resources to drain.
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def final_display_data(
|
|
334
|
+
self,
|
|
335
|
+
) -> tuple[
|
|
336
|
+
Any,
|
|
337
|
+
MutableMapping[str, Any],
|
|
338
|
+
dict[str, Any] | None,
|
|
339
|
+
dict[str, Any] | None,
|
|
340
|
+
str | None,
|
|
341
|
+
]:
|
|
342
|
+
return self._final_display
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class WebSocketSink(StreamSink):
|
|
346
|
+
"""WebSocket-only sink that mirrors dashboard streaming behaviour.
|
|
347
|
+
|
|
348
|
+
This sink broadcasts StreamingOutputEvent messages via WebSocket for
|
|
349
|
+
real-time dashboard updates. It uses fire-and-forget task scheduling
|
|
350
|
+
to avoid blocking the streaming loop while ensuring all events are
|
|
351
|
+
delivered via flush().
|
|
352
|
+
|
|
353
|
+
Event Sequence
|
|
354
|
+
--------------
|
|
355
|
+
Each event gets a monotonically increasing sequence number for ordering:
|
|
356
|
+
- on_status("Loading"): seq=0, output_type="log", content="Loading\\n"
|
|
357
|
+
- on_token("Hello", field): seq=1, output_type="llm_token", content="Hello"
|
|
358
|
+
- on_token(" world", field): seq=2, output_type="llm_token", content=" world"
|
|
359
|
+
- on_final(pred, 2): seq=3, output_type="log", content="\\nAmount of output tokens: 2", is_final=True
|
|
360
|
+
- seq=4, output_type="log", content="--- End of output ---", is_final=True
|
|
361
|
+
|
|
362
|
+
The two terminal events are required for dashboard compatibility and must
|
|
363
|
+
appear in this exact order with is_final=True.
|
|
364
|
+
|
|
365
|
+
Task Management
|
|
366
|
+
---------------
|
|
367
|
+
Events are broadcast using asyncio.create_task() to avoid blocking the
|
|
368
|
+
streaming loop. Tasks are tracked in a set and awaited during flush()
|
|
369
|
+
to ensure delivery before the stream completes.
|
|
370
|
+
|
|
371
|
+
Task lifecycle:
|
|
372
|
+
1. _schedule() creates task and adds to _tasks set
|
|
373
|
+
2. Task completion callback removes it from _tasks
|
|
374
|
+
3. flush() awaits remaining tasks with error handling
|
|
375
|
+
|
|
376
|
+
Error Handling
|
|
377
|
+
--------------
|
|
378
|
+
- Scheduling errors: Logged and ignored (event dropped)
|
|
379
|
+
- Broadcast errors: Caught during flush(), logged but don't raise
|
|
380
|
+
- Idempotent: on_final() checks _finalized flag to prevent duplicate terminal events
|
|
381
|
+
|
|
382
|
+
Thread Safety
|
|
383
|
+
-------------
|
|
384
|
+
NOT thread-safe. Assumes single-threaded async execution. Multiple
|
|
385
|
+
concurrent streams should use separate WebSocketSink instances.
|
|
386
|
+
|
|
387
|
+
Example
|
|
388
|
+
-------
|
|
389
|
+
async def broadcast(event: StreamingOutputEvent):
|
|
390
|
+
await websocket_manager.send_json(event.model_dump())
|
|
391
|
+
|
|
392
|
+
def event_factory(output_type, content, seq, is_final):
|
|
393
|
+
return StreamingOutputEvent(
|
|
394
|
+
correlation_id="corr-123",
|
|
395
|
+
agent_name="analyzer",
|
|
396
|
+
run_id="run-456",
|
|
397
|
+
output_type=output_type,
|
|
398
|
+
content=content,
|
|
399
|
+
sequence=seq,
|
|
400
|
+
is_final=is_final,
|
|
401
|
+
artifact_id="artifact-789",
|
|
402
|
+
artifact_type="Report",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
sink = WebSocketSink(ws_broadcast=broadcast, event_factory=event_factory)
|
|
406
|
+
|
|
407
|
+
await sink.on_status("Processing input")
|
|
408
|
+
await sink.on_token("Analysis", "output")
|
|
409
|
+
await sink.on_final(prediction, tokens_emitted=1)
|
|
410
|
+
await sink.flush() # Ensures all broadcasts complete
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
def __init__(
|
|
414
|
+
self,
|
|
415
|
+
*,
|
|
416
|
+
ws_broadcast: Callable[[StreamingOutputEvent], Awaitable[None]] | None,
|
|
417
|
+
event_factory: Callable[[str, str, int, bool], StreamingOutputEvent],
|
|
418
|
+
) -> None:
|
|
419
|
+
self._ws_broadcast = ws_broadcast
|
|
420
|
+
self._event_factory = event_factory
|
|
421
|
+
self._sequence = 0
|
|
422
|
+
self._tasks: set[asyncio.Task[Any]] = set()
|
|
423
|
+
self._finalized = False
|
|
424
|
+
|
|
425
|
+
def _schedule(
|
|
426
|
+
self,
|
|
427
|
+
output_type: str,
|
|
428
|
+
content: str,
|
|
429
|
+
*,
|
|
430
|
+
is_final: bool,
|
|
431
|
+
advance_sequence: bool = True,
|
|
432
|
+
) -> None:
|
|
433
|
+
if not self._ws_broadcast:
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
event = self._event_factory(output_type, content, self._sequence, is_final)
|
|
437
|
+
try:
|
|
438
|
+
task = asyncio.create_task(self._ws_broadcast(event))
|
|
439
|
+
except Exception as exc: # pragma: no cover - scheduling should rarely fail
|
|
440
|
+
logger.warning(f"Failed to schedule streaming event: {exc}")
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
self._tasks.add(task)
|
|
444
|
+
task.add_done_callback(self._tasks.discard)
|
|
445
|
+
|
|
446
|
+
if advance_sequence:
|
|
447
|
+
self._sequence += 1
|
|
448
|
+
|
|
449
|
+
async def on_status(self, text: str) -> None:
|
|
450
|
+
if not text:
|
|
451
|
+
return
|
|
452
|
+
self._schedule("log", f"{text}\n", is_final=False)
|
|
453
|
+
|
|
454
|
+
async def on_token(self, text: str, signature_field: str | None) -> None: # noqa: ARG002
|
|
455
|
+
if not text:
|
|
456
|
+
return
|
|
457
|
+
self._schedule("llm_token", text, is_final=False)
|
|
458
|
+
|
|
459
|
+
async def on_final(self, result: Any, tokens_emitted: int) -> None: # noqa: ARG002
|
|
460
|
+
if self._finalized:
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
self._schedule(
|
|
464
|
+
"log",
|
|
465
|
+
f"\nAmount of output tokens: {tokens_emitted}",
|
|
466
|
+
is_final=True,
|
|
467
|
+
)
|
|
468
|
+
self._schedule(
|
|
469
|
+
"log",
|
|
470
|
+
"--- End of output ---",
|
|
471
|
+
is_final=True,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
self._finalized = True
|
|
475
|
+
|
|
476
|
+
async def flush(self) -> None:
|
|
477
|
+
if not self._tasks:
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
pending = list(self._tasks)
|
|
481
|
+
self._tasks.clear()
|
|
482
|
+
|
|
483
|
+
results = await asyncio.gather(*pending, return_exceptions=True)
|
|
484
|
+
for result in results:
|
|
485
|
+
if isinstance(result, Exception):
|
|
486
|
+
logger.warning(f"Streaming broadcast task failed: {result}")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
__all__ = ["RichSink", "StreamSink", "WebSocketSink"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flock-core
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.21
|
|
4
4
|
Summary: Flock: A declrative framework for building and orchestrating AI agents.
|
|
5
5
|
Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
|
|
6
6
|
License: MIT
|
|
@@ -1071,7 +1071,7 @@ We're building enterprise infrastructure for AI agents and tracking the work pub
|
|
|
1071
1071
|
```python
|
|
1072
1072
|
import os
|
|
1073
1073
|
from flock import Flock, flock_type
|
|
1074
|
-
from flock.visibility import PrivateVisibility, TenantVisibility, LabelledVisibility
|
|
1074
|
+
from flock.core.visibility import PrivateVisibility, TenantVisibility, LabelledVisibility
|
|
1075
1075
|
from flock.identity import AgentIdentity
|
|
1076
1076
|
from pydantic import BaseModel
|
|
1077
1077
|
|
|
@@ -32,7 +32,7 @@ flock/core/subscription.py,sha256=jIxoET1hvIcRGeLOKfPfLAH0IJvVAUc5_ZZYWPCIjr0,54
|
|
|
32
32
|
flock/core/visibility.py,sha256=uwscg32t6Dp9LuA_EVDT5-_1lotzZWWS9Dl1foyJDxo,2926
|
|
33
33
|
flock/dashboard/__init__.py,sha256=_W6Id_Zb7mkSz0iHY1nfdUqF9p9uV_NyHsU7PFsRnFI,813
|
|
34
34
|
flock/dashboard/collector.py,sha256=TiMfp7i0lIJx4HTBKUwSj_vf5feNLyincge03mlWf2o,21843
|
|
35
|
-
flock/dashboard/events.py,sha256=
|
|
35
|
+
flock/dashboard/events.py,sha256=s81sAMQSlwRtwSfjrpauJTdJSYHwYtyvT50xrc7wdc4,7658
|
|
36
36
|
flock/dashboard/graph_builder.py,sha256=EJiebAmbD2s66l7B8QxExN710zvO3lq_RwkvxoDwdlo,32036
|
|
37
37
|
flock/dashboard/launcher.py,sha256=qnl1sFm9g2dAyzhyLPrfqAfld63G9aTNRHchgNuGnoo,8218
|
|
38
38
|
flock/dashboard/service.py,sha256=bLusnlQ2LzPy_OZlq6DpEEAn6Ru3Rwg8IXO31YA5lB0,5770
|
|
@@ -53,9 +53,11 @@ flock/engines/dspy_engine.py,sha256=tIdNCsQjyzWnTRzQBVLyfY4xP0FKoXHEfV4_4ch_xTQ,
|
|
|
53
53
|
flock/engines/dspy/__init__.py,sha256=ONqJyNfaZaqBno4ggDq092lquZ8KjbloRO9zTZISjoA,734
|
|
54
54
|
flock/engines/dspy/artifact_materializer.py,sha256=ZxcW2eUVt0PknM_qB4tbH4U5SiOrGWiku08joiqygVg,8001
|
|
55
55
|
flock/engines/dspy/signature_builder.py,sha256=4o2zNDCouUxAJOHoONolflnWu5WGrsKSvHuc5Dzetjk,17790
|
|
56
|
-
flock/engines/dspy/streaming_executor.py,sha256=
|
|
56
|
+
flock/engines/dspy/streaming_executor.py,sha256=6fERID0QQmhAMg1l2Zvtq65Ah9B0vBcFI5lTdS5UXsY,28437
|
|
57
57
|
flock/engines/examples/__init__.py,sha256=cDAjF8_NPL7nhpwa_xxgnPYRAXZWppKE2HkxmL8LGAI,126
|
|
58
58
|
flock/engines/examples/simple_batch_engine.py,sha256=p_4YkQ6PI1q09calMxDDRYOdB8naMxklKEzWPMQyrVE,2914
|
|
59
|
+
flock/engines/streaming/__init__.py,sha256=gyn-GQ538RiRbaE1SpBSejw7x6JNcii91Uw2yF7335E,62
|
|
60
|
+
flock/engines/streaming/sinks.py,sha256=eLboWkBuixiZHyHgU0pSQ9QTL6lcNY9EVmR9sFg4KpE,17079
|
|
59
61
|
flock/frontend/README.md,sha256=0dEzu4UxacGfSstz9_R0KSeFWJ1vNRi0p-KLIay_TfU,26212
|
|
60
62
|
flock/frontend/index.html,sha256=BFg1VR_YVAJ_MGN16xa7sT6wTGwtFYUhfJhGuKv89VM,312
|
|
61
63
|
flock/frontend/package-lock.json,sha256=vNOaISeq3EfEDDEsFYPWFaMsqypDgQIE0P_u6tzE0g4,150798
|
|
@@ -589,8 +591,8 @@ flock/utils/utilities.py,sha256=E3EQDo5SLU4XeoL3-PCc4lOatJHFEkMHKbZhnGhdMG4,1248
|
|
|
589
591
|
flock/utils/validation.py,sha256=Vqzd8Qi73ttJNSHdr8EUEonyfzAYWVfEyW0fhLs1bA4,1847
|
|
590
592
|
flock/utils/visibility.py,sha256=riJfHFWH8R2vk3DU7PYNi2LMaE8cy8pnMvxxiqWq-4I,2349
|
|
591
593
|
flock/utils/visibility_utils.py,sha256=eGA28aL8FnGyGzsbVNvqI1UoPauu1Jedl5LUZ5oWZZ0,3874
|
|
592
|
-
flock_core-0.5.
|
|
593
|
-
flock_core-0.5.
|
|
594
|
-
flock_core-0.5.
|
|
595
|
-
flock_core-0.5.
|
|
596
|
-
flock_core-0.5.
|
|
594
|
+
flock_core-0.5.21.dist-info/METADATA,sha256=hmjtqBjzTW2b_xq59iqZYSpkhRXVHTZDYBng_8AQuJg,52163
|
|
595
|
+
flock_core-0.5.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
596
|
+
flock_core-0.5.21.dist-info/entry_points.txt,sha256=UQdPmtHd97gSA_IdLt9MOd-1rrf_WO-qsQeIiHWVrp4,42
|
|
597
|
+
flock_core-0.5.21.dist-info/licenses/LICENSE,sha256=U3IZuTbC0yLj7huwJdldLBipSOHF4cPf6cUOodFiaBE,1072
|
|
598
|
+
flock_core-0.5.21.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|