flock-core 0.5.0b9__py3-none-any.whl → 0.5.0b11__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/components/__init__.py +5 -3
- flock/components/evaluation/declarative_evaluation_component.py +10 -2
- flock/core/component/agent_component_base.py +2 -1
- flock/core/component/evaluation_component.py +6 -0
- flock/core/flock_agent.py +1 -1
- flock/core/flock_factory.py +4 -0
- flock/webapp/app/api/execution.py +248 -58
- flock/webapp/app/chat.py +63 -2
- flock/webapp/app/services/feedback_file_service.py +363 -0
- flock/webapp/app/services/sharing_store.py +167 -5
- flock/webapp/templates/partials/_execution_form.html +61 -21
- flock/webapp/templates/partials/_registry_viewer_content.html +30 -6
- flock/webapp/templates/registry_viewer.html +21 -18
- {flock_core-0.5.0b9.dist-info → flock_core-0.5.0b11.dist-info}/METADATA +4 -1
- {flock_core-0.5.0b9.dist-info → flock_core-0.5.0b11.dist-info}/RECORD +18 -17
- {flock_core-0.5.0b9.dist-info → flock_core-0.5.0b11.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b9.dist-info → flock_core-0.5.0b11.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b9.dist-info → flock_core-0.5.0b11.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
"""
|