flock-core 0.4.511__py3-none-any.whl → 0.4.513__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 (56) hide show
  1. flock/core/config/flock_agent_config.py +11 -0
  2. flock/core/config/scheduled_agent_config.py +40 -0
  3. flock/core/flock_agent.py +7 -1
  4. flock/core/flock_factory.py +129 -2
  5. flock/core/flock_scheduler.py +166 -0
  6. flock/core/logging/logging.py +8 -0
  7. flock/core/mcp/flock_mcp_server.py +30 -4
  8. flock/core/mcp/flock_mcp_tool_base.py +1 -1
  9. flock/core/mcp/mcp_client.py +57 -28
  10. flock/core/mcp/mcp_client_manager.py +1 -1
  11. flock/core/mcp/mcp_config.py +245 -9
  12. flock/core/mcp/types/callbacks.py +3 -5
  13. flock/core/mcp/types/factories.py +12 -14
  14. flock/core/mcp/types/handlers.py +9 -12
  15. flock/core/mcp/types/types.py +205 -2
  16. flock/mcp/servers/sse/flock_sse_server.py +21 -14
  17. flock/mcp/servers/streamable_http/__init__.py +0 -0
  18. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +169 -0
  19. flock/mcp/servers/websockets/flock_websocket_server.py +3 -3
  20. flock/webapp/app/main.py +66 -11
  21. flock/webapp/app/services/sharing_store.py +173 -0
  22. flock/webapp/run.py +3 -1
  23. flock/webapp/templates/base.html +10 -11
  24. flock/webapp/templates/chat.html +7 -10
  25. flock/webapp/templates/chat_settings.html +3 -4
  26. flock/webapp/templates/flock_editor.html +1 -2
  27. flock/webapp/templates/index.html +1 -1
  28. flock/webapp/templates/partials/_agent_detail_form.html +7 -13
  29. flock/webapp/templates/partials/_agent_list.html +1 -2
  30. flock/webapp/templates/partials/_agent_manager_view.html +2 -3
  31. flock/webapp/templates/partials/_chat_container.html +2 -2
  32. flock/webapp/templates/partials/_chat_settings_form.html +6 -8
  33. flock/webapp/templates/partials/_create_flock_form.html +2 -4
  34. flock/webapp/templates/partials/_dashboard_flock_detail.html +2 -3
  35. flock/webapp/templates/partials/_dashboard_flock_file_list.html +1 -2
  36. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +2 -3
  37. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +1 -2
  38. flock/webapp/templates/partials/_env_vars_table.html +2 -4
  39. flock/webapp/templates/partials/_execution_form.html +12 -10
  40. flock/webapp/templates/partials/_execution_view_container.html +2 -3
  41. flock/webapp/templates/partials/_flock_file_list.html +2 -3
  42. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  43. flock/webapp/templates/partials/_flock_upload_form.html +1 -2
  44. flock/webapp/templates/partials/_load_manager_view.html +2 -3
  45. flock/webapp/templates/partials/_registry_viewer_content.html +4 -5
  46. flock/webapp/templates/partials/_settings_env_content.html +2 -3
  47. flock/webapp/templates/partials/_settings_theme_content.html +2 -2
  48. flock/webapp/templates/partials/_settings_view.html +2 -2
  49. flock/webapp/templates/partials/_sidebar.html +27 -39
  50. flock/webapp/templates/registry_viewer.html +7 -10
  51. flock/webapp/templates/shared_run_page.html +7 -10
  52. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/METADATA +3 -1
  53. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/RECORD +56 -51
  54. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/WHEEL +0 -0
  55. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/entry_points.txt +0 -0
  56. {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,13 @@
1
1
  """Types for Flock's MCP functionality."""
2
2
 
3
+ import importlib
4
+ import inspect
5
+ import os
3
6
  from collections.abc import Awaitable, Callable
4
7
  from contextlib import AbstractAsyncContextManager
5
- from typing import Any
8
+ from typing import Any, Literal
6
9
 
10
+ import httpx
7
11
  from anyio.streams.memory import (
8
12
  MemoryObjectReceiveStream,
9
13
  MemoryObjectSendStream,
@@ -36,6 +40,7 @@ from mcp.types import (
36
40
  from pydantic import AnyUrl, BaseModel, ConfigDict, Field
37
41
 
38
42
  from flock.core.mcp.util.helpers import get_default_env
43
+ from flock.core.serialization.serializable import Serializable
39
44
 
40
45
 
41
46
  class ServerNotification(_MCPServerNotification):
@@ -80,17 +85,40 @@ class MCPRoot(_MCPRoot):
80
85
  """Wrapper for mcp.types.Root."""
81
86
 
82
87
 
83
- class ServerParameters(BaseModel):
88
+ class ServerParameters(BaseModel, Serializable):
84
89
  """Base Type for server parameters."""
85
90
 
86
91
  model_config = ConfigDict(
87
92
  arbitrary_types_allowed=True,
88
93
  )
89
94
 
95
+ transport_type: Literal["stdio", "websockets", "sse", "streamable_http"] = Field(
96
+ ...,
97
+ description="which type of transport these connection params are used for."
98
+ )
99
+
100
+ def to_dict(self, path_type: str = "relative"):
101
+ """Serialize."""
102
+ return self.model_dump(
103
+ exclude_defaults=False,
104
+ exclude_none=True,
105
+ mode="json"
106
+ )
107
+
108
+ @classmethod
109
+ def from_dict(cls, data: dict[str, Any]):
110
+ """Deserialize."""
111
+ return cls(**data)
112
+
90
113
 
91
114
  class StdioServerParameters(_MCPStdioServerParameters, ServerParameters):
92
115
  """Base Type for Stdio Server parameters."""
93
116
 
117
+ transport_type: Literal["stdio"] = Field(
118
+ default="stdio",
119
+ description="Use stdio params."
120
+ )
121
+
94
122
  env: dict[str, str] | None = Field(
95
123
  default_factory=get_default_env,
96
124
  description="Environment for the MCP Server.",
@@ -100,12 +128,121 @@ class StdioServerParameters(_MCPStdioServerParameters, ServerParameters):
100
128
  class WebsocketServerParameters(ServerParameters):
101
129
  """Base Type for Websocket Server params."""
102
130
 
131
+ transport_type: Literal["websockets"] = Field(
132
+ default="websockets",
133
+ description="Use websocket params."
134
+ )
135
+
103
136
  url: str | AnyUrl = Field(..., description="Url the server listens at.")
104
137
 
138
+ class StreamableHttpServerParameters(ServerParameters):
139
+ """Base Type for StreamableHttp params."""
140
+
141
+ transport_type: Literal["streamable_http"] = Field(
142
+ default="streamable_http",
143
+ description="Use streamable http params."
144
+ )
145
+
146
+ url: str | AnyUrl = Field(
147
+ ...,
148
+ description="The url the server listens at."
149
+ )
150
+
151
+ headers: dict[str, Any] | None = Field(
152
+ default=None,
153
+ description="Additional headers to pass to the client."
154
+ )
155
+
156
+ timeout: float | int = Field(
157
+ default=5,
158
+ description="Http Timeout",
159
+ )
160
+
161
+ sse_read_timeout: float | int = Field(
162
+ default=60*5,
163
+ description="How long the client will wait before disconnecting from the server."
164
+ )
165
+
166
+ terminate_on_close: bool = Field(
167
+ default=True,
168
+ description="Terminate connection on close"
169
+ )
170
+
171
+ auth: httpx.Auth | None = Field(
172
+ default=None,
173
+ description="Httpx Auth Scheme"
174
+ )
175
+
176
+ @classmethod
177
+ def from_dict(cls, data: dict[str, Any]):
178
+ """Deserialize the object from a dict."""
179
+ # find and import the concrete implementation for
180
+ # the auth object
181
+ auth_obj: httpx.Auth | None = None
182
+ auth_impl = data.pop("auth", None)
183
+ if auth_impl:
184
+ # find the concrete implementation
185
+ impl = auth_impl.pop("implementation", None)
186
+ params = auth_impl.pop("params", None)
187
+ if impl:
188
+ mod = importlib.import_module(impl["module_path"])
189
+ real_cls = getattr(mod, impl["classname"])
190
+ if params:
191
+ auth_obj = real_cls(**{k: v for k, v in params.items()})
192
+ else:
193
+ # assume that the implementation handles it.
194
+ auth_obj = real_cls()
195
+ else:
196
+ raise ValueError("No concrete implementation for auth provided.")
197
+
198
+ data["auth"] = auth_obj
199
+ return cls(**{k: v for k, v in data.items()})
200
+
201
+ def to_dict(self, path_type = "relative"):
202
+ """Serialize the object."""
203
+ exclude = ["auth"]
204
+
205
+ data = self.model_dump(
206
+ exclude=exclude,
207
+ exclude_defaults=False,
208
+ exclude_none=True,
209
+ )
210
+
211
+ # inject implentation info for auth
212
+ if self.auth is not None:
213
+ file_path = inspect.getsourcefile(type(self.auth))
214
+ if path_type == "relative":
215
+ file_path = os.path.relpath(file_path)
216
+ try:
217
+ # params should be primitive types, keeping with the
218
+ # declarative approach of flock.
219
+ params = {
220
+ k: getattr(self.auth, k)
221
+ for k in getattr(self.auth, "__dict__", {})
222
+ if not k.startswith("_")
223
+ }
224
+ except Exception:
225
+ params = None
226
+
227
+ data["auth"] = {
228
+ "implementation": {
229
+ "class_name": type(self.auth).__name__,
230
+ "module_path": type(self.auth).__module__,
231
+ "file_path": file_path,
232
+ },
233
+ "params": params,
234
+ }
235
+
236
+ return data
105
237
 
106
238
  class SseServerParameters(ServerParameters):
107
239
  """Base Type for SSE Server params."""
108
240
 
241
+ transport_type: Literal["sse"] = Field(
242
+ default="sse",
243
+ description="Use sse server params."
244
+ )
245
+
109
246
  url: str | AnyUrl = Field(..., description="The url the server listens at.")
110
247
 
111
248
  headers: dict[str, Any] | None = Field(
@@ -119,6 +256,72 @@ class SseServerParameters(ServerParameters):
119
256
  description="How long the client will wait before disconnecting from the server.",
120
257
  )
121
258
 
259
+ auth: httpx.Auth | None = Field(
260
+ default=None,
261
+ description="Httpx Auth Scheme."
262
+ )
263
+
264
+ @classmethod
265
+ def from_dict(cls, data: dict[str, Any]):
266
+ """Deserialize the object from a dict."""
267
+ # find and import the concrete implementation for
268
+ # the auth object.
269
+ auth_obj: httpx.Auth | None = None
270
+ auth_impl = data.pop("auth", None) # get the specs for the auth class
271
+ if auth_impl:
272
+ # find the concrete implementation
273
+ impl = auth_impl.pop("implementation", None)
274
+ params = auth_impl.pop("params", None)
275
+ if impl:
276
+ mod = importlib.import_module(impl["module_path"])
277
+ real_cls = getattr(mod, impl["class_name"])
278
+ if params:
279
+ auth_obj = real_cls(**{k: v for k, v in params.items()})
280
+ else:
281
+ # assume that implementation handles it
282
+ auth_obj = real_cls()
283
+ else:
284
+ raise ValueError("No concrete implementation for auth provided.")
285
+
286
+ data["auth"] = auth_obj
287
+ return cls(**{k: v for k, v in data.items()})
288
+
289
+ def to_dict(self, path_type = "relative"):
290
+ """Serialize the object."""
291
+ exclude = ["auth"]
292
+
293
+ data = self.model_dump(
294
+ exclude=exclude,
295
+ exclude_defaults=False,
296
+ exclude_none=True,
297
+ )
298
+
299
+ # inject implentation info for auth
300
+ if self.auth is not None:
301
+ file_path = inspect.getsourcefile(type(self.auth))
302
+ if path_type == "relative":
303
+ file_path = os.path.relpath(file_path)
304
+ try:
305
+ # params should be primitive types, keeping with the
306
+ # declarative approach of flock.
307
+ params = {
308
+ k: getattr(self.auth, k)
309
+ for k in getattr(self.auth, "__dict__", {})
310
+ if not k.startswith("_")
311
+ }
312
+ except Exception:
313
+ params = None
314
+
315
+ data["auth"] = {
316
+ "implementation": {
317
+ "class_name": type(self.auth).__name__,
318
+ "module_path": type(self.auth).__module__,
319
+ "file_path": file_path,
320
+ },
321
+ "params": params,
322
+ }
323
+
324
+ return data
122
325
 
123
326
  MCPCLientInitFunction = Callable[
124
327
  ...,
@@ -4,12 +4,13 @@ import copy
4
4
  from contextlib import AbstractAsyncContextManager
5
5
  from typing import Any, Literal
6
6
 
7
+ import httpx
7
8
  from anyio.streams.memory import (
8
9
  MemoryObjectReceiveStream,
9
10
  MemoryObjectSendStream,
10
11
  )
11
12
  from mcp.client.sse import sse_client
12
- from mcp.types import JSONRPCMessage
13
+ from mcp.shared.message import SessionMessage
13
14
  from opentelemetry import trace
14
15
  from pydantic import Field
15
16
 
@@ -63,45 +64,51 @@ class FlockSSEClient(FlockMCPClientBase):
63
64
  additional_params: dict[str, Any] | None = None,
64
65
  ) -> AbstractAsyncContextManager[
65
66
  tuple[
66
- MemoryObjectReceiveStream[JSONRPCMessage | Exception],
67
- MemoryObjectSendStream[JSONRPCMessage],
67
+ MemoryObjectReceiveStream[SessionMessage | Exception],
68
+ MemoryObjectSendStream[SessionMessage],
68
69
  ]
69
70
  ]:
70
71
  """Return an async context manager whose __aenter__ method yields (read_stream, send_stream)."""
71
72
  # avoid modifying the config of the client as a side-effect.
72
73
  param_copy = copy.deepcopy(params)
73
74
 
74
- if self.additional_params:
75
+ if additional_params:
75
76
  override_headers = bool(
76
- self.additional_params.get("override_headers", False)
77
+ additional_params.get("override_headers", False)
77
78
  )
78
- if "headers" in self.additional_params:
79
+ if "headers" in additional_params:
79
80
  if override_headers:
80
- param_copy.headers = self.additional_params.get(
81
+ param_copy.headers = additional_params.get(
81
82
  "headers", params.headers
82
83
  )
83
84
  else:
84
85
  param_copy.headers.update(
85
- self.additional_params.get("headers", {})
86
+ additional_params.get("headers", {})
86
87
  )
87
- if "read_timeout_seconds" in self.additional_params:
88
- param_copy.timeout = self.additional_params.get(
88
+ if "read_timeout_seconds" in additional_params:
89
+ param_copy.timeout = additional_params.get(
89
90
  "read_timeout_seconds", params.timeout
90
91
  )
91
92
 
92
- if "sse_read_timeout" in self.additional_params:
93
- param_copy.sse_read_timeout = self.additional_params.get(
93
+ if "sse_read_timeout" in additional_params:
94
+ param_copy.sse_read_timeout = additional_params.get(
94
95
  "sse_read_timeout",
95
96
  params.sse_read_timeout,
96
97
  )
97
- if "url" in self.additional_params:
98
- param_copy.url = self.additional_params.get(
98
+ if "url" in additional_params:
99
+ param_copy.url = additional_params.get(
99
100
  "url",
100
101
  params.url,
101
102
  )
102
103
 
104
+ if "auth" in additional_params and isinstance(
105
+ additional_params.get("auth"), httpx.Auth
106
+ ):
107
+ param_copy.auth = additional_params.get("auth", param_copy.auth)
108
+
103
109
  return sse_client(
104
110
  url=param_copy.url,
111
+ auth=param_copy.auth,
105
112
  headers=param_copy.headers,
106
113
  timeout=float(param_copy.timeout),
107
114
  sse_read_timeout=float(param_copy.sse_read_timeout),
File without changes
@@ -0,0 +1,169 @@
1
+ """This module provides the Flock Streamable-Http functionality."""
2
+
3
+ import copy
4
+ from collections.abc import Callable
5
+ from contextlib import AbstractAsyncContextManager
6
+ from datetime import timedelta
7
+ from typing import Any, Literal
8
+
9
+ import httpx
10
+ from anyio.streams.memory import (
11
+ MemoryObjectReceiveStream,
12
+ MemoryObjectSendStream,
13
+ )
14
+ from mcp.client.streamable_http import streamablehttp_client
15
+ from mcp.shared.message import SessionMessage
16
+ from opentelemetry import trace
17
+ from pydantic import Field
18
+
19
+ from flock.core.logging.logging import get_logger
20
+ from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
21
+ from flock.core.mcp.mcp_client import FlockMCPClientBase
22
+ from flock.core.mcp.mcp_client_manager import FlockMCPClientManagerBase
23
+ from flock.core.mcp.mcp_config import (
24
+ FlockMCPConfigurationBase,
25
+ FlockMCPConnectionConfigurationBase,
26
+ )
27
+ from flock.core.mcp.types.types import (
28
+ StreamableHttpServerParameters,
29
+ )
30
+
31
+ logger = get_logger("mcp.streamable_http.server")
32
+ tracer = trace.get_tracer(__name__)
33
+
34
+ GetSessionIdCallback = Callable[[], str | None]
35
+
36
+
37
+ class FlockStreamableHttpConnectionConfig(FlockMCPConnectionConfigurationBase):
38
+ """Concrete ConnectionConfig for a StreamableHttpClient."""
39
+
40
+ # Only thing we need to override here is the concrete transport_type
41
+ # and connection parameter fields.
42
+ transport_type: Literal["streamable_http"] = Field(
43
+ default="streamable_http",
44
+ description="Use the streamable_http Transport type.",
45
+ )
46
+
47
+ connection_parameters: StreamableHttpServerParameters = Field(
48
+ ..., description="Streamable HTTP Server Connection Parameters."
49
+ )
50
+
51
+
52
+ class FlockStreamableHttpConfig(FlockMCPConfigurationBase):
53
+ """Configuration for Streamable HTTP Clients."""
54
+
55
+ # The only thing we need to override here is the
56
+ # concrete connection config.
57
+ # The rest is generic enough to handle everything else.
58
+ connection_config: FlockStreamableHttpConnectionConfig = Field(
59
+ ..., description="Concrete StreamableHttp Connection Configuration."
60
+ )
61
+
62
+
63
+ class FlockStreamableHttpClient(FlockMCPClientBase):
64
+ """Client for StreamableHttpServers."""
65
+
66
+ config: FlockStreamableHttpConfig = Field(
67
+ ..., description="Client configuration."
68
+ )
69
+
70
+ async def create_transport(
71
+ self,
72
+ params: StreamableHttpServerParameters,
73
+ additional_params: dict[str, Any] | None = None,
74
+ ) -> AbstractAsyncContextManager[
75
+ tuple[
76
+ MemoryObjectReceiveStream[SessionMessage | Exception],
77
+ MemoryObjectSendStream[SessionMessage],
78
+ GetSessionIdCallback,
79
+ ],
80
+ None,
81
+ ]:
82
+ """Return an async context manager whose __aenter__ method yields (read_stream, send_stream)."""
83
+ param_copy = copy.deepcopy(params)
84
+
85
+ if additional_params:
86
+ override_headers = bool(
87
+ additional_params.get("override_headers", False)
88
+ )
89
+
90
+ if "headers" in additional_params:
91
+ if override_headers:
92
+ param_copy.headers = additional_params.get(
93
+ "headers", params.headers
94
+ )
95
+ else:
96
+ param_copy.headers.update(
97
+ additional_params.get("headers", {})
98
+ )
99
+ if "auth" in additional_params and isinstance(
100
+ additional_params.get("auth"), httpx.Auth
101
+ ):
102
+ param_copy.auth = additional_params.get("auth", param_copy.auth)
103
+
104
+ if "read_timeout_seconds" in additional_params:
105
+ param_copy.timeout = additional_params.get(
106
+ "read_timeout_seconds", params.timeout
107
+ )
108
+
109
+ if "sse_read_timeout" in additional_params:
110
+ param_copy.sse_read_timeout = additional_params.get(
111
+ "sse_read_timeout",
112
+ params.sse_read_timeout,
113
+ )
114
+ if "url" in additional_params:
115
+ param_copy.url = additional_params.get(
116
+ "url",
117
+ params.url,
118
+ )
119
+
120
+ if "terminate_on_close" in additional_params:
121
+ param_copy.terminate_on_close = bool(
122
+ additional_params.get("terminate_on_close", True)
123
+ )
124
+
125
+ timeout_http = timedelta(seconds=param_copy.timeout)
126
+ sse_timeout = timedelta(seconds=param_copy.sse_read_timeout)
127
+
128
+ return streamablehttp_client(
129
+ url=param_copy.url,
130
+ headers=param_copy.headers,
131
+ timeout=timeout_http,
132
+ sse_read_timeout=sse_timeout,
133
+ terminate_on_close=param_copy.terminate_on_close,
134
+ auth=param_copy.auth,
135
+ )
136
+
137
+
138
+ class FlockStreamableHttpClientManager(FlockMCPClientManagerBase):
139
+ """Manager for handling StreamableHttpClients."""
140
+
141
+ client_config: FlockStreamableHttpConfig = Field(
142
+ ..., description="Configuration for clients."
143
+ )
144
+
145
+ async def make_client(
146
+ self, additional_params: dict[str, Any] | None = None
147
+ ) -> FlockStreamableHttpClient:
148
+ """Create a new client instance."""
149
+ new_client = FlockStreamableHttpClient(
150
+ config=self.client_config,
151
+ additional_params=additional_params,
152
+ )
153
+ return new_client
154
+
155
+
156
+ class FlockStreamableHttpServer(FlockMCPServerBase):
157
+ """Class which represents a MCP Server using the streamable Http Transport type."""
158
+
159
+ config: FlockStreamableHttpConfig = Field(
160
+ ..., description="Config for the server."
161
+ )
162
+
163
+ async def initialize(self) -> FlockStreamableHttpClientManager:
164
+ """Called when initializing the server."""
165
+ client_manager = FlockStreamableHttpClientManager(
166
+ client_config=self.config
167
+ )
168
+
169
+ return client_manager
@@ -9,7 +9,7 @@ from anyio.streams.memory import (
9
9
  MemoryObjectSendStream,
10
10
  )
11
11
  from mcp.client.websocket import websocket_client
12
- from mcp.types import JSONRPCMessage
12
+ from mcp.shared.message import SessionMessage
13
13
  from opentelemetry import trace
14
14
  from pydantic import Field
15
15
 
@@ -69,8 +69,8 @@ class FlockWSClient(FlockMCPClientBase):
69
69
  additional_params: dict[str, Any] | None = None,
70
70
  ) -> AbstractAsyncContextManager[
71
71
  tuple[
72
- MemoryObjectReceiveStream[JSONRPCMessage | Exception],
73
- MemoryObjectSendStream[JSONRPCMessage],
72
+ MemoryObjectReceiveStream[SessionMessage | Exception],
73
+ MemoryObjectSendStream[SessionMessage],
74
74
  ]
75
75
  ]:
76
76
  """Return an async context manager whose __aenter__ method yields a read_stream and a send_stream."""
flock/webapp/app/main.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # src/flock/webapp/app/main.py
2
+ import asyncio
2
3
  import json
3
4
  import os # Added import
4
5
  import shutil
@@ -8,6 +9,7 @@ import urllib.parse
8
9
  import uuid
9
10
  from contextlib import asynccontextmanager
10
11
  from pathlib import Path
12
+ from typing import Any
11
13
 
12
14
  import markdown2 # Import markdown2
13
15
  from fastapi import (
@@ -24,13 +26,13 @@ from fastapi.responses import HTMLResponse, RedirectResponse
24
26
  from fastapi.staticfiles import StaticFiles
25
27
  from fastapi.templating import Jinja2Templates
26
28
  from pydantic import BaseModel
27
- from typing import Any
28
29
 
29
30
  from flock.core.api.endpoints import create_api_router
30
31
  from flock.core.api.run_store import RunStore
31
32
 
32
33
  # Import core Flock components and API related modules
33
34
  from flock.core.flock import Flock # For type hinting
35
+ from flock.core.flock_scheduler import FlockScheduler
34
36
  from flock.core.logging.logging import get_logger # For logging
35
37
  from flock.core.util.spliter import parse_schema
36
38
 
@@ -44,7 +46,6 @@ from flock.webapp.app.api import (
44
46
  from flock.webapp.app.config import (
45
47
  DEFAULT_THEME_NAME,
46
48
  FLOCK_FILES_DIR,
47
- SHARED_LINKS_DB_PATH,
48
49
  THEMES_DIR,
49
50
  get_current_theme_name,
50
51
  )
@@ -72,7 +73,7 @@ from flock.webapp.app.services.flock_service import (
72
73
  from flock.webapp.app.services.sharing_models import SharedLinkConfig
73
74
  from flock.webapp.app.services.sharing_store import (
74
75
  SharedLinkStoreInterface,
75
- SQLiteSharedLinkStore,
76
+ create_shared_link_store,
76
77
  )
77
78
  from flock.webapp.app.theme_mapper import alacritty_to_pico
78
79
 
@@ -138,23 +139,21 @@ async def lifespan(app: FastAPI):
138
139
  logger.info("FastAPI application starting up...")
139
140
  # Flock instance and RunStore are expected to be set on app.state
140
141
  # by `start_unified_server` in `webapp/run.py` *before* uvicorn starts the app.
141
- # The call to `set_global_flock_services` also happens there.
142
-
143
- # Initialize and set the SharedLinkStore
142
+ # The call to `set_global_flock_services` also happens there. # Initialize and set the SharedLinkStore
144
143
  try:
145
- logger.info(f"Initializing SharedLinkStore with DB path: {SHARED_LINKS_DB_PATH}")
146
- shared_link_store = SQLiteSharedLinkStore(db_path=str(SHARED_LINKS_DB_PATH))
144
+ logger.info("Initializing SharedLinkStore using factory...")
145
+ shared_link_store = create_shared_link_store()
147
146
  await shared_link_store.initialize() # Create tables if they don't exist
148
147
  set_global_shared_link_store(shared_link_store)
149
148
  logger.info("SharedLinkStore initialized and set globally.")
150
149
  except Exception as e:
151
- logger.error(f"Failed to initialize SharedLinkStore: {e}", exc_info=True) # Configure chat features with clear precedence:
150
+ logger.error(f"Failed to initialize SharedLinkStore: {e}", exc_info=True)# Configure chat features with clear precedence:
152
151
  # 1. Value set by start_unified_server (programmatic)
153
152
  # 2. Environment variables (standalone mode)
154
153
  programmatic_chat_enabled = getattr(app.state, "chat_enabled", None)
155
154
  env_start_mode = os.environ.get("FLOCK_START_MODE")
156
155
  env_chat_enabled = os.environ.get("FLOCK_CHAT_ENABLED", "false").lower() == "true"
157
-
156
+
158
157
  if programmatic_chat_enabled is not None:
159
158
  # Programmatic setting takes precedence (from start_unified_server)
160
159
  should_enable_chat_routes = programmatic_chat_enabled
@@ -235,11 +234,66 @@ async def lifespan(app: FastAPI):
235
234
  logger.info(f"Lifespan: Added {len(pending_endpoints)} custom API routes to main app.")
236
235
  else:
237
236
  logger.warning("Lifespan: Pending custom endpoints found, but no Flock instance in app.state. Cannot add custom routes.")
237
+
238
+ # --- Add Scheduler Startup Logic ---
239
+ flock_instance_from_state: Flock | None = getattr(app.state, "flock_instance", None)
240
+ if flock_instance_from_state:
241
+ # Create and start the scheduler
242
+ scheduler = FlockScheduler(flock_instance_from_state)
243
+ app.state.flock_scheduler = scheduler # Store for access during shutdown
244
+
245
+ scheduler_loop_task = await scheduler.start() # Start returns the task
246
+ if scheduler_loop_task:
247
+ app.state.flock_scheduler_task = scheduler_loop_task # Store the task
248
+ logger.info("FlockScheduler background task started.")
249
+ else:
250
+ app.state.flock_scheduler_task = None
251
+ logger.info("FlockScheduler initialized, but no scheduled agents found or loop not started.")
252
+ else:
253
+ app.state.flock_scheduler = None
254
+ app.state.flock_scheduler_task = None
255
+ logger.warning("No Flock instance found in app.state; FlockScheduler not started.")
256
+ # --- End Scheduler Startup Logic ---
257
+
238
258
  yield
239
259
  logger.info("FastAPI application shutting down...")
240
260
 
241
- app = FastAPI(title="Flock Web UI & API", lifespan=lifespan)
261
+ # --- Add Scheduler Shutdown Logic ---
262
+ logger.info("FastAPI application initiating shutdown...")
263
+ scheduler_to_stop: FlockScheduler | None = getattr(app.state, "flock_scheduler", None)
264
+ scheduler_task_to_await: asyncio.Task | None = getattr(app.state, "flock_scheduler_task", None)
242
265
 
266
+ if scheduler_to_stop:
267
+ logger.info("Attempting to stop FlockScheduler...")
268
+ await scheduler_to_stop.stop() # Signal the scheduler loop to stop
269
+
270
+ if scheduler_task_to_await and not scheduler_task_to_await.done():
271
+ logger.info("Waiting for FlockScheduler task to complete...")
272
+ try:
273
+ await asyncio.wait_for(scheduler_task_to_await, timeout=10.0) # Wait for graceful exit
274
+ logger.info("FlockScheduler task completed gracefully.")
275
+ except asyncio.TimeoutError:
276
+ logger.warning("FlockScheduler task did not complete in time, cancelling.")
277
+ scheduler_task_to_await.cancel()
278
+ try:
279
+ await scheduler_task_to_await # Await cancellation
280
+ except asyncio.CancelledError:
281
+ logger.info("FlockScheduler task cancelled.")
282
+ except Exception as e:
283
+ logger.error(f"Error during FlockScheduler task finalization: {e}", exc_info=True)
284
+ elif scheduler_task_to_await and scheduler_task_to_await.done():
285
+ logger.info("FlockScheduler task was already done.")
286
+ else:
287
+ logger.info("FlockScheduler instance found, but no running task was stored to await.")
288
+ else:
289
+ logger.info("No active FlockScheduler found to stop.")
290
+
291
+ logger.info("FastAPI application finished shutdown sequence.")
292
+ # --- End Scheduler Shutdown Logic ---
293
+
294
+ app = FastAPI(title="Flock Web UI & API", lifespan=lifespan, docs_url="/docs",
295
+ openapi_url="/openapi.json", root_path=os.getenv("FLOCK_ROOT_PATH", ""))
296
+ logger.info("FastAPI booting complete.")
243
297
  BASE_DIR = Path(__file__).resolve().parent.parent
244
298
  app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
245
299
  templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
@@ -826,6 +880,7 @@ async def ui_create_flock_action(request: Request, flock_name: str = Form(...),
826
880
  context = get_base_context_web(request, success=success_msg_text, ui_mode=ui_mode_query)
827
881
  return templates.TemplateResponse("partials/_execution_view_container.html", context, headers=response_headers)
828
882
 
883
+
829
884
  # --- Settings Page & Endpoints ---
830
885
  @app.get("/ui/settings", response_class=HTMLResponse, tags=["UI Pages"])
831
886
  async def page_settings(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):