flock-core 0.4.0b46__py3-none-any.whl → 0.4.0b49__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 (50) hide show
  1. flock/__init__.py +45 -3
  2. flock/core/flock.py +105 -61
  3. flock/core/flock_registry.py +45 -38
  4. flock/core/util/spliter.py +4 -0
  5. flock/evaluators/__init__.py +1 -0
  6. flock/evaluators/declarative/__init__.py +1 -0
  7. flock/modules/__init__.py +1 -0
  8. flock/modules/assertion/__init__.py +1 -0
  9. flock/modules/callback/__init__.py +1 -0
  10. flock/modules/mem0/__init__.py +1 -0
  11. flock/modules/mem0/mem0_module.py +63 -0
  12. flock/modules/mem0graph/__init__.py +1 -0
  13. flock/modules/mem0graph/mem0_graph_module.py +63 -0
  14. flock/modules/memory/__init__.py +1 -0
  15. flock/modules/output/__init__.py +1 -0
  16. flock/modules/performance/__init__.py +1 -0
  17. flock/tools/__init__.py +188 -0
  18. flock/{core/tools → tools}/azure_tools.py +284 -0
  19. flock/tools/code_tools.py +56 -0
  20. flock/tools/file_tools.py +140 -0
  21. flock/{core/tools/dev_tools/github.py → tools/github_tools.py} +3 -3
  22. flock/{core/tools → tools}/markdown_tools.py +14 -4
  23. flock/tools/system_tools.py +9 -0
  24. flock/{core/tools/llm_tools.py → tools/text_tools.py} +47 -25
  25. flock/tools/web_tools.py +90 -0
  26. flock/{core/tools → tools}/zendesk_tools.py +6 -6
  27. flock/webapp/app/api/execution.py +130 -30
  28. flock/webapp/app/chat.py +303 -16
  29. flock/webapp/app/config.py +15 -1
  30. flock/webapp/app/dependencies.py +22 -0
  31. flock/webapp/app/main.py +509 -18
  32. flock/webapp/app/services/flock_service.py +38 -13
  33. flock/webapp/app/services/sharing_models.py +43 -0
  34. flock/webapp/app/services/sharing_store.py +156 -0
  35. flock/webapp/static/css/chat.css +57 -0
  36. flock/webapp/templates/chat.html +29 -4
  37. flock/webapp/templates/partials/_chat_messages.html +1 -1
  38. flock/webapp/templates/partials/_chat_settings_form.html +22 -0
  39. flock/webapp/templates/partials/_execution_form.html +28 -1
  40. flock/webapp/templates/partials/_share_chat_link_snippet.html +11 -0
  41. flock/webapp/templates/partials/_share_link_snippet.html +35 -0
  42. flock/webapp/templates/shared_run_page.html +116 -0
  43. flock/workflow/activities.py +1 -0
  44. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/METADATA +27 -14
  45. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/RECORD +48 -28
  46. flock/core/tools/basic_tools.py +0 -317
  47. flock/modules/zep/zep_module.py +0 -187
  48. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/WHEEL +0 -0
  49. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/entry_points.txt +0 -0
  50. {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/licenses/LICENSE +0 -0
flock/webapp/app/main.py CHANGED
@@ -1,14 +1,28 @@
1
1
  # src/flock/webapp/app/main.py
2
2
  import json
3
+ import os # Added import
3
4
  import shutil
4
5
  import urllib.parse
6
+
7
+ # Added for share link creation
8
+ import uuid
5
9
  from contextlib import asynccontextmanager
6
10
  from pathlib import Path
7
11
 
8
- from fastapi import FastAPI, File, Form, Query, Request, UploadFile
12
+ from fastapi import (
13
+ Depends,
14
+ FastAPI,
15
+ File,
16
+ Form,
17
+ HTTPException,
18
+ Query,
19
+ Request,
20
+ UploadFile,
21
+ )
9
22
  from fastapi.responses import HTMLResponse, RedirectResponse
10
23
  from fastapi.staticfiles import StaticFiles
11
24
  from fastapi.templating import Jinja2Templates
25
+ from pydantic import BaseModel
12
26
 
13
27
  from flock.core.api.endpoints import create_api_router
14
28
  from flock.core.api.run_store import RunStore
@@ -16,6 +30,7 @@ from flock.core.api.run_store import RunStore
16
30
  # Import core Flock components and API related modules
17
31
  from flock.core.flock import Flock # For type hinting
18
32
  from flock.core.logging.logging import get_logger # For logging
33
+ from flock.core.util.spliter import parse_schema
19
34
 
20
35
  # Import UI-specific routers
21
36
  from flock.webapp.app.api import (
@@ -27,6 +42,7 @@ from flock.webapp.app.api import (
27
42
  from flock.webapp.app.config import (
28
43
  DEFAULT_THEME_NAME,
29
44
  FLOCK_FILES_DIR,
45
+ SHARED_LINKS_DB_PATH,
30
46
  THEMES_DIR,
31
47
  get_current_theme_name,
32
48
  )
@@ -34,7 +50,9 @@ from flock.webapp.app.config import (
34
50
  # Import dependency management and config
35
51
  from flock.webapp.app.dependencies import (
36
52
  get_pending_custom_endpoints_and_clear,
53
+ get_shared_link_store,
37
54
  set_global_flock_services,
55
+ set_global_shared_link_store,
38
56
  )
39
57
 
40
58
  # Import service functions (which now expect app_state)
@@ -47,6 +65,13 @@ from flock.webapp.app.services.flock_service import (
47
65
  # Note: get_current_flock_instance/filename are removed from service,
48
66
  # as main.py will use request.app.state for this.
49
67
  )
68
+
69
+ # Added for share link creation
70
+ from flock.webapp.app.services.sharing_models import SharedLinkConfig
71
+ from flock.webapp.app.services.sharing_store import (
72
+ SharedLinkStoreInterface,
73
+ SQLiteSharedLinkStore,
74
+ )
50
75
  from flock.webapp.app.theme_mapper import alacritty_to_pico
51
76
 
52
77
  logger = get_logger("webapp.main")
@@ -113,6 +138,78 @@ async def lifespan(app: FastAPI):
113
138
  # by `start_unified_server` in `webapp/run.py` *before* uvicorn starts the app.
114
139
  # The call to `set_global_flock_services` also happens there.
115
140
 
141
+ # Initialize and set the SharedLinkStore
142
+ try:
143
+ logger.info(f"Initializing SharedLinkStore with DB path: {SHARED_LINKS_DB_PATH}")
144
+ shared_link_store = SQLiteSharedLinkStore(db_path=str(SHARED_LINKS_DB_PATH))
145
+ await shared_link_store.initialize() # Create tables if they don't exist
146
+ set_global_shared_link_store(shared_link_store)
147
+ logger.info("SharedLinkStore initialized and set globally.")
148
+ except Exception as e:
149
+ logger.error(f"Failed to initialize SharedLinkStore: {e}", exc_info=True)
150
+
151
+ # Configure chat features based on environment variables
152
+ # These are typically set by __init__.py when launching with --chat or --web --chat
153
+ flock_start_mode = os.environ.get("FLOCK_START_MODE")
154
+ flock_chat_enabled_env = os.environ.get("FLOCK_CHAT_ENABLED", "false").lower() == "true"
155
+
156
+ should_enable_chat_routes = False
157
+ if flock_start_mode == "chat":
158
+ should_enable_chat_routes = True
159
+ app.state.initial_redirect_to_chat = True # Signal dashboard to redirect
160
+ logger.info("FLOCK_START_MODE='chat'. Chat routes will be enabled and initial redirect to chat is set.")
161
+ elif flock_chat_enabled_env:
162
+ should_enable_chat_routes = True
163
+ logger.info("FLOCK_CHAT_ENABLED='true'. Chat routes will be enabled.")
164
+
165
+ app.state.chat_enabled = should_enable_chat_routes # For context in templates
166
+
167
+ if should_enable_chat_routes:
168
+ try:
169
+ from flock.webapp.app.chat import router as chat_router
170
+ app.include_router(chat_router, tags=["Chat"])
171
+ logger.info("Chat routes included in the application.")
172
+ except Exception as e:
173
+ logger.error(f"Failed to include chat routes during lifespan startup: {e}", exc_info=True)
174
+
175
+ # If in standalone chat mode, strip non-essential UI routes
176
+ if flock_start_mode == "chat":
177
+ from fastapi.routing import APIRoute
178
+ logger.info("FLOCK_START_MODE='chat'. Stripping non-chat UI routes.")
179
+
180
+ # Define tags for routes to KEEP.
181
+ # "Chat" for primary chat functionality.
182
+ # "Chat Sharing" for shared chat links & pages.
183
+ # API tags might be needed if chat agents make internal API calls or for general health/docs.
184
+ # Public static files (/static/...) are typically handled by app.mount and not in app.router.routes directly this way.
185
+ allowed_tags_for_chat_mode = {
186
+ "Chat",
187
+ "Chat Sharing",
188
+ "Flock API Core", # Keep core API for potential underlying needs
189
+ "Flock API Custom Endpoints" # Keep custom API endpoints
190
+ }
191
+
192
+ def _route_is_allowed_in_chat_mode(route: APIRoute) -> bool:
193
+ # Keep documentation (e.g. /docs, /openapi.json - usually no tags or specific tags)
194
+ # and non-API utility routes (often no tags).
195
+ if not hasattr(route, "tags") or not route.tags:
196
+ # Check common doc paths explicitly as they might not have tags or might have default tags
197
+ if route.path in ["/docs", "/openapi.json", "/redoc"]:
198
+ return True
199
+ # Allow other untagged routes for now, assuming they are essential (e.g. static mounts if they appeared here)
200
+ # This might need refinement if untagged UI routes exist.
201
+ return True
202
+ return any(tag in allowed_tags_for_chat_mode for tag in route.tags)
203
+
204
+ original_route_count = len(app.router.routes)
205
+ app.router.routes = [r for r in app.router.routes if _route_is_allowed_in_chat_mode(r)]
206
+ num_removed = original_route_count - len(app.router.routes)
207
+ logger.info(f"Stripped {num_removed} routes for chat-only mode. {len(app.router.routes)} routes remaining.")
208
+
209
+ if num_removed > 0 and hasattr(app, "openapi_schema"):
210
+ app.openapi_schema = None # Clear cached OpenAPI schema to regenerate
211
+ logger.info("Cleared OpenAPI schema cache due to route removal.")
212
+
116
213
  # Add custom routes if any were passed during server startup
117
214
  # These are retrieved from the dependency module where `start_unified_server` stored them.
118
215
  pending_endpoints = get_pending_custom_endpoints_and_clear()
@@ -147,20 +244,367 @@ app.include_router(agent_management.router, prefix="/ui/api/flock", tags=["UI Ag
147
244
  app.include_router(execution.router, prefix="/ui/api/flock", tags=["UI Execution"])
148
245
  app.include_router(registry_viewer.router, prefix="/ui/api/registry", tags=["UI Registry"])
149
246
 
247
+ # --- Share Link API Models and Endpoint ---
248
+ class CreateShareLinkRequest(BaseModel):
249
+ agent_name: str
250
+
251
+ class CreateShareLinkResponse(BaseModel):
252
+ share_url: str
253
+
254
+ @app.post("/api/v1/share/link", response_model=CreateShareLinkResponse, tags=["UI Sharing"])
255
+ async def create_share_link(
256
+ request: Request,
257
+ request_data: CreateShareLinkRequest,
258
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
259
+ ):
260
+ """Creates a new shareable link for an agent."""
261
+ share_id = uuid.uuid4().hex
262
+ agent_name = request_data.agent_name
263
+
264
+ if not agent_name: # Basic validation
265
+ raise HTTPException(status_code=400, detail="Agent name cannot be empty.")
266
+
267
+ current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
268
+ current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
269
+
270
+ if not current_flock_instance or not current_flock_filename:
271
+ logger.error("Cannot create share link: No Flock is currently loaded in the application state.")
272
+ raise HTTPException(status_code=400, detail="No Flock loaded. Cannot create share link.")
273
+
274
+ if agent_name not in current_flock_instance.agents:
275
+ logger.error(f"Agent '{agent_name}' not found in currently loaded Flock '{current_flock_instance.name}'.")
276
+ raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found in current Flock.")
277
+
278
+ try:
279
+ flock_file_path = FLOCK_FILES_DIR / current_flock_filename
280
+ if not flock_file_path.is_file():
281
+ logger.warning(f"Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
282
+ flock_definition_str = current_flock_instance.to_yaml()
283
+ else:
284
+ flock_definition_str = flock_file_path.read_text()
285
+ except Exception as e:
286
+ logger.error(f"Failed to get flock definition for sharing: {e}", exc_info=True)
287
+ raise HTTPException(status_code=500, detail="Could not retrieve Flock definition for sharing.")
288
+
289
+ config = SharedLinkConfig(
290
+ share_id=share_id,
291
+ agent_name=agent_name,
292
+ flock_definition=flock_definition_str
293
+ )
294
+ try:
295
+ await store.save_config(config)
296
+ share_url = f"/ui/shared-run/{share_id}" # Relative URL for client-side navigation
297
+ logger.info(f"Created share link for agent '{agent_name}' in Flock '{current_flock_instance.name}' with ID '{share_id}'. URL: {share_url}")
298
+ return CreateShareLinkResponse(share_url=share_url)
299
+ except Exception as e:
300
+ logger.error(f"Failed to create share link for agent '{agent_name}': {e}", exc_info=True)
301
+ raise HTTPException(status_code=500, detail=f"Failed to create share link: {e!s}")
302
+
303
+ # --- End Share Link API ---
304
+
305
+ # --- HTMX Endpoint for Generating Share Link Snippet ---
306
+ @app.post("/ui/htmx/share/generate-link", response_class=HTMLResponse, tags=["UI Sharing HTMX"])
307
+ async def htmx_generate_share_link(
308
+ request: Request,
309
+ start_agent_name: str | None = Form(None),
310
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
311
+ ):
312
+ if not start_agent_name:
313
+ logger.warning("HTMX generate share link: Agent name not provided.")
314
+ return templates.TemplateResponse(
315
+ "partials/_share_link_snippet.html",
316
+ {"request": request, "error_message": "No agent selected to share."}
317
+ )
318
+
319
+ current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
320
+ current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
321
+
322
+ if not current_flock_instance or not current_flock_filename:
323
+ logger.error("HTMX: Cannot create share link: No Flock is currently loaded.")
324
+ return templates.TemplateResponse(
325
+ "partials/_share_link_snippet.html",
326
+ {"request": request, "error_message": "No Flock loaded. Cannot create share link."}
327
+ )
328
+
329
+ if start_agent_name not in current_flock_instance.agents:
330
+ logger.error(f"HTMX: Agent '{start_agent_name}' not found in Flock '{current_flock_instance.name}'.")
331
+ return templates.TemplateResponse(
332
+ "partials/_share_link_snippet.html",
333
+ {"request": request, "error_message": f"Agent '{start_agent_name}' not found in current Flock."}
334
+ )
335
+
336
+ try:
337
+ flock_file_path = FLOCK_FILES_DIR / current_flock_filename
338
+ if not flock_file_path.is_file():
339
+ logger.warning(f"HTMX: Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
340
+ flock_definition_str = current_flock_instance.to_yaml()
341
+ else:
342
+ flock_definition_str = flock_file_path.read_text()
343
+ except Exception as e:
344
+ logger.error(f"HTMX: Failed to get flock definition for sharing: {e}", exc_info=True)
345
+ return templates.TemplateResponse(
346
+ "partials/_share_link_snippet.html",
347
+ {"request": request, "error_message": "Could not retrieve Flock definition for sharing."}
348
+ )
349
+
350
+ share_id = uuid.uuid4().hex
351
+ config = SharedLinkConfig(
352
+ share_id=share_id,
353
+ agent_name=start_agent_name,
354
+ flock_definition=flock_definition_str
355
+ )
356
+
357
+ try:
358
+ await store.save_config(config)
359
+ base_url = str(request.base_url)
360
+ full_share_url = f"{base_url.rstrip('/')}/ui/shared-run/{share_id}"
361
+
362
+ logger.info(f"HTMX: Generated share link for agent '{start_agent_name}' in Flock '{current_flock_instance.name}' with ID '{share_id}'. URL: {full_share_url}")
363
+ return templates.TemplateResponse(
364
+ "partials/_share_link_snippet.html",
365
+ {"request": request, "share_url": full_share_url, "flock_name": current_flock_instance.name, "agent_name": start_agent_name}
366
+ )
367
+ except Exception as e:
368
+ logger.error(f"HTMX: Failed to create share link for agent '{start_agent_name}': {e}", exc_info=True)
369
+ return templates.TemplateResponse(
370
+ "partials/_share_link_snippet.html",
371
+ {"request": request, "error_message": f"Could not generate link: {e!s}"}
372
+ )
373
+ # --- End HTMX Endpoint ---
374
+
375
+ # --- HTMX Endpoint for Generating SHARED CHAT Link Snippet ---
376
+ @app.post("/ui/htmx/share/chat/generate-link", response_class=HTMLResponse, tags=["UI Sharing HTMX"])
377
+ async def htmx_generate_share_chat_link(
378
+ request: Request,
379
+ agent_name: str | None = Form(None), # This is the chat agent
380
+ message_key: str | None = Form(None), # Changed default to None
381
+ history_key: str | None = Form(None), # Changed default to None
382
+ response_key: str | None = Form(None), # Changed default to None
383
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store)
384
+ ):
385
+ if not agent_name:
386
+ logger.warning("HTMX generate share chat link: Agent name not provided.")
387
+ return templates.TemplateResponse(
388
+ "partials/_share_chat_link_snippet.html", # Will create this template
389
+ {"request": request, "error_message": "No agent selected for chat sharing."}
390
+ )
391
+
392
+ current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
393
+ current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
394
+
395
+ if not current_flock_instance or not current_flock_filename:
396
+ logger.error("HTMX Chat Share: Cannot create share link: No Flock is currently loaded.")
397
+ return templates.TemplateResponse(
398
+ "partials/_share_chat_link_snippet.html",
399
+ {"request": request, "error_message": "No Flock loaded. Cannot create share link."}
400
+ )
401
+
402
+ if agent_name not in current_flock_instance.agents:
403
+ logger.error(f"HTMX Chat Share: Agent '{agent_name}' not found in Flock '{current_flock_instance.name}'.")
404
+ return templates.TemplateResponse(
405
+ "partials/_share_chat_link_snippet.html",
406
+ {"request": request, "error_message": f"Agent '{agent_name}' not found in current Flock."}
407
+ )
408
+
409
+ try:
410
+ flock_file_path = FLOCK_FILES_DIR / current_flock_filename
411
+ if not flock_file_path.is_file():
412
+ logger.warning(f"HTMX Chat Share: Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
413
+ flock_definition_str = current_flock_instance.to_yaml()
414
+ else:
415
+ flock_definition_str = flock_file_path.read_text()
416
+ except Exception as e:
417
+ logger.error(f"HTMX Chat Share: Failed to get flock definition for sharing: {e}", exc_info=True)
418
+ return templates.TemplateResponse(
419
+ "partials/_share_chat_link_snippet.html",
420
+ {"request": request, "error_message": "Could not retrieve Flock definition for sharing."}
421
+ )
422
+
423
+ share_id = uuid.uuid4().hex
424
+
425
+ # Explicitly convert empty strings from form to None for optional keys
426
+ actual_message_key = message_key if message_key else None
427
+ actual_history_key = history_key if history_key else None
428
+ actual_response_key = response_key if response_key else None
429
+
430
+ config = SharedLinkConfig(
431
+ share_id=share_id,
432
+ agent_name=agent_name, # agent_name from form is the chat agent
433
+ flock_definition=flock_definition_str,
434
+ share_type="chat",
435
+ chat_message_key=actual_message_key,
436
+ chat_history_key=actual_history_key,
437
+ chat_response_key=actual_response_key
438
+ )
439
+
440
+ try:
441
+ await store.save_config(config)
442
+ base_url = str(request.base_url)
443
+ # Link to the new /chat/shared/{share_id} endpoint
444
+ full_share_url = f"{base_url.rstrip('/')}/chat/shared/{share_id}"
445
+
446
+ logger.info(f"HTMX: Generated share CHAT link for agent '{agent_name}' in Flock '{current_flock_instance.name}' with ID '{share_id}'. URL: {full_share_url}")
447
+ return templates.TemplateResponse(
448
+ "partials/_share_chat_link_snippet.html", # Will create this template
449
+ {"request": request, "share_url": full_share_url, "flock_name": current_flock_instance.name, "agent_name": agent_name}
450
+ )
451
+ except Exception as e:
452
+ logger.error(f"HTMX Chat Share: Failed to create share link for agent '{agent_name}': {e}", exc_info=True)
453
+ return templates.TemplateResponse(
454
+ "partials/_share_chat_link_snippet.html",
455
+ {"request": request, "error_message": f"Could not generate chat link: {e!s}"}
456
+ )
457
+
458
+ # --- Route for Shared Run Page ---
459
+ @app.get("/ui/shared-run/{share_id}", response_class=HTMLResponse, tags=["UI Sharing"])
460
+ async def page_shared_run(
461
+ request: Request,
462
+ share_id: str,
463
+ store: SharedLinkStoreInterface = Depends(get_shared_link_store),
464
+ ):
465
+ logger.info(f"Accessed shared run page with share_id: {share_id}")
466
+ shared_config = await store.get_config(share_id)
467
+
468
+ if not shared_config:
469
+ logger.warning(f"Share ID {share_id} not found.")
470
+ return templates.TemplateResponse(
471
+ "error_page.html",
472
+ {"request": request, "error_title": "Link Not Found", "error_message": "The shared link does not exist or may have expired."},
473
+ status_code=404
474
+ )
475
+
476
+ agent_name_from_link = shared_config.agent_name
477
+ flock_definition_str = shared_config.flock_definition
478
+ context: dict[str, Any] = {"request": request, "is_shared_run_page": True, "share_id": share_id}
479
+
480
+ try:
481
+ from flock.core.flock import Flock as ConcreteFlock
482
+ loaded_flock = ConcreteFlock.from_yaml(flock_definition_str)
483
+
484
+ # Store the loaded_flock instance in app.state for later retrieval
485
+ if not hasattr(request.app.state, 'shared_flocks'):
486
+ request.app.state.shared_flocks = {}
487
+ request.app.state.shared_flocks[share_id] = loaded_flock
488
+ logger.info(f"Shared Run Page: Stored Flock instance for share_id {share_id} in app.state.")
489
+
490
+ context["flock"] = loaded_flock
491
+ context["selected_agent_name"] = agent_name_from_link # For pre-selection & hidden field
492
+ # flock_definition_str is no longer needed in the template for a hidden field if we reuse the instance
493
+ # context["flock_definition_str"] = flock_definition_str
494
+ logger.info(f"Shared Run Page: Loaded Flock '{loaded_flock.name}' for agent '{agent_name_from_link}'.")
495
+
496
+ if agent_name_from_link not in loaded_flock.agents:
497
+ context["error_message"] = f"Agent '{agent_name_from_link}' not found in the shared Flock definition."
498
+ logger.warning(context["error_message"])
499
+ else:
500
+ agent = loaded_flock.agents[agent_name_from_link]
501
+ input_fields = []
502
+ if agent.input and isinstance(agent.input, str):
503
+ try:
504
+ parsed_spec = parse_schema(agent.input) # parse_schema is imported at top of main.py
505
+ for name, type_str, description in parsed_spec:
506
+ field_info = {"name": name, "type": type_str.lower(), "description": description or ""}
507
+ if "bool" in field_info["type"]: field_info["html_type"] = "checkbox"
508
+ elif "int" in field_info["type"] or "float" in field_info["type"]: field_info["html_type"] = "number"
509
+ elif "list" in field_info["type"] or "dict" in field_info["type"]:
510
+ field_info["html_type"] = "textarea"; field_info["placeholder"] = f"Enter JSON for {field_info['type']}"
511
+ else: field_info["html_type"] = "text"
512
+ input_fields.append(field_info)
513
+ context["input_fields"] = input_fields
514
+ except Exception as e_parse:
515
+ logger.error(f"Shared Run Page: Error parsing input for '{agent_name_from_link}': {e_parse}", exc_info=True)
516
+ context["error_message"] = f"Could not parse inputs for agent '{agent_name_from_link}'."
517
+ else:
518
+ context["input_fields"] = [] # Agent has no inputs defined
519
+
520
+ except Exception as e_load:
521
+ logger.error(f"Shared Run Page: Failed to load Flock from definition for share_id {share_id}: {e_load}", exc_info=True)
522
+ context["error_message"] = f"Fatal: Could not load the shared Flock configuration: {e_load!s}"
523
+ context["flock"] = None
524
+ context["selected_agent_name"] = agent_name_from_link # Still pass for potential error display
525
+ context["input_fields"] = []
526
+ # context["flock_definition_str"] = flock_definition_str # Not needed if not sent to template
527
+
528
+ try:
529
+ current_theme_name = get_current_theme_name()
530
+ context["theme_css"] = generate_theme_css_web(current_theme_name)
531
+ context["active_theme_name"] = current_theme_name or DEFAULT_THEME_NAME
532
+ except Exception as e_theme:
533
+ logger.error(f"Shared Run Page: Error generating theme: {e_theme}", exc_info=True)
534
+ context["theme_css"] = ""
535
+ context["active_theme_name"] = DEFAULT_THEME_NAME
536
+
537
+ # The shared_run_page.html will now be a simple wrapper that includes _execution_form.html
538
+ return templates.TemplateResponse("shared_run_page.html", context)
539
+
540
+ # --- End Route for Shared Run Page ---
541
+
150
542
  def generate_theme_css_web(theme_name: str | None) -> str:
151
543
  if not THEME_LOADER_AVAILABLE or THEMES_DIR is None: return ""
152
- active_theme_name = theme_name or get_current_theme_name() or DEFAULT_THEME_NAME
153
- theme_filename = f"{active_theme_name}.toml"
154
- theme_path = THEMES_DIR / theme_filename
155
- if not theme_path.exists():
156
- logger.warning(f"Theme file not found: {theme_path}. Using default: {DEFAULT_THEME_NAME}.toml")
157
- theme_path = THEMES_DIR / f"{DEFAULT_THEME_NAME}.toml"
158
- active_theme_name = DEFAULT_THEME_NAME
544
+
545
+ chosen_theme_name_input = theme_name or get_current_theme_name() or DEFAULT_THEME_NAME
546
+
547
+ # Sanitize the input to get only the filename component
548
+ sanitized_name_part = Path(chosen_theme_name_input).name
549
+ # Ensure we have a stem
550
+ theme_stem_candidate = sanitized_name_part
551
+ if theme_stem_candidate.endswith(".toml"):
552
+ theme_stem_candidate = theme_stem_candidate[:-5]
553
+
554
+ effective_theme_filename = f"{theme_stem_candidate}.toml"
555
+ _theme_to_load_stem = theme_stem_candidate # This will be the name of the theme we attempt to load
556
+
557
+ try:
558
+ resolved_themes_dir = THEMES_DIR.resolve(strict=True) # Ensure THEMES_DIR itself is valid
559
+ prospective_theme_path = resolved_themes_dir / effective_theme_filename
560
+
561
+ # Resolve the prospective path
562
+ resolved_theme_path = prospective_theme_path.resolve()
563
+
564
+ # Validate:
565
+ # 1. Path is still within the resolved THEMES_DIR
566
+ # 2. The final filename component of the resolved path matches the intended filename
567
+ # (guards against symlinks or normalization changing the name unexpectedly)
568
+ # 3. The file exists
569
+ if (
570
+ str(resolved_theme_path).startswith(str(resolved_themes_dir)) and
571
+ resolved_theme_path.name == effective_theme_filename and
572
+ resolved_theme_path.is_file() # is_file checks existence too
573
+ ):
574
+ theme_path = resolved_theme_path
575
+ else:
576
+ logger.warning(
577
+ f"Validation failed or theme '{effective_theme_filename}' not found in '{resolved_themes_dir}'. "
578
+ f"Attempted path: '{prospective_theme_path}'. Resolved to: '{resolved_theme_path}'. "
579
+ f"Falling back to default theme: {DEFAULT_THEME_NAME}.toml"
580
+ )
581
+ _theme_to_load_stem = DEFAULT_THEME_NAME
582
+ theme_path = resolved_themes_dir / f"{DEFAULT_THEME_NAME}.toml"
583
+ if not theme_path.is_file():
584
+ logger.error(f"Default theme file '{theme_path}' not found. No theme CSS will be generated.")
585
+ return ""
586
+ except FileNotFoundError: # THEMES_DIR does not exist
587
+ logger.error(f"Themes directory '{THEMES_DIR}' not found. Falling back to default theme.")
588
+ _theme_to_load_stem = DEFAULT_THEME_NAME
589
+ # Attempt to use a conceptual default path if THEMES_DIR was bogus, though it's unlikely to succeed
590
+ theme_path = Path(f"{DEFAULT_THEME_NAME}.toml") # This won't be in THEMES_DIR if THEMES_DIR is bad
591
+ if not theme_path.exists(): # Check existence without assuming a base directory
592
+ logger.error(f"Default theme file '{DEFAULT_THEME_NAME}.toml' not found at root or THEMES_DIR is inaccessible. No theme CSS.")
593
+ return ""
594
+ except Exception as e:
595
+ logger.error(f"Error during theme path resolution for '{effective_theme_filename}': {e}. Falling back to default.")
596
+ _theme_to_load_stem = DEFAULT_THEME_NAME
597
+ theme_path = THEMES_DIR / f"{DEFAULT_THEME_NAME}.toml" if THEMES_DIR else Path(f"{DEFAULT_THEME_NAME}.toml")
159
598
  if not theme_path.exists():
160
- logger.warning(f"Default theme file not found: {theme_path}. No theme CSS.")
599
+ logger.error(f"Default theme file '{theme_path}' not found after error. No theme CSS.")
161
600
  return ""
162
- try: theme_dict = load_theme_from_file(str(theme_path))
163
- except Exception as e: logger.error(f"Error loading theme {theme_path}: {e}"); return ""
601
+
602
+ try:
603
+ theme_dict = load_theme_from_file(str(theme_path))
604
+ logger.debug(f"Successfully loaded theme '{_theme_to_load_stem}' from '{theme_path}'")
605
+ except Exception as e:
606
+ logger.error(f"Error loading theme file '{theme_path}' (intended: '{_theme_to_load_stem}.toml'): {e}")
607
+ return ""
164
608
 
165
609
  pico_vars = alacritty_to_pico(theme_dict)
166
610
  if not pico_vars: return ""
@@ -175,6 +619,7 @@ def get_base_context_web(
175
619
  current_flock_filename_from_state: str | None = getattr(request.app.state, "flock_filename", None)
176
620
  theme_name = get_current_theme_name()
177
621
  theme_css = generate_theme_css_web(theme_name)
622
+
178
623
  return {
179
624
  "request": request,
180
625
  "current_flock": flock_instance_from_state,
@@ -184,13 +629,18 @@ def get_base_context_web(
184
629
  "ui_mode": ui_mode,
185
630
  "theme_css": theme_css,
186
631
  "active_theme_name": theme_name,
187
- "chat_enabled": getattr(request.app.state, "chat_enabled", False),
632
+ "chat_enabled": getattr(request.app.state, "chat_enabled", False), # Reverted to app.state
188
633
  }
189
634
 
190
635
  @app.get("/", response_class=HTMLResponse, tags=["UI Pages"])
191
636
  async def page_dashboard(
192
637
  request: Request, error: str = None, success: str = None, ui_mode: str = Query(None)
193
638
  ):
639
+ # Handle initial redirect if flagged during app startup
640
+ if getattr(request.app.state, "initial_redirect_to_chat", False):
641
+ logger.info("Initial redirect to CHAT page triggered from dashboard (FLOCK_START_MODE='chat').")
642
+ return RedirectResponse(url="/chat", status_code=307)
643
+
194
644
  effective_ui_mode = ui_mode
195
645
  flock_is_preloaded = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
196
646
 
@@ -429,16 +879,57 @@ async def htmx_env_add(request: Request, var_name: str = Form(...), var_value: s
429
879
 
430
880
  @app.get("/ui/htmx/theme-preview", response_class=HTMLResponse, tags=["UI HTMX Partials"])
431
881
  async def htmx_theme_preview(request: Request, theme: str = Query(None)):
432
- theme_name = theme or get_current_theme_name() or DEFAULT_THEME_NAME
882
+ if not THEME_LOADER_AVAILABLE:
883
+ return HTMLResponse("<p>Theme loading functionality is not available.</p>", status_code=500)
884
+ if THEMES_DIR is None or not THEMES_DIR.exists():
885
+ return HTMLResponse("<p>Themes directory is not configured or does not exist.</p>", status_code=500)
886
+
887
+ chosen_theme_name_input = theme or get_current_theme_name() or DEFAULT_THEME_NAME
888
+
889
+ # Sanitize the input to get only the filename component
890
+ sanitized_name_part = Path(chosen_theme_name_input).name
891
+ # Ensure we have a stem
892
+ theme_stem_from_input = sanitized_name_part
893
+ if theme_stem_from_input.endswith(".toml"):
894
+ theme_stem_from_input = theme_stem_from_input[:-5]
895
+
896
+ theme_filename_to_load = f"{theme_stem_from_input}.toml"
897
+ theme_name_for_display = theme_stem_from_input # Use the sanitized stem for display/logging
898
+
433
899
  try:
434
- theme_path = THEMES_DIR / f"{theme_name}.toml" if THEMES_DIR else None
435
- if not (theme_path and theme_path.exists()): return HTMLResponse("<p>Theme not found.</p>")
900
+ resolved_themes_dir = THEMES_DIR.resolve(strict=True)
901
+ theme_path_candidate = resolved_themes_dir / theme_filename_to_load
902
+ resolved_theme_path = theme_path_candidate.resolve()
903
+
904
+ if not str(resolved_theme_path).startswith(str(resolved_themes_dir)) or \
905
+ resolved_theme_path.name != theme_filename_to_load:
906
+ logger.warning(f"Invalid theme path access attempt for '{theme_name_for_display}'. "
907
+ f"Original input: '{chosen_theme_name_input}', Sanitized filename: '{theme_filename_to_load}', "
908
+ f"Attempted path: '{theme_path_candidate}', Resolved to: '{resolved_theme_path}'")
909
+ return HTMLResponse(f"<p>Invalid theme name or path for '{theme_name_for_display}'.</p>", status_code=400)
910
+
911
+ if not resolved_theme_path.is_file():
912
+ logger.info(f"Theme preview: Theme file '{theme_filename_to_load}' not found at '{resolved_theme_path}'.")
913
+ return HTMLResponse(f"<p>Theme '{theme_name_for_display}' not found.</p>", status_code=404)
914
+
915
+ theme_path = resolved_theme_path
436
916
  theme_data = load_theme_from_file(str(theme_path))
437
- except Exception as e: return HTMLResponse(f"<p>Error loading theme: {e}</p>")
917
+ logger.debug(f"Successfully loaded theme '{theme_name_for_display}' for preview from '{theme_path}'")
918
+
919
+ except FileNotFoundError: # For THEMES_DIR.resolve(strict=True)
920
+ logger.error(f"Themes directory '{THEMES_DIR}' not found during preview for '{theme_name_for_display}'.")
921
+ return HTMLResponse("<p>Themes directory not found.</p>", status_code=500)
922
+ except Exception as e:
923
+ logger.error(f"Error loading theme '{theme_name_for_display}' for preview (path: '{theme_path_candidate if 'theme_path_candidate' in locals() else 'unknown'}'): {e}")
924
+ return HTMLResponse(f"<p>Error loading theme '{theme_name_for_display}': {e}</p>", status_code=500)
925
+
438
926
  css_vars = alacritty_to_pico(theme_data)
439
- css_vars_str = ":root {\n" + "\n".join([f" {k}: {v};" for k, v in css_vars.items()]) + "\n}"
927
+ if not css_vars:
928
+ return HTMLResponse(f"<p>Could not convert theme '{theme_name_for_display}' to CSS variables.</p>")
929
+
930
+ css_vars_str = ":root {\n" + "\\n".join([f" {k}: {v};" for k, v in css_vars.items()]) + "\\n}"
440
931
  main_colors = [("Background", css_vars.get("--pico-background-color")), ("Text", css_vars.get("--pico-color")), ("Primary", css_vars.get("--pico-primary")), ("Secondary", css_vars.get("--pico-secondary")), ("Muted", css_vars.get("--pico-muted-color"))]
441
- return templates.TemplateResponse("partials/_theme_preview.html", {"request": request, "theme_name": theme_name, "css_vars_str": css_vars_str, "main_colors": main_colors})
932
+ return templates.TemplateResponse("partials/_theme_preview.html", {"request": request, "theme_name": theme_name_for_display, "css_vars_str": css_vars_str, "main_colors": main_colors})
442
933
 
443
934
  @app.post("/ui/apply-theme", tags=["UI Actions"])
444
935
  async def apply_theme(request: Request, theme: str = Form(...)):