flock-core 0.3.22__py3-none-any.whl → 0.3.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

flock/core/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
- class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
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, description="The model to use (e.g., 'openai/gpt-4o')."
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
- "", description="A human-readable description of the agent."
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
- "A comma-separated list of input keys. Optionally supports type hints (:) and descriptions (|). "
41
- "For example: 'query: str | The search query, chapter_list: list[str] | The chapter list of the document'."
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
- "A comma-separated list of output keys. Optionally supports type hints (:) and descriptions (|). "
48
- "For example: 'result|The generated result, summary|A brief summary'."
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
- tools: list[Callable[..., Any] | Any] | None = Field(
53
- default=None,
54
- description="An optional list of callable tools that the agent can leverage during execution.",
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="Set to True to enable caching of the agent's results.",
78
+ description="Enable caching for the agent's evaluator (if supported).",
60
79
  )
61
80
 
62
- handoff_router: FlockRouter | None = Field(
81
+ # --- Components ---
82
+ evaluator: FlockEvaluator | None = Field( # Make optional, allow None
63
83
  default=None,
64
- description="Router to use for determining the next agent in the workflow.",
84
+ description="The evaluator instance defining the agent's core logic.",
65
85
  )
66
-
67
- evaluator: FlockEvaluator = Field(
68
- None,
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
- description="Context associated with flock",
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 | None]:
96
- """Get a module by name."""
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 hooks
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
- inputs = await module.pre_evaluate(self, inputs, self.context)
216
+ current_inputs = await module.pre_evaluate(
217
+ self, current_inputs, self.context
218
+ )
164
219
 
220
+ # Actual evaluation
165
221
  try:
166
- result = await self.evaluator.evaluate(self, inputs, self.tools)
167
-
168
- for module in self.get_enabled_modules():
169
- result = await module.post_evaluate(
170
- self, inputs, result, self.context
171
- )
172
-
173
- span.set_attribute("result", str(result))
174
-
175
- logger.info("Evaluation successful", agent=self.name)
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 evaluation",
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
- raise
185
-
186
- def save_to_file(self, file_path: str | None = None) -> None:
187
- """Save the serialized agent to a file."""
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
- # create all needed directories
193
- path = os.path.dirname(file_path)
194
- if path:
195
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
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
- with open(file_path, "w") as file:
198
- file.write(json.dumps(dict_data))
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
- """Run the agent with the given inputs and return its generated output."""
210
- return asyncio.run(self.run_async(inputs))
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.model = model
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
- await self.on_error(run_error, inputs)
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
- def resolve_callables(self, context) -> None:
275
- if isinstance(self.input, Callable):
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 isinstance(self.output, Callable):
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
- def to_dict(self) -> dict[str, Any]:
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
- data = self.model_dump()
293
- module_data = {}
294
- for name, module in self.modules.items():
295
- module_data[name] = module.dict()
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
- data["modules"] = module_data
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
- return convert_callable(data)
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
- def convert_callable(obj: Any) -> Any:
304
- if isinstance(obj, str) and len(obj) > 2:
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
- return cloudpickle.loads(bytes.fromhex(obj))
307
- except Exception:
308
- return obj
309
- if isinstance(obj, list):
310
- return [convert_callable(item) for item in obj]
311
- if isinstance(obj, dict):
312
- return {k: convert_callable(v) for k, v in obj.items()}
313
- return obj
314
-
315
- module_data = data.pop("modules", {})
316
- converted = convert_callable(data)
317
- agent = cls(**converted)
318
-
319
- for name, module_dict in module_data.items():
320
- module_type = module_dict.pop("type", None)
321
- if module_type:
322
- module_class = globals()[module_type]
323
- module = module_class(**module_dict)
324
- agent.add_module(module)
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
+ # }