flock-core 0.4.526__py3-none-any.whl → 0.4.527__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,8 +1,10 @@
1
1
  # src/flock/webapp/app/api/execution.py
2
2
  import json
3
+ from datetime import datetime
3
4
  from pathlib import Path
4
5
  from typing import TYPE_CHECKING, Any
5
6
 
7
+ import html
6
8
  import markdown2 # Import markdown2
7
9
  from fastapi import ( # Ensure Form and HTTPException are imported
8
10
  APIRouter,
@@ -11,7 +13,7 @@ from fastapi import ( # Ensure Form and HTTPException are imported
11
13
  Request,
12
14
  )
13
15
  from fastapi.encoders import jsonable_encoder
14
- from fastapi.responses import HTMLResponse
16
+ from fastapi.responses import FileResponse, HTMLResponse
15
17
  from fastapi.templating import Jinja2Templates
16
18
 
17
19
  if TYPE_CHECKING:
@@ -41,26 +43,30 @@ router = APIRouter()
41
43
  BASE_DIR = Path(__file__).resolve().parent.parent.parent
42
44
  templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
43
45
 
46
+
44
47
  # Add markdown2 filter to Jinja2 environment for this router
45
48
  def markdown_filter(text):
46
49
  return markdown2.markdown(text, extras=["tables", "fenced-code-blocks"])
47
50
 
48
- templates.env.filters['markdown'] = markdown_filter
51
+
52
+ templates.env.filters["markdown"] = markdown_filter
49
53
 
50
54
 
51
55
  @router.get("/htmx/execution-form-content", response_class=HTMLResponse)
52
56
  async def htmx_get_execution_form_content(
53
57
  request: Request,
54
- current_flock: "Flock | None" = Depends(get_optional_flock_instance) # Use optional if form can show 'no flock'
58
+ current_flock: "Flock | None" = Depends(
59
+ get_optional_flock_instance
60
+ ), # Use optional if form can show 'no flock'
55
61
  ):
56
62
  # flock instance is injected
57
63
  return templates.TemplateResponse(
58
64
  "partials/_execution_form.html",
59
65
  {
60
66
  "request": request,
61
- "flock": current_flock, # Pass the injected flock instance
67
+ "flock": current_flock, # Pass the injected flock instance
62
68
  "input_fields": [],
63
- "selected_agent_name": None, # Form starts with no agent selected
69
+ "selected_agent_name": None, # Form starts with no agent selected
64
70
  },
65
71
  )
66
72
 
@@ -69,7 +75,9 @@ async def htmx_get_execution_form_content(
69
75
  async def htmx_get_agent_input_form(
70
76
  request: Request,
71
77
  agent_name: str,
72
- current_flock: "Flock" = Depends(get_flock_instance) # Expect flock to be loaded
78
+ current_flock: "Flock" = Depends(
79
+ get_flock_instance
80
+ ), # Expect flock to be loaded
73
81
  ):
74
82
  # flock instance is injected
75
83
  agent = current_flock.agents.get(agent_name)
@@ -88,12 +96,21 @@ async def htmx_get_agent_input_form(
88
96
  "type": type_str.lower(),
89
97
  "description": description or "",
90
98
  }
91
- if "bool" in field_info["type"]: field_info["html_type"] = "checkbox"
92
- elif "int" in field_info["type"] or "float" in field_info["type"]: field_info["html_type"] = "number"
93
- elif "list" in field_info["type"] or "dict" in field_info["type"]:
99
+ if "bool" in field_info["type"]:
100
+ field_info["html_type"] = "checkbox"
101
+ elif (
102
+ "int" in field_info["type"] or "float" in field_info["type"]
103
+ ):
104
+ field_info["html_type"] = "number"
105
+ elif (
106
+ "list" in field_info["type"] or "dict" in field_info["type"]
107
+ ):
94
108
  field_info["html_type"] = "textarea"
95
- field_info["placeholder"] = f"Enter JSON for {field_info['type']}"
96
- else: field_info["html_type"] = "text"
109
+ field_info["placeholder"] = (
110
+ f"Enter JSON for {field_info['type']}"
111
+ )
112
+ else:
113
+ field_info["html_type"] = "text"
97
114
  input_fields.append(field_info)
98
115
  except Exception as e:
99
116
  return HTMLResponse(
@@ -109,7 +126,9 @@ async def htmx_get_agent_input_form(
109
126
  async def htmx_run_flock(
110
127
  request: Request,
111
128
  ):
112
- current_flock_from_state: Flock | None = getattr(request.app.state, 'flock_instance', None)
129
+ current_flock_from_state: Flock | None = getattr(
130
+ request.app.state, "flock_instance", None
131
+ )
113
132
  logger = get_flock_logger("webapp.execution.regular_run")
114
133
 
115
134
  if not current_flock_from_state:
@@ -125,7 +144,9 @@ async def htmx_run_flock(
125
144
 
126
145
  agent = current_flock_from_state.agents.get(start_agent_name)
127
146
  if not agent:
128
- logger.error(f"HTMX Run (Regular): Agent '{start_agent_name}' not found in Flock '{current_flock_from_state.name}'.")
147
+ logger.error(
148
+ f"HTMX Run (Regular): Agent '{start_agent_name}' not found in Flock '{current_flock_from_state.name}'."
149
+ )
129
150
  return HTMLResponse(
130
151
  f"<p class='error'>Agent '{start_agent_name}' not found in the current Flock.</p>"
131
152
  )
@@ -137,30 +158,57 @@ async def htmx_run_flock(
137
158
  for name, type_str, _ in parsed_spec:
138
159
  form_field_name = f"agent_input_{name}"
139
160
  raw_value = form_data.get(form_field_name)
140
- if raw_value is None and "bool" in type_str.lower(): inputs[name] = False; continue
141
- if raw_value is None: inputs[name] = None; continue
142
- if "int" in type_str.lower(): inputs[name] = int(raw_value)
143
- elif "float" in type_str.lower(): inputs[name] = float(raw_value)
144
- elif "bool" in type_str.lower(): inputs[name] = raw_value.lower() in ["true", "on", "1", "yes"]
145
- elif "list" in type_str.lower() or "dict" in type_str.lower(): inputs[name] = json.loads(raw_value)
146
- else: inputs[name] = raw_value
161
+ if raw_value is None and "bool" in type_str.lower():
162
+ inputs[name] = False
163
+ continue
164
+ if raw_value is None:
165
+ inputs[name] = None
166
+ continue
167
+ if "int" in type_str.lower():
168
+ inputs[name] = int(raw_value)
169
+ elif "float" in type_str.lower():
170
+ inputs[name] = float(raw_value)
171
+ elif "bool" in type_str.lower():
172
+ inputs[name] = raw_value.lower() in [
173
+ "true",
174
+ "on",
175
+ "1",
176
+ "yes",
177
+ ]
178
+ elif "list" in type_str.lower() or "dict" in type_str.lower():
179
+ inputs[name] = json.loads(raw_value)
180
+ else:
181
+ inputs[name] = raw_value
147
182
  except ValueError as ve:
148
- logger.error(f"HTMX Run (Regular): Input parsing error for agent '{start_agent_name}': {ve}", exc_info=True)
149
- return HTMLResponse(f"<p class='error'>Invalid input format: {ve!s}</p>")
183
+ logger.error(
184
+ f"HTMX Run (Regular): Input parsing error for agent '{start_agent_name}': {ve}",
185
+ exc_info=True,
186
+ )
187
+ return HTMLResponse(
188
+ "<p class='error'>Invalid input format. Please check your input and try again.</p>"
189
+ )
150
190
  except Exception as e_parse:
151
- logger.error(f"HTMX Run (Regular): Error processing inputs for '{start_agent_name}': {e_parse}", exc_info=True)
152
- return HTMLResponse(f"<p class='error'>Error processing inputs for {start_agent_name}: {e_parse}</p>")
153
-
154
- result_data = await run_current_flock_service(start_agent_name, inputs, request.app.state)
191
+ logger.error(
192
+ f"HTMX Run (Regular): Error processing inputs for '{start_agent_name}': {e_parse}",
193
+ exc_info=True,
194
+ )
195
+ return HTMLResponse(
196
+ f"<p class='error'>Error processing inputs for {html.escape(str(start_agent_name))}: {html.escape(str(e_parse))}</p>"
197
+ )
155
198
 
199
+ result_data = await run_current_flock_service(
200
+ start_agent_name, inputs, request.app.state
201
+ )
156
202
 
157
203
  raw_json_for_template = json.dumps(
158
- jsonable_encoder(result_data), # ← converts every nested BaseModel, datetime, etc.
204
+ jsonable_encoder(
205
+ result_data
206
+ ), # ← converts every nested BaseModel, datetime, etc.
159
207
  indent=2,
160
- ensure_ascii=False
208
+ ensure_ascii=False,
161
209
  )
162
210
  # Unescape newlines for proper display in HTML <pre> tag
163
- result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
211
+ result_data_raw_json_str = raw_json_for_template.replace("\\n", "\n")
164
212
  root_path = request.scope.get("root_path", "")
165
213
  return templates.TemplateResponse(
166
214
  "partials/_results_display.html",
@@ -173,7 +221,7 @@ async def htmx_run_flock(
173
221
  "flock_name": current_flock_from_state.name,
174
222
  "agent_name": start_agent_name,
175
223
  "flock_definition": current_flock_from_state.to_yaml(),
176
- }
224
+ },
177
225
  )
178
226
 
179
227
 
@@ -189,54 +237,98 @@ async def htmx_run_shared_flock(
189
237
 
190
238
  if not start_agent_name:
191
239
  shared_logger.warning("HTMX Run Shared: Starting agent not selected.")
192
- return HTMLResponse("<p class='error'>Starting agent not selected for shared run.</p>")
240
+ return HTMLResponse(
241
+ "<p class='error'>Starting agent not selected for shared run.</p>"
242
+ )
193
243
 
194
244
  inputs: dict[str, Any] = {}
195
245
  try:
196
- shared_flocks_store = getattr(request.app.state, 'shared_flocks', {})
246
+ shared_flocks_store = getattr(request.app.state, "shared_flocks", {})
197
247
  temp_flock = shared_flocks_store.get(share_id)
198
248
 
199
249
  if not temp_flock:
200
- shared_logger.error(f"HTMX Run Shared: Flock instance for share_id '{share_id}' not found in app.state.")
201
- return HTMLResponse(f"<p class='error'>Shared session not found or expired. Please try accessing the shared link again.</p>")
250
+ shared_logger.error(
251
+ f"HTMX Run Shared: Flock instance for share_id '{share_id}' not found in app.state."
252
+ )
253
+ return HTMLResponse(
254
+ f"<p class='error'>Shared session not found or expired. Please try accessing the shared link again.</p>"
255
+ )
202
256
 
203
- shared_logger.info(f"HTMX Run Shared: Successfully retrieved pre-loaded Flock '{temp_flock.name}' for agent '{start_agent_name}' (share_id: {share_id}).")
257
+ shared_logger.info(
258
+ f"HTMX Run Shared: Successfully retrieved pre-loaded Flock '{temp_flock.name}' for agent '{start_agent_name}' (share_id: {share_id})."
259
+ )
204
260
 
205
261
  agent = temp_flock.agents.get(start_agent_name)
206
262
  if not agent:
207
- shared_logger.error(f"HTMX Run Shared: Agent '{start_agent_name}' not found in shared Flock '{temp_flock.name}'.")
208
- return HTMLResponse(f"<p class='error'>Agent '{start_agent_name}' not found in the provided shared Flock definition.</p>")
263
+ shared_logger.error(
264
+ f"HTMX Run Shared: Agent '{start_agent_name}' not found in shared Flock '{temp_flock.name}'."
265
+ )
266
+ return HTMLResponse(
267
+ f"<p class='error'>Agent '{start_agent_name}' not found in the provided shared Flock definition.</p>"
268
+ )
209
269
 
210
270
  if agent.input and isinstance(agent.input, str):
211
271
  parsed_spec = parse_schema(agent.input)
212
272
  for name, type_str, _ in parsed_spec:
213
273
  form_field_name = f"agent_input_{name}"
214
274
  raw_value = form_data.get(form_field_name)
215
- if raw_value is None and "bool" in type_str.lower(): inputs[name] = False; continue
216
- if raw_value is None: inputs[name] = None; continue
217
- if "int" in type_str.lower(): inputs[name] = int(raw_value)
218
- elif "float" in type_str.lower(): inputs[name] = float(raw_value)
219
- elif "bool" in type_str.lower(): inputs[name] = raw_value.lower() in ["true", "on", "1", "yes"]
220
- elif "list" in type_str.lower() or "dict" in type_str.lower(): inputs[name] = json.loads(raw_value)
221
- else: inputs[name] = raw_value
222
-
223
- shared_logger.info(f"HTMX Run Shared: Executing agent '{start_agent_name}' in pre-loaded Flock '{temp_flock.name}'. Inputs: {list(inputs.keys())}")
224
- result_data = await temp_flock.run_async(start_agent=start_agent_name, input=inputs, box_result=False)
275
+ if raw_value is None and "bool" in type_str.lower():
276
+ inputs[name] = False
277
+ continue
278
+ if raw_value is None:
279
+ inputs[name] = None
280
+ continue
281
+ if "int" in type_str.lower():
282
+ inputs[name] = int(raw_value)
283
+ elif "float" in type_str.lower():
284
+ inputs[name] = float(raw_value)
285
+ elif "bool" in type_str.lower():
286
+ inputs[name] = raw_value.lower() in [
287
+ "true",
288
+ "on",
289
+ "1",
290
+ "yes",
291
+ ]
292
+ elif "list" in type_str.lower() or "dict" in type_str.lower():
293
+ inputs[name] = json.loads(raw_value)
294
+ else:
295
+ inputs[name] = raw_value
296
+
297
+ shared_logger.info(
298
+ f"HTMX Run Shared: Executing agent '{start_agent_name}' in pre-loaded Flock '{temp_flock.name}'. Inputs: {list(inputs.keys())}"
299
+ )
300
+ result_data = await temp_flock.run_async(
301
+ start_agent=start_agent_name, input=inputs, box_result=False
302
+ )
225
303
  raw_json_for_template = json.dumps(
226
- jsonable_encoder(result_data), # ← converts every nested BaseModel, datetime, etc.
304
+ jsonable_encoder(
305
+ result_data
306
+ ), # ← converts every nested BaseModel, datetime, etc.
227
307
  indent=2,
228
- ensure_ascii=False
308
+ ensure_ascii=False,
229
309
  )
230
310
  # Unescape newlines for proper display in HTML <pre> tag
231
- result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
232
- shared_logger.info(f"HTMX Run Shared: Agent '{start_agent_name}' executed. Result keys: {list(result_data.keys()) if isinstance(result_data, dict) else 'N/A'}")
311
+ result_data_raw_json_str = raw_json_for_template.replace("\\n", "\n")
312
+ shared_logger.info(
313
+ f"HTMX Run Shared: Agent '{start_agent_name}' executed. Result keys: {list(result_data.keys()) if isinstance(result_data, dict) else 'N/A'}"
314
+ )
233
315
 
234
316
  except ValueError as ve:
235
- shared_logger.error(f"HTMX Run Shared: Input parsing error for '{start_agent_name}' (share_id: {share_id}): {ve}", exc_info=True)
236
- return HTMLResponse(f"<p class='error'>Invalid input format: {ve!s}</p>")
317
+ shared_logger.error(
318
+ f"HTMX Run Shared: Input parsing error for '{start_agent_name}' (share_id: {share_id}): {ve}",
319
+ exc_info=True,
320
+ )
321
+ return HTMLResponse(
322
+ f"<p class='error'>Invalid input format: {ve!s}</p>"
323
+ )
237
324
  except Exception as e:
238
- shared_logger.error(f"HTMX Run Shared: Error during execution for '{start_agent_name}' (share_id: {share_id}): {e}", exc_info=True)
239
- return HTMLResponse(f"<p class='error'>An unexpected error occurred: {e!s}</p>")
325
+ shared_logger.error(
326
+ f"HTMX Run Shared: Error during execution for '{start_agent_name}' (share_id: {share_id}): {e}",
327
+ exc_info=True,
328
+ )
329
+ return HTMLResponse(
330
+ f"<p class='error'>An unexpected error occurred: {e!s}</p>"
331
+ )
240
332
  root_path = request.scope.get("root_path", "")
241
333
 
242
334
  return templates.TemplateResponse(
@@ -250,9 +342,10 @@ async def htmx_run_shared_flock(
250
342
  "flock_name": temp_flock.name,
251
343
  "agent_name": start_agent_name,
252
344
  "flock_definition": temp_flock.to_yaml(),
253
- }
345
+ },
254
346
  )
255
347
 
348
+
256
349
  # --- Feedback endpoints ---
257
350
  @router.post("/htmx/feedback", response_class=HTMLResponse)
258
351
  async def htmx_submit_feedback(
@@ -308,4 +401,190 @@ async def htmx_submit_feedback_shared(
308
401
  flock_definition=flock_definition,
309
402
  )
310
403
  await store.save_feedback(record)
311
- return HTMLResponse("<p>🙏 Feedback received for shared run – thank you!</p>")
404
+ return HTMLResponse(
405
+ "<p>🙏 Feedback received for shared run – thank you!</p>"
406
+ )
407
+
408
+
409
+ @router.get("/htmx/feedback-download/", response_class=FileResponse)
410
+ async def chat_feedback_download_all(
411
+ request: Request,
412
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store),
413
+ ):
414
+ """Download all feedback records for all agents in the current flock as a CSV file.
415
+
416
+ This function iterates through all agents in the currently loaded flock and collects
417
+ all feedback records for each agent, then exports them as a single CSV file.
418
+
419
+ Args:
420
+ request: The FastAPI request object
421
+ store: The shared link store interface dependency
422
+
423
+ Returns:
424
+ FileResponse: CSV file containing all feedback records for all agents
425
+
426
+ Raises:
427
+ HTTPException: If no flock is loaded or no agents are found in the flock
428
+ """
429
+ import csv
430
+ import tempfile
431
+ from pathlib import Path
432
+
433
+ # Get the current flock instance to retrieve all agent names
434
+ from flock.core.flock import Flock
435
+
436
+ current_flock_instance: Flock | None = getattr(
437
+ request.app.state, "flock_instance", None
438
+ )
439
+
440
+ if not current_flock_instance:
441
+ # If no flock is loaded, return an error response
442
+ from fastapi import HTTPException
443
+
444
+ raise HTTPException(
445
+ status_code=400, detail="No Flock loaded to download feedback for"
446
+ )
447
+
448
+ # Get all agent names from the current flock
449
+ all_agent_names = list(current_flock_instance.agents.keys())
450
+
451
+ if not all_agent_names:
452
+ # If no agents in the flock, return an error response
453
+ from fastapi import HTTPException
454
+
455
+ raise HTTPException(
456
+ status_code=400, detail="No agents found in the current Flock"
457
+ )
458
+
459
+ # Collect all feedback records from all agents
460
+ all_records = []
461
+ for agent_name in all_agent_names:
462
+ records = await store.get_all_feedback_records_for_agent(
463
+ agent_name=agent_name
464
+ )
465
+ all_records.extend(records)
466
+
467
+ # Create a temporary CSV file
468
+ temp_dir = tempfile.gettempdir()
469
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
470
+ csv_filename = f"flock_feedback_all_agents_{timestamp}.csv"
471
+ csv_path = Path(temp_dir) / csv_filename
472
+
473
+ # Define CSV headers based on FeedbackRecord fields
474
+ headers = [
475
+ "feedback_id",
476
+ "share_id",
477
+ "context_type",
478
+ "reason",
479
+ "expected_response",
480
+ "actual_response",
481
+ "created_at",
482
+ "flock_name",
483
+ "agent_name",
484
+ "flock_definition",
485
+ ]
486
+
487
+ # Write records to CSV
488
+ with open(csv_path, "w", newline="", encoding="utf-8") as csvfile:
489
+ writer = csv.DictWriter(csvfile, fieldnames=headers)
490
+ writer.writeheader()
491
+
492
+ for record in all_records:
493
+ # Convert the Pydantic model to dict and ensure all fields are present
494
+ row_data = {}
495
+ for header in headers:
496
+ value = getattr(record, header, None)
497
+ # Convert datetime to ISO string for CSV
498
+ if header == "created_at" and value:
499
+ row_data[header] = (
500
+ value.isoformat()
501
+ if isinstance(value, datetime)
502
+ else str(value)
503
+ )
504
+ else:
505
+ row_data[header] = str(value) if value is not None else ""
506
+ writer.writerow(row_data)
507
+
508
+ # Return FileResponse with the CSV file
509
+ return FileResponse(
510
+ path=str(csv_path),
511
+ filename=csv_filename,
512
+ media_type="text/csv",
513
+ headers={"Content-Disposition": f"attachment; filename={csv_filename}"},
514
+ )
515
+
516
+
517
+ @router.get("/htmx/feedback-download/{agent_name}", response_class=FileResponse)
518
+ async def chat_feedback_download(
519
+ request: Request,
520
+ agent_name: str,
521
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store),
522
+ ):
523
+ """Download all feedback records for a specific agent as a CSV file.
524
+
525
+ Args:
526
+ request: The FastAPI request object
527
+ agent_name: Name of the agent to download feedback for
528
+ store: The shared link store interface dependency
529
+
530
+ Returns:
531
+ FileResponse: CSV file containing all feedback records for the specified agent
532
+ """
533
+ import csv
534
+ import tempfile
535
+ from pathlib import Path
536
+ from werkzeug.utils import secure_filename
537
+
538
+ records = await store.get_all_feedback_records_for_agent(
539
+ agent_name=agent_name
540
+ )
541
+
542
+ # Create a temporary CSV file
543
+ temp_dir = tempfile.gettempdir()
544
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
545
+ safe_agent_name = secure_filename(agent_name)
546
+ csv_filename = f"flock_feedback_{safe_agent_name}_{timestamp}.csv"
547
+ csv_path = Path(temp_dir) / csv_filename
548
+
549
+ # Define CSV headers based on FeedbackRecord fields
550
+ headers = [
551
+ "feedback_id",
552
+ "share_id",
553
+ "context_type",
554
+ "reason",
555
+ "expected_response",
556
+ "actual_response",
557
+ "created_at",
558
+ "flock_name",
559
+ "agent_name",
560
+ "flock_definition",
561
+ ]
562
+
563
+ # Write records to CSV
564
+ with open(csv_path, "w", newline="", encoding="utf-8") as csvfile:
565
+ writer = csv.DictWriter(csvfile, fieldnames=headers)
566
+ writer.writeheader()
567
+
568
+ for record in records:
569
+ # Convert the Pydantic model to dict and ensure all fields are present
570
+ row_data = {}
571
+ for header in headers:
572
+ value = getattr(record, header, None)
573
+ # Convert datetime to ISO string for CSV
574
+ if header == "created_at" and value:
575
+ row_data[header] = (
576
+ value.isoformat()
577
+ if isinstance(value, datetime)
578
+ else str(value)
579
+ )
580
+ else:
581
+ row_data[header] = str(value) if value is not None else ""
582
+ writer.writerow(row_data)
583
+
584
+ # Return FileResponse with the CSV file
585
+ return FileResponse(
586
+ path=str(csv_path),
587
+ filename=csv_filename,
588
+ media_type="text/csv",
589
+ headers={"Content-Disposition": f"attachment; filename={csv_filename}"},
590
+ )
flock/webapp/app/chat.py CHANGED
@@ -7,7 +7,7 @@ from uuid import uuid4
7
7
 
8
8
  import markdown2 # Added for Markdown to HTML conversion
9
9
  from fastapi import APIRouter, Depends, Form, Request, Response
10
- from fastapi.responses import HTMLResponse
10
+ from fastapi.responses import FileResponse, HTMLResponse
11
11
  from pydantic import BaseModel
12
12
 
13
13
  from flock.core.flock import Flock
@@ -572,7 +572,6 @@ async def chat_feedback(request: Request,
572
572
  headers = {"HX-Trigger": json.dumps(toast_event)}
573
573
  return Response(status_code=204, headers=headers)
574
574
 
575
-
576
575
  @router.post("/chat/htmx/feedback-shared", response_class=HTMLResponse, include_in_schema=False)
577
576
  async def chat_feedback_shared(request: Request,
578
577
  share_id: str = Form(...),
@@ -602,3 +601,144 @@ async def chat_feedback_shared(request: Request,
602
601
  }
603
602
  headers = {"HX-Trigger": json.dumps(toast_event)}
604
603
  return Response(status_code=204, headers=headers)
604
+
605
+ @router.get("/chat/htmx/feedback-download/", response_class=FileResponse, include_in_schema=False)
606
+ async def chat_feedback_download_all(
607
+ request: Request,
608
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
609
+ ):
610
+ """Download all feedback records for all agents."""
611
+ import csv
612
+ import tempfile
613
+ from pathlib import Path
614
+
615
+
616
+ current_flock_instance: Flock | None = getattr(
617
+ request.app.state, "flock_instance", None
618
+ )
619
+
620
+ if not current_flock_instance:
621
+ from fastapi import HTTPException
622
+
623
+ raise HTTPException(
624
+ status_code=400, detail="No Flock loaded to download feedback for"
625
+ )
626
+
627
+ all_agent_names = list(current_flock_instance.agents.keys)
628
+
629
+ if not all_agent_names:
630
+ from fastapi import HTTPException
631
+
632
+ raise HTTPException(
633
+ status_code=400,
634
+ detail="No agents found in the current Flock"
635
+ )
636
+
637
+ all_records = []
638
+
639
+ for agent_name in all_agent_names:
640
+ records = await store.get_all_feedback_records_for_agent(
641
+ agent_name=agent_name
642
+ )
643
+
644
+ all_records.extend(records)
645
+
646
+ temp_dir = tempfile.gettempdir()
647
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
648
+ csv_filename = f"flock_feedback_all_agents_{timestamp}.csv"
649
+ csv_path = Path(temp_dir) / csv_filename
650
+
651
+ headers = [
652
+ "feedback_id",
653
+ "share_id",
654
+ "context_type",
655
+ "reason",
656
+ "excpected_response",
657
+ "actual_response",
658
+ "created_at",
659
+ "flock_name",
660
+ "agent_name",
661
+ "flock_definition"
662
+ ]
663
+
664
+ with open(csv_path, "w", newline="", encoding="utf-8") as csvfile:
665
+ writer = csv.DictWriter(csvfile, fieldnames=headers)
666
+ writer.writeheader()
667
+
668
+ for record in all_records:
669
+ row_data = {}
670
+ for header in headers:
671
+ value = getattr(record, header, None)
672
+ if header == "created_at" and value:
673
+ row_data[header] = (
674
+ value.isoformat()
675
+ if isinstance(value, datetime)
676
+ else str(value)
677
+ )
678
+ else:
679
+ row_data[header] = str(value) if value is not None else ""
680
+ writer.writerow(row_data)
681
+
682
+ return FileResponse(
683
+ path=str(csv_path),
684
+ filename=csv_filename,
685
+ media_type="text/csv",
686
+ headers={"Content-Disposition": f"attachment; filename={csv_filename}"}
687
+ )
688
+
689
+ @router.get("/chat/htmx/feedback-download/{agent_name}", response_class=FileResponse, include_in_schema=False)
690
+ async def chat_feedback_download(
691
+ request: Request,
692
+ agent_name: str,
693
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
694
+ ):
695
+ import csv
696
+ import tempfile
697
+ from pathlib import Path
698
+
699
+ records = await store.get_all_feedback_records_for_agent(agent_name=agent_name)
700
+
701
+ # Create a temporary CSV file
702
+ temp_dir = tempfile.gettempdir()
703
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
704
+ csv_filename = f"flock_feedback_{timestamp}.csv"
705
+ csv_path = Path(temp_dir) / csv_filename
706
+
707
+ # Define CSV headers based on FeedbackRecord fields
708
+ headers = [
709
+ "feedback_id",
710
+ "share_id",
711
+ "context_type",
712
+ "reason",
713
+ "expected_response",
714
+ "actual_response",
715
+ "created_at",
716
+ "flock_name",
717
+ "agent_name",
718
+ "flock_definition"
719
+ ]
720
+
721
+ # Write records to CSV
722
+ with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
723
+ writer = csv.DictWriter(csvfile, fieldnames=headers)
724
+ writer.writeheader()
725
+
726
+ for record in records:
727
+ # Convert the Pydantic model to dict and ensure all fields are present
728
+ row_data = {}
729
+ for header in headers:
730
+ value = getattr(record, header, None)
731
+ # Convert datetime to ISO string for CSV
732
+ if header == "created_at" and value:
733
+ row_data[header] = value.isoformat() if isinstance(value, datetime) else str(value)
734
+ else:
735
+ row_data[header] = str(value) if value is not None else ""
736
+ writer.writerow(row_data)
737
+
738
+ # Return FileResponse with the CSV file
739
+ return FileResponse(
740
+ path=str(csv_path),
741
+ filename=csv_filename,
742
+ media_type="text/csv",
743
+ headers={"Content-Disposition": f"attachment; filename={csv_filename}"}
744
+ )
@@ -56,6 +56,16 @@ class SharedLinkStoreInterface(ABC):
56
56
  """Persist a feedback record."""
57
57
  pass
58
58
 
59
+ @abstractmethod
60
+ async def get_feedback(self, id: str) -> FeedbackRecord | None:
61
+ """Get a single feedback record."""
62
+ pass
63
+
64
+ @abstractmethod
65
+ async def get_all_feedback_records_for_agent(self, agent_name: str) -> list[FeedbackRecord]:
66
+ """Get all feedback records for a given agent."""
67
+ pass
68
+
59
69
  class SQLiteSharedLinkStore(SharedLinkStoreInterface):
60
70
  """SQLite implementation for storing and retrieving shared link configurations."""
61
71
 
@@ -199,6 +209,71 @@ class SQLiteSharedLinkStore(SharedLinkStoreInterface):
199
209
  return False # Or raise
200
210
 
201
211
  # ----------------------- Feedback methods -----------------------
212
+ async def get_feedback(self, id: str) -> FeedbackRecord | None:
213
+ """Retrieve a single feedback record from SQLite."""
214
+ try:
215
+ async with aiosqlite.connect(self.db_path) as db, db.execute(
216
+ """SELECT
217
+ feedback_id, share_id, context_type, reason,
218
+ expected_response, actual_response, flock_name, agent_name, flock_definition, created_at
219
+ FROM feedback WHERE feedback_id = ?""",
220
+ (id,)
221
+ ) as cursor:
222
+ row = await cursor.fetchone()
223
+
224
+ if row:
225
+ logger.debug(f"Retrieved feedback record for ID: {id}")
226
+ return FeedbackRecord(
227
+ feedback_id=row[0],
228
+ share_id=row[1],
229
+ context_type=row[2],
230
+ reason=row[3],
231
+ expected_response=row[4],
232
+ actual_response=row[5],
233
+ flock_name=row[6],
234
+ agent_name=row[7],
235
+ flock_definition=row[8],
236
+ created_at=row[9], # SQLite stores as TEXT, Pydantic will parse from ISO format
237
+ )
238
+
239
+ logger.debug(f"No feedback record found for ID: {id}")
240
+ return None
241
+ except sqlite3.Error as e:
242
+ logger.error(f"SQLite error retrieving feedback for ID {id}: {e}", exc_info=True)
243
+ return None # Or raise, depending on desired error handling
244
+
245
+ async def get_all_feedback_records_for_agent(self, agent_name: str) -> list[FeedbackRecord]:
246
+ """Retrieve all feedback records from SQLite."""
247
+ try:
248
+ async with aiosqlite.connect(self.db_path) as db, db.execute(
249
+ """SELECT
250
+ feedback_id, share_id, context_type, reason,
251
+ expected_response, actual_response, flock_name, agent_name, flock_definition, created_at
252
+ FROM feedback WHERE agent_name = ? ORDER BY created_at DESC""",
253
+ (agent_name,)
254
+ ) as cursor:
255
+ rows = await cursor.fetchall()
256
+
257
+ records = []
258
+ for row in rows:
259
+ records.append(FeedbackRecord(
260
+ feedback_id=row[0],
261
+ share_id=row[1],
262
+ context_type=row[2],
263
+ reason=row[3],
264
+ expected_response=row[4],
265
+ actual_response=row[5],
266
+ flock_name=row[6],
267
+ agent_name=row[7],
268
+ flock_definition=row[8],
269
+ created_at=row[9], # SQLite stores as TEXT, Pydantic will parse from ISO format
270
+ ))
271
+
272
+ logger.debug(f"Retrieved {len(records)} feedback records")
273
+ return records
274
+ except sqlite3.Error as e:
275
+ logger.error(f"SQLite error retrieving all feedback records: {e}", exc_info=True)
276
+ return [] # Return empty list on error
202
277
 
203
278
  async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
204
279
  """Persist a feedback record to SQLite."""
@@ -366,9 +441,7 @@ class AzureTableSharedLinkStore(SharedLinkStoreInterface):
366
441
 
367
442
  # -------------------------------------------------------- save_feedback --
368
443
  async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
369
- """Persist a feedback record. If a flock_definition is present, upload it as a blob and
370
- store only a reference in the table row to avoid oversized entities (64 KiB limit).
371
- """
444
+ """Persist a feedback record. If a flock_definition is present, upload it as a blob and store only a reference in the table row to avoid oversized entities (64 KiB limit)."""
372
445
  tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
373
446
 
374
447
  # Core entity fields (avoid dumping the full Pydantic model – too many columns / large value)
@@ -404,17 +477,92 @@ class AzureTableSharedLinkStore(SharedLinkStoreInterface):
404
477
  f" → blob '{entity['flock_blob_name']}'" if "flock_blob_name" in entity else "")
405
478
  return record
406
479
 
480
+ # -------------------------------------------------------- get_feedback --
481
+ async def get_feedback(self, id: str) -> FeedbackRecord | None:
482
+ """Retrieve a single feedback record from Azure Table Storage."""
483
+ tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
484
+ try:
485
+ entity = await tbl_client.get_entity("feedback", id)
486
+ except ResourceNotFoundError:
487
+ logger.debug("No feedback record found for ID: %s", id)
488
+ return None
489
+
490
+ # Get flock_definition from blob if it exists
491
+ flock_definition = None
492
+ if "flock_blob_name" in entity:
493
+ blob_name = entity["flock_blob_name"]
494
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
495
+ try:
496
+ blob_bytes = await (await blob_client.download_blob()).readall()
497
+ flock_definition = blob_bytes.decode()
498
+ except Exception as e:
499
+ logger.error("Cannot download blob '%s' for feedback_id=%s: %s",
500
+ blob_name, id, e, exc_info=True)
501
+ # Continue without flock_definition rather than failing
502
+
503
+ return FeedbackRecord(
504
+ feedback_id=id,
505
+ share_id=entity.get("share_id"),
506
+ context_type=entity["context_type"],
507
+ reason=entity["reason"],
508
+ expected_response=entity.get("expected_response"),
509
+ actual_response=entity.get("actual_response"),
510
+ flock_name=entity.get("flock_name"),
511
+ agent_name=entity.get("agent_name"),
512
+ flock_definition=flock_definition,
513
+ created_at=entity["created_at"],
514
+ )
515
+
516
+
517
+ # ------------------------------------------------ get_all_feedback_records --
518
+ async def get_all_feedback_records_for_agent(self, agent_name: str) -> list[FeedbackRecord]:
519
+ """Retrieve all feedback records from Azure Table Storage for a specific agent."""
520
+ tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
521
+
522
+ # Use Azure Table Storage filtering to only get records for the specified agent
523
+ filter_query = f"agent_name eq '{agent_name}'"
524
+
525
+ records = []
526
+ async for entity in tbl_client.query_entities(filter_query):
527
+ # Get flock_definition from blob if it exists
528
+ flock_definition = None
529
+ if "flock_blob_name" in entity:
530
+ blob_name = entity["flock_blob_name"]
531
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
532
+ try:
533
+ blob_bytes = await (await blob_client.download_blob()).readall()
534
+ flock_definition = blob_bytes.decode()
535
+ except Exception as e:
536
+ logger.error("Cannot download blob '%s' for feedback_id=%s: %s",
537
+ blob_name, entity["RowKey"], e, exc_info=True)
538
+ # Continue without flock_definition rather than failing
539
+
540
+ records.append(FeedbackRecord(
541
+ feedback_id=entity["RowKey"],
542
+ share_id=entity.get("share_id"),
543
+ context_type=entity["context_type"],
544
+ reason=entity["reason"],
545
+ expected_response=entity.get("expected_response"),
546
+ actual_response=entity.get("actual_response"),
547
+ flock_name=entity.get("flock_name"),
548
+ agent_name=entity.get("agent_name"),
549
+ flock_definition=flock_definition,
550
+ created_at=entity["created_at"],
551
+ ))
552
+
553
+ logger.debug("Retrieved %d feedback records for agent %s", len(records), agent_name)
554
+ return records
407
555
 
408
556
 
409
557
  # ----------------------- Factory Function -----------------------
410
558
 
411
559
  def create_shared_link_store(store_type: str | None = None, connection_string: str | None = None) -> SharedLinkStoreInterface:
412
560
  """Factory function to create the appropriate shared link store based on configuration.
413
-
561
+
414
562
  Args:
415
563
  store_type: Type of store to create ("local" for SQLite, "azure-storage" for Azure Table Storage)
416
564
  connection_string: Connection string for the store (file path for SQLite, connection string for Azure)
417
-
565
+
418
566
  Returns:
419
567
  Configured SharedLinkStoreInterface implementation
420
568
  """
@@ -38,7 +38,9 @@ window.agentInputFormUrlTemplate = '{{ url_for("htmx_get_agent_input_form", agen
38
38
  <progress indeterminate></progress> Loading input form...
39
39
  </div>
40
40
 
41
- <button type="submit" {% if not flock.agents %}disabled{% endif %}>Run Flock</button> <div id="share-agent-link-container" style="margin-top: 0.5rem;"> <a href="#"
41
+ <button type="submit" {% if not flock.agents %}disabled{% endif %}>Run Flock</button>
42
+ <div id="share-agent-link-container" style="margin-top: 0.5rem;">
43
+ <a href="#"
42
44
  id="shareAgentHtmxLink"
43
45
  hx-post="{{ url_for('htmx_generate_share_link') }}"
44
46
  hx-target="#shareLinkDisplayArea"
@@ -53,6 +55,32 @@ window.agentInputFormUrlTemplate = '{{ url_for("htmx_get_agent_input_form", agen
53
55
  </span>
54
56
  </div>
55
57
 
58
+ <div id="feedback-download-container" style="margin-top: 0.5rem;">
59
+ <a href="#"
60
+ id="downloadFeedbackLink"
61
+ x-show="selectedAgentForInput"
62
+ hx-indicator="#download-feedback-loading-indicator"
63
+ hx-include="#start_agent_name_select"
64
+ :href="selectedAgentForInput ? '{{ url_for('chat_feedback_download', agent_name='AGENT_PLACEHOLDER') }}'.replace('AGENT_PLACEHOLDER', selectedAgentForInput) : '#'"
65
+ style="text-decoration: underline; cursor: pointer; color: var(--pico-primary);"
66
+ x-cloak>
67
+ Download feedback for the selected agent...
68
+ </a>
69
+ <span id="download-feedback-loading-indicator" class="htmx-indicator">
70
+ <progress indeterminate></progress> Preparing file...
71
+ </span>
72
+ </div>
73
+
74
+ <div id="feedback-download-container-all" style="margin-top: 0.5rem;">
75
+ <a href="{{ url_for('chat_feedback_download_all')}}"
76
+ id="downloadFeedbackLinkAll"
77
+ style="text-decoration: underline; cursor: pointer; color: var(--pico-primary);"
78
+ >Download feedback for all agents...</a>
79
+ <span id="download-feedback-loading-indicator-all" class="htmx-indicator">
80
+ <progress indeterminate></progress> Preparing file...
81
+ </span>
82
+ </div>
83
+
56
84
  <span id="run-loading-indicator" class="htmx-indicator">
57
85
  <progress indeterminate></progress> Running...
58
86
  </span>
@@ -5,21 +5,36 @@
5
5
  </header>
6
6
 
7
7
  <nav>
8
- <ul role="group"> <li>
9
- <button role="button" class="outline" hx-get="{{ url_for('htmx_get_registry_table', item_type='type') }}" hx-target="#registry-table-container" hx-indicator="#registry-loading" hx-on:click="setActiveButton(this)">
8
+ <ul role="group">
9
+ <li>
10
+ <button role="button" class="outline"
11
+ hx-get="{{ url_for('htmx_get_registry_table', item_type='type') }}"
12
+ hx-target="#registry-table-container" hx-indicator="#registry-loading"
13
+ hx-on:click="setActiveButton(this)">
10
14
  View Types
11
15
  </button>
12
16
  </li>
13
17
  <li>
14
- <button role="button" class="outline" hx-get="{{ url_for('htmx_get_registry_table', item_type='tool') }}" hx-target="#registry-table-container" hx-indicator="#registry-loading" hx-on:click="setActiveButton(this)">
18
+ <button role="button" class="outline"
19
+ hx-get="{{ url_for('htmx_get_registry_table', item_type='tool') }}"
20
+ hx-target="#registry-table-container" hx-indicator="#registry-loading"
21
+ hx-on:click="setActiveButton(this)">
15
22
  View Tools/Callables
16
23
  </button>
17
24
  </li>
18
25
  <li>
19
- <button role="button" class="outline" hx-get="{{ url_for('htmx_get_registry_table', item_type='component') }}" hx-target="#registry-table-container" hx-indicator="#registry-loading" hx-on:click="setActiveButton(this)">
26
+ <button role="button" class="outline"
27
+ hx-get="{{ url_for('htmx_get_registry_table', item_type='component') }}"
28
+ hx-target="#registry-table-container" hx-indicator="#registry-loading"
29
+ hx-on:click="setActiveButton(this)">
20
30
  View Components
21
31
  </button>
22
32
  </li>
33
+ <li>
34
+ <a role="button" class="outline" href="{{ url_for('chat_feedback_download_all')}}" download>
35
+ Feedback-Files
36
+ </a>
37
+ </li>
23
38
  </ul>
24
39
  </nav>
25
40
 
@@ -37,10 +52,10 @@
37
52
  function setActiveButton(clickedButton) {
38
53
  const buttons = clickedButton.closest('ul[role="group"]').querySelectorAll('button');
39
54
  buttons.forEach(button => {
40
- button.classList.remove('primary');
55
+ button.classList.remove('primary');
41
56
  button.classList.add('outline');
42
57
  });
43
58
  clickedButton.classList.remove('outline');
44
- clickedButton.classList.add('primary');
59
+ clickedButton.classList.add('primary');
45
60
  }
46
61
  </script>
@@ -12,32 +12,34 @@
12
12
  <nav>
13
13
  <ul role="group">
14
14
  <li>
15
- <button role="button"
16
- class="outline" hx-get="{{ url_for('htmx_get_registry_table', item_type='type') }}"
17
- hx-target="#registry-table-container"
18
- hx-indicator="#registry-loading"
19
- hx-on:click="setActiveButton(this)">
15
+ <button role="button" class="outline"
16
+ hx-get="{{ url_for('htmx_get_registry_table', item_type='type') }}"
17
+ hx-target="#registry-table-container" hx-indicator="#registry-loading"
18
+ hx-on:click="setActiveButton(this)">
20
19
  View Types
21
20
  </button>
22
21
  </li>
23
22
  <li>
24
- <button role="button"
25
- class="outline" hx-get="{{ url_for('htmx_get_registry_table', item_type='tool') }}"
26
- hx-target="#registry-table-container"
27
- hx-indicator="#registry-loading"
28
- hx-on:click="setActiveButton(this)">
29
- View Tools/Callables
23
+ <button role="button" class="outline"
24
+ hx-get="{{ url_for('htmx_get_registry_table', item_type='component') }}"
25
+ hx-target="#registry-table-container" hx-indicator="#registry-loading"
26
+ hx-on:click="setActiveButton(this)">
27
+ View Components
30
28
  </button>
31
29
  </li>
32
30
  <li>
33
- <button role="button"
34
- class="outline" hx-get="{{ url_for('htmx_get_registry_table', item_type='component') }}"
35
- hx-target="#registry-table-container"
36
- hx-indicator="#registry-loading"
37
- hx-on:click="setActiveButton(this)">
31
+ <button role="button" class="outline"
32
+ hx-get="{{ url_for('htmx_get_registry_table', item_type='component') }}"
33
+ hx-target="#registry-table-container" hx-indicator="#registry-loading"
34
+ hx-on:click="setActiveButton(this)">
38
35
  View Components
39
36
  </button>
40
37
  </li>
38
+ <li>
39
+ <button role="button" class="secondary" hx-get="{{ url_for('chat_feedback_download_all')}}" download>
40
+ Download Feedback-Files for Agents
41
+ </button>
42
+ </li>
41
43
  </ul>
42
44
  </nav>
43
45
 
@@ -50,7 +52,8 @@
50
52
  </div>
51
53
 
52
54
  <footer style="margin-top: 2rem;">
53
- <a href="{{ url_for('page_editor_section', section='execute') }}" role="button" class="secondary contrast">Back to Editor</a>
55
+ <a href="{{ url_for('page_editor_section', section='execute') }}" role="button" class="secondary contrast">Back
56
+ to Editor</a>
54
57
  </footer>
55
58
  </article>
56
59
 
@@ -70,7 +73,7 @@
70
73
 
71
74
  // Optional: Set the first button as active on initial load,
72
75
  // or if you want to default to loading "Types"
73
- document.addEventListener('DOMContentLoaded', function() {
76
+ document.addEventListener('DOMContentLoaded', function () {
74
77
  const firstButton = document.querySelector('nav ul[role="group"] button');
75
78
  if (firstButton) {
76
79
  // setActiveButton(firstButton); // Uncomment if you want a default active button
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flock-core
3
- Version: 0.4.526
3
+ Version: 0.4.527
4
4
  Summary: Declarative LLM Orchestration at Scale
5
5
  Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
6
6
  License-File: LICENSE
@@ -492,7 +492,7 @@ flock/tools/zendesk_tools.py,sha256=e7KMfHVl7wGbstwdz9CvoChyuoZfpS9n4TEtvrxawgI,
492
492
  flock/webapp/__init__.py,sha256=YtRbbyciN3Z2oMB9fdXZuvM3e49R8m2mY5qHLDoapRA,37
493
493
  flock/webapp/run.py,sha256=btKVwIqrFg3FhLRuj2RN_fazwaFat3Ue5yiFiIg60rQ,9054
494
494
  flock/webapp/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
495
- flock/webapp/app/chat.py,sha256=d5a_mr3H2nuWNFSpSlI_HyqX-J_4krndd4A-8S25EKM,28679
495
+ flock/webapp/app/chat.py,sha256=Wm7yr3j2Zjh2t6i5QF461m8FXZFfBMFtf7Sy_s2cigA,33159
496
496
  flock/webapp/app/config.py,sha256=lqmneujnNZk-EFJV5cWpvxkqisxH3T3zT_YOI0JYThE,4809
497
497
  flock/webapp/app/dependencies.py,sha256=JUcwY1N6SZplU141lMN2wk9dOC9er5HCedrKTJN9wJk,5533
498
498
  flock/webapp/app/main.py,sha256=J3NiwW4fFTrp_YKa-HFXs3gLtLC6qu9LjIb_i-jJHMk,58426
@@ -502,13 +502,13 @@ flock/webapp/app/theme_mapper.py,sha256=QzWwLWpED78oYp3FjZ9zxv1KxCyj43m8MZ0fhfzz
502
502
  flock/webapp/app/utils.py,sha256=RF8DMKKAj1XPmm4txUdo2OdswI1ATQ7cqUm6G9JFDzA,2942
503
503
  flock/webapp/app/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
504
504
  flock/webapp/app/api/agent_management.py,sha256=5xqO94QjjAYvxImyjKV9EGUQOvo4n3eqs7pGwGPSQJ4,10394
505
- flock/webapp/app/api/execution.py,sha256=EeOhpABZ1STcRKIa8pXRqWeqtD4KS8KAWTNYbVns2FQ,13432
505
+ flock/webapp/app/api/execution.py,sha256=ovtjcBmm_S7Gyg1_hM4euBdIPDTZJZu-hCQa64tQS8o,21109
506
506
  flock/webapp/app/api/flock_management.py,sha256=1o-6-36kTnUjI3am_BqLpdrcz0aqFXrxE-hQHIFcCsg,4869
507
507
  flock/webapp/app/api/registry_viewer.py,sha256=IoInxJiRR0yFlecG_l2_eRc6l35RQQyEDMG9BcBkipY,1020
508
508
  flock/webapp/app/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
509
509
  flock/webapp/app/services/flock_service.py,sha256=olU1My3YYkrTCVIOYPgRted-8YgAop-Yi7G4gbRHTrg,14941
510
510
  flock/webapp/app/services/sharing_models.py,sha256=XeJk1akILV_1l-cIUaG8k_eYhjV3EWBCWZ2kpwbdImA,3609
511
- flock/webapp/app/services/sharing_store.py,sha256=muYPI5KQUI3hg1rckCZ3UQ_nTxEJh6OiM15McyuG1tI,19669
511
+ flock/webapp/app/services/sharing_store.py,sha256=vUIFSDppxPL65DP2OWczYe5aheuv_oHO2cynCtsRgtE,26649
512
512
  flock/webapp/app/templates/theme_mapper.html,sha256=z8ZY7nmk6PiUGzD_-px7wSXcEnuBM121rMq6u-2oaCo,14249
513
513
  flock/webapp/static/css/chat.css,sha256=Njc9gXfQzbXMrqtFJH2Yda-IQlwNPd2z4apXxzfA0sY,8169
514
514
  flock/webapp/static/css/components.css,sha256=WnicEHy3ptPzggKmyG9_oZp3X30EMJBUW3KEXaiUCUE,6018
@@ -521,7 +521,7 @@ flock/webapp/templates/chat.html,sha256=kz1No9tZfOt6eFzI1KNmfYrSglWIr9bxkGZDEaBH
521
521
  flock/webapp/templates/chat_settings.html,sha256=eUHOOEt52jW8DxAMM6jLOPPsdHMdhiQzKuEdgeT0BOI,930
522
522
  flock/webapp/templates/flock_editor.html,sha256=Zlo-jLbNZshsxvC9vFDnqWNxdJiKelITlsvX5CAl4RI,547
523
523
  flock/webapp/templates/index.html,sha256=1SDfAfaGBNJhI26bc6z7qBo9mrOK7HKQIS9aRGvMnBA,429
524
- flock/webapp/templates/registry_viewer.html,sha256=Rq0Ao3r6mqwgF6bWM73d-KmnslwQoz79a2PGbL5M4Fo,3349
524
+ flock/webapp/templates/registry_viewer.html,sha256=BEJkuUdLy8FStF16P6HXH9AJTD62iQjpH1WYUQDqBdo,3413
525
525
  flock/webapp/templates/shared_run_page.html,sha256=RX1J3hgEuIJXaGYlquY7m54yxPTIrJJAZH2HB6v33WY,8109
526
526
  flock/webapp/templates/partials/_agent_detail_form.html,sha256=jpcbj2-zN_eWH6vlhWGBAviObmK2IZvezU_b2_UW-Tc,6067
527
527
  flock/webapp/templates/partials/_agent_list.html,sha256=0TcutldXJncQP6jMsXh8cnbjdzaVuEF-VTddQ6DZli0,1058
@@ -537,7 +537,7 @@ flock/webapp/templates/partials/_dashboard_flock_properties_preview.html,sha256=
537
537
  flock/webapp/templates/partials/_dashboard_upload_flock_form.html,sha256=UWU_WpIsq6iw5FwK989ANmhzcCOcDKVjdakZ4kxY8lU,961
538
538
  flock/webapp/templates/partials/_dynamic_input_form_content.html,sha256=WYr2M7Mb5vKITFhIVXXEHppRx9IjGew91yLo1_I9kkI,1230
539
539
  flock/webapp/templates/partials/_env_vars_table.html,sha256=st8bRQpIJ3TJ_znEdyOwDT43ZhO2QTLne2IZNxQdQNM,1106
540
- flock/webapp/templates/partials/_execution_form.html,sha256=yQbYgFYlfva4hgRAs791MR2IFgZnQBLbo4vvlPrDGeU,3686
540
+ flock/webapp/templates/partials/_execution_form.html,sha256=ln73ShbXU1ZNYoBkryvuK4RnPebimNyHHE9VK4heX0s,5113
541
541
  flock/webapp/templates/partials/_execution_view_container.html,sha256=w4QEr-yPs0k1vF2oxMhhPoOr-ki0YJzzUltGmZgfRfY,1672
542
542
  flock/webapp/templates/partials/_flock_file_list.html,sha256=3F1RE1EaeRSQeyIWeA2ALHU2j4oGwureDtY6k1aZVjQ,1261
543
543
  flock/webapp/templates/partials/_flock_properties_form.html,sha256=kUHoxB6_C_R9nMDD_ClLRDL_9zQ6E8gFCrlkEkqcUYo,2904
@@ -545,7 +545,7 @@ flock/webapp/templates/partials/_flock_upload_form.html,sha256=IK4Kk_82z3m9zlutK
545
545
  flock/webapp/templates/partials/_header_flock_status.html,sha256=reNB4Prsu9lObz5tFhGk3AMe4bNw92gDJZUKABVaVBM,158
546
546
  flock/webapp/templates/partials/_load_manager_view.html,sha256=lu_2yKJcBPF8yZEeA0rnCPpYODieVfQgjq8HzKoPTzs,2950
547
547
  flock/webapp/templates/partials/_registry_table.html,sha256=z4EW5G3DTknymBeSlpL2PZLcb2143P35upMnmHFfeJs,715
548
- flock/webapp/templates/partials/_registry_viewer_content.html,sha256=FBqP1YRL-rfE6YpgtWnpyFqATUhVKUHTGvF6QbIOKrw,1968
548
+ flock/webapp/templates/partials/_registry_viewer_content.html,sha256=GNI5LJsxefjwhxP24Q2flRgs-2yWAG-f-KFAAuV23tk,2347
549
549
  flock/webapp/templates/partials/_results_display.html,sha256=1UJvCeyJTPxuJYUcGUM8O8uhNvCxxOfxpCjC-PCr80U,5268
550
550
  flock/webapp/templates/partials/_settings_env_content.html,sha256=16h1ppTGNY7wNkxQkaNhouTNN22tlw_u5rBk6cdhzFk,597
551
551
  flock/webapp/templates/partials/_settings_theme_content.html,sha256=TPkqLyXJHwWOOgjLVURLpHA2JJRncZGf78Q6olIZIJc,852
@@ -562,8 +562,8 @@ flock/workflow/agent_execution_activity.py,sha256=Gy6FtuVAjf0NiUXmC3syS2eJpNQF4R
562
562
  flock/workflow/flock_workflow.py,sha256=iSUF_soFvWar0ffpkzE4irkDZRx0p4HnwmEBi_Ne2sY,9666
563
563
  flock/workflow/temporal_config.py,sha256=3_8O7SDEjMsSMXsWJBfnb6XTp0TFaz39uyzSlMTSF_I,3988
564
564
  flock/workflow/temporal_setup.py,sha256=YIHnSBntzOchHfMSh8hoLeNXrz3B1UbR14YrR6soM7A,1606
565
- flock_core-0.4.526.dist-info/METADATA,sha256=e1SqdKUXQdO7qHdNj0P-Bri-OlhLaM1Ot10s3qfgSc4,22786
566
- flock_core-0.4.526.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
567
- flock_core-0.4.526.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
568
- flock_core-0.4.526.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
569
- flock_core-0.4.526.dist-info/RECORD,,
565
+ flock_core-0.4.527.dist-info/METADATA,sha256=RvdNldnNPaR7aVN0c_pULSlP166YPJr5uJwey78shgo,22786
566
+ flock_core-0.4.527.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
567
+ flock_core-0.4.527.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
568
+ flock_core-0.4.527.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
569
+ flock_core-0.4.527.dist-info/RECORD,,