13 KiB
Gateway Service Architecture Documentation
This document describes the structure, design patterns, and key components of the two service architectures in the gateway:
modules/serviceCenter— the new service center (context-based DI, RBAC-aware)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
objectKeyfor 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:
context: ServiceCenterContext— user, mandate_id, feature_instance_id, workflowget_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 aget_servicecallable 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_SERVICESto resolve onlychat,ai,billing,streaming. - Returns a
_ChatbotServiceHubobject withchat,ai,billing,streaming,interfaceDbComponent, etc.
1.5 Initialization and Bootstrapping
-
app.pylifespan:registerAllFeaturesInCatalog(catalogService)→ callsregisterServiceObjects(catalogService)for service RBAC objectspreWarm()— imports all service modules to avoid first-request latency
-
registerAllFeaturesInCatalog(modules/system/registry.py):- Registers system RBAC objects
- Registers service center RBAC objects via
registerServiceObjects - Registers feature RBAC objects
-
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__frommodules/services/serviceX/mainServiceX.py. - Feature services: Discovered dynamically via
_loadFeatureServices()— scansmodules/features/*/service*/mainService*.pyand 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 hub’s 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
-
No startup bootstrap — services load on first
getInterface()call. -
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()— discoversinterfaceFeature*.pyin features - Calls
_loadFeatureServices()— discoversservice*/mainService*.pyin features, overrides hub attributes
- Loads DB interfaces (
-
Feature services: If a feature defines
serviceAi/mainServiceAi.py, it overridesservices.ai. SharedserviceAiis 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_hubis passed togetService. - 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,getServicefromserviceBilling). ServiceCenterContextis used when callinggetService. Features that passworkflow=Noneuse 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 |