@retrivora-ai/rag-engine 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handlers/index.js +142 -16
- package/dist/handlers/index.mjs +5848 -17
- package/dist/index.css +3162 -0
- package/dist/index.d.mts +9 -15
- package/dist/index.d.ts +9 -15
- package/dist/index.js +1355 -919
- package/dist/index.mjs +1437 -896
- package/dist/server.d.mts +6 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +171 -17
- package/dist/server.mjs +5923 -68
- package/package.json +8 -6
- package/src/app/globals.css +35 -11
- package/src/components/ChatWidget.tsx +0 -1
- package/src/components/MarkdownComponents.tsx +3 -0
- package/src/components/MessageBubble.tsx +3 -2
- package/src/components/ObservabilityPanel.tsx +1 -1
- package/src/components/UIDispatcher.tsx +0 -3
- package/src/config/ConfigBuilder.ts +38 -1
- package/src/core/LangChainAgent.ts +1 -4
- package/src/core/Pipeline.ts +31 -18
- package/src/core/QueryProcessor.ts +65 -0
- package/src/rag/Reranker.ts +99 -6
- package/src/tailwind.css +2 -0
- package/src/utils/ProductExtractor.ts +3 -3
- package/dist/ChromaDBProvider-MIDOR4FW.mjs +0 -8
- package/dist/MilvusProvider-U7SKC27V.mjs +0 -8
- package/dist/MongoDBProvider-YNKC7EJ6.mjs +0 -8
- package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +0 -8
- package/dist/PineconeProvider-QZNRKTN2.mjs +0 -8
- package/dist/QdrantProvider-RLJTNGPY.mjs +0 -8
- package/dist/RedisProvider-SR65SCKV.mjs +0 -8
- package/dist/SimpleGraphProvider-SLOXO4M7.mjs +0 -62
- package/dist/UniversalVectorProvider-IN67OS56.mjs +0 -9
- package/dist/WeaviateProvider-5FWDFITI.mjs +0 -8
- package/dist/chunk-5AJ4XHLW.mjs +0 -201
- package/dist/chunk-5YGUXK7Z.mjs +0 -80
- package/dist/chunk-CFVEZTBJ.mjs +0 -102
- package/dist/chunk-ICKRMZQK.mjs +0 -76
- package/dist/chunk-IMP6FUCY.mjs +0 -30
- package/dist/chunk-LR3VMDVK.mjs +0 -157
- package/dist/chunk-LZVVLSDN.mjs +0 -4077
- package/dist/chunk-M6JSPGAR.mjs +0 -117
- package/dist/chunk-OZFBG4BA.mjs +0 -291
- package/dist/chunk-PSFPZXHX.mjs +0 -245
- package/dist/chunk-U55XRW3U.mjs +0 -96
- package/dist/chunk-VUQJVIJT.mjs +0 -148
- package/dist/chunk-X4TOT24V.mjs +0 -89
- package/dist/chunk-YLTMFW4M.mjs +0 -49
package/dist/chunk-LZVVLSDN.mjs
DELETED
|
@@ -1,4077 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
resolveMetadataValue
|
|
3
|
-
} from "./chunk-ICKRMZQK.mjs";
|
|
4
|
-
import {
|
|
5
|
-
buildPayload,
|
|
6
|
-
mergeDefined,
|
|
7
|
-
resolvePath
|
|
8
|
-
} from "./chunk-YLTMFW4M.mjs";
|
|
9
|
-
import {
|
|
10
|
-
__asyncGenerator,
|
|
11
|
-
__await,
|
|
12
|
-
__forAwait,
|
|
13
|
-
__spreadProps,
|
|
14
|
-
__spreadValues,
|
|
15
|
-
__yieldStar
|
|
16
|
-
} from "./chunk-X4TOT24V.mjs";
|
|
17
|
-
|
|
18
|
-
// src/handlers/index.ts
|
|
19
|
-
import { NextResponse } from "next/server";
|
|
20
|
-
|
|
21
|
-
// src/config/constants.ts
|
|
22
|
-
var VECTOR_DB_PROVIDERS = [
|
|
23
|
-
"pinecone",
|
|
24
|
-
"pgvector",
|
|
25
|
-
"postgresql",
|
|
26
|
-
"mongodb",
|
|
27
|
-
"qdrant",
|
|
28
|
-
"milvus",
|
|
29
|
-
"chromadb",
|
|
30
|
-
"redis",
|
|
31
|
-
"weaviate",
|
|
32
|
-
"rest",
|
|
33
|
-
"universal_rest",
|
|
34
|
-
"custom"
|
|
35
|
-
];
|
|
36
|
-
var LLM_PROVIDERS = [
|
|
37
|
-
"openai",
|
|
38
|
-
"anthropic",
|
|
39
|
-
"ollama",
|
|
40
|
-
"gemini",
|
|
41
|
-
"rest",
|
|
42
|
-
"universal_rest",
|
|
43
|
-
"custom"
|
|
44
|
-
];
|
|
45
|
-
var EMBEDDING_PROVIDERS = [
|
|
46
|
-
"openai",
|
|
47
|
-
"ollama",
|
|
48
|
-
"gemini",
|
|
49
|
-
"rest",
|
|
50
|
-
"universal_rest",
|
|
51
|
-
"custom"
|
|
52
|
-
];
|
|
53
|
-
var PROVIDERS_WITH_EMBEDDINGS = [
|
|
54
|
-
"openai",
|
|
55
|
-
"ollama",
|
|
56
|
-
"gemini",
|
|
57
|
-
"rest",
|
|
58
|
-
"universal_rest"
|
|
59
|
-
];
|
|
60
|
-
var UI_BORDER_RADIUS_OPTIONS = ["none", "sm", "md", "lg", "xl", "full"];
|
|
61
|
-
|
|
62
|
-
// src/config/serverConfig.ts
|
|
63
|
-
function readString(env, name) {
|
|
64
|
-
var _a;
|
|
65
|
-
const value = (_a = env[name]) == null ? void 0 : _a.trim();
|
|
66
|
-
return value ? value : void 0;
|
|
67
|
-
}
|
|
68
|
-
function readNumber(env, name, fallback) {
|
|
69
|
-
const value = env[name];
|
|
70
|
-
if (!value) return fallback;
|
|
71
|
-
const parsed = Number(value);
|
|
72
|
-
if (!Number.isFinite(parsed)) {
|
|
73
|
-
throw new Error(`[getRagConfig] ${name} must be a valid number`);
|
|
74
|
-
}
|
|
75
|
-
return parsed;
|
|
76
|
-
}
|
|
77
|
-
function readEnum(env, name, fallback, allowed) {
|
|
78
|
-
var _a;
|
|
79
|
-
const value = (_a = readString(env, name)) != null ? _a : fallback;
|
|
80
|
-
if (allowed.includes(value)) {
|
|
81
|
-
return value;
|
|
82
|
-
}
|
|
83
|
-
throw new Error(`[getRagConfig] ${name} must be one of: ${allowed.join(", ")}`);
|
|
84
|
-
}
|
|
85
|
-
function getRagConfig(baseConfig, env = process.env) {
|
|
86
|
-
return getEnvConfig(env, baseConfig);
|
|
87
|
-
}
|
|
88
|
-
function getEnvConfig(env = process.env, base) {
|
|
89
|
-
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, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va, _wa, _xa, _ya, _za, _Aa, _Ba, _Ca, _Da, _Ea, _Fa;
|
|
90
|
-
const projectId = (_c = (_b = (_a = readString(env, "RAG_PROJECT_ID")) != null ? _a : readString(env, "NEXT_PUBLIC_PROJECT_ID")) != null ? _b : base == null ? void 0 : base.projectId) != null ? _c : "__default__";
|
|
91
|
-
const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
|
|
92
|
-
const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
|
|
93
|
-
const embeddingProvider = readEnum(env, "EMBEDDING_PROVIDER", "openai", EMBEDDING_PROVIDERS);
|
|
94
|
-
const embeddingDimensions = readNumber(env, "EMBEDDING_DIMENSIONS", 1536);
|
|
95
|
-
const vectorDbOptions = {};
|
|
96
|
-
if (vectorProvider === "pinecone") {
|
|
97
|
-
vectorDbOptions.apiKey = (_g = (_f = readString(env, "PINECONE_API_KEY")) != null ? _f : (_e = (_d = base == null ? void 0 : base.vectorDb) == null ? void 0 : _d.options) == null ? void 0 : _e.apiKey) != null ? _g : "";
|
|
98
|
-
vectorDbOptions.indexName = (_l = (_i = readString(env, "PINECONE_INDEX")) != null ? _i : (_h = base == null ? void 0 : base.vectorDb) == null ? void 0 : _h.indexName) != null ? _l : (_k = (_j = base == null ? void 0 : base.vectorDb) == null ? void 0 : _j.options) == null ? void 0 : _k.indexName;
|
|
99
|
-
} else if (vectorProvider === "pgvector" || vectorProvider === "postgresql") {
|
|
100
|
-
vectorDbOptions.connectionString = (_q = (_p = (_m = readString(env, "PGVECTOR_CONNECTION_STRING")) != null ? _m : readString(env, "POSTGRES_URL")) != null ? _p : (_o = (_n = base == null ? void 0 : base.vectorDb) == null ? void 0 : _n.options) == null ? void 0 : _o.connectionString) != null ? _q : "";
|
|
101
|
-
vectorDbOptions.tables = (_v = (_s = (_r = readString(env, "VECTOR_DB_TABLES")) != null ? _r : readString(env, "POSTGRES_TABLES")) == null ? void 0 : _s.split(",").map((t) => t.trim())) != null ? _v : (_u = (_t = base == null ? void 0 : base.vectorDb) == null ? void 0 : _t.options) == null ? void 0 : _u.tables;
|
|
102
|
-
vectorDbOptions.searchFields = (_z = (_w = readString(env, "POSTGRES_SEARCH_FIELDS")) == null ? void 0 : _w.split(",").map((f) => f.trim())) != null ? _z : (_y = (_x = base == null ? void 0 : base.vectorDb) == null ? void 0 : _x.options) == null ? void 0 : _y.searchFields;
|
|
103
|
-
vectorDbOptions.dimensions = embeddingDimensions;
|
|
104
|
-
} else if (vectorProvider === "mongodb") {
|
|
105
|
-
vectorDbOptions.uri = (_D = (_C = readString(env, "MONGODB_URI")) != null ? _C : (_B = (_A = base == null ? void 0 : base.vectorDb) == null ? void 0 : _A.options) == null ? void 0 : _B.uri) != null ? _D : "";
|
|
106
|
-
vectorDbOptions.database = (_H = (_G = readString(env, "MONGODB_DB")) != null ? _G : (_F = (_E = base == null ? void 0 : base.vectorDb) == null ? void 0 : _E.options) == null ? void 0 : _F.database) != null ? _H : "";
|
|
107
|
-
vectorDbOptions.collection = (_L = (_K = readString(env, "MONGODB_COLLECTION")) != null ? _K : (_J = (_I = base == null ? void 0 : base.vectorDb) == null ? void 0 : _I.options) == null ? void 0 : _J.collection) != null ? _L : "";
|
|
108
|
-
vectorDbOptions.indexName = (_R = (_Q = (_N = readString(env, "MONGODB_INDEX_NAME")) != null ? _N : (_M = base == null ? void 0 : base.vectorDb) == null ? void 0 : _M.indexName) != null ? _Q : (_P = (_O = base == null ? void 0 : base.vectorDb) == null ? void 0 : _O.options) == null ? void 0 : _P.indexName) != null ? _R : "vector_index";
|
|
109
|
-
} else if (vectorProvider === "qdrant") {
|
|
110
|
-
vectorDbOptions.baseUrl = (_V = (_U = readString(env, "QDRANT_URL")) != null ? _U : (_T = (_S = base == null ? void 0 : base.vectorDb) == null ? void 0 : _S.options) == null ? void 0 : _T.baseUrl) != null ? _V : "http://localhost:6333";
|
|
111
|
-
vectorDbOptions.apiKey = (_Y = readString(env, "QDRANT_API_KEY")) != null ? _Y : (_X = (_W = base == null ? void 0 : base.vectorDb) == null ? void 0 : _W.options) == null ? void 0 : _X.apiKey;
|
|
112
|
-
vectorDbOptions.dimensions = embeddingDimensions;
|
|
113
|
-
} else if (vectorProvider === "milvus") {
|
|
114
|
-
vectorDbOptions.baseUrl = (_Z = readString(env, "MILVUS_URL")) != null ? _Z : "http://localhost:19530";
|
|
115
|
-
vectorDbOptions.apiKey = readString(env, "MILVUS_API_KEY");
|
|
116
|
-
} else if (vectorProvider === "chromadb") {
|
|
117
|
-
vectorDbOptions.baseUrl = (__ = readString(env, "CHROMADB_URL")) != null ? __ : "http://localhost:8000";
|
|
118
|
-
} else if (vectorProvider === "weaviate") {
|
|
119
|
-
vectorDbOptions.baseUrl = (_$ = readString(env, "WEAVIATE_URL")) != null ? _$ : "http://localhost:8080";
|
|
120
|
-
vectorDbOptions.apiKey = readString(env, "WEAVIATE_API_KEY");
|
|
121
|
-
} else if (vectorProvider === "redis") {
|
|
122
|
-
vectorDbOptions.baseUrl = (_aa = readString(env, "REDIS_URL")) != null ? _aa : "";
|
|
123
|
-
vectorDbOptions.apiKey = readString(env, "REDIS_API_KEY");
|
|
124
|
-
} else if (vectorProvider === "rest") {
|
|
125
|
-
vectorDbOptions.baseUrl = (_ba = readString(env, "VECTOR_DB_REST_URL")) != null ? _ba : "";
|
|
126
|
-
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { "api-key": readString(env, "VECTOR_DB_REST_API_KEY") } : {};
|
|
127
|
-
} else if (vectorProvider === "universal_rest") {
|
|
128
|
-
vectorDbOptions.baseUrl = (_da = (_ca = readString(env, "VECTOR_BASE_URL")) != null ? _ca : readString(env, "VECTOR_DB_REST_URL")) != null ? _da : "";
|
|
129
|
-
vectorDbOptions.profile = readString(env, "VECTOR_UNIVERSAL_PROFILE");
|
|
130
|
-
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { Authorization: `Bearer ${readString(env, "VECTOR_DB_REST_API_KEY")}` } : {};
|
|
131
|
-
}
|
|
132
|
-
const llmApiKeyByProvider = {
|
|
133
|
-
openai: readString(env, "OPENAI_API_KEY"),
|
|
134
|
-
anthropic: readString(env, "ANTHROPIC_API_KEY"),
|
|
135
|
-
gemini: readString(env, "GEMINI_API_KEY"),
|
|
136
|
-
ollama: void 0,
|
|
137
|
-
universal_rest: readString(env, "LLM_API_KEY"),
|
|
138
|
-
rest: readString(env, "LLM_API_KEY"),
|
|
139
|
-
custom: readString(env, "LLM_API_KEY")
|
|
140
|
-
};
|
|
141
|
-
const embeddingApiKeyByProvider = {
|
|
142
|
-
openai: readString(env, "OPENAI_API_KEY"),
|
|
143
|
-
gemini: readString(env, "GEMINI_API_KEY"),
|
|
144
|
-
ollama: void 0,
|
|
145
|
-
universal_rest: (_ea = readString(env, "EMBEDDING_API_KEY")) != null ? _ea : readString(env, "OPENAI_API_KEY"),
|
|
146
|
-
custom: (_fa = readString(env, "EMBEDDING_API_KEY")) != null ? _fa : readString(env, "OPENAI_API_KEY")
|
|
147
|
-
};
|
|
148
|
-
return {
|
|
149
|
-
projectId,
|
|
150
|
-
vectorDb: {
|
|
151
|
-
provider: vectorProvider,
|
|
152
|
-
indexName: (_ha = (_ga = readString(env, "VECTOR_DB_INDEX")) != null ? _ga : vectorDbOptions.indexName) != null ? _ha : "rag-index",
|
|
153
|
-
options: vectorDbOptions
|
|
154
|
-
},
|
|
155
|
-
llm: {
|
|
156
|
-
provider: llmProvider,
|
|
157
|
-
model: (_ia = readString(env, "LLM_MODEL")) != null ? _ia : "gpt-4o",
|
|
158
|
-
apiKey: (_ja = llmApiKeyByProvider[llmProvider]) != null ? _ja : "",
|
|
159
|
-
baseUrl: readString(env, "LLM_BASE_URL"),
|
|
160
|
-
systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
|
|
161
|
-
maxTokens: readNumber(env, "LLM_MAX_TOKENS", 4096),
|
|
162
|
-
temperature: readNumber(env, "LLM_TEMPERATURE", 0.7),
|
|
163
|
-
options: {
|
|
164
|
-
profile: readString(env, "LLM_UNIVERSAL_PROFILE")
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
embedding: {
|
|
168
|
-
provider: embeddingProvider,
|
|
169
|
-
model: (_ka = readString(env, "EMBEDDING_MODEL")) != null ? _ka : "text-embedding-3-small",
|
|
170
|
-
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
171
|
-
baseUrl: readString(env, "EMBEDDING_BASE_URL"),
|
|
172
|
-
dimensions: embeddingDimensions,
|
|
173
|
-
queryPrefix: readString(env, "EMBEDDING_QUERY_PREFIX"),
|
|
174
|
-
documentPrefix: readString(env, "EMBEDDING_DOCUMENT_PREFIX"),
|
|
175
|
-
options: {
|
|
176
|
-
profile: readString(env, "EMBEDDING_UNIVERSAL_PROFILE")
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
ui: {
|
|
180
|
-
title: (_ma = (_la = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _la : readString(env, "UI_TITLE")) != null ? _ma : "AI Assistant",
|
|
181
|
-
subtitle: (_oa = (_na = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _na : readString(env, "UI_SUBTITLE")) != null ? _oa : "Powered by RAG",
|
|
182
|
-
primaryColor: (_qa = (_pa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _pa : readString(env, "UI_PRIMARY_COLOR")) != null ? _qa : "#10b981",
|
|
183
|
-
accentColor: (_sa = (_ra = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ra : readString(env, "UI_ACCENT_COLOR")) != null ? _sa : "#3b82f6",
|
|
184
|
-
logoUrl: (_ta = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ta : readString(env, "UI_LOGO_URL"),
|
|
185
|
-
placeholder: (_va = (_ua = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ua : readString(env, "UI_PLACEHOLDER")) != null ? _va : "Ask me anything\u2026",
|
|
186
|
-
showSources: ((_xa = (_wa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _wa : readString(env, "UI_SHOW_SOURCES")) != null ? _xa : "true") !== "false",
|
|
187
|
-
welcomeMessage: (_za = (_ya = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _ya : readString(env, "UI_WELCOME_MESSAGE")) != null ? _za : "Hello! I'm your AI assistant. Ask me anything about your documents.",
|
|
188
|
-
visualStyle: (_Ba = (_Aa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Aa : readString(env, "UI_VISUAL_STYLE")) != null ? _Ba : "glass",
|
|
189
|
-
borderRadius: (_Da = (_Ca = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Ca : readString(env, "UI_BORDER_RADIUS")) != null ? _Da : "xl",
|
|
190
|
-
allowUpload: ((_Fa = (_Ea = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Ea : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Fa : "false") === "true"
|
|
191
|
-
},
|
|
192
|
-
rag: {
|
|
193
|
-
topK: readNumber(env, "RAG_TOP_K", 5),
|
|
194
|
-
scoreThreshold: readNumber(env, "RAG_SCORE_THRESHOLD", 0),
|
|
195
|
-
chunkSize: readNumber(env, "RAG_CHUNK_SIZE", 1e3),
|
|
196
|
-
chunkOverlap: readNumber(env, "RAG_CHUNK_OVERLAP", 200)
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// src/core/ConfigResolver.ts
|
|
202
|
-
var ConfigResolver = class {
|
|
203
|
-
/**
|
|
204
|
-
* Resolves the final configuration by merging host-provided config with environment defaults.
|
|
205
|
-
* @param hostConfig - Partial configuration passed from the host application.
|
|
206
|
-
*/
|
|
207
|
-
static resolve(hostConfig, env = process.env) {
|
|
208
|
-
const envConfig = getEnvConfig(env);
|
|
209
|
-
if (!hostConfig) {
|
|
210
|
-
return envConfig;
|
|
211
|
-
}
|
|
212
|
-
return __spreadProps(__spreadValues(__spreadValues({}, envConfig), hostConfig), {
|
|
213
|
-
projectId: hostConfig.projectId || envConfig.projectId,
|
|
214
|
-
vectorDb: hostConfig.vectorDb ? __spreadProps(__spreadValues(__spreadValues({}, envConfig.vectorDb), hostConfig.vectorDb), {
|
|
215
|
-
options: __spreadValues(__spreadValues({}, envConfig.vectorDb.options), hostConfig.vectorDb.options || {})
|
|
216
|
-
}) : envConfig.vectorDb,
|
|
217
|
-
llm: hostConfig.llm ? __spreadProps(__spreadValues(__spreadValues({}, envConfig.llm), hostConfig.llm), {
|
|
218
|
-
options: __spreadValues(__spreadValues({}, envConfig.llm.options || {}), hostConfig.llm.options || {})
|
|
219
|
-
}) : envConfig.llm,
|
|
220
|
-
embedding: hostConfig.embedding ? __spreadProps(__spreadValues(__spreadValues({}, envConfig.embedding), hostConfig.embedding), {
|
|
221
|
-
options: __spreadValues(__spreadValues({}, envConfig.embedding.options || {}), hostConfig.embedding.options || {})
|
|
222
|
-
}) : envConfig.embedding,
|
|
223
|
-
ui: hostConfig.ui ? mergeDefined(envConfig.ui, hostConfig.ui) : envConfig.ui,
|
|
224
|
-
rag: hostConfig.rag ? __spreadValues(__spreadValues({}, envConfig.rag), hostConfig.rag) : envConfig.rag
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Validates the configuration for required fields.
|
|
229
|
-
*/
|
|
230
|
-
static validate(config) {
|
|
231
|
-
if (!config.projectId) {
|
|
232
|
-
throw new Error("[ConfigResolver] projectId is required");
|
|
233
|
-
}
|
|
234
|
-
if (!config.vectorDb.provider) {
|
|
235
|
-
throw new Error("[ConfigResolver] vectorDb.provider is required");
|
|
236
|
-
}
|
|
237
|
-
if (!config.llm.provider) {
|
|
238
|
-
throw new Error("[ConfigResolver] llm.provider is required");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// src/llm/providers/OpenAIProvider.ts
|
|
244
|
-
import OpenAI from "openai";
|
|
245
|
-
var OpenAIProvider = class {
|
|
246
|
-
constructor(llmConfig, embeddingConfig) {
|
|
247
|
-
if (!llmConfig.apiKey) throw new Error("[OpenAIProvider] llmConfig.apiKey is required");
|
|
248
|
-
this.client = new OpenAI({ apiKey: llmConfig.apiKey });
|
|
249
|
-
this.llmConfig = llmConfig;
|
|
250
|
-
this.embeddingConfig = embeddingConfig;
|
|
251
|
-
}
|
|
252
|
-
static getValidator() {
|
|
253
|
-
return {
|
|
254
|
-
validate(config) {
|
|
255
|
-
const errors = [];
|
|
256
|
-
const isEmbedding = config.provider === "openai" && "dimensions" in config;
|
|
257
|
-
const prefix = isEmbedding ? "embedding" : "llm";
|
|
258
|
-
if (!config.apiKey) {
|
|
259
|
-
errors.push({
|
|
260
|
-
field: `${prefix}.apiKey`,
|
|
261
|
-
message: "OpenAI API key is required",
|
|
262
|
-
suggestion: "Set OPENAI_API_KEY environment variable",
|
|
263
|
-
severity: "error"
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
if (!config.model) {
|
|
267
|
-
errors.push({
|
|
268
|
-
field: `${prefix}.model`,
|
|
269
|
-
message: "OpenAI model name is required",
|
|
270
|
-
suggestion: isEmbedding ? 'e.g., "text-embedding-3-small"' : 'e.g., "gpt-4o"',
|
|
271
|
-
severity: "error"
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
return errors;
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
static getHealthChecker() {
|
|
279
|
-
return {
|
|
280
|
-
async check(config) {
|
|
281
|
-
const timestamp = Date.now();
|
|
282
|
-
const apiKey = config.apiKey;
|
|
283
|
-
const modelName = config.model;
|
|
284
|
-
try {
|
|
285
|
-
const OpenAI2 = await import("openai");
|
|
286
|
-
const client = new OpenAI2.default({ apiKey });
|
|
287
|
-
const models = await client.models.list();
|
|
288
|
-
const hasModel = models.data.some((m) => m.id === modelName);
|
|
289
|
-
return {
|
|
290
|
-
healthy: true,
|
|
291
|
-
provider: "openai",
|
|
292
|
-
capabilities: { model: modelName, available: hasModel, totalModels: models.data.length },
|
|
293
|
-
timestamp
|
|
294
|
-
};
|
|
295
|
-
} catch (error) {
|
|
296
|
-
return {
|
|
297
|
-
healthy: false,
|
|
298
|
-
provider: "openai",
|
|
299
|
-
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
300
|
-
timestamp
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
async chat(messages, context, options) {
|
|
307
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
308
|
-
const resolvedSystemPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Answer questions based on the provided context.
|
|
309
|
-
|
|
310
|
-
Context:
|
|
311
|
-
${context}`;
|
|
312
|
-
const systemMessage = {
|
|
313
|
-
role: "system",
|
|
314
|
-
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
315
|
-
|
|
316
|
-
Context:
|
|
317
|
-
${context}`
|
|
318
|
-
};
|
|
319
|
-
const formattedMessages = [
|
|
320
|
-
systemMessage,
|
|
321
|
-
...messages.map((m) => ({
|
|
322
|
-
role: m.role,
|
|
323
|
-
content: m.content
|
|
324
|
-
}))
|
|
325
|
-
];
|
|
326
|
-
const completion = await this.client.chat.completions.create({
|
|
327
|
-
model: this.llmConfig.model,
|
|
328
|
-
messages: formattedMessages,
|
|
329
|
-
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
330
|
-
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
331
|
-
stop: options == null ? void 0 : options.stop
|
|
332
|
-
});
|
|
333
|
-
return (_i = (_h = (_g = completion.choices[0]) == null ? void 0 : _g.message) == null ? void 0 : _h.content) != null ? _i : "";
|
|
334
|
-
}
|
|
335
|
-
chatStream(messages, context, options) {
|
|
336
|
-
return __asyncGenerator(this, null, function* () {
|
|
337
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
338
|
-
const resolvedSystemPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Answer questions based on the provided context.
|
|
339
|
-
|
|
340
|
-
Context:
|
|
341
|
-
${context}`;
|
|
342
|
-
const systemMessage = {
|
|
343
|
-
role: "system",
|
|
344
|
-
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
345
|
-
|
|
346
|
-
Context:
|
|
347
|
-
${context}`
|
|
348
|
-
};
|
|
349
|
-
const formattedMessages = [
|
|
350
|
-
systemMessage,
|
|
351
|
-
...messages.map((m) => ({
|
|
352
|
-
role: m.role,
|
|
353
|
-
content: m.content
|
|
354
|
-
}))
|
|
355
|
-
];
|
|
356
|
-
const stream = yield new __await(this.client.chat.completions.create({
|
|
357
|
-
model: this.llmConfig.model,
|
|
358
|
-
messages: formattedMessages,
|
|
359
|
-
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
360
|
-
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
361
|
-
stop: options == null ? void 0 : options.stop,
|
|
362
|
-
stream: true
|
|
363
|
-
}));
|
|
364
|
-
if (!stream) {
|
|
365
|
-
throw new Error("[OpenAIProvider] completions.create stream is undefined");
|
|
366
|
-
}
|
|
367
|
-
try {
|
|
368
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
369
|
-
const chunk = temp.value;
|
|
370
|
-
const content = ((_h = (_g = chunk.choices[0]) == null ? void 0 : _g.delta) == null ? void 0 : _h.content) || "";
|
|
371
|
-
if (content) yield content;
|
|
372
|
-
}
|
|
373
|
-
} catch (temp) {
|
|
374
|
-
error = [temp];
|
|
375
|
-
} finally {
|
|
376
|
-
try {
|
|
377
|
-
more && (temp = iter.return) && (yield new __await(temp.call(iter)));
|
|
378
|
-
} finally {
|
|
379
|
-
if (error)
|
|
380
|
-
throw error[0];
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
async embed(text, options) {
|
|
386
|
-
const results = await this.batchEmbed([text], options);
|
|
387
|
-
return results[0];
|
|
388
|
-
}
|
|
389
|
-
async batchEmbed(texts, options) {
|
|
390
|
-
var _a, _b, _c, _d, _e;
|
|
391
|
-
const model = (_c = (_b = options == null ? void 0 : options.model) != null ? _b : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _c : "text-embedding-3-small";
|
|
392
|
-
const apiKey = (_e = (_d = this.embeddingConfig) == null ? void 0 : _d.apiKey) != null ? _e : this.llmConfig.apiKey;
|
|
393
|
-
const client = apiKey !== this.llmConfig.apiKey ? new OpenAI({ apiKey }) : this.client;
|
|
394
|
-
const response = await client.embeddings.create({ model, input: texts });
|
|
395
|
-
return response.data.map((d) => d.embedding);
|
|
396
|
-
}
|
|
397
|
-
async ping() {
|
|
398
|
-
try {
|
|
399
|
-
await this.client.models.list();
|
|
400
|
-
return true;
|
|
401
|
-
} catch (err) {
|
|
402
|
-
console.error("[OpenAIProvider] Ping failed:", err);
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
// src/llm/providers/AnthropicProvider.ts
|
|
409
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
410
|
-
var AnthropicProvider = class {
|
|
411
|
-
constructor(llmConfig, embeddingConfig) {
|
|
412
|
-
if (!llmConfig.apiKey) throw new Error("[AnthropicProvider] llmConfig.apiKey is required");
|
|
413
|
-
this.client = new Anthropic({ apiKey: llmConfig.apiKey });
|
|
414
|
-
this.llmConfig = llmConfig;
|
|
415
|
-
this.embeddingConfig = embeddingConfig;
|
|
416
|
-
}
|
|
417
|
-
static getValidator() {
|
|
418
|
-
return {
|
|
419
|
-
validate(config) {
|
|
420
|
-
const errors = [];
|
|
421
|
-
if (!config.apiKey) {
|
|
422
|
-
errors.push({
|
|
423
|
-
field: "llm.apiKey",
|
|
424
|
-
message: "Anthropic API key is required",
|
|
425
|
-
suggestion: "Set ANTHROPIC_API_KEY environment variable",
|
|
426
|
-
severity: "error"
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
if (!config.model) {
|
|
430
|
-
errors.push({
|
|
431
|
-
field: "llm.model",
|
|
432
|
-
message: "Anthropic model name is required",
|
|
433
|
-
suggestion: 'e.g., "claude-3-5-sonnet-20241022"',
|
|
434
|
-
severity: "error"
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
return errors;
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
static getHealthChecker() {
|
|
442
|
-
return {
|
|
443
|
-
async check(config) {
|
|
444
|
-
const timestamp = Date.now();
|
|
445
|
-
const apiKey = config.apiKey;
|
|
446
|
-
const modelName = config.model;
|
|
447
|
-
try {
|
|
448
|
-
const { default: Anthropic2 } = await import("@anthropic-ai/sdk");
|
|
449
|
-
const client = new Anthropic2({ apiKey });
|
|
450
|
-
await client.messages.create({
|
|
451
|
-
model: modelName,
|
|
452
|
-
max_tokens: 10,
|
|
453
|
-
messages: [{ role: "user", content: "ping" }]
|
|
454
|
-
});
|
|
455
|
-
return { healthy: true, provider: "anthropic", capabilities: { model: modelName }, timestamp };
|
|
456
|
-
} catch (error) {
|
|
457
|
-
return {
|
|
458
|
-
healthy: false,
|
|
459
|
-
provider: "anthropic",
|
|
460
|
-
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
461
|
-
timestamp
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
async chat(messages, context, options) {
|
|
468
|
-
var _a, _b, _c, _d;
|
|
469
|
-
const resolvedPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Use the context below to answer the user's question accurately.
|
|
470
|
-
|
|
471
|
-
Context:
|
|
472
|
-
${context}`;
|
|
473
|
-
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
474
|
-
|
|
475
|
-
Context:
|
|
476
|
-
${context}`;
|
|
477
|
-
const anthropicMessages = messages.map((m) => ({
|
|
478
|
-
role: m.role === "assistant" ? "assistant" : "user",
|
|
479
|
-
content: m.content
|
|
480
|
-
}));
|
|
481
|
-
const response = await this.client.messages.create({
|
|
482
|
-
model: this.llmConfig.model,
|
|
483
|
-
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
484
|
-
system,
|
|
485
|
-
messages: anthropicMessages
|
|
486
|
-
});
|
|
487
|
-
const block = response.content[0];
|
|
488
|
-
return block.type === "text" ? block.text : "";
|
|
489
|
-
}
|
|
490
|
-
chatStream(messages, context, options) {
|
|
491
|
-
return __asyncGenerator(this, null, function* () {
|
|
492
|
-
var _a, _b, _c, _d;
|
|
493
|
-
const resolvedPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Use the context below to answer the user's question accurately.
|
|
494
|
-
|
|
495
|
-
Context:
|
|
496
|
-
${context}`;
|
|
497
|
-
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
498
|
-
|
|
499
|
-
Context:
|
|
500
|
-
${context}`;
|
|
501
|
-
const anthropicMessages = messages.map((m) => ({
|
|
502
|
-
role: m.role === "assistant" ? "assistant" : "user",
|
|
503
|
-
content: m.content
|
|
504
|
-
}));
|
|
505
|
-
const stream = yield new __await(this.client.messages.create({
|
|
506
|
-
model: this.llmConfig.model,
|
|
507
|
-
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
508
|
-
system,
|
|
509
|
-
messages: anthropicMessages,
|
|
510
|
-
stream: true
|
|
511
|
-
}));
|
|
512
|
-
if (!stream) {
|
|
513
|
-
throw new Error("[AnthropicProvider] messages.create stream is undefined");
|
|
514
|
-
}
|
|
515
|
-
try {
|
|
516
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
517
|
-
const chunk = temp.value;
|
|
518
|
-
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
|
519
|
-
yield chunk.delta.text;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
} catch (temp) {
|
|
523
|
-
error = [temp];
|
|
524
|
-
} finally {
|
|
525
|
-
try {
|
|
526
|
-
more && (temp = iter.return) && (yield new __await(temp.call(iter)));
|
|
527
|
-
} finally {
|
|
528
|
-
if (error)
|
|
529
|
-
throw error[0];
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
async embed(text, options) {
|
|
535
|
-
void text;
|
|
536
|
-
void options;
|
|
537
|
-
throw new Error(
|
|
538
|
-
'[AnthropicProvider] Anthropic does not provide an embedding API. Set embedding.provider to "openai" or "ollama" in your RagConfig.'
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
async batchEmbed(texts, options) {
|
|
542
|
-
void texts;
|
|
543
|
-
void options;
|
|
544
|
-
throw new Error(
|
|
545
|
-
"[AnthropicProvider] Anthropic does not provide an embedding API."
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
async ping() {
|
|
549
|
-
try {
|
|
550
|
-
await this.client.models.list();
|
|
551
|
-
return true;
|
|
552
|
-
} catch (err) {
|
|
553
|
-
console.error("[AnthropicProvider] Ping failed:", err);
|
|
554
|
-
return false;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
// src/llm/providers/OllamaProvider.ts
|
|
560
|
-
import axios from "axios";
|
|
561
|
-
var OllamaProvider = class {
|
|
562
|
-
constructor(llmConfig, embeddingConfig) {
|
|
563
|
-
var _a;
|
|
564
|
-
const baseURL = (_a = llmConfig.baseUrl) != null ? _a : "http://localhost:11434";
|
|
565
|
-
this.http = axios.create({ baseURL, timeout: 12e4 });
|
|
566
|
-
this.llmConfig = llmConfig;
|
|
567
|
-
this.embeddingConfig = embeddingConfig;
|
|
568
|
-
}
|
|
569
|
-
static getValidator() {
|
|
570
|
-
return {
|
|
571
|
-
validate(config) {
|
|
572
|
-
const errors = [];
|
|
573
|
-
if (!config.model) {
|
|
574
|
-
const isEmbedding = config.provider === "ollama" && "dimensions" in config;
|
|
575
|
-
const prefix = isEmbedding ? "embedding" : "llm";
|
|
576
|
-
errors.push({
|
|
577
|
-
field: `${prefix}.model`,
|
|
578
|
-
message: "Ollama model name is required",
|
|
579
|
-
suggestion: 'e.g., "llama3" or "mistral"',
|
|
580
|
-
severity: "error"
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
return errors;
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
static getHealthChecker() {
|
|
588
|
-
return {
|
|
589
|
-
async check(config) {
|
|
590
|
-
const timestamp = Date.now();
|
|
591
|
-
const baseUrl = config.baseUrl || "http://localhost:11434";
|
|
592
|
-
const modelName = config.model;
|
|
593
|
-
try {
|
|
594
|
-
const axios3 = (await import("axios")).default;
|
|
595
|
-
const { data } = await axios3.get(`${baseUrl}/api/tags`);
|
|
596
|
-
const models = data.models || [];
|
|
597
|
-
const hasModel = models.some((m) => m.name === modelName || m.name.startsWith(`${modelName}:`));
|
|
598
|
-
return {
|
|
599
|
-
healthy: true,
|
|
600
|
-
provider: "ollama",
|
|
601
|
-
capabilities: {
|
|
602
|
-
baseUrl,
|
|
603
|
-
model: modelName,
|
|
604
|
-
available: hasModel,
|
|
605
|
-
totalModels: models.length
|
|
606
|
-
},
|
|
607
|
-
timestamp
|
|
608
|
-
};
|
|
609
|
-
} catch (e) {
|
|
610
|
-
return {
|
|
611
|
-
healthy: false,
|
|
612
|
-
provider: "ollama",
|
|
613
|
-
error: `Ollama server not reachable at ${baseUrl}. Is it running?`,
|
|
614
|
-
timestamp
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
async chat(messages, context, options) {
|
|
621
|
-
var _a, _b, _c, _d;
|
|
622
|
-
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
623
|
-
const { data } = await this.http.post("/api/chat", {
|
|
624
|
-
model: this.llmConfig.model,
|
|
625
|
-
stream: false,
|
|
626
|
-
options: {
|
|
627
|
-
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : 0.7,
|
|
628
|
-
num_predict: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024
|
|
629
|
-
},
|
|
630
|
-
messages: [
|
|
631
|
-
{ role: "system", content: system },
|
|
632
|
-
...messages.map((m) => ({ role: m.role, content: m.content }))
|
|
633
|
-
]
|
|
634
|
-
});
|
|
635
|
-
return data.message.content;
|
|
636
|
-
}
|
|
637
|
-
chatStream(messages, context, options) {
|
|
638
|
-
return __asyncGenerator(this, null, function* () {
|
|
639
|
-
var _a, _b, _c, _d, _e, _f;
|
|
640
|
-
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
641
|
-
const response = yield new __await(this.http.post("/api/chat", {
|
|
642
|
-
model: this.llmConfig.model,
|
|
643
|
-
stream: true,
|
|
644
|
-
options: {
|
|
645
|
-
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : 0.7,
|
|
646
|
-
num_predict: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024
|
|
647
|
-
},
|
|
648
|
-
messages: [
|
|
649
|
-
{ role: "system", content: system },
|
|
650
|
-
...messages.map((m) => ({ role: m.role, content: m.content }))
|
|
651
|
-
]
|
|
652
|
-
}, { responseType: "stream" }));
|
|
653
|
-
let lineBuffer = "";
|
|
654
|
-
const stream = response.data;
|
|
655
|
-
if (!stream) {
|
|
656
|
-
throw new Error("[OllamaProvider] response.data is undefined for stream request. Axios might not support streams in this environment.");
|
|
657
|
-
}
|
|
658
|
-
try {
|
|
659
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
660
|
-
const chunk = temp.value;
|
|
661
|
-
lineBuffer += chunk.toString();
|
|
662
|
-
const lines = lineBuffer.split("\n");
|
|
663
|
-
lineBuffer = lines.pop() || "";
|
|
664
|
-
for (const line of lines) {
|
|
665
|
-
if (!line.trim()) continue;
|
|
666
|
-
try {
|
|
667
|
-
const json = JSON.parse(line);
|
|
668
|
-
if ((_e = json.message) == null ? void 0 : _e.content) {
|
|
669
|
-
yield json.message.content;
|
|
670
|
-
}
|
|
671
|
-
if (json.done) return;
|
|
672
|
-
} catch (e) {
|
|
673
|
-
console.warn("[OllamaProvider] Failed to parse streaming line:", line);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
} catch (temp) {
|
|
678
|
-
error = [temp];
|
|
679
|
-
} finally {
|
|
680
|
-
try {
|
|
681
|
-
more && (temp = iter.return) && (yield new __await(temp.call(iter)));
|
|
682
|
-
} finally {
|
|
683
|
-
if (error)
|
|
684
|
-
throw error[0];
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
if (lineBuffer.trim()) {
|
|
688
|
-
try {
|
|
689
|
-
const json = JSON.parse(lineBuffer);
|
|
690
|
-
if ((_f = json.message) == null ? void 0 : _f.content) yield json.message.content;
|
|
691
|
-
} catch (e) {
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
buildSystemPrompt(context, overridePrompt) {
|
|
697
|
-
var _a;
|
|
698
|
-
if (overridePrompt) {
|
|
699
|
-
return overridePrompt;
|
|
700
|
-
}
|
|
701
|
-
const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Use the provided context to answer the user's question.
|
|
702
|
-
|
|
703
|
-
Context:
|
|
704
|
-
${context}`;
|
|
705
|
-
return systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
|
|
706
|
-
|
|
707
|
-
Context:
|
|
708
|
-
${context}`;
|
|
709
|
-
}
|
|
710
|
-
async embed(text, options) {
|
|
711
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
712
|
-
const model = (_c = (_b = options == null ? void 0 : options.model) != null ? _b : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _c : "nomic-embed-text";
|
|
713
|
-
const baseURL = (_f = (_e = (_d = this.embeddingConfig) == null ? void 0 : _d.baseUrl) != null ? _e : this.llmConfig.baseUrl) != null ? _f : "http://localhost:11434";
|
|
714
|
-
const client = baseURL !== ((_g = this.llmConfig.baseUrl) != null ? _g : "http://localhost:11434") ? axios.create({ baseURL, timeout: 6e4 }) : this.http;
|
|
715
|
-
let prompt = text;
|
|
716
|
-
const queryPrefix = (_i = (_h = this.embeddingConfig) == null ? void 0 : _h.queryPrefix) != null ? _i : model.includes("nomic") ? "search_query: " : "";
|
|
717
|
-
const docPrefix = (_k = (_j = this.embeddingConfig) == null ? void 0 : _j.documentPrefix) != null ? _k : model.includes("nomic") ? "search_document: " : "";
|
|
718
|
-
if ((options == null ? void 0 : options.taskType) === "query" && queryPrefix) {
|
|
719
|
-
if (!prompt.startsWith(queryPrefix)) {
|
|
720
|
-
prompt = `${queryPrefix}${text}`;
|
|
721
|
-
}
|
|
722
|
-
} else if ((options == null ? void 0 : options.taskType) === "document" && docPrefix) {
|
|
723
|
-
if (!prompt.startsWith(docPrefix)) {
|
|
724
|
-
prompt = `${docPrefix}${text}`;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
const { data } = await client.post("/api/embeddings", {
|
|
728
|
-
model,
|
|
729
|
-
prompt
|
|
730
|
-
});
|
|
731
|
-
return data.embedding;
|
|
732
|
-
}
|
|
733
|
-
async batchEmbed(texts, options) {
|
|
734
|
-
const vectors = [];
|
|
735
|
-
for (const text of texts) {
|
|
736
|
-
vectors.push(await this.embed(text, options));
|
|
737
|
-
}
|
|
738
|
-
return vectors;
|
|
739
|
-
}
|
|
740
|
-
async ping() {
|
|
741
|
-
try {
|
|
742
|
-
await this.http.get("/api/tags");
|
|
743
|
-
return true;
|
|
744
|
-
} catch (err) {
|
|
745
|
-
console.error("[OllamaProvider] Ping failed:", err);
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
// src/llm/providers/GeminiProvider.ts
|
|
752
|
-
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
753
|
-
var DEFAULT_EMBEDDING_MODEL = "text-embedding-004";
|
|
754
|
-
var DEFAULT_TEMPERATURE = 0.7;
|
|
755
|
-
var DEFAULT_MAX_TOKENS = 1024;
|
|
756
|
-
function sanitizeModel(model) {
|
|
757
|
-
return model ? model.split(":")[0] : model;
|
|
758
|
-
}
|
|
759
|
-
function buildClient(apiKey) {
|
|
760
|
-
return new GoogleGenerativeAI(apiKey);
|
|
761
|
-
}
|
|
762
|
-
function applyPrefix(text, taskType, queryPrefix, docPrefix) {
|
|
763
|
-
if (taskType === "query" && queryPrefix && !text.startsWith(queryPrefix)) {
|
|
764
|
-
return `${queryPrefix}${text}`;
|
|
765
|
-
}
|
|
766
|
-
if (taskType === "document" && docPrefix && !text.startsWith(docPrefix)) {
|
|
767
|
-
return `${docPrefix}${text}`;
|
|
768
|
-
}
|
|
769
|
-
return text;
|
|
770
|
-
}
|
|
771
|
-
var GeminiProvider = class {
|
|
772
|
-
constructor(llmConfig, embeddingConfig) {
|
|
773
|
-
if (!llmConfig.apiKey) {
|
|
774
|
-
throw new Error("[GeminiProvider] llmConfig.apiKey is required");
|
|
775
|
-
}
|
|
776
|
-
this.llmConfig = __spreadProps(__spreadValues({}, llmConfig), { model: sanitizeModel(llmConfig.model) });
|
|
777
|
-
this.client = buildClient(this.llmConfig.apiKey);
|
|
778
|
-
if (embeddingConfig) {
|
|
779
|
-
this.embeddingConfig = __spreadProps(__spreadValues({}, embeddingConfig), {
|
|
780
|
-
model: sanitizeModel(embeddingConfig.model)
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
// -------------------------------------------------------------------------
|
|
785
|
-
// Static factory helpers
|
|
786
|
-
// -------------------------------------------------------------------------
|
|
787
|
-
static getValidator() {
|
|
788
|
-
return {
|
|
789
|
-
validate(config) {
|
|
790
|
-
const errors = [];
|
|
791
|
-
if (!config.apiKey && !process.env.GOOGLE_GENAI_API_KEY) {
|
|
792
|
-
errors.push({
|
|
793
|
-
field: "llm.apiKey",
|
|
794
|
-
message: "Gemini API key is required",
|
|
795
|
-
suggestion: "Set GOOGLE_GENAI_API_KEY environment variable or provide in config",
|
|
796
|
-
severity: "error"
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
if (!config.model) {
|
|
800
|
-
errors.push({
|
|
801
|
-
field: "llm.model",
|
|
802
|
-
message: "Gemini model name is required",
|
|
803
|
-
severity: "error"
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
return errors;
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
static getHealthChecker() {
|
|
811
|
-
return {
|
|
812
|
-
async check(config) {
|
|
813
|
-
var _a, _b;
|
|
814
|
-
const timestamp = Date.now();
|
|
815
|
-
const apiKey = (_b = (_a = config.apiKey) != null ? _a : process.env.GOOGLE_GENAI_API_KEY) != null ? _b : "";
|
|
816
|
-
const modelName = sanitizeModel(config.model);
|
|
817
|
-
try {
|
|
818
|
-
const { GoogleGenerativeAI: GoogleGenerativeAI2 } = await import("@google/generative-ai");
|
|
819
|
-
const ai = new GoogleGenerativeAI2(apiKey);
|
|
820
|
-
const model = ai.getGenerativeModel({ model: DEFAULT_EMBEDDING_MODEL });
|
|
821
|
-
await model.embedContent("health-check");
|
|
822
|
-
return {
|
|
823
|
-
healthy: true,
|
|
824
|
-
provider: "gemini",
|
|
825
|
-
capabilities: { model: modelName },
|
|
826
|
-
timestamp
|
|
827
|
-
};
|
|
828
|
-
} catch (error) {
|
|
829
|
-
return {
|
|
830
|
-
healthy: false,
|
|
831
|
-
provider: "gemini",
|
|
832
|
-
error: error instanceof Error ? error.message : String(error),
|
|
833
|
-
timestamp
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
// -------------------------------------------------------------------------
|
|
840
|
-
// Private utilities
|
|
841
|
-
// -------------------------------------------------------------------------
|
|
842
|
-
/** Resolve the embedding client — uses a separate client when the embedding
|
|
843
|
-
* API key differs from the LLM API key. */
|
|
844
|
-
get embeddingClient() {
|
|
845
|
-
var _a;
|
|
846
|
-
if (((_a = this.embeddingConfig) == null ? void 0 : _a.apiKey) && this.embeddingConfig.apiKey !== this.llmConfig.apiKey) {
|
|
847
|
-
return buildClient(this.embeddingConfig.apiKey);
|
|
848
|
-
}
|
|
849
|
-
return this.client;
|
|
850
|
-
}
|
|
851
|
-
/** Resolve the embedding model to use, in order of specificity. */
|
|
852
|
-
resolveEmbeddingModel(optionsModel) {
|
|
853
|
-
var _a, _b;
|
|
854
|
-
return sanitizeModel((_b = optionsModel != null ? optionsModel : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _b : DEFAULT_EMBEDDING_MODEL);
|
|
855
|
-
}
|
|
856
|
-
/**
|
|
857
|
-
* Build the system instruction string, inserting the RAG context either via
|
|
858
|
-
* the `{{context}}` placeholder or by appending it.
|
|
859
|
-
*/
|
|
860
|
-
buildSystemInstruction(context) {
|
|
861
|
-
var _a;
|
|
862
|
-
const base = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
|
|
863
|
-
|
|
864
|
-
Context:
|
|
865
|
-
${context}`;
|
|
866
|
-
return base.includes("{{context}}") ? base.replace("{{context}}", context) : `${base}
|
|
867
|
-
|
|
868
|
-
Context:
|
|
869
|
-
${context}`;
|
|
870
|
-
}
|
|
871
|
-
/**
|
|
872
|
-
* Convert ChatMessage[] to the Gemini contents format.
|
|
873
|
-
*
|
|
874
|
-
* Rules enforced by the Gemini API:
|
|
875
|
-
* - Only `user` and `model` roles are allowed in `contents`.
|
|
876
|
-
* - System messages must be passed via `systemInstruction`, not here.
|
|
877
|
-
* - Messages must strictly alternate between `user` and `model`.
|
|
878
|
-
*/
|
|
879
|
-
buildGeminiContents(messages) {
|
|
880
|
-
return messages.filter((m) => m.role !== "system").map((m) => ({
|
|
881
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
882
|
-
parts: [{ text: m.content }]
|
|
883
|
-
}));
|
|
884
|
-
}
|
|
885
|
-
// -------------------------------------------------------------------------
|
|
886
|
-
// ILLMProvider — chat
|
|
887
|
-
// -------------------------------------------------------------------------
|
|
888
|
-
async chat(messages, context, options) {
|
|
889
|
-
var _a, _b, _c, _d;
|
|
890
|
-
const model = this.client.getGenerativeModel({
|
|
891
|
-
model: this.llmConfig.model,
|
|
892
|
-
systemInstruction: this.buildSystemInstruction(context)
|
|
893
|
-
});
|
|
894
|
-
const result = await model.generateContent({
|
|
895
|
-
contents: this.buildGeminiContents(messages),
|
|
896
|
-
generationConfig: {
|
|
897
|
-
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
898
|
-
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
899
|
-
stopSequences: options == null ? void 0 : options.stop
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
return result.response.text();
|
|
903
|
-
}
|
|
904
|
-
chatStream(messages, context, options) {
|
|
905
|
-
return __asyncGenerator(this, null, function* () {
|
|
906
|
-
var _a, _b, _c, _d;
|
|
907
|
-
const model = this.client.getGenerativeModel({
|
|
908
|
-
model: this.llmConfig.model,
|
|
909
|
-
systemInstruction: this.buildSystemInstruction(context)
|
|
910
|
-
});
|
|
911
|
-
const result = yield new __await(model.generateContentStream({
|
|
912
|
-
contents: this.buildGeminiContents(messages),
|
|
913
|
-
generationConfig: {
|
|
914
|
-
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
915
|
-
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
916
|
-
stopSequences: options == null ? void 0 : options.stop
|
|
917
|
-
}
|
|
918
|
-
}));
|
|
919
|
-
try {
|
|
920
|
-
for (var iter = __forAwait(result.stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
921
|
-
const chunk = temp.value;
|
|
922
|
-
const text = chunk.text();
|
|
923
|
-
if (text) yield text;
|
|
924
|
-
}
|
|
925
|
-
} catch (temp) {
|
|
926
|
-
error = [temp];
|
|
927
|
-
} finally {
|
|
928
|
-
try {
|
|
929
|
-
more && (temp = iter.return) && (yield new __await(temp.call(iter)));
|
|
930
|
-
} finally {
|
|
931
|
-
if (error)
|
|
932
|
-
throw error[0];
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
// -------------------------------------------------------------------------
|
|
938
|
-
// ILLMProvider — embeddings
|
|
939
|
-
// -------------------------------------------------------------------------
|
|
940
|
-
async embed(text, options) {
|
|
941
|
-
var _a, _b;
|
|
942
|
-
const content = applyPrefix(
|
|
943
|
-
text,
|
|
944
|
-
options == null ? void 0 : options.taskType,
|
|
945
|
-
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
946
|
-
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
947
|
-
);
|
|
948
|
-
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
949
|
-
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
950
|
-
const response = await model.embedContent({
|
|
951
|
-
content: { role: "user", parts: [{ text: content }] }
|
|
952
|
-
}).catch((err) => {
|
|
953
|
-
console.error(`[GeminiProvider] Embedding failed for model "${modelName}":`, err.message);
|
|
954
|
-
throw err;
|
|
955
|
-
});
|
|
956
|
-
return response.embedding.values;
|
|
957
|
-
}
|
|
958
|
-
async batchEmbed(texts, options) {
|
|
959
|
-
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
960
|
-
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
961
|
-
const requests = texts.map((text) => {
|
|
962
|
-
var _a, _b;
|
|
963
|
-
return {
|
|
964
|
-
content: {
|
|
965
|
-
role: "user",
|
|
966
|
-
parts: [{
|
|
967
|
-
text: applyPrefix(
|
|
968
|
-
text,
|
|
969
|
-
options == null ? void 0 : options.taskType,
|
|
970
|
-
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
971
|
-
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
972
|
-
)
|
|
973
|
-
}]
|
|
974
|
-
}
|
|
975
|
-
};
|
|
976
|
-
});
|
|
977
|
-
const response = await model.batchEmbedContents({
|
|
978
|
-
requests: requests.map((r) => ({
|
|
979
|
-
model: `models/${modelName}`,
|
|
980
|
-
content: r.content
|
|
981
|
-
}))
|
|
982
|
-
}).catch((err) => {
|
|
983
|
-
console.error(`[GeminiProvider] Batch embedding failed for model "${modelName}":`, err.message);
|
|
984
|
-
throw err;
|
|
985
|
-
});
|
|
986
|
-
return response.embeddings.map((e) => {
|
|
987
|
-
var _a;
|
|
988
|
-
return (_a = e.values) != null ? _a : [];
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
// -------------------------------------------------------------------------
|
|
992
|
-
// ILLMProvider — health
|
|
993
|
-
// -------------------------------------------------------------------------
|
|
994
|
-
async ping() {
|
|
995
|
-
try {
|
|
996
|
-
const model = this.embeddingClient.getGenerativeModel({
|
|
997
|
-
model: this.resolveEmbeddingModel()
|
|
998
|
-
});
|
|
999
|
-
await model.embedContent("ping");
|
|
1000
|
-
return true;
|
|
1001
|
-
} catch (err) {
|
|
1002
|
-
console.error("[GeminiProvider] Ping failed:", err);
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
// src/llm/providers/UniversalLLMAdapter.ts
|
|
1009
|
-
import axios2 from "axios";
|
|
1010
|
-
|
|
1011
|
-
// src/config/UniversalProfiles.ts
|
|
1012
|
-
var OPENAI_BASE = {
|
|
1013
|
-
chatPath: "/chat/completions",
|
|
1014
|
-
embedPath: "/embeddings",
|
|
1015
|
-
responseExtractPath: "choices[0].message.content",
|
|
1016
|
-
embedExtractPath: "data[0].embedding",
|
|
1017
|
-
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"max_tokens":{{maxTokens}},"temperature":{{temperature}}}'
|
|
1018
|
-
};
|
|
1019
|
-
var LLM_PROFILES = {
|
|
1020
|
-
"openai-compatible": OPENAI_BASE,
|
|
1021
|
-
"litellm": __spreadProps(__spreadValues({}, OPENAI_BASE), {
|
|
1022
|
-
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"max_tokens":{{maxTokens}},"temperature":{{temperature}},"stream":false}'
|
|
1023
|
-
}),
|
|
1024
|
-
"anthropic-claude": {
|
|
1025
|
-
chatPath: "/v1/messages",
|
|
1026
|
-
responseExtractPath: "content[0].text",
|
|
1027
|
-
headers: { "anthropic-version": "2023-06-01" },
|
|
1028
|
-
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"max_tokens":{{maxTokens}}}'
|
|
1029
|
-
},
|
|
1030
|
-
"google-gemini": {
|
|
1031
|
-
chatPath: "/v1beta/models/{{model}}:generateContent",
|
|
1032
|
-
responseExtractPath: "candidates[0].content.parts[0].text",
|
|
1033
|
-
chatPayloadTemplate: '{"contents":[{"parts":[{"text":"{{messages}}"}]}]}'
|
|
1034
|
-
},
|
|
1035
|
-
"github-copilot": __spreadProps(__spreadValues({}, OPENAI_BASE), {
|
|
1036
|
-
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"temperature":{{temperature}}}'
|
|
1037
|
-
}),
|
|
1038
|
-
"ollama-standard": {
|
|
1039
|
-
chatPath: "/api/chat",
|
|
1040
|
-
embedPath: "/api/embeddings",
|
|
1041
|
-
responseExtractPath: "message.content",
|
|
1042
|
-
embedExtractPath: "embedding",
|
|
1043
|
-
chatPayloadTemplate: '{"model":"{{model}}","messages":{{messages}},"stream":false}',
|
|
1044
|
-
embedPayloadTemplate: '{"model":"{{model}}","prompt":"{{input}}"}'
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
var VECTOR_PROFILES = {
|
|
1048
|
-
"pinecone-rest": {
|
|
1049
|
-
queryPath: "/query",
|
|
1050
|
-
upsertPath: "/vectors/upsert",
|
|
1051
|
-
responseExtractPath: "matches",
|
|
1052
|
-
idPath: "id",
|
|
1053
|
-
scorePath: "score",
|
|
1054
|
-
contentPath: "metadata.content",
|
|
1055
|
-
metadataPath: "metadata",
|
|
1056
|
-
queryPayloadTemplate: '{"vector":{{vector}},"topK":{{topK}},"includeMetadata":true,"namespace":"{{namespace}}"}'
|
|
1057
|
-
},
|
|
1058
|
-
"mongodb-atlas": {
|
|
1059
|
-
queryPath: "/action/aggregate",
|
|
1060
|
-
responseExtractPath: "documents",
|
|
1061
|
-
idPath: "_id",
|
|
1062
|
-
scorePath: "score",
|
|
1063
|
-
contentPath: "content",
|
|
1064
|
-
metadataPath: "metadata",
|
|
1065
|
-
queryPayloadTemplate: '{"collection":"{{index}}","database":"ai_db","dataSource":"Cluster0","pipeline":[{"$vectorSearch":{"index":"vector_index","path":"embedding","queryVector":{{vector}},"numCandidates":100,"limit":{{topK}}}},{"$project":{"_id":1,"content":1,"metadata":1,"score":{"$meta":"vectorSearchScore"}}}]}'
|
|
1066
|
-
},
|
|
1067
|
-
"chromadb": {
|
|
1068
|
-
queryPath: "/api/v1/collections/{{index}}/query",
|
|
1069
|
-
upsertPath: "/api/v1/collections/{{index}}/add",
|
|
1070
|
-
responseExtractPath: "matches",
|
|
1071
|
-
idPath: "id",
|
|
1072
|
-
scorePath: "distance",
|
|
1073
|
-
contentPath: "document",
|
|
1074
|
-
metadataPath: "metadata"
|
|
1075
|
-
},
|
|
1076
|
-
"qdrant": {
|
|
1077
|
-
queryPath: "/collections/{{index}}/points/search",
|
|
1078
|
-
upsertPath: "/collections/{{index}}/points",
|
|
1079
|
-
responseExtractPath: "result",
|
|
1080
|
-
idPath: "id",
|
|
1081
|
-
scorePath: "score",
|
|
1082
|
-
contentPath: "payload.content",
|
|
1083
|
-
metadataPath: "payload"
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
|
-
|
|
1087
|
-
// src/llm/providers/UniversalLLMAdapter.ts
|
|
1088
|
-
var UniversalLLMAdapter = class {
|
|
1089
|
-
constructor(config) {
|
|
1090
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
1091
|
-
this.model = config.model;
|
|
1092
|
-
const llmConfig = config;
|
|
1093
|
-
const options = (_a = llmConfig.options) != null ? _a : {};
|
|
1094
|
-
let profile = {};
|
|
1095
|
-
if (typeof options.profile === "string") {
|
|
1096
|
-
profile = LLM_PROFILES[options.profile] || {};
|
|
1097
|
-
} else if (typeof options.profile === "object" && options.profile !== null) {
|
|
1098
|
-
profile = options.profile;
|
|
1099
|
-
}
|
|
1100
|
-
this.opts = __spreadValues(__spreadValues({}, profile), (_b = llmConfig.options) != null ? _b : {});
|
|
1101
|
-
this.systemPrompt = (_c = llmConfig.systemPrompt) != null ? _c : "You are a helpful AI assistant. Use the provided context to answer the user.";
|
|
1102
|
-
this.maxTokens = (_d = llmConfig.maxTokens) != null ? _d : 1024;
|
|
1103
|
-
this.temperature = (_e = llmConfig.temperature) != null ? _e : 0;
|
|
1104
|
-
const baseUrl = (_f = llmConfig.baseUrl) != null ? _f : this.opts.baseUrl;
|
|
1105
|
-
if (!baseUrl) {
|
|
1106
|
-
throw new Error("[UniversalLLMAdapter] baseUrl is required in config or config.options");
|
|
1107
|
-
}
|
|
1108
|
-
this.http = axios2.create({
|
|
1109
|
-
baseURL: baseUrl,
|
|
1110
|
-
headers: __spreadValues(__spreadValues({
|
|
1111
|
-
"Content-Type": "application/json"
|
|
1112
|
-
}, config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), this.opts.headers || {}),
|
|
1113
|
-
timeout: (_g = this.opts.timeout) != null ? _g : 6e4
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
async chat(messages, context) {
|
|
1117
|
-
var _a, _b;
|
|
1118
|
-
const path = (_a = this.opts.chatPath) != null ? _a : "/chat/completions";
|
|
1119
|
-
const formattedMessages = [
|
|
1120
|
-
{ role: "system", content: `${this.systemPrompt}
|
|
1121
|
-
|
|
1122
|
-
Context:
|
|
1123
|
-
${context != null ? context : "None"}` },
|
|
1124
|
-
...messages
|
|
1125
|
-
];
|
|
1126
|
-
let payload;
|
|
1127
|
-
if (this.opts.chatPayloadTemplate) {
|
|
1128
|
-
payload = buildPayload(this.opts.chatPayloadTemplate, {
|
|
1129
|
-
model: this.model,
|
|
1130
|
-
messages: formattedMessages,
|
|
1131
|
-
maxTokens: this.maxTokens,
|
|
1132
|
-
temperature: this.temperature
|
|
1133
|
-
});
|
|
1134
|
-
} else {
|
|
1135
|
-
payload = {
|
|
1136
|
-
model: this.model,
|
|
1137
|
-
messages: formattedMessages,
|
|
1138
|
-
max_tokens: this.maxTokens,
|
|
1139
|
-
temperature: this.temperature
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
const { data } = await this.http.post(path, payload);
|
|
1143
|
-
const extractPath = (_b = this.opts.responseExtractPath) != null ? _b : "choices[0].message.content";
|
|
1144
|
-
const result = resolvePath(data, extractPath);
|
|
1145
|
-
if (result === void 0) {
|
|
1146
|
-
throw new Error(`[UniversalLLMAdapter] Could not extract text from path '${extractPath}' in response.`);
|
|
1147
|
-
}
|
|
1148
|
-
return String(result);
|
|
1149
|
-
}
|
|
1150
|
-
async embed(text) {
|
|
1151
|
-
var _a, _b;
|
|
1152
|
-
const path = (_a = this.opts.embedPath) != null ? _a : "/embeddings";
|
|
1153
|
-
let payload;
|
|
1154
|
-
if (this.opts.embedPayloadTemplate) {
|
|
1155
|
-
payload = buildPayload(this.opts.embedPayloadTemplate, {
|
|
1156
|
-
model: this.model,
|
|
1157
|
-
input: text
|
|
1158
|
-
});
|
|
1159
|
-
} else {
|
|
1160
|
-
payload = {
|
|
1161
|
-
model: this.model,
|
|
1162
|
-
input: text
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
const { data } = await this.http.post(path, payload);
|
|
1166
|
-
const extractPath = (_b = this.opts.embedExtractPath) != null ? _b : "data[0].embedding";
|
|
1167
|
-
const vector = resolvePath(data, extractPath);
|
|
1168
|
-
if (!Array.isArray(vector)) {
|
|
1169
|
-
throw new Error(`[UniversalLLMAdapter] Expected a number array at '${extractPath}' for embeddings.`);
|
|
1170
|
-
}
|
|
1171
|
-
return vector;
|
|
1172
|
-
}
|
|
1173
|
-
async batchEmbed(texts) {
|
|
1174
|
-
const vectors = [];
|
|
1175
|
-
for (const text of texts) {
|
|
1176
|
-
vectors.push(await this.embed(text));
|
|
1177
|
-
}
|
|
1178
|
-
return vectors;
|
|
1179
|
-
}
|
|
1180
|
-
async ping() {
|
|
1181
|
-
try {
|
|
1182
|
-
if (this.opts.pingPath) {
|
|
1183
|
-
await this.http.get(this.opts.pingPath);
|
|
1184
|
-
}
|
|
1185
|
-
return true;
|
|
1186
|
-
} catch (err) {
|
|
1187
|
-
console.error("[UniversalLLMAdapter] Ping failed:", err);
|
|
1188
|
-
return false;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
};
|
|
1192
|
-
|
|
1193
|
-
// src/llm/LLMFactory.ts
|
|
1194
|
-
var customProviders = /* @__PURE__ */ new Map();
|
|
1195
|
-
var LLMFactory = class _LLMFactory {
|
|
1196
|
-
/**
|
|
1197
|
-
* Register a custom LLM provider factory at runtime.
|
|
1198
|
-
*
|
|
1199
|
-
* Use this to add support for any LLM backend (Azure OpenAI, Cohere, Mistral,
|
|
1200
|
-
* Bedrock, etc.) without modifying this library's source code.
|
|
1201
|
-
*
|
|
1202
|
-
* @example
|
|
1203
|
-
* // In your Next.js app initialization:
|
|
1204
|
-
* import { LLMFactory } from '@retrivora-ai/rag-engine/server';
|
|
1205
|
-
* import { MyCustomProvider } from './providers/MyCustomProvider';
|
|
1206
|
-
*
|
|
1207
|
-
* LLMFactory.register('my-provider', (config) => new MyCustomProvider(config));
|
|
1208
|
-
*
|
|
1209
|
-
* // Then set in your .env.local:
|
|
1210
|
-
* // LLM_PROVIDER=my-provider
|
|
1211
|
-
*/
|
|
1212
|
-
static register(name, factory) {
|
|
1213
|
-
customProviders.set(name.toLowerCase(), factory);
|
|
1214
|
-
console.log(`[LLMFactory] Registered custom provider: "${name}"`);
|
|
1215
|
-
}
|
|
1216
|
-
/**
|
|
1217
|
-
* Unregister a previously registered custom provider.
|
|
1218
|
-
*/
|
|
1219
|
-
static unregister(name) {
|
|
1220
|
-
customProviders.delete(name.toLowerCase());
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* List all registered provider names (built-in + custom).
|
|
1224
|
-
*/
|
|
1225
|
-
static listProviders() {
|
|
1226
|
-
return [
|
|
1227
|
-
"openai",
|
|
1228
|
-
"anthropic",
|
|
1229
|
-
"ollama",
|
|
1230
|
-
"gemini",
|
|
1231
|
-
"rest",
|
|
1232
|
-
"universal_rest",
|
|
1233
|
-
"custom",
|
|
1234
|
-
...Array.from(customProviders.keys())
|
|
1235
|
-
];
|
|
1236
|
-
}
|
|
1237
|
-
static create(llmConfig, embeddingConfig) {
|
|
1238
|
-
var _a, _b;
|
|
1239
|
-
switch (llmConfig.provider) {
|
|
1240
|
-
case "openai":
|
|
1241
|
-
return new OpenAIProvider(llmConfig, embeddingConfig);
|
|
1242
|
-
case "anthropic":
|
|
1243
|
-
return new AnthropicProvider(llmConfig, embeddingConfig);
|
|
1244
|
-
case "ollama":
|
|
1245
|
-
return new OllamaProvider(llmConfig, embeddingConfig);
|
|
1246
|
-
case "gemini":
|
|
1247
|
-
return new GeminiProvider(llmConfig, embeddingConfig);
|
|
1248
|
-
case "rest":
|
|
1249
|
-
case "universal_rest":
|
|
1250
|
-
case "custom":
|
|
1251
|
-
return new UniversalLLMAdapter(llmConfig);
|
|
1252
|
-
default: {
|
|
1253
|
-
const providerName = String((_a = llmConfig.provider) != null ? _a : "").toLowerCase();
|
|
1254
|
-
const customFactory = customProviders.get(providerName);
|
|
1255
|
-
if (customFactory) {
|
|
1256
|
-
return customFactory(llmConfig);
|
|
1257
|
-
}
|
|
1258
|
-
if (llmConfig.baseUrl || ((_b = llmConfig.options) == null ? void 0 : _b.baseUrl)) {
|
|
1259
|
-
return new UniversalLLMAdapter(llmConfig);
|
|
1260
|
-
}
|
|
1261
|
-
throw new Error(
|
|
1262
|
-
`[LLMFactory] Unknown provider "${llmConfig.provider}". Built-in providers: ${_LLMFactory.listProviders().join(", ")}. Register a custom provider with LLMFactory.register().`
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
static getValidator(provider) {
|
|
1268
|
-
const providerClass = this.getProviderClass(provider);
|
|
1269
|
-
return providerClass && providerClass.getValidator ? providerClass.getValidator() : null;
|
|
1270
|
-
}
|
|
1271
|
-
static getHealthChecker(provider) {
|
|
1272
|
-
const providerClass = this.getProviderClass(provider);
|
|
1273
|
-
return providerClass && providerClass.getHealthChecker ? providerClass.getHealthChecker() : null;
|
|
1274
|
-
}
|
|
1275
|
-
static getProviderClass(provider) {
|
|
1276
|
-
switch (provider) {
|
|
1277
|
-
case "openai":
|
|
1278
|
-
return OpenAIProvider;
|
|
1279
|
-
case "anthropic":
|
|
1280
|
-
return AnthropicProvider;
|
|
1281
|
-
case "ollama":
|
|
1282
|
-
return OllamaProvider;
|
|
1283
|
-
case "gemini":
|
|
1284
|
-
return GeminiProvider;
|
|
1285
|
-
case "rest":
|
|
1286
|
-
case "universal_rest":
|
|
1287
|
-
case "custom":
|
|
1288
|
-
return UniversalLLMAdapter;
|
|
1289
|
-
default:
|
|
1290
|
-
return null;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
/**
|
|
1294
|
-
* Creates a dedicated embedding-only provider.
|
|
1295
|
-
*/
|
|
1296
|
-
static createEmbeddingProvider(embeddingConfig) {
|
|
1297
|
-
const fakeLLMConfig = {
|
|
1298
|
-
provider: embeddingConfig.provider,
|
|
1299
|
-
model: embeddingConfig.model,
|
|
1300
|
-
apiKey: embeddingConfig.apiKey,
|
|
1301
|
-
baseUrl: embeddingConfig.baseUrl,
|
|
1302
|
-
options: embeddingConfig.options
|
|
1303
|
-
};
|
|
1304
|
-
return _LLMFactory.create(fakeLLMConfig, embeddingConfig);
|
|
1305
|
-
}
|
|
1306
|
-
};
|
|
1307
|
-
|
|
1308
|
-
// src/core/ProviderRegistry.ts
|
|
1309
|
-
var ProviderRegistry = class {
|
|
1310
|
-
static registerVectorProvider(name, providerClass) {
|
|
1311
|
-
this.vectorProviders[name] = providerClass;
|
|
1312
|
-
if (providerClass.getValidator) {
|
|
1313
|
-
this.vectorValidators[name] = providerClass.getValidator();
|
|
1314
|
-
}
|
|
1315
|
-
if (providerClass.getHealthChecker) {
|
|
1316
|
-
this.vectorHealthCheckers[name] = providerClass.getHealthChecker();
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
static async getVectorValidator(provider) {
|
|
1320
|
-
if (this.vectorValidators[provider]) return this.vectorValidators[provider];
|
|
1321
|
-
try {
|
|
1322
|
-
const providerClass = await this.loadVectorProviderClass(provider);
|
|
1323
|
-
if (providerClass.getValidator) {
|
|
1324
|
-
this.vectorValidators[provider] = providerClass.getValidator();
|
|
1325
|
-
return this.vectorValidators[provider];
|
|
1326
|
-
}
|
|
1327
|
-
} catch (e) {
|
|
1328
|
-
console.warn(`[ProviderRegistry] Failed to load validator for ${provider}:`, e);
|
|
1329
|
-
}
|
|
1330
|
-
return null;
|
|
1331
|
-
}
|
|
1332
|
-
static async getVectorHealthChecker(provider) {
|
|
1333
|
-
if (this.vectorHealthCheckers[provider]) return this.vectorHealthCheckers[provider];
|
|
1334
|
-
try {
|
|
1335
|
-
const providerClass = await this.loadVectorProviderClass(provider);
|
|
1336
|
-
if (providerClass.getHealthChecker) {
|
|
1337
|
-
this.vectorHealthCheckers[provider] = providerClass.getHealthChecker();
|
|
1338
|
-
return this.vectorHealthCheckers[provider];
|
|
1339
|
-
}
|
|
1340
|
-
} catch (e) {
|
|
1341
|
-
console.warn(`[ProviderRegistry] Failed to load health checker for ${provider}:`, e);
|
|
1342
|
-
}
|
|
1343
|
-
return null;
|
|
1344
|
-
}
|
|
1345
|
-
static async loadVectorProviderClass(provider) {
|
|
1346
|
-
if (this.vectorProviders[provider]) return this.vectorProviders[provider];
|
|
1347
|
-
switch (provider) {
|
|
1348
|
-
case "pinecone": {
|
|
1349
|
-
const { PineconeProvider } = await import("./PineconeProvider-QZNRKTN2.mjs");
|
|
1350
|
-
return PineconeProvider;
|
|
1351
|
-
}
|
|
1352
|
-
case "pgvector":
|
|
1353
|
-
case "postgresql": {
|
|
1354
|
-
const { MultiTablePostgresProvider } = await import("./MultiTablePostgresProvider-ZLGSKTJR.mjs");
|
|
1355
|
-
return MultiTablePostgresProvider;
|
|
1356
|
-
}
|
|
1357
|
-
case "mongodb": {
|
|
1358
|
-
const { MongoDBProvider } = await import("./MongoDBProvider-YNKC7EJ6.mjs");
|
|
1359
|
-
return MongoDBProvider;
|
|
1360
|
-
}
|
|
1361
|
-
case "milvus": {
|
|
1362
|
-
const { MilvusProvider } = await import("./MilvusProvider-U7SKC27V.mjs");
|
|
1363
|
-
return MilvusProvider;
|
|
1364
|
-
}
|
|
1365
|
-
case "qdrant": {
|
|
1366
|
-
const { QdrantProvider } = await import("./QdrantProvider-RLJTNGPY.mjs");
|
|
1367
|
-
return QdrantProvider;
|
|
1368
|
-
}
|
|
1369
|
-
case "chromadb": {
|
|
1370
|
-
const { ChromaDBProvider } = await import("./ChromaDBProvider-MIDOR4FW.mjs");
|
|
1371
|
-
return ChromaDBProvider;
|
|
1372
|
-
}
|
|
1373
|
-
case "redis": {
|
|
1374
|
-
const { RedisProvider } = await import("./RedisProvider-SR65SCKV.mjs");
|
|
1375
|
-
return RedisProvider;
|
|
1376
|
-
}
|
|
1377
|
-
case "weaviate": {
|
|
1378
|
-
const { WeaviateProvider } = await import("./WeaviateProvider-5FWDFITI.mjs");
|
|
1379
|
-
return WeaviateProvider;
|
|
1380
|
-
}
|
|
1381
|
-
case "universal_rest":
|
|
1382
|
-
case "rest": {
|
|
1383
|
-
const { UniversalVectorProvider } = await import("./UniversalVectorProvider-IN67OS56.mjs");
|
|
1384
|
-
return UniversalVectorProvider;
|
|
1385
|
-
}
|
|
1386
|
-
default:
|
|
1387
|
-
throw new Error(`Unsupported vector provider: ${provider}`);
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
static async createVectorProvider(config) {
|
|
1391
|
-
const providerClass = await this.loadVectorProviderClass(config.provider);
|
|
1392
|
-
return new providerClass(config);
|
|
1393
|
-
}
|
|
1394
|
-
static async createGraphProvider(config) {
|
|
1395
|
-
const { provider } = config;
|
|
1396
|
-
if (this.graphProviders[provider]) {
|
|
1397
|
-
return new this.graphProviders[provider](config);
|
|
1398
|
-
}
|
|
1399
|
-
switch (provider) {
|
|
1400
|
-
case "simple": {
|
|
1401
|
-
const { SimpleGraphProvider } = await import("./SimpleGraphProvider-SLOXO4M7.mjs");
|
|
1402
|
-
return new SimpleGraphProvider(config);
|
|
1403
|
-
}
|
|
1404
|
-
default:
|
|
1405
|
-
throw new Error(`Unsupported graph provider: ${provider}`);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
static createLLMProvider(llmConfig, embeddingConfig) {
|
|
1409
|
-
return LLMFactory.create(llmConfig, embeddingConfig);
|
|
1410
|
-
}
|
|
1411
|
-
};
|
|
1412
|
-
ProviderRegistry.vectorProviders = {};
|
|
1413
|
-
ProviderRegistry.graphProviders = {};
|
|
1414
|
-
ProviderRegistry.vectorValidators = {};
|
|
1415
|
-
ProviderRegistry.vectorHealthCheckers = {};
|
|
1416
|
-
ProviderRegistry.llmValidators = {};
|
|
1417
|
-
ProviderRegistry.llmHealthCheckers = {};
|
|
1418
|
-
|
|
1419
|
-
// src/core/ConfigValidator.ts
|
|
1420
|
-
var ConfigValidator = class {
|
|
1421
|
-
/**
|
|
1422
|
-
* Validates the entire RagConfig object.
|
|
1423
|
-
*/
|
|
1424
|
-
static async validate(config) {
|
|
1425
|
-
const errors = [];
|
|
1426
|
-
if (!config.projectId) {
|
|
1427
|
-
errors.push({
|
|
1428
|
-
field: "projectId",
|
|
1429
|
-
message: "projectId is required",
|
|
1430
|
-
severity: "error"
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
errors.push(...await this.validateVectorDbConfig(config.vectorDb));
|
|
1434
|
-
errors.push(...await this.validateLLMConfig(config.llm));
|
|
1435
|
-
if (config.embedding) {
|
|
1436
|
-
errors.push(...await this.validateEmbeddingConfig(config.embedding));
|
|
1437
|
-
} else if (config.llm.provider === "anthropic") {
|
|
1438
|
-
errors.push({
|
|
1439
|
-
field: "embedding",
|
|
1440
|
-
message: "Embedding config is required when using Anthropic LLM",
|
|
1441
|
-
suggestion: 'Provide embedding config with provider (e.g., "openai" or "ollama")',
|
|
1442
|
-
severity: "error"
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
if (config.ui) {
|
|
1446
|
-
errors.push(...this.validateUIConfig(config.ui));
|
|
1447
|
-
}
|
|
1448
|
-
if (config.rag) {
|
|
1449
|
-
errors.push(...this.validateRAGConfig(config.rag));
|
|
1450
|
-
}
|
|
1451
|
-
return errors;
|
|
1452
|
-
}
|
|
1453
|
-
static async validateVectorDbConfig(config) {
|
|
1454
|
-
const errors = [];
|
|
1455
|
-
if (!config.provider) {
|
|
1456
|
-
errors.push({ field: "vectorDb.provider", message: "Vector database provider is required", severity: "error" });
|
|
1457
|
-
return errors;
|
|
1458
|
-
}
|
|
1459
|
-
if (!config.indexName) {
|
|
1460
|
-
errors.push({ field: "vectorDb.indexName", message: "Vector database index name is required", severity: "error" });
|
|
1461
|
-
}
|
|
1462
|
-
const validator = await ProviderRegistry.getVectorValidator(config.provider);
|
|
1463
|
-
if (validator) {
|
|
1464
|
-
errors.push(...validator.validate(config));
|
|
1465
|
-
} else {
|
|
1466
|
-
this.fallbackVectorValidation(config, errors);
|
|
1467
|
-
}
|
|
1468
|
-
return errors;
|
|
1469
|
-
}
|
|
1470
|
-
static async validateLLMConfig(config) {
|
|
1471
|
-
const errors = [];
|
|
1472
|
-
if (!config.provider) {
|
|
1473
|
-
errors.push({ field: "llm.provider", message: "LLM provider is required", severity: "error" });
|
|
1474
|
-
return errors;
|
|
1475
|
-
}
|
|
1476
|
-
const validator = LLMFactory.getValidator(config.provider);
|
|
1477
|
-
if (validator) {
|
|
1478
|
-
errors.push(...validator.validate(config));
|
|
1479
|
-
} else {
|
|
1480
|
-
this.fallbackLLMValidation(config, errors);
|
|
1481
|
-
}
|
|
1482
|
-
if (config.temperature !== void 0 && (config.temperature < 0 || config.temperature > 2)) {
|
|
1483
|
-
errors.push({ field: "llm.temperature", message: "Temperature must be between 0 and 2", severity: "error" });
|
|
1484
|
-
}
|
|
1485
|
-
return errors;
|
|
1486
|
-
}
|
|
1487
|
-
static async validateEmbeddingConfig(config) {
|
|
1488
|
-
const errors = [];
|
|
1489
|
-
if (!config.provider) {
|
|
1490
|
-
errors.push({ field: "embedding.provider", message: "Embedding provider is required", severity: "error" });
|
|
1491
|
-
return errors;
|
|
1492
|
-
}
|
|
1493
|
-
const validator = LLMFactory.getValidator(config.provider);
|
|
1494
|
-
if (validator) {
|
|
1495
|
-
errors.push(...validator.validate(config));
|
|
1496
|
-
} else {
|
|
1497
|
-
this.fallbackEmbeddingValidation(config, errors);
|
|
1498
|
-
}
|
|
1499
|
-
return errors;
|
|
1500
|
-
}
|
|
1501
|
-
/**
|
|
1502
|
-
* Temporary fallbacks for providers not yet migrated to the pluggable architecture.
|
|
1503
|
-
*/
|
|
1504
|
-
static fallbackVectorValidation(config, errors) {
|
|
1505
|
-
const opts = config.options || {};
|
|
1506
|
-
switch (config.provider) {
|
|
1507
|
-
case "milvus":
|
|
1508
|
-
if (!opts.baseUrl && !opts.uri && !(opts.host && opts.port)) {
|
|
1509
|
-
errors.push({ field: "vectorDb.options.baseUrl", message: "Milvus connection info required", severity: "error" });
|
|
1510
|
-
}
|
|
1511
|
-
break;
|
|
1512
|
-
case "qdrant":
|
|
1513
|
-
if (!opts.baseUrl && !opts.url) {
|
|
1514
|
-
errors.push({ field: "vectorDb.options.baseUrl", message: "Qdrant URL required", severity: "error" });
|
|
1515
|
-
}
|
|
1516
|
-
break;
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
static fallbackLLMValidation(config, errors) {
|
|
1520
|
-
if (!config.model) {
|
|
1521
|
-
errors.push({ field: "llm.model", message: "LLM model name is required", severity: "error" });
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
static fallbackEmbeddingValidation(config, errors) {
|
|
1525
|
-
if (!config.model) {
|
|
1526
|
-
errors.push({ field: "embedding.model", message: "Embedding model name is required", severity: "error" });
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
static validateUIConfig(config) {
|
|
1530
|
-
const errors = [];
|
|
1531
|
-
if (config.primaryColor && !this.isValidCSSColor(config.primaryColor)) {
|
|
1532
|
-
errors.push({ field: "ui.primaryColor", message: "Invalid CSS color format", severity: "warning" });
|
|
1533
|
-
}
|
|
1534
|
-
if (config.borderRadius && !UI_BORDER_RADIUS_OPTIONS.includes(config.borderRadius)) {
|
|
1535
|
-
errors.push({ field: "ui.borderRadius", message: `borderRadius must be one of: ${UI_BORDER_RADIUS_OPTIONS.join(", ")}`, severity: "warning" });
|
|
1536
|
-
}
|
|
1537
|
-
return errors;
|
|
1538
|
-
}
|
|
1539
|
-
static validateRAGConfig(config) {
|
|
1540
|
-
const errors = [];
|
|
1541
|
-
if (config.topK !== void 0 && (typeof config.topK !== "number" || config.topK <= 0)) {
|
|
1542
|
-
errors.push({ field: "rag.topK", message: "topK must be a positive integer", severity: "error" });
|
|
1543
|
-
}
|
|
1544
|
-
return errors;
|
|
1545
|
-
}
|
|
1546
|
-
static isValidCSSColor(color) {
|
|
1547
|
-
const hexRegex = /^#([0-9a-f]{3}){1,2}$/i;
|
|
1548
|
-
const rgbRegex = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/i;
|
|
1549
|
-
const namedColors = ["red", "blue", "green", "black", "white", "transparent"];
|
|
1550
|
-
return hexRegex.test(color) || rgbRegex.test(color) || namedColors.includes(color.toLowerCase());
|
|
1551
|
-
}
|
|
1552
|
-
static async validateAndThrow(config) {
|
|
1553
|
-
const errors = await this.validate(config);
|
|
1554
|
-
const errorItems = errors.filter((e) => e.severity === "error");
|
|
1555
|
-
if (errorItems.length > 0) {
|
|
1556
|
-
const message = errorItems.map((e) => `${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n ");
|
|
1557
|
-
throw new Error(`[ConfigValidator] Configuration validation failed:
|
|
1558
|
-
${message}`);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
|
-
// src/rag/DocumentChunker.ts
|
|
1564
|
-
var DocumentChunker = class {
|
|
1565
|
-
constructor(chunkSize = 1e3, chunkOverlap = 200, separators = ["\n# ", "\n## ", "\n### ", "\n#### ", "\n\n", "\n", " ", ""]) {
|
|
1566
|
-
this.chunkSize = chunkSize;
|
|
1567
|
-
this.chunkOverlap = chunkOverlap;
|
|
1568
|
-
this.separators = separators;
|
|
1569
|
-
}
|
|
1570
|
-
/**
|
|
1571
|
-
* Split a single text string into overlapping chunks using a recursive strategy.
|
|
1572
|
-
* Preserves structural boundaries (Markdown headers) where possible.
|
|
1573
|
-
*/
|
|
1574
|
-
chunk(text, options = {}) {
|
|
1575
|
-
const {
|
|
1576
|
-
chunkSize = this.chunkSize,
|
|
1577
|
-
chunkOverlap = this.chunkOverlap,
|
|
1578
|
-
docId = `doc_${Date.now()}`,
|
|
1579
|
-
metadata = {},
|
|
1580
|
-
separators = this.separators
|
|
1581
|
-
} = options;
|
|
1582
|
-
const finalChunks = [];
|
|
1583
|
-
const splits = this.recursiveSplit(text, separators, chunkSize);
|
|
1584
|
-
let currentChunk = [];
|
|
1585
|
-
let currentLength = 0;
|
|
1586
|
-
let chunkIndex = 0;
|
|
1587
|
-
for (const split of splits) {
|
|
1588
|
-
if (currentLength + split.length > chunkSize && currentChunk.length > 0) {
|
|
1589
|
-
finalChunks.push({
|
|
1590
|
-
id: `${docId}_chunk_${chunkIndex++}`,
|
|
1591
|
-
content: currentChunk.join("").trim(),
|
|
1592
|
-
metadata: __spreadProps(__spreadValues({}, metadata), { docId, chunkIndex: chunkIndex - 1 })
|
|
1593
|
-
});
|
|
1594
|
-
const overlapItems = [];
|
|
1595
|
-
let overlapLen = 0;
|
|
1596
|
-
for (let i = currentChunk.length - 1; i >= 0; i--) {
|
|
1597
|
-
if (overlapLen + currentChunk[i].length <= chunkOverlap) {
|
|
1598
|
-
overlapItems.unshift(currentChunk[i]);
|
|
1599
|
-
overlapLen += currentChunk[i].length;
|
|
1600
|
-
} else {
|
|
1601
|
-
break;
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
currentChunk = overlapItems;
|
|
1605
|
-
currentLength = overlapLen;
|
|
1606
|
-
}
|
|
1607
|
-
currentChunk.push(split);
|
|
1608
|
-
currentLength += split.length;
|
|
1609
|
-
}
|
|
1610
|
-
if (currentChunk.length > 0) {
|
|
1611
|
-
finalChunks.push({
|
|
1612
|
-
id: `${docId}_chunk_${chunkIndex}`,
|
|
1613
|
-
content: currentChunk.join("").trim(),
|
|
1614
|
-
metadata: __spreadProps(__spreadValues({}, metadata), { docId, chunkIndex })
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
return finalChunks;
|
|
1618
|
-
}
|
|
1619
|
-
recursiveSplit(text, separators, chunkSize) {
|
|
1620
|
-
const finalSplits = [];
|
|
1621
|
-
let separator = separators[separators.length - 1];
|
|
1622
|
-
let nextSeparators = [];
|
|
1623
|
-
for (let i = 0; i < separators.length; i++) {
|
|
1624
|
-
const sep = separators[i];
|
|
1625
|
-
if (text.includes(sep)) {
|
|
1626
|
-
separator = sep;
|
|
1627
|
-
nextSeparators = separators.slice(i + 1);
|
|
1628
|
-
break;
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
const parts = text.split(separator);
|
|
1632
|
-
for (const part of parts) {
|
|
1633
|
-
if (part.length <= chunkSize) {
|
|
1634
|
-
finalSplits.push(part + (part === parts[parts.length - 1] ? "" : separator));
|
|
1635
|
-
} else if (nextSeparators.length > 0) {
|
|
1636
|
-
finalSplits.push(...this.recursiveSplit(part, nextSeparators, chunkSize));
|
|
1637
|
-
} else {
|
|
1638
|
-
finalSplits.push(part);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
return finalSplits;
|
|
1642
|
-
}
|
|
1643
|
-
chunkMany(documents) {
|
|
1644
|
-
return documents.flatMap(
|
|
1645
|
-
(doc) => this.chunk(doc.content, { docId: doc.docId, metadata: doc.metadata })
|
|
1646
|
-
);
|
|
1647
|
-
}
|
|
1648
|
-
};
|
|
1649
|
-
|
|
1650
|
-
// src/rag/EntityExtractor.ts
|
|
1651
|
-
var EntityExtractor = class {
|
|
1652
|
-
constructor(llm) {
|
|
1653
|
-
this.llm = llm;
|
|
1654
|
-
}
|
|
1655
|
-
/**
|
|
1656
|
-
* Extract nodes and edges from a text chunk.
|
|
1657
|
-
*/
|
|
1658
|
-
async extract(text) {
|
|
1659
|
-
const prompt = `
|
|
1660
|
-
Extract entities and relationships from the following text.
|
|
1661
|
-
Format the output as a JSON object with exactly two keys: "nodes" and "edges".
|
|
1662
|
-
|
|
1663
|
-
Nodes: {id, label, properties}
|
|
1664
|
-
Edges: {source, target, type, properties}
|
|
1665
|
-
|
|
1666
|
-
IMPORTANT:
|
|
1667
|
-
- Ensure all property values are simple JSON types (string, number, boolean).
|
|
1668
|
-
- Do not use mathematical expressions or complex nested objects in properties.
|
|
1669
|
-
- If no entities are found, return empty arrays.
|
|
1670
|
-
- RESPOND ONLY WITH THE JSON OBJECT.
|
|
1671
|
-
|
|
1672
|
-
Text to extract from:
|
|
1673
|
-
"${text}"
|
|
1674
|
-
`;
|
|
1675
|
-
const response = await this.llm.chat([
|
|
1676
|
-
{ role: "system", content: "You are a precise knowledge graph extraction engine. You always output valid JSON and nothing else." },
|
|
1677
|
-
{ role: "user", content: prompt }
|
|
1678
|
-
], "", { maxTokens: 4096, temperature: 0.1 });
|
|
1679
|
-
try {
|
|
1680
|
-
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1681
|
-
const cleanJson = jsonMatch ? jsonMatch[0] : response.trim();
|
|
1682
|
-
return JSON.parse(cleanJson);
|
|
1683
|
-
} catch (e) {
|
|
1684
|
-
try {
|
|
1685
|
-
const repaired = this.repairTruncatedJson(response);
|
|
1686
|
-
if (repaired) return JSON.parse(repaired);
|
|
1687
|
-
} catch (e2) {
|
|
1688
|
-
}
|
|
1689
|
-
console.warn(`[EntityExtractor] Failed to parse LLM response. Length: ${response.length} chars.`);
|
|
1690
|
-
console.warn("[EntityExtractor] Snippet:", response.substring(0, 100) + "...");
|
|
1691
|
-
return { nodes: [], edges: [] };
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
/**
|
|
1695
|
-
* Attempts to fix JSON that was cut off mid-generation.
|
|
1696
|
-
* Strategy: Find the last valid element in an array and close the structure.
|
|
1697
|
-
*/
|
|
1698
|
-
repairTruncatedJson(json) {
|
|
1699
|
-
let text = json.trim();
|
|
1700
|
-
const startIdx = text.indexOf("{");
|
|
1701
|
-
if (startIdx === -1) return null;
|
|
1702
|
-
text = text.substring(startIdx);
|
|
1703
|
-
const lastCompleteObjectIdx = text.lastIndexOf("}");
|
|
1704
|
-
if (lastCompleteObjectIdx === -1) return null;
|
|
1705
|
-
let repaired = text.substring(0, lastCompleteObjectIdx + 1);
|
|
1706
|
-
const openBrackets = (repaired.match(/\{/g) || []).length;
|
|
1707
|
-
const closedBrackets = (repaired.match(/\}/g) || []).length;
|
|
1708
|
-
const openSquares = (repaired.match(/\[/g) || []).length;
|
|
1709
|
-
const closedSquares = (repaired.match(/\]/g) || []).length;
|
|
1710
|
-
for (let i = 0; i < openSquares - closedSquares; i++) repaired += "]";
|
|
1711
|
-
for (let i = 0; i < openBrackets - closedBrackets; i++) repaired += "}";
|
|
1712
|
-
return repaired;
|
|
1713
|
-
}
|
|
1714
|
-
};
|
|
1715
|
-
|
|
1716
|
-
// src/rag/Reranker.ts
|
|
1717
|
-
var Reranker = class {
|
|
1718
|
-
/**
|
|
1719
|
-
* Re-ranks matches based on a secondary relevance score.
|
|
1720
|
-
* In a production environment, this would call a Cross-Encoder model.
|
|
1721
|
-
* Here we implement a placeholder that filters by score and limits count.
|
|
1722
|
-
*/
|
|
1723
|
-
async rerank(matches, query, limit = 5) {
|
|
1724
|
-
return matches.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1725
|
-
}
|
|
1726
|
-
};
|
|
1727
|
-
|
|
1728
|
-
// src/rag/LlamaIndexIngestor.ts
|
|
1729
|
-
var LlamaIndexIngestor = class {
|
|
1730
|
-
/**
|
|
1731
|
-
* Chunks document content using LlamaIndex SentenceSplitter.
|
|
1732
|
-
* This respects sentence and paragraph boundaries much more effectively
|
|
1733
|
-
* than standard character-count splitting.
|
|
1734
|
-
*/
|
|
1735
|
-
async chunk(text, options = {}) {
|
|
1736
|
-
var _a, _b;
|
|
1737
|
-
try {
|
|
1738
|
-
const { Document, SentenceSplitter, MetadataMode } = await import(`${"llamaindex"}`);
|
|
1739
|
-
const splitter = new SentenceSplitter({
|
|
1740
|
-
chunkSize: (_a = options.chunkSize) != null ? _a : 1e3,
|
|
1741
|
-
chunkOverlap: (_b = options.chunkOverlap) != null ? _b : 200
|
|
1742
|
-
});
|
|
1743
|
-
const doc = new Document({ text, metadata: options.metadata || {} });
|
|
1744
|
-
const nodes = splitter.getNodesFromDocuments([doc]);
|
|
1745
|
-
return nodes.map((node, index) => ({
|
|
1746
|
-
id: `${options.docId || "doc"}_node_${index}`,
|
|
1747
|
-
content: node.getContent(MetadataMode.ALL),
|
|
1748
|
-
metadata: __spreadProps(__spreadValues(__spreadValues({}, options.metadata), node.metadata), {
|
|
1749
|
-
nodeId: node.id_,
|
|
1750
|
-
chunkIndex: index
|
|
1751
|
-
})
|
|
1752
|
-
}));
|
|
1753
|
-
} catch (error) {
|
|
1754
|
-
console.warn("[LlamaIndexIngestor] LlamaIndex package not found or failed. Falling back to default chunker.");
|
|
1755
|
-
throw error;
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Batch processing for multiple documents.
|
|
1760
|
-
*/
|
|
1761
|
-
async chunkMany(documents, options = {}) {
|
|
1762
|
-
const allChunks = [];
|
|
1763
|
-
for (const doc of documents) {
|
|
1764
|
-
const chunks = await this.chunk(doc.content, __spreadProps(__spreadValues({}, options), { docId: doc.docId, metadata: doc.metadata }));
|
|
1765
|
-
allChunks.push(...chunks);
|
|
1766
|
-
}
|
|
1767
|
-
return allChunks;
|
|
1768
|
-
}
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
// src/core/LangChainAgent.ts
|
|
1772
|
-
var LangChainAgent = class {
|
|
1773
|
-
constructor(pipeline, config) {
|
|
1774
|
-
this.pipeline = pipeline;
|
|
1775
|
-
this.config = config;
|
|
1776
|
-
}
|
|
1777
|
-
/**
|
|
1778
|
-
* Initializes the LangChain ReAct agent with the RAG pipeline as a tool.
|
|
1779
|
-
* Dynamically imports langchain so the package doesn't crash if it isn't installed.
|
|
1780
|
-
*/
|
|
1781
|
-
async initialize(chatModel) {
|
|
1782
|
-
try {
|
|
1783
|
-
const [{ DynamicTool }, { HumanMessage }, { createAgent }] = await Promise.all([
|
|
1784
|
-
import("@langchain/core/tools"),
|
|
1785
|
-
import("@langchain/core/messages"),
|
|
1786
|
-
import("langchain")
|
|
1787
|
-
]);
|
|
1788
|
-
const searchTool = new DynamicTool({
|
|
1789
|
-
name: "document_search",
|
|
1790
|
-
description: "Use this tool to search through the knowledge base and document repository. Input should be a specific search query.",
|
|
1791
|
-
func: async (query) => {
|
|
1792
|
-
const response = await this.pipeline.ask(query);
|
|
1793
|
-
return `Search Results:
|
|
1794
|
-
${response.reply}
|
|
1795
|
-
|
|
1796
|
-
Sources Used: ${JSON.stringify(
|
|
1797
|
-
response.sources.map((s) => s.id)
|
|
1798
|
-
)}`;
|
|
1799
|
-
}
|
|
1800
|
-
});
|
|
1801
|
-
const defaultAgentPrompt = "You are a helpful AI assistant with access to a document search tool.";
|
|
1802
|
-
let finalSystemPrompt = this.config.llm.systemPrompt || defaultAgentPrompt;
|
|
1803
|
-
const chartInstruction = `
|
|
1804
|
-
|
|
1805
|
-
When presenting structured data, statistics, or comparisons, decide if it is best presented as a table or a chart.
|
|
1806
|
-
- For tables: use standard Markdown tables.
|
|
1807
|
-
- For charts (bar, line, pie): You MUST output a fenced code block with the language 'chart'.
|
|
1808
|
-
- CRITICAL RULES:
|
|
1809
|
-
1. Use ONLY valid JSON (double quotes, no trailing commas).
|
|
1810
|
-
2. NO math expressions (e.g., use 100, NOT 50+50).
|
|
1811
|
-
3. dataKeys MUST exactly match the property names in the data objects.
|
|
1812
|
-
4. data objects MUST be FLAT (no nested objects or arrays).
|
|
1813
|
-
5. DO NOT output naked JSON or markdown tables for visualizations.
|
|
1814
|
-
6. ALWAYS wrap your visualization data in a \`\`\`chart ... \`\`\` code block.
|
|
1815
|
-
\`\`\`chart
|
|
1816
|
-
{
|
|
1817
|
-
"type": "bar" | "line" | "pie",
|
|
1818
|
-
"xAxisKey": "name",
|
|
1819
|
-
"name": "Distribution Chart",
|
|
1820
|
-
"dataKeys": ["value"],
|
|
1821
|
-
"data": [{"name": "A", "value": 10}]
|
|
1822
|
-
}
|
|
1823
|
-
\`\`\``;
|
|
1824
|
-
if (!finalSystemPrompt.includes("chart")) {
|
|
1825
|
-
finalSystemPrompt += chartInstruction;
|
|
1826
|
-
}
|
|
1827
|
-
this.agent = createAgent({
|
|
1828
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1829
|
-
model: chatModel,
|
|
1830
|
-
tools: [searchTool],
|
|
1831
|
-
systemPrompt: finalSystemPrompt
|
|
1832
|
-
});
|
|
1833
|
-
void HumanMessage;
|
|
1834
|
-
} catch (error) {
|
|
1835
|
-
const isMissing = error instanceof Error && error.message.includes("Cannot find module");
|
|
1836
|
-
const hint = isMissing ? " Make sure 'langchain' and '@langchain/core' are installed:\n npm install langchain @langchain/core" : "";
|
|
1837
|
-
throw new Error(
|
|
1838
|
-
`[LangChainAgent] Failed to initialize.${hint}
|
|
1839
|
-
${error instanceof Error ? error.message : String(error)}`
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
/**
|
|
1844
|
-
* Run the agentic flow.
|
|
1845
|
-
*
|
|
1846
|
-
* LangChain v1.x: invoke takes `{ messages: [...] }` instead of `{ input, chat_history }`.
|
|
1847
|
-
* The agent returns `{ messages: [...] }` — the last message is the final answer.
|
|
1848
|
-
*/
|
|
1849
|
-
async run(input, chatHistory = []) {
|
|
1850
|
-
var _a, _b, _c;
|
|
1851
|
-
if (!this.agent) {
|
|
1852
|
-
throw new Error("[LangChainAgent] Agent not initialized. Call initialize() first.");
|
|
1853
|
-
}
|
|
1854
|
-
const { HumanMessage, AIMessage } = await import("@langchain/core/messages");
|
|
1855
|
-
const historyMessages = chatHistory.map(
|
|
1856
|
-
(m) => m.role === "user" ? new HumanMessage(m.content) : new AIMessage(m.content)
|
|
1857
|
-
);
|
|
1858
|
-
const response = await this.agent.invoke({
|
|
1859
|
-
messages: [...historyMessages, new HumanMessage(input)]
|
|
1860
|
-
});
|
|
1861
|
-
const lastMessage = (_a = response == null ? void 0 : response.messages) == null ? void 0 : _a.at(-1);
|
|
1862
|
-
if (lastMessage && typeof lastMessage.content === "string") {
|
|
1863
|
-
return lastMessage.content;
|
|
1864
|
-
}
|
|
1865
|
-
if (Array.isArray(lastMessage == null ? void 0 : lastMessage.content)) {
|
|
1866
|
-
return lastMessage.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
1867
|
-
}
|
|
1868
|
-
return String((_c = (_b = response == null ? void 0 : response.output) != null ? _b : response) != null ? _c : "");
|
|
1869
|
-
}
|
|
1870
|
-
};
|
|
1871
|
-
|
|
1872
|
-
// src/core/BatchProcessor.ts
|
|
1873
|
-
function isTransientError(error) {
|
|
1874
|
-
if (!(error instanceof Error)) return false;
|
|
1875
|
-
const message = error.message.toLowerCase();
|
|
1876
|
-
if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("timeout") || message.includes("network") || message.includes("socket")) {
|
|
1877
|
-
return true;
|
|
1878
|
-
}
|
|
1879
|
-
if (message.includes("429") || message.includes("503") || message.includes("502") || message.includes("rate limit") || message.includes("too many requests")) {
|
|
1880
|
-
return true;
|
|
1881
|
-
}
|
|
1882
|
-
return false;
|
|
1883
|
-
}
|
|
1884
|
-
function calculateBackoffDelay(attempt, initialDelayMs, maxDelayMs, multiplier) {
|
|
1885
|
-
const exponentialDelay = Math.min(
|
|
1886
|
-
initialDelayMs * Math.pow(multiplier, attempt),
|
|
1887
|
-
maxDelayMs
|
|
1888
|
-
);
|
|
1889
|
-
const jitter = exponentialDelay * 0.1 * (Math.random() * 2 - 1);
|
|
1890
|
-
return Math.max(0, exponentialDelay + jitter);
|
|
1891
|
-
}
|
|
1892
|
-
function sleep(ms) {
|
|
1893
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1894
|
-
}
|
|
1895
|
-
var BatchProcessor = class {
|
|
1896
|
-
/**
|
|
1897
|
-
* Processes an array of items in configurable batches with retry logic.
|
|
1898
|
-
*
|
|
1899
|
-
* @param items - Items to process
|
|
1900
|
-
* @param processor - Async function that processes a batch of items
|
|
1901
|
-
* @param options - Configuration for batch size, retries, etc.
|
|
1902
|
-
* @returns Object with results, errors, and statistics
|
|
1903
|
-
*
|
|
1904
|
-
* @example
|
|
1905
|
-
* const docs = [...];
|
|
1906
|
-
* const result = await BatchProcessor.processBatch(
|
|
1907
|
-
* docs,
|
|
1908
|
-
* (batch) => vectorDB.batchUpsert(batch),
|
|
1909
|
-
* { batchSize: 100, maxRetries: 3 }
|
|
1910
|
-
* );
|
|
1911
|
-
*/
|
|
1912
|
-
static async processBatch(items, processor, options) {
|
|
1913
|
-
const {
|
|
1914
|
-
batchSize = 100,
|
|
1915
|
-
maxRetries = 3,
|
|
1916
|
-
initialDelayMs = 100,
|
|
1917
|
-
maxDelayMs = 1e4,
|
|
1918
|
-
backoffMultiplier = 2,
|
|
1919
|
-
throwOnPartialFailure = false
|
|
1920
|
-
} = options != null ? options : {};
|
|
1921
|
-
const results = [];
|
|
1922
|
-
const errors = [];
|
|
1923
|
-
const batches = [];
|
|
1924
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
1925
|
-
batches.push(items.slice(i, i + batchSize));
|
|
1926
|
-
}
|
|
1927
|
-
if (batches.length === 0) {
|
|
1928
|
-
return { results: [], errors: [], totalProcessed: 0, totalFailed: 0 };
|
|
1929
|
-
}
|
|
1930
|
-
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
|
1931
|
-
const batch = batches[batchIndex];
|
|
1932
|
-
let success = false;
|
|
1933
|
-
let lastError;
|
|
1934
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1935
|
-
try {
|
|
1936
|
-
const result = await processor(batch);
|
|
1937
|
-
results.push(result);
|
|
1938
|
-
success = true;
|
|
1939
|
-
break;
|
|
1940
|
-
} catch (err) {
|
|
1941
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1942
|
-
if (!isTransientError(err) || attempt === maxRetries) {
|
|
1943
|
-
break;
|
|
1944
|
-
}
|
|
1945
|
-
const delay = calculateBackoffDelay(
|
|
1946
|
-
attempt,
|
|
1947
|
-
initialDelayMs,
|
|
1948
|
-
maxDelayMs,
|
|
1949
|
-
backoffMultiplier
|
|
1950
|
-
);
|
|
1951
|
-
await sleep(delay);
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
if (!success && lastError) {
|
|
1955
|
-
errors.push({
|
|
1956
|
-
index: batchIndex,
|
|
1957
|
-
error: lastError,
|
|
1958
|
-
itemCount: batch.length
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
const totalProcessed = items.length - (errors.length > 0 ? errors.reduce((sum, e) => sum + (e.itemCount || 0), 0) : 0);
|
|
1963
|
-
const totalFailed = errors.reduce((sum, e) => sum + (e.itemCount || 0), 0);
|
|
1964
|
-
if (throwOnPartialFailure && errors.length > 0) {
|
|
1965
|
-
const errorMessages = errors.map((e) => `Batch ${e.index} (${e.itemCount} items): ${e.error.message}`).join("\n");
|
|
1966
|
-
throw new Error(
|
|
1967
|
-
`[BatchProcessor] Batch processing failed:
|
|
1968
|
-
${errorMessages}`
|
|
1969
|
-
);
|
|
1970
|
-
}
|
|
1971
|
-
return { results, errors, totalProcessed, totalFailed };
|
|
1972
|
-
}
|
|
1973
|
-
/**
|
|
1974
|
-
* Processes items sequentially (one at a time) with retry logic.
|
|
1975
|
-
* Useful for operations that don't support batching or when granular
|
|
1976
|
-
* error tracking is needed.
|
|
1977
|
-
*/
|
|
1978
|
-
static async processSequential(items, processor, options) {
|
|
1979
|
-
const {
|
|
1980
|
-
maxRetries = 3,
|
|
1981
|
-
initialDelayMs = 100,
|
|
1982
|
-
maxDelayMs = 1e4,
|
|
1983
|
-
backoffMultiplier = 2,
|
|
1984
|
-
throwOnPartialFailure = false
|
|
1985
|
-
} = options != null ? options : {};
|
|
1986
|
-
const results = [];
|
|
1987
|
-
const errors = [];
|
|
1988
|
-
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
1989
|
-
const item = items[itemIndex];
|
|
1990
|
-
let success = false;
|
|
1991
|
-
let lastError;
|
|
1992
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1993
|
-
try {
|
|
1994
|
-
const result = await processor(item);
|
|
1995
|
-
results.push(result);
|
|
1996
|
-
success = true;
|
|
1997
|
-
break;
|
|
1998
|
-
} catch (err) {
|
|
1999
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2000
|
-
if (!isTransientError(err) || attempt === maxRetries) {
|
|
2001
|
-
break;
|
|
2002
|
-
}
|
|
2003
|
-
const delay = calculateBackoffDelay(
|
|
2004
|
-
attempt,
|
|
2005
|
-
initialDelayMs,
|
|
2006
|
-
maxDelayMs,
|
|
2007
|
-
backoffMultiplier
|
|
2008
|
-
);
|
|
2009
|
-
await sleep(delay);
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
if (!success && lastError) {
|
|
2013
|
-
errors.push({ index: itemIndex, error: lastError, itemCount: 1 });
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
const totalProcessed = items.length - errors.length;
|
|
2017
|
-
const totalFailed = errors.length;
|
|
2018
|
-
if (throwOnPartialFailure && errors.length > 0) {
|
|
2019
|
-
const errorMessages = errors.map((e) => `Item ${e.index}: ${e.error.message}`).join("\n");
|
|
2020
|
-
throw new Error(
|
|
2021
|
-
`[BatchProcessor] Sequential processing failed:
|
|
2022
|
-
${errorMessages}`
|
|
2023
|
-
);
|
|
2024
|
-
}
|
|
2025
|
-
return { results, errors, totalProcessed, totalFailed };
|
|
2026
|
-
}
|
|
2027
|
-
/**
|
|
2028
|
-
* Maps over items with retry logic, returning results in order.
|
|
2029
|
-
* Like Array.map() but with async processing and automatic retries.
|
|
2030
|
-
*/
|
|
2031
|
-
static async mapWithRetry(items, mapper, options) {
|
|
2032
|
-
const result = await this.processSequential(items, mapper, options);
|
|
2033
|
-
if (result.errors.length > 0) {
|
|
2034
|
-
console.warn(
|
|
2035
|
-
`[BatchProcessor] mapWithRetry: ${result.errors.length} items failed`
|
|
2036
|
-
);
|
|
2037
|
-
}
|
|
2038
|
-
const orderedResults = new Array(items.length);
|
|
2039
|
-
let resultIndex = 0;
|
|
2040
|
-
let errorIndex = 0;
|
|
2041
|
-
for (let i = 0; i < items.length; i++) {
|
|
2042
|
-
if (errorIndex < result.errors.length && result.errors[errorIndex].index === i) {
|
|
2043
|
-
orderedResults[i] = void 0;
|
|
2044
|
-
errorIndex++;
|
|
2045
|
-
} else {
|
|
2046
|
-
orderedResults[i] = result.results[resultIndex];
|
|
2047
|
-
resultIndex++;
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
return orderedResults.filter((r) => r !== void 0);
|
|
2051
|
-
}
|
|
2052
|
-
/**
|
|
2053
|
-
* Parallel processor with concurrency limit.
|
|
2054
|
-
* Processes up to `concurrency` items at the same time.
|
|
2055
|
-
*/
|
|
2056
|
-
static async processConcurrent(items, processor, concurrency = 5, options) {
|
|
2057
|
-
const { throwOnPartialFailure = false } = options != null ? options : {};
|
|
2058
|
-
const results = [];
|
|
2059
|
-
const errors = [];
|
|
2060
|
-
for (let i = 0; i < items.length; i += concurrency) {
|
|
2061
|
-
const chunk = items.slice(i, i + concurrency).map((item, idx) => ({
|
|
2062
|
-
item,
|
|
2063
|
-
originalIndex: i + idx
|
|
2064
|
-
}));
|
|
2065
|
-
const chunkPromises = chunk.map(async ({ item, originalIndex }) => {
|
|
2066
|
-
try {
|
|
2067
|
-
const result = await processor(item);
|
|
2068
|
-
return { success: true, result, index: originalIndex };
|
|
2069
|
-
} catch (err) {
|
|
2070
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
2071
|
-
return { success: false, error, index: originalIndex };
|
|
2072
|
-
}
|
|
2073
|
-
});
|
|
2074
|
-
const chunkResults = await Promise.all(chunkPromises);
|
|
2075
|
-
for (const { success, result, error, index } of chunkResults) {
|
|
2076
|
-
if (success) {
|
|
2077
|
-
results.push(result);
|
|
2078
|
-
} else {
|
|
2079
|
-
errors.push({ index, error, itemCount: 1 });
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
const totalProcessed = items.length - errors.length;
|
|
2084
|
-
const totalFailed = errors.length;
|
|
2085
|
-
if (throwOnPartialFailure && errors.length > 0) {
|
|
2086
|
-
const errorMessages = errors.map((e) => `Item ${e.index}: ${e.error.message}`).join("\n");
|
|
2087
|
-
throw new Error(
|
|
2088
|
-
`[BatchProcessor] Concurrent processing failed:
|
|
2089
|
-
${errorMessages}`
|
|
2090
|
-
);
|
|
2091
|
-
}
|
|
2092
|
-
return { results, errors, totalProcessed, totalFailed };
|
|
2093
|
-
}
|
|
2094
|
-
};
|
|
2095
|
-
|
|
2096
|
-
// src/config/EmbeddingStrategy.ts
|
|
2097
|
-
var EmbeddingStrategy = /* @__PURE__ */ ((EmbeddingStrategy2) => {
|
|
2098
|
-
EmbeddingStrategy2["INTEGRATED"] = "integrated";
|
|
2099
|
-
EmbeddingStrategy2["SEPARATE"] = "separate";
|
|
2100
|
-
EmbeddingStrategy2["EXTERNAL"] = "external";
|
|
2101
|
-
return EmbeddingStrategy2;
|
|
2102
|
-
})(EmbeddingStrategy || {});
|
|
2103
|
-
var EmbeddingStrategyResolver = class {
|
|
2104
|
-
/**
|
|
2105
|
-
* Determine strategy based on LLM and embedding configs
|
|
2106
|
-
*/
|
|
2107
|
-
static determineStrategy(llmConfig, embeddingConfig) {
|
|
2108
|
-
if (!embeddingConfig) {
|
|
2109
|
-
return this.supportsEmbedding(llmConfig.provider) ? "integrated" /* INTEGRATED */ : "separate" /* SEPARATE */;
|
|
2110
|
-
}
|
|
2111
|
-
if (embeddingConfig.provider !== llmConfig.provider) {
|
|
2112
|
-
return "separate" /* SEPARATE */;
|
|
2113
|
-
}
|
|
2114
|
-
if (embeddingConfig.model !== llmConfig.model) {
|
|
2115
|
-
return "external" /* EXTERNAL */;
|
|
2116
|
-
}
|
|
2117
|
-
return "integrated" /* INTEGRATED */;
|
|
2118
|
-
}
|
|
2119
|
-
/**
|
|
2120
|
-
* Resolve and initialize providers according to the strategy
|
|
2121
|
-
*/
|
|
2122
|
-
static async resolve(llmConfig, embeddingConfig) {
|
|
2123
|
-
const strategy = this.determineStrategy(llmConfig, embeddingConfig);
|
|
2124
|
-
const llmProvider = LLMFactory.create(llmConfig, embeddingConfig);
|
|
2125
|
-
let embeddingProvider;
|
|
2126
|
-
switch (strategy) {
|
|
2127
|
-
case "integrated" /* INTEGRATED */:
|
|
2128
|
-
embeddingProvider = llmProvider;
|
|
2129
|
-
break;
|
|
2130
|
-
case "separate" /* SEPARATE */:
|
|
2131
|
-
if (!embeddingConfig) {
|
|
2132
|
-
throw new Error(
|
|
2133
|
-
"[EmbeddingStrategyResolver] SEPARATE strategy requires embeddingConfig"
|
|
2134
|
-
);
|
|
2135
|
-
}
|
|
2136
|
-
embeddingProvider = LLMFactory.createEmbeddingProvider(embeddingConfig);
|
|
2137
|
-
break;
|
|
2138
|
-
case "external" /* EXTERNAL */:
|
|
2139
|
-
if (!embeddingConfig) {
|
|
2140
|
-
throw new Error(
|
|
2141
|
-
"[EmbeddingStrategyResolver] EXTERNAL strategy requires embeddingConfig"
|
|
2142
|
-
);
|
|
2143
|
-
}
|
|
2144
|
-
embeddingProvider = LLMFactory.createEmbeddingProvider(embeddingConfig);
|
|
2145
|
-
break;
|
|
2146
|
-
default:
|
|
2147
|
-
throw new Error(`[EmbeddingStrategyResolver] Unknown strategy: ${strategy}`);
|
|
2148
|
-
}
|
|
2149
|
-
return {
|
|
2150
|
-
strategy,
|
|
2151
|
-
embeddingProvider,
|
|
2152
|
-
llmProvider
|
|
2153
|
-
};
|
|
2154
|
-
}
|
|
2155
|
-
/**
|
|
2156
|
-
* Check if an LLM provider natively supports embeddings
|
|
2157
|
-
*/
|
|
2158
|
-
static supportsEmbedding(provider) {
|
|
2159
|
-
return PROVIDERS_WITH_EMBEDDINGS.includes(provider);
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* Get a human-readable description of the strategy
|
|
2163
|
-
*/
|
|
2164
|
-
static getDescription(strategy) {
|
|
2165
|
-
switch (strategy) {
|
|
2166
|
-
case "integrated" /* INTEGRATED */:
|
|
2167
|
-
return "Using LLM provider for both chat and embeddings";
|
|
2168
|
-
case "separate" /* SEPARATE */:
|
|
2169
|
-
return "Using separate embedding provider";
|
|
2170
|
-
case "external" /* EXTERNAL */:
|
|
2171
|
-
return "Using external embedding service";
|
|
2172
|
-
default:
|
|
2173
|
-
return "Unknown strategy";
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
};
|
|
2177
|
-
|
|
2178
|
-
// src/core/QueryProcessor.ts
|
|
2179
|
-
var QueryProcessor = class {
|
|
2180
|
-
/**
|
|
2181
|
-
* Normalizes a string value by collapsing whitespace and trimming.
|
|
2182
|
-
*/
|
|
2183
|
-
static normalizeHintValue(value) {
|
|
2184
|
-
return value.replace(/\s+/g, " ").trim();
|
|
2185
|
-
}
|
|
2186
|
-
/**
|
|
2187
|
-
* Checks if a string is likely a question word or common prompt phrase.
|
|
2188
|
-
*/
|
|
2189
|
-
static isLikelyPromptPhrase(value) {
|
|
2190
|
-
return /^(what|which|who|where|when|why|how|the|is|are|was|were|in|on|at|by|for|with|about|a|an|to|of)\b/i.test(value.trim());
|
|
2191
|
-
}
|
|
2192
|
-
/**
|
|
2193
|
-
* Scans a natural language question for potential metadata hints and keywords.
|
|
2194
|
-
* @param question The user's query
|
|
2195
|
-
* @param validFields Optional list of known filterable fields to look for
|
|
2196
|
-
*/
|
|
2197
|
-
static extractQueryFieldHints(question, validFields = []) {
|
|
2198
|
-
var _a, _b, _c, _d;
|
|
2199
|
-
if (!question.trim()) return [];
|
|
2200
|
-
const hints = /* @__PURE__ */ new Map();
|
|
2201
|
-
const addHint = (value, field) => {
|
|
2202
|
-
const normalizedValue = this.normalizeHintValue(value);
|
|
2203
|
-
if (!normalizedValue) return;
|
|
2204
|
-
const normalizedField = field ? field.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() : void 0;
|
|
2205
|
-
const key = `${normalizedField != null ? normalizedField : "*"}::${normalizedValue.toLowerCase()}`;
|
|
2206
|
-
if (!hints.has(key)) {
|
|
2207
|
-
hints.set(key, __spreadValues({
|
|
2208
|
-
value: normalizedValue
|
|
2209
|
-
}, normalizedField ? { field: normalizedField } : {}));
|
|
2210
|
-
}
|
|
2211
|
-
};
|
|
2212
|
-
for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
|
|
2213
|
-
addHint(match[1]);
|
|
2214
|
-
}
|
|
2215
|
-
const naturalQuestionPatterns = [
|
|
2216
|
-
/\b(?:what|which)\s+(?:is|are|was|were)\s+(?:the\s+)?([^?.!,]{1,60}?)\s+of\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2217
|
-
/\b(?:who|what)\s+(?:is|are|was|were)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2218
|
-
/\b(?:about|for|regarding)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi
|
|
2219
|
-
];
|
|
2220
|
-
const personCompanyPatterns = [
|
|
2221
|
-
/\bcompany(?:\s+name)?\s+(?:of|for)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2222
|
-
/\b(?:which|what)\s+company\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi,
|
|
2223
|
-
/\bwhere\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi
|
|
2224
|
-
];
|
|
2225
|
-
for (const pattern of personCompanyPatterns) {
|
|
2226
|
-
for (const match of question.matchAll(pattern)) {
|
|
2227
|
-
const name = match[1];
|
|
2228
|
-
if (name) addHint(name, "name");
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
const genericPattern = /\b([a-z][a-z0-9_]{1,30})\s*[:=]\s*["']?([^"'\n?.!,]{1,60})["']?/gi;
|
|
2232
|
-
for (const match of question.matchAll(genericPattern)) {
|
|
2233
|
-
const field = match[1];
|
|
2234
|
-
const value = match[2];
|
|
2235
|
-
if (field && !this.isLikelyPromptPhrase(field)) {
|
|
2236
|
-
addHint(value, field);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
if (validFields.length > 0) {
|
|
2240
|
-
for (const field of validFields) {
|
|
2241
|
-
const pattern = new RegExp(`\\b${field}\\s*(?:is|are|:|of)?\\s+["']?([^"\\n?.!,]{1,60})["']?(?=[?.!,]|$)`, "gi");
|
|
2242
|
-
for (const match of question.matchAll(pattern)) {
|
|
2243
|
-
const value = match[1];
|
|
2244
|
-
if (value && !this.isLikelyPromptPhrase(value)) {
|
|
2245
|
-
addHint(value, field);
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
const universalPatterns = [
|
|
2251
|
-
{ regex: /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi, field: "email", group: 1 },
|
|
2252
|
-
{ regex: /(\+?\d[\d\-\.\s\(\)]{6,}\d)/g, field: "phone", group: 1 },
|
|
2253
|
-
{ regex: /(\b\d{4}-\d{2}-\d{2}\b|\b\d{1,2}\/\d{1,2}\/\d{2,4}\b)/g, field: "date", group: 1 },
|
|
2254
|
-
{ regex: /\b(ID|id|identifier)[: ]\s*([A-Za-z0-9\-]{3,})\b/gi, field: "id", group: 2 },
|
|
2255
|
-
{ regex: /"([^"]{2,120})"/g, group: 1 },
|
|
2256
|
-
{ regex: /'([^']{2,120})'/g, group: 1 }
|
|
2257
|
-
];
|
|
2258
|
-
for (const p of universalPatterns) {
|
|
2259
|
-
for (const match of question.matchAll(p.regex)) {
|
|
2260
|
-
const val = p.group ? (_a = match[p.group]) != null ? _a : match[0] : match[0];
|
|
2261
|
-
if (!val) continue;
|
|
2262
|
-
if (p.field) addHint(val, p.field);
|
|
2263
|
-
else addHint(val);
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
for (const pattern of naturalQuestionPatterns) {
|
|
2267
|
-
for (const match of question.matchAll(pattern)) {
|
|
2268
|
-
const value = (_b = match[2]) != null ? _b : match[1];
|
|
2269
|
-
if (value) addHint(value);
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
|
|
2273
|
-
const valuePattern = `([^\\n?.!,]{1,120}?)`;
|
|
2274
|
-
const fieldValuePatterns = [
|
|
2275
|
-
new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
2276
|
-
new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
2277
|
-
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
|
|
2278
|
-
];
|
|
2279
|
-
for (const pattern of fieldValuePatterns) {
|
|
2280
|
-
for (const match of question.matchAll(pattern)) {
|
|
2281
|
-
const field = this.normalizeHintValue((_c = match[1]) != null ? _c : "");
|
|
2282
|
-
const value = (_d = match[2]) != null ? _d : "";
|
|
2283
|
-
if (field && !this.isLikelyPromptPhrase(field)) {
|
|
2284
|
-
addHint(value, field);
|
|
2285
|
-
} else {
|
|
2286
|
-
addHint(value);
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
|
|
2291
|
-
addHint(match[0]);
|
|
2292
|
-
}
|
|
2293
|
-
return [...hints.values()];
|
|
2294
|
-
}
|
|
2295
|
-
/**
|
|
2296
|
-
* Constructs a QueryFilter object from extracted hints.
|
|
2297
|
-
*
|
|
2298
|
-
* Note: proper nouns are extracted inside `extractQueryFieldHints` and surfaced
|
|
2299
|
-
* as field-less hints; `buildQueryFilter` adds them to `keywords` without
|
|
2300
|
-
* re-running the proper noun regex to avoid duplication.
|
|
2301
|
-
*/
|
|
2302
|
-
static buildQueryFilter(question, hints) {
|
|
2303
|
-
const filter = { metadata: {}, keywords: [], queryText: question };
|
|
2304
|
-
for (const hint of hints) {
|
|
2305
|
-
if (hint.field) {
|
|
2306
|
-
filter.metadata[hint.field] = hint.value;
|
|
2307
|
-
} else {
|
|
2308
|
-
if (!filter.keywords.includes(hint.value)) {
|
|
2309
|
-
filter.keywords.push(hint.value);
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
|
|
2314
|
-
if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
|
|
2315
|
-
return filter;
|
|
2316
|
-
}
|
|
2317
|
-
};
|
|
2318
|
-
|
|
2319
|
-
// src/utils/UITransformer.ts
|
|
2320
|
-
var UITransformer = class {
|
|
2321
|
-
/**
|
|
2322
|
-
* Main transformation method
|
|
2323
|
-
* Analyzes user query and retrieved data to determine if a product carousel is needed.
|
|
2324
|
-
*/
|
|
2325
|
-
static transform(userQuery, retrievedData, config, trainedSchema) {
|
|
2326
|
-
if (!retrievedData || retrievedData.length === 0) {
|
|
2327
|
-
return this.createTextResponse("No data available", "No relevant data found for your query.");
|
|
2328
|
-
}
|
|
2329
|
-
const isStockRequest = this.isStockQuery(userQuery);
|
|
2330
|
-
const filteredData = isStockRequest ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
|
|
2331
|
-
const categories = this.detectCategories(filteredData);
|
|
2332
|
-
const hasProducts = filteredData.some((item) => this.isProductData(item));
|
|
2333
|
-
const isTimeSeries = filteredData.some((item) => this.isTimeSeriesData(item));
|
|
2334
|
-
const isTrendQuery = this.isTrendQuery(userQuery);
|
|
2335
|
-
if (isTrendQuery && isTimeSeries) {
|
|
2336
|
-
return this.transformToLineChart(filteredData);
|
|
2337
|
-
}
|
|
2338
|
-
if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
|
|
2339
|
-
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
2340
|
-
}
|
|
2341
|
-
if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
|
|
2342
|
-
return this.transformToPieChart(filteredData);
|
|
2343
|
-
}
|
|
2344
|
-
if (hasProducts) {
|
|
2345
|
-
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
2346
|
-
}
|
|
2347
|
-
if (this.hasMultipleFields(filteredData)) {
|
|
2348
|
-
return this.transformToTable(filteredData);
|
|
2349
|
-
}
|
|
2350
|
-
return this.transformToText(filteredData);
|
|
2351
|
-
}
|
|
2352
|
-
/**
|
|
2353
|
-
* Transform data to product carousel format
|
|
2354
|
-
*/
|
|
2355
|
-
static transformToProductCarousel(data, config, trainedSchema) {
|
|
2356
|
-
const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
|
|
2357
|
-
return {
|
|
2358
|
-
type: "product_carousel",
|
|
2359
|
-
title: "Recommended Products",
|
|
2360
|
-
description: `Found ${products.length} relevant products`,
|
|
2361
|
-
data: products
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
/**
|
|
2365
|
-
* Transform data to pie chart format
|
|
2366
|
-
*/
|
|
2367
|
-
static transformToPieChart(data) {
|
|
2368
|
-
const categories = this.detectCategories(data);
|
|
2369
|
-
const categoryData = this.aggregateByCategory(data, categories);
|
|
2370
|
-
const pieData = Object.entries(categoryData).map(([label, count]) => {
|
|
2371
|
-
const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
|
|
2372
|
-
return {
|
|
2373
|
-
label,
|
|
2374
|
-
value: count,
|
|
2375
|
-
inStockCount,
|
|
2376
|
-
outOfStockCount
|
|
2377
|
-
};
|
|
2378
|
-
});
|
|
2379
|
-
return {
|
|
2380
|
-
type: "pie_chart",
|
|
2381
|
-
title: "Distribution by Category",
|
|
2382
|
-
description: `Showing breakdown across ${categories.length} categories`,
|
|
2383
|
-
data: pieData
|
|
2384
|
-
};
|
|
2385
|
-
}
|
|
2386
|
-
/**
|
|
2387
|
-
* Transform data to line chart format
|
|
2388
|
-
*/
|
|
2389
|
-
static transformToLineChart(data) {
|
|
2390
|
-
const timePoints = this.extractTimeSeriesData(data);
|
|
2391
|
-
const lineData = timePoints.map((point) => ({
|
|
2392
|
-
timestamp: point.timestamp,
|
|
2393
|
-
value: point.value,
|
|
2394
|
-
label: point.label
|
|
2395
|
-
}));
|
|
2396
|
-
return {
|
|
2397
|
-
type: "line_chart",
|
|
2398
|
-
title: "Trend Over Time",
|
|
2399
|
-
description: `Showing ${lineData.length} data points`,
|
|
2400
|
-
data: lineData
|
|
2401
|
-
};
|
|
2402
|
-
}
|
|
2403
|
-
/**
|
|
2404
|
-
* Transform data to table format
|
|
2405
|
-
*/
|
|
2406
|
-
static transformToTable(data) {
|
|
2407
|
-
const columns = this.extractTableColumns(data);
|
|
2408
|
-
const rows = data.map((item) => this.extractTableRow(item, columns));
|
|
2409
|
-
const tableData = {
|
|
2410
|
-
columns,
|
|
2411
|
-
rows
|
|
2412
|
-
};
|
|
2413
|
-
return {
|
|
2414
|
-
type: "table",
|
|
2415
|
-
title: "Detailed Results",
|
|
2416
|
-
description: `Showing ${data.length} results`,
|
|
2417
|
-
data: tableData
|
|
2418
|
-
};
|
|
2419
|
-
}
|
|
2420
|
-
/**
|
|
2421
|
-
* Transform data to text format (fallback)
|
|
2422
|
-
*/
|
|
2423
|
-
static transformToText(data) {
|
|
2424
|
-
const textContent = data.map((item) => item.content).join("\n\n");
|
|
2425
|
-
return this.createTextResponse(
|
|
2426
|
-
"Search Results",
|
|
2427
|
-
textContent,
|
|
2428
|
-
`Found ${data.length} relevant results`
|
|
2429
|
-
);
|
|
2430
|
-
}
|
|
2431
|
-
/**
|
|
2432
|
-
* Helper: Create text response
|
|
2433
|
-
*/
|
|
2434
|
-
static createTextResponse(title, content, description) {
|
|
2435
|
-
return {
|
|
2436
|
-
type: "text",
|
|
2437
|
-
title,
|
|
2438
|
-
description,
|
|
2439
|
-
data: { content }
|
|
2440
|
-
};
|
|
2441
|
-
}
|
|
2442
|
-
static parseTransformationResponse(raw) {
|
|
2443
|
-
const payloadText = this.extractJsonCandidate(raw);
|
|
2444
|
-
if (!payloadText) return null;
|
|
2445
|
-
try {
|
|
2446
|
-
const parsed = JSON.parse(payloadText);
|
|
2447
|
-
return this.normalizeTransformation(parsed);
|
|
2448
|
-
} catch (e) {
|
|
2449
|
-
return null;
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
static extractJsonCandidate(raw) {
|
|
2453
|
-
if (!raw) return null;
|
|
2454
|
-
const cleaned = raw.replace(/```(?:json|chart|ui)?\s*/gi, "").replace(/```/g, "").trim();
|
|
2455
|
-
const start = cleaned.indexOf("{");
|
|
2456
|
-
if (start === -1) return null;
|
|
2457
|
-
let depth = 0;
|
|
2458
|
-
let inString = false;
|
|
2459
|
-
let escape = false;
|
|
2460
|
-
for (let i = start; i < cleaned.length; i += 1) {
|
|
2461
|
-
const char = cleaned[i];
|
|
2462
|
-
if (escape) {
|
|
2463
|
-
escape = false;
|
|
2464
|
-
continue;
|
|
2465
|
-
}
|
|
2466
|
-
if (char === "\\") {
|
|
2467
|
-
escape = true;
|
|
2468
|
-
continue;
|
|
2469
|
-
}
|
|
2470
|
-
if (char === '"') {
|
|
2471
|
-
inString = !inString;
|
|
2472
|
-
continue;
|
|
2473
|
-
}
|
|
2474
|
-
if (inString) continue;
|
|
2475
|
-
if (char === "{") depth += 1;
|
|
2476
|
-
if (char === "}") {
|
|
2477
|
-
depth -= 1;
|
|
2478
|
-
if (depth === 0) {
|
|
2479
|
-
return cleaned.slice(start, i + 1);
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
return null;
|
|
2484
|
-
}
|
|
2485
|
-
static normalizeTransformation(payload) {
|
|
2486
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
2487
|
-
if (!payload || typeof payload !== "object") return null;
|
|
2488
|
-
const payloadObj = payload;
|
|
2489
|
-
const type = this.normalizeVisualizationType(String((_c = (_b = (_a = payloadObj.type) != null ? _a : payloadObj.view) != null ? _b : payloadObj.chartType) != null ? _c : ""));
|
|
2490
|
-
if (!type) return null;
|
|
2491
|
-
const title = String((_e = (_d = payloadObj.title) != null ? _d : payloadObj.heading) != null ? _e : "Visualization");
|
|
2492
|
-
const description = payloadObj.description ? String(payloadObj.description) : void 0;
|
|
2493
|
-
const rawData = (_j = (_i = (_h = (_g = (_f = payloadObj.data) != null ? _f : payloadObj.table) != null ? _g : payloadObj.rows) != null ? _h : payloadObj.items) != null ? _i : payloadObj.content) != null ? _j : null;
|
|
2494
|
-
const data = type === "text" && typeof rawData === "string" ? { content: rawData } : rawData;
|
|
2495
|
-
const transformation = {
|
|
2496
|
-
type,
|
|
2497
|
-
title,
|
|
2498
|
-
description,
|
|
2499
|
-
data
|
|
2500
|
-
};
|
|
2501
|
-
return this.validateTransformation(transformation) ? transformation : null;
|
|
2502
|
-
}
|
|
2503
|
-
static normalizeVisualizationType(type) {
|
|
2504
|
-
var _a;
|
|
2505
|
-
const mapping = {
|
|
2506
|
-
pie: "pie_chart",
|
|
2507
|
-
pie_chart: "pie_chart",
|
|
2508
|
-
bar: "bar_chart",
|
|
2509
|
-
bar_chart: "bar_chart",
|
|
2510
|
-
line: "line_chart",
|
|
2511
|
-
line_chart: "line_chart",
|
|
2512
|
-
radar: "radar_chart",
|
|
2513
|
-
radar_chart: "radar_chart",
|
|
2514
|
-
table: "table",
|
|
2515
|
-
text: "text",
|
|
2516
|
-
product_carousel: "product_carousel",
|
|
2517
|
-
carousel: "carousel"
|
|
2518
|
-
};
|
|
2519
|
-
return (_a = mapping[type.toLowerCase()]) != null ? _a : null;
|
|
2520
|
-
}
|
|
2521
|
-
static validateTransformation(transformation) {
|
|
2522
|
-
const { type, data } = transformation;
|
|
2523
|
-
switch (type) {
|
|
2524
|
-
case "pie_chart":
|
|
2525
|
-
case "bar_chart":
|
|
2526
|
-
return Array.isArray(data) && data.every(
|
|
2527
|
-
(item) => item !== null && typeof item === "object" && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
2528
|
-
);
|
|
2529
|
-
case "line_chart":
|
|
2530
|
-
return Array.isArray(data) && data.every(
|
|
2531
|
-
(item) => item !== null && typeof item === "object" && "timestamp" in item && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
2532
|
-
);
|
|
2533
|
-
case "radar_chart":
|
|
2534
|
-
return Array.isArray(data) && data.every(
|
|
2535
|
-
(item) => item !== null && typeof item === "object" && "attribute" in item
|
|
2536
|
-
);
|
|
2537
|
-
case "table":
|
|
2538
|
-
return typeof data === "object" && data !== null && Array.isArray(data.columns) && Array.isArray(data.rows);
|
|
2539
|
-
case "text":
|
|
2540
|
-
return typeof data === "object" && data !== null && typeof data.content === "string";
|
|
2541
|
-
case "product_carousel":
|
|
2542
|
-
case "carousel":
|
|
2543
|
-
return Array.isArray(data) && data.every(
|
|
2544
|
-
(item) => item !== null && typeof item === "object" && ("id" in item || "name" in item)
|
|
2545
|
-
);
|
|
2546
|
-
default:
|
|
2547
|
-
return false;
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
/**
|
|
2551
|
-
* Helper: Check if data item is product-related
|
|
2552
|
-
*/
|
|
2553
|
-
static isProductData(item) {
|
|
2554
|
-
const content = (item.content || "").toLowerCase();
|
|
2555
|
-
const productKeywords = [
|
|
2556
|
-
"product",
|
|
2557
|
-
"price",
|
|
2558
|
-
"stock",
|
|
2559
|
-
"item",
|
|
2560
|
-
"sku",
|
|
2561
|
-
"brand",
|
|
2562
|
-
"model",
|
|
2563
|
-
"msrp",
|
|
2564
|
-
"inventory",
|
|
2565
|
-
"buy",
|
|
2566
|
-
"shop",
|
|
2567
|
-
"beauty",
|
|
2568
|
-
"care",
|
|
2569
|
-
"cosmetic",
|
|
2570
|
-
"facial",
|
|
2571
|
-
"cream",
|
|
2572
|
-
"serum",
|
|
2573
|
-
"mask",
|
|
2574
|
-
"makeup",
|
|
2575
|
-
"fragrance"
|
|
2576
|
-
];
|
|
2577
|
-
const hasKeywords = productKeywords.some((kw) => content.includes(kw));
|
|
2578
|
-
const hasMetadataKey = Object.keys(item.metadata || {}).some(
|
|
2579
|
-
(k) => ["name", "price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase())
|
|
2580
|
-
);
|
|
2581
|
-
const hasPricePattern = /\$\s*\d+/.test(content);
|
|
2582
|
-
return hasKeywords || hasMetadataKey || hasPricePattern;
|
|
2583
|
-
}
|
|
2584
|
-
/**
|
|
2585
|
-
* Helper: Check if data contains time series
|
|
2586
|
-
*/
|
|
2587
|
-
static isTimeSeriesData(item) {
|
|
2588
|
-
const content = (item.content || "").toLowerCase();
|
|
2589
|
-
const timeKeywords = ["trend", "historical", "growth", "decline", "change", "increase", "decrease"];
|
|
2590
|
-
const hasTimeKeyword = timeKeywords.some((kw) => content.includes(kw));
|
|
2591
|
-
const metadata = item.metadata || {};
|
|
2592
|
-
const maybeDateKeys = Object.keys(metadata).filter(
|
|
2593
|
-
(k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
|
|
2594
|
-
);
|
|
2595
|
-
const hasValidDateValue = maybeDateKeys.some((key) => {
|
|
2596
|
-
const value = metadata[key];
|
|
2597
|
-
if (typeof value === "string") {
|
|
2598
|
-
return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
|
|
2599
|
-
}
|
|
2600
|
-
return typeof value === "number";
|
|
2601
|
-
});
|
|
2602
|
-
return hasTimeKeyword || hasValidDateValue;
|
|
2603
|
-
}
|
|
2604
|
-
static shouldShowCategoryChart(query, categories) {
|
|
2605
|
-
if (categories.length < 2) {
|
|
2606
|
-
return false;
|
|
2607
|
-
}
|
|
2608
|
-
const normalized = query.toLowerCase();
|
|
2609
|
-
const chartKeywords = [
|
|
2610
|
-
"distribution",
|
|
2611
|
-
"breakdown",
|
|
2612
|
-
"by category",
|
|
2613
|
-
"by type",
|
|
2614
|
-
"compare",
|
|
2615
|
-
"share",
|
|
2616
|
-
"percentage",
|
|
2617
|
-
"segmentation",
|
|
2618
|
-
"split",
|
|
2619
|
-
"category breakdown",
|
|
2620
|
-
"category distribution"
|
|
2621
|
-
];
|
|
2622
|
-
return chartKeywords.some((keyword) => normalized.includes(keyword));
|
|
2623
|
-
}
|
|
2624
|
-
static isTrendQuery(query) {
|
|
2625
|
-
const normalized = query.toLowerCase();
|
|
2626
|
-
const trendKeywords = [
|
|
2627
|
-
"trend",
|
|
2628
|
-
"over time",
|
|
2629
|
-
"historical",
|
|
2630
|
-
"growth",
|
|
2631
|
-
"decline",
|
|
2632
|
-
"increase",
|
|
2633
|
-
"decrease",
|
|
2634
|
-
"year",
|
|
2635
|
-
"month",
|
|
2636
|
-
"week",
|
|
2637
|
-
"day",
|
|
2638
|
-
"comparison",
|
|
2639
|
-
"compare",
|
|
2640
|
-
"changes",
|
|
2641
|
-
"timeline"
|
|
2642
|
-
];
|
|
2643
|
-
return trendKeywords.some((keyword) => normalized.includes(keyword));
|
|
2644
|
-
}
|
|
2645
|
-
static isStockQuery(query) {
|
|
2646
|
-
const normalized = query.toLowerCase();
|
|
2647
|
-
return normalized.includes("in stock") || normalized.includes("available") || normalized.includes("availability") || normalized.includes("inventory") || normalized.includes("stock status");
|
|
2648
|
-
}
|
|
2649
|
-
/**
|
|
2650
|
-
* Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
|
|
2651
|
-
*/
|
|
2652
|
-
static getDynamicVal(meta, uiKey, config, trainedSchema) {
|
|
2653
|
-
var _a;
|
|
2654
|
-
if (!meta) return void 0;
|
|
2655
|
-
const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
|
|
2656
|
-
if (mapping && mapping[uiKey]) {
|
|
2657
|
-
const mappedKey = mapping[uiKey];
|
|
2658
|
-
if (meta[mappedKey] !== void 0) return meta[mappedKey];
|
|
2659
|
-
}
|
|
2660
|
-
if (trainedSchema && typeof trainedSchema === "object" && trainedSchema !== null) {
|
|
2661
|
-
const trainedKey = trainedSchema[uiKey];
|
|
2662
|
-
if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
|
|
2663
|
-
}
|
|
2664
|
-
return resolveMetadataValue(meta, uiKey);
|
|
2665
|
-
}
|
|
2666
|
-
static extractProductInfo(item, config, trainedSchema) {
|
|
2667
|
-
const meta = item.metadata || {};
|
|
2668
|
-
const name = this.getDynamicVal(meta, "name", config, trainedSchema);
|
|
2669
|
-
const price = this.getDynamicVal(meta, "price", config, trainedSchema);
|
|
2670
|
-
const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
|
|
2671
|
-
if (name || this.isProductData(item)) {
|
|
2672
|
-
let finalName = name ? String(name) : void 0;
|
|
2673
|
-
if (!finalName) {
|
|
2674
|
-
const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
2675
|
-
finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
|
|
2676
|
-
}
|
|
2677
|
-
let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
|
|
2678
|
-
if (!finalPrice) {
|
|
2679
|
-
const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
|
|
2680
|
-
if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
|
|
2681
|
-
}
|
|
2682
|
-
const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
|
|
2683
|
-
return {
|
|
2684
|
-
id: item.id,
|
|
2685
|
-
name: finalName,
|
|
2686
|
-
price: finalPrice,
|
|
2687
|
-
image: typeof imageValue === "string" ? imageValue : void 0,
|
|
2688
|
-
brand: brand ? String(brand) : void 0,
|
|
2689
|
-
description: item.content,
|
|
2690
|
-
inStock: this.determineStockStatus(item)
|
|
2691
|
-
};
|
|
2692
|
-
}
|
|
2693
|
-
return null;
|
|
2694
|
-
}
|
|
2695
|
-
/**
|
|
2696
|
-
* Helper: Detect categories in data
|
|
2697
|
-
*/
|
|
2698
|
-
static detectCategories(data) {
|
|
2699
|
-
const categories = /* @__PURE__ */ new Set();
|
|
2700
|
-
data.forEach((item) => {
|
|
2701
|
-
const meta = item.metadata || {};
|
|
2702
|
-
if (meta.category) {
|
|
2703
|
-
categories.add(String(meta.category));
|
|
2704
|
-
}
|
|
2705
|
-
if (meta.type) {
|
|
2706
|
-
categories.add(String(meta.type));
|
|
2707
|
-
}
|
|
2708
|
-
if (meta.tag) {
|
|
2709
|
-
const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
|
|
2710
|
-
tags.forEach((t) => categories.add(String(t)));
|
|
2711
|
-
}
|
|
2712
|
-
const contentCategories = Array.from(new Set(
|
|
2713
|
-
Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
|
|
2714
|
-
));
|
|
2715
|
-
contentCategories.forEach((category) => categories.add(category));
|
|
2716
|
-
const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
|
|
2717
|
-
if (categoryMatch) {
|
|
2718
|
-
categories.add(categoryMatch[1].trim());
|
|
2719
|
-
}
|
|
2720
|
-
});
|
|
2721
|
-
return Array.from(categories);
|
|
2722
|
-
}
|
|
2723
|
-
/**
|
|
2724
|
-
* Helper: Aggregate data by category
|
|
2725
|
-
*/
|
|
2726
|
-
static aggregateByCategory(data, categories) {
|
|
2727
|
-
const result = {};
|
|
2728
|
-
categories.forEach((cat) => {
|
|
2729
|
-
result[cat] = 0;
|
|
2730
|
-
});
|
|
2731
|
-
data.forEach((item) => {
|
|
2732
|
-
const meta = item.metadata || {};
|
|
2733
|
-
const itemCategory = meta.category || meta.type || "Other";
|
|
2734
|
-
if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
|
|
2735
|
-
result[itemCategory]++;
|
|
2736
|
-
} else {
|
|
2737
|
-
result["Other"] = (result["Other"] || 0) + 1;
|
|
2738
|
-
}
|
|
2739
|
-
});
|
|
2740
|
-
return result;
|
|
2741
|
-
}
|
|
2742
|
-
/**
|
|
2743
|
-
* Helper: Extract time series data
|
|
2744
|
-
*/
|
|
2745
|
-
static extractTimeSeriesData(data) {
|
|
2746
|
-
return data.map((item) => {
|
|
2747
|
-
const meta = item.metadata || {};
|
|
2748
|
-
return {
|
|
2749
|
-
timestamp: meta.timestamp || meta.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2750
|
-
value: meta.value || item.score || 0,
|
|
2751
|
-
label: meta.label || item.content.substring(0, 50)
|
|
2752
|
-
};
|
|
2753
|
-
});
|
|
2754
|
-
}
|
|
2755
|
-
/**
|
|
2756
|
-
* Helper: Extract table columns
|
|
2757
|
-
*/
|
|
2758
|
-
static extractTableColumns(data) {
|
|
2759
|
-
const columnSet = /* @__PURE__ */ new Set();
|
|
2760
|
-
columnSet.add("Content");
|
|
2761
|
-
data.forEach((item) => {
|
|
2762
|
-
Object.keys(item.metadata || {}).forEach((key) => {
|
|
2763
|
-
columnSet.add(key.charAt(0).toUpperCase() + key.slice(1));
|
|
2764
|
-
});
|
|
2765
|
-
});
|
|
2766
|
-
return Array.from(columnSet);
|
|
2767
|
-
}
|
|
2768
|
-
/**
|
|
2769
|
-
* Helper: Extract table row
|
|
2770
|
-
*/
|
|
2771
|
-
static extractTableRow(item, columns) {
|
|
2772
|
-
const meta = item.metadata || {};
|
|
2773
|
-
return columns.map((col) => {
|
|
2774
|
-
if (col === "Content") {
|
|
2775
|
-
return item.content.substring(0, 100);
|
|
2776
|
-
}
|
|
2777
|
-
const metaKey = col.charAt(0).toLowerCase() + col.slice(1);
|
|
2778
|
-
const value = meta[metaKey];
|
|
2779
|
-
return value !== void 0 ? String(value) : "";
|
|
2780
|
-
});
|
|
2781
|
-
}
|
|
2782
|
-
/**
|
|
2783
|
-
* Helper: Calculate stock counts by category
|
|
2784
|
-
*/
|
|
2785
|
-
static calculateStockCounts(category, data) {
|
|
2786
|
-
let inStock = 0;
|
|
2787
|
-
let outOfStock = 0;
|
|
2788
|
-
data.forEach((d) => {
|
|
2789
|
-
const meta = d.metadata || {};
|
|
2790
|
-
const itemCategory = meta.category || meta.type || "Other";
|
|
2791
|
-
if (itemCategory === category) {
|
|
2792
|
-
if (this.determineStockStatus(d)) {
|
|
2793
|
-
inStock++;
|
|
2794
|
-
} else {
|
|
2795
|
-
outOfStock++;
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
});
|
|
2799
|
-
return { inStockCount: inStock, outOfStockCount: outOfStock };
|
|
2800
|
-
}
|
|
2801
|
-
/**
|
|
2802
|
-
* Helper: Determine if item is in stock
|
|
2803
|
-
*/
|
|
2804
|
-
static determineStockStatus(item) {
|
|
2805
|
-
const meta = item.metadata || {};
|
|
2806
|
-
if (meta.inStock !== void 0) {
|
|
2807
|
-
return Boolean(meta.inStock);
|
|
2808
|
-
}
|
|
2809
|
-
if (meta.stock !== void 0) {
|
|
2810
|
-
return Boolean(meta.stock);
|
|
2811
|
-
}
|
|
2812
|
-
if (meta.available !== void 0) {
|
|
2813
|
-
return Boolean(meta.available);
|
|
2814
|
-
}
|
|
2815
|
-
const content = (item.content || "").toLowerCase();
|
|
2816
|
-
if (content.includes("out of stock") || content.includes("unavailable")) {
|
|
2817
|
-
return false;
|
|
2818
|
-
}
|
|
2819
|
-
if (content.includes("in stock") || content.includes("available")) {
|
|
2820
|
-
return true;
|
|
2821
|
-
}
|
|
2822
|
-
return true;
|
|
2823
|
-
}
|
|
2824
|
-
/**
|
|
2825
|
-
* Helper: Check if data has multiple fields
|
|
2826
|
-
*/
|
|
2827
|
-
static hasMultipleFields(data) {
|
|
2828
|
-
const fieldCount = /* @__PURE__ */ new Set();
|
|
2829
|
-
data.forEach((item) => {
|
|
2830
|
-
Object.keys(item.metadata || {}).forEach((key) => {
|
|
2831
|
-
fieldCount.add(key);
|
|
2832
|
-
});
|
|
2833
|
-
});
|
|
2834
|
-
return fieldCount.size > 2;
|
|
2835
|
-
}
|
|
2836
|
-
// ─── LLM-Driven Visualization Decision ────────────────────────────────────
|
|
2837
|
-
/**
|
|
2838
|
-
* analyzeAndDecide — sends user question + RAG data to the LLM with a
|
|
2839
|
-
* structured system prompt and parses the JSON response into a
|
|
2840
|
-
* UITransformationResponse.
|
|
2841
|
-
*
|
|
2842
|
-
* This is the recommended entry point for production use. The heuristic
|
|
2843
|
-
* `transform()` method is used as a fallback if the LLM call fails.
|
|
2844
|
-
*
|
|
2845
|
-
* System prompt instructs the LLM to:
|
|
2846
|
-
* - Analyze the question and retrieved data
|
|
2847
|
-
* - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
|
|
2848
|
-
* - Return a strict JSON object — no prose, no markdown fences
|
|
2849
|
-
*
|
|
2850
|
-
* @param query - the original user question
|
|
2851
|
-
* @param sources - vector DB matches returned by RAG retrieval
|
|
2852
|
-
* @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
|
|
2853
|
-
* @returns - a validated UITransformationResponse (type + title + description + data)
|
|
2854
|
-
*/
|
|
2855
|
-
static async analyzeAndDecide(query, sources, llm) {
|
|
2856
|
-
try {
|
|
2857
|
-
const context = this.buildContextSummary(sources);
|
|
2858
|
-
const systemPrompt = this.buildVisualizationSystemPrompt();
|
|
2859
|
-
const userPrompt = [
|
|
2860
|
-
`USER QUESTION: ${query}`,
|
|
2861
|
-
"",
|
|
2862
|
-
"RETRIEVED DATA (JSON):",
|
|
2863
|
-
context
|
|
2864
|
-
].join("\n");
|
|
2865
|
-
const rawResponse = await llm.chat(
|
|
2866
|
-
[{ role: "user", content: userPrompt }],
|
|
2867
|
-
"",
|
|
2868
|
-
{ systemPrompt, temperature: 0 }
|
|
2869
|
-
);
|
|
2870
|
-
const parsed = this.parseTransformationResponse(rawResponse);
|
|
2871
|
-
if (parsed) {
|
|
2872
|
-
console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
|
|
2873
|
-
return parsed;
|
|
2874
|
-
}
|
|
2875
|
-
console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
|
|
2876
|
-
} catch (err) {
|
|
2877
|
-
console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
|
|
2878
|
-
}
|
|
2879
|
-
return this.transform(query, sources);
|
|
2880
|
-
}
|
|
2881
|
-
/**
|
|
2882
|
-
* Build the system prompt that instructs the LLM to return a visualization JSON.
|
|
2883
|
-
*/
|
|
2884
|
-
static buildVisualizationSystemPrompt() {
|
|
2885
|
-
return `You are a data visualization expert embedded in a RAG chat system.
|
|
2886
|
-
You will receive a user question and structured data retrieved from a vector database.
|
|
2887
|
-
Your ONLY job is to analyze this information and return a single JSON object that tells
|
|
2888
|
-
the frontend how to visualize it.
|
|
2889
|
-
|
|
2890
|
-
Return ONLY a valid JSON object \u2014 no markdown code fences, no explanation, no prose.
|
|
2891
|
-
|
|
2892
|
-
The JSON must have this exact shape:
|
|
2893
|
-
{
|
|
2894
|
-
"type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
|
|
2895
|
-
"title": "A concise, descriptive title for the visualization",
|
|
2896
|
-
"description": "One sentence describing what the visualization shows",
|
|
2897
|
-
"data": <structured data \u2014 see rules below>
|
|
2898
|
-
}
|
|
2899
|
-
|
|
2900
|
-
DATA SHAPE per type:
|
|
2901
|
-
- bar_chart: array of { "category": string, "value": number }
|
|
2902
|
-
- line_chart: array of { "timestamp": string, "value": number, "label": string }
|
|
2903
|
-
- pie_chart: array of { "label": string, "value": number }
|
|
2904
|
-
- radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
|
|
2905
|
-
Example for radar_chart:
|
|
2906
|
-
[
|
|
2907
|
-
{ "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
|
|
2908
|
-
{ "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
|
|
2909
|
-
{ "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
|
|
2910
|
-
]
|
|
2911
|
-
- table: { "columns": string[], "rows": (string|number)[][] }
|
|
2912
|
-
- text: { "content": "<prose answer>" }
|
|
2913
|
-
|
|
2914
|
-
DECISION RULES (follow strictly):
|
|
2915
|
-
1. bar_chart \u2192 comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
|
|
2916
|
-
2. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
|
|
2917
|
-
3. pie_chart \u2192 proportional breakdown or percentage distribution
|
|
2918
|
-
4. radar_chart \u2192 comparing 2 or more products across MULTIPLE attributes (e.g. comparing features, ratings, or characteristics of 2 specific products). If the user asks to "compare" products and the data contains multiple dimensions or ratings for them, you MUST use this.
|
|
2919
|
-
5. table \u2192 multi-field structured records where each item has \u2265 3 attributes
|
|
2920
|
-
6. text \u2192 conversational or free-form answers where no chart adds value
|
|
2921
|
-
|
|
2922
|
-
IMPORTANT:
|
|
2923
|
-
- Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
|
|
2924
|
-
- Ensure all "value" fields are numbers, not strings.
|
|
2925
|
-
- For bar/line/pie, keep at most 12 data points for readability.
|
|
2926
|
-
- Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
|
|
2927
|
-
}
|
|
2928
|
-
/**
|
|
2929
|
-
* Serialize retrieved vector matches into a compact JSON context string.
|
|
2930
|
-
* Limits the total character count to avoid exceeding LLM context windows.
|
|
2931
|
-
*/
|
|
2932
|
-
static buildContextSummary(sources, maxChars = 6e3) {
|
|
2933
|
-
const items = sources.map((s, i) => {
|
|
2934
|
-
var _a, _b, _c, _d;
|
|
2935
|
-
return {
|
|
2936
|
-
index: i + 1,
|
|
2937
|
-
content: (_b = (_a = s.content) == null ? void 0 : _a.substring(0, 400)) != null ? _b : "",
|
|
2938
|
-
metadata: (_c = s.metadata) != null ? _c : {},
|
|
2939
|
-
score: (_d = s.score) != null ? _d : 0
|
|
2940
|
-
};
|
|
2941
|
-
});
|
|
2942
|
-
const full = JSON.stringify(items, null, 2);
|
|
2943
|
-
if (full.length <= maxChars) return full;
|
|
2944
|
-
const partial = [];
|
|
2945
|
-
let chars = 2;
|
|
2946
|
-
for (const item of items) {
|
|
2947
|
-
const chunk = JSON.stringify(item);
|
|
2948
|
-
if (chars + chunk.length + 2 > maxChars) break;
|
|
2949
|
-
partial.push(item);
|
|
2950
|
-
chars += chunk.length + 2;
|
|
2951
|
-
}
|
|
2952
|
-
return JSON.stringify(partial, null, 2) + "\n// ... truncated";
|
|
2953
|
-
}
|
|
2954
|
-
};
|
|
2955
|
-
|
|
2956
|
-
// src/utils/SchemaMapper.ts
|
|
2957
|
-
var SchemaMapper = class {
|
|
2958
|
-
/**
|
|
2959
|
-
* Trains the plugin on a set of keys.
|
|
2960
|
-
* This is done once per schema and cached.
|
|
2961
|
-
*/
|
|
2962
|
-
static async train(llm, projectId, keys) {
|
|
2963
|
-
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
2964
|
-
if (this.cache.has(cacheKey)) {
|
|
2965
|
-
return this.cache.get(cacheKey);
|
|
2966
|
-
}
|
|
2967
|
-
console.log(`[SchemaMapper] \u{1F9E0} Training AI on new schema keys: ${keys.join(", ")}`);
|
|
2968
|
-
const propertyList = Object.entries(this.TARGET_PROPERTIES).map(([prop, desc]) => `- ${prop} (${desc})`).join("\n");
|
|
2969
|
-
const prompt = `
|
|
2970
|
-
Given these metadata keys from a database: [${keys.join(", ")}]
|
|
2971
|
-
|
|
2972
|
-
Identify which keys best correspond to these standard UI properties:
|
|
2973
|
-
${propertyList}
|
|
2974
|
-
|
|
2975
|
-
Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
|
|
2976
|
-
If no good match is found for a property, omit it.
|
|
2977
|
-
|
|
2978
|
-
Example:
|
|
2979
|
-
{
|
|
2980
|
-
"name": "Product_Title",
|
|
2981
|
-
"price": "MSRP_USD",
|
|
2982
|
-
"brand": "VendorName"
|
|
2983
|
-
}
|
|
2984
|
-
`;
|
|
2985
|
-
try {
|
|
2986
|
-
const response = await llm.chat(
|
|
2987
|
-
[
|
|
2988
|
-
{ role: "system", content: "You are a database schema expert. You ONLY respond with valid JSON." },
|
|
2989
|
-
{ role: "user", content: prompt }
|
|
2990
|
-
],
|
|
2991
|
-
""
|
|
2992
|
-
);
|
|
2993
|
-
const startIdx = response.indexOf("{");
|
|
2994
|
-
if (startIdx !== -1) {
|
|
2995
|
-
let braceCount = 0;
|
|
2996
|
-
let endIdx = -1;
|
|
2997
|
-
for (let i = startIdx; i < response.length; i++) {
|
|
2998
|
-
if (response[i] === "{") braceCount++;
|
|
2999
|
-
else if (response[i] === "}") {
|
|
3000
|
-
braceCount--;
|
|
3001
|
-
if (braceCount === 0) {
|
|
3002
|
-
endIdx = i;
|
|
3003
|
-
break;
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
if (endIdx !== -1) {
|
|
3008
|
-
const jsonContent = response.substring(startIdx, endIdx + 1);
|
|
3009
|
-
const cleanJson = this.sanitizeJson(jsonContent);
|
|
3010
|
-
const mapping = JSON.parse(cleanJson);
|
|
3011
|
-
this.cache.set(cacheKey, mapping);
|
|
3012
|
-
return mapping;
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
} catch (error) {
|
|
3016
|
-
console.warn("[SchemaMapper] AI training failed, falling back to heuristics:", error);
|
|
3017
|
-
}
|
|
3018
|
-
return {};
|
|
3019
|
-
}
|
|
3020
|
-
/**
|
|
3021
|
-
* Forgiving JSON parser that fixes common AI formatting mistakes.
|
|
3022
|
-
*/
|
|
3023
|
-
static sanitizeJson(s) {
|
|
3024
|
-
return s.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "").replace(/[\u201C\u201D\u2018\u2019]/g, '"').replace(/'/g, '"').replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":').replace(/,\s*([}\]])/g, "$1").trim();
|
|
3025
|
-
}
|
|
3026
|
-
static getCached(projectId, keys) {
|
|
3027
|
-
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
3028
|
-
return this.cache.get(cacheKey);
|
|
3029
|
-
}
|
|
3030
|
-
};
|
|
3031
|
-
SchemaMapper.cache = /* @__PURE__ */ new Map();
|
|
3032
|
-
/**
|
|
3033
|
-
* Descriptions of standard UI properties to help the AI map fields accurately.
|
|
3034
|
-
*/
|
|
3035
|
-
SchemaMapper.TARGET_PROPERTIES = {
|
|
3036
|
-
name: "The primary title, name, or label of the item",
|
|
3037
|
-
price: "The numeric cost, price, MSRP, or amount",
|
|
3038
|
-
brand: "The manufacturer, vendor, brand, or maker",
|
|
3039
|
-
image: "The URL to an image, thumbnail, photo, or picture",
|
|
3040
|
-
stock: "The availability, inventory count, quantity, or stock level",
|
|
3041
|
-
description: "The detailed text content, summary, or body info",
|
|
3042
|
-
category: "The group, department, type, or category name"
|
|
3043
|
-
};
|
|
3044
|
-
|
|
3045
|
-
// src/core/Pipeline.ts
|
|
3046
|
-
var LRUEmbeddingCache = class {
|
|
3047
|
-
constructor(maxSize = 500) {
|
|
3048
|
-
this.cache = /* @__PURE__ */ new Map();
|
|
3049
|
-
this.maxSize = maxSize;
|
|
3050
|
-
}
|
|
3051
|
-
get(key) {
|
|
3052
|
-
const value = this.cache.get(key);
|
|
3053
|
-
if (value !== void 0) {
|
|
3054
|
-
this.cache.delete(key);
|
|
3055
|
-
this.cache.set(key, value);
|
|
3056
|
-
}
|
|
3057
|
-
return value;
|
|
3058
|
-
}
|
|
3059
|
-
set(key, value) {
|
|
3060
|
-
if (this.cache.has(key)) {
|
|
3061
|
-
this.cache.delete(key);
|
|
3062
|
-
} else if (this.cache.size >= this.maxSize) {
|
|
3063
|
-
const oldestKey = this.cache.keys().next().value;
|
|
3064
|
-
if (oldestKey !== void 0) this.cache.delete(oldestKey);
|
|
3065
|
-
}
|
|
3066
|
-
this.cache.set(key, value);
|
|
3067
|
-
}
|
|
3068
|
-
clear() {
|
|
3069
|
-
this.cache.clear();
|
|
3070
|
-
}
|
|
3071
|
-
get size() {
|
|
3072
|
-
return this.cache.size;
|
|
3073
|
-
}
|
|
3074
|
-
};
|
|
3075
|
-
function estimateTokens(text) {
|
|
3076
|
-
return Math.ceil(text.length / 4);
|
|
3077
|
-
}
|
|
3078
|
-
var MODEL_COST_PER_1K = {
|
|
3079
|
-
"gpt-4o": { input: 25e-4, output: 0.01 },
|
|
3080
|
-
"gpt-4o-mini": { input: 15e-5, output: 6e-4 },
|
|
3081
|
-
"gpt-4-turbo": { input: 0.01, output: 0.03 },
|
|
3082
|
-
"gpt-3.5-turbo": { input: 5e-4, output: 15e-4 },
|
|
3083
|
-
"claude-3-5-sonnet": { input: 3e-3, output: 0.015 },
|
|
3084
|
-
"claude-3-haiku": { input: 25e-5, output: 125e-5 },
|
|
3085
|
-
"gemini-1.5-flash": { input: 75e-6, output: 3e-4 },
|
|
3086
|
-
"gemini-1.5-pro": { input: 125e-5, output: 5e-3 }
|
|
3087
|
-
};
|
|
3088
|
-
function estimateCostUsd(promptTokens, completionTokens, model) {
|
|
3089
|
-
if (!model) return void 0;
|
|
3090
|
-
const key = Object.keys(MODEL_COST_PER_1K).find((k) => model.toLowerCase().includes(k));
|
|
3091
|
-
if (!key) return void 0;
|
|
3092
|
-
const prices = MODEL_COST_PER_1K[key];
|
|
3093
|
-
return promptTokens / 1e3 * prices.input + completionTokens / 1e3 * prices.output;
|
|
3094
|
-
}
|
|
3095
|
-
async function scoreHallucination(llm, answer, context) {
|
|
3096
|
-
const maxContextChars = 3e3;
|
|
3097
|
-
const truncatedContext = context.length > maxContextChars ? context.slice(0, maxContextChars) + "\n...[truncated]" : context;
|
|
3098
|
-
const prompt = `You are an AI quality checker. Given the CONTEXT retrieved from a knowledge base and the ANSWER generated from it, rate how well-grounded the answer is.
|
|
3099
|
-
|
|
3100
|
-
CONTEXT:
|
|
3101
|
-
${truncatedContext}
|
|
3102
|
-
|
|
3103
|
-
ANSWER:
|
|
3104
|
-
${answer}
|
|
3105
|
-
|
|
3106
|
-
Return ONLY a valid JSON object with no markdown fences:
|
|
3107
|
-
{"score": <float 0-1>, "reason": "<one sentence>"}
|
|
3108
|
-
|
|
3109
|
-
Where score 0 = fully grounded in context, score 1 = likely hallucinated or unsupported.`;
|
|
3110
|
-
try {
|
|
3111
|
-
const raw = await llm.chat(
|
|
3112
|
-
[{ role: "user", content: prompt }],
|
|
3113
|
-
"",
|
|
3114
|
-
{ temperature: 0, maxTokens: 120 }
|
|
3115
|
-
);
|
|
3116
|
-
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
3117
|
-
if (!jsonMatch) return void 0;
|
|
3118
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
3119
|
-
if (typeof parsed.score === "number" && typeof parsed.reason === "string") {
|
|
3120
|
-
return { score: Math.min(1, Math.max(0, parsed.score)), reason: parsed.reason };
|
|
3121
|
-
}
|
|
3122
|
-
} catch (e) {
|
|
3123
|
-
}
|
|
3124
|
-
return void 0;
|
|
3125
|
-
}
|
|
3126
|
-
var Pipeline = class {
|
|
3127
|
-
constructor(config) {
|
|
3128
|
-
this.config = config;
|
|
3129
|
-
/** LRU-bounded cache: avoids re-embedding identical queries within the same process. */
|
|
3130
|
-
this.embeddingCache = new LRUEmbeddingCache(500);
|
|
3131
|
-
this.initialised = false;
|
|
3132
|
-
var _a, _b, _c, _d, _e;
|
|
3133
|
-
this.chunker = new DocumentChunker(
|
|
3134
|
-
(_b = (_a = config.rag) == null ? void 0 : _a.chunkSize) != null ? _b : 1e3,
|
|
3135
|
-
(_d = (_c = config.rag) == null ? void 0 : _c.chunkOverlap) != null ? _d : 200
|
|
3136
|
-
);
|
|
3137
|
-
if (((_e = config.rag) == null ? void 0 : _e.chunkingStrategy) === "llamaindex") {
|
|
3138
|
-
this.llamaIngestor = new LlamaIndexIngestor();
|
|
3139
|
-
}
|
|
3140
|
-
this.reranker = new Reranker();
|
|
3141
|
-
}
|
|
3142
|
-
/**
|
|
3143
|
-
* Expose the underlying LLM provider (set after initialize()).
|
|
3144
|
-
* Used by the stream handler to pass to UITransformer.analyzeAndDecide().
|
|
3145
|
-
*/
|
|
3146
|
-
getLLMProvider() {
|
|
3147
|
-
return this.initialised ? this.llmProvider : void 0;
|
|
3148
|
-
}
|
|
3149
|
-
async initialize() {
|
|
3150
|
-
var _a;
|
|
3151
|
-
if (this.initialised) return;
|
|
3152
|
-
const chartInstruction = `You are a helpful product assistant. Use the provided context to answer questions accurately.
|
|
3153
|
-
|
|
3154
|
-
### UI STYLE RULES (CRITICAL):
|
|
3155
|
-
- NEVER generate markdown tables. If you do, the UI will break.
|
|
3156
|
-
- NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
|
|
3157
|
-
- NEVER generate text-based charts or graphs.
|
|
3158
|
-
- ONLY use plain text and bullet points.
|
|
3159
|
-
|
|
3160
|
-
### PRODUCT DISPLAY:
|
|
3161
|
-
- When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
|
|
3162
|
-
- The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
|
|
3163
|
-
- Do NOT try to format product lists as tables.
|
|
3164
|
-
`;
|
|
3165
|
-
this.config.llm.systemPrompt = chartInstruction;
|
|
3166
|
-
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
3167
|
-
const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
|
|
3168
|
-
this.config.llm,
|
|
3169
|
-
this.config.embedding
|
|
3170
|
-
);
|
|
3171
|
-
this.llmProvider = llmProvider;
|
|
3172
|
-
this.embeddingProvider = embeddingProvider;
|
|
3173
|
-
if (this.config.graphDb) {
|
|
3174
|
-
this.graphDB = await ProviderRegistry.createGraphProvider(this.config.graphDb);
|
|
3175
|
-
await this.graphDB.initialize();
|
|
3176
|
-
this.entityExtractor = new EntityExtractor(this.llmProvider);
|
|
3177
|
-
}
|
|
3178
|
-
await this.vectorDB.initialize();
|
|
3179
|
-
if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic") {
|
|
3180
|
-
this.agent = new LangChainAgent(this, this.config);
|
|
3181
|
-
await this.agent.initialize(this.llmProvider);
|
|
3182
|
-
}
|
|
3183
|
-
this.initialised = true;
|
|
3184
|
-
}
|
|
3185
|
-
/**
|
|
3186
|
-
* Ingest documents with automatic chunking, embedding, and batch upsert.
|
|
3187
|
-
*/
|
|
3188
|
-
async ingest(documents, namespace) {
|
|
3189
|
-
await this.initialize();
|
|
3190
|
-
const ns = namespace != null ? namespace : this.config.projectId;
|
|
3191
|
-
const results = [];
|
|
3192
|
-
for (const doc of documents) {
|
|
3193
|
-
try {
|
|
3194
|
-
const chunks = await this.prepareChunks(doc);
|
|
3195
|
-
const vectors = await this.processEmbeddings(chunks);
|
|
3196
|
-
const upsertDocs = chunks.map((chunk, i) => ({
|
|
3197
|
-
id: chunk.id,
|
|
3198
|
-
vector: vectors[i],
|
|
3199
|
-
content: chunk.content,
|
|
3200
|
-
metadata: chunk.metadata
|
|
3201
|
-
}));
|
|
3202
|
-
const totalProcessed = await this.processUpserts(upsertDocs, ns);
|
|
3203
|
-
results.push({ docId: doc.docId, chunksIngested: totalProcessed });
|
|
3204
|
-
if (this.graphDB && this.entityExtractor) {
|
|
3205
|
-
await this.processGraphIngestion(doc.docId, chunks);
|
|
3206
|
-
}
|
|
3207
|
-
} catch (error) {
|
|
3208
|
-
console.error(`[Pipeline] Failed to ingest document ${doc.docId}:`, error);
|
|
3209
|
-
results.push({ docId: doc.docId, chunksIngested: 0 });
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
return results;
|
|
3213
|
-
}
|
|
3214
|
-
/** Step 1: Chunk the document content. */
|
|
3215
|
-
async prepareChunks(doc) {
|
|
3216
|
-
var _a, _b;
|
|
3217
|
-
if (this.llamaIngestor) {
|
|
3218
|
-
return this.llamaIngestor.chunk(doc.content, {
|
|
3219
|
-
docId: doc.docId,
|
|
3220
|
-
metadata: doc.metadata,
|
|
3221
|
-
chunkSize: (_a = this.config.rag) == null ? void 0 : _a.chunkSize,
|
|
3222
|
-
chunkOverlap: (_b = this.config.rag) == null ? void 0 : _b.chunkOverlap
|
|
3223
|
-
});
|
|
3224
|
-
}
|
|
3225
|
-
return this.chunker.chunk(doc.content, { docId: doc.docId, metadata: doc.metadata });
|
|
3226
|
-
}
|
|
3227
|
-
/**
|
|
3228
|
-
* Step 2: Generate embeddings for chunks with retry logic.
|
|
3229
|
-
* Uses batchEmbed when available for efficiency; falls back to sequential embedding.
|
|
3230
|
-
*/
|
|
3231
|
-
async processEmbeddings(chunks) {
|
|
3232
|
-
const embedBatchOptions = {
|
|
3233
|
-
batchSize: 50,
|
|
3234
|
-
maxRetries: 3,
|
|
3235
|
-
initialDelayMs: 100
|
|
3236
|
-
};
|
|
3237
|
-
const vectors = await BatchProcessor.mapWithRetry(
|
|
3238
|
-
chunks.map((c) => c.content),
|
|
3239
|
-
(text) => this.embeddingProvider.embed(text, { taskType: "document" }),
|
|
3240
|
-
embedBatchOptions
|
|
3241
|
-
);
|
|
3242
|
-
if (vectors.length !== chunks.length) {
|
|
3243
|
-
throw new Error(
|
|
3244
|
-
`[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. Check embedding provider logs for individual chunk failures.`
|
|
3245
|
-
);
|
|
3246
|
-
}
|
|
3247
|
-
return vectors;
|
|
3248
|
-
}
|
|
3249
|
-
/** Step 3: Upsert chunks to vector database with retry logic. */
|
|
3250
|
-
async processUpserts(upsertDocs, namespace) {
|
|
3251
|
-
const upsertBatchOptions = {
|
|
3252
|
-
batchSize: 100,
|
|
3253
|
-
maxRetries: 3,
|
|
3254
|
-
initialDelayMs: 100
|
|
3255
|
-
};
|
|
3256
|
-
const upsertResult = await BatchProcessor.processBatch(
|
|
3257
|
-
upsertDocs,
|
|
3258
|
-
(batch) => this.vectorDB.batchUpsert(batch, namespace),
|
|
3259
|
-
upsertBatchOptions
|
|
3260
|
-
);
|
|
3261
|
-
if (upsertResult.errors.length > 0) {
|
|
3262
|
-
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed`);
|
|
3263
|
-
}
|
|
3264
|
-
return upsertResult.totalProcessed;
|
|
3265
|
-
}
|
|
3266
|
-
/** Step 4: Optional graph-based entity extraction and ingestion. */
|
|
3267
|
-
async processGraphIngestion(docId, chunks) {
|
|
3268
|
-
const extractionOptions = {
|
|
3269
|
-
batchSize: 2,
|
|
3270
|
-
maxRetries: 1,
|
|
3271
|
-
initialDelayMs: 500
|
|
3272
|
-
};
|
|
3273
|
-
await BatchProcessor.processBatch(
|
|
3274
|
-
chunks,
|
|
3275
|
-
async (batch) => {
|
|
3276
|
-
for (const chunk of batch) {
|
|
3277
|
-
try {
|
|
3278
|
-
const { nodes, edges } = await this.entityExtractor.extract(chunk.content);
|
|
3279
|
-
if (nodes.length > 0) await this.graphDB.addNodes(nodes);
|
|
3280
|
-
if (edges.length > 0) await this.graphDB.addEdges(edges);
|
|
3281
|
-
} catch (err) {
|
|
3282
|
-
console.warn(`[Pipeline] Entity extraction failed for chunk in doc ${docId}:`, err);
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3285
|
-
},
|
|
3286
|
-
extractionOptions
|
|
3287
|
-
);
|
|
3288
|
-
}
|
|
3289
|
-
async ask(question, history = [], namespace) {
|
|
3290
|
-
var _a;
|
|
3291
|
-
await this.initialize();
|
|
3292
|
-
if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic" && this.agent) {
|
|
3293
|
-
const agentReply = await this.agent.run(question, history);
|
|
3294
|
-
return { reply: agentReply, sources: [] };
|
|
3295
|
-
}
|
|
3296
|
-
const stream = this.askStream(question, history, namespace);
|
|
3297
|
-
let reply = "";
|
|
3298
|
-
let sources = [];
|
|
3299
|
-
let graphData;
|
|
3300
|
-
let uiTransformation;
|
|
3301
|
-
let trace;
|
|
3302
|
-
try {
|
|
3303
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
3304
|
-
const chunk = temp.value;
|
|
3305
|
-
if (typeof chunk === "string") {
|
|
3306
|
-
reply += chunk;
|
|
3307
|
-
} else if (typeof chunk === "object" && chunk !== null) {
|
|
3308
|
-
if ("sources" in chunk) sources = chunk.sources;
|
|
3309
|
-
if ("graphData" in chunk) graphData = chunk.graphData;
|
|
3310
|
-
if ("ui_transformation" in chunk) uiTransformation = chunk.ui_transformation;
|
|
3311
|
-
if ("trace" in chunk) trace = chunk.trace;
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
} catch (temp) {
|
|
3315
|
-
error = [temp];
|
|
3316
|
-
} finally {
|
|
3317
|
-
try {
|
|
3318
|
-
more && (temp = iter.return) && await temp.call(iter);
|
|
3319
|
-
} finally {
|
|
3320
|
-
if (error)
|
|
3321
|
-
throw error[0];
|
|
3322
|
-
}
|
|
3323
|
-
}
|
|
3324
|
-
return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
|
|
3325
|
-
}
|
|
3326
|
-
/**
|
|
3327
|
-
* High-performance streaming RAG flow.
|
|
3328
|
-
* Yields text chunks first, then the retrieval metadata + observability trace at the end.
|
|
3329
|
-
*/
|
|
3330
|
-
askStream(_0) {
|
|
3331
|
-
return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
|
|
3332
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
3333
|
-
yield new __await(this.initialize());
|
|
3334
|
-
const ns = namespace != null ? namespace : this.config.projectId;
|
|
3335
|
-
const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
|
|
3336
|
-
const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
|
|
3337
|
-
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3338
|
-
const requestStart = performance.now();
|
|
3339
|
-
try {
|
|
3340
|
-
let searchQuery = question;
|
|
3341
|
-
let rewrittenQuery;
|
|
3342
|
-
if ((_e = this.config.rag) == null ? void 0 : _e.useQueryTransformation) {
|
|
3343
|
-
searchQuery = yield new __await(this.rewriteQuery(question, history));
|
|
3344
|
-
rewrittenQuery = searchQuery !== question ? searchQuery : void 0;
|
|
3345
|
-
}
|
|
3346
|
-
const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
|
|
3347
|
-
const filter = QueryProcessor.buildQueryFilter(question, hints);
|
|
3348
|
-
const embedStart = performance.now();
|
|
3349
|
-
const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
|
|
3350
|
-
namespace: ns,
|
|
3351
|
-
topK: topK * 2,
|
|
3352
|
-
filter
|
|
3353
|
-
}));
|
|
3354
|
-
const retrieveEnd = performance.now();
|
|
3355
|
-
const embedMs = retrieveEnd - embedStart;
|
|
3356
|
-
const retrieveMs = retrieveEnd - embedStart;
|
|
3357
|
-
const rerankStart = performance.now();
|
|
3358
|
-
let sources = rawSources.filter((m) => m.score >= scoreThreshold);
|
|
3359
|
-
if ((_g = this.config.rag) == null ? void 0 : _g.useReranking) {
|
|
3360
|
-
sources = yield new __await(this.reranker.rerank(sources, question, topK));
|
|
3361
|
-
} else {
|
|
3362
|
-
sources = sources.slice(0, topK);
|
|
3363
|
-
}
|
|
3364
|
-
const rerankMs = performance.now() - rerankStart;
|
|
3365
|
-
let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
3366
|
-
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
3367
|
-
if (graphData && graphData.nodes.length > 0) {
|
|
3368
|
-
const graphContext = graphData.nodes.map(
|
|
3369
|
-
(n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
|
|
3370
|
-
).join("\n");
|
|
3371
|
-
context = `GRAPH KNOWLEDGE:
|
|
3372
|
-
${graphContext}
|
|
3373
|
-
|
|
3374
|
-
VECTOR CONTEXT:
|
|
3375
|
-
${context}`;
|
|
3376
|
-
}
|
|
3377
|
-
const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
|
|
3378
|
-
const trainingPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys) : Promise.resolve(void 0);
|
|
3379
|
-
const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
|
|
3380
|
-
const hardenedHistory = [...history];
|
|
3381
|
-
const userQuestion = { role: "user", content: question + restrictionSuffix };
|
|
3382
|
-
const messages = [...hardenedHistory, userQuestion];
|
|
3383
|
-
const systemPrompt = (_h = this.config.llm.systemPrompt) != null ? _h : "";
|
|
3384
|
-
const userPrompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
|
|
3385
|
-
let fullReply = "";
|
|
3386
|
-
const generateStart = performance.now();
|
|
3387
|
-
if (this.llmProvider.chatStream) {
|
|
3388
|
-
const stream = this.llmProvider.chatStream(messages, context);
|
|
3389
|
-
if (!stream) {
|
|
3390
|
-
throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
|
|
3391
|
-
}
|
|
3392
|
-
try {
|
|
3393
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
3394
|
-
const chunk = temp.value;
|
|
3395
|
-
fullReply += chunk;
|
|
3396
|
-
yield chunk;
|
|
3397
|
-
}
|
|
3398
|
-
} catch (temp) {
|
|
3399
|
-
error = [temp];
|
|
3400
|
-
} finally {
|
|
3401
|
-
try {
|
|
3402
|
-
more && (temp = iter.return) && (yield new __await(temp.call(iter)));
|
|
3403
|
-
} finally {
|
|
3404
|
-
if (error)
|
|
3405
|
-
throw error[0];
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
} else {
|
|
3409
|
-
const reply = yield new __await(this.llmProvider.chat(messages, context));
|
|
3410
|
-
fullReply = reply;
|
|
3411
|
-
yield reply;
|
|
3412
|
-
}
|
|
3413
|
-
const generateMs = performance.now() - generateStart;
|
|
3414
|
-
const totalMs = performance.now() - requestStart;
|
|
3415
|
-
const latency = {
|
|
3416
|
-
embedMs: Math.round(embedMs),
|
|
3417
|
-
retrieveMs: Math.round(retrieveMs),
|
|
3418
|
-
rerankMs: ((_i = this.config.rag) == null ? void 0 : _i.useReranking) ? Math.round(rerankMs) : void 0,
|
|
3419
|
-
generateMs: Math.round(generateMs),
|
|
3420
|
-
totalMs: Math.round(totalMs)
|
|
3421
|
-
};
|
|
3422
|
-
const promptText = systemPrompt + "\n" + context + "\n" + userPrompt;
|
|
3423
|
-
const promptTokens = estimateTokens(promptText);
|
|
3424
|
-
const completionTokens = estimateTokens(fullReply);
|
|
3425
|
-
const tokens = {
|
|
3426
|
-
promptTokens,
|
|
3427
|
-
completionTokens,
|
|
3428
|
-
totalTokens: promptTokens + completionTokens,
|
|
3429
|
-
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model)
|
|
3430
|
-
};
|
|
3431
|
-
const hallucinationResult = yield new __await(scoreHallucination(this.llmProvider, fullReply, context).catch(() => void 0));
|
|
3432
|
-
const trainedSchema = yield new __await(trainingPromise);
|
|
3433
|
-
const uiTransformation = yield new __await(this.generateUiTransformation(question, sources, context, trainedSchema));
|
|
3434
|
-
const trace = {
|
|
3435
|
-
requestId,
|
|
3436
|
-
query: question,
|
|
3437
|
-
rewrittenQuery,
|
|
3438
|
-
systemPrompt,
|
|
3439
|
-
userPrompt: question + restrictionSuffix,
|
|
3440
|
-
chunks: sources.map((s) => {
|
|
3441
|
-
var _a2;
|
|
3442
|
-
return {
|
|
3443
|
-
id: s.id,
|
|
3444
|
-
score: s.score,
|
|
3445
|
-
content: s.content,
|
|
3446
|
-
metadata: (_a2 = s.metadata) != null ? _a2 : {},
|
|
3447
|
-
namespace: ns
|
|
3448
|
-
};
|
|
3449
|
-
}),
|
|
3450
|
-
latency,
|
|
3451
|
-
tokens,
|
|
3452
|
-
hallucinationScore: hallucinationResult == null ? void 0 : hallucinationResult.score,
|
|
3453
|
-
hallucinationReason: hallucinationResult == null ? void 0 : hallucinationResult.reason,
|
|
3454
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3455
|
-
};
|
|
3456
|
-
yield {
|
|
3457
|
-
reply: "",
|
|
3458
|
-
sources,
|
|
3459
|
-
graphData,
|
|
3460
|
-
ui_transformation: uiTransformation,
|
|
3461
|
-
trace
|
|
3462
|
-
};
|
|
3463
|
-
} catch (error2) {
|
|
3464
|
-
throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
3465
|
-
}
|
|
3466
|
-
});
|
|
3467
|
-
}
|
|
3468
|
-
/**
|
|
3469
|
-
* Universal retrieval method combining all enabled providers.
|
|
3470
|
-
* Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
|
|
3471
|
-
*/
|
|
3472
|
-
async generateUiTransformation(question, sources, _context, trainedSchema) {
|
|
3473
|
-
if (!sources || sources.length === 0) {
|
|
3474
|
-
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
3475
|
-
}
|
|
3476
|
-
try {
|
|
3477
|
-
return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
|
|
3478
|
-
} catch (err) {
|
|
3479
|
-
console.warn("[Pipeline] generateUiTransformation failed, using heuristic fallback:", err);
|
|
3480
|
-
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
async retrieve(query, options) {
|
|
3484
|
-
var _a, _b, _c;
|
|
3485
|
-
const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
|
|
3486
|
-
const topK = (_b = options.topK) != null ? _b : 5;
|
|
3487
|
-
const cacheKey = `${ns}::${query}`;
|
|
3488
|
-
let queryVector = this.embeddingCache.get(cacheKey);
|
|
3489
|
-
const [retrievedVector, graphData] = await Promise.all([
|
|
3490
|
-
queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: "query" }),
|
|
3491
|
-
this.graphDB && ((_c = this.config.rag) == null ? void 0 : _c.useGraphRetrieval) ? this.graphDB.query(query) : Promise.resolve(void 0)
|
|
3492
|
-
]);
|
|
3493
|
-
if (!queryVector) {
|
|
3494
|
-
this.embeddingCache.set(cacheKey, retrievedVector);
|
|
3495
|
-
queryVector = retrievedVector;
|
|
3496
|
-
}
|
|
3497
|
-
const sources = await this.vectorDB.query(queryVector, topK, ns, options.filter);
|
|
3498
|
-
return { sources, graphData };
|
|
3499
|
-
}
|
|
3500
|
-
/** Rewrite the user query for better retrieval performance. */
|
|
3501
|
-
async rewriteQuery(question, history) {
|
|
3502
|
-
const prompt = `Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
3503
|
-
Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
|
|
3504
|
-
|
|
3505
|
-
History:
|
|
3506
|
-
${history.map((m) => `${m.role}: ${m.content}`).join("\n")}
|
|
3507
|
-
|
|
3508
|
-
New Question: ${question}
|
|
3509
|
-
|
|
3510
|
-
Optimized Search Query:`;
|
|
3511
|
-
const rewrite = await this.llmProvider.chat(
|
|
3512
|
-
[
|
|
3513
|
-
{ role: "system", content: "You are an assistant that optimizes search queries for RAG systems." },
|
|
3514
|
-
{ role: "user", content: prompt }
|
|
3515
|
-
],
|
|
3516
|
-
""
|
|
3517
|
-
);
|
|
3518
|
-
return rewrite.trim() || question;
|
|
3519
|
-
}
|
|
3520
|
-
/** Generate 3-5 short, relevant questions based on the vector database content. */
|
|
3521
|
-
async getSuggestions(query, namespace) {
|
|
3522
|
-
if (!query || query.trim().length < 3) return [];
|
|
3523
|
-
await this.initialize();
|
|
3524
|
-
const ns = namespace != null ? namespace : this.config.projectId;
|
|
3525
|
-
try {
|
|
3526
|
-
const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
|
|
3527
|
-
if (sources.length === 0) return [];
|
|
3528
|
-
const context = sources.map((s) => s.content).join("\n\n---\n\n");
|
|
3529
|
-
const prompt = `Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
|
|
3530
|
-
Focus on questions that can be answered by the context.
|
|
3531
|
-
Keep each question under 10 words and make them very specific to the content.
|
|
3532
|
-
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
|
|
3533
|
-
|
|
3534
|
-
Context:
|
|
3535
|
-
${context}
|
|
3536
|
-
|
|
3537
|
-
Suggestions:`;
|
|
3538
|
-
const response = await this.llmProvider.chat(
|
|
3539
|
-
[
|
|
3540
|
-
{ role: "system", content: "You are a helpful assistant that generates search suggestions." },
|
|
3541
|
-
{ role: "user", content: prompt }
|
|
3542
|
-
],
|
|
3543
|
-
""
|
|
3544
|
-
);
|
|
3545
|
-
const match = response.match(/\[[\s\S]*\]/);
|
|
3546
|
-
if (match) {
|
|
3547
|
-
const suggestions = JSON.parse(match[0]);
|
|
3548
|
-
if (Array.isArray(suggestions)) {
|
|
3549
|
-
return suggestions.map((s) => String(s)).slice(0, 3);
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
} catch (error) {
|
|
3553
|
-
console.error("[Pipeline] Failed to generate suggestions:", error);
|
|
3554
|
-
}
|
|
3555
|
-
return [];
|
|
3556
|
-
}
|
|
3557
|
-
};
|
|
3558
|
-
|
|
3559
|
-
// src/core/ProviderHealthCheck.ts
|
|
3560
|
-
var ProviderHealthCheck = class {
|
|
3561
|
-
/**
|
|
3562
|
-
* Validates vector database configuration before initialization.
|
|
3563
|
-
*/
|
|
3564
|
-
static async checkVectorProvider(config) {
|
|
3565
|
-
const timestamp = Date.now();
|
|
3566
|
-
const configErrors = await ConfigValidator.validate({
|
|
3567
|
-
projectId: "health-check",
|
|
3568
|
-
vectorDb: config,
|
|
3569
|
-
llm: { provider: "openai", model: "gpt-4o", apiKey: "none" },
|
|
3570
|
-
embedding: { provider: "openai", model: "text-embedding-3-small", apiKey: "none" }
|
|
3571
|
-
});
|
|
3572
|
-
const vectorDbErrors = configErrors.filter((e) => e.field.startsWith("vectorDb"));
|
|
3573
|
-
if (vectorDbErrors.length > 0) {
|
|
3574
|
-
return {
|
|
3575
|
-
healthy: false,
|
|
3576
|
-
provider: config.provider,
|
|
3577
|
-
error: `Configuration validation failed: ${vectorDbErrors.map((e) => e.message).join("; ")}`,
|
|
3578
|
-
timestamp
|
|
3579
|
-
};
|
|
3580
|
-
}
|
|
3581
|
-
try {
|
|
3582
|
-
const checker = await ProviderRegistry.getVectorHealthChecker(config.provider);
|
|
3583
|
-
if (checker) {
|
|
3584
|
-
return await checker.check(config);
|
|
3585
|
-
}
|
|
3586
|
-
return await this.fallbackVectorHealthCheck(config, timestamp);
|
|
3587
|
-
} catch (error) {
|
|
3588
|
-
return {
|
|
3589
|
-
healthy: false,
|
|
3590
|
-
provider: config.provider,
|
|
3591
|
-
error: error instanceof Error ? error.message : String(error),
|
|
3592
|
-
timestamp
|
|
3593
|
-
};
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
static async fallbackVectorHealthCheck(config, timestamp) {
|
|
3597
|
-
const opts = config.options || {};
|
|
3598
|
-
const baseUrl = opts.baseUrl || opts.uri;
|
|
3599
|
-
if (baseUrl && baseUrl.startsWith("http")) {
|
|
3600
|
-
try {
|
|
3601
|
-
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/health`);
|
|
3602
|
-
return {
|
|
3603
|
-
healthy: response.ok,
|
|
3604
|
-
provider: config.provider,
|
|
3605
|
-
error: response.ok ? void 0 : `Health check failed: ${response.status}`,
|
|
3606
|
-
timestamp
|
|
3607
|
-
};
|
|
3608
|
-
} catch (e) {
|
|
3609
|
-
return { healthy: false, provider: config.provider, error: "Provider unreachable", timestamp };
|
|
3610
|
-
}
|
|
3611
|
-
}
|
|
3612
|
-
return {
|
|
3613
|
-
healthy: true,
|
|
3614
|
-
provider: config.provider,
|
|
3615
|
-
error: "Pluggable health check not implemented; basic reachability assumed.",
|
|
3616
|
-
timestamp
|
|
3617
|
-
};
|
|
3618
|
-
}
|
|
3619
|
-
/**
|
|
3620
|
-
* Validates LLM provider configuration.
|
|
3621
|
-
*/
|
|
3622
|
-
static async checkLLMProvider(config) {
|
|
3623
|
-
const timestamp = Date.now();
|
|
3624
|
-
try {
|
|
3625
|
-
const checker = LLMFactory.getHealthChecker(config.provider);
|
|
3626
|
-
if (checker) {
|
|
3627
|
-
return await checker.check(config);
|
|
3628
|
-
}
|
|
3629
|
-
return {
|
|
3630
|
-
healthy: true,
|
|
3631
|
-
provider: config.provider,
|
|
3632
|
-
error: "Pluggable health check not implemented; basic reachability assumed.",
|
|
3633
|
-
timestamp
|
|
3634
|
-
};
|
|
3635
|
-
} catch (error) {
|
|
3636
|
-
return {
|
|
3637
|
-
healthy: false,
|
|
3638
|
-
provider: config.provider,
|
|
3639
|
-
error: error instanceof Error ? error.message : String(error),
|
|
3640
|
-
timestamp
|
|
3641
|
-
};
|
|
3642
|
-
}
|
|
3643
|
-
}
|
|
3644
|
-
/**
|
|
3645
|
-
* Runs comprehensive health checks on all configured providers in parallel.
|
|
3646
|
-
*/
|
|
3647
|
-
static async checkAll(vectorDbConfig, llmConfig, embeddingConfig) {
|
|
3648
|
-
const [vectorDb, llm, embedding] = await Promise.all([
|
|
3649
|
-
this.checkVectorProvider(vectorDbConfig),
|
|
3650
|
-
this.checkLLMProvider(llmConfig),
|
|
3651
|
-
embeddingConfig ? this.checkLLMProvider(embeddingConfig) : Promise.resolve(void 0)
|
|
3652
|
-
]);
|
|
3653
|
-
return {
|
|
3654
|
-
vectorDb,
|
|
3655
|
-
llm,
|
|
3656
|
-
embedding,
|
|
3657
|
-
allHealthy: vectorDb.healthy && llm.healthy && (!embedding || embedding.healthy)
|
|
3658
|
-
};
|
|
3659
|
-
}
|
|
3660
|
-
};
|
|
3661
|
-
|
|
3662
|
-
// src/core/VectorPlugin.ts
|
|
3663
|
-
var VectorPlugin = class {
|
|
3664
|
-
constructor(hostConfig) {
|
|
3665
|
-
this.config = ConfigResolver.resolve(hostConfig);
|
|
3666
|
-
this.validationPromise = ConfigValidator.validateAndThrow(this.config);
|
|
3667
|
-
this.pipeline = new Pipeline(this.config);
|
|
3668
|
-
}
|
|
3669
|
-
/**
|
|
3670
|
-
* Get the current resolved configuration.
|
|
3671
|
-
*/
|
|
3672
|
-
getConfig() {
|
|
3673
|
-
return this.config;
|
|
3674
|
-
}
|
|
3675
|
-
/**
|
|
3676
|
-
* Get the initialized LLM provider (available after the first request).
|
|
3677
|
-
* Used by handlers to pass to UITransformer.analyzeAndDecide() for
|
|
3678
|
-
* LLM-driven visualization decisions.
|
|
3679
|
-
*/
|
|
3680
|
-
getLLMProvider() {
|
|
3681
|
-
return this.pipeline.getLLMProvider();
|
|
3682
|
-
}
|
|
3683
|
-
/**
|
|
3684
|
-
* Perform pre-flight health checks on all configured providers.
|
|
3685
|
-
* Useful to verify connectivity before running operations.
|
|
3686
|
-
*
|
|
3687
|
-
* @returns Health status for vector DB, LLM, and embedding providers
|
|
3688
|
-
*/
|
|
3689
|
-
async checkHealth() {
|
|
3690
|
-
return ProviderHealthCheck.checkAll(
|
|
3691
|
-
this.config.vectorDb,
|
|
3692
|
-
this.config.llm,
|
|
3693
|
-
this.config.embedding
|
|
3694
|
-
);
|
|
3695
|
-
}
|
|
3696
|
-
/**
|
|
3697
|
-
* Run a chat query.
|
|
3698
|
-
*/
|
|
3699
|
-
async chat(message, history = [], namespace) {
|
|
3700
|
-
await this.validationPromise;
|
|
3701
|
-
return this.pipeline.ask(message, history, namespace);
|
|
3702
|
-
}
|
|
3703
|
-
/**
|
|
3704
|
-
* Run a streaming chat query.
|
|
3705
|
-
*/
|
|
3706
|
-
chatStream(_0) {
|
|
3707
|
-
return __asyncGenerator(this, arguments, function* (message, history = [], namespace) {
|
|
3708
|
-
yield new __await(this.validationPromise);
|
|
3709
|
-
yield* __yieldStar(this.pipeline.askStream(message, history, namespace));
|
|
3710
|
-
});
|
|
3711
|
-
}
|
|
3712
|
-
/**
|
|
3713
|
-
* Ingest documents into the vector database.
|
|
3714
|
-
*/
|
|
3715
|
-
async ingest(documents, namespace) {
|
|
3716
|
-
await this.validationPromise;
|
|
3717
|
-
return this.pipeline.ingest(documents, namespace);
|
|
3718
|
-
}
|
|
3719
|
-
/**
|
|
3720
|
-
* Get auto-suggestions based on a query prefix.
|
|
3721
|
-
*/
|
|
3722
|
-
async getSuggestions(query, namespace) {
|
|
3723
|
-
await this.validationPromise;
|
|
3724
|
-
return this.pipeline.getSuggestions(query, namespace);
|
|
3725
|
-
}
|
|
3726
|
-
};
|
|
3727
|
-
|
|
3728
|
-
// src/utils/DocumentParser.ts
|
|
3729
|
-
var DocumentParser = class {
|
|
3730
|
-
/**
|
|
3731
|
-
* Extract text from a File or Buffer based on its type.
|
|
3732
|
-
*/
|
|
3733
|
-
static async parse(file, fileName, mimeType) {
|
|
3734
|
-
var _a;
|
|
3735
|
-
const extension = (_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase();
|
|
3736
|
-
if (extension === "txt" || extension === "md" || mimeType === "text/plain" || mimeType === "text/markdown") {
|
|
3737
|
-
return this.readAsText(file);
|
|
3738
|
-
}
|
|
3739
|
-
if (extension === "json" || mimeType === "application/json") {
|
|
3740
|
-
const text = await this.readAsText(file);
|
|
3741
|
-
try {
|
|
3742
|
-
const obj = JSON.parse(text);
|
|
3743
|
-
return JSON.stringify(obj, null, 2);
|
|
3744
|
-
} catch (e) {
|
|
3745
|
-
return text;
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
3748
|
-
if (extension === "csv" || mimeType === "text/csv") {
|
|
3749
|
-
return this.readAsText(file);
|
|
3750
|
-
}
|
|
3751
|
-
if (extension === "pdf" || mimeType === "application/pdf") {
|
|
3752
|
-
try {
|
|
3753
|
-
const pdf = await import("pdf-parse");
|
|
3754
|
-
const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
|
|
3755
|
-
const data = await pdf.default(buffer);
|
|
3756
|
-
return data.text;
|
|
3757
|
-
} catch (err) {
|
|
3758
|
-
console.warn('[DocumentParser] PDF parsing failed. Make sure "pdf-parse" is installed.', err);
|
|
3759
|
-
return `[PDF Parsing Error for ${fileName}]`;
|
|
3760
|
-
}
|
|
3761
|
-
}
|
|
3762
|
-
if (extension === "docx" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
|
|
3763
|
-
try {
|
|
3764
|
-
const mammoth = await import("mammoth");
|
|
3765
|
-
const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
|
|
3766
|
-
const result = await mammoth.extractRawText({ buffer });
|
|
3767
|
-
return result.value;
|
|
3768
|
-
} catch (err) {
|
|
3769
|
-
console.warn('[DocumentParser] DOCX parsing failed. Make sure "mammoth" is installed.', err);
|
|
3770
|
-
return `[DOCX Parsing Error for ${fileName}]`;
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
try {
|
|
3774
|
-
return await this.readAsText(file);
|
|
3775
|
-
} catch (e) {
|
|
3776
|
-
throw new Error(`[DocumentParser] Unsupported file format: ${fileName} (${mimeType})`);
|
|
3777
|
-
}
|
|
3778
|
-
}
|
|
3779
|
-
static async readAsText(file) {
|
|
3780
|
-
if (Buffer.isBuffer(file)) {
|
|
3781
|
-
return file.toString("utf-8");
|
|
3782
|
-
}
|
|
3783
|
-
return await file.text();
|
|
3784
|
-
}
|
|
3785
|
-
};
|
|
3786
|
-
|
|
3787
|
-
// src/handlers/index.ts
|
|
3788
|
-
function sseFrame(payload) {
|
|
3789
|
-
return `data: ${JSON.stringify(payload)}
|
|
3790
|
-
|
|
3791
|
-
`;
|
|
3792
|
-
}
|
|
3793
|
-
function sseTextFrame(text) {
|
|
3794
|
-
return `data: ${JSON.stringify({ type: "text", text })}
|
|
3795
|
-
|
|
3796
|
-
`;
|
|
3797
|
-
}
|
|
3798
|
-
function sseMetaFrame(meta) {
|
|
3799
|
-
return `data: ${JSON.stringify(__spreadValues({ type: "metadata" }, meta != null ? meta : {}))}
|
|
3800
|
-
|
|
3801
|
-
`;
|
|
3802
|
-
}
|
|
3803
|
-
function sseUIFrame(uiTransformation) {
|
|
3804
|
-
return `data: ${JSON.stringify({ type: "ui_transformation", data: uiTransformation })}
|
|
3805
|
-
|
|
3806
|
-
`;
|
|
3807
|
-
}
|
|
3808
|
-
function sseObservabilityFrame(trace) {
|
|
3809
|
-
return `data: ${JSON.stringify({ type: "observability", data: trace })}
|
|
3810
|
-
|
|
3811
|
-
`;
|
|
3812
|
-
}
|
|
3813
|
-
function sseErrorFrame(message) {
|
|
3814
|
-
return `data: ${JSON.stringify({ type: "error", error: message })}
|
|
3815
|
-
|
|
3816
|
-
`;
|
|
3817
|
-
}
|
|
3818
|
-
var SSE_HEADERS = {
|
|
3819
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
3820
|
-
"Cache-Control": "no-cache, no-transform",
|
|
3821
|
-
Connection: "keep-alive",
|
|
3822
|
-
"X-Accel-Buffering": "no"
|
|
3823
|
-
// Disable Nginx buffering for streaming
|
|
3824
|
-
};
|
|
3825
|
-
function createChatHandler(configOrPlugin) {
|
|
3826
|
-
const plugin = configOrPlugin instanceof VectorPlugin ? configOrPlugin : new VectorPlugin(configOrPlugin);
|
|
3827
|
-
return async function POST(req) {
|
|
3828
|
-
try {
|
|
3829
|
-
const body = await req.json();
|
|
3830
|
-
const { message, history = [], namespace } = body;
|
|
3831
|
-
if (!(message == null ? void 0 : message.trim())) {
|
|
3832
|
-
return NextResponse.json({ error: "message is required" }, { status: 400 });
|
|
3833
|
-
}
|
|
3834
|
-
const result = await plugin.chat(message, history, namespace);
|
|
3835
|
-
return NextResponse.json(result);
|
|
3836
|
-
} catch (err) {
|
|
3837
|
-
const message = err instanceof Error ? err.message : "Internal server error";
|
|
3838
|
-
return NextResponse.json({ error: message }, { status: 500 });
|
|
3839
|
-
}
|
|
3840
|
-
};
|
|
3841
|
-
}
|
|
3842
|
-
function createStreamHandler(configOrPlugin) {
|
|
3843
|
-
const plugin = configOrPlugin instanceof VectorPlugin ? configOrPlugin : new VectorPlugin(configOrPlugin);
|
|
3844
|
-
return async function POST(req) {
|
|
3845
|
-
let body;
|
|
3846
|
-
try {
|
|
3847
|
-
body = await req.json();
|
|
3848
|
-
} catch (e) {
|
|
3849
|
-
return new Response(JSON.stringify({ error: "Invalid JSON body" }), {
|
|
3850
|
-
status: 400,
|
|
3851
|
-
headers: { "Content-Type": "application/json" }
|
|
3852
|
-
});
|
|
3853
|
-
}
|
|
3854
|
-
const { message, history = [], namespace } = body;
|
|
3855
|
-
if (!(message == null ? void 0 : message.trim())) {
|
|
3856
|
-
return new Response(JSON.stringify({ error: "message is required" }), {
|
|
3857
|
-
status: 400,
|
|
3858
|
-
headers: { "Content-Type": "application/json" }
|
|
3859
|
-
});
|
|
3860
|
-
}
|
|
3861
|
-
const encoder = new TextEncoder();
|
|
3862
|
-
const stream = new ReadableStream({
|
|
3863
|
-
async start(controller) {
|
|
3864
|
-
var _a, _b;
|
|
3865
|
-
const enqueue = (text) => controller.enqueue(encoder.encode(text));
|
|
3866
|
-
try {
|
|
3867
|
-
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
3868
|
-
try {
|
|
3869
|
-
for (var iter = __forAwait(pipelineStream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
3870
|
-
const chunk = temp.value;
|
|
3871
|
-
if (typeof chunk === "string") {
|
|
3872
|
-
enqueue(sseTextFrame(chunk));
|
|
3873
|
-
} else {
|
|
3874
|
-
enqueue(sseMetaFrame(chunk));
|
|
3875
|
-
const responseChunk = chunk;
|
|
3876
|
-
const sources = (responseChunk == null ? void 0 : responseChunk.sources) || [];
|
|
3877
|
-
if (responseChunk == null ? void 0 : responseChunk.trace) {
|
|
3878
|
-
enqueue(sseObservabilityFrame(responseChunk.trace));
|
|
3879
|
-
}
|
|
3880
|
-
if (sources.length > 0) {
|
|
3881
|
-
try {
|
|
3882
|
-
const llmProvider = (_a = plugin.getLLMProvider) == null ? void 0 : _a.call(plugin);
|
|
3883
|
-
const uiTransformation = (_b = responseChunk == null ? void 0 : responseChunk.ui_transformation) != null ? _b : llmProvider ? await UITransformer.analyzeAndDecide(message, sources, llmProvider) : UITransformer.transform(message, sources, plugin.getConfig());
|
|
3884
|
-
if (uiTransformation) {
|
|
3885
|
-
enqueue(sseUIFrame(uiTransformation));
|
|
3886
|
-
}
|
|
3887
|
-
} catch (transformError) {
|
|
3888
|
-
console.warn("[createStreamHandler] UI transformation warning:", transformError);
|
|
3889
|
-
try {
|
|
3890
|
-
const fallback = UITransformer.transform(message, sources, plugin.getConfig());
|
|
3891
|
-
if (fallback) enqueue(sseUIFrame(fallback));
|
|
3892
|
-
} catch (e) {
|
|
3893
|
-
}
|
|
3894
|
-
}
|
|
3895
|
-
}
|
|
3896
|
-
}
|
|
3897
|
-
}
|
|
3898
|
-
} catch (temp) {
|
|
3899
|
-
error = [temp];
|
|
3900
|
-
} finally {
|
|
3901
|
-
try {
|
|
3902
|
-
more && (temp = iter.return) && await temp.call(iter);
|
|
3903
|
-
} finally {
|
|
3904
|
-
if (error)
|
|
3905
|
-
throw error[0];
|
|
3906
|
-
}
|
|
3907
|
-
}
|
|
3908
|
-
} catch (streamError) {
|
|
3909
|
-
const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
|
|
3910
|
-
console.error("[createStreamHandler] Stream error:", streamError);
|
|
3911
|
-
enqueue(sseErrorFrame(errorMessage));
|
|
3912
|
-
} finally {
|
|
3913
|
-
controller.close();
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
});
|
|
3917
|
-
return new Response(stream, { headers: SSE_HEADERS });
|
|
3918
|
-
};
|
|
3919
|
-
}
|
|
3920
|
-
function createIngestHandler(configOrPlugin) {
|
|
3921
|
-
const plugin = configOrPlugin instanceof VectorPlugin ? configOrPlugin : new VectorPlugin(configOrPlugin);
|
|
3922
|
-
return async function POST(req) {
|
|
3923
|
-
try {
|
|
3924
|
-
const body = await req.json();
|
|
3925
|
-
const { documents, namespace } = body;
|
|
3926
|
-
if (!Array.isArray(documents) || documents.length === 0) {
|
|
3927
|
-
return NextResponse.json({ error: "documents array is required" }, { status: 400 });
|
|
3928
|
-
}
|
|
3929
|
-
const results = await plugin.ingest(documents, namespace);
|
|
3930
|
-
return NextResponse.json({ results });
|
|
3931
|
-
} catch (err) {
|
|
3932
|
-
const message = err instanceof Error ? err.message : "Internal server error";
|
|
3933
|
-
return NextResponse.json({ error: message }, { status: 500 });
|
|
3934
|
-
}
|
|
3935
|
-
};
|
|
3936
|
-
}
|
|
3937
|
-
function createHealthHandler(configOrPlugin) {
|
|
3938
|
-
const plugin = configOrPlugin instanceof VectorPlugin ? configOrPlugin : new VectorPlugin(configOrPlugin);
|
|
3939
|
-
return async function GET() {
|
|
3940
|
-
try {
|
|
3941
|
-
const health = await plugin.checkHealth();
|
|
3942
|
-
const status = health.allHealthy ? "ok" : "degraded";
|
|
3943
|
-
return NextResponse.json(__spreadProps(__spreadValues({
|
|
3944
|
-
status
|
|
3945
|
-
}, health), {
|
|
3946
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3947
|
-
}));
|
|
3948
|
-
} catch (err) {
|
|
3949
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
3950
|
-
return NextResponse.json({ status: "error", error: message }, { status: 500 });
|
|
3951
|
-
}
|
|
3952
|
-
};
|
|
3953
|
-
}
|
|
3954
|
-
function createUploadHandler(configOrPlugin) {
|
|
3955
|
-
const plugin = configOrPlugin instanceof VectorPlugin ? configOrPlugin : new VectorPlugin(configOrPlugin);
|
|
3956
|
-
return async function POST(req) {
|
|
3957
|
-
try {
|
|
3958
|
-
const formData = await req.formData();
|
|
3959
|
-
const files = formData.getAll("files");
|
|
3960
|
-
const namespace = formData.get("namespace") || void 0;
|
|
3961
|
-
const dimensionRaw = formData.get("dimension");
|
|
3962
|
-
const dimension = dimensionRaw ? parseInt(dimensionRaw, 10) : void 0;
|
|
3963
|
-
if (!files || files.length === 0) {
|
|
3964
|
-
return NextResponse.json({ error: "No files provided" }, { status: 400 });
|
|
3965
|
-
}
|
|
3966
|
-
const documents = [];
|
|
3967
|
-
for (const file of files) {
|
|
3968
|
-
if (file.name.toLowerCase().endsWith(".csv") || file.type === "text/csv") {
|
|
3969
|
-
const text = await file.text();
|
|
3970
|
-
const Papa = await import("papaparse");
|
|
3971
|
-
const parsed = Papa.default.parse(text, { header: true, skipEmptyLines: true });
|
|
3972
|
-
if (parsed.data && parsed.data.length > 0) {
|
|
3973
|
-
let i = 0;
|
|
3974
|
-
let lastRowData = null;
|
|
3975
|
-
for (const row of parsed.data) {
|
|
3976
|
-
i++;
|
|
3977
|
-
const rowData = row;
|
|
3978
|
-
const groupingKey = Object.keys(rowData)[0];
|
|
3979
|
-
if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
|
|
3980
|
-
for (const key of Object.keys(rowData)) {
|
|
3981
|
-
if (!rowData[key] && lastRowData[key]) {
|
|
3982
|
-
rowData[key] = lastRowData[key];
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
|
-
lastRowData = __spreadValues({}, rowData);
|
|
3987
|
-
const contentParts = [];
|
|
3988
|
-
for (const [key, val] of Object.entries(rowData)) {
|
|
3989
|
-
if (key && val) {
|
|
3990
|
-
contentParts.push(`${key}: ${val}`);
|
|
3991
|
-
}
|
|
3992
|
-
}
|
|
3993
|
-
documents.push({
|
|
3994
|
-
docId: `${file.name}-row-${i}`,
|
|
3995
|
-
content: contentParts.join(", "),
|
|
3996
|
-
metadata: __spreadValues(__spreadValues({
|
|
3997
|
-
fileName: file.name,
|
|
3998
|
-
fileSize: file.size,
|
|
3999
|
-
fileType: file.type,
|
|
4000
|
-
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4001
|
-
}, dimension ? { dimension } : {}), rowData)
|
|
4002
|
-
});
|
|
4003
|
-
}
|
|
4004
|
-
}
|
|
4005
|
-
} else {
|
|
4006
|
-
const content = await DocumentParser.parse(file, file.name, file.type);
|
|
4007
|
-
documents.push({
|
|
4008
|
-
docId: file.name,
|
|
4009
|
-
content,
|
|
4010
|
-
metadata: __spreadValues({
|
|
4011
|
-
fileName: file.name,
|
|
4012
|
-
fileSize: file.size,
|
|
4013
|
-
fileType: file.type,
|
|
4014
|
-
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4015
|
-
}, dimension ? { dimension } : {})
|
|
4016
|
-
});
|
|
4017
|
-
}
|
|
4018
|
-
}
|
|
4019
|
-
const results = await plugin.ingest(documents, namespace);
|
|
4020
|
-
return NextResponse.json({ message: "Upload successful", results });
|
|
4021
|
-
} catch (err) {
|
|
4022
|
-
const message = err instanceof Error ? err.message : "Upload failed";
|
|
4023
|
-
return NextResponse.json({ error: message }, { status: 500 });
|
|
4024
|
-
}
|
|
4025
|
-
};
|
|
4026
|
-
}
|
|
4027
|
-
function createSuggestionsHandler(configOrPlugin) {
|
|
4028
|
-
const plugin = configOrPlugin instanceof VectorPlugin ? configOrPlugin : new VectorPlugin(configOrPlugin);
|
|
4029
|
-
return async function POST(req) {
|
|
4030
|
-
try {
|
|
4031
|
-
const body = await req.json();
|
|
4032
|
-
const { query, namespace } = body;
|
|
4033
|
-
if (typeof query !== "string") {
|
|
4034
|
-
return NextResponse.json({ error: "query is required" }, { status: 400 });
|
|
4035
|
-
}
|
|
4036
|
-
const suggestions = await plugin.getSuggestions(query, namespace);
|
|
4037
|
-
return NextResponse.json({ suggestions });
|
|
4038
|
-
} catch (err) {
|
|
4039
|
-
const message = err instanceof Error ? err.message : "Internal server error";
|
|
4040
|
-
return NextResponse.json({ error: message }, { status: 500 });
|
|
4041
|
-
}
|
|
4042
|
-
};
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4045
|
-
export {
|
|
4046
|
-
getRagConfig,
|
|
4047
|
-
ConfigResolver,
|
|
4048
|
-
OpenAIProvider,
|
|
4049
|
-
AnthropicProvider,
|
|
4050
|
-
OllamaProvider,
|
|
4051
|
-
LLM_PROFILES,
|
|
4052
|
-
VECTOR_PROFILES,
|
|
4053
|
-
UniversalLLMAdapter,
|
|
4054
|
-
LLMFactory,
|
|
4055
|
-
ProviderRegistry,
|
|
4056
|
-
ConfigValidator,
|
|
4057
|
-
DocumentChunker,
|
|
4058
|
-
BatchProcessor,
|
|
4059
|
-
EmbeddingStrategy,
|
|
4060
|
-
EmbeddingStrategyResolver,
|
|
4061
|
-
Pipeline,
|
|
4062
|
-
ProviderHealthCheck,
|
|
4063
|
-
VectorPlugin,
|
|
4064
|
-
DocumentParser,
|
|
4065
|
-
sseFrame,
|
|
4066
|
-
sseTextFrame,
|
|
4067
|
-
sseMetaFrame,
|
|
4068
|
-
sseUIFrame,
|
|
4069
|
-
sseObservabilityFrame,
|
|
4070
|
-
sseErrorFrame,
|
|
4071
|
-
createChatHandler,
|
|
4072
|
-
createStreamHandler,
|
|
4073
|
-
createIngestHandler,
|
|
4074
|
-
createHealthHandler,
|
|
4075
|
-
createUploadHandler,
|
|
4076
|
-
createSuggestionsHandler
|
|
4077
|
-
};
|