#!/usr/bin/env node /** * nox-memory.js — Mémoire vectorielle de Nox via Qdrant * Utilise node fetch (Node 22+) + OpenAI embeddings * * Usage: * node nox-memory.js add "texte" --type fact --tags "ha,lumiere" --importance 3 * node nox-memory.js search "question naturelle" [--limit 5] [--type fact] * node nox-memory.js list [--type fact] [--limit 20] * node nox-memory.js delete * node nox-memory.js import-md * node nox-memory.js stats */ const QDRANT_URL = 'http://192.168.1.150:6333'; const COLLECTION = 'nox-memory'; const OPENAI_URL = 'https://api.openai.com/v1/embeddings'; const EMBED_MODEL = 'text-embedding-3-small'; const EMBED_DIM = 1536; // ─── Helpers ────────────────────────────────────────────────────────────────── async function qdrant(method, path, body) { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const r = await fetch(`${QDRANT_URL}${path}`, opts); return r.json(); } async function embed(text) { const r = await fetch(OPENAI_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }, body: JSON.stringify({ model: EMBED_MODEL, input: text }) }); const json = await r.json(); if (!json.data) throw new Error('Embedding failed: ' + JSON.stringify(json)); return json.data[0].embedding; } function genId() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } // ─── Commandes ──────────────────────────────────────────────────────────────── async function cmdAdd(text, opts) { const vector = await embed(text); const id = genId(); const payload = { text, type: opts.type || 'fact', tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : [], importance: parseInt(opts.importance || '3'), timestamp: Date.now(), date: new Date().toISOString() }; const res = await qdrant('PUT', `/collections/${COLLECTION}/points`, { points: [{ id, vector, payload }] }); if (res.status === 'ok') { console.log(`✅ Mémorisé [${id}]`); console.log(` type: ${payload.type} | importance: ${payload.importance} | tags: ${payload.tags.join(', ') || 'aucun'}`); console.log(` "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`); } else { console.error('❌ Erreur:', JSON.stringify(res)); } } async function cmdSearch(query, opts) { const vector = await embed(query); const limit = parseInt(opts.limit || '5'); const body = { vector, limit, with_payload: true, score_threshold: 0.3 }; if (opts.type) { body.filter = { must: [{ key: 'type', match: { value: opts.type } }] }; } const res = await qdrant('POST', `/collections/${COLLECTION}/points/search`, body); const results = res.result || []; if (results.length === 0) { console.log('🔍 Aucun résultat pertinent trouvé.'); return; } console.log(`🔍 ${results.length} résultat(s) pour : "${query}"\n`); results.forEach((p, i) => { const pl = p.payload; const score = (p.score * 100).toFixed(1); console.log(`[${i+1}] ${score}% — ${pl.type} | imp:${pl.importance} | ${pl.date?.slice(0,10) || '?'}`); if (pl.tags?.length) console.log(` Tags: ${pl.tags.join(', ')}`); console.log(` ${pl.text}`); console.log(` ID: ${p.id}\n`); }); } async function cmdList(opts) { const limit = parseInt(opts.limit || '20'); const body = { limit, with_payload: true }; if (opts.type) { body.filter = { must: [{ key: 'type', match: { value: opts.type } }] }; } const res = await qdrant('POST', `/collections/${COLLECTION}/points/scroll`, body); const points = res.result?.points || []; if (points.length === 0) { console.log('📭 Collection vide.'); return; } console.log(`📋 ${points.length} souvenir(s) :\n`); points.forEach(p => { const pl = p.payload; console.log(`• [${pl.type}|${pl.importance}] ${pl.date?.slice(0,10)} — ${pl.text?.slice(0,100)}${pl.text?.length>100?'...':''}`); }); } async function cmdDelete(id) { const res = await qdrant('POST', `/collections/${COLLECTION}/points/delete`, { points: [id] }); console.log(res.status === 'ok' ? `🗑️ Point ${id} supprimé.` : `❌ Erreur: ${JSON.stringify(res)}`); } async function cmdStats() { const res = await qdrant('GET', `/collections/${COLLECTION}`); const info = res.result; console.log('📊 Collection nox-memory :'); console.log(` Points : ${info.points_count}`); console.log(` Segments : ${info.segments_count}`); console.log(` Statut : ${info.status}`); console.log(` Modèle : ${EMBED_MODEL} (${EMBED_DIM} dims)`); } async function cmdImportMd(file) { const fs = require('fs'); const text = fs.readFileSync(file, 'utf8'); const chunks = []; let current = ''; for (const line of text.split('\n')) { if ((line.startsWith('## ') || line.startsWith('### ')) && current.trim()) { chunks.push(current.trim()); current = line; } else { current += '\n' + line; } } if (current.trim()) chunks.push(current.trim()); const valid = chunks.filter(c => c.length > 20); console.log(`📥 Import de ${valid.length} chunks depuis ${file}...`); for (let i = 0; i < valid.length; i++) { process.stdout.write(` [${i+1}/${valid.length}] `); try { await cmdAdd(valid[i], { type: 'semantic', importance: '4', tags: 'memory.md,import' }); } catch(e) { console.error('Erreur:', e.message); } await new Promise(r => setTimeout(r, 300)); } console.log('\n✅ Import terminé !'); } // ─── Main ────────────────────────────────────────────────────────────────────── async function main() { const args = process.argv.slice(2); const cmd = args[0]; const opts = {}; const positional = []; for (let i = 1; i < args.length; i++) { if (args[i].startsWith('--')) { opts[args[i].slice(2)] = args[i+1]; i++; } else { positional.push(args[i]); } } switch (cmd) { case 'add': if (!positional[0]) { console.error('Usage: add "texte"'); process.exit(1); } await cmdAdd(positional[0], opts); break; case 'search': if (!positional[0]) { console.error('Usage: search "query"'); process.exit(1); } await cmdSearch(positional[0], opts); break; case 'list': await cmdList(opts); break; case 'delete': if (!positional[0]) { console.error('Usage: delete '); process.exit(1); } await cmdDelete(positional[0]); break; case 'stats': await cmdStats(); break; case 'import-md': if (!positional[0]) { console.error('Usage: import-md '); process.exit(1); } await cmdImportMd(positional[0]); break; default: console.log(` nox-memory — Mémoire vectorielle (Qdrant + OpenAI embeddings) add "texte" Ajouter un souvenir --type fact | semantic | preference | episodic (défaut: fact) --tags "tag1,tag2" --importance 1-5 (défaut: 3) search "query" Recherche sémantique --limit Résultats (défaut: 5) --type Filtrer par type list Lister les souvenirs --limit Nombre (défaut: 20) --type Filtrer par type delete Supprimer un point stats Stats de la collection import-md Importer un fichier Markdown `); } } main().catch(e => { console.error('Fatal:', e.message); process.exit(1); });