voyageai-cli 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
@@ -90,6 +90,28 @@ function createPlaygroundServer() {
90
90
  return;
91
91
  }
92
92
 
93
+ // API: Concepts (from vai explain)
94
+ if (req.method === 'GET' && req.url === '/api/concepts') {
95
+ const { concepts } = require('../lib/explanations');
96
+ // Strip picocolors ANSI from content for web display
97
+ // eslint-disable-next-line no-control-regex
98
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
99
+ const stripped = {};
100
+ for (const [key, concept] of Object.entries(concepts)) {
101
+ stripped[key] = {
102
+ title: concept.title,
103
+ summary: concept.summary,
104
+ content: (typeof concept.content === 'string' ? concept.content : concept.content).replace(ANSI_RE, ''),
105
+ links: concept.links || [],
106
+ tryIt: concept.tryIt || [],
107
+ keyPoints: concept.keyPoints || [],
108
+ };
109
+ }
110
+ res.writeHead(200, { 'Content-Type': 'application/json' });
111
+ res.end(JSON.stringify({ concepts: stripped }));
112
+ return;
113
+ }
114
+
93
115
  // API: Config
94
116
  if (req.method === 'GET' && req.url === '/api/config') {
95
117
  const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
@@ -1003,6 +1003,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
1003
1003
 
1004
1004
  <!-- ========== EXPLORE TAB ========== -->
1005
1005
  <div class="tab-panel" id="tab-explore">
1006
+ <div style="margin-bottom:16px;">
1007
+ <input type="text" id="exploreSearch" placeholder="🔍 Search concepts..." oninput="filterExplore()" style="max-width:400px;">
1008
+ </div>
1006
1009
  <div class="explore-grid" id="exploreGrid"></div>
1007
1010
  </div>
1008
1011
 
@@ -1022,7 +1025,7 @@ let lastEmbedding = null;
1022
1025
  async function init() {
1023
1026
  setupTabs();
1024
1027
  await loadConfig();
1025
- await loadModels();
1028
+ await Promise.all([loadModels(), loadConcepts()]);
1026
1029
  populateModelSelects();
1027
1030
  buildExploreCards();
1028
1031
  }
@@ -1393,103 +1396,102 @@ function createResultItem(rank, result, maxScore, movement) {
1393
1396
  }
1394
1397
 
1395
1398
  // ── Explore ──
1396
- const exploreTopics = [
1397
- {
1398
- key: 'embeddings', icon: '🧮', title: 'Embeddings',
1399
- summary: 'Numerical representations that capture meaning',
1400
- content: 'Vector embeddings are arrays of floating-point numbers (typically 256–2048 dimensions) that capture the semantic meaning of text. When you embed text, a neural network reads the input and produces a fixed-size vector. Texts with similar meanings end up close together in this high-dimensional space, even if they share no words.\n\nHigher dimensions capture more nuance but cost more to store and search. Voyage 4 models default to 1024 dimensions but support 256–2048 via Matryoshka representation learning — you can truncate embeddings without retraining.',
1401
- tab: 'embed', prefill: () => { document.getElementById('embedInput').value = 'Artificial intelligence is transforming how we build software applications.'; }
1402
- },
1403
- {
1404
- key: 'reranking', icon: '🏆', title: 'Reranking',
1405
- summary: 'Second-stage precision with cross-attention',
1406
- content: 'Reranking re-scores candidate documents against a query using cross-attention — it reads the query and each document together, producing much more accurate relevance scores than embedding similarity alone.\n\nThe two-stage pattern: embedding search retrieves a broad set (high recall), then the reranker re-orders them (high precision). This adds ~50-200ms but dramatically improves result quality.',
1407
- tab: 'search', prefill: () => {
1408
- document.getElementById('searchQuery').value = 'How do I implement semantic search?';
1409
- document.getElementById('searchDocs').value = 'MongoDB Atlas provides vector search capabilities\nThe recipe calls for two cups of flour\nSemantic search uses embeddings to find meaning\nVector databases store high-dimensional data\nThe weather forecast predicts rain tomorrow';
1410
- }
1411
- },
1412
- {
1413
- key: 'vector-search', icon: '🔎', title: 'Vector Search',
1414
- summary: 'Finding documents by meaning, not keywords',
1415
- content: 'Vector search finds documents whose embeddings are closest to a query embedding. Instead of matching keywords, it matches meaning. MongoDB Atlas Vector Search uses $vectorSearch with HNSW (Hierarchical Navigable Small World) graph indexes for fast approximate nearest neighbor search.\n\nSimilarity functions: cosine (direction, ignoring magnitude — best default), dotProduct (magnitude-sensitive), euclidean (straight-line distance).',
1416
- tab: 'search', prefill: () => {}
1417
- },
1418
- {
1419
- key: 'rag', icon: '🤖', title: 'RAG',
1420
- summary: 'Retrieval-Augmented Generation',
1421
- content: 'RAG combines retrieval with LLM generation: instead of relying on the LLM\'s training data alone, you retrieve relevant context from your own data and include it in the prompt.\n\nThe pattern: 1) Embed your corpus and store vectors, 2) Embed the user\'s question and run vector search, 3) Pass retrieved documents + question to an LLM. Adding reranking between steps 2 and 3 dramatically improves answer quality.',
1422
- tab: 'search', prefill: () => {}
1423
- },
1424
- {
1425
- key: 'cosine', icon: '📐', title: 'Cosine Similarity',
1426
- summary: 'Measuring the angle between vectors',
1427
- content: 'Cosine similarity measures the angle between two vectors, ignoring magnitude. Vectors pointing the same direction score 1, perpendicular score 0, opposite score -1.\n\nFor text embeddings (which are typically normalized), cosine similarity and dot product give identical rankings. Cosine is preferred because it\'s intuitive: it measures how similar the direction (meaning) is, regardless of scale.',
1428
- tab: 'compare', prefill: () => {
1429
- document.getElementById('compareA').value = 'The database stores information efficiently';
1430
- document.getElementById('compareB').value = 'Data is saved in an optimized storage system';
1431
- }
1432
- },
1433
- {
1434
- key: 'two-stage', icon: '🎯', title: 'Two-Stage Retrieval',
1435
- summary: 'Embed → Search → Rerank for best results',
1436
- content: 'Two-stage retrieval combines a fast first stage (embedding search for recall) with a precise second stage (reranking for precision).\n\nStage 1: Embed query, run ANN search, retrieve top-100 candidates (fast, milliseconds). Stage 2: Feed query + candidates to a reranker with cross-attention, return top-5-10 (precise, ~100ms extra). This gives you both speed and accuracy.',
1437
- tab: 'search', prefill: () => {}
1438
- },
1439
- {
1440
- key: 'input-types', icon: '🏷️', title: 'Input Types',
1441
- summary: 'Query vs document — why it matters',
1442
- content: 'The input_type parameter tells the model whether text is a search query or a document being indexed. The model internally prepends different prompt prefixes for each, optimizing embeddings for asymmetric retrieval.\n\nAlways use input_type="query" for search queries and input_type="document" for corpus text. Omitting this parameter degrades retrieval accuracy.',
1443
- tab: 'embed', prefill: () => {
1444
- document.getElementById('embedInput').value = 'What is vector search and how does it work?';
1445
- document.getElementById('embedInputType').value = 'query';
1446
- }
1447
- },
1448
- {
1449
- key: 'models', icon: '🧠', title: 'Models',
1450
- summary: 'Choosing the right model for your task',
1451
- content: 'Voyage 4 Series: voyage-4-large (best quality, $0.12/1M tokens), voyage-4 (balanced, $0.06), voyage-4-lite (budget, $0.02). All share the same embedding space — you can mix models.\n\nDomain-specific: voyage-code-3 (code), voyage-finance-2 (financial), voyage-law-2 (legal). Rerankers: rerank-2.5 (best quality), rerank-2.5-lite (faster). Start with voyage-4 for general use.',
1452
- tab: 'embed', prefill: () => {}
1453
- },
1454
- ];
1399
+ // ── Explore: icons and tab mappings per concept ──
1400
+ const CONCEPT_META = {
1401
+ embeddings: { icon: '🧮', tab: 'embed' },
1402
+ reranking: { icon: '🏆', tab: 'search' },
1403
+ 'vector-search': { icon: '🔎', tab: 'search' },
1404
+ rag: { icon: '🤖', tab: 'search' },
1405
+ 'cosine-similarity': { icon: '📐', tab: 'compare' },
1406
+ 'two-stage-retrieval': { icon: '🎯', tab: 'search' },
1407
+ 'input-type': { icon: '🏷️', tab: 'embed' },
1408
+ models: { icon: '🧠', tab: 'embed' },
1409
+ 'api-keys': { icon: '🔑', tab: 'embed' },
1410
+ 'api-access': { icon: '🌐', tab: 'embed' },
1411
+ 'batch-processing': { icon: '📦', tab: 'embed' },
1412
+ benchmarking: { icon: '', tab: 'benchmark' },
1413
+ };
1414
+
1415
+ let exploreConcepts = {};
1416
+
1417
+ async function loadConcepts() {
1418
+ try {
1419
+ const res = await fetch('/api/concepts');
1420
+ const data = await res.json();
1421
+ exploreConcepts = data.concepts || {};
1422
+ } catch {
1423
+ console.error('Failed to load concepts');
1424
+ }
1425
+ }
1426
+
1427
+ function escapeHtml(str) {
1428
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1429
+ }
1455
1430
 
1456
1431
  function buildExploreCards() {
1457
1432
  const grid = document.getElementById('exploreGrid');
1458
1433
  grid.innerHTML = '';
1459
- exploreTopics.forEach(topic => {
1434
+
1435
+ for (const [key, concept] of Object.entries(exploreConcepts)) {
1436
+ const meta = CONCEPT_META[key] || { icon: '📚', tab: 'embed' };
1460
1437
  const card = document.createElement('div');
1461
1438
  card.className = 'explore-card';
1439
+ card.dataset.key = key;
1440
+
1441
+ // Build links HTML
1442
+ let linksHtml = '';
1443
+ if (concept.links && concept.links.length > 0) {
1444
+ linksHtml = '<div style="margin-top:12px;"><strong style="color:var(--accent);font-size:12px;">LEARN MORE</strong><br>' +
1445
+ concept.links.map(url => `<a href="${escapeHtml(url)}" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;word-break:break-all;">${escapeHtml(url)}</a>`).join('<br>') +
1446
+ '</div>';
1447
+ }
1448
+
1449
+ // Build try-it HTML
1450
+ let tryItHtml = '';
1451
+ if (concept.tryIt && concept.tryIt.length > 0) {
1452
+ tryItHtml = '<div style="margin-top:12px;"><strong style="color:var(--accent);font-size:12px;">TRY IT</strong>' +
1453
+ concept.tryIt.map(cmd => `<div style="font-family:var(--mono);font-size:12px;color:var(--text-dim);background:var(--bg);padding:4px 8px;border-radius:4px;margin-top:4px;">$ ${escapeHtml(cmd)}</div>`).join('') +
1454
+ '</div>';
1455
+ }
1456
+
1462
1457
  card.innerHTML = `
1463
- <div class="explore-card-icon">${topic.icon}</div>
1464
- <div class="explore-card-title">${topic.title}</div>
1465
- <div class="explore-card-summary">${topic.summary}</div>
1466
- <div class="explore-card-content">${topic.content}</div>
1458
+ <div class="explore-card-icon">${meta.icon}</div>
1459
+ <div class="explore-card-title">${escapeHtml(concept.title)}</div>
1460
+ <div class="explore-card-summary">${escapeHtml(concept.summary)}</div>
1461
+ <div class="explore-card-content">${escapeHtml(concept.content)}${linksHtml}${tryItHtml}</div>
1467
1462
  <div class="explore-card-actions">
1468
- <button class="btn btn-small" onclick="tryTopic('${topic.key}')">Try it →</button>
1463
+ <button class="btn btn-small" onclick="tryTopic('${escapeHtml(key)}')">Try it in playground →</button>
1469
1464
  <button class="btn btn-secondary btn-small" onclick="collapseTopic(this)">Collapse</button>
1470
1465
  </div>
1471
1466
  `;
1472
1467
  card.addEventListener('click', function(e) {
1473
- if (e.target.tagName === 'BUTTON') return;
1468
+ if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') return;
1474
1469
  if (!this.classList.contains('expanded')) {
1475
1470
  this.classList.add('expanded');
1476
1471
  }
1477
1472
  });
1478
1473
  grid.appendChild(card);
1479
- });
1474
+ }
1480
1475
  }
1481
1476
 
1482
1477
  window.tryTopic = function(key) {
1483
- const topic = exploreTopics.find(t => t.key === key);
1484
- if (!topic) return;
1485
- if (topic.prefill) topic.prefill();
1486
- switchTab(topic.tab);
1478
+ const meta = CONCEPT_META[key];
1479
+ if (meta) switchTab(meta.tab);
1487
1480
  };
1488
1481
 
1489
1482
  window.collapseTopic = function(btn) {
1490
1483
  btn.closest('.explore-card').classList.remove('expanded');
1491
1484
  };
1492
1485
 
1486
+ window.filterExplore = function() {
1487
+ const q = document.getElementById('exploreSearch').value.toLowerCase().trim();
1488
+ document.querySelectorAll('#exploreGrid .explore-card').forEach(card => {
1489
+ if (!q) { card.style.display = ''; return; }
1490
+ const text = card.textContent.toLowerCase();
1491
+ card.style.display = text.includes(q) ? '' : 'none';
1492
+ });
1493
+ };
1494
+
1493
1495
  // ── Benchmark: Sub-panel switching ──
1494
1496
  document.querySelectorAll('.bench-panel-btn').forEach(btn => {
1495
1497
  btn.addEventListener('click', () => {