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.

@@ -0,0 +1,363 @@
1
+ """Methods for creating Feedback-File Downloads."""
2
+ import csv
3
+ import tempfile
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from werkzeug.utils import secure_filename
7
+ from fastapi import Request
8
+ from fastapi.responses import FileResponse
9
+
10
+ from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
11
+
12
+
13
+ async def create_xlsx_feedback_file_for_agent(
14
+ request: Request,
15
+ store: SharedLinkStoreInterface,
16
+ agent_name: str,
17
+ ) -> FileResponse:
18
+ """Creates an XLSX-File containing all feedback-entries for a single agent."""
19
+ from flock.core.flock import Flock
20
+
21
+ current_flock_instance: Flock | None = getattr(
22
+ request.app.state, "flock_instance", None
23
+ )
24
+
25
+ if not current_flock_instance:
26
+ from fastapi import HTTPException
27
+
28
+ raise HTTPException(
29
+ status_code=400,
30
+ detail="No Flock loaded to download feedback for"
31
+ )
32
+
33
+ all_agent_names = list(current_flock_instance.agents.keys())
34
+
35
+ if not all_agent_names:
36
+ from fastapi import HTTPException
37
+
38
+ raise HTTPException(
39
+ status_code=400,
40
+ detail="No agents found in the current Flock"
41
+ )
42
+
43
+ all_records = await store.get_all_feedback_records_for_agent(
44
+ agent_name=agent_name
45
+ )
46
+
47
+ temp_dir = tempfile.gettempdir()
48
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
49
+ safe_agent_name = secure_filename(agent_name)
50
+ xlsx_filename = f"flock_feedback_{safe_agent_name}_{timestamp}.xlsx"
51
+ xlsx_path = Path(temp_dir) / xlsx_filename
52
+ headers = [
53
+ "feedback_id",
54
+ "share_id",
55
+ "context_type",
56
+ "reason",
57
+ "expected_response",
58
+ "actual_response",
59
+ "created_at",
60
+ "flock_name",
61
+ "agent_name",
62
+ "flock_definition",
63
+ ]
64
+
65
+ return await _write_xlsx_file(
66
+ records=all_records,
67
+ path=xlsx_path,
68
+ filename=xlsx_filename,
69
+ headers=headers,
70
+ )
71
+
72
+ async def create_xlsx_feedback_file(
73
+ request: Request,
74
+ store: SharedLinkStoreInterface
75
+ ) -> FileResponse:
76
+ """Creates an XLSX-File containing all feddback-entries for all agents."""
77
+ from flock.core.flock import Flock
78
+
79
+ current_flock_instance: Flock | None = getattr(
80
+ request.app.state, "flock_instance", None
81
+ )
82
+
83
+ if not current_flock_instance:
84
+ # If no flock is loaded, return an error response
85
+ from fastapi import HTTPException
86
+
87
+ raise HTTPException(
88
+ status_code=400,
89
+ detail="No Flock loaded to download feedback for"
90
+ )
91
+
92
+ # Get all agent names from the current flock
93
+ all_agent_names = list(current_flock_instance.agents.keys())
94
+
95
+ if not all_agent_names:
96
+
97
+ from fastapi import HTTPException
98
+
99
+ raise HTTPException(
100
+ status_code=400,
101
+ detail="No agents found in the current Flock"
102
+ )
103
+
104
+ all_records = []
105
+ for agent_name in all_agent_names:
106
+ agent_records = await store.get_all_feedback_records_for_agent(
107
+ agent_name=agent_name,
108
+ )
109
+ all_records.extend(agent_records)
110
+
111
+ temp_dir = tempfile.gettempdir()
112
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
113
+ xlsx_filename = f"flock_feedback_all_agents_{timestamp}.xlsx"
114
+ xlsx_path = Path(temp_dir) / xlsx_filename
115
+ headers = [
116
+ "feedback_id",
117
+ "share_id",
118
+ "context_type",
119
+ "reason",
120
+ "expected_response",
121
+ "actual_response",
122
+ "created_at",
123
+ "flock_name",
124
+ "agent_name",
125
+ "flock_definition",
126
+ ]
127
+
128
+ return await _write_xlsx_file(
129
+ records=all_records,
130
+ path=xlsx_path,
131
+ filename=xlsx_filename,
132
+ headers=headers,
133
+ )
134
+
135
+
136
+ async def create_csv_feedback_file_for_agent(
137
+ request: Request,
138
+ store: SharedLinkStoreInterface,
139
+ agent_name: str,
140
+ separator: str = ",",
141
+ ) -> FileResponse:
142
+ """Creates a CSV-File filled with the feedback-records for a single agent."""
143
+ from flock.core.flock import Flock
144
+
145
+ current_flock_instance: Flock | None = getattr(
146
+ request.app.state, "flock_instance", None
147
+ )
148
+
149
+ if not current_flock_instance:
150
+ # If no flock is loaded, return an error response
151
+ from fastapi import HTTPException
152
+
153
+ raise HTTPException(
154
+ status_code=400,
155
+ detail="No Flock loaded to download feedback for"
156
+ )
157
+
158
+
159
+ # Get all agent names from the current flock
160
+ all_agent_names = list(current_flock_instance.agents.keys())
161
+
162
+
163
+ if not all_agent_names:
164
+
165
+ from fastapi import HTTPException
166
+
167
+ raise HTTPException(
168
+ status_code=400,
169
+ detail="No agents found in the current Flock"
170
+ )
171
+
172
+ all_records = await store.get_all_feedback_records_for_agent(
173
+ agent_name=agent_name
174
+ )
175
+ temp_dir = tempfile.gettempdir()
176
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
177
+ safe_agent_name = secure_filename(agent_name)
178
+ csv_filename = f"flock_feedback_{safe_agent_name}_{timestamp}.csv"
179
+ csv_path = Path(temp_dir) / csv_filename
180
+ headers = [
181
+ "feedback_id",
182
+ "share_id",
183
+ "context_type",
184
+ "reason",
185
+ "expected_response",
186
+ "actual_response",
187
+ "created_at",
188
+ "flock_name",
189
+ "agent_name",
190
+ "flock_definition",
191
+ ]
192
+ return await _write_csv_file(
193
+ records=all_records,
194
+ path=csv_path,
195
+ headers=headers,
196
+ separator=separator,
197
+ filename=csv_filename,
198
+ )
199
+
200
+
201
+ async def create_csv_feedback_file(
202
+ request: Request,
203
+ store: SharedLinkStoreInterface,
204
+ separator: str = ",",
205
+
206
+ ) -> FileResponse:
207
+ """Creates a CSV-File filled with the feedback-records for all agents."""
208
+ from flock.core.flock import Flock
209
+
210
+ current_flock_instance: Flock | None = getattr(
211
+ request.app.state, "flock_instance", None
212
+ )
213
+
214
+ if not current_flock_instance:
215
+ # If no flock is loaded, return an error response
216
+ from fastapi import HTTPException
217
+
218
+ raise HTTPException(
219
+ status_code=400,
220
+ detail="No Flock loaded to download feedback for"
221
+ )
222
+
223
+ # Get all agent names from the current flock
224
+ all_agent_names = list(current_flock_instance.agents.keys())
225
+
226
+ if not all_agent_names:
227
+
228
+ from fastapi import HTTPException
229
+
230
+ raise HTTPException(
231
+ status_code=400,
232
+ detail="No agents found in the current Flock"
233
+ )
234
+
235
+ all_records = []
236
+ for agent_name in all_agent_names:
237
+ records_for_agent = await store.get_all_feedback_records_for_agent(
238
+ agent_name=agent_name
239
+ )
240
+ all_records.extend(records_for_agent)
241
+
242
+ temp_dir = tempfile.gettempdir()
243
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
244
+ csv_filename = f"flock_feedback_all_agents_{timestamp}.csv"
245
+ csv_path = Path(temp_dir) / csv_filename
246
+ headers = [
247
+ "feedback_id",
248
+ "share_id",
249
+ "context_type",
250
+ "reason",
251
+ "expected_response",
252
+ "actual_response",
253
+ "created_at",
254
+ "flock_name",
255
+ "agent_name",
256
+ "flock_definition",
257
+ ]
258
+
259
+ return await _write_csv_file(
260
+ records=all_records,
261
+ path=csv_path,
262
+ headers=headers,
263
+ filename=csv_filename,
264
+ separator=separator,
265
+ )
266
+
267
+ async def _write_xlsx_file(
268
+ records: list[dict],
269
+ headers: list,
270
+ path: str | Path,
271
+ filename: str,
272
+ ) -> FileResponse:
273
+ """Writes an xlsx-file with the specified records."""
274
+ try:
275
+ import pandas as pd
276
+
277
+ # Convert records to a format suitable for
278
+ # pandas DataFrame
279
+ data_rows = []
280
+ for record in records:
281
+ row_data = {}
282
+ for header in headers:
283
+ value = getattr(record, header, None)
284
+ # Convert datetime to string for excel
285
+ if header == "created_at" and value:
286
+ row_data[header] = (
287
+ value.isoformat()
288
+ if isinstance(value, datetime)
289
+ else str(value)
290
+ )
291
+ else:
292
+ row_data[header] = str(value) if value is not None else ""
293
+ data_rows.append(row_data)
294
+
295
+ # Create DataFrame and write to Excel
296
+ df = pd.DataFrame(data_rows)
297
+ df.to_excel(str(path), index=False)
298
+
299
+ return FileResponse(
300
+ path=str(path),
301
+ filename=filename,
302
+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
303
+ headers={
304
+ "Content-Disposition": f"attachment; filename={filename}"
305
+ }
306
+ )
307
+
308
+ except Exception:
309
+ from fastapi import HTTPException
310
+
311
+ raise HTTPException(
312
+ status_code=500,
313
+ detail="Unable to create feedback Excel file"
314
+ )
315
+
316
+ async def _write_csv_file(
317
+ records: list[dict],
318
+ path: str | Path,
319
+ filename: str,
320
+ headers: list,
321
+ separator: str = ","
322
+ ) -> FileResponse:
323
+ """Writes a CSV_File with the specified records."""
324
+ try:
325
+ with open(path, "w", newline="", encoding="utf-8") as csvfile:
326
+ writer = csv.DictWriter(
327
+ csvfile,
328
+ fieldnames=headers,
329
+ delimiter=separator
330
+ )
331
+ writer.writeheader()
332
+ for record in records:
333
+ # Convert the Pydantic model
334
+ # to dict and ensure all fields are present
335
+ row_data = {}
336
+ for header in headers:
337
+ value = getattr(record, header, None)
338
+ # Convert datetime to ISO string for CSV
339
+ if header == "created_at" and value:
340
+ row_data[header] = (
341
+ value.isoformat()
342
+ if isinstance(value, datetime)
343
+ else str(value)
344
+ )
345
+ else:
346
+ row_data[header] = str(value) if value is not None else ""
347
+ writer.writerow(row_data)
348
+
349
+ return FileResponse(
350
+ path=str(path),
351
+ filename=filename,
352
+ media_type="text/csv",
353
+ headers={
354
+ "Content-Disposition": f"attachment; filename={filename}"
355
+ }
356
+ )
357
+ except Exception:
358
+ from fastapi import HTTPException
359
+
360
+ raise HTTPException(
361
+ status_code=500,
362
+ detail="Unable to create feedback-file"
363
+ )
@@ -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."""
@@ -367,9 +442,7 @@ class AzureTableSharedLinkStore(SharedLinkStoreInterface):
367
442
 
368
443
  # -------------------------------------------------------- save_feedback --
369
444
  async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
370
- """Persist a feedback record. If a flock_definition is present, upload it as a blob and
371
- store only a reference in the table row to avoid oversized entities (64 KiB limit).
372
- """
445
+ """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)."""
373
446
  tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
374
447
 
375
448
  # Core entity fields (avoid dumping the full Pydantic model – too many columns / large value)
@@ -383,6 +456,9 @@ class AzureTableSharedLinkStore(SharedLinkStoreInterface):
383
456
  "actual_response": record.actual_response,
384
457
  "created_at": record.created_at.isoformat(),
385
458
  }
459
+
460
+ # additional sanity check
461
+
386
462
  if record.flock_name is not None:
387
463
  entity["flock_name"] = record.flock_name
388
464
  if record.agent_name is not None:
@@ -405,17 +481,103 @@ class AzureTableSharedLinkStore(SharedLinkStoreInterface):
405
481
  f" → blob '{entity['flock_blob_name']}'" if "flock_blob_name" in entity else "")
406
482
  return record
407
483
 
484
+ # -------------------------------------------------------- get_feedback --
485
+ async def get_feedback(self, id: str) -> FeedbackRecord | None:
486
+ """Retrieve a single feedback record from Azure Table Storage."""
487
+ tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
488
+ try:
489
+ entity = await tbl_client.get_entity("feedback", id)
490
+ except ResourceNotFoundError:
491
+ logger.debug("No feedback record found for ID: %s", id)
492
+ return None
493
+
494
+ # Get flock_definition from blob if it exists
495
+ flock_definition = None
496
+ if "flock_blob_name" in entity:
497
+ blob_name = entity["flock_blob_name"]
498
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
499
+ try:
500
+ blob_bytes = await (await blob_client.download_blob()).readall()
501
+ flock_definition = blob_bytes.decode()
502
+ except Exception as e:
503
+ logger.error("Cannot download blob '%s' for feedback_id=%s: %s",
504
+ blob_name, id, e, exc_info=True)
505
+ # Continue without flock_definition rather than failing
506
+
507
+ return FeedbackRecord(
508
+ feedback_id=id,
509
+ share_id=entity.get("share_id"),
510
+ context_type=entity["context_type"],
511
+ reason=entity["reason"],
512
+ expected_response=entity.get("expected_response"),
513
+ actual_response=entity.get("actual_response"),
514
+ flock_name=entity.get("flock_name"),
515
+ agent_name=entity.get("agent_name"),
516
+ flock_definition=flock_definition,
517
+ created_at=entity["created_at"],
518
+ )
519
+
520
+
521
+ # ------------------------------------------------ get_all_feedback_records --
522
+ async def get_all_feedback_records_for_agent(self, agent_name: str) -> list[FeedbackRecord]:
523
+ """Retrieve all feedback records from Azure Table Storage for a specific agent."""
524
+ tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
525
+
526
+ # Use Azure Table Storage filtering to only get records for the specified agent
527
+ escaped_agent_name = agent_name.replace("'", "''")
528
+ filter_query = f"agent_name eq '{escaped_agent_name}'"
529
+
530
+ logger.debug(f"Querying feedback records with filter: {filter_query}")
531
+
532
+ records = []
533
+ try:
534
+ async for entity in tbl_client.query_entities(filter_query):
535
+ # Get flock_definition from blob if it exists
536
+ flock_definition = None
537
+ if "flock_blob_name" in entity:
538
+ blob_name = entity["flock_blob_name"]
539
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
540
+ try:
541
+ blob_bytes = await (await blob_client.download_blob()).readall()
542
+ flock_definition = blob_bytes.decode()
543
+ except Exception as e:
544
+ logger.error("Cannot download blob '%s' for feedback_id=%s: %s",
545
+ blob_name, entity["RowKey"], e, exc_info=True)
546
+ # Continue without flock_definition rather than failing
547
+
548
+ records.append(FeedbackRecord(
549
+ feedback_id=entity["RowKey"],
550
+ share_id=entity.get("share_id"),
551
+ context_type=entity["context_type"],
552
+ reason=entity["reason"],
553
+ expected_response=entity.get("expected_response"),
554
+ actual_response=entity.get("actual_response"),
555
+ flock_name=entity.get("flock_name"),
556
+ agent_name=entity.get("agent_name"),
557
+ flock_definition=flock_definition,
558
+ created_at=entity["created_at"],
559
+ ))
560
+
561
+ logger.debug("Retrieved %d feedback records for agent %s", len(records), agent_name)
562
+ return records
563
+
564
+ except Exception as e:
565
+ # Log the error.
566
+ logger.error(
567
+ f"Unable to query entries for agent {agent_name}. Exception: {e}"
568
+ )
569
+ return records
408
570
 
409
571
 
410
572
  # ----------------------- Factory Function -----------------------
411
573
 
412
574
  def create_shared_link_store(store_type: str | None = None, connection_string: str | None = None) -> SharedLinkStoreInterface:
413
575
  """Factory function to create the appropriate shared link store based on configuration.
414
-
576
+
415
577
  Args:
416
578
  store_type: Type of store to create ("local" for SQLite, "azure-storage" for Azure Table Storage)
417
579
  connection_string: Connection string for the store (file path for SQLite, connection string for Azure)
418
-
580
+
419
581
  Returns:
420
582
  Configured SharedLinkStoreInterface implementation
421
583
  """