flock-core 0.3.41__py3-none-any.whl → 0.4.0b1__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.py CHANGED
@@ -7,7 +7,7 @@ import asyncio
7
7
  import os
8
8
  import uuid
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, TypeVar
10
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar
11
11
 
12
12
  from box import Box
13
13
  from opentelemetry import trace
@@ -446,33 +446,421 @@ class Flock(BaseModel, Serializable):
446
446
 
447
447
  # --- ADDED Serialization Methods ---
448
448
 
449
- def to_dict(self) -> dict[str, Any]:
450
- """Convert Flock instance to dictionary representation."""
449
+ def to_dict(
450
+ self, path_type: Literal["absolute", "relative"] = "absolute"
451
+ ) -> dict[str, Any]:
452
+ """Convert Flock instance to dictionary representation.
453
+
454
+ Args:
455
+ path_type: How file paths should be formatted ('absolute' or 'relative')
456
+ """
451
457
  logger.debug("Serializing Flock instance to dict.")
452
458
  # Use Pydantic's dump for base fields
453
459
  data = self.model_dump(mode="json", exclude_none=True)
460
+ logger.info(
461
+ f"Serializing Flock '{self.name}' with {len(self._agents)} agents"
462
+ )
454
463
 
455
464
  # Manually add serialized agents
456
465
  data["agents"] = {}
466
+
467
+ # Track custom types used across all agents
468
+ custom_types = {}
469
+ # Track components used across all agents
470
+ components = {}
471
+
457
472
  for name, agent_instance in self._agents.items():
458
473
  try:
474
+ logger.debug(f"Serializing agent '{name}'")
459
475
  # Agents handle their own serialization via their to_dict
460
- data["agents"][name] = agent_instance.to_dict()
476
+ agent_data = agent_instance.to_dict()
477
+ data["agents"][name] = agent_data
478
+
479
+ # Extract type information from agent outputs
480
+ if agent_instance.output:
481
+ logger.debug(
482
+ f"Extracting type information from agent '{name}' output: {agent_instance.output}"
483
+ )
484
+ output_types = self._extract_types_from_signature(
485
+ agent_instance.output
486
+ )
487
+ if output_types:
488
+ logger.debug(
489
+ f"Found output types in agent '{name}': {output_types}"
490
+ )
491
+ custom_types.update(
492
+ self._get_type_definitions(output_types)
493
+ )
494
+
495
+ # Extract component information
496
+ if (
497
+ "evaluator" in agent_data
498
+ and "type" in agent_data["evaluator"]
499
+ ):
500
+ component_type = agent_data["evaluator"]["type"]
501
+ logger.debug(
502
+ f"Adding evaluator component '{component_type}' from agent '{name}'"
503
+ )
504
+ components[component_type] = self._get_component_definition(
505
+ component_type, path_type
506
+ )
507
+
508
+ # Extract module component information
509
+ if "modules" in agent_data:
510
+ for module_name, module_data in agent_data[
511
+ "modules"
512
+ ].items():
513
+ if "type" in module_data:
514
+ component_type = module_data["type"]
515
+ logger.debug(
516
+ f"Adding module component '{component_type}' from module '{module_name}' in agent '{name}'"
517
+ )
518
+ components[component_type] = (
519
+ self._get_component_definition(
520
+ component_type, path_type
521
+ )
522
+ )
523
+
524
+ # Extract tool (callable) information
525
+ if agent_data.get("tools"):
526
+ logger.debug(
527
+ f"Extracting tool information from agent '{name}': {agent_data['tools']}"
528
+ )
529
+ # Get references to the actual tool objects
530
+ tool_objs = (
531
+ agent_instance.tools if agent_instance.tools else []
532
+ )
533
+ for i, tool_name in enumerate(agent_data["tools"]):
534
+ if i < len(tool_objs):
535
+ tool = tool_objs[i]
536
+ if callable(tool) and not isinstance(tool, type):
537
+ # Get the fully qualified name for registry lookup
538
+ path_str = (
539
+ get_registry().get_callable_path_string(
540
+ tool
541
+ )
542
+ )
543
+ if path_str:
544
+ logger.debug(
545
+ f"Adding tool '{tool_name}' (from path '{path_str}') to components"
546
+ )
547
+ # Add definition using just the function name as the key
548
+ components[tool_name] = (
549
+ self._get_callable_definition(
550
+ path_str, tool_name, path_type
551
+ )
552
+ )
553
+
461
554
  except Exception as e:
462
555
  logger.error(
463
- f"Failed to serialize agent '{name}' within Flock: {e}"
556
+ f"Failed to serialize agent '{name}' within Flock: {e}",
557
+ exc_info=True,
464
558
  )
465
559
  # Optionally skip problematic agents or raise error
466
560
  # data["agents"][name] = {"error": f"Serialization failed: {e}"}
467
561
 
468
- # Exclude runtime fields that shouldn't be serialized
469
- # These are not Pydantic fields, so they aren't dumped by model_dump
470
- # No need to explicitly remove _start_agent_name, _start_input unless added manually
562
+ # Add type definitions to the serialized output if any were found
563
+ if custom_types:
564
+ logger.info(
565
+ f"Adding {len(custom_types)} custom type definitions to serialized output"
566
+ )
567
+ data["types"] = custom_types
568
+
569
+ # Add component definitions to the serialized output if any were found
570
+ if components:
571
+ logger.info(
572
+ f"Adding {len(components)} component definitions to serialized output"
573
+ )
574
+ data["components"] = components
575
+
576
+ # Add dependencies section
577
+ data["dependencies"] = self._get_dependencies()
578
+
579
+ # Add serialization settings
580
+ data["metadata"] = {"path_type": path_type}
581
+
582
+ logger.debug(
583
+ f"Flock serialization complete with {len(data['agents'])} agents, {len(custom_types)} types, {len(components)} components"
584
+ )
471
585
 
472
- # Filter final dict (optional, Pydantic's exclude_none helps)
473
- # return self._filter_none_values(data)
474
586
  return data
475
587
 
588
+ def _extract_types_from_signature(self, signature: str) -> list[str]:
589
+ """Extract type names from an input/output signature string."""
590
+ if not signature:
591
+ return []
592
+
593
+ # Basic type extraction - handles simple cases like "result: TypeName" or "list[TypeName]"
594
+ custom_types = []
595
+
596
+ # Look for type annotations (everything after ":")
597
+ parts = signature.split(":")
598
+ if len(parts) > 1:
599
+ type_part = parts[1].strip()
600
+
601
+ # Extract from list[Type]
602
+ if "list[" in type_part:
603
+ inner_type = type_part.split("list[")[1].split("]")[0].strip()
604
+ if inner_type and inner_type.lower() not in [
605
+ "str",
606
+ "int",
607
+ "float",
608
+ "bool",
609
+ "dict",
610
+ "list",
611
+ ]:
612
+ custom_types.append(inner_type)
613
+
614
+ # Extract direct type references
615
+ elif type_part and type_part.lower() not in [
616
+ "str",
617
+ "int",
618
+ "float",
619
+ "bool",
620
+ "dict",
621
+ "list",
622
+ ]:
623
+ custom_types.append(
624
+ type_part.split()[0]
625
+ ) # Take the first word in case there's a description
626
+
627
+ return custom_types
628
+
629
+ def _get_type_definitions(self, type_names: list[str]) -> dict[str, Any]:
630
+ """Get definitions for the specified custom types."""
631
+ from flock.core.flock_registry import get_registry
632
+
633
+ type_definitions = {}
634
+ registry = get_registry()
635
+
636
+ for type_name in type_names:
637
+ try:
638
+ # Try to get the type from registry
639
+ type_obj = registry._types.get(type_name)
640
+ if type_obj:
641
+ type_def = self._extract_type_definition(
642
+ type_name, type_obj
643
+ )
644
+ if type_def:
645
+ type_definitions[type_name] = type_def
646
+ except Exception as e:
647
+ logger.warning(
648
+ f"Could not extract definition for type {type_name}: {e}"
649
+ )
650
+
651
+ return type_definitions
652
+
653
+ def _extract_type_definition(
654
+ self, type_name: str, type_obj: type
655
+ ) -> dict[str, Any]:
656
+ """Extract a definition for a custom type."""
657
+ import inspect
658
+ from dataclasses import is_dataclass
659
+
660
+ type_def = {
661
+ "module_path": type_obj.__module__,
662
+ }
663
+
664
+ # Handle Pydantic models
665
+ if hasattr(type_obj, "model_json_schema") and callable(
666
+ getattr(type_obj, "model_json_schema")
667
+ ):
668
+ type_def["type"] = "pydantic.BaseModel"
669
+ try:
670
+ schema = type_obj.model_json_schema()
671
+ # Clean up schema to remove unnecessary fields
672
+ if "title" in schema and schema["title"] == type_name:
673
+ del schema["title"]
674
+ type_def["schema"] = schema
675
+ except Exception as e:
676
+ logger.warning(
677
+ f"Could not extract schema for Pydantic model {type_name}: {e}"
678
+ )
679
+
680
+ # Handle dataclasses
681
+ elif is_dataclass(type_obj):
682
+ type_def["type"] = "dataclass"
683
+ fields = {}
684
+ for field_name, field in type_obj.__dataclass_fields__.items():
685
+ fields[field_name] = {
686
+ "type": str(field.type),
687
+ "default": str(field.default)
688
+ if field.default is not inspect.Parameter.empty
689
+ else None,
690
+ }
691
+ type_def["fields"] = fields
692
+
693
+ # Handle other types - just store basic information
694
+ else:
695
+ type_def["type"] = "custom"
696
+
697
+ # Extract import statement (simplified version)
698
+ type_def["imports"] = [f"from {type_obj.__module__} import {type_name}"]
699
+
700
+ return type_def
701
+
702
+ def _get_component_definition(
703
+ self, component_type: str, path_type: Literal["absolute", "relative"]
704
+ ) -> dict[str, Any]:
705
+ """Get definition for a component type."""
706
+ import os
707
+ import sys
708
+
709
+ from flock.core.flock_registry import get_registry
710
+
711
+ registry = get_registry()
712
+ component_def = {}
713
+
714
+ try:
715
+ # Try to get the component class from registry
716
+ component_class = registry._components.get(component_type)
717
+ if component_class:
718
+ # Get the standard module path
719
+ module_path = component_class.__module__
720
+
721
+ # Get the actual file system path if possible
722
+ file_path = None
723
+ try:
724
+ if (
725
+ hasattr(component_class, "__module__")
726
+ and component_class.__module__
727
+ ):
728
+ module = sys.modules.get(component_class.__module__)
729
+ if module and hasattr(module, "__file__"):
730
+ file_path = os.path.abspath(module.__file__)
731
+ # Convert to relative path if needed
732
+ if path_type == "relative" and file_path:
733
+ try:
734
+ file_path = os.path.relpath(file_path)
735
+ except ValueError:
736
+ # Keep as absolute if can't make relative
737
+ logger.warning(
738
+ f"Could not convert path to relative: {file_path}"
739
+ )
740
+ except Exception as e:
741
+ # If we can't get the file path, we'll just use the module path
742
+ logger.warning(
743
+ f"Error getting file path for component {component_type}: {e}"
744
+ )
745
+ pass
746
+
747
+ component_def = {
748
+ "type": "flock_component",
749
+ "module_path": module_path,
750
+ "file_path": file_path, # Include actual file system path
751
+ "description": getattr(
752
+ component_class, "__doc__", ""
753
+ ).strip()
754
+ or f"{component_type} component",
755
+ }
756
+ except Exception as e:
757
+ logger.warning(
758
+ f"Could not extract definition for component {component_type}: {e}"
759
+ )
760
+ # Provide minimal information if we can't extract details
761
+ component_def = {
762
+ "type": "flock_component",
763
+ "module_path": "unknown",
764
+ "file_path": None,
765
+ "description": f"{component_type} component (definition incomplete)",
766
+ }
767
+
768
+ return component_def
769
+
770
+ def _get_callable_definition(
771
+ self,
772
+ callable_ref: str,
773
+ func_name: str,
774
+ path_type: Literal["absolute", "relative"],
775
+ ) -> dict[str, Any]:
776
+ """Get definition for a callable reference.
777
+
778
+ Args:
779
+ callable_ref: The fully qualified path to the callable
780
+ func_name: The simple function name (for display purposes)
781
+ path_type: How file paths should be formatted ('absolute' or 'relative')
782
+ """
783
+ import inspect
784
+ import os
785
+ import sys
786
+
787
+ from flock.core.flock_registry import get_registry
788
+
789
+ registry = get_registry()
790
+ callable_def = {}
791
+
792
+ try:
793
+ # Try to get the callable from registry
794
+ logger.debug(
795
+ f"Getting callable definition for '{callable_ref}' (display name: '{func_name}')"
796
+ )
797
+ func = registry.get_callable(callable_ref)
798
+ if func:
799
+ # Get the standard module path
800
+ module_path = func.__module__
801
+
802
+ # Get the actual file system path if possible
803
+ file_path = None
804
+ try:
805
+ if func.__module__ and func.__module__ != "builtins":
806
+ module = sys.modules.get(func.__module__)
807
+ if module and hasattr(module, "__file__"):
808
+ file_path = os.path.abspath(module.__file__)
809
+ # Convert to relative path if needed
810
+ if path_type == "relative" and file_path:
811
+ try:
812
+ file_path = os.path.relpath(file_path)
813
+ except ValueError:
814
+ # Keep as absolute if can't make relative
815
+ logger.warning(
816
+ f"Could not convert path to relative: {file_path}"
817
+ )
818
+ except Exception as e:
819
+ # If we can't get the file path, just use the module path
820
+ logger.warning(
821
+ f"Error getting file path for callable {callable_ref}: {e}"
822
+ )
823
+ pass
824
+
825
+ # Get the docstring for description
826
+ docstring = (
827
+ inspect.getdoc(func) or f"Callable function {func_name}"
828
+ )
829
+
830
+ callable_def = {
831
+ "type": "flock_callable",
832
+ "module_path": module_path,
833
+ "file_path": file_path,
834
+ "description": docstring.strip(),
835
+ }
836
+ logger.debug(
837
+ f"Created callable definition for '{func_name}': module={module_path}, file={file_path}"
838
+ )
839
+ except Exception as e:
840
+ logger.warning(
841
+ f"Could not extract definition for callable {callable_ref}: {e}"
842
+ )
843
+ # Provide minimal information
844
+ callable_def = {
845
+ "type": "flock_callable",
846
+ "module_path": callable_ref.split(".")[0]
847
+ if "." in callable_ref
848
+ else "unknown",
849
+ "file_path": None,
850
+ "description": f"Callable {func_name} (definition incomplete)",
851
+ }
852
+
853
+ return callable_def
854
+
855
+ def _get_dependencies(self) -> list[str]:
856
+ """Get list of dependencies required by this Flock."""
857
+ # This is a simplified version - in production, you might want to detect
858
+ # actual versions of installed packages
859
+ return [
860
+ "pydantic>=2.0.0",
861
+ "flock>=0.3.41", # Assuming this is the package name
862
+ ]
863
+
476
864
  @classmethod
477
865
  def from_dict(cls: type[T], data: dict[str, Any]) -> T:
478
866
  """Create Flock instance from dictionary representation."""
@@ -480,6 +868,30 @@ class Flock(BaseModel, Serializable):
480
868
  f"Deserializing Flock from dict. Provided keys: {list(data.keys())}"
481
869
  )
482
870
 
871
+ # Check for serialization settings
872
+ serialization_settings = data.pop("serialization_settings", {})
873
+ path_type = serialization_settings.get("path_type", "absolute")
874
+ logger.debug(
875
+ f"Using path_type '{path_type}' from serialization settings"
876
+ )
877
+
878
+ # First, handle type definitions if present
879
+ if "types" in data:
880
+ logger.info(f"Processing {len(data['types'])} type definitions")
881
+ cls._register_type_definitions(data["types"])
882
+
883
+ # Then, handle component definitions if present
884
+ if "components" in data:
885
+ logger.info(
886
+ f"Processing {len(data['components'])} component definitions"
887
+ )
888
+ cls._register_component_definitions(data["components"], path_type)
889
+
890
+ # Check dependencies if present
891
+ if "dependencies" in data:
892
+ logger.debug(f"Checking {len(data['dependencies'])} dependencies")
893
+ cls._check_dependencies(data["dependencies"])
894
+
483
895
  # Ensure FlockAgent is importable for type checking later
484
896
  try:
485
897
  from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
@@ -491,11 +903,22 @@ class Flock(BaseModel, Serializable):
491
903
 
492
904
  # Extract agent data before initializing Flock base model
493
905
  agents_data = data.pop("agents", {})
906
+ logger.info(f"Found {len(agents_data)} agents to deserialize")
907
+
908
+ # Remove types, components, and dependencies sections as they're not part of Flock fields
909
+ data.pop("types", None)
910
+ data.pop("components", None)
911
+ data.pop("dependencies", None)
912
+ # Remove metadata if present
913
+ data.pop("metadata", None)
494
914
 
495
915
  # Create Flock instance using Pydantic constructor for basic fields
496
916
  try:
497
917
  # Pass only fields defined in Flock's Pydantic model
498
918
  init_data = {k: v for k, v in data.items() if k in cls.model_fields}
919
+ logger.debug(
920
+ f"Creating Flock instance with fields: {list(init_data.keys())}"
921
+ )
499
922
  flock_instance = cls(**init_data)
500
923
  except Exception as e:
501
924
  logger.error(
@@ -508,6 +931,7 @@ class Flock(BaseModel, Serializable):
508
931
  # Deserialize and add agents AFTER Flock instance exists
509
932
  for name, agent_data in agents_data.items():
510
933
  try:
934
+ logger.debug(f"Deserializing agent '{name}'")
511
935
  # Ensure agent_data has the name, or add it from the key
512
936
  agent_data.setdefault("name", name)
513
937
  # Use FlockAgent's from_dict method
@@ -515,6 +939,7 @@ class Flock(BaseModel, Serializable):
515
939
  flock_instance.add_agent(
516
940
  agent_instance
517
941
  ) # Adds to _agents and registers
942
+ logger.debug(f"Successfully added agent '{name}' to Flock")
518
943
  except Exception as e:
519
944
  logger.error(
520
945
  f"Failed to deserialize or add agent '{name}' during Flock deserialization: {e}",
@@ -522,9 +947,412 @@ class Flock(BaseModel, Serializable):
522
947
  )
523
948
  # Decide: skip agent or raise error?
524
949
 
525
- logger.info("Successfully deserialized Flock instance.")
950
+ logger.info(
951
+ f"Successfully deserialized Flock instance '{flock_instance.name}' with {len(flock_instance._agents)} agents"
952
+ )
526
953
  return flock_instance
527
954
 
955
+ @classmethod
956
+ def _register_type_definitions(cls, type_defs: dict[str, Any]) -> None:
957
+ """Register type definitions from serialized data."""
958
+ import importlib
959
+
960
+ from flock.core.flock_registry import get_registry
961
+
962
+ registry = get_registry()
963
+
964
+ for type_name, type_def in type_defs.items():
965
+ logger.debug(f"Registering type: {type_name}")
966
+
967
+ try:
968
+ # First try to import the type directly
969
+ module_path = type_def.get("module_path")
970
+ if module_path:
971
+ try:
972
+ module = importlib.import_module(module_path)
973
+ if hasattr(module, type_name):
974
+ type_obj = getattr(module, type_name)
975
+ registry.register_type(type_obj, type_name)
976
+ logger.info(
977
+ f"Registered type {type_name} from module {module_path}"
978
+ )
979
+ continue
980
+ except ImportError:
981
+ logger.debug(
982
+ f"Could not import {module_path}, trying dynamic type creation"
983
+ )
984
+
985
+ # If direct import fails, try to create the type dynamically
986
+ if (
987
+ type_def.get("type") == "pydantic.BaseModel"
988
+ and "schema" in type_def
989
+ ):
990
+ cls._create_pydantic_model(type_name, type_def)
991
+ elif (
992
+ type_def.get("type") == "dataclass" and "fields" in type_def
993
+ ):
994
+ cls._create_dataclass(type_name, type_def)
995
+ else:
996
+ logger.warning(
997
+ f"Unsupported type definition for {type_name}, type: {type_def.get('type')}"
998
+ )
999
+
1000
+ except Exception as e:
1001
+ logger.error(f"Failed to register type {type_name}: {e}")
1002
+
1003
+ @classmethod
1004
+ def _create_pydantic_model(
1005
+ cls, type_name: str, type_def: dict[str, Any]
1006
+ ) -> None:
1007
+ """Dynamically create a Pydantic model from a schema definition."""
1008
+ from pydantic import create_model
1009
+
1010
+ from flock.core.flock_registry import get_registry
1011
+
1012
+ registry = get_registry()
1013
+ schema = type_def.get("schema", {})
1014
+
1015
+ try:
1016
+ # Extract field definitions from schema
1017
+ fields = {}
1018
+ properties = schema.get("properties", {})
1019
+ required = schema.get("required", [])
1020
+
1021
+ for field_name, field_schema in properties.items():
1022
+ # Determine the field type based on schema
1023
+ field_type = cls._get_type_from_schema(field_schema)
1024
+
1025
+ # Determine if field is required
1026
+ default = ... if field_name in required else None
1027
+
1028
+ # Add to fields dict
1029
+ fields[field_name] = (field_type, default)
1030
+
1031
+ # Create the model
1032
+ DynamicModel = create_model(type_name, **fields)
1033
+
1034
+ # Register it
1035
+ registry.register_type(DynamicModel, type_name)
1036
+ logger.info(f"Created and registered Pydantic model: {type_name}")
1037
+
1038
+ except Exception as e:
1039
+ logger.error(f"Failed to create Pydantic model {type_name}: {e}")
1040
+
1041
+ @classmethod
1042
+ def _get_type_from_schema(cls, field_schema: dict[str, Any]) -> Any:
1043
+ """Convert JSON schema type to Python type."""
1044
+ schema_type = field_schema.get("type")
1045
+
1046
+ # Basic type mapping
1047
+ type_mapping = {
1048
+ "string": str,
1049
+ "integer": int,
1050
+ "number": float,
1051
+ "boolean": bool,
1052
+ "array": list,
1053
+ "object": dict,
1054
+ }
1055
+
1056
+ # Handle basic types
1057
+ if schema_type in type_mapping:
1058
+ return type_mapping[schema_type]
1059
+
1060
+ # Handle enums
1061
+ if "enum" in field_schema:
1062
+ from typing import Literal
1063
+
1064
+ return Literal[tuple(field_schema["enum"])]
1065
+
1066
+ # Default
1067
+ return Any
1068
+
1069
+ @classmethod
1070
+ def _create_dataclass(
1071
+ cls, type_name: str, type_def: dict[str, Any]
1072
+ ) -> None:
1073
+ """Dynamically create a dataclass from a field definition."""
1074
+ from dataclasses import make_dataclass
1075
+
1076
+ from flock.core.flock_registry import get_registry
1077
+
1078
+ registry = get_registry()
1079
+ fields_def = type_def.get("fields", {})
1080
+
1081
+ try:
1082
+ fields = []
1083
+ for field_name, field_props in fields_def.items():
1084
+ field_type = eval(
1085
+ field_props.get("type", "str")
1086
+ ) # Note: eval is used here for simplicity
1087
+ fields.append((field_name, field_type))
1088
+
1089
+ # Create the dataclass
1090
+ DynamicDataclass = make_dataclass(type_name, fields)
1091
+
1092
+ # Register it
1093
+ registry.register_type(DynamicDataclass, type_name)
1094
+ logger.info(f"Created and registered dataclass: {type_name}")
1095
+
1096
+ except Exception as e:
1097
+ logger.error(f"Failed to create dataclass {type_name}: {e}")
1098
+
1099
+ @classmethod
1100
+ def _register_component_definitions(
1101
+ cls,
1102
+ component_defs: dict[str, Any],
1103
+ path_type: Literal["absolute", "relative"],
1104
+ ) -> None:
1105
+ """Register component definitions from serialized data."""
1106
+ import importlib
1107
+ import importlib.util
1108
+ import os
1109
+ import sys
1110
+
1111
+ from flock.core.flock_registry import get_registry
1112
+
1113
+ registry = get_registry()
1114
+
1115
+ for component_name, component_def in component_defs.items():
1116
+ logger.debug(f"Registering component: {component_name}")
1117
+ component_type = component_def.get("type", "flock_component")
1118
+
1119
+ try:
1120
+ # Handle callables differently than components
1121
+ if component_type == "flock_callable":
1122
+ # For callables, component_name is just the function name
1123
+ func_name = component_name
1124
+ module_path = component_def.get("module_path")
1125
+ file_path = component_def.get("file_path")
1126
+
1127
+ # Convert relative path to absolute if needed
1128
+ if (
1129
+ path_type == "relative"
1130
+ and file_path
1131
+ and not os.path.isabs(file_path)
1132
+ ):
1133
+ try:
1134
+ # Make absolute based on current directory
1135
+ file_path = os.path.abspath(file_path)
1136
+ logger.debug(
1137
+ f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
1138
+ )
1139
+ except Exception as e:
1140
+ logger.warning(
1141
+ f"Could not convert relative path to absolute: {e}"
1142
+ )
1143
+
1144
+ logger.debug(
1145
+ f"Processing callable '{func_name}' from module '{module_path}', file: {file_path}"
1146
+ )
1147
+
1148
+ # Try direct import first
1149
+ if module_path:
1150
+ try:
1151
+ logger.debug(
1152
+ f"Attempting to import module: {module_path}"
1153
+ )
1154
+ module = importlib.import_module(module_path)
1155
+ if hasattr(module, func_name):
1156
+ callable_obj = getattr(module, func_name)
1157
+ # Register with just the name for easier lookup
1158
+ registry.register_callable(
1159
+ callable_obj, func_name
1160
+ )
1161
+ logger.info(
1162
+ f"Registered callable with name: {func_name}"
1163
+ )
1164
+ # Also register with fully qualified path for compatibility
1165
+ if module_path != "__main__":
1166
+ full_path = f"{module_path}.{func_name}"
1167
+ registry.register_callable(
1168
+ callable_obj, full_path
1169
+ )
1170
+ logger.info(
1171
+ f"Also registered callable with full path: {full_path}"
1172
+ )
1173
+ logger.info(
1174
+ f"Successfully registered callable {func_name} from module {module_path}"
1175
+ )
1176
+ continue
1177
+ else:
1178
+ logger.warning(
1179
+ f"Function '{func_name}' not found in module {module_path}"
1180
+ )
1181
+ except ImportError:
1182
+ logger.debug(
1183
+ f"Could not import module {module_path}, trying file path"
1184
+ )
1185
+
1186
+ # Try file path if module import fails
1187
+ if file_path and os.path.exists(file_path):
1188
+ try:
1189
+ logger.debug(
1190
+ f"Attempting to load file: {file_path}"
1191
+ )
1192
+ # Create a module name from file path
1193
+ mod_name = f"{func_name}_module"
1194
+ spec = importlib.util.spec_from_file_location(
1195
+ mod_name, file_path
1196
+ )
1197
+ if spec and spec.loader:
1198
+ module = importlib.util.module_from_spec(spec)
1199
+ sys.modules[spec.name] = module
1200
+ spec.loader.exec_module(module)
1201
+ logger.debug(
1202
+ f"Successfully loaded module from file, searching for function '{func_name}'"
1203
+ )
1204
+
1205
+ # Look for the function in the loaded module
1206
+ if hasattr(module, func_name):
1207
+ callable_obj = getattr(module, func_name)
1208
+ registry.register_callable(
1209
+ callable_obj, func_name
1210
+ )
1211
+ logger.info(
1212
+ f"Successfully registered callable {func_name} from file {file_path}"
1213
+ )
1214
+ else:
1215
+ logger.warning(
1216
+ f"Function {func_name} not found in file {file_path}"
1217
+ )
1218
+ else:
1219
+ logger.warning(
1220
+ f"Could not create import spec for {file_path}"
1221
+ )
1222
+ except Exception as e:
1223
+ logger.error(
1224
+ f"Error loading callable {func_name} from file {file_path}: {e}",
1225
+ exc_info=True,
1226
+ )
1227
+
1228
+ # Handle regular components (existing code)
1229
+ else:
1230
+ # First try using the module path (Python import)
1231
+ module_path = component_def.get("module_path")
1232
+ if module_path and module_path != "unknown":
1233
+ try:
1234
+ logger.debug(
1235
+ f"Attempting to import module '{module_path}' for component '{component_name}'"
1236
+ )
1237
+ module = importlib.import_module(module_path)
1238
+ # Find the component class in the module
1239
+ for attr_name in dir(module):
1240
+ if attr_name == component_name:
1241
+ component_class = getattr(module, attr_name)
1242
+ registry.register_component(
1243
+ component_class, component_name
1244
+ )
1245
+ logger.info(
1246
+ f"Registered component {component_name} from {module_path}"
1247
+ )
1248
+ break
1249
+ else:
1250
+ logger.warning(
1251
+ f"Component {component_name} not found in module {module_path}"
1252
+ )
1253
+ # If we didn't find the component, try using file_path next
1254
+ raise ImportError(
1255
+ f"Component {component_name} not found in module {module_path}"
1256
+ )
1257
+ except ImportError:
1258
+ # If module import fails, try file_path approach
1259
+ file_path = component_def.get("file_path")
1260
+
1261
+ # Convert relative path to absolute if needed
1262
+ if (
1263
+ path_type == "relative"
1264
+ and file_path
1265
+ and not os.path.isabs(file_path)
1266
+ ):
1267
+ try:
1268
+ # Make absolute based on current directory
1269
+ file_path = os.path.abspath(file_path)
1270
+ logger.debug(
1271
+ f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
1272
+ )
1273
+ except Exception as e:
1274
+ logger.warning(
1275
+ f"Could not convert relative path to absolute: {e}"
1276
+ )
1277
+
1278
+ if file_path and os.path.exists(file_path):
1279
+ logger.debug(
1280
+ f"Attempting to load {component_name} from file: {file_path}"
1281
+ )
1282
+ try:
1283
+ # Load the module from file path
1284
+ spec = (
1285
+ importlib.util.spec_from_file_location(
1286
+ f"{component_name}_module",
1287
+ file_path,
1288
+ )
1289
+ )
1290
+ if spec and spec.loader:
1291
+ module = (
1292
+ importlib.util.module_from_spec(
1293
+ spec
1294
+ )
1295
+ )
1296
+ sys.modules[spec.name] = module
1297
+ spec.loader.exec_module(module)
1298
+ logger.debug(
1299
+ f"Successfully loaded module from file, searching for component class '{component_name}'"
1300
+ )
1301
+
1302
+ # Find the component class in the loaded module
1303
+ for attr_name in dir(module):
1304
+ if attr_name == component_name:
1305
+ component_class = getattr(
1306
+ module, attr_name
1307
+ )
1308
+ registry.register_component(
1309
+ component_class,
1310
+ component_name,
1311
+ )
1312
+ logger.info(
1313
+ f"Registered component {component_name} from file {file_path}"
1314
+ )
1315
+ break
1316
+ else:
1317
+ logger.warning(
1318
+ f"Component {component_name} not found in file {file_path}"
1319
+ )
1320
+ except Exception as e:
1321
+ logger.error(
1322
+ f"Error loading component {component_name} from file {file_path}: {e}",
1323
+ exc_info=True,
1324
+ )
1325
+ else:
1326
+ logger.warning(
1327
+ f"No valid file path found for component {component_name}"
1328
+ )
1329
+ else:
1330
+ logger.warning(
1331
+ f"Missing or unknown module path for component {component_name}"
1332
+ )
1333
+ except Exception as e:
1334
+ logger.error(
1335
+ f"Failed to register component {component_name}: {e}",
1336
+ exc_info=True,
1337
+ )
1338
+
1339
+ @classmethod
1340
+ def _check_dependencies(cls, dependencies: list[str]) -> None:
1341
+ """Check if required dependencies are available."""
1342
+ import importlib
1343
+ import re
1344
+
1345
+ for dependency in dependencies:
1346
+ # Extract package name and version
1347
+ match = re.match(r"([^>=<]+)([>=<].+)?", dependency)
1348
+ if match:
1349
+ package_name = match.group(1)
1350
+ try:
1351
+ importlib.import_module(package_name.replace("-", "_"))
1352
+ logger.debug(f"Dependency {package_name} is available")
1353
+ except ImportError:
1354
+ logger.warning(f"Dependency {dependency} is not installed")
1355
+
528
1356
  # --- API Start Method ---
529
1357
  def start_api(
530
1358
  self,
@@ -600,20 +1428,31 @@ class Flock(BaseModel, Serializable):
600
1428
  if not p.exists():
601
1429
  raise FileNotFoundError(f"Flock file not found: {file_path}")
602
1430
 
603
- if p.suffix in [".yaml", ".yml"]:
604
- return Flock.from_yaml_file(p)
605
- elif p.suffix == ".json":
606
- return Flock.from_json(p.read_text())
607
- elif p.suffix == ".msgpack":
608
- return Flock.from_msgpack_file(p)
609
- elif p.suffix == ".pkl":
610
- if PICKLE_AVAILABLE:
611
- return Flock.from_pickle_file(p)
1431
+ try:
1432
+ if p.suffix in [".yaml", ".yml"]:
1433
+ return Flock.from_yaml_file(p)
1434
+ elif p.suffix == ".json":
1435
+ return Flock.from_json(p.read_text())
1436
+ elif p.suffix == ".msgpack":
1437
+ return Flock.from_msgpack_file(p)
1438
+ elif p.suffix == ".pkl":
1439
+ if PICKLE_AVAILABLE:
1440
+ return Flock.from_pickle_file(p)
1441
+ else:
1442
+ raise RuntimeError(
1443
+ "Cannot load Pickle file: cloudpickle not installed."
1444
+ )
612
1445
  else:
613
- raise RuntimeError(
614
- "Cannot load Pickle file: cloudpickle not installed."
1446
+ raise ValueError(
1447
+ f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
615
1448
  )
616
- else:
617
- raise ValueError(
618
- f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
619
- )
1449
+ except Exception as e:
1450
+ # Check if it's an exception about missing types
1451
+ if "Could not get registered type name" in str(e):
1452
+ logger.error(
1453
+ f"Failed to load Flock from {file_path}: Missing type definition. "
1454
+ "This may happen if the YAML was created on a system with different types registered. "
1455
+ "Check if the file includes 'types' section with necessary type definitions."
1456
+ )
1457
+ logger.error(f"Error loading Flock from {file_path}: {e}")
1458
+ raise