@retrivora-ai/rag-engine 0.1.5 → 0.1.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/{DocumentChunker-cfaMidtA.d.mts → DocumentChunker-BEyzadsv.d.mts} +2 -2
- package/dist/{DocumentChunker-cfaMidtA.d.ts → DocumentChunker-BEyzadsv.d.ts} +2 -2
- package/dist/{PineconeProvider-NJ675H7U.mjs → PineconeProvider-ZRAFNFEC.mjs} +1 -1
- package/dist/{PostgreSQLProvider-ISNMD3BE.mjs → PostgreSQLProvider-ZNXA67IM.mjs} +1 -1
- package/dist/{QdrantProvider-LJWOIGES.mjs → QdrantProvider-VAED5VA7.mjs} +1 -1
- package/dist/{RagConfig-DG_0f8ka.d.mts → RagConfig-hBGXJmSx.d.mts} +3 -3
- package/dist/{RagConfig-DG_0f8ka.d.ts → RagConfig-hBGXJmSx.d.ts} +3 -3
- package/dist/chunk-7YQWGERZ.mjs +1764 -0
- package/dist/chunk-CWQQHAF6.mjs +157 -0
- package/dist/{chunk-S5DRHETN.mjs → chunk-IUTAZ7QR.mjs} +31 -2
- package/dist/{chunk-AALIF3AL.mjs → chunk-ZM6TYIDH.mjs} +3 -3
- package/dist/handlers/index.d.mts +3 -44
- package/dist/handlers/index.d.ts +3 -44
- package/dist/handlers/index.js +1371 -60
- package/dist/handlers/index.mjs +1 -1
- package/dist/index-Bx182KKn.d.ts +64 -0
- package/dist/index-Ck2pt7-8.d.mts +64 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/server.d.mts +74 -18
- package/dist/server.d.ts +74 -18
- package/dist/server.js +1371 -60
- package/dist/server.mjs +4 -4
- package/package.json +2 -1
- package/src/config/serverConfig.ts +4 -0
- package/src/core/BatchProcessor.ts +347 -0
- package/src/core/ConfigValidator.ts +568 -0
- package/src/core/Pipeline.ts +89 -39
- package/src/core/ProviderHealthCheck.ts +568 -0
- package/src/core/VectorPlugin.ts +49 -8
- package/src/handlers/index.ts +2 -2
- package/src/providers/vectordb/BaseVectorProvider.ts +1 -1
- package/src/providers/vectordb/MilvusProvider.ts +1 -1
- package/src/providers/vectordb/MongoDBProvider.ts +1 -1
- package/src/providers/vectordb/PineconeProvider.ts +4 -4
- package/src/providers/vectordb/PostgreSQLProvider.ts +35 -3
- package/src/providers/vectordb/QdrantProvider.ts +81 -4
- package/src/rag/DocumentChunker.ts +2 -2
- package/src/types/index.ts +3 -3
- package/dist/chunk-6FODXNUF.mjs +0 -91
- package/dist/chunk-BP4U4TT5.mjs +0 -548
package/dist/handlers/index.js
CHANGED
|
@@ -557,7 +557,7 @@ var init_PineconeProvider = __esm({
|
|
|
557
557
|
var _a;
|
|
558
558
|
await this.index(namespace).upsert({
|
|
559
559
|
records: [{
|
|
560
|
-
id: doc.id,
|
|
560
|
+
id: String(doc.id),
|
|
561
561
|
values: doc.vector,
|
|
562
562
|
metadata: __spreadValues({ content: doc.content }, (_a = doc.metadata) != null ? _a : {})
|
|
563
563
|
}]
|
|
@@ -569,7 +569,7 @@ var init_PineconeProvider = __esm({
|
|
|
569
569
|
const records = docs.slice(i, i + BATCH).map((d) => {
|
|
570
570
|
var _a;
|
|
571
571
|
return {
|
|
572
|
-
id: d.id,
|
|
572
|
+
id: String(d.id),
|
|
573
573
|
values: d.vector,
|
|
574
574
|
metadata: __spreadValues({ content: d.content }, (_a = d.metadata) != null ? _a : {})
|
|
575
575
|
};
|
|
@@ -595,7 +595,7 @@ var init_PineconeProvider = __esm({
|
|
|
595
595
|
});
|
|
596
596
|
}
|
|
597
597
|
async delete(id, namespace) {
|
|
598
|
-
await this.index(namespace).deleteOne({ id });
|
|
598
|
+
await this.index(namespace).deleteOne({ id: String(id) });
|
|
599
599
|
}
|
|
600
600
|
async deleteNamespace(namespace) {
|
|
601
601
|
await this.client.index(this.indexName).namespace(namespace).deleteAll();
|
|
@@ -673,8 +673,37 @@ var init_PostgreSQLProvider = __esm({
|
|
|
673
673
|
);
|
|
674
674
|
}
|
|
675
675
|
async batchUpsert(docs, namespace = "") {
|
|
676
|
-
|
|
677
|
-
|
|
676
|
+
if (docs.length === 0) return;
|
|
677
|
+
const client = await this.pool.connect();
|
|
678
|
+
try {
|
|
679
|
+
await client.query("BEGIN");
|
|
680
|
+
const BATCH_SIZE = 50;
|
|
681
|
+
for (let i = 0; i < docs.length; i += BATCH_SIZE) {
|
|
682
|
+
const batch = docs.slice(i, i + BATCH_SIZE);
|
|
683
|
+
const values = [];
|
|
684
|
+
const valuePlaceholders = batch.map((doc, idx) => {
|
|
685
|
+
var _a;
|
|
686
|
+
const offset = idx * 5;
|
|
687
|
+
values.push(doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), `[${doc.vector.join(",")}]`);
|
|
688
|
+
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}::vector)`;
|
|
689
|
+
}).join(", ");
|
|
690
|
+
const query = `
|
|
691
|
+
INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
|
|
692
|
+
VALUES ${valuePlaceholders}
|
|
693
|
+
ON CONFLICT (id) DO UPDATE
|
|
694
|
+
SET namespace = EXCLUDED.namespace,
|
|
695
|
+
content = EXCLUDED.content,
|
|
696
|
+
metadata = EXCLUDED.metadata,
|
|
697
|
+
embedding = EXCLUDED.embedding
|
|
698
|
+
`;
|
|
699
|
+
await client.query(query, values);
|
|
700
|
+
}
|
|
701
|
+
await client.query("COMMIT");
|
|
702
|
+
} catch (error) {
|
|
703
|
+
await client.query("ROLLBACK");
|
|
704
|
+
throw error;
|
|
705
|
+
} finally {
|
|
706
|
+
client.release();
|
|
678
707
|
}
|
|
679
708
|
}
|
|
680
709
|
async query(vector, topK, namespace, filter) {
|
|
@@ -926,11 +955,12 @@ var QdrantProvider_exports = {};
|
|
|
926
955
|
__export(QdrantProvider_exports, {
|
|
927
956
|
QdrantProvider: () => QdrantProvider
|
|
928
957
|
});
|
|
929
|
-
var import_axios4, QdrantProvider;
|
|
958
|
+
var import_axios4, import_crypto, QdrantProvider;
|
|
930
959
|
var init_QdrantProvider = __esm({
|
|
931
960
|
"src/providers/vectordb/QdrantProvider.ts"() {
|
|
932
961
|
"use strict";
|
|
933
962
|
import_axios4 = __toESM(require("axios"));
|
|
963
|
+
import_crypto = __toESM(require("crypto"));
|
|
934
964
|
init_BaseVectorProvider();
|
|
935
965
|
QdrantProvider = class extends BaseVectorProvider {
|
|
936
966
|
constructor(config) {
|
|
@@ -947,6 +977,61 @@ var init_QdrantProvider = __esm({
|
|
|
947
977
|
}
|
|
948
978
|
async initialize() {
|
|
949
979
|
await this.ping();
|
|
980
|
+
await this.ensureCollection();
|
|
981
|
+
await this.ensureIndex();
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Ensures the collection exists. Creates it if missing.
|
|
985
|
+
*/
|
|
986
|
+
async ensureCollection() {
|
|
987
|
+
var _a;
|
|
988
|
+
try {
|
|
989
|
+
const opts = this.config.options;
|
|
990
|
+
const dimensions = opts.dimensions || 1536;
|
|
991
|
+
await this.http.get(`/collections/${this.indexName}`);
|
|
992
|
+
console.log(`[QdrantProvider] \u2705 Collection "${this.indexName}" already exists.`);
|
|
993
|
+
} catch (err) {
|
|
994
|
+
if (import_axios4.default.isAxiosError(err) && ((_a = err.response) == null ? void 0 : _a.status) === 404) {
|
|
995
|
+
const opts = this.config.options;
|
|
996
|
+
const dimensions = opts.dimensions || 1536;
|
|
997
|
+
console.log(`[QdrantProvider] \u23F3 Creating collection "${this.indexName}" (dimensions: ${dimensions})...`);
|
|
998
|
+
await this.http.put(`/collections/${this.indexName}`, {
|
|
999
|
+
vectors: {
|
|
1000
|
+
size: dimensions,
|
|
1001
|
+
distance: "Cosine"
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
console.log(`[QdrantProvider] \u2705 Created collection "${this.indexName}"`);
|
|
1005
|
+
} else {
|
|
1006
|
+
throw err;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Ensures that the 'namespace' field has a keyword index for efficient filtering.
|
|
1012
|
+
* Qdrant requires this for search filters to work in many configurations.
|
|
1013
|
+
*/
|
|
1014
|
+
async ensureIndex() {
|
|
1015
|
+
var _a, _b;
|
|
1016
|
+
try {
|
|
1017
|
+
await this.http.put(`/collections/${this.indexName}/index`, {
|
|
1018
|
+
field_name: "namespace",
|
|
1019
|
+
field_schema: "keyword"
|
|
1020
|
+
});
|
|
1021
|
+
console.log(`[QdrantProvider] \u2705 Ensured keyword index for "namespace" on collection "${this.indexName}"`);
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
let status;
|
|
1024
|
+
let data;
|
|
1025
|
+
if (import_axios4.default.isAxiosError(err)) {
|
|
1026
|
+
status = (_a = err.response) == null ? void 0 : _a.status;
|
|
1027
|
+
data = (_b = err.response) == null ? void 0 : _b.data;
|
|
1028
|
+
}
|
|
1029
|
+
if (status === 409) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1033
|
+
console.warn(`[QdrantProvider] \u26A0\uFE0F Could not ensure namespace index (Status: ${status}):`, JSON.stringify(data || errorMessage));
|
|
1034
|
+
}
|
|
950
1035
|
}
|
|
951
1036
|
async upsert(doc, namespace) {
|
|
952
1037
|
await this.batchUpsert([doc], namespace);
|
|
@@ -954,7 +1039,7 @@ var init_QdrantProvider = __esm({
|
|
|
954
1039
|
async batchUpsert(docs, namespace) {
|
|
955
1040
|
const payload = {
|
|
956
1041
|
points: docs.map((doc) => ({
|
|
957
|
-
id: doc.id,
|
|
1042
|
+
id: this.normalizeId(doc.id),
|
|
958
1043
|
vector: doc.vector,
|
|
959
1044
|
payload: __spreadValues({
|
|
960
1045
|
content: doc.content,
|
|
@@ -988,10 +1073,9 @@ var init_QdrantProvider = __esm({
|
|
|
988
1073
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
989
1074
|
async delete(id, _namespace) {
|
|
990
1075
|
await this.http.post(`/collections/${this.indexName}/points/delete`, {
|
|
991
|
-
points: [id]
|
|
1076
|
+
points: [this.normalizeId(id)]
|
|
992
1077
|
});
|
|
993
1078
|
}
|
|
994
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
995
1079
|
async deleteNamespace(_namespace) {
|
|
996
1080
|
await this.http.post(`/collections/${this.indexName}/points/delete`, {
|
|
997
1081
|
filter: {
|
|
@@ -1007,6 +1091,17 @@ var init_QdrantProvider = __esm({
|
|
|
1007
1091
|
return false;
|
|
1008
1092
|
}
|
|
1009
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Normalizes an ID into a format Qdrant accepts (UUID or integer).
|
|
1096
|
+
* For string IDs that aren't UUIDs, it generates a deterministic UUID v5-like hash.
|
|
1097
|
+
*/
|
|
1098
|
+
normalizeId(id) {
|
|
1099
|
+
if (typeof id === "number") return id;
|
|
1100
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1101
|
+
if (uuidRegex.test(id)) return id.toLowerCase();
|
|
1102
|
+
const hash = import_crypto.default.createHash("md5").update(id).digest("hex");
|
|
1103
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
|
|
1104
|
+
}
|
|
1010
1105
|
async disconnect() {
|
|
1011
1106
|
}
|
|
1012
1107
|
};
|
|
@@ -1335,7 +1430,7 @@ function readEnum(env, name, fallback, allowed) {
|
|
|
1335
1430
|
throw new Error(`[getRagConfig] ${name} must be one of: ${allowed.join(", ")}`);
|
|
1336
1431
|
}
|
|
1337
1432
|
function getRagConfig(env = process.env) {
|
|
1338
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J;
|
|
1433
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K;
|
|
1339
1434
|
const projectId = (_b = (_a = readString(env, "RAG_PROJECT_ID")) != null ? _a : readString(env, "NEXT_PUBLIC_PROJECT_ID")) != null ? _b : "__default__";
|
|
1340
1435
|
const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
|
|
1341
1436
|
const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
|
|
@@ -1360,6 +1455,10 @@ function getRagConfig(env = process.env) {
|
|
|
1360
1455
|
vectorDbOptions.database = (_j = readString(env, "MONGODB_DB")) != null ? _j : "";
|
|
1361
1456
|
vectorDbOptions.collection = (_k = readString(env, "MONGODB_COLLECTION")) != null ? _k : "";
|
|
1362
1457
|
vectorDbOptions.indexName = (_l = readString(env, "MONGODB_INDEX_NAME")) != null ? _l : "vector_index";
|
|
1458
|
+
} else if (vectorProvider === "qdrant") {
|
|
1459
|
+
vectorDbOptions.baseUrl = (_m = readString(env, "QDRANT_URL")) != null ? _m : "http://localhost:6333";
|
|
1460
|
+
vectorDbOptions.apiKey = readString(env, "QDRANT_API_KEY");
|
|
1461
|
+
vectorDbOptions.dimensions = embeddingDimensions;
|
|
1363
1462
|
}
|
|
1364
1463
|
const llmApiKeyByProvider = {
|
|
1365
1464
|
openai: readString(env, "OPENAI_API_KEY"),
|
|
@@ -1370,19 +1469,19 @@ function getRagConfig(env = process.env) {
|
|
|
1370
1469
|
const embeddingApiKeyByProvider = {
|
|
1371
1470
|
openai: readString(env, "OPENAI_API_KEY"),
|
|
1372
1471
|
ollama: void 0,
|
|
1373
|
-
custom: (
|
|
1472
|
+
custom: (_n = readString(env, "EMBEDDING_API_KEY")) != null ? _n : readString(env, "OPENAI_API_KEY")
|
|
1374
1473
|
};
|
|
1375
1474
|
return {
|
|
1376
1475
|
projectId,
|
|
1377
1476
|
vectorDb: {
|
|
1378
1477
|
provider: vectorProvider,
|
|
1379
|
-
indexName: (
|
|
1478
|
+
indexName: (_o = readString(env, "VECTOR_DB_INDEX")) != null ? _o : "rag-index",
|
|
1380
1479
|
options: vectorDbOptions
|
|
1381
1480
|
},
|
|
1382
1481
|
llm: {
|
|
1383
1482
|
provider: llmProvider,
|
|
1384
|
-
model: (
|
|
1385
|
-
apiKey: (
|
|
1483
|
+
model: (_p = readString(env, "LLM_MODEL")) != null ? _p : "gpt-4o",
|
|
1484
|
+
apiKey: (_q = llmApiKeyByProvider[llmProvider]) != null ? _q : "",
|
|
1386
1485
|
baseUrl: readString(env, "LLM_BASE_URL"),
|
|
1387
1486
|
systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
|
|
1388
1487
|
maxTokens: readNumber(env, "LLM_MAX_TOKENS", 1024),
|
|
@@ -1393,7 +1492,7 @@ function getRagConfig(env = process.env) {
|
|
|
1393
1492
|
},
|
|
1394
1493
|
embedding: {
|
|
1395
1494
|
provider: embeddingProvider,
|
|
1396
|
-
model: (
|
|
1495
|
+
model: (_r = readString(env, "EMBEDDING_MODEL")) != null ? _r : "text-embedding-3-small",
|
|
1397
1496
|
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
1398
1497
|
baseUrl: readString(env, "EMBEDDING_BASE_URL"),
|
|
1399
1498
|
dimensions: embeddingDimensions,
|
|
@@ -1402,16 +1501,16 @@ function getRagConfig(env = process.env) {
|
|
|
1402
1501
|
}
|
|
1403
1502
|
},
|
|
1404
1503
|
ui: {
|
|
1405
|
-
title: (
|
|
1406
|
-
subtitle: (
|
|
1407
|
-
primaryColor: (
|
|
1408
|
-
accentColor: (
|
|
1409
|
-
logoUrl: (
|
|
1410
|
-
placeholder: (
|
|
1411
|
-
showSources: ((
|
|
1412
|
-
welcomeMessage: (
|
|
1413
|
-
visualStyle: (
|
|
1414
|
-
borderRadius: (
|
|
1504
|
+
title: (_t = (_s = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _s : readString(env, "UI_TITLE")) != null ? _t : "AI Assistant",
|
|
1505
|
+
subtitle: (_v = (_u = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _u : readString(env, "UI_SUBTITLE")) != null ? _v : "Powered by RAG",
|
|
1506
|
+
primaryColor: (_x = (_w = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _w : readString(env, "UI_PRIMARY_COLOR")) != null ? _x : "#10b981",
|
|
1507
|
+
accentColor: (_z = (_y = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _y : readString(env, "UI_ACCENT_COLOR")) != null ? _z : "#3b82f6",
|
|
1508
|
+
logoUrl: (_A = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _A : readString(env, "UI_LOGO_URL"),
|
|
1509
|
+
placeholder: (_C = (_B = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _B : readString(env, "UI_PLACEHOLDER")) != null ? _C : "Ask me anything\u2026",
|
|
1510
|
+
showSources: ((_E = (_D = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _D : readString(env, "UI_SHOW_SOURCES")) != null ? _E : "true") !== "false",
|
|
1511
|
+
welcomeMessage: (_G = (_F = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _F : readString(env, "UI_WELCOME_MESSAGE")) != null ? _G : "Hello! I'm your AI assistant. Ask me anything about your documents.",
|
|
1512
|
+
visualStyle: (_I = (_H = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _H : readString(env, "UI_VISUAL_STYLE")) != null ? _I : "glass",
|
|
1513
|
+
borderRadius: (_K = (_J = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _J : readString(env, "UI_BORDER_RADIUS")) != null ? _K : "xl"
|
|
1415
1514
|
},
|
|
1416
1515
|
rag: {
|
|
1417
1516
|
topK: readNumber(env, "RAG_TOP_K", 5),
|
|
@@ -1464,6 +1563,467 @@ var ConfigResolver = class {
|
|
|
1464
1563
|
}
|
|
1465
1564
|
};
|
|
1466
1565
|
|
|
1566
|
+
// src/core/ConfigValidator.ts
|
|
1567
|
+
var ConfigValidator = class {
|
|
1568
|
+
/**
|
|
1569
|
+
* Validates the entire RagConfig object.
|
|
1570
|
+
* Returns an array of validation errors. Empty array = valid config.
|
|
1571
|
+
*/
|
|
1572
|
+
static validate(config) {
|
|
1573
|
+
const errors = [];
|
|
1574
|
+
if (!config.projectId) {
|
|
1575
|
+
errors.push({
|
|
1576
|
+
field: "projectId",
|
|
1577
|
+
message: "projectId is required",
|
|
1578
|
+
severity: "error"
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
errors.push(...this.validateVectorDbConfig(config.vectorDb));
|
|
1582
|
+
errors.push(...this.validateLLMConfig(config.llm));
|
|
1583
|
+
if (config.embedding) {
|
|
1584
|
+
errors.push(...this.validateEmbeddingConfig(config.embedding));
|
|
1585
|
+
} else if (config.llm.provider === "anthropic") {
|
|
1586
|
+
errors.push({
|
|
1587
|
+
field: "embedding",
|
|
1588
|
+
message: "Embedding config is required when using Anthropic LLM",
|
|
1589
|
+
suggestion: 'Provide embedding config with provider (e.g., "openai")',
|
|
1590
|
+
severity: "error"
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
if (config.ui) {
|
|
1594
|
+
errors.push(...this.validateUIConfig(config.ui));
|
|
1595
|
+
}
|
|
1596
|
+
if (config.rag) {
|
|
1597
|
+
errors.push(...this.validateRAGConfig(config.rag));
|
|
1598
|
+
}
|
|
1599
|
+
return errors;
|
|
1600
|
+
}
|
|
1601
|
+
static validateVectorDbConfig(config) {
|
|
1602
|
+
const errors = [];
|
|
1603
|
+
if (!config.provider) {
|
|
1604
|
+
errors.push({
|
|
1605
|
+
field: "vectorDb.provider",
|
|
1606
|
+
message: "Vector database provider is required",
|
|
1607
|
+
severity: "error"
|
|
1608
|
+
});
|
|
1609
|
+
return errors;
|
|
1610
|
+
}
|
|
1611
|
+
if (!config.indexName) {
|
|
1612
|
+
errors.push({
|
|
1613
|
+
field: "vectorDb.indexName",
|
|
1614
|
+
message: "Vector database index name is required",
|
|
1615
|
+
severity: "error"
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
switch (config.provider) {
|
|
1619
|
+
case "pinecone":
|
|
1620
|
+
errors.push(...this.validatePineconeConfig(config));
|
|
1621
|
+
break;
|
|
1622
|
+
case "pgvector":
|
|
1623
|
+
case "postgresql":
|
|
1624
|
+
errors.push(...this.validatePostgresConfig(config));
|
|
1625
|
+
break;
|
|
1626
|
+
case "mongodb":
|
|
1627
|
+
errors.push(...this.validateMongoDBConfig(config));
|
|
1628
|
+
break;
|
|
1629
|
+
case "milvus":
|
|
1630
|
+
errors.push(...this.validateMilvusConfig(config));
|
|
1631
|
+
break;
|
|
1632
|
+
case "qdrant":
|
|
1633
|
+
errors.push(...this.validateQdrantConfig(config));
|
|
1634
|
+
break;
|
|
1635
|
+
case "chromadb":
|
|
1636
|
+
errors.push(...this.validateChromaDBConfig(config));
|
|
1637
|
+
break;
|
|
1638
|
+
case "redis":
|
|
1639
|
+
errors.push(...this.validateRedisConfig(config));
|
|
1640
|
+
break;
|
|
1641
|
+
case "weaviate":
|
|
1642
|
+
errors.push(...this.validateWeaviateConfig(config));
|
|
1643
|
+
break;
|
|
1644
|
+
case "universal_rest":
|
|
1645
|
+
case "rest":
|
|
1646
|
+
errors.push(...this.validateRestConfig(config));
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
return errors;
|
|
1650
|
+
}
|
|
1651
|
+
static validatePineconeConfig(config) {
|
|
1652
|
+
const errors = [];
|
|
1653
|
+
const opts = config.options;
|
|
1654
|
+
if (!opts.apiKey) {
|
|
1655
|
+
errors.push({
|
|
1656
|
+
field: "vectorDb.options.apiKey",
|
|
1657
|
+
message: "Pinecone API key is required",
|
|
1658
|
+
suggestion: "Set PINECONE_API_KEY environment variable",
|
|
1659
|
+
severity: "error"
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
if (!opts.environment) {
|
|
1663
|
+
errors.push({
|
|
1664
|
+
field: "vectorDb.options.environment",
|
|
1665
|
+
message: "Pinecone environment is required",
|
|
1666
|
+
suggestion: 'Set PINECONE_ENVIRONMENT environment variable (e.g., "gcp-starter")',
|
|
1667
|
+
severity: "error"
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
return errors;
|
|
1671
|
+
}
|
|
1672
|
+
static validatePostgresConfig(config) {
|
|
1673
|
+
const errors = [];
|
|
1674
|
+
const opts = config.options;
|
|
1675
|
+
if (!opts.connectionString) {
|
|
1676
|
+
errors.push({
|
|
1677
|
+
field: "vectorDb.options.connectionString",
|
|
1678
|
+
message: "PostgreSQL connection string is required",
|
|
1679
|
+
suggestion: "Set PGVECTOR_CONNECTION_STRING environment variable",
|
|
1680
|
+
severity: "error"
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
return errors;
|
|
1684
|
+
}
|
|
1685
|
+
static validateMongoDBConfig(config) {
|
|
1686
|
+
const errors = [];
|
|
1687
|
+
const opts = config.options;
|
|
1688
|
+
if (!opts.uri) {
|
|
1689
|
+
errors.push({
|
|
1690
|
+
field: "vectorDb.options.uri",
|
|
1691
|
+
message: "MongoDB connection URI is required",
|
|
1692
|
+
suggestion: "Set MONGODB_URI environment variable",
|
|
1693
|
+
severity: "error"
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
if (!opts.database) {
|
|
1697
|
+
errors.push({
|
|
1698
|
+
field: "vectorDb.options.database",
|
|
1699
|
+
message: "MongoDB database name is required",
|
|
1700
|
+
suggestion: "Set MONGODB_DB environment variable",
|
|
1701
|
+
severity: "error"
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
if (!opts.collection) {
|
|
1705
|
+
errors.push({
|
|
1706
|
+
field: "vectorDb.options.collection",
|
|
1707
|
+
message: "MongoDB collection name is required",
|
|
1708
|
+
suggestion: "Set MONGODB_COLLECTION environment variable",
|
|
1709
|
+
severity: "error"
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
return errors;
|
|
1713
|
+
}
|
|
1714
|
+
static validateMilvusConfig(config) {
|
|
1715
|
+
const errors = [];
|
|
1716
|
+
const opts = config.options;
|
|
1717
|
+
if (!opts.host) {
|
|
1718
|
+
errors.push({
|
|
1719
|
+
field: "vectorDb.options.host",
|
|
1720
|
+
message: "Milvus host is required",
|
|
1721
|
+
suggestion: "Set MILVUS_HOST environment variable",
|
|
1722
|
+
severity: "error"
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
if (!opts.port) {
|
|
1726
|
+
errors.push({
|
|
1727
|
+
field: "vectorDb.options.port",
|
|
1728
|
+
message: "Milvus port is required",
|
|
1729
|
+
suggestion: "Set MILVUS_PORT environment variable (default: 19530)",
|
|
1730
|
+
severity: "error"
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
return errors;
|
|
1734
|
+
}
|
|
1735
|
+
static validateQdrantConfig(config) {
|
|
1736
|
+
const errors = [];
|
|
1737
|
+
const opts = config.options;
|
|
1738
|
+
if (!opts.baseUrl && !opts.url) {
|
|
1739
|
+
errors.push({
|
|
1740
|
+
field: "vectorDb.options.baseUrl",
|
|
1741
|
+
message: "Qdrant base URL is required",
|
|
1742
|
+
suggestion: 'Set QDRANT_URL environment variable (e.g., "http://localhost:6333")',
|
|
1743
|
+
severity: "error"
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return errors;
|
|
1747
|
+
}
|
|
1748
|
+
static validateChromaDBConfig(config) {
|
|
1749
|
+
const errors = [];
|
|
1750
|
+
const opts = config.options;
|
|
1751
|
+
if (!opts.host && !opts.path) {
|
|
1752
|
+
errors.push({
|
|
1753
|
+
field: "vectorDb.options.host",
|
|
1754
|
+
message: "ChromaDB host or path is required",
|
|
1755
|
+
suggestion: "Set CHROMADB_HOST or CHROMADB_PATH environment variable",
|
|
1756
|
+
severity: "error"
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
return errors;
|
|
1760
|
+
}
|
|
1761
|
+
static validateRedisConfig(config) {
|
|
1762
|
+
const errors = [];
|
|
1763
|
+
const opts = config.options;
|
|
1764
|
+
if (!opts.url && (!opts.host || !opts.port)) {
|
|
1765
|
+
errors.push({
|
|
1766
|
+
field: "vectorDb.options.url",
|
|
1767
|
+
message: "Redis connection URL or host:port is required",
|
|
1768
|
+
suggestion: "Set REDIS_URL or REDIS_HOST/REDIS_PORT environment variables",
|
|
1769
|
+
severity: "error"
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
return errors;
|
|
1773
|
+
}
|
|
1774
|
+
static validateWeaviateConfig(config) {
|
|
1775
|
+
const errors = [];
|
|
1776
|
+
const opts = config.options;
|
|
1777
|
+
if (!opts.url) {
|
|
1778
|
+
errors.push({
|
|
1779
|
+
field: "vectorDb.options.url",
|
|
1780
|
+
message: "Weaviate instance URL is required",
|
|
1781
|
+
suggestion: "Set WEAVIATE_URL environment variable",
|
|
1782
|
+
severity: "error"
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
return errors;
|
|
1786
|
+
}
|
|
1787
|
+
static validateRestConfig(config) {
|
|
1788
|
+
const errors = [];
|
|
1789
|
+
const opts = config.options;
|
|
1790
|
+
if (!opts.baseUrl) {
|
|
1791
|
+
errors.push({
|
|
1792
|
+
field: "vectorDb.options.baseUrl",
|
|
1793
|
+
message: "REST API base URL is required",
|
|
1794
|
+
suggestion: "Set VECTOR_BASE_URL environment variable",
|
|
1795
|
+
severity: "error"
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
return errors;
|
|
1799
|
+
}
|
|
1800
|
+
static validateLLMConfig(config) {
|
|
1801
|
+
var _a;
|
|
1802
|
+
const errors = [];
|
|
1803
|
+
if (!config.provider) {
|
|
1804
|
+
errors.push({
|
|
1805
|
+
field: "llm.provider",
|
|
1806
|
+
message: "LLM provider is required",
|
|
1807
|
+
severity: "error"
|
|
1808
|
+
});
|
|
1809
|
+
return errors;
|
|
1810
|
+
}
|
|
1811
|
+
if (!config.model) {
|
|
1812
|
+
errors.push({
|
|
1813
|
+
field: "llm.model",
|
|
1814
|
+
message: "LLM model name is required",
|
|
1815
|
+
suggestion: `e.g., "gpt-4o" for OpenAI, "claude-3-opus" for Anthropic`,
|
|
1816
|
+
severity: "error"
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
switch (config.provider) {
|
|
1820
|
+
case "openai":
|
|
1821
|
+
if (!config.apiKey) {
|
|
1822
|
+
errors.push({
|
|
1823
|
+
field: "llm.apiKey",
|
|
1824
|
+
message: "OpenAI API key is required",
|
|
1825
|
+
suggestion: "Set OPENAI_API_KEY environment variable",
|
|
1826
|
+
severity: "error"
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
break;
|
|
1830
|
+
case "anthropic":
|
|
1831
|
+
if (!config.apiKey) {
|
|
1832
|
+
errors.push({
|
|
1833
|
+
field: "llm.apiKey",
|
|
1834
|
+
message: "Anthropic API key is required",
|
|
1835
|
+
suggestion: "Set ANTHROPIC_API_KEY environment variable",
|
|
1836
|
+
severity: "error"
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
break;
|
|
1840
|
+
case "ollama":
|
|
1841
|
+
if (!config.baseUrl) {
|
|
1842
|
+
errors.push({
|
|
1843
|
+
field: "llm.baseUrl",
|
|
1844
|
+
message: "Ollama base URL is required",
|
|
1845
|
+
suggestion: 'Set LLM_BASE_URL environment variable (e.g., "http://localhost:11434")',
|
|
1846
|
+
severity: "error"
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
break;
|
|
1850
|
+
case "rest":
|
|
1851
|
+
case "universal_rest":
|
|
1852
|
+
if (!config.baseUrl && !((_a = config.options) == null ? void 0 : _a.baseUrl)) {
|
|
1853
|
+
errors.push({
|
|
1854
|
+
field: "llm.baseUrl",
|
|
1855
|
+
message: "REST API base URL is required",
|
|
1856
|
+
suggestion: "Set LLM_BASE_URL environment variable",
|
|
1857
|
+
severity: "error"
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
if (config.temperature !== void 0) {
|
|
1863
|
+
if (config.temperature < 0 || config.temperature > 2) {
|
|
1864
|
+
errors.push({
|
|
1865
|
+
field: "llm.temperature",
|
|
1866
|
+
message: "Temperature must be between 0 and 2",
|
|
1867
|
+
severity: "error"
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (config.maxTokens !== void 0 && config.maxTokens <= 0) {
|
|
1872
|
+
errors.push({
|
|
1873
|
+
field: "llm.maxTokens",
|
|
1874
|
+
message: "maxTokens must be greater than 0",
|
|
1875
|
+
severity: "error"
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
return errors;
|
|
1879
|
+
}
|
|
1880
|
+
static validateEmbeddingConfig(config) {
|
|
1881
|
+
const errors = [];
|
|
1882
|
+
if (!config.provider) {
|
|
1883
|
+
errors.push({
|
|
1884
|
+
field: "embedding.provider",
|
|
1885
|
+
message: "Embedding provider is required",
|
|
1886
|
+
severity: "error"
|
|
1887
|
+
});
|
|
1888
|
+
return errors;
|
|
1889
|
+
}
|
|
1890
|
+
if (!config.model) {
|
|
1891
|
+
errors.push({
|
|
1892
|
+
field: "embedding.model",
|
|
1893
|
+
message: "Embedding model name is required",
|
|
1894
|
+
suggestion: 'e.g., "text-embedding-3-small" for OpenAI',
|
|
1895
|
+
severity: "error"
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
if (config.provider === "openai" && !config.apiKey) {
|
|
1899
|
+
errors.push({
|
|
1900
|
+
field: "embedding.apiKey",
|
|
1901
|
+
message: "OpenAI API key is required for embedding",
|
|
1902
|
+
suggestion: "Set OPENAI_API_KEY environment variable",
|
|
1903
|
+
severity: "error"
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
if (config.provider === "ollama" && !config.baseUrl) {
|
|
1907
|
+
errors.push({
|
|
1908
|
+
field: "embedding.baseUrl",
|
|
1909
|
+
message: "Ollama base URL is required",
|
|
1910
|
+
suggestion: "Set EMBEDDING_BASE_URL environment variable",
|
|
1911
|
+
severity: "error"
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
if (config.dimensions !== void 0 && config.dimensions <= 0) {
|
|
1915
|
+
errors.push({
|
|
1916
|
+
field: "embedding.dimensions",
|
|
1917
|
+
message: "Embedding dimensions must be greater than 0",
|
|
1918
|
+
severity: "error"
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
return errors;
|
|
1922
|
+
}
|
|
1923
|
+
static validateUIConfig(config) {
|
|
1924
|
+
const errors = [];
|
|
1925
|
+
if (config.primaryColor) {
|
|
1926
|
+
if (!this.isValidCSSColor(config.primaryColor)) {
|
|
1927
|
+
errors.push({
|
|
1928
|
+
field: "ui.primaryColor",
|
|
1929
|
+
message: "Invalid CSS color format",
|
|
1930
|
+
suggestion: "Use valid CSS colors: hex (#FF0000), rgb, hsl, or named colors",
|
|
1931
|
+
severity: "warning"
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (config.borderRadius) {
|
|
1936
|
+
const validValues = ["none", "sm", "md", "lg", "xl", "full"];
|
|
1937
|
+
if (!validValues.includes(config.borderRadius)) {
|
|
1938
|
+
errors.push({
|
|
1939
|
+
field: "ui.borderRadius",
|
|
1940
|
+
message: `borderRadius must be one of: ${validValues.join(", ")}`,
|
|
1941
|
+
severity: "warning"
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
if (config.visualStyle) {
|
|
1946
|
+
const validValues = ["glass", "solid"];
|
|
1947
|
+
if (!validValues.includes(config.visualStyle)) {
|
|
1948
|
+
errors.push({
|
|
1949
|
+
field: "ui.visualStyle",
|
|
1950
|
+
message: `visualStyle must be one of: ${validValues.join(", ")}`,
|
|
1951
|
+
severity: "warning"
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return errors;
|
|
1956
|
+
}
|
|
1957
|
+
static validateRAGConfig(config) {
|
|
1958
|
+
const errors = [];
|
|
1959
|
+
if (config.topK !== void 0) {
|
|
1960
|
+
if (typeof config.topK !== "number" || config.topK <= 0) {
|
|
1961
|
+
errors.push({
|
|
1962
|
+
field: "rag.topK",
|
|
1963
|
+
message: "topK must be a positive integer",
|
|
1964
|
+
severity: "error"
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (config.scoreThreshold !== void 0) {
|
|
1969
|
+
if (typeof config.scoreThreshold !== "number" || config.scoreThreshold < 0 || config.scoreThreshold > 1) {
|
|
1970
|
+
errors.push({
|
|
1971
|
+
field: "rag.scoreThreshold",
|
|
1972
|
+
message: "scoreThreshold must be between 0 and 1",
|
|
1973
|
+
severity: "error"
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (config.chunkSize !== void 0) {
|
|
1978
|
+
if (typeof config.chunkSize !== "number" || config.chunkSize <= 0) {
|
|
1979
|
+
errors.push({
|
|
1980
|
+
field: "rag.chunkSize",
|
|
1981
|
+
message: "chunkSize must be a positive integer",
|
|
1982
|
+
severity: "error"
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (config.chunkOverlap !== void 0) {
|
|
1987
|
+
if (typeof config.chunkOverlap !== "number" || config.chunkOverlap < 0) {
|
|
1988
|
+
errors.push({
|
|
1989
|
+
field: "rag.chunkOverlap",
|
|
1990
|
+
message: "chunkOverlap must be a non-negative integer",
|
|
1991
|
+
severity: "error"
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return errors;
|
|
1996
|
+
}
|
|
1997
|
+
static isValidCSSColor(color) {
|
|
1998
|
+
const hexRegex = /^#([0-9a-f]{3}){1,2}$/i;
|
|
1999
|
+
const rgbRegex = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/i;
|
|
2000
|
+
const hslRegex = /^hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*(\d+(?:\.\d+)?))?\)$/i;
|
|
2001
|
+
const namedColors = ["red", "blue", "green", "black", "white", "transparent"];
|
|
2002
|
+
return hexRegex.test(color) || rgbRegex.test(color) || hslRegex.test(color) || namedColors.includes(color.toLowerCase());
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Throws if there are error-level validation issues.
|
|
2006
|
+
* Logs warnings to console.
|
|
2007
|
+
*/
|
|
2008
|
+
static validateAndThrow(config) {
|
|
2009
|
+
const errors = this.validate(config);
|
|
2010
|
+
const errorItems = errors.filter((e) => e.severity === "error");
|
|
2011
|
+
const warnings = errors.filter((e) => e.severity === "warning");
|
|
2012
|
+
if (warnings.length > 0) {
|
|
2013
|
+
console.warn("[ConfigValidator] Configuration warnings:");
|
|
2014
|
+
warnings.forEach((w) => {
|
|
2015
|
+
console.warn(` ${w.field}: ${w.message}`);
|
|
2016
|
+
if (w.suggestion) console.warn(` Suggestion: ${w.suggestion}`);
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
if (errorItems.length > 0) {
|
|
2020
|
+
const message = errorItems.map((e) => `${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n ");
|
|
2021
|
+
throw new Error(`[ConfigValidator] Configuration validation failed:
|
|
2022
|
+
${message}`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
|
|
1467
2027
|
// src/rag/DocumentChunker.ts
|
|
1468
2028
|
var DocumentChunker = class {
|
|
1469
2029
|
constructor(chunkSize = 1e3, chunkOverlap = 200) {
|
|
@@ -1579,6 +2139,230 @@ var ProviderRegistry = class {
|
|
|
1579
2139
|
};
|
|
1580
2140
|
ProviderRegistry.vectorProviders = {};
|
|
1581
2141
|
|
|
2142
|
+
// src/core/BatchProcessor.ts
|
|
2143
|
+
function isTransientError(error) {
|
|
2144
|
+
if (!(error instanceof Error)) return false;
|
|
2145
|
+
const message = error.message.toLowerCase();
|
|
2146
|
+
if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("timeout") || message.includes("network") || message.includes("socket")) {
|
|
2147
|
+
return true;
|
|
2148
|
+
}
|
|
2149
|
+
if (message.includes("429") || message.includes("503") || message.includes("502") || message.includes("rate limit") || message.includes("too many requests")) {
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
return false;
|
|
2153
|
+
}
|
|
2154
|
+
function calculateBackoffDelay(attempt, initialDelayMs, maxDelayMs, multiplier) {
|
|
2155
|
+
const exponentialDelay = Math.min(
|
|
2156
|
+
initialDelayMs * Math.pow(multiplier, attempt),
|
|
2157
|
+
maxDelayMs
|
|
2158
|
+
);
|
|
2159
|
+
const jitter = exponentialDelay * 0.1 * (Math.random() * 2 - 1);
|
|
2160
|
+
return Math.max(0, exponentialDelay + jitter);
|
|
2161
|
+
}
|
|
2162
|
+
function sleep(ms) {
|
|
2163
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2164
|
+
}
|
|
2165
|
+
var BatchProcessor = class {
|
|
2166
|
+
/**
|
|
2167
|
+
* Processes an array of items in configurable batches with retry logic.
|
|
2168
|
+
*
|
|
2169
|
+
* @param items - Items to process
|
|
2170
|
+
* @param processor - Async function that processes a batch of items
|
|
2171
|
+
* @param options - Configuration for batch size, retries, etc.
|
|
2172
|
+
* @returns Object with results, errors, and statistics
|
|
2173
|
+
*
|
|
2174
|
+
* @example
|
|
2175
|
+
* const docs = [...];
|
|
2176
|
+
* const result = await BatchProcessor.processBatch(
|
|
2177
|
+
* docs,
|
|
2178
|
+
* (batch) => vectorDB.batchUpsert(batch),
|
|
2179
|
+
* { batchSize: 100, maxRetries: 3 }
|
|
2180
|
+
* );
|
|
2181
|
+
*/
|
|
2182
|
+
static async processBatch(items, processor, options) {
|
|
2183
|
+
const {
|
|
2184
|
+
batchSize = 100,
|
|
2185
|
+
maxRetries = 3,
|
|
2186
|
+
initialDelayMs = 100,
|
|
2187
|
+
maxDelayMs = 1e4,
|
|
2188
|
+
backoffMultiplier = 2,
|
|
2189
|
+
throwOnPartialFailure = false
|
|
2190
|
+
} = options != null ? options : {};
|
|
2191
|
+
const results = [];
|
|
2192
|
+
const errors = [];
|
|
2193
|
+
const batches = [];
|
|
2194
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
2195
|
+
batches.push(items.slice(i, i + batchSize));
|
|
2196
|
+
}
|
|
2197
|
+
if (batches.length === 0) {
|
|
2198
|
+
return { results: [], errors: [], totalProcessed: 0, totalFailed: 0 };
|
|
2199
|
+
}
|
|
2200
|
+
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
|
2201
|
+
const batch = batches[batchIndex];
|
|
2202
|
+
let success = false;
|
|
2203
|
+
let lastError;
|
|
2204
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2205
|
+
try {
|
|
2206
|
+
const result = await processor(batch);
|
|
2207
|
+
results.push(result);
|
|
2208
|
+
success = true;
|
|
2209
|
+
break;
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2212
|
+
if (!isTransientError(err) || attempt === maxRetries) {
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
const delay = calculateBackoffDelay(
|
|
2216
|
+
attempt,
|
|
2217
|
+
initialDelayMs,
|
|
2218
|
+
maxDelayMs,
|
|
2219
|
+
backoffMultiplier
|
|
2220
|
+
);
|
|
2221
|
+
await sleep(delay);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
if (!success && lastError) {
|
|
2225
|
+
errors.push({
|
|
2226
|
+
index: batchIndex,
|
|
2227
|
+
error: lastError,
|
|
2228
|
+
itemCount: batch.length
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
const totalProcessed = items.length - (errors.length > 0 ? errors.reduce((sum, e) => sum + (e.itemCount || 0), 0) : 0);
|
|
2233
|
+
const totalFailed = errors.reduce((sum, e) => sum + (e.itemCount || 0), 0);
|
|
2234
|
+
if (throwOnPartialFailure && errors.length > 0) {
|
|
2235
|
+
const errorMessages = errors.map((e) => `Batch ${e.index} (${e.itemCount} items): ${e.error.message}`).join("\n");
|
|
2236
|
+
throw new Error(
|
|
2237
|
+
`[BatchProcessor] Batch processing failed:
|
|
2238
|
+
${errorMessages}`
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
return { results, errors, totalProcessed, totalFailed };
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Processes items sequentially (one at a time) with retry logic.
|
|
2245
|
+
* Useful for operations that don't support batching or when granular
|
|
2246
|
+
* error tracking is needed.
|
|
2247
|
+
*/
|
|
2248
|
+
static async processSequential(items, processor, options) {
|
|
2249
|
+
const {
|
|
2250
|
+
maxRetries = 3,
|
|
2251
|
+
initialDelayMs = 100,
|
|
2252
|
+
maxDelayMs = 1e4,
|
|
2253
|
+
backoffMultiplier = 2,
|
|
2254
|
+
throwOnPartialFailure = false
|
|
2255
|
+
} = options != null ? options : {};
|
|
2256
|
+
const results = [];
|
|
2257
|
+
const errors = [];
|
|
2258
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
2259
|
+
const item = items[itemIndex];
|
|
2260
|
+
let success = false;
|
|
2261
|
+
let lastError;
|
|
2262
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2263
|
+
try {
|
|
2264
|
+
const result = await processor(item);
|
|
2265
|
+
results.push(result);
|
|
2266
|
+
success = true;
|
|
2267
|
+
break;
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2270
|
+
if (!isTransientError(err) || attempt === maxRetries) {
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
const delay = calculateBackoffDelay(
|
|
2274
|
+
attempt,
|
|
2275
|
+
initialDelayMs,
|
|
2276
|
+
maxDelayMs,
|
|
2277
|
+
backoffMultiplier
|
|
2278
|
+
);
|
|
2279
|
+
await sleep(delay);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (!success && lastError) {
|
|
2283
|
+
errors.push({ index: itemIndex, error: lastError, itemCount: 1 });
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const totalProcessed = items.length - errors.length;
|
|
2287
|
+
const totalFailed = errors.length;
|
|
2288
|
+
if (throwOnPartialFailure && errors.length > 0) {
|
|
2289
|
+
const errorMessages = errors.map((e) => `Item ${e.index}: ${e.error.message}`).join("\n");
|
|
2290
|
+
throw new Error(
|
|
2291
|
+
`[BatchProcessor] Sequential processing failed:
|
|
2292
|
+
${errorMessages}`
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
return { results, errors, totalProcessed, totalFailed };
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Maps over items with retry logic, returning results in order.
|
|
2299
|
+
* Like Array.map() but with async processing and automatic retries.
|
|
2300
|
+
*/
|
|
2301
|
+
static async mapWithRetry(items, mapper, options) {
|
|
2302
|
+
const result = await this.processSequential(items, mapper, options);
|
|
2303
|
+
if (result.errors.length > 0) {
|
|
2304
|
+
console.warn(
|
|
2305
|
+
`[BatchProcessor] mapWithRetry: ${result.errors.length} items failed`
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
const orderedResults = new Array(items.length);
|
|
2309
|
+
let resultIndex = 0;
|
|
2310
|
+
let errorIndex = 0;
|
|
2311
|
+
for (let i = 0; i < items.length; i++) {
|
|
2312
|
+
if (errorIndex < result.errors.length && result.errors[errorIndex].index === i) {
|
|
2313
|
+
orderedResults[i] = void 0;
|
|
2314
|
+
errorIndex++;
|
|
2315
|
+
} else {
|
|
2316
|
+
orderedResults[i] = result.results[resultIndex];
|
|
2317
|
+
resultIndex++;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return orderedResults.filter((r) => r !== void 0);
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Parallel processor with concurrency limit.
|
|
2324
|
+
* Processes up to `concurrency` items at the same time.
|
|
2325
|
+
*/
|
|
2326
|
+
static async processConcurrent(items, processor, concurrency = 5, options) {
|
|
2327
|
+
const { throwOnPartialFailure = false } = options != null ? options : {};
|
|
2328
|
+
const results = [];
|
|
2329
|
+
const errors = [];
|
|
2330
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
2331
|
+
const chunk = items.slice(i, i + concurrency).map((item, idx) => ({
|
|
2332
|
+
item,
|
|
2333
|
+
originalIndex: i + idx
|
|
2334
|
+
}));
|
|
2335
|
+
const chunkPromises = chunk.map(async ({ item, originalIndex }) => {
|
|
2336
|
+
try {
|
|
2337
|
+
const result = await processor(item);
|
|
2338
|
+
return { success: true, result, index: originalIndex };
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2341
|
+
return { success: false, error, index: originalIndex };
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
const chunkResults = await Promise.all(chunkPromises);
|
|
2345
|
+
for (const { success, result, error, index } of chunkResults) {
|
|
2346
|
+
if (success) {
|
|
2347
|
+
results.push(result);
|
|
2348
|
+
} else {
|
|
2349
|
+
errors.push({ index, error, itemCount: 1 });
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
const totalProcessed = items.length - errors.length;
|
|
2354
|
+
const totalFailed = errors.length;
|
|
2355
|
+
if (throwOnPartialFailure && errors.length > 0) {
|
|
2356
|
+
const errorMessages = errors.map((e) => `Item ${e.index}: ${e.error.message}`).join("\n");
|
|
2357
|
+
throw new Error(
|
|
2358
|
+
`[BatchProcessor] Concurrent processing failed:
|
|
2359
|
+
${errorMessages}`
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
return { results, errors, totalProcessed, totalFailed };
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
|
|
1582
2366
|
// src/core/Pipeline.ts
|
|
1583
2367
|
var Pipeline = class {
|
|
1584
2368
|
constructor(config) {
|
|
@@ -1603,24 +2387,64 @@ var Pipeline = class {
|
|
|
1603
2387
|
await this.vectorDB.initialize();
|
|
1604
2388
|
this.initialised = true;
|
|
1605
2389
|
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Ingest documents with automatic chunking, embedding, and batch processing.
|
|
2392
|
+
* Handles retries for transient failures.
|
|
2393
|
+
*/
|
|
1606
2394
|
async ingest(documents, namespace) {
|
|
1607
2395
|
await this.initialize();
|
|
1608
2396
|
const ns = namespace != null ? namespace : this.config.projectId;
|
|
1609
2397
|
const results = [];
|
|
1610
2398
|
for (const doc of documents) {
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2399
|
+
try {
|
|
2400
|
+
const chunks = this.chunker.chunk(doc.content, {
|
|
2401
|
+
docId: doc.docId,
|
|
2402
|
+
metadata: doc.metadata
|
|
2403
|
+
});
|
|
2404
|
+
const batchOptions = {
|
|
2405
|
+
batchSize: 50,
|
|
2406
|
+
// Embedding batch size
|
|
2407
|
+
maxRetries: 3,
|
|
2408
|
+
initialDelayMs: 100
|
|
2409
|
+
};
|
|
2410
|
+
const vectors = await BatchProcessor.mapWithRetry(
|
|
2411
|
+
chunks.map((c) => c.content),
|
|
2412
|
+
(text) => this.embeddingProvider.embed(text),
|
|
2413
|
+
batchOptions
|
|
2414
|
+
);
|
|
2415
|
+
if (vectors.length !== chunks.length) {
|
|
2416
|
+
throw new Error(`Embedding failed: got ${vectors.length} vectors for ${chunks.length} chunks`);
|
|
2417
|
+
}
|
|
2418
|
+
const upsertDocs = chunks.map((chunk, i) => ({
|
|
2419
|
+
id: chunk.id,
|
|
2420
|
+
vector: vectors[i],
|
|
2421
|
+
content: chunk.content,
|
|
2422
|
+
metadata: chunk.metadata
|
|
2423
|
+
}));
|
|
2424
|
+
const upsertBatchOptions = {
|
|
2425
|
+
batchSize: 100,
|
|
2426
|
+
maxRetries: 3,
|
|
2427
|
+
initialDelayMs: 100
|
|
2428
|
+
};
|
|
2429
|
+
const upsertResult = await BatchProcessor.processBatch(
|
|
2430
|
+
upsertDocs,
|
|
2431
|
+
(batch) => this.vectorDB.batchUpsert(batch, ns),
|
|
2432
|
+
upsertBatchOptions
|
|
2433
|
+
);
|
|
2434
|
+
if (upsertResult.errors.length > 0) {
|
|
2435
|
+
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed for doc ${doc.docId}`);
|
|
2436
|
+
}
|
|
2437
|
+
results.push({
|
|
2438
|
+
docId: doc.docId,
|
|
2439
|
+
chunksIngested: upsertResult.totalProcessed
|
|
2440
|
+
});
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
console.error(`[Pipeline] Failed to ingest document ${doc.docId}:`, error);
|
|
2443
|
+
results.push({
|
|
2444
|
+
docId: doc.docId,
|
|
2445
|
+
chunksIngested: 0
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
1624
2448
|
}
|
|
1625
2449
|
return results;
|
|
1626
2450
|
}
|
|
@@ -1630,22 +2454,486 @@ var Pipeline = class {
|
|
|
1630
2454
|
const ns = namespace != null ? namespace : this.config.projectId;
|
|
1631
2455
|
const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
|
|
1632
2456
|
const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
2457
|
+
try {
|
|
2458
|
+
const queryVector = await this.embeddingProvider.embed(question);
|
|
2459
|
+
const rawMatches = await this.vectorDB.query(queryVector, topK, ns);
|
|
2460
|
+
const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
|
|
2461
|
+
const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
1637
2462
|
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
2463
|
+
const messages = [...history, { role: "user", content: question }];
|
|
2464
|
+
const reply = await this.llmProvider.chat(messages, context);
|
|
2465
|
+
return { reply, sources };
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
throw new Error(`[Pipeline] Chat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2468
|
+
}
|
|
1641
2469
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
2470
|
+
};
|
|
2471
|
+
|
|
2472
|
+
// src/core/ProviderHealthCheck.ts
|
|
2473
|
+
var ProviderHealthCheck = class {
|
|
2474
|
+
/**
|
|
2475
|
+
* Validates vector database configuration before initialization.
|
|
2476
|
+
* Performs connectivity checks and verifies required resources exist.
|
|
2477
|
+
*/
|
|
2478
|
+
static async checkVectorProvider(config) {
|
|
2479
|
+
const timestamp = Date.now();
|
|
2480
|
+
const configErrors = ConfigValidator.validate({
|
|
2481
|
+
projectId: "health-check",
|
|
2482
|
+
vectorDb: config,
|
|
2483
|
+
llm: { provider: "openai", model: "gpt-4o" },
|
|
2484
|
+
// dummy
|
|
2485
|
+
embedding: { provider: "openai", model: "text-embedding-3-small" }
|
|
2486
|
+
});
|
|
2487
|
+
const vectorDbErrors = configErrors.filter((e) => e.field.startsWith("vectorDb"));
|
|
2488
|
+
if (vectorDbErrors.length > 0) {
|
|
2489
|
+
return {
|
|
2490
|
+
healthy: false,
|
|
2491
|
+
provider: config.provider,
|
|
2492
|
+
error: `Configuration validation failed: ${vectorDbErrors.map((e) => e.message).join("; ")}`,
|
|
2493
|
+
timestamp
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
try {
|
|
2497
|
+
switch (config.provider) {
|
|
2498
|
+
case "pinecone":
|
|
2499
|
+
return await this.checkPinecone(config, timestamp);
|
|
2500
|
+
case "pgvector":
|
|
2501
|
+
case "postgresql":
|
|
2502
|
+
return await this.checkPostgres(config, timestamp);
|
|
2503
|
+
case "mongodb":
|
|
2504
|
+
return await this.checkMongoDB(config, timestamp);
|
|
2505
|
+
case "milvus":
|
|
2506
|
+
return await this.checkMilvus(config, timestamp);
|
|
2507
|
+
case "qdrant":
|
|
2508
|
+
return await this.checkQdrant(config, timestamp);
|
|
2509
|
+
case "chromadb":
|
|
2510
|
+
return await this.checkChromaDB(config, timestamp);
|
|
2511
|
+
case "redis":
|
|
2512
|
+
return await this.checkRedis(config, timestamp);
|
|
2513
|
+
case "weaviate":
|
|
2514
|
+
return await this.checkWeaviate(config, timestamp);
|
|
2515
|
+
case "rest":
|
|
2516
|
+
case "universal_rest":
|
|
2517
|
+
return await this.checkRestAPI(config, timestamp);
|
|
2518
|
+
default:
|
|
2519
|
+
return {
|
|
2520
|
+
healthy: false,
|
|
2521
|
+
provider: config.provider,
|
|
2522
|
+
error: `Unsupported provider: ${config.provider}`,
|
|
2523
|
+
timestamp
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
return {
|
|
2528
|
+
healthy: false,
|
|
2529
|
+
provider: config.provider,
|
|
2530
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2531
|
+
timestamp
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
static async checkPinecone(config, timestamp) {
|
|
2536
|
+
var _a, _b;
|
|
2537
|
+
const opts = config.options;
|
|
2538
|
+
try {
|
|
2539
|
+
const { Pinecone: Pinecone2 } = await import("@pinecone-database/pinecone");
|
|
2540
|
+
const client = new Pinecone2({ apiKey: opts.apiKey });
|
|
2541
|
+
const indexes = await client.listIndexes();
|
|
2542
|
+
const indexNames = (_b = (_a = indexes.indexes) == null ? void 0 : _a.map((i) => i.name)) != null ? _b : [];
|
|
2543
|
+
if (!indexNames.includes(config.indexName)) {
|
|
2544
|
+
return {
|
|
2545
|
+
healthy: false,
|
|
2546
|
+
provider: "pinecone",
|
|
2547
|
+
error: `Index "${config.indexName}" not found. Available: ${indexNames.join(", ")}`,
|
|
2548
|
+
timestamp
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
return {
|
|
2552
|
+
healthy: true,
|
|
2553
|
+
provider: "pinecone",
|
|
2554
|
+
capabilities: {
|
|
2555
|
+
indexes: indexNames.length,
|
|
2556
|
+
targetIndex: config.indexName
|
|
2557
|
+
},
|
|
2558
|
+
timestamp
|
|
2559
|
+
};
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
return {
|
|
2562
|
+
healthy: false,
|
|
2563
|
+
provider: "pinecone",
|
|
2564
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2565
|
+
timestamp
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
static async checkPostgres(config, timestamp) {
|
|
2570
|
+
const opts = config.options;
|
|
2571
|
+
try {
|
|
2572
|
+
const { Client } = await import("pg");
|
|
2573
|
+
const client = new Client({ connectionString: opts.connectionString });
|
|
2574
|
+
await client.connect();
|
|
2575
|
+
const result = await client.query(`
|
|
2576
|
+
SELECT EXISTS(
|
|
2577
|
+
SELECT 1 FROM pg_extension WHERE extname = 'vector'
|
|
2578
|
+
);
|
|
2579
|
+
`);
|
|
2580
|
+
const hasVector = result.rows[0].exists;
|
|
2581
|
+
await client.end();
|
|
2582
|
+
return {
|
|
2583
|
+
healthy: true,
|
|
2584
|
+
provider: "postgresql",
|
|
2585
|
+
capabilities: { pgvectorInstalled: hasVector },
|
|
2586
|
+
timestamp
|
|
2587
|
+
};
|
|
2588
|
+
} catch (error) {
|
|
2589
|
+
return {
|
|
2590
|
+
healthy: false,
|
|
2591
|
+
provider: "postgresql",
|
|
2592
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2593
|
+
timestamp
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
static async checkMongoDB(config, timestamp) {
|
|
2598
|
+
const opts = config.options;
|
|
2599
|
+
try {
|
|
2600
|
+
const { MongoClient: MongoClient2 } = await import("mongodb");
|
|
2601
|
+
const client = new MongoClient2(opts.uri);
|
|
2602
|
+
await client.connect();
|
|
2603
|
+
const db = client.db(opts.database);
|
|
2604
|
+
const collections = await db.listCollections().toArray();
|
|
2605
|
+
const collectionNames = collections.map((c) => c.name);
|
|
2606
|
+
const hasCollection = collectionNames.includes(opts.collection);
|
|
2607
|
+
await client.close();
|
|
2608
|
+
return {
|
|
2609
|
+
healthy: true,
|
|
2610
|
+
provider: "mongodb",
|
|
2611
|
+
capabilities: {
|
|
2612
|
+
collections: collectionNames.length,
|
|
2613
|
+
targetCollection: hasCollection ? opts.collection : "NOT FOUND"
|
|
2614
|
+
},
|
|
2615
|
+
timestamp
|
|
2616
|
+
};
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
return {
|
|
2619
|
+
healthy: false,
|
|
2620
|
+
provider: "mongodb",
|
|
2621
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2622
|
+
timestamp
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
static async checkMilvus(config, timestamp) {
|
|
2627
|
+
const opts = config.options;
|
|
2628
|
+
const host = opts.host || "localhost";
|
|
2629
|
+
const port = opts.port || 19530;
|
|
2630
|
+
try {
|
|
2631
|
+
const controller = new AbortController();
|
|
2632
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
2633
|
+
const response = await fetch(`http://${host}:${port}/healthz`, {
|
|
2634
|
+
signal: controller.signal
|
|
2635
|
+
});
|
|
2636
|
+
clearTimeout(timeoutId);
|
|
2637
|
+
if (!response.ok) {
|
|
2638
|
+
throw new Error(`Health check returned ${response.status}`);
|
|
2639
|
+
}
|
|
2640
|
+
return {
|
|
2641
|
+
healthy: true,
|
|
2642
|
+
provider: "milvus",
|
|
2643
|
+
capabilities: { endpoint: `${host}:${port}` },
|
|
2644
|
+
timestamp
|
|
2645
|
+
};
|
|
2646
|
+
} catch (error) {
|
|
2647
|
+
return {
|
|
2648
|
+
healthy: false,
|
|
2649
|
+
provider: "milvus",
|
|
2650
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2651
|
+
timestamp
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
static async checkQdrant(config, timestamp) {
|
|
2656
|
+
const opts = config.options;
|
|
2657
|
+
const baseUrl = opts.baseUrl || opts.url || "http://localhost:6333";
|
|
2658
|
+
try {
|
|
2659
|
+
const apiKey = opts.apiKey;
|
|
2660
|
+
const response = await fetch(`${baseUrl}/`, {
|
|
2661
|
+
headers: apiKey ? { "api-key": apiKey } : {}
|
|
2662
|
+
});
|
|
2663
|
+
if (!response.ok) {
|
|
2664
|
+
throw new Error(`Health check returned ${response.status}`);
|
|
2665
|
+
}
|
|
2666
|
+
const health = await response.json();
|
|
2667
|
+
return {
|
|
2668
|
+
healthy: true,
|
|
2669
|
+
provider: "qdrant",
|
|
2670
|
+
capabilities: health,
|
|
2671
|
+
timestamp
|
|
2672
|
+
};
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
return {
|
|
2675
|
+
healthy: false,
|
|
2676
|
+
provider: "qdrant",
|
|
2677
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2678
|
+
timestamp
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
static async checkChromaDB(config, timestamp) {
|
|
2683
|
+
const opts = config.options;
|
|
2684
|
+
const host = opts.host || "localhost";
|
|
2685
|
+
const port = opts.port || 8e3;
|
|
2686
|
+
try {
|
|
2687
|
+
const response = await fetch(`http://${host}:${port}/api/v1/heartbeat`);
|
|
2688
|
+
if (!response.ok) {
|
|
2689
|
+
throw new Error(`Health check returned ${response.status}`);
|
|
2690
|
+
}
|
|
2691
|
+
return {
|
|
2692
|
+
healthy: true,
|
|
2693
|
+
provider: "chromadb",
|
|
2694
|
+
capabilities: { endpoint: `${host}:${port}` },
|
|
2695
|
+
timestamp
|
|
2696
|
+
};
|
|
2697
|
+
} catch (error) {
|
|
2698
|
+
return {
|
|
2699
|
+
healthy: false,
|
|
2700
|
+
provider: "chromadb",
|
|
2701
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2702
|
+
timestamp
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
static async checkRedis(config, timestamp) {
|
|
2707
|
+
const opts = config.options;
|
|
2708
|
+
const url = opts.url || `redis://${opts.host}:${opts.port}`;
|
|
2709
|
+
try {
|
|
2710
|
+
await fetch(url);
|
|
2711
|
+
return {
|
|
2712
|
+
healthy: true,
|
|
2713
|
+
provider: "redis",
|
|
2714
|
+
capabilities: { endpoint: url },
|
|
2715
|
+
timestamp
|
|
2716
|
+
};
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
return {
|
|
2719
|
+
healthy: false,
|
|
2720
|
+
provider: "redis",
|
|
2721
|
+
error: `Connection check failed: ${error instanceof Error ? error.message : String(error)}. Ensure Redis is running at ${url}`,
|
|
2722
|
+
timestamp
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
static async checkWeaviate(config, timestamp) {
|
|
2727
|
+
const opts = config.options;
|
|
2728
|
+
const url = (opts.url || "http://localhost:8080").replace(/\/$/, "");
|
|
2729
|
+
try {
|
|
2730
|
+
const response = await fetch(`${url}/.well-known/ready`);
|
|
2731
|
+
if (!response.ok) {
|
|
2732
|
+
throw new Error(`Health check returned ${response.status}`);
|
|
2733
|
+
}
|
|
2734
|
+
return {
|
|
2735
|
+
healthy: true,
|
|
2736
|
+
provider: "weaviate",
|
|
2737
|
+
capabilities: { endpoint: url },
|
|
2738
|
+
timestamp
|
|
2739
|
+
};
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
return {
|
|
2742
|
+
healthy: false,
|
|
2743
|
+
provider: "weaviate",
|
|
2744
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2745
|
+
timestamp
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
static async checkRestAPI(config, timestamp) {
|
|
2750
|
+
const opts = config.options;
|
|
2751
|
+
const baseUrl = (opts.baseUrl || "").replace(/\/$/, "");
|
|
2752
|
+
if (!baseUrl) {
|
|
2753
|
+
return {
|
|
2754
|
+
healthy: false,
|
|
2755
|
+
provider: "rest",
|
|
2756
|
+
error: "baseUrl is required",
|
|
2757
|
+
timestamp
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
try {
|
|
2761
|
+
const response = await fetch(`${baseUrl}/health`, {
|
|
2762
|
+
headers: opts.headers ? JSON.parse(opts.headers) : {}
|
|
2763
|
+
});
|
|
2764
|
+
return {
|
|
2765
|
+
healthy: response.ok,
|
|
2766
|
+
provider: "rest",
|
|
2767
|
+
error: response.ok ? void 0 : `Health check returned ${response.status}`,
|
|
2768
|
+
timestamp
|
|
2769
|
+
};
|
|
2770
|
+
} catch (error) {
|
|
2771
|
+
return {
|
|
2772
|
+
healthy: false,
|
|
2773
|
+
provider: "rest",
|
|
2774
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2775
|
+
timestamp
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Validates LLM provider configuration.
|
|
2781
|
+
*/
|
|
2782
|
+
static async checkLLMProvider(config) {
|
|
2783
|
+
const timestamp = Date.now();
|
|
2784
|
+
try {
|
|
2785
|
+
switch (config.provider) {
|
|
2786
|
+
case "openai":
|
|
2787
|
+
return await this.checkOpenAI(config, timestamp);
|
|
2788
|
+
case "anthropic":
|
|
2789
|
+
return await this.checkAnthropic(config, timestamp);
|
|
2790
|
+
case "ollama":
|
|
2791
|
+
return await this.checkOllama(config, timestamp);
|
|
2792
|
+
case "rest":
|
|
2793
|
+
case "universal_rest":
|
|
2794
|
+
return await this.checkLLMRestAPI(config, timestamp);
|
|
2795
|
+
default:
|
|
2796
|
+
return {
|
|
2797
|
+
healthy: false,
|
|
2798
|
+
provider: config.provider,
|
|
2799
|
+
error: `Unsupported provider: ${config.provider}`,
|
|
2800
|
+
timestamp
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
} catch (error) {
|
|
2804
|
+
return {
|
|
2805
|
+
healthy: false,
|
|
2806
|
+
provider: config.provider,
|
|
2807
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2808
|
+
timestamp
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
static async checkOpenAI(config, timestamp) {
|
|
2813
|
+
try {
|
|
2814
|
+
const OpenAI2 = await import("openai");
|
|
2815
|
+
const client = new OpenAI2.default({ apiKey: config.apiKey });
|
|
2816
|
+
const models = await client.models.list();
|
|
2817
|
+
const hasModel = models.data.some((m) => m.id === config.model);
|
|
2818
|
+
return {
|
|
2819
|
+
healthy: true,
|
|
2820
|
+
provider: "openai",
|
|
2821
|
+
capabilities: {
|
|
2822
|
+
model: config.model,
|
|
2823
|
+
available: hasModel,
|
|
2824
|
+
totalModels: models.data.length
|
|
2825
|
+
},
|
|
2826
|
+
timestamp
|
|
2827
|
+
};
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
return {
|
|
2830
|
+
healthy: false,
|
|
2831
|
+
provider: "openai",
|
|
2832
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2833
|
+
timestamp
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
static async checkAnthropic(config, timestamp) {
|
|
2838
|
+
try {
|
|
2839
|
+
const { default: Anthropic2 } = await import("@anthropic-ai/sdk");
|
|
2840
|
+
const client = new Anthropic2({ apiKey: config.apiKey });
|
|
2841
|
+
await client.messages.create({
|
|
2842
|
+
model: config.model,
|
|
2843
|
+
max_tokens: 10,
|
|
2844
|
+
messages: [{ role: "user", content: "ping" }]
|
|
2845
|
+
});
|
|
2846
|
+
return {
|
|
2847
|
+
healthy: true,
|
|
2848
|
+
provider: "anthropic",
|
|
2849
|
+
capabilities: { model: config.model },
|
|
2850
|
+
timestamp
|
|
2851
|
+
};
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
return {
|
|
2854
|
+
healthy: false,
|
|
2855
|
+
provider: "anthropic",
|
|
2856
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2857
|
+
timestamp
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
static async checkOllama(config, timestamp) {
|
|
2862
|
+
const baseUrl = (config.baseUrl || "http://localhost:11434").replace(/\/$/, "");
|
|
2863
|
+
try {
|
|
2864
|
+
const response = await fetch(`${baseUrl}/api/tags`);
|
|
2865
|
+
if (!response.ok) {
|
|
2866
|
+
throw new Error(`Health check returned ${response.status}`);
|
|
2867
|
+
}
|
|
2868
|
+
const data = await response.json();
|
|
2869
|
+
const models = data.models || [];
|
|
2870
|
+
const hasModel = models.some((m) => m.name === config.model);
|
|
2871
|
+
return {
|
|
2872
|
+
healthy: true,
|
|
2873
|
+
provider: "ollama",
|
|
2874
|
+
capabilities: {
|
|
2875
|
+
model: config.model,
|
|
2876
|
+
available: hasModel,
|
|
2877
|
+
totalModels: models.length
|
|
2878
|
+
},
|
|
2879
|
+
timestamp
|
|
2880
|
+
};
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
return {
|
|
2883
|
+
healthy: false,
|
|
2884
|
+
provider: "ollama",
|
|
2885
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2886
|
+
timestamp
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
static async checkLLMRestAPI(config, timestamp) {
|
|
2891
|
+
var _a, _b;
|
|
2892
|
+
const baseUrlRaw = config.baseUrl || ((_a = config.options) == null ? void 0 : _a.baseUrl);
|
|
2893
|
+
const baseUrl = (typeof baseUrlRaw === "string" ? baseUrlRaw : "").replace(/\/$/, "");
|
|
2894
|
+
if (!baseUrl) {
|
|
2895
|
+
return {
|
|
2896
|
+
healthy: false,
|
|
2897
|
+
provider: config.provider,
|
|
2898
|
+
error: "baseUrl is required",
|
|
2899
|
+
timestamp
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
try {
|
|
2903
|
+
const headers = (_b = config.options) == null ? void 0 : _b.headers;
|
|
2904
|
+
const response = await fetch(`${baseUrl}/health`, {
|
|
2905
|
+
headers: headers || void 0
|
|
2906
|
+
});
|
|
2907
|
+
return {
|
|
2908
|
+
healthy: response.ok,
|
|
2909
|
+
provider: config.provider,
|
|
2910
|
+
error: response.ok ? void 0 : `Health check returned ${response.status}`,
|
|
2911
|
+
timestamp
|
|
2912
|
+
};
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
return {
|
|
2915
|
+
healthy: false,
|
|
2916
|
+
provider: config.provider,
|
|
2917
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2918
|
+
timestamp
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Runs comprehensive health checks on all configured providers.
|
|
2924
|
+
*/
|
|
2925
|
+
static async checkAll(vectorDbConfig, llmConfig, embeddingConfig) {
|
|
2926
|
+
const [vectorDb, llm, embedding] = await Promise.all([
|
|
2927
|
+
this.checkVectorProvider(vectorDbConfig),
|
|
2928
|
+
this.checkLLMProvider(llmConfig),
|
|
2929
|
+
embeddingConfig ? this.checkLLMProvider(embeddingConfig) : Promise.resolve(void 0)
|
|
1647
2930
|
]);
|
|
1648
|
-
return {
|
|
2931
|
+
return {
|
|
2932
|
+
vectorDb,
|
|
2933
|
+
llm,
|
|
2934
|
+
embedding,
|
|
2935
|
+
allHealthy: vectorDb.healthy && llm.healthy && (!embedding || embedding.healthy)
|
|
2936
|
+
};
|
|
1649
2937
|
}
|
|
1650
2938
|
};
|
|
1651
2939
|
|
|
@@ -1654,10 +2942,26 @@ var VectorPlugin = class {
|
|
|
1654
2942
|
/**
|
|
1655
2943
|
* Initializes the plugin with the host configuration.
|
|
1656
2944
|
* @param hostConfig - Configuration object passed from the host application.
|
|
2945
|
+
* @throws Error if configuration is invalid or providers are unhealthy
|
|
2946
|
+
*
|
|
2947
|
+
* @example
|
|
2948
|
+
* const plugin = new VectorPlugin({
|
|
2949
|
+
* projectId: 'my-app',
|
|
2950
|
+
* vectorDb: {
|
|
2951
|
+
* provider: 'pinecone',
|
|
2952
|
+
* indexName: 'my-index',
|
|
2953
|
+
* options: { apiKey: process.env.PINECONE_API_KEY }
|
|
2954
|
+
* },
|
|
2955
|
+
* llm: {
|
|
2956
|
+
* provider: 'openai',
|
|
2957
|
+
* model: 'gpt-4o',
|
|
2958
|
+
* apiKey: process.env.OPENAI_API_KEY
|
|
2959
|
+
* }
|
|
2960
|
+
* });
|
|
1657
2961
|
*/
|
|
1658
2962
|
constructor(hostConfig) {
|
|
1659
2963
|
this.config = ConfigResolver.resolve(hostConfig);
|
|
1660
|
-
|
|
2964
|
+
ConfigValidator.validateAndThrow(this.config);
|
|
1661
2965
|
this.pipeline = new Pipeline(this.config);
|
|
1662
2966
|
}
|
|
1663
2967
|
/**
|
|
@@ -1666,6 +2970,19 @@ var VectorPlugin = class {
|
|
|
1666
2970
|
getConfig() {
|
|
1667
2971
|
return this.config;
|
|
1668
2972
|
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Perform pre-flight health checks on all configured providers.
|
|
2975
|
+
* Useful to verify connectivity before running operations.
|
|
2976
|
+
*
|
|
2977
|
+
* @returns Health status for vector DB, LLM, and embedding providers
|
|
2978
|
+
*/
|
|
2979
|
+
async checkHealth() {
|
|
2980
|
+
return ProviderHealthCheck.checkAll(
|
|
2981
|
+
this.config.vectorDb,
|
|
2982
|
+
this.config.llm,
|
|
2983
|
+
this.config.embedding
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
1669
2986
|
/**
|
|
1670
2987
|
* Run a chat query.
|
|
1671
2988
|
*/
|
|
@@ -1678,12 +2995,6 @@ var VectorPlugin = class {
|
|
|
1678
2995
|
async ingest(documents, namespace) {
|
|
1679
2996
|
return this.pipeline.ingest(documents, namespace);
|
|
1680
2997
|
}
|
|
1681
|
-
/**
|
|
1682
|
-
* Check the health of the connected providers.
|
|
1683
|
-
*/
|
|
1684
|
-
async health() {
|
|
1685
|
-
return this.pipeline.health();
|
|
1686
|
-
}
|
|
1687
2998
|
};
|
|
1688
2999
|
|
|
1689
3000
|
// src/utils/DocumentParser.ts
|
|
@@ -1784,8 +3095,8 @@ function createHealthHandler(config) {
|
|
|
1784
3095
|
const plugin = new VectorPlugin(config);
|
|
1785
3096
|
return async function GET() {
|
|
1786
3097
|
try {
|
|
1787
|
-
const health = await plugin.
|
|
1788
|
-
const status = health.
|
|
3098
|
+
const health = await plugin.checkHealth();
|
|
3099
|
+
const status = health.allHealthy ? "ok" : "degraded";
|
|
1789
3100
|
return import_server.NextResponse.json(__spreadProps(__spreadValues({
|
|
1790
3101
|
status
|
|
1791
3102
|
}, health), {
|