@retrivora-ai/rag-engine 1.0.9 → 1.1.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/server.js CHANGED
@@ -854,20 +854,76 @@ var init_QdrantProvider = __esm({
854
854
  QdrantProvider = class extends BaseVectorProvider {
855
855
  constructor(config) {
856
856
  super(config);
857
+ this.schemaDiscovered = false;
857
858
  const opts = config.options;
858
859
  const baseUrl = opts.baseUrl;
859
860
  if (!baseUrl) throw new Error("[QdrantProvider] baseUrl is required");
861
+ this.contentField = opts.contentField || "content";
862
+ this.metadataField = opts.metadataField || "metadata";
863
+ this.isFlatPayload = !!opts.flatPayload;
860
864
  this.http = import_axios4.default.create({
861
865
  baseURL: baseUrl,
866
+ timeout: 15e3,
867
+ // 15s timeout for vector DB operations
862
868
  headers: __spreadValues({
863
869
  "Content-Type": "application/json"
864
870
  }, opts.apiKey ? { "api-key": opts.apiKey } : {})
865
871
  });
866
872
  }
867
873
  async initialize() {
874
+ if (this.schemaDiscovered) return;
868
875
  await this.ping();
869
876
  await this.ensureCollection();
870
- await this.ensureIndex();
877
+ console.log(`[QdrantProvider] \u{1F50D} Discovering schema for collection "${this.indexName}"...`);
878
+ const discoveredFields = await this.discoverSchema();
879
+ const opts = this.config.options;
880
+ const configFields = opts.filterableFields || [];
881
+ const allFields = [.../* @__PURE__ */ new Set([...discoveredFields, ...configFields])];
882
+ if (allFields.length > 0) {
883
+ console.log(`[QdrantProvider] \u{1F6E0} Ensuring indexes for ${allFields.length} discovered fields...`);
884
+ await Promise.all(
885
+ allFields.map(async (fieldInfo) => {
886
+ const [fieldName, schemaType] = fieldInfo.split(":");
887
+ return this.ensureIndex(fieldName, schemaType || "keyword");
888
+ })
889
+ );
890
+ } else {
891
+ console.log(`[QdrantProvider] \u2139\uFE0F No fields discovered for indexing.`);
892
+ }
893
+ this.schemaDiscovered = true;
894
+ }
895
+ /**
896
+ * Samples points from the collection to discover available payload fields.
897
+ */
898
+ async discoverSchema() {
899
+ var _a;
900
+ try {
901
+ const { data } = await this.http.post(`/collections/${this.indexName}/points/scroll`, {
902
+ limit: 20,
903
+ with_payload: true,
904
+ with_vector: false
905
+ });
906
+ const points = ((_a = data.result) == null ? void 0 : _a.points) || [];
907
+ const keys = /* @__PURE__ */ new Set();
908
+ for (const point of points) {
909
+ const payload = point.payload || {};
910
+ Object.keys(payload).forEach((k) => {
911
+ if (k !== "namespace" && k !== this.contentField && k !== this.metadataField) {
912
+ keys.add(k);
913
+ }
914
+ });
915
+ if (!this.isFlatPayload && payload[this.metadataField]) {
916
+ const meta = payload[this.metadataField];
917
+ if (typeof meta === "object" && meta !== null) {
918
+ Object.keys(meta).forEach((k) => keys.add(k));
919
+ }
920
+ }
921
+ }
922
+ return Array.from(keys);
923
+ } catch (err) {
924
+ console.warn(`[QdrantProvider] \u26A0\uFE0F Schema discovery failed:`, err instanceof Error ? err.message : String(err));
925
+ return [];
926
+ }
871
927
  }
872
928
  /**
873
929
  * Ensures the collection exists. Creates it if missing.
@@ -895,29 +951,25 @@ var init_QdrantProvider = __esm({
895
951
  }
896
952
  }
897
953
  /**
898
- * Ensures that the 'namespace' field has a keyword index for efficient filtering.
899
- * Qdrant requires this for search filters to work in many configurations.
954
+ * Ensures that a payload field has an index.
900
955
  */
901
- async ensureIndex() {
902
- var _a, _b;
956
+ async ensureIndex(fieldName, schema = "keyword") {
957
+ var _a;
958
+ let fullPath = fieldName;
959
+ if (!this.isFlatPayload && !fieldName.includes(".") && fieldName !== "namespace" && fieldName !== this.contentField) {
960
+ fullPath = `${this.metadataField}.${fieldName}`;
961
+ }
903
962
  try {
904
963
  await this.http.put(`/collections/${this.indexName}/index`, {
905
- field_name: "namespace",
906
- field_schema: "keyword"
964
+ field_name: fullPath,
965
+ field_schema: schema
907
966
  });
908
- console.log(`[QdrantProvider] \u2705 Ensured keyword index for "namespace" on collection "${this.indexName}"`);
967
+ console.log(`[QdrantProvider] \u2705 Ensured ${schema} index for "${fullPath}"`);
909
968
  } catch (err) {
910
969
  let status;
911
- let data;
912
- if (import_axios4.default.isAxiosError(err)) {
913
- status = (_a = err.response) == null ? void 0 : _a.status;
914
- data = (_b = err.response) == null ? void 0 : _b.data;
915
- }
916
- if (status === 409) {
917
- return;
918
- }
919
- const errorMessage = err instanceof Error ? err.message : String(err);
920
- console.warn(`[QdrantProvider] \u26A0\uFE0F Could not ensure namespace index (Status: ${status}):`, JSON.stringify(data || errorMessage));
970
+ if (import_axios4.default.isAxiosError(err)) status = (_a = err.response) == null ? void 0 : _a.status;
971
+ if (status === 409) return;
972
+ console.warn(`[QdrantProvider] \u26A0\uFE0F Could not ensure index for "${fullPath}":`, err instanceof Error ? err.message : String(err));
921
973
  }
922
974
  }
923
975
  async upsert(doc, namespace) {
@@ -925,14 +977,21 @@ var init_QdrantProvider = __esm({
925
977
  }
926
978
  async batchUpsert(docs, namespace) {
927
979
  const payload = {
928
- points: docs.map((doc) => ({
929
- id: this.normalizeId(doc.id),
930
- vector: doc.vector,
931
- payload: __spreadValues({
932
- content: doc.content,
933
- metadata: doc.metadata || {}
934
- }, namespace ? { namespace } : {})
935
- }))
980
+ points: docs.map((doc) => {
981
+ const pointPayload = __spreadValues({
982
+ [this.contentField]: doc.content
983
+ }, namespace ? { namespace } : {});
984
+ if (this.isFlatPayload) {
985
+ Object.assign(pointPayload, doc.metadata || {});
986
+ } else {
987
+ pointPayload[this.metadataField] = doc.metadata || {};
988
+ }
989
+ return {
990
+ id: this.normalizeId(doc.id),
991
+ vector: doc.vector,
992
+ payload: pointPayload
993
+ };
994
+ })
936
995
  };
937
996
  await this.http.put(`/collections/${this.indexName}/points`, payload);
938
997
  }
@@ -943,7 +1002,15 @@ var init_QdrantProvider = __esm({
943
1002
  }
944
1003
  const sanitizedFilter = this.sanitizeFilter(_filter);
945
1004
  for (const [key, value] of Object.entries(sanitizedFilter)) {
946
- must.push({ key: `metadata.${key}`, match: { value } });
1005
+ let filterKey = key;
1006
+ if (!this.isFlatPayload && !key.includes(".") && key !== "namespace" && key !== this.contentField) {
1007
+ filterKey = `${this.metadataField}.${key}`;
1008
+ }
1009
+ if (Array.isArray(value)) {
1010
+ must.push({ key: filterKey, match: { any: value } });
1011
+ } else {
1012
+ must.push({ key: filterKey, match: { value } });
1013
+ }
947
1014
  }
948
1015
  const payload = {
949
1016
  vector,
@@ -955,28 +1022,36 @@ var init_QdrantProvider = __esm({
955
1022
  },
956
1023
  filter: must.length > 0 ? { must } : void 0
957
1024
  };
958
- console.log(`[QdrantProvider] \u{1F50D} Searching "${this.indexName}" | Namespace: ${namespace || "default"} | TopK: ${topK}`);
959
- if (must.length > (namespace ? 1 : 0)) {
960
- console.log(`[QdrantProvider] \u{1F6E0} Filters:`, JSON.stringify(must.filter((m) => m.key !== "namespace")));
961
- }
962
1025
  const { data } = await this.http.post(`/collections/${this.indexName}/points/search`, payload);
963
1026
  const results = (data.result || []).map((res) => {
964
- var _a, _b;
1027
+ const p = res.payload || {};
1028
+ let content = p[this.contentField] || "";
1029
+ if (!content) {
1030
+ const stringFields = Object.entries(p).filter(([_, v]) => typeof v === "string").map(([k, v]) => ({ key: k, val: v }));
1031
+ if (stringFields.length > 0) {
1032
+ const bestMatch = stringFields.sort((a, b) => b.val.length - a.val.length)[0];
1033
+ content = bestMatch.val;
1034
+ } else {
1035
+ content = JSON.stringify(p);
1036
+ }
1037
+ }
1038
+ let metadata = {};
1039
+ if (this.isFlatPayload) {
1040
+ metadata = __spreadValues({}, p);
1041
+ delete metadata[this.contentField];
1042
+ delete metadata["namespace"];
1043
+ } else {
1044
+ metadata = p[this.metadataField] || p;
1045
+ }
965
1046
  return {
966
- id: res["id"],
967
- score: res["score"],
968
- content: ((_a = res["payload"]) == null ? void 0 : _a.content) || "",
969
- metadata: ((_b = res["payload"]) == null ? void 0 : _b.metadata) || {}
1047
+ id: res.id,
1048
+ score: res.score,
1049
+ content,
1050
+ metadata
970
1051
  };
971
1052
  });
972
- if (results.length === 0) {
973
- console.warn(`[QdrantProvider] \u26A0\uFE0F Retrieval returned 0 results. Check if data exists in namespace "${namespace}"`);
974
- } else {
975
- console.log(`[QdrantProvider] \u2705 Found ${results.length} results (best score: ${results[0].score.toFixed(4)})`);
976
- }
977
1053
  return results;
978
1054
  }
979
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
980
1055
  async delete(id, _namespace) {
981
1056
  await this.http.post(`/collections/${this.indexName}/points/delete`, {
982
1057
  points: [this.normalizeId(id)]
@@ -997,10 +1072,6 @@ var init_QdrantProvider = __esm({
997
1072
  return false;
998
1073
  }
999
1074
  }
1000
- /**
1001
- * Normalizes an ID into a format Qdrant accepts (UUID or integer).
1002
- * For string IDs that aren't UUIDs, it generates a deterministic UUID v5-like hash.
1003
- */
1004
1075
  normalizeId(id) {
1005
1076
  if (typeof id === "number") return id;
1006
1077
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -1916,6 +1987,53 @@ ${context}`
1916
1987
  });
1917
1988
  return (_h = (_g = (_f = completion.choices[0]) == null ? void 0 : _f.message) == null ? void 0 : _g.content) != null ? _h : "";
1918
1989
  }
1990
+ chatStream(messages, context, options) {
1991
+ return __asyncGenerator(this, null, function* () {
1992
+ var _a, _b, _c, _d, _e, _f, _g;
1993
+ const systemContent = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
1994
+
1995
+ Context:
1996
+ ${context}`;
1997
+ const systemMessage = {
1998
+ role: "system",
1999
+ content: systemContent.includes("{{context}}") ? systemContent.replace("{{context}}", context) : `${systemContent}
2000
+
2001
+ Context:
2002
+ ${context}`
2003
+ };
2004
+ const formattedMessages = [
2005
+ systemMessage,
2006
+ ...messages.map((m) => ({
2007
+ role: m.role,
2008
+ content: m.content
2009
+ }))
2010
+ ];
2011
+ const stream = yield new __await(this.client.chat.completions.create({
2012
+ model: this.llmConfig.model,
2013
+ messages: formattedMessages,
2014
+ max_tokens: (_c = (_b = options == null ? void 0 : options.maxTokens) != null ? _b : this.llmConfig.maxTokens) != null ? _c : 1024,
2015
+ temperature: (_e = (_d = options == null ? void 0 : options.temperature) != null ? _d : this.llmConfig.temperature) != null ? _e : 0.7,
2016
+ stop: options == null ? void 0 : options.stop,
2017
+ stream: true
2018
+ }));
2019
+ try {
2020
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
2021
+ const chunk = temp.value;
2022
+ const content = ((_g = (_f = chunk.choices[0]) == null ? void 0 : _f.delta) == null ? void 0 : _g.content) || "";
2023
+ if (content) yield content;
2024
+ }
2025
+ } catch (temp) {
2026
+ error = [temp];
2027
+ } finally {
2028
+ try {
2029
+ more && (temp = iter.return) && (yield new __await(temp.call(iter)));
2030
+ } finally {
2031
+ if (error)
2032
+ throw error[0];
2033
+ }
2034
+ }
2035
+ });
2036
+ }
1919
2037
  async embed(text, options) {
1920
2038
  const results = await this.batchEmbed([text], options);
1921
2039
  return results[0];
@@ -2021,6 +2139,47 @@ ${context}`;
2021
2139
  const block = response.content[0];
2022
2140
  return block.type === "text" ? block.text : "";
2023
2141
  }
2142
+ chatStream(messages, context, options) {
2143
+ return __asyncGenerator(this, null, function* () {
2144
+ var _a, _b, _c;
2145
+ const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Use the context below to answer the user's question accurately.
2146
+
2147
+ Context:
2148
+ ${context}`;
2149
+ const system = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
2150
+
2151
+ Context:
2152
+ ${context}`;
2153
+ const anthropicMessages = messages.map((m) => ({
2154
+ role: m.role === "assistant" ? "assistant" : "user",
2155
+ content: m.content
2156
+ }));
2157
+ const stream = yield new __await(this.client.messages.create({
2158
+ model: this.llmConfig.model,
2159
+ max_tokens: (_c = (_b = options == null ? void 0 : options.maxTokens) != null ? _b : this.llmConfig.maxTokens) != null ? _c : 1024,
2160
+ system,
2161
+ messages: anthropicMessages,
2162
+ stream: true
2163
+ }));
2164
+ try {
2165
+ for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
2166
+ const chunk = temp.value;
2167
+ if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
2168
+ yield chunk.delta.text;
2169
+ }
2170
+ }
2171
+ } catch (temp) {
2172
+ error = [temp];
2173
+ } finally {
2174
+ try {
2175
+ more && (temp = iter.return) && (yield new __await(temp.call(iter)));
2176
+ } finally {
2177
+ if (error)
2178
+ throw error[0];
2179
+ }
2180
+ }
2181
+ });
2182
+ }
2024
2183
  async embed(text, options) {
2025
2184
  void text;
2026
2185
  void options;
@@ -2126,7 +2285,7 @@ var OllamaProvider = class {
2126
2285
  }
2127
2286
  chatStream(messages, context, options) {
2128
2287
  return __asyncGenerator(this, null, function* () {
2129
- var _a, _b, _c, _d, _e;
2288
+ var _a, _b, _c, _d, _e, _f;
2130
2289
  const system = this.buildSystemPrompt(context);
2131
2290
  const response = yield new __await(this.http.post("/api/chat", {
2132
2291
  model: this.llmConfig.model,
@@ -2140,11 +2299,15 @@ var OllamaProvider = class {
2140
2299
  ...messages.map((m) => ({ role: m.role, content: m.content }))
2141
2300
  ]
2142
2301
  }, { responseType: "stream" }));
2302
+ let lineBuffer = "";
2143
2303
  try {
2144
2304
  for (var iter = __forAwait(response.data), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
2145
2305
  const chunk = temp.value;
2146
- const lines = chunk.toString().split("\n").filter(Boolean);
2306
+ lineBuffer += chunk.toString();
2307
+ const lines = lineBuffer.split("\n");
2308
+ lineBuffer = lines.pop() || "";
2147
2309
  for (const line of lines) {
2310
+ if (!line.trim()) continue;
2148
2311
  try {
2149
2312
  const json = JSON.parse(line);
2150
2313
  if ((_e = json.message) == null ? void 0 : _e.content) {
@@ -2152,6 +2315,7 @@ var OllamaProvider = class {
2152
2315
  }
2153
2316
  if (json.done) return;
2154
2317
  } catch (e) {
2318
+ console.warn("[OllamaProvider] Failed to parse streaming line:", line);
2155
2319
  }
2156
2320
  }
2157
2321
  }
@@ -2165,6 +2329,13 @@ var OllamaProvider = class {
2165
2329
  throw error[0];
2166
2330
  }
2167
2331
  }
2332
+ if (lineBuffer.trim()) {
2333
+ try {
2334
+ const json = JSON.parse(lineBuffer);
2335
+ if ((_f = json.message) == null ? void 0 : _f.content) yield json.message.content;
2336
+ } catch (e) {
2337
+ }
2338
+ }
2168
2339
  });
2169
2340
  }
2170
2341
  buildSystemPrompt(context) {
@@ -2310,6 +2481,49 @@ ${context}`;
2310
2481
  });
2311
2482
  return (_f = response.text) != null ? _f : "";
2312
2483
  }
2484
+ chatStream(messages, context, options) {
2485
+ return __asyncGenerator(this, null, function* () {
2486
+ var _a, _b, _c, _d, _e;
2487
+ const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
2488
+
2489
+ Context:
2490
+ ${context}`;
2491
+ const systemInstruction = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
2492
+
2493
+ Context:
2494
+ ${context}`;
2495
+ const geminiMessages = messages.map((m) => ({
2496
+ role: m.role === "assistant" ? "model" : "user",
2497
+ parts: [{ text: m.content }]
2498
+ }));
2499
+ const result = yield new __await(this.client.models.generateContentStream({
2500
+ model: this.llmConfig.model,
2501
+ contents: geminiMessages,
2502
+ config: {
2503
+ systemInstruction,
2504
+ temperature: (_c = (_b = options == null ? void 0 : options.temperature) != null ? _b : this.llmConfig.temperature) != null ? _c : 0.7,
2505
+ maxOutputTokens: (_e = (_d = options == null ? void 0 : options.maxTokens) != null ? _d : this.llmConfig.maxTokens) != null ? _e : 1024,
2506
+ stopSequences: options == null ? void 0 : options.stop
2507
+ }
2508
+ }));
2509
+ try {
2510
+ for (var iter = __forAwait(result.stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
2511
+ const chunk = temp.value;
2512
+ const text = chunk.text;
2513
+ if (text) yield text;
2514
+ }
2515
+ } catch (temp) {
2516
+ error = [temp];
2517
+ } finally {
2518
+ try {
2519
+ more && (temp = iter.return) && (yield new __await(temp.call(iter)));
2520
+ } finally {
2521
+ if (error)
2522
+ throw error[0];
2523
+ }
2524
+ }
2525
+ });
2526
+ }
2313
2527
  async embed(text, options) {
2314
2528
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
2315
2529
  const model = this.sanitizeModel(
@@ -3857,6 +4071,7 @@ var Pipeline = class {
3857
4071
  }
3858
4072
  const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
3859
4073
  const filter = QueryProcessor.buildQueryFilter(question, hints);
4074
+ console.log(`[Pipeline] \u{1F9E9} Extracted filters:`, JSON.stringify(filter));
3860
4075
  const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
3861
4076
  namespace: ns,
3862
4077
  topK: topK * 2,
package/dist/server.mjs CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  sseFrame,
41
41
  sseMetaFrame,
42
42
  sseTextFrame
43
- } from "./chunk-7S2SRQGL.mjs";
43
+ } from "./chunk-JZ4H7EP6.mjs";
44
44
  import "./chunk-YLTMFW4M.mjs";
45
45
  import {
46
46
  PineconeProvider
@@ -56,7 +56,7 @@ import {
56
56
  } from "./chunk-U55XRW3U.mjs";
57
57
  import {
58
58
  QdrantProvider
59
- } from "./chunk-NT5VP7MT.mjs";
59
+ } from "./chunk-63HITIWC.mjs";
60
60
  import {
61
61
  BaseVectorProvider
62
62
  } from "./chunk-IMP6FUCY.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -317,6 +317,8 @@ export class Pipeline {
317
317
  // 2. Parallel Retrieval
318
318
  const hints = QueryProcessor.extractQueryFieldHints(question, this.config.rag?.filterableFields);
319
319
  const filter = QueryProcessor.buildQueryFilter(question, hints);
320
+
321
+ console.log(`[Pipeline] 🧩 Extracted filters:`, JSON.stringify(filter));
320
322
 
321
323
  const { sources: rawSources, graphData } = await this.retrieve(searchQuery, {
322
324
  namespace: ns,
@@ -98,6 +98,35 @@ export class AnthropicProvider implements ILLMProvider {
98
98
  return block.type === 'text' ? block.text : '';
99
99
  }
100
100
 
101
+ async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
102
+ const systemPrompt =
103
+ this.llmConfig.systemPrompt ??
104
+ `You are a helpful assistant. Use the context below to answer the user's question accurately.\n\nContext:\n${context}`;
105
+
106
+ const system = systemPrompt.includes('{{context}}')
107
+ ? systemPrompt.replace('{{context}}', context)
108
+ : `${systemPrompt}\n\nContext:\n${context}`;
109
+
110
+ const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
111
+ role: m.role === 'assistant' ? 'assistant' : 'user',
112
+ content: m.content,
113
+ }));
114
+
115
+ const stream = await this.client.messages.create({
116
+ model: this.llmConfig.model,
117
+ max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
118
+ system,
119
+ messages: anthropicMessages,
120
+ stream: true,
121
+ });
122
+
123
+ for await (const chunk of stream) {
124
+ if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
125
+ yield chunk.delta.text;
126
+ }
127
+ }
128
+ }
129
+
101
130
  async embed(text: string, options?: EmbedOptions): Promise<number[]> {
102
131
  void text;
103
132
  void options;
@@ -130,6 +130,37 @@ export class GeminiProvider implements ILLMProvider {
130
130
  return response.text ?? '';
131
131
  }
132
132
 
133
+ async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
134
+ const systemPrompt =
135
+ this.llmConfig.systemPrompt ??
136
+ `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
137
+
138
+ const systemInstruction = systemPrompt.includes('{{context}}')
139
+ ? systemPrompt.replace('{{context}}', context)
140
+ : `${systemPrompt}\n\nContext:\n${context}`;
141
+
142
+ const geminiMessages = messages.map((m) => ({
143
+ role: m.role === 'assistant' ? 'model' : 'user',
144
+ parts: [{ text: m.content }],
145
+ }));
146
+
147
+ const result = await this.client.models.generateContentStream({
148
+ model: this.llmConfig.model,
149
+ contents: geminiMessages,
150
+ config: {
151
+ systemInstruction,
152
+ temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
153
+ maxOutputTokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
154
+ stopSequences: options?.stop,
155
+ },
156
+ });
157
+
158
+ for await (const chunk of result.stream) {
159
+ const text = chunk.text;
160
+ if (text) yield text;
161
+ }
162
+ }
163
+
133
164
  async embed(text: string, options?: EmbedOptions): Promise<number[]> {
134
165
  const model = this.sanitizeModel(
135
166
  options?.model ??
@@ -118,9 +118,16 @@ export class OllamaProvider implements ILLMProvider {
118
118
  ],
119
119
  }, { responseType: 'stream' });
120
120
 
121
+ let lineBuffer = '';
121
122
  for await (const chunk of response.data) {
122
- const lines = chunk.toString().split('\n').filter(Boolean);
123
+ lineBuffer += chunk.toString();
124
+ const lines = lineBuffer.split('\n');
125
+
126
+ // Keep the last partial line in the buffer
127
+ lineBuffer = lines.pop() || '';
128
+
123
129
  for (const line of lines) {
130
+ if (!line.trim()) continue;
124
131
  try {
125
132
  const json = JSON.parse(line);
126
133
  if (json.message?.content) {
@@ -128,10 +135,22 @@ export class OllamaProvider implements ILLMProvider {
128
135
  }
129
136
  if (json.done) return;
130
137
  } catch {
131
- // Ignore parsing errors for partial lines
138
+ // If parsing fails, it might be a malformed line or something else
139
+ // But with line buffering, this should be rare
140
+ console.warn('[OllamaProvider] Failed to parse streaming line:', line);
132
141
  }
133
142
  }
134
143
  }
144
+
145
+ // Process anything left in the buffer at the end
146
+ if (lineBuffer.trim()) {
147
+ try {
148
+ const json = JSON.parse(lineBuffer);
149
+ if (json.message?.content) yield json.message.content;
150
+ } catch {
151
+ // Final cleanup
152
+ }
153
+ }
135
154
  }
136
155
 
137
156
  private buildSystemPrompt(context: string): string {
@@ -108,6 +108,41 @@ export class OpenAIProvider implements ILLMProvider {
108
108
 
109
109
  return completion.choices[0]?.message?.content ?? '';
110
110
  }
111
+
112
+ async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
113
+ const systemContent =
114
+ this.llmConfig.systemPrompt ??
115
+ `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
116
+
117
+ const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
118
+ role: 'system',
119
+ content: systemContent.includes('{{context}}')
120
+ ? systemContent.replace('{{context}}', context)
121
+ : `${systemContent}\n\nContext:\n${context}`,
122
+ };
123
+
124
+ const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
125
+ systemMessage,
126
+ ...messages.map((m) => ({
127
+ role: m.role as 'user' | 'assistant',
128
+ content: m.content,
129
+ })),
130
+ ];
131
+
132
+ const stream = await this.client.chat.completions.create({
133
+ model: this.llmConfig.model,
134
+ messages: formattedMessages,
135
+ max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
136
+ temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
137
+ stop: options?.stop,
138
+ stream: true,
139
+ });
140
+
141
+ for await (const chunk of stream) {
142
+ const content = chunk.choices[0]?.delta?.content || '';
143
+ if (content) yield content;
144
+ }
145
+ }
111
146
 
112
147
  async embed(text: string, options?: EmbedOptions): Promise<number[]> {
113
148
  const results = await this.batchEmbed([text], options);