| | import os |
| | import yaml |
| | import json |
| | import copy |
| | import logging |
| | from typing import Callable, Any, Dict, List |
| | from pydantic import BaseModel, ValidationError |
| | from pydantic._internal._model_construction import ModelMetaclass |
| |
|
| | from .logging import logger |
| | from .callbacks import callback_manager, exception_buffer |
| | from .module_utils import ( |
| | save_json, |
| | custom_serializer, |
| | parse_json_from_text, |
| | get_error_message, |
| | get_base_module_init_error_message |
| | ) |
| | from .registry import register_module, MODULE_REGISTRY |
| |
|
| |
|
| | class MetaModule(ModelMetaclass): |
| | """ |
| | MetaModule is a metaclass that automatically registers all subclasses of BaseModule. |
| | |
| | |
| | Attributes: |
| | No public attributes |
| | """ |
| | def __new__(mcs, name, bases, namespace, **kwargs): |
| | """ |
| | Creates a new class and registers it in MODULE_REGISTRY. |
| | |
| | Args: |
| | mcs: The metaclass itself |
| | name: The name of the class being created |
| | bases: Tuple of base classes |
| | namespace: Dictionary containing the class attributes and methods |
| | **kwargs: Additional keyword arguments |
| | |
| | Returns: |
| | The created class object |
| | """ |
| | cls = super().__new__(mcs, name, bases, namespace) |
| | register_module(name, cls) |
| | return cls |
| |
|
| |
|
| | class BaseModule(BaseModel, metaclass=MetaModule): |
| | """ |
| | Base module class that serves as the foundation for all modules in the EvoAgentX framework. |
| | |
| | This class provides serialization/deserialization capabilities, supports creating instances from |
| | dictionaries, JSON, or files, and exporting instances to these formats. |
| | |
| | Attributes: |
| | class_name: The class name, defaults to None but is automatically set during subclass initialization |
| | model_config: Pydantic model configuration that controls type matching and behavior |
| | """ |
| |
|
| | class_name: str = None |
| | |
| | model_config = {"arbitrary_types_allowed": True, "extra": "allow", "protected_namespaces": (), "validate_assignment": False} |
| |
|
| | def __init_subclass__(cls, **kwargs): |
| | """ |
| | Subclass initialization method that automatically sets the class_name attribute. |
| | |
| | Args: |
| | cls (Type): The subclass being initialized |
| | **kwargs (Any): Additional keyword arguments |
| | """ |
| | super().__init_subclass__(**kwargs) |
| | cls.class_name = cls.__name__ |
| | |
| | def __init__(self, **kwargs): |
| | """ |
| | Initializes a BaseModule instance. |
| | |
| | Args: |
| | **kwargs (Any): Keyword arguments used to initialize the instance |
| | |
| | Raises: |
| | ValidationError: When parameter validation fails |
| | Exception: When other errors occur during initialization |
| | """ |
| |
|
| | try: |
| | for field_name, _ in type(self).model_fields.items(): |
| | field_value = kwargs.get(field_name, None) |
| | if field_value: |
| | kwargs[field_name] = self._process_data(field_value) |
| | |
| | |
| | |
| | |
| | super().__init__(**kwargs) |
| | self.init_module() |
| | except (ValidationError, Exception) as e: |
| | exception_handler = callback_manager.get_callback("exception_buffer") |
| | if exception_handler is None: |
| | error_message = get_base_module_init_error_message( |
| | cls=self.__class__, |
| | data=kwargs, |
| | errors=e |
| | ) |
| | logger.error(error_message) |
| | raise |
| | else: |
| | exception_handler.add(e) |
| | |
| | def init_module(self): |
| | """ |
| | Module initialization method that subclasses can override to provide additional initialization logic. |
| | """ |
| | pass |
| |
|
| | def __str__(self) -> str: |
| | """ |
| | Returns a string representation of the object. |
| | |
| | Returns: |
| | str: String representation of the object |
| | """ |
| | return self.to_str() |
| | |
| | @property |
| | def kwargs(self) -> dict: |
| | """ |
| | Returns the extra fields of the model. |
| | |
| | Returns: |
| | dict: Dictionary containing all extra keyword arguments |
| | """ |
| | return self.model_extra |
| | |
| | @classmethod |
| | def _create_instance(cls, data: Dict[str, Any]) -> "BaseModule": |
| | """ |
| | Internal method for creating an instance from a dictionary. |
| | |
| | Args: |
| | data: Dictionary containing instance data |
| | |
| | Returns: |
| | BaseModule: The created instance |
| | """ |
| | processed_data = {k: cls._process_data(v) for k, v in data.items()} |
| | |
| | return cls.model_validate(processed_data) |
| |
|
| | @classmethod |
| | def _process_data(cls, data: Any) -> Any: |
| | """ |
| | Recursive method for processing data, with special handling for dictionaries containing class_name. |
| | |
| | Args: |
| | data: Data to be processed |
| | |
| | Returns: |
| | Processed data |
| | """ |
| | if isinstance(data, dict): |
| | if "class_name" in data: |
| | sub_class = MODULE_REGISTRY.get_module(data.get("class_name")) |
| | return sub_class._create_instance(data) |
| | else: |
| | return {k: cls._process_data(v) for k, v in data.items()} |
| | elif isinstance(data, (list, tuple)): |
| | return [cls._process_data(x) for x in data] |
| | else: |
| | return data |
| |
|
| | @classmethod |
| | def from_dict(cls, data: Dict[str, Any], **kwargs) -> "BaseModule": |
| | """ |
| | Instantiate the BaseModule from a dictionary. |
| | |
| | Args: |
| | data: Dictionary containing instance data |
| | **kwargs (Any): Additional keyword arguments, can include log to control logging output |
| | |
| | Returns: |
| | BaseModule: The created module instance |
| | |
| | Raises: |
| | Exception: When errors occur during initialization |
| | """ |
| | use_logger = kwargs.get("log", True) |
| | with exception_buffer() as buffer: |
| | try: |
| | class_name = data.get("class_name", None) |
| | if class_name: |
| | cls = MODULE_REGISTRY.get_module(class_name) |
| | module = cls._create_instance(data) |
| | |
| | if len(buffer.exceptions) > 0: |
| | error_message = get_base_module_init_error_message(cls, data, buffer.exceptions) |
| | if use_logger: |
| | logger.error(error_message) |
| | raise Exception(get_error_message(buffer.exceptions)) |
| | finally: |
| | pass |
| | return module |
| | |
| | @classmethod |
| | def from_json(cls, content: str, **kwargs) -> "BaseModule": |
| | """ |
| | Construct the BaseModule from a JSON string. |
| | |
| | This method uses yaml.safe_load to parse the JSON string into a Python object, |
| | which supports more flexible parsing than standard json.loads (including handling |
| | single quotes, trailing commas, etc). The parsed data is then passed to from_dict |
| | to create the instance. |
| | |
| | Args: |
| | content: JSON string |
| | **kwargs (Any): Additional keyword arguments, can include `log` to control logging output |
| | |
| | Returns: |
| | BaseModule: The created module instance |
| | |
| | Raises: |
| | ValueError: When the input is not a valid JSON string |
| | """ |
| | use_logger = kwargs.get("log", True) |
| | try: |
| | data = yaml.safe_load(content) |
| | except Exception: |
| | error_message = f"Can not instantiate {cls.__name__}. The input to {cls.__name__}.from_json is not a valid JSON string." |
| | if use_logger: |
| | logger.error(error_message) |
| | raise ValueError(error_message) |
| | |
| | if not isinstance(data, (list, dict)): |
| | error_message = f"Can not instantiate {cls.__name__}. The input to {cls.__name__}.from_json is not a valid JSON string." |
| | if use_logger: |
| | logger.error(error_message) |
| | raise ValueError(error_message) |
| |
|
| | return cls.from_dict(data, log=use_logger) |
| | |
| | @classmethod |
| | def from_str(cls, content: str, **kwargs) -> "BaseModule": |
| | """ |
| | Construct the BaseModule from a string that may contain JSON. |
| | |
| | This method is more forgiving than `from_json` as it can extract valid JSON |
| | objects embedded within larger text. It uses `parse_json_from_text` to extract |
| | all potential JSON strings from the input text, then tries to create an instance |
| | from each extracted JSON string until successful. |
| | |
| | Args: |
| | content: Text that may contain JSON strings |
| | **kwargs (Any): Additional keyword arguments, can include `log` to control logging output |
| | |
| | Returns: |
| | BaseModule: The created module instance |
| | |
| | Raises: |
| | ValueError: When the input does not contain valid JSON strings or the JSON is incompatible with the class |
| | """ |
| | use_logger = kwargs.get("log", True) |
| | |
| | extracted_json_list = parse_json_from_text(content) |
| | if len(extracted_json_list) == 0: |
| | error_message = f"The input to {cls.__name__}.from_str does not contain any valid JSON str." |
| | if use_logger: |
| | logger.error(error_message) |
| | raise ValueError(error_message) |
| | |
| | module = None |
| | for json_str in extracted_json_list: |
| | try: |
| | module = cls.from_json(json_str, log=False) |
| | except Exception: |
| | continue |
| | break |
| | |
| | if module is None: |
| | error_message = f"Can not instantiate {cls.__name__}. The input to {cls.__name__}.from_str either does not contain a valide JSON str, or the JSON str is incomplete or incompatable (incorrect variables or types) with {cls.__name__}." |
| | error_message += f"\nInput:\n{content}" |
| | if use_logger: |
| | logger.error(error_message) |
| | raise ValueError(error_message) |
| | |
| | return module |
| | |
| | @classmethod |
| | def load_module(cls, path: str, **kwargs) -> dict: |
| | """ |
| | Load the values for a module from a file. |
| | |
| | By default, it opens the specified file and uses `yaml.safe_load` to parse its contents |
| | into a Python object (typically a dictionary). |
| | |
| | Args: |
| | path: The path of the file |
| | **kwargs (Any): Additional keyword arguments |
| | |
| | Returns: |
| | dict: The JSON object instantiated from the file |
| | """ |
| | with open(path, mode="r", encoding="utf-8") as file: |
| | content = yaml.safe_load(file.read()) |
| | return content |
| |
|
| | @classmethod |
| | def from_file(cls, path: str, load_function: Callable=None, **kwargs) -> "BaseModule": |
| | """ |
| | Construct the BaseModule from a file. |
| | |
| | This method reads and parses a file into a data structure, then creates |
| | a module instance from that data. It first verifies that the file exists, |
| | then uses either the provided `load_function` or the default `load_module` |
| | method to read and parse the file content, and finally calls `from_dict` |
| | to create the instance. |
| | |
| | Args: |
| | path: The path of the file |
| | load_function: The function used to load the data, takes a file path as input and returns a JSON object |
| | **kwargs (Any): Additional keyword arguments, can include `log` to control logging output |
| | |
| | Returns: |
| | BaseModule: The created module instance |
| | |
| | Raises: |
| | ValueError: When the file does not exist |
| | """ |
| | use_logger = kwargs.get("log", True) |
| | if not os.path.exists(path): |
| | error_message = f"File \"{path}\" does not exist!" |
| | if use_logger: |
| | logger.error(error_message) |
| | raise ValueError(error_message) |
| | |
| | function = load_function or cls.load_module |
| | content = function(path, **kwargs) |
| | module = cls.from_dict(content, log=use_logger) |
| |
|
| | return module |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | def to_dict(self, exclude_none: bool = True, ignore: List[str] = [], **kwargs) -> dict: |
| | """ |
| | Convert the BaseModule to a dictionary. |
| | |
| | Args: |
| | exclude_none: Whether to exclude fields with None values |
| | ignore: List of field names to ignore |
| | **kwargs (Any): Additional keyword arguments |
| | |
| | Returns: |
| | dict: Dictionary containing the object data |
| | """ |
| | data = {} |
| | for field_name, _ in type(self).model_fields.items(): |
| | if field_name in ignore: |
| | continue |
| | field_value = getattr(self, field_name, None) |
| | if exclude_none and field_value is None: |
| | continue |
| | if isinstance(field_value, BaseModule): |
| | data[field_name] = field_value.to_dict(exclude_none=exclude_none, ignore=ignore) |
| | elif isinstance(field_value, list): |
| | data[field_name] = [ |
| | item.to_dict(exclude_none=exclude_none, ignore=ignore) if isinstance(item, BaseModule) else item |
| | for item in field_value |
| | ] |
| | elif isinstance(field_value, dict): |
| | data[field_name] = { |
| | key: value.to_dict(exclude_none=exclude_none, ignore=ignore) if isinstance(value, BaseModule) else value |
| | for key, value in field_value.items() |
| | } |
| | else: |
| | data[field_name] = field_value |
| | |
| | return data |
| | |
| | def to_json(self, use_indent: bool=False, ignore: List[str] = [], **kwargs) -> str: |
| | """ |
| | Convert the BaseModule to a JSON string. |
| | |
| | Args: |
| | use_indent: Whether to use indentation |
| | ignore: List of field names to ignore |
| | **kwargs (Any): Additional keyword arguments |
| | |
| | Returns: |
| | str: The JSON string |
| | """ |
| | if use_indent: |
| | kwargs["indent"] = kwargs.get("indent", 4) |
| | else: |
| | kwargs.pop("indent", None) |
| | if kwargs.get("default", None) is None: |
| | kwargs["default"] = custom_serializer |
| | data = self.to_dict(exclude_none=True) |
| | for ignore_field in ignore: |
| | data.pop(ignore_field, None) |
| | return json.dumps(data, **kwargs) |
| | |
| | def to_str(self, **kwargs) -> str: |
| | """ |
| | Convert the BaseModule to a string. Use .to_json to output JSON string by default. |
| | |
| | Args: |
| | **kwargs (Any): Additional keyword arguments |
| | |
| | Returns: |
| | str: The string |
| | """ |
| | return self.to_json(use_indent=False) |
| | |
| | def save_module(self, path: str, ignore: List[str] = [], **kwargs)-> str: |
| | """ |
| | Save the BaseModule to a file. |
| | |
| | This method will set non-serializable objects to None by default. |
| | If you want to save non-serializable objects, override this method. |
| | Remember to also override the `load_module` function to ensure the loaded |
| | object can be correctly parsed by `cls.from_dict`. |
| | |
| | Args: |
| | path: The path to save the file |
| | ignore: List of field names to ignore |
| | **kwargs (Any): Additional keyword arguments |
| | |
| | Returns: |
| | str: The path where the file is saved, same as the input path |
| | """ |
| | logger.info("Saving {} to {}", self.__class__.__name__, path) |
| | return save_json(self.to_json(use_indent=True, default=lambda x: None, ignore=ignore), path=path) |
| | |
| | def deepcopy(self): |
| | """Deep copy the module. |
| | |
| | This is a tweak to the default python deepcopy that only deep copies `self.parameters()`, and for other |
| | attributes, we just do the shallow copy. |
| | """ |
| | try: |
| | |
| | |
| | return copy.deepcopy(self) |
| | except Exception: |
| | pass |
| |
|
| | |
| | new_instance = self.__class__.__new__(self.__class__) |
| | |
| | for attr, value in self.__dict__.items(): |
| | if isinstance(value, BaseModule): |
| | setattr(new_instance, attr, value.deepcopy()) |
| | else: |
| | try: |
| | |
| | setattr(new_instance, attr, copy.deepcopy(value)) |
| | except Exception: |
| | logging.warning( |
| | f"Failed to deep copy attribute '{attr}' of {self.__class__.__name__}, " |
| | "falling back to shallow copy or reference copy." |
| | ) |
| | try: |
| | |
| | setattr(new_instance, attr, copy.copy(value)) |
| | except Exception: |
| | |
| | setattr(new_instance, attr, value) |
| |
|
| | return new_instance |
| | __all__ = ["BaseModule"] |
| |
|
| |
|