flock-core 0.4.0b42__py3-none-any.whl → 0.4.0b44__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (42) hide show
  1. flock/core/api/__init__.py +1 -2
  2. flock/core/api/endpoints.py +149 -217
  3. flock/core/api/main.py +134 -653
  4. flock/core/api/service.py +214 -0
  5. flock/core/flock.py +192 -133
  6. flock/webapp/app/api/agent_management.py +135 -164
  7. flock/webapp/app/api/execution.py +76 -85
  8. flock/webapp/app/api/flock_management.py +60 -33
  9. flock/webapp/app/chat.py +233 -0
  10. flock/webapp/app/config.py +6 -3
  11. flock/webapp/app/dependencies.py +95 -0
  12. flock/webapp/app/main.py +320 -906
  13. flock/webapp/app/services/flock_service.py +183 -161
  14. flock/webapp/run.py +178 -97
  15. flock/webapp/static/css/chat.css +227 -0
  16. flock/webapp/static/css/components.css +167 -0
  17. flock/webapp/static/css/header.css +39 -0
  18. flock/webapp/static/css/layout.css +46 -0
  19. flock/webapp/static/css/sidebar.css +127 -0
  20. flock/webapp/templates/base.html +6 -1
  21. flock/webapp/templates/chat.html +60 -0
  22. flock/webapp/templates/chat_settings.html +20 -0
  23. flock/webapp/templates/flock_editor.html +1 -1
  24. flock/webapp/templates/partials/_agent_detail_form.html +4 -4
  25. flock/webapp/templates/partials/_agent_list.html +2 -2
  26. flock/webapp/templates/partials/_agent_manager_view.html +3 -4
  27. flock/webapp/templates/partials/_chat_container.html +9 -0
  28. flock/webapp/templates/partials/_chat_messages.html +13 -0
  29. flock/webapp/templates/partials/_chat_settings_form.html +65 -0
  30. flock/webapp/templates/partials/_execution_form.html +2 -2
  31. flock/webapp/templates/partials/_execution_view_container.html +1 -1
  32. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  33. flock/webapp/templates/partials/_registry_viewer_content.html +3 -3
  34. flock/webapp/templates/partials/_sidebar.html +17 -1
  35. flock/webapp/templates/registry_viewer.html +3 -3
  36. {flock_core-0.4.0b42.dist-info → flock_core-0.4.0b44.dist-info}/METADATA +1 -1
  37. {flock_core-0.4.0b42.dist-info → flock_core-0.4.0b44.dist-info}/RECORD +40 -29
  38. flock/webapp/static/css/custom.css +0 -612
  39. flock/webapp/templates/partials/_agent_manager_view_old.html +0 -19
  40. {flock_core-0.4.0b42.dist-info → flock_core-0.4.0b44.dist-info}/WHEEL +0 -0
  41. {flock_core-0.4.0b42.dist-info → flock_core-0.4.0b44.dist-info}/entry_points.txt +0 -0
  42. {flock_core-0.4.0b42.dist-info → flock_core-0.4.0b44.dist-info}/licenses/LICENSE +0 -0
flock/core/api/main.py CHANGED
@@ -1,681 +1,162 @@
1
1
  # src/flock/core/api/main.py
2
- """Main Flock API server class and setup."""
2
+ """This module defines the FlockAPI class, which is now primarily responsible for
3
+ managing and adding user-defined custom API endpoints to a main FastAPI application.
4
+ """
3
5
 
6
+ import inspect
4
7
  from collections.abc import Callable, Sequence
5
8
  from typing import TYPE_CHECKING, Any
6
9
 
7
- import uvicorn
8
- from fastapi import FastAPI
9
- from fastapi.responses import RedirectResponse
10
- from pydantic import BaseModel
10
+ from fastapi import ( # Ensure Request is aliased
11
+ Body,
12
+ Depends,
13
+ FastAPI,
14
+ Request as FastAPIRequest,
15
+ )
11
16
 
12
- # Flock core imports
13
- from flock.core.api.models import FlockBatchRequest
14
17
  from flock.core.logging.logging import get_logger
15
18
 
19
+ from .custom_endpoint import FlockEndpoint
20
+
16
21
  if TYPE_CHECKING:
17
- # These imports are only for type hints
18
22
  from flock.core.flock import Flock
19
23
 
20
- logger = get_logger("api.main")
21
-
22
- from .endpoints import create_api_router
23
-
24
- # Import components from the api package
25
- from .run_store import RunStore
26
-
27
- # Conditionally import for the new UI integration
28
- NEW_UI_SERVICE_AVAILABLE = False
29
- WEBAPP_FASTAPI_APP = None
30
- try:
31
- from flock.webapp.app.main import (
32
- app as webapp_fastapi_app, # Import the FastAPI app instance
33
- )
34
- from flock.webapp.app.services.flock_service import (
35
- set_current_flock_instance_programmatically,
36
- )
37
-
38
- WEBAPP_FASTAPI_APP = webapp_fastapi_app
39
- NEW_UI_SERVICE_AVAILABLE = True
40
- except ImportError:
41
- logger.warning(
42
- "New webapp components (flock.webapp.app.main:app or flock.webapp.app.services.flock_service) not found. "
43
- "UI mode will fall back to old FastHTML UI if available."
44
- )
45
- # Fallback: Import old UI components if new one isn't available and create_ui is True
46
- try:
47
- from .ui.routes import FASTHTML_AVAILABLE, create_ui_app
48
-
49
- if FASTHTML_AVAILABLE: # Only import utils if fasthtml is there
50
- from .ui.utils import format_result_to_html, parse_input_spec
51
- else:
52
- # Define placeholders if fasthtml itself is not available
53
- def parse_input_spec(*args, **kwargs):
54
- return []
55
-
56
- def format_result_to_html(*args, **kwargs):
57
- return ""
58
-
59
- FASTHTML_AVAILABLE = False # Ensure it's false if import failed
60
-
61
- except ImportError:
62
- FASTHTML_AVAILABLE = False # Ensure it's defined as false
63
-
64
- # Define placeholders if utils can't be imported
65
- def parse_input_spec(*args, **kwargs):
66
- return []
67
-
68
- def format_result_to_html(*args, **kwargs):
69
- return ""
70
-
71
- from flock.core.api.custom_endpoint import FlockEndpoint
24
+ logger = get_logger("core.api.custom_setup")
72
25
 
73
26
 
74
27
  class FlockAPI:
75
- """Coordinates the Flock API server, including endpoints and UI.
76
-
77
- A user can provide custom FastAPI-style routes via the ``custom_endpoints`` dict.
78
- Each key is a tuple of ``(<path:str>, <methods:list[str] | None>)`` and the
79
- value is a callback ``Callable``. ``methods`` can be ``None`` or an empty
80
- list to default to ``["GET"]``. The callback can be synchronous or
81
- ``async``. At execution time we provide the following keyword arguments and
82
- filter them to the callback's signature:
83
-
84
- • ``body`` – request json/plain payload (for POST/PUT/PATCH)
85
- • ``query`` – dict of query parameters
86
- • ``flock`` – current :class:`Flock` instance
87
- • any path parameters extracted from the route pattern
28
+ """A helper class to manage the addition of user-defined custom API endpoints
29
+ to an existing FastAPI application, in the context of a Flock instance.
88
30
  """
89
31
 
90
32
  def __init__(
91
33
  self,
92
- flock: "Flock",
34
+ flock_instance: "Flock",
93
35
  custom_endpoints: Sequence[FlockEndpoint] | dict[tuple[str, list[str] | None], Callable[..., Any]] | None = None,
94
36
  ):
95
- self.flock = flock
96
- # Normalize into list[FlockEndpoint]
97
- self.custom_endpoints: list[FlockEndpoint] = []
37
+ self.flock = flock_instance
38
+ self.processed_custom_endpoints: list[FlockEndpoint] = []
98
39
  if custom_endpoints:
99
- merged: list[FlockEndpoint] = []
100
40
  if isinstance(custom_endpoints, dict):
41
+ logger.warning("Received custom_endpoints as dict, converting. Prefer Sequence[FlockEndpoint].")
101
42
  for (path, methods), cb in custom_endpoints.items():
102
- merged.append(
43
+ self.processed_custom_endpoints.append(
103
44
  FlockEndpoint(path=path, methods=list(methods) if methods else ["GET"], callback=cb)
104
45
  )
105
- else:
106
- merged.extend(list(custom_endpoints))
107
-
108
- pending_endpoints = merged
109
- else:
110
- pending_endpoints = []
111
-
112
- # FastAPI app instance
113
- self.app = FastAPI(title="Flock API")
114
-
115
- # Store run information
116
- self.run_store = RunStore()
117
-
118
- # Register any pending custom endpoints collected before app creation
119
- if pending_endpoints:
120
- self.custom_endpoints.extend(pending_endpoints)
121
-
122
- self._setup_routes()
123
-
124
- def _setup_routes(self):
125
- """Includes API routers."""
126
- # Create and include the API router, passing self
127
- api_router = create_api_router(self)
128
- self.app.include_router(api_router)
129
-
130
- # Root redirect (if UI is enabled later) will be added in start()
131
-
132
- # --- Register user-supplied custom endpoints ---------------------
133
- if self.custom_endpoints:
134
- import inspect
135
-
136
- from fastapi import Body, Depends, Request
137
-
138
- # Register any endpoints collected during __init__ (self.custom_endpoints)
139
- if self.custom_endpoints:
140
- def _create_handler_factory(callback: Callable[..., Any], req_model: type[BaseModel] | None, query_model: type[BaseModel] | None):
141
- async def _invoke(request: Request, body, query):
142
- payload: dict[str, Any] = {"flock": self.flock}
143
- if request:
144
- payload.update(request.path_params)
145
- if query is None:
146
- payload["query"] = dict(request.query_params)
147
- else:
148
- payload["query"] = query
149
- else:
150
- payload["query"] = query or {}
151
- if body is not None:
152
- payload["body"] = body
153
- elif request and request.method in {"POST", "PUT", "PATCH"} and req_model is None:
154
- try:
155
- payload["body"] = await request.json()
156
- except Exception:
157
- payload["body"] = await request.body()
158
-
159
- sig = inspect.signature(callback)
160
- filtered_kwargs = {k: v for k, v in payload.items() if k in sig.parameters}
161
- if inspect.iscoroutinefunction(callback):
162
- return await callback(**filtered_kwargs)
163
- return callback(**filtered_kwargs)
164
-
165
- # Dynamically build wrapper with appropriate signature so FastAPI can document it
166
- params: list[str] = []
167
- if req_model is not None:
168
- params.append("body")
169
- if query_model is not None:
170
- params.append("query")
171
-
172
- # Build wrapper function based on which params are present
173
- if req_model and query_model:
174
- async def _route_handler(
175
- request: Request,
176
- body: req_model = Body(...), # type: ignore[arg-type,valid-type]
177
- query: query_model = Depends(), # type: ignore[arg-type,valid-type]
178
- ):
179
- return await _invoke(request, body, query)
180
-
181
- elif req_model and not query_model:
182
- async def _route_handler(
183
- request: Request,
184
- body: req_model = Body(...), # type: ignore[arg-type,valid-type]
185
- ):
186
- return await _invoke(request, body, None)
187
-
188
- elif query_model and not req_model:
189
- async def _route_handler(
190
- request: Request,
191
- query: query_model = Depends(), # type: ignore[arg-type,valid-type]
192
- ):
193
- return await _invoke(request, None, query)
194
-
46
+ elif isinstance(custom_endpoints, Sequence):
47
+ for ep_item in custom_endpoints: # Renamed loop variable
48
+ if isinstance(ep_item, FlockEndpoint):
49
+ self.processed_custom_endpoints.append(ep_item)
195
50
  else:
196
- async def _route_handler(request: Request):
197
- return await _invoke(request, None, None)
198
-
199
- return _route_handler
200
-
201
- for ep in self.custom_endpoints:
202
- self.app.add_api_route(
203
- ep.path,
204
- _create_handler_factory(ep.callback, ep.request_model, ep.query_model),
205
- methods=ep.methods or ["GET"],
206
- name=ep.name or f"custom:{ep.path}",
207
- include_in_schema=ep.include_in_schema,
208
- response_model=ep.response_model,
209
- summary=ep.summary,
210
- description=ep.description,
211
- dependencies=ep.dependencies,
212
- )
213
-
214
- # --- Core Execution Helper Methods ---
215
- # These remain here as they need access to self.flock and self.run_store
216
-
217
- async def _run_agent(
218
- self, run_id: str, agent_name: str, inputs: dict[str, Any]
219
- ):
220
- """Executes a single agent run (internal helper)."""
221
- try:
222
- if agent_name not in self.flock.agents:
223
- raise ValueError(f"Agent '{agent_name}' not found")
224
- agent = self.flock.agents[agent_name]
225
- # Type conversion (remains important)
226
- typed_inputs = self._type_convert_inputs(agent_name, inputs)
227
-
228
- logger.debug(
229
- f"Executing single agent '{agent_name}' (run_id: {run_id})",
230
- inputs=typed_inputs,
231
- )
232
- result = await agent.run_async(typed_inputs)
233
- logger.info(
234
- f"Single agent '{agent_name}' completed (run_id: {run_id})"
235
- )
236
-
237
- # Use RunStore to update
238
- self.run_store.update_run_result(run_id, result)
239
-
240
- except Exception as e:
241
- logger.error(
242
- f"Error in single agent run {run_id} ('{agent_name}'): {e!s}",
243
- exc_info=True,
244
- )
245
- # Update store status
246
- self.run_store.update_run_status(run_id, "failed", str(e))
247
- raise # Re-raise for the endpoint handler
248
-
249
- async def _run_flock(
250
- self, run_id: str, agent_name: str, inputs: dict[str, Any]
251
- ):
252
- """Executes a flock workflow run (internal helper)."""
253
- try:
254
- if agent_name not in self.flock.agents:
255
- raise ValueError(f"Starting agent '{agent_name}' not found")
256
-
257
- # Type conversion
258
- typed_inputs = self._type_convert_inputs(agent_name, inputs)
259
-
260
- logger.debug(
261
- f"Executing flock workflow starting with '{agent_name}' (run_id: {run_id})",
262
- inputs=typed_inputs,
263
- )
264
- result = await self.flock.run_async(
265
- start_agent=agent_name, input=typed_inputs
266
- )
267
- # Result is potentially a Box object
268
-
269
- # Use RunStore to update
270
- self.run_store.update_run_result(run_id, result)
271
-
272
- # Log using the local result variable
273
- final_agent_name = (
274
- result.get("agent_name", "N/A") if result is not None else "N/A"
275
- )
276
- logger.info(
277
- f"Flock workflow completed (run_id: {run_id})",
278
- final_agent=final_agent_name,
279
- )
280
-
281
- except Exception as e:
282
- logger.error(
283
- f"Error in flock run {run_id} (started with '{agent_name}'): {e!s}",
284
- exc_info=True,
285
- )
286
- # Update store status
287
- self.run_store.update_run_status(run_id, "failed", str(e))
288
- raise # Re-raise for the endpoint handler
289
-
290
- async def _run_batch(self, batch_id: str, request: "FlockBatchRequest"):
291
- """Executes a batch of runs (internal helper)."""
292
- try:
293
- if request.agent_name not in self.flock.agents:
294
- raise ValueError(f"Agent '{request.agent_name}' not found")
295
-
296
- logger.debug(
297
- f"Executing batch run starting with '{request.agent_name}' (batch_id: {batch_id})",
298
- batch_size=len(request.batch_inputs)
299
- if isinstance(request.batch_inputs, list)
300
- else "CSV",
301
- )
302
-
303
- # Import the thread pool executor here to avoid circular imports
304
- import asyncio
305
- import threading
306
- from concurrent.futures import ThreadPoolExecutor
307
-
308
- # Define a synchronous function to run the batch processing
309
- def run_batch_sync():
310
- # Use a new event loop for the batch processing
311
- loop = asyncio.new_event_loop()
312
- asyncio.set_event_loop(loop)
313
- try:
314
- # Set the total number of batch items if possible
315
- batch_size = (
316
- len(request.batch_inputs)
317
- if isinstance(request.batch_inputs, list)
318
- else 0
319
- )
320
- if batch_size > 0:
321
- # Directly call the store method - no need for asyncio here
322
- # since we're already in a separate thread
323
- self.run_store.set_batch_total_items(
324
- batch_id, batch_size
325
- )
326
-
327
- # Custom progress tracking wrapper
328
- class ProgressTracker:
329
- def __init__(self, store, batch_id, total_size):
330
- self.store = store
331
- self.batch_id = batch_id
332
- self.current_count = 0
333
- self.total_size = total_size
334
- self._lock = threading.Lock()
335
- self.partial_results = []
336
-
337
- def increment(self, result=None):
338
- with self._lock:
339
- self.current_count += 1
340
- if result is not None:
341
- # Store partial result
342
- self.partial_results.append(result)
343
-
344
- # Directly call the store method - no need for asyncio here
345
- # since we're already in a separate thread
346
- try:
347
- self.store.update_batch_progress(
348
- self.batch_id,
349
- self.current_count,
350
- self.partial_results,
351
- )
352
- except Exception as e:
353
- logger.error(
354
- f"Error updating progress: {e}"
355
- )
356
- return self.current_count
357
-
358
- # Create a progress tracker
359
- progress_tracker = ProgressTracker(
360
- self.run_store, batch_id, batch_size
361
- )
362
-
363
- # Define a custom worker that reports progress
364
- async def progress_aware_worker(index, item_inputs):
365
- try:
366
- result = await self.flock.run_async(
367
- start_agent=request.agent_name,
368
- input=item_inputs,
369
- box_result=request.box_results,
370
- )
371
- # Report progress after each item
372
- progress_tracker.increment(result)
373
- return result
374
- except Exception as e:
375
- logger.error(
376
- f"Error processing batch item {index}: {e}"
377
- )
378
- progress_tracker.increment(
379
- e if request.return_errors else None
380
- )
381
- if request.return_errors:
382
- return e
383
- return None
384
-
385
- # Process the batch items with progress tracking
386
- batch_inputs = request.batch_inputs
387
- if isinstance(batch_inputs, list):
388
- # Process list of inputs with progress tracking
389
- tasks = []
390
- for i, item_inputs in enumerate(batch_inputs):
391
- # Combine with static inputs if provided
392
- full_inputs = {
393
- **(request.static_inputs or {}),
394
- **item_inputs,
395
- }
396
- tasks.append(progress_aware_worker(i, full_inputs))
397
-
398
- # Run all tasks
399
- if request.parallel and request.max_workers > 1:
400
- # Run in parallel with semaphore for max_workers
401
- semaphore = asyncio.Semaphore(request.max_workers)
402
-
403
- async def bounded_worker(i, inputs):
404
- async with semaphore:
405
- return await progress_aware_worker(
406
- i, inputs
407
- )
408
-
409
- bounded_tasks = []
410
- for i, item_inputs in enumerate(batch_inputs):
411
- full_inputs = {
412
- **(request.static_inputs or {}),
413
- **item_inputs,
414
- }
415
- bounded_tasks.append(
416
- bounded_worker(i, full_inputs)
417
- )
418
-
419
- results = loop.run_until_complete(
420
- asyncio.gather(*bounded_tasks)
421
- )
422
- else:
423
- # Run sequentially
424
- results = []
425
- for i, item_inputs in enumerate(batch_inputs):
426
- full_inputs = {
427
- **(request.static_inputs or {}),
428
- **item_inputs,
429
- }
430
- result = loop.run_until_complete(
431
- progress_aware_worker(i, full_inputs)
432
- )
433
- results.append(result)
434
- else:
435
- # Let the original run_batch_async handle DataFrame or CSV
436
- results = loop.run_until_complete(
437
- self.flock.run_batch_async(
438
- start_agent=request.agent_name,
439
- batch_inputs=request.batch_inputs,
440
- input_mapping=request.input_mapping,
441
- static_inputs=request.static_inputs,
442
- parallel=request.parallel,
443
- max_workers=request.max_workers,
444
- use_temporal=request.use_temporal,
445
- box_results=request.box_results,
446
- return_errors=request.return_errors,
447
- silent_mode=request.silent_mode,
448
- write_to_csv=request.write_to_csv,
449
- )
450
- )
451
-
452
- # Update progress one last time with final count
453
- if results:
454
- progress_tracker.current_count = len(results)
455
- self.run_store.update_batch_progress(
456
- batch_id,
457
- len(results),
458
- results, # Include all results as partial results
459
- )
460
-
461
- # Update store with results from this thread
462
- self.run_store.update_batch_result(batch_id, results)
463
-
464
- logger.info(
465
- f"Batch run completed (batch_id: {batch_id})",
466
- num_results=len(results),
467
- )
468
- return results
469
- except Exception as e:
470
- logger.error(
471
- f"Error in batch run {batch_id} (started with '{request.agent_name}'): {e!s}",
472
- exc_info=True,
473
- )
474
- # Update store status
475
- self.run_store.update_batch_status(
476
- batch_id, "failed", str(e)
477
- )
478
- return None
479
- finally:
480
- loop.close()
481
-
482
- # Run the batch processing in a thread pool
483
- try:
484
- loop = asyncio.get_running_loop()
485
- with ThreadPoolExecutor() as pool:
486
- await loop.run_in_executor(pool, run_batch_sync)
487
- except Exception as e:
488
- error_msg = f"Error running batch in thread pool: {e!s}"
489
- logger.error(error_msg, exc_info=True)
490
- self.run_store.update_batch_status(
491
- batch_id, "failed", error_msg
492
- )
493
-
494
- except Exception as e:
495
- logger.error(
496
- f"Error setting up batch run {batch_id} (started with '{request.agent_name}'): {e!s}",
497
- exc_info=True,
498
- )
499
- # Update store status
500
- self.run_store.update_batch_status(batch_id, "failed", str(e))
501
- raise # Re-raise for the endpoint handler
502
-
503
- # --- UI Helper Methods (kept here as they are called by endpoints via self) ---
504
-
505
- def _parse_input_spec(self, input_spec: str) -> list[dict[str, str]]:
506
- """Parses an agent input string into a list of field definitions."""
507
- # Use the implementation moved to ui.utils
508
- return parse_input_spec(input_spec)
509
-
510
- def _format_result_to_html(self, data: Any) -> str:
511
- """Recursively formats a Python object into an HTML string."""
512
- # Use the implementation moved to ui.utils
513
- return format_result_to_html(data)
514
-
515
- def _type_convert_inputs(
516
- self, agent_name: str, inputs: dict[str, Any]
517
- ) -> dict[str, Any]:
518
- """Converts input values (esp. from forms) to expected Python types."""
519
- typed_inputs = {}
520
- agent_def = self.flock.agents.get(agent_name)
521
- if not agent_def or not agent_def.input:
522
- return inputs # Return original if no spec
523
-
524
- parsed_fields = self._parse_input_spec(agent_def.input)
525
- field_types = {f["name"]: f["type"] for f in parsed_fields}
526
-
527
- for k, v in inputs.items():
528
- target_type = field_types.get(k)
529
- if target_type and target_type.startswith("bool"):
530
- typed_inputs[k] = (
531
- str(v).lower() in ["true", "on", "1", "yes"]
532
- if isinstance(v, str)
533
- else bool(v)
534
- )
535
- elif target_type and target_type.startswith("int"):
536
- try:
537
- typed_inputs[k] = int(v)
538
- except (ValueError, TypeError):
539
- logger.warning(
540
- f"Could not convert input '{k}' value '{v}' to int for agent '{agent_name}'"
541
- )
542
- typed_inputs[k] = v
543
- elif target_type and target_type.startswith("float"):
544
- try:
545
- typed_inputs[k] = float(v)
546
- except (ValueError, TypeError):
547
- logger.warning(
548
- f"Could not convert input '{k}' value '{v}' to float for agent '{agent_name}'"
549
- )
550
- typed_inputs[k] = v
551
- # TODO: Add list/dict parsing (e.g., json.loads) if needed
552
- else:
553
- typed_inputs[k] = v # Assume string or already correct type
554
- return typed_inputs
555
-
556
- # --- Server Start/Stop ---
557
-
558
- def start(
559
- self,
560
- host: str = "0.0.0.0",
561
- port: int = 8344,
562
- server_name: str = "Flock API",
563
- create_ui: bool = False,
564
- #custom_endpoints: Sequence[FlockEndpoint] | dict[tuple[str, list[str] | None], Callable[..., Any]] | None = None,
565
- ):
566
- """Start the API server. If create_ui is True, it mounts the new webapp or the old FastHTML UI at the root."""
567
- if create_ui:
568
- if NEW_UI_SERVICE_AVAILABLE and WEBAPP_FASTAPI_APP:
569
- logger.info(
570
- f"Preparing to mount new Scoped Web UI at root for Flock: {self.flock.name}"
571
- )
572
- try:
573
- # Set the flock instance for the webapp
574
- set_current_flock_instance_programmatically(
575
- self.flock,
576
- f"{self.flock.name.replace(' ', '_').lower()}_api_scoped.flock",
577
- )
578
- logger.info(
579
- f"Flock '{self.flock.name}' set for the new web UI (now part of the main app)."
580
- )
581
-
582
- # Mount the new web UI app at the root of self.app
583
- # The WEBAPP_FASTAPI_APP handles its own routes including '/', static files etc.
584
- # It will need to be started with ui_mode=scoped, which should be handled by
585
- # the client accessing /?ui_mode=scoped initially.
586
- self.app.mount(
587
- "/", WEBAPP_FASTAPI_APP, name="flock_ui_root"
588
- )
589
- logger.info(
590
- f"New Web UI (scoped mode) mounted at root. Access at http://{host}:{port}/?ui_mode=scoped"
591
- )
592
- # No explicit root redirect needed from self.app to WEBAPP_FASTAPI_APP's root,
593
- # as WEBAPP_FASTAPI_APP now *is* the handler for "/".
594
- # The API's own routes (e.g. /api/...) will still be served by self.app if they don't conflict.
595
-
596
- logger.info(
597
- f"API server '{server_name}' (with integrated UI) starting on http://{host}:{port}"
598
- )
599
- logger.info(
600
- f"Access the Scoped UI for '{self.flock.name}' at http://{host}:{port}/?ui_mode=scoped"
601
- )
602
-
603
- except Exception as e:
604
- logger.error(
605
- f"Error setting up or mounting new scoped UI at root: {e}. "
606
- "API will start, UI might be impacted.",
607
- exc_info=True,
608
- )
609
- elif FASTHTML_AVAILABLE: # Fallback to old FastHTML UI
610
- logger.warning(
611
- "New webapp not available or WEBAPP_FASTAPI_APP is None. Falling back to old FastHTML UI (mounted at /ui)."
612
- )
613
- try:
614
- from .ui.routes import create_ui_app
615
- except ImportError:
616
- logger.error(
617
- "Failed to import create_ui_app for old UI. API running without UI."
618
- )
619
- FASTHTML_AVAILABLE = False
620
-
621
- if FASTHTML_AVAILABLE:
622
- logger.info(
623
- "Attempting to create and mount old FastHTML UI at /ui"
624
- ) # Old UI stays at /ui
625
- try:
626
- fh_app = create_ui_app(
627
- self,
628
- api_host=host,
629
- api_port=port,
630
- server_name=server_name,
631
- )
632
- self.app.mount(
633
- "/ui", fh_app, name="old_flock_ui"
634
- ) # Old UI still at /ui
635
- logger.info(
636
- "Old FastHTML UI mounted successfully at /ui."
637
- )
638
-
639
- @self.app.get(
640
- "/",
641
- include_in_schema=False,
642
- response_class=RedirectResponse,
643
- )
644
- async def root_redirect_to_old_ui(): # Redirect / to /ui/ for old UI
645
- logger.debug("Redirecting / to /ui/ (old UI)")
646
- return RedirectResponse(url="/ui/", status_code=303)
647
-
648
- logger.info(
649
- f"Old FastHTML UI available at http://{host}:{port}/ui/"
650
- )
651
- except Exception as e:
652
- logger.error(
653
- f"An error occurred setting up the old FastHTML UI: {e}. Running API only.",
654
- exc_info=True,
655
- )
51
+ logger.warning(f"Skipping non-FlockEndpoint item in custom_endpoints sequence: {type(ep_item)}")
656
52
  else:
657
- logger.error(
658
- "No UI components available. API running without UI."
659
- )
660
-
661
- if not create_ui:
662
- logger.info(
663
- f"API server '{server_name}' starting on http://{host}:{port} (UI not requested)."
53
+ logger.warning(f"Unsupported type for custom_endpoints: {type(custom_endpoints)}")
54
+ logger.info(
55
+ f"FlockAPI helper initialized for Flock: '{self.flock.name}'. "
56
+ f"Prepared {len(self.processed_custom_endpoints)} custom endpoints."
57
+ )
58
+
59
+ def add_custom_routes_to_app(self, app: FastAPI):
60
+ if not self.processed_custom_endpoints:
61
+ logger.debug("No custom endpoints to add to the FastAPI app.")
62
+ return
63
+
64
+ logger.info(f"Adding {len(self.processed_custom_endpoints)} custom endpoints to the FastAPI app instance.")
65
+
66
+ for current_ep_def in self.processed_custom_endpoints: # Use current_ep_def to avoid closure issues
67
+
68
+ # This factory now takes current_ep_def to ensure it uses the correct endpoint's details
69
+ def _create_handler_factory(
70
+ # Capture the specific endpoint definition for this factory instance
71
+ specific_ep: FlockEndpoint
72
+ ):
73
+ # This inner function prepares the payload and calls the user's callback
74
+ async def _invoke_user_callback(
75
+ request_param: FastAPIRequest, # Parameter for FastAPI's Request object
76
+ body_param: Any, # Will be populated by the _route_handler
77
+ query_param: Any # Will be populated by the _route_handler
78
+ ):
79
+ payload_to_user: dict[str, Any] = {"flock": self.flock} # self here refers to FlockAPI instance
80
+
81
+ if request_param: # Ensure request_param is not None
82
+ payload_to_user.update(request_param.path_params)
83
+ # query_param is already the parsed Pydantic model or None
84
+ if specific_ep.query_model and query_param is not None:
85
+ payload_to_user["query"] = query_param
86
+ # Fallback for raw query if callback expects 'query' but no query_model was set
87
+ elif 'query' in inspect.signature(specific_ep.callback).parameters and not specific_ep.query_model:
88
+ if request_param.query_params:
89
+ payload_to_user["query"] = dict(request_param.query_params)
90
+
91
+ # body_param is already the parsed Pydantic model or None
92
+ if specific_ep.request_model and body_param is not None:
93
+ payload_to_user["body"] = body_param
94
+ # Fallback for raw body if callback expects 'body' but no request_model was set
95
+ elif 'body' in inspect.signature(specific_ep.callback).parameters and \
96
+ not specific_ep.request_model and \
97
+ request_param.method in {"POST", "PUT", "PATCH"}:
98
+ try: payload_to_user["body"] = await request_param.json()
99
+ except Exception: payload_to_user["body"] = await request_param.body()
100
+
101
+ # If user callback explicitly asks for 'request'
102
+ if 'request' in inspect.signature(specific_ep.callback).parameters:
103
+ payload_to_user['request'] = request_param
104
+
105
+
106
+ user_callback_sig = inspect.signature(specific_ep.callback)
107
+ final_kwargs = {
108
+ k: v for k, v in payload_to_user.items() if k in user_callback_sig.parameters
109
+ }
110
+
111
+ if inspect.iscoroutinefunction(specific_ep.callback):
112
+ return await specific_ep.callback(**final_kwargs)
113
+ return specific_ep.callback(**final_kwargs)
114
+
115
+ # --- Select the correct handler signature based on specific_ep's models ---
116
+ if specific_ep.request_model and specific_ep.query_model:
117
+ async def _route_handler_body_query(
118
+ request: FastAPIRequest, # Correct alias for FastAPI Request
119
+ body: specific_ep.request_model = Body(...), # type: ignore
120
+ query: specific_ep.query_model = Depends(specific_ep.query_model) # type: ignore
121
+ ):
122
+ return await _invoke_user_callback(request, body, query)
123
+ return _route_handler_body_query
124
+ elif specific_ep.request_model and not specific_ep.query_model:
125
+ async def _route_handler_body_only(
126
+ request: FastAPIRequest, # Correct alias
127
+ body: specific_ep.request_model = Body(...) # type: ignore
128
+ ):
129
+ return await _invoke_user_callback(request, body, None)
130
+ return _route_handler_body_only
131
+ elif not specific_ep.request_model and specific_ep.query_model:
132
+ async def _route_handler_query_only(
133
+ request: FastAPIRequest, # Correct alias
134
+ query: specific_ep.query_model = Depends(specific_ep.query_model) # type: ignore
135
+ ):
136
+ return await _invoke_user_callback(request, None, query)
137
+ return _route_handler_query_only
138
+ else: # Neither request_model nor query_model
139
+ async def _route_handler_request_only(
140
+ request: FastAPIRequest # Correct alias
141
+ ):
142
+ return await _invoke_user_callback(request, None, None)
143
+ return _route_handler_request_only
144
+
145
+ # Create the handler for the current_ep_def
146
+ selected_handler = _create_handler_factory(current_ep_def) # Pass current_ep_def
147
+ selected_handler.__name__ = f"handler_for_{current_ep_def.path.replace('/', '_').lstrip('_')}_{current_ep_def.methods[0]}"
148
+
149
+
150
+ app.add_api_route(
151
+ current_ep_def.path,
152
+ selected_handler,
153
+ methods=current_ep_def.methods or ["GET"],
154
+ name=current_ep_def.name or f"custom:{current_ep_def.path.replace('/', '_').lstrip('_')}",
155
+ include_in_schema=current_ep_def.include_in_schema,
156
+ response_model=current_ep_def.response_model,
157
+ summary=current_ep_def.summary,
158
+ description=current_ep_def.description,
159
+ dependencies=current_ep_def.dependencies,
160
+ tags=["Flock API Custom Endpoints"],
664
161
  )
665
- elif (
666
- not (NEW_UI_SERVICE_AVAILABLE and WEBAPP_FASTAPI_APP)
667
- and not FASTHTML_AVAILABLE
668
- ):
669
- logger.info(
670
- f"API server '{server_name}' starting on http://{host}:{port}. UI was requested but no components found."
671
- )
672
-
673
- uvicorn.run(self.app, host=host, port=port)
674
-
675
- async def stop(self):
676
- """Stop the API server."""
677
- logger.info("Stopping API server (cleanup if necessary)")
678
- pass
679
-
680
-
681
- # --- End of file ---
162
+ logger.debug(f"Added custom route to app: {current_ep_def.methods} {current_ep_def.path} (Handler: {selected_handler.__name__}, Summary: {current_ep_def.summary})")