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

318 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.
```python
# 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
```python
# 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):**
```python
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"`.
```python
# 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.
```python
# 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:**
```python
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 |