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.

@@ -0,0 +1,717 @@
1
+ # src/flock/core/serialization/flock_serializer.py
2
+ """Handles serialization and deserialization logic for Flock instances."""
3
+
4
+ import builtins
5
+ import importlib
6
+ import importlib.util
7
+ import inspect
8
+ import os
9
+ import re
10
+ import sys
11
+ from dataclasses import is_dataclass
12
+ from typing import TYPE_CHECKING, Any, Literal
13
+
14
+ from pydantic import BaseModel, create_model
15
+
16
+ # Need registry access
17
+ from flock.core.flock_registry import get_registry
18
+ from flock.core.logging.logging import get_logger
19
+ from flock.core.serialization.serialization_utils import (
20
+ extract_pydantic_models_from_type_string, # Assuming this handles basic serialization needs
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from flock.core.flock import Flock
25
+
26
+
27
+ logger = get_logger("serialization.flock")
28
+ FlockRegistry = get_registry()
29
+
30
+
31
+ class FlockSerializer:
32
+ """Provides static methods for serializing and deserializing Flock instances."""
33
+
34
+ @staticmethod
35
+ def serialize(
36
+ flock_instance: "Flock",
37
+ path_type: Literal["absolute", "relative"] = "relative",
38
+ ) -> dict[str, Any]:
39
+ """Convert Flock instance to dictionary representation.
40
+
41
+ Args:
42
+ flock_instance: The Flock instance to serialize.
43
+ path_type: How file paths should be formatted ('absolute' or 'relative').
44
+ """
45
+ logger.debug(
46
+ f"Serializing Flock instance '{flock_instance.name}' to dict."
47
+ )
48
+ # Use Pydantic's dump for base fields defined in Flock's model
49
+ data = flock_instance.model_dump(mode="json", exclude_none=True)
50
+ logger.info(
51
+ f"Serializing Flock '{flock_instance.name}' with {len(flock_instance._agents)} agents"
52
+ )
53
+
54
+ data["agents"] = {}
55
+ custom_types = {}
56
+ components = {}
57
+
58
+ for name, agent_instance in flock_instance._agents.items():
59
+ try:
60
+ logger.debug(f"Serializing agent '{name}'")
61
+ # Agents handle their own serialization via their to_dict
62
+ agent_data = (
63
+ agent_instance.to_dict()
64
+ ) # This now uses the agent's refined to_dict
65
+ data["agents"][name] = agent_data
66
+
67
+ # --- Extract Types from Agent Signatures ---
68
+ input_types = []
69
+ if agent_instance.input:
70
+ input_types = FlockSerializer._extract_types_from_signature(
71
+ agent_instance.input
72
+ )
73
+ if input_types:
74
+ logger.debug(
75
+ f"Found input types in agent '{name}': {input_types}"
76
+ )
77
+
78
+ output_types = []
79
+ if agent_instance.output:
80
+ output_types = (
81
+ FlockSerializer._extract_types_from_signature(
82
+ agent_instance.output
83
+ )
84
+ )
85
+ if output_types:
86
+ logger.debug(
87
+ f"Found output types in agent '{name}': {output_types}"
88
+ )
89
+
90
+ all_types = set(input_types + output_types)
91
+ if all_types:
92
+ custom_types.update(
93
+ FlockSerializer._get_type_definitions(list(all_types))
94
+ )
95
+
96
+ # --- Extract Component Information ---
97
+ # Evaluator
98
+ if (
99
+ "evaluator" in agent_data
100
+ and agent_data["evaluator"]
101
+ and "type" in agent_data["evaluator"]
102
+ ):
103
+ component_type = agent_data["evaluator"]["type"]
104
+ if component_type not in components:
105
+ logger.debug(
106
+ f"Adding evaluator component '{component_type}' from agent '{name}'"
107
+ )
108
+ components[component_type] = (
109
+ FlockSerializer._get_component_definition(
110
+ component_type, path_type
111
+ )
112
+ )
113
+
114
+ # Modules
115
+ if "modules" in agent_data:
116
+ for module_name, module_data in agent_data[
117
+ "modules"
118
+ ].items():
119
+ if module_data and "type" in module_data:
120
+ component_type = module_data["type"]
121
+ if component_type not in components:
122
+ logger.debug(
123
+ f"Adding module component '{component_type}' from module '{module_name}' in agent '{name}'"
124
+ )
125
+ components[component_type] = (
126
+ FlockSerializer._get_component_definition(
127
+ component_type, path_type
128
+ )
129
+ )
130
+
131
+ # Router
132
+ if (
133
+ "handoff_router" in agent_data
134
+ and agent_data["handoff_router"]
135
+ and "type" in agent_data["handoff_router"]
136
+ ):
137
+ component_type = agent_data["handoff_router"]["type"]
138
+ if component_type not in components:
139
+ logger.debug(
140
+ f"Adding router component '{component_type}' from agent '{name}'"
141
+ )
142
+ components[component_type] = (
143
+ FlockSerializer._get_component_definition(
144
+ component_type, path_type
145
+ )
146
+ )
147
+
148
+ # Tools (Callables)
149
+ if agent_data.get("tools"):
150
+ logger.debug(
151
+ f"Extracting tool information from agent '{name}': {agent_data['tools']}"
152
+ )
153
+ tool_objs = (
154
+ agent_instance.tools if agent_instance.tools else []
155
+ )
156
+ for i, tool_name in enumerate(agent_data["tools"]):
157
+ if tool_name not in components and i < len(tool_objs):
158
+ tool = tool_objs[i]
159
+ if callable(tool) and not isinstance(tool, type):
160
+ path_str = (
161
+ FlockRegistry.get_callable_path_string(tool)
162
+ )
163
+ if path_str:
164
+ logger.debug(
165
+ f"Adding tool '{tool_name}' (from path '{path_str}') to components"
166
+ )
167
+ components[tool_name] = (
168
+ FlockSerializer._get_callable_definition(
169
+ path_str, tool_name, path_type
170
+ )
171
+ )
172
+
173
+ except Exception as e:
174
+ logger.error(
175
+ f"Failed to serialize agent '{name}' within Flock: {e}",
176
+ exc_info=True,
177
+ )
178
+
179
+ if custom_types:
180
+ logger.info(f"Adding {len(custom_types)} custom type definitions")
181
+ data["types"] = custom_types
182
+ if components:
183
+ logger.info(
184
+ f"Adding {len(components)} component/callable definitions"
185
+ )
186
+ data["components"] = components
187
+
188
+ data["dependencies"] = FlockSerializer._get_dependencies()
189
+ data["metadata"] = {
190
+ "path_type": path_type,
191
+ "flock_version": "0.4.0",
192
+ } # Example version
193
+
194
+ logger.debug(f"Flock '{flock_instance.name}' serialization complete.")
195
+ return data
196
+
197
+ @staticmethod
198
+ def deserialize(cls: type["Flock"], data: dict[str, Any]) -> "Flock":
199
+ """Create Flock instance from dictionary representation."""
200
+ # Import concrete types needed for instantiation
201
+ from flock.core.flock import Flock # Import the actual class
202
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
203
+
204
+ logger.debug(
205
+ f"Deserializing Flock from dict. Provided keys: {list(data.keys())}"
206
+ )
207
+
208
+ metadata = data.pop("metadata", {})
209
+ path_type = metadata.get(
210
+ "path_type", "relative"
211
+ ) # Default to relative for loading flexibility
212
+ logger.debug(
213
+ f"Using path_type '{path_type}' from metadata for component loading"
214
+ )
215
+
216
+ if "types" in data:
217
+ logger.info(f"Processing {len(data['types'])} type definitions")
218
+ FlockSerializer._register_type_definitions(data.pop("types"))
219
+
220
+ if "components" in data:
221
+ logger.info(
222
+ f"Processing {len(data['components'])} component/callable definitions"
223
+ )
224
+ FlockSerializer._register_component_definitions(
225
+ data.pop("components"), path_type
226
+ )
227
+
228
+ if "dependencies" in data:
229
+ logger.debug(f"Checking {len(data['dependencies'])} dependencies")
230
+ FlockSerializer._check_dependencies(data.pop("dependencies"))
231
+
232
+ agents_data = data.pop("agents", {})
233
+ logger.info(f"Found {len(agents_data)} agents to deserialize")
234
+
235
+ try:
236
+ # Pass only fields defined in Flock's Pydantic model to constructor
237
+ init_data = {
238
+ k: v for k, v in data.items() if k in Flock.model_fields
239
+ }
240
+ logger.debug(
241
+ f"Creating Flock instance with fields: {list(init_data.keys())}"
242
+ )
243
+ flock_instance = cls(**init_data) # Use cls which is Flock
244
+ except Exception as e:
245
+ logger.error(
246
+ f"Pydantic validation/init failed for Flock: {e}", exc_info=True
247
+ )
248
+ raise ValueError(
249
+ f"Failed to initialize Flock from dict: {e}"
250
+ ) from e
251
+
252
+ # Deserialize and add agents AFTER Flock instance exists
253
+ for name, agent_data in agents_data.items():
254
+ try:
255
+ logger.debug(f"Deserializing agent '{name}'")
256
+ agent_data.setdefault("name", name)
257
+ agent_instance = ConcreteFlockAgent.from_dict(agent_data)
258
+ flock_instance.add_agent(agent_instance)
259
+ logger.debug(f"Successfully added agent '{name}' to Flock")
260
+ except Exception as e:
261
+ logger.error(
262
+ f"Failed to deserialize/add agent '{name}': {e}",
263
+ exc_info=True,
264
+ )
265
+
266
+ logger.info(
267
+ f"Successfully deserialized Flock '{flock_instance.name}' with {len(flock_instance._agents)} agents"
268
+ )
269
+ return flock_instance
270
+
271
+ # --- Helper methods moved from Flock ---
272
+ # (Keep all the _extract..., _get..., _register..., _create... methods here)
273
+ # Ensure they use FlockSerializer._... or are standalone functions called directly.
274
+ # Make static if they don't need instance state (which they shouldn't here).
275
+
276
+ @staticmethod
277
+ def _extract_types_from_signature(signature: str) -> list[str]:
278
+ """Extract type names from an input/output signature string."""
279
+ if not signature:
280
+ return []
281
+ from flock.core.util.input_resolver import (
282
+ split_top_level, # Import locally if needed
283
+ )
284
+
285
+ type_names = set()
286
+ try:
287
+ parts = split_top_level(signature)
288
+ for part in parts:
289
+ if ":" in part:
290
+ type_str = part.split(":", 1)[1].split("|", 1)[0].strip()
291
+ # Use the more robust extractor
292
+ models = extract_pydantic_models_from_type_string(type_str)
293
+ for model in models:
294
+ type_names.add(model.__name__)
295
+ except Exception as e:
296
+ logger.warning(
297
+ f"Could not fully parse types from signature '{signature}': {e}"
298
+ )
299
+ return list(type_names)
300
+
301
+ @staticmethod
302
+ def _get_type_definitions(type_names: list[str]) -> dict[str, Any]:
303
+ """Get definitions for the specified custom types from the registry."""
304
+ type_definitions = {}
305
+ for type_name in type_names:
306
+ try:
307
+ type_obj = FlockRegistry.get_type(
308
+ type_name
309
+ ) # Throws KeyError if not found
310
+ type_def = FlockSerializer._extract_type_definition(
311
+ type_name, type_obj
312
+ )
313
+ if type_def:
314
+ type_definitions[type_name] = type_def
315
+ except KeyError:
316
+ logger.warning(
317
+ f"Type '{type_name}' requested but not found in registry."
318
+ )
319
+ except Exception as e:
320
+ logger.warning(
321
+ f"Could not extract definition for type {type_name}: {e}"
322
+ )
323
+ return type_definitions
324
+
325
+ @staticmethod
326
+ def _extract_type_definition(
327
+ type_name: str, type_obj: type
328
+ ) -> dict[str, Any] | None:
329
+ """Extract a definition for a custom type (Pydantic or Dataclass)."""
330
+ # Definition includes module path and schema/fields
331
+ module_path = getattr(type_obj, "__module__", "unknown")
332
+ type_def = {"module_path": module_path}
333
+ try:
334
+ if issubclass(type_obj, BaseModel):
335
+ type_def["type"] = "pydantic.BaseModel"
336
+ schema = type_obj.model_json_schema()
337
+ if "title" in schema and schema["title"] == type_name:
338
+ del schema["title"]
339
+ type_def["schema"] = schema
340
+ return type_def
341
+ elif is_dataclass(type_obj):
342
+ type_def["type"] = "dataclass"
343
+ fields = {}
344
+ for field_name, field in getattr(
345
+ type_obj, "__dataclass_fields__", {}
346
+ ).items():
347
+ # Attempt to get a string representation of the type
348
+ try:
349
+ type_repr = str(field.type)
350
+ except Exception:
351
+ type_repr = "unknown"
352
+ fields[field_name] = {
353
+ "type": type_repr,
354
+ "default": str(field.default)
355
+ if field.default is not inspect.Parameter.empty
356
+ else None,
357
+ }
358
+ type_def["fields"] = fields
359
+ return type_def
360
+ else:
361
+ logger.debug(
362
+ f"Type '{type_name}' is not Pydantic or Dataclass, skipping detailed definition."
363
+ )
364
+ return (
365
+ None # Don't include non-data types in the 'types' section
366
+ )
367
+ except Exception as e:
368
+ logger.warning(f"Error extracting definition for {type_name}: {e}")
369
+ return None
370
+
371
+ @staticmethod
372
+ def _get_component_definition(
373
+ component_type_name: str, path_type: Literal["absolute", "relative"]
374
+ ) -> dict[str, Any]:
375
+ """Get definition for a component type from the registry."""
376
+ component_def = {
377
+ "type": "flock_component",
378
+ "module_path": "unknown",
379
+ "file_path": None,
380
+ }
381
+ try:
382
+ component_class = FlockRegistry.get_component(
383
+ component_type_name
384
+ ) # Raises KeyError if not found
385
+ component_def["module_path"] = getattr(
386
+ component_class, "__module__", "unknown"
387
+ )
388
+ component_def["description"] = (
389
+ inspect.getdoc(component_class)
390
+ or f"{component_type_name} component"
391
+ )
392
+
393
+ # Get file path
394
+ try:
395
+ file_path_abs = inspect.getfile(component_class)
396
+ component_def["file_path"] = (
397
+ os.path.relpath(file_path_abs)
398
+ if path_type == "relative"
399
+ else file_path_abs
400
+ )
401
+ except (TypeError, ValueError) as e:
402
+ logger.debug(
403
+ f"Could not determine file path for component {component_type_name}: {e}"
404
+ )
405
+
406
+ except KeyError:
407
+ logger.warning(
408
+ f"Component class '{component_type_name}' not found in registry."
409
+ )
410
+ component_def["description"] = (
411
+ f"{component_type_name} component (class not found in registry)"
412
+ )
413
+ except Exception as e:
414
+ logger.warning(
415
+ f"Could not extract full definition for component {component_type_name}: {e}"
416
+ )
417
+ return component_def
418
+
419
+ @staticmethod
420
+ def _get_callable_definition(
421
+ callable_path: str,
422
+ func_name: str,
423
+ path_type: Literal["absolute", "relative"],
424
+ ) -> dict[str, Any]:
425
+ """Get definition for a callable using its registry path."""
426
+ callable_def = {
427
+ "type": "flock_callable",
428
+ "module_path": "unknown",
429
+ "file_path": None,
430
+ }
431
+ try:
432
+ func = FlockRegistry.get_callable(
433
+ callable_path
434
+ ) # Raises KeyError if not found
435
+ callable_def["module_path"] = getattr(func, "__module__", "unknown")
436
+ callable_def["description"] = (
437
+ inspect.getdoc(func) or f"Callable function {func_name}"
438
+ )
439
+ # Get file path
440
+ try:
441
+ file_path_abs = inspect.getfile(func)
442
+ callable_def["file_path"] = (
443
+ os.path.relpath(file_path_abs)
444
+ if path_type == "relative"
445
+ else file_path_abs
446
+ )
447
+ except (TypeError, ValueError) as e:
448
+ logger.debug(
449
+ f"Could not determine file path for callable {callable_path}: {e}"
450
+ )
451
+
452
+ except KeyError:
453
+ logger.warning(
454
+ f"Callable '{callable_path}' (for tool '{func_name}') not found in registry."
455
+ )
456
+ callable_def["description"] = (
457
+ f"Callable {func_name} (function not found in registry)"
458
+ )
459
+ except Exception as e:
460
+ logger.warning(
461
+ f"Could not extract full definition for callable {callable_path}: {e}"
462
+ )
463
+ return callable_def
464
+
465
+ @staticmethod
466
+ def _get_dependencies() -> list[str]:
467
+ """Get list of core dependencies required by Flock."""
468
+ # Basic static list for now
469
+ return [
470
+ "pydantic>=2.0.0",
471
+ "flock-core>=0.4.0",
472
+ ] # Update version as needed
473
+
474
+ @staticmethod
475
+ def _register_type_definitions(type_defs: dict[str, Any]) -> None:
476
+ """Register type definitions from serialized data."""
477
+ # (Logic remains largely the same as original, ensure it uses FlockRegistry)
478
+ for type_name, type_def in type_defs.items():
479
+ logger.debug(f"Registering type definition for: {type_name}")
480
+ # Prioritize direct import
481
+ module_path = type_def.get("module_path")
482
+ registered = False
483
+ if module_path and module_path != "unknown":
484
+ try:
485
+ module = importlib.import_module(module_path)
486
+ if hasattr(module, type_name):
487
+ type_obj = getattr(module, type_name)
488
+ FlockRegistry.register_type(type_obj, type_name)
489
+ logger.info(
490
+ f"Registered type '{type_name}' from module '{module_path}'"
491
+ )
492
+ registered = True
493
+ except ImportError:
494
+ logger.debug(
495
+ f"Could not import module {module_path} for type {type_name}"
496
+ )
497
+ except Exception as e:
498
+ logger.warning(
499
+ f"Error registering type {type_name} from module: {e}"
500
+ )
501
+
502
+ if registered:
503
+ continue
504
+
505
+ # Attempt dynamic creation if direct import failed or wasn't possible
506
+ type_kind = type_def.get("type")
507
+ if type_kind == "pydantic.BaseModel" and "schema" in type_def:
508
+ FlockSerializer._create_pydantic_model(type_name, type_def)
509
+ elif type_kind == "dataclass" and "fields" in type_def:
510
+ FlockSerializer._create_dataclass(type_name, type_def)
511
+ else:
512
+ logger.warning(
513
+ f"Cannot dynamically register type '{type_name}' with kind '{type_kind}'"
514
+ )
515
+
516
+ @staticmethod
517
+ def _create_pydantic_model(
518
+ type_name: str, type_def: dict[str, Any]
519
+ ) -> None:
520
+ """Dynamically create and register a Pydantic model from schema."""
521
+ # (Logic remains the same, ensure it uses FlockRegistry.register_type)
522
+ schema = type_def.get("schema", {})
523
+ try:
524
+ fields = {}
525
+ properties = schema.get("properties", {})
526
+ required = schema.get("required", [])
527
+ for field_name, field_schema in properties.items():
528
+ field_type = FlockSerializer._get_type_from_schema(field_schema)
529
+ default = ... if field_name in required else None
530
+ fields[field_name] = (field_type, default)
531
+
532
+ DynamicModel = create_model(type_name, **fields)
533
+ FlockRegistry.register_type(DynamicModel, type_name)
534
+ logger.info(
535
+ f"Dynamically created and registered Pydantic model: {type_name}"
536
+ )
537
+ except Exception as e:
538
+ logger.error(f"Failed to create Pydantic model {type_name}: {e}")
539
+
540
+ @staticmethod
541
+ def _get_type_from_schema(field_schema: dict[str, Any]) -> Any:
542
+ """Convert JSON schema type to Python type."""
543
+ # (Logic remains the same)
544
+ schema_type = field_schema.get("type")
545
+ type_mapping = {
546
+ "string": str,
547
+ "integer": int,
548
+ "number": float,
549
+ "boolean": bool,
550
+ "array": list,
551
+ "object": dict,
552
+ }
553
+ if schema_type in type_mapping:
554
+ return type_mapping[schema_type]
555
+ if "enum" in field_schema:
556
+ from typing import Literal
557
+
558
+ return Literal[tuple(field_schema["enum"])] # type: ignore
559
+ return Any
560
+
561
+ @staticmethod
562
+ def _create_dataclass(type_name: str, type_def: dict[str, Any]) -> None:
563
+ """Dynamically create and register a dataclass."""
564
+ # (Logic remains the same, ensure it uses FlockRegistry.register_type)
565
+ from dataclasses import make_dataclass
566
+
567
+ fields_def = type_def.get("fields", {})
568
+ try:
569
+ fields = []
570
+ for field_name, field_props in fields_def.items():
571
+ # Safely evaluate type string - requires care!
572
+ field_type_str = field_props.get("type", "str")
573
+ try:
574
+ field_type = eval(
575
+ field_type_str,
576
+ {"__builtins__": builtins.__dict__},
577
+ {"List": list, "Dict": dict},
578
+ ) # Allow basic types
579
+ except Exception:
580
+ field_type = Any
581
+ fields.append((field_name, field_type))
582
+
583
+ DynamicDataclass = make_dataclass(type_name, fields)
584
+ FlockRegistry.register_type(DynamicDataclass, type_name)
585
+ logger.info(
586
+ f"Dynamically created and registered dataclass: {type_name}"
587
+ )
588
+ except Exception as e:
589
+ logger.error(f"Failed to create dataclass {type_name}: {e}")
590
+
591
+ @staticmethod
592
+ def _register_component_definitions(
593
+ component_defs: dict[str, Any],
594
+ path_type: Literal["absolute", "relative"],
595
+ ) -> None:
596
+ """Register component/callable definitions from serialized data."""
597
+ # (Logic remains the same, ensure it uses FlockRegistry.register_component/register_callable)
598
+ # Key change: Ensure file_path is handled correctly based on path_type from metadata
599
+ for name, comp_def in component_defs.items():
600
+ logger.debug(
601
+ f"Registering component/callable definition for: {name}"
602
+ )
603
+ kind = comp_def.get("type")
604
+ module_path = comp_def.get("module_path")
605
+ file_path = comp_def.get("file_path")
606
+ registered = False
607
+
608
+ # Resolve file path if relative
609
+ if (
610
+ path_type == "relative"
611
+ and file_path
612
+ and not os.path.isabs(file_path)
613
+ ):
614
+ abs_file_path = os.path.abspath(file_path)
615
+ logger.debug(
616
+ f"Resolved relative path '{file_path}' to absolute '{abs_file_path}'"
617
+ )
618
+ file_path = abs_file_path # Use absolute path for loading
619
+
620
+ # 1. Try importing from module_path
621
+ if module_path and module_path != "unknown":
622
+ try:
623
+ module = importlib.import_module(module_path)
624
+ if hasattr(module, name):
625
+ obj = getattr(module, name)
626
+ if kind == "flock_callable" and callable(obj):
627
+ FlockRegistry.register_callable(
628
+ obj, name
629
+ ) # Register by simple name
630
+ # Also register by full path if possible
631
+ full_path = f"{module_path}.{name}"
632
+ if full_path != name:
633
+ FlockRegistry.register_callable(obj, full_path)
634
+ logger.info(
635
+ f"Registered callable '{name}' from module '{module_path}'"
636
+ )
637
+ registered = True
638
+ elif kind == "flock_component" and isinstance(
639
+ obj, type
640
+ ):
641
+ FlockRegistry.register_component(obj, name)
642
+ logger.info(
643
+ f"Registered component '{name}' from module '{module_path}'"
644
+ )
645
+ registered = True
646
+ except (ImportError, AttributeError):
647
+ logger.debug(
648
+ f"Could not import '{name}' from module '{module_path}', trying file path."
649
+ )
650
+ except Exception as e:
651
+ logger.warning(
652
+ f"Error registering '{name}' from module '{module_path}': {e}"
653
+ )
654
+
655
+ if registered:
656
+ continue
657
+
658
+ # 2. Try importing from file_path if module import failed or wasn't possible
659
+ if file_path and os.path.exists(file_path):
660
+ logger.debug(
661
+ f"Attempting to load '{name}' from file: {file_path}"
662
+ )
663
+ try:
664
+ mod_name = f"flock_dynamic_{name}" # Unique module name
665
+ spec = importlib.util.spec_from_file_location(
666
+ mod_name, file_path
667
+ )
668
+ if spec and spec.loader:
669
+ module = importlib.util.module_from_spec(spec)
670
+ sys.modules[spec.name] = (
671
+ module # Important for pickle/cloudpickle
672
+ )
673
+ spec.loader.exec_module(module)
674
+ if hasattr(module, name):
675
+ obj = getattr(module, name)
676
+ if kind == "flock_callable" and callable(obj):
677
+ FlockRegistry.register_callable(obj, name)
678
+ logger.info(
679
+ f"Registered callable '{name}' from file '{file_path}'"
680
+ )
681
+ elif kind == "flock_component" and isinstance(
682
+ obj, type
683
+ ):
684
+ FlockRegistry.register_component(obj, name)
685
+ logger.info(
686
+ f"Registered component '{name}' from file '{file_path}'"
687
+ )
688
+ else:
689
+ logger.warning(
690
+ f"'{name}' not found in loaded file '{file_path}'"
691
+ )
692
+ else:
693
+ logger.warning(
694
+ f"Could not create import spec for file '{file_path}'"
695
+ )
696
+ except Exception as e:
697
+ logger.error(
698
+ f"Error loading '{name}' from file '{file_path}': {e}",
699
+ exc_info=True,
700
+ )
701
+ elif not registered:
702
+ logger.warning(
703
+ f"Could not register '{name}'. No valid module or file path found."
704
+ )
705
+
706
+ @staticmethod
707
+ def _check_dependencies(dependencies: list[str]) -> None:
708
+ """Check if required dependencies are available (basic check)."""
709
+ # (Logic remains the same)
710
+ for dep in dependencies:
711
+ match = re.match(r"([^>=<]+)", dep)
712
+ if match:
713
+ pkg_name = match.group(1).replace("-", "_")
714
+ try:
715
+ importlib.import_module(pkg_name)
716
+ except ImportError:
717
+ logger.warning(f"Dependency '{dep}' might be missing.")