diff --git a/nox-memory.js b/nox-memory.js index 504b349..8a4345f 100644 --- a/nox-memory.js +++ b/nox-memory.js @@ -1,19 +1,18 @@ #!/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 à mémoriser" --type fact --tags "ha,lumiere" --importance 3 - * node nox-memory.js search "question en langage naturel" --limit 5 + * 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 ← importe MEMORY.md + * node nox-memory.js import-md * node nox-memory.js stats */ -const { chromium } = require('./node_modules/playwright'); - -const QDRANT_URL = 'http://192.168.1.150:6333'; +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'; @@ -21,17 +20,18 @@ const EMBED_DIM = 1536; // ─── Helpers ────────────────────────────────────────────────────────────────── -async function getRequestContext() { - const browser = await chromium.launch({ - executablePath: '/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome', - args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] - }); - const context = await browser.newContext(); - return { browser, req: context.request }; +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 res = await fetch(OPENAI_URL, { + const r = await fetch(OPENAI_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -39,13 +39,12 @@ async function embed(text) { }, body: JSON.stringify({ model: EMBED_MODEL, input: text }) }); - const json = await res.json(); + const json = await r.json(); if (!json.data) throw new Error('Embedding failed: ' + JSON.stringify(json)); return json.data[0].embedding; } function genId() { - // UUID v4 simplifié 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); @@ -55,125 +54,91 @@ function genId() { // ─── Commandes ──────────────────────────────────────────────────────────────── async function cmdAdd(text, opts) { - const { browser, req } = await getRequestContext(); - try { - 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 r = await req.put(`${QDRANT_URL}/collections/${COLLECTION}/points`, { - data: { points: [{ id, vector, payload }] } - }); - const body = await r.json(); - if (body.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(body)); - } - } finally { - await browser.close(); + 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 { browser, req } = await getRequestContext(); - try { - 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 r = await req.post(`${QDRANT_URL}/collections/${COLLECTION}/points/search`, { data: body }); - const res = await r.json(); - 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`); - }); - } finally { - await browser.close(); + 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 { browser, req } = await getRequestContext(); - try { - 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 r = await req.post(`${QDRANT_URL}/collections/${COLLECTION}/points/scroll`, { data: body }); - const res = await r.json(); - 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?'...':''}`); - }); - } finally { - await browser.close(); + 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 { browser, req } = await getRequestContext(); - try { - const r = await req.post(`${QDRANT_URL}/collections/${COLLECTION}/points/delete`, { - data: { points: [id] } - }); - const body = await r.json(); - console.log(body.status === 'ok' ? `🗑️ Point ${id} supprimé.` : `❌ Erreur: ${JSON.stringify(body)}`); - } finally { - await browser.close(); - } + 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 { browser, req } = await getRequestContext(); - try { - const r = await req.get(`${QDRANT_URL}/collections/${COLLECTION}`); - const body = await r.json(); - const info = body.result; - console.log('📊 Collection nox-memory :'); - console.log(` Points : ${info.points_count}`); - console.log(` Vecteurs : ${info.vectors_count}`); - console.log(` Segments : ${info.segments_count}`); - console.log(` Statut : ${info.status}`); - console.log(` Dim. : ${EMBED_DIM} (${EMBED_MODEL})`); - } finally { - await browser.close(); - } + 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'); - // Découper en sections par ## ou ### ou lignes non vides significatives const chunks = []; let current = ''; for (const line of text.split('\n')) { @@ -185,19 +150,15 @@ async function cmdImportMd(file) { } } 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++) { - const chunk = valid[i]; process.stdout.write(` [${i+1}/${valid.length}] `); try { - await cmdAdd(chunk, { type: 'semantic', importance: '4', tags: 'memory.md,import' }); + await cmdAdd(valid[i], { type: 'semantic', importance: '4', tags: 'memory.md,import' }); } catch(e) { console.error('Erreur:', e.message); } - // Pause pour éviter rate limit OpenAI await new Promise(r => setTimeout(r, 300)); } console.log('\n✅ Import terminé !'); @@ -208,8 +169,6 @@ async function cmdImportMd(file) { async function main() { const args = process.argv.slice(2); const cmd = args[0]; - - // Parser les options --key value const opts = {}; const positional = []; for (let i = 1; i < args.length; i++) { @@ -248,21 +207,20 @@ async function main() { console.log(` nox-memory — Mémoire vectorielle (Qdrant + OpenAI embeddings) -Commandes : add "texte" Ajouter un souvenir - --type fact | semantic | preference | episodic (défaut: fact) + --type fact | semantic | preference | episodic (défaut: fact) --tags "tag1,tag2" - --importance 1-5 (défaut: 3) - + --importance 1-5 (défaut: 3) + search "query" Recherche sémantique - --limit Nombre de résultats (défaut: 5) + --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 par ID + + delete Supprimer un point stats Stats de la collection import-md Importer un fichier Markdown `);