/ src / solace_agent_mesh / common / services / employee_service.py
employee_service.py
  1  """
  2  Defines the abstract base class and factory for creating Employee Service providers.
  3  """
  4  
  5  import logging
  6  import importlib
  7  from abc import ABC, abstractmethod
  8  from typing import Any, Dict, List, Optional
  9  import importlib.metadata as metadata
 10  
 11  
 12  from ..utils.in_memory_cache import InMemoryCache
 13  import pandas as pd
 14  
 15  log = logging.getLogger(__name__)
 16  
 17  class BaseEmployeeService(ABC):
 18      """
 19      Abstract base class for Employee Service providers.
 20  
 21      This class defines a "thin provider" contract. Implementations should focus
 22      solely on fetching data from a source system (like an HR platform) and
 23      mapping it to the canonical format. All complex business logic, such as
 24      building organizational charts or calculating availability schedules, should
 25      be handled by the tools that consume this service.
 26  
 27      Canonical Employee Schema
 28      This schema defines the standard fields that all EmployeeService providers should aim to return.
 29      - id (string): A unique, stable, and lowercase identifier for the employee (e.g., email, GUID).
 30      - displayName (string): The employee's full name for display purposes.
 31      - workEmail (string): The employee's primary work email address.
 32      - jobTitle (string): The employee's official job title.
 33      - department (string): The department the employee belongs to.
 34      - location (string): The physical or regional location of the employee.
 35      - supervisorId (string): The unique id of the employee's direct manager.
 36      - hireDate (string): The date the employee was hired (ISO 8601: YYYY-MM-DD).
 37      - mobilePhone (string): The employee's mobile phone number (optional).
 38  
 39      """
 40  
 41      def __init__(self, config: Dict[str, Any]):
 42          """
 43          Initializes the service with its specific configuration block.
 44  
 45          Args:
 46              config: The dictionary of configuration parameters for this provider.
 47          """
 48          self.config = config
 49          self.log_identifier = f"[{self.__class__.__name__}]"
 50          self.cache_ttl = config.get("cache_ttl_seconds", 3600)
 51          self.cache = InMemoryCache() if self.cache_ttl > 0 else None
 52          log.info(
 53              "%s Initialized. Cache TTL: %d seconds.",
 54              self.log_identifier,
 55              self.cache_ttl,
 56          )
 57  
 58      @abstractmethod
 59      async def get_employee_dataframe(self) -> pd.DataFrame:
 60          """Returns the entire employee directory as a pandas DataFrame."""
 61          pass
 62  
 63      @abstractmethod
 64      async def get_employee_profile(self, employee_id: str) -> Optional[Dict[str, Any]]:
 65          """Fetches the profile for a single employee."""
 66          pass
 67  
 68      @abstractmethod
 69      async def get_time_off_data(self, employee_id: str, start_date: Optional[str] = None, end_date: Optional[str] = None) -> List[Dict[str, Any]]:
 70          """
 71          Retrieves a list of raw time-off entries for an employee.
 72  
 73          The tool consuming this data is responsible for interpreting it.
 74          Each dictionary in the list MUST contain the following keys:
 75          - 'start' (str): The start date of the leave (YYYY-MM-DD).
 76          - 'end' (str): The end date of the leave (YYYY-MM-DD).
 77          - 'type' (str): The category of leave (e.g., 'Vacation', 'Sick', 'Holiday').
 78          - 'amount' (str): The amount of time taken, must be one of 'full_day' or 'half_day'.
 79  
 80          Example:
 81          [
 82              {'start': '2025-07-04', 'end': '2025-07-04', 'type': 'Holiday', 'amount': 'full_day'},
 83              {'start': '2025-08-15', 'end': '2025-08-15', 'type': 'Vacation', 'amount': 'full_day'}
 84          ]
 85  
 86          Args:
 87              employee_id: The unique identifier for the employee.
 88  
 89          Returns:
 90              A list of dictionaries, each representing a time-off entry.
 91          """
 92          pass
 93  
 94      @abstractmethod
 95      async def get_employee_profile_picture(self, employee_id: str) -> Optional[str]:
 96          """
 97          Fetches an employee's profile picture and returns it as a data URI.
 98  
 99          Args:
100              employee_id: The unique identifier for the employee.
101  
102          Returns:
103              A string containing the data URI (e.g., 'data:image/jpeg;base64,...')
104              or None if not available.
105          """
106          pass
107  
108  
109  def create_employee_service(
110      config: Optional[Dict[str, Any]],
111  ) -> Optional[BaseEmployeeService]:
112      """
113      Factory function to create an instance of an Employee Service provider
114      based on the provided configuration.
115      """
116      if not config:
117          log.info(
118              "[EmployeeFactory] No 'employee_service' configuration found. Skipping creation."
119          )
120          return None
121  
122      provider_type = config.get("type")
123      if not provider_type:
124          raise ValueError("Employee service config must contain a 'type' key.")
125  
126      log.info(
127          f"[EmployeeFactory] Attempting to create employee service of type: {provider_type}"
128      )
129  
130      try:
131          entry_points = metadata.entry_points(group="solace_agent_mesh.plugins")
132          provider_info_entry = next(
133              (ep for ep in entry_points if ep.name == provider_type), None
134          )
135  
136          if not provider_info_entry:
137              raise ValueError(
138                  f"No plugin provider found for type '{provider_type}' under the 'solace_agent_mesh.plugins' entry point."
139              )
140  
141          provider_info = provider_info_entry.load()
142          class_path = provider_info.get("class_path")
143          if not class_path:
144              raise ValueError(
145                  f"Plugin '{provider_type}' is missing 'class_path' in its info dictionary."
146              )
147  
148          module_path, class_name = class_path.rsplit(".", 1)
149          module = importlib.import_module(module_path)
150          provider_class = getattr(module, class_name)
151  
152          if not issubclass(provider_class, BaseEmployeeService):
153              raise TypeError(
154                  f"Provider class '{class_path}' does not inherit from BaseEmployeeService."
155              )
156  
157          log.info(f"Successfully loaded employee provider plugin: {provider_type}")
158          return provider_class(config)
159      except (ImportError, AttributeError, TypeError, ValueError) as e:
160          log.exception(
161              f"[EmployeeFactory] Failed to load employee provider plugin '{provider_type}'. "
162              "Ensure the plugin is installed and the entry point is correct."
163          )
164          raise ValueError(f"Could not load employee provider plugin: {e}") from e