flock-core 0.4.0b48__py3-none-any.whl → 0.4.0b50__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/modules/mem0/mem0_module.py +63 -0
- flock/modules/mem0graph/__init__.py +1 -0
- flock/modules/mem0graph/mem0_graph_module.py +63 -0
- flock/webapp/app/api/execution.py +105 -47
- flock/webapp/app/chat.py +315 -24
- flock/webapp/app/config.py +15 -1
- flock/webapp/app/dependencies.py +22 -0
- flock/webapp/app/main.py +414 -14
- 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/base.html +91 -1
- flock/webapp/templates/chat.html +93 -5
- flock/webapp/templates/partials/_agent_detail_form.html +3 -3
- 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/_flock_properties_form.html +2 -2
- flock/webapp/templates/partials/_results_display.html +15 -11
- flock/webapp/templates/partials/_share_chat_link_snippet.html +11 -0
- flock/webapp/templates/partials/_share_link_snippet.html +35 -0
- flock/webapp/templates/partials/_structured_data_view.html +2 -2
- flock/webapp/templates/shared_run_page.html +143 -0
- {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/METADATA +4 -2
- {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/RECORD +31 -24
- flock/modules/zep/zep_module.py +0 -187
- /flock/modules/{zep → mem0}/__init__.py +0 -0
- {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b48.dist-info → flock_core-0.4.0b50.dist-info}/licenses/LICENSE +0 -0
flock/webapp/app/main.py
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
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
|
-
|
|
12
|
+
import markdown2 # Import markdown2
|
|
13
|
+
from fastapi import (
|
|
14
|
+
Depends,
|
|
15
|
+
FastAPI,
|
|
16
|
+
File,
|
|
17
|
+
Form,
|
|
18
|
+
HTTPException,
|
|
19
|
+
Query,
|
|
20
|
+
Request,
|
|
21
|
+
UploadFile,
|
|
22
|
+
)
|
|
9
23
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
10
24
|
from fastapi.staticfiles import StaticFiles
|
|
11
25
|
from fastapi.templating import Jinja2Templates
|
|
26
|
+
from pydantic import BaseModel
|
|
12
27
|
|
|
13
28
|
from flock.core.api.endpoints import create_api_router
|
|
14
29
|
from flock.core.api.run_store import RunStore
|
|
@@ -16,6 +31,7 @@ from flock.core.api.run_store import RunStore
|
|
|
16
31
|
# Import core Flock components and API related modules
|
|
17
32
|
from flock.core.flock import Flock # For type hinting
|
|
18
33
|
from flock.core.logging.logging import get_logger # For logging
|
|
34
|
+
from flock.core.util.spliter import parse_schema
|
|
19
35
|
|
|
20
36
|
# Import UI-specific routers
|
|
21
37
|
from flock.webapp.app.api import (
|
|
@@ -27,6 +43,7 @@ from flock.webapp.app.api import (
|
|
|
27
43
|
from flock.webapp.app.config import (
|
|
28
44
|
DEFAULT_THEME_NAME,
|
|
29
45
|
FLOCK_FILES_DIR,
|
|
46
|
+
SHARED_LINKS_DB_PATH,
|
|
30
47
|
THEMES_DIR,
|
|
31
48
|
get_current_theme_name,
|
|
32
49
|
)
|
|
@@ -34,7 +51,9 @@ from flock.webapp.app.config import (
|
|
|
34
51
|
# Import dependency management and config
|
|
35
52
|
from flock.webapp.app.dependencies import (
|
|
36
53
|
get_pending_custom_endpoints_and_clear,
|
|
54
|
+
get_shared_link_store,
|
|
37
55
|
set_global_flock_services,
|
|
56
|
+
set_global_shared_link_store,
|
|
38
57
|
)
|
|
39
58
|
|
|
40
59
|
# Import service functions (which now expect app_state)
|
|
@@ -47,6 +66,13 @@ from flock.webapp.app.services.flock_service import (
|
|
|
47
66
|
# Note: get_current_flock_instance/filename are removed from service,
|
|
48
67
|
# as main.py will use request.app.state for this.
|
|
49
68
|
)
|
|
69
|
+
|
|
70
|
+
# Added for share link creation
|
|
71
|
+
from flock.webapp.app.services.sharing_models import SharedLinkConfig
|
|
72
|
+
from flock.webapp.app.services.sharing_store import (
|
|
73
|
+
SharedLinkStoreInterface,
|
|
74
|
+
SQLiteSharedLinkStore,
|
|
75
|
+
)
|
|
50
76
|
from flock.webapp.app.theme_mapper import alacritty_to_pico
|
|
51
77
|
|
|
52
78
|
logger = get_logger("webapp.main")
|
|
@@ -113,6 +139,78 @@ async def lifespan(app: FastAPI):
|
|
|
113
139
|
# by `start_unified_server` in `webapp/run.py` *before* uvicorn starts the app.
|
|
114
140
|
# The call to `set_global_flock_services` also happens there.
|
|
115
141
|
|
|
142
|
+
# Initialize and set the SharedLinkStore
|
|
143
|
+
try:
|
|
144
|
+
logger.info(f"Initializing SharedLinkStore with DB path: {SHARED_LINKS_DB_PATH}")
|
|
145
|
+
shared_link_store = SQLiteSharedLinkStore(db_path=str(SHARED_LINKS_DB_PATH))
|
|
146
|
+
await shared_link_store.initialize() # Create tables if they don't exist
|
|
147
|
+
set_global_shared_link_store(shared_link_store)
|
|
148
|
+
logger.info("SharedLinkStore initialized and set globally.")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Failed to initialize SharedLinkStore: {e}", exc_info=True)
|
|
151
|
+
|
|
152
|
+
# Configure chat features based on environment variables
|
|
153
|
+
# These are typically set by __init__.py when launching with --chat or --web --chat
|
|
154
|
+
flock_start_mode = os.environ.get("FLOCK_START_MODE")
|
|
155
|
+
flock_chat_enabled_env = os.environ.get("FLOCK_CHAT_ENABLED", "false").lower() == "true"
|
|
156
|
+
|
|
157
|
+
should_enable_chat_routes = False
|
|
158
|
+
if flock_start_mode == "chat":
|
|
159
|
+
should_enable_chat_routes = True
|
|
160
|
+
app.state.initial_redirect_to_chat = True # Signal dashboard to redirect
|
|
161
|
+
logger.info("FLOCK_START_MODE='chat'. Chat routes will be enabled and initial redirect to chat is set.")
|
|
162
|
+
elif flock_chat_enabled_env:
|
|
163
|
+
should_enable_chat_routes = True
|
|
164
|
+
logger.info("FLOCK_CHAT_ENABLED='true'. Chat routes will be enabled.")
|
|
165
|
+
|
|
166
|
+
app.state.chat_enabled = should_enable_chat_routes # For context in templates
|
|
167
|
+
|
|
168
|
+
if should_enable_chat_routes:
|
|
169
|
+
try:
|
|
170
|
+
from flock.webapp.app.chat import router as chat_router
|
|
171
|
+
app.include_router(chat_router, tags=["Chat"])
|
|
172
|
+
logger.info("Chat routes included in the application.")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Failed to include chat routes during lifespan startup: {e}", exc_info=True)
|
|
175
|
+
|
|
176
|
+
# If in standalone chat mode, strip non-essential UI routes
|
|
177
|
+
if flock_start_mode == "chat":
|
|
178
|
+
from fastapi.routing import APIRoute
|
|
179
|
+
logger.info("FLOCK_START_MODE='chat'. Stripping non-chat UI routes.")
|
|
180
|
+
|
|
181
|
+
# Define tags for routes to KEEP.
|
|
182
|
+
# "Chat" for primary chat functionality.
|
|
183
|
+
# "Chat Sharing" for shared chat links & pages.
|
|
184
|
+
# API tags might be needed if chat agents make internal API calls or for general health/docs.
|
|
185
|
+
# Public static files (/static/...) are typically handled by app.mount and not in app.router.routes directly this way.
|
|
186
|
+
allowed_tags_for_chat_mode = {
|
|
187
|
+
"Chat",
|
|
188
|
+
"Chat Sharing",
|
|
189
|
+
"Flock API Core", # Keep core API for potential underlying needs
|
|
190
|
+
"Flock API Custom Endpoints" # Keep custom API endpoints
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
def _route_is_allowed_in_chat_mode(route: APIRoute) -> bool:
|
|
194
|
+
# Keep documentation (e.g. /docs, /openapi.json - usually no tags or specific tags)
|
|
195
|
+
# and non-API utility routes (often no tags).
|
|
196
|
+
if not hasattr(route, "tags") or not route.tags:
|
|
197
|
+
# Check common doc paths explicitly as they might not have tags or might have default tags
|
|
198
|
+
if route.path in ["/docs", "/openapi.json", "/redoc"]:
|
|
199
|
+
return True
|
|
200
|
+
# Allow other untagged routes for now, assuming they are essential (e.g. static mounts if they appeared here)
|
|
201
|
+
# This might need refinement if untagged UI routes exist.
|
|
202
|
+
return True
|
|
203
|
+
return any(tag in allowed_tags_for_chat_mode for tag in route.tags)
|
|
204
|
+
|
|
205
|
+
original_route_count = len(app.router.routes)
|
|
206
|
+
app.router.routes = [r for r in app.router.routes if _route_is_allowed_in_chat_mode(r)]
|
|
207
|
+
num_removed = original_route_count - len(app.router.routes)
|
|
208
|
+
logger.info(f"Stripped {num_removed} routes for chat-only mode. {len(app.router.routes)} routes remaining.")
|
|
209
|
+
|
|
210
|
+
if num_removed > 0 and hasattr(app, "openapi_schema"):
|
|
211
|
+
app.openapi_schema = None # Clear cached OpenAPI schema to regenerate
|
|
212
|
+
logger.info("Cleared OpenAPI schema cache due to route removal.")
|
|
213
|
+
|
|
116
214
|
# Add custom routes if any were passed during server startup
|
|
117
215
|
# These are retrieved from the dependency module where `start_unified_server` stored them.
|
|
118
216
|
pending_endpoints = get_pending_custom_endpoints_and_clear()
|
|
@@ -140,6 +238,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|
|
140
238
|
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
|
141
239
|
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
|
142
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
|
+
|
|
143
247
|
core_api_router = create_api_router()
|
|
144
248
|
app.include_router(core_api_router, prefix="/api", tags=["Flock API Core"])
|
|
145
249
|
app.include_router(flock_management.router, prefix="/ui/api/flock", tags=["UI Flock Management"])
|
|
@@ -147,6 +251,301 @@ app.include_router(agent_management.router, prefix="/ui/api/flock", tags=["UI Ag
|
|
|
147
251
|
app.include_router(execution.router, prefix="/ui/api/flock", tags=["UI Execution"])
|
|
148
252
|
app.include_router(registry_viewer.router, prefix="/ui/api/registry", tags=["UI Registry"])
|
|
149
253
|
|
|
254
|
+
# --- Share Link API Models and Endpoint ---
|
|
255
|
+
class CreateShareLinkRequest(BaseModel):
|
|
256
|
+
agent_name: str
|
|
257
|
+
|
|
258
|
+
class CreateShareLinkResponse(BaseModel):
|
|
259
|
+
share_url: str
|
|
260
|
+
|
|
261
|
+
@app.post("/api/v1/share/link", response_model=CreateShareLinkResponse, tags=["UI Sharing"])
|
|
262
|
+
async def create_share_link(
|
|
263
|
+
request: Request,
|
|
264
|
+
request_data: CreateShareLinkRequest,
|
|
265
|
+
store: SharedLinkStoreInterface = Depends(get_shared_link_store)
|
|
266
|
+
):
|
|
267
|
+
"""Creates a new shareable link for an agent."""
|
|
268
|
+
share_id = uuid.uuid4().hex
|
|
269
|
+
agent_name = request_data.agent_name
|
|
270
|
+
|
|
271
|
+
if not agent_name: # Basic validation
|
|
272
|
+
raise HTTPException(status_code=400, detail="Agent name cannot be empty.")
|
|
273
|
+
|
|
274
|
+
current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
|
|
275
|
+
current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
|
|
276
|
+
|
|
277
|
+
if not current_flock_instance or not current_flock_filename:
|
|
278
|
+
logger.error("Cannot create share link: No Flock is currently loaded in the application state.")
|
|
279
|
+
raise HTTPException(status_code=400, detail="No Flock loaded. Cannot create share link.")
|
|
280
|
+
|
|
281
|
+
if agent_name not in current_flock_instance.agents:
|
|
282
|
+
logger.error(f"Agent '{agent_name}' not found in currently loaded Flock '{current_flock_instance.name}'.")
|
|
283
|
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found in current Flock.")
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
flock_file_path = FLOCK_FILES_DIR / current_flock_filename
|
|
287
|
+
if not flock_file_path.is_file():
|
|
288
|
+
logger.warning(f"Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
|
|
289
|
+
flock_definition_str = current_flock_instance.to_yaml()
|
|
290
|
+
else:
|
|
291
|
+
flock_definition_str = flock_file_path.read_text()
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error(f"Failed to get flock definition for sharing: {e}", exc_info=True)
|
|
294
|
+
raise HTTPException(status_code=500, detail="Could not retrieve Flock definition for sharing.")
|
|
295
|
+
|
|
296
|
+
config = SharedLinkConfig(
|
|
297
|
+
share_id=share_id,
|
|
298
|
+
agent_name=agent_name,
|
|
299
|
+
flock_definition=flock_definition_str
|
|
300
|
+
)
|
|
301
|
+
try:
|
|
302
|
+
await store.save_config(config)
|
|
303
|
+
share_url = f"/ui/shared-run/{share_id}" # Relative URL for client-side navigation
|
|
304
|
+
logger.info(f"Created share link for agent '{agent_name}' in Flock '{current_flock_instance.name}' with ID '{share_id}'. URL: {share_url}")
|
|
305
|
+
return CreateShareLinkResponse(share_url=share_url)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Failed to create share link for agent '{agent_name}': {e}", exc_info=True)
|
|
308
|
+
raise HTTPException(status_code=500, detail=f"Failed to create share link: {e!s}")
|
|
309
|
+
|
|
310
|
+
# --- End Share Link API ---
|
|
311
|
+
|
|
312
|
+
# --- HTMX Endpoint for Generating Share Link Snippet ---
|
|
313
|
+
@app.post("/ui/htmx/share/generate-link", response_class=HTMLResponse, tags=["UI Sharing HTMX"])
|
|
314
|
+
async def htmx_generate_share_link(
|
|
315
|
+
request: Request,
|
|
316
|
+
start_agent_name: str | None = Form(None),
|
|
317
|
+
store: SharedLinkStoreInterface = Depends(get_shared_link_store)
|
|
318
|
+
):
|
|
319
|
+
if not start_agent_name:
|
|
320
|
+
logger.warning("HTMX generate share link: Agent name not provided.")
|
|
321
|
+
return templates.TemplateResponse(
|
|
322
|
+
"partials/_share_link_snippet.html",
|
|
323
|
+
{"request": request, "error_message": "No agent selected to share."}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
|
|
327
|
+
current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
|
|
328
|
+
|
|
329
|
+
if not current_flock_instance or not current_flock_filename:
|
|
330
|
+
logger.error("HTMX: Cannot create share link: No Flock is currently loaded.")
|
|
331
|
+
return templates.TemplateResponse(
|
|
332
|
+
"partials/_share_link_snippet.html",
|
|
333
|
+
{"request": request, "error_message": "No Flock loaded. Cannot create share link."}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if start_agent_name not in current_flock_instance.agents:
|
|
337
|
+
logger.error(f"HTMX: Agent '{start_agent_name}' not found in Flock '{current_flock_instance.name}'.")
|
|
338
|
+
return templates.TemplateResponse(
|
|
339
|
+
"partials/_share_link_snippet.html",
|
|
340
|
+
{"request": request, "error_message": f"Agent '{start_agent_name}' not found in current Flock."}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
flock_file_path = FLOCK_FILES_DIR / current_flock_filename
|
|
345
|
+
if not flock_file_path.is_file():
|
|
346
|
+
logger.warning(f"HTMX: Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
|
|
347
|
+
flock_definition_str = current_flock_instance.to_yaml()
|
|
348
|
+
else:
|
|
349
|
+
flock_definition_str = flock_file_path.read_text()
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.error(f"HTMX: Failed to get flock definition for sharing: {e}", exc_info=True)
|
|
352
|
+
return templates.TemplateResponse(
|
|
353
|
+
"partials/_share_link_snippet.html",
|
|
354
|
+
{"request": request, "error_message": "Could not retrieve Flock definition for sharing."}
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
share_id = uuid.uuid4().hex
|
|
358
|
+
config = SharedLinkConfig(
|
|
359
|
+
share_id=share_id,
|
|
360
|
+
agent_name=start_agent_name,
|
|
361
|
+
flock_definition=flock_definition_str
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
await store.save_config(config)
|
|
366
|
+
base_url = str(request.base_url)
|
|
367
|
+
full_share_url = f"{base_url.rstrip('/')}/ui/shared-run/{share_id}"
|
|
368
|
+
|
|
369
|
+
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}")
|
|
370
|
+
return templates.TemplateResponse(
|
|
371
|
+
"partials/_share_link_snippet.html",
|
|
372
|
+
{"request": request, "share_url": full_share_url, "flock_name": current_flock_instance.name, "agent_name": start_agent_name}
|
|
373
|
+
)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"HTMX: Failed to create share link for agent '{start_agent_name}': {e}", exc_info=True)
|
|
376
|
+
return templates.TemplateResponse(
|
|
377
|
+
"partials/_share_link_snippet.html",
|
|
378
|
+
{"request": request, "error_message": f"Could not generate link: {e!s}"}
|
|
379
|
+
)
|
|
380
|
+
# --- End HTMX Endpoint ---
|
|
381
|
+
|
|
382
|
+
# --- HTMX Endpoint for Generating SHARED CHAT Link Snippet ---
|
|
383
|
+
@app.post("/ui/htmx/share/chat/generate-link", response_class=HTMLResponse, tags=["UI Sharing HTMX"])
|
|
384
|
+
async def htmx_generate_share_chat_link(
|
|
385
|
+
request: Request,
|
|
386
|
+
agent_name: str | None = Form(None), # This is the chat agent
|
|
387
|
+
message_key: str | None = Form(None), # Changed default to None
|
|
388
|
+
history_key: str | None = Form(None), # Changed default to None
|
|
389
|
+
response_key: str | None = Form(None), # Changed default to None
|
|
390
|
+
store: SharedLinkStoreInterface = Depends(get_shared_link_store)
|
|
391
|
+
):
|
|
392
|
+
if not agent_name:
|
|
393
|
+
logger.warning("HTMX generate share chat link: Agent name not provided.")
|
|
394
|
+
return templates.TemplateResponse(
|
|
395
|
+
"partials/_share_chat_link_snippet.html", # Will create this template
|
|
396
|
+
{"request": request, "error_message": "No agent selected for chat sharing."}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
current_flock_instance: Flock | None = getattr(request.app.state, "flock_instance", None)
|
|
400
|
+
current_flock_filename: str | None = getattr(request.app.state, "flock_filename", None)
|
|
401
|
+
|
|
402
|
+
if not current_flock_instance or not current_flock_filename:
|
|
403
|
+
logger.error("HTMX Chat Share: Cannot create share link: No Flock is currently loaded.")
|
|
404
|
+
return templates.TemplateResponse(
|
|
405
|
+
"partials/_share_chat_link_snippet.html",
|
|
406
|
+
{"request": request, "error_message": "No Flock loaded. Cannot create share link."}
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if agent_name not in current_flock_instance.agents:
|
|
410
|
+
logger.error(f"HTMX Chat Share: Agent '{agent_name}' not found in Flock '{current_flock_instance.name}'.")
|
|
411
|
+
return templates.TemplateResponse(
|
|
412
|
+
"partials/_share_chat_link_snippet.html",
|
|
413
|
+
{"request": request, "error_message": f"Agent '{agent_name}' not found in current Flock."}
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
flock_file_path = FLOCK_FILES_DIR / current_flock_filename
|
|
418
|
+
if not flock_file_path.is_file():
|
|
419
|
+
logger.warning(f"HTMX Chat Share: Flock file {current_flock_filename} not found at {flock_file_path} for sharing. Using in-memory definition.")
|
|
420
|
+
flock_definition_str = current_flock_instance.to_yaml()
|
|
421
|
+
else:
|
|
422
|
+
flock_definition_str = flock_file_path.read_text()
|
|
423
|
+
except Exception as e:
|
|
424
|
+
logger.error(f"HTMX Chat Share: Failed to get flock definition for sharing: {e}", exc_info=True)
|
|
425
|
+
return templates.TemplateResponse(
|
|
426
|
+
"partials/_share_chat_link_snippet.html",
|
|
427
|
+
{"request": request, "error_message": "Could not retrieve Flock definition for sharing."}
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
share_id = uuid.uuid4().hex
|
|
431
|
+
|
|
432
|
+
# Explicitly convert empty strings from form to None for optional keys
|
|
433
|
+
actual_message_key = message_key if message_key else None
|
|
434
|
+
actual_history_key = history_key if history_key else None
|
|
435
|
+
actual_response_key = response_key if response_key else None
|
|
436
|
+
|
|
437
|
+
config = SharedLinkConfig(
|
|
438
|
+
share_id=share_id,
|
|
439
|
+
agent_name=agent_name, # agent_name from form is the chat agent
|
|
440
|
+
flock_definition=flock_definition_str,
|
|
441
|
+
share_type="chat",
|
|
442
|
+
chat_message_key=actual_message_key,
|
|
443
|
+
chat_history_key=actual_history_key,
|
|
444
|
+
chat_response_key=actual_response_key
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
await store.save_config(config)
|
|
449
|
+
base_url = str(request.base_url)
|
|
450
|
+
# Link to the new /chat/shared/{share_id} endpoint
|
|
451
|
+
full_share_url = f"{base_url.rstrip('/')}/chat/shared/{share_id}"
|
|
452
|
+
|
|
453
|
+
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}")
|
|
454
|
+
return templates.TemplateResponse(
|
|
455
|
+
"partials/_share_chat_link_snippet.html", # Will create this template
|
|
456
|
+
{"request": request, "share_url": full_share_url, "flock_name": current_flock_instance.name, "agent_name": agent_name}
|
|
457
|
+
)
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.error(f"HTMX Chat Share: Failed to create share link for agent '{agent_name}': {e}", exc_info=True)
|
|
460
|
+
return templates.TemplateResponse(
|
|
461
|
+
"partials/_share_chat_link_snippet.html",
|
|
462
|
+
{"request": request, "error_message": f"Could not generate chat link: {e!s}"}
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# --- Route for Shared Run Page ---
|
|
466
|
+
@app.get("/ui/shared-run/{share_id}", response_class=HTMLResponse, tags=["UI Sharing"])
|
|
467
|
+
async def page_shared_run(
|
|
468
|
+
request: Request,
|
|
469
|
+
share_id: str,
|
|
470
|
+
store: SharedLinkStoreInterface = Depends(get_shared_link_store),
|
|
471
|
+
):
|
|
472
|
+
logger.info(f"Accessed shared run page with share_id: {share_id}")
|
|
473
|
+
shared_config = await store.get_config(share_id)
|
|
474
|
+
|
|
475
|
+
if not shared_config:
|
|
476
|
+
logger.warning(f"Share ID {share_id} not found.")
|
|
477
|
+
return templates.TemplateResponse(
|
|
478
|
+
"error_page.html",
|
|
479
|
+
{"request": request, "error_title": "Link Not Found", "error_message": "The shared link does not exist or may have expired."},
|
|
480
|
+
status_code=404
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
agent_name_from_link = shared_config.agent_name
|
|
484
|
+
flock_definition_str = shared_config.flock_definition
|
|
485
|
+
context: dict[str, Any] = {"request": request, "is_shared_run_page": True, "share_id": share_id}
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
from flock.core.flock import Flock as ConcreteFlock
|
|
489
|
+
loaded_flock = ConcreteFlock.from_yaml(flock_definition_str)
|
|
490
|
+
|
|
491
|
+
# Store the loaded_flock instance in app.state for later retrieval
|
|
492
|
+
if not hasattr(request.app.state, 'shared_flocks'):
|
|
493
|
+
request.app.state.shared_flocks = {}
|
|
494
|
+
request.app.state.shared_flocks[share_id] = loaded_flock
|
|
495
|
+
logger.info(f"Shared Run Page: Stored Flock instance for share_id {share_id} in app.state.")
|
|
496
|
+
|
|
497
|
+
context["flock"] = loaded_flock
|
|
498
|
+
context["selected_agent_name"] = agent_name_from_link # For pre-selection & hidden field
|
|
499
|
+
# flock_definition_str is no longer needed in the template for a hidden field if we reuse the instance
|
|
500
|
+
# context["flock_definition_str"] = flock_definition_str
|
|
501
|
+
logger.info(f"Shared Run Page: Loaded Flock '{loaded_flock.name}' for agent '{agent_name_from_link}'.")
|
|
502
|
+
|
|
503
|
+
if agent_name_from_link not in loaded_flock.agents:
|
|
504
|
+
context["error_message"] = f"Agent '{agent_name_from_link}' not found in the shared Flock definition."
|
|
505
|
+
logger.warning(context["error_message"])
|
|
506
|
+
else:
|
|
507
|
+
agent = loaded_flock.agents[agent_name_from_link]
|
|
508
|
+
input_fields = []
|
|
509
|
+
if agent.input and isinstance(agent.input, str):
|
|
510
|
+
try:
|
|
511
|
+
parsed_spec = parse_schema(agent.input) # parse_schema is imported at top of main.py
|
|
512
|
+
for name, type_str, description in parsed_spec:
|
|
513
|
+
field_info = {"name": name, "type": type_str.lower(), "description": description or ""}
|
|
514
|
+
if "bool" in field_info["type"]: field_info["html_type"] = "checkbox"
|
|
515
|
+
elif "int" in field_info["type"] or "float" in field_info["type"]: field_info["html_type"] = "number"
|
|
516
|
+
elif "list" in field_info["type"] or "dict" in field_info["type"]:
|
|
517
|
+
field_info["html_type"] = "textarea"; field_info["placeholder"] = f"Enter JSON for {field_info['type']}"
|
|
518
|
+
else: field_info["html_type"] = "text"
|
|
519
|
+
input_fields.append(field_info)
|
|
520
|
+
context["input_fields"] = input_fields
|
|
521
|
+
except Exception as e_parse:
|
|
522
|
+
logger.error(f"Shared Run Page: Error parsing input for '{agent_name_from_link}': {e_parse}", exc_info=True)
|
|
523
|
+
context["error_message"] = f"Could not parse inputs for agent '{agent_name_from_link}'."
|
|
524
|
+
else:
|
|
525
|
+
context["input_fields"] = [] # Agent has no inputs defined
|
|
526
|
+
|
|
527
|
+
except Exception as e_load:
|
|
528
|
+
logger.error(f"Shared Run Page: Failed to load Flock from definition for share_id {share_id}: {e_load}", exc_info=True)
|
|
529
|
+
context["error_message"] = f"Fatal: Could not load the shared Flock configuration: {e_load!s}"
|
|
530
|
+
context["flock"] = None
|
|
531
|
+
context["selected_agent_name"] = agent_name_from_link # Still pass for potential error display
|
|
532
|
+
context["input_fields"] = []
|
|
533
|
+
# context["flock_definition_str"] = flock_definition_str # Not needed if not sent to template
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
current_theme_name = get_current_theme_name()
|
|
537
|
+
context["theme_css"] = generate_theme_css_web(current_theme_name)
|
|
538
|
+
context["active_theme_name"] = current_theme_name or DEFAULT_THEME_NAME
|
|
539
|
+
except Exception as e_theme:
|
|
540
|
+
logger.error(f"Shared Run Page: Error generating theme: {e_theme}", exc_info=True)
|
|
541
|
+
context["theme_css"] = ""
|
|
542
|
+
context["active_theme_name"] = DEFAULT_THEME_NAME
|
|
543
|
+
|
|
544
|
+
# The shared_run_page.html will now be a simple wrapper that includes _execution_form.html
|
|
545
|
+
return templates.TemplateResponse("shared_run_page.html", context)
|
|
546
|
+
|
|
547
|
+
# --- End Route for Shared Run Page ---
|
|
548
|
+
|
|
150
549
|
def generate_theme_css_web(theme_name: str | None) -> str:
|
|
151
550
|
if not THEME_LOADER_AVAILABLE or THEMES_DIR is None: return ""
|
|
152
551
|
|
|
@@ -227,6 +626,7 @@ def get_base_context_web(
|
|
|
227
626
|
current_flock_filename_from_state: str | None = getattr(request.app.state, "flock_filename", None)
|
|
228
627
|
theme_name = get_current_theme_name()
|
|
229
628
|
theme_css = generate_theme_css_web(theme_name)
|
|
629
|
+
|
|
230
630
|
return {
|
|
231
631
|
"request": request,
|
|
232
632
|
"current_flock": flock_instance_from_state,
|
|
@@ -236,13 +636,18 @@ def get_base_context_web(
|
|
|
236
636
|
"ui_mode": ui_mode,
|
|
237
637
|
"theme_css": theme_css,
|
|
238
638
|
"active_theme_name": theme_name,
|
|
239
|
-
"chat_enabled": getattr(request.app.state, "chat_enabled", False),
|
|
639
|
+
"chat_enabled": getattr(request.app.state, "chat_enabled", False), # Reverted to app.state
|
|
240
640
|
}
|
|
241
641
|
|
|
242
642
|
@app.get("/", response_class=HTMLResponse, tags=["UI Pages"])
|
|
243
643
|
async def page_dashboard(
|
|
244
644
|
request: Request, error: str = None, success: str = None, ui_mode: str = Query(None)
|
|
245
645
|
):
|
|
646
|
+
# Handle initial redirect if flagged during app startup
|
|
647
|
+
if getattr(request.app.state, "initial_redirect_to_chat", False):
|
|
648
|
+
logger.info("Initial redirect to CHAT page triggered from dashboard (FLOCK_START_MODE='chat').")
|
|
649
|
+
return RedirectResponse(url="/chat", status_code=307)
|
|
650
|
+
|
|
246
651
|
effective_ui_mode = ui_mode
|
|
247
652
|
flock_is_preloaded = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
|
|
248
653
|
|
|
@@ -360,11 +765,10 @@ async def ui_load_flock_by_name_action(request: Request, selected_flock_filename
|
|
|
360
765
|
ui_mode_query = request.query_params.get("ui_mode", "standalone")
|
|
361
766
|
if loaded_flock:
|
|
362
767
|
success_message_text = f"Flock '{loaded_flock.name}' loaded from '{selected_flock_filename}'."
|
|
363
|
-
response_headers["HX-Push-Url"] = "/ui/editor/
|
|
768
|
+
response_headers["HX-Push-Url"] = "/ui/editor/execute?ui_mode=" + ui_mode_query
|
|
364
769
|
response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_message_text}})
|
|
365
|
-
# Use get_base_context_web to ensure all necessary context vars are present for the partial
|
|
366
770
|
context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
|
|
367
|
-
return templates.TemplateResponse("partials/
|
|
771
|
+
return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
|
|
368
772
|
else:
|
|
369
773
|
error_message_text = f"Failed to load flock file '{selected_flock_filename}'."
|
|
370
774
|
response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": error_message_text}})
|
|
@@ -392,16 +796,14 @@ async def ui_load_flock_by_upload_action(request: Request, flock_file_upload: Up
|
|
|
392
796
|
loaded_flock = load_flock_from_file_service(filename_to_load, request.app.state)
|
|
393
797
|
if loaded_flock:
|
|
394
798
|
success_message_text = f"Flock '{loaded_flock.name}' loaded from '{filename_to_load}'."
|
|
395
|
-
response_headers["HX-Push-Url"] = f"/ui/editor/
|
|
799
|
+
response_headers["HX-Push-Url"] = f"/ui/editor/execute?ui_mode={ui_mode_query}"
|
|
396
800
|
response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "flockFileListChanged": None, "notify": {"type": "success", "message": success_message_text}})
|
|
397
|
-
# CORRECTED CALL:
|
|
398
801
|
context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
|
|
399
|
-
return templates.TemplateResponse("partials/
|
|
802
|
+
return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
|
|
400
803
|
else: error_message_text = f"Failed to process uploaded '{filename_to_load}'."
|
|
401
804
|
|
|
402
805
|
final_error_msg = error_message_text or "Upload failed."
|
|
403
806
|
response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": final_error_msg}})
|
|
404
|
-
# CORRECTED CALL:
|
|
405
807
|
context = get_base_context_web(request, error=final_error_msg, ui_mode=ui_mode_query)
|
|
406
808
|
return templates.TemplateResponse("partials/_create_flock_form.html", context, headers=response_headers)
|
|
407
809
|
|
|
@@ -409,16 +811,14 @@ async def ui_load_flock_by_upload_action(request: Request, flock_file_upload: Up
|
|
|
409
811
|
async def ui_create_flock_action(request: Request, flock_name: str = Form(...), default_model: str = Form(None), description: str = Form(None)):
|
|
410
812
|
ui_mode_query = request.query_params.get("ui_mode", "standalone")
|
|
411
813
|
if not flock_name.strip():
|
|
412
|
-
# CORRECTED CALL:
|
|
413
814
|
context = get_base_context_web(request, error="Flock name cannot be empty.", ui_mode=ui_mode_query)
|
|
414
815
|
return templates.TemplateResponse("partials/_create_flock_form.html", context)
|
|
415
816
|
|
|
416
817
|
new_flock = create_new_flock_service(flock_name, default_model, description, request.app.state)
|
|
417
|
-
success_msg_text = f"New flock '{new_flock.name}' created. Configure properties and
|
|
418
|
-
response_headers = {"HX-Push-Url": f"/ui/editor/
|
|
419
|
-
# 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}})}
|
|
420
820
|
context = get_base_context_web(request, success=success_msg_text, ui_mode=ui_mode_query)
|
|
421
|
-
return templates.TemplateResponse("partials/
|
|
821
|
+
return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
|
|
422
822
|
|
|
423
823
|
# --- Settings Page & Endpoints ---
|
|
424
824
|
@app.get("/ui/settings", response_class=HTMLResponse, tags=["UI Pages"])
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# src/flock/webapp/app/services/flock_service.py
|
|
2
|
-
from typing import TYPE_CHECKING, Optional
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
3
3
|
|
|
4
4
|
import yaml
|
|
5
5
|
|
|
@@ -248,25 +248,50 @@ def remove_agent_from_current_flock_service(agent_name: str, app_state: object)
|
|
|
248
248
|
|
|
249
249
|
|
|
250
250
|
async def run_current_flock_service(
|
|
251
|
-
start_agent_name: str,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
start_agent_name: str,
|
|
252
|
+
inputs: dict[str, Any],
|
|
253
|
+
app_state: Any,
|
|
254
|
+
) -> dict[str, Any]:
|
|
255
|
+
"""Runs the specified agent from the current flock instance in app_state."""
|
|
256
|
+
logger.info(f"Attempting to run agent: {start_agent_name} using flock from app_state.")
|
|
257
|
+
|
|
258
|
+
current_flock: Flock | None = getattr(app_state, "flock_instance", None)
|
|
259
|
+
run_store: RunStore | None = getattr(app_state, "run_store", None)
|
|
260
|
+
|
|
255
261
|
if not current_flock:
|
|
256
|
-
logger.error("
|
|
257
|
-
return "
|
|
258
|
-
if not
|
|
259
|
-
logger.error(
|
|
260
|
-
|
|
262
|
+
logger.error("Run service: No Flock instance available in app_state.")
|
|
263
|
+
return {"error": "No Flock loaded in the application."}
|
|
264
|
+
if not run_store:
|
|
265
|
+
logger.error("Run service: No RunStore instance available in app_state.")
|
|
266
|
+
# Attempt to initialize a default run_store if missing and this service is critical
|
|
267
|
+
# This might indicate an issue in the application lifecycle setup
|
|
268
|
+
logger.warning("Run service: Initializing a default RunStore as none was found in app_state.")
|
|
269
|
+
run_store = RunStore()
|
|
270
|
+
setattr(app_state, "run_store", run_store)
|
|
271
|
+
# Also update global DI if this is how it's managed elsewhere for consistency,
|
|
272
|
+
# though ideally DI setup handles this more centrally.
|
|
273
|
+
# from flock.webapp.app.dependencies import set_global_flock_services
|
|
274
|
+
# set_global_flock_services(current_flock, run_store)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if start_agent_name not in current_flock.agents:
|
|
278
|
+
logger.error(f"Run service: Agent '{start_agent_name}' not found in current flock '{current_flock.name}'.")
|
|
279
|
+
return {"error": f"Agent '{start_agent_name}' not found."}
|
|
280
|
+
|
|
261
281
|
try:
|
|
262
|
-
logger.info(f"
|
|
282
|
+
logger.info(f"Executing agent '{start_agent_name}' from flock '{current_flock.name}' using app_state.")
|
|
283
|
+
# Direct execution using the flock from app_state
|
|
263
284
|
result = await current_flock.run_async(
|
|
264
285
|
start_agent=start_agent_name, input=inputs, box_result=False
|
|
265
286
|
)
|
|
287
|
+
# Store run details using the run_store from app_state
|
|
288
|
+
if hasattr(run_store, "add_run_details"): # Check if RunStore has this method
|
|
289
|
+
run_id = result.get("run_id", "unknown_run_id") # Assuming run_async result might contain run_id
|
|
290
|
+
run_store.add_run_details(run_id=run_id, agent_name=start_agent_name, inputs=inputs, outputs=result)
|
|
266
291
|
return result
|
|
267
292
|
except Exception as e:
|
|
268
|
-
logger.error(f"
|
|
269
|
-
return f"
|
|
293
|
+
logger.error(f"Run service: Error during agent execution: {e}", exc_info=True)
|
|
294
|
+
return {"error": f"An error occurred: {e}"}
|
|
270
295
|
|
|
271
296
|
|
|
272
297
|
def get_registered_items_service(item_type: str) -> list[dict]:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SharedLinkConfig(BaseModel):
|
|
7
|
+
"""Configuration for a shared Flock agent execution link or chat session."""
|
|
8
|
+
|
|
9
|
+
share_id: str = Field(..., description="Unique identifier for the shared link.")
|
|
10
|
+
agent_name: str = Field(..., description="The name of the agent being shared (for run) or the chat agent (for chat).")
|
|
11
|
+
flock_definition: str = Field(..., description="The YAML/JSON string definition of the Flock the agent belongs to.")
|
|
12
|
+
created_at: datetime = Field(
|
|
13
|
+
default_factory=datetime.utcnow, description="Timestamp of when the link was created."
|
|
14
|
+
)
|
|
15
|
+
share_type: str = Field(default="agent_run", description="Type of share: 'agent_run' or 'chat'")
|
|
16
|
+
|
|
17
|
+
# Chat-specific settings (only relevant if share_type is 'chat')
|
|
18
|
+
chat_message_key: str | None = Field(None, description="Message key for chat input mapping.")
|
|
19
|
+
chat_history_key: str | None = Field(None, description="History key for chat input mapping.")
|
|
20
|
+
chat_response_key: str | None = Field(None, description="Response key for chat output mapping.")
|
|
21
|
+
|
|
22
|
+
# Placeholder for future enhancement: pre-filled input values
|
|
23
|
+
# input_values: Optional[Dict[str, Any]] = Field(
|
|
24
|
+
# None, description="Optional pre-filled input values for the agent."
|
|
25
|
+
# )
|
|
26
|
+
|
|
27
|
+
model_config = {
|
|
28
|
+
"from_attributes": True,
|
|
29
|
+
"json_schema_extra": {
|
|
30
|
+
"examples": [
|
|
31
|
+
{
|
|
32
|
+
"share_id": "abcdef123456",
|
|
33
|
+
"agent_name": "MyChatAgent",
|
|
34
|
+
"flock_definition": "name: MySharedFlock\nagents:\n MyChatAgent:\n input: 'message: str'\n output: 'response: str'\n # ... rest of flock YAML ...",
|
|
35
|
+
"created_at": "2023-10-26T10:00:00Z",
|
|
36
|
+
"share_type": "chat",
|
|
37
|
+
"chat_message_key": "user_input",
|
|
38
|
+
"chat_history_key": "conversation_history",
|
|
39
|
+
"chat_response_key": "agent_output"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|