aigency 0.0.1.dev20250904075757__py3-none-any.whl → 0.0.1rc62012314__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.
@@ -1,4 +1,24 @@
1
- """Configuration service for loading and parsing agent configurations."""
1
+ """Configuration service for loading and parsing agent configurations.
2
+
3
+ This module provides comprehensive configuration management for Aigency agents,
4
+ including YAML file loading, environment-specific configuration merging, and
5
+ validation through Pydantic models. It supports hierarchical configuration
6
+ structures and environment-based overrides.
7
+
8
+ The ConfigService class handles the complete configuration lifecycle from file
9
+ loading through parsing and validation, ensuring that agent configurations
10
+ are properly structured and ready for use by the agent system.
11
+
12
+ Example:
13
+ Loading and parsing agent configuration:
14
+
15
+ >>> service = ConfigService("config.yaml", environment="production")
16
+ >>> config = service.config
17
+ >>> agent_name = config.metadata.name
18
+
19
+ Attributes:
20
+ logger: Module-level logger instance for configuration events.
21
+ """
2
22
 
3
23
  import os
4
24
  from pathlib import Path
@@ -6,63 +26,136 @@ from typing import Any, Dict, Optional
6
26
 
7
27
  import yaml
8
28
 
9
- from aigency.models.config import AgentConfig
29
+ from aigency.schemas.aigency_config import AigencyConfig
10
30
  from aigency.utils.logger import get_logger
11
31
 
12
32
 
13
33
  logger = get_logger()
14
34
 
35
+
15
36
  class ConfigService:
16
- """Service for loading and managing agent configurations."""
37
+ """Service for loading and managing agent configurations.
38
+
39
+ This service handles loading YAML configuration files, merging environment-specific
40
+ configurations, and parsing them into AigencyConfig objects.
41
+
42
+ Attributes:
43
+ config_file (str): Path to the main configuration file.
44
+ environment (str | None): Environment name for environment-specific configs.
45
+ config (AigencyConfig): Parsed configuration object.
46
+ """
47
+
17
48
  def __init__(self, config_file: str, environment: Optional[str] = None):
49
+ """Initialize the configuration service.
50
+
51
+ Args:
52
+ config_file (str): Path to the main configuration file.
53
+ environment (str, optional): Environment name for environment-specific
54
+ configs. Defaults to None.
55
+ """
18
56
  self.config_file = config_file
19
- self.environment = environment or os.getenv('ENVIRONMENT', None)
57
+ self.environment = environment or os.getenv("ENVIRONMENT", None)
20
58
  self.config = self._load_and_parse()
21
59
 
22
- def _load_and_parse(self) -> AgentConfig:
23
- """Carga los YAMLs, los mergea y parsea según AgentConfig."""
60
+ def _load_and_parse(self) -> AigencyConfig:
61
+ """Load YAMLs, merge them and parse according to AigencyConfig.
62
+
63
+ Loads the main configuration file and optionally merges it with
64
+ environment-specific configuration if available.
65
+
66
+ Returns:
67
+ AigencyConfig: Parsed and validated configuration object.
68
+ """
24
69
 
25
70
  logger.info(f"Loading configuration from {self.config_file}")
26
71
  config = self._load_yaml(self.config_file)
27
72
 
28
73
  if self.environment is not None:
29
- logger.info(f"Environment '{self.environment}' detected, loading environment-specific configuration")
74
+ logger.info(
75
+ f"Environment '{self.environment}' detected, loading environment-specific configuration"
76
+ )
30
77
  env_config = self._load_env_config()
31
78
  if env_config:
32
- logger.info(f"Successfully loaded environment configuration with {len(env_config)} keys: {list(env_config.keys())}")
79
+ logger.info(
80
+ f"Successfully loaded environment configuration with {len(env_config)} keys: {list(env_config.keys())}"
81
+ )
33
82
  config = self._merge_configs(config, env_config)
34
- logger.debug(f"Configuration merged successfully for environment '{self.environment}'")
83
+ logger.debug(
84
+ f"Configuration merged successfully for environment '{self.environment}'"
85
+ )
35
86
  else:
36
- logger.warning(f"No environment-specific configuration found for '{self.environment}', using base configuration only")
37
-
38
- return AgentConfig(**config)
87
+ logger.warning(
88
+ f"No environment-specific configuration found for '{self.environment}', using base configuration only"
89
+ )
90
+
91
+ return AigencyConfig(**config)
39
92
 
40
93
  def _load_yaml(self, file_path: str) -> Dict[str, Any]:
41
- """Carga un archivo YAML."""
94
+ """Load a YAML file.
95
+
96
+ Args:
97
+ file_path (str): Path to the YAML file to load.
98
+
99
+ Returns:
100
+ Dict[str, Any]: Parsed YAML content as dictionary.
101
+
102
+ Raises:
103
+ FileNotFoundError: If the configuration file is not found.
104
+ ValueError: If there's an error parsing the YAML file.
105
+ """
42
106
  try:
43
107
  with open(file_path, "r", encoding="utf-8") as file:
44
108
  return yaml.safe_load(file) or {}
45
109
  except FileNotFoundError:
46
- raise FileNotFoundError(f"Archivo de configuración no encontrado: {file_path}")
110
+ raise FileNotFoundError(f"Configuration file not found: {file_path}")
47
111
  except yaml.YAMLError as e:
48
- raise ValueError(f"Error al parsear YAML {file_path}: {e}")
112
+ raise ValueError(f"Error parsing YAML {file_path}: {e}")
49
113
 
50
114
  def _load_env_config(self) -> Optional[Dict[str, Any]]:
51
- """Carga configuración específica del entorno."""
115
+ """Load environment-specific configuration.
116
+
117
+ Attempts to load a configuration file with environment-specific naming
118
+ pattern (e.g., config.dev.yaml for 'dev' environment).
119
+
120
+ Returns:
121
+ Dict[str, Any] | None: Environment configuration dictionary or None
122
+ if no environment-specific file exists.
123
+ """
52
124
  config_path = Path(self.config_file)
53
- env_file = config_path.parent / f"{config_path.stem}.{self.environment}{config_path.suffix}"
54
-
125
+ env_file = (
126
+ config_path.parent
127
+ / f"{config_path.stem}.{self.environment}{config_path.suffix}"
128
+ )
129
+
55
130
  return self._load_yaml(str(env_file)) if env_file.exists() else None
56
131
 
57
- def _merge_configs(self, base: Dict[str, Any], env: Optional[Dict[str, Any]]) -> Dict[str, Any]:
58
- """Mergea configuración base con configuración de entorno."""
132
+ def _merge_configs(
133
+ self, base: Dict[str, Any], env: Optional[Dict[str, Any]]
134
+ ) -> Dict[str, Any]:
135
+ """Merge base configuration with environment configuration.
136
+
137
+ Recursively merges environment-specific configuration into the base
138
+ configuration, with environment values taking precedence.
139
+
140
+ Args:
141
+ base (Dict[str, Any]): Base configuration dictionary.
142
+ env (Dict[str, Any] | None): Environment-specific configuration
143
+ dictionary.
144
+
145
+ Returns:
146
+ Dict[str, Any]: Merged configuration dictionary.
147
+ """
59
148
  if not env:
60
149
  return base
61
-
150
+
62
151
  result = base.copy()
63
152
  for key, value in env.items():
64
- if key in result and isinstance(result[key], dict) and isinstance(value, dict):
153
+ if (
154
+ key in result
155
+ and isinstance(result[key], dict)
156
+ and isinstance(value, dict)
157
+ ):
65
158
  result[key] = self._merge_configs(result[key], value)
66
159
  else:
67
160
  result[key] = value
68
- return result
161
+ return result
aigency/utils/logger.py CHANGED
@@ -1,3 +1,24 @@
1
+ """Singleton logger implementation for consistent application-wide logging.
2
+
3
+ This module provides a centralized logging system using the Singleton pattern to
4
+ ensure consistent logging behavior across the entire Aigency application. It supports
5
+ configurable log levels, multiple output handlers, and dynamic configuration updates.
6
+
7
+ The Logger class extends the Singleton base class to guarantee only one logger
8
+ instance exists throughout the application lifecycle, preventing configuration
9
+ conflicts and ensuring unified log formatting and output destinations.
10
+
11
+ Example:
12
+ Basic logger usage:
13
+
14
+ >>> logger = get_logger({"log_level": "DEBUG", "log_file": "app.log"})
15
+ >>> logger.info("Application started")
16
+ >>> logger.error("An error occurred", exc_info=True)
17
+
18
+ Attributes:
19
+ None: This module contains only class definitions and utility functions.
20
+ """
21
+
1
22
  import logging
2
23
  import sys
3
24
  from typing import Optional, Dict, Any
@@ -5,97 +26,165 @@ from aigency.utils.singleton import Singleton
5
26
 
6
27
 
7
28
  class Logger(Singleton):
29
+ """Singleton logger class for consistent logging across the application.
30
+
31
+ This class provides a centralized logging mechanism that ensures only one
32
+ logger instance exists throughout the application lifecycle.
33
+
34
+ Attributes:
35
+ config (Dict[str, Any]): Configuration dictionary for logger settings.
36
+ _logger (logging.Logger): Internal logger instance.
37
+ _initialized (bool): Flag to track initialization state.
38
+ """
39
+
8
40
  def __init__(self, config: Optional[Dict[str, Any]] = None):
9
- if hasattr(self, '_initialized'):
10
- # Si ya está inicializado y se pasa nueva config, actualizar
11
- if config and config != getattr(self, 'config', {}):
41
+ """Initialize the logger with optional configuration.
42
+
43
+ Args:
44
+ config (Dict[str, Any], optional): Dictionary containing logger
45
+ configuration. Defaults to None.
46
+ """
47
+ if hasattr(self, "_initialized"):
48
+ # If already initialized and new config is passed, update
49
+ if config and config != getattr(self, "config", {}):
12
50
  self.config.update(config)
13
51
  self._setup_logger()
14
52
  return
15
-
53
+
16
54
  self._initialized = True
17
55
  self.config = config or {}
18
56
  self._logger = None
19
57
  self._setup_logger()
20
-
58
+
21
59
  def _setup_logger(self):
22
- """Configura el logger con la configuración proporcionada"""
23
- # Obtener configuración del logger
24
- log_level = self.config.get('log_level', 'INFO').upper()
25
- log_format = self.config.get('log_format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
26
- log_file = self.config.get('log_file')
27
- logger_name = self.config.get('logger_name', 'aigency')
28
-
29
- # Crear logger
60
+ """Configure the logger with the provided configuration.
61
+
62
+ Sets up the internal logger instance with handlers, formatters, and
63
+ log levels based on the configuration dictionary.
64
+ """
65
+ # Get logger configuration
66
+ log_level = self.config.get("log_level", "INFO").upper()
67
+ log_format = self.config.get(
68
+ "log_format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
69
+ )
70
+ log_file = self.config.get("log_file")
71
+ logger_name = self.config.get("logger_name", "aigency")
72
+
73
+ # Create logger
30
74
  self._logger = logging.getLogger(logger_name)
31
75
  self._logger.setLevel(getattr(logging, log_level, logging.INFO))
32
-
33
- # Evitar duplicar handlers si ya existen
76
+
77
+ # Avoid duplicating handlers if they already exist
34
78
  if self._logger.handlers:
35
79
  return
36
-
37
- # Crear formatter
80
+
81
+ # Create formatter
38
82
  formatter = logging.Formatter(log_format)
39
-
40
- # Handler para consola
83
+
84
+ # Console handler
41
85
  console_handler = logging.StreamHandler(sys.stdout)
42
86
  console_handler.setLevel(getattr(logging, log_level, logging.INFO))
43
87
  console_handler.setFormatter(formatter)
44
88
  self._logger.addHandler(console_handler)
45
-
46
- # Handler para archivo si se especifica
89
+
90
+ # File handler if specified
47
91
  if log_file:
48
92
  file_handler = logging.FileHandler(log_file)
49
93
  file_handler.setLevel(getattr(logging, log_level, logging.INFO))
50
94
  file_handler.setFormatter(formatter)
51
95
  self._logger.addHandler(file_handler)
52
-
96
+
53
97
  def debug(self, message: str, *args, **kwargs):
54
- """Log a debug message"""
98
+ """Log a debug message.
99
+
100
+ Args:
101
+ message (str): The message to log.
102
+ *args: Variable length argument list.
103
+ **kwargs: Arbitrary keyword arguments.
104
+ """
55
105
  self._logger.debug(message, *args, **kwargs)
56
-
106
+
57
107
  def info(self, message: str, *args, **kwargs):
58
- """Log an info message"""
108
+ """Log an info message.
109
+
110
+ Args:
111
+ message (str): The message to log.
112
+ *args: Variable length argument list.
113
+ **kwargs: Arbitrary keyword arguments.
114
+ """
59
115
  self._logger.info(message, *args, **kwargs)
60
-
116
+
61
117
  def warning(self, message: str, *args, **kwargs):
62
- """Log a warning message"""
118
+ """Log a warning message.
119
+
120
+ Args:
121
+ message (str): The message to log.
122
+ *args: Variable length argument list.
123
+ **kwargs: Arbitrary keyword arguments.
124
+ """
63
125
  self._logger.warning(message, *args, **kwargs)
64
-
126
+
65
127
  def error(self, message: str, *args, **kwargs):
66
- """Log an error message"""
128
+ """Log an error message.
129
+
130
+ Args:
131
+ message (str): The message to log.
132
+ *args: Variable length argument list.
133
+ **kwargs: Arbitrary keyword arguments.
134
+ """
67
135
  self._logger.error(message, *args, **kwargs)
68
-
136
+
69
137
  def critical(self, message: str, *args, **kwargs):
70
- """Log a critical message"""
138
+ """Log a critical message.
139
+
140
+ Args:
141
+ message (str): The message to log.
142
+ *args: Variable length argument list.
143
+ **kwargs: Arbitrary keyword arguments.
144
+ """
71
145
  self._logger.critical(message, *args, **kwargs)
72
-
146
+
73
147
  def exception(self, message: str, *args, **kwargs):
74
- """Log an exception with traceback"""
148
+ """Log an exception with traceback.
149
+
150
+ Args:
151
+ message (str): The message to log.
152
+ *args: Variable length argument list.
153
+ **kwargs: Arbitrary keyword arguments.
154
+ """
75
155
  self._logger.exception(message, *args, **kwargs)
76
-
156
+
77
157
  def set_level(self, level: str):
78
- """Cambiar el nivel de logging dinámicamente"""
158
+ """Change the logging level dynamically.
159
+
160
+ Args:
161
+ level (str): The new logging level as a string.
162
+ """
79
163
  log_level = level.upper()
80
164
  self._logger.setLevel(getattr(logging, log_level, logging.INFO))
81
165
  for handler in self._logger.handlers:
82
166
  handler.setLevel(getattr(logging, log_level, logging.INFO))
83
-
167
+
84
168
  def get_logger(self):
85
- """Obtener la instancia del logger interno"""
169
+ """Get the internal logger instance.
170
+
171
+ Returns:
172
+ logging.Logger: The internal logging.Logger instance.
173
+ """
86
174
  return self._logger
87
175
 
88
176
 
89
- # Función de conveniencia para obtener la instancia del logger
177
+ # Convenience function to get the logger instance
90
178
  def get_logger(config: Optional[Dict[str, Any]] = None) -> Logger:
91
- """
92
- Obtiene la instancia singleton del logger.
93
- Si es la primera vez que se llama y se proporciona config, se usa esa configuración.
94
-
179
+ """Get the singleton logger instance.
180
+
181
+ If this is the first call and config is provided, that configuration is used.
182
+
95
183
  Args:
96
- config: Configuración opcional para el logger (solo se usa en la primera llamada)
97
-
184
+ config (Dict[str, Any], optional): Optional configuration for the logger
185
+ (only used on first call). Defaults to None.
186
+
98
187
  Returns:
99
- Instancia singleton del Logger
188
+ Logger: Singleton Logger instance.
100
189
  """
101
- return Logger(config)
190
+ return Logger(config)
@@ -1,7 +1,54 @@
1
+ """Singleton pattern implementation using metaclasses.
2
+
3
+ This module provides a robust implementation of the Singleton design pattern using
4
+ Python metaclasses. It ensures that classes inheriting from the Singleton base class
5
+ can have only one instance throughout the application lifecycle, which is useful
6
+ for shared resources like loggers, configuration managers, and database connections.
7
+
8
+ The implementation uses a metaclass approach to control instance creation at the
9
+ class level, providing thread-safe singleton behavior without requiring explicit
10
+ synchronization in the client code.
11
+
12
+ Example:
13
+ Creating singleton classes:
14
+
15
+ >>> class DatabaseManager(Singleton):
16
+ ... def __init__(self):
17
+ ... self.connection = "db_connection"
18
+ >>>
19
+ >>> db1 = DatabaseManager()
20
+ >>> db2 = DatabaseManager()
21
+ >>> db1 is db2
22
+ True
23
+
24
+ Attributes:
25
+ None: This module contains only class definitions.
26
+ """
27
+
28
+
1
29
  class SingletonMeta(type):
30
+ """Metaclass that implements the Singleton pattern.
31
+
32
+ This metaclass ensures that only one instance of a class can exist.
33
+ When a class uses this metaclass, subsequent instantiation attempts
34
+ will return the same instance.
35
+
36
+ Attributes:
37
+ _instances (dict): Dictionary storing singleton instances by class.
38
+ """
39
+
2
40
  _instances = {}
3
41
 
4
42
  def __call__(cls, *args, **kwargs):
43
+ """Control instance creation to ensure singleton behavior.
44
+
45
+ Args:
46
+ *args: Variable length argument list for class instantiation.
47
+ **kwargs: Arbitrary keyword arguments for class instantiation.
48
+
49
+ Returns:
50
+ object: The singleton instance of the class.
51
+ """
5
52
  if cls not in cls._instances:
6
53
  instance = super().__call__(*args, **kwargs)
7
54
  cls._instances[cls] = instance
@@ -9,4 +56,18 @@ class SingletonMeta(type):
9
56
 
10
57
 
11
58
  class Singleton(metaclass=SingletonMeta):
12
- pass
59
+ """Base class for implementing singleton pattern.
60
+
61
+ Classes that inherit from this base class will automatically
62
+ follow the singleton pattern, ensuring only one instance exists.
63
+
64
+ Example:
65
+ >>> class MyClass(Singleton):
66
+ ... pass
67
+ >>> obj1 = MyClass()
68
+ >>> obj2 = MyClass()
69
+ >>> obj1 is obj2
70
+ True
71
+ """
72
+
73
+ pass
aigency/utils/utils.py CHANGED
@@ -1,4 +1,23 @@
1
- """Utility functions for type conversions and environment variable handling."""
1
+ """Utility functions for type conversions and environment variable handling.
2
+
3
+ This module provides essential utility functions for the Aigency framework, including
4
+ type conversions between A2A and Google GenAI formats, environment variable expansion,
5
+ URL generation, and safe asynchronous execution helpers.
6
+
7
+ The utilities handle common operations needed across the framework, such as converting
8
+ message parts between different protocol formats, managing environment variables in
9
+ configurations, and safely running coroutines in various asyncio contexts.
10
+
11
+ Example:
12
+ Converting between A2A and GenAI formats:
13
+
14
+ >>> a2a_part = TextPart(text="Hello world")
15
+ >>> genai_part = convert_a2a_part_to_genai(a2a_part)
16
+ >>> back_to_a2a = convert_genai_part_to_a2a(genai_part)
17
+
18
+ Attributes:
19
+ logger: Module-level logger instance for utility operations.
20
+ """
2
21
 
3
22
  import asyncio
4
23
  import os
@@ -11,6 +30,7 @@ from aigency.utils.logger import get_logger
11
30
 
12
31
  logger = get_logger()
13
32
 
33
+
14
34
  def convert_a2a_part_to_genai(part: Part) -> types.Part:
15
35
  """Convert a single A2A Part type into a Google Gen AI Part type.
16
36
 
@@ -54,10 +74,19 @@ def convert_genai_part_to_a2a(part: types.Part) -> Part:
54
74
  )
55
75
  raise ValueError(f"Unsupported part type: {part}")
56
76
 
77
+
57
78
  def expand_env_vars(env_dict):
58
- """
59
- Expande los valores del diccionario usando variables de entorno solo si el valor es una clave de entorno existente.
60
- Si la variable no existe en el entorno, deja el valor literal.
79
+ """Expand dictionary values using environment variables.
80
+
81
+ Expands values in the dictionary using environment variables only if the value
82
+ is an existing environment variable key. If the variable doesn't exist in the
83
+ environment, leaves the literal value.
84
+
85
+ Args:
86
+ env_dict (dict): Dictionary with potential environment variable references.
87
+
88
+ Returns:
89
+ dict: Dictionary with expanded environment variable values.
61
90
  """
62
91
  result = {}
63
92
  for k, v in env_dict.items():
@@ -67,8 +96,36 @@ def expand_env_vars(env_dict):
67
96
  logger.warning(f"Environment variable {v} not found")
68
97
  return result
69
98
 
99
+
100
+ def generate_url(host: str, port: int, path: str = "") -> str:
101
+ """Generate a URL from host, port, and path components.
102
+
103
+ Args:
104
+ host (str): Hostname or IP address.
105
+ port (int): Port number.
106
+ path (str, optional): URL path. Defaults to "".
107
+
108
+ Returns:
109
+ str: Complete URL in the format http://host:port/path.
110
+ """
111
+ return f"http://{host}:{port}{path}"
112
+
113
+
70
114
  def safe_async_run(coro):
71
- """Simple wrapper to safely run async code."""
115
+ """Simple wrapper to safely run async code.
116
+
117
+ This function handles different asyncio event loop scenarios to safely
118
+ execute coroutines, including cases where a loop is already running.
119
+
120
+ Args:
121
+ coro: The coroutine to execute.
122
+
123
+ Returns:
124
+ Any: The result of the coroutine execution.
125
+
126
+ Raises:
127
+ Exception: Any exception raised by the coroutine.
128
+ """
72
129
  try:
73
130
  loop = asyncio.get_event_loop()
74
131
  if loop.is_running():
@@ -93,4 +150,4 @@ def safe_async_run(coro):
93
150
  else:
94
151
  return loop.run_until_complete(coro)
95
152
  except RuntimeError:
96
- return asyncio.run(coro)
153
+ return asyncio.run(coro)