Architecture backend
Détail de l'architecture hexagonale du service Core, couches, ports, adapters et patterns d'implémentation.
Le service Core suit strictement l'architecture hexagonale (Ports & Adapters). Ce document détaille chaque couche, ses responsabilités, et les patterns d'implémentation.
Structure des couches
services/core/src/
├── domain/ # Entités, services domaine, règles métier pures
├── application/ # Use cases, ports (outbound), services d'application
│ ├── ports/
│ │ └── outbound/ # Interfaces (Protocol) que l'infra doit implémenter
│ ├── services/
│ │ └── executors/ # Node executors (Agent, Condition, Loop, etc.)
│ └── use_cases/ # Orchestration des opérations métier
├── infrastructure/ # Adapters concrets
│ ├── persistence/ # SQLAlchemy models, repositories, session, UoW
│ ├── llm/ # Adapters LLM (OpenAI, Bedrock, Ollama, etc.)
│ ├── secrets/ # Chiffrement Fernet
│ ├── logging/ # structlog adapter
│ ├── observability/ # OpenTelemetry tracing + metrics
│ └── di/ # Conteneur d'injection de dépendances
└── interfaces/ # Couche HTTP
├── api/
│ ├── routes/ # Endpoints FastAPI
│ ├── schemas/ # Pydantic request/response DTO
│ └── dependencies/ # Auth, tenant context
└── middleware/Le domaine
Le domaine contient les entités et les règles métier pures. Aucune dépendance externe — juste du Python standard.
Entités principales :
| Entité | Responsabilité |
|---|---|
Flow | Graphe de nodes avec leurs connexions (edges) |
Node | Bloc fonctionnel (Agent, Condition, etc.) avec sa configuration |
Run | Instance d'exécution d'un flow avec son état |
Channel | Point de déploiement d'un flow |
Tenant | Espace isolé d'un client |
La couche application
Ports (interfaces)
Les ports définissent ce dont l'application a besoin sans savoir comment c'est implémenté. Ils sont tous dans application/ports/outbound/ et utilisent Protocol (Python typing).
Principaux ports :
| Port | Responsabilité |
|---|---|
IFlowRepository | CRUD des flows |
IRunRepository | Gestion des exécutions |
IChannelRepository | Gestion des channels |
ITenantRepository | Gestion des tenants |
IApiKeyRepository | Clés API par channel |
ISecretRepository | Secrets chiffrés |
IFlowTemplateRepository | Templates réutilisables |
IWorkspaceAISettingsRepository | Configuration IA par workspace |
IEventStore | Journal d'événements (audit) |
IMetricsPort | Collection de métriques |
LLMPort | Appel aux fournisseurs LLM |
LoggerPort | Logging structuré |
SecretsPort | Récupération de secrets (partagé) |
Use cases
Chaque opération métier est un use case isolé. Pattern standard :
class CreateFlowUseCase:
def __init__(self, flow_repo: IFlowRepository, logger: LoggerPort):
self._flow_repo = flow_repo
self._logger = logger
async def execute(self, tenant_id: str, data: CreateFlowInput) -> Flow:
# Validation métier
# Appel aux ports
# Retour du résultat domaineLes use cases ne connaissent que les ports, jamais les adapters concrets. L'injection de dépendances (via le DIContainer) fournit les implémentations.
Node executors
Les executors sont dans application/services/executors/. Chaque type de node a son executor :
| Executor | Node | Rôle |
|---|---|---|
AgentNodeExecutor | Agent | Appel LLM avec prompt, tools, streaming |
CodeExecutor | Code | Exécution Python sandboxée |
ConditionExecutor | Condition | Évaluation d'expressions, branchement |
HTTPRequestExecutor | HTTP Request | Appel API externe |
HumanInputExecutor | Human Input | Pause en attente de saisie humaine |
LoopContainerExecutor | Loop | Itération sur une liste |
ParseDocExecutor | Parse Document | Extraction de texte (PDF, DOCX) |
MergeDocsExecutor | Merge Documents | Fusion de plusieurs documents |
SetVariableExecutor | Set Variable | Définition de variable de contexte |
DirectMessageExecutor | Direct Message | Envoi de message direct |
TriggerFromChannelExecutor | Trigger Channel | Déclenchement depuis un channel |
ManualTriggerExecutor | Trigger Manual | Déclenchement manuel |
Le NodeExecutorRegistry (dans application/services/) résout le bon executor en fonction du node_type.
Pour les détails de chaque executor, voir Référence des node executors.
L'infrastructure
Persistence (SQLAlchemy)
- ORM : SQLAlchemy 2 en mode async (
asyncpgpour PostgreSQL,aiosqlitepour SQLite dev). - Pattern : Repository + Unit of Work.
- Migrations : Alembic (dans
services/core/alembic/). - Multi-tenant : Chaque modèle porte un
tenant_id. Les repositories filtrent automatiquement.
Modèles principaux : FlowModel, RunModel, ChannelModel, TenantModel, UserModel, ApiKeyModel, SecretModel, FlowTemplateModel, PolicyModel, EventModel, AgentTemplateModel, WorkspaceAISettingsModel.
LLM adapters
Chaque fournisseur LLM a son adapter dans infrastructure/llm/ qui implémente le LLMPort :
| Adapter | Fournisseur |
|---|---|
OpenAIAdapter | OpenAI (GPT-4o, GPT-4 Turbo) |
AzureOpenAIAdapter | Azure OpenAI (accès entreprise) |
BedrockAdapter | AWS Bedrock (Claude, Titan, Llama) |
WatsonXAdapter | IBM WatsonX (Granite, Llama) |
OllamaAdapter | Ollama (modèles locaux) |
VLLMAdapter | vLLM (modèles auto-hébergés) |
Le LLMAdapterFactory résout le bon adapter à partir de la configuration du workspace et des secrets.
Secrets
Les secrets (clés API, credentials) sont chiffrés au repos avec Fernet (AES-128). L'adapter DatabaseSecretsAdapter gère le chiffrement/déchiffrement transparent.
Observabilité
- Logging :
structlogavec leStructlogAdapterqui implémenteLoggerPort. - Tracing : OpenTelemetry via
OTelTracingAdapter. - Métriques :
LogMetricsAdapterimplémenteIMetricsPort.
Injection de dépendances
Le DIContainer (infrastructure/di/) câble les ports aux adapters au démarrage. Les routes FastAPI utilisent Depends() pour injecter les use cases configurés.
La couche interfaces
Routes
Les endpoints FastAPI sont dans interfaces/api/routes/. Chaque module correspond à un domaine fonctionnel (flows, runs, channels, tenants, etc.).
Schémas
Les DTOs Pydantic sont dans interfaces/api/schemas/. Ils valident les entrées et formatent les sorties. Ils ne contiennent pas de logique métier.
Dépendances
auth.py: Validation JWT, extraction du user, vérification du tenant.tenant.py: Injection duTenantContextdans le cycle de vie de la requête.
Pour aller plus loin
- Référence des node executors — Détail de chaque executor
- Référence API — Endpoints HTTP
- Guide de test — Comment tester chaque couche