@retrivora-ai/rag-engine 1.7.5 → 1.7.7

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/dist/server.js CHANGED
@@ -3865,65 +3865,410 @@ var QueryProcessor = class {
3865
3865
  }
3866
3866
  };
3867
3867
 
3868
- // src/core/Pipeline.ts
3869
- var LRUEmbeddingCache = class {
3870
- constructor(maxSize = 500) {
3871
- this.cache = /* @__PURE__ */ new Map();
3872
- this.maxSize = maxSize;
3873
- }
3874
- get(key) {
3875
- const value = this.cache.get(key);
3876
- if (value !== void 0) {
3877
- this.cache.delete(key);
3878
- this.cache.set(key, value);
3868
+ // src/utils/UITransformer.ts
3869
+ var UITransformer = class {
3870
+ /**
3871
+ * Main transformation method
3872
+ * Analyzes user query and retrieved data to determine the best visualization format
3873
+ *
3874
+ * @param userQuery - The original user question/query
3875
+ * @param retrievedData - Vector database retrieval results
3876
+ * @returns Structured JSON response for UI rendering
3877
+ */
3878
+ static transform(userQuery, retrievedData) {
3879
+ if (!retrievedData || retrievedData.length === 0) {
3880
+ return this.createTextResponse("No data available", "No relevant data found for your query.");
3881
+ }
3882
+ const analysis = this.analyzeData(userQuery, retrievedData);
3883
+ switch (analysis.suggestedType) {
3884
+ case "product_carousel":
3885
+ return this.transformToProductCarousel(retrievedData);
3886
+ case "pie_chart":
3887
+ return this.transformToPieChart(retrievedData);
3888
+ case "bar_chart":
3889
+ return this.transformToBarChart(retrievedData);
3890
+ case "line_chart":
3891
+ return this.transformToLineChart(retrievedData);
3892
+ case "table":
3893
+ return this.transformToTable(retrievedData);
3894
+ default:
3895
+ return this.transformToText(retrievedData);
3879
3896
  }
3880
- return value;
3881
3897
  }
3882
- set(key, value) {
3883
- if (this.cache.has(key)) {
3884
- this.cache.delete(key);
3885
- } else if (this.cache.size >= this.maxSize) {
3886
- const oldestKey = this.cache.keys().next().value;
3887
- if (oldestKey !== void 0) this.cache.delete(oldestKey);
3898
+ /**
3899
+ * Analyzes the data and query to suggest the best visualization type
3900
+ */
3901
+ static analyzeData(query, data) {
3902
+ const queryLower = query.toLowerCase();
3903
+ const hasProducts = data.some(
3904
+ (item) => this.isProductData(item)
3905
+ );
3906
+ const hasTimeSeries = data.some(
3907
+ (item) => this.isTimeSeriesData(item)
3908
+ );
3909
+ const hasCategories = this.detectCategories(data).length > 0;
3910
+ const isDistributionQuery = queryLower.includes("distribution") || queryLower.includes("breakdown") || queryLower.includes("percentage") || queryLower.includes("proportion");
3911
+ const isComparisonQuery = queryLower.includes("compare") || queryLower.includes("vs") || queryLower.includes("difference");
3912
+ const isTrendQuery = queryLower.includes("trend") || queryLower.includes("over time") || queryLower.includes("growth") || queryLower.includes("decline");
3913
+ if (hasProducts && !hasTimeSeries && !isDistributionQuery) {
3914
+ return { suggestedType: "product_carousel", confidence: 0.95, hasProducts, hasTimeSeries, hasCategories };
3888
3915
  }
3889
- this.cache.set(key, value);
3916
+ if (isTrendQuery && hasTimeSeries) {
3917
+ return { suggestedType: "line_chart", confidence: 0.9, hasProducts, hasTimeSeries, hasCategories };
3918
+ }
3919
+ if (isDistributionQuery && hasCategories) {
3920
+ return { suggestedType: "pie_chart", confidence: 0.85, hasProducts, hasTimeSeries, hasCategories };
3921
+ }
3922
+ if (isComparisonQuery && hasCategories && data.length > 2) {
3923
+ return { suggestedType: "bar_chart", confidence: 0.8, hasProducts, hasTimeSeries, hasCategories };
3924
+ }
3925
+ if (data.length > 5 && this.hasMultipleFields(data)) {
3926
+ return { suggestedType: "table", confidence: 0.75, hasProducts, hasTimeSeries, hasCategories };
3927
+ }
3928
+ return { suggestedType: "text", confidence: 0.5, hasProducts, hasTimeSeries, hasCategories };
3890
3929
  }
3891
- clear() {
3892
- this.cache.clear();
3930
+ /**
3931
+ * Transform data to product carousel format
3932
+ */
3933
+ static transformToProductCarousel(data) {
3934
+ const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item)).filter((p) => p !== null);
3935
+ return {
3936
+ type: "product_carousel",
3937
+ title: "Recommended Products",
3938
+ description: `Found ${products.length} relevant products`,
3939
+ data: products
3940
+ };
3893
3941
  }
3894
- get size() {
3895
- return this.cache.size;
3942
+ /**
3943
+ * Transform data to pie chart format
3944
+ */
3945
+ static transformToPieChart(data) {
3946
+ const categories = this.detectCategories(data);
3947
+ const categoryData = this.aggregateByCategory(data, categories);
3948
+ const pieData = Object.entries(categoryData).map(([label, count]) => ({
3949
+ label,
3950
+ value: count,
3951
+ inStock: this.checkStockStatus(label, data)
3952
+ }));
3953
+ return {
3954
+ type: "pie_chart",
3955
+ title: "Distribution by Category",
3956
+ description: `Showing breakdown across ${categories.length} categories`,
3957
+ data: pieData
3958
+ };
3896
3959
  }
3897
- };
3898
- var Pipeline = class {
3899
- constructor(config) {
3900
- this.config = config;
3901
- /** LRU-bounded cache: avoids re-embedding identical queries within the same process. */
3902
- this.embeddingCache = new LRUEmbeddingCache(500);
3903
- this.initialised = false;
3904
- var _a, _b, _c, _d, _e;
3905
- this.chunker = new DocumentChunker(
3906
- (_b = (_a = config.rag) == null ? void 0 : _a.chunkSize) != null ? _b : 1e3,
3907
- (_d = (_c = config.rag) == null ? void 0 : _c.chunkOverlap) != null ? _d : 200
3960
+ /**
3961
+ * Transform data to bar chart format
3962
+ */
3963
+ static transformToBarChart(data) {
3964
+ const categories = this.detectCategories(data);
3965
+ const categoryData = this.aggregateByCategory(data, categories);
3966
+ const barData = Object.entries(categoryData).map(([category, value]) => ({
3967
+ category,
3968
+ value,
3969
+ inStock: this.checkStockStatus(category, data)
3970
+ }));
3971
+ return {
3972
+ type: "bar_chart",
3973
+ title: "Comparison by Category",
3974
+ description: `Comparing ${categories.length} categories`,
3975
+ data: barData
3976
+ };
3977
+ }
3978
+ /**
3979
+ * Transform data to line chart format
3980
+ */
3981
+ static transformToLineChart(data) {
3982
+ const timePoints = this.extractTimeSeriesData(data);
3983
+ const lineData = timePoints.map((point) => ({
3984
+ timestamp: point.timestamp,
3985
+ value: point.value,
3986
+ label: point.label
3987
+ }));
3988
+ return {
3989
+ type: "line_chart",
3990
+ title: "Trend Over Time",
3991
+ description: `Showing ${lineData.length} data points`,
3992
+ data: lineData
3993
+ };
3994
+ }
3995
+ /**
3996
+ * Transform data to table format
3997
+ */
3998
+ static transformToTable(data) {
3999
+ const columns = this.extractTableColumns(data);
4000
+ const rows = data.map((item) => this.extractTableRow(item, columns));
4001
+ const tableData = {
4002
+ columns,
4003
+ rows
4004
+ };
4005
+ return {
4006
+ type: "table",
4007
+ title: "Detailed Results",
4008
+ description: `Showing ${data.length} results`,
4009
+ data: tableData
4010
+ };
4011
+ }
4012
+ /**
4013
+ * Transform data to text format (fallback)
4014
+ */
4015
+ static transformToText(data) {
4016
+ const textContent = data.map((item) => item.content).join("\n\n");
4017
+ return this.createTextResponse(
4018
+ "Search Results",
4019
+ textContent,
4020
+ `Found ${data.length} relevant results`
3908
4021
  );
3909
- if (((_e = config.rag) == null ? void 0 : _e.chunkingStrategy) === "llamaindex") {
3910
- this.llamaIngestor = new LlamaIndexIngestor();
4022
+ }
4023
+ /**
4024
+ * Helper: Create text response
4025
+ */
4026
+ static createTextResponse(title, content, description) {
4027
+ return {
4028
+ type: "text",
4029
+ title,
4030
+ description,
4031
+ data: { content }
4032
+ };
4033
+ }
4034
+ /**
4035
+ * Helper: Check if data item is product-related
4036
+ */
4037
+ static isProductData(item) {
4038
+ const content = (item.content || "").toLowerCase();
4039
+ const productKeywords = ["product", "price", "stock", "item", "sku", "brand", "model"];
4040
+ return productKeywords.some((kw) => content.includes(kw)) || Object.keys(item.metadata || {}).some(
4041
+ (k) => ["name", "price", "product", "sku"].includes(k.toLowerCase())
4042
+ );
4043
+ }
4044
+ /**
4045
+ * Helper: Check if data contains time series
4046
+ */
4047
+ static isTimeSeriesData(item) {
4048
+ const content = (item.content || "").toLowerCase();
4049
+ const timeKeywords = ["date", "time", "year", "month", "week", "day", "hour", "trend", "historical"];
4050
+ return timeKeywords.some((kw) => content.includes(kw)) || Object.keys(item.metadata || {}).some(
4051
+ (k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
4052
+ );
4053
+ }
4054
+ /**
4055
+ * Helper: Extract product information from vector match
4056
+ */
4057
+ static extractProductInfo(item) {
4058
+ const meta = item.metadata || {};
4059
+ if (meta.name || meta.product) {
4060
+ return {
4061
+ id: item.id,
4062
+ name: meta.name || meta.product,
4063
+ price: meta.price,
4064
+ image: meta.image || meta.imageUrl,
4065
+ brand: meta.brand,
4066
+ description: item.content,
4067
+ inStock: this.determineStockStatus(item)
4068
+ };
3911
4069
  }
3912
- this.reranker = new Reranker();
4070
+ const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
4071
+ const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d.]+)/i);
4072
+ if (nameMatch) {
4073
+ return {
4074
+ id: item.id,
4075
+ name: nameMatch[1],
4076
+ price: priceMatch ? parseFloat(priceMatch[1]) : void 0,
4077
+ description: item.content,
4078
+ inStock: this.determineStockStatus(item)
4079
+ };
4080
+ }
4081
+ return null;
3913
4082
  }
3914
- async initialize() {
3915
- var _a, _b;
3916
- if (this.initialised) return;
3917
- const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
3918
- const chartInstruction = `
3919
-
3920
- ${CHART_MARKER}
3921
- ### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
3922
-
3923
- You are responsible for returning a SINGLE structured UI block when visualization is required.
3924
-
3925
- ---
3926
-
4083
+ /**
4084
+ * Helper: Detect categories in data
4085
+ */
4086
+ static detectCategories(data) {
4087
+ const categories = /* @__PURE__ */ new Set();
4088
+ data.forEach((item) => {
4089
+ const meta = item.metadata || {};
4090
+ if (meta.category) {
4091
+ categories.add(String(meta.category));
4092
+ }
4093
+ if (meta.type) {
4094
+ categories.add(String(meta.type));
4095
+ }
4096
+ if (meta.tag) {
4097
+ const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
4098
+ tags.forEach((t) => categories.add(String(t)));
4099
+ }
4100
+ const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
4101
+ if (categoryMatch) {
4102
+ categories.add(categoryMatch[1].trim());
4103
+ }
4104
+ });
4105
+ return Array.from(categories);
4106
+ }
4107
+ /**
4108
+ * Helper: Aggregate data by category
4109
+ */
4110
+ static aggregateByCategory(data, categories) {
4111
+ const result = {};
4112
+ categories.forEach((cat) => {
4113
+ result[cat] = 0;
4114
+ });
4115
+ data.forEach((item) => {
4116
+ const meta = item.metadata || {};
4117
+ const itemCategory = meta.category || meta.type || "Other";
4118
+ if (result.hasOwnProperty(itemCategory)) {
4119
+ result[itemCategory]++;
4120
+ } else {
4121
+ result["Other"] = (result["Other"] || 0) + 1;
4122
+ }
4123
+ });
4124
+ return result;
4125
+ }
4126
+ /**
4127
+ * Helper: Extract time series data
4128
+ */
4129
+ static extractTimeSeriesData(data) {
4130
+ return data.map((item) => {
4131
+ const meta = item.metadata || {};
4132
+ return {
4133
+ timestamp: meta.timestamp || meta.date || (/* @__PURE__ */ new Date()).toISOString(),
4134
+ value: meta.value || item.score || 0,
4135
+ label: meta.label || item.content.substring(0, 50)
4136
+ };
4137
+ });
4138
+ }
4139
+ /**
4140
+ * Helper: Extract table columns
4141
+ */
4142
+ static extractTableColumns(data) {
4143
+ const columnSet = /* @__PURE__ */ new Set();
4144
+ columnSet.add("Content");
4145
+ data.forEach((item) => {
4146
+ Object.keys(item.metadata || {}).forEach((key) => {
4147
+ columnSet.add(key.charAt(0).toUpperCase() + key.slice(1));
4148
+ });
4149
+ });
4150
+ return Array.from(columnSet);
4151
+ }
4152
+ /**
4153
+ * Helper: Extract table row
4154
+ */
4155
+ static extractTableRow(item, columns) {
4156
+ const meta = item.metadata || {};
4157
+ return columns.map((col) => {
4158
+ if (col === "Content") {
4159
+ return item.content.substring(0, 100);
4160
+ }
4161
+ const metaKey = col.charAt(0).toLowerCase() + col.slice(1);
4162
+ const value = meta[metaKey];
4163
+ return value !== void 0 ? String(value) : "";
4164
+ });
4165
+ }
4166
+ /**
4167
+ * Helper: Check stock status
4168
+ */
4169
+ static checkStockStatus(category, data) {
4170
+ const item = data.find((d) => {
4171
+ const meta = d.metadata || {};
4172
+ return meta.category === category || meta.type === category;
4173
+ });
4174
+ return this.determineStockStatus(item || {});
4175
+ }
4176
+ /**
4177
+ * Helper: Determine if item is in stock
4178
+ */
4179
+ static determineStockStatus(item) {
4180
+ const meta = item.metadata || {};
4181
+ if (meta.inStock !== void 0) {
4182
+ return Boolean(meta.inStock);
4183
+ }
4184
+ if (meta.stock !== void 0) {
4185
+ return Boolean(meta.stock);
4186
+ }
4187
+ if (meta.available !== void 0) {
4188
+ return Boolean(meta.available);
4189
+ }
4190
+ const content = (item.content || "").toLowerCase();
4191
+ if (content.includes("out of stock") || content.includes("unavailable")) {
4192
+ return false;
4193
+ }
4194
+ if (content.includes("in stock") || content.includes("available")) {
4195
+ return true;
4196
+ }
4197
+ return true;
4198
+ }
4199
+ /**
4200
+ * Helper: Check if data has multiple fields
4201
+ */
4202
+ static hasMultipleFields(data) {
4203
+ const fieldCount = /* @__PURE__ */ new Set();
4204
+ data.forEach((item) => {
4205
+ Object.keys(item.metadata || {}).forEach((key) => {
4206
+ fieldCount.add(key);
4207
+ });
4208
+ });
4209
+ return fieldCount.size > 2;
4210
+ }
4211
+ };
4212
+
4213
+ // src/core/Pipeline.ts
4214
+ var LRUEmbeddingCache = class {
4215
+ constructor(maxSize = 500) {
4216
+ this.cache = /* @__PURE__ */ new Map();
4217
+ this.maxSize = maxSize;
4218
+ }
4219
+ get(key) {
4220
+ const value = this.cache.get(key);
4221
+ if (value !== void 0) {
4222
+ this.cache.delete(key);
4223
+ this.cache.set(key, value);
4224
+ }
4225
+ return value;
4226
+ }
4227
+ set(key, value) {
4228
+ if (this.cache.has(key)) {
4229
+ this.cache.delete(key);
4230
+ } else if (this.cache.size >= this.maxSize) {
4231
+ const oldestKey = this.cache.keys().next().value;
4232
+ if (oldestKey !== void 0) this.cache.delete(oldestKey);
4233
+ }
4234
+ this.cache.set(key, value);
4235
+ }
4236
+ clear() {
4237
+ this.cache.clear();
4238
+ }
4239
+ get size() {
4240
+ return this.cache.size;
4241
+ }
4242
+ };
4243
+ var Pipeline = class {
4244
+ constructor(config) {
4245
+ this.config = config;
4246
+ /** LRU-bounded cache: avoids re-embedding identical queries within the same process. */
4247
+ this.embeddingCache = new LRUEmbeddingCache(500);
4248
+ this.initialised = false;
4249
+ var _a, _b, _c, _d, _e;
4250
+ this.chunker = new DocumentChunker(
4251
+ (_b = (_a = config.rag) == null ? void 0 : _a.chunkSize) != null ? _b : 1e3,
4252
+ (_d = (_c = config.rag) == null ? void 0 : _c.chunkOverlap) != null ? _d : 200
4253
+ );
4254
+ if (((_e = config.rag) == null ? void 0 : _e.chunkingStrategy) === "llamaindex") {
4255
+ this.llamaIngestor = new LlamaIndexIngestor();
4256
+ }
4257
+ this.reranker = new Reranker();
4258
+ }
4259
+ async initialize() {
4260
+ var _a, _b;
4261
+ if (this.initialised) return;
4262
+ const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
4263
+ const chartInstruction = `
4264
+
4265
+ ${CHART_MARKER}
4266
+ ### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
4267
+
4268
+ You are responsible for returning a SINGLE structured UI block when visualization is required.
4269
+
4270
+ ---
4271
+
3927
4272
  ## 1. INTENT CLASSIFICATION (MANDATORY)
3928
4273
 
3929
4274
  Classify the query into ONE of the following intents:
@@ -4016,11 +4361,14 @@ Classify the query into ONE of the following intents:
4016
4361
 
4017
4362
  ## 5. DATA RULES
4018
4363
 
4019
- - NEVER hallucinate fields
4364
+ - NEVER hallucinate fields or data
4020
4365
  - ONLY use retrieved vector DB data
4021
4366
  - NO placeholders ("...", "N/A")
4022
4367
  - If image/link missing \u2192 REMOVE FIELD
4023
4368
  - Aggregate BEFORE rendering chart
4369
+ - IF NO DATA IS FOUND in the retrieved context:
4370
+ \u2192 DO NOT output a \`\`\`ui\`\`\` block
4371
+ \u2192 Instead, state clearly that no relevant products or data were found.
4024
4372
 
4025
4373
  ---
4026
4374
 
@@ -4040,6 +4388,8 @@ Classify the query into ONE of the following intents:
4040
4388
  - ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block
4041
4389
  - JSON must be VALID
4042
4390
  - No explanation inside block
4391
+ - NEVER use ASCII charts, Markdown tables, or text-based drawings for data.
4392
+ - If data is tabular or statistical, ALWAYS use the \`\`\`ui\`\`\` block.
4043
4393
 
4044
4394
  ---
4045
4395
 
@@ -4085,1318 +4435,983 @@ User: "distribution of products across categories"
4085
4435
  "metadata": {
4086
4436
  "intent": "analyze",
4087
4437
  "confidence": 0.98
4088
- },
4089
- "chart": {
4090
- "type": "pie",
4091
- "xKey": "label",
4092
- "yKeys": ["value"],
4093
- "data": [
4094
- { "label": "Beauty", "value": 15 },
4095
- { "label": "Electronics", "value": 10 }
4096
- ]
4097
- },
4098
- "insights": ["Beauty dominates inventory"]
4099
- }
4100
- \`\`\`
4101
- `;
4102
- if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
4103
- let cleanPrompt = this.config.llm.systemPrompt || "";
4104
- cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, "");
4105
- cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, "");
4106
- this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
4107
- }
4108
- console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
4109
- this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
4110
- const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
4111
- this.config.llm,
4112
- this.config.embedding
4113
- );
4114
- this.llmProvider = llmProvider;
4115
- this.embeddingProvider = embeddingProvider;
4116
- if (this.config.graphDb) {
4117
- this.graphDB = await ProviderRegistry.createGraphProvider(this.config.graphDb);
4118
- await this.graphDB.initialize();
4119
- this.entityExtractor = new EntityExtractor(this.llmProvider);
4120
- }
4121
- await this.vectorDB.initialize();
4122
- if (((_b = this.config.rag) == null ? void 0 : _b.architecture) === "agentic") {
4123
- this.agent = new LangChainAgent(this, this.config);
4124
- await this.agent.initialize(this.llmProvider);
4125
- }
4126
- this.initialised = true;
4127
- }
4128
- /**
4129
- * Ingest documents with automatic chunking, embedding, and batch upsert.
4130
- * Handles retries for transient failures.
4131
- */
4132
- async ingest(documents, namespace) {
4133
- await this.initialize();
4134
- const ns = namespace != null ? namespace : this.config.projectId;
4135
- const results = [];
4136
- for (const doc of documents) {
4137
- try {
4138
- const chunks = await this.prepareChunks(doc);
4139
- const vectors = await this.processEmbeddings(chunks);
4140
- const upsertDocs = chunks.map((chunk, i) => ({
4141
- id: chunk.id,
4142
- vector: vectors[i],
4143
- content: chunk.content,
4144
- metadata: chunk.metadata
4145
- }));
4146
- const totalProcessed = await this.processUpserts(upsertDocs, ns);
4147
- results.push({
4148
- docId: doc.docId,
4149
- chunksIngested: totalProcessed
4150
- });
4151
- if (this.graphDB && this.entityExtractor) {
4152
- await this.processGraphIngestion(doc.docId, chunks);
4153
- }
4154
- } catch (error) {
4155
- console.error(`[Pipeline] Failed to ingest document ${doc.docId}:`, error);
4156
- results.push({ docId: doc.docId, chunksIngested: 0 });
4157
- }
4158
- }
4159
- return results;
4160
- }
4161
- /**
4162
- * Step 1: Chunk the document content.
4163
- */
4164
- async prepareChunks(doc) {
4165
- var _a, _b;
4166
- if (this.llamaIngestor) {
4167
- return await this.llamaIngestor.chunk(doc.content, {
4168
- docId: doc.docId,
4169
- metadata: doc.metadata,
4170
- chunkSize: (_a = this.config.rag) == null ? void 0 : _a.chunkSize,
4171
- chunkOverlap: (_b = this.config.rag) == null ? void 0 : _b.chunkOverlap
4172
- });
4173
- }
4174
- return this.chunker.chunk(doc.content, {
4175
- docId: doc.docId,
4176
- metadata: doc.metadata
4177
- });
4178
- }
4179
- /**
4180
- * Step 2: Generate embeddings for chunks with retry logic.
4181
- * Uses batchEmbed when available for efficiency; falls back to sequential embedding.
4182
- */
4183
- async processEmbeddings(chunks) {
4184
- const embedBatchOptions = {
4185
- batchSize: 50,
4186
- maxRetries: 3,
4187
- initialDelayMs: 100
4188
- };
4189
- const vectors = await BatchProcessor.mapWithRetry(
4190
- chunks.map((c) => c.content),
4191
- (text) => this.embeddingProvider.embed(text, { taskType: "document" }),
4192
- embedBatchOptions
4193
- );
4194
- if (vectors.length !== chunks.length) {
4195
- throw new Error(
4196
- `[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. Check embedding provider logs for individual chunk failures.`
4197
- );
4198
- }
4199
- return vectors;
4200
- }
4201
- /**
4202
- * Step 3: Upsert chunks to vector database with retry logic.
4203
- */
4204
- async processUpserts(upsertDocs, namespace) {
4205
- const upsertBatchOptions = {
4206
- batchSize: 100,
4207
- maxRetries: 3,
4208
- initialDelayMs: 100
4209
- };
4210
- const upsertResult = await BatchProcessor.processBatch(
4211
- upsertDocs,
4212
- (batch) => this.vectorDB.batchUpsert(batch, namespace),
4213
- upsertBatchOptions
4214
- );
4215
- if (upsertResult.errors.length > 0) {
4216
- console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed`);
4217
- }
4218
- return upsertResult.totalProcessed;
4219
- }
4220
- /**
4221
- * Step 4: Optional graph-based entity extraction and ingestion.
4222
- */
4223
- async processGraphIngestion(docId, chunks) {
4224
- console.log(`[Pipeline] Extracting entities for doc ${docId} (${chunks.length} chunks)...`);
4225
- const extractionOptions = {
4226
- batchSize: 2,
4227
- // Low concurrency for LLM extraction
4228
- maxRetries: 1,
4229
- initialDelayMs: 500
4230
- };
4231
- await BatchProcessor.processBatch(
4232
- chunks,
4233
- async (batch) => {
4234
- for (const chunk of batch) {
4235
- try {
4236
- const { nodes, edges } = await this.entityExtractor.extract(chunk.content);
4237
- if (nodes.length > 0) await this.graphDB.addNodes(nodes);
4238
- if (edges.length > 0) await this.graphDB.addEdges(edges);
4239
- } catch (err) {
4240
- console.warn(`[Pipeline] Entity extraction failed for chunk:`, err);
4241
- }
4242
- }
4243
- },
4244
- extractionOptions
4245
- );
4246
- }
4247
- async ask(question, history = [], namespace) {
4248
- var _a;
4249
- await this.initialize();
4250
- if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic" && this.agent) {
4251
- console.log("[Pipeline] \u{1F916} Executing in Agentic Mode...");
4252
- const agentReply = await this.agent.run(question, history);
4253
- return { reply: agentReply, sources: [] };
4254
- }
4255
- const stream = this.askStream(question, history, namespace);
4256
- let reply = "";
4257
- let sources = [];
4258
- let graphData;
4259
- try {
4260
- for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
4261
- const chunk = temp.value;
4262
- if (typeof chunk === "string") {
4263
- reply += chunk;
4264
- } else if ("sources" in chunk) {
4265
- sources = chunk.sources;
4266
- graphData = chunk.graphData;
4267
- }
4268
- }
4269
- } catch (temp) {
4270
- error = [temp];
4271
- } finally {
4272
- try {
4273
- more && (temp = iter.return) && await temp.call(iter);
4274
- } finally {
4275
- if (error)
4276
- throw error[0];
4277
- }
4278
- }
4279
- return { reply, sources, graphData };
4280
- }
4281
- /**
4282
- * High-performance streaming RAG flow.
4283
- * Yields text chunks first, then the retrieval metadata at the end.
4284
- */
4285
- askStream(_0) {
4286
- return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
4287
- var _a, _b, _c, _d, _e, _f, _g;
4288
- yield new __await(this.initialize());
4289
- const ns = namespace != null ? namespace : this.config.projectId;
4290
- const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
4291
- const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
4292
- try {
4293
- let searchQuery = question;
4294
- if ((_e = this.config.rag) == null ? void 0 : _e.useQueryTransformation) {
4295
- searchQuery = yield new __await(this.rewriteQuery(question, history));
4296
- }
4297
- const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
4298
- const filter = QueryProcessor.buildQueryFilter(question, hints);
4299
- console.log(`[Pipeline] \u{1F9E9} Extracted filters:`, JSON.stringify(filter));
4300
- const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
4301
- namespace: ns,
4302
- topK: topK * 2,
4303
- filter
4304
- }));
4305
- let sources = rawSources.filter((m) => m.score >= scoreThreshold);
4306
- if ((_g = this.config.rag) == null ? void 0 : _g.useReranking) {
4307
- sources = yield new __await(this.reranker.rerank(sources, question, topK));
4308
- } else {
4309
- sources = sources.slice(0, topK);
4310
- }
4311
- let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
4312
- ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
4313
- if (graphData && graphData.nodes.length > 0) {
4314
- const graphContext = graphData.nodes.map(
4315
- (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
4316
- ).join("\n");
4317
- context = `GRAPH KNOWLEDGE:
4318
- ${graphContext}
4319
-
4320
- VECTOR CONTEXT:
4321
- ${context}`;
4322
- }
4323
- const messages = [...history, { role: "user", content: question }];
4324
- if (this.llmProvider.chatStream) {
4325
- const stream = this.llmProvider.chatStream(messages, context);
4326
- if (!stream) {
4327
- throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
4328
- }
4329
- try {
4330
- for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
4331
- const chunk = temp.value;
4332
- yield chunk;
4333
- }
4334
- } catch (temp) {
4335
- error = [temp];
4336
- } finally {
4337
- try {
4338
- more && (temp = iter.return) && (yield new __await(temp.call(iter)));
4339
- } finally {
4340
- if (error)
4341
- throw error[0];
4342
- }
4343
- }
4344
- } else {
4345
- const reply = yield new __await(this.llmProvider.chat(messages, context));
4346
- yield reply;
4347
- }
4348
- yield { reply: "", sources, graphData };
4349
- } catch (error2) {
4350
- throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
4351
- }
4352
- });
4353
- }
4354
- /**
4355
- * Universal retrieval method combining all enabled providers.
4356
- * Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
4357
- */
4358
- async retrieve(query, options) {
4359
- var _a, _b, _c;
4360
- const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
4361
- const topK = (_b = options.topK) != null ? _b : 5;
4362
- const cacheKey = `${ns}::${query}`;
4363
- let queryVector = this.embeddingCache.get(cacheKey);
4364
- const [retrievedVector, graphData] = await Promise.all([
4365
- queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: "query" }),
4366
- this.graphDB && ((_c = this.config.rag) == null ? void 0 : _c.useGraphRetrieval) ? this.graphDB.query(query) : Promise.resolve(void 0)
4367
- ]);
4368
- if (!queryVector) {
4369
- this.embeddingCache.set(cacheKey, retrievedVector);
4370
- queryVector = retrievedVector;
4371
- }
4372
- const sources = await this.vectorDB.query(queryVector, topK, ns, options.filter);
4373
- return { sources, graphData };
4374
- }
4375
- /**
4376
- * Rewrite the user query for better retrieval performance.
4377
- */
4378
- async rewriteQuery(question, history) {
4379
- const prompt = `
4380
- Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
4381
- Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
4382
-
4383
- History:
4384
- ${history.map((m) => `${m.role}: ${m.content}`).join("\n")}
4385
-
4386
- New Question: ${question}
4387
-
4388
- Optimized Search Query:`;
4389
- const rewrite = await this.llmProvider.chat(
4390
- [
4391
- { role: "system", content: "You are an assistant that optimizes search queries for RAG systems." },
4392
- { role: "user", content: prompt }
4393
- ],
4394
- ""
4395
- );
4396
- return rewrite.trim() || question;
4397
- }
4398
- /**
4399
- * Generate 3-5 short, relevant questions based on the vector database content.
4400
- */
4401
- async getSuggestions(query, namespace) {
4402
- if (!query || query.trim().length < 3) {
4403
- return [];
4404
- }
4405
- await this.initialize();
4406
- const ns = namespace != null ? namespace : this.config.projectId;
4407
- try {
4408
- const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
4409
- if (sources.length === 0) {
4410
- return [];
4411
- }
4412
- const context = sources.map((s) => s.content).join("\n\n---\n\n");
4413
- const prompt = `
4414
- Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
4415
- Focus on questions that can be answered by the context.
4416
- Keep each question under 10 words and make them very specific to the content.
4417
- Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
4418
-
4419
- Context:
4420
- ${context}
4421
-
4422
- Suggestions:`;
4423
- const response = await this.llmProvider.chat(
4424
- [
4425
- { role: "system", content: "You are a helpful assistant that generates search suggestions." },
4426
- { role: "user", content: prompt }
4427
- ],
4428
- ""
4429
- );
4430
- const match = response.match(/\[[\s\S]*\]/);
4431
- if (match) {
4432
- const suggestions = JSON.parse(match[0]);
4433
- if (Array.isArray(suggestions)) {
4434
- return suggestions.map((s) => String(s)).slice(0, 3);
4435
- }
4436
- }
4437
- } catch (error) {
4438
- console.error("[Pipeline] Failed to generate suggestions:", error);
4439
- }
4440
- return [];
4441
- }
4442
- };
4443
-
4444
- // src/core/ProviderHealthCheck.ts
4445
- var ProviderHealthCheck = class {
4446
- /**
4447
- * Validates vector database configuration before initialization.
4448
- */
4449
- static async checkVectorProvider(config) {
4450
- const timestamp = Date.now();
4451
- const configErrors = await ConfigValidator.validate({
4452
- projectId: "health-check",
4453
- vectorDb: config,
4454
- llm: { provider: "openai", model: "gpt-4o", apiKey: "none" },
4455
- embedding: { provider: "openai", model: "text-embedding-3-small", apiKey: "none" }
4456
- });
4457
- const vectorDbErrors = configErrors.filter((e) => e.field.startsWith("vectorDb"));
4458
- if (vectorDbErrors.length > 0) {
4459
- return {
4460
- healthy: false,
4461
- provider: config.provider,
4462
- error: `Configuration validation failed: ${vectorDbErrors.map((e) => e.message).join("; ")}`,
4463
- timestamp
4464
- };
4465
- }
4466
- try {
4467
- const checker = await ProviderRegistry.getVectorHealthChecker(config.provider);
4468
- if (checker) {
4469
- return await checker.check(config);
4470
- }
4471
- return await this.fallbackVectorHealthCheck(config, timestamp);
4472
- } catch (error) {
4473
- return {
4474
- healthy: false,
4475
- provider: config.provider,
4476
- error: error instanceof Error ? error.message : String(error),
4477
- timestamp
4478
- };
4479
- }
4480
- }
4481
- static async fallbackVectorHealthCheck(config, timestamp) {
4482
- const opts = config.options || {};
4483
- const baseUrl = opts.baseUrl || opts.uri;
4484
- if (baseUrl && baseUrl.startsWith("http")) {
4485
- try {
4486
- const response = await fetch(`${baseUrl.replace(/\/$/, "")}/health`);
4487
- return {
4488
- healthy: response.ok,
4489
- provider: config.provider,
4490
- error: response.ok ? void 0 : `Health check failed: ${response.status}`,
4491
- timestamp
4492
- };
4493
- } catch (e) {
4494
- return { healthy: false, provider: config.provider, error: "Provider unreachable", timestamp };
4495
- }
4496
- }
4497
- return {
4498
- healthy: true,
4499
- provider: config.provider,
4500
- error: "Pluggable health check not implemented; basic reachability assumed.",
4501
- timestamp
4502
- };
4503
- }
4504
- /**
4505
- * Validates LLM provider configuration.
4506
- */
4507
- static async checkLLMProvider(config) {
4508
- const timestamp = Date.now();
4509
- try {
4510
- const checker = LLMFactory.getHealthChecker(config.provider);
4511
- if (checker) {
4512
- return await checker.check(config);
4513
- }
4514
- return {
4515
- healthy: true,
4516
- provider: config.provider,
4517
- error: "Pluggable health check not implemented; basic reachability assumed.",
4518
- timestamp
4519
- };
4520
- } catch (error) {
4521
- return {
4522
- healthy: false,
4523
- provider: config.provider,
4524
- error: error instanceof Error ? error.message : String(error),
4525
- timestamp
4526
- };
4527
- }
4528
- }
4529
- /**
4530
- * Runs comprehensive health checks on all configured providers in parallel.
4531
- */
4532
- static async checkAll(vectorDbConfig, llmConfig, embeddingConfig) {
4533
- const [vectorDb, llm, embedding] = await Promise.all([
4534
- this.checkVectorProvider(vectorDbConfig),
4535
- this.checkLLMProvider(llmConfig),
4536
- embeddingConfig ? this.checkLLMProvider(embeddingConfig) : Promise.resolve(void 0)
4537
- ]);
4538
- return {
4539
- vectorDb,
4540
- llm,
4541
- embedding,
4542
- allHealthy: vectorDb.healthy && llm.healthy && (!embedding || embedding.healthy)
4543
- };
4544
- }
4545
- };
4546
-
4547
- // src/core/VectorPlugin.ts
4548
- var VectorPlugin = class {
4549
- constructor(hostConfig) {
4550
- this.config = ConfigResolver.resolve(hostConfig);
4551
- this.validationPromise = ConfigValidator.validateAndThrow(this.config);
4552
- this.pipeline = new Pipeline(this.config);
4553
- }
4554
- /**
4555
- * Get the current resolved configuration.
4556
- */
4557
- getConfig() {
4558
- return this.config;
4559
- }
4560
- /**
4561
- * Perform pre-flight health checks on all configured providers.
4562
- * Useful to verify connectivity before running operations.
4563
- *
4564
- * @returns Health status for vector DB, LLM, and embedding providers
4565
- */
4566
- async checkHealth() {
4567
- return ProviderHealthCheck.checkAll(
4568
- this.config.vectorDb,
4438
+ },
4439
+ "chart": {
4440
+ "type": "pie",
4441
+ "xKey": "label",
4442
+ "yKeys": ["value"],
4443
+ "data": [
4444
+ { "label": "Beauty", "value": 15 },
4445
+ { "label": "Electronics", "value": 10 }
4446
+ ]
4447
+ },
4448
+ "insights": ["Beauty dominates inventory"]
4449
+ }
4450
+ \`\`\`
4451
+ `;
4452
+ if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
4453
+ let cleanPrompt = this.config.llm.systemPrompt || "";
4454
+ cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, "");
4455
+ cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, "");
4456
+ this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
4457
+ }
4458
+ console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
4459
+ this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
4460
+ const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
4569
4461
  this.config.llm,
4570
4462
  this.config.embedding
4571
4463
  );
4464
+ this.llmProvider = llmProvider;
4465
+ this.embeddingProvider = embeddingProvider;
4466
+ if (this.config.graphDb) {
4467
+ this.graphDB = await ProviderRegistry.createGraphProvider(this.config.graphDb);
4468
+ await this.graphDB.initialize();
4469
+ this.entityExtractor = new EntityExtractor(this.llmProvider);
4470
+ }
4471
+ await this.vectorDB.initialize();
4472
+ if (((_b = this.config.rag) == null ? void 0 : _b.architecture) === "agentic") {
4473
+ this.agent = new LangChainAgent(this, this.config);
4474
+ await this.agent.initialize(this.llmProvider);
4475
+ }
4476
+ this.initialised = true;
4572
4477
  }
4573
4478
  /**
4574
- * Run a chat query.
4575
- */
4576
- async chat(message, history = [], namespace) {
4577
- await this.validationPromise;
4578
- return this.pipeline.ask(message, history, namespace);
4579
- }
4580
- /**
4581
- * Run a streaming chat query.
4582
- */
4583
- chatStream(_0) {
4584
- return __asyncGenerator(this, arguments, function* (message, history = [], namespace) {
4585
- yield new __await(this.validationPromise);
4586
- yield* __yieldStar(this.pipeline.askStream(message, history, namespace));
4587
- });
4588
- }
4589
- /**
4590
- * Ingest documents into the vector database.
4479
+ * Ingest documents with automatic chunking, embedding, and batch upsert.
4480
+ * Handles retries for transient failures.
4591
4481
  */
4592
4482
  async ingest(documents, namespace) {
4593
- await this.validationPromise;
4594
- return this.pipeline.ingest(documents, namespace);
4595
- }
4596
- /**
4597
- * Get auto-suggestions based on a query prefix.
4598
- */
4599
- async getSuggestions(query, namespace) {
4600
- await this.validationPromise;
4601
- return this.pipeline.getSuggestions(query, namespace);
4602
- }
4603
- };
4604
-
4605
- // src/config/ConfigBuilder.ts
4606
- var ConfigBuilder = class {
4607
- /**
4608
- * Set the project/application ID for namespacing
4609
- */
4610
- projectId(id) {
4611
- this._projectId = id;
4612
- return this;
4613
- }
4614
- /**
4615
- * Configure the vector database provider
4616
- */
4617
- vectorDb(provider, options) {
4618
- var _a;
4619
- if (provider === "auto") {
4620
- this._vectorDb = this._autoDetectVectorDb();
4621
- } else {
4622
- this._vectorDb = {
4623
- provider,
4624
- indexName: (_a = options == null ? void 0 : options.indexName) != null ? _a : "default",
4625
- options: __spreadValues({}, options)
4626
- };
4483
+ await this.initialize();
4484
+ const ns = namespace != null ? namespace : this.config.projectId;
4485
+ const results = [];
4486
+ for (const doc of documents) {
4487
+ try {
4488
+ const chunks = await this.prepareChunks(doc);
4489
+ const vectors = await this.processEmbeddings(chunks);
4490
+ const upsertDocs = chunks.map((chunk, i) => ({
4491
+ id: chunk.id,
4492
+ vector: vectors[i],
4493
+ content: chunk.content,
4494
+ metadata: chunk.metadata
4495
+ }));
4496
+ const totalProcessed = await this.processUpserts(upsertDocs, ns);
4497
+ results.push({
4498
+ docId: doc.docId,
4499
+ chunksIngested: totalProcessed
4500
+ });
4501
+ if (this.graphDB && this.entityExtractor) {
4502
+ await this.processGraphIngestion(doc.docId, chunks);
4503
+ }
4504
+ } catch (error) {
4505
+ console.error(`[Pipeline] Failed to ingest document ${doc.docId}:`, error);
4506
+ results.push({ docId: doc.docId, chunksIngested: 0 });
4507
+ }
4627
4508
  }
4628
- return this;
4509
+ return results;
4629
4510
  }
4630
4511
  /**
4631
- * Configure the LLM provider for chat
4512
+ * Step 1: Chunk the document content.
4632
4513
  */
4633
- llm(provider, model, apiKey, options) {
4514
+ async prepareChunks(doc) {
4634
4515
  var _a, _b;
4635
- if (provider === "auto") {
4636
- this._llm = this._autoDetectLLM();
4637
- } else {
4638
- this._llm = {
4639
- provider,
4640
- model: model != null ? model : "default-model",
4641
- apiKey,
4642
- systemPrompt: options == null ? void 0 : options.systemPrompt,
4643
- maxTokens: (_a = options == null ? void 0 : options.maxTokens) != null ? _a : 1024,
4644
- temperature: (_b = options == null ? void 0 : options.temperature) != null ? _b : 0.7,
4645
- baseUrl: options == null ? void 0 : options.baseUrl,
4646
- options
4647
- };
4648
- }
4649
- return this;
4650
- }
4651
- /**
4652
- * Configure the embedding provider
4653
- */
4654
- embedding(provider, model, apiKey, options) {
4655
- if (provider === "auto") {
4656
- this._embedding = this._autoDetectEmbedding();
4657
- } else {
4658
- this._embedding = {
4659
- provider,
4660
- model: model != null ? model : "default-embedding",
4661
- apiKey,
4662
- baseUrl: options == null ? void 0 : options.baseUrl,
4663
- options
4664
- };
4516
+ if (this.llamaIngestor) {
4517
+ return await this.llamaIngestor.chunk(doc.content, {
4518
+ docId: doc.docId,
4519
+ metadata: doc.metadata,
4520
+ chunkSize: (_a = this.config.rag) == null ? void 0 : _a.chunkSize,
4521
+ chunkOverlap: (_b = this.config.rag) == null ? void 0 : _b.chunkOverlap
4522
+ });
4665
4523
  }
4666
- return this;
4667
- }
4668
- /**
4669
- * Set RAG-specific pipeline parameters
4670
- */
4671
- rag(options) {
4672
- var _a;
4673
- this._rag = __spreadValues(__spreadValues({}, (_a = this._rag) != null ? _a : {}), options);
4674
- return this;
4675
- }
4676
- /**
4677
- * Set UI branding and appearance options.
4678
- * Accepts the full UIConfig interface.
4679
- */
4680
- ui(options) {
4681
- this._ui = options;
4682
- return this;
4524
+ return this.chunker.chunk(doc.content, {
4525
+ docId: doc.docId,
4526
+ metadata: doc.metadata
4527
+ });
4683
4528
  }
4684
4529
  /**
4685
- * Build and return the final RagConfig.
4686
- * Throws if required fields (projectId, vectorDb, llm, embedding) are not set.
4530
+ * Step 2: Generate embeddings for chunks with retry logic.
4531
+ * Uses batchEmbed when available for efficiency; falls back to sequential embedding.
4687
4532
  */
4688
- build() {
4689
- var _a;
4690
- const missing = [];
4691
- if (!this._projectId) missing.push('projectId (call .projectId("my-app"))');
4692
- if (!this._vectorDb) missing.push('vectorDb (call .vectorDb("pinecone", { ... }))');
4693
- if (!this._llm) missing.push('llm (call .llm("openai", "gpt-4o", apiKey))');
4694
- if (!this._embedding) missing.push('embedding (call .embedding("openai", "text-embedding-3-small"))');
4695
- if (missing.length > 0) {
4533
+ async processEmbeddings(chunks) {
4534
+ const embedBatchOptions = {
4535
+ batchSize: 50,
4536
+ maxRetries: 3,
4537
+ initialDelayMs: 100
4538
+ };
4539
+ const vectors = await BatchProcessor.mapWithRetry(
4540
+ chunks.map((c) => c.content),
4541
+ (text) => this.embeddingProvider.embed(text, { taskType: "document" }),
4542
+ embedBatchOptions
4543
+ );
4544
+ if (vectors.length !== chunks.length) {
4696
4545
  throw new Error(
4697
- `[ConfigBuilder] Cannot build \u2014 required fields are missing:
4698
- ${missing.join("\n ")}`
4546
+ `[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. Check embedding provider logs for individual chunk failures.`
4699
4547
  );
4700
4548
  }
4701
- return __spreadValues({
4702
- projectId: this._projectId,
4703
- vectorDb: this._vectorDb,
4704
- llm: this._llm,
4705
- embedding: this._embedding,
4706
- ui: this._rag ? this._ui : void 0,
4707
- rag: (_a = this._rag) != null ? _a : { chunkSize: 1e3, chunkOverlap: 200, topK: 5 }
4708
- }, this._ui ? { ui: this._ui } : {});
4709
- }
4710
- /**
4711
- * Build and return as JSON for serialization
4712
- */
4713
- toJSON() {
4714
- return JSON.stringify(this.build());
4715
- }
4716
- // ============================================================================
4717
- // Private helper methods for auto-detection
4718
- // ============================================================================
4719
- _autoDetectVectorDb() {
4720
- if (process.env.PINECONE_API_KEY && process.env.PINECONE_INDEX) {
4721
- return { provider: "pinecone", indexName: process.env.PINECONE_INDEX, options: { apiKey: process.env.PINECONE_API_KEY } };
4722
- }
4723
- if (process.env.QDRANT_URL) {
4724
- return { provider: "qdrant", indexName: process.env.QDRANT_COLLECTION || "documents", options: { url: process.env.QDRANT_URL, apiKey: process.env.QDRANT_API_KEY } };
4725
- }
4726
- if (process.env.DATABASE_URL) {
4727
- return { provider: "postgresql", indexName: process.env.PG_TABLE || "documents", options: { connectionString: process.env.DATABASE_URL } };
4728
- }
4729
- if (process.env.MONGODB_URI) {
4730
- return { provider: "mongodb", indexName: process.env.MONGODB_COLLECTION || "documents", options: { uri: process.env.MONGODB_URI, database: process.env.MONGODB_DB || "ai_db", collection: process.env.MONGODB_COLLECTION || "documents" } };
4731
- }
4732
- if (process.env.REDIS_URL) {
4733
- return { provider: "redis", indexName: process.env.REDIS_INDEX || "documents", options: { url: process.env.REDIS_URL } };
4734
- }
4735
- return { provider: "qdrant", indexName: "documents", options: { url: process.env.QDRANT_URL || "http://localhost:6333" } };
4736
- }
4737
- _autoDetectLLM() {
4738
- if (process.env.OPENAI_API_KEY) {
4739
- return { provider: "openai", model: process.env.OPENAI_MODEL || "gpt-4o-mini", apiKey: process.env.OPENAI_API_KEY, maxTokens: parseInt(process.env.OPENAI_MAX_TOKENS || "1024"), temperature: parseFloat(process.env.OPENAI_TEMPERATURE || "0.7") };
4740
- }
4741
- if (process.env.ANTHROPIC_API_KEY) {
4742
- return { provider: "anthropic", model: process.env.ANTHROPIC_MODEL || "claude-3-haiku-20240307", apiKey: process.env.ANTHROPIC_API_KEY, maxTokens: parseInt(process.env.ANTHROPIC_MAX_TOKENS || "1024"), temperature: parseFloat(process.env.ANTHROPIC_TEMPERATURE || "0.7") };
4743
- }
4744
- if (process.env.OLLAMA_BASE_URL) {
4745
- return { provider: "ollama", model: process.env.OLLAMA_MODEL || "mistral", baseUrl: process.env.OLLAMA_BASE_URL, maxTokens: parseInt(process.env.OLLAMA_MAX_TOKENS || "1024"), temperature: parseFloat(process.env.OLLAMA_TEMPERATURE || "0.7") };
4746
- }
4747
- return { provider: "openai", model: "gpt-4o-mini", apiKey: process.env.OPENAI_API_KEY };
4549
+ return vectors;
4748
4550
  }
4749
- _autoDetectEmbedding() {
4750
- if (process.env.EMBEDDING_PROVIDER) {
4751
- return { provider: process.env.EMBEDDING_PROVIDER, model: process.env.EMBEDDING_MODEL || "default", apiKey: process.env.EMBEDDING_API_KEY, baseUrl: process.env.EMBEDDING_BASE_URL };
4551
+ /**
4552
+ * Step 3: Upsert chunks to vector database with retry logic.
4553
+ */
4554
+ async processUpserts(upsertDocs, namespace) {
4555
+ const upsertBatchOptions = {
4556
+ batchSize: 100,
4557
+ maxRetries: 3,
4558
+ initialDelayMs: 100
4559
+ };
4560
+ const upsertResult = await BatchProcessor.processBatch(
4561
+ upsertDocs,
4562
+ (batch) => this.vectorDB.batchUpsert(batch, namespace),
4563
+ upsertBatchOptions
4564
+ );
4565
+ if (upsertResult.errors.length > 0) {
4566
+ console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed`);
4752
4567
  }
4753
- return { provider: "openai", model: "text-embedding-3-small", apiKey: process.env.OPENAI_API_KEY };
4568
+ return upsertResult.totalProcessed;
4754
4569
  }
4755
- };
4756
- var PRESETS = {
4757
- /** OpenAI + Pinecone: Production-ready cloud setup */
4758
- "openai-pinecone": { vectorDb: "pinecone", llm: "openai", embedding: "openai" },
4759
- /** Claude + Qdrant: Open-source vector DB + proprietary LLM */
4760
- "claude-qdrant": { vectorDb: "qdrant", llm: "anthropic", embedding: "openai" },
4761
- /** Local development: Ollama + local Qdrant */
4762
- "local-dev": { vectorDb: "qdrant", llm: "ollama", embedding: "ollama" },
4763
- /** Fully open-source: Ollama LLM + Qdrant + Ollama embeddings */
4764
- "fully-open-source": { vectorDb: "qdrant", llm: "ollama", embedding: "ollama" },
4765
- /** PostgreSQL stack: pgvector + OpenAI */
4766
- "postgres-openai": { vectorDb: "postgresql", llm: "openai", embedding: "openai" },
4767
- /** Enterprise MongoDB: MongoDB Atlas with OpenAI */
4768
- "mongodb-openai": { vectorDb: "mongodb", llm: "openai", embedding: "openai" },
4769
- /** Redis stack for caching + search */
4770
- "redis-openai": { vectorDb: "redis", llm: "openai", embedding: "openai" }
4771
- };
4772
- function createFromPreset(presetName) {
4773
- const preset = PRESETS[presetName];
4774
- return new ConfigBuilder().vectorDb(preset.vectorDb, { apiKey: process.env[`${preset.vectorDb.toUpperCase()}_API_KEY`] }).llm(preset.llm, void 0, process.env[`${preset.llm.toUpperCase()}_API_KEY`]).embedding(preset.embedding, void 0, process.env[`${preset.embedding.toUpperCase()}_API_KEY`]);
4775
- }
4776
-
4777
- // src/utils/DocumentParser.ts
4778
- var DocumentParser = class {
4779
4570
  /**
4780
- * Extract text from a File or Buffer based on its type.
4571
+ * Step 4: Optional graph-based entity extraction and ingestion.
4781
4572
  */
4782
- static async parse(file, fileName, mimeType) {
4573
+ async processGraphIngestion(docId, chunks) {
4574
+ console.log(`[Pipeline] Extracting entities for doc ${docId} (${chunks.length} chunks)...`);
4575
+ const extractionOptions = {
4576
+ batchSize: 2,
4577
+ // Low concurrency for LLM extraction
4578
+ maxRetries: 1,
4579
+ initialDelayMs: 500
4580
+ };
4581
+ await BatchProcessor.processBatch(
4582
+ chunks,
4583
+ async (batch) => {
4584
+ for (const chunk of batch) {
4585
+ try {
4586
+ const { nodes, edges } = await this.entityExtractor.extract(chunk.content);
4587
+ if (nodes.length > 0) await this.graphDB.addNodes(nodes);
4588
+ if (edges.length > 0) await this.graphDB.addEdges(edges);
4589
+ } catch (err) {
4590
+ console.warn(`[Pipeline] Entity extraction failed for chunk:`, err);
4591
+ }
4592
+ }
4593
+ },
4594
+ extractionOptions
4595
+ );
4596
+ }
4597
+ async ask(question, history = [], namespace) {
4783
4598
  var _a;
4784
- const extension = (_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase();
4785
- if (extension === "txt" || extension === "md" || mimeType === "text/plain" || mimeType === "text/markdown") {
4786
- return this.readAsText(file);
4787
- }
4788
- if (extension === "json" || mimeType === "application/json") {
4789
- const text = await this.readAsText(file);
4790
- try {
4791
- const obj = JSON.parse(text);
4792
- return JSON.stringify(obj, null, 2);
4793
- } catch (e) {
4794
- return text;
4795
- }
4796
- }
4797
- if (extension === "csv" || mimeType === "text/csv") {
4798
- return this.readAsText(file);
4599
+ await this.initialize();
4600
+ if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic" && this.agent) {
4601
+ console.log("[Pipeline] \u{1F916} Executing in Agentic Mode...");
4602
+ const agentReply = await this.agent.run(question, history);
4603
+ return { reply: agentReply, sources: [] };
4799
4604
  }
4800
- if (extension === "pdf" || mimeType === "application/pdf") {
4801
- try {
4802
- const pdf = await import("pdf-parse");
4803
- const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
4804
- const data = await pdf.default(buffer);
4805
- return data.text;
4806
- } catch (err) {
4807
- console.warn('[DocumentParser] PDF parsing failed. Make sure "pdf-parse" is installed.', err);
4808
- return `[PDF Parsing Error for ${fileName}]`;
4605
+ const stream = this.askStream(question, history, namespace);
4606
+ let reply = "";
4607
+ let sources = [];
4608
+ let graphData;
4609
+ try {
4610
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
4611
+ const chunk = temp.value;
4612
+ if (typeof chunk === "string") {
4613
+ reply += chunk;
4614
+ } else if ("sources" in chunk) {
4615
+ sources = chunk.sources;
4616
+ graphData = chunk.graphData;
4617
+ }
4809
4618
  }
4810
- }
4811
- if (extension === "docx" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
4619
+ } catch (temp) {
4620
+ error = [temp];
4621
+ } finally {
4812
4622
  try {
4813
- const mammoth = await import("mammoth");
4814
- const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
4815
- const result = await mammoth.extractRawText({ buffer });
4816
- return result.value;
4817
- } catch (err) {
4818
- console.warn('[DocumentParser] DOCX parsing failed. Make sure "mammoth" is installed.', err);
4819
- return `[DOCX Parsing Error for ${fileName}]`;
4623
+ more && (temp = iter.return) && await temp.call(iter);
4624
+ } finally {
4625
+ if (error)
4626
+ throw error[0];
4820
4627
  }
4821
4628
  }
4822
- try {
4823
- return await this.readAsText(file);
4824
- } catch (e) {
4825
- throw new Error(`[DocumentParser] Unsupported file format: ${fileName} (${mimeType})`);
4826
- }
4827
- }
4828
- static async readAsText(file) {
4829
- if (Buffer.isBuffer(file)) {
4830
- return file.toString("utf-8");
4831
- }
4832
- return await file.text();
4629
+ return { reply, sources, graphData };
4833
4630
  }
4834
- };
4835
-
4836
- // src/server.ts
4837
- init_BaseVectorProvider();
4838
- init_PineconeProvider();
4839
- init_PostgreSQLProvider();
4631
+ /**
4632
+ * High-performance streaming RAG flow.
4633
+ * Yields text chunks first, then the retrieval metadata at the end.
4634
+ */
4635
+ askStream(_0) {
4636
+ return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
4637
+ var _a, _b, _c, _d, _e, _f, _g;
4638
+ yield new __await(this.initialize());
4639
+ const ns = namespace != null ? namespace : this.config.projectId;
4640
+ const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
4641
+ const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
4642
+ try {
4643
+ let searchQuery = question;
4644
+ if ((_e = this.config.rag) == null ? void 0 : _e.useQueryTransformation) {
4645
+ searchQuery = yield new __await(this.rewriteQuery(question, history));
4646
+ }
4647
+ const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
4648
+ const filter = QueryProcessor.buildQueryFilter(question, hints);
4649
+ console.log(`[Pipeline] \u{1F9E9} Extracted filters:`, JSON.stringify(filter));
4650
+ const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
4651
+ namespace: ns,
4652
+ topK: topK * 2,
4653
+ filter
4654
+ }));
4655
+ let sources = rawSources.filter((m) => m.score >= scoreThreshold);
4656
+ if ((_g = this.config.rag) == null ? void 0 : _g.useReranking) {
4657
+ sources = yield new __await(this.reranker.rerank(sources, question, topK));
4658
+ } else {
4659
+ sources = sources.slice(0, topK);
4660
+ }
4661
+ let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
4662
+ ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
4663
+ if (graphData && graphData.nodes.length > 0) {
4664
+ const graphContext = graphData.nodes.map(
4665
+ (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
4666
+ ).join("\n");
4667
+ context = `GRAPH KNOWLEDGE:
4668
+ ${graphContext}
4840
4669
 
4841
- // src/providers/vectordb/MultiTablePostgresProvider.ts
4842
- var import_pg2 = require("pg");
4843
- init_BaseVectorProvider();
4844
- var MultiTablePostgresProvider = class extends BaseVectorProvider {
4845
- constructor(config) {
4846
- var _a, _b, _c, _d, _e;
4847
- super(config);
4848
- const opts = config.options || {};
4849
- if (!opts.connectionString) {
4850
- throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
4851
- }
4852
- this.connectionString = opts.connectionString;
4853
- this.dimensions = (_a = opts.dimensions) != null ? _a : 768;
4854
- const rawTables = (_c = (_b = opts.tables) != null ? _b : process.env.VECTOR_DB_TABLES) != null ? _c : "";
4855
- this.tables = typeof rawTables === "string" ? rawTables.split(",").map((t) => t.trim()).filter(Boolean) : rawTables;
4856
- if (this.tables.length === 0) {
4857
- console.warn(
4858
- "[MultiTablePostgresProvider] No tables configured. Set VECTOR_DB_TABLES as a comma-separated list of table names or pass options.tables."
4859
- );
4860
- }
4861
- const rawSearchFields = (_e = (_d = opts.searchFields) != null ? _d : process.env.VECTOR_DB_SEARCH_FIELDS) != null ? _e : ["name", "product_name", "productname", "title"];
4862
- this.searchFields = typeof rawSearchFields === "string" ? rawSearchFields.split(",").map((f) => f.trim()).filter(Boolean) : rawSearchFields;
4863
- }
4864
- async initialize() {
4865
- this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
4866
- const client = await this.pool.connect();
4867
- try {
4868
- const res = await client.query(`
4869
- SELECT table_name
4870
- FROM information_schema.columns
4871
- WHERE column_name = 'embedding'
4872
- AND table_schema = 'public'
4873
- `);
4874
- const discoveredTables = res.rows.map((r) => r.table_name);
4875
- if (this.tables.length === 0) {
4876
- this.tables = discoveredTables;
4877
- } else {
4878
- const staticTables = [...this.tables];
4879
- this.tables = staticTables.filter((t) => discoveredTables.includes(t));
4880
- if (this.tables.length < staticTables.length) {
4881
- const missing = staticTables.filter((t) => !discoveredTables.includes(t));
4882
- console.warn(`[MultiTablePostgresProvider] Some configured tables were skipped (missing 'embedding' column): ${missing.join(", ")}`);
4670
+ VECTOR CONTEXT:
4671
+ ${context}`;
4883
4672
  }
4673
+ const messages = [...history, { role: "user", content: question }];
4674
+ if (this.llmProvider.chatStream) {
4675
+ const stream = this.llmProvider.chatStream(messages, context);
4676
+ if (!stream) {
4677
+ throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
4678
+ }
4679
+ try {
4680
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
4681
+ const chunk = temp.value;
4682
+ yield chunk;
4683
+ }
4684
+ } catch (temp) {
4685
+ error = [temp];
4686
+ } finally {
4687
+ try {
4688
+ more && (temp = iter.return) && (yield new __await(temp.call(iter)));
4689
+ } finally {
4690
+ if (error)
4691
+ throw error[0];
4692
+ }
4693
+ }
4694
+ } else {
4695
+ const reply = yield new __await(this.llmProvider.chat(messages, context));
4696
+ yield reply;
4697
+ }
4698
+ const uiTransformation = UITransformer.transform(question, sources);
4699
+ yield {
4700
+ reply: "",
4701
+ sources,
4702
+ graphData,
4703
+ ui_transformation: uiTransformation
4704
+ };
4705
+ } catch (error2) {
4706
+ throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
4884
4707
  }
4885
- if (this.tables.length === 0) {
4886
- console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
4887
- } else {
4888
- console.log(
4889
- `[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
4890
- );
4891
- }
4892
- } finally {
4893
- client.release();
4894
- }
4708
+ });
4895
4709
  }
4896
4710
  /**
4897
- * Upsert is not supported for MultiTablePostgresProvider as it's designed for
4898
- * searching across pre-existing tables with varying schemas.
4711
+ * Universal retrieval method combining all enabled providers.
4712
+ * Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
4899
4713
  */
4900
- async upsert(_doc, _namespace) {
4901
- throw new Error(
4902
- "[MultiTablePostgresProvider] upsert() is not supported in multi-table mode. Please use the standard PostgreSQLProvider for single-table managed indices."
4903
- );
4714
+ async retrieve(query, options) {
4715
+ var _a, _b, _c;
4716
+ const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
4717
+ const topK = (_b = options.topK) != null ? _b : 5;
4718
+ const cacheKey = `${ns}::${query}`;
4719
+ let queryVector = this.embeddingCache.get(cacheKey);
4720
+ const [retrievedVector, graphData] = await Promise.all([
4721
+ queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: "query" }),
4722
+ this.graphDB && ((_c = this.config.rag) == null ? void 0 : _c.useGraphRetrieval) ? this.graphDB.query(query) : Promise.resolve(void 0)
4723
+ ]);
4724
+ if (!queryVector) {
4725
+ this.embeddingCache.set(cacheKey, retrievedVector);
4726
+ queryVector = retrievedVector;
4727
+ }
4728
+ const sources = await this.vectorDB.query(queryVector, topK, ns, options.filter);
4729
+ return { sources, graphData };
4904
4730
  }
4905
4731
  /**
4906
- * Batch upsert is not supported for MultiTablePostgresProvider.
4732
+ * Rewrite the user query for better retrieval performance.
4907
4733
  */
4908
- async batchUpsert(_docs, _namespace) {
4909
- throw new Error(
4910
- "[MultiTablePostgresProvider] batchUpsert() is not supported in multi-table mode."
4734
+ async rewriteQuery(question, history) {
4735
+ const prompt = `
4736
+ Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
4737
+ Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
4738
+
4739
+ History:
4740
+ ${history.map((m) => `${m.role}: ${m.content}`).join("\n")}
4741
+
4742
+ New Question: ${question}
4743
+
4744
+ Optimized Search Query:`;
4745
+ const rewrite = await this.llmProvider.chat(
4746
+ [
4747
+ { role: "system", content: "You are an assistant that optimizes search queries for RAG systems." },
4748
+ { role: "user", content: prompt }
4749
+ ],
4750
+ ""
4911
4751
  );
4752
+ return rewrite.trim() || question;
4912
4753
  }
4913
4754
  /**
4914
- * Query all configured tables and merge results, sorted by cosine similarity score.
4755
+ * Generate 3-5 short, relevant questions based on the vector database content.
4915
4756
  */
4916
- async query(vector, topK, _namespace, _filter) {
4917
- var _a, _b, _c;
4918
- if (!this.pool) {
4919
- throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
4757
+ async getSuggestions(query, namespace) {
4758
+ if (!query || query.trim().length < 3) {
4759
+ return [];
4920
4760
  }
4921
- const vectorLiteral = `[${vector.join(",")}]`;
4922
- const allResults = [];
4923
- console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
4924
- const queryText = _filter == null ? void 0 : _filter.queryText;
4925
- const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter(
4926
- (hint) => typeof hint === "object" && hint !== null && typeof hint.value === "string"
4927
- ).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
4928
- console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
4929
- console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
4930
- const cleanKeywordQuery = (text) => {
4931
- return text.toLowerCase().replace(/\b(show|me|the|product|cards|of|category|find|list|browse|what|are|display)\b/g, "").trim().replace(/\s+/g, " & ");
4932
- };
4933
- const queryPromises = this.tables.map(async (table) => {
4934
- try {
4935
- let sqlQuery = "";
4936
- let params = [];
4937
- if (queryText) {
4938
- const cleanedText = cleanKeywordQuery(queryText) || queryText;
4939
- const hasEntityHints = entityHints.length > 0;
4940
- const exactNameScoreExpr = hasEntityHints ? `+ (
4941
- SELECT COALESCE(MAX(
4942
- CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
4943
- THEN 1.0 ELSE 0.0 END
4944
- ), 0)
4945
- FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
4946
- WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
4947
- ) * 3.0` : "";
4948
- sqlQuery = `
4949
- SELECT *,
4950
- (1 - (embedding <=> $1::vector)) AS vector_score,
4951
- COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
4952
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
4953
- FROM "${table}" t
4954
- ORDER BY hybrid_score DESC
4955
- LIMIT 50
4956
- `;
4957
- params = [vectorLiteral, cleanedText];
4958
- } else {
4959
- sqlQuery = `
4960
- SELECT *,
4961
- (1 - (embedding <=> $1::vector)) AS hybrid_score
4962
- FROM "${table}" t
4963
- ORDER BY hybrid_score DESC
4964
- LIMIT 50
4965
- `;
4966
- params = [vectorLiteral];
4967
- }
4968
- const result = await this.pool.query(sqlQuery, params);
4969
- if (result.rowCount && result.rowCount > 0) {
4970
- console.log(` \u2705 Table "${table}": Found ${result.rowCount} potential matches. Top score: ${result.rows[0].hybrid_score.toFixed(4)}`);
4971
- } else {
4972
- console.log(` \u26AA Table "${table}": No relevant matches found.`);
4973
- }
4974
- const tableResults = [];
4975
- for (const row of result.rows) {
4976
- const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
4977
- delete rest.embedding;
4978
- delete rest.vector_score;
4979
- delete rest.keyword_score;
4980
- delete rest.exact_name_score;
4981
- const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
4982
- ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
4983
- tableResults.push({
4984
- id: `${table}-${id}`,
4985
- score: parseFloat(String(hybrid_score)),
4986
- content,
4987
- metadata: __spreadProps(__spreadValues({}, rest), { source_table: table })
4988
- });
4989
- }
4990
- return tableResults;
4991
- } catch (err) {
4992
- console.error(
4993
- `[MultiTablePostgresProvider] Error querying table "${table}":`,
4994
- err.message
4995
- );
4761
+ await this.initialize();
4762
+ const ns = namespace != null ? namespace : this.config.projectId;
4763
+ try {
4764
+ const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
4765
+ if (sources.length === 0) {
4996
4766
  return [];
4997
4767
  }
4998
- });
4999
- const resultsArray = await Promise.all(queryPromises);
5000
- for (const tableResults of resultsArray) {
5001
- allResults.push(...tableResults);
5002
- }
5003
- const resultsByTable = {};
5004
- for (const res of allResults) {
5005
- const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
5006
- if (!resultsByTable[table]) resultsByTable[table] = [];
5007
- resultsByTable[table].push(res);
4768
+ const context = sources.map((s) => s.content).join("\n\n---\n\n");
4769
+ const prompt = `
4770
+ Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
4771
+ Focus on questions that can be answered by the context.
4772
+ Keep each question under 10 words and make them very specific to the content.
4773
+ Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
4774
+
4775
+ Context:
4776
+ ${context}
4777
+
4778
+ Suggestions:`;
4779
+ const response = await this.llmProvider.chat(
4780
+ [
4781
+ { role: "system", content: "You are a helpful assistant that generates search suggestions." },
4782
+ { role: "user", content: prompt }
4783
+ ],
4784
+ ""
4785
+ );
4786
+ const match = response.match(/\[[\s\S]*\]/);
4787
+ if (match) {
4788
+ const suggestions = JSON.parse(match[0]);
4789
+ if (Array.isArray(suggestions)) {
4790
+ return suggestions.map((s) => String(s)).slice(0, 3);
4791
+ }
4792
+ }
4793
+ } catch (error) {
4794
+ console.error("[Pipeline] Failed to generate suggestions:", error);
5008
4795
  }
5009
- const balancedResults = [];
5010
- const tables = Object.keys(resultsByTable);
5011
- for (const table of tables) {
5012
- balancedResults.push(...resultsByTable[table].slice(0, 3));
4796
+ return [];
4797
+ }
4798
+ };
4799
+
4800
+ // src/core/ProviderHealthCheck.ts
4801
+ var ProviderHealthCheck = class {
4802
+ /**
4803
+ * Validates vector database configuration before initialization.
4804
+ */
4805
+ static async checkVectorProvider(config) {
4806
+ const timestamp = Date.now();
4807
+ const configErrors = await ConfigValidator.validate({
4808
+ projectId: "health-check",
4809
+ vectorDb: config,
4810
+ llm: { provider: "openai", model: "gpt-4o", apiKey: "none" },
4811
+ embedding: { provider: "openai", model: "text-embedding-3-small", apiKey: "none" }
4812
+ });
4813
+ const vectorDbErrors = configErrors.filter((e) => e.field.startsWith("vectorDb"));
4814
+ if (vectorDbErrors.length > 0) {
4815
+ return {
4816
+ healthy: false,
4817
+ provider: config.provider,
4818
+ error: `Configuration validation failed: ${vectorDbErrors.map((e) => e.message).join("; ")}`,
4819
+ timestamp
4820
+ };
5013
4821
  }
5014
- const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
5015
- if (finalSorted.length > 0) {
5016
- console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
5017
- console.log(`[MultiTablePostgresProvider] Final top match from "${(_c = (_b = finalSorted[0].metadata) == null ? void 0 : _b.source_table) != null ? _c : "unknown"}" with score ${finalSorted[0].score.toFixed(4)}`);
4822
+ try {
4823
+ const checker = await ProviderRegistry.getVectorHealthChecker(config.provider);
4824
+ if (checker) {
4825
+ return await checker.check(config);
4826
+ }
4827
+ return await this.fallbackVectorHealthCheck(config, timestamp);
4828
+ } catch (error) {
4829
+ return {
4830
+ healthy: false,
4831
+ provider: config.provider,
4832
+ error: error instanceof Error ? error.message : String(error),
4833
+ timestamp
4834
+ };
5018
4835
  }
5019
- return finalSorted.slice(0, Math.max(topK, 15));
5020
- }
5021
- async delete(_id, _namespace) {
5022
- console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
5023
4836
  }
5024
- async deleteNamespace(_namespace) {
5025
- console.warn("[MultiTablePostgresProvider] deleteNamespace() is a no-op for multi-table mode.");
4837
+ static async fallbackVectorHealthCheck(config, timestamp) {
4838
+ const opts = config.options || {};
4839
+ const baseUrl = opts.baseUrl || opts.uri;
4840
+ if (baseUrl && baseUrl.startsWith("http")) {
4841
+ try {
4842
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/health`);
4843
+ return {
4844
+ healthy: response.ok,
4845
+ provider: config.provider,
4846
+ error: response.ok ? void 0 : `Health check failed: ${response.status}`,
4847
+ timestamp
4848
+ };
4849
+ } catch (e) {
4850
+ return { healthy: false, provider: config.provider, error: "Provider unreachable", timestamp };
4851
+ }
4852
+ }
4853
+ return {
4854
+ healthy: true,
4855
+ provider: config.provider,
4856
+ error: "Pluggable health check not implemented; basic reachability assumed.",
4857
+ timestamp
4858
+ };
5026
4859
  }
5027
- async ping() {
4860
+ /**
4861
+ * Validates LLM provider configuration.
4862
+ */
4863
+ static async checkLLMProvider(config) {
4864
+ const timestamp = Date.now();
5028
4865
  try {
5029
- await this.pool.query("SELECT 1");
5030
- return true;
5031
- } catch (e) {
5032
- return false;
4866
+ const checker = LLMFactory.getHealthChecker(config.provider);
4867
+ if (checker) {
4868
+ return await checker.check(config);
4869
+ }
4870
+ return {
4871
+ healthy: true,
4872
+ provider: config.provider,
4873
+ error: "Pluggable health check not implemented; basic reachability assumed.",
4874
+ timestamp
4875
+ };
4876
+ } catch (error) {
4877
+ return {
4878
+ healthy: false,
4879
+ provider: config.provider,
4880
+ error: error instanceof Error ? error.message : String(error),
4881
+ timestamp
4882
+ };
5033
4883
  }
5034
4884
  }
5035
- async disconnect() {
5036
- if (this.pool) {
5037
- await this.pool.end();
5038
- }
4885
+ /**
4886
+ * Runs comprehensive health checks on all configured providers in parallel.
4887
+ */
4888
+ static async checkAll(vectorDbConfig, llmConfig, embeddingConfig) {
4889
+ const [vectorDb, llm, embedding] = await Promise.all([
4890
+ this.checkVectorProvider(vectorDbConfig),
4891
+ this.checkLLMProvider(llmConfig),
4892
+ embeddingConfig ? this.checkLLMProvider(embeddingConfig) : Promise.resolve(void 0)
4893
+ ]);
4894
+ return {
4895
+ vectorDb,
4896
+ llm,
4897
+ embedding,
4898
+ allHealthy: vectorDb.healthy && llm.healthy && (!embedding || embedding.healthy)
4899
+ };
5039
4900
  }
5040
4901
  };
5041
4902
 
5042
- // src/server.ts
5043
- init_MongoDBProvider();
5044
- init_MilvusProvider();
5045
- init_QdrantProvider();
5046
- init_ChromaDBProvider();
5047
- init_RedisProvider();
5048
- init_WeaviateProvider();
5049
- init_UniversalVectorProvider();
5050
-
5051
- // src/handlers/index.ts
5052
- var import_server = require("next/server");
5053
-
5054
- // src/utils/UITransformer.ts
5055
- var UITransformer = class {
4903
+ // src/core/VectorPlugin.ts
4904
+ var VectorPlugin = class {
4905
+ constructor(hostConfig) {
4906
+ this.config = ConfigResolver.resolve(hostConfig);
4907
+ this.validationPromise = ConfigValidator.validateAndThrow(this.config);
4908
+ this.pipeline = new Pipeline(this.config);
4909
+ }
5056
4910
  /**
5057
- * Main transformation method
5058
- * Analyzes user query and retrieved data to determine the best visualization format
5059
- *
5060
- * @param userQuery - The original user question/query
5061
- * @param retrievedData - Vector database retrieval results
5062
- * @returns Structured JSON response for UI rendering
4911
+ * Get the current resolved configuration.
5063
4912
  */
5064
- static transform(userQuery, retrievedData) {
5065
- if (!retrievedData || retrievedData.length === 0) {
5066
- return this.createTextResponse("No data available", "No relevant data found for your query.");
5067
- }
5068
- const analysis = this.analyzeData(userQuery, retrievedData);
5069
- switch (analysis.suggestedType) {
5070
- case "product_carousel":
5071
- return this.transformToProductCarousel(retrievedData);
5072
- case "pie_chart":
5073
- return this.transformToPieChart(retrievedData);
5074
- case "bar_chart":
5075
- return this.transformToBarChart(retrievedData);
5076
- case "line_chart":
5077
- return this.transformToLineChart(retrievedData);
5078
- case "table":
5079
- return this.transformToTable(retrievedData);
5080
- default:
5081
- return this.transformToText(retrievedData);
5082
- }
4913
+ getConfig() {
4914
+ return this.config;
5083
4915
  }
5084
4916
  /**
5085
- * Analyzes the data and query to suggest the best visualization type
4917
+ * Perform pre-flight health checks on all configured providers.
4918
+ * Useful to verify connectivity before running operations.
4919
+ *
4920
+ * @returns Health status for vector DB, LLM, and embedding providers
5086
4921
  */
5087
- static analyzeData(query, data) {
5088
- const queryLower = query.toLowerCase();
5089
- const hasProducts = data.some(
5090
- (item) => this.isProductData(item)
5091
- );
5092
- const hasTimeSeries = data.some(
5093
- (item) => this.isTimeSeriesData(item)
4922
+ async checkHealth() {
4923
+ return ProviderHealthCheck.checkAll(
4924
+ this.config.vectorDb,
4925
+ this.config.llm,
4926
+ this.config.embedding
5094
4927
  );
5095
- const hasCategories = this.detectCategories(data).length > 0;
5096
- const isDistributionQuery = queryLower.includes("distribution") || queryLower.includes("breakdown") || queryLower.includes("percentage") || queryLower.includes("proportion");
5097
- const isComparisonQuery = queryLower.includes("compare") || queryLower.includes("vs") || queryLower.includes("difference");
5098
- const isTrendQuery = queryLower.includes("trend") || queryLower.includes("over time") || queryLower.includes("growth") || queryLower.includes("decline");
5099
- if (hasProducts && !hasTimeSeries && !isDistributionQuery) {
5100
- return { suggestedType: "product_carousel", confidence: 0.95, hasProducts, hasTimeSeries, hasCategories };
5101
- }
5102
- if (isTrendQuery && hasTimeSeries) {
5103
- return { suggestedType: "line_chart", confidence: 0.9, hasProducts, hasTimeSeries, hasCategories };
5104
- }
5105
- if (isDistributionQuery && hasCategories) {
5106
- return { suggestedType: "pie_chart", confidence: 0.85, hasProducts, hasTimeSeries, hasCategories };
5107
- }
5108
- if (isComparisonQuery && hasCategories && data.length > 2) {
5109
- return { suggestedType: "bar_chart", confidence: 0.8, hasProducts, hasTimeSeries, hasCategories };
5110
- }
5111
- if (data.length > 5 && this.hasMultipleFields(data)) {
5112
- return { suggestedType: "table", confidence: 0.75, hasProducts, hasTimeSeries, hasCategories };
5113
- }
5114
- return { suggestedType: "text", confidence: 0.5, hasProducts, hasTimeSeries, hasCategories };
5115
4928
  }
5116
4929
  /**
5117
- * Transform data to product carousel format
4930
+ * Run a chat query.
5118
4931
  */
5119
- static transformToProductCarousel(data) {
5120
- const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item)).filter((p) => p !== null);
5121
- return {
5122
- type: "product_carousel",
5123
- title: "Recommended Products",
5124
- description: `Found ${products.length} relevant products`,
5125
- data: products
5126
- };
4932
+ async chat(message, history = [], namespace) {
4933
+ await this.validationPromise;
4934
+ return this.pipeline.ask(message, history, namespace);
5127
4935
  }
5128
4936
  /**
5129
- * Transform data to pie chart format
4937
+ * Run a streaming chat query.
5130
4938
  */
5131
- static transformToPieChart(data) {
5132
- const categories = this.detectCategories(data);
5133
- const categoryData = this.aggregateByCategory(data, categories);
5134
- const pieData = Object.entries(categoryData).map(([label, count]) => ({
5135
- label,
5136
- value: count,
5137
- inStock: this.checkStockStatus(label, data)
5138
- }));
5139
- return {
5140
- type: "pie_chart",
5141
- title: "Distribution by Category",
5142
- description: `Showing breakdown across ${categories.length} categories`,
5143
- data: pieData
5144
- };
4939
+ chatStream(_0) {
4940
+ return __asyncGenerator(this, arguments, function* (message, history = [], namespace) {
4941
+ yield new __await(this.validationPromise);
4942
+ yield* __yieldStar(this.pipeline.askStream(message, history, namespace));
4943
+ });
5145
4944
  }
5146
4945
  /**
5147
- * Transform data to bar chart format
4946
+ * Ingest documents into the vector database.
5148
4947
  */
5149
- static transformToBarChart(data) {
5150
- const categories = this.detectCategories(data);
5151
- const categoryData = this.aggregateByCategory(data, categories);
5152
- const barData = Object.entries(categoryData).map(([category, value]) => ({
5153
- category,
5154
- value,
5155
- inStock: this.checkStockStatus(category, data)
5156
- }));
5157
- return {
5158
- type: "bar_chart",
5159
- title: "Comparison by Category",
5160
- description: `Comparing ${categories.length} categories`,
5161
- data: barData
5162
- };
4948
+ async ingest(documents, namespace) {
4949
+ await this.validationPromise;
4950
+ return this.pipeline.ingest(documents, namespace);
5163
4951
  }
5164
4952
  /**
5165
- * Transform data to line chart format
4953
+ * Get auto-suggestions based on a query prefix.
5166
4954
  */
5167
- static transformToLineChart(data) {
5168
- const timePoints = this.extractTimeSeriesData(data);
5169
- const lineData = timePoints.map((point) => ({
5170
- timestamp: point.timestamp,
5171
- value: point.value,
5172
- label: point.label
5173
- }));
5174
- return {
5175
- type: "line_chart",
5176
- title: "Trend Over Time",
5177
- description: `Showing ${lineData.length} data points`,
5178
- data: lineData
5179
- };
4955
+ async getSuggestions(query, namespace) {
4956
+ await this.validationPromise;
4957
+ return this.pipeline.getSuggestions(query, namespace);
5180
4958
  }
4959
+ };
4960
+
4961
+ // src/config/ConfigBuilder.ts
4962
+ var ConfigBuilder = class {
5181
4963
  /**
5182
- * Transform data to table format
4964
+ * Set the project/application ID for namespacing
5183
4965
  */
5184
- static transformToTable(data) {
5185
- const columns = this.extractTableColumns(data);
5186
- const rows = data.map((item) => this.extractTableRow(item, columns));
5187
- const tableData = {
5188
- columns,
5189
- rows
5190
- };
5191
- return {
5192
- type: "table",
5193
- title: "Detailed Results",
5194
- description: `Showing ${data.length} results`,
5195
- data: tableData
5196
- };
4966
+ projectId(id) {
4967
+ this._projectId = id;
4968
+ return this;
4969
+ }
4970
+ /**
4971
+ * Configure the vector database provider
4972
+ */
4973
+ vectorDb(provider, options) {
4974
+ var _a;
4975
+ if (provider === "auto") {
4976
+ this._vectorDb = this._autoDetectVectorDb();
4977
+ } else {
4978
+ this._vectorDb = {
4979
+ provider,
4980
+ indexName: (_a = options == null ? void 0 : options.indexName) != null ? _a : "default",
4981
+ options: __spreadValues({}, options)
4982
+ };
4983
+ }
4984
+ return this;
4985
+ }
4986
+ /**
4987
+ * Configure the LLM provider for chat
4988
+ */
4989
+ llm(provider, model, apiKey, options) {
4990
+ var _a, _b;
4991
+ if (provider === "auto") {
4992
+ this._llm = this._autoDetectLLM();
4993
+ } else {
4994
+ this._llm = {
4995
+ provider,
4996
+ model: model != null ? model : "default-model",
4997
+ apiKey,
4998
+ systemPrompt: options == null ? void 0 : options.systemPrompt,
4999
+ maxTokens: (_a = options == null ? void 0 : options.maxTokens) != null ? _a : 1024,
5000
+ temperature: (_b = options == null ? void 0 : options.temperature) != null ? _b : 0.7,
5001
+ baseUrl: options == null ? void 0 : options.baseUrl,
5002
+ options
5003
+ };
5004
+ }
5005
+ return this;
5197
5006
  }
5198
5007
  /**
5199
- * Transform data to text format (fallback)
5008
+ * Configure the embedding provider
5200
5009
  */
5201
- static transformToText(data) {
5202
- const textContent = data.map((item) => item.content).join("\n\n");
5203
- return this.createTextResponse(
5204
- "Search Results",
5205
- textContent,
5206
- `Found ${data.length} relevant results`
5207
- );
5010
+ embedding(provider, model, apiKey, options) {
5011
+ if (provider === "auto") {
5012
+ this._embedding = this._autoDetectEmbedding();
5013
+ } else {
5014
+ this._embedding = {
5015
+ provider,
5016
+ model: model != null ? model : "default-embedding",
5017
+ apiKey,
5018
+ baseUrl: options == null ? void 0 : options.baseUrl,
5019
+ options
5020
+ };
5021
+ }
5022
+ return this;
5208
5023
  }
5209
5024
  /**
5210
- * Helper: Create text response
5025
+ * Set RAG-specific pipeline parameters
5211
5026
  */
5212
- static createTextResponse(title, content, description) {
5213
- return {
5214
- type: "text",
5215
- title,
5216
- description,
5217
- data: { content }
5218
- };
5027
+ rag(options) {
5028
+ var _a;
5029
+ this._rag = __spreadValues(__spreadValues({}, (_a = this._rag) != null ? _a : {}), options);
5030
+ return this;
5219
5031
  }
5220
5032
  /**
5221
- * Helper: Check if data item is product-related
5033
+ * Set UI branding and appearance options.
5034
+ * Accepts the full UIConfig interface.
5222
5035
  */
5223
- static isProductData(item) {
5224
- const content = (item.content || "").toLowerCase();
5225
- const productKeywords = ["product", "price", "stock", "item", "sku", "brand", "model"];
5226
- return productKeywords.some((kw) => content.includes(kw)) || Object.keys(item.metadata || {}).some(
5227
- (k) => ["name", "price", "product", "sku"].includes(k.toLowerCase())
5228
- );
5036
+ ui(options) {
5037
+ this._ui = options;
5038
+ return this;
5229
5039
  }
5230
5040
  /**
5231
- * Helper: Check if data contains time series
5041
+ * Build and return the final RagConfig.
5042
+ * Throws if required fields (projectId, vectorDb, llm, embedding) are not set.
5232
5043
  */
5233
- static isTimeSeriesData(item) {
5234
- const content = (item.content || "").toLowerCase();
5235
- const timeKeywords = ["date", "time", "year", "month", "week", "day", "hour", "trend", "historical"];
5236
- return timeKeywords.some((kw) => content.includes(kw)) || Object.keys(item.metadata || {}).some(
5237
- (k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
5238
- );
5044
+ build() {
5045
+ var _a;
5046
+ const missing = [];
5047
+ if (!this._projectId) missing.push('projectId (call .projectId("my-app"))');
5048
+ if (!this._vectorDb) missing.push('vectorDb (call .vectorDb("pinecone", { ... }))');
5049
+ if (!this._llm) missing.push('llm (call .llm("openai", "gpt-4o", apiKey))');
5050
+ if (!this._embedding) missing.push('embedding (call .embedding("openai", "text-embedding-3-small"))');
5051
+ if (missing.length > 0) {
5052
+ throw new Error(
5053
+ `[ConfigBuilder] Cannot build \u2014 required fields are missing:
5054
+ ${missing.join("\n ")}`
5055
+ );
5056
+ }
5057
+ return __spreadValues({
5058
+ projectId: this._projectId,
5059
+ vectorDb: this._vectorDb,
5060
+ llm: this._llm,
5061
+ embedding: this._embedding,
5062
+ ui: this._rag ? this._ui : void 0,
5063
+ rag: (_a = this._rag) != null ? _a : { chunkSize: 1e3, chunkOverlap: 200, topK: 5 }
5064
+ }, this._ui ? { ui: this._ui } : {});
5239
5065
  }
5240
5066
  /**
5241
- * Helper: Extract product information from vector match
5067
+ * Build and return as JSON for serialization
5242
5068
  */
5243
- static extractProductInfo(item) {
5244
- const meta = item.metadata || {};
5245
- if (meta.name || meta.product) {
5246
- return {
5247
- id: item.id,
5248
- name: meta.name || meta.product,
5249
- price: meta.price,
5250
- image: meta.image || meta.imageUrl,
5251
- brand: meta.brand,
5252
- description: item.content,
5253
- inStock: this.determineStockStatus(item)
5254
- };
5069
+ toJSON() {
5070
+ return JSON.stringify(this.build());
5071
+ }
5072
+ // ============================================================================
5073
+ // Private helper methods for auto-detection
5074
+ // ============================================================================
5075
+ _autoDetectVectorDb() {
5076
+ if (process.env.PINECONE_API_KEY && process.env.PINECONE_INDEX) {
5077
+ return { provider: "pinecone", indexName: process.env.PINECONE_INDEX, options: { apiKey: process.env.PINECONE_API_KEY } };
5255
5078
  }
5256
- const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
5257
- const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d.]+)/i);
5258
- if (nameMatch) {
5259
- return {
5260
- id: item.id,
5261
- name: nameMatch[1],
5262
- price: priceMatch ? parseFloat(priceMatch[1]) : void 0,
5263
- description: item.content,
5264
- inStock: this.determineStockStatus(item)
5265
- };
5079
+ if (process.env.QDRANT_URL) {
5080
+ return { provider: "qdrant", indexName: process.env.QDRANT_COLLECTION || "documents", options: { url: process.env.QDRANT_URL, apiKey: process.env.QDRANT_API_KEY } };
5266
5081
  }
5267
- return null;
5082
+ if (process.env.DATABASE_URL) {
5083
+ return { provider: "postgresql", indexName: process.env.PG_TABLE || "documents", options: { connectionString: process.env.DATABASE_URL } };
5084
+ }
5085
+ if (process.env.MONGODB_URI) {
5086
+ return { provider: "mongodb", indexName: process.env.MONGODB_COLLECTION || "documents", options: { uri: process.env.MONGODB_URI, database: process.env.MONGODB_DB || "ai_db", collection: process.env.MONGODB_COLLECTION || "documents" } };
5087
+ }
5088
+ if (process.env.REDIS_URL) {
5089
+ return { provider: "redis", indexName: process.env.REDIS_INDEX || "documents", options: { url: process.env.REDIS_URL } };
5090
+ }
5091
+ return { provider: "qdrant", indexName: "documents", options: { url: process.env.QDRANT_URL || "http://localhost:6333" } };
5092
+ }
5093
+ _autoDetectLLM() {
5094
+ if (process.env.OPENAI_API_KEY) {
5095
+ return { provider: "openai", model: process.env.OPENAI_MODEL || "gpt-4o-mini", apiKey: process.env.OPENAI_API_KEY, maxTokens: parseInt(process.env.OPENAI_MAX_TOKENS || "1024"), temperature: parseFloat(process.env.OPENAI_TEMPERATURE || "0.7") };
5096
+ }
5097
+ if (process.env.ANTHROPIC_API_KEY) {
5098
+ return { provider: "anthropic", model: process.env.ANTHROPIC_MODEL || "claude-3-haiku-20240307", apiKey: process.env.ANTHROPIC_API_KEY, maxTokens: parseInt(process.env.ANTHROPIC_MAX_TOKENS || "1024"), temperature: parseFloat(process.env.ANTHROPIC_TEMPERATURE || "0.7") };
5099
+ }
5100
+ if (process.env.OLLAMA_BASE_URL) {
5101
+ return { provider: "ollama", model: process.env.OLLAMA_MODEL || "mistral", baseUrl: process.env.OLLAMA_BASE_URL, maxTokens: parseInt(process.env.OLLAMA_MAX_TOKENS || "1024"), temperature: parseFloat(process.env.OLLAMA_TEMPERATURE || "0.7") };
5102
+ }
5103
+ return { provider: "openai", model: "gpt-4o-mini", apiKey: process.env.OPENAI_API_KEY };
5104
+ }
5105
+ _autoDetectEmbedding() {
5106
+ if (process.env.EMBEDDING_PROVIDER) {
5107
+ return { provider: process.env.EMBEDDING_PROVIDER, model: process.env.EMBEDDING_MODEL || "default", apiKey: process.env.EMBEDDING_API_KEY, baseUrl: process.env.EMBEDDING_BASE_URL };
5108
+ }
5109
+ return { provider: "openai", model: "text-embedding-3-small", apiKey: process.env.OPENAI_API_KEY };
5268
5110
  }
5111
+ };
5112
+ var PRESETS = {
5113
+ /** OpenAI + Pinecone: Production-ready cloud setup */
5114
+ "openai-pinecone": { vectorDb: "pinecone", llm: "openai", embedding: "openai" },
5115
+ /** Claude + Qdrant: Open-source vector DB + proprietary LLM */
5116
+ "claude-qdrant": { vectorDb: "qdrant", llm: "anthropic", embedding: "openai" },
5117
+ /** Local development: Ollama + local Qdrant */
5118
+ "local-dev": { vectorDb: "qdrant", llm: "ollama", embedding: "ollama" },
5119
+ /** Fully open-source: Ollama LLM + Qdrant + Ollama embeddings */
5120
+ "fully-open-source": { vectorDb: "qdrant", llm: "ollama", embedding: "ollama" },
5121
+ /** PostgreSQL stack: pgvector + OpenAI */
5122
+ "postgres-openai": { vectorDb: "postgresql", llm: "openai", embedding: "openai" },
5123
+ /** Enterprise MongoDB: MongoDB Atlas with OpenAI */
5124
+ "mongodb-openai": { vectorDb: "mongodb", llm: "openai", embedding: "openai" },
5125
+ /** Redis stack for caching + search */
5126
+ "redis-openai": { vectorDb: "redis", llm: "openai", embedding: "openai" }
5127
+ };
5128
+ function createFromPreset(presetName) {
5129
+ const preset = PRESETS[presetName];
5130
+ return new ConfigBuilder().vectorDb(preset.vectorDb, { apiKey: process.env[`${preset.vectorDb.toUpperCase()}_API_KEY`] }).llm(preset.llm, void 0, process.env[`${preset.llm.toUpperCase()}_API_KEY`]).embedding(preset.embedding, void 0, process.env[`${preset.embedding.toUpperCase()}_API_KEY`]);
5131
+ }
5132
+
5133
+ // src/utils/DocumentParser.ts
5134
+ var DocumentParser = class {
5269
5135
  /**
5270
- * Helper: Detect categories in data
5136
+ * Extract text from a File or Buffer based on its type.
5271
5137
  */
5272
- static detectCategories(data) {
5273
- const categories = /* @__PURE__ */ new Set();
5274
- data.forEach((item) => {
5275
- const meta = item.metadata || {};
5276
- if (meta.category) {
5277
- categories.add(String(meta.category));
5278
- }
5279
- if (meta.type) {
5280
- categories.add(String(meta.type));
5138
+ static async parse(file, fileName, mimeType) {
5139
+ var _a;
5140
+ const extension = (_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase();
5141
+ if (extension === "txt" || extension === "md" || mimeType === "text/plain" || mimeType === "text/markdown") {
5142
+ return this.readAsText(file);
5143
+ }
5144
+ if (extension === "json" || mimeType === "application/json") {
5145
+ const text = await this.readAsText(file);
5146
+ try {
5147
+ const obj = JSON.parse(text);
5148
+ return JSON.stringify(obj, null, 2);
5149
+ } catch (e) {
5150
+ return text;
5281
5151
  }
5282
- if (meta.tag) {
5283
- const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
5284
- tags.forEach((t) => categories.add(String(t)));
5152
+ }
5153
+ if (extension === "csv" || mimeType === "text/csv") {
5154
+ return this.readAsText(file);
5155
+ }
5156
+ if (extension === "pdf" || mimeType === "application/pdf") {
5157
+ try {
5158
+ const pdf = await import("pdf-parse");
5159
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
5160
+ const data = await pdf.default(buffer);
5161
+ return data.text;
5162
+ } catch (err) {
5163
+ console.warn('[DocumentParser] PDF parsing failed. Make sure "pdf-parse" is installed.', err);
5164
+ return `[PDF Parsing Error for ${fileName}]`;
5285
5165
  }
5286
- const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
5287
- if (categoryMatch) {
5288
- categories.add(categoryMatch[1].trim());
5166
+ }
5167
+ if (extension === "docx" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
5168
+ try {
5169
+ const mammoth = await import("mammoth");
5170
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
5171
+ const result = await mammoth.extractRawText({ buffer });
5172
+ return result.value;
5173
+ } catch (err) {
5174
+ console.warn('[DocumentParser] DOCX parsing failed. Make sure "mammoth" is installed.', err);
5175
+ return `[DOCX Parsing Error for ${fileName}]`;
5289
5176
  }
5290
- });
5291
- return Array.from(categories);
5177
+ }
5178
+ try {
5179
+ return await this.readAsText(file);
5180
+ } catch (e) {
5181
+ throw new Error(`[DocumentParser] Unsupported file format: ${fileName} (${mimeType})`);
5182
+ }
5183
+ }
5184
+ static async readAsText(file) {
5185
+ if (Buffer.isBuffer(file)) {
5186
+ return file.toString("utf-8");
5187
+ }
5188
+ return await file.text();
5189
+ }
5190
+ };
5191
+
5192
+ // src/server.ts
5193
+ init_BaseVectorProvider();
5194
+ init_PineconeProvider();
5195
+ init_PostgreSQLProvider();
5196
+
5197
+ // src/providers/vectordb/MultiTablePostgresProvider.ts
5198
+ var import_pg2 = require("pg");
5199
+ init_BaseVectorProvider();
5200
+ var MultiTablePostgresProvider = class extends BaseVectorProvider {
5201
+ constructor(config) {
5202
+ var _a, _b, _c, _d, _e;
5203
+ super(config);
5204
+ const opts = config.options || {};
5205
+ if (!opts.connectionString) {
5206
+ throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
5207
+ }
5208
+ this.connectionString = opts.connectionString;
5209
+ this.dimensions = (_a = opts.dimensions) != null ? _a : 768;
5210
+ const rawTables = (_c = (_b = opts.tables) != null ? _b : process.env.VECTOR_DB_TABLES) != null ? _c : "";
5211
+ this.tables = typeof rawTables === "string" ? rawTables.split(",").map((t) => t.trim()).filter(Boolean) : rawTables;
5212
+ if (this.tables.length === 0) {
5213
+ console.warn(
5214
+ "[MultiTablePostgresProvider] No tables configured. Set VECTOR_DB_TABLES as a comma-separated list of table names or pass options.tables."
5215
+ );
5216
+ }
5217
+ const rawSearchFields = (_e = (_d = opts.searchFields) != null ? _d : process.env.VECTOR_DB_SEARCH_FIELDS) != null ? _e : ["name", "product_name", "productname", "title"];
5218
+ this.searchFields = typeof rawSearchFields === "string" ? rawSearchFields.split(",").map((f) => f.trim()).filter(Boolean) : rawSearchFields;
5292
5219
  }
5293
- /**
5294
- * Helper: Aggregate data by category
5295
- */
5296
- static aggregateByCategory(data, categories) {
5297
- const result = {};
5298
- categories.forEach((cat) => {
5299
- result[cat] = 0;
5300
- });
5301
- data.forEach((item) => {
5302
- const meta = item.metadata || {};
5303
- const itemCategory = meta.category || meta.type || "Other";
5304
- if (result.hasOwnProperty(itemCategory)) {
5305
- result[itemCategory]++;
5220
+ async initialize() {
5221
+ this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
5222
+ const client = await this.pool.connect();
5223
+ try {
5224
+ const res = await client.query(`
5225
+ SELECT table_name
5226
+ FROM information_schema.columns
5227
+ WHERE column_name = 'embedding'
5228
+ AND table_schema = 'public'
5229
+ `);
5230
+ const discoveredTables = res.rows.map((r) => r.table_name);
5231
+ if (this.tables.length === 0) {
5232
+ this.tables = discoveredTables;
5306
5233
  } else {
5307
- result["Other"] = (result["Other"] || 0) + 1;
5234
+ const staticTables = [...this.tables];
5235
+ this.tables = staticTables.filter((t) => discoveredTables.includes(t));
5236
+ if (this.tables.length < staticTables.length) {
5237
+ const missing = staticTables.filter((t) => !discoveredTables.includes(t));
5238
+ console.warn(`[MultiTablePostgresProvider] Some configured tables were skipped (missing 'embedding' column): ${missing.join(", ")}`);
5239
+ }
5308
5240
  }
5309
- });
5310
- return result;
5241
+ if (this.tables.length === 0) {
5242
+ console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
5243
+ } else {
5244
+ console.log(
5245
+ `[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
5246
+ );
5247
+ }
5248
+ } finally {
5249
+ client.release();
5250
+ }
5311
5251
  }
5312
5252
  /**
5313
- * Helper: Extract time series data
5253
+ * Upsert is not supported for MultiTablePostgresProvider as it's designed for
5254
+ * searching across pre-existing tables with varying schemas.
5314
5255
  */
5315
- static extractTimeSeriesData(data) {
5316
- return data.map((item) => {
5317
- const meta = item.metadata || {};
5318
- return {
5319
- timestamp: meta.timestamp || meta.date || (/* @__PURE__ */ new Date()).toISOString(),
5320
- value: meta.value || item.score || 0,
5321
- label: meta.label || item.content.substring(0, 50)
5322
- };
5323
- });
5256
+ async upsert(_doc, _namespace) {
5257
+ throw new Error(
5258
+ "[MultiTablePostgresProvider] upsert() is not supported in multi-table mode. Please use the standard PostgreSQLProvider for single-table managed indices."
5259
+ );
5324
5260
  }
5325
5261
  /**
5326
- * Helper: Extract table columns
5262
+ * Batch upsert is not supported for MultiTablePostgresProvider.
5327
5263
  */
5328
- static extractTableColumns(data) {
5329
- const columnSet = /* @__PURE__ */ new Set();
5330
- columnSet.add("Content");
5331
- data.forEach((item) => {
5332
- Object.keys(item.metadata || {}).forEach((key) => {
5333
- columnSet.add(key.charAt(0).toUpperCase() + key.slice(1));
5334
- });
5335
- });
5336
- return Array.from(columnSet);
5264
+ async batchUpsert(_docs, _namespace) {
5265
+ throw new Error(
5266
+ "[MultiTablePostgresProvider] batchUpsert() is not supported in multi-table mode."
5267
+ );
5337
5268
  }
5338
5269
  /**
5339
- * Helper: Extract table row
5270
+ * Query all configured tables and merge results, sorted by cosine similarity score.
5340
5271
  */
5341
- static extractTableRow(item, columns) {
5342
- const meta = item.metadata || {};
5343
- return columns.map((col) => {
5344
- if (col === "Content") {
5345
- return item.content.substring(0, 100);
5272
+ async query(vector, topK, _namespace, _filter) {
5273
+ var _a, _b, _c;
5274
+ if (!this.pool) {
5275
+ throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
5276
+ }
5277
+ const vectorLiteral = `[${vector.join(",")}]`;
5278
+ const allResults = [];
5279
+ console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
5280
+ const queryText = _filter == null ? void 0 : _filter.queryText;
5281
+ const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter(
5282
+ (hint) => typeof hint === "object" && hint !== null && typeof hint.value === "string"
5283
+ ).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
5284
+ console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
5285
+ console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
5286
+ const getDynamicKeywordQuery = () => {
5287
+ if (entityHints.length > 0) {
5288
+ return entityHints.map((h) => h.includes(" ") ? `"${h}"` : h).join(" & ");
5289
+ }
5290
+ if (queryText) {
5291
+ return queryText.toLowerCase().replace(/\b(show|me|the|product[s]?|item[s]?|card[s]?|of|category|find|list|browse|what|are|display|all|under|for|in|with|about)\b/g, "").trim().replace(/\s+/g, " & ");
5292
+ }
5293
+ return "";
5294
+ };
5295
+ const dynamicKeywordQuery = getDynamicKeywordQuery();
5296
+ const queryPromises = this.tables.map(async (table) => {
5297
+ try {
5298
+ let sqlQuery = "";
5299
+ let params = [];
5300
+ if (queryText) {
5301
+ const hasEntityHints = entityHints.length > 0;
5302
+ const exactNameScoreExpr = hasEntityHints ? `+ (
5303
+ SELECT COALESCE(MAX(
5304
+ CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
5305
+ THEN 1.0 ELSE 0.0 END
5306
+ ), 0)
5307
+ FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
5308
+ WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
5309
+ ) * 3.0` : "";
5310
+ sqlQuery = `
5311
+ SELECT *,
5312
+ (1 - (embedding <=> $1::vector)) AS vector_score,
5313
+ COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
5314
+ ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
5315
+ FROM "${table}" t
5316
+ ORDER BY hybrid_score DESC
5317
+ LIMIT 50
5318
+ `;
5319
+ params = [vectorLiteral, dynamicKeywordQuery];
5320
+ } else {
5321
+ sqlQuery = `
5322
+ SELECT *,
5323
+ (1 - (embedding <=> $1::vector)) AS hybrid_score
5324
+ FROM "${table}" t
5325
+ ORDER BY hybrid_score DESC
5326
+ LIMIT 50
5327
+ `;
5328
+ params = [vectorLiteral];
5329
+ }
5330
+ const result = await this.pool.query(sqlQuery, params);
5331
+ if (result.rowCount && result.rowCount > 0) {
5332
+ console.log(` \u2705 Table "${table}": Found ${result.rowCount} potential matches. Top score: ${result.rows[0].hybrid_score.toFixed(4)}`);
5333
+ } else {
5334
+ console.log(` \u26AA Table "${table}": No relevant matches found.`);
5335
+ }
5336
+ const tableResults = [];
5337
+ for (const row of result.rows) {
5338
+ const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
5339
+ delete rest.embedding;
5340
+ delete rest.vector_score;
5341
+ delete rest.keyword_score;
5342
+ delete rest.exact_name_score;
5343
+ const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
5344
+ ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
5345
+ tableResults.push({
5346
+ id: `${table}-${id}`,
5347
+ score: parseFloat(String(hybrid_score)),
5348
+ content,
5349
+ metadata: __spreadProps(__spreadValues({}, rest), { source_table: table })
5350
+ });
5351
+ }
5352
+ return tableResults;
5353
+ } catch (err) {
5354
+ console.error(
5355
+ `[MultiTablePostgresProvider] Error querying table "${table}":`,
5356
+ err.message
5357
+ );
5358
+ return [];
5346
5359
  }
5347
- const metaKey = col.charAt(0).toLowerCase() + col.slice(1);
5348
- const value = meta[metaKey];
5349
- return value !== void 0 ? String(value) : "";
5350
- });
5351
- }
5352
- /**
5353
- * Helper: Check stock status
5354
- */
5355
- static checkStockStatus(category, data) {
5356
- const item = data.find((d) => {
5357
- const meta = d.metadata || {};
5358
- return meta.category === category || meta.type === category;
5359
5360
  });
5360
- return this.determineStockStatus(item || {});
5361
- }
5362
- /**
5363
- * Helper: Determine if item is in stock
5364
- */
5365
- static determineStockStatus(item) {
5366
- const meta = item.metadata || {};
5367
- if (meta.inStock !== void 0) {
5368
- return Boolean(meta.inStock);
5361
+ const resultsArray = await Promise.all(queryPromises);
5362
+ for (const tableResults of resultsArray) {
5363
+ allResults.push(...tableResults);
5369
5364
  }
5370
- if (meta.stock !== void 0) {
5371
- return Boolean(meta.stock);
5365
+ const resultsByTable = {};
5366
+ for (const res of allResults) {
5367
+ const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
5368
+ if (!resultsByTable[table]) resultsByTable[table] = [];
5369
+ resultsByTable[table].push(res);
5372
5370
  }
5373
- if (meta.available !== void 0) {
5374
- return Boolean(meta.available);
5371
+ const balancedResults = [];
5372
+ const tables = Object.keys(resultsByTable);
5373
+ for (const table of tables) {
5374
+ balancedResults.push(...resultsByTable[table].slice(0, 3));
5375
5375
  }
5376
- const content = (item.content || "").toLowerCase();
5377
- if (content.includes("out of stock") || content.includes("unavailable")) {
5378
- return false;
5376
+ const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
5377
+ if (finalSorted.length > 0) {
5378
+ console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
5379
+ console.log(`[MultiTablePostgresProvider] Final top match from "${(_c = (_b = finalSorted[0].metadata) == null ? void 0 : _b.source_table) != null ? _c : "unknown"}" with score ${finalSorted[0].score.toFixed(4)}`);
5379
5380
  }
5380
- if (content.includes("in stock") || content.includes("available")) {
5381
+ return finalSorted.slice(0, Math.max(topK, 15));
5382
+ }
5383
+ async delete(_id, _namespace) {
5384
+ console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
5385
+ }
5386
+ async deleteNamespace(_namespace) {
5387
+ console.warn("[MultiTablePostgresProvider] deleteNamespace() is a no-op for multi-table mode.");
5388
+ }
5389
+ async ping() {
5390
+ try {
5391
+ await this.pool.query("SELECT 1");
5381
5392
  return true;
5393
+ } catch (e) {
5394
+ return false;
5382
5395
  }
5383
- return true;
5384
5396
  }
5385
- /**
5386
- * Helper: Check if data has multiple fields
5387
- */
5388
- static hasMultipleFields(data) {
5389
- const fieldCount = /* @__PURE__ */ new Set();
5390
- data.forEach((item) => {
5391
- Object.keys(item.metadata || {}).forEach((key) => {
5392
- fieldCount.add(key);
5393
- });
5394
- });
5395
- return fieldCount.size > 2;
5397
+ async disconnect() {
5398
+ if (this.pool) {
5399
+ await this.pool.end();
5400
+ }
5396
5401
  }
5397
5402
  };
5398
5403
 
5404
+ // src/server.ts
5405
+ init_MongoDBProvider();
5406
+ init_MilvusProvider();
5407
+ init_QdrantProvider();
5408
+ init_ChromaDBProvider();
5409
+ init_RedisProvider();
5410
+ init_WeaviateProvider();
5411
+ init_UniversalVectorProvider();
5412
+
5399
5413
  // src/handlers/index.ts
5414
+ var import_server = require("next/server");
5400
5415
  function sseFrame(payload) {
5401
5416
  return `data: ${JSON.stringify(payload)}
5402
5417