Monorepo pnpm avec NestJS backend en architecture hexagonale. - Structure hexagonale complète (ports, adapters, domain entities) - 9 entities TypeORM (Home, User, Device, Credentials, Session, Message, Memory, Timer) - Migration initiale SQL avec pgvector support - Docker Compose (PostgreSQL 16 + pgvector + Redis 7) - Config partagée (tsconfig, ESLint, Prettier) - Outbound ports définis (STT, TTS, LLM, Cache, Storage, VectorStore) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
257 lines
12 KiB
Markdown
257 lines
12 KiB
Markdown
# Modèle de données
|
|
|
|
## Diagramme des entités
|
|
|
|
```
|
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
│ Home │ 1───N │ User │ 1───N │ UserService │
|
|
│ │ │ │ │ Credential │
|
|
│ id │ │ id │ │ │
|
|
│ name │ │ home_id (FK) │ │ id │
|
|
│ created_at │ │ email │ │ user_id (FK) │
|
|
│ updated_at │ │ password_hash│ │ service_type │
|
|
└──────────────┘ │ display_name │ │ encrypted_ │
|
|
│ role │ │ tokens │
|
|
│ preferences │ │ metadata │
|
|
│ created_at │ │ created_at │
|
|
│ updated_at │ │ updated_at │
|
|
└──────┬───────┘ └──────────────┘
|
|
│
|
|
│ 1───N
|
|
▼
|
|
┌──────────────┐ ┌──────────────┐
|
|
│ Device │ │Conversation │
|
|
│ │ │ Session │
|
|
│ id │ │ │
|
|
│ home_id (FK) │ │ id │
|
|
│ name │ │ user_id (FK) │
|
|
│ device_token │ │ device_id(FK)│
|
|
│ config │ │ status │
|
|
│ status │ │ started_at │
|
|
│ last_seen_at │ │ ended_at │
|
|
│ created_at │ │ summary │
|
|
│ updated_at │ │ created_at │
|
|
└──────────────┘ └──────┬───────┘
|
|
│
|
|
│ 1───N
|
|
▼
|
|
┌──────────────┐ ┌──────────────┐
|
|
│ Message │ │MemoryEntry │
|
|
│ │ │ │
|
|
│ id │ │ id │
|
|
│ session_id │ │ user_id (FK) │
|
|
│ (FK) │ │ session_id │
|
|
│ role │ │ (FK, opt) │
|
|
│ content │ │ type │
|
|
│ tool_calls │ │ content │
|
|
│ audio_url │ │ tags │
|
|
│ created_at │ │ created_at │
|
|
└──────────────┘ │ updated_at │
|
|
└──────────────┘
|
|
|
|
┌──────────────┐ ┌──────────────┐
|
|
│MemoryEmbed │ │ Timer │
|
|
│ │ │ │
|
|
│ id │ │ id │
|
|
│ memory_id(FK)│ │ user_id (FK) │
|
|
│ user_id (FK) │ │ device_id(FK)│
|
|
│ embedding │ │ type │
|
|
│ vector(1536) │ label │
|
|
│ created_at │ │ trigger_at │
|
|
└──────────────┘ │ recurrence │
|
|
│ status │
|
|
│ created_at │
|
|
└──────────────┘
|
|
```
|
|
|
|
## Détail des tables
|
|
|
|
### Home
|
|
|
|
Représente un foyer. Permet de regrouper plusieurs utilisateurs et robots dans un même espace logique (inspiré du modèle Google Home).
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| name | VARCHAR(100) | Nom du foyer ("Maison d'Arthur") |
|
|
| created_at | TIMESTAMPTZ | Date de création |
|
|
| updated_at | TIMESTAMPTZ | Dernière modification |
|
|
|
|
### User
|
|
|
|
Un utilisateur du système. Rattaché à un Home.
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| home_id | UUID (FK → Home) | Foyer de l'utilisateur |
|
|
| email | VARCHAR(255) UNIQUE | Email de connexion |
|
|
| password_hash | VARCHAR(255) | Hash bcrypt du mot de passe |
|
|
| display_name | VARCHAR(100) | Nom affiché |
|
|
| role | ENUM('owner', 'member') | Rôle dans le foyer |
|
|
| preferences | JSONB | Préférences utilisateur (langue, timezone, etc.) |
|
|
| created_at | TIMESTAMPTZ | Date de création |
|
|
| updated_at | TIMESTAMPTZ | Dernière modification |
|
|
|
|
Le champ `preferences` est un JSONB flexible pour stocker les préférences sans migration de schéma à chaque ajout :
|
|
|
|
```json
|
|
{
|
|
"language": "fr",
|
|
"timezone": "Europe/Paris",
|
|
"tts_voice": "elevenlabs_rachel",
|
|
"llm_model": "gpt-4",
|
|
"wake_word": "hey-ti-pote",
|
|
"do_not_disturb": {
|
|
"enabled": true,
|
|
"start": "23:00",
|
|
"end": "07:00"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Device
|
|
|
|
Un robot Ti-Pote physique. Rattaché à un Home, partagé entre les Users du Home.
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| home_id | UUID (FK → Home) | Foyer du robot |
|
|
| name | VARCHAR(100) | Nom du robot ("Ti-Pote Bureau") |
|
|
| device_token_hash | VARCHAR(255) | Hash du JWT token du device |
|
|
| config | JSONB | Configuration hardware (modules actifs, volume, etc.) |
|
|
| status | ENUM('online', 'offline', 'updating') | Statut actuel |
|
|
| firmware_version | VARCHAR(20) | Version du firmware |
|
|
| last_seen_at | TIMESTAMPTZ | Dernier ping reçu |
|
|
| created_at | TIMESTAMPTZ | Date d'enregistrement |
|
|
| updated_at | TIMESTAMPTZ | Dernière modification |
|
|
|
|
Le champ `config` :
|
|
|
|
```json
|
|
{
|
|
"modules": {
|
|
"camera": true,
|
|
"mobile_base": false,
|
|
"screen": true
|
|
},
|
|
"volume": 75,
|
|
"led_brightness": 50,
|
|
"mic_sensitivity": "auto"
|
|
}
|
|
```
|
|
|
|
### UserServiceCredential
|
|
|
|
Tokens OAuth et credentials pour les services tiers, chiffrés en AES-256-GCM.
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| user_id | UUID (FK → User) | Propriétaire |
|
|
| service_type | ENUM('google', 'apple', 'microsoft', 'smtp', 'whatsapp') | Type de service |
|
|
| encrypted_tokens | TEXT | Tokens chiffrés (access + refresh) |
|
|
| metadata | JSONB | Infos non sensibles (email du compte, scopes, etc.) |
|
|
| expires_at | TIMESTAMPTZ | Expiration de l'access token (pour refresh proactif) |
|
|
| created_at | TIMESTAMPTZ | Date de liaison |
|
|
| updated_at | TIMESTAMPTZ | Dernier refresh |
|
|
|
|
### ConversationSession
|
|
|
|
Une session de conversation (du wake word au silence / fin de conversation).
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| user_id | UUID (FK → User) | Utilisateur qui parle |
|
|
| device_id | UUID (FK → Device) | Robot utilisé |
|
|
| status | ENUM('active', 'ended', 'timeout') | Statut |
|
|
| started_at | TIMESTAMPTZ | Début de la session |
|
|
| ended_at | TIMESTAMPTZ | Fin de la session |
|
|
| summary | TEXT | Résumé généré par le LLM à la clôture |
|
|
| extracted_facts | JSONB | Faits extraits de la conversation |
|
|
| message_count | INTEGER | Nombre de messages |
|
|
| total_tokens | INTEGER | Tokens LLM consommés |
|
|
| created_at | TIMESTAMPTZ | Date de création |
|
|
|
|
### Message
|
|
|
|
Un message dans une conversation (message utilisateur ou réponse de Ti-Pote).
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| session_id | UUID (FK → ConversationSession) | Session parente |
|
|
| role | ENUM('user', 'assistant', 'system', 'tool') | Rôle dans la conversation |
|
|
| content | TEXT | Contenu textuel du message |
|
|
| tool_calls | JSONB | Si le LLM a appelé des functions |
|
|
| tool_result | JSONB | Si c'est la réponse d'un tool call |
|
|
| audio_url | VARCHAR(500) | URL du fichier audio (optionnel, pour replay) |
|
|
| tokens_used | INTEGER | Tokens consommés pour ce message |
|
|
| created_at | TIMESTAMPTZ | Timestamp du message |
|
|
|
|
### MemoryEntry
|
|
|
|
Un souvenir extrait d'une conversation ou ajouté manuellement. C'est la mémoire épisodique et sémantique.
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| user_id | UUID (FK → User) | Utilisateur concerné |
|
|
| session_id | UUID (FK → ConversationSession, nullable) | Session source (si extrait d'une conversation) |
|
|
| type | ENUM('fact', 'preference', 'episode', 'profile') | Type de souvenir |
|
|
| content | TEXT | Contenu du souvenir en langage naturel |
|
|
| tags | TEXT[] | Tags pour le filtrage rapide |
|
|
| importance | FLOAT | Score d'importance (0-1), utilisé pour le ranking |
|
|
| is_active | BOOLEAN DEFAULT true | Soft delete (l'utilisateur peut "oublier") |
|
|
| created_at | TIMESTAMPTZ | Date de création |
|
|
| updated_at | TIMESTAMPTZ | Dernière modification |
|
|
|
|
### MemoryEmbedding
|
|
|
|
Vecteur d'embedding associé à un souvenir, pour la recherche sémantique.
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| memory_id | UUID (FK → MemoryEntry) | Souvenir associé |
|
|
| user_id | UUID (FK → User) | Pour l'indexation rapide |
|
|
| embedding | VECTOR(1536) | Vecteur d'embedding |
|
|
| created_at | TIMESTAMPTZ | Date de création |
|
|
|
|
Index : `USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)` — optimisé pour la recherche par similarité cosinus.
|
|
|
|
### Timer
|
|
|
|
Minuteurs et alarmes.
|
|
|
|
| Colonne | Type | Description |
|
|
|---------|------|-------------|
|
|
| id | UUID (PK) | Identifiant unique |
|
|
| user_id | UUID (FK → User) | Propriétaire |
|
|
| device_id | UUID (FK → Device) | Robot qui doit sonner |
|
|
| type | ENUM('timer', 'alarm') | Minuteur ou alarme |
|
|
| label | VARCHAR(200) | Description ("Pâtes", "Réveil") |
|
|
| trigger_at | TIMESTAMPTZ | Quand déclencher |
|
|
| recurrence | VARCHAR(50) | Règle de récurrence (cron-like, nullable) |
|
|
| status | ENUM('active', 'triggered', 'cancelled') | Statut |
|
|
| created_at | TIMESTAMPTZ | Date de création |
|
|
|
|
## Données en Redis (non persistées en SQL)
|
|
|
|
### Session active (clé : `session:{session_id}`)
|
|
Historique des messages de la conversation en cours. TTL configurable (par défaut 3 minutes de silence, voir [memory-system.md](memory-system.md)). Structure : liste ordonnée de messages JSON.
|
|
|
|
### Statut device (clé : `device:{device_id}:status`)
|
|
État en temps réel du robot (connecté, en écoute, en train de parler…). TTL de 60 secondes, rafraîchi par heartbeat.
|
|
|
|
### Timer actif (clé : `timer:{timer_id}`)
|
|
Duplication du timer en Redis avec TTL pour la notification via keyspace events. PostgreSQL est la source de vérité (persistance, récurrence), Redis sert uniquement de trigger temps réel.
|
|
|
|
## Migrations
|
|
|
|
On utilise un outil de migration TypeScript compatible avec NestJS. Options recommandées : TypeORM Migrations, Prisma Migrate, ou Knex Migrations. Le choix sera fait au moment de l'implémentation en fonction de l'ORM retenu.
|
|
|
|
Chaque migration est versionnée et réversible. Pas de modification directe du schéma en production.
|