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.
- flock/adapter/__init__.py +14 -0
- flock/adapter/azure_adapter.py +68 -0
- flock/adapter/chroma_adapter.py +73 -0
- flock/adapter/faiss_adapter.py +97 -0
- flock/adapter/pinecone_adapter.py +51 -0
- flock/adapter/vector_base.py +47 -0
- flock/config.py +1 -1
- flock/core/context/context.py +20 -0
- flock/core/flock.py +71 -91
- flock/core/flock_agent.py +58 -3
- flock/core/flock_module.py +5 -0
- flock/di.py +41 -0
- flock/modules/enterprise_memory/README.md +99 -0
- flock/modules/enterprise_memory/enterprise_memory_module.py +526 -0
- flock/modules/mem0/mem0_module.py +79 -16
- flock/modules/mem0_async/async_mem0_module.py +126 -0
- flock/modules/memory/memory_module.py +28 -8
- flock/modules/performance/metrics_module.py +24 -1
- flock/modules/zep/__init__.py +1 -0
- flock/modules/zep/zep_module.py +192 -0
- flock/webapp/app/api/execution.py +108 -73
- flock/webapp/app/chat.py +96 -12
- flock/webapp/app/config.py +1 -1
- flock/webapp/app/main.py +14 -12
- flock/webapp/app/services/sharing_models.py +38 -0
- flock/webapp/app/services/sharing_store.py +60 -1
- flock/webapp/static/css/chat.css +2 -0
- flock/webapp/templates/base.html +91 -1
- flock/webapp/templates/chat.html +64 -1
- flock/webapp/templates/partials/_agent_detail_form.html +3 -3
- flock/webapp/templates/partials/_chat_messages.html +50 -4
- flock/webapp/templates/partials/_flock_properties_form.html +2 -2
- flock/webapp/templates/partials/_results_display.html +54 -11
- flock/webapp/templates/partials/_structured_data_view.html +2 -2
- flock/webapp/templates/shared_run_page.html +27 -0
- {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/METADATA +6 -4
- {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/RECORD +41 -30
- flock/modules/mem0graph/mem0_graph_module.py +0 -63
- /flock/modules/{mem0graph → mem0_async}/__init__.py +0 -0
- {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b49.dist-info → flock_core-0.4.1.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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")
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
{
|
|
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(...),
|
|
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
|
|
195
|
-
return HTMLResponse("<p class='error'>
|
|
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}"
|
|
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
|
-
|
|
240
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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({
|
|
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",
|
|
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
|
-
"""
|
|
294
|
+
"""Handles chat settings submission and triggers a toast notification."""
|
|
284
295
|
cfg = get_chat_config(request)
|
|
285
|
-
cfg.agent_name = agent_name
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
return
|
|
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({
|
|
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)
|
flock/webapp/app/config.py
CHANGED
|
@@ -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.
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
816
|
-
response_headers = {"HX-Push-Url": f"/ui/editor/
|
|
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/
|
|
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
|
|
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
|