flock-core 0.5.0b21__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,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.0b21
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
@@ -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.0b21.dist-info/METADATA,sha256=UV0JZGY5epos-_zJXKP6msrOE9M21KBkIlDRVvbXzvk,9997
555
- flock_core-0.5.0b21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
556
- flock_core-0.5.0b21.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
557
- flock_core-0.5.0b21.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
558
- flock_core-0.5.0b21.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,,