flock-core 0.3.23__py3-none-any.whl → 0.3.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/__init__.py +23 -11
- flock/cli/constants.py +2 -4
- flock/cli/create_flock.py +220 -1
- flock/cli/execute_flock.py +200 -0
- flock/cli/load_flock.py +27 -7
- flock/cli/loaded_flock_cli.py +202 -0
- flock/cli/manage_agents.py +443 -0
- flock/cli/view_results.py +29 -0
- flock/cli/yaml_editor.py +283 -0
- flock/core/__init__.py +2 -2
- flock/core/api/__init__.py +11 -0
- flock/core/api/endpoints.py +222 -0
- flock/core/api/main.py +237 -0
- flock/core/api/models.py +34 -0
- flock/core/api/run_store.py +72 -0
- flock/core/api/ui/__init__.py +0 -0
- flock/core/api/ui/routes.py +271 -0
- flock/core/api/ui/utils.py +119 -0
- flock/core/flock.py +509 -388
- flock/core/flock_agent.py +384 -121
- flock/core/flock_registry.py +532 -0
- flock/core/logging/logging.py +97 -23
- flock/core/mixin/dspy_integration.py +363 -158
- flock/core/serialization/__init__.py +7 -1
- flock/core/serialization/callable_registry.py +52 -0
- flock/core/serialization/serializable.py +259 -37
- flock/core/serialization/serialization_utils.py +199 -0
- flock/evaluators/declarative/declarative_evaluator.py +2 -0
- flock/modules/memory/memory_module.py +17 -4
- flock/modules/output/output_module.py +9 -3
- flock/workflow/activities.py +2 -2
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/METADATA +6 -3
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/RECORD +36 -22
- flock/core/flock_api.py +0 -214
- flock/core/registry/agent_registry.py +0 -120
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/WHEEL +0 -0
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/entry_points.txt +0 -0
- {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_agent.py
CHANGED
|
@@ -1,113 +1,149 @@
|
|
|
1
|
+
# src/flock/core/flock_agent.py
|
|
1
2
|
"""FlockAgent is the core, declarative base class for all agents in the Flock framework."""
|
|
2
3
|
|
|
3
4
|
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
5
|
from abc import ABC
|
|
7
6
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any, TypeVar
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from flock.core.context.context import FlockContext
|
|
11
|
+
from flock.core.flock_evaluator import FlockEvaluator
|
|
12
|
+
from flock.core.flock_module import FlockModule
|
|
13
|
+
from flock.core.flock_router import FlockRouter
|
|
9
14
|
|
|
10
|
-
import cloudpickle
|
|
11
15
|
from opentelemetry import trace
|
|
12
16
|
from pydantic import BaseModel, Field
|
|
13
17
|
|
|
18
|
+
# Core Flock components (ensure these are importable)
|
|
14
19
|
from flock.core.context.context import FlockContext
|
|
15
20
|
from flock.core.flock_evaluator import FlockEvaluator
|
|
16
21
|
from flock.core.flock_module import FlockModule
|
|
17
22
|
from flock.core.flock_router import FlockRouter
|
|
18
23
|
from flock.core.logging.logging import get_logger
|
|
24
|
+
|
|
25
|
+
# Mixins and Serialization components
|
|
19
26
|
from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
|
|
27
|
+
from flock.core.serialization.serializable import (
|
|
28
|
+
Serializable, # Import Serializable base
|
|
29
|
+
)
|
|
30
|
+
from flock.core.serialization.serialization_utils import (
|
|
31
|
+
deserialize_component,
|
|
32
|
+
serialize_item,
|
|
33
|
+
)
|
|
20
34
|
|
|
21
35
|
logger = get_logger("agent")
|
|
22
36
|
tracer = trace.get_tracer(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
37
|
T = TypeVar("T", bound="FlockAgent")
|
|
26
38
|
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
# Make FlockAgent inherit from Serializable
|
|
41
|
+
class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
42
|
+
"""Core, declarative base class for Flock agents, enabling serialization,
|
|
43
|
+
modularity, and integration with evaluation and routing components.
|
|
44
|
+
Inherits from Pydantic BaseModel, ABC, DSPyIntegrationMixin, and Serializable.
|
|
45
|
+
"""
|
|
46
|
+
|
|
29
47
|
name: str = Field(..., description="Unique identifier for the agent.")
|
|
30
48
|
model: str | None = Field(
|
|
31
|
-
None,
|
|
49
|
+
None,
|
|
50
|
+
description="The model identifier to use (e.g., 'openai/gpt-4o'). If None, uses Flock's default.",
|
|
32
51
|
)
|
|
33
52
|
description: str | Callable[..., str] | None = Field(
|
|
34
|
-
"",
|
|
53
|
+
"",
|
|
54
|
+
description="A human-readable description or a callable returning one.",
|
|
35
55
|
)
|
|
36
|
-
|
|
37
56
|
input: str | Callable[..., str] | None = Field(
|
|
38
57
|
None,
|
|
39
58
|
description=(
|
|
40
|
-
"
|
|
41
|
-
"
|
|
59
|
+
"Signature for input keys. Supports type hints (:) and descriptions (|). "
|
|
60
|
+
"E.g., 'query: str | Search query, context: dict | Conversation context'. Can be a callable."
|
|
42
61
|
),
|
|
43
62
|
)
|
|
44
63
|
output: str | Callable[..., str] | None = Field(
|
|
45
64
|
None,
|
|
46
65
|
description=(
|
|
47
|
-
"
|
|
48
|
-
"
|
|
66
|
+
"Signature for output keys. Supports type hints (:) and descriptions (|). "
|
|
67
|
+
"E.g., 'result: str | Generated result, summary: str | Brief summary'. Can be a callable."
|
|
49
68
|
),
|
|
50
69
|
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
tools: list[Callable[..., Any]] | None = (
|
|
71
|
+
Field( # Assume tools are always callable for serialization simplicity
|
|
72
|
+
default=None,
|
|
73
|
+
description="List of callable tools the agent can use. These must be registered.",
|
|
74
|
+
)
|
|
55
75
|
)
|
|
56
|
-
|
|
57
76
|
use_cache: bool = Field(
|
|
58
77
|
default=True,
|
|
59
|
-
description="
|
|
78
|
+
description="Enable caching for the agent's evaluator (if supported).",
|
|
60
79
|
)
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
# --- Components ---
|
|
82
|
+
evaluator: FlockEvaluator | None = Field( # Make optional, allow None
|
|
63
83
|
default=None,
|
|
64
|
-
description="
|
|
84
|
+
description="The evaluator instance defining the agent's core logic.",
|
|
65
85
|
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
description="Evaluator to use for agent evaluation",
|
|
86
|
+
handoff_router: FlockRouter | None = Field( # Make optional, allow None
|
|
87
|
+
default=None,
|
|
88
|
+
description="Router determining the next agent in the workflow.",
|
|
70
89
|
)
|
|
71
|
-
|
|
72
|
-
modules: dict[str, FlockModule] = Field(
|
|
90
|
+
modules: dict[str, FlockModule] = Field( # Keep as dict
|
|
73
91
|
default_factory=dict,
|
|
74
|
-
description="FlockModules attached to this agent",
|
|
92
|
+
description="Dictionary of FlockModules attached to this agent.",
|
|
75
93
|
)
|
|
76
94
|
|
|
95
|
+
# --- Runtime State (Excluded from Serialization) ---
|
|
77
96
|
context: FlockContext | None = Field(
|
|
78
97
|
default=None,
|
|
79
|
-
|
|
98
|
+
exclude=True, # Exclude context from model_dump and serialization
|
|
99
|
+
description="Runtime context associated with the flock execution.",
|
|
80
100
|
)
|
|
81
101
|
|
|
102
|
+
# --- Existing Methods (add_module, remove_module, etc.) ---
|
|
103
|
+
# (Keep these methods as they were, adding type hints where useful)
|
|
82
104
|
def add_module(self, module: FlockModule) -> None:
|
|
83
105
|
"""Add a module to this agent."""
|
|
106
|
+
if not module.name:
|
|
107
|
+
logger.error("Module must have a name to be added.")
|
|
108
|
+
return
|
|
109
|
+
if module.name in self.modules:
|
|
110
|
+
logger.warning(f"Overwriting existing module: {module.name}")
|
|
84
111
|
self.modules[module.name] = module
|
|
112
|
+
logger.debug(f"Added module '{module.name}' to agent '{self.name}'")
|
|
85
113
|
|
|
86
114
|
def remove_module(self, module_name: str) -> None:
|
|
87
115
|
"""Remove a module from this agent."""
|
|
88
116
|
if module_name in self.modules:
|
|
89
117
|
del self.modules[module_name]
|
|
118
|
+
logger.debug(
|
|
119
|
+
f"Removed module '{module_name}' from agent '{self.name}'"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
logger.warning(
|
|
123
|
+
f"Module '{module_name}' not found on agent '{self.name}'."
|
|
124
|
+
)
|
|
90
125
|
|
|
91
126
|
def get_module(self, module_name: str) -> FlockModule | None:
|
|
92
127
|
"""Get a module by name."""
|
|
93
128
|
return self.modules.get(module_name)
|
|
94
129
|
|
|
95
|
-
def get_enabled_modules(self) -> list[FlockModule
|
|
96
|
-
"""Get a
|
|
130
|
+
def get_enabled_modules(self) -> list[FlockModule]:
|
|
131
|
+
"""Get a list of currently enabled modules attached to this agent."""
|
|
97
132
|
return [m for m in self.modules.values() if m.config.enabled]
|
|
98
133
|
|
|
99
|
-
# Lifecycle
|
|
134
|
+
# --- Lifecycle Hooks (Keep as they were) ---
|
|
100
135
|
async def initialize(self, inputs: dict[str, Any]) -> None:
|
|
136
|
+
"""Initialize agent and run module initializers."""
|
|
137
|
+
logger.debug(f"Initializing agent '{self.name}'")
|
|
101
138
|
with tracer.start_as_current_span("agent.initialize") as span:
|
|
102
139
|
span.set_attribute("agent.name", self.name)
|
|
103
140
|
span.set_attribute("inputs", str(inputs))
|
|
104
|
-
|
|
141
|
+
logger.info(
|
|
142
|
+
f"agent.initialize",
|
|
143
|
+
agent=self.name,
|
|
144
|
+
)
|
|
105
145
|
try:
|
|
106
146
|
for module in self.get_enabled_modules():
|
|
107
|
-
logger.info(
|
|
108
|
-
f"agent.initialize - module {module.name}",
|
|
109
|
-
agent=self.name,
|
|
110
|
-
)
|
|
111
147
|
await module.initialize(self, inputs, self.context)
|
|
112
148
|
except Exception as module_error:
|
|
113
149
|
logger.error(
|
|
@@ -120,6 +156,8 @@ class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
|
|
|
120
156
|
async def terminate(
|
|
121
157
|
self, inputs: dict[str, Any], result: dict[str, Any]
|
|
122
158
|
) -> None:
|
|
159
|
+
"""Terminate agent and run module terminators."""
|
|
160
|
+
logger.debug(f"Terminating agent '{self.name}'")
|
|
123
161
|
with tracer.start_as_current_span("agent.terminate") as span:
|
|
124
162
|
span.set_attribute("agent.name", self.name)
|
|
125
163
|
span.set_attribute("inputs", str(inputs))
|
|
@@ -140,6 +178,8 @@ class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
|
|
|
140
178
|
span.record_exception(module_error)
|
|
141
179
|
|
|
142
180
|
async def on_error(self, error: Exception, inputs: dict[str, Any]) -> None:
|
|
181
|
+
"""Handle errors and run module error handlers."""
|
|
182
|
+
logger.error(f"Error occurred in agent '{self.name}': {error}")
|
|
143
183
|
with tracer.start_as_current_span("agent.on_error") as span:
|
|
144
184
|
span.set_attribute("agent.name", self.name)
|
|
145
185
|
span.set_attribute("inputs", str(inputs))
|
|
@@ -155,74 +195,98 @@ class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
|
|
|
155
195
|
span.record_exception(module_error)
|
|
156
196
|
|
|
157
197
|
async def evaluate(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
198
|
+
"""Core evaluation logic, calling the assigned evaluator and modules."""
|
|
199
|
+
if not self.evaluator:
|
|
200
|
+
raise RuntimeError(
|
|
201
|
+
f"Agent '{self.name}' has no evaluator assigned."
|
|
202
|
+
)
|
|
158
203
|
with tracer.start_as_current_span("agent.evaluate") as span:
|
|
159
204
|
span.set_attribute("agent.name", self.name)
|
|
160
205
|
span.set_attribute("inputs", str(inputs))
|
|
206
|
+
logger.info(
|
|
207
|
+
f"agent.evaluate",
|
|
208
|
+
agent=self.name,
|
|
209
|
+
)
|
|
161
210
|
|
|
211
|
+
logger.debug(f"Evaluating agent '{self.name}'")
|
|
212
|
+
current_inputs = inputs
|
|
213
|
+
|
|
214
|
+
# Pre-evaluate hooks
|
|
162
215
|
for module in self.get_enabled_modules():
|
|
163
|
-
|
|
216
|
+
current_inputs = await module.pre_evaluate(
|
|
217
|
+
self, current_inputs, self.context
|
|
218
|
+
)
|
|
164
219
|
|
|
220
|
+
# Actual evaluation
|
|
165
221
|
try:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return result
|
|
222
|
+
# Pass registered tools if the evaluator needs them
|
|
223
|
+
registered_tools = []
|
|
224
|
+
if self.tools:
|
|
225
|
+
# Ensure tools are actually retrieved/validated if needed by evaluator type
|
|
226
|
+
# For now, assume evaluator handles tool resolution if necessary
|
|
227
|
+
registered_tools = self.tools
|
|
228
|
+
|
|
229
|
+
result = await self.evaluator.evaluate(
|
|
230
|
+
self, current_inputs, registered_tools
|
|
231
|
+
)
|
|
177
232
|
except Exception as eval_error:
|
|
178
233
|
logger.error(
|
|
179
|
-
"Error during
|
|
234
|
+
"Error during evaluate",
|
|
180
235
|
agent=self.name,
|
|
181
236
|
error=str(eval_error),
|
|
182
237
|
)
|
|
183
238
|
span.record_exception(eval_error)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if file_path is None:
|
|
189
|
-
file_path = f"{self.name}.json"
|
|
190
|
-
dict_data = self.to_dict()
|
|
239
|
+
await self.on_error(
|
|
240
|
+
eval_error, current_inputs
|
|
241
|
+
) # Call error hook
|
|
242
|
+
raise # Re-raise the exception
|
|
191
243
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
244
|
+
# Post-evaluate hooks
|
|
245
|
+
current_result = result
|
|
246
|
+
for module in self.get_enabled_modules():
|
|
247
|
+
current_result = await module.post_evaluate(
|
|
248
|
+
self, current_inputs, current_result, self.context
|
|
249
|
+
)
|
|
196
250
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
@classmethod
|
|
201
|
-
def load_from_file(cls: type[T], file_path: str) -> T:
|
|
202
|
-
"""Load a serialized agent from a file."""
|
|
203
|
-
with open(file_path) as file:
|
|
204
|
-
data = json.load(file)
|
|
205
|
-
# Fallback: use the current class.
|
|
206
|
-
return cls.from_dict(data)
|
|
251
|
+
logger.debug(f"Evaluation completed for agent '{self.name}'")
|
|
252
|
+
return current_result
|
|
207
253
|
|
|
208
254
|
def run(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
209
|
-
"""
|
|
210
|
-
|
|
255
|
+
"""Synchronous wrapper for run_async."""
|
|
256
|
+
try:
|
|
257
|
+
loop = asyncio.get_running_loop()
|
|
258
|
+
except (
|
|
259
|
+
RuntimeError
|
|
260
|
+
): # 'RuntimeError: There is no current event loop...'
|
|
261
|
+
loop = asyncio.new_event_loop()
|
|
262
|
+
asyncio.set_event_loop(loop)
|
|
263
|
+
return loop.run_until_complete(self.run_async(inputs))
|
|
211
264
|
|
|
212
265
|
def set_model(self, model: str):
|
|
213
|
-
"""Set the model for the agent."""
|
|
266
|
+
"""Set the model for the agent and its evaluator."""
|
|
214
267
|
self.model = model
|
|
215
|
-
self.evaluator.config
|
|
268
|
+
if self.evaluator and hasattr(self.evaluator, "config"):
|
|
269
|
+
self.evaluator.config.model = model
|
|
270
|
+
logger.info(
|
|
271
|
+
f"Set model to '{model}' for agent '{self.name}' and its evaluator."
|
|
272
|
+
)
|
|
273
|
+
elif self.evaluator:
|
|
274
|
+
logger.warning(
|
|
275
|
+
f"Evaluator for agent '{self.name}' does not have a standard config to set model."
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
logger.warning(
|
|
279
|
+
f"Agent '{self.name}' has no evaluator to set model for."
|
|
280
|
+
)
|
|
216
281
|
|
|
217
282
|
async def run_async(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
283
|
+
"""Asynchronous execution logic with lifecycle hooks."""
|
|
218
284
|
with tracer.start_as_current_span("agent.run") as span:
|
|
219
285
|
span.set_attribute("agent.name", self.name)
|
|
220
286
|
span.set_attribute("inputs", str(inputs))
|
|
221
287
|
try:
|
|
222
288
|
await self.initialize(inputs)
|
|
223
|
-
|
|
224
289
|
result = await self.evaluate(inputs)
|
|
225
|
-
|
|
226
290
|
await self.terminate(inputs, result)
|
|
227
291
|
span.set_attribute("result", str(result))
|
|
228
292
|
logger.info("Agent run completed", agent=self.name)
|
|
@@ -231,9 +295,16 @@ class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
|
|
|
231
295
|
logger.error(
|
|
232
296
|
"Error running agent", agent=self.name, error=str(run_error)
|
|
233
297
|
)
|
|
234
|
-
|
|
298
|
+
if "evaluate" not in str(
|
|
299
|
+
run_error
|
|
300
|
+
): # Simple check, might need refinement
|
|
301
|
+
await self.on_error(run_error, inputs)
|
|
302
|
+
logger.error(
|
|
303
|
+
f"Agent '{self.name}' run failed: {run_error}",
|
|
304
|
+
exc_info=True,
|
|
305
|
+
)
|
|
235
306
|
span.record_exception(run_error)
|
|
236
|
-
raise
|
|
307
|
+
raise # Re-raise after handling
|
|
237
308
|
|
|
238
309
|
async def run_temporal(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
239
310
|
with tracer.start_as_current_span("agent.run_temporal") as span:
|
|
@@ -271,56 +342,248 @@ class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
|
|
|
271
342
|
span.record_exception(temporal_error)
|
|
272
343
|
raise
|
|
273
344
|
|
|
274
|
-
|
|
275
|
-
|
|
345
|
+
# resolve_callables remains useful for dynamic definitions
|
|
346
|
+
def resolve_callables(self, context: FlockContext | None = None) -> None:
|
|
347
|
+
"""Resolves callable fields (description, input, output) using context."""
|
|
348
|
+
if callable(self.description):
|
|
349
|
+
self.description = self.description(
|
|
350
|
+
context
|
|
351
|
+
) # Pass context if needed by callable
|
|
352
|
+
if callable(self.input):
|
|
276
353
|
self.input = self.input(context)
|
|
277
|
-
if
|
|
354
|
+
if callable(self.output):
|
|
278
355
|
self.output = self.output(context)
|
|
279
|
-
if isinstance(self.description, Callable):
|
|
280
|
-
self.description = self.description(context)
|
|
281
356
|
|
|
282
|
-
|
|
283
|
-
def convert_callable(obj: Any) -> Any:
|
|
284
|
-
if callable(obj) and not isinstance(obj, type):
|
|
285
|
-
return cloudpickle.dumps(obj).hex()
|
|
286
|
-
if isinstance(obj, list):
|
|
287
|
-
return [convert_callable(item) for item in obj]
|
|
288
|
-
if isinstance(obj, dict):
|
|
289
|
-
return {k: convert_callable(v) for k, v in obj.items()}
|
|
290
|
-
return obj
|
|
357
|
+
# --- Serialization Implementation ---
|
|
291
358
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
359
|
+
def to_dict(self) -> dict[str, Any]:
|
|
360
|
+
"""Convert instance to dictionary representation suitable for serialization."""
|
|
361
|
+
from flock.core.flock_registry import get_registry
|
|
362
|
+
|
|
363
|
+
FlockRegistry = get_registry()
|
|
364
|
+
logger.debug(f"Serializing agent '{self.name}' to dict.")
|
|
365
|
+
# Use Pydantic's dump, exclude manually handled fields and runtime context
|
|
366
|
+
data = self.model_dump(
|
|
367
|
+
exclude={
|
|
368
|
+
"context",
|
|
369
|
+
"evaluator",
|
|
370
|
+
"modules",
|
|
371
|
+
"handoff_router",
|
|
372
|
+
"tools",
|
|
373
|
+
},
|
|
374
|
+
mode="json", # Use json mode for better handling of standard types by Pydantic
|
|
375
|
+
exclude_none=True, # Exclude None values for cleaner output
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# --- Serialize Components using Registry Type Names ---
|
|
379
|
+
# Evaluator
|
|
380
|
+
if self.evaluator:
|
|
381
|
+
evaluator_type_name = FlockRegistry.get_component_type_name(
|
|
382
|
+
type(self.evaluator)
|
|
383
|
+
)
|
|
384
|
+
if evaluator_type_name:
|
|
385
|
+
# Recursively serialize the evaluator's dict representation
|
|
386
|
+
evaluator_dict = serialize_item(
|
|
387
|
+
self.evaluator.model_dump(mode="json", exclude_none=True)
|
|
388
|
+
)
|
|
389
|
+
evaluator_dict["type"] = evaluator_type_name # Add type marker
|
|
390
|
+
data["evaluator"] = evaluator_dict
|
|
391
|
+
else:
|
|
392
|
+
logger.warning(
|
|
393
|
+
f"Could not get registered type name for evaluator {type(self.evaluator).__name__} in agent '{self.name}'. Skipping serialization."
|
|
394
|
+
)
|
|
296
395
|
|
|
297
|
-
|
|
396
|
+
# Router
|
|
397
|
+
if self.handoff_router:
|
|
398
|
+
router_type_name = FlockRegistry.get_component_type_name(
|
|
399
|
+
type(self.handoff_router)
|
|
400
|
+
)
|
|
401
|
+
if router_type_name:
|
|
402
|
+
router_dict = serialize_item(
|
|
403
|
+
self.handoff_router.model_dump(
|
|
404
|
+
mode="json", exclude_none=True
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
router_dict["type"] = router_type_name
|
|
408
|
+
data["handoff_router"] = router_dict
|
|
409
|
+
else:
|
|
410
|
+
logger.warning(
|
|
411
|
+
f"Could not get registered type name for router {type(self.handoff_router).__name__} in agent '{self.name}'. Skipping serialization."
|
|
412
|
+
)
|
|
298
413
|
|
|
299
|
-
|
|
414
|
+
# Modules
|
|
415
|
+
if self.modules:
|
|
416
|
+
serialized_modules = {}
|
|
417
|
+
for name, module_instance in self.modules.items():
|
|
418
|
+
module_type_name = FlockRegistry.get_component_type_name(
|
|
419
|
+
type(module_instance)
|
|
420
|
+
)
|
|
421
|
+
if module_type_name:
|
|
422
|
+
module_dict = serialize_item(
|
|
423
|
+
module_instance.model_dump(
|
|
424
|
+
mode="json", exclude_none=True
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
module_dict["type"] = module_type_name
|
|
428
|
+
serialized_modules[name] = module_dict
|
|
429
|
+
else:
|
|
430
|
+
logger.warning(
|
|
431
|
+
f"Could not get registered type name for module {type(module_instance).__name__} ('{name}') in agent '{self.name}'. Skipping."
|
|
432
|
+
)
|
|
433
|
+
if serialized_modules:
|
|
434
|
+
data["modules"] = serialized_modules
|
|
435
|
+
|
|
436
|
+
# --- Serialize Tools (Callables) ---
|
|
437
|
+
if self.tools:
|
|
438
|
+
serialized_tools = []
|
|
439
|
+
for tool in self.tools:
|
|
440
|
+
if callable(tool) and not isinstance(tool, type):
|
|
441
|
+
path_str = FlockRegistry.get_callable_path_string(tool)
|
|
442
|
+
if path_str:
|
|
443
|
+
serialized_tools.append({"__callable_ref__": path_str})
|
|
444
|
+
else:
|
|
445
|
+
logger.warning(
|
|
446
|
+
f"Could not get path string for tool {tool} in agent '{self.name}'. Skipping."
|
|
447
|
+
)
|
|
448
|
+
# Silently skip non-callable items or log warning
|
|
449
|
+
# else:
|
|
450
|
+
# logger.warning(f"Non-callable item found in tools list for agent '{self.name}': {tool}. Skipping.")
|
|
451
|
+
if serialized_tools:
|
|
452
|
+
data["tools"] = serialized_tools
|
|
453
|
+
|
|
454
|
+
# No need to call _filter_none_values here as model_dump(exclude_none=True) handles it
|
|
455
|
+
return data
|
|
300
456
|
|
|
301
457
|
@classmethod
|
|
302
458
|
def from_dict(cls: type[T], data: dict[str, Any]) -> T:
|
|
303
|
-
|
|
304
|
-
|
|
459
|
+
"""Create instance from dictionary representation."""
|
|
460
|
+
from flock.core.flock_registry import get_registry
|
|
461
|
+
|
|
462
|
+
logger.debug(
|
|
463
|
+
f"Deserializing agent from dict. Provided keys: {list(data.keys())}"
|
|
464
|
+
)
|
|
465
|
+
if "name" not in data:
|
|
466
|
+
raise ValueError("Agent data must include a 'name' field.")
|
|
467
|
+
FlockRegistry = get_registry()
|
|
468
|
+
agent_name = data["name"] # For logging context
|
|
469
|
+
|
|
470
|
+
# Pop complex components to handle them after basic agent instantiation
|
|
471
|
+
evaluator_data = data.pop("evaluator", None)
|
|
472
|
+
router_data = data.pop("handoff_router", None)
|
|
473
|
+
modules_data = data.pop("modules", {})
|
|
474
|
+
tools_data = data.pop("tools", [])
|
|
475
|
+
|
|
476
|
+
# Deserialize remaining data recursively (handles nested basic types/callables)
|
|
477
|
+
# Note: Pydantic v2 handles most basic deserialization well if types match.
|
|
478
|
+
# Explicit deserialize_item might be needed if complex non-pydantic structures exist.
|
|
479
|
+
# For now, assume Pydantic handles basic fields based on type hints.
|
|
480
|
+
deserialized_basic_data = data # Assume Pydantic handles basic fields
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
# Create the agent instance using Pydantic's constructor
|
|
484
|
+
agent = cls(**deserialized_basic_data)
|
|
485
|
+
except Exception as e:
|
|
486
|
+
logger.error(
|
|
487
|
+
f"Pydantic validation/init failed for agent '{agent_name}': {e}",
|
|
488
|
+
exc_info=True,
|
|
489
|
+
)
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"Failed to initialize agent '{agent_name}' from dict: {e}"
|
|
492
|
+
) from e
|
|
493
|
+
|
|
494
|
+
# --- Deserialize and Attach Components ---
|
|
495
|
+
# Evaluator
|
|
496
|
+
if evaluator_data:
|
|
497
|
+
try:
|
|
498
|
+
agent.evaluator = deserialize_component(
|
|
499
|
+
evaluator_data, FlockEvaluator
|
|
500
|
+
)
|
|
501
|
+
if agent.evaluator is None:
|
|
502
|
+
raise ValueError("deserialize_component returned None")
|
|
503
|
+
logger.debug(
|
|
504
|
+
f"Deserialized evaluator '{agent.evaluator.name}' for agent '{agent_name}'"
|
|
505
|
+
)
|
|
506
|
+
except Exception as e:
|
|
507
|
+
logger.error(
|
|
508
|
+
f"Failed to deserialize evaluator for agent '{agent_name}': {e}",
|
|
509
|
+
exc_info=True,
|
|
510
|
+
)
|
|
511
|
+
# Decide: raise error or continue without evaluator?
|
|
512
|
+
# raise ValueError(f"Failed to deserialize evaluator for agent '{agent_name}': {e}") from e
|
|
513
|
+
|
|
514
|
+
# Router
|
|
515
|
+
if router_data:
|
|
516
|
+
try:
|
|
517
|
+
agent.handoff_router = deserialize_component(
|
|
518
|
+
router_data, FlockRouter
|
|
519
|
+
)
|
|
520
|
+
if agent.handoff_router is None:
|
|
521
|
+
raise ValueError("deserialize_component returned None")
|
|
522
|
+
logger.debug(
|
|
523
|
+
f"Deserialized router '{agent.handoff_router.name}' for agent '{agent_name}'"
|
|
524
|
+
)
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.error(
|
|
527
|
+
f"Failed to deserialize router for agent '{agent_name}': {e}",
|
|
528
|
+
exc_info=True,
|
|
529
|
+
)
|
|
530
|
+
# Decide: raise error or continue without router?
|
|
531
|
+
|
|
532
|
+
# Modules
|
|
533
|
+
if modules_data:
|
|
534
|
+
agent.modules = {} # Ensure it's initialized
|
|
535
|
+
for name, module_data in modules_data.items():
|
|
305
536
|
try:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
537
|
+
module_instance = deserialize_component(
|
|
538
|
+
module_data, FlockModule
|
|
539
|
+
)
|
|
540
|
+
if module_instance:
|
|
541
|
+
# Ensure instance name matches key if possible
|
|
542
|
+
module_instance.name = module_data.get("name", name)
|
|
543
|
+
agent.add_module(
|
|
544
|
+
module_instance
|
|
545
|
+
) # Use add_module for consistency
|
|
546
|
+
else:
|
|
547
|
+
raise ValueError("deserialize_component returned None")
|
|
548
|
+
except Exception as e:
|
|
549
|
+
logger.error(
|
|
550
|
+
f"Failed to deserialize module '{name}' for agent '{agent_name}': {e}",
|
|
551
|
+
exc_info=True,
|
|
552
|
+
)
|
|
553
|
+
# Decide: skip module or raise error?
|
|
554
|
+
|
|
555
|
+
# --- Deserialize Tools ---
|
|
556
|
+
agent.tools = [] # Initialize tools list
|
|
557
|
+
if tools_data:
|
|
558
|
+
for tool_ref in tools_data:
|
|
559
|
+
if (
|
|
560
|
+
isinstance(tool_ref, dict)
|
|
561
|
+
and "__callable_ref__" in tool_ref
|
|
562
|
+
):
|
|
563
|
+
path_str = tool_ref["__callable_ref__"]
|
|
564
|
+
try:
|
|
565
|
+
tool_func = FlockRegistry.get_callable(path_str)
|
|
566
|
+
agent.tools.append(tool_func)
|
|
567
|
+
except KeyError:
|
|
568
|
+
logger.error(
|
|
569
|
+
f"Tool callable '{path_str}' not found in registry for agent '{agent_name}'. Skipping."
|
|
570
|
+
)
|
|
571
|
+
else:
|
|
572
|
+
logger.warning(
|
|
573
|
+
f"Invalid tool format found during deserialization for agent '{agent_name}': {tool_ref}. Skipping."
|
|
574
|
+
)
|
|
325
575
|
|
|
576
|
+
logger.info(f"Successfully deserialized agent: {agent.name}")
|
|
326
577
|
return agent
|
|
578
|
+
|
|
579
|
+
# --- Pydantic v2 Configuration ---
|
|
580
|
+
class Config:
|
|
581
|
+
arbitrary_types_allowed = (
|
|
582
|
+
True # Important for components like evaluator, router etc.
|
|
583
|
+
)
|
|
584
|
+
# Might need custom json_encoders if not using model_dump(mode='json') everywhere
|
|
585
|
+
# json_encoders = {
|
|
586
|
+
# FlockEvaluator: lambda v: v.to_dict() if v else None,
|
|
587
|
+
# FlockRouter: lambda v: v.to_dict() if v else None,
|
|
588
|
+
# FlockModule: lambda v: v.to_dict() if v else None,
|
|
589
|
+
# }
|