@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.
Files changed (46) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BfRgI1Xh.d.mts} +58 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BfRgI1Xh.d.ts} +58 -1
  5. package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +8 -0
  6. package/dist/{chunk-PV3MFHWU.mjs → chunk-BFYLQYQU.mjs} +808 -439
  7. package/dist/chunk-R3RGUMHE.mjs +218 -0
  8. package/dist/handlers/index.d.mts +2 -2
  9. package/dist/handlers/index.d.ts +2 -2
  10. package/dist/handlers/index.js +995 -603
  11. package/dist/handlers/index.mjs +1 -1
  12. package/dist/{index-Bb2yEopi.d.mts → index-1Z4GuYBi.d.ts} +7 -1
  13. package/dist/{index-CkbTzj9J.d.ts → index-BV0z5mb6.d.mts} +7 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +491 -316
  17. package/dist/index.mjs +411 -217
  18. package/dist/server.d.mts +35 -5
  19. package/dist/server.d.ts +35 -5
  20. package/dist/server.js +1146 -777
  21. package/dist/server.mjs +163 -176
  22. package/package.json +4 -2
  23. package/src/app/page.tsx +1 -2
  24. package/src/components/MessageBubble.tsx +211 -69
  25. package/src/components/ProductCard.tsx +2 -2
  26. package/src/components/VisualizationRenderer.tsx +347 -247
  27. package/src/config/RagConfig.ts +5 -0
  28. package/src/config/serverConfig.ts +3 -1
  29. package/src/core/Pipeline.ts +65 -206
  30. package/src/core/ProviderRegistry.ts +2 -2
  31. package/src/core/VectorPlugin.ts +9 -0
  32. package/src/handlers/index.ts +23 -7
  33. package/src/hooks/useRagChat.ts +2 -2
  34. package/src/llm/LLMFactory.ts +54 -2
  35. package/src/llm/providers/AnthropicProvider.ts +12 -8
  36. package/src/llm/providers/GeminiProvider.ts +188 -143
  37. package/src/llm/providers/OllamaProvider.ts +7 -3
  38. package/src/llm/providers/OpenAIProvider.ts +12 -8
  39. package/src/types/chat.ts +6 -0
  40. package/src/types/index.ts +80 -0
  41. package/src/utils/SchemaMapper.ts +129 -0
  42. package/src/utils/UITransformer.ts +491 -181
  43. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  44. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  45. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  46. 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 = (_o = readString(env, "PGVECTOR_CONNECTION_STRING")) != null ? _o : (_n = (_m = base == null ? void 0 : base.vectorDb) == null ? void 0 : _m.options) == null ? void 0 : _n.connectionString) != null ? _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 = (_t = (_s = readString(env, "MONGODB_URI")) != null ? _s : (_r = (_q = base == null ? void 0 : base.vectorDb) == null ? void 0 : _q.options) == null ? void 0 : _r.uri) != null ? _t : "";
101
- vectorDbOptions.database = (_x = (_w = readString(env, "MONGODB_DB")) != null ? _w : (_v = (_u = base == null ? void 0 : base.vectorDb) == null ? void 0 : _u.options) == null ? void 0 : _v.database) != null ? _x : "";
102
- vectorDbOptions.collection = (_B = (_A = readString(env, "MONGODB_COLLECTION")) != null ? _A : (_z = (_y = base == null ? void 0 : base.vectorDb) == null ? void 0 : _y.options) == null ? void 0 : _z.collection) != null ? _B : "";
103
- vectorDbOptions.indexName = (_H = (_G = (_D = readString(env, "MONGODB_INDEX_NAME")) != null ? _D : (_C = base == null ? void 0 : base.vectorDb) == null ? void 0 : _C.indexName) != null ? _G : (_F = (_E = base == null ? void 0 : base.vectorDb) == null ? void 0 : _E.options) == null ? void 0 : _F.indexName) != null ? _H : "vector_index";
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 = (_L = (_K = readString(env, "QDRANT_URL")) != null ? _K : (_J = (_I = base == null ? void 0 : base.vectorDb) == null ? void 0 : _I.options) == null ? void 0 : _J.baseUrl) != null ? _L : "http://localhost:6333";
106
- vectorDbOptions.apiKey = (_O = readString(env, "QDRANT_API_KEY")) != null ? _O : (_N = (_M = base == null ? void 0 : base.vectorDb) == null ? void 0 : _M.options) == null ? void 0 : _N.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 = (_P = readString(env, "MILVUS_URL")) != null ? _P : "http://localhost:19530";
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 = (_Q = readString(env, "CHROMADB_URL")) != null ? _Q : "http://localhost:8000";
114
+ vectorDbOptions.baseUrl = (__ = readString(env, "CHROMADB_URL")) != null ? __ : "http://localhost:8000";
113
115
  } else if (vectorProvider === "weaviate") {
114
- vectorDbOptions.baseUrl = (_R = readString(env, "WEAVIATE_URL")) != null ? _R : "http://localhost:8080";
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 = (_S = readString(env, "REDIS_URL")) != null ? _S : "";
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 = (_T = readString(env, "VECTOR_DB_REST_URL")) != null ? _T : "";
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 = (_V = (_U = readString(env, "VECTOR_BASE_URL")) != null ? _U : readString(env, "VECTOR_DB_REST_URL")) != null ? _V : "";
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: (_W = readString(env, "EMBEDDING_API_KEY")) != null ? _W : readString(env, "OPENAI_API_KEY"),
141
- custom: (_X = readString(env, "EMBEDDING_API_KEY")) != null ? _X : readString(env, "OPENAI_API_KEY")
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: (_Z = (_Y = readString(env, "VECTOR_DB_INDEX")) != null ? _Y : vectorDbOptions.indexName) != null ? _Z : "rag-index",
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: (__ = readString(env, "LLM_MODEL")) != null ? __ : "gpt-4o",
153
- apiKey: (_$ = llmApiKeyByProvider[llmProvider]) != null ? _$ : "",
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: (_aa = readString(env, "EMBEDDING_MODEL")) != null ? _aa : "text-embedding-3-small",
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: (_ca = (_ba = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _ba : readString(env, "UI_TITLE")) != null ? _ca : "AI Assistant",
176
- subtitle: (_ea = (_da = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _da : readString(env, "UI_SUBTITLE")) != null ? _ea : "Powered by RAG",
177
- primaryColor: (_ga = (_fa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _fa : readString(env, "UI_PRIMARY_COLOR")) != null ? _ga : "#10b981",
178
- accentColor: (_ia = (_ha = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ha : readString(env, "UI_ACCENT_COLOR")) != null ? _ia : "#3b82f6",
179
- logoUrl: (_ja = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ja : readString(env, "UI_LOGO_URL"),
180
- placeholder: (_la = (_ka = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ka : readString(env, "UI_PLACEHOLDER")) != null ? _la : "Ask me anything\u2026",
181
- showSources: ((_na = (_ma = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _ma : readString(env, "UI_SHOW_SOURCES")) != null ? _na : "true") !== "false",
182
- welcomeMessage: (_pa = (_oa = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _oa : readString(env, "UI_WELCOME_MESSAGE")) != null ? _pa : "Hello! I'm your AI assistant. Ask me anything about your documents.",
183
- visualStyle: (_ra = (_qa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _qa : readString(env, "UI_VISUAL_STYLE")) != null ? _ra : "glass",
184
- borderRadius: (_ta = (_sa = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _sa : readString(env, "UI_BORDER_RADIUS")) != null ? _ta : "xl",
185
- allowUpload: ((_va = (_ua = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _ua : readString(env, "UI_ALLOW_UPLOAD")) != null ? _va : "false") === "true"
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 systemContent = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
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: systemContent.includes("{{context}}") ? systemContent.replace("{{context}}", context) : `${systemContent}
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: (_c = (_b = options == null ? void 0 : options.maxTokens) != null ? _b : this.llmConfig.maxTokens) != null ? _c : 1024,
325
- temperature: (_e = (_d = options == null ? void 0 : options.temperature) != null ? _d : this.llmConfig.temperature) != null ? _e : 0.7,
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 (_h = (_g = (_f = completion.choices[0]) == null ? void 0 : _f.message) == null ? void 0 : _g.content) != null ? _h : "";
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 systemContent = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
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: systemContent.includes("{{context}}") ? systemContent.replace("{{context}}", context) : `${systemContent}
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: (_c = (_b = options == null ? void 0 : options.maxTokens) != null ? _b : this.llmConfig.maxTokens) != null ? _c : 1024,
355
- temperature: (_e = (_d = options == null ? void 0 : options.temperature) != null ? _d : this.llmConfig.temperature) != null ? _e : 0.7,
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 = ((_g = (_f = chunk.choices[0]) == null ? void 0 : _f.delta) == null ? void 0 : _g.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 systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Use the context below to answer the user's question accurately.
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 = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
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: (_c = (_b = options == null ? void 0 : options.maxTokens) != null ? _b : this.llmConfig.maxTokens) != null ? _c : 1024,
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 systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Use the context below to answer the user's question accurately.
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 = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
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: (_c = (_b = options == null ? void 0 : options.maxTokens) != null ? _b : this.llmConfig.maxTokens) != null ? _c : 1024,
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 { GoogleGenAI } from "@google/genai";
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) throw new Error("[GeminiProvider] llmConfig.apiKey is required");
748
- this.client = new GoogleGenAI({ apiKey: llmConfig.apiKey });
749
- this.llmConfig = __spreadProps(__spreadValues({}, llmConfig), {
750
- model: this.sanitizeModel(llmConfig.model)
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: this.sanitizeModel(embeddingConfig.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({ field: "llm.model", message: "Gemini model name is required", severity: "error" });
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 || process.env.GOOGLE_GENAI_API_KEY || "";
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 { GoogleGenAI: GoogleGenAI2 } = await import("@google/genai");
785
- const genAI = new GoogleGenAI2({ apiKey });
786
- await genAI.models.get({ model: modelName });
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
- sanitizeModel(model) {
805
- if (!model) return model;
806
- return model.split(":")[0];
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
- async chat(messages, context, options) {
809
- var _a, _b, _c, _d, _e, _f;
810
- const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
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
- const systemInstruction = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
863
+ return base.includes("{{context}}") ? base.replace("{{context}}", context) : `${base}
815
864
 
816
865
  Context:
817
866
  ${context}`;
818
- const geminiMessages = messages.map((m) => ({
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
- const response = await this.client.models.generateContent({
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
- contents: geminiMessages,
825
- config: {
826
- systemInstruction,
827
- temperature: (_c = (_b = options == null ? void 0 : options.temperature) != null ? _b : this.llmConfig.temperature) != null ? _c : 0.7,
828
- maxOutputTokens: (_e = (_d = options == null ? void 0 : options.maxTokens) != null ? _d : this.llmConfig.maxTokens) != null ? _e : 1024,
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
- const text = typeof response.text === "function" ? response.text() : (_f = response.text) != null ? _f : "";
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, _e, _f;
838
- const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
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
- contents: geminiMessages,
853
- config: {
854
- systemInstruction,
855
- temperature: (_c = (_b = options == null ? void 0 : options.temperature) != null ? _b : this.llmConfig.temperature) != null ? _c : 0.7,
856
- maxOutputTokens: (_e = (_d = options == null ? void 0 : options.maxTokens) != null ? _d : this.llmConfig.maxTokens) != null ? _e : 1024,
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 = typeof chunk.text === "function" ? chunk.text() : chunk.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, _c, _d, _e, _f, _g, _h, _i, _j, _k;
884
- const model = this.sanitizeModel(
885
- (_c = (_b = options == null ? void 0 : options.model) != null ? _b : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _c : "text-embedding-004"
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 apiKey = (_e = (_d = this.embeddingConfig) == null ? void 0 : _d.apiKey) != null ? _e : this.llmConfig.apiKey;
888
- const client = apiKey !== this.llmConfig.apiKey ? new GoogleGenAI({ apiKey }) : this.client;
889
- let content = text;
890
- const queryPrefix = (_f = this.embeddingConfig) == null ? void 0 : _f.queryPrefix;
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 "${model}":`, err.message);
950
+ console.error(`[GeminiProvider] Embedding failed for model "${modelName}":`, err.message);
907
951
  throw err;
908
952
  });
909
- return (_k = (_j = (_i = response.embeddings) == null ? void 0 : _i[0]) == null ? void 0 : _j.values) != null ? _k : [];
953
+ return response.embedding.values;
910
954
  }
911
955
  async batchEmbed(texts, options) {
912
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
913
- const model = this.sanitizeModel(
914
- (_c = (_b = options == null ? void 0 : options.model) != null ? _b : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _c : "text-embedding-004"
915
- );
916
- const apiKey = (_e = (_d = this.embeddingConfig) == null ? void 0 : _d.apiKey) != null ? _e : this.llmConfig.apiKey;
917
- const client = apiKey !== this.llmConfig.apiKey ? new GoogleGenAI({ apiKey }) : this.client;
918
- let contents = texts;
919
- const queryPrefix = (_f = this.embeddingConfig) == null ? void 0 : _f.queryPrefix;
920
- const docPrefix = (_g = this.embeddingConfig) == null ? void 0 : _g.documentPrefix;
921
- if ((options == null ? void 0 : options.taskType) === "query" && queryPrefix) {
922
- contents = texts.map((text) => text.startsWith(queryPrefix) ? text : `${queryPrefix}${text}`);
923
- } else if ((options == null ? void 0 : options.taskType) === "document" && docPrefix) {
924
- contents = texts.map((text) => text.startsWith(docPrefix) ? text : `${docPrefix}${text}`);
925
- }
926
- const response = await client.models.embedContent({
927
- model,
928
- contents,
929
- config: ((_h = this.embeddingConfig) == null ? void 0 : _h.dimensions) ? { outputDimensionality: this.embeddingConfig.dimensions } : void 0
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 "${model}":`, err.message);
980
+ console.error(`[GeminiProvider] Batch embedding failed for model "${modelName}":`, err.message);
932
981
  throw err;
933
982
  });
934
- return (_j = (_i = response.embeddings) == null ? void 0 : _i.map((e) => {
935
- var _a2;
936
- return (_a2 = e.values) != null ? _a2 : [];
937
- })) != null ? _j : [];
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
- await this.client.models.embedContent({
942
- model: "text-embedding-004",
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
- if (llmConfig.baseUrl || ((_a = llmConfig.options) == null ? void 0 : _a.baseUrl)) {
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(`[LLMFactory] Unknown provider "${llmConfig.provider}"`);
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 { PostgreSQLProvider } = await import("./PostgreSQLProvider-BMOETDZA.mjs");
1250
- return PostgreSQLProvider;
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 the best visualization format
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 analysis = this.analyzeData(userQuery, retrievedData);
2229
- switch (analysis.suggestedType) {
2230
- case "product_carousel":
2231
- return this.transformToProductCarousel(retrievedData);
2232
- case "pie_chart":
2233
- return this.transformToPieChart(retrievedData);
2234
- case "bar_chart":
2235
- return this.transformToBarChart(retrievedData);
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 (isTrendQuery && hasTimeSeries) {
2263
- return { suggestedType: "line_chart", confidence: 0.9, hasProducts, hasTimeSeries, hasCategories };
2338
+ if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
2339
+ return this.transformToPieChart(filteredData);
2264
2340
  }
2265
- if (isDistributionQuery && hasCategories) {
2266
- return { suggestedType: "pie_chart", confidence: 0.85, hasProducts, hasTimeSeries, hasCategories };
2341
+ if (hasProducts) {
2342
+ return this.transformToProductCarousel(filteredData, config, trainedSchema);
2267
2343
  }
2268
- if (isComparisonQuery && hasCategories && data.length > 2) {
2269
- return { suggestedType: "bar_chart", confidence: 0.8, hasProducts, hasTimeSeries, hasCategories };
2344
+ if (this.hasMultipleFields(filteredData)) {
2345
+ return this.transformToTable(filteredData);
2270
2346
  }
2271
- if (data.length > 5 && this.hasMultipleFields(data)) {
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 = ["product", "price", "stock", "item", "sku", "brand", "model"];
2394
- return productKeywords.some((kw) => content.includes(kw)) || Object.keys(item.metadata || {}).some(
2395
- (k) => ["name", "price", "product", "sku"].includes(k.toLowerCase())
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 = ["date", "time", "year", "month", "week", "day", "hour", "trend", "historical"];
2404
- return timeKeywords.some((kw) => content.includes(kw)) || Object.keys(item.metadata || {}).some(
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 product information from vector match
2669
+ * Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
2410
2670
  */
2411
- static extractProductInfo(item) {
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
- if (meta.name || meta.product) {
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: meta.name || meta.product,
2417
- price: meta.price,
2418
- image: meta.image || meta.imageUrl,
2419
- brand: meta.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, _b;
3138
+ var _a;
2624
3139
  if (this.initialised) return;
2625
- const CHART_MARKER = "<!-- UI_PROTOCOL_V7 -->";
2626
3140
  const chartInstruction = `
2627
-
2628
- ${CHART_MARKER}
2629
- ### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
2630
-
2631
- You are responsible for returning a SINGLE structured UI block when visualization is required.
2632
-
2633
- ---
2634
-
2635
- ## 1. INTENT CLASSIFICATION (MANDATORY)
2636
-
2637
- Classify the query into ONE of the following intents:
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
- if (!((_a = this.config.llm.systemPrompt) == null ? void 0 : _a.includes(CHART_MARKER))) {
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 (((_b = this.config.rag) == null ? void 0 : _b.architecture) === "agentic") {
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 messages = [...history, { role: "user", content: question }];
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 uiTransformation = UITransformer.transform(question, sources);
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(__spreadValues({ type: "ui_transformation" }, uiTransformation != null ? uiTransformation : {}))}
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 sources = (chunk == null ? void 0 : chunk.sources) || [];
3468
- if (sources && sources.length > 0) {
3827
+ const responseChunk = chunk;
3828
+ const sources = (responseChunk == null ? void 0 : responseChunk.sources) || [];
3829
+ if (sources.length > 0) {
3469
3830
  try {
3470
- const uiTransformation = UITransformer.transform(message, sources);
3471
- enqueue(sseUIFrame(uiTransformation));
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
  }