flock-core 0.5.0b19__py3-none-any.whl → 0.5.0b22__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/components/evaluation/declarative_evaluation_component.py +272 -44
- flock/components/utility/output_utility_component.py +12 -0
- flock/core/agent/default_agent.py +9 -2
- flock/core/util/cli_helper.py +3 -3
- flock/webapp/app/api/execution.py +232 -24
- flock/webapp/templates/partials/_streaming_results_container.html +195 -0
- {flock_core-0.5.0b19.dist-info → flock_core-0.5.0b22.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b19.dist-info → flock_core-0.5.0b22.dist-info}/RECORD +11 -10
- {flock_core-0.5.0b19.dist-info → flock_core-0.5.0b22.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b19.dist-info → flock_core-0.5.0b22.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b19.dist-info → flock_core-0.5.0b22.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# src/flock/components/evaluation/declarative_evaluation_component.py
|
|
2
2
|
"""DeclarativeEvaluationComponent - DSPy-based evaluation using the unified component system."""
|
|
3
3
|
|
|
4
|
-
from collections
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from collections.abc import Callable, Generator
|
|
6
|
+
from contextlib import nullcontext
|
|
5
7
|
from typing import Any, Literal, override
|
|
6
8
|
|
|
7
9
|
from temporalio import workflow
|
|
@@ -22,6 +24,73 @@ from flock.core.registry import flock_component
|
|
|
22
24
|
logger = get_logger("components.evaluation.declarative")
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
_live_patch_applied = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ensure_live_crop_above() -> None:
|
|
31
|
+
"""Monkeypatch rich.live_render to support 'crop_above' overflow."""
|
|
32
|
+
global _live_patch_applied
|
|
33
|
+
if _live_patch_applied:
|
|
34
|
+
return
|
|
35
|
+
try:
|
|
36
|
+
from typing import Literal as _Literal
|
|
37
|
+
|
|
38
|
+
from rich import live_render as _lr
|
|
39
|
+
except Exception:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Extend the accepted literal at runtime so type checks don't block the new option.
|
|
43
|
+
current_args = getattr(_lr.VerticalOverflowMethod, '__args__', ())
|
|
44
|
+
if 'crop_above' not in current_args:
|
|
45
|
+
_lr.VerticalOverflowMethod = _Literal['crop', 'crop_above', 'ellipsis', 'visible'] # type: ignore[assignment]
|
|
46
|
+
|
|
47
|
+
if getattr(_lr.LiveRender.__rich_console__, '_flock_crop_above', False):
|
|
48
|
+
_live_patch_applied = True
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
Segment = _lr.Segment
|
|
52
|
+
Text = _lr.Text
|
|
53
|
+
loop_last = _lr.loop_last
|
|
54
|
+
|
|
55
|
+
def _patched_rich_console(self, console, options):
|
|
56
|
+
renderable = self.renderable
|
|
57
|
+
style = console.get_style(self.style)
|
|
58
|
+
lines = console.render_lines(renderable, options, style=style, pad=False)
|
|
59
|
+
shape = Segment.get_shape(lines)
|
|
60
|
+
|
|
61
|
+
_, height = shape
|
|
62
|
+
max_height = options.size.height
|
|
63
|
+
if height > max_height:
|
|
64
|
+
if self.vertical_overflow == 'crop':
|
|
65
|
+
lines = lines[: max_height]
|
|
66
|
+
shape = Segment.get_shape(lines)
|
|
67
|
+
elif self.vertical_overflow == 'crop_above':
|
|
68
|
+
lines = lines[-max_height:]
|
|
69
|
+
shape = Segment.get_shape(lines)
|
|
70
|
+
elif self.vertical_overflow == 'ellipsis' and max_height > 0:
|
|
71
|
+
lines = lines[: (max_height - 1)]
|
|
72
|
+
overflow_text = Text(
|
|
73
|
+
'...',
|
|
74
|
+
overflow='crop',
|
|
75
|
+
justify='center',
|
|
76
|
+
end='',
|
|
77
|
+
style='live.ellipsis',
|
|
78
|
+
)
|
|
79
|
+
lines.append(list(console.render(overflow_text)))
|
|
80
|
+
shape = Segment.get_shape(lines)
|
|
81
|
+
self._shape = shape
|
|
82
|
+
|
|
83
|
+
new_line = Segment.line()
|
|
84
|
+
for last, line in loop_last(lines):
|
|
85
|
+
yield from line
|
|
86
|
+
if not last:
|
|
87
|
+
yield new_line
|
|
88
|
+
|
|
89
|
+
_patched_rich_console._flock_crop_above = True # type: ignore[attr-defined]
|
|
90
|
+
_lr.LiveRender.__rich_console__ = _patched_rich_console
|
|
91
|
+
_live_patch_applied = True
|
|
92
|
+
|
|
93
|
+
|
|
25
94
|
class DeclarativeEvaluationConfig(AgentComponentConfig):
|
|
26
95
|
"""Configuration for the DeclarativeEvaluationComponent."""
|
|
27
96
|
|
|
@@ -32,6 +101,10 @@ class DeclarativeEvaluationConfig(AgentComponentConfig):
|
|
|
32
101
|
max_tokens: int = 32000
|
|
33
102
|
max_retries: int = 3
|
|
34
103
|
max_tool_calls: int = 10
|
|
104
|
+
no_output: bool = Field(
|
|
105
|
+
default=False,
|
|
106
|
+
description="Disable output from the underlying DSPy program.",
|
|
107
|
+
)
|
|
35
108
|
stream: bool = Field(
|
|
36
109
|
default=False,
|
|
37
110
|
description="Enable streaming output from the underlying DSPy program.",
|
|
@@ -52,6 +125,11 @@ class DeclarativeEvaluationConfig(AgentComponentConfig):
|
|
|
52
125
|
default=None,
|
|
53
126
|
description="Extraction LM for TwoStepAdapter when adapter='two_step'",
|
|
54
127
|
)
|
|
128
|
+
stream_callbacks: list[Callable[..., Any] | Any] | None = None
|
|
129
|
+
stream_vertical_overflow: Literal["crop", "ellipsis", "crop_above", "visible"] = Field(
|
|
130
|
+
default="crop_above",
|
|
131
|
+
description=("Rich Live vertical overflow strategy; select how tall output is handled; 'crop_above' keeps the most recent rows visible."),
|
|
132
|
+
)
|
|
55
133
|
kwargs: dict[str, Any] = Field(default_factory=dict)
|
|
56
134
|
|
|
57
135
|
|
|
@@ -165,7 +243,7 @@ class DeclarativeEvaluationComponent(
|
|
|
165
243
|
return await self._execute_standard(agent_task, inputs, agent)
|
|
166
244
|
|
|
167
245
|
async def _execute_streaming(self, signature, agent_task, inputs: dict[str, Any], agent: Any, console) -> dict[str, Any]:
|
|
168
|
-
"""Execute DSPy program in streaming mode
|
|
246
|
+
"""Execute DSPy program in streaming mode with rich table updates."""
|
|
169
247
|
logger.info(f"Evaluating agent '{agent.name}' with async streaming.")
|
|
170
248
|
|
|
171
249
|
if not callable(agent_task):
|
|
@@ -177,7 +255,9 @@ class DeclarativeEvaluationComponent(
|
|
|
177
255
|
try:
|
|
178
256
|
for name, field in signature.output_fields.items():
|
|
179
257
|
if field.annotation is str:
|
|
180
|
-
listeners.append(
|
|
258
|
+
listeners.append(
|
|
259
|
+
dspy.streaming.StreamListener(signature_field_name=name)
|
|
260
|
+
)
|
|
181
261
|
except Exception:
|
|
182
262
|
listeners = []
|
|
183
263
|
|
|
@@ -188,56 +268,148 @@ class DeclarativeEvaluationComponent(
|
|
|
188
268
|
)
|
|
189
269
|
stream_generator: Generator = streaming_task(**inputs)
|
|
190
270
|
|
|
191
|
-
|
|
271
|
+
from collections import defaultdict
|
|
272
|
+
|
|
273
|
+
from rich.live import Live
|
|
274
|
+
|
|
275
|
+
signature_order = []
|
|
276
|
+
try:
|
|
277
|
+
signature_order = list(signature.output_fields.keys())
|
|
278
|
+
except Exception:
|
|
279
|
+
signature_order = []
|
|
280
|
+
|
|
281
|
+
display_data: OrderedDict[str, Any] = OrderedDict()
|
|
282
|
+
for key in inputs:
|
|
283
|
+
display_data[key] = inputs[key]
|
|
284
|
+
|
|
285
|
+
for field_name in signature_order:
|
|
286
|
+
if field_name not in display_data:
|
|
287
|
+
display_data[field_name] = ""
|
|
288
|
+
|
|
289
|
+
stream_buffers: defaultdict[str, list[str]] = defaultdict(list)
|
|
290
|
+
|
|
291
|
+
formatter = theme_dict = styles = agent_label = None
|
|
292
|
+
live_cm = nullcontext()
|
|
293
|
+
overflow_mode = self.config.stream_vertical_overflow
|
|
294
|
+
initial_panel = None
|
|
295
|
+
if not self.config.no_output:
|
|
296
|
+
_ensure_live_crop_above()
|
|
297
|
+
(
|
|
298
|
+
formatter,
|
|
299
|
+
theme_dict,
|
|
300
|
+
styles,
|
|
301
|
+
agent_label,
|
|
302
|
+
) = self._prepare_stream_formatter(agent)
|
|
303
|
+
initial_panel = formatter.format_result(
|
|
304
|
+
display_data, agent_label, theme_dict, styles
|
|
305
|
+
)
|
|
306
|
+
live_cm = Live(
|
|
307
|
+
initial_panel,
|
|
308
|
+
console=console,
|
|
309
|
+
refresh_per_second=400,
|
|
310
|
+
transient=False,
|
|
311
|
+
vertical_overflow=overflow_mode,
|
|
312
|
+
)
|
|
313
|
+
|
|
192
314
|
final_result: dict[str, Any] | None = None
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if isinstance(value, StatusMessage):
|
|
206
|
-
# Optionally surface status to console
|
|
207
|
-
console.print(f"[status] {getattr(value, 'message', '')}")
|
|
208
|
-
continue
|
|
209
|
-
if isinstance(value, StreamResponse):
|
|
210
|
-
token = getattr(value, "token", None)
|
|
211
|
-
if token:
|
|
212
|
-
console.print(token, end="")
|
|
213
|
-
continue
|
|
214
|
-
if isinstance(value, ModelResponseStream):
|
|
215
|
-
# Raw model chunk; print minimal content if available for debug
|
|
315
|
+
|
|
316
|
+
with live_cm as live:
|
|
317
|
+
def _refresh_panel() -> None:
|
|
318
|
+
if formatter is None or live is None:
|
|
319
|
+
return
|
|
320
|
+
live.update(
|
|
321
|
+
formatter.format_result(
|
|
322
|
+
display_data, agent_label, theme_dict, styles
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
async for value in stream_generator:
|
|
216
327
|
try:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
console.print(text, end="")
|
|
328
|
+
import dspy as _d
|
|
329
|
+
from dspy.streaming import StatusMessage, StreamResponse
|
|
330
|
+
from litellm import ModelResponseStream
|
|
221
331
|
except Exception:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
332
|
+
StatusMessage = object # type: ignore
|
|
333
|
+
StreamResponse = object # type: ignore
|
|
334
|
+
ModelResponseStream = object # type: ignore
|
|
335
|
+
_d = None
|
|
336
|
+
|
|
337
|
+
if isinstance(value, StatusMessage):
|
|
338
|
+
message = getattr(value, "message", "")
|
|
339
|
+
if message and live is not None:
|
|
340
|
+
live.console.log(f"[status] {message}")
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
if isinstance(value, StreamResponse):
|
|
344
|
+
for callback in self.config.stream_callbacks or []:
|
|
345
|
+
try:
|
|
346
|
+
callback(value)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.warning(f"Stream callback error: {e}")
|
|
349
|
+
token = getattr(value, "chunk", None)
|
|
350
|
+
signature_field = getattr(value, "signature_field_name", None)
|
|
351
|
+
if signature_field:
|
|
352
|
+
if signature_field not in display_data:
|
|
353
|
+
display_data[signature_field] = ""
|
|
354
|
+
if token:
|
|
355
|
+
stream_buffers[signature_field].append(str(token))
|
|
356
|
+
display_data[signature_field] = "".join(
|
|
357
|
+
stream_buffers[signature_field]
|
|
358
|
+
)
|
|
359
|
+
if formatter is not None:
|
|
360
|
+
_refresh_panel()
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
if isinstance(value, ModelResponseStream):
|
|
364
|
+
try:
|
|
365
|
+
chunk = value
|
|
366
|
+
text = chunk.choices[0].delta.content or ""
|
|
367
|
+
if text and live is not None:
|
|
368
|
+
live.console.log(text)
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
if _d and isinstance(value, _d.Prediction):
|
|
374
|
+
result_dict, cost, lm_history = self._process_result(
|
|
375
|
+
value, inputs
|
|
376
|
+
)
|
|
377
|
+
self._cost = cost
|
|
378
|
+
self._lm_history = lm_history
|
|
379
|
+
final_result = result_dict
|
|
380
|
+
|
|
381
|
+
if formatter is not None:
|
|
382
|
+
ordered_final = OrderedDict()
|
|
383
|
+
for key in inputs:
|
|
384
|
+
if key in final_result:
|
|
385
|
+
ordered_final[key] = final_result[key]
|
|
386
|
+
for field_name in signature_order:
|
|
387
|
+
if field_name in final_result:
|
|
388
|
+
ordered_final[field_name] = final_result[field_name]
|
|
389
|
+
for key, val in final_result.items():
|
|
390
|
+
if key not in ordered_final:
|
|
391
|
+
ordered_final[key] = val
|
|
392
|
+
display_data.clear()
|
|
393
|
+
display_data.update(ordered_final)
|
|
394
|
+
_refresh_panel()
|
|
395
|
+
|
|
232
396
|
if final_result is None:
|
|
233
397
|
raise RuntimeError("Streaming did not yield a final prediction.")
|
|
234
|
-
|
|
398
|
+
|
|
399
|
+
filtered_result = self.filter_reasoning(
|
|
235
400
|
final_result, self.config.include_reasoning
|
|
236
401
|
)
|
|
237
|
-
|
|
238
|
-
|
|
402
|
+
filtered_result = self.filter_thought_process(
|
|
403
|
+
filtered_result, self.config.include_thought_process
|
|
239
404
|
)
|
|
240
405
|
|
|
406
|
+
if not self.config.no_output:
|
|
407
|
+
context = getattr(agent, "context", None)
|
|
408
|
+
if context is not None:
|
|
409
|
+
context.state["_flock_stream_live_active"] = True
|
|
410
|
+
|
|
411
|
+
return filtered_result
|
|
412
|
+
|
|
241
413
|
async def _execute_standard(self, agent_task, inputs: dict[str, Any], agent: Any) -> dict[str, Any]:
|
|
242
414
|
"""Execute DSPy program in standard mode (from original implementation)."""
|
|
243
415
|
logger.info(f"Evaluating agent '{agent.name}' without streaming.")
|
|
@@ -261,6 +433,62 @@ class DeclarativeEvaluationComponent(
|
|
|
261
433
|
)
|
|
262
434
|
raise RuntimeError(f"Evaluation failed: {e}") from e
|
|
263
435
|
|
|
436
|
+
def _prepare_stream_formatter(
|
|
437
|
+
self, agent: Any
|
|
438
|
+
) -> tuple[Any, dict[str, Any], dict[str, Any], str]:
|
|
439
|
+
"""Build formatter + theme metadata for streaming tables."""
|
|
440
|
+
import pathlib
|
|
441
|
+
|
|
442
|
+
from flock.core.logging.formatters.themed_formatter import (
|
|
443
|
+
ThemedAgentResultFormatter,
|
|
444
|
+
create_pygments_syntax_theme,
|
|
445
|
+
get_default_styles,
|
|
446
|
+
load_syntax_theme_from_file,
|
|
447
|
+
load_theme_from_file,
|
|
448
|
+
)
|
|
449
|
+
from flock.core.logging.formatters.themes import OutputTheme
|
|
450
|
+
|
|
451
|
+
stream_theme = OutputTheme.afterglow
|
|
452
|
+
output_component = None
|
|
453
|
+
try:
|
|
454
|
+
output_component = agent.get_component("output_formatter")
|
|
455
|
+
except Exception:
|
|
456
|
+
output_component = None
|
|
457
|
+
if output_component and getattr(output_component, "config", None):
|
|
458
|
+
stream_theme = getattr(
|
|
459
|
+
output_component.config, "theme", stream_theme
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
formatter = ThemedAgentResultFormatter(theme=stream_theme)
|
|
463
|
+
|
|
464
|
+
themes_dir = pathlib.Path(__file__).resolve().parents[2] / "themes"
|
|
465
|
+
theme_filename = stream_theme.value
|
|
466
|
+
if not theme_filename.endswith(".toml"):
|
|
467
|
+
theme_filename = f"{theme_filename}.toml"
|
|
468
|
+
theme_path = themes_dir / theme_filename
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
theme_dict = load_theme_from_file(theme_path)
|
|
472
|
+
except Exception:
|
|
473
|
+
fallback_path = themes_dir / "afterglow.toml"
|
|
474
|
+
theme_dict = load_theme_from_file(fallback_path)
|
|
475
|
+
theme_path = fallback_path
|
|
476
|
+
|
|
477
|
+
styles = get_default_styles(theme_dict)
|
|
478
|
+
formatter.styles = styles
|
|
479
|
+
try:
|
|
480
|
+
syntax_theme = load_syntax_theme_from_file(theme_path)
|
|
481
|
+
formatter.syntax_style = create_pygments_syntax_theme(syntax_theme)
|
|
482
|
+
except Exception:
|
|
483
|
+
formatter.syntax_style = None
|
|
484
|
+
|
|
485
|
+
model_label = getattr(agent, "model", None) or self.config.model or ""
|
|
486
|
+
agent_label = (
|
|
487
|
+
agent.name if not model_label else f"{agent.name} - {model_label}"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
return formatter, theme_dict, styles, agent_label
|
|
491
|
+
|
|
264
492
|
def filter_thought_process(
|
|
265
493
|
self, result_dict: dict[str, Any], include_thought_process: bool
|
|
266
494
|
) -> dict[str, Any]:
|
|
@@ -146,11 +146,23 @@ class OutputUtilityComponent(UtilityComponent):
|
|
|
146
146
|
"""Format and display the output."""
|
|
147
147
|
logger.debug("Formatting and displaying output")
|
|
148
148
|
|
|
149
|
+
streaming_live_handled = False
|
|
150
|
+
if context:
|
|
151
|
+
streaming_live_handled = bool(
|
|
152
|
+
context.get_variable("_flock_stream_live_active", False)
|
|
153
|
+
)
|
|
154
|
+
if streaming_live_handled:
|
|
155
|
+
context.state.pop("_flock_stream_live_active", None)
|
|
156
|
+
|
|
149
157
|
# Determine if output should be suppressed
|
|
150
158
|
is_silent = self.config.no_output or (
|
|
151
159
|
context and context.get_variable(FLOCK_BATCH_SILENT_MODE, False)
|
|
152
160
|
)
|
|
153
161
|
|
|
162
|
+
if streaming_live_handled:
|
|
163
|
+
logger.debug("Skipping static table because streaming rendered live output.")
|
|
164
|
+
return result
|
|
165
|
+
|
|
154
166
|
if is_silent:
|
|
155
167
|
logger.debug("Output suppressed (config or batch silent mode).")
|
|
156
168
|
return result # Skip console output
|
|
@@ -8,7 +8,7 @@ and composes the standard components under the hood.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from collections.abc import Callable
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Literal
|
|
12
12
|
|
|
13
13
|
from flock.components.utility.metrics_utility_component import (
|
|
14
14
|
MetricsUtilityComponent,
|
|
@@ -46,7 +46,9 @@ class DefaultAgent(FlockAgent):
|
|
|
46
46
|
max_tokens: int | None = None,
|
|
47
47
|
max_tool_calls: int = 0,
|
|
48
48
|
max_retries: int = 2,
|
|
49
|
-
stream: bool =
|
|
49
|
+
stream: bool = True,
|
|
50
|
+
stream_callbacks: list[Callable[..., Any] | Any] | None = None,
|
|
51
|
+
stream_vertical_overflow: Literal["crop", "ellipsis", "crop_above", "visible"] = "crop_above",
|
|
50
52
|
include_thought_process: bool = False,
|
|
51
53
|
include_reasoning: bool = False,
|
|
52
54
|
# Output utility parameters
|
|
@@ -84,6 +86,8 @@ class DefaultAgent(FlockAgent):
|
|
|
84
86
|
max_tool_calls: Maximum number of tool calls per evaluation
|
|
85
87
|
max_retries: Maximum retries for failed LLM calls
|
|
86
88
|
stream: Whether to enable streaming responses
|
|
89
|
+
stream_callbacks: Optional callbacks invoked with each streaming chunk
|
|
90
|
+
stream_vertical_overflow: Rich Live overflow handling ('ellipsis', 'crop', 'crop_above', 'visible')
|
|
87
91
|
include_thought_process: Include reasoning in output
|
|
88
92
|
include_reasoning: Include detailed reasoning steps
|
|
89
93
|
enable_rich_tables: Enable rich table formatting for output
|
|
@@ -119,7 +123,10 @@ class DefaultAgent(FlockAgent):
|
|
|
119
123
|
temperature=temperature,
|
|
120
124
|
max_tool_calls=max_tool_calls,
|
|
121
125
|
max_retries=max_retries,
|
|
126
|
+
no_output=no_output,
|
|
122
127
|
stream=stream,
|
|
128
|
+
stream_callbacks=stream_callbacks,
|
|
129
|
+
stream_vertical_overflow=stream_vertical_overflow,
|
|
123
130
|
include_thought_process=include_thought_process,
|
|
124
131
|
include_reasoning=include_reasoning,
|
|
125
132
|
)
|
flock/core/util/cli_helper.py
CHANGED
|
@@ -51,7 +51,7 @@ def init_console(clear_screen: bool = True, show_banner: bool = True, model: str
|
|
|
51
51
|
│ ▒█▀▀▀ █░░ █▀▀█ █▀▀ █░█ │
|
|
52
52
|
│ ▒█▀▀▀ █░░ █░░█ █░░ █▀▄ │
|
|
53
53
|
│ ▒█░░░ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀░▀ │
|
|
54
|
-
╰━━━━━━━━v{__version__}
|
|
54
|
+
╰━━━━━━━━v{__version__}━━━━━━━╯
|
|
55
55
|
🦆 🐤 🐧 🐓
|
|
56
56
|
""",
|
|
57
57
|
justify="center",
|
|
@@ -63,11 +63,11 @@ def init_console(clear_screen: bool = True, show_banner: bool = True, model: str
|
|
|
63
63
|
if show_banner:
|
|
64
64
|
console.print(banner_text)
|
|
65
65
|
console.print(
|
|
66
|
-
"[italic]'
|
|
66
|
+
"[italic]'Kea'[/] milestone - [bold]white duck GmbH[/] - [cyan]https://whiteduck.de[/]\n"
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
if model:
|
|
70
|
-
console.print(f"[italic]Global Model:[/] {model}")
|
|
70
|
+
console.print(f"[italic]Global Model:[/] {model}\n")
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def display_banner_no_version():
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# src/flock/webapp/app/api/execution.py
|
|
2
|
+
import asyncio
|
|
2
3
|
import html
|
|
3
4
|
import json
|
|
5
|
+
import uuid
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import TYPE_CHECKING, Any, Literal
|
|
6
8
|
|
|
@@ -12,7 +14,7 @@ from fastapi import ( # Ensure Form and HTTPException are imported
|
|
|
12
14
|
Request,
|
|
13
15
|
)
|
|
14
16
|
from fastapi.encoders import jsonable_encoder
|
|
15
|
-
from fastapi.responses import FileResponse, HTMLResponse
|
|
17
|
+
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
|
|
16
18
|
from fastapi.templating import Jinja2Templates
|
|
17
19
|
from werkzeug.utils import secure_filename
|
|
18
20
|
|
|
@@ -40,10 +42,6 @@ from flock.webapp.app.dependencies import (
|
|
|
40
42
|
)
|
|
41
43
|
|
|
42
44
|
# Service function now takes app_state
|
|
43
|
-
from flock.webapp.app.services.flock_service import (
|
|
44
|
-
run_current_flock_service,
|
|
45
|
-
# get_current_flock_instance IS NO LONGER IMPORTED
|
|
46
|
-
)
|
|
47
45
|
from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
|
|
48
46
|
|
|
49
47
|
router = APIRouter()
|
|
@@ -59,6 +57,183 @@ def markdown_filter(text):
|
|
|
59
57
|
templates.env.filters["markdown"] = markdown_filter
|
|
60
58
|
|
|
61
59
|
|
|
60
|
+
class ExecutionStreamManager:
|
|
61
|
+
"""In-memory tracker for live streaming sessions."""
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self._sessions: dict[str, asyncio.Queue] = {}
|
|
65
|
+
self._lock = asyncio.Lock()
|
|
66
|
+
|
|
67
|
+
async def create_session(self) -> tuple[str, asyncio.Queue]:
|
|
68
|
+
run_id = uuid.uuid4().hex
|
|
69
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
70
|
+
async with self._lock:
|
|
71
|
+
self._sessions[run_id] = queue
|
|
72
|
+
return run_id, queue
|
|
73
|
+
|
|
74
|
+
async def get_queue(self, run_id: str) -> asyncio.Queue | None:
|
|
75
|
+
async with self._lock:
|
|
76
|
+
return self._sessions.get(run_id)
|
|
77
|
+
|
|
78
|
+
async def remove_session(self, run_id: str) -> None:
|
|
79
|
+
async with self._lock:
|
|
80
|
+
self._sessions.pop(run_id, None)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
execution_stream_manager = ExecutionStreamManager()
|
|
84
|
+
stream_logger = get_flock_logger("webapp.execution.stream")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _execute_agent_with_stream(
|
|
88
|
+
run_id: str,
|
|
89
|
+
queue: asyncio.Queue,
|
|
90
|
+
start_agent_name: str,
|
|
91
|
+
inputs: dict[str, Any],
|
|
92
|
+
app_state: Any,
|
|
93
|
+
template_context: dict[str, Any],
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Run the requested agent while forwarding streaming chunks to the UI."""
|
|
96
|
+
|
|
97
|
+
completed = False
|
|
98
|
+
|
|
99
|
+
def emit(payload: dict[str, Any]) -> None:
|
|
100
|
+
try:
|
|
101
|
+
queue.put_nowait(payload)
|
|
102
|
+
except asyncio.QueueFull:
|
|
103
|
+
stream_logger.warning(
|
|
104
|
+
"Dropping streaming payload for run %s due to full queue", run_id
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def terminate_with_error(message: str) -> None:
|
|
108
|
+
emit({"type": "error", "message": message})
|
|
109
|
+
await finalize_stream()
|
|
110
|
+
|
|
111
|
+
async def finalize_stream() -> None:
|
|
112
|
+
nonlocal completed
|
|
113
|
+
if not completed:
|
|
114
|
+
emit({"type": "complete"})
|
|
115
|
+
completed = True
|
|
116
|
+
await queue.put(None)
|
|
117
|
+
|
|
118
|
+
current_flock: "Flock | None" = getattr(app_state, "flock_instance", None)
|
|
119
|
+
run_store: RunStore | None = getattr(app_state, "run_store", None)
|
|
120
|
+
|
|
121
|
+
if not current_flock:
|
|
122
|
+
stream_logger.error("Stream run aborted: no flock loaded in app state.")
|
|
123
|
+
await terminate_with_error("No Flock loaded in the application.")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
agent = current_flock.agents.get(start_agent_name)
|
|
127
|
+
if not agent:
|
|
128
|
+
stream_logger.error(
|
|
129
|
+
"Stream run aborted: agent '%s' not found in flock '%s'.",
|
|
130
|
+
start_agent_name,
|
|
131
|
+
current_flock.name,
|
|
132
|
+
)
|
|
133
|
+
await terminate_with_error(
|
|
134
|
+
f"Agent '{html.escape(str(start_agent_name))}' not found."
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
evaluator = getattr(agent, "evaluator", None)
|
|
139
|
+
previous_callbacks: list[Any] | None = None
|
|
140
|
+
original_stream_setting: bool | None = None
|
|
141
|
+
|
|
142
|
+
if evaluator is not None:
|
|
143
|
+
previous_callbacks = list(evaluator.config.stream_callbacks or [])
|
|
144
|
+
original_stream_setting = getattr(evaluator.config, "stream", False)
|
|
145
|
+
|
|
146
|
+
def stream_callback(message: Any) -> None:
|
|
147
|
+
chunk = getattr(message, "chunk", None)
|
|
148
|
+
signature_field = getattr(message, "signature_field_name", None)
|
|
149
|
+
if chunk is None:
|
|
150
|
+
return
|
|
151
|
+
emit(
|
|
152
|
+
{
|
|
153
|
+
"type": "token",
|
|
154
|
+
"chunk": str(chunk),
|
|
155
|
+
"field": signature_field,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
evaluator.config.stream_callbacks = [
|
|
160
|
+
*previous_callbacks,
|
|
161
|
+
stream_callback,
|
|
162
|
+
]
|
|
163
|
+
if not original_stream_setting:
|
|
164
|
+
evaluator.config.stream = True
|
|
165
|
+
else:
|
|
166
|
+
emit(
|
|
167
|
+
{
|
|
168
|
+
"type": "status",
|
|
169
|
+
"message": "Streaming not available for this agent; results will appear when the run completes.",
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
emit(
|
|
175
|
+
{
|
|
176
|
+
"type": "status",
|
|
177
|
+
"message": f"Running agent '{start_agent_name}'...",
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
result_data = await current_flock.run_async(
|
|
181
|
+
agent=start_agent_name, input=inputs, box_result=False
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if run_store and hasattr(run_store, "add_run_details"):
|
|
185
|
+
run_identifier = (
|
|
186
|
+
result_data.get("run_id", run_id)
|
|
187
|
+
if isinstance(result_data, dict)
|
|
188
|
+
else run_id
|
|
189
|
+
)
|
|
190
|
+
run_store.add_run_details(
|
|
191
|
+
run_id=run_identifier,
|
|
192
|
+
agent_name=start_agent_name,
|
|
193
|
+
inputs=inputs,
|
|
194
|
+
outputs=result_data,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
encoded_result = jsonable_encoder(result_data)
|
|
198
|
+
raw_json = json.dumps(
|
|
199
|
+
encoded_result, indent=2, ensure_ascii=False
|
|
200
|
+
).replace("\\n", "\n")
|
|
201
|
+
|
|
202
|
+
template = templates.get_template("partials/_results_display.html")
|
|
203
|
+
final_html = template.render(
|
|
204
|
+
{
|
|
205
|
+
**template_context,
|
|
206
|
+
"result": result_data,
|
|
207
|
+
"result_raw_json": raw_json,
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
emit(
|
|
212
|
+
{
|
|
213
|
+
"type": "final",
|
|
214
|
+
"html": final_html,
|
|
215
|
+
"result": encoded_result,
|
|
216
|
+
"raw_json": raw_json,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
220
|
+
stream_logger.error(
|
|
221
|
+
"Streamed execution for agent '%s' failed: %s",
|
|
222
|
+
start_agent_name,
|
|
223
|
+
exc,
|
|
224
|
+
exc_info=True,
|
|
225
|
+
)
|
|
226
|
+
await terminate_with_error(f"An error occurred: {html.escape(str(exc))}")
|
|
227
|
+
return
|
|
228
|
+
finally:
|
|
229
|
+
if evaluator is not None:
|
|
230
|
+
if previous_callbacks is not None:
|
|
231
|
+
evaluator.config.stream_callbacks = previous_callbacks
|
|
232
|
+
if original_stream_setting is not None:
|
|
233
|
+
evaluator.config.stream = original_stream_setting
|
|
234
|
+
await finalize_stream()
|
|
235
|
+
|
|
236
|
+
|
|
62
237
|
@router.get("/htmx/execution-form-content", response_class=HTMLResponse)
|
|
63
238
|
async def htmx_get_execution_form_content(
|
|
64
239
|
request: Request,
|
|
@@ -203,35 +378,68 @@ async def htmx_run_flock(
|
|
|
203
378
|
f"<p class='error'>Error processing inputs for {html.escape(str(start_agent_name))}: {html.escape(str(e_parse))}</p>"
|
|
204
379
|
)
|
|
205
380
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
381
|
+
run_id, queue = await execution_stream_manager.create_session()
|
|
382
|
+
stream_url = str(request.url_for("htmx_stream_run", run_id=run_id))
|
|
383
|
+
root_path = request.scope.get("root_path", "")
|
|
209
384
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
385
|
+
template_context = {
|
|
386
|
+
"request": request,
|
|
387
|
+
"feedback_endpoint": f"{root_path}/ui/api/flock/htmx/feedback",
|
|
388
|
+
"share_id": None,
|
|
389
|
+
"flock_name": current_flock_from_state.name,
|
|
390
|
+
"agent_name": start_agent_name,
|
|
391
|
+
"flock_definition": current_flock_from_state.to_yaml(),
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
asyncio.create_task(
|
|
395
|
+
_execute_agent_with_stream(
|
|
396
|
+
run_id=run_id,
|
|
397
|
+
queue=queue,
|
|
398
|
+
start_agent_name=start_agent_name,
|
|
399
|
+
inputs=inputs,
|
|
400
|
+
app_state=request.app.state,
|
|
401
|
+
template_context=template_context,
|
|
402
|
+
)
|
|
216
403
|
)
|
|
217
|
-
|
|
218
|
-
result_data_raw_json_str = raw_json_for_template.replace("\\n", "\n")
|
|
219
|
-
root_path = request.scope.get("root_path", "")
|
|
404
|
+
|
|
220
405
|
return templates.TemplateResponse(
|
|
221
|
-
"partials/
|
|
406
|
+
"partials/_streaming_results_container.html",
|
|
222
407
|
{
|
|
223
408
|
"request": request,
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"feedback_endpoint": f"{root_path}/ui/api/flock/htmx/feedback",
|
|
227
|
-
"share_id": None,
|
|
228
|
-
"flock_name": current_flock_from_state.name,
|
|
409
|
+
"run_id": run_id,
|
|
410
|
+
"stream_url": stream_url,
|
|
229
411
|
"agent_name": start_agent_name,
|
|
230
|
-
"
|
|
412
|
+
"flock_name": current_flock_from_state.name,
|
|
231
413
|
},
|
|
232
414
|
)
|
|
233
415
|
|
|
234
416
|
|
|
417
|
+
@router.get("/htmx/run-stream/{run_id}")
|
|
418
|
+
async def htmx_stream_run(run_id: str):
|
|
419
|
+
"""Server-Sent Events endpoint streaming live agent output."""
|
|
420
|
+
|
|
421
|
+
queue = await execution_stream_manager.get_queue(run_id)
|
|
422
|
+
if queue is None:
|
|
423
|
+
return HTMLResponse(
|
|
424
|
+
"<p class='error'>Streaming session not found or already closed.</p>",
|
|
425
|
+
status_code=404,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
async def event_generator():
|
|
429
|
+
try:
|
|
430
|
+
while True:
|
|
431
|
+
payload = await queue.get()
|
|
432
|
+
if payload is None:
|
|
433
|
+
yield "event: close\ndata: {}\n\n"
|
|
434
|
+
break
|
|
435
|
+
data = json.dumps(payload, ensure_ascii=False)
|
|
436
|
+
yield f"data: {data}\n\n"
|
|
437
|
+
finally:
|
|
438
|
+
await execution_stream_manager.remove_session(run_id)
|
|
439
|
+
|
|
440
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
441
|
+
|
|
442
|
+
|
|
235
443
|
# --- NEW ENDPOINT FOR SHARED RUNS ---
|
|
236
444
|
@router.post("/htmx/run-shared", response_class=HTMLResponse)
|
|
237
445
|
async def htmx_run_shared_flock(
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<div id="streaming-results-wrapper"
|
|
2
|
+
data-run-id="{{ run_id }}"
|
|
3
|
+
data-stream-url="{{ stream_url }}"
|
|
4
|
+
data-target-id="results-display"
|
|
5
|
+
data-agent-name="{{ agent_name }}">
|
|
6
|
+
<header style="margin-bottom: 0.75rem;">
|
|
7
|
+
<h5 style="margin: 0;">Streaming {{ agent_name }}</h5>
|
|
8
|
+
<p style="margin: 0; color: var(--pico-muted-color);">Live output appears below while the agent runs.</p>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<p class="error" data-role="error" hidden></p>
|
|
12
|
+
<div class="stream-output" data-role="output" style="min-height: 8rem; white-space: normal; word-break: break-word; font-family: var(--pico-code-font-family, monospace);">Connecting to agent…</div>
|
|
13
|
+
<div data-role="progress" style="margin-top: 0.5rem;" role="status">
|
|
14
|
+
<progress indeterminate></progress> Streaming response…
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
(function () {
|
|
20
|
+
const script = document.currentScript;
|
|
21
|
+
if (!script) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const wrapper = script.previousElementSibling;
|
|
25
|
+
if (!(wrapper instanceof HTMLElement)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (wrapper.dataset.streamInit === '1') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
wrapper.dataset.streamInit = '1';
|
|
32
|
+
|
|
33
|
+
const streamUrl = wrapper.dataset.streamUrl;
|
|
34
|
+
const runId = wrapper.dataset.runId;
|
|
35
|
+
const targetId = wrapper.dataset.targetId || 'results-display';
|
|
36
|
+
|
|
37
|
+
const outputEl = wrapper.querySelector('[data-role="output"]');
|
|
38
|
+
const errorEl = wrapper.querySelector('[data-role="error"]');
|
|
39
|
+
const progressEl = wrapper.querySelector('[data-role="progress"]');
|
|
40
|
+
|
|
41
|
+
if (!streamUrl || !runId || !(outputEl instanceof HTMLElement)) {
|
|
42
|
+
if (errorEl instanceof HTMLElement) {
|
|
43
|
+
errorEl.textContent = 'Streaming setup failed due to missing metadata.';
|
|
44
|
+
errorEl.hidden = false;
|
|
45
|
+
}
|
|
46
|
+
if (progressEl instanceof HTMLElement) {
|
|
47
|
+
progressEl.hidden = true;
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let plainText = outputEl instanceof HTMLElement ? outputEl.textContent || '' : '';
|
|
53
|
+
let source;
|
|
54
|
+
const fieldValues = new Map();
|
|
55
|
+
let latestStatus = '';
|
|
56
|
+
|
|
57
|
+
function escapeHtml(value) {
|
|
58
|
+
return value.replace(/[&<>"']/g, (char) => ({
|
|
59
|
+
'&': '&',
|
|
60
|
+
'<': '<',
|
|
61
|
+
'>': '>',
|
|
62
|
+
'"': '"',
|
|
63
|
+
"'": ''',
|
|
64
|
+
})[char]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderTableCell(value) {
|
|
68
|
+
return escapeHtml(value).replace(/\n/g, '<br>');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderStream() {
|
|
72
|
+
if (!(outputEl instanceof HTMLElement)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (fieldValues.size > 0) {
|
|
76
|
+
outputEl.style.whiteSpace = 'normal';
|
|
77
|
+
let rows = '';
|
|
78
|
+
fieldValues.forEach((value, field) => {
|
|
79
|
+
rows += `<tr><td class="stream-field" style="white-space: nowrap; padding-right: 1rem; vertical-align: top;">${escapeHtml(field)}</td><td>${renderTableCell(value)}</td></tr>`;
|
|
80
|
+
});
|
|
81
|
+
if (latestStatus) {
|
|
82
|
+
rows += `<tr class="stream-status"><td class="stream-field" style="white-space: nowrap; padding-right: 1rem; vertical-align: top;">status</td><td>${escapeHtml(latestStatus)}</td></tr>`;
|
|
83
|
+
}
|
|
84
|
+
outputEl.innerHTML = `<table class="structured-table streaming-table" style="width:100%; border-collapse: collapse; table-layout: auto;"><tbody>${rows}</tbody></table>`;
|
|
85
|
+
} else {
|
|
86
|
+
outputEl.style.whiteSpace = 'pre-wrap';
|
|
87
|
+
outputEl.textContent = plainText;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function showError(message) {
|
|
92
|
+
if (errorEl instanceof HTMLElement) {
|
|
93
|
+
errorEl.textContent = message;
|
|
94
|
+
errorEl.hidden = false;
|
|
95
|
+
}
|
|
96
|
+
if (progressEl instanceof HTMLElement) {
|
|
97
|
+
progressEl.hidden = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function closeStream() {
|
|
102
|
+
if (source) {
|
|
103
|
+
source.close();
|
|
104
|
+
source = undefined;
|
|
105
|
+
}
|
|
106
|
+
if (progressEl instanceof HTMLElement) {
|
|
107
|
+
progressEl.hidden = true;
|
|
108
|
+
}
|
|
109
|
+
renderStream();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderFinal(html, rawJson) {
|
|
113
|
+
const target = document.getElementById(targetId);
|
|
114
|
+
if (!target) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
target.innerHTML = html;
|
|
118
|
+
if (window.htmx) {
|
|
119
|
+
window.htmx.process(target);
|
|
120
|
+
}
|
|
121
|
+
if (window.Prism) {
|
|
122
|
+
window.Prism.highlightAllUnder(target);
|
|
123
|
+
}
|
|
124
|
+
if (outputEl instanceof HTMLElement) {
|
|
125
|
+
outputEl.textContent = '';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
source = new EventSource(streamUrl);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('Failed to start EventSource', err);
|
|
133
|
+
showError('Failed to connect for streaming.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
source.onmessage = (event) => {
|
|
138
|
+
if (!event.data) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let payload;
|
|
142
|
+
try {
|
|
143
|
+
payload = JSON.parse(event.data);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.warn('Unable to parse streaming payload', err);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
switch (payload.type) {
|
|
150
|
+
case 'token':
|
|
151
|
+
if (typeof payload.chunk === 'string') {
|
|
152
|
+
if (payload.field) {
|
|
153
|
+
const existing = fieldValues.get(payload.field) || '';
|
|
154
|
+
fieldValues.set(payload.field, existing + payload.chunk);
|
|
155
|
+
} else {
|
|
156
|
+
plainText = (plainText === 'Connecting to agent…' ? '' : plainText) + payload.chunk;
|
|
157
|
+
}
|
|
158
|
+
renderStream();
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case 'status':
|
|
162
|
+
if (payload.message) {
|
|
163
|
+
latestStatus = payload.message;
|
|
164
|
+
if (fieldValues.size === 0) {
|
|
165
|
+
plainText = (plainText === 'Connecting to agent…' ? '' : plainText);
|
|
166
|
+
if (plainText && !plainText.endsWith('\n')) {
|
|
167
|
+
plainText += '\n';
|
|
168
|
+
}
|
|
169
|
+
plainText += payload.message + '\n';
|
|
170
|
+
}
|
|
171
|
+
renderStream();
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case 'error':
|
|
175
|
+
showError(payload.message || 'An unexpected error occurred while streaming.');
|
|
176
|
+
closeStream();
|
|
177
|
+
break;
|
|
178
|
+
case 'final':
|
|
179
|
+
closeStream();
|
|
180
|
+
renderFinal(payload.html || '', payload.raw_json || payload.rawJson);
|
|
181
|
+
break;
|
|
182
|
+
case 'complete':
|
|
183
|
+
closeStream();
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
source.onerror = () => {
|
|
191
|
+
showError('Connection lost while streaming.');
|
|
192
|
+
closeStream();
|
|
193
|
+
};
|
|
194
|
+
})();
|
|
195
|
+
</script>
|
|
@@ -27,7 +27,7 @@ flock/cli/yaml_editor.py,sha256=K3N0bh61G1TSDAZDnurqW9e_-hO6CtSQKXQqlDhCjVo,1252
|
|
|
27
27
|
flock/cli/assets/release_notes.md,sha256=bqnk50jxM3w5uY44Dc7MkdT8XmRREFxrVBAG9XCOSSU,4896
|
|
28
28
|
flock/components/__init__.py,sha256=qDcaP0O7_b5RlUEXluqwskpKCkhM73kSMeNXReze63M,963
|
|
29
29
|
flock/components/evaluation/__init__.py,sha256=_M3UlRFeNN90fEny6byt5VdLDE5o5khbd0EPT0o9S9k,303
|
|
30
|
-
flock/components/evaluation/declarative_evaluation_component.py,sha256=
|
|
30
|
+
flock/components/evaluation/declarative_evaluation_component.py,sha256=a-RPCcpdXq66XFSPYite2OYwC_5Cnckf4VQK1ie1E-w,20269
|
|
31
31
|
flock/components/routing/__init__.py,sha256=BH_pFm9T6bUuf8HH4byDJ0dO0fzEVHv9m-ghUdDVdm0,542
|
|
32
32
|
flock/components/routing/conditional_routing_component.py,sha256=WqZLMz-0Dhfb97xvttNrJCIVe6FNMLEQ2m4KQTDpIbI,21374
|
|
33
33
|
flock/components/routing/default_routing_component.py,sha256=ZHt2Kjf-GHB5n7evU5NSGeQJ1Wuims5soeMswqaUb1E,3370
|
|
@@ -35,7 +35,7 @@ flock/components/routing/llm_routing_component.py,sha256=SAaOFjlnhnenM6QEBn3WIpj
|
|
|
35
35
|
flock/components/utility/__init__.py,sha256=JRj932upddjzZMWs1avOupEFr_GZNu21ac66Rhw_XgY,532
|
|
36
36
|
flock/components/utility/memory_utility_component.py,sha256=26Io61bbCGjD8UQ4BltMA5RLkMXp8tQoQmddXbQSrzA,20183
|
|
37
37
|
flock/components/utility/metrics_utility_component.py,sha256=Mck_sFCkfXvNpoSgW2N_WOLnjxazzx8jh79tIx5zJhw,24635
|
|
38
|
-
flock/components/utility/output_utility_component.py,sha256=
|
|
38
|
+
flock/components/utility/output_utility_component.py,sha256=TdHhY5qJJDUk-_LK54zAFMSG_Zafe-UiEkwiJwPjfh0,8063
|
|
39
39
|
flock/core/__init__.py,sha256=OkjsVjRkAB-I6ibeTKVikZ3MxLIcTIzWKphHTbzbr7s,3231
|
|
40
40
|
flock/core/flock.py,sha256=wRycQlGeaq-Vd75mFpPe02qyWTOEyXthT873iBhA3TI,23388
|
|
41
41
|
flock/core/flock_agent.py,sha256=4Vdhyk-rdsPEuN3xYBsLBBsfpklad6bNj_it9r6XIDc,12868
|
|
@@ -43,7 +43,7 @@ flock/core/flock_factory.py,sha256=Z6GJpYXN9_DXuOqvBH9ir0SMoUw78DkWhrhkm90luAQ,2
|
|
|
43
43
|
flock/core/flock_scheduler.py,sha256=ng_s7gyijmc-AmYvBn5rtg61CSUZiIkXPRSlA1xO6VQ,8766
|
|
44
44
|
flock/core/flock_server_manager.py,sha256=tM_nOs37vAbEvxmhwy_DL2JPvgFViWroNxrRSu5MfUQ,4523
|
|
45
45
|
flock/core/agent/__init__.py,sha256=l32KFMJnC_gidMXpAXK8-OX228bWOhNc8OY_NzXm59Q,515
|
|
46
|
-
flock/core/agent/default_agent.py,sha256=
|
|
46
|
+
flock/core/agent/default_agent.py,sha256=924SWDx8axJ57JCWREZuLzV8039Wt_-5WIBNTvx479Y,7483
|
|
47
47
|
flock/core/agent/flock_agent_components.py,sha256=LamOgpRC7wDKuU3d6enDG0UFlNxyKPErLpH7SQ_Pi74,4539
|
|
48
48
|
flock/core/agent/flock_agent_execution.py,sha256=pdOddBGv8y1P89Ix8XFWa1eW9i3bWjOYiQQxeY2K0yo,4217
|
|
49
49
|
flock/core/agent/flock_agent_integration.py,sha256=fnxzEA8-gIopHwD1de8QKt2A7Ilb1iH5Koxk1uiASas,10737
|
|
@@ -125,7 +125,7 @@ flock/core/serialization/json_encoder.py,sha256=gAKj2zU_8wQiNvdkby2hksSA4fbPNwTj
|
|
|
125
125
|
flock/core/serialization/secure_serializer.py,sha256=n5-zRvvXddgJv1FFHsaQ2wuYdL3WUSGPvG_LGaffEJo,6144
|
|
126
126
|
flock/core/serialization/serializable.py,sha256=qlv8TsTqRuklXiNuCMrvro5VKz764xC2i3FlgLJSkdk,12129
|
|
127
127
|
flock/core/serialization/serialization_utils.py,sha256=kxsuWy-8kFBcihHQvSOSNYp96ZPKxBMnasyRTtvIktY,15532
|
|
128
|
-
flock/core/util/cli_helper.py,sha256=
|
|
128
|
+
flock/core/util/cli_helper.py,sha256=upRcEvWdGTrZvaKhi701PtFyW-Wp_B8PY3Gt0QY9szY,50053
|
|
129
129
|
flock/core/util/file_path_utils.py,sha256=Odf7uU32C-x1KNighbNERSiMtkzW4h8laABIoFK7A5M,6246
|
|
130
130
|
flock/core/util/hydrator.py,sha256=qRfVTDBEwqv1-ET2D4s5NI25f-UA_tGsoAmt5jaJMDI,10693
|
|
131
131
|
flock/core/util/input_resolver.py,sha256=t3C98xz_-LGnDH0YeWQyV8yKZrls-_ekOYR-IKrAXDs,6232
|
|
@@ -490,7 +490,7 @@ flock/webapp/app/theme_mapper.py,sha256=QzWwLWpED78oYp3FjZ9zxv1KxCyj43m8MZ0fhfzz
|
|
|
490
490
|
flock/webapp/app/utils.py,sha256=RF8DMKKAj1XPmm4txUdo2OdswI1ATQ7cqUm6G9JFDzA,2942
|
|
491
491
|
flock/webapp/app/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
492
492
|
flock/webapp/app/api/agent_management.py,sha256=5xqO94QjjAYvxImyjKV9EGUQOvo4n3eqs7pGwGPSQJ4,10394
|
|
493
|
-
flock/webapp/app/api/execution.py,sha256=
|
|
493
|
+
flock/webapp/app/api/execution.py,sha256=D-gqeF0JRZ_9TZTuf1-7gLIPL-p8F9Qp66CjVgJKSlw,24758
|
|
494
494
|
flock/webapp/app/api/flock_management.py,sha256=1o-6-36kTnUjI3am_BqLpdrcz0aqFXrxE-hQHIFcCsg,4869
|
|
495
495
|
flock/webapp/app/api/registry_viewer.py,sha256=IoInxJiRR0yFlecG_l2_eRc6l35RQQyEDMG9BcBkipY,1020
|
|
496
496
|
flock/webapp/app/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -542,6 +542,7 @@ flock/webapp/templates/partials/_settings_view.html,sha256=f2h9jnDv8-JRkDzsbk_1o
|
|
|
542
542
|
flock/webapp/templates/partials/_share_chat_link_snippet.html,sha256=N83lNAbkZiDfzZYviKwURPGGErSZhRlxnNzUqXsB7lE,793
|
|
543
543
|
flock/webapp/templates/partials/_share_link_snippet.html,sha256=6en9lOdtu8FwVbtmkJzSQpHQ1WFXHnCbe84FDgAEF3U,1533
|
|
544
544
|
flock/webapp/templates/partials/_sidebar.html,sha256=yfhEcF3xKI5j1c3iq46mU8mmPvgyvCHXe6xT7vsE6KM,4984
|
|
545
|
+
flock/webapp/templates/partials/_streaming_results_container.html,sha256=WVp_IafF1_4puyfs3ueIJ16ehZHiMDBADUXRoZJ1_Yo,6684
|
|
545
546
|
flock/webapp/templates/partials/_structured_data_view.html,sha256=TEaXcMGba9ruxEc_MLxygIO1qWcuSTo1FnosFtGSKWI,2101
|
|
546
547
|
flock/webapp/templates/partials/_theme_preview.html,sha256=THeMYTXzgzHJxzWqaTtUhmJyBZT3saLRAa6wzZa4qnk,1347
|
|
547
548
|
flock/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -551,8 +552,8 @@ flock/workflow/agent_execution_activity.py,sha256=0exwmeWKYXXxdUqDf4YaUVpn0zl06S
|
|
|
551
552
|
flock/workflow/flock_workflow.py,sha256=sKFsRIL_bDGonXSNhK1zwu6UechghC_PihJJMidI-VI,9139
|
|
552
553
|
flock/workflow/temporal_config.py,sha256=3_8O7SDEjMsSMXsWJBfnb6XTp0TFaz39uyzSlMTSF_I,3988
|
|
553
554
|
flock/workflow/temporal_setup.py,sha256=KR6MlWOrpMtv8NyhaIPAsfl4tjobt81OBByQvg8Kw-Y,1948
|
|
554
|
-
flock_core-0.5.
|
|
555
|
-
flock_core-0.5.
|
|
556
|
-
flock_core-0.5.
|
|
557
|
-
flock_core-0.5.
|
|
558
|
-
flock_core-0.5.
|
|
555
|
+
flock_core-0.5.0b22.dist-info/METADATA,sha256=OQrcZ9gIQRqbbhfU0RSIbo7auCybTmemyee39C6XWHc,9997
|
|
556
|
+
flock_core-0.5.0b22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
557
|
+
flock_core-0.5.0b22.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
|
|
558
|
+
flock_core-0.5.0b22.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
|
|
559
|
+
flock_core-0.5.0b22.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|