flock-core 0.5.0b10__py3-none-any.whl → 0.5.0b12__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.

@@ -2,7 +2,7 @@
2
2
  """DeclarativeEvaluationComponent - DSPy-based evaluation using the unified component system."""
3
3
 
4
4
  from collections.abc import Generator
5
- from typing import Any
5
+ from typing import Any, override
6
6
 
7
7
  from temporalio import workflow
8
8
 
@@ -29,7 +29,7 @@ class DeclarativeEvaluationConfig(AgentComponentConfig):
29
29
  model: str | None = "openai/gpt-4o"
30
30
  use_cache: bool = True
31
31
  temperature: float = 0.0
32
- max_tokens: int = 4096
32
+ max_tokens: int = 8192
33
33
  max_retries: int = 3
34
34
  max_tool_calls: int = 10
35
35
  stream: bool = Field(
@@ -69,6 +69,14 @@ class DeclarativeEvaluationComponent(
69
69
  def __init__(self, **data):
70
70
  super().__init__(**data)
71
71
 
72
+ @override
73
+ def set_model(self, model: str, temperature: float = 0.0, max_tokens: int = 8192) -> None:
74
+ """Set the model for the evaluation component."""
75
+ self.config.model = model
76
+ if "gpt-oss" in model and temperature == 0.0 and max_tokens == 8192:
77
+ self.config.temperature = 1.0
78
+ self.config.max_tokens = 32768
79
+
72
80
  async def evaluate_core(
73
81
  self,
74
82
  agent: Any,
@@ -180,7 +180,8 @@ class OutputUtilityComponent(UtilityComponent):
180
180
  max_length=self.config.max_length,
181
181
  render_table=self.config.render_table,
182
182
  )
183
- self._formatter.display_result(result_to_display, agent.name)
183
+ model = agent.model if agent.model else context.get_variable("model")
184
+ self._formatter.display_result(result_to_display, agent.name + " - " + model)
184
185
 
185
186
  return result # Return the original, unmodified result
186
187
 
@@ -24,6 +24,12 @@ class EvaluationComponent(AgentComponent):
24
24
  - LLMEvaluationComponent (direct LLM API)
25
25
  """
26
26
 
27
+ @abstractmethod
28
+ def set_model(self, model: str, temperature: float = 0.0, max_tokens: int = 4096) -> None:
29
+ """Set the model for the evaluation component."""
30
+ pass
31
+
32
+
27
33
  @abstractmethod
28
34
  async def evaluate_core(
29
35
  self,
flock/core/flock_agent.py CHANGED
@@ -258,7 +258,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
258
258
  """
259
259
  self.model = model
260
260
  if self.evaluator and hasattr(self.evaluator, "config"):
261
- self.evaluator.config.model = model
261
+ self.evaluator.set_model(model)
262
262
  logger.info(
263
263
  f"Set model to '{model}' for agent '{self.name}' and its evaluator."
264
264
  )
@@ -435,6 +435,10 @@ class FlockFactory:
435
435
  OutputUtilityConfig,
436
436
  )
437
437
 
438
+ if model and "gpt-oss" in model:
439
+ temperature = 1.0
440
+ max_tokens = 32768
441
+
438
442
  # Create evaluation component
439
443
  eval_config = DeclarativeEvaluationConfig(
440
444
  model=model,
@@ -133,7 +133,7 @@ class FlockExecution:
133
133
  span.set_attribute("enable_temporal", self.flock.enable_temporal)
134
134
 
135
135
  logger.info(
136
- f"Initiating Flock run '{self.flock.name}'. Start Agent: '{start_agent_name}'. Temporal: {self.flock.enable_temporal}."
136
+ f"Initiating Flock run '{self.flock.name}'. Model: '{self.flock.model}'. Start Agent: '{start_agent_name}'. Temporal: {self.flock.enable_temporal}."
137
137
  )
138
138
 
139
139
  try:
@@ -41,7 +41,7 @@ class FlockInitialization:
41
41
 
42
42
  # Initialize console if needed for banner
43
43
  if self.flock.show_flock_banner:
44
- init_console(clear_screen=True, show_banner=self.flock.show_flock_banner)
44
+ init_console(clear_screen=True, show_banner=self.flock.show_flock_banner, model=self.flock.model)
45
45
 
46
46
  # Set Temporal debug environment variable
47
47
  self._set_temporal_debug_flag()
@@ -38,7 +38,7 @@ def display_hummingbird():
38
38
  """)
39
39
 
40
40
 
41
- def init_console(clear_screen: bool = True, show_banner: bool = True):
41
+ def init_console(clear_screen: bool = True, show_banner: bool = True, model: str = ""):
42
42
  """Display the Flock banner."""
43
43
  from rich.console import Console
44
44
  from rich.syntax import Text
@@ -66,6 +66,9 @@ def init_console(clear_screen: bool = True, show_banner: bool = True):
66
66
  "[italic]'Magpie'[/] milestone - [bold]white duck GmbH[/] - [cyan]https://whiteduck.de[/]\n"
67
67
  )
68
68
 
69
+ if model:
70
+ console.print(f"[italic]Global Model:[/] {model}")
71
+
69
72
 
70
73
  def display_banner_no_version():
71
74
  """Display the Flock banner."""
@@ -1,7 +1,8 @@
1
1
  # src/flock/webapp/app/api/execution.py
2
+ import html
2
3
  import json
3
4
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, Literal
5
6
 
6
7
  import markdown2 # Import markdown2
7
8
  from fastapi import ( # Ensure Form and HTTPException are imported
@@ -11,8 +12,16 @@ from fastapi import ( # Ensure Form and HTTPException are imported
11
12
  Request,
12
13
  )
13
14
  from fastapi.encoders import jsonable_encoder
14
- from fastapi.responses import HTMLResponse
15
+ from fastapi.responses import FileResponse, HTMLResponse
15
16
  from fastapi.templating import Jinja2Templates
17
+ from werkzeug.utils import secure_filename
18
+
19
+ from flock.webapp.app.services.feedback_file_service import (
20
+ create_csv_feedback_file,
21
+ create_csv_feedback_file_for_agent,
22
+ create_xlsx_feedback_file,
23
+ create_xlsx_feedback_file_for_agent,
24
+ )
16
25
 
17
26
  if TYPE_CHECKING:
18
27
  from flock.core.flock import Flock
@@ -41,26 +50,30 @@ router = APIRouter()
41
50
  BASE_DIR = Path(__file__).resolve().parent.parent.parent
42
51
  templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
43
52
 
53
+
44
54
  # Add markdown2 filter to Jinja2 environment for this router
45
55
  def markdown_filter(text):
46
56
  return markdown2.markdown(text, extras=["tables", "fenced-code-blocks"])
47
57
 
48
- templates.env.filters['markdown'] = markdown_filter
58
+
59
+ templates.env.filters["markdown"] = markdown_filter
49
60
 
50
61
 
51
62
  @router.get("/htmx/execution-form-content", response_class=HTMLResponse)
52
63
  async def htmx_get_execution_form_content(
53
64
  request: Request,
54
- current_flock: "Flock | None" = Depends(get_optional_flock_instance) # Use optional if form can show 'no flock'
65
+ current_flock: "Flock | None" = Depends(
66
+ get_optional_flock_instance
67
+ ), # Use optional if form can show 'no flock'
55
68
  ):
56
69
  # flock instance is injected
57
70
  return templates.TemplateResponse(
58
71
  "partials/_execution_form.html",
59
72
  {
60
73
  "request": request,
61
- "flock": current_flock, # Pass the injected flock instance
74
+ "flock": current_flock, # Pass the injected flock instance
62
75
  "input_fields": [],
63
- "selected_agent_name": None, # Form starts with no agent selected
76
+ "selected_agent_name": None, # Form starts with no agent selected
64
77
  },
65
78
  )
66
79
 
@@ -69,7 +82,9 @@ async def htmx_get_execution_form_content(
69
82
  async def htmx_get_agent_input_form(
70
83
  request: Request,
71
84
  agent_name: str,
72
- current_flock: "Flock" = Depends(get_flock_instance) # Expect flock to be loaded
85
+ current_flock: "Flock" = Depends(
86
+ get_flock_instance
87
+ ), # Expect flock to be loaded
73
88
  ):
74
89
  # flock instance is injected
75
90
  agent = current_flock.agents.get(agent_name)
@@ -88,12 +103,21 @@ async def htmx_get_agent_input_form(
88
103
  "type": type_str.lower(),
89
104
  "description": description or "",
90
105
  }
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"]:
106
+ if "bool" in field_info["type"]:
107
+ field_info["html_type"] = "checkbox"
108
+ elif (
109
+ "int" in field_info["type"] or "float" in field_info["type"]
110
+ ):
111
+ field_info["html_type"] = "number"
112
+ elif (
113
+ "list" in field_info["type"] or "dict" in field_info["type"]
114
+ ):
94
115
  field_info["html_type"] = "textarea"
95
- field_info["placeholder"] = f"Enter JSON for {field_info['type']}"
96
- else: field_info["html_type"] = "text"
116
+ field_info["placeholder"] = (
117
+ f"Enter JSON for {field_info['type']}"
118
+ )
119
+ else:
120
+ field_info["html_type"] = "text"
97
121
  input_fields.append(field_info)
98
122
  except Exception as e:
99
123
  return HTMLResponse(
@@ -109,7 +133,9 @@ async def htmx_get_agent_input_form(
109
133
  async def htmx_run_flock(
110
134
  request: Request,
111
135
  ):
112
- current_flock_from_state: Flock | None = getattr(request.app.state, 'flock_instance', None)
136
+ current_flock_from_state: Flock | None = getattr(
137
+ request.app.state, "flock_instance", None
138
+ )
113
139
  logger = get_flock_logger("webapp.execution.regular_run")
114
140
 
115
141
  if not current_flock_from_state:
@@ -125,7 +151,9 @@ async def htmx_run_flock(
125
151
 
126
152
  agent = current_flock_from_state.agents.get(start_agent_name)
127
153
  if not agent:
128
- logger.error(f"HTMX Run (Regular): Agent '{start_agent_name}' not found in Flock '{current_flock_from_state.name}'.")
154
+ logger.error(
155
+ f"HTMX Run (Regular): Agent '{start_agent_name}' not found in Flock '{current_flock_from_state.name}'."
156
+ )
129
157
  return HTMLResponse(
130
158
  f"<p class='error'>Agent '{start_agent_name}' not found in the current Flock.</p>"
131
159
  )
@@ -137,30 +165,57 @@ async def htmx_run_flock(
137
165
  for name, type_str, _ in parsed_spec:
138
166
  form_field_name = f"agent_input_{name}"
139
167
  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
168
+ if raw_value is None and "bool" in type_str.lower():
169
+ inputs[name] = False
170
+ continue
171
+ if raw_value is None:
172
+ inputs[name] = None
173
+ continue
174
+ if "int" in type_str.lower():
175
+ inputs[name] = int(raw_value)
176
+ elif "float" in type_str.lower():
177
+ inputs[name] = float(raw_value)
178
+ elif "bool" in type_str.lower():
179
+ inputs[name] = raw_value.lower() in [
180
+ "true",
181
+ "on",
182
+ "1",
183
+ "yes",
184
+ ]
185
+ elif "list" in type_str.lower() or "dict" in type_str.lower():
186
+ inputs[name] = json.loads(raw_value)
187
+ else:
188
+ inputs[name] = raw_value
147
189
  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>")
190
+ logger.error(
191
+ f"HTMX Run (Regular): Input parsing error for agent '{start_agent_name}': {ve}",
192
+ exc_info=True,
193
+ )
194
+ return HTMLResponse(
195
+ "<p class='error'>Invalid input format. Please check your input and try again.</p>"
196
+ )
150
197
  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)
198
+ logger.error(
199
+ f"HTMX Run (Regular): Error processing inputs for '{start_agent_name}': {e_parse}",
200
+ exc_info=True,
201
+ )
202
+ return HTMLResponse(
203
+ f"<p class='error'>Error processing inputs for {html.escape(str(start_agent_name))}: {html.escape(str(e_parse))}</p>"
204
+ )
155
205
 
206
+ result_data = await run_current_flock_service(
207
+ start_agent_name, inputs, request.app.state
208
+ )
156
209
 
157
210
  raw_json_for_template = json.dumps(
158
- jsonable_encoder(result_data), # ← converts every nested BaseModel, datetime, etc.
211
+ jsonable_encoder(
212
+ result_data
213
+ ), # ← converts every nested BaseModel, datetime, etc.
159
214
  indent=2,
160
- ensure_ascii=False
215
+ ensure_ascii=False,
161
216
  )
162
217
  # Unescape newlines for proper display in HTML <pre> tag
163
- result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
218
+ result_data_raw_json_str = raw_json_for_template.replace("\\n", "\n")
164
219
  root_path = request.scope.get("root_path", "")
165
220
  return templates.TemplateResponse(
166
221
  "partials/_results_display.html",
@@ -173,7 +228,7 @@ async def htmx_run_flock(
173
228
  "flock_name": current_flock_from_state.name,
174
229
  "agent_name": start_agent_name,
175
230
  "flock_definition": current_flock_from_state.to_yaml(),
176
- }
231
+ },
177
232
  )
178
233
 
179
234
 
@@ -189,54 +244,98 @@ async def htmx_run_shared_flock(
189
244
 
190
245
  if not start_agent_name:
191
246
  shared_logger.warning("HTMX Run Shared: Starting agent not selected.")
192
- return HTMLResponse("<p class='error'>Starting agent not selected for shared run.</p>")
247
+ return HTMLResponse(
248
+ "<p class='error'>Starting agent not selected for shared run.</p>"
249
+ )
193
250
 
194
251
  inputs: dict[str, Any] = {}
195
252
  try:
196
- shared_flocks_store = getattr(request.app.state, 'shared_flocks', {})
253
+ shared_flocks_store = getattr(request.app.state, "shared_flocks", {})
197
254
  temp_flock = shared_flocks_store.get(share_id)
198
255
 
199
256
  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>")
257
+ shared_logger.error(
258
+ f"HTMX Run Shared: Flock instance for share_id '{share_id}' not found in app.state."
259
+ )
260
+ return HTMLResponse(
261
+ f"<p class='error'>Shared session not found or expired. Please try accessing the shared link again.</p>"
262
+ )
202
263
 
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}).")
264
+ shared_logger.info(
265
+ f"HTMX Run Shared: Successfully retrieved pre-loaded Flock '{temp_flock.name}' for agent '{start_agent_name}' (share_id: {share_id})."
266
+ )
204
267
 
205
268
  agent = temp_flock.agents.get(start_agent_name)
206
269
  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>")
270
+ shared_logger.error(
271
+ f"HTMX Run Shared: Agent '{start_agent_name}' not found in shared Flock '{temp_flock.name}'."
272
+ )
273
+ return HTMLResponse(
274
+ f"<p class='error'>Agent '{start_agent_name}' not found in the provided shared Flock definition.</p>"
275
+ )
209
276
 
210
277
  if agent.input and isinstance(agent.input, str):
211
278
  parsed_spec = parse_schema(agent.input)
212
279
  for name, type_str, _ in parsed_spec:
213
280
  form_field_name = f"agent_input_{name}"
214
281
  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)
282
+ if raw_value is None and "bool" in type_str.lower():
283
+ inputs[name] = False
284
+ continue
285
+ if raw_value is None:
286
+ inputs[name] = None
287
+ continue
288
+ if "int" in type_str.lower():
289
+ inputs[name] = int(raw_value)
290
+ elif "float" in type_str.lower():
291
+ inputs[name] = float(raw_value)
292
+ elif "bool" in type_str.lower():
293
+ inputs[name] = raw_value.lower() in [
294
+ "true",
295
+ "on",
296
+ "1",
297
+ "yes",
298
+ ]
299
+ elif "list" in type_str.lower() or "dict" in type_str.lower():
300
+ inputs[name] = json.loads(raw_value)
301
+ else:
302
+ inputs[name] = raw_value
303
+
304
+ shared_logger.info(
305
+ f"HTMX Run Shared: Executing agent '{start_agent_name}' in pre-loaded Flock '{temp_flock.name}'. Inputs: {list(inputs.keys())}"
306
+ )
307
+ result_data = await temp_flock.run_async(
308
+ start_agent=start_agent_name, input=inputs, box_result=False
309
+ )
225
310
  raw_json_for_template = json.dumps(
226
- jsonable_encoder(result_data), # ← converts every nested BaseModel, datetime, etc.
311
+ jsonable_encoder(
312
+ result_data
313
+ ), # ← converts every nested BaseModel, datetime, etc.
227
314
  indent=2,
228
- ensure_ascii=False
315
+ ensure_ascii=False,
229
316
  )
230
317
  # 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'}")
318
+ result_data_raw_json_str = raw_json_for_template.replace("\\n", "\n")
319
+ shared_logger.info(
320
+ f"HTMX Run Shared: Agent '{start_agent_name}' executed. Result keys: {list(result_data.keys()) if isinstance(result_data, dict) else 'N/A'}"
321
+ )
233
322
 
234
323
  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>")
324
+ shared_logger.error(
325
+ f"HTMX Run Shared: Input parsing error for '{start_agent_name}' (share_id: {share_id}): {ve}",
326
+ exc_info=True,
327
+ )
328
+ return HTMLResponse(
329
+ f"<p class='error'>Invalid input format: {ve!s}</p>"
330
+ )
237
331
  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>")
332
+ shared_logger.error(
333
+ f"HTMX Run Shared: Error during execution for '{start_agent_name}' (share_id: {share_id}): {e}",
334
+ exc_info=True,
335
+ )
336
+ return HTMLResponse(
337
+ f"<p class='error'>An unexpected error occurred: {e!s}</p>"
338
+ )
240
339
  root_path = request.scope.get("root_path", "")
241
340
 
242
341
  return templates.TemplateResponse(
@@ -250,9 +349,10 @@ async def htmx_run_shared_flock(
250
349
  "flock_name": temp_flock.name,
251
350
  "agent_name": start_agent_name,
252
351
  "flock_definition": temp_flock.to_yaml(),
253
- }
352
+ },
254
353
  )
255
354
 
355
+
256
356
  # --- Feedback endpoints ---
257
357
  @router.post("/htmx/feedback", response_class=HTMLResponse)
258
358
  async def htmx_submit_feedback(
@@ -292,6 +392,8 @@ async def htmx_submit_feedback_shared(
292
392
  expected_response: str | None = Form(None),
293
393
  actual_response: str | None = Form(None),
294
394
  flock_definition: str | None = Form(None),
395
+ flock_name: str | None = Form(None),
396
+ agent_name: str | None = Form(None),
295
397
  store: SharedLinkStoreInterface = Depends(get_shared_link_store),
296
398
  ):
297
399
  from uuid import uuid4
@@ -306,6 +408,94 @@ async def htmx_submit_feedback_shared(
306
408
  expected_response=expected_response,
307
409
  actual_response=actual_response,
308
410
  flock_definition=flock_definition,
411
+ agent_name=agent_name,
412
+ flock_name=flock_name,
309
413
  )
310
414
  await store.save_feedback(record)
311
- return HTMLResponse("<p>🙏 Feedback received for shared run – thank you!</p>")
415
+ return HTMLResponse(
416
+ "<p>🙏 Feedback received for shared run – thank you!</p>"
417
+ )
418
+
419
+
420
+ @router.get("/htmx/feedback-download/{format}", response_class=FileResponse)
421
+ async def chat_feedback_download_all(
422
+ request: Request,
423
+ format: Literal["csv", "xlsx"] = "csv",
424
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store),
425
+ ):
426
+ """Download all feedback records for all agents in the current flock as a CSV file.
427
+
428
+ This function iterates through all agents in the currently loaded flock and collects
429
+ all feedback records for each agent, then exports them as a single CSV file.
430
+
431
+ Args:
432
+ request: The FastAPI request object
433
+ store: The shared link store interface dependency
434
+
435
+ Returns:
436
+ FileResponse: CSV/XLSX file containing all feedback records for all agents
437
+
438
+ Raises:
439
+ HTTPException: If no flock is loaded or no agents are found in the flock
440
+ """
441
+ safe_format = secure_filename(format)
442
+ if safe_format == "csv":
443
+ return await create_csv_feedback_file(
444
+ request=request,
445
+ store=store,
446
+ separator=","
447
+ )
448
+ elif safe_format == "xlsx":
449
+ return await create_xlsx_feedback_file(
450
+ request=request,
451
+ store=store,
452
+ )
453
+ else:
454
+ from fastapi import HTTPException
455
+
456
+ raise HTTPException(
457
+ status_code=400,
458
+ detail="Invalid file-format specified. Valid formats are: 'csv', 'xlsx'"
459
+ )
460
+
461
+
462
+ @router.get("/htmx/feedback-download/{agent_name}/{format}", response_class=FileResponse)
463
+ async def chat_feedback_download(
464
+ request: Request,
465
+ agent_name: str,
466
+ format: Literal["csv", "xlsx"] = "csv",
467
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store),
468
+ ):
469
+ """Download all feedback records for a specific agent as a file.
470
+
471
+ Args:
472
+ request: The FastAPI request object
473
+ agent_name: Name of the agent to download feedback for
474
+ store: The shared link store interface dependency
475
+ format: Either 'csv' or 'xlsx' the file format to use
476
+
477
+ Returns:
478
+ FileResponse: CSV/XLSX file containing all feedback records for the specified agent
479
+ """
480
+ safe_format = secure_filename(format)
481
+ safe_agent_name = secure_filename(agent_name)
482
+ if safe_format == "csv":
483
+ return await create_csv_feedback_file_for_agent(
484
+ request=request,
485
+ store=store,
486
+ separator=",",
487
+ agent_name=safe_agent_name,
488
+ )
489
+ elif safe_format == "xlsx":
490
+ return await create_xlsx_feedback_file_for_agent(
491
+ request=request,
492
+ store=store,
493
+ agent_name=safe_agent_name,
494
+ )
495
+ else:
496
+ from fastapi import HTTPException
497
+
498
+ raise HTTPException(
499
+ status_code=400,
500
+ detail="Invalid file-format specified. Valid formats are: 'csv', 'xlsx'"
501
+ )
flock/webapp/app/chat.py CHANGED
@@ -3,17 +3,24 @@ from __future__ import annotations
3
3
  import ast # Add import ast
4
4
  import json
5
5
  from datetime import datetime
6
+ from typing import Literal
6
7
  from uuid import uuid4
7
8
 
8
9
  import markdown2 # Added for Markdown to HTML conversion
9
10
  from fastapi import APIRouter, Depends, Form, Request, Response
10
- from fastapi.responses import HTMLResponse
11
+ from fastapi.responses import FileResponse, HTMLResponse
11
12
  from pydantic import BaseModel
12
13
 
13
14
  from flock.core.flock import Flock
14
15
  from flock.core.logging.logging import get_logger
15
16
  from flock.webapp.app.dependencies import get_shared_link_store
16
17
  from flock.webapp.app.main import get_base_context_web, templates
18
+ from flock.webapp.app.services.feedback_file_service import (
19
+ create_csv_feedback_file,
20
+ create_csv_feedback_file_for_agent,
21
+ create_xlsx_feedback_file,
22
+ create_xlsx_feedback_file_for_agent,
23
+ )
17
24
  from flock.webapp.app.services.sharing_models import (
18
25
  FeedbackRecord,
19
26
  SharedLinkConfig,
@@ -572,7 +579,6 @@ async def chat_feedback(request: Request,
572
579
  headers = {"HX-Trigger": json.dumps(toast_event)}
573
580
  return Response(status_code=204, headers=headers)
574
581
 
575
-
576
582
  @router.post("/chat/htmx/feedback-shared", response_class=HTMLResponse, include_in_schema=False)
577
583
  async def chat_feedback_shared(request: Request,
578
584
  share_id: str = Form(...),
@@ -602,3 +608,58 @@ async def chat_feedback_shared(request: Request,
602
608
  }
603
609
  headers = {"HX-Trigger": json.dumps(toast_event)}
604
610
  return Response(status_code=204, headers=headers)
611
+
612
+ @router.get("/chat/htmx/feedback-download/{format}", response_class=FileResponse, include_in_schema=False)
613
+ async def chat_feedback_download_all(
614
+ request: Request,
615
+ format: Literal["csv", "xlsx"] = "csv",
616
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
617
+ ):
618
+ """Download all feedback records for all agents."""
619
+ if format == "csv":
620
+ return await create_csv_feedback_file(
621
+ request=request,
622
+ store=store,
623
+ separator=","
624
+ )
625
+ elif format == "xlsx":
626
+ return await create_xlsx_feedback_file(
627
+ request=request,
628
+ store=store,
629
+ )
630
+ else:
631
+ from fastapi import HTTPException
632
+
633
+ raise HTTPException(
634
+ status_code=400,
635
+ detail="Invalid file-format specified. Valid formats are: 'csv', 'xlsx'"
636
+ )
637
+
638
+ @router.get("/chat/htmx/feedback-download/{agent_name}/{format}", response_class=FileResponse, include_in_schema=False)
639
+ async def chat_feedback_download(
640
+ request: Request,
641
+ agent_name: str,
642
+ format: Literal["csv", "xlsx"] = "csv",
643
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
644
+ ):
645
+ """Download all feedback records for a specific agent as a File."""
646
+ if format == "csv":
647
+ return await create_csv_feedback_file_for_agent(
648
+ request=request,
649
+ store=store,
650
+ separator=",",
651
+ agent_name=agent_name,
652
+ )
653
+ elif format == "xlsx":
654
+ return await create_xlsx_feedback_file_for_agent(
655
+ request=request,
656
+ store=store,
657
+ agent_name=agent_name,
658
+ )
659
+ else:
660
+ from fastapi import HTTPException
661
+
662
+ raise HTTPException(
663
+ status_code=400,
664
+ detail="Invalid file-format specified. Valid formats are: 'csv', 'xlsx'"
665
+ )