flock-core 0.4.0b43__py3-none-any.whl → 0.4.0b45__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/cli/manage_agents.py +19 -4
- flock/core/api/__init__.py +1 -2
- flock/core/api/endpoints.py +150 -218
- flock/core/api/main.py +134 -653
- flock/core/api/service.py +214 -0
- flock/core/flock.py +192 -134
- flock/core/flock_agent.py +31 -0
- flock/webapp/app/api/agent_management.py +135 -164
- flock/webapp/app/api/execution.py +76 -85
- flock/webapp/app/api/flock_management.py +60 -33
- flock/webapp/app/chat.py +233 -0
- flock/webapp/app/config.py +6 -3
- flock/webapp/app/dependencies.py +95 -0
- flock/webapp/app/main.py +320 -906
- flock/webapp/app/services/flock_service.py +183 -161
- flock/webapp/run.py +176 -100
- flock/webapp/static/css/chat.css +227 -0
- flock/webapp/static/css/components.css +167 -0
- flock/webapp/static/css/header.css +39 -0
- flock/webapp/static/css/layout.css +46 -0
- flock/webapp/static/css/sidebar.css +127 -0
- flock/webapp/templates/base.html +6 -1
- flock/webapp/templates/chat.html +60 -0
- flock/webapp/templates/chat_settings.html +20 -0
- flock/webapp/templates/flock_editor.html +1 -1
- flock/webapp/templates/partials/_agent_detail_form.html +8 -7
- flock/webapp/templates/partials/_agent_list.html +3 -3
- flock/webapp/templates/partials/_agent_manager_view.html +3 -4
- flock/webapp/templates/partials/_chat_container.html +9 -0
- flock/webapp/templates/partials/_chat_messages.html +13 -0
- flock/webapp/templates/partials/_chat_settings_form.html +65 -0
- flock/webapp/templates/partials/_execution_form.html +2 -2
- flock/webapp/templates/partials/_execution_view_container.html +1 -1
- flock/webapp/templates/partials/_flock_properties_form.html +2 -2
- flock/webapp/templates/partials/_registry_viewer_content.html +3 -3
- flock/webapp/templates/partials/_sidebar.html +17 -1
- flock/webapp/templates/registry_viewer.html +3 -3
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/METADATA +1 -1
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/RECORD +42 -31
- flock/webapp/static/css/custom.css +0 -612
- flock/webapp/templates/partials/_agent_manager_view_old.html +0 -19
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.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
|
-
"""
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
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
|
-
"""
|
|
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
|
-
|
|
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 =
|
|
96
|
-
|
|
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
|
-
|
|
43
|
+
self.processed_custom_endpoints.append(
|
|
103
44
|
FlockEndpoint(path=path, methods=list(methods) if methods else ["GET"], callback=cb)
|
|
104
45
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
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})")
|