flock-core 0.4.0b49__py3-none-any.whl → 0.4.1__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.

Files changed (42) hide show
  1. flock/adapter/__init__.py +14 -0
  2. flock/adapter/azure_adapter.py +68 -0
  3. flock/adapter/chroma_adapter.py +73 -0
  4. flock/adapter/faiss_adapter.py +97 -0
  5. flock/adapter/pinecone_adapter.py +51 -0
  6. flock/adapter/vector_base.py +47 -0
  7. flock/config.py +1 -1
  8. flock/core/context/context.py +20 -0
  9. flock/core/flock.py +71 -91
  10. flock/core/flock_agent.py +58 -3
  11. flock/core/flock_module.py +5 -0
  12. flock/di.py +41 -0
  13. flock/modules/enterprise_memory/README.md +99 -0
  14. flock/modules/enterprise_memory/enterprise_memory_module.py +526 -0
  15. flock/modules/mem0/mem0_module.py +79 -16
  16. flock/modules/mem0_async/async_mem0_module.py +126 -0
  17. flock/modules/memory/memory_module.py +28 -8
  18. flock/modules/performance/metrics_module.py +24 -1
  19. flock/modules/zep/__init__.py +1 -0
  20. flock/modules/zep/zep_module.py +192 -0
  21. flock/webapp/app/api/execution.py +108 -73
  22. flock/webapp/app/chat.py +96 -12
  23. flock/webapp/app/config.py +1 -1
  24. flock/webapp/app/main.py +14 -12
  25. flock/webapp/app/services/sharing_models.py +38 -0
  26. flock/webapp/app/services/sharing_store.py +60 -1
  27. flock/webapp/static/css/chat.css +2 -0
  28. flock/webapp/templates/base.html +91 -1
  29. flock/webapp/templates/chat.html +64 -1
  30. flock/webapp/templates/partials/_agent_detail_form.html +3 -3
  31. flock/webapp/templates/partials/_chat_messages.html +50 -4
  32. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  33. flock/webapp/templates/partials/_results_display.html +54 -11
  34. flock/webapp/templates/partials/_structured_data_view.html +2 -2
  35. flock/webapp/templates/shared_run_page.html +27 -0
  36. {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/METADATA +6 -4
  37. {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/RECORD +41 -30
  38. flock/modules/mem0graph/mem0_graph_module.py +0 -63
  39. /flock/modules/{mem0graph → mem0_async}/__init__.py +0 -0
  40. {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/WHEEL +0 -0
  41. {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/entry_points.txt +0 -0
  42. {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,7 @@ import json
3
3
  from pathlib import Path
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
+ import markdown2 # Import markdown2
6
7
  from fastapi import ( # Ensure Form and HTTPException are imported
7
8
  APIRouter,
8
9
  Depends,
@@ -16,9 +17,6 @@ if TYPE_CHECKING:
16
17
  from flock.core.flock import Flock
17
18
 
18
19
 
19
- from flock.core.flock import (
20
- Flock as ConcreteFlock, # For creating Flock instance
21
- )
22
20
  from flock.core.logging.logging import (
23
21
  get_logger as get_flock_logger, # For logging within the new endpoint
24
22
  )
@@ -28,6 +26,7 @@ from flock.core.util.spliter import parse_schema
28
26
  from flock.webapp.app.dependencies import (
29
27
  get_flock_instance,
30
28
  get_optional_flock_instance,
29
+ get_shared_link_store,
31
30
  )
32
31
 
33
32
  # Service function now takes app_state
@@ -35,12 +34,18 @@ from flock.webapp.app.services.flock_service import (
35
34
  run_current_flock_service,
36
35
  # get_current_flock_instance IS NO LONGER IMPORTED
37
36
  )
38
- from flock.webapp.app.utils import pydantic_to_dict
37
+ from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
39
38
 
40
39
  router = APIRouter()
41
40
  BASE_DIR = Path(__file__).resolve().parent.parent.parent
42
41
  templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
43
42
 
43
+ # Add markdown2 filter to Jinja2 environment for this router
44
+ def markdown_filter(text):
45
+ return markdown2.markdown(text, extras=["tables", "fenced-code-blocks"])
46
+
47
+ templates.env.filters['markdown'] = markdown_filter
48
+
44
49
 
45
50
  @router.get("/htmx/execution-form-content", response_class=HTMLResponse)
46
51
  async def htmx_get_execution_form_content(
@@ -102,14 +107,9 @@ async def htmx_get_agent_input_form(
102
107
  @router.post("/htmx/run", response_class=HTMLResponse)
103
108
  async def htmx_run_flock(
104
109
  request: Request,
105
- # current_flock: Flock = Depends(get_flock_instance) # Service will use app_state
106
110
  ):
107
- # The service function run_current_flock_service now takes app_state
108
- # We retrieve current_flock from app_state inside the service or before calling if needed for validation here
109
-
110
- # It's better to get flock from app_state here to validate before calling service
111
111
  current_flock_from_state: Flock | None = getattr(request.app.state, 'flock_instance', None)
112
- logger = get_flock_logger("webapp.execution.regular_run") # Standard logger
112
+ logger = get_flock_logger("webapp.execution.regular_run")
113
113
 
114
114
  if not current_flock_from_state:
115
115
  logger.error("HTMX Run (Regular): No Flock loaded in app_state.")
@@ -150,30 +150,23 @@ async def htmx_run_flock(
150
150
  logger.error(f"HTMX Run (Regular): Error processing inputs for '{start_agent_name}': {e_parse}", exc_info=True)
151
151
  return HTMLResponse(f"<p class='error'>Error processing inputs for {start_agent_name}: {e_parse}</p>")
152
152
 
153
- try:
154
- logger.info(f"HTMX Run (Regular): Executing agent '{start_agent_name}' from Flock '{current_flock_from_state.name}' via service.")
155
- result_data = await run_current_flock_service(start_agent_name, inputs, request.app.state)
156
- except Exception as e_run:
157
- logger.error(f"HTMX Run (Regular): Error during service execution for '{start_agent_name}': {e_run}", exc_info=True)
158
- return templates.TemplateResponse(
159
- "partials/_results_display.html",
160
- {"request": request, "result_data": {"error": f"Error during execution: {e_run!s}"}},
161
- )
162
-
163
- # Process and serialize result for template
164
- try:
165
- processed_result = pydantic_to_dict(result_data)
166
- try: json.dumps(processed_result)
167
- except (TypeError, ValueError) as ser_e:
168
- processed_result = f"Error: Result contains non-serializable data: {ser_e!s}\nOriginal result preview: {str(result_data)[:500]}..."
169
- logger.warning(f"HTMX Run (Regular): Serialization issue: {processed_result}")
170
- except Exception as proc_e:
171
- processed_result = f"Error: Failed to process result data: {proc_e!s}"
172
- logger.error(f"HTMX Run (Regular): Result processing error: {processed_result}", exc_info=True)
153
+ result_data = await run_current_flock_service(start_agent_name, inputs, request.app.state)
154
+ raw_json_for_template = json.dumps(result_data, indent=2)
155
+ # Unescape newlines for proper display in HTML <pre> tag
156
+ result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
173
157
 
174
158
  return templates.TemplateResponse(
175
159
  "partials/_results_display.html",
176
- {"request": request, "result_data": processed_result},
160
+ {
161
+ "request": request,
162
+ "result": result_data,
163
+ "result_raw_json": result_data_raw_json_str,
164
+ "feedback_endpoint": "/ui/api/flock/htmx/feedback",
165
+ "share_id": None,
166
+ "flock_name": current_flock_from_state.name,
167
+ "agent_name": start_agent_name,
168
+ "flock_definition": current_flock_from_state.to_yaml(),
169
+ }
177
170
  )
178
171
 
179
172
 
@@ -181,27 +174,18 @@ async def htmx_run_flock(
181
174
  @router.post("/htmx/run-shared", response_class=HTMLResponse)
182
175
  async def htmx_run_shared_flock(
183
176
  request: Request,
184
- share_id: str = Form(...), # Expect share_id from the form
185
- # flock_definition_str: str = Form(...), # No longer needed
186
- # start_agent_name from form is still required implicitly via form_data.get()
187
- # No DI for current_flock, as we are using the one from app.state via share_id
177
+ share_id: str = Form(...),
188
178
  ):
189
179
  shared_logger = get_flock_logger("webapp.execution.shared_run_stateful")
190
180
  form_data = await request.form()
191
181
  start_agent_name = form_data.get("start_agent_name")
192
182
 
193
183
  if not start_agent_name:
194
- shared_logger.warning("HTMX Run Shared (Stateful): Starting agent name not provided in form.")
195
- return HTMLResponse("<p class='error'>Critical error: Starting agent name missing from shared run form.</p>")
196
-
197
- shared_logger.info(f"HTMX Run Shared (Stateful): Attempting to execute agent '{start_agent_name}' using pre-loaded Flock for share_id '{share_id}'.")
198
-
199
- inputs = {}
200
- result_data: Any = None
201
- temp_flock: ConcreteFlock | None = None
184
+ shared_logger.warning("HTMX Run Shared: Starting agent not selected.")
185
+ return HTMLResponse("<p class='error'>Starting agent not selected for shared run.</p>")
202
186
 
187
+ inputs: dict[str, Any] = {}
203
188
  try:
204
- # Retrieve the pre-loaded Flock instance from app.state
205
189
  shared_flocks_store = getattr(request.app.state, 'shared_flocks', {})
206
190
  temp_flock = shared_flocks_store.get(share_id)
207
191
 
@@ -216,14 +200,11 @@ async def htmx_run_shared_flock(
216
200
  shared_logger.error(f"HTMX Run Shared: Agent '{start_agent_name}' not found in shared Flock '{temp_flock.name}'.")
217
201
  return HTMLResponse(f"<p class='error'>Agent '{start_agent_name}' not found in the provided shared Flock definition.</p>")
218
202
 
219
- # Parse inputs for the agent from the temporary flock
220
203
  if agent.input and isinstance(agent.input, str):
221
204
  parsed_spec = parse_schema(agent.input)
222
205
  for name, type_str, _ in parsed_spec:
223
- form_field_name = f"agent_input_{name}" # Name used in shared_run_page.html
206
+ form_field_name = f"agent_input_{name}"
224
207
  raw_value = form_data.get(form_field_name)
225
-
226
- # Input type conversion (consistent with the other endpoint)
227
208
  if raw_value is None and "bool" in type_str.lower(): inputs[name] = False; continue
228
209
  if raw_value is None: inputs[name] = None; continue
229
210
  if "int" in type_str.lower(): inputs[name] = int(raw_value)
@@ -234,31 +215,85 @@ async def htmx_run_shared_flock(
234
215
 
235
216
  shared_logger.info(f"HTMX Run Shared: Executing agent '{start_agent_name}' in pre-loaded Flock '{temp_flock.name}'. Inputs: {list(inputs.keys())}")
236
217
  result_data = await temp_flock.run_async(start_agent=start_agent_name, input=inputs, box_result=False)
218
+ raw_json_for_template = json.dumps(result_data, indent=2)
219
+ # Unescape newlines for proper display in HTML <pre> tag
220
+ result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
237
221
  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'}")
238
222
 
239
- # Process and serialize result for template (same logic as other endpoint)
240
- try:
241
- processed_result = pydantic_to_dict(result_data)
242
- try: json.dumps(processed_result)
243
- except (TypeError, ValueError) as ser_e:
244
- processed_result = f"Error: Result contains non-serializable data: {ser_e!s}\nOriginal result preview: {str(result_data)[:500]}..."
245
- shared_logger.warning(f"HTMX Run Shared: Serialization issue: {processed_result}")
246
- except Exception as proc_e:
247
- processed_result = f"Error: Failed to process result data: {proc_e!s}"
248
- shared_logger.error(f"HTMX Run Shared: Result processing error: {processed_result}", exc_info=True)
249
-
250
- return templates.TemplateResponse(
251
- "partials/_results_display.html",
252
- {"request": request, "result_data": processed_result},
253
- )
254
-
255
- except ValueError as ve: # Catch specific input parsing errors
256
- shared_logger.error(f"HTMX Run Shared: Input parsing error for agent '{start_agent_name}': {ve}", exc_info=True)
223
+ except ValueError as ve:
224
+ shared_logger.error(f"HTMX Run Shared: Input parsing error for '{start_agent_name}' (share_id: {share_id}): {ve}", exc_info=True)
257
225
  return HTMLResponse(f"<p class='error'>Invalid input format: {ve!s}</p>")
258
- except Exception as e_general:
259
- error_message = f"An unexpected error occurred during shared execution: {e_general!s}"
260
- shared_logger.error(f"HTMX Run Shared: General error for agent '{start_agent_name}': {e_general}", exc_info=True)
261
- return templates.TemplateResponse(
262
- "partials/_results_display.html",
263
- {"request": request, "result_data": {"error": error_message } },
264
- )
226
+ except Exception as e:
227
+ shared_logger.error(f"HTMX Run Shared: Error during execution for '{start_agent_name}' (share_id: {share_id}): {e}", exc_info=True)
228
+ return HTMLResponse(f"<p class='error'>An unexpected error occurred: {e!s}</p>")
229
+
230
+ return templates.TemplateResponse(
231
+ "partials/_results_display.html",
232
+ {
233
+ "request": request,
234
+ "result": result_data,
235
+ "result_raw_json": result_data_raw_json_str,
236
+ "feedback_endpoint": "/ui/api/flock/htmx/feedback-shared",
237
+ "share_id": share_id,
238
+ "flock_name": temp_flock.name,
239
+ "agent_name": start_agent_name,
240
+ "flock_definition": temp_flock.to_yaml(),
241
+ }
242
+ )
243
+
244
+ # --- Feedback endpoints ---
245
+ @router.post("/htmx/feedback", response_class=HTMLResponse)
246
+ async def htmx_submit_feedback(
247
+ request: Request,
248
+ reason: str = Form(...),
249
+ expected_response: str | None = Form(None),
250
+ actual_response: str | None = Form(None),
251
+ flock_name: str | None = Form(None),
252
+ agent_name: str | None = Form(None),
253
+ flock_definition: str | None = Form(None),
254
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store),
255
+ ):
256
+ from uuid import uuid4
257
+
258
+ from flock.webapp.app.services.sharing_models import FeedbackRecord
259
+
260
+ record = FeedbackRecord(
261
+ feedback_id=uuid4().hex,
262
+ share_id=None,
263
+ context_type="agent_run",
264
+ reason=reason,
265
+ expected_response=expected_response,
266
+ actual_response=actual_response,
267
+ flock_name=flock_name,
268
+ agent_name=agent_name,
269
+ flock_definition=flock_definition,
270
+ )
271
+ await store.save_feedback(record)
272
+ return HTMLResponse("<p>🙏 Feedback received – thank you!</p>")
273
+
274
+
275
+ @router.post("/htmx/feedback-shared", response_class=HTMLResponse)
276
+ async def htmx_submit_feedback_shared(
277
+ request: Request,
278
+ share_id: str = Form(...),
279
+ reason: str = Form(...),
280
+ expected_response: str | None = Form(None),
281
+ actual_response: str | None = Form(None),
282
+ flock_definition: str | None = Form(None),
283
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store),
284
+ ):
285
+ from uuid import uuid4
286
+
287
+ from flock.webapp.app.services.sharing_models import FeedbackRecord
288
+
289
+ record = FeedbackRecord(
290
+ feedback_id=uuid4().hex,
291
+ share_id=share_id,
292
+ context_type="agent_run",
293
+ reason=reason,
294
+ expected_response=expected_response,
295
+ actual_response=actual_response,
296
+ flock_definition=flock_definition,
297
+ )
298
+ await store.save_feedback(record)
299
+ return HTMLResponse("<p>🙏 Feedback received for shared run – thank you!</p>")
flock/webapp/app/chat.py CHANGED
@@ -6,7 +6,7 @@ from datetime import datetime
6
6
  from uuid import uuid4
7
7
 
8
8
  import markdown2 # Added for Markdown to HTML conversion
9
- from fastapi import APIRouter, Depends, Form, Request
9
+ from fastapi import APIRouter, Depends, Form, Request, Response
10
10
  from fastapi.responses import HTMLResponse
11
11
  from pydantic import BaseModel
12
12
 
@@ -14,7 +14,10 @@ from flock.core.flock import Flock
14
14
  from flock.core.logging.logging import get_logger
15
15
  from flock.webapp.app.dependencies import get_shared_link_store
16
16
  from flock.webapp.app.main import get_base_context_web, templates
17
- from flock.webapp.app.services.sharing_models import SharedLinkConfig
17
+ from flock.webapp.app.services.sharing_models import (
18
+ FeedbackRecord,
19
+ SharedLinkConfig,
20
+ )
18
21
  from flock.webapp.app.services.sharing_store import SharedLinkStoreInterface
19
22
 
20
23
  router = APIRouter()
@@ -224,7 +227,15 @@ async def chat_send(request: Request, message: str = Form(...)):
224
227
  # If even echo should be markdown, remove is_error=True here and let it pass through. For now, plain.
225
228
 
226
229
  duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
227
- history.append({"role": "bot", "text": bot_text, "timestamp": current_time, "agent": bot_agent or "echo", "duration_ms": duration_ms})
230
+ history.append({
231
+ "role": "bot",
232
+ "text": bot_text,
233
+ "timestamp": current_time,
234
+ "agent": bot_agent or "echo",
235
+ "duration_ms": duration_ms,
236
+ "raw_json": original_bot_text if 'original_bot_text' in locals() else bot_text,
237
+ "flock_yaml": getattr(flock_inst, 'to_yaml', lambda: "")()
238
+ })
228
239
  # Return updated history partial
229
240
  return templates.TemplateResponse(
230
241
  "partials/_chat_messages.html",
@@ -272,7 +283,7 @@ async def chat_settings_form(request: Request):
272
283
  return templates.TemplateResponse("partials/_chat_settings_form.html", context)
273
284
 
274
285
 
275
- @router.post("/chat/settings", response_class=HTMLResponse, include_in_schema=False)
286
+ @router.post("/chat/settings", include_in_schema=False)
276
287
  async def chat_settings_submit(
277
288
  request: Request,
278
289
  agent_name: str | None = Form(default=None),
@@ -280,19 +291,23 @@ async def chat_settings_submit(
280
291
  history_key: str = Form("history"),
281
292
  response_key: str = Form("response"),
282
293
  ):
283
- """Apply submitted chat config, then re-render the form with a success message."""
294
+ """Handles chat settings submission and triggers a toast notification."""
284
295
  cfg = get_chat_config(request)
285
- cfg.agent_name = agent_name or None
296
+ cfg.agent_name = agent_name
286
297
  cfg.message_key = message_key
287
298
  cfg.history_key = history_key
288
299
  cfg.response_key = response_key
289
300
 
290
- headers = {
291
- "HX-Trigger": json.dumps({"notify": {"type": "success", "message": "Chat settings saved"}}),
292
- "HX-Redirect": "/chat"
301
+ logger.info(f"Chat settings updated: Agent: {cfg.agent_name}, MsgKey: {cfg.message_key}, HistKey: {cfg.history_key}, RespKey: {cfg.response_key}")
302
+
303
+ toast_event = {
304
+ "showGlobalToast": {
305
+ "message": "Chat settings saved successfully!",
306
+ "type": "success"
307
+ }
293
308
  }
294
- # Response body empty; HTMX will redirect
295
- return HTMLResponse("", headers=headers)
309
+ headers = {"HX-Trigger": json.dumps(toast_event)}
310
+ return Response(status_code=204, headers=headers)
296
311
 
297
312
 
298
313
  # --- Stand-alone Chat HTML page access to settings --------------------------
@@ -512,9 +527,78 @@ async def chat_send_shared(
512
527
  bot_text = f"Shared Echo: {message}"
513
528
 
514
529
  duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
515
- history.append({"role": "bot", "text": bot_text, "timestamp": current_time, "agent": bot_agent or "shared-echo", "duration_ms": duration_ms})
530
+ history.append({
531
+ "role": "bot",
532
+ "text": bot_text,
533
+ "timestamp": current_time,
534
+ "agent": bot_agent or "shared-echo",
535
+ "duration_ms": duration_ms,
536
+ "raw_json": original_bot_text if 'original_bot_text' in locals() else bot_text,
537
+ "flock_yaml": getattr(flock_inst, 'to_yaml', lambda: "")()
538
+ })
516
539
 
517
540
  return templates.TemplateResponse(
518
541
  "partials/_chat_messages.html",
519
542
  {"request": request, "history": history, "now": datetime.now}
520
543
  )
544
+
545
+ # ---------------- Feedback endpoints ----------------
546
+ @router.post("/chat/htmx/feedback", response_class=HTMLResponse, include_in_schema=False)
547
+ async def chat_feedback(request: Request,
548
+ reason: str = Form(...),
549
+ expected_response: str | None = Form(None),
550
+ actual_response: str | None = Form(None),
551
+ flock_definition: str | None = Form(None),
552
+ agent_name: str | None = Form(None),
553
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)):
554
+ from uuid import uuid4
555
+ rec = FeedbackRecord(
556
+ feedback_id=uuid4().hex,
557
+ share_id=None,
558
+ context_type="chat",
559
+ reason=reason,
560
+ expected_response=expected_response,
561
+ actual_response=actual_response,
562
+ flock_definition=flock_definition,
563
+ agent_name=agent_name,
564
+ )
565
+ await store.save_feedback(rec)
566
+ toast_event = {
567
+ "showGlobalToast": {
568
+ "message": "Feedback received! Thanks",
569
+ "type": "success"
570
+ }
571
+ }
572
+ headers = {"HX-Trigger": json.dumps(toast_event)}
573
+ return Response(status_code=204, headers=headers)
574
+
575
+
576
+ @router.post("/chat/htmx/feedback-shared", response_class=HTMLResponse, include_in_schema=False)
577
+ async def chat_feedback_shared(request: Request,
578
+ share_id: str = Form(...),
579
+ reason: str = Form(...),
580
+ expected_response: str | None = Form(None),
581
+ actual_response: str | None = Form(None),
582
+ flock_definition: str | None = Form(None),
583
+ agent_name: str | None = Form(None),
584
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)):
585
+ from uuid import uuid4
586
+ rec = FeedbackRecord(
587
+ feedback_id=uuid4().hex,
588
+ share_id=share_id,
589
+ context_type="chat",
590
+ reason=reason,
591
+ expected_response=expected_response,
592
+ actual_response=actual_response,
593
+ flock_definition=flock_definition,
594
+ agent_name=agent_name,
595
+ )
596
+ await store.save_feedback(rec)
597
+ toast_event = {
598
+ "showGlobalToast": {
599
+ "message": "Feedback received! Thanks",
600
+ "type": "success"
601
+ }
602
+ }
603
+ headers = {"HX-Trigger": json.dumps(toast_event)}
604
+ return Response(status_code=204, headers=headers)
@@ -38,7 +38,7 @@ THEMES_DIR = FLOCK_BASE_DIR / "themes"
38
38
  CURRENT_FLOCK_INSTANCE: "Flock | None" = None
39
39
  CURRENT_FLOCK_FILENAME: str | None = None
40
40
 
41
- DEFAULT_THEME_NAME = OutputTheme.everblush.value # Default if random fails or invalid theme specified
41
+ DEFAULT_THEME_NAME = OutputTheme.ciapre.value # Default if random fails or invalid theme specified
42
42
 
43
43
  def list_available_themes() -> list[str]:
44
44
  """Scans the THEMES_DIR for .toml files and returns their names (without .toml)."""
flock/webapp/app/main.py CHANGED
@@ -9,6 +9,7 @@ import uuid
9
9
  from contextlib import asynccontextmanager
10
10
  from pathlib import Path
11
11
 
12
+ import markdown2 # Import markdown2
12
13
  from fastapi import (
13
14
  Depends,
14
15
  FastAPI,
@@ -237,6 +238,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
237
238
  app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
238
239
  templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
239
240
 
241
+ # Add markdown2 filter to Jinja2 environment
242
+ def markdown_filter(text):
243
+ return markdown2.markdown(text, extras=["tables", "fenced-code-blocks"])
244
+
245
+ templates.env.filters['markdown'] = markdown_filter
246
+
240
247
  core_api_router = create_api_router()
241
248
  app.include_router(core_api_router, prefix="/api", tags=["Flock API Core"])
242
249
  app.include_router(flock_management.router, prefix="/ui/api/flock", tags=["UI Flock Management"])
@@ -758,11 +765,10 @@ async def ui_load_flock_by_name_action(request: Request, selected_flock_filename
758
765
  ui_mode_query = request.query_params.get("ui_mode", "standalone")
759
766
  if loaded_flock:
760
767
  success_message_text = f"Flock '{loaded_flock.name}' loaded from '{selected_flock_filename}'."
761
- response_headers["HX-Push-Url"] = "/ui/editor/properties?ui_mode=" + request.query_params.get("ui_mode", "standalone")
768
+ response_headers["HX-Push-Url"] = "/ui/editor/execute?ui_mode=" + ui_mode_query
762
769
  response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_message_text}})
763
- # Use get_base_context_web to ensure all necessary context vars are present for the partial
764
770
  context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
765
- return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
771
+ return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
766
772
  else:
767
773
  error_message_text = f"Failed to load flock file '{selected_flock_filename}'."
768
774
  response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": error_message_text}})
@@ -790,16 +796,14 @@ async def ui_load_flock_by_upload_action(request: Request, flock_file_upload: Up
790
796
  loaded_flock = load_flock_from_file_service(filename_to_load, request.app.state)
791
797
  if loaded_flock:
792
798
  success_message_text = f"Flock '{loaded_flock.name}' loaded from '{filename_to_load}'."
793
- response_headers["HX-Push-Url"] = f"/ui/editor/properties?ui_mode={ui_mode_query}"
799
+ response_headers["HX-Push-Url"] = f"/ui/editor/execute?ui_mode={ui_mode_query}"
794
800
  response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "flockFileListChanged": None, "notify": {"type": "success", "message": success_message_text}})
795
- # CORRECTED CALL:
796
801
  context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
797
- return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
802
+ return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
798
803
  else: error_message_text = f"Failed to process uploaded '{filename_to_load}'."
799
804
 
800
805
  final_error_msg = error_message_text or "Upload failed."
801
806
  response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": final_error_msg}})
802
- # CORRECTED CALL:
803
807
  context = get_base_context_web(request, error=final_error_msg, ui_mode=ui_mode_query)
804
808
  return templates.TemplateResponse("partials/_create_flock_form.html", context, headers=response_headers)
805
809
 
@@ -807,16 +811,14 @@ async def ui_load_flock_by_upload_action(request: Request, flock_file_upload: Up
807
811
  async def ui_create_flock_action(request: Request, flock_name: str = Form(...), default_model: str = Form(None), description: str = Form(None)):
808
812
  ui_mode_query = request.query_params.get("ui_mode", "standalone")
809
813
  if not flock_name.strip():
810
- # CORRECTED CALL:
811
814
  context = get_base_context_web(request, error="Flock name cannot be empty.", ui_mode=ui_mode_query)
812
815
  return templates.TemplateResponse("partials/_create_flock_form.html", context)
813
816
 
814
817
  new_flock = create_new_flock_service(flock_name, default_model, description, request.app.state)
815
- success_msg_text = f"New flock '{new_flock.name}' created. Configure properties and save."
816
- response_headers = {"HX-Push-Url": f"/ui/editor/properties?ui_mode={ui_mode_query}", "HX-Trigger": json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_msg_text}})}
817
- # CORRECTED CALL:
818
+ success_msg_text = f"New flock '{new_flock.name}' created. Navigating to Execute page. Configure properties and agents as needed."
819
+ response_headers = {"HX-Push-Url": f"/ui/editor/execute?ui_mode={ui_mode_query}", "HX-Trigger": json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_msg_text}})}
818
820
  context = get_base_context_web(request, success=success_msg_text, ui_mode=ui_mode_query)
819
- return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
821
+ return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
820
822
 
821
823
  # --- Settings Page & Endpoints ---
822
824
  @app.get("/ui/settings", response_class=HTMLResponse, tags=["UI Pages"])
@@ -41,3 +41,41 @@ class SharedLinkConfig(BaseModel):
41
41
  ]
42
42
  }
43
43
  }
44
+
45
+ # -----------------------------------------------------------
46
+ # Feedback model (user ratings / corrections)
47
+ # -----------------------------------------------------------
48
+
49
+
50
+ class FeedbackRecord(BaseModel):
51
+ """A user-submitted piece of feedback for an agent run or chat turn."""
52
+
53
+ feedback_id: str = Field(..., description="Unique identifier for this feedback entry.")
54
+ share_id: str | None = Field(None, description="If the feedback refers to a shared link, its ID; otherwise None.")
55
+ context_type: str = Field(
56
+ default="agent_run",
57
+ description="Where the feedback originates (agent_run | chat | other)",
58
+ )
59
+ reason: str = Field(..., description="User-supplied reason / comment.")
60
+ expected_response: str | None = Field(
61
+ None,
62
+ description="Desired or corrected response in JSON or plain text.",
63
+ )
64
+ actual_response: str | None = Field(
65
+ None,
66
+ description="Original response shown to the user (optional, for context).",
67
+ )
68
+ created_at: datetime = Field(default_factory=datetime.utcnow)
69
+ # When share_id is None store explicit target identifiers
70
+ flock_name: str | None = Field(
71
+ None,
72
+ description="Name of the flock that produced the result when no share_id is used.",
73
+ )
74
+ agent_name: str | None = Field(
75
+ None,
76
+ description="Name of the agent or chat involved in the feedback.",
77
+ )
78
+ flock_definition: str | None = Field(
79
+ None,
80
+ description="Full YAML definition of the flock when feedback was submitted.",
81
+ )
@@ -5,7 +5,10 @@ from pathlib import Path
5
5
 
6
6
  import aiosqlite
7
7
 
8
- from flock.webapp.app.services.sharing_models import SharedLinkConfig
8
+ from flock.webapp.app.services.sharing_models import (
9
+ FeedbackRecord,
10
+ SharedLinkConfig,
11
+ )
9
12
 
10
13
  # Get a logger instance
11
14
  logger = logging.getLogger(__name__)
@@ -33,6 +36,12 @@ class SharedLinkStoreInterface(ABC):
33
36
  """Deletes a shared link configuration by its ID. Returns True if deleted, False otherwise."""
34
37
  pass
35
38
 
39
+ # Feedback
40
+ @abstractmethod
41
+ async def save_feedback(self, record: FeedbackRecord):
42
+ """Persist a feedback record."""
43
+ pass
44
+
36
45
  class SQLiteSharedLinkStore(SharedLinkStoreInterface):
37
46
  """SQLite implementation for storing and retrieving shared link configurations."""
38
47
 
@@ -76,6 +85,25 @@ class SQLiteSharedLinkStore(SharedLinkStoreInterface):
76
85
  else:
77
86
  raise # Re-raise if it's a different operational error
78
87
 
88
+ # Feedback table
89
+ await db.execute(
90
+ """
91
+ CREATE TABLE IF NOT EXISTS feedback (
92
+ feedback_id TEXT PRIMARY KEY,
93
+ share_id TEXT,
94
+ context_type TEXT NOT NULL,
95
+ reason TEXT NOT NULL,
96
+ expected_response TEXT,
97
+ actual_response TEXT,
98
+ flock_name TEXT,
99
+ agent_name TEXT,
100
+ flock_definition TEXT,
101
+ created_at TEXT NOT NULL,
102
+ FOREIGN KEY(share_id) REFERENCES shared_links(share_id)
103
+ )
104
+ """
105
+ )
106
+
79
107
  await db.commit()
80
108
  logger.info(f"Database initialized and shared_links table schema ensured at {self.db_path}")
81
109
  except sqlite3.Error as e:
@@ -154,3 +182,34 @@ class SQLiteSharedLinkStore(SharedLinkStoreInterface):
154
182
  except sqlite3.Error as e:
155
183
  logger.error(f"SQLite error deleting config for ID {share_id}: {e}", exc_info=True)
156
184
  return False # Or raise
185
+
186
+ # ----------------------- Feedback methods -----------------------
187
+
188
+ async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
189
+ """Persist a feedback record to SQLite."""
190
+ try:
191
+ async with aiosqlite.connect(self.db_path) as db:
192
+ await db.execute(
193
+ """INSERT INTO feedback (
194
+ feedback_id, share_id, context_type, reason,
195
+ expected_response, actual_response, flock_name, agent_name, flock_definition, created_at
196
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
197
+ (
198
+ record.feedback_id,
199
+ record.share_id,
200
+ record.context_type,
201
+ record.reason,
202
+ record.expected_response,
203
+ record.actual_response,
204
+ record.flock_name,
205
+ record.agent_name,
206
+ record.flock_definition,
207
+ record.created_at.isoformat(),
208
+ ),
209
+ )
210
+ await db.commit()
211
+ logger.info(f"Saved feedback {record.feedback_id} (share={record.share_id})")
212
+ return record
213
+ except sqlite3.Error as e:
214
+ logger.error(f"SQLite error saving feedback {record.feedback_id}: {e}", exc_info=True)
215
+ raise
@@ -39,6 +39,8 @@ body.chat-page {
39
39
  background-position: 0 0, 10px 10px;
40
40
  }
41
41
 
42
+
43
+
42
44
  .bubble {
43
45
  position: relative;
44
46
  padding: 0.75rem 1rem;