flock-core 0.4.0b3__py3-none-any.whl → 0.4.0b5__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 +12 -0
- flock/cli/config.py +8 -0
- flock/cli/constants.py +11 -0
- flock/cli/create_flock.py +18 -6
- flock/cli/execute_flock.py +397 -1
- flock/cli/loaded_flock_cli.py +19 -4
- flock/cli/runner.py +41 -0
- flock/config.py +5 -0
- flock/core/api/endpoints.py +102 -2
- flock/core/api/main.py +214 -0
- flock/core/api/models.py +63 -0
- flock/core/api/run_store.py +153 -1
- flock/core/api/runner.py +38 -0
- flock/core/context/context_vars.py +1 -0
- flock/core/evaluation/utils.py +312 -0
- flock/core/execution/batch_executor.py +325 -0
- flock/core/execution/evaluation_executor.py +438 -0
- flock/core/flock.py +325 -1146
- flock/core/serialization/flock_serializer.py +717 -0
- flock/core/tools/azure_tools.py +2 -1
- flock/core/tools/basic_tools.py +1 -1
- flock/core/util/loader.py +59 -0
- flock/modules/output/output_module.py +43 -8
- {flock_core-0.4.0b3.dist-info → flock_core-0.4.0b5.dist-info}/METADATA +4 -1
- {flock_core-0.4.0b3.dist-info → flock_core-0.4.0b5.dist-info}/RECORD +28 -20
- {flock_core-0.4.0b3.dist-info → flock_core-0.4.0b5.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b3.dist-info → flock_core-0.4.0b5.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b3.dist-info → flock_core-0.4.0b5.dist-info}/licenses/LICENSE +0 -0
flock/core/flock.py
CHANGED
|
@@ -1,81 +1,71 @@
|
|
|
1
1
|
# src/flock/core/flock.py
|
|
2
|
-
"""High-level orchestrator for
|
|
2
|
+
"""High-level orchestrator for managing and executing agents within the Flock framework."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations # Ensure forward references work
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import os
|
|
8
8
|
import uuid
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
Literal,
|
|
15
|
+
TypeVar,
|
|
16
|
+
)
|
|
11
17
|
|
|
18
|
+
# Third-party imports
|
|
12
19
|
from box import Box
|
|
20
|
+
from datasets import Dataset
|
|
13
21
|
from opentelemetry import trace
|
|
14
22
|
from opentelemetry.baggage import get_baggage, set_baggage
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
from pydantic import BaseModel, Field # Using Pydantic directly now
|
|
23
|
+
from pandas import DataFrame
|
|
24
|
+
from pydantic import BaseModel, Field
|
|
18
25
|
|
|
19
26
|
# Flock core components & utilities
|
|
20
|
-
from flock.config import TELEMETRY
|
|
27
|
+
from flock.config import DEFAULT_MODEL, TELEMETRY
|
|
21
28
|
from flock.core.context.context import FlockContext
|
|
22
29
|
from flock.core.context.context_manager import initialize_context
|
|
23
30
|
from flock.core.execution.local_executor import run_local_workflow
|
|
24
31
|
from flock.core.execution.temporal_executor import run_temporal_workflow
|
|
32
|
+
from flock.core.flock_evaluator import FlockEvaluator
|
|
25
33
|
from flock.core.logging.logging import LOGGERS, get_logger, get_module_loggers
|
|
26
|
-
from flock.core.serialization.
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
from flock.core.util.input_resolver import split_top_level
|
|
34
|
+
from flock.core.serialization.serializable import Serializable
|
|
35
|
+
from flock.core.util.cli_helper import init_console
|
|
30
36
|
|
|
31
37
|
# Import FlockAgent using TYPE_CHECKING to avoid circular import at runtime
|
|
32
38
|
if TYPE_CHECKING:
|
|
39
|
+
# These imports are only for type hints
|
|
33
40
|
from flock.core.flock_agent import FlockAgent
|
|
34
|
-
else:
|
|
35
|
-
# Provide a forward reference string or Any for runtime if FlockAgent is used in hints here
|
|
36
|
-
FlockAgent = "FlockAgent" # Forward reference string for Pydantic/runtime
|
|
37
41
|
|
|
38
|
-
# Registry and Serialization
|
|
39
|
-
from flock.core.flock_registry import (
|
|
40
|
-
get_registry, # Use the unified registry
|
|
41
|
-
)
|
|
42
|
-
from flock.core.serialization.serializable import (
|
|
43
|
-
Serializable, # Import Serializable base
|
|
44
|
-
)
|
|
45
42
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
# relying on FlockAgent's implementation instead.
|
|
49
|
-
# from flock.core.serialization.serialization_utils import serialize_item, deserialize_item
|
|
50
|
-
# CLI Helper (if still used directly, otherwise can be removed)
|
|
51
|
-
from flock.core.util.cli_helper import init_console
|
|
43
|
+
# Registry
|
|
44
|
+
from flock.core.flock_registry import get_registry
|
|
52
45
|
|
|
53
|
-
# Cloudpickle for fallback/direct serialization if needed
|
|
54
46
|
try:
|
|
55
|
-
import
|
|
47
|
+
import pandas as pd
|
|
56
48
|
|
|
57
|
-
|
|
49
|
+
PANDAS_AVAILABLE = True
|
|
58
50
|
except ImportError:
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
pd = None
|
|
52
|
+
PANDAS_AVAILABLE = False
|
|
61
53
|
|
|
62
54
|
logger = get_logger("flock")
|
|
63
55
|
TELEMETRY.setup_tracing() # Setup OpenTelemetry
|
|
64
56
|
tracer = trace.get_tracer(__name__)
|
|
65
57
|
FlockRegistry = get_registry() # Get the registry instance
|
|
66
58
|
|
|
67
|
-
# Define TypeVar for generic methods like from_dict
|
|
59
|
+
# Define TypeVar for generic class methods like from_dict
|
|
68
60
|
T = TypeVar("T", bound="Flock")
|
|
69
61
|
|
|
70
62
|
|
|
71
|
-
# Inherit from Serializable for YAML/JSON/etc. methods
|
|
72
|
-
# Use BaseModel directly for Pydantic features
|
|
73
63
|
class Flock(BaseModel, Serializable):
|
|
74
|
-
"""
|
|
64
|
+
"""Orchestrator for managing and executing agent systems.
|
|
75
65
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
Manages agent definitions, context, and execution flow (local or Temporal).
|
|
67
|
+
Relies on FlockSerializer for serialization/deserialization logic.
|
|
68
|
+
Inherits from Pydantic BaseModel and Serializable.
|
|
79
69
|
"""
|
|
80
70
|
|
|
81
71
|
name: str | None = Field(
|
|
@@ -83,8 +73,8 @@ class Flock(BaseModel, Serializable):
|
|
|
83
73
|
description="A unique identifier for this Flock instance.",
|
|
84
74
|
)
|
|
85
75
|
model: str | None = Field(
|
|
86
|
-
default=
|
|
87
|
-
description="Default model identifier
|
|
76
|
+
default=DEFAULT_MODEL,
|
|
77
|
+
description="Default model identifier for agents if not specified otherwise.",
|
|
88
78
|
)
|
|
89
79
|
description: str | None = Field(
|
|
90
80
|
default=None,
|
|
@@ -100,47 +90,43 @@ class Flock(BaseModel, Serializable):
|
|
|
100
90
|
)
|
|
101
91
|
show_flock_banner: bool = Field(
|
|
102
92
|
default=True,
|
|
103
|
-
description="If True, show the Flock banner.",
|
|
93
|
+
description="If True, show the Flock banner on console interactions.",
|
|
104
94
|
)
|
|
105
|
-
#
|
|
106
|
-
# Store agents internally but don't make it part of the Pydantic model definition
|
|
107
|
-
# Use a regular attribute, initialized in __init__
|
|
108
|
-
# Pydantic V2 handles __init__ and attributes not in Field correctly
|
|
95
|
+
# Internal agent storage - not part of the Pydantic model for direct serialization
|
|
109
96
|
_agents: dict[str, FlockAgent]
|
|
110
|
-
_start_agent_name: str | None
|
|
111
|
-
_start_input: dict
|
|
97
|
+
_start_agent_name: str | None = None # For potential pre-configuration
|
|
98
|
+
_start_input: dict = {} # For potential pre-configuration
|
|
112
99
|
|
|
113
100
|
# Pydantic v2 model config
|
|
114
101
|
model_config = {
|
|
115
102
|
"arbitrary_types_allowed": True,
|
|
116
|
-
"ignored_types": (
|
|
117
|
-
type(FlockRegistry),
|
|
118
|
-
), # Prevent validation issues with registry
|
|
119
|
-
# No need to exclude fields here, handled in to_dict
|
|
103
|
+
"ignored_types": (type(FlockRegistry),),
|
|
120
104
|
}
|
|
121
105
|
|
|
122
106
|
def __init__(
|
|
123
107
|
self,
|
|
124
108
|
name: str | None = None,
|
|
125
|
-
model: str | None =
|
|
109
|
+
model: str | None = DEFAULT_MODEL,
|
|
126
110
|
description: str | None = None,
|
|
127
111
|
show_flock_banner: bool = True,
|
|
128
112
|
enable_temporal: bool = False,
|
|
129
|
-
enable_logging: bool
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
**kwargs, # Allow extra fields during init if needed, Pydantic handles it
|
|
113
|
+
enable_logging: bool | list[str] = False,
|
|
114
|
+
agents: list[FlockAgent] | None = None,
|
|
115
|
+
**kwargs,
|
|
133
116
|
):
|
|
134
117
|
"""Initialize the Flock orchestrator."""
|
|
118
|
+
# Use provided name or generate default BEFORE super init if needed elsewhere
|
|
119
|
+
effective_name = name or f"flock_{uuid.uuid4().hex[:8]}"
|
|
120
|
+
|
|
135
121
|
# Initialize Pydantic fields
|
|
136
122
|
super().__init__(
|
|
137
|
-
name=
|
|
123
|
+
name=effective_name,
|
|
138
124
|
model=model,
|
|
139
125
|
description=description,
|
|
140
126
|
enable_temporal=enable_temporal,
|
|
141
127
|
enable_logging=enable_logging,
|
|
142
128
|
show_flock_banner=show_flock_banner,
|
|
143
|
-
**kwargs,
|
|
129
|
+
**kwargs,
|
|
144
130
|
)
|
|
145
131
|
|
|
146
132
|
# Initialize runtime attributes AFTER super().__init__()
|
|
@@ -148,13 +134,11 @@ class Flock(BaseModel, Serializable):
|
|
|
148
134
|
self._start_agent_name = None
|
|
149
135
|
self._start_input = {}
|
|
150
136
|
|
|
151
|
-
# Set up logging
|
|
152
|
-
self._configure_logging(enable_logging)
|
|
137
|
+
# Set up logging based on the enable_logging flag
|
|
138
|
+
self._configure_logging(enable_logging) # Use instance attribute
|
|
153
139
|
|
|
154
140
|
# Register passed agents
|
|
155
141
|
if agents:
|
|
156
|
-
# Ensure FlockAgent type is available for isinstance check
|
|
157
|
-
# This import might need to be deferred or handled carefully if it causes issues
|
|
158
142
|
from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
|
|
159
143
|
|
|
160
144
|
for agent in agents:
|
|
@@ -165,8 +149,8 @@ class Flock(BaseModel, Serializable):
|
|
|
165
149
|
f"Item provided in 'agents' list is not a FlockAgent: {type(agent)}"
|
|
166
150
|
)
|
|
167
151
|
|
|
168
|
-
# Initialize console if needed
|
|
169
|
-
if show_flock_banner:
|
|
152
|
+
# Initialize console if needed for banner
|
|
153
|
+
if self.show_flock_banner: # Use instance attribute
|
|
170
154
|
init_console()
|
|
171
155
|
|
|
172
156
|
# Set Temporal debug environment variable
|
|
@@ -177,24 +161,20 @@ class Flock(BaseModel, Serializable):
|
|
|
177
161
|
|
|
178
162
|
logger.info(
|
|
179
163
|
"Flock instance initialized",
|
|
164
|
+
name=self.name,
|
|
180
165
|
model=self.model,
|
|
181
166
|
enable_temporal=self.enable_temporal,
|
|
182
167
|
)
|
|
183
168
|
|
|
184
|
-
# --- Keep _configure_logging, _set_temporal_debug_flag, _ensure_session_id ---
|
|
185
|
-
# ... (implementation as before) ...
|
|
186
169
|
def _configure_logging(self, enable_logging: bool | list[str]):
|
|
187
170
|
"""Configure logging levels based on the enable_logging flag."""
|
|
188
|
-
# logger.debug(f"Configuring logging, enable_logging={enable_logging}")
|
|
189
171
|
is_enabled_globally = False
|
|
190
172
|
enabled_loggers = []
|
|
191
173
|
|
|
192
174
|
if isinstance(enable_logging, bool):
|
|
193
175
|
is_enabled_globally = enable_logging
|
|
194
176
|
elif isinstance(enable_logging, list):
|
|
195
|
-
is_enabled_globally = bool(
|
|
196
|
-
enable_logging
|
|
197
|
-
) # Enable if list is not empty
|
|
177
|
+
is_enabled_globally = bool(enable_logging)
|
|
198
178
|
enabled_loggers = enable_logging
|
|
199
179
|
|
|
200
180
|
# Configure core loggers
|
|
@@ -235,11 +215,8 @@ class Flock(BaseModel, Serializable):
|
|
|
235
215
|
set_baggage("session_id", session_id)
|
|
236
216
|
logger.debug(f"Generated new session_id: {session_id}")
|
|
237
217
|
|
|
238
|
-
# --- Keep add_agent, agents property, run, run_async ---
|
|
239
|
-
# ... (implementation as before, ensuring FlockAgent type hint is handled) ...
|
|
240
218
|
def add_agent(self, agent: FlockAgent) -> FlockAgent:
|
|
241
|
-
"""Adds an agent instance to this Flock configuration."""
|
|
242
|
-
# Ensure FlockAgent type is available for isinstance check
|
|
219
|
+
"""Adds an agent instance to this Flock configuration and registry."""
|
|
243
220
|
from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
|
|
244
221
|
|
|
245
222
|
if not isinstance(agent, ConcreteFlockAgent):
|
|
@@ -248,16 +225,13 @@ class Flock(BaseModel, Serializable):
|
|
|
248
225
|
raise ValueError("Agent must have a name.")
|
|
249
226
|
|
|
250
227
|
if agent.name in self._agents:
|
|
251
|
-
logger.warning(
|
|
252
|
-
f"Agent '{agent.name}' already exists in this Flock instance. Overwriting."
|
|
253
|
-
)
|
|
228
|
+
logger.warning(f"Agent '{agent.name}' already exists. Overwriting.")
|
|
254
229
|
self._agents[agent.name] = agent
|
|
255
|
-
FlockRegistry.register_agent(agent) #
|
|
230
|
+
FlockRegistry.register_agent(agent) # Register globally
|
|
256
231
|
|
|
257
232
|
# Set default model if agent doesn't have one
|
|
258
233
|
if agent.model is None:
|
|
259
|
-
|
|
260
|
-
if self.model: # Ensure Flock has a model defined
|
|
234
|
+
if self.model:
|
|
261
235
|
agent.set_model(self.model)
|
|
262
236
|
logger.debug(
|
|
263
237
|
f"Agent '{agent.name}' using Flock default model: {self.model}"
|
|
@@ -267,7 +241,7 @@ class Flock(BaseModel, Serializable):
|
|
|
267
241
|
f"Agent '{agent.name}' has no model and Flock default model is not set."
|
|
268
242
|
)
|
|
269
243
|
|
|
270
|
-
logger.info(f"Agent '{agent.name}' added to Flock.")
|
|
244
|
+
logger.info(f"Agent '{agent.name}' added to Flock '{self.name}'.")
|
|
271
245
|
return agent
|
|
272
246
|
|
|
273
247
|
@property
|
|
@@ -279,31 +253,46 @@ class Flock(BaseModel, Serializable):
|
|
|
279
253
|
self,
|
|
280
254
|
start_agent: FlockAgent | str | None = None,
|
|
281
255
|
input: dict = {},
|
|
282
|
-
context: FlockContext
|
|
283
|
-
| None = None, # Allow passing initial context state
|
|
256
|
+
context: FlockContext | None = None,
|
|
284
257
|
run_id: str = "",
|
|
285
|
-
box_result: bool = True,
|
|
286
|
-
agents: list[FlockAgent] | None = None,
|
|
287
|
-
) -> Box:
|
|
258
|
+
box_result: bool = True,
|
|
259
|
+
agents: list[FlockAgent] | None = None,
|
|
260
|
+
) -> Box | dict:
|
|
288
261
|
"""Entry point for running an agent system synchronously."""
|
|
289
|
-
# Check if an event loop is already running
|
|
290
262
|
try:
|
|
291
263
|
loop = asyncio.get_running_loop()
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
264
|
+
# If loop exists, check if it's closed
|
|
265
|
+
if loop.is_closed():
|
|
266
|
+
raise RuntimeError("Event loop is closed")
|
|
267
|
+
except RuntimeError: # No running loop
|
|
295
268
|
loop = asyncio.new_event_loop()
|
|
296
269
|
asyncio.set_event_loop(loop)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
270
|
+
|
|
271
|
+
# Ensure the loop runs the task and handles closure if we created it
|
|
272
|
+
if asyncio.get_event_loop() is loop and not loop.is_running():
|
|
273
|
+
result = loop.run_until_complete(
|
|
274
|
+
self.run_async(
|
|
275
|
+
start_agent=start_agent,
|
|
276
|
+
input=input,
|
|
277
|
+
context=context,
|
|
278
|
+
run_id=run_id,
|
|
279
|
+
box_result=box_result,
|
|
280
|
+
agents=agents,
|
|
281
|
+
)
|
|
305
282
|
)
|
|
306
|
-
|
|
283
|
+
return result
|
|
284
|
+
else:
|
|
285
|
+
future = asyncio.ensure_future(
|
|
286
|
+
self.run_async(
|
|
287
|
+
start_agent=start_agent,
|
|
288
|
+
input=input,
|
|
289
|
+
context=context,
|
|
290
|
+
run_id=run_id,
|
|
291
|
+
box_result=box_result,
|
|
292
|
+
agents=agents,
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
return loop.run_until_complete(future)
|
|
307
296
|
|
|
308
297
|
async def run_async(
|
|
309
298
|
self,
|
|
@@ -311,11 +300,11 @@ class Flock(BaseModel, Serializable):
|
|
|
311
300
|
input: dict | None = None,
|
|
312
301
|
context: FlockContext | None = None,
|
|
313
302
|
run_id: str = "",
|
|
314
|
-
box_result: bool = True,
|
|
315
|
-
agents: list[FlockAgent] | None = None,
|
|
316
|
-
) -> Box:
|
|
303
|
+
box_result: bool = True,
|
|
304
|
+
agents: list[FlockAgent] | None = None,
|
|
305
|
+
) -> Box | dict:
|
|
317
306
|
"""Entry point for running an agent system asynchronously."""
|
|
318
|
-
#
|
|
307
|
+
# Import here to allow forward reference resolution
|
|
319
308
|
from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
|
|
320
309
|
|
|
321
310
|
with tracer.start_as_current_span("flock.run_async") as span:
|
|
@@ -323,9 +312,7 @@ class Flock(BaseModel, Serializable):
|
|
|
323
312
|
if agents:
|
|
324
313
|
for agent_obj in agents:
|
|
325
314
|
if isinstance(agent_obj, ConcreteFlockAgent):
|
|
326
|
-
self.add_agent(
|
|
327
|
-
agent_obj
|
|
328
|
-
) # Adds to self._agents and registry
|
|
315
|
+
self.add_agent(agent_obj)
|
|
329
316
|
else:
|
|
330
317
|
logger.warning(
|
|
331
318
|
f"Item in 'agents' list is not a FlockAgent: {type(agent_obj)}"
|
|
@@ -336,1054 +323,298 @@ class Flock(BaseModel, Serializable):
|
|
|
336
323
|
if isinstance(start_agent, ConcreteFlockAgent):
|
|
337
324
|
start_agent_name = start_agent.name
|
|
338
325
|
if start_agent_name not in self._agents:
|
|
339
|
-
self.add_agent(
|
|
340
|
-
start_agent
|
|
341
|
-
) # Add if instance was passed but not added
|
|
326
|
+
self.add_agent(start_agent)
|
|
342
327
|
elif isinstance(start_agent, str):
|
|
343
328
|
start_agent_name = start_agent
|
|
344
329
|
else:
|
|
345
|
-
start_agent_name =
|
|
346
|
-
self._start_agent_name
|
|
347
|
-
) # Use pre-configured if any
|
|
330
|
+
start_agent_name = self._start_agent_name
|
|
348
331
|
|
|
349
332
|
# Default to first agent if only one exists and none specified
|
|
350
333
|
if not start_agent_name and len(self._agents) == 1:
|
|
351
334
|
start_agent_name = list(self._agents.keys())[0]
|
|
352
335
|
elif not start_agent_name:
|
|
353
336
|
raise ValueError(
|
|
354
|
-
"No start_agent specified and multiple agents exist
|
|
337
|
+
"No start_agent specified and multiple/no agents exist."
|
|
355
338
|
)
|
|
356
339
|
|
|
357
|
-
# Get starting input
|
|
358
340
|
run_input = input if input is not None else self._start_input
|
|
341
|
+
effective_run_id = run_id or f"flockrun_{uuid.uuid4().hex[:8]}"
|
|
359
342
|
|
|
360
|
-
# Log and trace start info
|
|
361
343
|
span.set_attribute("start_agent", start_agent_name)
|
|
362
344
|
span.set_attribute("input", str(run_input))
|
|
363
|
-
span.set_attribute("run_id",
|
|
345
|
+
span.set_attribute("run_id", effective_run_id)
|
|
364
346
|
span.set_attribute("enable_temporal", self.enable_temporal)
|
|
365
347
|
logger.info(
|
|
366
|
-
f"Initiating Flock run. Start Agent: '{start_agent_name}'. Temporal: {self.enable_temporal}."
|
|
348
|
+
f"Initiating Flock run '{self.name}'. Start Agent: '{start_agent_name}'. Temporal: {self.enable_temporal}."
|
|
367
349
|
)
|
|
368
350
|
|
|
369
351
|
try:
|
|
370
|
-
# Resolve start agent instance from internal dict
|
|
371
352
|
resolved_start_agent = self._agents.get(start_agent_name)
|
|
372
353
|
if not resolved_start_agent:
|
|
373
|
-
# Maybe it's only in the global registry? (Less common)
|
|
374
354
|
resolved_start_agent = FlockRegistry.get_agent(
|
|
375
355
|
start_agent_name
|
|
376
356
|
)
|
|
377
357
|
if not resolved_start_agent:
|
|
378
358
|
raise ValueError(
|
|
379
|
-
f"Start agent '{start_agent_name}' not found
|
|
359
|
+
f"Start agent '{start_agent_name}' not found."
|
|
380
360
|
)
|
|
381
|
-
|
|
382
|
-
# If found globally, add it to this instance for consistency during run
|
|
383
|
-
self.add_agent(resolved_start_agent)
|
|
361
|
+
self.add_agent(resolved_start_agent)
|
|
384
362
|
|
|
385
|
-
# Create or use provided context
|
|
386
363
|
run_context = context if context else FlockContext()
|
|
387
|
-
|
|
388
|
-
run_id = f"flockrun_{uuid.uuid4().hex[:8]}"
|
|
389
|
-
set_baggage("run_id", run_id) # Ensure run_id is in baggage
|
|
364
|
+
set_baggage("run_id", effective_run_id)
|
|
390
365
|
|
|
391
|
-
# Initialize context
|
|
392
366
|
initialize_context(
|
|
393
367
|
run_context,
|
|
394
368
|
start_agent_name,
|
|
395
369
|
run_input,
|
|
396
|
-
|
|
370
|
+
effective_run_id,
|
|
397
371
|
not self.enable_temporal,
|
|
398
|
-
self.model
|
|
399
|
-
or resolved_start_agent.model
|
|
400
|
-
or "default-model-missing", # Pass effective model
|
|
372
|
+
self.model or resolved_start_agent.model or DEFAULT_MODEL,
|
|
401
373
|
)
|
|
374
|
+
# Add agent definitions to context for routing/serialization within workflow
|
|
375
|
+
for agent_name, agent_instance in self.agents.items():
|
|
376
|
+
# Agents already handle their serialization
|
|
377
|
+
agent_dict_repr = agent_instance.to_dict()
|
|
378
|
+
run_context.add_agent_definition(
|
|
379
|
+
agent_type=type(agent_instance),
|
|
380
|
+
agent_name=agent_name,
|
|
381
|
+
agent_data=agent_dict_repr, # Pass the serialized dict
|
|
382
|
+
)
|
|
402
383
|
|
|
403
|
-
# Execute workflow
|
|
404
384
|
logger.info(
|
|
405
385
|
"Starting agent execution",
|
|
406
386
|
agent=start_agent_name,
|
|
407
387
|
enable_temporal=self.enable_temporal,
|
|
408
388
|
)
|
|
409
389
|
|
|
390
|
+
# Execute workflow
|
|
410
391
|
if not self.enable_temporal:
|
|
411
392
|
result = await run_local_workflow(
|
|
412
393
|
run_context, box_result=False
|
|
413
|
-
)
|
|
394
|
+
)
|
|
414
395
|
else:
|
|
415
396
|
result = await run_temporal_workflow(
|
|
416
397
|
run_context, box_result=False
|
|
417
|
-
)
|
|
398
|
+
)
|
|
418
399
|
|
|
419
400
|
span.set_attribute("result.type", str(type(result)))
|
|
420
|
-
# Avoid overly large results in trace attributes
|
|
421
401
|
result_str = str(result)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
402
|
+
span.set_attribute(
|
|
403
|
+
"result.preview",
|
|
404
|
+
result_str[:1000]
|
|
405
|
+
+ ("..." if len(result_str) > 1000 else ""),
|
|
406
|
+
)
|
|
425
407
|
|
|
426
|
-
# Optionally box result before returning
|
|
427
408
|
if box_result:
|
|
428
409
|
try:
|
|
429
|
-
from box import Box
|
|
430
|
-
|
|
431
410
|
logger.debug("Boxing final result.")
|
|
432
411
|
return Box(result)
|
|
433
412
|
except ImportError:
|
|
434
413
|
logger.warning(
|
|
435
|
-
"Box library not installed, returning raw dict.
|
|
414
|
+
"Box library not installed, returning raw dict."
|
|
436
415
|
)
|
|
437
416
|
return result
|
|
438
417
|
else:
|
|
439
418
|
return result
|
|
440
419
|
|
|
441
420
|
except Exception as e:
|
|
442
|
-
logger.error(
|
|
421
|
+
logger.error(
|
|
422
|
+
f"Flock run '{self.name}' failed: {e}", exc_info=True
|
|
423
|
+
)
|
|
443
424
|
span.record_exception(e)
|
|
444
425
|
span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
|
|
445
|
-
# Depending on desired behavior, either raise or return an error dict
|
|
446
|
-
# raise # Option 1: Let the exception propagate
|
|
447
426
|
return {
|
|
448
427
|
"error": str(e),
|
|
449
|
-
"details": "Flock run failed.",
|
|
450
|
-
} # Option 2: Return error dict
|
|
451
|
-
|
|
452
|
-
# --- ADDED Serialization Methods ---
|
|
453
|
-
|
|
454
|
-
def to_dict(
|
|
455
|
-
self, path_type: Literal["absolute", "relative"] = "absolute"
|
|
456
|
-
) -> dict[str, Any]:
|
|
457
|
-
"""Convert Flock instance to dictionary representation.
|
|
458
|
-
|
|
459
|
-
Args:
|
|
460
|
-
path_type: How file paths should be formatted ('absolute' or 'relative')
|
|
461
|
-
"""
|
|
462
|
-
logger.debug("Serializing Flock instance to dict.")
|
|
463
|
-
# Use Pydantic's dump for base fields
|
|
464
|
-
data = self.model_dump(mode="json", exclude_none=True)
|
|
465
|
-
logger.info(
|
|
466
|
-
f"Serializing Flock '{self.name}' with {len(self._agents)} agents"
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
# Manually add serialized agents
|
|
470
|
-
data["agents"] = {}
|
|
471
|
-
|
|
472
|
-
# Track custom types used across all agents
|
|
473
|
-
custom_types = {}
|
|
474
|
-
# Track components used across all agents
|
|
475
|
-
components = {}
|
|
476
|
-
|
|
477
|
-
for name, agent_instance in self._agents.items():
|
|
478
|
-
try:
|
|
479
|
-
logger.debug(f"Serializing agent '{name}'")
|
|
480
|
-
# Agents handle their own serialization via their to_dict
|
|
481
|
-
agent_data = agent_instance.to_dict()
|
|
482
|
-
data["agents"][name] = agent_data
|
|
483
|
-
|
|
484
|
-
if agent_instance.input:
|
|
485
|
-
logger.debug(
|
|
486
|
-
f"Extracting type information from agent '{name}' input: {agent_instance.input}"
|
|
487
|
-
)
|
|
488
|
-
input_types = self._extract_types_from_signature(
|
|
489
|
-
agent_instance.input
|
|
490
|
-
)
|
|
491
|
-
if input_types:
|
|
492
|
-
logger.debug(
|
|
493
|
-
f"Found input types in agent '{name}': {input_types}"
|
|
494
|
-
)
|
|
495
|
-
custom_types.update(
|
|
496
|
-
self._get_type_definitions(input_types)
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
# Extract type information from agent outputs
|
|
500
|
-
if agent_instance.output:
|
|
501
|
-
logger.debug(
|
|
502
|
-
f"Extracting type information from agent '{name}' output: {agent_instance.output}"
|
|
503
|
-
)
|
|
504
|
-
output_types = self._extract_types_from_signature(
|
|
505
|
-
agent_instance.output
|
|
506
|
-
)
|
|
507
|
-
if output_types:
|
|
508
|
-
logger.debug(
|
|
509
|
-
f"Found output types in agent '{name}': {output_types}"
|
|
510
|
-
)
|
|
511
|
-
custom_types.update(
|
|
512
|
-
self._get_type_definitions(output_types)
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
# Extract component information
|
|
516
|
-
if (
|
|
517
|
-
"evaluator" in agent_data
|
|
518
|
-
and "type" in agent_data["evaluator"]
|
|
519
|
-
):
|
|
520
|
-
component_type = agent_data["evaluator"]["type"]
|
|
521
|
-
logger.debug(
|
|
522
|
-
f"Adding evaluator component '{component_type}' from agent '{name}'"
|
|
523
|
-
)
|
|
524
|
-
components[component_type] = self._get_component_definition(
|
|
525
|
-
component_type, path_type
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
# Extract module component information
|
|
529
|
-
if "modules" in agent_data:
|
|
530
|
-
for module_name, module_data in agent_data[
|
|
531
|
-
"modules"
|
|
532
|
-
].items():
|
|
533
|
-
if "type" in module_data:
|
|
534
|
-
component_type = module_data["type"]
|
|
535
|
-
logger.debug(
|
|
536
|
-
f"Adding module component '{component_type}' from module '{module_name}' in agent '{name}'"
|
|
537
|
-
)
|
|
538
|
-
components[component_type] = (
|
|
539
|
-
self._get_component_definition(
|
|
540
|
-
component_type, path_type
|
|
541
|
-
)
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
# Extract tool (callable) information
|
|
545
|
-
if agent_data.get("tools"):
|
|
546
|
-
logger.debug(
|
|
547
|
-
f"Extracting tool information from agent '{name}': {agent_data['tools']}"
|
|
548
|
-
)
|
|
549
|
-
# Get references to the actual tool objects
|
|
550
|
-
tool_objs = (
|
|
551
|
-
agent_instance.tools if agent_instance.tools else []
|
|
552
|
-
)
|
|
553
|
-
for i, tool_name in enumerate(agent_data["tools"]):
|
|
554
|
-
if i < len(tool_objs):
|
|
555
|
-
tool = tool_objs[i]
|
|
556
|
-
if callable(tool) and not isinstance(tool, type):
|
|
557
|
-
# Get the fully qualified name for registry lookup
|
|
558
|
-
path_str = (
|
|
559
|
-
get_registry().get_callable_path_string(
|
|
560
|
-
tool
|
|
561
|
-
)
|
|
562
|
-
)
|
|
563
|
-
if path_str:
|
|
564
|
-
logger.debug(
|
|
565
|
-
f"Adding tool '{tool_name}' (from path '{path_str}') to components"
|
|
566
|
-
)
|
|
567
|
-
# Add definition using just the function name as the key
|
|
568
|
-
components[tool_name] = (
|
|
569
|
-
self._get_callable_definition(
|
|
570
|
-
path_str, tool_name, path_type
|
|
571
|
-
)
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
except Exception as e:
|
|
575
|
-
logger.error(
|
|
576
|
-
f"Failed to serialize agent '{name}' within Flock: {e}",
|
|
577
|
-
exc_info=True,
|
|
578
|
-
)
|
|
579
|
-
# Optionally skip problematic agents or raise error
|
|
580
|
-
# data["agents"][name] = {"error": f"Serialization failed: {e}"}
|
|
581
|
-
|
|
582
|
-
# Add type definitions to the serialized output if any were found
|
|
583
|
-
if custom_types:
|
|
584
|
-
logger.info(
|
|
585
|
-
f"Adding {len(custom_types)} custom type definitions to serialized output"
|
|
586
|
-
)
|
|
587
|
-
data["types"] = custom_types
|
|
588
|
-
|
|
589
|
-
# Add component definitions to the serialized output if any were found
|
|
590
|
-
if components:
|
|
591
|
-
logger.info(
|
|
592
|
-
f"Adding {len(components)} component definitions to serialized output"
|
|
593
|
-
)
|
|
594
|
-
data["components"] = components
|
|
595
|
-
|
|
596
|
-
# Add dependencies section
|
|
597
|
-
data["dependencies"] = self._get_dependencies()
|
|
598
|
-
|
|
599
|
-
# Add serialization settings
|
|
600
|
-
data["metadata"] = {"path_type": path_type}
|
|
601
|
-
|
|
602
|
-
logger.debug(
|
|
603
|
-
f"Flock serialization complete with {len(data['agents'])} agents, {len(custom_types)} types, {len(components)} components"
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
return data
|
|
607
|
-
|
|
608
|
-
def _extract_types_from_signature(self, signature: str) -> list[str]:
|
|
609
|
-
"""Extract type names from an input/output signature string."""
|
|
610
|
-
if not signature:
|
|
611
|
-
return []
|
|
612
|
-
|
|
613
|
-
signature_parts = split_top_level(signature)
|
|
614
|
-
|
|
615
|
-
# Basic type extraction - handles simple cases like "result: TypeName" or "list[TypeName]"
|
|
616
|
-
custom_types = []
|
|
617
|
-
|
|
618
|
-
# Look for type annotations (everything after ":")
|
|
619
|
-
for part in signature_parts:
|
|
620
|
-
parts = part.split(":")
|
|
621
|
-
if len(parts) > 1:
|
|
622
|
-
type_part = parts[1].strip()
|
|
623
|
-
|
|
624
|
-
pydantic_models = extract_pydantic_models_from_type_string(
|
|
625
|
-
type_part
|
|
626
|
-
)
|
|
627
|
-
if pydantic_models:
|
|
628
|
-
for model in pydantic_models:
|
|
629
|
-
custom_types.append(model.__name__)
|
|
630
|
-
|
|
631
|
-
# # Extract from list[Type]
|
|
632
|
-
# if "list[" in type_part:
|
|
633
|
-
# inner_type = type_part.split("list[")[1].split("]")[0].strip()
|
|
634
|
-
# if inner_type and inner_type.lower() not in [
|
|
635
|
-
# "str",
|
|
636
|
-
# "int",
|
|
637
|
-
# "float",
|
|
638
|
-
# "bool",
|
|
639
|
-
# "dict",
|
|
640
|
-
# "list",
|
|
641
|
-
# ]:
|
|
642
|
-
# custom_types.append(inner_type)
|
|
643
|
-
|
|
644
|
-
# # Extract direct type references
|
|
645
|
-
# elif type_part and type_part.lower() not in [
|
|
646
|
-
# "str",
|
|
647
|
-
# "int",
|
|
648
|
-
# "float",
|
|
649
|
-
# "bool",
|
|
650
|
-
# "dict",
|
|
651
|
-
# "list",
|
|
652
|
-
# ]:
|
|
653
|
-
# custom_types.append(
|
|
654
|
-
# type_part.split()[0]
|
|
655
|
-
# ) # Take the first word in case there's a description
|
|
656
|
-
|
|
657
|
-
return custom_types
|
|
658
|
-
|
|
659
|
-
def _get_type_definitions(self, type_names: list[str]) -> dict[str, Any]:
|
|
660
|
-
"""Get definitions for the specified custom types."""
|
|
661
|
-
from flock.core.flock_registry import get_registry
|
|
662
|
-
|
|
663
|
-
type_definitions = {}
|
|
664
|
-
registry = get_registry()
|
|
665
|
-
|
|
666
|
-
for type_name in type_names:
|
|
667
|
-
try:
|
|
668
|
-
# Try to get the type from registry
|
|
669
|
-
type_obj = registry._types.get(type_name)
|
|
670
|
-
if type_obj:
|
|
671
|
-
type_def = self._extract_type_definition(
|
|
672
|
-
type_name, type_obj
|
|
673
|
-
)
|
|
674
|
-
if type_def:
|
|
675
|
-
type_definitions[type_name] = type_def
|
|
676
|
-
except Exception as e:
|
|
677
|
-
logger.warning(
|
|
678
|
-
f"Could not extract definition for type {type_name}: {e}"
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
return type_definitions
|
|
682
|
-
|
|
683
|
-
def _extract_type_definition(
|
|
684
|
-
self, type_name: str, type_obj: type
|
|
685
|
-
) -> dict[str, Any]:
|
|
686
|
-
"""Extract a definition for a custom type."""
|
|
687
|
-
import inspect
|
|
688
|
-
from dataclasses import is_dataclass
|
|
689
|
-
|
|
690
|
-
type_def = {
|
|
691
|
-
"module_path": type_obj.__module__,
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
# Handle Pydantic models
|
|
695
|
-
if hasattr(type_obj, "model_json_schema") and callable(
|
|
696
|
-
getattr(type_obj, "model_json_schema")
|
|
697
|
-
):
|
|
698
|
-
type_def["type"] = "pydantic.BaseModel"
|
|
699
|
-
try:
|
|
700
|
-
schema = type_obj.model_json_schema()
|
|
701
|
-
# Clean up schema to remove unnecessary fields
|
|
702
|
-
if "title" in schema and schema["title"] == type_name:
|
|
703
|
-
del schema["title"]
|
|
704
|
-
type_def["schema"] = schema
|
|
705
|
-
except Exception as e:
|
|
706
|
-
logger.warning(
|
|
707
|
-
f"Could not extract schema for Pydantic model {type_name}: {e}"
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
# Handle dataclasses
|
|
711
|
-
elif is_dataclass(type_obj):
|
|
712
|
-
type_def["type"] = "dataclass"
|
|
713
|
-
fields = {}
|
|
714
|
-
for field_name, field in type_obj.__dataclass_fields__.items():
|
|
715
|
-
fields[field_name] = {
|
|
716
|
-
"type": str(field.type),
|
|
717
|
-
"default": str(field.default)
|
|
718
|
-
if field.default is not inspect.Parameter.empty
|
|
719
|
-
else None,
|
|
720
|
-
}
|
|
721
|
-
type_def["fields"] = fields
|
|
722
|
-
|
|
723
|
-
# Handle other types - just store basic information
|
|
724
|
-
else:
|
|
725
|
-
type_def["type"] = "custom"
|
|
726
|
-
|
|
727
|
-
# Extract import statement (simplified version)
|
|
728
|
-
type_def["imports"] = [f"from {type_obj.__module__} import {type_name}"]
|
|
729
|
-
|
|
730
|
-
return type_def
|
|
731
|
-
|
|
732
|
-
def _get_component_definition(
|
|
733
|
-
self, component_type: str, path_type: Literal["absolute", "relative"]
|
|
734
|
-
) -> dict[str, Any]:
|
|
735
|
-
"""Get definition for a component type."""
|
|
736
|
-
import os
|
|
737
|
-
import sys
|
|
738
|
-
|
|
739
|
-
from flock.core.flock_registry import get_registry
|
|
740
|
-
|
|
741
|
-
registry = get_registry()
|
|
742
|
-
component_def = {}
|
|
743
|
-
|
|
744
|
-
try:
|
|
745
|
-
# Try to get the component class from registry
|
|
746
|
-
component_class = registry._components.get(component_type)
|
|
747
|
-
if component_class:
|
|
748
|
-
# Get the standard module path
|
|
749
|
-
module_path = component_class.__module__
|
|
750
|
-
|
|
751
|
-
# Get the actual file system path if possible
|
|
752
|
-
file_path = None
|
|
753
|
-
try:
|
|
754
|
-
if (
|
|
755
|
-
hasattr(component_class, "__module__")
|
|
756
|
-
and component_class.__module__
|
|
757
|
-
):
|
|
758
|
-
module = sys.modules.get(component_class.__module__)
|
|
759
|
-
if module and hasattr(module, "__file__"):
|
|
760
|
-
file_path = os.path.abspath(module.__file__)
|
|
761
|
-
# Convert to relative path if needed
|
|
762
|
-
if path_type == "relative" and file_path:
|
|
763
|
-
try:
|
|
764
|
-
file_path = os.path.relpath(file_path)
|
|
765
|
-
except ValueError:
|
|
766
|
-
# Keep as absolute if can't make relative
|
|
767
|
-
logger.warning(
|
|
768
|
-
f"Could not convert path to relative: {file_path}"
|
|
769
|
-
)
|
|
770
|
-
except Exception as e:
|
|
771
|
-
# If we can't get the file path, we'll just use the module path
|
|
772
|
-
logger.warning(
|
|
773
|
-
f"Error getting file path for component {component_type}: {e}"
|
|
774
|
-
)
|
|
775
|
-
pass
|
|
776
|
-
|
|
777
|
-
component_def = {
|
|
778
|
-
"type": "flock_component",
|
|
779
|
-
"module_path": module_path,
|
|
780
|
-
"file_path": file_path, # Include actual file system path
|
|
781
|
-
"description": getattr(
|
|
782
|
-
component_class, "__doc__", ""
|
|
783
|
-
).strip()
|
|
784
|
-
or f"{component_type} component",
|
|
428
|
+
"details": f"Flock run '{self.name}' failed.",
|
|
785
429
|
}
|
|
786
|
-
except Exception as e:
|
|
787
|
-
logger.warning(
|
|
788
|
-
f"Could not extract definition for component {component_type}: {e}"
|
|
789
|
-
)
|
|
790
|
-
# Provide minimal information if we can't extract details
|
|
791
|
-
component_def = {
|
|
792
|
-
"type": "flock_component",
|
|
793
|
-
"module_path": "unknown",
|
|
794
|
-
"file_path": None,
|
|
795
|
-
"description": f"{component_type} component (definition incomplete)",
|
|
796
|
-
}
|
|
797
430
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
def _get_callable_definition(
|
|
431
|
+
# --- Batch Processing (Delegation) ---
|
|
432
|
+
async def run_batch_async(
|
|
801
433
|
self,
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
434
|
+
start_agent: FlockAgent | str,
|
|
435
|
+
batch_inputs: list[dict[str, Any]] | DataFrame | str,
|
|
436
|
+
input_mapping: dict[str, str] | None = None,
|
|
437
|
+
static_inputs: dict[str, Any] | None = None,
|
|
438
|
+
parallel: bool = True,
|
|
439
|
+
max_workers: int = 5,
|
|
440
|
+
use_temporal: bool | None = None,
|
|
441
|
+
box_results: bool = True,
|
|
442
|
+
return_errors: bool = False,
|
|
443
|
+
silent_mode: bool = False,
|
|
444
|
+
write_to_csv: str | None = None,
|
|
445
|
+
) -> list[Box | dict | None | Exception]:
|
|
446
|
+
"""Runs the specified agent/workflow for each item in a batch asynchronously (delegated)."""
|
|
447
|
+
# Import processor locally
|
|
448
|
+
from flock.core.execution.batch_executor import BatchProcessor
|
|
449
|
+
|
|
450
|
+
processor = BatchProcessor(self) # Pass self
|
|
451
|
+
return await processor.run_batch_async(
|
|
452
|
+
start_agent=start_agent,
|
|
453
|
+
batch_inputs=batch_inputs,
|
|
454
|
+
input_mapping=input_mapping,
|
|
455
|
+
static_inputs=static_inputs,
|
|
456
|
+
parallel=parallel,
|
|
457
|
+
max_workers=max_workers,
|
|
458
|
+
use_temporal=use_temporal,
|
|
459
|
+
box_results=box_results,
|
|
460
|
+
return_errors=return_errors,
|
|
461
|
+
silent_mode=silent_mode,
|
|
462
|
+
write_to_csv=write_to_csv,
|
|
463
|
+
)
|
|
821
464
|
|
|
465
|
+
def run_batch(
|
|
466
|
+
self,
|
|
467
|
+
start_agent: FlockAgent | str,
|
|
468
|
+
batch_inputs: list[dict[str, Any]] | DataFrame | str,
|
|
469
|
+
input_mapping: dict[str, str] | None = None,
|
|
470
|
+
static_inputs: dict[str, Any] | None = None,
|
|
471
|
+
parallel: bool = True,
|
|
472
|
+
max_workers: int = 5,
|
|
473
|
+
use_temporal: bool | None = None,
|
|
474
|
+
box_results: bool = True,
|
|
475
|
+
return_errors: bool = False,
|
|
476
|
+
silent_mode: bool = False,
|
|
477
|
+
write_to_csv: str | None = None,
|
|
478
|
+
) -> list[Box | dict | None | Exception]:
|
|
479
|
+
"""Synchronous wrapper for run_batch_async."""
|
|
480
|
+
# (Standard asyncio run wrapper logic)
|
|
822
481
|
try:
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
# Get the standard module path
|
|
830
|
-
module_path = func.__module__
|
|
831
|
-
|
|
832
|
-
# Get the actual file system path if possible
|
|
833
|
-
file_path = None
|
|
834
|
-
try:
|
|
835
|
-
if func.__module__ and func.__module__ != "builtins":
|
|
836
|
-
module = sys.modules.get(func.__module__)
|
|
837
|
-
if module and hasattr(module, "__file__"):
|
|
838
|
-
file_path = os.path.abspath(module.__file__)
|
|
839
|
-
# Convert to relative path if needed
|
|
840
|
-
if path_type == "relative" and file_path:
|
|
841
|
-
try:
|
|
842
|
-
file_path = os.path.relpath(file_path)
|
|
843
|
-
except ValueError:
|
|
844
|
-
# Keep as absolute if can't make relative
|
|
845
|
-
logger.warning(
|
|
846
|
-
f"Could not convert path to relative: {file_path}"
|
|
847
|
-
)
|
|
848
|
-
except Exception as e:
|
|
849
|
-
# If we can't get the file path, just use the module path
|
|
850
|
-
logger.warning(
|
|
851
|
-
f"Error getting file path for callable {callable_ref}: {e}"
|
|
852
|
-
)
|
|
853
|
-
pass
|
|
482
|
+
loop = asyncio.get_running_loop()
|
|
483
|
+
if loop.is_closed():
|
|
484
|
+
raise RuntimeError("Event loop is closed")
|
|
485
|
+
except RuntimeError: # No running loop
|
|
486
|
+
loop = asyncio.new_event_loop()
|
|
487
|
+
asyncio.set_event_loop(loop)
|
|
854
488
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
489
|
+
coro = self.run_batch_async(
|
|
490
|
+
start_agent=start_agent,
|
|
491
|
+
batch_inputs=batch_inputs,
|
|
492
|
+
input_mapping=input_mapping,
|
|
493
|
+
static_inputs=static_inputs,
|
|
494
|
+
parallel=parallel,
|
|
495
|
+
max_workers=max_workers,
|
|
496
|
+
use_temporal=use_temporal,
|
|
497
|
+
box_results=box_results,
|
|
498
|
+
return_errors=return_errors,
|
|
499
|
+
silent_mode=silent_mode,
|
|
500
|
+
write_to_csv=write_to_csv,
|
|
501
|
+
)
|
|
859
502
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
logger.debug(
|
|
867
|
-
f"Created callable definition for '{func_name}': module={module_path}, file={file_path}"
|
|
868
|
-
)
|
|
869
|
-
except Exception as e:
|
|
870
|
-
logger.warning(
|
|
871
|
-
f"Could not extract definition for callable {callable_ref}: {e}"
|
|
872
|
-
)
|
|
873
|
-
# Provide minimal information
|
|
874
|
-
callable_def = {
|
|
875
|
-
"type": "flock_callable",
|
|
876
|
-
"module_path": callable_ref.split(".")[0]
|
|
877
|
-
if "." in callable_ref
|
|
878
|
-
else "unknown",
|
|
879
|
-
"file_path": None,
|
|
880
|
-
"description": f"Callable {func_name} (definition incomplete)",
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
return callable_def
|
|
884
|
-
|
|
885
|
-
def _get_dependencies(self) -> list[str]:
|
|
886
|
-
"""Get list of dependencies required by this Flock."""
|
|
887
|
-
# This is a simplified version - in production, you might want to detect
|
|
888
|
-
# actual versions of installed packages
|
|
889
|
-
return [
|
|
890
|
-
"pydantic>=2.0.0",
|
|
891
|
-
"flock>=0.3.41", # Assuming this is the package name
|
|
892
|
-
]
|
|
503
|
+
if asyncio.get_event_loop() is loop and not loop.is_running():
|
|
504
|
+
results = loop.run_until_complete(coro)
|
|
505
|
+
return results
|
|
506
|
+
else:
|
|
507
|
+
future = asyncio.ensure_future(coro)
|
|
508
|
+
return loop.run_until_complete(future)
|
|
893
509
|
|
|
894
|
-
|
|
895
|
-
def
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
510
|
+
# --- Evaluation (Delegation) ---
|
|
511
|
+
async def evaluate_async(
|
|
512
|
+
self,
|
|
513
|
+
dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset,
|
|
514
|
+
start_agent: FlockAgent | str,
|
|
515
|
+
input_mapping: dict[str, str],
|
|
516
|
+
answer_mapping: dict[str, str],
|
|
517
|
+
metrics: list[
|
|
518
|
+
str
|
|
519
|
+
| Callable[[Any, Any], bool | float | dict[str, Any]]
|
|
520
|
+
| FlockAgent
|
|
521
|
+
| FlockEvaluator
|
|
522
|
+
],
|
|
523
|
+
metric_configs: dict[str, dict[str, Any]] | None = None,
|
|
524
|
+
static_inputs: dict[str, Any] | None = None,
|
|
525
|
+
parallel: bool = True,
|
|
526
|
+
max_workers: int = 5,
|
|
527
|
+
use_temporal: bool | None = None,
|
|
528
|
+
error_handling: Literal["raise", "skip", "log"] = "log",
|
|
529
|
+
output_file: str | Path | None = None,
|
|
530
|
+
return_dataframe: bool = True,
|
|
531
|
+
silent_mode: bool = False,
|
|
532
|
+
metadata_columns: list[str] | None = None,
|
|
533
|
+
) -> DataFrame | list[dict[str, Any]]:
|
|
534
|
+
"""Evaluates the Flock's performance against a dataset (delegated)."""
|
|
535
|
+
# Import processor locally
|
|
536
|
+
from flock.core.execution.evaluation_executor import (
|
|
537
|
+
EvaluationExecutor,
|
|
899
538
|
)
|
|
900
539
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
540
|
+
processor = EvaluationExecutor(self) # Pass self
|
|
541
|
+
return await processor.evaluate_async(
|
|
542
|
+
dataset=dataset,
|
|
543
|
+
start_agent=start_agent,
|
|
544
|
+
input_mapping=input_mapping,
|
|
545
|
+
answer_mapping=answer_mapping,
|
|
546
|
+
metrics=metrics,
|
|
547
|
+
metric_configs=metric_configs,
|
|
548
|
+
static_inputs=static_inputs,
|
|
549
|
+
parallel=parallel,
|
|
550
|
+
max_workers=max_workers,
|
|
551
|
+
use_temporal=use_temporal,
|
|
552
|
+
error_handling=error_handling,
|
|
553
|
+
output_file=output_file,
|
|
554
|
+
return_dataframe=return_dataframe,
|
|
555
|
+
silent_mode=silent_mode,
|
|
556
|
+
metadata_columns=metadata_columns,
|
|
906
557
|
)
|
|
907
558
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
# Extract agent data before initializing Flock base model
|
|
935
|
-
agents_data = data.pop("agents", {})
|
|
936
|
-
logger.info(f"Found {len(agents_data)} agents to deserialize")
|
|
937
|
-
|
|
938
|
-
# Remove types, components, and dependencies sections as they're not part of Flock fields
|
|
939
|
-
data.pop("types", None)
|
|
940
|
-
data.pop("components", None)
|
|
941
|
-
data.pop("dependencies", None)
|
|
942
|
-
# Remove metadata if present
|
|
943
|
-
data.pop("metadata", None)
|
|
944
|
-
|
|
945
|
-
# Create Flock instance using Pydantic constructor for basic fields
|
|
559
|
+
def evaluate(
|
|
560
|
+
self,
|
|
561
|
+
dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset,
|
|
562
|
+
start_agent: FlockAgent | str,
|
|
563
|
+
input_mapping: dict[str, str],
|
|
564
|
+
answer_mapping: dict[str, str],
|
|
565
|
+
metrics: list[
|
|
566
|
+
str
|
|
567
|
+
| Callable[[Any, Any], bool | float | dict[str, Any]]
|
|
568
|
+
| FlockAgent
|
|
569
|
+
| FlockEvaluator
|
|
570
|
+
],
|
|
571
|
+
metric_configs: dict[str, dict[str, Any]] | None = None,
|
|
572
|
+
static_inputs: dict[str, Any] | None = None,
|
|
573
|
+
parallel: bool = True,
|
|
574
|
+
max_workers: int = 5,
|
|
575
|
+
use_temporal: bool | None = None,
|
|
576
|
+
error_handling: Literal["raise", "skip", "log"] = "log",
|
|
577
|
+
output_file: str | Path | None = None,
|
|
578
|
+
return_dataframe: bool = True,
|
|
579
|
+
silent_mode: bool = False,
|
|
580
|
+
metadata_columns: list[str] | None = None,
|
|
581
|
+
) -> DataFrame | list[dict[str, Any]]:
|
|
582
|
+
"""Synchronous wrapper for evaluate_async."""
|
|
583
|
+
# (Standard asyncio run wrapper logic)
|
|
946
584
|
try:
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
)
|
|
952
|
-
|
|
953
|
-
except Exception as e:
|
|
954
|
-
logger.error(
|
|
955
|
-
f"Pydantic validation/init failed for Flock: {e}", exc_info=True
|
|
956
|
-
)
|
|
957
|
-
raise ValueError(
|
|
958
|
-
f"Failed to initialize Flock from dict: {e}"
|
|
959
|
-
) from e
|
|
960
|
-
|
|
961
|
-
# Deserialize and add agents AFTER Flock instance exists
|
|
962
|
-
for name, agent_data in agents_data.items():
|
|
963
|
-
try:
|
|
964
|
-
logger.debug(f"Deserializing agent '{name}'")
|
|
965
|
-
# Ensure agent_data has the name, or add it from the key
|
|
966
|
-
agent_data.setdefault("name", name)
|
|
967
|
-
# Use FlockAgent's from_dict method
|
|
968
|
-
agent_instance = ConcreteFlockAgent.from_dict(agent_data)
|
|
969
|
-
flock_instance.add_agent(
|
|
970
|
-
agent_instance
|
|
971
|
-
) # Adds to _agents and registers
|
|
972
|
-
logger.debug(f"Successfully added agent '{name}' to Flock")
|
|
973
|
-
except Exception as e:
|
|
974
|
-
logger.error(
|
|
975
|
-
f"Failed to deserialize or add agent '{name}' during Flock deserialization: {e}",
|
|
976
|
-
exc_info=True,
|
|
977
|
-
)
|
|
978
|
-
# Decide: skip agent or raise error?
|
|
585
|
+
loop = asyncio.get_running_loop()
|
|
586
|
+
if loop.is_closed():
|
|
587
|
+
raise RuntimeError("Event loop is closed")
|
|
588
|
+
except RuntimeError: # No running loop
|
|
589
|
+
loop = asyncio.new_event_loop()
|
|
590
|
+
asyncio.set_event_loop(loop)
|
|
979
591
|
|
|
980
|
-
|
|
981
|
-
|
|
592
|
+
coro = self.evaluate_async(
|
|
593
|
+
dataset=dataset,
|
|
594
|
+
start_agent=start_agent,
|
|
595
|
+
input_mapping=input_mapping,
|
|
596
|
+
answer_mapping=answer_mapping,
|
|
597
|
+
metrics=metrics,
|
|
598
|
+
metric_configs=metric_configs,
|
|
599
|
+
static_inputs=static_inputs,
|
|
600
|
+
parallel=parallel,
|
|
601
|
+
max_workers=max_workers,
|
|
602
|
+
use_temporal=use_temporal,
|
|
603
|
+
error_handling=error_handling,
|
|
604
|
+
output_file=output_file,
|
|
605
|
+
return_dataframe=return_dataframe,
|
|
606
|
+
silent_mode=silent_mode,
|
|
607
|
+
metadata_columns=metadata_columns,
|
|
982
608
|
)
|
|
983
|
-
return flock_instance
|
|
984
|
-
|
|
985
|
-
@classmethod
|
|
986
|
-
def _register_type_definitions(cls, type_defs: dict[str, Any]) -> None:
|
|
987
|
-
"""Register type definitions from serialized data."""
|
|
988
|
-
import importlib
|
|
989
|
-
|
|
990
|
-
from flock.core.flock_registry import get_registry
|
|
991
|
-
|
|
992
|
-
registry = get_registry()
|
|
993
|
-
|
|
994
|
-
for type_name, type_def in type_defs.items():
|
|
995
|
-
logger.debug(f"Registering type: {type_name}")
|
|
996
|
-
|
|
997
|
-
try:
|
|
998
|
-
# First try to import the type directly
|
|
999
|
-
module_path = type_def.get("module_path")
|
|
1000
|
-
if module_path:
|
|
1001
|
-
try:
|
|
1002
|
-
module = importlib.import_module(module_path)
|
|
1003
|
-
if hasattr(module, type_name):
|
|
1004
|
-
type_obj = getattr(module, type_name)
|
|
1005
|
-
registry.register_type(type_obj, type_name)
|
|
1006
|
-
logger.info(
|
|
1007
|
-
f"Registered type {type_name} from module {module_path}"
|
|
1008
|
-
)
|
|
1009
|
-
continue
|
|
1010
|
-
except ImportError:
|
|
1011
|
-
logger.debug(
|
|
1012
|
-
f"Could not import {module_path}, trying dynamic type creation"
|
|
1013
|
-
)
|
|
1014
|
-
|
|
1015
|
-
# If direct import fails, try to create the type dynamically
|
|
1016
|
-
if (
|
|
1017
|
-
type_def.get("type") == "pydantic.BaseModel"
|
|
1018
|
-
and "schema" in type_def
|
|
1019
|
-
):
|
|
1020
|
-
cls._create_pydantic_model(type_name, type_def)
|
|
1021
|
-
elif (
|
|
1022
|
-
type_def.get("type") == "dataclass" and "fields" in type_def
|
|
1023
|
-
):
|
|
1024
|
-
cls._create_dataclass(type_name, type_def)
|
|
1025
|
-
else:
|
|
1026
|
-
logger.warning(
|
|
1027
|
-
f"Unsupported type definition for {type_name}, type: {type_def.get('type')}"
|
|
1028
|
-
)
|
|
1029
|
-
|
|
1030
|
-
except Exception as e:
|
|
1031
|
-
logger.error(f"Failed to register type {type_name}: {e}")
|
|
1032
|
-
|
|
1033
|
-
@classmethod
|
|
1034
|
-
def _create_pydantic_model(
|
|
1035
|
-
cls, type_name: str, type_def: dict[str, Any]
|
|
1036
|
-
) -> None:
|
|
1037
|
-
"""Dynamically create a Pydantic model from a schema definition."""
|
|
1038
|
-
from pydantic import create_model
|
|
1039
|
-
|
|
1040
|
-
from flock.core.flock_registry import get_registry
|
|
1041
|
-
|
|
1042
|
-
registry = get_registry()
|
|
1043
|
-
schema = type_def.get("schema", {})
|
|
1044
|
-
|
|
1045
|
-
try:
|
|
1046
|
-
# Extract field definitions from schema
|
|
1047
|
-
fields = {}
|
|
1048
|
-
properties = schema.get("properties", {})
|
|
1049
|
-
required = schema.get("required", [])
|
|
1050
|
-
|
|
1051
|
-
for field_name, field_schema in properties.items():
|
|
1052
|
-
# Determine the field type based on schema
|
|
1053
|
-
field_type = cls._get_type_from_schema(field_schema)
|
|
1054
|
-
|
|
1055
|
-
# Determine if field is required
|
|
1056
|
-
default = ... if field_name in required else None
|
|
1057
|
-
|
|
1058
|
-
# Add to fields dict
|
|
1059
|
-
fields[field_name] = (field_type, default)
|
|
1060
|
-
|
|
1061
|
-
# Create the model
|
|
1062
|
-
DynamicModel = create_model(type_name, **fields)
|
|
1063
|
-
|
|
1064
|
-
# Register it
|
|
1065
|
-
registry.register_type(DynamicModel, type_name)
|
|
1066
|
-
logger.info(f"Created and registered Pydantic model: {type_name}")
|
|
1067
|
-
|
|
1068
|
-
except Exception as e:
|
|
1069
|
-
logger.error(f"Failed to create Pydantic model {type_name}: {e}")
|
|
1070
609
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
type_mapping = {
|
|
1078
|
-
"string": str,
|
|
1079
|
-
"integer": int,
|
|
1080
|
-
"number": float,
|
|
1081
|
-
"boolean": bool,
|
|
1082
|
-
"array": list,
|
|
1083
|
-
"object": dict,
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
# Handle basic types
|
|
1087
|
-
if schema_type in type_mapping:
|
|
1088
|
-
return type_mapping[schema_type]
|
|
1089
|
-
|
|
1090
|
-
# Handle enums
|
|
1091
|
-
if "enum" in field_schema:
|
|
1092
|
-
from typing import Literal
|
|
1093
|
-
|
|
1094
|
-
return Literal[tuple(field_schema["enum"])]
|
|
1095
|
-
|
|
1096
|
-
# Default
|
|
1097
|
-
return Any
|
|
1098
|
-
|
|
1099
|
-
@classmethod
|
|
1100
|
-
def _create_dataclass(
|
|
1101
|
-
cls, type_name: str, type_def: dict[str, Any]
|
|
1102
|
-
) -> None:
|
|
1103
|
-
"""Dynamically create a dataclass from a field definition."""
|
|
1104
|
-
from dataclasses import make_dataclass
|
|
1105
|
-
|
|
1106
|
-
from flock.core.flock_registry import get_registry
|
|
1107
|
-
|
|
1108
|
-
registry = get_registry()
|
|
1109
|
-
fields_def = type_def.get("fields", {})
|
|
1110
|
-
|
|
1111
|
-
try:
|
|
1112
|
-
fields = []
|
|
1113
|
-
for field_name, field_props in fields_def.items():
|
|
1114
|
-
field_type = eval(
|
|
1115
|
-
field_props.get("type", "str")
|
|
1116
|
-
) # Note: eval is used here for simplicity
|
|
1117
|
-
fields.append((field_name, field_type))
|
|
1118
|
-
|
|
1119
|
-
# Create the dataclass
|
|
1120
|
-
DynamicDataclass = make_dataclass(type_name, fields)
|
|
1121
|
-
|
|
1122
|
-
# Register it
|
|
1123
|
-
registry.register_type(DynamicDataclass, type_name)
|
|
1124
|
-
logger.info(f"Created and registered dataclass: {type_name}")
|
|
1125
|
-
|
|
1126
|
-
except Exception as e:
|
|
1127
|
-
logger.error(f"Failed to create dataclass {type_name}: {e}")
|
|
1128
|
-
|
|
1129
|
-
@classmethod
|
|
1130
|
-
def _register_component_definitions(
|
|
1131
|
-
cls,
|
|
1132
|
-
component_defs: dict[str, Any],
|
|
1133
|
-
path_type: Literal["absolute", "relative"],
|
|
1134
|
-
) -> None:
|
|
1135
|
-
"""Register component definitions from serialized data."""
|
|
1136
|
-
import importlib
|
|
1137
|
-
import importlib.util
|
|
1138
|
-
import os
|
|
1139
|
-
import sys
|
|
1140
|
-
|
|
1141
|
-
from flock.core.flock_registry import get_registry
|
|
1142
|
-
|
|
1143
|
-
registry = get_registry()
|
|
1144
|
-
|
|
1145
|
-
for component_name, component_def in component_defs.items():
|
|
1146
|
-
logger.debug(f"Registering component: {component_name}")
|
|
1147
|
-
component_type = component_def.get("type", "flock_component")
|
|
1148
|
-
|
|
1149
|
-
try:
|
|
1150
|
-
# Handle callables differently than components
|
|
1151
|
-
if component_type == "flock_callable":
|
|
1152
|
-
# For callables, component_name is just the function name
|
|
1153
|
-
func_name = component_name
|
|
1154
|
-
module_path = component_def.get("module_path")
|
|
1155
|
-
file_path = component_def.get("file_path")
|
|
1156
|
-
|
|
1157
|
-
# Convert relative path to absolute if needed
|
|
1158
|
-
if (
|
|
1159
|
-
path_type == "relative"
|
|
1160
|
-
and file_path
|
|
1161
|
-
and not os.path.isabs(file_path)
|
|
1162
|
-
):
|
|
1163
|
-
try:
|
|
1164
|
-
# Make absolute based on current directory
|
|
1165
|
-
file_path = os.path.abspath(file_path)
|
|
1166
|
-
logger.debug(
|
|
1167
|
-
f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
|
|
1168
|
-
)
|
|
1169
|
-
except Exception as e:
|
|
1170
|
-
logger.warning(
|
|
1171
|
-
f"Could not convert relative path to absolute: {e}"
|
|
1172
|
-
)
|
|
1173
|
-
|
|
1174
|
-
logger.debug(
|
|
1175
|
-
f"Processing callable '{func_name}' from module '{module_path}', file: {file_path}"
|
|
1176
|
-
)
|
|
1177
|
-
|
|
1178
|
-
# Try direct import first
|
|
1179
|
-
if module_path:
|
|
1180
|
-
try:
|
|
1181
|
-
logger.debug(
|
|
1182
|
-
f"Attempting to import module: {module_path}"
|
|
1183
|
-
)
|
|
1184
|
-
module = importlib.import_module(module_path)
|
|
1185
|
-
if hasattr(module, func_name):
|
|
1186
|
-
callable_obj = getattr(module, func_name)
|
|
1187
|
-
# Register with just the name for easier lookup
|
|
1188
|
-
registry.register_callable(
|
|
1189
|
-
callable_obj, func_name
|
|
1190
|
-
)
|
|
1191
|
-
logger.info(
|
|
1192
|
-
f"Registered callable with name: {func_name}"
|
|
1193
|
-
)
|
|
1194
|
-
# Also register with fully qualified path for compatibility
|
|
1195
|
-
if module_path != "__main__":
|
|
1196
|
-
full_path = f"{module_path}.{func_name}"
|
|
1197
|
-
registry.register_callable(
|
|
1198
|
-
callable_obj, full_path
|
|
1199
|
-
)
|
|
1200
|
-
logger.info(
|
|
1201
|
-
f"Also registered callable with full path: {full_path}"
|
|
1202
|
-
)
|
|
1203
|
-
logger.info(
|
|
1204
|
-
f"Successfully registered callable {func_name} from module {module_path}"
|
|
1205
|
-
)
|
|
1206
|
-
continue
|
|
1207
|
-
else:
|
|
1208
|
-
logger.warning(
|
|
1209
|
-
f"Function '{func_name}' not found in module {module_path}"
|
|
1210
|
-
)
|
|
1211
|
-
except ImportError:
|
|
1212
|
-
logger.debug(
|
|
1213
|
-
f"Could not import module {module_path}, trying file path"
|
|
1214
|
-
)
|
|
1215
|
-
|
|
1216
|
-
# Try file path if module import fails
|
|
1217
|
-
if file_path and os.path.exists(file_path):
|
|
1218
|
-
try:
|
|
1219
|
-
logger.debug(
|
|
1220
|
-
f"Attempting to load file: {file_path}"
|
|
1221
|
-
)
|
|
1222
|
-
# Create a module name from file path
|
|
1223
|
-
mod_name = f"{func_name}_module"
|
|
1224
|
-
spec = importlib.util.spec_from_file_location(
|
|
1225
|
-
mod_name, file_path
|
|
1226
|
-
)
|
|
1227
|
-
if spec and spec.loader:
|
|
1228
|
-
module = importlib.util.module_from_spec(spec)
|
|
1229
|
-
sys.modules[spec.name] = module
|
|
1230
|
-
spec.loader.exec_module(module)
|
|
1231
|
-
logger.debug(
|
|
1232
|
-
f"Successfully loaded module from file, searching for function '{func_name}'"
|
|
1233
|
-
)
|
|
1234
|
-
|
|
1235
|
-
# Look for the function in the loaded module
|
|
1236
|
-
if hasattr(module, func_name):
|
|
1237
|
-
callable_obj = getattr(module, func_name)
|
|
1238
|
-
registry.register_callable(
|
|
1239
|
-
callable_obj, func_name
|
|
1240
|
-
)
|
|
1241
|
-
logger.info(
|
|
1242
|
-
f"Successfully registered callable {func_name} from file {file_path}"
|
|
1243
|
-
)
|
|
1244
|
-
else:
|
|
1245
|
-
logger.warning(
|
|
1246
|
-
f"Function {func_name} not found in file {file_path}"
|
|
1247
|
-
)
|
|
1248
|
-
else:
|
|
1249
|
-
logger.warning(
|
|
1250
|
-
f"Could not create import spec for {file_path}"
|
|
1251
|
-
)
|
|
1252
|
-
except Exception as e:
|
|
1253
|
-
logger.error(
|
|
1254
|
-
f"Error loading callable {func_name} from file {file_path}: {e}",
|
|
1255
|
-
exc_info=True,
|
|
1256
|
-
)
|
|
1257
|
-
|
|
1258
|
-
# Handle regular components (existing code)
|
|
1259
|
-
else:
|
|
1260
|
-
# First try using the module path (Python import)
|
|
1261
|
-
module_path = component_def.get("module_path")
|
|
1262
|
-
if module_path and module_path != "unknown":
|
|
1263
|
-
try:
|
|
1264
|
-
logger.debug(
|
|
1265
|
-
f"Attempting to import module '{module_path}' for component '{component_name}'"
|
|
1266
|
-
)
|
|
1267
|
-
module = importlib.import_module(module_path)
|
|
1268
|
-
# Find the component class in the module
|
|
1269
|
-
for attr_name in dir(module):
|
|
1270
|
-
if attr_name == component_name:
|
|
1271
|
-
component_class = getattr(module, attr_name)
|
|
1272
|
-
registry.register_component(
|
|
1273
|
-
component_class, component_name
|
|
1274
|
-
)
|
|
1275
|
-
logger.info(
|
|
1276
|
-
f"Registered component {component_name} from {module_path}"
|
|
1277
|
-
)
|
|
1278
|
-
break
|
|
1279
|
-
else:
|
|
1280
|
-
logger.warning(
|
|
1281
|
-
f"Component {component_name} not found in module {module_path}"
|
|
1282
|
-
)
|
|
1283
|
-
# If we didn't find the component, try using file_path next
|
|
1284
|
-
raise ImportError(
|
|
1285
|
-
f"Component {component_name} not found in module {module_path}"
|
|
1286
|
-
)
|
|
1287
|
-
except ImportError:
|
|
1288
|
-
# If module import fails, try file_path approach
|
|
1289
|
-
file_path = component_def.get("file_path")
|
|
1290
|
-
|
|
1291
|
-
# Convert relative path to absolute if needed
|
|
1292
|
-
if (
|
|
1293
|
-
path_type == "relative"
|
|
1294
|
-
and file_path
|
|
1295
|
-
and not os.path.isabs(file_path)
|
|
1296
|
-
):
|
|
1297
|
-
try:
|
|
1298
|
-
# Make absolute based on current directory
|
|
1299
|
-
file_path = os.path.abspath(file_path)
|
|
1300
|
-
logger.debug(
|
|
1301
|
-
f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
|
|
1302
|
-
)
|
|
1303
|
-
except Exception as e:
|
|
1304
|
-
logger.warning(
|
|
1305
|
-
f"Could not convert relative path to absolute: {e}"
|
|
1306
|
-
)
|
|
1307
|
-
|
|
1308
|
-
if file_path and os.path.exists(file_path):
|
|
1309
|
-
logger.debug(
|
|
1310
|
-
f"Attempting to load {component_name} from file: {file_path}"
|
|
1311
|
-
)
|
|
1312
|
-
try:
|
|
1313
|
-
# Load the module from file path
|
|
1314
|
-
spec = (
|
|
1315
|
-
importlib.util.spec_from_file_location(
|
|
1316
|
-
f"{component_name}_module",
|
|
1317
|
-
file_path,
|
|
1318
|
-
)
|
|
1319
|
-
)
|
|
1320
|
-
if spec and spec.loader:
|
|
1321
|
-
module = (
|
|
1322
|
-
importlib.util.module_from_spec(
|
|
1323
|
-
spec
|
|
1324
|
-
)
|
|
1325
|
-
)
|
|
1326
|
-
sys.modules[spec.name] = module
|
|
1327
|
-
spec.loader.exec_module(module)
|
|
1328
|
-
logger.debug(
|
|
1329
|
-
f"Successfully loaded module from file, searching for component class '{component_name}'"
|
|
1330
|
-
)
|
|
1331
|
-
|
|
1332
|
-
# Find the component class in the loaded module
|
|
1333
|
-
for attr_name in dir(module):
|
|
1334
|
-
if attr_name == component_name:
|
|
1335
|
-
component_class = getattr(
|
|
1336
|
-
module, attr_name
|
|
1337
|
-
)
|
|
1338
|
-
registry.register_component(
|
|
1339
|
-
component_class,
|
|
1340
|
-
component_name,
|
|
1341
|
-
)
|
|
1342
|
-
logger.info(
|
|
1343
|
-
f"Registered component {component_name} from file {file_path}"
|
|
1344
|
-
)
|
|
1345
|
-
break
|
|
1346
|
-
else:
|
|
1347
|
-
logger.warning(
|
|
1348
|
-
f"Component {component_name} not found in file {file_path}"
|
|
1349
|
-
)
|
|
1350
|
-
except Exception as e:
|
|
1351
|
-
logger.error(
|
|
1352
|
-
f"Error loading component {component_name} from file {file_path}: {e}",
|
|
1353
|
-
exc_info=True,
|
|
1354
|
-
)
|
|
1355
|
-
else:
|
|
1356
|
-
logger.warning(
|
|
1357
|
-
f"No valid file path found for component {component_name}"
|
|
1358
|
-
)
|
|
1359
|
-
else:
|
|
1360
|
-
logger.warning(
|
|
1361
|
-
f"Missing or unknown module path for component {component_name}"
|
|
1362
|
-
)
|
|
1363
|
-
except Exception as e:
|
|
1364
|
-
logger.error(
|
|
1365
|
-
f"Failed to register component {component_name}: {e}",
|
|
1366
|
-
exc_info=True,
|
|
1367
|
-
)
|
|
610
|
+
if asyncio.get_event_loop() is loop and not loop.is_running():
|
|
611
|
+
results = loop.run_until_complete(coro)
|
|
612
|
+
return results
|
|
613
|
+
else:
|
|
614
|
+
future = asyncio.ensure_future(coro)
|
|
615
|
+
return loop.run_until_complete(future)
|
|
1368
616
|
|
|
1369
|
-
|
|
1370
|
-
def _check_dependencies(cls, dependencies: list[str]) -> None:
|
|
1371
|
-
"""Check if required dependencies are available."""
|
|
1372
|
-
import importlib
|
|
1373
|
-
import re
|
|
1374
|
-
|
|
1375
|
-
for dependency in dependencies:
|
|
1376
|
-
# Extract package name and version
|
|
1377
|
-
match = re.match(r"([^>=<]+)([>=<].+)?", dependency)
|
|
1378
|
-
if match:
|
|
1379
|
-
package_name = match.group(1)
|
|
1380
|
-
try:
|
|
1381
|
-
importlib.import_module(package_name.replace("-", "_"))
|
|
1382
|
-
logger.debug(f"Dependency {package_name} is available")
|
|
1383
|
-
except ImportError:
|
|
1384
|
-
logger.warning(f"Dependency {dependency} is not installed")
|
|
1385
|
-
|
|
1386
|
-
# --- API Start Method ---
|
|
617
|
+
# --- API Server Starter ---
|
|
1387
618
|
def start_api(
|
|
1388
619
|
self,
|
|
1389
620
|
host: str = "127.0.0.1",
|
|
@@ -1391,98 +622,46 @@ class Flock(BaseModel, Serializable):
|
|
|
1391
622
|
server_name: str = "Flock API",
|
|
1392
623
|
create_ui: bool = False,
|
|
1393
624
|
) -> None:
|
|
1394
|
-
"""
|
|
1395
|
-
# Import locally
|
|
1396
|
-
|
|
1397
|
-
from flock.core.api import FlockAPI
|
|
1398
|
-
except ImportError:
|
|
1399
|
-
logger.error(
|
|
1400
|
-
"API components not found. Cannot start API. "
|
|
1401
|
-
"Ensure 'fastapi' and 'uvicorn' are installed."
|
|
1402
|
-
)
|
|
1403
|
-
return
|
|
625
|
+
"""Starts a REST API server for this Flock instance."""
|
|
626
|
+
# Import runner locally
|
|
627
|
+
from flock.core.api.runner import start_flock_api
|
|
1404
628
|
|
|
1405
|
-
|
|
1406
|
-
f"Preparing to start API server on {host}:{port} {'with UI' if create_ui else 'without UI'}"
|
|
1407
|
-
)
|
|
1408
|
-
api_instance = FlockAPI(self) # Pass the current Flock instance
|
|
1409
|
-
# Use the start method of FlockAPI
|
|
1410
|
-
api_instance.start(
|
|
1411
|
-
host=host, port=port, server_name=server_name, create_ui=create_ui
|
|
1412
|
-
)
|
|
629
|
+
start_flock_api(self, host, port, server_name, create_ui)
|
|
1413
630
|
|
|
1414
|
-
# --- CLI
|
|
631
|
+
# --- CLI Starter ---
|
|
1415
632
|
def start_cli(
|
|
1416
633
|
self,
|
|
1417
634
|
server_name: str = "Flock CLI",
|
|
1418
635
|
show_results: bool = False,
|
|
1419
636
|
edit_mode: bool = False,
|
|
1420
637
|
) -> None:
|
|
1421
|
-
"""
|
|
638
|
+
"""Starts an interactive CLI for this Flock instance."""
|
|
639
|
+
# Import runner locally
|
|
640
|
+
from flock.cli.runner import start_flock_cli
|
|
1422
641
|
|
|
1423
|
-
|
|
1424
|
-
allowing users to execute, edit, or manage agents from the existing configuration.
|
|
642
|
+
start_flock_cli(self, server_name, show_results, edit_mode)
|
|
1425
643
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
# Import locally to avoid circular imports
|
|
1432
|
-
try:
|
|
1433
|
-
from flock.cli.loaded_flock_cli import start_loaded_flock_cli
|
|
1434
|
-
except ImportError:
|
|
1435
|
-
logger.error(
|
|
1436
|
-
"CLI components not found. Cannot start CLI. "
|
|
1437
|
-
"Ensure the CLI modules are properly installed."
|
|
1438
|
-
)
|
|
1439
|
-
return
|
|
644
|
+
# --- Serialization Delegation Methods ---
|
|
645
|
+
def to_dict(self, path_type: str = "relative") -> dict[str, Any]:
|
|
646
|
+
"""Serialize Flock instance to dictionary using FlockSerializer."""
|
|
647
|
+
# Import locally to prevent circular imports at module level if structure is complex
|
|
648
|
+
from flock.core.serialization.flock_serializer import FlockSerializer
|
|
1440
649
|
|
|
1441
|
-
|
|
1442
|
-
f"Starting CLI interface with loaded Flock instance ({len(self._agents)} agents)"
|
|
1443
|
-
)
|
|
650
|
+
return FlockSerializer.serialize(self, path_type=path_type)
|
|
1444
651
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
)
|
|
652
|
+
@classmethod
|
|
653
|
+
def from_dict(cls: type[T], data: dict[str, Any]) -> T:
|
|
654
|
+
"""Deserialize Flock instance from dictionary using FlockSerializer."""
|
|
655
|
+
# Import locally
|
|
656
|
+
from flock.core.serialization.flock_serializer import FlockSerializer
|
|
657
|
+
|
|
658
|
+
return FlockSerializer.deserialize(cls, data)
|
|
1452
659
|
|
|
1453
|
-
# --- Static Method
|
|
660
|
+
# --- Static Method Loader (Delegates to loader module) ---
|
|
1454
661
|
@staticmethod
|
|
1455
662
|
def load_from_file(file_path: str) -> Flock:
|
|
1456
|
-
"""Load a Flock instance from various file formats (
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
raise FileNotFoundError(f"Flock file not found: {file_path}")
|
|
663
|
+
"""Load a Flock instance from various file formats (delegates to loader)."""
|
|
664
|
+
# Import locally
|
|
665
|
+
from flock.core.util.loader import load_flock_from_file
|
|
1460
666
|
|
|
1461
|
-
|
|
1462
|
-
if p.suffix in [".yaml", ".yml"]:
|
|
1463
|
-
return Flock.from_yaml_file(p)
|
|
1464
|
-
elif p.suffix == ".json":
|
|
1465
|
-
return Flock.from_json(p.read_text())
|
|
1466
|
-
elif p.suffix == ".msgpack":
|
|
1467
|
-
return Flock.from_msgpack_file(p)
|
|
1468
|
-
elif p.suffix == ".pkl":
|
|
1469
|
-
if PICKLE_AVAILABLE:
|
|
1470
|
-
return Flock.from_pickle_file(p)
|
|
1471
|
-
else:
|
|
1472
|
-
raise RuntimeError(
|
|
1473
|
-
"Cannot load Pickle file: cloudpickle not installed."
|
|
1474
|
-
)
|
|
1475
|
-
else:
|
|
1476
|
-
raise ValueError(
|
|
1477
|
-
f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
|
|
1478
|
-
)
|
|
1479
|
-
except Exception as e:
|
|
1480
|
-
# Check if it's an exception about missing types
|
|
1481
|
-
if "Could not get registered type name" in str(e):
|
|
1482
|
-
logger.error(
|
|
1483
|
-
f"Failed to load Flock from {file_path}: Missing type definition. "
|
|
1484
|
-
"This may happen if the YAML was created on a system with different types registered. "
|
|
1485
|
-
"Check if the file includes 'types' section with necessary type definitions."
|
|
1486
|
-
)
|
|
1487
|
-
logger.error(f"Error loading Flock from {file_path}: {e}")
|
|
1488
|
-
raise
|
|
667
|
+
return load_flock_from_file(file_path)
|