flock-core 0.4.525__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.
- flock/routers/conditional/conditional_router.py +4 -0
- flock/webapp/app/api/execution.py +336 -57
- flock/webapp/app/chat.py +142 -2
- flock/webapp/app/services/sharing_store.py +153 -5
- flock/webapp/templates/partials/_execution_form.html +29 -1
- flock/webapp/templates/partials/_registry_viewer_content.html +21 -6
- flock/webapp/templates/registry_viewer.html +21 -18
- {flock_core-0.4.525.dist-info → flock_core-0.4.527.dist-info}/METADATA +1 -1
- {flock_core-0.4.525.dist-info → flock_core-0.4.527.dist-info}/RECORD +12 -12
- {flock_core-0.4.525.dist-info → flock_core-0.4.527.dist-info}/WHEEL +0 -0
- {flock_core-0.4.525.dist-info → flock_core-0.4.527.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.525.dist-info → flock_core-0.4.527.dist-info}/licenses/LICENSE +0 -0
|
@@ -115,6 +115,10 @@ class ConditionalRouterConfig(FlockRouterConfig):
|
|
|
115
115
|
default="flock.assertion_feedback", # Useful if paired with AssertionCheckerModule
|
|
116
116
|
description="Optional context key containing feedback message to potentially include when retrying.",
|
|
117
117
|
)
|
|
118
|
+
feedback_on_failure: str | None = Field(
|
|
119
|
+
default=None,
|
|
120
|
+
description="Default feedback message to use when condition evaluation fails.",
|
|
121
|
+
)
|
|
118
122
|
retry_count_context_key_prefix: str = Field(
|
|
119
123
|
default="flock.conditional_retry_count_",
|
|
120
124
|
description="Internal prefix for context key storing retry attempts per agent.",
|
|
@@ -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
|
-
|
|
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(
|
|
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,
|
|
67
|
+
"flock": current_flock, # Pass the injected flock instance
|
|
62
68
|
"input_fields": [],
|
|
63
|
-
"selected_agent_name": None,
|
|
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(
|
|
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"]:
|
|
92
|
-
|
|
93
|
-
elif
|
|
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"] =
|
|
96
|
-
|
|
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(
|
|
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(
|
|
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():
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
149
|
-
|
|
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(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
201
|
-
|
|
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(
|
|
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(
|
|
208
|
-
|
|
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():
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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(
|
|
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(
|
|
232
|
-
shared_logger.info(
|
|
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(
|
|
236
|
-
|
|
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(
|
|
239
|
-
|
|
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(
|
|
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>
|
|
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">
|
|
9
|
-
|
|
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"
|
|
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"
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
@@ -136,7 +136,7 @@ flock/routers/__init__.py,sha256=w9uL34Auuo26-q_EGlE8Z9iHsw6S8qutTAH_ZI7pn7M,39
|
|
|
136
136
|
flock/routers/agent/__init__.py,sha256=0ZOYpR8BMnR5iCGfcUiv99g7aT_g13xvm2Shl-XzybY,65
|
|
137
137
|
flock/routers/agent/agent_router.py,sha256=d4rberqXguJFmDB_hLTaeaDP_rOvCnVQufPELA-pD6M,8327
|
|
138
138
|
flock/routers/agent/handoff_agent.py,sha256=p-0XEPXIyv1T3DGAhhXg2SYXmrwEaJ5pnuLgRSvbiZg,1903
|
|
139
|
-
flock/routers/conditional/conditional_router.py,sha256=
|
|
139
|
+
flock/routers/conditional/conditional_router.py,sha256=eBFA8ZRFwCDCggtQcVLj8907K-piOk-K0fXezJfjnRg,21422
|
|
140
140
|
flock/routers/default/__init__.py,sha256=DOatGX_aE2DWvf55a0Tv7qDK05QFD-hL3sm7g58hmLU,61
|
|
141
141
|
flock/routers/default/default_router.py,sha256=RgJm6RcS8ah1S49mM9TccfJpenQ0SzzbPCX0K8ZtnHs,2384
|
|
142
142
|
flock/routers/feedback/feedback_router.py,sha256=RODEmPrrNZ-ODdZ0mGfmO8auEnH6KvHN3f5rmFGeq1M,4947
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
566
|
-
flock_core-0.4.
|
|
567
|
-
flock_core-0.4.
|
|
568
|
-
flock_core-0.4.
|
|
569
|
-
flock_core-0.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|