perf: réécriture nox-memory.js avec node fetch natif (sans Playwright)
This commit is contained in:
+28
-70
@@ -1,18 +1,17 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* nox-memory.js — Mémoire vectorielle de Nox via Qdrant
|
* nox-memory.js — Mémoire vectorielle de Nox via Qdrant
|
||||||
|
* Utilise node fetch (Node 22+) + OpenAI embeddings
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node nox-memory.js add "texte à mémoriser" --type fact --tags "ha,lumiere" --importance 3
|
* node nox-memory.js add "texte" --type fact --tags "ha,lumiere" --importance 3
|
||||||
* node nox-memory.js search "question en langage naturel" --limit 5
|
* node nox-memory.js search "question naturelle" [--limit 5] [--type fact]
|
||||||
* node nox-memory.js list [--type fact] [--limit 20]
|
* node nox-memory.js list [--type fact] [--limit 20]
|
||||||
* node nox-memory.js delete <id>
|
* node nox-memory.js delete <id>
|
||||||
* node nox-memory.js import-md <fichier.md> ← importe MEMORY.md
|
* node nox-memory.js import-md <fichier.md>
|
||||||
* node nox-memory.js stats
|
* 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 COLLECTION = 'nox-memory';
|
||||||
const OPENAI_URL = 'https://api.openai.com/v1/embeddings';
|
const OPENAI_URL = 'https://api.openai.com/v1/embeddings';
|
||||||
@@ -21,17 +20,18 @@ const EMBED_DIM = 1536;
|
|||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function getRequestContext() {
|
async function qdrant(method, path, body) {
|
||||||
const browser = await chromium.launch({
|
const opts = {
|
||||||
executablePath: '/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome',
|
method,
|
||||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
};
|
||||||
const context = await browser.newContext();
|
if (body) opts.body = JSON.stringify(body);
|
||||||
return { browser, req: context.request };
|
const r = await fetch(`${QDRANT_URL}${path}`, opts);
|
||||||
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function embed(text) {
|
async function embed(text) {
|
||||||
const res = await fetch(OPENAI_URL, {
|
const r = await fetch(OPENAI_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -39,13 +39,12 @@ async function embed(text) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ model: EMBED_MODEL, input: 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));
|
if (!json.data) throw new Error('Embedding failed: ' + JSON.stringify(json));
|
||||||
return json.data[0].embedding;
|
return json.data[0].embedding;
|
||||||
}
|
}
|
||||||
|
|
||||||
function genId() {
|
function genId() {
|
||||||
// UUID v4 simplifié
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||||
const r = Math.random() * 16 | 0;
|
const r = Math.random() * 16 | 0;
|
||||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||||
@@ -55,8 +54,6 @@ function genId() {
|
|||||||
// ─── Commandes ────────────────────────────────────────────────────────────────
|
// ─── Commandes ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function cmdAdd(text, opts) {
|
async function cmdAdd(text, opts) {
|
||||||
const { browser, req } = await getRequestContext();
|
|
||||||
try {
|
|
||||||
const vector = await embed(text);
|
const vector = await embed(text);
|
||||||
const id = genId();
|
const id = genId();
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -67,25 +64,19 @@ async function cmdAdd(text, opts) {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
date: new Date().toISOString()
|
date: new Date().toISOString()
|
||||||
};
|
};
|
||||||
const r = await req.put(`${QDRANT_URL}/collections/${COLLECTION}/points`, {
|
const res = await qdrant('PUT', `/collections/${COLLECTION}/points`, {
|
||||||
data: { points: [{ id, vector, payload }] }
|
points: [{ id, vector, payload }]
|
||||||
});
|
});
|
||||||
const body = await r.json();
|
if (res.status === 'ok') {
|
||||||
if (body.status === 'ok') {
|
|
||||||
console.log(`✅ Mémorisé [${id}]`);
|
console.log(`✅ Mémorisé [${id}]`);
|
||||||
console.log(` type: ${payload.type} | importance: ${payload.importance} | tags: ${payload.tags.join(', ') || 'aucun'}`);
|
console.log(` type: ${payload.type} | importance: ${payload.importance} | tags: ${payload.tags.join(', ') || 'aucun'}`);
|
||||||
console.log(` "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`);
|
console.log(` "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`);
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ Erreur:', JSON.stringify(body));
|
console.error('❌ Erreur:', JSON.stringify(res));
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await browser.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdSearch(query, opts) {
|
async function cmdSearch(query, opts) {
|
||||||
const { browser, req } = await getRequestContext();
|
|
||||||
try {
|
|
||||||
const vector = await embed(query);
|
const vector = await embed(query);
|
||||||
const limit = parseInt(opts.limit || '5');
|
const limit = parseInt(opts.limit || '5');
|
||||||
const body = {
|
const body = {
|
||||||
@@ -97,8 +88,7 @@ async function cmdSearch(query, opts) {
|
|||||||
if (opts.type) {
|
if (opts.type) {
|
||||||
body.filter = { must: [{ key: 'type', match: { value: 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 qdrant('POST', `/collections/${COLLECTION}/points/search`, body);
|
||||||
const res = await r.json();
|
|
||||||
const results = res.result || [];
|
const results = res.result || [];
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
console.log('🔍 Aucun résultat pertinent trouvé.');
|
console.log('🔍 Aucun résultat pertinent trouvé.');
|
||||||
@@ -113,21 +103,15 @@ async function cmdSearch(query, opts) {
|
|||||||
console.log(` ${pl.text}`);
|
console.log(` ${pl.text}`);
|
||||||
console.log(` ID: ${p.id}\n`);
|
console.log(` ID: ${p.id}\n`);
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdList(opts) {
|
async function cmdList(opts) {
|
||||||
const { browser, req } = await getRequestContext();
|
|
||||||
try {
|
|
||||||
const limit = parseInt(opts.limit || '20');
|
const limit = parseInt(opts.limit || '20');
|
||||||
const body = { limit, with_payload: true };
|
const body = { limit, with_payload: true };
|
||||||
if (opts.type) {
|
if (opts.type) {
|
||||||
body.filter = { must: [{ key: 'type', match: { value: 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 qdrant('POST', `/collections/${COLLECTION}/points/scroll`, body);
|
||||||
const res = await r.json();
|
|
||||||
const points = res.result?.points || [];
|
const points = res.result?.points || [];
|
||||||
if (points.length === 0) { console.log('📭 Collection vide.'); return; }
|
if (points.length === 0) { console.log('📭 Collection vide.'); return; }
|
||||||
console.log(`📋 ${points.length} souvenir(s) :\n`);
|
console.log(`📋 ${points.length} souvenir(s) :\n`);
|
||||||
@@ -135,45 +119,26 @@ async function cmdList(opts) {
|
|||||||
const pl = p.payload;
|
const pl = p.payload;
|
||||||
console.log(`• [${pl.type}|${pl.importance}] ${pl.date?.slice(0,10)} — ${pl.text?.slice(0,100)}${pl.text?.length>100?'...':''}`);
|
console.log(`• [${pl.type}|${pl.importance}] ${pl.date?.slice(0,10)} — ${pl.text?.slice(0,100)}${pl.text?.length>100?'...':''}`);
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdDelete(id) {
|
async function cmdDelete(id) {
|
||||||
const { browser, req } = await getRequestContext();
|
const res = await qdrant('POST', `/collections/${COLLECTION}/points/delete`, { points: [id] });
|
||||||
try {
|
console.log(res.status === 'ok' ? `🗑️ Point ${id} supprimé.` : `❌ Erreur: ${JSON.stringify(res)}`);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdStats() {
|
async function cmdStats() {
|
||||||
const { browser, req } = await getRequestContext();
|
const res = await qdrant('GET', `/collections/${COLLECTION}`);
|
||||||
try {
|
const info = res.result;
|
||||||
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('📊 Collection nox-memory :');
|
||||||
console.log(` Points : ${info.points_count}`);
|
console.log(` Points : ${info.points_count}`);
|
||||||
console.log(` Vecteurs : ${info.vectors_count}`);
|
|
||||||
console.log(` Segments : ${info.segments_count}`);
|
console.log(` Segments : ${info.segments_count}`);
|
||||||
console.log(` Statut : ${info.status}`);
|
console.log(` Statut : ${info.status}`);
|
||||||
console.log(` Dim. : ${EMBED_DIM} (${EMBED_MODEL})`);
|
console.log(` Modèle : ${EMBED_MODEL} (${EMBED_DIM} dims)`);
|
||||||
} finally {
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdImportMd(file) {
|
async function cmdImportMd(file) {
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const text = fs.readFileSync(file, 'utf8');
|
const text = fs.readFileSync(file, 'utf8');
|
||||||
// Découper en sections par ## ou ### ou lignes non vides significatives
|
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
let current = '';
|
let current = '';
|
||||||
for (const line of text.split('\n')) {
|
for (const line of text.split('\n')) {
|
||||||
@@ -185,19 +150,15 @@ async function cmdImportMd(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (current.trim()) chunks.push(current.trim());
|
if (current.trim()) chunks.push(current.trim());
|
||||||
|
|
||||||
const valid = chunks.filter(c => c.length > 20);
|
const valid = chunks.filter(c => c.length > 20);
|
||||||
console.log(`📥 Import de ${valid.length} chunks depuis ${file}...`);
|
console.log(`📥 Import de ${valid.length} chunks depuis ${file}...`);
|
||||||
|
|
||||||
for (let i = 0; i < valid.length; i++) {
|
for (let i = 0; i < valid.length; i++) {
|
||||||
const chunk = valid[i];
|
|
||||||
process.stdout.write(` [${i+1}/${valid.length}] `);
|
process.stdout.write(` [${i+1}/${valid.length}] `);
|
||||||
try {
|
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) {
|
} catch(e) {
|
||||||
console.error('Erreur:', e.message);
|
console.error('Erreur:', e.message);
|
||||||
}
|
}
|
||||||
// Pause pour éviter rate limit OpenAI
|
|
||||||
await new Promise(r => setTimeout(r, 300));
|
await new Promise(r => setTimeout(r, 300));
|
||||||
}
|
}
|
||||||
console.log('\n✅ Import terminé !');
|
console.log('\n✅ Import terminé !');
|
||||||
@@ -208,8 +169,6 @@ async function cmdImportMd(file) {
|
|||||||
async function main() {
|
async function main() {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const cmd = args[0];
|
const cmd = args[0];
|
||||||
|
|
||||||
// Parser les options --key value
|
|
||||||
const opts = {};
|
const opts = {};
|
||||||
const positional = [];
|
const positional = [];
|
||||||
for (let i = 1; i < args.length; i++) {
|
for (let i = 1; i < args.length; i++) {
|
||||||
@@ -248,21 +207,20 @@ async function main() {
|
|||||||
console.log(`
|
console.log(`
|
||||||
nox-memory — Mémoire vectorielle (Qdrant + OpenAI embeddings)
|
nox-memory — Mémoire vectorielle (Qdrant + OpenAI embeddings)
|
||||||
|
|
||||||
Commandes :
|
|
||||||
add "texte" Ajouter un souvenir
|
add "texte" Ajouter un souvenir
|
||||||
--type fact | semantic | preference | episodic (défaut: fact)
|
--type fact | semantic | preference | episodic (défaut: fact)
|
||||||
--tags "tag1,tag2"
|
--tags "tag1,tag2"
|
||||||
--importance 1-5 (défaut: 3)
|
--importance 1-5 (défaut: 3)
|
||||||
|
|
||||||
search "query" Recherche sémantique
|
search "query" Recherche sémantique
|
||||||
--limit Nombre de résultats (défaut: 5)
|
--limit Résultats (défaut: 5)
|
||||||
--type Filtrer par type
|
--type Filtrer par type
|
||||||
|
|
||||||
list Lister les souvenirs
|
list Lister les souvenirs
|
||||||
--limit Nombre (défaut: 20)
|
--limit Nombre (défaut: 20)
|
||||||
--type Filtrer par type
|
--type Filtrer par type
|
||||||
|
|
||||||
delete <id> Supprimer un point par ID
|
delete <id> Supprimer un point
|
||||||
stats Stats de la collection
|
stats Stats de la collection
|
||||||
import-md <file> Importer un fichier Markdown
|
import-md <file> Importer un fichier Markdown
|
||||||
`);
|
`);
|
||||||
|
|||||||
Reference in New Issue
Block a user