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>
18 KiB
Architecture Logicielle
Principes directeurs
Ti-Pote suit une architecture hexagonale (Ports & Adapters). L'objectif est de découpler totalement la logique métier des détails d'implémentation (bases de données, APIs tierces, protocoles de communication). Chaque brique peut être remplacée sans impacter le reste du système.
Pourquoi l'architecture hexagonale ?
Le projet intègre de nombreux services externes (STT, TTS, LLM, Google Calendar, SMTP…) qui sont susceptibles de changer. En isolant chaque intégration derrière un port (interface TypeScript), on peut swapper un provider sans toucher au code métier. Par exemple, passer de Deepgram à Whisper pour le STT ne modifie que l'adaptateur, pas le service de conversation.
Vue d'ensemble
┌─────────────────────────────────────────────┐
│ ADAPTATEURS ENTRANTS │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │WebSocket │ │ REST API │ │ Web App │ │
│ │(Robot) │ │(Config) │ │(React) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼─────────────┼────────┘
│ │ │
┌───────▼──────────────▼─────────────▼────────┐
│ │
│ PORTS ENTRANTS │
│ (Interfaces TypeScript) │
│ │
│ IConversationPort IConfigPort │
│ IAuthPort IDevicePort │
│ │
├─────────────────────────────────────────────┤
│ │
│ CORE (DOMAINE) │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Conversation │ │ Calendar │ │
│ │ Service │ │ Service │ │
│ └───────────────┘ └───────────────┘ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Mail │ │ Timer/Alarm │ │
│ │ Service │ │ Service │ │
│ └───────────────┘ └───────────────┘ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Memory │ │ WebSearch │ │
│ │ Service │ │ Service │ │
│ └───────────────┘ └───────────────┘ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ User │ │ Device │ │
│ │ Service │ │ Service │ │
│ └───────────────┘ └───────────────┘ │
│ ┌───────────────┐ │
│ │ Context │ │
│ │ Builder │ │
│ └───────────────┘ │
│ │
├─────────────────────────────────────────────┤
│ │
│ PORTS SORTANTS │
│ (Interfaces TypeScript) │
│ │
│ ISTTPort ITTSPort │
│ ILLMPort ICalendarPort │
│ IMailPort ISearchPort │
│ IStoragePort IVectorStorePort │
│ ICachePort INotificationPort │
│ │
├─────────────────────────────────────────────┤
│ ADAPTATEURS SORTANTS │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Deepgram │ │ElevenLabs│ │OpenAI │ │
│ │(STT) │ │(TTS) │ │(LLM) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Google │ │SMTP │ │PostgreSQL│ │
│ │Calendar │ │(Mail) │ │(Storage) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ │
│ │Redis │ │pgvector │ │
│ │(Cache) │ │(Vectors) │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
Services du Core
ConversationService
Le service central qui orchestre chaque échange vocal. Responsabilités :
- Recevoir l'audio streamé depuis le robot via WebSocket
- Déléguer la transcription au port STT
- Construire le contexte (via ContextBuilder) en injectant la mémoire pertinente
- Envoyer le prompt au LLM avec les définitions de functions/tools
- Interpréter la réponse du LLM : réponse directe ou function call
- Si function call → exécuter via le service concerné → renvoyer le résultat au LLM
- Envoyer la réponse texte au TTS et streamer l'audio de retour
Audio entrant (robot)
│
▼
┌──────────┐ texte ┌──────────────┐ prompt ┌─────────┐
│ STT │ ──────────► │ Context │ ───────────► │ LLM │
│ │ │ Builder │ │ │
└──────────┘ └──────────────┘ └────┬────┘
│
┌─────────────────────────────┤
│ │
function call réponse directe
│ │
▼ ▼
┌──────────────┐ ┌──────────┐
│ Service │ │ TTS │
│ métier │ │ │
│ (Calendar, │ └────┬─────┘
│ Mail, etc.) │ │
└──────┬───────┘ ▼
│ Audio sortant (robot)
▼
Résultat → LLM → TTS → Audio
ContextBuilder
Service dédié à la construction du prompt envoyé au LLM. Il assemble :
- System prompt — Personnalité de Ti-Pote, instructions, capacités disponibles
- Profil utilisateur — Faits connus sur l'utilisateur (préférences, contacts fréquents…)
- Souvenirs pertinents — Récupérés par recherche sémantique dans pgvector
- Historique de session — Les N derniers messages de la conversation active (depuis Redis)
- Définitions de tools — Les functions que le LLM peut appeler
- Message courant — La transcription de ce que l'utilisateur vient de dire
Le ContextBuilder respecte un budget de tokens configurable. Si le contexte dépasse le budget, il priorise : message courant > historique récent > profil > souvenirs, et tronque les éléments les moins prioritaires.
CalendarService
Gestion des rendez-vous et événements. Expose des méthodes métier (createEvent, listEvents, findFreeSlots…) qui sont mappées comme tools pour le LLM. L'adaptateur sortant implémente l'intégration OAuth2 avec Google Calendar (et Apple Calendar à terme).
MailService
Envoi et lecture d'emails. Supporte SMTP pour l'envoi et IMAP pour la lecture. À terme, intégration WhatsApp et autres messageries.
TimerAlarmService
Gestion des minuteurs et alarmes. Les timers sont stockés en Redis avec un TTL. Quand le timer expire, le robot est notifié via WebSocket pour jouer un son ou annoncer vocalement la fin du timer.
MemoryService
Gère les 3 niveaux de mémoire (session, épisodique, sémantique). Voir memory-system.md pour le détail complet.
WebSearchService
Recherche sur internet à la demande de l'utilisateur. Utilise une API de recherche (SearXNG auto-hébergé ou API tierce) et peut extraire le contenu des pages pour le résumer.
UserService
Gestion des utilisateurs, authentification, préférences. Stockage des credentials chiffrés (AES-256) pour les services tiers.
DeviceService
Gestion des robots (devices). Enregistrement, statut de connexion, configuration (wake word, volume, langue…). Chaque robot maintient une connexion WebSocket persistante identifiée par un device ID.
Structure NestJS proposée
src/
├── main.ts
├── app.module.ts
│
├── core/ # Domaine métier (aucune dépendance externe)
│ ├── ports/
│ │ ├── inbound/ # Ports entrants (interfaces)
│ │ │ ├── conversation.port.ts
│ │ │ ├── config.port.ts
│ │ │ ├── auth.port.ts
│ │ │ └── device.port.ts
│ │ └── outbound/ # Ports sortants (interfaces)
│ │ ├── stt.port.ts
│ │ ├── tts.port.ts
│ │ ├── llm.port.ts
│ │ ├── calendar.port.ts
│ │ ├── mail.port.ts
│ │ ├── search.port.ts
│ │ ├── storage.port.ts
│ │ ├── vector-store.port.ts
│ │ └── cache.port.ts
│ ├── services/
│ │ ├── conversation.service.ts
│ │ ├── context-builder.service.ts
│ │ ├── calendar.service.ts
│ │ ├── mail.service.ts
│ │ ├── timer-alarm.service.ts
│ │ ├── memory.service.ts
│ │ ├── web-search.service.ts
│ │ ├── user.service.ts
│ │ └── device.service.ts
│ ├── domain/ # Entités et value objects
│ │ ├── user.entity.ts
│ │ ├── device.entity.ts
│ │ ├── conversation-session.entity.ts
│ │ ├── memory-entry.entity.ts
│ │ └── ...
│ └── tools/ # Définitions des functions pour le LLM
│ ├── calendar.tools.ts
│ ├── mail.tools.ts
│ ├── timer.tools.ts
│ ├── search.tools.ts
│ └── index.ts
│
├── adapters/
│ ├── inbound/ # Adaptateurs entrants
│ │ ├── websocket/
│ │ │ └── robot.gateway.ts # WebSocket Gateway NestJS
│ │ ├── rest/
│ │ │ ├── config.controller.ts
│ │ │ ├── auth.controller.ts
│ │ │ └── device.controller.ts
│ │ └── web/ # Serveur du frontend
│ │ └── static.module.ts
│ └── outbound/ # Adaptateurs sortants
│ ├── stt/
│ │ ├── deepgram.adapter.ts
│ │ └── whisper.adapter.ts
│ ├── tts/
│ │ ├── elevenlabs.adapter.ts
│ │ └── azure-tts.adapter.ts
│ ├── llm/
│ │ ├── openai.adapter.ts
│ │ └── anthropic.adapter.ts
│ ├── calendar/
│ │ └── google-calendar.adapter.ts
│ ├── mail/
│ │ └── smtp.adapter.ts
│ ├── search/
│ │ └── searxng.adapter.ts
│ ├── storage/
│ │ └── postgresql.adapter.ts
│ ├── vector-store/
│ │ └── pgvector.adapter.ts
│ └── cache/
│ └── redis.adapter.ts
│
├── config/ # Configuration applicative
│ ├── database.config.ts
│ ├── redis.config.ts
│ ├── llm.config.ts
│ └── app.config.ts
│
└── shared/ # Utilitaires partagés
├── crypto.util.ts # Chiffrement AES-256
├── token-counter.util.ts # Comptage de tokens
└── logger.ts
Communication Robot ↔ Core
WebSocket (audio bidirectionnel)
Le robot maintient une connexion WebSocket permanente avec le core. Le protocole :
- Authentification — À la connexion, le robot envoie un
device_tokenJWT. Le core valide et associe la session au device + user. - Streaming audio entrant — Le robot envoie des chunks audio (PCM 16kHz 16bit mono) en continu pendant que l'utilisateur parle.
- Événements de contrôle — Wake word détecté, fin de parole (VAD), interruption utilisateur.
- Streaming audio sortant — Le core streame les chunks audio TTS au fur et à mesure de la génération.
- Notifications — Timer expiré, rappel de rendez-vous, alertes.
// Messages WebSocket (types)
type RobotMessage =
| { type: 'audio_chunk'; data: Buffer; sampleRate: number }
| { type: 'wake_word_detected' }
| { type: 'speech_end' } // VAD détecte fin de parole
| { type: 'user_interrupt' } // L'utilisateur parle pendant la réponse
type CoreMessage =
| { type: 'audio_chunk'; data: Buffer }
| { type: 'response_start' }
| { type: 'response_end' }
| { type: 'notification'; payload: NotificationPayload }
| { type: 'status'; state: 'listening' | 'thinking' | 'speaking' }
REST API (configuration)
Utilisée par l'app web/mobile pour toute la configuration :
- CRUD utilisateurs et devices
- Gestion des credentials OAuth (Google, Apple, etc.)
- Configuration du robot (wake word, volume, voix TTS, modèle LLM…)
- Consultation de l'historique et de la mémoire
- Gestion des timers et alarmes
Gestion des erreurs et résilience
- Circuit breaker sur les appels aux services externes (STT, TTS, LLM) — si un provider tombe, le système peut fallback sur un autre (ex: Deepgram → Whisper local).
- Retry avec backoff exponentiel sur les appels API.
- Queue de messages pour les function calls non critiques (ex: envoi d'email) — si ça échoue, on retry sans bloquer la conversation.
- Mode dégradé offline — le robot peut toujours gérer les timers, alarmes, et commandes basiques en local si le core est injoignable.