# 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.