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.
- flock/__init__.py +45 -3
- flock/core/flock.py +105 -61
- flock/core/flock_registry.py +45 -38
- flock/core/util/spliter.py +4 -0
- flock/evaluators/__init__.py +1 -0
- flock/evaluators/declarative/__init__.py +1 -0
- flock/modules/__init__.py +1 -0
- flock/modules/assertion/__init__.py +1 -0
- flock/modules/callback/__init__.py +1 -0
- flock/modules/mem0/__init__.py +1 -0
- flock/modules/mem0/mem0_module.py +63 -0
- flock/modules/mem0graph/__init__.py +1 -0
- flock/modules/mem0graph/mem0_graph_module.py +63 -0
- flock/modules/memory/__init__.py +1 -0
- flock/modules/output/__init__.py +1 -0
- flock/modules/performance/__init__.py +1 -0
- flock/tools/__init__.py +188 -0
- flock/{core/tools → tools}/azure_tools.py +284 -0
- flock/tools/code_tools.py +56 -0
- flock/tools/file_tools.py +140 -0
- flock/{core/tools/dev_tools/github.py → tools/github_tools.py} +3 -3
- flock/{core/tools → tools}/markdown_tools.py +14 -4
- flock/tools/system_tools.py +9 -0
- flock/{core/tools/llm_tools.py → tools/text_tools.py} +47 -25
- flock/tools/web_tools.py +90 -0
- flock/{core/tools → tools}/zendesk_tools.py +6 -6
- flock/webapp/app/api/execution.py +130 -30
- flock/webapp/app/chat.py +303 -16
- flock/webapp/app/config.py +15 -1
- flock/webapp/app/dependencies.py +22 -0
- flock/webapp/app/main.py +509 -18
- flock/webapp/app/services/flock_service.py +38 -13
- flock/webapp/app/services/sharing_models.py +43 -0
- flock/webapp/app/services/sharing_store.py +156 -0
- flock/webapp/static/css/chat.css +57 -0
- flock/webapp/templates/chat.html +29 -4
- flock/webapp/templates/partials/_chat_messages.html +1 -1
- flock/webapp/templates/partials/_chat_settings_form.html +22 -0
- flock/webapp/templates/partials/_execution_form.html +28 -1
- flock/webapp/templates/partials/_share_chat_link_snippet.html +11 -0
- flock/webapp/templates/partials/_share_link_snippet.html +35 -0
- flock/webapp/templates/shared_run_page.html +116 -0
- flock/workflow/activities.py +1 -0
- {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/METADATA +27 -14
- {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/RECORD +48 -28
- flock/core/tools/basic_tools.py +0 -317
- flock/modules/zep/zep_module.py +0 -187
- {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b46.dist-info → flock_core-0.4.0b49.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
599
|
+
logger.error(f"Default theme file '{theme_path}' not found after error. No theme CSS.")
|
|
161
600
|
return ""
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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(...)):
|