gateway/docs/SERVICE_ARCHITECTURE_DOCUMENTATION.md
2026-03-06 14:03:18 +01:00

13 KiB
Raw Blame History

Gateway Service Architecture Documentation

This document describes the structure, design patterns, and key components of the two service architectures in the gateway:

  1. modules/serviceCenter — the new service center (context-based DI, RBAC-aware)
  2. modules/services — the legacy services hub (monolithic hub, eager loading)

1. modules/serviceCenter — New Service Center

1.1 File/Folder Structure

modules/serviceCenter/
├── __init__.py           # Public API: getService, preWarm, registerServiceObjects, can_access_service
├── context.py            # ServiceCenterContext dataclass
├── registry.py           # Service definitions (CORE_SERVICES, IMPORTABLE_SERVICES, RBAC)
├── resolver.py           # Resolution logic, dependency injection, legacy fallback
├── core/                 # Core services (internal building blocks, no RBAC)
│   ├── __init__.py
│   ├── serviceUtils/
│   │   └── mainServiceUtils.py
│   ├── serviceSecurity/
│   │   └── mainServiceSecurity.py
│   └── serviceStreaming/
│       └── mainServiceStreaming.py
└── services/             # Feature-facing importable services (RBAC-protected)
    ├── __init__.py
    ├── serviceAi/
    ├── serviceBilling/
    ├── serviceChat/
    ├── serviceExtraction/
    ├── serviceGeneration/
    ├── serviceMessaging/
    ├── serviceNeutralization/
    ├── serviceSharepoint/
    ├── serviceTicket/
    └── serviceWeb/

Design distinction:

  • Core services — Internal utilities (utils, security, streaming). Never directly requested by features. No RBAC. Security and streaming are fully migrated; legacy hub delegates to service center.
  • Importable services — Feature-facing. Have objectKey for RBAC (e.g. service.web, service.extraction).

1.2 Service Registration and Discovery

Registration: Services are declared statically in registry.py.

  • CORE_SERVICES: Internal services with module, class, dependencies.
  • IMPORTABLE_SERVICES: Feature-facing services with module, class, dependencies, objectKey, label.
  • SERVICE_RBAC_OBJECTS: Derived from IMPORTABLE_SERVICES for catalog registration.

Discovery: No dynamic discovery. All services are explicitly listed in the registry. Adding a service requires editing registry.py.

# Example from registry.py
IMPORTABLE_SERVICES = {
    "ai": {
        "module": "modules.serviceCenter.services.serviceAi.mainServiceAi",
        "class": "AiService",
        "dependencies": ["chat", "utils", "extraction", "billing"],
        "objectKey": "service.ai",
        "label": {"en": "AI", "de": "KI", "fr": "IA"},
    },
    # ...
}

1.3 Dependency Injection and Factory Patterns

Constructor pattern: Services receive two arguments from the resolver:

  1. context: ServiceCenterContext — user, mandate_id, feature_instance_id, workflow
  2. get_service: Callable[[str], Any] — function to resolve other services by key
# Service Center service constructor
def __init__(self, context, get_service: Callable[[str], Any]):
    self._context = context
    self._get_service = get_service

Dependency resolution:

  • The resolver (resolver.py) builds a get_service callable that recursively resolves dependencies.
  • Dependencies are declared in the registry (e.g. "dependencies": ["chat", "utils", "extraction", "billing"]).
  • Circular dependencies are detected and raise RuntimeError.
  • Resolution is cached per (user, mandate_id, feature_instance_id) + key.

Legacy fallback: If a service fails to load from the service center, the resolver falls back to the legacy Services hub when legacy_hub is provided.


1.4 Main Entry Points and Usage Patterns

Entry Point Purpose
getService(key, context, legacy_hub=None) Resolve a service by key for the given context
preWarm(service_keys=None) Pre-load service modules at startup (called in app.py lifespan)
registerServiceObjects(catalogService) Register service RBAC objects (called via registerAllFeaturesInCatalog)
can_access_service(user, rbac, service_key, ...) RBAC check for service access
ServiceCenterContext(user, mandate_id, feature_instance_id, workflow) Context dataclass

Typical usage (chatbot feature):

from modules.serviceCenter import getService
from modules.serviceCenter.context import ServiceCenterContext

ctx = ServiceCenterContext(user=user, mandate_id=mandateId, feature_instance_id=featureInstanceId, workflow=workflow)
ai_service = getService("ai", ctx, legacy_hub=None)
chat_service = getService("chat", ctx, legacy_hub=None)

Feature-level hub (e.g. chatbot):

  • getChatbotServices() builds a lightweight hub with only required services.
  • Uses REQUIRED_SERVICES to resolve only chat, ai, billing, streaming.
  • Returns a _ChatbotServiceHub object with chat, ai, billing, streaming, interfaceDbComponent, etc.

1.5 Initialization and Bootstrapping

  1. app.py lifespan:

    • registerAllFeaturesInCatalog(catalogService) → calls registerServiceObjects(catalogService) for service RBAC objects
    • preWarm() — imports all service modules to avoid first-request latency
  2. registerAllFeaturesInCatalog (modules/system/registry.py):

    • Registers system RBAC objects
    • Registers service center RBAC objects via registerServiceObjects
    • Registers feature RBAC objects
  3. First request:

    • getService(key, ctx)resolve() loads module, instantiates class with (context, get_service), caches instance

2. modules/services — Legacy Services Hub

2.1 File/Folder Structure

modules/services/
├── __init__.py           # Services class, getInterface(), PublicService, _loadFeatureInterfaces, _loadFeatureServices
├── serviceAi/
│   └── mainServiceAi.py
├── serviceBilling/
│   └── mainServiceBilling.py
├── serviceChat/
│   └── mainServiceChat.py
├── serviceExtraction/
│   ├── extractors/
│   ├── chunking/
│   ├── merging/
│   ├── subRegistry.py
│   ├── subPipeline.py
│   └── mainServiceExtraction.py
├── serviceGeneration/
│   ├── paths/
│   ├── renderers/
│   └── mainServiceGeneration.py
├── serviceMessaging/
│   └── mainServiceMessaging.py
├── serviceNormalization/
│   └── mainServiceNormalization.py
├── serviceSharepoint/
│   └── mainServiceSharepoint.py
├── serviceStreaming/
│   ├── eventManager.py
│   ├── helpers.py
│   └── mainServiceStreaming.py
├── serviceTicket/
│   └── mainServiceTicket.py
├── serviceUtils/
│   └── mainServiceUtils.py
├── serviceWeb/
│   └── mainServiceWeb.py
└── serviceSecurity/
    └── mainServiceSecurity.py

No core vs. services split. All services live in flat serviceX/ directories.


2.2 Service Registration and Discovery

Registration: Services are eagerly loaded in Services.__init__() via hardcoded imports. No registry file.

Discovery:

  • Shared services: Loaded explicitly in __init__ from modules/services/serviceX/mainServiceX.py.
  • Feature services: Discovered dynamically via _loadFeatureServices() — scans modules/features/*/service*/mainService*.py and instantiates classes ending with "Service".
# Shared services — hardcoded in Services.__init__
from .serviceSharepoint.mainServiceSharepoint import SharepointService
self.sharepoint = PublicService(SharepointService(self))
from .serviceChat.mainServiceChat import ChatService
self.chat = PublicService(ChatService(self))
# ... etc.

2.3 Dependency Injection / Factory Patterns

Constructor pattern: Services receive the entire Services hub as their single dependency.

# Legacy service constructor
def __init__(self, services):
    self.services = services

No explicit dependency graph. Services access other services via self.services.<attr> (e.g. self.services.interfaceDbComponent, self.services.extraction). All services are loaded at construction time.

PublicService proxy: Services are wrapped in PublicService(target, functionsOnly=True) to expose only callable, non-private attributes. Reduces accidental access to internal state.

BillingService: Uses a separate factory getService(currentUser, mandateId, featureInstanceId, featureCode) and a module-level cache. Not integrated with the hubs constructor pattern.


2.4 Main Entry Points and Usage Patterns

Entry Point Purpose
getInterface(user, workflow, mandateId, featureInstanceId) Returns a Services instance
Services Central hub with all services and interfaces

Typical usage:

from modules.services import getInterface as getServices

services = getServices(user, workflow, mandateId=mandateId, featureInstanceId=featureInstanceId)
ai = services.ai
extraction = services.extraction

Interfaces loaded at construction:

  • interfaceDbApp, interfaceDbComponent, interfaceDbChat, rbac
  • Plus dynamically loaded interfaceFeature* from feature containers

2.5 Initialization and Bootstrapping

  1. No startup bootstrap — services load on first getInterface() call.

  2. Construction flow:

    • getInterface(user, ...)Services(user, ...)
    • Services.__init__:
      • Loads DB interfaces (interfaceDbApp, interfaceDbComponent, interfaceDbChat)
      • Instantiates all shared services (sharepoint, ticket, chat, utils, security, streaming, ai, extraction, generation, web)
      • Calls _loadFeatureInterfaces() — discovers interfaceFeature*.py in features
      • Calls _loadFeatureServices() — discovers service*/mainService*.py in features, overrides hub attributes
  3. Feature services: If a feature defines serviceAi/mainServiceAi.py, it overrides services.ai. Shared serviceAi is only used when no feature override exists.


3. Side-by-Side Comparison

Aspect Service Center Legacy Services
Location modules/serviceCenter/ modules/services/
Entry point getService(key, context, legacy_hub) getInterface(user, ...)Services
Constructor (context, get_service) (services) — full hub
Context ServiceCenterContext (user, mandate_id, feature_instance_id, workflow) Full Services with interfaces
Dependencies Declared in registry, resolved lazily via get_service("key") Via self.services.<attr>
Loading Lazy (on first getService), cached per context Eager (all at construction)
RBAC Per-service objectKey in registry, can_access_service() Shared via hub .rbac
Feature services Not applicable — features use getService(key, ctx) Discovered via _loadFeatureServices()
Pre-warm preWarm() in app lifespan None
Bootstrap registerServiceObjects() via registerAllFeaturesInCatalog None

4. Coexistence and Migration

  • Service center can fall back to legacy hub when legacy_hub is passed to getService.
  • Chatbot uses service center via getChatbotServices() and does not use the legacy hub.
  • Billing, routes, teamsbot, commcoach, etc. still use modules.services (e.g. getInterface, getService from serviceBilling).
  • ServiceCenterContext is used when calling getService. Features that pass workflow=None use a placeholder workflow for billing/featureCode.
  • Migration plan: docs/SERVICE_CENTER_MIGRATION_PLAN.md.

5. Service Center Resolver Flow

getService("ai", ctx, legacy_hub)
    → resolve("ai", ctx, cache, resolving, legacy_hub)
    → Check cache (cache_key = user_mandate_feature_ai)
    → If cache hit: return cached instance
    → If miss:
        → _load_service_class("modules.serviceCenter.services.serviceAi.mainServiceAi", "AiService")
        → Resolve dependencies: chat, utils, extraction, billing (recursive resolve)
        → instance = AiService(ctx, get_service)
        → cache[cache_key] = instance
        → return instance
    → On ImportError/ModuleNotFoundError: _get_from_legacy(legacy_hub, "ai") if legacy_hub

6. Key Files Reference

File Purpose
serviceCenter/registry.py Service definitions, dependency graph, RBAC keys
serviceCenter/resolver.py Resolution logic, caching, legacy fallback
serviceCenter/context.py ServiceCenterContext dataclass
serviceCenter/__init__.py getService, preWarm, registerServiceObjects, can_access_service
services/__init__.py Services class, getInterface, PublicService, feature discovery
system/registry.py registerAllFeaturesInCatalog (calls registerServiceObjects)
app.py Lifespan: preWarm(), registerAllFeaturesInCatalog()
features/chatbot/mainChatbot.py Example: getChatbotServices() using service center