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.

@@ -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.abc import Generator
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 (from original implementation)."""
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(dspy.streaming.StreamListener(signature_field_name=name))
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
- console.print("\n")
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
- async for value in stream_generator:
194
- # Handle DSPy streaming artifacts
195
- try:
196
- from dspy.streaming import StatusMessage, StreamResponse
197
- from litellm import ModelResponseStream
198
- import dspy as _d
199
- except Exception:
200
- StatusMessage = object # type: ignore
201
- StreamResponse = object # type: ignore
202
- ModelResponseStream = object # type: ignore
203
- _d = None
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
- chunk = value
218
- text = chunk.choices[0].delta.content or ""
219
- if text:
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
- pass
223
- continue
224
- if _d and isinstance(value, _d.Prediction):
225
- # Final prediction
226
- result_dict, cost, lm_history = self._process_result(value, inputs)
227
- self._cost = cost
228
- self._lm_history = lm_history
229
- final_result = result_dict
230
-
231
- console.print("\n")
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
- final_result = self.filter_reasoning(
398
+
399
+ filtered_result = self.filter_reasoning(
235
400
  final_result, self.config.include_reasoning
236
401
  )
237
- return self.filter_thought_process(
238
- final_result, self.config.include_thought_process
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 = False,
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
  )
@@ -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]'Magpie'[/] milestone - [bold]white duck GmbH[/] - [cyan]https://whiteduck.de[/]\n"
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
- result_data = await run_current_flock_service(
207
- start_agent_name, inputs, request.app.state
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
- raw_json_for_template = json.dumps(
211
- jsonable_encoder(
212
- result_data
213
- ), # ← converts every nested BaseModel, datetime, etc.
214
- indent=2,
215
- ensure_ascii=False,
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
- # Unescape newlines for proper display in HTML <pre> tag
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/_results_display.html",
406
+ "partials/_streaming_results_container.html",
222
407
  {
223
408
  "request": request,
224
- "result": result_data,
225
- "result_raw_json": result_data_raw_json_str,
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
- "flock_definition": current_flock_from_state.to_yaml(),
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
+ '&': '&amp;',
60
+ '<': '&lt;',
61
+ '>': '&gt;',
62
+ '"': '&quot;',
63
+ "'": '&#39;',
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>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flock-core
3
- Version: 0.5.0b19
3
+ Version: 0.5.0b22
4
4
  Summary: Declarative LLM Orchestration at Scale
5
5
  Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
6
6
  License-File: LICENSE
@@ -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=OXuJlH7TTQAy3upg3K68oSr4cS89XlSty2GeM3l6fDs,11606
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=c4K_PL3bGqdyy_v6dnOrmTqV-MkWKAB2w0HS8kzg82k,7613
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=W5ewr4l4adjZjstcCvr6S8r2EnrmH0wFOVEtA8OQprI,6962
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=w8N7UJZOdOFhkcUSSusnL22JDlmJGgWmH0DgO--j-5c,50057
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=OzTjCP5CxAGdD2YrX7vI2qkQejqikX9Jn8_sq2o6yKA,18163
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.0b19.dist-info/METADATA,sha256=IIENs1thHIvHmhkyhR3G62jimiMS7_ImCmVLv3AKAlQ,9997
555
- flock_core-0.5.0b19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
556
- flock_core-0.5.0b19.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
557
- flock_core-0.5.0b19.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
558
- flock_core-0.5.0b19.dist-info/RECORD,,
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,,