@retrivora-ai/rag-engine 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
- package/dist/{index-DPsQodME.d.mts → ILLMProvider-BfRgI1Xh.d.mts} +58 -1
- package/dist/{index-DPsQodME.d.ts → ILLMProvider-BfRgI1Xh.d.ts} +58 -1
- package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +8 -0
- package/dist/{chunk-PV3MFHWU.mjs → chunk-BFYLQYQU.mjs} +808 -439
- package/dist/chunk-R3RGUMHE.mjs +218 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +995 -603
- package/dist/handlers/index.mjs +1 -1
- package/dist/{index-Bb2yEopi.d.mts → index-1Z4GuYBi.d.ts} +7 -1
- package/dist/{index-CkbTzj9J.d.ts → index-BV0z5mb6.d.mts} +7 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +491 -316
- package/dist/index.mjs +411 -217
- package/dist/server.d.mts +35 -5
- package/dist/server.d.ts +35 -5
- package/dist/server.js +1146 -777
- package/dist/server.mjs +163 -176
- package/package.json +4 -2
- package/src/app/page.tsx +1 -2
- package/src/components/MessageBubble.tsx +211 -69
- package/src/components/ProductCard.tsx +2 -2
- package/src/components/VisualizationRenderer.tsx +347 -247
- package/src/config/RagConfig.ts +5 -0
- package/src/config/serverConfig.ts +3 -1
- package/src/core/Pipeline.ts +65 -206
- package/src/core/ProviderRegistry.ts +2 -2
- package/src/core/VectorPlugin.ts +9 -0
- package/src/handlers/index.ts +23 -7
- package/src/hooks/useRagChat.ts +2 -2
- package/src/llm/LLMFactory.ts +54 -2
- package/src/llm/providers/AnthropicProvider.ts +12 -8
- package/src/llm/providers/GeminiProvider.ts +188 -143
- package/src/llm/providers/OllamaProvider.ts +7 -3
- package/src/llm/providers/OpenAIProvider.ts +12 -8
- package/src/types/chat.ts +6 -0
- package/src/types/index.ts +80 -0
- package/src/utils/SchemaMapper.ts +129 -0
- package/src/utils/UITransformer.ts +491 -181
- package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
- package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
- package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
- package/dist/chunk-FLOSGE6A.mjs +0 -202
|
@@ -83,7 +83,7 @@ function getRagConfig(baseConfig, env = process.env) {
|
|
|
83
83
|
return getEnvConfig(env, baseConfig);
|
|
84
84
|
}
|
|
85
85
|
function getEnvConfig(env = process.env, base) {
|
|
86
|
-
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;
|
|
86
|
+
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;
|
|
87
87
|
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__";
|
|
88
88
|
const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
|
|
89
89
|
const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
|
|
@@ -94,33 +94,35 @@ function getEnvConfig(env = process.env, base) {
|
|
|
94
94
|
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 : "";
|
|
95
95
|
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;
|
|
96
96
|
} else if (vectorProvider === "pgvector" || vectorProvider === "postgresql") {
|
|
97
|
-
vectorDbOptions.connectionString = (_p = (
|
|
97
|
+
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 : "";
|
|
98
|
+
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;
|
|
99
|
+
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;
|
|
98
100
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
99
101
|
} else if (vectorProvider === "mongodb") {
|
|
100
|
-
vectorDbOptions.uri = (
|
|
101
|
-
vectorDbOptions.database = (
|
|
102
|
-
vectorDbOptions.collection = (
|
|
103
|
-
vectorDbOptions.indexName = (
|
|
102
|
+
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 : "";
|
|
103
|
+
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 : "";
|
|
104
|
+
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 : "";
|
|
105
|
+
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";
|
|
104
106
|
} else if (vectorProvider === "qdrant") {
|
|
105
|
-
vectorDbOptions.baseUrl = (
|
|
106
|
-
vectorDbOptions.apiKey = (
|
|
107
|
+
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";
|
|
108
|
+
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;
|
|
107
109
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
108
110
|
} else if (vectorProvider === "milvus") {
|
|
109
|
-
vectorDbOptions.baseUrl = (
|
|
111
|
+
vectorDbOptions.baseUrl = (_Z = readString(env, "MILVUS_URL")) != null ? _Z : "http://localhost:19530";
|
|
110
112
|
vectorDbOptions.apiKey = readString(env, "MILVUS_API_KEY");
|
|
111
113
|
} else if (vectorProvider === "chromadb") {
|
|
112
|
-
vectorDbOptions.baseUrl = (
|
|
114
|
+
vectorDbOptions.baseUrl = (__ = readString(env, "CHROMADB_URL")) != null ? __ : "http://localhost:8000";
|
|
113
115
|
} else if (vectorProvider === "weaviate") {
|
|
114
|
-
vectorDbOptions.baseUrl = (
|
|
116
|
+
vectorDbOptions.baseUrl = (_$ = readString(env, "WEAVIATE_URL")) != null ? _$ : "http://localhost:8080";
|
|
115
117
|
vectorDbOptions.apiKey = readString(env, "WEAVIATE_API_KEY");
|
|
116
118
|
} else if (vectorProvider === "redis") {
|
|
117
|
-
vectorDbOptions.baseUrl = (
|
|
119
|
+
vectorDbOptions.baseUrl = (_aa = readString(env, "REDIS_URL")) != null ? _aa : "";
|
|
118
120
|
vectorDbOptions.apiKey = readString(env, "REDIS_API_KEY");
|
|
119
121
|
} else if (vectorProvider === "rest") {
|
|
120
|
-
vectorDbOptions.baseUrl = (
|
|
122
|
+
vectorDbOptions.baseUrl = (_ba = readString(env, "VECTOR_DB_REST_URL")) != null ? _ba : "";
|
|
121
123
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { "api-key": readString(env, "VECTOR_DB_REST_API_KEY") } : {};
|
|
122
124
|
} else if (vectorProvider === "universal_rest") {
|
|
123
|
-
vectorDbOptions.baseUrl = (
|
|
125
|
+
vectorDbOptions.baseUrl = (_da = (_ca = readString(env, "VECTOR_BASE_URL")) != null ? _ca : readString(env, "VECTOR_DB_REST_URL")) != null ? _da : "";
|
|
124
126
|
vectorDbOptions.profile = readString(env, "VECTOR_UNIVERSAL_PROFILE");
|
|
125
127
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { Authorization: `Bearer ${readString(env, "VECTOR_DB_REST_API_KEY")}` } : {};
|
|
126
128
|
}
|
|
@@ -137,20 +139,20 @@ function getEnvConfig(env = process.env, base) {
|
|
|
137
139
|
openai: readString(env, "OPENAI_API_KEY"),
|
|
138
140
|
gemini: readString(env, "GEMINI_API_KEY"),
|
|
139
141
|
ollama: void 0,
|
|
140
|
-
universal_rest: (
|
|
141
|
-
custom: (
|
|
142
|
+
universal_rest: (_ea = readString(env, "EMBEDDING_API_KEY")) != null ? _ea : readString(env, "OPENAI_API_KEY"),
|
|
143
|
+
custom: (_fa = readString(env, "EMBEDDING_API_KEY")) != null ? _fa : readString(env, "OPENAI_API_KEY")
|
|
142
144
|
};
|
|
143
145
|
return {
|
|
144
146
|
projectId,
|
|
145
147
|
vectorDb: {
|
|
146
148
|
provider: vectorProvider,
|
|
147
|
-
indexName: (
|
|
149
|
+
indexName: (_ha = (_ga = readString(env, "VECTOR_DB_INDEX")) != null ? _ga : vectorDbOptions.indexName) != null ? _ha : "rag-index",
|
|
148
150
|
options: vectorDbOptions
|
|
149
151
|
},
|
|
150
152
|
llm: {
|
|
151
153
|
provider: llmProvider,
|
|
152
|
-
model: (
|
|
153
|
-
apiKey: (
|
|
154
|
+
model: (_ia = readString(env, "LLM_MODEL")) != null ? _ia : "gpt-4o",
|
|
155
|
+
apiKey: (_ja = llmApiKeyByProvider[llmProvider]) != null ? _ja : "",
|
|
154
156
|
baseUrl: readString(env, "LLM_BASE_URL"),
|
|
155
157
|
systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
|
|
156
158
|
maxTokens: readNumber(env, "LLM_MAX_TOKENS", 4096),
|
|
@@ -161,7 +163,7 @@ function getEnvConfig(env = process.env, base) {
|
|
|
161
163
|
},
|
|
162
164
|
embedding: {
|
|
163
165
|
provider: embeddingProvider,
|
|
164
|
-
model: (
|
|
166
|
+
model: (_ka = readString(env, "EMBEDDING_MODEL")) != null ? _ka : "text-embedding-3-small",
|
|
165
167
|
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
166
168
|
baseUrl: readString(env, "EMBEDDING_BASE_URL"),
|
|
167
169
|
dimensions: embeddingDimensions,
|
|
@@ -172,17 +174,17 @@ function getEnvConfig(env = process.env, base) {
|
|
|
172
174
|
}
|
|
173
175
|
},
|
|
174
176
|
ui: {
|
|
175
|
-
title: (
|
|
176
|
-
subtitle: (
|
|
177
|
-
primaryColor: (
|
|
178
|
-
accentColor: (
|
|
179
|
-
logoUrl: (
|
|
180
|
-
placeholder: (
|
|
181
|
-
showSources: ((
|
|
182
|
-
welcomeMessage: (
|
|
183
|
-
visualStyle: (
|
|
184
|
-
borderRadius: (
|
|
185
|
-
allowUpload: ((
|
|
177
|
+
title: (_ma = (_la = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _la : readString(env, "UI_TITLE")) != null ? _ma : "AI Assistant",
|
|
178
|
+
subtitle: (_oa = (_na = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _na : readString(env, "UI_SUBTITLE")) != null ? _oa : "Powered by RAG",
|
|
179
|
+
primaryColor: (_qa = (_pa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _pa : readString(env, "UI_PRIMARY_COLOR")) != null ? _qa : "#10b981",
|
|
180
|
+
accentColor: (_sa = (_ra = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ra : readString(env, "UI_ACCENT_COLOR")) != null ? _sa : "#3b82f6",
|
|
181
|
+
logoUrl: (_ta = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ta : readString(env, "UI_LOGO_URL"),
|
|
182
|
+
placeholder: (_va = (_ua = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ua : readString(env, "UI_PLACEHOLDER")) != null ? _va : "Ask me anything\u2026",
|
|
183
|
+
showSources: ((_xa = (_wa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _wa : readString(env, "UI_SHOW_SOURCES")) != null ? _xa : "true") !== "false",
|
|
184
|
+
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.",
|
|
185
|
+
visualStyle: (_Ba = (_Aa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Aa : readString(env, "UI_VISUAL_STYLE")) != null ? _Ba : "glass",
|
|
186
|
+
borderRadius: (_Da = (_Ca = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Ca : readString(env, "UI_BORDER_RADIUS")) != null ? _Da : "xl",
|
|
187
|
+
allowUpload: ((_Fa = (_Ea = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Ea : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Fa : "false") === "true"
|
|
186
188
|
},
|
|
187
189
|
rag: {
|
|
188
190
|
topK: readNumber(env, "RAG_TOP_K", 5),
|
|
@@ -299,14 +301,14 @@ var OpenAIProvider = class {
|
|
|
299
301
|
};
|
|
300
302
|
}
|
|
301
303
|
async chat(messages, context, options) {
|
|
302
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
303
|
-
const
|
|
304
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
305
|
+
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.
|
|
304
306
|
|
|
305
307
|
Context:
|
|
306
308
|
${context}`;
|
|
307
309
|
const systemMessage = {
|
|
308
310
|
role: "system",
|
|
309
|
-
content:
|
|
311
|
+
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
310
312
|
|
|
311
313
|
Context:
|
|
312
314
|
${context}`
|
|
@@ -321,22 +323,22 @@ ${context}`
|
|
|
321
323
|
const completion = await this.client.chat.completions.create({
|
|
322
324
|
model: this.llmConfig.model,
|
|
323
325
|
messages: formattedMessages,
|
|
324
|
-
max_tokens: (
|
|
325
|
-
temperature: (
|
|
326
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
327
|
+
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
326
328
|
stop: options == null ? void 0 : options.stop
|
|
327
329
|
});
|
|
328
|
-
return (
|
|
330
|
+
return (_i = (_h = (_g = completion.choices[0]) == null ? void 0 : _g.message) == null ? void 0 : _h.content) != null ? _i : "";
|
|
329
331
|
}
|
|
330
332
|
chatStream(messages, context, options) {
|
|
331
333
|
return __asyncGenerator(this, null, function* () {
|
|
332
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
333
|
-
const
|
|
334
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
335
|
+
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.
|
|
334
336
|
|
|
335
337
|
Context:
|
|
336
338
|
${context}`;
|
|
337
339
|
const systemMessage = {
|
|
338
340
|
role: "system",
|
|
339
|
-
content:
|
|
341
|
+
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
340
342
|
|
|
341
343
|
Context:
|
|
342
344
|
${context}`
|
|
@@ -351,8 +353,8 @@ ${context}`
|
|
|
351
353
|
const stream = yield new __await(this.client.chat.completions.create({
|
|
352
354
|
model: this.llmConfig.model,
|
|
353
355
|
messages: formattedMessages,
|
|
354
|
-
max_tokens: (
|
|
355
|
-
temperature: (
|
|
356
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
357
|
+
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
356
358
|
stop: options == null ? void 0 : options.stop,
|
|
357
359
|
stream: true
|
|
358
360
|
}));
|
|
@@ -362,7 +364,7 @@ ${context}`
|
|
|
362
364
|
try {
|
|
363
365
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
364
366
|
const chunk = temp.value;
|
|
365
|
-
const content = ((
|
|
367
|
+
const content = ((_h = (_g = chunk.choices[0]) == null ? void 0 : _g.delta) == null ? void 0 : _h.content) || "";
|
|
366
368
|
if (content) yield content;
|
|
367
369
|
}
|
|
368
370
|
} catch (temp) {
|
|
@@ -460,12 +462,12 @@ var AnthropicProvider = class {
|
|
|
460
462
|
};
|
|
461
463
|
}
|
|
462
464
|
async chat(messages, context, options) {
|
|
463
|
-
var _a, _b, _c;
|
|
464
|
-
const
|
|
465
|
+
var _a, _b, _c, _d;
|
|
466
|
+
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.
|
|
465
467
|
|
|
466
468
|
Context:
|
|
467
469
|
${context}`;
|
|
468
|
-
const system =
|
|
470
|
+
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
469
471
|
|
|
470
472
|
Context:
|
|
471
473
|
${context}`;
|
|
@@ -475,7 +477,7 @@ ${context}`;
|
|
|
475
477
|
}));
|
|
476
478
|
const response = await this.client.messages.create({
|
|
477
479
|
model: this.llmConfig.model,
|
|
478
|
-
max_tokens: (
|
|
480
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
479
481
|
system,
|
|
480
482
|
messages: anthropicMessages
|
|
481
483
|
});
|
|
@@ -484,12 +486,12 @@ ${context}`;
|
|
|
484
486
|
}
|
|
485
487
|
chatStream(messages, context, options) {
|
|
486
488
|
return __asyncGenerator(this, null, function* () {
|
|
487
|
-
var _a, _b, _c;
|
|
488
|
-
const
|
|
489
|
+
var _a, _b, _c, _d;
|
|
490
|
+
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.
|
|
489
491
|
|
|
490
492
|
Context:
|
|
491
493
|
${context}`;
|
|
492
|
-
const system =
|
|
494
|
+
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
493
495
|
|
|
494
496
|
Context:
|
|
495
497
|
${context}`;
|
|
@@ -499,7 +501,7 @@ ${context}`;
|
|
|
499
501
|
}));
|
|
500
502
|
const stream = yield new __await(this.client.messages.create({
|
|
501
503
|
model: this.llmConfig.model,
|
|
502
|
-
max_tokens: (
|
|
504
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
503
505
|
system,
|
|
504
506
|
messages: anthropicMessages,
|
|
505
507
|
stream: true
|
|
@@ -614,7 +616,7 @@ var OllamaProvider = class {
|
|
|
614
616
|
}
|
|
615
617
|
async chat(messages, context, options) {
|
|
616
618
|
var _a, _b, _c, _d;
|
|
617
|
-
const system = this.buildSystemPrompt(context);
|
|
619
|
+
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
618
620
|
const { data } = await this.http.post("/api/chat", {
|
|
619
621
|
model: this.llmConfig.model,
|
|
620
622
|
stream: false,
|
|
@@ -632,7 +634,7 @@ var OllamaProvider = class {
|
|
|
632
634
|
chatStream(messages, context, options) {
|
|
633
635
|
return __asyncGenerator(this, null, function* () {
|
|
634
636
|
var _a, _b, _c, _d, _e, _f;
|
|
635
|
-
const system = this.buildSystemPrompt(context);
|
|
637
|
+
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
636
638
|
const response = yield new __await(this.http.post("/api/chat", {
|
|
637
639
|
model: this.llmConfig.model,
|
|
638
640
|
stream: true,
|
|
@@ -688,8 +690,11 @@ var OllamaProvider = class {
|
|
|
688
690
|
}
|
|
689
691
|
});
|
|
690
692
|
}
|
|
691
|
-
buildSystemPrompt(context) {
|
|
693
|
+
buildSystemPrompt(context, overridePrompt) {
|
|
692
694
|
var _a;
|
|
695
|
+
if (overridePrompt) {
|
|
696
|
+
return overridePrompt;
|
|
697
|
+
}
|
|
693
698
|
const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Use the provided context to answer the user's question.
|
|
694
699
|
|
|
695
700
|
Context:
|
|
@@ -741,20 +746,41 @@ ${context}`;
|
|
|
741
746
|
};
|
|
742
747
|
|
|
743
748
|
// src/llm/providers/GeminiProvider.ts
|
|
744
|
-
import {
|
|
749
|
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
750
|
+
var DEFAULT_EMBEDDING_MODEL = "text-embedding-004";
|
|
751
|
+
var DEFAULT_TEMPERATURE = 0.7;
|
|
752
|
+
var DEFAULT_MAX_TOKENS = 1024;
|
|
753
|
+
function sanitizeModel(model) {
|
|
754
|
+
return model ? model.split(":")[0] : model;
|
|
755
|
+
}
|
|
756
|
+
function buildClient(apiKey) {
|
|
757
|
+
return new GoogleGenerativeAI(apiKey);
|
|
758
|
+
}
|
|
759
|
+
function applyPrefix(text, taskType, queryPrefix, docPrefix) {
|
|
760
|
+
if (taskType === "query" && queryPrefix && !text.startsWith(queryPrefix)) {
|
|
761
|
+
return `${queryPrefix}${text}`;
|
|
762
|
+
}
|
|
763
|
+
if (taskType === "document" && docPrefix && !text.startsWith(docPrefix)) {
|
|
764
|
+
return `${docPrefix}${text}`;
|
|
765
|
+
}
|
|
766
|
+
return text;
|
|
767
|
+
}
|
|
745
768
|
var GeminiProvider = class {
|
|
746
769
|
constructor(llmConfig, embeddingConfig) {
|
|
747
|
-
if (!llmConfig.apiKey)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
770
|
+
if (!llmConfig.apiKey) {
|
|
771
|
+
throw new Error("[GeminiProvider] llmConfig.apiKey is required");
|
|
772
|
+
}
|
|
773
|
+
this.llmConfig = __spreadProps(__spreadValues({}, llmConfig), { model: sanitizeModel(llmConfig.model) });
|
|
774
|
+
this.client = buildClient(this.llmConfig.apiKey);
|
|
752
775
|
if (embeddingConfig) {
|
|
753
776
|
this.embeddingConfig = __spreadProps(__spreadValues({}, embeddingConfig), {
|
|
754
|
-
model:
|
|
777
|
+
model: sanitizeModel(embeddingConfig.model)
|
|
755
778
|
});
|
|
756
779
|
}
|
|
757
780
|
}
|
|
781
|
+
// -------------------------------------------------------------------------
|
|
782
|
+
// Static factory helpers
|
|
783
|
+
// -------------------------------------------------------------------------
|
|
758
784
|
static getValidator() {
|
|
759
785
|
return {
|
|
760
786
|
validate(config) {
|
|
@@ -768,7 +794,11 @@ var GeminiProvider = class {
|
|
|
768
794
|
});
|
|
769
795
|
}
|
|
770
796
|
if (!config.model) {
|
|
771
|
-
errors.push({
|
|
797
|
+
errors.push({
|
|
798
|
+
field: "llm.model",
|
|
799
|
+
message: "Gemini model name is required",
|
|
800
|
+
severity: "error"
|
|
801
|
+
});
|
|
772
802
|
}
|
|
773
803
|
return errors;
|
|
774
804
|
}
|
|
@@ -777,13 +807,15 @@ var GeminiProvider = class {
|
|
|
777
807
|
static getHealthChecker() {
|
|
778
808
|
return {
|
|
779
809
|
async check(config) {
|
|
810
|
+
var _a, _b;
|
|
780
811
|
const timestamp = Date.now();
|
|
781
|
-
const apiKey = config.apiKey
|
|
782
|
-
const modelName = config.model;
|
|
812
|
+
const apiKey = (_b = (_a = config.apiKey) != null ? _a : process.env.GOOGLE_GENAI_API_KEY) != null ? _b : "";
|
|
813
|
+
const modelName = sanitizeModel(config.model);
|
|
783
814
|
try {
|
|
784
|
-
const {
|
|
785
|
-
const
|
|
786
|
-
|
|
815
|
+
const { GoogleGenerativeAI: GoogleGenerativeAI2 } = await import("@google/generative-ai");
|
|
816
|
+
const ai = new GoogleGenerativeAI2(apiKey);
|
|
817
|
+
const model = ai.getGenerativeModel({ model: DEFAULT_EMBEDDING_MODEL });
|
|
818
|
+
await model.embedContent("health-check");
|
|
787
819
|
return {
|
|
788
820
|
healthy: true,
|
|
789
821
|
provider: "gemini",
|
|
@@ -801,70 +833,90 @@ var GeminiProvider = class {
|
|
|
801
833
|
}
|
|
802
834
|
};
|
|
803
835
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
836
|
+
// -------------------------------------------------------------------------
|
|
837
|
+
// Private utilities
|
|
838
|
+
// -------------------------------------------------------------------------
|
|
839
|
+
/** Resolve the embedding client — uses a separate client when the embedding
|
|
840
|
+
* API key differs from the LLM API key. */
|
|
841
|
+
get embeddingClient() {
|
|
842
|
+
var _a;
|
|
843
|
+
if (((_a = this.embeddingConfig) == null ? void 0 : _a.apiKey) && this.embeddingConfig.apiKey !== this.llmConfig.apiKey) {
|
|
844
|
+
return buildClient(this.embeddingConfig.apiKey);
|
|
845
|
+
}
|
|
846
|
+
return this.client;
|
|
847
|
+
}
|
|
848
|
+
/** Resolve the embedding model to use, in order of specificity. */
|
|
849
|
+
resolveEmbeddingModel(optionsModel) {
|
|
850
|
+
var _a, _b;
|
|
851
|
+
return sanitizeModel((_b = optionsModel != null ? optionsModel : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _b : DEFAULT_EMBEDDING_MODEL);
|
|
807
852
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
853
|
+
/**
|
|
854
|
+
* Build the system instruction string, inserting the RAG context either via
|
|
855
|
+
* the `{{context}}` placeholder or by appending it.
|
|
856
|
+
*/
|
|
857
|
+
buildSystemInstruction(context) {
|
|
858
|
+
var _a;
|
|
859
|
+
const base = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
|
|
811
860
|
|
|
812
861
|
Context:
|
|
813
862
|
${context}`;
|
|
814
|
-
|
|
863
|
+
return base.includes("{{context}}") ? base.replace("{{context}}", context) : `${base}
|
|
815
864
|
|
|
816
865
|
Context:
|
|
817
866
|
${context}`;
|
|
818
|
-
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Convert ChatMessage[] to the Gemini contents format.
|
|
870
|
+
*
|
|
871
|
+
* Rules enforced by the Gemini API:
|
|
872
|
+
* - Only `user` and `model` roles are allowed in `contents`.
|
|
873
|
+
* - System messages must be passed via `systemInstruction`, not here.
|
|
874
|
+
* - Messages must strictly alternate between `user` and `model`.
|
|
875
|
+
*/
|
|
876
|
+
buildGeminiContents(messages) {
|
|
877
|
+
return messages.filter((m) => m.role !== "system").map((m) => ({
|
|
819
878
|
role: m.role === "assistant" ? "model" : "user",
|
|
820
879
|
parts: [{ text: m.content }]
|
|
821
880
|
}));
|
|
822
|
-
|
|
881
|
+
}
|
|
882
|
+
// -------------------------------------------------------------------------
|
|
883
|
+
// ILLMProvider — chat
|
|
884
|
+
// -------------------------------------------------------------------------
|
|
885
|
+
async chat(messages, context, options) {
|
|
886
|
+
var _a, _b, _c, _d;
|
|
887
|
+
const model = this.client.getGenerativeModel({
|
|
823
888
|
model: this.llmConfig.model,
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
889
|
+
systemInstruction: this.buildSystemInstruction(context)
|
|
890
|
+
});
|
|
891
|
+
const result = await model.generateContent({
|
|
892
|
+
contents: this.buildGeminiContents(messages),
|
|
893
|
+
generationConfig: {
|
|
894
|
+
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
895
|
+
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
829
896
|
stopSequences: options == null ? void 0 : options.stop
|
|
830
897
|
}
|
|
831
898
|
});
|
|
832
|
-
|
|
833
|
-
return text;
|
|
899
|
+
return result.response.text();
|
|
834
900
|
}
|
|
835
901
|
chatStream(messages, context, options) {
|
|
836
902
|
return __asyncGenerator(this, null, function* () {
|
|
837
|
-
var _a, _b, _c, _d
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
Context:
|
|
841
|
-
${context}`;
|
|
842
|
-
const systemInstruction = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
|
|
843
|
-
|
|
844
|
-
Context:
|
|
845
|
-
${context}`;
|
|
846
|
-
const geminiMessages = messages.map((m) => ({
|
|
847
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
848
|
-
parts: [{ text: m.content }]
|
|
849
|
-
}));
|
|
850
|
-
const result = yield new __await(this.client.models.generateContentStream({
|
|
903
|
+
var _a, _b, _c, _d;
|
|
904
|
+
const model = this.client.getGenerativeModel({
|
|
851
905
|
model: this.llmConfig.model,
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
906
|
+
systemInstruction: this.buildSystemInstruction(context)
|
|
907
|
+
});
|
|
908
|
+
const result = yield new __await(model.generateContentStream({
|
|
909
|
+
contents: this.buildGeminiContents(messages),
|
|
910
|
+
generationConfig: {
|
|
911
|
+
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
912
|
+
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
857
913
|
stopSequences: options == null ? void 0 : options.stop
|
|
858
914
|
}
|
|
859
915
|
}));
|
|
860
|
-
const stream = (_f = result == null ? void 0 : result.stream) != null ? _f : result;
|
|
861
|
-
if (!stream) {
|
|
862
|
-
throw new Error("[GeminiProvider] generateContentStream returned undefined");
|
|
863
|
-
}
|
|
864
916
|
try {
|
|
865
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
917
|
+
for (var iter = __forAwait(result.stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
866
918
|
const chunk = temp.value;
|
|
867
|
-
const text =
|
|
919
|
+
const text = chunk.text();
|
|
868
920
|
if (text) yield text;
|
|
869
921
|
}
|
|
870
922
|
} catch (temp) {
|
|
@@ -879,69 +931,69 @@ ${context}`;
|
|
|
879
931
|
}
|
|
880
932
|
});
|
|
881
933
|
}
|
|
934
|
+
// -------------------------------------------------------------------------
|
|
935
|
+
// ILLMProvider — embeddings
|
|
936
|
+
// -------------------------------------------------------------------------
|
|
882
937
|
async embed(text, options) {
|
|
883
|
-
var _a, _b
|
|
884
|
-
const
|
|
885
|
-
|
|
938
|
+
var _a, _b;
|
|
939
|
+
const content = applyPrefix(
|
|
940
|
+
text,
|
|
941
|
+
options == null ? void 0 : options.taskType,
|
|
942
|
+
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
943
|
+
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
886
944
|
);
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const docPrefix = (_g = this.embeddingConfig) == null ? void 0 : _g.documentPrefix;
|
|
892
|
-
if ((options == null ? void 0 : options.taskType) === "query" && queryPrefix) {
|
|
893
|
-
if (!content.startsWith(queryPrefix)) {
|
|
894
|
-
content = `${queryPrefix}${text}`;
|
|
895
|
-
}
|
|
896
|
-
} else if ((options == null ? void 0 : options.taskType) === "document" && docPrefix) {
|
|
897
|
-
if (!content.startsWith(docPrefix)) {
|
|
898
|
-
content = `${docPrefix}${text}`;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const response = await client.models.embedContent({
|
|
902
|
-
model,
|
|
903
|
-
contents: content,
|
|
904
|
-
config: ((_h = this.embeddingConfig) == null ? void 0 : _h.dimensions) ? { outputDimensionality: this.embeddingConfig.dimensions } : void 0
|
|
945
|
+
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
946
|
+
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
947
|
+
const response = await model.embedContent({
|
|
948
|
+
content: { role: "user", parts: [{ text: content }] }
|
|
905
949
|
}).catch((err) => {
|
|
906
|
-
console.error(`[GeminiProvider] Embedding failed for model "${
|
|
950
|
+
console.error(`[GeminiProvider] Embedding failed for model "${modelName}":`, err.message);
|
|
907
951
|
throw err;
|
|
908
952
|
});
|
|
909
|
-
return
|
|
953
|
+
return response.embedding.values;
|
|
910
954
|
}
|
|
911
955
|
async batchEmbed(texts, options) {
|
|
912
|
-
|
|
913
|
-
const model = this.
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
956
|
+
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
957
|
+
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
958
|
+
const requests = texts.map((text) => {
|
|
959
|
+
var _a, _b;
|
|
960
|
+
return {
|
|
961
|
+
content: {
|
|
962
|
+
role: "user",
|
|
963
|
+
parts: [{
|
|
964
|
+
text: applyPrefix(
|
|
965
|
+
text,
|
|
966
|
+
options == null ? void 0 : options.taskType,
|
|
967
|
+
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
968
|
+
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
969
|
+
)
|
|
970
|
+
}]
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
});
|
|
974
|
+
const response = await model.batchEmbedContents({
|
|
975
|
+
requests: requests.map((r) => ({
|
|
976
|
+
model: `models/${modelName}`,
|
|
977
|
+
content: r.content
|
|
978
|
+
}))
|
|
930
979
|
}).catch((err) => {
|
|
931
|
-
console.error(`[GeminiProvider] Batch embedding failed for model "${
|
|
980
|
+
console.error(`[GeminiProvider] Batch embedding failed for model "${modelName}":`, err.message);
|
|
932
981
|
throw err;
|
|
933
982
|
});
|
|
934
|
-
return
|
|
935
|
-
var
|
|
936
|
-
return (
|
|
937
|
-
})
|
|
983
|
+
return response.embeddings.map((e) => {
|
|
984
|
+
var _a;
|
|
985
|
+
return (_a = e.values) != null ? _a : [];
|
|
986
|
+
});
|
|
938
987
|
}
|
|
988
|
+
// -------------------------------------------------------------------------
|
|
989
|
+
// ILLMProvider — health
|
|
990
|
+
// -------------------------------------------------------------------------
|
|
939
991
|
async ping() {
|
|
940
992
|
try {
|
|
941
|
-
|
|
942
|
-
model:
|
|
943
|
-
contents: "ping"
|
|
993
|
+
const model = this.embeddingClient.getGenerativeModel({
|
|
994
|
+
model: this.resolveEmbeddingModel()
|
|
944
995
|
});
|
|
996
|
+
await model.embedContent("ping");
|
|
945
997
|
return true;
|
|
946
998
|
} catch (err) {
|
|
947
999
|
console.error("[GeminiProvider] Ping failed:", err);
|
|
@@ -1136,9 +1188,51 @@ ${context != null ? context : "None"}` },
|
|
|
1136
1188
|
};
|
|
1137
1189
|
|
|
1138
1190
|
// src/llm/LLMFactory.ts
|
|
1191
|
+
var customProviders = /* @__PURE__ */ new Map();
|
|
1139
1192
|
var LLMFactory = class _LLMFactory {
|
|
1193
|
+
/**
|
|
1194
|
+
* Register a custom LLM provider factory at runtime.
|
|
1195
|
+
*
|
|
1196
|
+
* Use this to add support for any LLM backend (Azure OpenAI, Cohere, Mistral,
|
|
1197
|
+
* Bedrock, etc.) without modifying this library's source code.
|
|
1198
|
+
*
|
|
1199
|
+
* @example
|
|
1200
|
+
* // In your Next.js app initialization:
|
|
1201
|
+
* import { LLMFactory } from '@retrivora-ai/rag-engine/server';
|
|
1202
|
+
* import { MyCustomProvider } from './providers/MyCustomProvider';
|
|
1203
|
+
*
|
|
1204
|
+
* LLMFactory.register('my-provider', (config) => new MyCustomProvider(config));
|
|
1205
|
+
*
|
|
1206
|
+
* // Then set in your .env.local:
|
|
1207
|
+
* // LLM_PROVIDER=my-provider
|
|
1208
|
+
*/
|
|
1209
|
+
static register(name, factory) {
|
|
1210
|
+
customProviders.set(name.toLowerCase(), factory);
|
|
1211
|
+
console.log(`[LLMFactory] Registered custom provider: "${name}"`);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Unregister a previously registered custom provider.
|
|
1215
|
+
*/
|
|
1216
|
+
static unregister(name) {
|
|
1217
|
+
customProviders.delete(name.toLowerCase());
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* List all registered provider names (built-in + custom).
|
|
1221
|
+
*/
|
|
1222
|
+
static listProviders() {
|
|
1223
|
+
return [
|
|
1224
|
+
"openai",
|
|
1225
|
+
"anthropic",
|
|
1226
|
+
"ollama",
|
|
1227
|
+
"gemini",
|
|
1228
|
+
"rest",
|
|
1229
|
+
"universal_rest",
|
|
1230
|
+
"custom",
|
|
1231
|
+
...Array.from(customProviders.keys())
|
|
1232
|
+
];
|
|
1233
|
+
}
|
|
1140
1234
|
static create(llmConfig, embeddingConfig) {
|
|
1141
|
-
var _a;
|
|
1235
|
+
var _a, _b;
|
|
1142
1236
|
switch (llmConfig.provider) {
|
|
1143
1237
|
case "openai":
|
|
1144
1238
|
return new OpenAIProvider(llmConfig, embeddingConfig);
|
|
@@ -1152,11 +1246,19 @@ var LLMFactory = class _LLMFactory {
|
|
|
1152
1246
|
case "universal_rest":
|
|
1153
1247
|
case "custom":
|
|
1154
1248
|
return new UniversalLLMAdapter(llmConfig);
|
|
1155
|
-
default:
|
|
1156
|
-
|
|
1249
|
+
default: {
|
|
1250
|
+
const providerName = String((_a = llmConfig.provider) != null ? _a : "").toLowerCase();
|
|
1251
|
+
const customFactory = customProviders.get(providerName);
|
|
1252
|
+
if (customFactory) {
|
|
1253
|
+
return customFactory(llmConfig);
|
|
1254
|
+
}
|
|
1255
|
+
if (llmConfig.baseUrl || ((_b = llmConfig.options) == null ? void 0 : _b.baseUrl)) {
|
|
1157
1256
|
return new UniversalLLMAdapter(llmConfig);
|
|
1158
1257
|
}
|
|
1159
|
-
throw new Error(
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
`[LLMFactory] Unknown provider "${llmConfig.provider}". Built-in providers: ${_LLMFactory.listProviders().join(", ")}. Register a custom provider with LLMFactory.register().`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1160
1262
|
}
|
|
1161
1263
|
}
|
|
1162
1264
|
static getValidator(provider) {
|
|
@@ -1246,8 +1348,8 @@ var ProviderRegistry = class {
|
|
|
1246
1348
|
}
|
|
1247
1349
|
case "pgvector":
|
|
1248
1350
|
case "postgresql": {
|
|
1249
|
-
const {
|
|
1250
|
-
return
|
|
1351
|
+
const { MultiTablePostgresProvider } = await import("./MultiTablePostgresProvider-YY7LPNJK.mjs");
|
|
1352
|
+
return MultiTablePostgresProvider;
|
|
1251
1353
|
}
|
|
1252
1354
|
case "mongodb": {
|
|
1253
1355
|
const { MongoDBProvider } = await import("./MongoDBProvider-YNKC7EJ6.mjs");
|
|
@@ -2215,69 +2317,40 @@ var QueryProcessor = class {
|
|
|
2215
2317
|
var UITransformer = class {
|
|
2216
2318
|
/**
|
|
2217
2319
|
* Main transformation method
|
|
2218
|
-
* Analyzes user query and retrieved data to determine
|
|
2219
|
-
*
|
|
2220
|
-
* @param userQuery - The original user question/query
|
|
2221
|
-
* @param retrievedData - Vector database retrieval results
|
|
2222
|
-
* @returns Structured JSON response for UI rendering
|
|
2320
|
+
* Analyzes user query and retrieved data to determine if a product carousel is needed.
|
|
2223
2321
|
*/
|
|
2224
|
-
static transform(userQuery, retrievedData) {
|
|
2322
|
+
static transform(userQuery, retrievedData, config, trainedSchema) {
|
|
2225
2323
|
if (!retrievedData || retrievedData.length === 0) {
|
|
2226
2324
|
return this.createTextResponse("No data available", "No relevant data found for your query.");
|
|
2227
2325
|
}
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
case "line_chart":
|
|
2237
|
-
return this.transformToLineChart(retrievedData);
|
|
2238
|
-
case "table":
|
|
2239
|
-
return this.transformToTable(retrievedData);
|
|
2240
|
-
default:
|
|
2241
|
-
return this.transformToText(retrievedData);
|
|
2326
|
+
const isStockRequest = this.isStockQuery(userQuery);
|
|
2327
|
+
const filteredData = isStockRequest ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
|
|
2328
|
+
const categories = this.detectCategories(filteredData);
|
|
2329
|
+
const hasProducts = filteredData.some((item) => this.isProductData(item));
|
|
2330
|
+
const isTimeSeries = filteredData.some((item) => this.isTimeSeriesData(item));
|
|
2331
|
+
const isTrendQuery = this.isTrendQuery(userQuery);
|
|
2332
|
+
if (isTrendQuery && isTimeSeries) {
|
|
2333
|
+
return this.transformToLineChart(filteredData);
|
|
2242
2334
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
* Analyzes the data and query to suggest the best visualization type
|
|
2246
|
-
*/
|
|
2247
|
-
static analyzeData(query, data) {
|
|
2248
|
-
const queryLower = query.toLowerCase();
|
|
2249
|
-
const hasProducts = data.some(
|
|
2250
|
-
(item) => this.isProductData(item)
|
|
2251
|
-
);
|
|
2252
|
-
const hasTimeSeries = data.some(
|
|
2253
|
-
(item) => this.isTimeSeriesData(item)
|
|
2254
|
-
);
|
|
2255
|
-
const hasCategories = this.detectCategories(data).length > 0;
|
|
2256
|
-
const isDistributionQuery = queryLower.includes("distribution") || queryLower.includes("breakdown") || queryLower.includes("percentage") || queryLower.includes("proportion");
|
|
2257
|
-
const isComparisonQuery = queryLower.includes("compare") || queryLower.includes("vs") || queryLower.includes("difference");
|
|
2258
|
-
const isTrendQuery = queryLower.includes("trend") || queryLower.includes("over time") || queryLower.includes("growth") || queryLower.includes("decline");
|
|
2259
|
-
if (hasProducts && !hasTimeSeries && !isDistributionQuery) {
|
|
2260
|
-
return { suggestedType: "product_carousel", confidence: 0.95, hasProducts, hasTimeSeries, hasCategories };
|
|
2335
|
+
if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
|
|
2336
|
+
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
2261
2337
|
}
|
|
2262
|
-
if (
|
|
2263
|
-
return
|
|
2338
|
+
if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
|
|
2339
|
+
return this.transformToPieChart(filteredData);
|
|
2264
2340
|
}
|
|
2265
|
-
if (
|
|
2266
|
-
return
|
|
2341
|
+
if (hasProducts) {
|
|
2342
|
+
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
2267
2343
|
}
|
|
2268
|
-
if (
|
|
2269
|
-
return
|
|
2344
|
+
if (this.hasMultipleFields(filteredData)) {
|
|
2345
|
+
return this.transformToTable(filteredData);
|
|
2270
2346
|
}
|
|
2271
|
-
|
|
2272
|
-
return { suggestedType: "table", confidence: 0.75, hasProducts, hasTimeSeries, hasCategories };
|
|
2273
|
-
}
|
|
2274
|
-
return { suggestedType: "text", confidence: 0.5, hasProducts, hasTimeSeries, hasCategories };
|
|
2347
|
+
return this.transformToText(filteredData);
|
|
2275
2348
|
}
|
|
2276
2349
|
/**
|
|
2277
2350
|
* Transform data to product carousel format
|
|
2278
2351
|
*/
|
|
2279
|
-
static transformToProductCarousel(data) {
|
|
2280
|
-
const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item)).filter((p) => p !== null);
|
|
2352
|
+
static transformToProductCarousel(data, config, trainedSchema) {
|
|
2353
|
+
const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
|
|
2281
2354
|
return {
|
|
2282
2355
|
type: "product_carousel",
|
|
2283
2356
|
title: "Recommended Products",
|
|
@@ -2385,49 +2458,261 @@ var UITransformer = class {
|
|
|
2385
2458
|
data: { content }
|
|
2386
2459
|
};
|
|
2387
2460
|
}
|
|
2461
|
+
static parseTransformationResponse(raw) {
|
|
2462
|
+
const payloadText = this.extractJsonCandidate(raw);
|
|
2463
|
+
if (!payloadText) return null;
|
|
2464
|
+
try {
|
|
2465
|
+
const parsed = JSON.parse(payloadText);
|
|
2466
|
+
return this.normalizeTransformation(parsed);
|
|
2467
|
+
} catch (e) {
|
|
2468
|
+
return null;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
static extractJsonCandidate(raw) {
|
|
2472
|
+
if (!raw) return null;
|
|
2473
|
+
const cleaned = raw.replace(/```(?:json|chart|ui)?\s*/gi, "").replace(/```/g, "").trim();
|
|
2474
|
+
const start = cleaned.indexOf("{");
|
|
2475
|
+
if (start === -1) return null;
|
|
2476
|
+
let depth = 0;
|
|
2477
|
+
let inString = false;
|
|
2478
|
+
let escape = false;
|
|
2479
|
+
for (let i = start; i < cleaned.length; i += 1) {
|
|
2480
|
+
const char = cleaned[i];
|
|
2481
|
+
if (escape) {
|
|
2482
|
+
escape = false;
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
if (char === "\\") {
|
|
2486
|
+
escape = true;
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
if (char === '"') {
|
|
2490
|
+
inString = !inString;
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
if (inString) continue;
|
|
2494
|
+
if (char === "{") depth += 1;
|
|
2495
|
+
if (char === "}") {
|
|
2496
|
+
depth -= 1;
|
|
2497
|
+
if (depth === 0) {
|
|
2498
|
+
return cleaned.slice(start, i + 1);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
static normalizeTransformation(payload) {
|
|
2505
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
2506
|
+
if (!payload || typeof payload !== "object") return null;
|
|
2507
|
+
const payloadObj = payload;
|
|
2508
|
+
const type = this.normalizeVisualizationType(String((_c = (_b = (_a = payloadObj.type) != null ? _a : payloadObj.view) != null ? _b : payloadObj.chartType) != null ? _c : ""));
|
|
2509
|
+
if (!type) return null;
|
|
2510
|
+
const title = String((_e = (_d = payloadObj.title) != null ? _d : payloadObj.heading) != null ? _e : "Visualization");
|
|
2511
|
+
const description = payloadObj.description ? String(payloadObj.description) : void 0;
|
|
2512
|
+
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;
|
|
2513
|
+
const data = type === "text" && typeof rawData === "string" ? { content: rawData } : rawData;
|
|
2514
|
+
const transformation = {
|
|
2515
|
+
type,
|
|
2516
|
+
title,
|
|
2517
|
+
description,
|
|
2518
|
+
data
|
|
2519
|
+
};
|
|
2520
|
+
return this.validateTransformation(transformation) ? transformation : null;
|
|
2521
|
+
}
|
|
2522
|
+
static normalizeVisualizationType(type) {
|
|
2523
|
+
var _a;
|
|
2524
|
+
const mapping = {
|
|
2525
|
+
pie: "pie_chart",
|
|
2526
|
+
pie_chart: "pie_chart",
|
|
2527
|
+
bar: "bar_chart",
|
|
2528
|
+
bar_chart: "bar_chart",
|
|
2529
|
+
line: "line_chart",
|
|
2530
|
+
line_chart: "line_chart",
|
|
2531
|
+
radar: "radar_chart",
|
|
2532
|
+
radar_chart: "radar_chart",
|
|
2533
|
+
table: "table",
|
|
2534
|
+
text: "text",
|
|
2535
|
+
product_carousel: "product_carousel",
|
|
2536
|
+
carousel: "carousel"
|
|
2537
|
+
};
|
|
2538
|
+
return (_a = mapping[type.toLowerCase()]) != null ? _a : null;
|
|
2539
|
+
}
|
|
2540
|
+
static validateTransformation(transformation) {
|
|
2541
|
+
const { type, data } = transformation;
|
|
2542
|
+
switch (type) {
|
|
2543
|
+
case "pie_chart":
|
|
2544
|
+
case "bar_chart":
|
|
2545
|
+
return Array.isArray(data) && data.every(
|
|
2546
|
+
(item) => item !== null && typeof item === "object" && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
2547
|
+
);
|
|
2548
|
+
case "line_chart":
|
|
2549
|
+
return Array.isArray(data) && data.every(
|
|
2550
|
+
(item) => item !== null && typeof item === "object" && "timestamp" in item && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
2551
|
+
);
|
|
2552
|
+
case "radar_chart":
|
|
2553
|
+
return Array.isArray(data) && data.every(
|
|
2554
|
+
(item) => item !== null && typeof item === "object" && "attribute" in item
|
|
2555
|
+
);
|
|
2556
|
+
case "table":
|
|
2557
|
+
return typeof data === "object" && data !== null && Array.isArray(data.columns) && Array.isArray(data.rows);
|
|
2558
|
+
case "text":
|
|
2559
|
+
return typeof data === "object" && data !== null && typeof data.content === "string";
|
|
2560
|
+
case "product_carousel":
|
|
2561
|
+
case "carousel":
|
|
2562
|
+
return Array.isArray(data) && data.every(
|
|
2563
|
+
(item) => item !== null && typeof item === "object" && ("id" in item || "name" in item)
|
|
2564
|
+
);
|
|
2565
|
+
default:
|
|
2566
|
+
return false;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2388
2569
|
/**
|
|
2389
2570
|
* Helper: Check if data item is product-related
|
|
2390
2571
|
*/
|
|
2391
2572
|
static isProductData(item) {
|
|
2392
2573
|
const content = (item.content || "").toLowerCase();
|
|
2393
|
-
const productKeywords = [
|
|
2394
|
-
|
|
2395
|
-
|
|
2574
|
+
const productKeywords = [
|
|
2575
|
+
"product",
|
|
2576
|
+
"price",
|
|
2577
|
+
"stock",
|
|
2578
|
+
"item",
|
|
2579
|
+
"sku",
|
|
2580
|
+
"brand",
|
|
2581
|
+
"model",
|
|
2582
|
+
"msrp",
|
|
2583
|
+
"inventory",
|
|
2584
|
+
"buy",
|
|
2585
|
+
"shop",
|
|
2586
|
+
"beauty",
|
|
2587
|
+
"care",
|
|
2588
|
+
"cosmetic",
|
|
2589
|
+
"facial",
|
|
2590
|
+
"cream",
|
|
2591
|
+
"serum",
|
|
2592
|
+
"mask",
|
|
2593
|
+
"makeup",
|
|
2594
|
+
"fragrance"
|
|
2595
|
+
];
|
|
2596
|
+
const hasKeywords = productKeywords.some((kw) => content.includes(kw));
|
|
2597
|
+
const hasMetadataKey = Object.keys(item.metadata || {}).some(
|
|
2598
|
+
(k) => ["name", "price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase())
|
|
2396
2599
|
);
|
|
2600
|
+
const hasPricePattern = /\$\s*\d+/.test(content);
|
|
2601
|
+
return hasKeywords || hasMetadataKey || hasPricePattern;
|
|
2397
2602
|
}
|
|
2398
2603
|
/**
|
|
2399
2604
|
* Helper: Check if data contains time series
|
|
2400
2605
|
*/
|
|
2401
2606
|
static isTimeSeriesData(item) {
|
|
2402
2607
|
const content = (item.content || "").toLowerCase();
|
|
2403
|
-
const timeKeywords = ["
|
|
2404
|
-
|
|
2608
|
+
const timeKeywords = ["trend", "historical", "growth", "decline", "change", "increase", "decrease"];
|
|
2609
|
+
const hasTimeKeyword = timeKeywords.some((kw) => content.includes(kw));
|
|
2610
|
+
const metadata = item.metadata || {};
|
|
2611
|
+
const maybeDateKeys = Object.keys(metadata).filter(
|
|
2405
2612
|
(k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
|
|
2406
2613
|
);
|
|
2614
|
+
const hasValidDateValue = maybeDateKeys.some((key) => {
|
|
2615
|
+
const value = metadata[key];
|
|
2616
|
+
if (typeof value === "string") {
|
|
2617
|
+
return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
|
|
2618
|
+
}
|
|
2619
|
+
return typeof value === "number";
|
|
2620
|
+
});
|
|
2621
|
+
return hasTimeKeyword || hasValidDateValue;
|
|
2622
|
+
}
|
|
2623
|
+
static shouldShowCategoryChart(query, categories) {
|
|
2624
|
+
if (categories.length < 2) {
|
|
2625
|
+
return false;
|
|
2626
|
+
}
|
|
2627
|
+
const normalized = query.toLowerCase();
|
|
2628
|
+
const chartKeywords = [
|
|
2629
|
+
"distribution",
|
|
2630
|
+
"breakdown",
|
|
2631
|
+
"by category",
|
|
2632
|
+
"by type",
|
|
2633
|
+
"compare",
|
|
2634
|
+
"share",
|
|
2635
|
+
"percentage",
|
|
2636
|
+
"segmentation",
|
|
2637
|
+
"split",
|
|
2638
|
+
"category breakdown",
|
|
2639
|
+
"category distribution"
|
|
2640
|
+
];
|
|
2641
|
+
return chartKeywords.some((keyword) => normalized.includes(keyword));
|
|
2642
|
+
}
|
|
2643
|
+
static isTrendQuery(query) {
|
|
2644
|
+
const normalized = query.toLowerCase();
|
|
2645
|
+
const trendKeywords = [
|
|
2646
|
+
"trend",
|
|
2647
|
+
"over time",
|
|
2648
|
+
"historical",
|
|
2649
|
+
"growth",
|
|
2650
|
+
"decline",
|
|
2651
|
+
"increase",
|
|
2652
|
+
"decrease",
|
|
2653
|
+
"year",
|
|
2654
|
+
"month",
|
|
2655
|
+
"week",
|
|
2656
|
+
"day",
|
|
2657
|
+
"comparison",
|
|
2658
|
+
"compare",
|
|
2659
|
+
"changes",
|
|
2660
|
+
"timeline"
|
|
2661
|
+
];
|
|
2662
|
+
return trendKeywords.some((keyword) => normalized.includes(keyword));
|
|
2663
|
+
}
|
|
2664
|
+
static isStockQuery(query) {
|
|
2665
|
+
const normalized = query.toLowerCase();
|
|
2666
|
+
return normalized.includes("in stock") || normalized.includes("available") || normalized.includes("availability") || normalized.includes("inventory") || normalized.includes("stock status");
|
|
2407
2667
|
}
|
|
2408
2668
|
/**
|
|
2409
|
-
* Helper: Extract
|
|
2669
|
+
* Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
|
|
2410
2670
|
*/
|
|
2411
|
-
static
|
|
2671
|
+
static getDynamicVal(meta, uiKey, config, trainedSchema) {
|
|
2672
|
+
var _a;
|
|
2673
|
+
if (!meta) return void 0;
|
|
2674
|
+
const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
|
|
2675
|
+
if (mapping && mapping[uiKey]) {
|
|
2676
|
+
const mappedKey = mapping[uiKey];
|
|
2677
|
+
if (meta[mappedKey] !== void 0) return meta[mappedKey];
|
|
2678
|
+
}
|
|
2679
|
+
if (trainedSchema && typeof trainedSchema === "object" && trainedSchema !== null) {
|
|
2680
|
+
const trainedKey = trainedSchema[uiKey];
|
|
2681
|
+
if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
|
|
2682
|
+
}
|
|
2683
|
+
const synonyms = this.SYNONYMS[uiKey] || [];
|
|
2684
|
+
const searchKeys = [uiKey, ...synonyms].map((k) => k.toLowerCase());
|
|
2685
|
+
const foundDirectKey = Object.keys(meta).find((k) => searchKeys.includes(k.toLowerCase()));
|
|
2686
|
+
if (foundDirectKey) return meta[foundDirectKey];
|
|
2687
|
+
const foundPartialKey = Object.keys(meta).find((k) => {
|
|
2688
|
+
const keyLow = k.toLowerCase();
|
|
2689
|
+
return searchKeys.some((sk) => keyLow.includes(sk) || sk.includes(keyLow));
|
|
2690
|
+
});
|
|
2691
|
+
return foundPartialKey ? meta[foundPartialKey] : void 0;
|
|
2692
|
+
}
|
|
2693
|
+
static extractProductInfo(item, config, trainedSchema) {
|
|
2412
2694
|
const meta = item.metadata || {};
|
|
2413
|
-
|
|
2695
|
+
const name = this.getDynamicVal(meta, "name", config, trainedSchema);
|
|
2696
|
+
const price = this.getDynamicVal(meta, "price", config, trainedSchema);
|
|
2697
|
+
const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
|
|
2698
|
+
if (name || this.isProductData(item)) {
|
|
2699
|
+
let finalName = name ? String(name) : void 0;
|
|
2700
|
+
if (!finalName) {
|
|
2701
|
+
const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
2702
|
+
finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
|
|
2703
|
+
}
|
|
2704
|
+
let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
|
|
2705
|
+
if (!finalPrice) {
|
|
2706
|
+
const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
|
|
2707
|
+
if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
|
|
2708
|
+
}
|
|
2709
|
+
const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
|
|
2414
2710
|
return {
|
|
2415
2711
|
id: item.id,
|
|
2416
|
-
name:
|
|
2417
|
-
price:
|
|
2418
|
-
image:
|
|
2419
|
-
brand:
|
|
2420
|
-
description: item.content,
|
|
2421
|
-
inStock: this.determineStockStatus(item)
|
|
2422
|
-
};
|
|
2423
|
-
}
|
|
2424
|
-
const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
2425
|
-
const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d.]+)/i);
|
|
2426
|
-
if (nameMatch) {
|
|
2427
|
-
return {
|
|
2428
|
-
id: item.id,
|
|
2429
|
-
name: nameMatch[1],
|
|
2430
|
-
price: priceMatch ? parseFloat(priceMatch[1]) : void 0,
|
|
2712
|
+
name: finalName,
|
|
2713
|
+
price: finalPrice,
|
|
2714
|
+
image: typeof imageValue === "string" ? imageValue : void 0,
|
|
2715
|
+
brand: brand ? String(brand) : void 0,
|
|
2431
2716
|
description: item.content,
|
|
2432
2717
|
inStock: this.determineStockStatus(item)
|
|
2433
2718
|
};
|
|
@@ -2451,6 +2736,10 @@ var UITransformer = class {
|
|
|
2451
2736
|
const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
|
|
2452
2737
|
tags.forEach((t) => categories.add(String(t)));
|
|
2453
2738
|
}
|
|
2739
|
+
const contentCategories = Array.from(new Set(
|
|
2740
|
+
Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
|
|
2741
|
+
));
|
|
2742
|
+
contentCategories.forEach((category) => categories.add(category));
|
|
2454
2743
|
const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
|
|
2455
2744
|
if (categoryMatch) {
|
|
2456
2745
|
categories.add(categoryMatch[1].trim());
|
|
@@ -2571,6 +2860,225 @@ var UITransformer = class {
|
|
|
2571
2860
|
});
|
|
2572
2861
|
return fieldCount.size > 2;
|
|
2573
2862
|
}
|
|
2863
|
+
// ─── LLM-Driven Visualization Decision ────────────────────────────────────
|
|
2864
|
+
/**
|
|
2865
|
+
* analyzeAndDecide — sends user question + RAG data to the LLM with a
|
|
2866
|
+
* structured system prompt and parses the JSON response into a
|
|
2867
|
+
* UITransformationResponse.
|
|
2868
|
+
*
|
|
2869
|
+
* This is the recommended entry point for production use. The heuristic
|
|
2870
|
+
* `transform()` method is used as a fallback if the LLM call fails.
|
|
2871
|
+
*
|
|
2872
|
+
* System prompt instructs the LLM to:
|
|
2873
|
+
* - Analyze the question and retrieved data
|
|
2874
|
+
* - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
|
|
2875
|
+
* - Return a strict JSON object — no prose, no markdown fences
|
|
2876
|
+
*
|
|
2877
|
+
* @param query - the original user question
|
|
2878
|
+
* @param sources - vector DB matches returned by RAG retrieval
|
|
2879
|
+
* @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
|
|
2880
|
+
* @returns - a validated UITransformationResponse (type + title + description + data)
|
|
2881
|
+
*/
|
|
2882
|
+
static async analyzeAndDecide(query, sources, llm) {
|
|
2883
|
+
try {
|
|
2884
|
+
const context = this.buildContextSummary(sources);
|
|
2885
|
+
const systemPrompt = this.buildVisualizationSystemPrompt();
|
|
2886
|
+
const userPrompt = [
|
|
2887
|
+
`USER QUESTION: ${query}`,
|
|
2888
|
+
"",
|
|
2889
|
+
"RETRIEVED DATA (JSON):",
|
|
2890
|
+
context
|
|
2891
|
+
].join("\n");
|
|
2892
|
+
const rawResponse = await llm.chat(
|
|
2893
|
+
[{ role: "user", content: userPrompt }],
|
|
2894
|
+
"",
|
|
2895
|
+
{ systemPrompt, temperature: 0 }
|
|
2896
|
+
);
|
|
2897
|
+
const parsed = this.parseTransformationResponse(rawResponse);
|
|
2898
|
+
if (parsed) {
|
|
2899
|
+
console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
|
|
2900
|
+
return parsed;
|
|
2901
|
+
}
|
|
2902
|
+
console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
|
|
2903
|
+
} catch (err) {
|
|
2904
|
+
console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
|
|
2905
|
+
}
|
|
2906
|
+
return this.transform(query, sources);
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Build the system prompt that instructs the LLM to return a visualization JSON.
|
|
2910
|
+
*/
|
|
2911
|
+
static buildVisualizationSystemPrompt() {
|
|
2912
|
+
return `You are a data visualization expert embedded in a RAG chat system.
|
|
2913
|
+
You will receive a user question and structured data retrieved from a vector database.
|
|
2914
|
+
Your ONLY job is to analyze this information and return a single JSON object that tells
|
|
2915
|
+
the frontend how to visualize it.
|
|
2916
|
+
|
|
2917
|
+
Return ONLY a valid JSON object \u2014 no markdown code fences, no explanation, no prose.
|
|
2918
|
+
|
|
2919
|
+
The JSON must have this exact shape:
|
|
2920
|
+
{
|
|
2921
|
+
"type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
|
|
2922
|
+
"title": "A concise, descriptive title for the visualization",
|
|
2923
|
+
"description": "One sentence describing what the visualization shows",
|
|
2924
|
+
"data": <structured data \u2014 see rules below>
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
DATA SHAPE per type:
|
|
2928
|
+
- bar_chart: array of { "category": string, "value": number }
|
|
2929
|
+
- line_chart: array of { "timestamp": string, "value": number, "label": string }
|
|
2930
|
+
- pie_chart: array of { "label": string, "value": number }
|
|
2931
|
+
- radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
|
|
2932
|
+
Example for radar_chart:
|
|
2933
|
+
[
|
|
2934
|
+
{ "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
|
|
2935
|
+
{ "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
|
|
2936
|
+
{ "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
|
|
2937
|
+
]
|
|
2938
|
+
- table: { "columns": string[], "rows": (string|number)[][] }
|
|
2939
|
+
- text: { "content": "<prose answer>" }
|
|
2940
|
+
|
|
2941
|
+
DECISION RULES (follow strictly):
|
|
2942
|
+
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.
|
|
2943
|
+
2. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
|
|
2944
|
+
3. pie_chart \u2192 proportional breakdown or percentage distribution
|
|
2945
|
+
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.
|
|
2946
|
+
5. table \u2192 multi-field structured records where each item has \u2265 3 attributes
|
|
2947
|
+
6. text \u2192 conversational or free-form answers where no chart adds value
|
|
2948
|
+
|
|
2949
|
+
IMPORTANT:
|
|
2950
|
+
- Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
|
|
2951
|
+
- Ensure all "value" fields are numbers, not strings.
|
|
2952
|
+
- For bar/line/pie, keep at most 12 data points for readability.
|
|
2953
|
+
- Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Serialize retrieved vector matches into a compact JSON context string.
|
|
2957
|
+
* Limits the total character count to avoid exceeding LLM context windows.
|
|
2958
|
+
*/
|
|
2959
|
+
static buildContextSummary(sources, maxChars = 6e3) {
|
|
2960
|
+
const items = sources.map((s, i) => {
|
|
2961
|
+
var _a, _b, _c, _d;
|
|
2962
|
+
return {
|
|
2963
|
+
index: i + 1,
|
|
2964
|
+
content: (_b = (_a = s.content) == null ? void 0 : _a.substring(0, 400)) != null ? _b : "",
|
|
2965
|
+
metadata: (_c = s.metadata) != null ? _c : {},
|
|
2966
|
+
score: (_d = s.score) != null ? _d : 0
|
|
2967
|
+
};
|
|
2968
|
+
});
|
|
2969
|
+
const full = JSON.stringify(items, null, 2);
|
|
2970
|
+
if (full.length <= maxChars) return full;
|
|
2971
|
+
const partial = [];
|
|
2972
|
+
let chars = 2;
|
|
2973
|
+
for (const item of items) {
|
|
2974
|
+
const chunk = JSON.stringify(item);
|
|
2975
|
+
if (chars + chunk.length + 2 > maxChars) break;
|
|
2976
|
+
partial.push(item);
|
|
2977
|
+
chars += chunk.length + 2;
|
|
2978
|
+
}
|
|
2979
|
+
return JSON.stringify(partial, null, 2) + "\n// ... truncated";
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
/**
|
|
2983
|
+
* Central dictionary of common synonyms for UI properties.
|
|
2984
|
+
* This allows the system to be schema-agnostic by guessing field names.
|
|
2985
|
+
*/
|
|
2986
|
+
UITransformer.SYNONYMS = {
|
|
2987
|
+
name: ["product", "item", "title", "label", "heading", "subject"],
|
|
2988
|
+
price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd"],
|
|
2989
|
+
brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
|
|
2990
|
+
image: ["imageUrl", "thumbnail", "img", "url", "photo", "picture", "media", "image_url", "main_image", "product_image", "thumb"],
|
|
2991
|
+
stock: ["inventory", "quantity", "count", "availability", "stock_level", "inStock", "is_available"],
|
|
2992
|
+
description: ["summary", "content", "body", "text", "info", "details"]
|
|
2993
|
+
};
|
|
2994
|
+
|
|
2995
|
+
// src/utils/SchemaMapper.ts
|
|
2996
|
+
var SchemaMapper = class {
|
|
2997
|
+
/**
|
|
2998
|
+
* Trains the plugin on a set of keys.
|
|
2999
|
+
* This is done once per schema and cached.
|
|
3000
|
+
*/
|
|
3001
|
+
static async train(llm, projectId, keys) {
|
|
3002
|
+
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
3003
|
+
if (this.cache.has(cacheKey)) {
|
|
3004
|
+
return this.cache.get(cacheKey);
|
|
3005
|
+
}
|
|
3006
|
+
console.log(`[SchemaMapper] \u{1F9E0} Training AI on new schema keys: ${keys.join(", ")}`);
|
|
3007
|
+
const propertyList = Object.entries(this.TARGET_PROPERTIES).map(([prop, desc]) => `- ${prop} (${desc})`).join("\n");
|
|
3008
|
+
const prompt = `
|
|
3009
|
+
Given these metadata keys from a database: [${keys.join(", ")}]
|
|
3010
|
+
|
|
3011
|
+
Identify which keys best correspond to these standard UI properties:
|
|
3012
|
+
${propertyList}
|
|
3013
|
+
|
|
3014
|
+
Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
|
|
3015
|
+
If no good match is found for a property, omit it.
|
|
3016
|
+
|
|
3017
|
+
Example:
|
|
3018
|
+
{
|
|
3019
|
+
"name": "Product_Title",
|
|
3020
|
+
"price": "MSRP_USD",
|
|
3021
|
+
"brand": "VendorName"
|
|
3022
|
+
}
|
|
3023
|
+
`;
|
|
3024
|
+
try {
|
|
3025
|
+
const response = await llm.chat(
|
|
3026
|
+
[
|
|
3027
|
+
{ role: "system", content: "You are a database schema expert. You ONLY respond with valid JSON." },
|
|
3028
|
+
{ role: "user", content: prompt }
|
|
3029
|
+
],
|
|
3030
|
+
""
|
|
3031
|
+
);
|
|
3032
|
+
const startIdx = response.indexOf("{");
|
|
3033
|
+
if (startIdx !== -1) {
|
|
3034
|
+
let braceCount = 0;
|
|
3035
|
+
let endIdx = -1;
|
|
3036
|
+
for (let i = startIdx; i < response.length; i++) {
|
|
3037
|
+
if (response[i] === "{") braceCount++;
|
|
3038
|
+
else if (response[i] === "}") {
|
|
3039
|
+
braceCount--;
|
|
3040
|
+
if (braceCount === 0) {
|
|
3041
|
+
endIdx = i;
|
|
3042
|
+
break;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
if (endIdx !== -1) {
|
|
3047
|
+
const jsonContent = response.substring(startIdx, endIdx + 1);
|
|
3048
|
+
const cleanJson = this.sanitizeJson(jsonContent);
|
|
3049
|
+
const mapping = JSON.parse(cleanJson);
|
|
3050
|
+
this.cache.set(cacheKey, mapping);
|
|
3051
|
+
return mapping;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
} catch (error) {
|
|
3055
|
+
console.warn("[SchemaMapper] AI training failed, falling back to heuristics:", error);
|
|
3056
|
+
}
|
|
3057
|
+
return {};
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Forgiving JSON parser that fixes common AI formatting mistakes.
|
|
3061
|
+
*/
|
|
3062
|
+
static sanitizeJson(s) {
|
|
3063
|
+
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();
|
|
3064
|
+
}
|
|
3065
|
+
static getCached(projectId, keys) {
|
|
3066
|
+
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
3067
|
+
return this.cache.get(cacheKey);
|
|
3068
|
+
}
|
|
3069
|
+
};
|
|
3070
|
+
SchemaMapper.cache = /* @__PURE__ */ new Map();
|
|
3071
|
+
/**
|
|
3072
|
+
* Descriptions of standard UI properties to help the AI map fields accurately.
|
|
3073
|
+
*/
|
|
3074
|
+
SchemaMapper.TARGET_PROPERTIES = {
|
|
3075
|
+
name: "The primary title, name, or label of the item",
|
|
3076
|
+
price: "The numeric cost, price, MSRP, or amount",
|
|
3077
|
+
brand: "The manufacturer, vendor, brand, or maker",
|
|
3078
|
+
image: "The URL to an image, thumbnail, photo, or picture",
|
|
3079
|
+
stock: "The availability, inventory count, quantity, or stock level",
|
|
3080
|
+
description: "The detailed text content, summary, or body info",
|
|
3081
|
+
category: "The group, department, type, or category name"
|
|
2574
3082
|
};
|
|
2575
3083
|
|
|
2576
3084
|
// src/core/Pipeline.ts
|
|
@@ -2619,205 +3127,31 @@ var Pipeline = class {
|
|
|
2619
3127
|
}
|
|
2620
3128
|
this.reranker = new Reranker();
|
|
2621
3129
|
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Expose the underlying LLM provider (set after initialize()).
|
|
3132
|
+
* Used by the stream handler to pass to UITransformer.analyzeAndDecide().
|
|
3133
|
+
*/
|
|
3134
|
+
getLLMProvider() {
|
|
3135
|
+
return this.initialised ? this.llmProvider : void 0;
|
|
3136
|
+
}
|
|
2622
3137
|
async initialize() {
|
|
2623
|
-
var _a
|
|
3138
|
+
var _a;
|
|
2624
3139
|
if (this.initialised) return;
|
|
2625
|
-
const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
|
|
2626
3140
|
const chartInstruction = `
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
###
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
- "explore" \u2192 user wants to browse items (e.g., "show me products")
|
|
2640
|
-
- "analyze" \u2192 user wants aggregation/distribution (e.g., "distribution of products")
|
|
2641
|
-
- "compare" \u2192 user wants side-by-side comparison
|
|
2642
|
-
- "detail" \u2192 user wants structured/tabular data
|
|
2643
|
-
|
|
2644
|
-
---
|
|
2645
|
-
|
|
2646
|
-
## 2. VIEW SELECTION (DYNAMIC)
|
|
2647
|
-
|
|
2648
|
-
Choose the best primary "view" ("carousel", "chart", or "table") based on the data:
|
|
2649
|
-
- TRENDS/GROWTH \u2192 "chart" (type: "line")
|
|
2650
|
-
- DISTRIBUTIONS/RATIOS \u2192 "chart" (type: "pie")
|
|
2651
|
-
- COMPARISONS \u2192 "chart" (type: "bar")
|
|
2652
|
-
- PRODUCT LISTS \u2192 "carousel"
|
|
2653
|
-
- RAW DATA/SPECS \u2192 "table"
|
|
2654
|
-
|
|
2655
|
-
\u26A0\uFE0F Dynamic Overrides:
|
|
2656
|
-
- If query is "show products", ALWAYS use carousel.
|
|
2657
|
-
- If data is statistical, ALWAYS use a chart.
|
|
2658
|
-
- NEVER explain why you chose a view.
|
|
2659
|
-
|
|
2660
|
-
---
|
|
2661
|
-
|
|
2662
|
-
## 3. RESPONSE FORMAT (STRICT JSON ONLY INSIDE BLOCK)
|
|
2663
|
-
|
|
2664
|
-
\`\`\`ui
|
|
2665
|
-
{
|
|
2666
|
-
"view": "carousel" | "chart" | "table",
|
|
2667
|
-
"title": "string",
|
|
2668
|
-
"description": "string",
|
|
2669
|
-
"metadata": {
|
|
2670
|
-
"intent": "explore" | "analyze" | "compare" | "detail",
|
|
2671
|
-
"confidence": 0.0-1.0
|
|
2672
|
-
},
|
|
2673
|
-
|
|
2674
|
-
// CHART ONLY
|
|
2675
|
-
"chart": {
|
|
2676
|
-
"type": "pie" | "bar" | "line",
|
|
2677
|
-
"xKey": "label",
|
|
2678
|
-
"yKeys": ["value"],
|
|
2679
|
-
"data": []
|
|
2680
|
-
},
|
|
2681
|
-
|
|
2682
|
-
// TABLE ONLY
|
|
2683
|
-
"table": {
|
|
2684
|
-
"columns": [],
|
|
2685
|
-
"rows": []
|
|
2686
|
-
},
|
|
2687
|
-
|
|
2688
|
-
// CAROUSEL ONLY
|
|
2689
|
-
"carousel": {
|
|
2690
|
-
"items": []
|
|
2691
|
-
},
|
|
2692
|
-
|
|
2693
|
-
"insights": []
|
|
2694
|
-
}
|
|
2695
|
-
\`\`\`
|
|
2696
|
-
|
|
2697
|
-
---
|
|
2698
|
-
|
|
2699
|
-
## 4. DATA CONTRACT (CRITICAL)
|
|
2700
|
-
|
|
2701
|
-
### \u{1F539} CAROUSEL ITEM FORMAT
|
|
2702
|
-
{
|
|
2703
|
-
"id": "string",
|
|
2704
|
-
"name": "string",
|
|
2705
|
-
"brand": "string",
|
|
2706
|
-
"price": number,
|
|
2707
|
-
"image": "url",
|
|
2708
|
-
"link": "url",
|
|
2709
|
-
"inStock": boolean
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
### \u{1F539} CHART DATA FORMAT
|
|
2713
|
-
{
|
|
2714
|
-
"label": "string",
|
|
2715
|
-
"value": number,
|
|
2716
|
-
"inStockCount": number (optional),
|
|
2717
|
-
"outOfStockCount": number (optional)
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
### \u{1F539} TABLE FORMAT
|
|
2721
|
-
- columns MUST match row keys
|
|
2722
|
-
- rows MUST be flat objects
|
|
2723
|
-
|
|
2724
|
-
---
|
|
2725
|
-
|
|
2726
|
-
## 5. DATA RULES
|
|
2727
|
-
|
|
2728
|
-
- NEVER hallucinate fields or data.
|
|
2729
|
-
- ONLY use data provided in the retrieved context.
|
|
2730
|
-
- STRICT DATA ISOLATION: The labels and numbers in the EXAMPLES section below are placeholders. NEVER use them.
|
|
2731
|
-
- IF NO DATA IS FOUND: You MUST NOT generate a chart. You MUST NOT say "This example assumes" or provide hypothetical numbers. Simply state "No relevant data found in the database." and STOP.
|
|
2732
|
-
- NO META-COMMENTARY: NEVER talk about the task itself (e.g., "I've used JSON", "In a real-world app", "Dedicated library").
|
|
2733
|
-
- NEVER explain YOUR formatting choice.
|
|
2734
|
-
- Aggregate BEFORE rendering chart.
|
|
2735
|
-
- DYNAMIC INTELLIGENCE: Autonomously extract relevant categories/values from the context and calculate totals for the "data" array based on the user query.
|
|
2736
|
-
|
|
2737
|
-
---
|
|
2738
|
-
|
|
2739
|
-
## 6. SMART RULES (IMPORTANT)
|
|
2740
|
-
|
|
2741
|
-
- If user asks BOTH:
|
|
2742
|
-
"show products AND distribution"
|
|
2743
|
-
\u2192 PRIORITIZE "explore" \u2192 carousel
|
|
2744
|
-
|
|
2745
|
-
- If dataset size > 20:
|
|
2746
|
-
\u2192 aggregate \u2192 chart or table
|
|
2747
|
-
|
|
2748
|
-
---
|
|
2749
|
-
|
|
2750
|
-
## 7. OUTPUT RULES
|
|
2751
|
-
|
|
2752
|
-
- ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block.
|
|
2753
|
-
- JSON must be VALID.
|
|
2754
|
-
- FIELD MAPPING (MANDATORY): Always use "label" for the name/category and "value" for the numeric count.
|
|
2755
|
-
- NO xKey or yKeys: Do not include these fields; the UI uses "label" and "value" by default.
|
|
2756
|
-
- SILENT DATA: If a chart is provided, DO NOT say "Here is the JSON" or "Below is the data". Simply output the block.
|
|
2757
|
-
- TEXT OUTSIDE THE BLOCK: One brief sentence maximum.
|
|
2758
|
-
|
|
2759
|
-
---
|
|
2760
|
-
|
|
2761
|
-
## 8. EXAMPLES
|
|
2762
|
-
|
|
2763
|
-
User: "show me beauty products"
|
|
2764
|
-
|
|
2765
|
-
\`\`\`ui
|
|
2766
|
-
{
|
|
2767
|
-
"view": "carousel",
|
|
2768
|
-
"title": "TITLE",
|
|
2769
|
-
"description": "DESCRIPTION",
|
|
2770
|
-
"metadata": {
|
|
2771
|
-
"intent": "explore",
|
|
2772
|
-
"confidence": 0.95
|
|
2773
|
-
},
|
|
2774
|
-
"carousel": {
|
|
2775
|
-
"items": [
|
|
2776
|
-
{
|
|
2777
|
-
"id": "ID",
|
|
2778
|
-
"name": "PRODUCT_NAME",
|
|
2779
|
-
"brand": "BRAND",
|
|
2780
|
-
"price": 0,
|
|
2781
|
-
"image": "URL",
|
|
2782
|
-
"link": "URL",
|
|
2783
|
-
"inStock": true
|
|
2784
|
-
}
|
|
2785
|
-
]
|
|
2786
|
-
},
|
|
2787
|
-
"insights": ["INSIGHT"]
|
|
2788
|
-
}
|
|
2789
|
-
\`\`\`
|
|
2790
|
-
|
|
2791
|
-
---
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
\`\`\`ui
|
|
2796
|
-
{
|
|
2797
|
-
"view": "chart",
|
|
2798
|
-
"title": "TITLE",
|
|
2799
|
-
"description": "DESCRIPTION",
|
|
2800
|
-
"metadata": {
|
|
2801
|
-
"intent": "analyze",
|
|
2802
|
-
"confidence": 0.98
|
|
2803
|
-
},
|
|
2804
|
-
"chart": {
|
|
2805
|
-
"type": "pie",
|
|
2806
|
-
"data": [
|
|
2807
|
-
{ "label": "LABEL_A", "value": 0, "inStockCount": 0, "outOfStockCount": 0 },
|
|
2808
|
-
{ "label": "LABEL_B", "value": 0, "inStockCount": 0, "outOfStockCount": 0 }
|
|
2809
|
-
]
|
|
2810
|
-
},
|
|
2811
|
-
"insights": ["INSIGHT"]
|
|
2812
|
-
}
|
|
2813
|
-
\`\`\`
|
|
3141
|
+
You are a helpful product assistant. Use the provided context to answer questions accurately.
|
|
3142
|
+
|
|
3143
|
+
### UI STYLE RULES (CRITICAL):
|
|
3144
|
+
- NEVER generate markdown tables. If you do, the UI will break.
|
|
3145
|
+
- NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
|
|
3146
|
+
- NEVER generate text-based charts or graphs.
|
|
3147
|
+
- ONLY use plain text and bullet points.
|
|
3148
|
+
|
|
3149
|
+
### PRODUCT DISPLAY:
|
|
3150
|
+
- When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
|
|
3151
|
+
- The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
|
|
3152
|
+
- Do NOT try to format product lists as tables.
|
|
2814
3153
|
`;
|
|
2815
|
-
|
|
2816
|
-
let cleanPrompt = this.config.llm.systemPrompt || "";
|
|
2817
|
-
cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, "");
|
|
2818
|
-
cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, "");
|
|
2819
|
-
this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
|
|
2820
|
-
}
|
|
3154
|
+
this.config.llm.systemPrompt = chartInstruction;
|
|
2821
3155
|
console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
|
|
2822
3156
|
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
2823
3157
|
const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
|
|
@@ -2832,7 +3166,7 @@ User: "show me beauty products"
|
|
|
2832
3166
|
this.entityExtractor = new EntityExtractor(this.llmProvider);
|
|
2833
3167
|
}
|
|
2834
3168
|
await this.vectorDB.initialize();
|
|
2835
|
-
if (((
|
|
3169
|
+
if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic") {
|
|
2836
3170
|
this.agent = new LangChainAgent(this, this.config);
|
|
2837
3171
|
await this.agent.initialize(this.llmProvider);
|
|
2838
3172
|
}
|
|
@@ -3035,7 +3369,12 @@ ${graphContext}
|
|
|
3035
3369
|
VECTOR CONTEXT:
|
|
3036
3370
|
${context}`;
|
|
3037
3371
|
}
|
|
3038
|
-
const
|
|
3372
|
+
const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
|
|
3373
|
+
const trainingPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys) : Promise.resolve(void 0);
|
|
3374
|
+
const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
|
|
3375
|
+
const hardenedHistory = [...history];
|
|
3376
|
+
const userQuestion = { role: "user", content: question + restrictionSuffix };
|
|
3377
|
+
const messages = [...hardenedHistory, userQuestion];
|
|
3039
3378
|
if (this.llmProvider.chatStream) {
|
|
3040
3379
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
3041
3380
|
if (!stream) {
|
|
@@ -3060,7 +3399,8 @@ ${context}`;
|
|
|
3060
3399
|
const reply = yield new __await(this.llmProvider.chat(messages, context));
|
|
3061
3400
|
yield reply;
|
|
3062
3401
|
}
|
|
3063
|
-
const
|
|
3402
|
+
const trainedSchema = yield new __await(trainingPromise);
|
|
3403
|
+
const uiTransformation = yield new __await(this.generateUiTransformation(question, sources, context, trainedSchema));
|
|
3064
3404
|
yield {
|
|
3065
3405
|
reply: "",
|
|
3066
3406
|
sources,
|
|
@@ -3076,6 +3416,17 @@ ${context}`;
|
|
|
3076
3416
|
* Universal retrieval method combining all enabled providers.
|
|
3077
3417
|
* Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
|
|
3078
3418
|
*/
|
|
3419
|
+
async generateUiTransformation(question, sources, _context, trainedSchema) {
|
|
3420
|
+
if (!sources || sources.length === 0) {
|
|
3421
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
3422
|
+
}
|
|
3423
|
+
try {
|
|
3424
|
+
return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
console.warn("[Pipeline] generateUiTransformation failed, using heuristic fallback:", err);
|
|
3427
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3079
3430
|
async retrieve(query, options) {
|
|
3080
3431
|
var _a, _b, _c;
|
|
3081
3432
|
const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
|
|
@@ -3278,6 +3629,14 @@ var VectorPlugin = class {
|
|
|
3278
3629
|
getConfig() {
|
|
3279
3630
|
return this.config;
|
|
3280
3631
|
}
|
|
3632
|
+
/**
|
|
3633
|
+
* Get the initialized LLM provider (available after the first request).
|
|
3634
|
+
* Used by handlers to pass to UITransformer.analyzeAndDecide() for
|
|
3635
|
+
* LLM-driven visualization decisions.
|
|
3636
|
+
*/
|
|
3637
|
+
getLLMProvider() {
|
|
3638
|
+
return this.pipeline.getLLMProvider();
|
|
3639
|
+
}
|
|
3281
3640
|
/**
|
|
3282
3641
|
* Perform pre-flight health checks on all configured providers.
|
|
3283
3642
|
* Useful to verify connectivity before running operations.
|
|
@@ -3399,7 +3758,7 @@ function sseMetaFrame(meta) {
|
|
|
3399
3758
|
`;
|
|
3400
3759
|
}
|
|
3401
3760
|
function sseUIFrame(uiTransformation) {
|
|
3402
|
-
return `data: ${JSON.stringify(
|
|
3761
|
+
return `data: ${JSON.stringify({ type: "ui_transformation", data: uiTransformation })}
|
|
3403
3762
|
|
|
3404
3763
|
`;
|
|
3405
3764
|
}
|
|
@@ -3454,6 +3813,7 @@ function createStreamHandler(configOrPlugin) {
|
|
|
3454
3813
|
const encoder = new TextEncoder();
|
|
3455
3814
|
const stream = new ReadableStream({
|
|
3456
3815
|
async start(controller) {
|
|
3816
|
+
var _a, _b;
|
|
3457
3817
|
const enqueue = (text) => controller.enqueue(encoder.encode(text));
|
|
3458
3818
|
try {
|
|
3459
3819
|
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
@@ -3464,13 +3824,22 @@ function createStreamHandler(configOrPlugin) {
|
|
|
3464
3824
|
enqueue(sseTextFrame(chunk));
|
|
3465
3825
|
} else {
|
|
3466
3826
|
enqueue(sseMetaFrame(chunk));
|
|
3467
|
-
const
|
|
3468
|
-
|
|
3827
|
+
const responseChunk = chunk;
|
|
3828
|
+
const sources = (responseChunk == null ? void 0 : responseChunk.sources) || [];
|
|
3829
|
+
if (sources.length > 0) {
|
|
3469
3830
|
try {
|
|
3470
|
-
const
|
|
3471
|
-
|
|
3831
|
+
const llmProvider = (_a = plugin.getLLMProvider) == null ? void 0 : _a.call(plugin);
|
|
3832
|
+
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());
|
|
3833
|
+
if (uiTransformation) {
|
|
3834
|
+
enqueue(sseUIFrame(uiTransformation));
|
|
3835
|
+
}
|
|
3472
3836
|
} catch (transformError) {
|
|
3473
3837
|
console.warn("[createStreamHandler] UI transformation warning:", transformError);
|
|
3838
|
+
try {
|
|
3839
|
+
const fallback = UITransformer.transform(message, sources, plugin.getConfig());
|
|
3840
|
+
if (fallback) enqueue(sseUIFrame(fallback));
|
|
3841
|
+
} catch (e) {
|
|
3842
|
+
}
|
|
3474
3843
|
}
|
|
3475
3844
|
}
|
|
3476
3845
|
}
|