# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Services Module. Central service registry that provides access to shared services. IMPORTANT: Import-Regelwerk - Zentrale Module (wie dieses) dürfen KEINE Feature-Container importieren - Feature-spezifische Services werden dynamisch geladen - Nur Shared Services werden direkt geladen """ import os import importlib import glob from typing import Any, Optional, TYPE_CHECKING import logging from modules.datamodels.datamodelUam import User if TYPE_CHECKING: from modules.features.aichat.datamodelFeatureAiChat import ChatWorkflow logger = logging.getLogger(__name__) # Path to feature containers _FEATURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "features") class PublicService: """Lightweight proxy exposing only public callable attributes of a target.""" def __init__(self, target: Any, functionsOnly: bool = True, nameFilter=None): self._target = target self._functionsOnly = functionsOnly self._nameFilter = nameFilter def __getattr__(self, name: str): if name.startswith('_'): raise AttributeError(f"'{type(self._target).__name__}' attribute '{name}' is private") if self._nameFilter and not self._nameFilter(name): raise AttributeError(f"'{name}' not exposed by policy") attr = getattr(self._target, name) if self._functionsOnly and not callable(attr): raise AttributeError(f"'{name}' is not a function") return attr def __dir__(self): return sorted([ n for n in dir(self._target) if not n.startswith('_') and (not self._functionsOnly or callable(getattr(self._target, n, None))) and (self._nameFilter(n) if self._nameFilter else True) ]) class Services: """ Central Services class providing access to all services. Import-Regelwerk: - Shared Services are loaded directly (from modules/services/) - Feature-specific Services are loaded dynamically via filename discovery """ def __init__(self, user: User, workflow: "ChatWorkflow" = None, mandateId: Optional[str] = None): self.user: User = user self.workflow = workflow self.mandateId: Optional[str] = mandateId self.currentUserPrompt: str = "" self.rawUserPrompt: str = "" # Initialize central interfaces from modules.interfaces.interfaceDbApp import getInterface as getAppInterface self.interfaceDbApp = getAppInterface(user, mandateId=mandateId) from modules.interfaces.interfaceDbManagement import getInterface as getComponentInterface self.interfaceDbComponent = getComponentInterface(user, mandateId=mandateId) self.rbac = self.interfaceDbApp.rbac if self.interfaceDbApp else None # ============================================================ # SHARED SERVICES (from modules/services/) # ============================================================ from .serviceSharepoint.mainServiceSharepoint import SharepointService self.sharepoint = PublicService(SharepointService(self)) from .serviceTicket.mainServiceTicket import TicketService self.ticket = PublicService(TicketService(self)) from .serviceChat.mainServiceChat import ChatService self.chat = PublicService(ChatService(self)) from .serviceUtils.mainServiceUtils import UtilsService self.utils = PublicService(UtilsService(self)) from .serviceSecurity.mainServiceSecurity import SecurityService self.security = PublicService(SecurityService(self)) from .serviceMessaging.mainServiceMessaging import MessagingService self.messaging = PublicService(MessagingService(self)) # ============================================================ # FEATURE SERVICES (dynamically loaded by filename discovery) # ============================================================ self._loadFeatureInterfaces() self._loadFeatureServices() def _loadFeatureInterfaces(self): """Dynamically load interfaces from feature containers by filename pattern.""" # Find all interfaceFeature*.py files pattern = os.path.join(_FEATURES_DIR, "*", "interfaceFeature*.py") for filepath in glob.glob(pattern): try: # Extract feature name and interface name featureDir = os.path.basename(os.path.dirname(filepath)) filename = os.path.basename(filepath)[:-3] # Remove .py # Build module path: modules.features.. modulePath = f"modules.features.{featureDir}.{filename}" module = importlib.import_module(modulePath) # Get interface via getInterface() if hasattr(module, "getInterface"): interface = module.getInterface(self.user, mandateId=self.mandateId) # Derive attribute name: interfaceFeatureAiChat -> interfaceDbChat attrName = filename.replace("interfaceFeature", "interfaceDb") setattr(self, attrName, interface) logger.debug(f"Loaded interface: {attrName} from {modulePath}") except Exception as e: logger.debug(f"Could not load interface from {filepath}: {e}") def _loadFeatureServices(self): """Dynamically load services from feature containers by filename pattern.""" # Find all service*/mainService*.py files in feature containers pattern = os.path.join(_FEATURES_DIR, "*", "service*", "mainService*.py") for filepath in glob.glob(pattern): try: # Extract paths serviceDir = os.path.basename(os.path.dirname(filepath)) featureDir = os.path.basename(os.path.dirname(os.path.dirname(filepath))) filename = os.path.basename(filepath)[:-3] # Remove .py # Build module path: modules.features... modulePath = f"modules.features.{featureDir}.{serviceDir}.{filename}" module = importlib.import_module(modulePath) # Find service class (ends with "Service") serviceClass = None for name in dir(module): if name.endswith("Service") and not name.startswith("_"): cls = getattr(module, name) if isinstance(cls, type): serviceClass = cls break if serviceClass: # Derive attribute name: serviceAi -> ai, serviceExtraction -> extraction attrName = serviceDir.replace("service", "").lower() if not attrName: attrName = serviceDir.lower() # Check if it needs functionsOnly=False (for AI service) functionsOnly = attrName != "ai" serviceInstance = serviceClass(self) setattr(self, attrName, PublicService(serviceInstance, functionsOnly=functionsOnly)) logger.debug(f"Loaded service: {attrName} from {modulePath}") except Exception as e: logger.debug(f"Could not load service from {filepath}: {e}") def getInterface(user: User, workflow: "ChatWorkflow" = None, mandateId: Optional[str] = None) -> Services: """Get Services instance for the given user and mandate context.""" return Services(user, workflow, mandateId=mandateId)