CS.08, CASE STUDY · CÔTÉ SYSTÈME
Lyra, IA persistante
Projet personnel de recherche
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.
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();
}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.
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
];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.
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);
}
}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