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>
12 KiB
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 |
| 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 :
{
"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 :
{
"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). 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.