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.
- flock/core/config/flock_agent_config.py +11 -0
- flock/core/config/scheduled_agent_config.py +40 -0
- flock/core/flock_agent.py +7 -1
- flock/core/flock_factory.py +129 -2
- flock/core/flock_scheduler.py +166 -0
- flock/core/logging/logging.py +8 -0
- flock/core/mcp/flock_mcp_server.py +30 -4
- flock/core/mcp/flock_mcp_tool_base.py +1 -1
- flock/core/mcp/mcp_client.py +57 -28
- flock/core/mcp/mcp_client_manager.py +1 -1
- flock/core/mcp/mcp_config.py +245 -9
- flock/core/mcp/types/callbacks.py +3 -5
- flock/core/mcp/types/factories.py +12 -14
- flock/core/mcp/types/handlers.py +9 -12
- flock/core/mcp/types/types.py +205 -2
- flock/mcp/servers/sse/flock_sse_server.py +21 -14
- flock/mcp/servers/streamable_http/__init__.py +0 -0
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +169 -0
- flock/mcp/servers/websockets/flock_websocket_server.py +3 -3
- flock/webapp/app/main.py +66 -11
- flock/webapp/app/services/sharing_store.py +173 -0
- flock/webapp/run.py +3 -1
- flock/webapp/templates/base.html +10 -11
- flock/webapp/templates/chat.html +7 -10
- flock/webapp/templates/chat_settings.html +3 -4
- flock/webapp/templates/flock_editor.html +1 -2
- flock/webapp/templates/index.html +1 -1
- flock/webapp/templates/partials/_agent_detail_form.html +7 -13
- flock/webapp/templates/partials/_agent_list.html +1 -2
- flock/webapp/templates/partials/_agent_manager_view.html +2 -3
- flock/webapp/templates/partials/_chat_container.html +2 -2
- flock/webapp/templates/partials/_chat_settings_form.html +6 -8
- flock/webapp/templates/partials/_create_flock_form.html +2 -4
- flock/webapp/templates/partials/_dashboard_flock_detail.html +2 -3
- flock/webapp/templates/partials/_dashboard_flock_file_list.html +1 -2
- flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +2 -3
- flock/webapp/templates/partials/_dashboard_upload_flock_form.html +1 -2
- flock/webapp/templates/partials/_env_vars_table.html +2 -4
- flock/webapp/templates/partials/_execution_form.html +12 -10
- flock/webapp/templates/partials/_execution_view_container.html +2 -3
- flock/webapp/templates/partials/_flock_file_list.html +2 -3
- flock/webapp/templates/partials/_flock_properties_form.html +2 -2
- flock/webapp/templates/partials/_flock_upload_form.html +1 -2
- flock/webapp/templates/partials/_load_manager_view.html +2 -3
- flock/webapp/templates/partials/_registry_viewer_content.html +4 -5
- flock/webapp/templates/partials/_settings_env_content.html +2 -3
- flock/webapp/templates/partials/_settings_theme_content.html +2 -2
- flock/webapp/templates/partials/_settings_view.html +2 -2
- flock/webapp/templates/partials/_sidebar.html +27 -39
- flock/webapp/templates/registry_viewer.html +7 -10
- flock/webapp/templates/shared_run_page.html +7 -10
- {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/METADATA +3 -1
- {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/RECORD +56 -51
- {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/WHEEL +0 -0
- {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.511.dist-info → flock_core-0.4.513.dist-info}/licenses/LICENSE +0 -0
flock/core/mcp/types/types.py
CHANGED
|
@@ -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.
|
|
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[
|
|
67
|
-
MemoryObjectSendStream[
|
|
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
|
|
75
|
+
if additional_params:
|
|
75
76
|
override_headers = bool(
|
|
76
|
-
|
|
77
|
+
additional_params.get("override_headers", False)
|
|
77
78
|
)
|
|
78
|
-
if "headers" in
|
|
79
|
+
if "headers" in additional_params:
|
|
79
80
|
if override_headers:
|
|
80
|
-
param_copy.headers =
|
|
81
|
+
param_copy.headers = additional_params.get(
|
|
81
82
|
"headers", params.headers
|
|
82
83
|
)
|
|
83
84
|
else:
|
|
84
85
|
param_copy.headers.update(
|
|
85
|
-
|
|
86
|
+
additional_params.get("headers", {})
|
|
86
87
|
)
|
|
87
|
-
if "read_timeout_seconds" in
|
|
88
|
-
param_copy.timeout =
|
|
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
|
|
93
|
-
param_copy.sse_read_timeout =
|
|
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
|
|
98
|
-
param_copy.url =
|
|
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.
|
|
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[
|
|
73
|
-
MemoryObjectSendStream[
|
|
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
|
-
|
|
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(
|
|
146
|
-
shared_link_store =
|
|
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)
|
|
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
|
-
|
|
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")):
|