flock-core 0.3.22__py3-none-any.whl → 0.3.30__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/core/__init__.py CHANGED
@@ -2,18 +2,18 @@
2
2
 
3
3
  from flock.core.flock import Flock
4
4
  from flock.core.flock_agent import FlockAgent
5
- from flock.core.flock_api import FlockAPI
6
5
  from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
7
6
  from flock.core.flock_factory import FlockFactory
8
7
  from flock.core.flock_module import FlockModule, FlockModuleConfig
8
+ from flock.core.flock_registry import FlockRegistry
9
9
 
10
10
  __all__ = [
11
11
  "Flock",
12
- "FlockAPI",
13
12
  "FlockAgent",
14
13
  "FlockEvaluator",
15
14
  "FlockEvaluatorConfig",
16
15
  "FlockFactory",
17
16
  "FlockModule",
18
17
  "FlockModuleConfig",
18
+ "FlockRegistry",
19
19
  ]
@@ -0,0 +1,11 @@
1
+ # src/flock/core/api/__init__.py
2
+ """Flock API Server components."""
3
+
4
+ from .main import FlockAPI
5
+ from .models import FlockAPIRequest, FlockAPIResponse
6
+
7
+ __all__ = [
8
+ "FlockAPI",
9
+ "FlockAPIRequest",
10
+ "FlockAPIResponse",
11
+ ]
@@ -0,0 +1,222 @@
1
+ # src/flock/core/api/endpoints.py
2
+ """FastAPI endpoints for the Flock API."""
3
+
4
+ import html # For escaping
5
+ import uuid
6
+ from typing import TYPE_CHECKING # Added Any for type hinting clarity
7
+
8
+ from fastapi import (
9
+ APIRouter,
10
+ BackgroundTasks,
11
+ HTTPException,
12
+ Request as FastAPIRequest,
13
+ )
14
+
15
+ # Import HTMLResponse for the UI form endpoint
16
+ from fastapi.responses import HTMLResponse
17
+
18
+ from flock.core.logging.logging import get_logger
19
+
20
+ # Import models and UI utils
21
+ from .models import FlockAPIRequest, FlockAPIResponse
22
+
23
+ # Import UI utils - assuming they are now in ui/utils.py
24
+
25
+ # Use TYPE_CHECKING to avoid circular imports for type hints
26
+ if TYPE_CHECKING:
27
+ from flock.core.flock import Flock
28
+
29
+ from .main import FlockAPI
30
+ from .run_store import RunStore
31
+
32
+ logger = get_logger("api.endpoints")
33
+
34
+
35
+ # Factory function to create the router with dependencies
36
+ def create_api_router(flock_api: "FlockAPI") -> APIRouter:
37
+ """Creates the APIRouter and defines endpoints, injecting dependencies."""
38
+ router = APIRouter()
39
+ # Get dependencies from the main FlockAPI instance passed in
40
+ run_store: RunStore = flock_api.run_store
41
+ flock_instance: Flock = flock_api.flock
42
+
43
+ # --- API Endpoints ---
44
+ @router.post("/run/flock", response_model=FlockAPIResponse, tags=["API"])
45
+ async def run_flock_json(
46
+ request: FlockAPIRequest, background_tasks: BackgroundTasks
47
+ ):
48
+ """Run a flock workflow starting with the specified agent (expects JSON)."""
49
+ run_id = None
50
+ try:
51
+ run_id = str(uuid.uuid4())
52
+ run_store.create_run(run_id) # Use RunStore
53
+ response = run_store.get_run(
54
+ run_id
55
+ ) # Get initial response from store
56
+
57
+ processed_inputs = request.inputs if request.inputs else {}
58
+ logger.info(
59
+ f"API request: run flock '{request.agent_name}' (run_id: {run_id})",
60
+ inputs=processed_inputs,
61
+ )
62
+
63
+ if request.async_run:
64
+ logger.debug(
65
+ f"Running flock '{request.agent_name}' asynchronously (run_id: {run_id})"
66
+ )
67
+ # Call the helper method on the passed FlockAPI instance
68
+ background_tasks.add_task(
69
+ flock_api._run_flock,
70
+ run_id,
71
+ request.agent_name,
72
+ processed_inputs,
73
+ )
74
+ run_store.update_run_status(run_id, "running")
75
+ response.status = "running" # Update local response copy too
76
+ else:
77
+ logger.debug(
78
+ f"Running flock '{request.agent_name}' synchronously (run_id: {run_id})"
79
+ )
80
+ # Call the helper method on the passed FlockAPI instance
81
+ await flock_api._run_flock(
82
+ run_id, request.agent_name, processed_inputs
83
+ )
84
+ response = run_store.get_run(
85
+ run_id
86
+ ) # Fetch updated status/result
87
+
88
+ return response
89
+ except ValueError as ve:
90
+ logger.error(f"Value error starting run: {ve}")
91
+ if run_id:
92
+ run_store.update_run_status(run_id, "failed", str(ve))
93
+ raise HTTPException(status_code=400, detail=str(ve))
94
+ except Exception as e:
95
+ error_msg = f"Internal server error: {type(e).__name__}"
96
+ logger.error(f"Error starting run: {e!s}", exc_info=True)
97
+ if run_id:
98
+ run_store.update_run_status(run_id, "failed", error_msg)
99
+ raise HTTPException(status_code=500, detail=error_msg)
100
+
101
+ @router.get("/run/{run_id}", response_model=FlockAPIResponse, tags=["API"])
102
+ async def get_run_status(run_id: str):
103
+ """Get the status of a specific run."""
104
+ logger.debug(f"API request: get status for run_id: {run_id}")
105
+ run_data = run_store.get_run(run_id)
106
+ if not run_data:
107
+ logger.warning(f"Run ID not found: {run_id}")
108
+ raise HTTPException(status_code=404, detail="Run not found")
109
+ return run_data
110
+
111
+ @router.get("/agents", tags=["API"])
112
+ async def list_agents():
113
+ """List all available agents."""
114
+ logger.debug("API request: list agents")
115
+ # Access flock instance via factory closure
116
+ agents_list = [
117
+ {"name": agent.name, "description": agent.description or agent.name}
118
+ for agent in flock_instance.agents.values()
119
+ ]
120
+ return {"agents": agents_list}
121
+
122
+ # --- UI Form Endpoint ---
123
+ @router.post("/ui/run-agent-form", response_class=HTMLResponse, tags=["UI"])
124
+ async def run_flock_form(fastapi_req: FastAPIRequest):
125
+ """Endpoint to handle form submissions from the UI."""
126
+ run_id = None
127
+ try:
128
+ form_data = await fastapi_req.form()
129
+ agent_name = form_data.get("agent_name")
130
+ if not agent_name:
131
+ logger.warning("UI form submission missing agent_name")
132
+ return HTMLResponse(
133
+ '<div id="result-content" class="error-message">Error: Agent name not provided.</div>',
134
+ status_code=400,
135
+ )
136
+
137
+ logger.info(f"UI Form submission for agent: {agent_name}")
138
+ form_inputs = {}
139
+ # Access flock instance via factory closure
140
+ agent_def = flock_instance.agents.get(agent_name)
141
+ # Use helper from flock_api instance for parsing
142
+ defined_input_fields = (
143
+ flock_api._parse_input_spec(agent_def.input or "")
144
+ if agent_def
145
+ else []
146
+ )
147
+
148
+ for key, value in form_data.items():
149
+ if key.startswith("inputs."):
150
+ form_inputs[key[len("inputs.") :]] = value
151
+ for field in defined_input_fields: # Handle checkboxes
152
+ if (
153
+ field["html_type"] == "checkbox"
154
+ and field["name"] not in form_inputs
155
+ ):
156
+ form_inputs[field["name"]] = False
157
+ elif (
158
+ field["html_type"] == "checkbox"
159
+ and field["name"] in form_inputs
160
+ ):
161
+ form_inputs[field["name"]] = True
162
+
163
+ logger.debug(f"Parsed form inputs for UI run: {form_inputs}")
164
+ run_id = str(uuid.uuid4())
165
+ run_store.create_run(run_id) # Use RunStore
166
+ logger.debug(
167
+ f"Running flock '{agent_name}' synchronously from UI (run_id: {run_id})"
168
+ )
169
+
170
+ # Call helper method on flock_api instance
171
+ await flock_api._run_flock(run_id, agent_name, form_inputs)
172
+
173
+ final_status = run_store.get_run(run_id)
174
+ if final_status and final_status.status == "completed":
175
+ # Use helper from flock_api instance for formatting
176
+ formatted_html = flock_api._format_result_to_html(
177
+ final_status.result
178
+ )
179
+ logger.info(f"UI run completed successfully (run_id: {run_id})")
180
+ return HTMLResponse(
181
+ f"<div id='result-content'>{formatted_html}</div>"
182
+ ) # Wrap in target div
183
+ elif final_status and final_status.status == "failed":
184
+ logger.error(
185
+ f"UI run failed (run_id: {run_id}): {final_status.error}"
186
+ )
187
+ error_msg = html.escape(final_status.error or "Unknown error")
188
+ return HTMLResponse(
189
+ f"<div id='result-content' class='error-message'>Run Failed: {error_msg}</div>",
190
+ status_code=500,
191
+ )
192
+ else:
193
+ status_str = (
194
+ final_status.status if final_status else "Not Found"
195
+ )
196
+ logger.warning(
197
+ f"UI run {run_id} ended in unexpected state: {status_str}"
198
+ )
199
+ return HTMLResponse(
200
+ f"<div id='result-content' class='error-message'>Run ended unexpectedly. Status: {status_str}</div>",
201
+ status_code=500,
202
+ )
203
+
204
+ except ValueError as ve:
205
+ logger.error(f"Value error processing UI form run: {ve}")
206
+ if run_id:
207
+ run_store.update_run_status(run_id, "failed", str(ve))
208
+ return HTMLResponse(
209
+ f"<div id='result-content' class='error-message'>Error: {html.escape(str(ve))}</div>",
210
+ status_code=400,
211
+ )
212
+ except Exception as e:
213
+ error_msg = f"Internal server error: {type(e).__name__}"
214
+ logger.error(f"Error processing UI form run: {e!s}", exc_info=True)
215
+ if run_id:
216
+ run_store.update_run_status(run_id, "failed", error_msg)
217
+ return HTMLResponse(
218
+ f"<div id='result-content' class='error-message'>{html.escape(error_msg)}</div>",
219
+ status_code=500,
220
+ )
221
+
222
+ return router
flock/core/api/main.py ADDED
@@ -0,0 +1,237 @@
1
+ # src/flock/core/api/main.py
2
+ """Main Flock API server class and setup."""
3
+
4
+ from typing import Any
5
+
6
+ import uvicorn
7
+ from fastapi import FastAPI
8
+ from fastapi.responses import RedirectResponse
9
+
10
+ # Flock core imports
11
+ from flock.core.flock import Flock
12
+ from flock.core.logging.logging import get_logger
13
+
14
+ from .endpoints import create_api_router
15
+
16
+ # Import components from the api package
17
+ from .run_store import RunStore
18
+ from .ui.routes import FASTHTML_AVAILABLE, create_ui_app
19
+ from .ui.utils import format_result_to_html, parse_input_spec # Import UI utils
20
+
21
+ logger = get_logger("api.main")
22
+
23
+
24
+ class FlockAPI:
25
+ """Coordinates the Flock API server, including endpoints and UI."""
26
+
27
+ def __init__(self, flock: Flock):
28
+ self.flock = flock
29
+ self.app = FastAPI(title="Flock API")
30
+ self.run_store = RunStore() # Create the run store instance
31
+ self._setup_routes()
32
+
33
+ def _setup_routes(self):
34
+ """Includes API routers."""
35
+ # Create and include the API router, passing self
36
+ api_router = create_api_router(self)
37
+ self.app.include_router(api_router)
38
+
39
+ # Root redirect (if UI is enabled later) will be added in start()
40
+
41
+ # --- Core Execution Helper Methods ---
42
+ # These remain here as they need access to self.flock and self.run_store
43
+
44
+ async def _run_agent(
45
+ self, run_id: str, agent_name: str, inputs: dict[str, Any]
46
+ ):
47
+ """Executes a single agent run (internal helper)."""
48
+ try:
49
+ if agent_name not in self.flock.agents:
50
+ raise ValueError(f"Agent '{agent_name}' not found")
51
+ agent = self.flock.agents[agent_name]
52
+ # Type conversion (remains important)
53
+ typed_inputs = self._type_convert_inputs(agent_name, inputs)
54
+
55
+ logger.debug(
56
+ f"Executing single agent '{agent_name}' (run_id: {run_id})",
57
+ inputs=typed_inputs,
58
+ )
59
+ result = await agent.run_async(typed_inputs)
60
+ logger.info(
61
+ f"Single agent '{agent_name}' completed (run_id: {run_id})"
62
+ )
63
+
64
+ # Use RunStore to update
65
+ self.run_store.update_run_result(run_id, result)
66
+
67
+ except Exception as e:
68
+ logger.error(
69
+ f"Error in single agent run {run_id} ('{agent_name}'): {e!s}",
70
+ exc_info=True,
71
+ )
72
+ # Update store status
73
+ self.run_store.update_run_status(run_id, "failed", str(e))
74
+ raise # Re-raise for the endpoint handler
75
+
76
+ async def _run_flock(
77
+ self, run_id: str, agent_name: str, inputs: dict[str, Any]
78
+ ):
79
+ """Executes a flock workflow run (internal helper)."""
80
+ try:
81
+ if agent_name not in self.flock.agents:
82
+ raise ValueError(f"Starting agent '{agent_name}' not found")
83
+
84
+ # Type conversion
85
+ typed_inputs = self._type_convert_inputs(agent_name, inputs)
86
+
87
+ logger.debug(
88
+ f"Executing flock workflow starting with '{agent_name}' (run_id: {run_id})",
89
+ inputs=typed_inputs,
90
+ )
91
+ result = await self.flock.run_async(
92
+ start_agent=agent_name, input=typed_inputs
93
+ )
94
+ # Result is potentially a Box object
95
+
96
+ # Use RunStore to update
97
+ self.run_store.update_run_result(run_id, result)
98
+
99
+ # Log using the local result variable
100
+ final_agent_name = (
101
+ result.get("agent_name", "N/A") if result is not None else "N/A"
102
+ )
103
+ logger.info(
104
+ f"Flock workflow completed (run_id: {run_id})",
105
+ final_agent=final_agent_name,
106
+ )
107
+
108
+ except Exception as e:
109
+ logger.error(
110
+ f"Error in flock run {run_id} (started with '{agent_name}'): {e!s}",
111
+ exc_info=True,
112
+ )
113
+ # Update store status
114
+ self.run_store.update_run_status(run_id, "failed", str(e))
115
+ raise # Re-raise for the endpoint handler
116
+
117
+ # --- UI Helper Methods (kept here as they are called by endpoints via self) ---
118
+
119
+ def _parse_input_spec(self, input_spec: str) -> list[dict[str, str]]:
120
+ """Parses an agent input string into a list of field definitions."""
121
+ # Use the implementation moved to ui.utils
122
+ return parse_input_spec(input_spec)
123
+
124
+ def _format_result_to_html(self, data: Any) -> str:
125
+ """Recursively formats a Python object into an HTML string."""
126
+ # Use the implementation moved to ui.utils
127
+ return format_result_to_html(data)
128
+
129
+ def _type_convert_inputs(
130
+ self, agent_name: str, inputs: dict[str, Any]
131
+ ) -> dict[str, Any]:
132
+ """Converts input values (esp. from forms) to expected Python types."""
133
+ typed_inputs = {}
134
+ agent_def = self.flock.agents.get(agent_name)
135
+ if not agent_def or not agent_def.input:
136
+ return inputs # Return original if no spec
137
+
138
+ parsed_fields = self._parse_input_spec(agent_def.input)
139
+ field_types = {f["name"]: f["type"] for f in parsed_fields}
140
+
141
+ for k, v in inputs.items():
142
+ target_type = field_types.get(k)
143
+ if target_type and target_type.startswith("bool"):
144
+ typed_inputs[k] = (
145
+ str(v).lower() in ["true", "on", "1", "yes"]
146
+ if isinstance(v, str)
147
+ else bool(v)
148
+ )
149
+ elif target_type and target_type.startswith("int"):
150
+ try:
151
+ typed_inputs[k] = int(v)
152
+ except (ValueError, TypeError):
153
+ logger.warning(
154
+ f"Could not convert input '{k}' value '{v}' to int for agent '{agent_name}'"
155
+ )
156
+ typed_inputs[k] = v
157
+ elif target_type and target_type.startswith("float"):
158
+ try:
159
+ typed_inputs[k] = float(v)
160
+ except (ValueError, TypeError):
161
+ logger.warning(
162
+ f"Could not convert input '{k}' value '{v}' to float for agent '{agent_name}'"
163
+ )
164
+ typed_inputs[k] = v
165
+ # TODO: Add list/dict parsing (e.g., json.loads) if needed
166
+ else:
167
+ typed_inputs[k] = v # Assume string or already correct type
168
+ return typed_inputs
169
+
170
+ # --- Server Start/Stop ---
171
+
172
+ def start(
173
+ self,
174
+ host: str = "0.0.0.0",
175
+ port: int = 8344,
176
+ server_name: str = "Flock API",
177
+ create_ui: bool = False,
178
+ ):
179
+ """Start the API server, optionally creating and mounting a FastHTML UI."""
180
+ if create_ui:
181
+ if not FASTHTML_AVAILABLE:
182
+ logger.error(
183
+ "FastHTML not installed. Cannot create UI. Running API only."
184
+ )
185
+ else:
186
+ logger.info("Attempting to create and mount FastHTML UI at /ui")
187
+ try:
188
+ # Pass self (FlockAPI instance) to the UI creation function
189
+ # It needs access to self.flock and self._parse_input_spec
190
+ fh_app = create_ui_app(
191
+ self,
192
+ api_host=host,
193
+ api_port=port,
194
+ server_name=server_name,
195
+ )
196
+ self.app.mount("/ui", fh_app, name="ui")
197
+ logger.info("FastHTML UI mounted successfully.")
198
+
199
+ # Add root redirect only if UI was successfully mounted
200
+ @self.app.get(
201
+ "/",
202
+ include_in_schema=False,
203
+ response_class=RedirectResponse,
204
+ )
205
+ async def root_redirect():
206
+ logger.debug("Redirecting / to /ui/")
207
+ return "/ui/"
208
+
209
+ except ImportError as e:
210
+ logger.error(
211
+ f"Could not create UI due to import error: {e}. Running API only."
212
+ )
213
+ except Exception as e:
214
+ logger.error(
215
+ f"An error occurred setting up the UI: {e}. Running API only.",
216
+ exc_info=True,
217
+ )
218
+
219
+ logger.info(f"Starting API server on http://{host}:{port}")
220
+ if (
221
+ create_ui
222
+ and FASTHTML_AVAILABLE
223
+ and any(
224
+ m.path == "/ui" for m in self.app.routes if hasattr(m, "path")
225
+ )
226
+ ):
227
+ logger.info(f"UI available at http://{host}:{port}/ui/")
228
+
229
+ uvicorn.run(self.app, host=host, port=port)
230
+
231
+ async def stop(self):
232
+ """Stop the API server."""
233
+ logger.info("Stopping API server (cleanup if necessary)")
234
+ pass # Add cleanup logic if needed
235
+
236
+
237
+ # --- End of file ---
@@ -0,0 +1,34 @@
1
+ # src/flock/core/api/models.py
2
+ """Pydantic models for the Flock API."""
3
+
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class FlockAPIRequest(BaseModel):
11
+ """Request model for running an agent via JSON API."""
12
+
13
+ agent_name: str = Field(..., description="Name of the agent to run")
14
+ inputs: dict[str, Any] = Field(
15
+ default_factory=dict, description="Input data for the agent"
16
+ )
17
+ async_run: bool = Field(
18
+ default=False, description="Whether to run asynchronously"
19
+ )
20
+
21
+
22
+ class FlockAPIResponse(BaseModel):
23
+ """Response model for API run requests."""
24
+
25
+ run_id: str = Field(..., description="Unique ID for this run")
26
+ status: str = Field(..., description="Status of the run")
27
+ result: dict[str, Any] | None = Field(
28
+ None, description="Run result if completed"
29
+ )
30
+ started_at: datetime = Field(..., description="When the run started")
31
+ completed_at: datetime | None = Field(
32
+ None, description="When the run completed"
33
+ )
34
+ error: str | None = Field(None, description="Error message if failed")
@@ -0,0 +1,72 @@
1
+ # src/flock/core/api/run_store.py
2
+ """Manages the state of active and completed Flock runs."""
3
+
4
+ import threading
5
+ from datetime import datetime
6
+
7
+ from flock.core.logging.logging import get_logger
8
+
9
+ from .models import FlockAPIResponse # Import from the models file
10
+
11
+ logger = get_logger("api.run_store")
12
+
13
+
14
+ class RunStore:
15
+ """Stores and manages the state of Flock runs."""
16
+
17
+ def __init__(self):
18
+ self._runs: dict[str, FlockAPIResponse] = {}
19
+ self._lock = threading.Lock() # Basic lock for thread safety
20
+
21
+ def create_run(self, run_id: str) -> FlockAPIResponse:
22
+ """Creates a new run record with 'starting' status."""
23
+ with self._lock:
24
+ if run_id in self._runs:
25
+ logger.warning(f"Run ID {run_id} already exists. Overwriting.")
26
+ response = FlockAPIResponse(
27
+ run_id=run_id, status="starting", started_at=datetime.now()
28
+ )
29
+ self._runs[run_id] = response
30
+ logger.debug(f"Created run record for run_id: {run_id}")
31
+ return response
32
+
33
+ def get_run(self, run_id: str) -> FlockAPIResponse | None:
34
+ """Gets the status of a run."""
35
+ with self._lock:
36
+ return self._runs.get(run_id)
37
+
38
+ def update_run_status(
39
+ self, run_id: str, status: str, error: str | None = None
40
+ ):
41
+ """Updates the status and potentially error of a run."""
42
+ with self._lock:
43
+ if run_id in self._runs:
44
+ self._runs[run_id].status = status
45
+ if error:
46
+ self._runs[run_id].error = error
47
+ if status in ["completed", "failed"]:
48
+ self._runs[run_id].completed_at = datetime.now()
49
+ logger.debug(f"Updated status for run_id {run_id} to {status}")
50
+ else:
51
+ logger.warning(
52
+ f"Attempted to update status for non-existent run_id: {run_id}"
53
+ )
54
+
55
+ def update_run_result(self, run_id: str, result: dict):
56
+ """Updates the result of a completed run."""
57
+ with self._lock:
58
+ if run_id in self._runs:
59
+ # Ensure result is serializable (e.g., convert Box)
60
+ final_result = (
61
+ dict(result) if hasattr(result, "to_dict") else result
62
+ )
63
+ self._runs[run_id].result = final_result
64
+ self._runs[run_id].status = "completed"
65
+ self._runs[run_id].completed_at = datetime.now()
66
+ logger.debug(f"Updated result for completed run_id: {run_id}")
67
+ else:
68
+ logger.warning(
69
+ f"Attempted to update result for non-existent run_id: {run_id}"
70
+ )
71
+
72
+ # Add methods for cleanup, persistence, etc. later
File without changes