flock-core 0.3.23__py3-none-any.whl → 0.3.31__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 +23 -11
- flock/cli/constants.py +2 -4
- flock/cli/create_flock.py +220 -1
- flock/cli/execute_flock.py +200 -0
- flock/cli/load_flock.py +27 -7
- flock/cli/loaded_flock_cli.py +202 -0
- flock/cli/manage_agents.py +443 -0
- flock/cli/view_results.py +29 -0
- flock/cli/yaml_editor.py +283 -0
- flock/core/__init__.py +2 -2
- flock/core/api/__init__.py +11 -0
- flock/core/api/endpoints.py +222 -0
- flock/core/api/main.py +237 -0
- flock/core/api/models.py +34 -0
- flock/core/api/run_store.py +72 -0
- flock/core/api/ui/__init__.py +0 -0
- flock/core/api/ui/routes.py +271 -0
- flock/core/api/ui/utils.py +119 -0
- flock/core/flock.py +509 -388
- flock/core/flock_agent.py +384 -121
- flock/core/flock_registry.py +532 -0
- flock/core/logging/logging.py +97 -23
- flock/core/mixin/dspy_integration.py +363 -158
- flock/core/serialization/__init__.py +7 -1
- flock/core/serialization/callable_registry.py +52 -0
- flock/core/serialization/serializable.py +259 -37
- flock/core/serialization/serialization_utils.py +199 -0
- flock/evaluators/declarative/declarative_evaluator.py +2 -0
- flock/modules/memory/memory_module.py +17 -4
- flock/modules/output/output_module.py +9 -3
- flock/workflow/activities.py +2 -2
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/METADATA +6 -3
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/RECORD +36 -22
- flock/core/flock_api.py +0 -214
- flock/core/registry/agent_registry.py +0 -120
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/WHEEL +0 -0
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/entry_points.txt +0 -0
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/licenses/LICENSE +0 -0
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 ---
|
flock/core/api/models.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# src/flock/core/api/ui/routes.py
|
|
2
|
+
"""FastHTML UI routes for the Flock API."""
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
# --- Conditional FastHTML Imports ---
|
|
7
|
+
try:
|
|
8
|
+
import httpx
|
|
9
|
+
from fasthtml.common import *
|
|
10
|
+
|
|
11
|
+
# Import Form explicitly with an alias to avoid collisions
|
|
12
|
+
from fasthtml.common import Form as FHForm
|
|
13
|
+
|
|
14
|
+
FASTHTML_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
FASTHTML_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
# Define necessary dummies if not available
|
|
19
|
+
class Request:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class Titled:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class Div:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
class H1:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
class P:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
class H2:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
class Pre:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
class Code:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
class Label:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
class Select:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
class Option:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
class FHForm:
|
|
53
|
+
pass # Dummy alias if not available
|
|
54
|
+
|
|
55
|
+
class Button:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
class Span:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
class Script:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
class Style:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
class Hidden:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
class Textarea:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
class Input:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def fast_app():
|
|
77
|
+
return None, None
|
|
78
|
+
|
|
79
|
+
def picolink():
|
|
80
|
+
return None
|
|
81
|
+
# ------------------------------------
|
|
82
|
+
|
|
83
|
+
# Use TYPE_CHECKING to avoid circular import errors for type hints
|
|
84
|
+
if TYPE_CHECKING:
|
|
85
|
+
from flock.core.api.main import FlockAPI
|
|
86
|
+
|
|
87
|
+
# Import logger and utils needed by UI routes
|
|
88
|
+
from flock.core.logging.logging import get_logger
|
|
89
|
+
|
|
90
|
+
logger = get_logger("api.ui")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_ui_app(
|
|
94
|
+
flock_api_instance: "FlockAPI",
|
|
95
|
+
api_host: str,
|
|
96
|
+
api_port: int,
|
|
97
|
+
server_name: str,
|
|
98
|
+
) -> Any:
|
|
99
|
+
"""Creates and configures the FastHTML application and its routes."""
|
|
100
|
+
if not FASTHTML_AVAILABLE:
|
|
101
|
+
raise ImportError("FastHTML is not installed. Cannot create UI.")
|
|
102
|
+
logger.debug("Creating FastHTML application instance for UI")
|
|
103
|
+
|
|
104
|
+
# Use the passed FlockAPI instance to access necessary data/methods
|
|
105
|
+
flock_instance = flock_api_instance.flock
|
|
106
|
+
parse_input_spec_func = (
|
|
107
|
+
flock_api_instance._parse_input_spec
|
|
108
|
+
) # Get reference to parser
|
|
109
|
+
|
|
110
|
+
fh_app, fh_rt = fast_app(
|
|
111
|
+
hdrs=(
|
|
112
|
+
Script(src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js"),
|
|
113
|
+
picolink, # Pass directly
|
|
114
|
+
Style("""
|
|
115
|
+
body { padding: 20px; max-width: 800px; margin: auto; font-family: sans-serif; }
|
|
116
|
+
label { display: block; margin-top: 1rem; font-weight: bold;}
|
|
117
|
+
input, select, textarea { width: 100%; margin-top: 0.25rem; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
|
|
118
|
+
input[type=checkbox] { width: auto; margin-right: 0.5rem; vertical-align: middle; }
|
|
119
|
+
label[for^=input_] { font-weight: normal; display: inline; margin-top: 0;} /* Style for checkbox labels */
|
|
120
|
+
button[type=submit] { margin-top: 1.5rem; padding: 0.75rem 1.5rem; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;}
|
|
121
|
+
button[type=submit]:hover { background-color: #0056b3; }
|
|
122
|
+
#result-area { margin-top: 2rem; background-color: #f8f9fa; padding: 15px; border: 1px solid #dee2e6; border-radius: 5px; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; }
|
|
123
|
+
.htmx-indicator { display: none; margin-left: 10px; font-style: italic; color: #6c757d; }
|
|
124
|
+
.htmx-request .htmx-indicator { display: inline; }
|
|
125
|
+
.htmx-request.htmx-indicator { display: inline; }
|
|
126
|
+
.error-message { color: #721c24; margin-top: 10px; font-weight: bold; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 10px; border-radius: 5px;}
|
|
127
|
+
"""),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@fh_rt("/get-agent-inputs")
|
|
132
|
+
def get_agent_inputs(request: Request):
|
|
133
|
+
"""Endpoint called by HTMX to get agent input fields."""
|
|
134
|
+
agent_name = request.query_params.get("agent_name")
|
|
135
|
+
logger.debug(f"UI requesting inputs for agent: {agent_name}")
|
|
136
|
+
if not agent_name:
|
|
137
|
+
return Div("Please select an agent.", cls="error-message")
|
|
138
|
+
|
|
139
|
+
# Access agents via the passed FlockAPI instance
|
|
140
|
+
agent_def = flock_instance.agents.get(agent_name)
|
|
141
|
+
if not agent_def:
|
|
142
|
+
logger.warning(f"Agent '{agent_name}' not found for UI.")
|
|
143
|
+
return Div(f"Agent '{agent_name}' not found.", cls="error-message")
|
|
144
|
+
|
|
145
|
+
# Use the parsing function from the FlockAPI instance
|
|
146
|
+
input_fields = parse_input_spec_func(agent_def.input or "")
|
|
147
|
+
logger.debug(f"Parsed input fields for {agent_name}: {input_fields}")
|
|
148
|
+
|
|
149
|
+
inputs_html = []
|
|
150
|
+
for field in input_fields:
|
|
151
|
+
field_id = f"input_{field['name']}"
|
|
152
|
+
label_text = f"{field['name']}"
|
|
153
|
+
if field["type"] != "bool":
|
|
154
|
+
label_text += f" ({field['type']})"
|
|
155
|
+
label = Label(label_text, fr=field_id)
|
|
156
|
+
input_attrs = dict(
|
|
157
|
+
id=field_id,
|
|
158
|
+
name=f"inputs.{field['name']}",
|
|
159
|
+
type=field["html_type"],
|
|
160
|
+
)
|
|
161
|
+
if field.get("step"):
|
|
162
|
+
input_attrs["step"] = field["step"]
|
|
163
|
+
if field.get("desc"):
|
|
164
|
+
input_attrs["placeholder"] = field["desc"]
|
|
165
|
+
if field.get("rows"):
|
|
166
|
+
input_attrs["rows"] = field["rows"]
|
|
167
|
+
|
|
168
|
+
if field["html_type"] == "textarea":
|
|
169
|
+
input_el = Textarea(**input_attrs)
|
|
170
|
+
elif field["html_type"] == "checkbox":
|
|
171
|
+
input_el = Div(
|
|
172
|
+
Input(**input_attrs, value="true"),
|
|
173
|
+
Label(f" Enable?", fr=field_id),
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
input_el = Input(**input_attrs)
|
|
177
|
+
|
|
178
|
+
inputs_html.append(
|
|
179
|
+
Div(label, input_el, style="margin-bottom: 1rem;")
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
inputs_html.append(
|
|
183
|
+
Hidden(
|
|
184
|
+
id="selected_agent_name", name="agent_name", value=agent_name
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
return (
|
|
188
|
+
Div(*inputs_html)
|
|
189
|
+
if inputs_html
|
|
190
|
+
else P("This agent requires no input.")
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@fh_rt("/")
|
|
194
|
+
async def ui_root(request: Request):
|
|
195
|
+
"""Serves the main UI page."""
|
|
196
|
+
logger.info("Serving main UI page /ui/")
|
|
197
|
+
agents_list = []
|
|
198
|
+
error_msg = None
|
|
199
|
+
api_url = f"http://{api_host}:{api_port}/agents"
|
|
200
|
+
try:
|
|
201
|
+
async with httpx.AsyncClient() as client:
|
|
202
|
+
logger.debug(f"UI fetching agents from {api_url}")
|
|
203
|
+
response = await client.get(api_url)
|
|
204
|
+
response.raise_for_status()
|
|
205
|
+
agent_data = response.json()
|
|
206
|
+
agents_list = agent_data.get("agents", [])
|
|
207
|
+
logger.debug(f"Fetched {len(agents_list)} agents for UI")
|
|
208
|
+
except Exception as e:
|
|
209
|
+
error_msg = f"UI Error: Could not fetch agent list from API at {api_url}. Details: {e}"
|
|
210
|
+
logger.error(error_msg, exc_info=True)
|
|
211
|
+
|
|
212
|
+
options = [
|
|
213
|
+
Option("-- Select Agent --", value="", selected=True, disabled=True)
|
|
214
|
+
] + [
|
|
215
|
+
Option(
|
|
216
|
+
f"{agent['name']}: {agent['description']}", value=agent["name"]
|
|
217
|
+
)
|
|
218
|
+
for agent in agents_list
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
# Use FHForm alias here
|
|
222
|
+
content = Div(
|
|
223
|
+
H2(f"Agent Runner"),
|
|
224
|
+
P(
|
|
225
|
+
"Select an agent, provide the required inputs, and click 'Run Flock'."
|
|
226
|
+
),
|
|
227
|
+
Label("Select Starting Agent:", fr="agent_select"),
|
|
228
|
+
Select(
|
|
229
|
+
*options,
|
|
230
|
+
id="agent_select",
|
|
231
|
+
name="agent_name",
|
|
232
|
+
hx_get="/ui/get-agent-inputs",
|
|
233
|
+
hx_trigger="change",
|
|
234
|
+
hx_target="#agent-inputs-container",
|
|
235
|
+
hx_indicator="#loading-indicator",
|
|
236
|
+
),
|
|
237
|
+
FHForm(
|
|
238
|
+
Div(id="agent-inputs-container", style="margin-top: 1rem;"),
|
|
239
|
+
Button("Run Flock", type="submit"),
|
|
240
|
+
Span(
|
|
241
|
+
" Processing...",
|
|
242
|
+
id="loading-indicator",
|
|
243
|
+
cls="htmx-indicator",
|
|
244
|
+
),
|
|
245
|
+
hx_post="/ui/run-agent-form", # Target the dedicated form endpoint
|
|
246
|
+
hx_target="#result-area",
|
|
247
|
+
hx_swap="innerHTML",
|
|
248
|
+
hx_indicator="#loading-indicator",
|
|
249
|
+
),
|
|
250
|
+
H2("Result"),
|
|
251
|
+
Div(
|
|
252
|
+
Pre(
|
|
253
|
+
Code(
|
|
254
|
+
"Result will appear here...",
|
|
255
|
+
id="result-content",
|
|
256
|
+
class_="language-json",
|
|
257
|
+
)
|
|
258
|
+
),
|
|
259
|
+
id="result-area",
|
|
260
|
+
style="min-height: 100px;",
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if error_msg:
|
|
265
|
+
content = Div(
|
|
266
|
+
H1("Flock UI - Error"), P(error_msg, cls="error-message")
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return Titled(f"{server_name}", content)
|
|
270
|
+
|
|
271
|
+
return fh_app
|