# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Service Hub. Consumer-facing aggregation layer for services, DB interfaces, and runtime state. Architecture: - serviceHub delegates service resolution to serviceCenter (DI container) - serviceHub owns DB interface initialization and runtime state - serviceCenter knows nothing about serviceHub (one-way dependency) Import-Regelwerk: - Zentrale Module (wie dieses) duerfen KEINE Feature-Container importieren - Feature-spezifische Services werden dynamisch geladen - Shared Services werden via serviceCenter resolved """ 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.datamodels.datamodelChat import ChatWorkflow logger = logging.getLogger(__name__) _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 ServiceHub: """ Consumer-facing aggregation of services, DB interfaces, and runtime state. Services are lazy-resolved via serviceCenter on first access. DB interfaces and runtime state are initialized eagerly. Feature services/interfaces are discovered dynamically from features/. """ _SERVICE_CENTER_WRAPPING = { "ai": {"functionsOnly": False}, } def __init__(self, user: User, workflow: "ChatWorkflow" = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None): self.user: User = user self.workflow = workflow self.mandateId: Optional[str] = mandateId self.featureInstanceId: Optional[str] = featureInstanceId self.currentUserPrompt: str = "" self.rawUserPrompt: str = "" from modules.serviceCenter.context import ServiceCenterContext self._serviceCenterContext = ServiceCenterContext( user=user, workflow=workflow, mandate_id=mandateId, feature_instance_id=featureInstanceId, ) 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 from modules.interfaces.interfaceDbChat import getInterface as getChatInterface self.interfaceDbChat = getChatInterface(user, mandateId=mandateId, featureInstanceId=featureInstanceId) self._loadFeatureInterfaces() self._loadFeatureServices() def __getattr__(self, name: str): """Lazy-resolve services via serviceCenter on first access.""" if name.startswith('_'): raise AttributeError(name) try: from modules.serviceCenter import getService service = getService(name, self._serviceCenterContext) wrapping = self._SERVICE_CENTER_WRAPPING.get(name, {}) functionsOnly = wrapping.get("functionsOnly", True) wrapped = PublicService(service, functionsOnly=functionsOnly) setattr(self, name, wrapped) return wrapped except KeyError: raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'") def _loadFeatureInterfaces(self): """Dynamically load interfaces from feature containers by filename pattern.""" pattern = os.path.join(_FEATURES_DIR, "*", "interfaceFeature*.py") for filepath in glob.glob(pattern): try: featureDir = os.path.basename(os.path.dirname(filepath)) filename = os.path.basename(filepath)[:-3] modulePath = f"modules.features.{featureDir}.{filename}" module = importlib.import_module(modulePath) if hasattr(module, "getInterface"): interface = module.getInterface(self.user, mandateId=self.mandateId, featureInstanceId=self.featureInstanceId) 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.""" pattern = os.path.join(_FEATURES_DIR, "*", "service*", "mainService*.py") for filepath in glob.glob(pattern): try: 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] modulePath = f"modules.features.{featureDir}.{serviceDir}.{filename}" module = importlib.import_module(modulePath) serviceClass = None for attrName in dir(module): if attrName.endswith("Service") and not attrName.startswith("_"): cls = getattr(module, attrName) if isinstance(cls, type): serviceClass = cls break if serviceClass: attrName = serviceDir.replace("service", "").lower() if not attrName: attrName = serviceDir.lower() 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}") # Backward-compatible alias Services = ServiceHub def getInterface(user: User, workflow: "ChatWorkflow" = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None) -> ServiceHub: """Get ServiceHub instance for the given user, mandate, and feature instance context.""" return ServiceHub(user, workflow, mandateId=mandateId, featureInstanceId=featureInstanceId)