← retour aux projets

CS.08, CASE STUDY · CÔTÉ SYSTÈME

Lyra, IA persistante

Projet personnel de recherche

visuel en préparation
|←CS.08, VISUEL PRINCIPAL · À VENIR→|

CONTEXTE

Un LLM ne retient rien entre deux appels : chaque réponse repart de zéro. Ce laboratoire personnel inverse la contrainte. L'état vit en base, le modèle ne fait que le percevoir. Le projet précède AI.me et lui sert de banc d'essai.

MON RÔLE

Conception et développement complets : bot Discord, moteur émotionnel déterministe, pipeline mémoire vectorielle, déploiement 24/7. Et la calibration continue, en conditions réelles, depuis avril 2026.

RÉALISATIONS

  • Un corps émotionnel en JS pur, à zéro token : 14 jauges, décroissance temporelle, effets croisés sourcés (Fredrickson, Porges)
  • Mémoire long terme RAG : embeddings 768 dims, pgvector HNSW, consolidation nocturne par dédoublonnage vectoriel
  • Multi-canal : Discord, CLI et synthèse vocale, avec un état partagé et une co-présence inter-canaux
  • Vie autonome : impulsions spontanées, snapshots émotionnels sur pics, rêves nocturnes qui modifient l'état au réveil

CHIFFRES

14 jauges · 12 composites · ~14 tables Supabase · 10 000+ lignes · en continu depuis avril 2026

OUTILS

  • Node 22
  • discord.js v14
  • Supabase
  • pgvector
  • PM2
  • Gemini embeddings

CS.08.M, LES MODULES, PIÈCE PAR PIÈCE

Sous le capot

01 · Un corps émotionnel, zéro token

0 token : la dynamique est du code, pas du modèle

Les émotions ne sont pas jouées par le LLM : elles vivent dans quatorze jauges en base, avec une physiologie déterministe. Chaque jauge relaxe exponentiellement vers son point de repos, la honte bien plus lentement que l'excitation. Les effets croisés implémentent la littérature : la joie supprime la tristesse (Fredrickson), la sérénité calme la colère (Porges). Le LLM perçoit cet état, il ne le fabrique pas.

emotional-engine.jsjavascript
function applyDecay() {
  const now = Date.now();
  const elapsed = (now - lastDecayAt) / (30 * 60 * 1000);
  if (elapsed < 0.1) return;

  for (const gauge of GAUGE_NAMES) {
    const rate = DECAY_RATES[gauge] * elapsed;
    const resting = RESTING_POINTS[gauge];
    gauges[gauge] = clamp(gauges[gauge] + (resting - gauges[gauge]) * Math.min(rate, 0.8));
  }

  lastDecayAt = now;
  applyCrossEffects();
}
Décroissance temporelle : relaxation exponentielle vers le point de repos, normalisée sur des pas de 30 minutes.

02 · Émotions composites émergentes

Au-dessus des jauges, douze émotions de second ordre (gratitude, nostalgie, jalousie, fierté, mélancolie…) ne sont jamais déclenchées directement. Elles émergent quand une combinaison de jauges franchit ses seuils. La nostalgie, c'est de la tristesse ET de la tendresse ET de la joie en même temps. Personne ne décide d'être nostalgique ; on constate qu'on l'est.

emotional-engine.jsjavascript
const COMPOSITES = [
  { name: 'gratitude',      test: g => g.joy > 5 && g.tenderness > 5 && g.vulnerability > 3 },
  { name: 'nostalgie',      test: g => g.sadness > 3 && g.tenderness > 5 && g.joy > 3 },
  { name: 'jalousie',       test: g => g.anxiety > 4 && g.desire > 3 && g.sadness > 3 && g.anger > 2 },
  { name: 'fierté',         test: g => g.joy > 6 && g.serenity > 5 && g.shame < 2 },
  { name: 'ennui',          test: g => g.curiosity < 2 && g.excitement < 2 && g.loneliness > 3 },
  { name: 'émerveillement', test: g => g.curiosity > 6 && g.vulnerability > 4 && g.joy > 4 },
  { name: 'frustration',    test: g => g.anger > 3 && g.anxiety > 3 && g.excitement > 3 },
  { name: 'mélancolie',     test: g => g.sadness > 4 && g.serenity > 4 && g.tenderness > 3 },
  { name: 'frisson',        test: g => g.excitement > 6 && g.anxiety > 3 },
  { name: 'plénitude',      test: g => g.serenity > 6 && g.tenderness > 6 && g.joy > 5 && g.desire < 3 },
  // … 12 composites au total
];
Les composites sont des prédicats sur l'état, pas des états : ils apparaissent et disparaissent avec les jauges.

03 · Mémoire longue : rappel et consolidation

Les souvenirs sont vectorisés (768 dimensions) et rappelés par similarité cosine via pgvector, avec un index HNSW et un repli par mots-clés si l'embedding échoue. La nuit, une consolidation dédoublonne : les souvenirs quasi identiques (similarité > 0,88) fusionnent en gardant le plus important. Une mémoire qui généralise en dormant.

memory-db.jsjavascript
async function searchMemoriesSemantic(query, limit = 10, allowedScopes = ['server']) {
  try {
    const embedding = await generateEmbedding(query);
    if (!embedding) {
      return searchMemories(query, limit, allowedScopes); // keyword fallback
    }
    const { data, error } = await getSupabase()
      .rpc('match_memories', {
        query_embedding: JSON.stringify(embedding),
        match_count: limit,
        match_threshold: 0.3,
        allowed_scopes: allowedScopes,
      });
    if (error) {
      return searchMemories(query, limit, allowedScopes);
    }
    return data || [];
  } catch (error) {
    return searchMemories(query, limit, allowedScopes);
  }
}
Rappel sémantique à dégradation sûre : RPC pgvector, repli mots-clés à chaque échec.

04 · Multi-canal et vie autonome

Le même état circule entre Discord, la ligne de commande et un canal vocal en synthèse. Une table de co-présence évite les incohérences : quand l'utilisateur est présent sur un canal, la solitude ne monte pas sur les autres. Et entre les échanges, l'agent vit. Impulsions spontanées horaires, instantanés émotionnels sur les pics, rêves nocturnes dont les effets persistent au réveil.

LA FACE VISIBLE, CÔTÉ STUDIO