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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FlockAgentConfig(BaseModel):
|
|
7
|
+
"""FlockAgentConfig is a class that holds the configuration for a Flock agent.
|
|
8
|
+
It is used to store various settings and parameters that can be accessed throughout the agent's lifecycle.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
pass
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from flock.core.config.flock_agent_config import FlockAgentConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ScheduledAgentConfig(FlockAgentConfig):
|
|
10
|
+
"""Configuration specific to agents that run on a schedule."""
|
|
11
|
+
schedule_expression: str = Field(
|
|
12
|
+
...,
|
|
13
|
+
description="Defines when the agent should run. "
|
|
14
|
+
"Examples: 'every 60m', 'every 1h', 'daily at 02:00', '0 */2 * * *' (cron expression)"
|
|
15
|
+
)
|
|
16
|
+
enabled: bool = Field(
|
|
17
|
+
True,
|
|
18
|
+
description="Whether the scheduled agent is enabled. "
|
|
19
|
+
"If False, the agent will not run even if the schedule expression is valid."
|
|
20
|
+
)
|
|
21
|
+
initial_run: bool = Field(
|
|
22
|
+
False,
|
|
23
|
+
description="If True, the agent will run immediately after being scheduled, "
|
|
24
|
+
"regardless of the schedule expression."
|
|
25
|
+
)
|
|
26
|
+
max_runs: int = Field(
|
|
27
|
+
0,
|
|
28
|
+
description="Maximum number of times the agent can run. "
|
|
29
|
+
"0 means unlimited runs. If set, the agent will stop running after reaching this limit."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __init__(self, **kwargs):
|
|
33
|
+
super().__init__(**kwargs)
|
|
34
|
+
# Ensure schedule_expression is always set
|
|
35
|
+
if 'schedule_expression' not in kwargs:
|
|
36
|
+
raise ValueError("schedule_expression is required for ScheduledAgentConfig")
|
|
37
|
+
|
|
38
|
+
# Validate initial_run and max_runs
|
|
39
|
+
if self.initial_run and self.max_runs > 0:
|
|
40
|
+
raise ValueError("Cannot set initial_run to True if max_runs is greater than 0")
|
flock/core/flock_agent.py
CHANGED
|
@@ -10,6 +10,7 @@ from collections.abc import Callable
|
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
12
12
|
|
|
13
|
+
from flock.core.config.flock_agent_config import FlockAgentConfig
|
|
13
14
|
from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
|
|
14
15
|
from flock.core.serialization.json_encoder import FlockJSONEncoder
|
|
15
16
|
from flock.workflow.temporal_config import TemporalActivityConfig
|
|
@@ -124,6 +125,11 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
124
125
|
description="Dictionary of FlockModules attached to this agent.",
|
|
125
126
|
)
|
|
126
127
|
|
|
128
|
+
config: FlockAgentConfig = Field(
|
|
129
|
+
default_factory=lambda: FlockAgentConfig(),
|
|
130
|
+
description="Configuration for this agent, holding various settings and parameters.",
|
|
131
|
+
)
|
|
132
|
+
|
|
127
133
|
# --- Temporal Configuration (Optional) ---
|
|
128
134
|
temporal_activity_config: TemporalActivityConfig | None = Field(
|
|
129
135
|
default=None,
|
|
@@ -378,7 +384,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
378
384
|
)
|
|
379
385
|
else:
|
|
380
386
|
logger.warning(
|
|
381
|
-
f"No Server with name '{server}' registered! Skipping."
|
|
387
|
+
f"No Server with name '{server.config.name}' registered! Skipping."
|
|
382
388
|
)
|
|
383
389
|
mcp_tools = mcp_tools + server_tools
|
|
384
390
|
|
flock/core/flock_factory.py
CHANGED
|
@@ -5,8 +5,10 @@ from collections.abc import Callable
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Literal
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import httpx
|
|
9
|
+
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, FileUrl
|
|
9
10
|
|
|
11
|
+
from flock.core.config.scheduled_agent_config import ScheduledAgentConfig
|
|
10
12
|
from flock.core.flock_agent import FlockAgent, SignatureType
|
|
11
13
|
from flock.core.logging.formatters.themes import OutputTheme
|
|
12
14
|
from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
|
|
@@ -23,6 +25,7 @@ from flock.core.mcp.types.types import (
|
|
|
23
25
|
MCPRoot,
|
|
24
26
|
SseServerParameters,
|
|
25
27
|
StdioServerParameters,
|
|
28
|
+
StreamableHttpServerParameters,
|
|
26
29
|
WebsocketServerParameters,
|
|
27
30
|
)
|
|
28
31
|
from flock.evaluators.declarative.declarative_evaluator import (
|
|
@@ -39,6 +42,11 @@ from flock.mcp.servers.stdio.flock_stdio_server import (
|
|
|
39
42
|
FlockStdioConfig,
|
|
40
43
|
FlockStdioConnectionConfig,
|
|
41
44
|
)
|
|
45
|
+
from flock.mcp.servers.streamable_http.flock_streamable_http_server import (
|
|
46
|
+
FlockStreamableHttpConfig,
|
|
47
|
+
FlockStreamableHttpConnectionConfig,
|
|
48
|
+
FlockStreamableHttpServer,
|
|
49
|
+
)
|
|
42
50
|
from flock.mcp.servers.websockets.flock_websocket_server import (
|
|
43
51
|
FlockWSConfig,
|
|
44
52
|
FlockWSConnectionConfig,
|
|
@@ -100,6 +108,44 @@ class FlockFactory:
|
|
|
100
108
|
description="The text encoding error handler. See https://docs.python.org/3/library/codecs.html#codec-base-classes for explanations of possible values",
|
|
101
109
|
)
|
|
102
110
|
|
|
111
|
+
class StreamableHttpParams(BaseModel):
|
|
112
|
+
"""Factory-Params for Streamable Http Servers."""
|
|
113
|
+
|
|
114
|
+
url: str | AnyUrl = Field(
|
|
115
|
+
...,
|
|
116
|
+
description="Url the server listens at."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
headers: dict[str, Any] | None = Field(
|
|
120
|
+
default=None,
|
|
121
|
+
description="Additional Headers to pass to the client."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
auth: httpx.Auth | None = Field(
|
|
125
|
+
default=None,
|
|
126
|
+
description="Httpx Auth Schema."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
timeout_seconds: float | int = Field(
|
|
130
|
+
default=5,
|
|
131
|
+
description="Http Timeout in Seconds"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
sse_read_timeout_seconds: float | int = Field(
|
|
135
|
+
default=60*5,
|
|
136
|
+
description="How many seconds to wait for server-sent events until closing the connection."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
terminate_on_close: bool = Field(
|
|
140
|
+
default=True,
|
|
141
|
+
description="Whether or not to terminate the underlying connection on close."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
model_config = ConfigDict(
|
|
145
|
+
arbitrary_types_allowed=True,
|
|
146
|
+
extra="allow",
|
|
147
|
+
)
|
|
148
|
+
|
|
103
149
|
class SSEParams(BaseModel):
|
|
104
150
|
"""Factory-Params for SSE-Servers."""
|
|
105
151
|
|
|
@@ -122,6 +168,16 @@ class FlockFactory:
|
|
|
122
168
|
description="How many seconds to wait for server-sent events until closing the connection. (connections will be automatically re-established.)",
|
|
123
169
|
)
|
|
124
170
|
|
|
171
|
+
auth: httpx.Auth | None = Field(
|
|
172
|
+
default=None,
|
|
173
|
+
description="Httpx Auth Scheme."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
model_config = ConfigDict(
|
|
177
|
+
arbitrary_types_allowed=True,
|
|
178
|
+
extra="allow",
|
|
179
|
+
)
|
|
180
|
+
|
|
125
181
|
class WebsocketParams(BaseModel):
|
|
126
182
|
"""Factory-Params for Websocket Servers."""
|
|
127
183
|
|
|
@@ -133,7 +189,7 @@ class FlockFactory:
|
|
|
133
189
|
@staticmethod
|
|
134
190
|
def create_mcp_server(
|
|
135
191
|
name: str,
|
|
136
|
-
connection_params: SSEParams | StdioParams | WebsocketParams,
|
|
192
|
+
connection_params: StreamableHttpParams | SSEParams | StdioParams | WebsocketParams,
|
|
137
193
|
max_retries: int = 3,
|
|
138
194
|
mount_points: list[str | MCPRoot] | None = None,
|
|
139
195
|
timeout_seconds: int | float = 10,
|
|
@@ -175,6 +231,9 @@ class FlockFactory:
|
|
|
175
231
|
if isinstance(connection_params, FlockFactory.WebsocketParams):
|
|
176
232
|
server_kind = "websockets"
|
|
177
233
|
concrete_server_cls = FlockWSServer
|
|
234
|
+
if isinstance(connection_params, FlockFactory.StreamableHttpParams):
|
|
235
|
+
server_kind = "streamable_http"
|
|
236
|
+
concrete_server_cls = FlockStreamableHttpServer
|
|
178
237
|
|
|
179
238
|
# convert mount points.
|
|
180
239
|
mounts: list[MCPRoot] = []
|
|
@@ -243,12 +302,37 @@ class FlockFactory:
|
|
|
243
302
|
caching_config=caching_config,
|
|
244
303
|
callback_config=callback_config,
|
|
245
304
|
)
|
|
305
|
+
elif server_kind == "streamable_http":
|
|
306
|
+
# build streamable http config
|
|
307
|
+
connection_config = FlockStreamableHttpConnectionConfig(
|
|
308
|
+
max_retries=max_retries,
|
|
309
|
+
connection_parameters=StreamableHttpServerParameters(
|
|
310
|
+
url=connection_params.url,
|
|
311
|
+
headers=connection_params.headers,
|
|
312
|
+
auth=connection_params.auth,
|
|
313
|
+
timeout=connection_params.timeout_seconds,
|
|
314
|
+
sse_read_timeout=connection_params.sse_read_timeout_seconds,
|
|
315
|
+
terminate_on_close=connection_params.terminate_on_close,
|
|
316
|
+
),
|
|
317
|
+
mount_points=mounts,
|
|
318
|
+
server_logging_level=server_logging_level,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
server_config = FlockStreamableHttpConfig(
|
|
322
|
+
name=name,
|
|
323
|
+
connection_config=connection_config,
|
|
324
|
+
feature_config=feature_config,
|
|
325
|
+
caching_config=caching_config,
|
|
326
|
+
callback_config=callback_config,
|
|
327
|
+
)
|
|
328
|
+
|
|
246
329
|
elif server_kind == "sse":
|
|
247
330
|
# build sse config
|
|
248
331
|
connection_config = FlockSSEConnectionConfig(
|
|
249
332
|
max_retries=max_retries,
|
|
250
333
|
connection_parameters=SseServerParameters(
|
|
251
334
|
url=connection_params.url,
|
|
335
|
+
auth=connection_params.auth,
|
|
252
336
|
headers=connection_params.headers,
|
|
253
337
|
timeout=connection_params.timeout_seconds,
|
|
254
338
|
sse_read_timeout=connection_params.sse_read_timeout_seconds,
|
|
@@ -381,3 +465,46 @@ class FlockFactory:
|
|
|
381
465
|
agent.add_module(output_module)
|
|
382
466
|
agent.add_module(metrics_module)
|
|
383
467
|
return agent
|
|
468
|
+
|
|
469
|
+
@staticmethod
|
|
470
|
+
def create_scheduled_agent(
|
|
471
|
+
name: str,
|
|
472
|
+
schedule_expression: str, # e.g., "every 1h", "0 0 * * *"
|
|
473
|
+
description: str | Callable[..., str] | None = None,
|
|
474
|
+
model: str | Callable[..., str] | None = None,
|
|
475
|
+
input: SignatureType = None, # Input might be implicit or none
|
|
476
|
+
output: SignatureType = None, # Input might be implicit or none
|
|
477
|
+
tools: list[Callable[..., Any] | Any] | None = None,
|
|
478
|
+
servers: list[str | FlockMCPServerBase] | None = None,
|
|
479
|
+
use_cache: bool = False, # Whether to cache results
|
|
480
|
+
temperature: float = 0.7, # Temperature for model responses
|
|
481
|
+
# ... other common agent params from create_default_agent ...
|
|
482
|
+
temporal_activity_config: TemporalActivityConfig | None = None, # If you want scheduled tasks to be Temporal activities
|
|
483
|
+
**kwargs # Forward other standard agent params
|
|
484
|
+
) -> FlockAgent:
|
|
485
|
+
"""Creates a FlockAgent configured to run on a schedule."""
|
|
486
|
+
agent_config = ScheduledAgentConfig( # Use the new config type
|
|
487
|
+
schedule_expression=schedule_expression,
|
|
488
|
+
enabled=True,
|
|
489
|
+
initial_run=True,
|
|
490
|
+
max_runs=0,
|
|
491
|
+
**kwargs
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
agent = FlockFactory.create_default_agent( # Reuse your existing factory
|
|
496
|
+
name=name,
|
|
497
|
+
description=description,
|
|
498
|
+
model=model,
|
|
499
|
+
input=input + ", trigger_time: str | Time of scheduled execution",
|
|
500
|
+
output=output,
|
|
501
|
+
tools=tools,
|
|
502
|
+
servers=servers,
|
|
503
|
+
temporal_activity_config=temporal_activity_config,
|
|
504
|
+
use_cache=use_cache,
|
|
505
|
+
temperature=temperature,
|
|
506
|
+
**kwargs
|
|
507
|
+
)
|
|
508
|
+
agent.config = agent_config # Assign the scheduled agent config
|
|
509
|
+
|
|
510
|
+
return agent
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# src/flock/core/scheduler.py (new file or inside flock.py)
|
|
2
|
+
import asyncio
|
|
3
|
+
import traceback
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
|
|
6
|
+
from croniter import croniter # pip install croniter
|
|
7
|
+
|
|
8
|
+
from flock.core.flock import Flock
|
|
9
|
+
from flock.core.flock_agent import FlockAgent # For type hinting
|
|
10
|
+
from flock.core.logging.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("flock.scheduler")
|
|
13
|
+
|
|
14
|
+
class FlockScheduler:
|
|
15
|
+
def __init__(self, flock_instance: Flock):
|
|
16
|
+
self.flock = flock_instance
|
|
17
|
+
self._scheduled_tasks: list[tuple[FlockAgent, croniter | timedelta, datetime | None]] = []
|
|
18
|
+
self._stop_event = asyncio.Event()
|
|
19
|
+
self._is_running = False
|
|
20
|
+
|
|
21
|
+
def _parse_schedule_expression(self, expression: str) -> croniter | timedelta:
|
|
22
|
+
"""Parses a schedule expression.
|
|
23
|
+
Supports cron syntax and simple intervals like 'every Xm', 'every Xh', 'every Xd'.
|
|
24
|
+
"""
|
|
25
|
+
expression = expression.strip().lower()
|
|
26
|
+
if expression.startswith("every "):
|
|
27
|
+
try:
|
|
28
|
+
parts = expression.split(" ")
|
|
29
|
+
value = int(parts[1][:-1])
|
|
30
|
+
unit = parts[1][-1]
|
|
31
|
+
if unit == 's': return timedelta(seconds=value)
|
|
32
|
+
if unit == 'm': return timedelta(minutes=value)
|
|
33
|
+
elif unit == 'h': return timedelta(hours=value)
|
|
34
|
+
elif unit == 'd': return timedelta(days=value)
|
|
35
|
+
else: raise ValueError(f"Invalid time unit: {unit}")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.error(f"Invalid interval expression '{expression}': {e}")
|
|
38
|
+
raise ValueError(f"Invalid interval expression: {expression}") from e
|
|
39
|
+
else:
|
|
40
|
+
# Assume cron expression
|
|
41
|
+
if not croniter.is_valid(expression):
|
|
42
|
+
raise ValueError(f"Invalid cron expression: {expression}")
|
|
43
|
+
return croniter(expression, datetime.now(timezone.utc))
|
|
44
|
+
|
|
45
|
+
def add_agent(self, agent: FlockAgent):
|
|
46
|
+
"""Adds an agent to the scheduler if it has a schedule expression."""
|
|
47
|
+
# Assuming schedule_expression is stored in agent._flock_config or a dedicated field
|
|
48
|
+
schedule_expression = None
|
|
49
|
+
if hasattr(agent, '_flock_config') and isinstance(agent._flock_config, dict):
|
|
50
|
+
schedule_expression = agent._flock_config.get('schedule_expression')
|
|
51
|
+
elif hasattr(agent, 'schedule_expression'): # If directly on agent
|
|
52
|
+
schedule_expression = agent.schedule_expression
|
|
53
|
+
elif hasattr(agent, 'config') and hasattr(agent.config, 'schedule_expression'): # If on agent.config
|
|
54
|
+
schedule_expression = agent.config.schedule_expression
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if not schedule_expression:
|
|
58
|
+
logger.warning(f"Agent '{agent.name}' has no schedule_expression. Skipping.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
parsed_schedule = self._parse_schedule_expression(schedule_expression)
|
|
63
|
+
self._scheduled_tasks.append((agent, parsed_schedule, None)) # agent, schedule, last_run_utc
|
|
64
|
+
logger.info(f"Scheduled agent '{agent.name}' with expression: {schedule_expression}")
|
|
65
|
+
except ValueError as e:
|
|
66
|
+
logger.error(f"Could not schedule agent '{agent.name}': {e}")
|
|
67
|
+
|
|
68
|
+
def _load_scheduled_agents_from_flock(self):
|
|
69
|
+
"""Scans the Flock instance for agents with scheduling configuration."""
|
|
70
|
+
self._scheduled_tasks = [] # Clear existing before loading
|
|
71
|
+
for agent_name, agent_instance in self.flock.agents.items():
|
|
72
|
+
self.add_agent(agent_instance)
|
|
73
|
+
logger.info(f"Loaded {len(self._scheduled_tasks)} scheduled agents from Flock '{self.flock.name}'.")
|
|
74
|
+
|
|
75
|
+
async def _run_agent_task(self, agent: FlockAgent, trigger_time: datetime):
|
|
76
|
+
logger.info(f"Triggering scheduled agent '{agent.name}' at {trigger_time.isoformat()}")
|
|
77
|
+
try:
|
|
78
|
+
# Input for a scheduled agent could include the trigger time
|
|
79
|
+
await self.flock.run_async(start_agent=agent.name, input={"trigger_time": trigger_time})
|
|
80
|
+
logger.info(f"Scheduled agent '{agent.name}' finished successfully.")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Error running scheduled agent '{agent.name}': {e}\n{traceback.format_exc()}")
|
|
83
|
+
|
|
84
|
+
async def _scheduler_loop(self):
|
|
85
|
+
self._is_running = True
|
|
86
|
+
logger.info("FlockScheduler loop started.")
|
|
87
|
+
while not self._stop_event.is_set():
|
|
88
|
+
now_utc = datetime.now(timezone.utc)
|
|
89
|
+
tasks_to_run_this_cycle = []
|
|
90
|
+
|
|
91
|
+
for i, (agent, schedule, last_run_utc) in enumerate(self._scheduled_tasks):
|
|
92
|
+
should_run = False
|
|
93
|
+
next_run_utc = None
|
|
94
|
+
|
|
95
|
+
if isinstance(schedule, croniter):
|
|
96
|
+
# For cron, get the next scheduled time AFTER the last run (or now if never run)
|
|
97
|
+
base_time = last_run_utc if last_run_utc else now_utc
|
|
98
|
+
# Croniter's get_next gives the next time *after* the base_time.
|
|
99
|
+
# If last_run_utc is None (first run), we check if the *current* now_utc
|
|
100
|
+
# is past the first scheduled time.
|
|
101
|
+
# A simpler check: is `now_utc` >= `schedule.get_next(datetime, base_time=last_run_utc if last_run_utc else now_utc - timedelta(seconds=1))` ?
|
|
102
|
+
# Let's refine croniter check to be more precise.
|
|
103
|
+
# If we are past the *next* scheduled time since *last check*
|
|
104
|
+
if last_run_utc is None: # First run check
|
|
105
|
+
next_run_utc = schedule.get_next(datetime, start_time=now_utc - timedelta(seconds=1)) # Check if first run is due
|
|
106
|
+
if next_run_utc <= now_utc:
|
|
107
|
+
should_run = True
|
|
108
|
+
else:
|
|
109
|
+
next_run_utc = schedule.get_next(datetime, start_time=last_run_utc)
|
|
110
|
+
if next_run_utc <= now_utc:
|
|
111
|
+
should_run = True
|
|
112
|
+
|
|
113
|
+
elif isinstance(schedule, timedelta): # Simple interval
|
|
114
|
+
if last_run_utc is None or (now_utc - last_run_utc >= schedule):
|
|
115
|
+
should_run = True
|
|
116
|
+
next_run_utc = now_utc # Or now_utc + schedule for next interval start
|
|
117
|
+
else:
|
|
118
|
+
next_run_utc = last_run_utc + schedule
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if should_run:
|
|
122
|
+
tasks_to_run_this_cycle.append(self._run_agent_task(agent, now_utc))
|
|
123
|
+
# Update last_run_utc for this agent *before* awaiting its execution
|
|
124
|
+
# For cron, advance the iterator to the *current* scheduled time that triggered it.
|
|
125
|
+
if isinstance(schedule, croniter):
|
|
126
|
+
current_cron_trigger = schedule.get_current(datetime) # This is the time it *should* have run
|
|
127
|
+
self._scheduled_tasks[i] = (agent, schedule, current_cron_trigger)
|
|
128
|
+
|
|
129
|
+
elif isinstance(schedule, timedelta):
|
|
130
|
+
self._scheduled_tasks[i] = (agent, schedule, now_utc) # Mark as run now
|
|
131
|
+
|
|
132
|
+
if tasks_to_run_this_cycle:
|
|
133
|
+
await asyncio.gather(*tasks_to_run_this_cycle, return_exceptions=True)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Sleep for a short interval, e.g., 10 seconds, or until stop_event is set
|
|
137
|
+
await asyncio.wait_for(self._stop_event.wait(), timeout=10.0)
|
|
138
|
+
except asyncio.TimeoutError:
|
|
139
|
+
pass # Timeout is normal, means continue loop
|
|
140
|
+
self._is_running = False
|
|
141
|
+
logger.info("FlockScheduler loop stopped.")
|
|
142
|
+
|
|
143
|
+
async def start(self) -> asyncio.Task | None: # Modified to return Task or None
|
|
144
|
+
if self._is_running:
|
|
145
|
+
logger.warning("Scheduler is already running.")
|
|
146
|
+
return None # Or return the existing task if you store it
|
|
147
|
+
|
|
148
|
+
self._load_scheduled_agents_from_flock()
|
|
149
|
+
if not self._scheduled_tasks:
|
|
150
|
+
logger.info("No scheduled agents found. Scheduler will not start a loop task.")
|
|
151
|
+
return None # Return None if no tasks to schedule
|
|
152
|
+
|
|
153
|
+
self._stop_event.clear()
|
|
154
|
+
loop_task = asyncio.create_task(self._scheduler_loop())
|
|
155
|
+
# Store the task if you need to reference it, e.g., for forced cancellation beyond _stop_event
|
|
156
|
+
# self._loop_task = loop_task
|
|
157
|
+
return loop_task # Return the created task
|
|
158
|
+
|
|
159
|
+
async def stop(self):
|
|
160
|
+
if not self._is_running and not self._stop_event.is_set(): # Check if stop already called
|
|
161
|
+
logger.info("Scheduler is not running or already signaled to stop.")
|
|
162
|
+
return
|
|
163
|
+
logger.info("Stopping FlockScheduler...")
|
|
164
|
+
self._stop_event.set()
|
|
165
|
+
# If you stored self._loop_task, you can await it here or in the lifespan manager
|
|
166
|
+
# await self._loop_task # (This might block if loop doesn't exit quickly)
|
flock/core/logging/logging.py
CHANGED
|
@@ -68,6 +68,14 @@ COLOR_MAP = {
|
|
|
68
68
|
"workflow": "cyan", # Color only
|
|
69
69
|
"activities": "cyan",
|
|
70
70
|
"context": "green",
|
|
71
|
+
"mcp.server": "blue",
|
|
72
|
+
"mcp.tool": "cyan",
|
|
73
|
+
"mcp.client_manager": "light-blue",
|
|
74
|
+
"mcp.client": "light-cyan",
|
|
75
|
+
"mcp.callback.logging": "white",
|
|
76
|
+
"mcp.callback.sampling": "pink",
|
|
77
|
+
"mcp.callback.root": "light-yellow",
|
|
78
|
+
"mcp.callback.message": "light-blue",
|
|
71
79
|
# Components & Mechanisms
|
|
72
80
|
"registry": "yellow", # Color only
|
|
73
81
|
"serialization": "yellow",
|
|
@@ -26,7 +26,7 @@ from flock.core.serialization.serialization_utils import (
|
|
|
26
26
|
serialize_item,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
logger = get_logger("
|
|
29
|
+
logger = get_logger("mcp.server")
|
|
30
30
|
tracer = trace.get_tracer(__name__)
|
|
31
31
|
T = TypeVar("T", bound="FlockMCPServerBase")
|
|
32
32
|
|
|
@@ -206,7 +206,6 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
206
206
|
async with self.condition:
|
|
207
207
|
try:
|
|
208
208
|
await self.pre_mcp_call()
|
|
209
|
-
# TODO: inject additional params here.
|
|
210
209
|
additional_params: dict[str, Any] = {}
|
|
211
210
|
additional_params = await self.before_connect(
|
|
212
211
|
additional_params=additional_params
|
|
@@ -314,7 +313,7 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
314
313
|
async def post_terminate(self) -> None:
|
|
315
314
|
"""Run post-terminate hooks on modules."""
|
|
316
315
|
logger.debug(
|
|
317
|
-
f"Running
|
|
316
|
+
f"Running post_terminate hooks for modules in server: '{self.config.name}'"
|
|
318
317
|
)
|
|
319
318
|
with tracer.start_as_current_span("server.post_terminate") as span:
|
|
320
319
|
span.set_attribute("server.name", self.config.name)
|
|
@@ -437,7 +436,7 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
437
436
|
|
|
438
437
|
FlockRegistry = get_registry()
|
|
439
438
|
|
|
440
|
-
exclude = ["modules"]
|
|
439
|
+
exclude = ["modules", "config"]
|
|
441
440
|
|
|
442
441
|
logger.debug(f"Serializing server '{self.config.name}' to dict.")
|
|
443
442
|
# Use Pydantic's dump, exclued manually handled fields.
|
|
@@ -447,6 +446,11 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
447
446
|
exclude_none=True, # Exclude None values for cleaner output
|
|
448
447
|
)
|
|
449
448
|
|
|
449
|
+
# --- Let the config handle its own serialization ---
|
|
450
|
+
config_data = self.config.to_dict(path_type=path_type)
|
|
451
|
+
data["config"] = config_data
|
|
452
|
+
|
|
453
|
+
|
|
450
454
|
builtin_by_transport = {}
|
|
451
455
|
|
|
452
456
|
try:
|
|
@@ -454,12 +458,16 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
454
458
|
from flock.mcp.servers.stdio.flock_stdio_server import (
|
|
455
459
|
FlockMCPStdioServer,
|
|
456
460
|
)
|
|
461
|
+
from flock.mcp.servers.streamable_http.flock_streamable_http_server import (
|
|
462
|
+
FlockStreamableHttpServer,
|
|
463
|
+
)
|
|
457
464
|
from flock.mcp.servers.websockets.flock_websocket_server import (
|
|
458
465
|
FlockWSServer,
|
|
459
466
|
)
|
|
460
467
|
|
|
461
468
|
builtin_by_transport = {
|
|
462
469
|
"stdio": FlockMCPStdioServer,
|
|
470
|
+
"streamable_http": FlockStreamableHttpServer,
|
|
463
471
|
"sse": FlockSSEServer,
|
|
464
472
|
"websockets": FlockWSServer,
|
|
465
473
|
}
|
|
@@ -570,6 +578,9 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
570
578
|
from flock.mcp.servers.stdio.flock_stdio_server import (
|
|
571
579
|
FlockMCPStdioServer,
|
|
572
580
|
)
|
|
581
|
+
from flock.mcp.servers.streamable_http.flock_streamable_http_server import (
|
|
582
|
+
FlockStreamableHttpServer,
|
|
583
|
+
)
|
|
573
584
|
from flock.mcp.servers.websockets.flock_websocket_server import (
|
|
574
585
|
FlockWSServer,
|
|
575
586
|
)
|
|
@@ -577,6 +588,7 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
577
588
|
builtin_by_transport = {
|
|
578
589
|
"stdio": FlockMCPStdioServer,
|
|
579
590
|
"sse": FlockSSEServer,
|
|
591
|
+
"streamable_http": FlockStreamableHttpServer,
|
|
580
592
|
"websockets": FlockWSServer,
|
|
581
593
|
}
|
|
582
594
|
except ImportError:
|
|
@@ -592,6 +604,20 @@ class FlockMCPServerBase(BaseModel, Serializable, ABC):
|
|
|
592
604
|
transport = data["config"]["connection_config"]["transport_type"]
|
|
593
605
|
real_cls = builtin_by_transport.get(transport, cls)
|
|
594
606
|
|
|
607
|
+
# deserialize the config:
|
|
608
|
+
config_data = data.pop("config", None)
|
|
609
|
+
if config_data:
|
|
610
|
+
# Forcing a square into a round hole
|
|
611
|
+
# pretty ugly, but gets the job done.
|
|
612
|
+
try:
|
|
613
|
+
config_field = real_cls.model_fields["config"]
|
|
614
|
+
config_cls = config_field.annotation
|
|
615
|
+
except (AttributeError, KeyError):
|
|
616
|
+
# fallback if Pydantic v1 or missing
|
|
617
|
+
config_cls = FlockMCPConfigurationBase
|
|
618
|
+
config_object = config_cls.from_dict(config_data)
|
|
619
|
+
data["config"] = config_object
|
|
620
|
+
|
|
595
621
|
# now construct
|
|
596
622
|
server = real_cls(**{k: v for k, v in data.items() if k != "modules"})
|
|
597
623
|
|
|
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field
|
|
|
10
10
|
|
|
11
11
|
from flock.core.logging.logging import get_logger
|
|
12
12
|
|
|
13
|
-
logger = get_logger("
|
|
13
|
+
logger = get_logger("mcp.tool")
|
|
14
14
|
tracer = trace.get_tracer(__name__)
|
|
15
15
|
|
|
16
16
|
T = TypeVar("T", bound="FlockMCPToolBase")
|