flock-core 0.3.41__py3-none-any.whl → 0.4.0b2__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 +31 -0
- flock/cli/create_flock.py +58 -3
- flock/cli/load_flock.py +135 -1
- flock/cli/registry_management.py +367 -96
- flock/cli/yaml_editor.py +119 -6
- flock/core/__init__.py +13 -1
- flock/core/flock.py +918 -49
- flock/core/flock_agent.py +114 -22
- flock/core/flock_registry.py +37 -5
- flock/core/serialization/serializable.py +35 -8
- flock/core/serialization/serialization_utils.py +96 -1
- flock/core/util/cli_helper.py +2 -2
- flock/core/util/file_path_utils.py +223 -0
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b2.dist-info}/METADATA +1 -1
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b2.dist-info}/RECORD +18 -17
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b2.dist-info}/WHEEL +0 -0
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b2.dist-info}/entry_points.txt +0 -0
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b2.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_agent.py
CHANGED
|
@@ -374,10 +374,12 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
374
374
|
mode="json", # Use json mode for better handling of standard types by Pydantic
|
|
375
375
|
exclude_none=True, # Exclude None values for cleaner output
|
|
376
376
|
)
|
|
377
|
+
logger.debug(f"Base agent data for '{self.name}': {list(data.keys())}")
|
|
377
378
|
|
|
378
379
|
# --- Serialize Components using Registry Type Names ---
|
|
379
380
|
# Evaluator
|
|
380
381
|
if self.evaluator:
|
|
382
|
+
logger.debug(f"Serializing evaluator for agent '{self.name}'")
|
|
381
383
|
evaluator_type_name = FlockRegistry.get_component_type_name(
|
|
382
384
|
type(self.evaluator)
|
|
383
385
|
)
|
|
@@ -388,6 +390,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
388
390
|
)
|
|
389
391
|
evaluator_dict["type"] = evaluator_type_name # Add type marker
|
|
390
392
|
data["evaluator"] = evaluator_dict
|
|
393
|
+
logger.debug(
|
|
394
|
+
f"Added evaluator of type '{evaluator_type_name}' to agent '{self.name}'"
|
|
395
|
+
)
|
|
391
396
|
else:
|
|
392
397
|
logger.warning(
|
|
393
398
|
f"Could not get registered type name for evaluator {type(self.evaluator).__name__} in agent '{self.name}'. Skipping serialization."
|
|
@@ -395,6 +400,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
395
400
|
|
|
396
401
|
# Router
|
|
397
402
|
if self.handoff_router:
|
|
403
|
+
logger.debug(f"Serializing router for agent '{self.name}'")
|
|
398
404
|
router_type_name = FlockRegistry.get_component_type_name(
|
|
399
405
|
type(self.handoff_router)
|
|
400
406
|
)
|
|
@@ -406,6 +412,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
406
412
|
)
|
|
407
413
|
router_dict["type"] = router_type_name
|
|
408
414
|
data["handoff_router"] = router_dict
|
|
415
|
+
logger.debug(
|
|
416
|
+
f"Added router of type '{router_type_name}' to agent '{self.name}'"
|
|
417
|
+
)
|
|
409
418
|
else:
|
|
410
419
|
logger.warning(
|
|
411
420
|
f"Could not get registered type name for router {type(self.handoff_router).__name__} in agent '{self.name}'. Skipping serialization."
|
|
@@ -413,6 +422,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
413
422
|
|
|
414
423
|
# Modules
|
|
415
424
|
if self.modules:
|
|
425
|
+
logger.debug(
|
|
426
|
+
f"Serializing {len(self.modules)} modules for agent '{self.name}'"
|
|
427
|
+
)
|
|
416
428
|
serialized_modules = {}
|
|
417
429
|
for name, module_instance in self.modules.items():
|
|
418
430
|
module_type_name = FlockRegistry.get_component_type_name(
|
|
@@ -426,32 +438,55 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
426
438
|
)
|
|
427
439
|
module_dict["type"] = module_type_name
|
|
428
440
|
serialized_modules[name] = module_dict
|
|
441
|
+
logger.debug(
|
|
442
|
+
f"Added module '{name}' of type '{module_type_name}' to agent '{self.name}'"
|
|
443
|
+
)
|
|
429
444
|
else:
|
|
430
445
|
logger.warning(
|
|
431
446
|
f"Could not get registered type name for module {type(module_instance).__name__} ('{name}') in agent '{self.name}'. Skipping."
|
|
432
447
|
)
|
|
433
448
|
if serialized_modules:
|
|
434
449
|
data["modules"] = serialized_modules
|
|
450
|
+
logger.debug(
|
|
451
|
+
f"Added {len(serialized_modules)} modules to agent '{self.name}'"
|
|
452
|
+
)
|
|
435
453
|
|
|
436
454
|
# --- Serialize Tools (Callables) ---
|
|
437
455
|
if self.tools:
|
|
456
|
+
logger.debug(
|
|
457
|
+
f"Serializing {len(self.tools)} tools for agent '{self.name}'"
|
|
458
|
+
)
|
|
438
459
|
serialized_tools = []
|
|
439
460
|
for tool in self.tools:
|
|
440
461
|
if callable(tool) and not isinstance(tool, type):
|
|
441
462
|
path_str = FlockRegistry.get_callable_path_string(tool)
|
|
442
463
|
if path_str:
|
|
443
|
-
|
|
464
|
+
# Get just the function name from the path string
|
|
465
|
+
# If it's a namespaced path like module.submodule.function_name
|
|
466
|
+
# Just use the function_name part
|
|
467
|
+
func_name = path_str.split(".")[-1]
|
|
468
|
+
serialized_tools.append(func_name)
|
|
469
|
+
logger.debug(
|
|
470
|
+
f"Added tool '{func_name}' (from path '{path_str}') to agent '{self.name}'"
|
|
471
|
+
)
|
|
444
472
|
else:
|
|
445
473
|
logger.warning(
|
|
446
474
|
f"Could not get path string for tool {tool} in agent '{self.name}'. Skipping."
|
|
447
475
|
)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
476
|
+
else:
|
|
477
|
+
logger.warning(
|
|
478
|
+
f"Non-callable item found in tools list for agent '{self.name}': {tool}. Skipping."
|
|
479
|
+
)
|
|
451
480
|
if serialized_tools:
|
|
452
481
|
data["tools"] = serialized_tools
|
|
482
|
+
logger.debug(
|
|
483
|
+
f"Added {len(serialized_tools)} tools to agent '{self.name}'"
|
|
484
|
+
)
|
|
453
485
|
|
|
454
486
|
# No need to call _filter_none_values here as model_dump(exclude_none=True) handles it
|
|
487
|
+
logger.info(
|
|
488
|
+
f"Serialization of agent '{self.name}' complete with {len(data)} fields"
|
|
489
|
+
)
|
|
455
490
|
return data
|
|
456
491
|
|
|
457
492
|
@classmethod
|
|
@@ -466,6 +501,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
466
501
|
raise ValueError("Agent data must include a 'name' field.")
|
|
467
502
|
FlockRegistry = get_registry()
|
|
468
503
|
agent_name = data["name"] # For logging context
|
|
504
|
+
logger.info(f"Deserializing agent '{agent_name}'")
|
|
469
505
|
|
|
470
506
|
# Pop complex components to handle them after basic agent instantiation
|
|
471
507
|
evaluator_data = data.pop("evaluator", None)
|
|
@@ -473,6 +509,10 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
473
509
|
modules_data = data.pop("modules", {})
|
|
474
510
|
tools_data = data.pop("tools", [])
|
|
475
511
|
|
|
512
|
+
logger.debug(
|
|
513
|
+
f"Agent '{agent_name}' has {len(modules_data)} modules and {len(tools_data)} tools"
|
|
514
|
+
)
|
|
515
|
+
|
|
476
516
|
# Deserialize remaining data recursively (handles nested basic types/callables)
|
|
477
517
|
# Note: Pydantic v2 handles most basic deserialization well if types match.
|
|
478
518
|
# Explicit deserialize_item might be needed if complex non-pydantic structures exist.
|
|
@@ -481,6 +521,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
481
521
|
|
|
482
522
|
try:
|
|
483
523
|
# Create the agent instance using Pydantic's constructor
|
|
524
|
+
logger.debug(
|
|
525
|
+
f"Creating agent instance with fields: {list(deserialized_basic_data.keys())}"
|
|
526
|
+
)
|
|
484
527
|
agent = cls(**deserialized_basic_data)
|
|
485
528
|
except Exception as e:
|
|
486
529
|
logger.error(
|
|
@@ -495,13 +538,16 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
495
538
|
# Evaluator
|
|
496
539
|
if evaluator_data:
|
|
497
540
|
try:
|
|
541
|
+
logger.debug(
|
|
542
|
+
f"Deserializing evaluator for agent '{agent_name}'"
|
|
543
|
+
)
|
|
498
544
|
agent.evaluator = deserialize_component(
|
|
499
545
|
evaluator_data, FlockEvaluator
|
|
500
546
|
)
|
|
501
547
|
if agent.evaluator is None:
|
|
502
548
|
raise ValueError("deserialize_component returned None")
|
|
503
549
|
logger.debug(
|
|
504
|
-
f"Deserialized evaluator '{agent.evaluator.name}' for agent '{agent_name}'"
|
|
550
|
+
f"Deserialized evaluator '{agent.evaluator.name}' of type '{evaluator_data.get('type')}' for agent '{agent_name}'"
|
|
505
551
|
)
|
|
506
552
|
except Exception as e:
|
|
507
553
|
logger.error(
|
|
@@ -514,13 +560,14 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
514
560
|
# Router
|
|
515
561
|
if router_data:
|
|
516
562
|
try:
|
|
563
|
+
logger.debug(f"Deserializing router for agent '{agent_name}'")
|
|
517
564
|
agent.handoff_router = deserialize_component(
|
|
518
565
|
router_data, FlockRouter
|
|
519
566
|
)
|
|
520
567
|
if agent.handoff_router is None:
|
|
521
568
|
raise ValueError("deserialize_component returned None")
|
|
522
569
|
logger.debug(
|
|
523
|
-
f"Deserialized router '{agent.handoff_router.name}' for agent '{agent_name}'"
|
|
570
|
+
f"Deserialized router '{agent.handoff_router.name}' of type '{router_data.get('type')}' for agent '{agent_name}'"
|
|
524
571
|
)
|
|
525
572
|
except Exception as e:
|
|
526
573
|
logger.error(
|
|
@@ -532,8 +579,14 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
532
579
|
# Modules
|
|
533
580
|
if modules_data:
|
|
534
581
|
agent.modules = {} # Ensure it's initialized
|
|
582
|
+
logger.debug(
|
|
583
|
+
f"Deserializing {len(modules_data)} modules for agent '{agent_name}'"
|
|
584
|
+
)
|
|
535
585
|
for name, module_data in modules_data.items():
|
|
536
586
|
try:
|
|
587
|
+
logger.debug(
|
|
588
|
+
f"Deserializing module '{name}' of type '{module_data.get('type')}' for agent '{agent_name}'"
|
|
589
|
+
)
|
|
537
590
|
module_instance = deserialize_component(
|
|
538
591
|
module_data, FlockModule
|
|
539
592
|
)
|
|
@@ -543,6 +596,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
543
596
|
agent.add_module(
|
|
544
597
|
module_instance
|
|
545
598
|
) # Use add_module for consistency
|
|
599
|
+
logger.debug(
|
|
600
|
+
f"Successfully added module '{name}' to agent '{agent_name}'"
|
|
601
|
+
)
|
|
546
602
|
else:
|
|
547
603
|
raise ValueError("deserialize_component returned None")
|
|
548
604
|
except Exception as e:
|
|
@@ -555,25 +611,61 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
555
611
|
# --- Deserialize Tools ---
|
|
556
612
|
agent.tools = [] # Initialize tools list
|
|
557
613
|
if tools_data:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
614
|
+
# Get component registry to look up function imports
|
|
615
|
+
registry = get_registry()
|
|
616
|
+
components = getattr(registry, "_callables", {})
|
|
617
|
+
logger.debug(
|
|
618
|
+
f"Deserializing {len(tools_data)} tools for agent '{agent_name}'"
|
|
619
|
+
)
|
|
620
|
+
logger.debug(
|
|
621
|
+
f"Available callables in registry: {list(components.keys())}"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
for tool_name in tools_data:
|
|
625
|
+
try:
|
|
626
|
+
logger.debug(f"Looking for tool '{tool_name}' in registry")
|
|
627
|
+
# First try to lookup by simple name in the registry's callables
|
|
628
|
+
found = False
|
|
629
|
+
for path_str, func in components.items():
|
|
630
|
+
if (
|
|
631
|
+
path_str.endswith("." + tool_name)
|
|
632
|
+
or path_str == tool_name
|
|
633
|
+
):
|
|
634
|
+
agent.tools.append(func)
|
|
635
|
+
found = True
|
|
636
|
+
logger.info(
|
|
637
|
+
f"Found tool '{tool_name}' via path '{path_str}' for agent '{agent_name}'"
|
|
638
|
+
)
|
|
639
|
+
break
|
|
640
|
+
|
|
641
|
+
# If not found by simple name, try manual import
|
|
642
|
+
if not found:
|
|
643
|
+
logger.debug(
|
|
644
|
+
f"Attempting to import tool '{tool_name}' from modules"
|
|
570
645
|
)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
646
|
+
# Check in relevant modules (could be customized based on project structure)
|
|
647
|
+
import __main__
|
|
648
|
+
|
|
649
|
+
if hasattr(__main__, tool_name):
|
|
650
|
+
agent.tools.append(getattr(__main__, tool_name))
|
|
651
|
+
found = True
|
|
652
|
+
logger.info(
|
|
653
|
+
f"Found tool '{tool_name}' in __main__ module for agent '{agent_name}'"
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
if not found:
|
|
657
|
+
logger.warning(
|
|
658
|
+
f"Could not find tool '{tool_name}' for agent '{agent_name}'"
|
|
659
|
+
)
|
|
660
|
+
except Exception as e:
|
|
661
|
+
logger.error(
|
|
662
|
+
f"Error adding tool '{tool_name}' to agent '{agent_name}': {e}",
|
|
663
|
+
exc_info=True,
|
|
574
664
|
)
|
|
575
665
|
|
|
576
|
-
logger.info(
|
|
666
|
+
logger.info(
|
|
667
|
+
f"Successfully deserialized agent '{agent_name}' with {len(agent.modules)} modules and {len(agent.tools)} tools"
|
|
668
|
+
)
|
|
577
669
|
return agent
|
|
578
670
|
|
|
579
671
|
# --- Pydantic v2 Configuration ---
|
flock/core/flock_registry.py
CHANGED
|
@@ -162,16 +162,20 @@ class FlockRegistry:
|
|
|
162
162
|
and self._callables[path_str] != func
|
|
163
163
|
):
|
|
164
164
|
logger.warning(
|
|
165
|
-
f"Callable '{path_str}' already registered. Overwriting."
|
|
165
|
+
f"Callable '{path_str}' already registered with a different function. Overwriting."
|
|
166
166
|
)
|
|
167
167
|
self._callables[path_str] = func
|
|
168
|
-
logger.debug(f"Registered callable: {path_str}")
|
|
168
|
+
logger.debug(f"Registered callable: '{path_str}' ({func.__name__})")
|
|
169
169
|
return path_str
|
|
170
|
+
logger.warning(
|
|
171
|
+
f"Could not register callable {func.__name__}: Unable to determine path string"
|
|
172
|
+
)
|
|
170
173
|
return None
|
|
171
174
|
|
|
172
175
|
def get_callable(self, path_str: str) -> Callable:
|
|
173
176
|
"""Retrieves a callable by its path string, attempting dynamic import if not found."""
|
|
174
177
|
if path_str in self._callables:
|
|
178
|
+
logger.debug(f"Found callable '{path_str}' in registry")
|
|
175
179
|
return self._callables[path_str]
|
|
176
180
|
|
|
177
181
|
logger.debug(
|
|
@@ -179,14 +183,20 @@ class FlockRegistry:
|
|
|
179
183
|
)
|
|
180
184
|
try:
|
|
181
185
|
if "." not in path_str: # Built-ins
|
|
186
|
+
logger.debug(f"Trying to import built-in callable '{path_str}'")
|
|
182
187
|
builtins_module = importlib.import_module("builtins")
|
|
183
188
|
if hasattr(builtins_module, path_str):
|
|
184
189
|
func = getattr(builtins_module, path_str)
|
|
185
190
|
if callable(func):
|
|
186
191
|
self.register_callable(func, path_str) # Cache it
|
|
192
|
+
logger.info(
|
|
193
|
+
f"Successfully imported built-in callable '{path_str}'"
|
|
194
|
+
)
|
|
187
195
|
return func
|
|
196
|
+
logger.error(f"Built-in callable '{path_str}' not found.")
|
|
188
197
|
raise KeyError(f"Built-in callable '{path_str}' not found.")
|
|
189
198
|
|
|
199
|
+
logger.debug(f"Trying to import module callable '{path_str}'")
|
|
190
200
|
module_name, func_name = path_str.rsplit(".", 1)
|
|
191
201
|
module = importlib.import_module(module_name)
|
|
192
202
|
func = getattr(module, func_name)
|
|
@@ -194,14 +204,21 @@ class FlockRegistry:
|
|
|
194
204
|
self.register_callable(
|
|
195
205
|
func, path_str
|
|
196
206
|
) # Cache dynamically imported
|
|
207
|
+
logger.info(
|
|
208
|
+
f"Successfully imported module callable '{path_str}'"
|
|
209
|
+
)
|
|
197
210
|
return func
|
|
198
211
|
else:
|
|
212
|
+
logger.error(
|
|
213
|
+
f"Dynamically imported object '{path_str}' is not callable."
|
|
214
|
+
)
|
|
199
215
|
raise TypeError(
|
|
200
216
|
f"Dynamically imported object '{path_str}' is not callable."
|
|
201
217
|
)
|
|
202
218
|
except (ImportError, AttributeError, KeyError, TypeError) as e:
|
|
203
219
|
logger.error(
|
|
204
|
-
f"Failed to dynamically load/find callable '{path_str}': {e}"
|
|
220
|
+
f"Failed to dynamically load/find callable '{path_str}': {e}",
|
|
221
|
+
exc_info=True,
|
|
205
222
|
)
|
|
206
223
|
raise KeyError(
|
|
207
224
|
f"Callable '{path_str}' not found or failed to load: {e}"
|
|
@@ -209,11 +226,26 @@ class FlockRegistry:
|
|
|
209
226
|
|
|
210
227
|
def get_callable_path_string(self, func: Callable) -> str | None:
|
|
211
228
|
"""Gets the path string for a callable, registering it if necessary."""
|
|
229
|
+
# First try to find by direct identity
|
|
212
230
|
for path_str, registered_func in self._callables.items():
|
|
213
231
|
if func == registered_func:
|
|
232
|
+
logger.debug(
|
|
233
|
+
f"Found existing path string for callable: '{path_str}'"
|
|
234
|
+
)
|
|
214
235
|
return path_str
|
|
236
|
+
|
|
215
237
|
# If not found by identity, generate path, register, and return
|
|
216
|
-
|
|
238
|
+
path_str = self.register_callable(func)
|
|
239
|
+
if path_str:
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"Generated and registered new path string for callable: '{path_str}'"
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
logger.warning(
|
|
245
|
+
f"Failed to generate path string for callable {func.__name__}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return path_str
|
|
217
249
|
|
|
218
250
|
# --- Type Registration ---
|
|
219
251
|
def register_type(
|
|
@@ -227,7 +259,7 @@ class FlockRegistry:
|
|
|
227
259
|
f"Type '{type_name}' already registered. Overwriting."
|
|
228
260
|
)
|
|
229
261
|
self._types[type_name] = type_obj
|
|
230
|
-
|
|
262
|
+
logger.debug(f"Registered type: {type_name}")
|
|
231
263
|
return type_name
|
|
232
264
|
return None
|
|
233
265
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import json
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, TypeVar
|
|
5
|
+
from typing import Any, Literal, TypeVar
|
|
6
6
|
|
|
7
7
|
# Use yaml if available, otherwise skip yaml methods
|
|
8
8
|
try:
|
|
@@ -85,16 +85,32 @@ class Serializable(ABC):
|
|
|
85
85
|
) from e
|
|
86
86
|
|
|
87
87
|
# --- YAML Methods ---
|
|
88
|
-
def to_yaml(
|
|
89
|
-
|
|
88
|
+
def to_yaml(
|
|
89
|
+
self,
|
|
90
|
+
path_type: Literal["absolute", "relative"] = "relative",
|
|
91
|
+
sort_keys=False,
|
|
92
|
+
default_flow_style=False,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Serialize to YAML string.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
path_type: How file paths should be formatted ('absolute' or 'relative')
|
|
98
|
+
sort_keys: Whether to sort dictionary keys
|
|
99
|
+
default_flow_style: YAML flow style setting
|
|
100
|
+
"""
|
|
90
101
|
if not YAML_AVAILABLE:
|
|
91
102
|
raise NotImplementedError(
|
|
92
103
|
"YAML support requires PyYAML: pip install pyyaml"
|
|
93
104
|
)
|
|
94
105
|
try:
|
|
95
|
-
# to_dict
|
|
106
|
+
# If to_dict supports path_type, pass it; otherwise use standard to_dict
|
|
107
|
+
if "path_type" in self.to_dict.__code__.co_varnames:
|
|
108
|
+
dict_data = self.to_dict(path_type=path_type)
|
|
109
|
+
else:
|
|
110
|
+
dict_data = self.to_dict()
|
|
111
|
+
|
|
96
112
|
return yaml.dump(
|
|
97
|
-
|
|
113
|
+
dict_data,
|
|
98
114
|
sort_keys=sort_keys,
|
|
99
115
|
default_flow_style=default_flow_style,
|
|
100
116
|
allow_unicode=True,
|
|
@@ -125,8 +141,19 @@ class Serializable(ABC):
|
|
|
125
141
|
f"Failed to deserialize {cls.__name__} from YAML: {e}"
|
|
126
142
|
) from e
|
|
127
143
|
|
|
128
|
-
def to_yaml_file(
|
|
129
|
-
|
|
144
|
+
def to_yaml_file(
|
|
145
|
+
self,
|
|
146
|
+
path: Path | str,
|
|
147
|
+
path_type: Literal["absolute", "relative"] = "relative",
|
|
148
|
+
**yaml_dump_kwargs,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Serialize to YAML file.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
path: File path to write to
|
|
154
|
+
path_type: How file paths should be formatted ('absolute' or 'relative')
|
|
155
|
+
**yaml_dump_kwargs: Additional arguments to pass to yaml.dump
|
|
156
|
+
"""
|
|
130
157
|
if not YAML_AVAILABLE:
|
|
131
158
|
raise NotImplementedError(
|
|
132
159
|
"YAML support requires PyYAML: pip install pyyaml"
|
|
@@ -134,7 +161,7 @@ class Serializable(ABC):
|
|
|
134
161
|
path = Path(path)
|
|
135
162
|
try:
|
|
136
163
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
-
yaml_str = self.to_yaml(**yaml_dump_kwargs)
|
|
164
|
+
yaml_str = self.to_yaml(path_type=path_type, **yaml_dump_kwargs)
|
|
138
165
|
path.write_text(yaml_str, encoding="utf-8")
|
|
139
166
|
except Exception as e:
|
|
140
167
|
raise RuntimeError(
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# src/flock/core/serialization/serialization_utils.py
|
|
2
2
|
"""Utilities for recursive serialization/deserialization with callable handling."""
|
|
3
3
|
|
|
4
|
+
import ast
|
|
5
|
+
import builtins
|
|
4
6
|
import importlib
|
|
7
|
+
import sys
|
|
5
8
|
from collections.abc import Mapping, Sequence
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any, get_args, get_origin
|
|
7
10
|
|
|
8
11
|
from pydantic import BaseModel
|
|
9
12
|
|
|
@@ -21,6 +24,98 @@ logger = get_logger("serialization.utils")
|
|
|
21
24
|
# --- Serialization Helper ---
|
|
22
25
|
|
|
23
26
|
|
|
27
|
+
def extract_identifiers_from_type_str(type_str: str) -> set[str]:
|
|
28
|
+
"""Extract all identifiers from a type annotation string using the AST."""
|
|
29
|
+
tree = ast.parse(type_str, mode="eval")
|
|
30
|
+
identifiers = set()
|
|
31
|
+
|
|
32
|
+
class IdentifierVisitor(ast.NodeVisitor):
|
|
33
|
+
def visit_Name(self, node):
|
|
34
|
+
identifiers.add(node.id)
|
|
35
|
+
|
|
36
|
+
def visit_Attribute(self, node):
|
|
37
|
+
# Optionally support dotted names like mymodule.MyModel
|
|
38
|
+
full_name = []
|
|
39
|
+
while isinstance(node, ast.Attribute):
|
|
40
|
+
full_name.append(node.attr)
|
|
41
|
+
node = node.value
|
|
42
|
+
if isinstance(node, ast.Name):
|
|
43
|
+
full_name.append(node.id)
|
|
44
|
+
identifiers.add(".".join(reversed(full_name)))
|
|
45
|
+
|
|
46
|
+
IdentifierVisitor().visit(tree)
|
|
47
|
+
return identifiers
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_name(name: str):
|
|
51
|
+
"""Resolve a name to a Python object from loaded modules."""
|
|
52
|
+
# Try dotted names first
|
|
53
|
+
parts = name.split(".")
|
|
54
|
+
obj = None
|
|
55
|
+
|
|
56
|
+
if len(parts) == 1:
|
|
57
|
+
# Search globals and builtins
|
|
58
|
+
if parts[0] in globals():
|
|
59
|
+
return globals()[parts[0]]
|
|
60
|
+
if parts[0] in builtins.__dict__:
|
|
61
|
+
return builtins.__dict__[parts[0]]
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
obj = sys.modules[parts[0]]
|
|
65
|
+
for part in parts[1:]:
|
|
66
|
+
obj = getattr(obj, part)
|
|
67
|
+
return obj
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Try all loaded modules' symbols
|
|
72
|
+
for module in list(sys.modules.values()):
|
|
73
|
+
if module is None or not hasattr(module, "__dict__"):
|
|
74
|
+
continue
|
|
75
|
+
if parts[0] in module.__dict__:
|
|
76
|
+
return module.__dict__[parts[0]]
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def extract_pydantic_models_from_type_string(
|
|
82
|
+
type_str: str,
|
|
83
|
+
) -> list[type[BaseModel]]:
|
|
84
|
+
identifiers = extract_identifiers_from_type_str(type_str)
|
|
85
|
+
models = []
|
|
86
|
+
for name in identifiers:
|
|
87
|
+
resolved = resolve_name(name)
|
|
88
|
+
if (
|
|
89
|
+
isinstance(resolved, type)
|
|
90
|
+
and issubclass(resolved, BaseModel)
|
|
91
|
+
and resolved is not BaseModel
|
|
92
|
+
):
|
|
93
|
+
models.append(resolved)
|
|
94
|
+
return models
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def collect_pydantic_models(
|
|
98
|
+
type_hint, seen: set[type[BaseModel]] | None = None
|
|
99
|
+
) -> set[type[BaseModel]]:
|
|
100
|
+
if seen is None:
|
|
101
|
+
seen = set()
|
|
102
|
+
|
|
103
|
+
origin = get_origin(type_hint)
|
|
104
|
+
args = get_args(type_hint)
|
|
105
|
+
|
|
106
|
+
# Direct BaseModel
|
|
107
|
+
if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
|
|
108
|
+
seen.add(type_hint)
|
|
109
|
+
return seen
|
|
110
|
+
|
|
111
|
+
# For Unions, Lists, Dicts, Tuples, etc.
|
|
112
|
+
if origin is not None:
|
|
113
|
+
for arg in args:
|
|
114
|
+
collect_pydantic_models(arg, seen)
|
|
115
|
+
|
|
116
|
+
return seen
|
|
117
|
+
|
|
118
|
+
|
|
24
119
|
def serialize_item(item: Any) -> Any:
|
|
25
120
|
"""Recursively prepares an item for serialization (e.g., to dict for YAML/JSON).
|
|
26
121
|
Converts known callables to their path strings using FlockRegistry.
|
flock/core/util/cli_helper.py
CHANGED
|
@@ -52,7 +52,7 @@ def init_console(clear_screen: bool = True):
|
|
|
52
52
|
│ ▒█▀▀▀ █░░ █▀▀█ █▀▀ █░█ │
|
|
53
53
|
│ ▒█▀▀▀ █░░ █░░█ █░░ █▀▄ │
|
|
54
54
|
│ ▒█░░░ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀░▀ │
|
|
55
|
-
|
|
55
|
+
╰━━━━━━━━v{__version__}━━━━━━━━╯
|
|
56
56
|
🦆 🐤 🐧 🐓
|
|
57
57
|
""",
|
|
58
58
|
justify="center",
|
|
@@ -62,7 +62,7 @@ def init_console(clear_screen: bool = True):
|
|
|
62
62
|
console.clear()
|
|
63
63
|
console.print(banner_text)
|
|
64
64
|
console.print(
|
|
65
|
-
f"[italic]'
|
|
65
|
+
f"[italic]'Magpie'[/] milestone - [bold]white duck GmbH[/] - [cyan]https://whiteduck.de[/]\n"
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
|