vectra-js 0.9.2 → 0.9.4

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/src/core.js CHANGED
@@ -16,12 +16,20 @@ const { LLMReranker } = require('./reranker');
16
16
  const { InMemoryHistory, RedisHistory, PostgresHistory } = require('./memory');
17
17
  const { OllamaBackend } = require('./backends/ollama');
18
18
  const { v5: uuidv5 } = require('uuid');
19
+ const { v4: uuidv4 } = require('uuid');
20
+ const SQLiteLogger = require('./observability');
19
21
 
20
22
  class VectraClient {
21
23
  constructor(config) {
22
24
  const parsed = RAGConfigSchema.parse(config);
23
25
  this.config = parsed;
24
26
  this.callbacks = config.callbacks || [];
27
+
28
+ // Initialize observability
29
+ this.logger = (this.config.observability && this.config.observability.enabled)
30
+ ? new SQLiteLogger(this.config.observability)
31
+ : null;
32
+
25
33
  // Initialize processor
26
34
  const agenticLlm = (this.config.chunking && this.config.chunking.agenticLlm)
27
35
  ? this.createLLM(this.config.chunking.agenticLlm)
@@ -128,6 +136,12 @@ class VectraClient {
128
136
  }
129
137
 
130
138
  async ingestDocuments(filePath) {
139
+ const traceId = uuidv4();
140
+ const rootSpanId = uuidv4();
141
+ const tStart = Date.now();
142
+ const provider = this.config.embedding.provider;
143
+ const modelName = this.config.embedding.modelName;
144
+
131
145
  try {
132
146
  const stats = await fs.promises.stat(filePath);
133
147
 
@@ -292,8 +306,35 @@ class VectraClient {
292
306
  }
293
307
  const durationMs = Date.now() - t0;
294
308
  this.trigger('onIngestEnd', filePath, chunks.length, durationMs);
309
+
310
+ this.logger.logTrace({
311
+ traceId,
312
+ spanId: rootSpanId,
313
+ name: 'ingestDocuments',
314
+ startTime: tStart,
315
+ endTime: Date.now(),
316
+ input: { filePath },
317
+ output: { chunks: chunks.length, durationMs },
318
+ attributes: { fileSize: size },
319
+ provider,
320
+ modelName
321
+ });
322
+ this.logger.logMetric({ name: 'ingest_latency', value: durationMs, tags: { type: 'single_file' } });
323
+
295
324
  } catch (e) {
296
325
  this.trigger('onError', e);
326
+ this.logger.logTrace({
327
+ traceId,
328
+ spanId: rootSpanId,
329
+ name: 'ingestDocuments',
330
+ startTime: tStart,
331
+ endTime: Date.now(),
332
+ input: { filePath },
333
+ error: { message: e.message },
334
+ status: 'error',
335
+ provider,
336
+ modelName
337
+ });
297
338
  throw e;
298
339
  }
299
340
  }
@@ -459,6 +500,19 @@ class VectraClient {
459
500
  }
460
501
 
461
502
  async queryRAG(query, filter = null, stream = false, sessionId = null) {
503
+ const traceId = uuidv4();
504
+ const rootSpanId = uuidv4();
505
+ const tStart = Date.now();
506
+
507
+ if (sessionId) {
508
+ this.logger.updateSession(sessionId, null, { lastQuery: query });
509
+ }
510
+
511
+ const provider = this.config.llm.provider;
512
+ const modelName = this.config.llm.modelName;
513
+ const embeddingProvider = this.config.embedding.provider;
514
+ const embeddingModelName = this.config.embedding.modelName;
515
+
462
516
  try {
463
517
  const tRetrieval = Date.now();
464
518
  this.trigger('onRetrievalStart', query);
@@ -505,6 +559,20 @@ class VectraClient {
505
559
 
506
560
  const retrievalMs = Date.now() - tRetrieval;
507
561
  this.trigger('onRetrievalEnd', docs.length, retrievalMs);
562
+
563
+ this.logger.logTrace({
564
+ traceId,
565
+ spanId: uuidv4(),
566
+ parentSpanId: rootSpanId,
567
+ name: 'retrieval',
568
+ startTime: tRetrieval,
569
+ endTime: Date.now(),
570
+ input: { query, filter, strategy },
571
+ output: { documentsFound: docs.length },
572
+ provider: embeddingProvider,
573
+ modelName: embeddingModelName
574
+ });
575
+
508
576
  const terms = query.toLowerCase().split(/\W+/).filter(t=>t.length>2);
509
577
  docs = docs.map(d => {
510
578
  const kws = Array.isArray(d.metadata?.keywords) ? d.metadata.keywords.map(k=>String(k).toLowerCase()) : [];
@@ -547,7 +615,91 @@ class VectraClient {
547
615
  if (stream) {
548
616
  // Streaming return
549
617
  if (!this.llm.generateStream) throw new Error("Streaming not implemented for this provider");
550
- return this.llm.generateStream(prompt, systemInst);
618
+
619
+ this.logger.logTrace({
620
+ traceId,
621
+ spanId: uuidv4(),
622
+ parentSpanId: rootSpanId,
623
+ name: 'generation_stream_start',
624
+ startTime: tGen,
625
+ endTime: Date.now(),
626
+ input: { prompt },
627
+ output: { stream: true },
628
+ provider,
629
+ modelName
630
+ });
631
+
632
+ const originalStream = await this.llm.generateStream(prompt, systemInst);
633
+ const self = this;
634
+
635
+ async function* wrappedStream() {
636
+ let fullAnswer = '';
637
+ try {
638
+ for await (const chunk of originalStream) {
639
+ const delta = (chunk && chunk.delta) ? chunk.delta : (typeof chunk === 'string' ? chunk : '');
640
+ fullAnswer += delta;
641
+ yield chunk;
642
+ }
643
+ } catch (e) {
644
+ self.trigger('onError', e);
645
+ self.logger.logTrace({
646
+ traceId,
647
+ spanId: rootSpanId,
648
+ name: 'queryRAG',
649
+ startTime: tStart,
650
+ endTime: Date.now(),
651
+ input: { query, sessionId },
652
+ error: { message: e.message, stack: e.stack },
653
+ status: 'error',
654
+ provider,
655
+ modelName
656
+ });
657
+ throw e;
658
+ }
659
+
660
+ // Stream finished successfully
661
+ const genMs = Date.now() - tGen;
662
+ self.trigger('onGenerationEnd', fullAnswer, genMs);
663
+
664
+ const promptChars = prompt.length;
665
+ const answerChars = fullAnswer.length;
666
+
667
+ self.logger.logTrace({
668
+ traceId,
669
+ spanId: uuidv4(),
670
+ parentSpanId: rootSpanId,
671
+ name: 'generation',
672
+ startTime: tGen,
673
+ endTime: Date.now(),
674
+ input: { prompt },
675
+ output: { answer: fullAnswer.substring(0, 1000) },
676
+ attributes: { prompt_chars: promptChars, completion_chars: answerChars },
677
+ provider,
678
+ modelName
679
+ });
680
+
681
+ self.logger.logMetric({ name: 'prompt_chars', value: promptChars });
682
+ self.logger.logMetric({ name: 'completion_chars', value: answerChars });
683
+
684
+ self.logger.logTrace({
685
+ traceId,
686
+ spanId: rootSpanId,
687
+ name: 'queryRAG',
688
+ startTime: tStart,
689
+ endTime: Date.now(),
690
+ input: { query, sessionId },
691
+ output: { success: true },
692
+ attributes: { retrievalMs, genMs, docCount: docs.length },
693
+ provider,
694
+ modelName
695
+ });
696
+
697
+ self.logger.logMetric({ name: 'query_latency', value: Date.now() - tStart, tags: { type: 'total' } });
698
+ self.logger.logMetric({ name: 'retrieval_latency', value: retrievalMs, tags: { type: 'retrieval' } });
699
+ self.logger.logMetric({ name: 'generation_latency', value: genMs, tags: { type: 'generation' } });
700
+ }
701
+
702
+ return wrappedStream();
551
703
  } else {
552
704
  const answer = await this.llm.generate(prompt, systemInst);
553
705
  if (this.history && sessionId) {
@@ -561,6 +713,44 @@ class VectraClient {
561
713
  }
562
714
  const genMs = Date.now() - tGen;
563
715
  this.trigger('onGenerationEnd', answer, genMs);
716
+
717
+ const promptChars = prompt.length;
718
+ const answerChars = answer ? String(answer).length : 0;
719
+
720
+ this.logger.logTrace({
721
+ traceId,
722
+ spanId: uuidv4(),
723
+ parentSpanId: rootSpanId,
724
+ name: 'generation',
725
+ startTime: tGen,
726
+ endTime: Date.now(),
727
+ input: { prompt },
728
+ output: { answer: String(answer).substring(0, 1000) }, // Truncate for log
729
+ attributes: { prompt_chars: promptChars, completion_chars: answerChars },
730
+ provider,
731
+ modelName
732
+ });
733
+
734
+ this.logger.logMetric({ name: 'prompt_chars', value: promptChars });
735
+ this.logger.logMetric({ name: 'completion_chars', value: answerChars });
736
+
737
+ this.logger.logTrace({
738
+ traceId,
739
+ spanId: rootSpanId,
740
+ name: 'queryRAG',
741
+ startTime: tStart,
742
+ endTime: Date.now(),
743
+ input: { query, sessionId },
744
+ output: { success: true },
745
+ attributes: { retrievalMs, genMs, docCount: docs.length },
746
+ provider,
747
+ modelName
748
+ });
749
+
750
+ this.logger.logMetric({ name: 'query_latency', value: Date.now() - tStart, tags: { type: 'total' } });
751
+ this.logger.logMetric({ name: 'retrieval_latency', value: retrievalMs, tags: { type: 'retrieval' } });
752
+ this.logger.logMetric({ name: 'generation_latency', value: genMs, tags: { type: 'generation' } });
753
+
564
754
  if (this.config.generation && this.config.generation.outputFormat === 'json') {
565
755
  try { const parsed = JSON.parse(String(answer)); return { answer: parsed, sources: docs.map(d => d.metadata) }; } catch { return { answer, sources: docs.map(d => d.metadata) }; }
566
756
  }
@@ -568,6 +758,18 @@ class VectraClient {
568
758
  }
569
759
  } catch (e) {
570
760
  this.trigger('onError', e);
761
+ this.logger.logTrace({
762
+ traceId,
763
+ spanId: rootSpanId,
764
+ name: 'queryRAG',
765
+ startTime: tStart,
766
+ endTime: Date.now(),
767
+ input: { query, sessionId },
768
+ error: { message: e.message, stack: e.stack },
769
+ status: 'error',
770
+ provider,
771
+ modelName
772
+ });
571
773
  throw e;
572
774
  }
573
775
  }
@@ -0,0 +1,260 @@
1
+ // State
2
+ let currentView = 'overview';
3
+ let currentProject = 'all';
4
+ let lastStats = null;
5
+
6
+ // Init
7
+ document.addEventListener('DOMContentLoaded', () => {
8
+ loadProjects();
9
+ loadDashboardData();
10
+
11
+ // Auto-refresh every 30s
12
+ setInterval(loadDashboardData, 30000);
13
+
14
+ // Initialize Icons
15
+ if (window.lucide) {
16
+ window.lucide.createIcons();
17
+ }
18
+
19
+ // Project filter
20
+ const projectSelect = document.getElementById('projectSelect');
21
+ if (projectSelect) {
22
+ projectSelect.addEventListener('change', (e) => {
23
+ currentProject = e.target.value;
24
+ loadDashboardData();
25
+ });
26
+ }
27
+ });
28
+
29
+ function switchView(view) {
30
+ currentView = view;
31
+
32
+ // Update content visibility
33
+ ['overview', 'traces', 'sessions'].forEach(v => {
34
+ const div = document.getElementById(`view-${v}`);
35
+ if (v === view) {
36
+ div.classList.remove('hidden');
37
+ } else {
38
+ div.classList.add('hidden');
39
+ }
40
+ });
41
+
42
+ // Update Sidebar Links
43
+ document.querySelectorAll('.sidebar-link').forEach(el => {
44
+ el.classList.remove('active', 'bg-gray-100', 'text-slate-900');
45
+ el.classList.add('text-gray-600');
46
+ });
47
+
48
+ const btn = document.getElementById(`btn-${view}`);
49
+ if (btn) {
50
+ btn.classList.add('active', 'bg-gray-100', 'text-slate-900');
51
+ btn.classList.remove('text-gray-600');
52
+ }
53
+
54
+ if (view === 'traces') loadTraces();
55
+ if (view === 'sessions') loadSessions();
56
+ }
57
+
58
+ async function fetchAPI(endpoint) {
59
+ try {
60
+ const url = `/api/observability/${endpoint}${currentProject !== 'all' ? `?projectId=${currentProject}` : ''}`;
61
+ const res = await fetch(url);
62
+ if (!res.ok) throw new Error('API Error');
63
+ return await res.json();
64
+ } catch (e) {
65
+ console.error('Fetch error:', e);
66
+ return null;
67
+ }
68
+ }
69
+
70
+ async function loadProjects() {
71
+ const projects = await fetchAPI('projects');
72
+ const select = document.getElementById('projectSelect');
73
+ if (projects && projects.length > 0) {
74
+ // Keep 'all' option
75
+ // Add projects
76
+ projects.forEach(p => {
77
+ if (p === 'all') return; // skip if somehow in db
78
+ const opt = document.createElement('option');
79
+ opt.value = p;
80
+ opt.textContent = p;
81
+ select.appendChild(opt);
82
+ });
83
+ }
84
+ }
85
+
86
+ async function loadDashboardData() {
87
+ const stats = await fetchAPI('stats');
88
+ if (!stats) return;
89
+
90
+ lastStats = stats;
91
+
92
+ // Update Stats Cards
93
+ document.getElementById('stat-total-req').textContent = stats.totalRequests || 0;
94
+ document.getElementById('stat-avg-latency').textContent = Math.round(stats.avgLatency || 0);
95
+ document.getElementById('stat-tokens').textContent = ((stats.totalPromptChars || 0) + (stats.totalCompletionChars || 0)).toLocaleString();
96
+ document.getElementById('stat-errors').textContent = '0%'; // Placeholder for now
97
+
98
+ // Update Charts
99
+ updateCharts(stats.history);
100
+ }
101
+
102
+ let latencyChartInst = null;
103
+ let tokenChartInst = null;
104
+
105
+ function updateCharts(history = []) {
106
+ // History is expected to be array of { timestamp, latency, tokens }
107
+ // If not provided by API yet, mock or skip
108
+ if (!history.length) return;
109
+
110
+ const labels = history.map(h => new Date(h.timestamp).toLocaleTimeString());
111
+ const latencies = history.map(h => h.latency);
112
+ const tokens = history.map(h => h.tokens);
113
+
114
+ // Latency Chart
115
+ const ctxL = document.getElementById('latencyChart').getContext('2d');
116
+ const isDark = document.documentElement.classList.contains('dark');
117
+ const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
118
+ const textColor = isDark ? '#9ca3af' : '#64748b';
119
+
120
+ if (latencyChartInst) latencyChartInst.destroy();
121
+ latencyChartInst = new Chart(ctxL, {
122
+ type: 'line',
123
+ data: {
124
+ labels,
125
+ datasets: [{
126
+ label: 'Latency (ms)',
127
+ data: latencies,
128
+ borderColor: '#8b5cf6',
129
+ tension: 0.4,
130
+ fill: true,
131
+ backgroundColor: isDark ? 'rgba(139, 92, 246, 0.1)' : 'rgba(139, 92, 246, 0.05)'
132
+ }]
133
+ },
134
+ options: {
135
+ responsive: true,
136
+ maintainAspectRatio: false,
137
+ plugins: { legend: { display: false } },
138
+ scales: {
139
+ y: {
140
+ beginAtZero: true,
141
+ grid: { color: gridColor, borderDash: [2, 4] },
142
+ ticks: { color: textColor }
143
+ },
144
+ x: {
145
+ grid: { display: false },
146
+ ticks: { color: textColor }
147
+ }
148
+ }
149
+ }
150
+ });
151
+
152
+ // Token Chart
153
+ const ctxT = document.getElementById('tokenChart').getContext('2d');
154
+ if (tokenChartInst) tokenChartInst.destroy();
155
+ tokenChartInst = new Chart(ctxT, {
156
+ type: 'bar',
157
+ data: {
158
+ labels,
159
+ datasets: [{
160
+ label: 'Tokens',
161
+ data: tokens,
162
+ backgroundColor: '#0ea5e9',
163
+ borderRadius: 4
164
+ }]
165
+ },
166
+ options: {
167
+ responsive: true,
168
+ maintainAspectRatio: false,
169
+ plugins: { legend: { display: false } },
170
+ scales: {
171
+ y: {
172
+ beginAtZero: true,
173
+ grid: { color: gridColor, borderDash: [2, 4] },
174
+ ticks: { color: textColor }
175
+ },
176
+ x: {
177
+ grid: { display: false },
178
+ ticks: { color: textColor }
179
+ }
180
+ }
181
+ }
182
+ });
183
+ }
184
+
185
+ async function loadTraces() {
186
+ const traces = await fetchAPI('traces'); // Expects list of recent traces
187
+ const tbody = document.getElementById('traces-table-body');
188
+ tbody.innerHTML = '';
189
+
190
+ if (!traces || traces.length === 0) {
191
+ tbody.innerHTML = '<tr><td colspan="6" class="px-6 py-4 text-center text-sm text-slate-500 dark:text-gray-400">No traces found</td></tr>';
192
+ return;
193
+ }
194
+
195
+ traces.forEach(t => {
196
+ const row = document.createElement('tr');
197
+ row.className = 'hover:bg-slate-50 dark:hover:bg-white/5 transition-colors cursor-pointer';
198
+ row.onclick = () => window.location.href = `/dashboard/trace.html?id=${t.trace_id}`;
199
+
200
+ const statusColor = t.error && t.error !== '{}' ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' : 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400';
201
+ const statusText = t.error && t.error !== '{}' ? 'Error' : 'Success';
202
+
203
+ row.innerHTML = `
204
+ <td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-slate-500 dark:text-gray-400">${t.trace_id.slice(0, 8)}...</td>
205
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900 dark:text-white">${t.name}</td>
206
+ <td class="px-6 py-4 whitespace-nowrap"><span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusColor}">${statusText}</span></td>
207
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-gray-400">${t.end_time - t.start_time}ms</td>
208
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-gray-400">${new Date(t.start_time).toLocaleString()}</td>
209
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-brand-600 dark:text-brand-400 hover:text-brand-900 dark:hover:text-brand-300">View</td>
210
+ `;
211
+ tbody.appendChild(row);
212
+ });
213
+ }
214
+
215
+ async function loadSessions() {
216
+ const sessions = await fetchAPI('sessions');
217
+ const tbody = document.getElementById('sessions-table-body');
218
+ tbody.innerHTML = '';
219
+
220
+ if (!sessions || sessions.length === 0) {
221
+ tbody.innerHTML = '<tr><td colspan="4" class="px-6 py-4 text-center text-sm text-slate-500">No active sessions</td></tr>';
222
+ return;
223
+ }
224
+
225
+ sessions.forEach(s => {
226
+ const row = document.createElement('tr');
227
+ row.className = 'hover:bg-slate-50 dark:hover:bg-white/5 transition-colors';
228
+
229
+ row.innerHTML = `
230
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900 dark:text-white">${s.session_id}</td>
231
+ <td class="px-6 py-4 text-sm text-slate-500 dark:text-gray-400 truncate max-w-xs">${s.metadata?.last_query || '-'}</td>
232
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-gray-400">${new Date(s.last_activity_time).toLocaleString()}</td>
233
+ <td class="px-6 py-4 text-sm text-slate-500 dark:text-gray-400 font-mono text-xs">${JSON.stringify(s.metadata || {}).slice(0, 30)}...</td>
234
+ `;
235
+ tbody.appendChild(row);
236
+ });
237
+ }
238
+
239
+ /* Modal functions removed as trace details are now on a separate page */
240
+ function showTraceDetails(traceId) {
241
+ window.location.href = `/dashboard/trace.html?id=${traceId}`;
242
+ }
243
+
244
+ function closeModal() {
245
+ // Deprecated
246
+ }
247
+
248
+ function toggleTheme() {
249
+ if (document.documentElement.classList.contains('dark')) {
250
+ document.documentElement.classList.remove('dark');
251
+ localStorage.setItem('color-theme', 'light');
252
+ } else {
253
+ document.documentElement.classList.add('dark');
254
+ localStorage.setItem('color-theme', 'dark');
255
+ }
256
+
257
+ if (lastStats && lastStats.history) {
258
+ updateCharts(lastStats.history);
259
+ }
260
+ }