vectra-js 0.9.3 → 0.9.5

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/README.md CHANGED
@@ -161,6 +161,13 @@ const config = {
161
161
  - `enrichment`: boolean; generate `summary`, `keywords`, `hypothetical_questions`
162
162
  - Callbacks
163
163
  - `callbacks`: array of handlers; use `LoggingCallbackHandler` or `StructuredLoggingCallbackHandler`
164
+ - Observability
165
+ - `enabled`: boolean; enable SQLite-based observability (default: false)
166
+ - `sqlitePath`: string; path to SQLite database file (default: 'vectra-observability.db')
167
+ - `projectId`: string; project identifier for multi-project support (default: 'default')
168
+ - `trackMetrics`: boolean; track latency and other metrics
169
+ - `trackTraces`: boolean; track detailed workflow traces
170
+ - `sessionTracking`: boolean; track chat sessions
164
171
  - Index Helpers (Postgres + Prisma)
165
172
  - `ensureIndexes()`: creates ivfflat and GIN FTS indexes and optional `tsvector` trigger
166
173
 
@@ -601,6 +608,29 @@ const { StructuredLoggingCallbackHandler } = require('vectra-js/src/callbacks');
601
608
  const config = { callbacks: [ new StructuredLoggingCallbackHandler() ] };
602
609
  ```
603
610
 
611
+ ### Observability
612
+
613
+ Built-in SQLite-based observability to track metrics, traces, and sessions.
614
+
615
+ ```javascript
616
+ const config = {
617
+ // ...
618
+ observability: {
619
+ enabled: true,
620
+ sqlitePath: 'vectra-observability.db',
621
+ projectId: 'my-project',
622
+ trackMetrics: true,
623
+ trackTraces: true,
624
+ sessionTracking: true
625
+ }
626
+ };
627
+ ```
628
+
629
+ This tracks:
630
+ - **Metrics**: Latency (ingest, query).
631
+ - **Traces**: Detailed spans for retrieval, generation, and ingestion workflows.
632
+ - **Sessions**: Chat session history and last query tracking.
633
+
604
634
  ### Vector Stores
605
635
  - Prisma (Postgres + pgvector), Chroma, Qdrant, Milvus.
606
636
  - Configure `database.type`, `tableName`, `columnMap`, `clientInstance`.
package/bin/vectra.js CHANGED
@@ -28,12 +28,18 @@ async function run() {
28
28
 
29
29
  if (cmd === 'webconfig') {
30
30
  const cfgPath = configPath || path.join(process.cwd(), 'vectra-config.json');
31
- startWebConfig(cfgPath);
31
+ startWebConfig(cfgPath, 'webconfig');
32
32
  return;
33
33
  }
34
34
 
35
- if (!cmd || (!target && cmd !== 'webconfig')) {
36
- console.error('Usage: vectra <ingest|query|webconfig> <path|text> [--config=path] [--stream]');
35
+ if (cmd === 'dashboard') {
36
+ const cfgPath = configPath || path.join(process.cwd(), 'vectra-config.json');
37
+ startWebConfig(cfgPath, 'dashboard');
38
+ return;
39
+ }
40
+
41
+ if (!cmd || (!target && cmd !== 'webconfig' && cmd !== 'dashboard')) {
42
+ console.error('Usage: vectra <ingest|query|webconfig|dashboard> <path|text> [--config=path] [--stream]');
37
43
  process.exit(1);
38
44
  }
39
45
 
package/package.json CHANGED
@@ -1,19 +1,14 @@
1
1
  {
2
2
  "name": "vectra-js",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "A production-ready, provider-agnostic Node.js SDK for End-to-End RAG pipelines.",
5
5
  "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
8
- "prisma:generate": "prisma generate",
9
- "lint": "eslint . --ext .js,.cjs,.mjs",
10
- "lint:fix": "eslint . --ext .js,.cjs,.mjs --fix",
11
- "prepublishOnly": "pnpm run lint",
12
- "publish": "npm publish --access public"
13
- },
14
6
  "bin": {
15
7
  "vectra": "bin/vectra.js"
16
8
  },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
17
12
  "keywords": [
18
13
  "rag",
19
14
  "llm",
@@ -33,22 +28,29 @@
33
28
  "author": "Abhishek N",
34
29
  "license": "GPL-3.0",
35
30
  "dependencies": {
36
- "@anthropic-ai/sdk": "^0.20.0",
37
- "@google/genai": "^1.30.0",
38
- "openai": "^4.0.0",
39
- "zod": "^3.22.0",
40
- "pdf-parse": "^1.1.1",
41
- "mammoth": "^1.7.0",
31
+ "@anthropic-ai/sdk": "^0.20.9",
32
+ "@google/genai": "^1.34.0",
33
+ "dotenv": "^16.6.1",
34
+ "mammoth": "^1.11.0",
35
+ "openai": "^6.15.0",
36
+ "pdf-parse": "^2.4.5",
37
+ "sqlite3": "^5.1.7",
38
+ "uuid": "^9.0.1",
42
39
  "xlsx": "^0.18.5",
43
- "uuid": "^9.0.0",
44
- "dotenv": "^16.0.0"
40
+ "zod": "^3.25.76"
45
41
  },
46
42
  "peerDependencies": {
47
43
  "@prisma/client": "^5.0.0"
48
44
  },
49
45
  "devDependencies": {
50
- "prisma": "^5.22.0",
51
- "eslint": "^9.13.0",
52
- "globals": "^13.24.0"
46
+ "eslint": "^9.39.2",
47
+ "globals": "^16.5.0",
48
+ "prisma": "^7.2.0"
49
+ },
50
+ "scripts": {
51
+ "test": "echo \"Error: no test specified\" && exit 1",
52
+ "prisma:generate": "prisma generate",
53
+ "lint": "eslint . --ext .js,.cjs,.mjs",
54
+ "lint:fix": "eslint . --ext .js,.cjs,.mjs --fix"
53
55
  }
54
- }
56
+ }
@@ -15,11 +15,22 @@ class ChromaVectorStore extends VectorStore {
15
15
  }
16
16
  }
17
17
 
18
+ _cleanMetadata(meta) {
19
+ if (!meta) return {};
20
+ const out = {};
21
+ for (const [k, v] of Object.entries(meta)) {
22
+ if (v !== undefined && v !== null) {
23
+ out[k] = v;
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+
18
29
  async addDocuments(docs) {
19
30
  await this._init();
20
31
  const ids = docs.map((d) => d.id || uuidv4());
21
32
  const embeddings = docs.map(d => d.embedding);
22
- const metadatas = docs.map(d => d.metadata);
33
+ const metadatas = docs.map(d => this._cleanMetadata(d.metadata));
23
34
  const documents = docs.map(d => d.content);
24
35
 
25
36
  await this.collection.add({
@@ -34,7 +45,7 @@ class ChromaVectorStore extends VectorStore {
34
45
  await this._init();
35
46
  const ids = docs.map((d) => d.id || uuidv4());
36
47
  const embeddings = docs.map(d => d.embedding);
37
- const metadatas = docs.map(d => d.metadata);
48
+ const metadatas = docs.map(d => this._cleanMetadata(d.metadata));
38
49
  const documents = docs.map(d => d.content);
39
50
  if (typeof this.collection.upsert === 'function') {
40
51
  await this.collection.upsert({ ids, embeddings, metadatas, documents });
package/src/config.js CHANGED
@@ -114,6 +114,15 @@ const RAGConfigSchema = z.object({
114
114
  prompts: z.object({ query: z.string().optional(), reranking: z.string().optional() }).optional(),
115
115
  tracing: z.object({ enable: z.boolean().default(false) }).optional(),
116
116
  callbacks: z.array(z.custom((val) => true)).optional(),
117
+ observability: z.object({
118
+ enabled: z.boolean().default(false),
119
+ sqlitePath: z.string().default('vectra-observability.db'),
120
+ projectId: z.string().default('default'),
121
+ trackMetrics: z.boolean().default(true),
122
+ trackTraces: z.boolean().default(true),
123
+ trackLogs: z.boolean().default(true),
124
+ sessionTracking: z.boolean().default(true)
125
+ }).default({})
117
126
  });
118
127
 
119
128
  module.exports = {
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
  }