Files
kxkm_clown/scripts/bench-embeddings.js
L'électron rare d2fed6087a feat: Add MCP server and smoke test scripts
- Introduced `mcp-server.js` to expose KXKM personas as MCP tools, supporting chat, persona listing, web search, and status checks.
- Implemented `mcp-server-smoke.js` for testing the MCP server functionality, ensuring compatibility with both new and legacy message formats.
- Created `setup-voice-clone.sh` for managing voice cloning environment setup, including bootstrapping, sample generation, and smoke testing.
- Added `state.json` to track project status and task outputs for various batches.
- Generated summary files for deep cycle and overall project status, capturing performance and security findings.
2026-03-19 16:18:44 +01:00

308 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Benchmark local embeddings models via Ollama.
*
* Usage:
* node scripts/bench-embeddings.js
* node scripts/bench-embeddings.js --models nomic-embed-text,bge-m3 --json-only
* node scripts/bench-embeddings.js --list-models
*/
const DEFAULT_MODELS = ["nomic-embed-text", "bge-m3"];
const DEFAULT_OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
const TEST_DOCS = [
"Pierre Schaeffer invente la musique concrete en 1948 avec les Etudes de bruits.",
"Le cyberfeminisme radical est un acte de sabotage du big daddy mainframe.",
"Un LLM local qui refuse le cloud centralise est un acte politique.",
"Le Node Engine orchestre les workflows de training via graphes DAG.",
"Eliane Radigue compose des drones minimalistes avec le synthetiseur ARP 2500.",
];
const TEST_QUERIES = [
"musique concrete Schaeffer",
"cyberfeminisme VNS Matrix",
"infrastructure locale auto-hebergee",
"pipeline training DAG",
"drone minimaliste synthetiseur",
];
function parseArgs(argv) {
const options = {
ollamaUrl: DEFAULT_OLLAMA_URL,
models: [...DEFAULT_MODELS],
json: false,
jsonOnly: false,
listModels: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case "--ollama-url":
index += 1;
if (!argv[index]) throw new Error("--ollama-url requires a value");
options.ollamaUrl = argv[index];
break;
case "--models":
index += 1;
if (!argv[index]) throw new Error("--models requires a value");
options.models = argv[index]
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
break;
case "--json":
options.json = true;
break;
case "--json-only":
options.json = true;
options.jsonOnly = true;
break;
case "--list-models":
options.listModels = true;
break;
case "--help":
case "-h":
process.stdout.write(
[
"Usage: node scripts/bench-embeddings.js [options]",
"",
"Options:",
" --ollama-url URL Ollama base URL (default: env OLLAMA_URL or http://localhost:11434)",
" --models A,B Comma-separated models to benchmark",
" --list-models Print models currently available in Ollama and exit",
" --json Emit JSON after the human summary",
" --json-only Emit JSON only",
" --help Show this help",
].join("\n") + "\n",
);
process.exit(0);
break;
default:
throw new Error(`unknown option: ${arg}`);
}
}
if (options.models.length === 0) {
throw new Error("at least one model is required");
}
return options;
}
function log(enabled, message) {
if (enabled) {
process.stdout.write(`${message}\n`);
}
}
function stripTag(name) {
return name.split(":")[0];
}
function resolveModelName(requestedModel, availableModels) {
if (availableModels.includes(requestedModel)) {
return requestedModel;
}
const requestedBase = stripTag(requestedModel);
const matching = availableModels.filter((model) => stripTag(model) === requestedBase);
if (matching.length === 0) {
return null;
}
return matching.find((model) => model.endsWith(":latest")) || matching[0];
}
function cosineSimilarity(a, b) {
let dot = 0;
let normA = 0;
let normB = 0;
for (let index = 0; index < a.length; index += 1) {
dot += a[index] * b[index];
normA += a[index] * a[index];
normB += b[index] * b[index];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
async function fetchAvailableModels(ollamaUrl) {
const response = await fetch(`${ollamaUrl}/api/tags`, {
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) {
throw new Error(`Ollama tags failed: ${response.status} ${response.statusText}`);
}
const body = await response.json();
return (body.models || []).map((model) => model.name).filter(Boolean);
}
async function embed(ollamaUrl, model, text) {
const response = await fetch(`${ollamaUrl}/api/embed`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, input: text }),
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
const details = await response.text();
throw new Error(`${model}: ${response.status} ${response.statusText}${details ? ` — ${details.slice(0, 200)}` : ""}`);
}
const body = await response.json();
return body.embeddings?.[0] || body.embedding;
}
async function benchModel(ollamaUrl, model, humanOutput) {
log(humanOutput, `\n Testing ${model}...`);
const docStart = performance.now();
const docEmbeddings = [];
for (const doc of TEST_DOCS) {
docEmbeddings.push(await embed(ollamaUrl, model, doc));
}
const docLatency = performance.now() - docStart;
const queryStart = performance.now();
const similarities = [];
for (let index = 0; index < TEST_QUERIES.length; index += 1) {
const queryEmbedding = await embed(ollamaUrl, model, TEST_QUERIES[index]);
similarities.push(cosineSimilarity(queryEmbedding, docEmbeddings[index]));
}
const queryLatency = performance.now() - queryStart;
const avgSimilarity = similarities.reduce((sum, item) => sum + item, 0) / similarities.length;
return {
model,
dims: docEmbeddings[0]?.length || 0,
doc_embed_ms: Math.round(docLatency),
query_embed_ms: Math.round(queryLatency),
avg_latency_per_doc_ms: Math.round(docLatency / TEST_DOCS.length),
avg_latency_per_query_ms: Math.round(queryLatency / TEST_QUERIES.length),
avg_similarity: Math.round(avgSimilarity * 1000) / 1000,
similarities: similarities.map((item) => Math.round(item * 1000) / 1000),
};
}
function printTable(results) {
process.stdout.write("\n ┌─────────────────┬──────┬──────────┬──────────┬───────────┐\n");
process.stdout.write(" │ Model │ Dims │ Doc ms │ Query ms │ Avg Sim │\n");
process.stdout.write(" ├─────────────────┼──────┼──────────┼──────────┼───────────┤\n");
for (const result of results) {
const name = result.model.padEnd(15);
const dims = String(result.dims).padStart(4);
const doc = `${String(result.avg_latency_per_doc_ms).padStart(6)}ms`;
const query = `${String(result.avg_latency_per_query_ms).padStart(6)}ms`;
const similarity = String(result.avg_similarity).padStart(7);
process.stdout.write(` │ ${name}${dims}${doc}${query}${similarity} │\n`);
}
process.stdout.write(" └─────────────────┴──────┴──────────┴──────────┴───────────┘\n");
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const humanOutput = !options.jsonOnly;
const availableModels = await fetchAvailableModels(options.ollamaUrl);
if (options.listModels) {
const payload = { ollamaUrl: options.ollamaUrl, availableModels };
if (options.jsonOnly || options.json) {
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
} else {
log(true, availableModels.length > 0
? availableModels.join("\n")
: "(no Ollama models available)");
}
return;
}
const requestedModels = options.models;
const resolvedModels = requestedModels.map((model) => ({
requested: model,
resolved: resolveModelName(model, availableModels),
}));
const benchmarkableModels = [...new Set(resolvedModels.map((entry) => entry.resolved).filter(Boolean))];
const missingModels = resolvedModels.filter((entry) => !entry.resolved).map((entry) => entry.requested);
if (humanOutput) {
log(true, "╔══════════════════════════════════════╗");
log(true, "║ KXKM Embeddings Benchmark ║");
log(true, "╚══════════════════════════════════════╝");
log(true, ` Ollama: ${options.ollamaUrl}`);
log(true, ` Requested: ${requestedModels.join(", ")}`);
log(true, ` Available: ${availableModels.length > 0 ? availableModels.join(", ") : "(none)"}`);
if (benchmarkableModels.length > 0) {
log(true, ` Resolved: ${benchmarkableModels.join(", ")}`);
}
log(true, ` Docs: ${TEST_DOCS.length}, Queries: ${TEST_QUERIES.length}`);
}
const results = [];
const errors = [];
for (const model of benchmarkableModels) {
try {
results.push(await benchModel(options.ollamaUrl, model, humanOutput));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errors.push({ model, error: message });
if (humanOutput) {
log(true, ` ${model} failed: ${message}`);
}
}
}
const payload = {
ollamaUrl: options.ollamaUrl,
requestedModels,
resolvedModels,
availableModels,
missingModels,
benchmarkedModels: results.map((result) => result.model),
errors,
results,
};
if (results.length === 0) {
if (humanOutput) {
log(true, "\n No requested benchmark models are installed in Ollama.");
if (missingModels.length > 0) {
log(true, ` Install with: ${missingModels.map((model) => `ollama pull ${model}`).join(" && ")}`);
}
}
if (options.json) {
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
}
process.exit(1);
}
if (humanOutput) {
printTable(results);
if (results.length >= 2) {
const best = results.reduce((left, right) => (left.avg_similarity > right.avg_similarity ? left : right));
const fastest = results.reduce((left, right) => (left.avg_latency_per_query_ms < right.avg_latency_per_query_ms ? left : right));
log(true, `\n Best similarity: ${best.model} (${best.avg_similarity})`);
log(true, ` Fastest: ${fastest.model} (${fastest.avg_latency_per_query_ms}ms/query)`);
}
if (errors.length > 0) {
log(true, "\n Model errors:");
for (const item of errors) {
log(true, ` - ${item.model}: ${item.error}`);
}
}
}
if (options.json) {
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
}
}
main().catch((error) => {
process.stderr.write(`Fatal: ${error.message}\n`);
process.exit(1);
});