@devilsdev/rag-connectors 3.6.0 → 3.7.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @devilsdev/rag-connectors
2
2
 
3
- Embedders, LLMs and vector stores for @devilsdev/rag-core: OpenAI, Anthropic, Cohere, Ollama, the AI SDK, Qdrant, pgvector and Pinecone.
3
+ Embedders, LLMs and vector stores for @devilsdev/rag-core: OpenAI, Anthropic, Cohere, Ollama, the AI SDK, Qdrant, pgvector, Pinecone and Weaviate.
4
4
 
5
5
  Part of [@devilsdev/rag-pipeline-utils](https://www.npmjs.com/package/@devilsdev/rag-pipeline-utils), split into packages so you install only what you use. The objects are the same ones the full package exports; versions move in lockstep.
6
6
 
@@ -8,7 +8,7 @@ Part of [@devilsdev/rag-pipeline-utils](https://www.npmjs.com/package/@devilsdev
8
8
  npm install @devilsdev/rag-connectors
9
9
  ```
10
10
 
11
- Exports: `OpenAIConnector`, `AnthropicConnector`, `CohereConnector`, `OllamaConnector`, `LocalEmbedder`, `QdrantRetriever`, `PineconeRetriever`, `PgVectorRetriever`, `createAiSdkAdapter`.
11
+ Exports: `OpenAIConnector`, `AnthropicConnector`, `CohereConnector`, `OllamaConnector`, `LocalEmbedder`, `QdrantRetriever`, `PineconeRetriever`, `PgVectorRetriever`, `WeaviateRetriever`, `createAiSdkAdapter`.
12
12
 
13
13
  Documentation: https://devilsdev.github.io/rag-pipeline-utils/
14
14
 
package/dist/index.cjs CHANGED
@@ -233,8 +233,25 @@ var require_openai_connector = __commonJS({
233
233
  _request(signal, fn) {
234
234
  return this.withRetry(fn, this.config.maxRetries, this.config.retryDelay, {
235
235
  signal
236
+ }).catch((err) => {
237
+ throw this._tagError(err);
236
238
  });
237
239
  }
240
+ /**
241
+ * Name the provider on an HTTP error from the SDK, as the axios-based
242
+ * connectors do, so a caller such as the CLI can say which key was refused.
243
+ * A custom baseURL never reads OPENAI_API_KEY, so it gets no envVar.
244
+ * @private
245
+ */
246
+ _tagError(err) {
247
+ if (err && typeof err === "object" && typeof err.status === "number") {
248
+ if (!err.provider) {
249
+ err.provider = this.config.baseURL ? "The OpenAI-compatible API" : "OpenAI";
250
+ }
251
+ if (!this.config.baseURL && !err.envVar) err.envVar = API_KEY_ENV;
252
+ }
253
+ return err;
254
+ }
238
255
  /**
239
256
  * Generate a chat completion using the provided query and optional RAG context.
240
257
  *
@@ -278,19 +295,24 @@ var require_openai_connector = __commonJS({
278
295
  this._requireApiKey();
279
296
  const client = this._getClient();
280
297
  const messages = this._buildMessages(query, context);
281
- const stream = await raceAbort(
282
- client.chat.completions.create(
283
- {
284
- model: options.model || this.config.model,
285
- messages,
286
- temperature: options.temperature ?? this.config.temperature,
287
- max_tokens: options.maxTokens ?? this.config.maxTokens,
288
- stream: true
289
- },
290
- signal ? { signal } : void 0
291
- ),
292
- signal
293
- );
298
+ let stream;
299
+ try {
300
+ stream = await raceAbort(
301
+ client.chat.completions.create(
302
+ {
303
+ model: options.model || this.config.model,
304
+ messages,
305
+ temperature: options.temperature ?? this.config.temperature,
306
+ max_tokens: options.maxTokens ?? this.config.maxTokens,
307
+ stream: true
308
+ },
309
+ signal ? { signal } : void 0
310
+ ),
311
+ signal
312
+ );
313
+ } catch (err) {
314
+ throw this._tagError(err);
315
+ }
294
316
  for await (const chunk of stream) {
295
317
  throwIfAborted(signal);
296
318
  const token = chunk.choices[0]?.delta?.content || "";
@@ -1621,12 +1643,15 @@ var require_pinecone_retriever = __commonJS({
1621
1643
  var CONTROL_PLANE = "https://api.pinecone.io";
1622
1644
  var DEFAULT_NAMESPACE = "rag_documents";
1623
1645
  var UPSERT_BATCH = 100;
1646
+ var EMBED_UPSERT_BATCH = 96;
1647
+ var REQUEST_BYTES = 2e6 - 1024;
1624
1648
  var DELETE_BATCH = 1e3;
1625
1649
  var PAGE = 100;
1626
1650
  var SEARCH_MAX = 1e4;
1627
1651
  var PLAIN_ID = /^[\x20-\x7e]{1,512}$/;
1628
1652
  var FIELDS = ["rag_id", "rag_content", "rag_metadata"];
1629
1653
  var OURS = { rag_id: { $exists: true } };
1654
+ var PROBE_QUERY = "document";
1630
1655
  function recordId(id) {
1631
1656
  const s = String(id);
1632
1657
  if (PLAIN_ID.test(s)) return s;
@@ -1671,6 +1696,7 @@ var require_pinecone_retriever = __commonJS({
1671
1696
  * @param {string} [options.namespace='rag_documents'] - A namespace this retriever owns
1672
1697
  * @param {'auto'|'documents'|'vectors'} [options.api='auto'] - Document index (Documents API) or vector index; auto reads it from the index description
1673
1698
  * @param {string} [options.vectorField] - The document index's dense_vector field (default: the schema's)
1699
+ * @param {string} [options.textField] - The document index's text field that Pinecone embeds (integrated embedding; default: the schema's, when the index has no dense_vector field)
1674
1700
  * @param {number} [options.freshnessTimeoutMs=30000] - How long a read waits for this retriever's writes to be indexed; 0 to not wait
1675
1701
  * @param {string} [options.cacheKey] - Cache identity; change it when the index changes
1676
1702
  * @param {Function} [options.fetch] - fetch implementation (defaults to the global)
@@ -1683,6 +1709,7 @@ var require_pinecone_retriever = __commonJS({
1683
1709
  namespace = DEFAULT_NAMESPACE,
1684
1710
  api = "auto",
1685
1711
  vectorField,
1712
+ textField,
1686
1713
  freshnessTimeoutMs = 3e4,
1687
1714
  cacheKey,
1688
1715
  fetch: fetchImpl = globalThis.fetch
@@ -1705,6 +1732,16 @@ var require_pinecone_retriever = __commonJS({
1705
1732
  'PineconeRetriever: api must be "auto", "documents" or "vectors"'
1706
1733
  );
1707
1734
  }
1735
+ if (vectorField && textField) {
1736
+ throw new Error(
1737
+ "PineconeRetriever: pass vectorField (your vectors) or textField (Pinecone embeds the text), not both"
1738
+ );
1739
+ }
1740
+ if (textField && api === "vectors") {
1741
+ throw new Error(
1742
+ 'PineconeRetriever: textField needs a document index (api "documents" or "auto")'
1743
+ );
1744
+ }
1708
1745
  if (typeof fetchImpl !== "function") {
1709
1746
  throw new Error("PineconeRetriever needs fetch (Node.js 18 or later)");
1710
1747
  }
@@ -1716,6 +1753,7 @@ var require_pinecone_retriever = __commonJS({
1716
1753
  this._host = host ? normalizeHost(host) : null;
1717
1754
  this._api = api;
1718
1755
  this._vectorField = vectorField;
1756
+ this._textField = textField;
1719
1757
  this._fetch = fetchImpl === globalThis.fetch ? fetchImpl.bind(globalThis) : fetchImpl;
1720
1758
  this._resolving = null;
1721
1759
  this._writeLsn = 0;
@@ -1731,6 +1769,18 @@ var require_pinecone_retriever = __commonJS({
1731
1769
  if (role !== "retriever" || !this.cacheKey) return null;
1732
1770
  return `pinecone|${this._host || this.index}|${this.namespace}|${this.cacheKey}`;
1733
1771
  }
1772
+ /**
1773
+ * Whether the index embeds text itself (integrated embedding): then
1774
+ * store() needs no vectors and retrieve() takes the query text, so no
1775
+ * embedder is needed. The CLI checks this before requiring an embedder.
1776
+ * Reads the index description on first use.
1777
+ * @param {{ signal?: AbortSignal }} [options]
1778
+ * @returns {Promise<boolean>}
1779
+ */
1780
+ async embedsText({ signal } = {}) {
1781
+ await this._resolve(signal);
1782
+ return this._integrated;
1783
+ }
1734
1784
  /**
1735
1785
  * One REST call; resolves the parsed body and the response headers.
1736
1786
  * @private
@@ -1779,12 +1829,21 @@ var require_pinecone_retriever = __commonJS({
1779
1829
  }
1780
1830
  return { json, headers: res.headers };
1781
1831
  }
1832
+ /** True on a document index that embeds text itself. @private */
1833
+ get _integrated() {
1834
+ return this._api === "documents" && Boolean(this._textField);
1835
+ }
1782
1836
  /**
1783
1837
  * The index's host and kind, from its description (once): a document
1784
- * index's dense_vector field and dimension, or a vector index's.
1838
+ * index's dense_vector field and dimension or the text field it embeds,
1839
+ * or a vector index's dimension.
1785
1840
  * @private
1786
1841
  */
1787
1842
  _resolve(signal) {
1843
+ if (this._host && this._textField) {
1844
+ this._api = "documents";
1845
+ return Promise.resolve();
1846
+ }
1788
1847
  if (this._host && this._api !== "auto" && (this._api === "vectors" || this._vectorField)) {
1789
1848
  return Promise.resolve();
1790
1849
  }
@@ -1830,15 +1889,27 @@ var require_pinecone_retriever = __commonJS({
1830
1889
  const fields = info.schema && info.schema.fields || null;
1831
1890
  const named = fields ? Object.entries(fields).filter(([name]) => !name.startsWith("_")) : [];
1832
1891
  const denseFields = named.filter(([, f]) => f && f.type === "dense_vector");
1892
+ const textFields = named.filter(
1893
+ ([, f]) => f && f.type === "string" && f.embed
1894
+ );
1833
1895
  if (this._api === "auto") {
1834
1896
  this._api = named.length > 0 ? "documents" : "vectors";
1835
1897
  }
1836
- if (this._api === "documents") {
1898
+ if (this._textField) {
1899
+ if (this._api !== "documents" || !textFields.some(([name]) => name === this._textField)) {
1900
+ throw new Error(
1901
+ `PineconeRetriever: index "${info.name}" has no text field named "${this._textField}" that it embeds`
1902
+ );
1903
+ }
1904
+ } else if (this._api === "documents") {
1837
1905
  const field = this._vectorField ? denseFields.find(([name]) => name === this._vectorField) : denseFields[0];
1906
+ if (!field && !this._vectorField && textFields.length > 0) {
1907
+ this._textField = textFields[0][0];
1908
+ return;
1909
+ }
1838
1910
  if (!field) {
1839
- const embeds = fields && Object.values(fields).some((f) => f && f.embed);
1840
1911
  throw new Error(
1841
- embeds ? `PineconeRetriever: index "${info.name}" embeds text itself (integrated embedding) and has no dense_vector field for your vectors; create an index with a dense_vector field` : `PineconeRetriever: index "${info.name}" has no dense_vector field${this._vectorField ? ` named "${this._vectorField}"` : ""}`
1912
+ `PineconeRetriever: index "${info.name}" has no dense_vector field${this._vectorField ? ` named "${this._vectorField}"` : ""}`
1842
1913
  );
1843
1914
  }
1844
1915
  this._vectorField = field[0];
@@ -1871,20 +1942,21 @@ var require_pinecone_retriever = __commonJS({
1871
1942
  /**
1872
1943
  * A search, repeated until it reflects this retriever's writes (when the
1873
1944
  * index reports log sequence numbers) or `freshnessTimeoutMs` passes.
1874
- * Resolves `[{ id, score, fields }]`.
1945
+ * `query` is a vector, or the query text on an index that embeds text
1946
+ * itself. Resolves `[{ id, score, fields }]`.
1875
1947
  * @private
1876
1948
  */
1877
- async _search(vector, topK, signal, filter) {
1949
+ async _search(query, topK, signal, filter) {
1878
1950
  await this._resolve(signal);
1879
1951
  const documents = this._api === "documents";
1880
1952
  const [path, body] = documents ? [
1881
1953
  this._docs("search"),
1882
1954
  {
1883
1955
  score_by: [
1884
- {
1956
+ this._integrated ? { type: "embed", fields: [this._textField], query } : {
1885
1957
  type: "dense_vector",
1886
1958
  fields: [this._vectorField],
1887
- values: vector
1959
+ values: query
1888
1960
  }
1889
1961
  ],
1890
1962
  top_k: topK,
@@ -1894,7 +1966,7 @@ var require_pinecone_retriever = __commonJS({
1894
1966
  ] : [
1895
1967
  "/query",
1896
1968
  {
1897
- vector,
1969
+ vector: query,
1898
1970
  topK,
1899
1971
  namespace: this.namespace,
1900
1972
  includeMetadata: true,
@@ -1925,17 +1997,25 @@ var require_pinecone_retriever = __commonJS({
1925
1997
  /** Waits until this retriever's writes are indexed (for list and size). @private */
1926
1998
  async _waitForWrites(signal) {
1927
1999
  if (!this._writeLsn) return;
2000
+ const probe = this._probe();
2001
+ if (probe) await this._search(probe, 1, signal);
2002
+ }
2003
+ /**
2004
+ * What list() and size() search with: a unit vector of the index's
2005
+ * dimension, or a fixed text on an index that embeds text itself.
2006
+ * @private
2007
+ */
2008
+ _probe() {
2009
+ if (this._integrated) return PROBE_QUERY;
1928
2010
  const dimension = this._indexDimension || this._dimension;
1929
- if (!dimension) return;
1930
- await this._search(
1931
- new Array(dimension).fill(0).map((_, i) => i === 0 ? 1 : 0),
1932
- 1,
1933
- signal
1934
- );
2011
+ if (!dimension) return null;
2012
+ return new Array(dimension).fill(0).map((_, i) => i === 0 ? 1 : 0);
1935
2013
  }
1936
2014
  /**
1937
- * Inserts or replaces documents.
1938
- * @param {Array<{ id: string, vector: number[], content?: string, metadata?: Object }>} docs
2015
+ * Inserts or replaces documents. On an index that embeds text itself,
2016
+ * Pinecone embeds `content` (required there) and `vector` is ignored.
2017
+ * @param {Array<{ id: string, vector?: number[], content?: string, metadata?: Object }>} docs
2018
+ * `vector` is required unless the index embeds text itself
1939
2019
  * @returns {Promise<void>}
1940
2020
  */
1941
2021
  async store(docs) {
@@ -1944,6 +2024,7 @@ var require_pinecone_retriever = __commonJS({
1944
2024
  }
1945
2025
  if (docs.length === 0) return;
1946
2026
  await this._resolve();
2027
+ if (this._integrated) return this._storeText(docs);
1947
2028
  const expected = this._indexDimension || this._dimension;
1948
2029
  let dimensions = null;
1949
2030
  const records = docs.map((doc) => {
@@ -2012,29 +2093,93 @@ var require_pinecone_retriever = __commonJS({
2012
2093
  }
2013
2094
  this._dimension = dimensions;
2014
2095
  }
2096
+ /** store() on an index that embeds text itself. @private */
2097
+ async _storeText(docs) {
2098
+ const records = docs.map((doc) => {
2099
+ const id = doc && doc.id;
2100
+ if (typeof id !== "string" || id.length === 0) {
2101
+ throw new Error(
2102
+ "PineconeRetriever.store(): every document needs a string id"
2103
+ );
2104
+ }
2105
+ if (typeof doc.content !== "string" || doc.content.trim() === "") {
2106
+ throw new Error(
2107
+ `PineconeRetriever.store(): document "${id}" has no content for Pinecone to embed (the index embeds ${this._textField})`
2108
+ );
2109
+ }
2110
+ const metadata = doc.metadata || {};
2111
+ return {
2112
+ _id: recordId(id),
2113
+ [this._textField]: doc.content,
2114
+ rag_id: id,
2115
+ rag_content: doc.content,
2116
+ rag_metadata: JSON.stringify(metadata),
2117
+ ...metadata.source !== void 0 && metadata.source !== null && {
2118
+ rag_source: String(metadata.source)
2119
+ }
2120
+ };
2121
+ });
2122
+ const encoder = new TextEncoder();
2123
+ let batch = [];
2124
+ let bytes = 0;
2125
+ const send = async () => {
2126
+ if (batch.length === 0) return;
2127
+ await this._data(
2128
+ "POST",
2129
+ this._docs("upsert"),
2130
+ { documents: batch },
2131
+ { write: true }
2132
+ );
2133
+ batch = [];
2134
+ bytes = 0;
2135
+ };
2136
+ for (const record of records) {
2137
+ const size = encoder.encode(JSON.stringify(record)).length + 1;
2138
+ if (batch.length === EMBED_UPSERT_BATCH || batch.length > 0 && bytes + size > REQUEST_BYTES) {
2139
+ await send();
2140
+ }
2141
+ batch.push(record);
2142
+ bytes += size;
2143
+ }
2144
+ await send();
2145
+ }
2015
2146
  /**
2016
- * The documents most similar to a query vector.
2017
- * @param {number[]|{ queryVector?: number[], topK?: number, signal?: AbortSignal }} request
2018
- * A vector, or an object with one (required; optional in the type to
2019
- * match the retriever contract)
2020
- * @param {number} [k=10] - Results when `request` is a vector
2147
+ * The documents most similar to a query vector, or to the query text on
2148
+ * an index that embeds text itself (Pinecone embeds the query; a
2149
+ * `queryVector` is then ignored).
2150
+ * @param {number[]|string|{ query?: string, queryVector?: number[], topK?: number, signal?: AbortSignal }} request
2151
+ * A vector, the query text, or an object with them: `queryVector`, or
2152
+ * `query` on an index that embeds text itself (required; optional in
2153
+ * the type to match the retriever contract)
2154
+ * @param {number} [k=10] - Results when `request` is a vector or text
2021
2155
  * @returns {Promise<Array<{ id: string, score: number, content: string, metadata: Object }>>}
2022
2156
  */
2023
2157
  async retrieve(request, k = 10) {
2024
- const {
2025
- queryVector,
2026
- topK = k,
2027
- signal
2028
- } = Array.isArray(request) ? { queryVector: request } : request || {};
2158
+ let input = request || {};
2159
+ if (Array.isArray(request)) input = { queryVector: request };
2160
+ else if (typeof request === "string") input = { query: request };
2161
+ const { query, queryVector, topK = k, signal } = input;
2029
2162
  if (!Number.isInteger(topK) || topK < 1) {
2030
2163
  throw new Error(
2031
2164
  "PineconeRetriever.retrieve(): topK must be a positive integer"
2032
2165
  );
2033
2166
  }
2034
- checkVector(queryVector, "query");
2035
2167
  throwIfAborted(signal);
2168
+ await this._resolve(signal);
2169
+ if (!this._integrated) {
2170
+ if (queryVector === void 0 || queryVector === null) {
2171
+ throw new Error(
2172
+ "PineconeRetriever.retrieve(): needs a queryVector; this index does not embed text itself, so use an embedder"
2173
+ );
2174
+ }
2175
+ checkVector(queryVector, "query");
2176
+ } else if (typeof query !== "string" || query.trim() === "") {
2177
+ throw new Error(
2178
+ `PineconeRetriever.retrieve(): the index embeds text itself (integrated embedding, field ${this._textField}), so it needs the query text: pass { query }`
2179
+ );
2180
+ }
2036
2181
  const matches = await this._search(
2037
- queryVector,
2182
+ this._integrated ? query : queryVector,
2038
2183
  Math.min(topK, 1e4),
2039
2184
  signal
2040
2185
  );
@@ -2113,15 +2258,10 @@ var require_pinecone_retriever = __commonJS({
2113
2258
  }
2114
2259
  /** Documents API: fetch by filter, page by page. @private */
2115
2260
  async _listDocuments(limit) {
2116
- const dimension = this._indexDimension || this._dimension;
2117
- if (dimension) {
2261
+ const probe = this._probe();
2262
+ if (probe) {
2118
2263
  const max = Math.min(limit || SEARCH_MAX, SEARCH_MAX);
2119
- const found = await this._search(
2120
- new Array(dimension).fill(0).map((_, i) => i === 0 ? 1 : 0),
2121
- max,
2122
- void 0,
2123
- OURS
2124
- );
2264
+ const found = await this._search(probe, max, void 0, OURS);
2125
2265
  if (found.length < SEARCH_MAX || limit) {
2126
2266
  const seen = /* @__PURE__ */ new Set();
2127
2267
  return found.filter((m) => !seen.has(m.id) && seen.add(m.id)).map((m) => fromFields(m.fields, m.id));
@@ -2540,6 +2680,453 @@ var require_pgvector_retriever = __commonJS({
2540
2680
  }
2541
2681
  });
2542
2682
 
2683
+ // src/retrieval/weaviate-retriever.js
2684
+ var require_weaviate_retriever = __commonJS({
2685
+ "src/retrieval/weaviate-retriever.js"(exports2, module2) {
2686
+ "use strict";
2687
+ var { throwIfAborted } = require_connector_utils();
2688
+ var { pointId } = require_qdrant_retriever();
2689
+ var COLLECTION = /^[A-Za-z][A-Za-z0-9_]{0,254}$/;
2690
+ var DEFAULT_COLLECTION = "RagDocuments";
2691
+ var PAGE = 256;
2692
+ var UPSERT_BATCH = 256;
2693
+ var DELETE_BATCH = 256;
2694
+ var PROPERTIES = [
2695
+ {
2696
+ name: "rag_id",
2697
+ dataType: ["text"],
2698
+ tokenization: "field",
2699
+ indexFilterable: true,
2700
+ indexSearchable: false
2701
+ },
2702
+ { name: "content", dataType: ["text"], indexFilterable: false },
2703
+ {
2704
+ name: "rag_source",
2705
+ dataType: ["text"],
2706
+ tokenization: "field",
2707
+ indexFilterable: true,
2708
+ indexSearchable: false
2709
+ },
2710
+ {
2711
+ name: "rag_metadata",
2712
+ dataType: ["text"],
2713
+ indexFilterable: false,
2714
+ indexSearchable: false
2715
+ }
2716
+ ];
2717
+ var OURS = { path: ["rag_id"], operator: "Like", valueText: "*" };
2718
+ var OURS_GRAPHQL = '{ path: ["rag_id"], operator: Like, valueText: "*" }';
2719
+ function checkVector(vector, id) {
2720
+ if (!Array.isArray(vector) || vector.length === 0) {
2721
+ throw new Error(`WeaviateRetriever: document "${id}" has no vector`);
2722
+ }
2723
+ if (!vector.every((x) => typeof x === "number" && Number.isFinite(x))) {
2724
+ throw new Error(
2725
+ `WeaviateRetriever: document "${id}" has a vector with a non-finite value`
2726
+ );
2727
+ }
2728
+ }
2729
+ function trimSlashes(url) {
2730
+ let end = url.length;
2731
+ while (end > 0 && url[end - 1] === "/") end--;
2732
+ return url.slice(0, end);
2733
+ }
2734
+ function parseMetadata(text) {
2735
+ if (!text) return {};
2736
+ try {
2737
+ const value = JSON.parse(text);
2738
+ return value && typeof value === "object" ? value : {};
2739
+ } catch {
2740
+ return {};
2741
+ }
2742
+ }
2743
+ function errorDetail(json) {
2744
+ const list = json && (json.error || json.errors) || [];
2745
+ if (Array.isArray(list) && list.length) {
2746
+ return list.map((e) => e && e.message || String(e)).join("; ");
2747
+ }
2748
+ return json && json.message || "";
2749
+ }
2750
+ var WeaviateRetriever = class {
2751
+ /**
2752
+ * @param {Object} [options]
2753
+ * @param {string} [options.url='http://localhost:8080'] - Weaviate's REST endpoint (without /v1)
2754
+ * @param {string} [options.apiKey] - Sent as `Authorization: Bearer <key>`
2755
+ * @param {string} [options.collection='RagDocuments'] - Class name; the first letter is upper-cased, as Weaviate does
2756
+ * @param {number} [options.dimensions] - Expected vector size; defaults to the collection's or the first stored vector's
2757
+ * @param {boolean} [options.createCollection=true] - Create the collection (and any missing properties) on first store
2758
+ * @param {string} [options.cacheKey] - Cache identity; change it when the collection changes
2759
+ * @param {Function} [options.fetch] - fetch implementation (defaults to the global)
2760
+ */
2761
+ constructor(options = {}) {
2762
+ const {
2763
+ url = "http://localhost:8080",
2764
+ apiKey,
2765
+ collection = DEFAULT_COLLECTION,
2766
+ dimensions,
2767
+ createCollection = true,
2768
+ cacheKey,
2769
+ fetch: fetchImpl = globalThis.fetch
2770
+ } = options;
2771
+ if (typeof collection !== "string" || !COLLECTION.test(collection)) {
2772
+ throw new Error(
2773
+ `WeaviateRetriever: invalid collection name "${collection}" (a letter, then letters, digits and "_")`
2774
+ );
2775
+ }
2776
+ if (dimensions !== void 0 && !(Number.isInteger(dimensions) && dimensions > 0)) {
2777
+ throw new Error(
2778
+ "WeaviateRetriever: `dimensions` must be a positive integer"
2779
+ );
2780
+ }
2781
+ if (typeof fetchImpl !== "function") {
2782
+ throw new Error("WeaviateRetriever needs fetch (Node.js 18 or later)");
2783
+ }
2784
+ this.url = trimSlashes(String(url));
2785
+ this.collection = collection[0].toUpperCase() + collection.slice(1);
2786
+ this.dimensions = dimensions;
2787
+ this.createCollection = createCollection;
2788
+ this.cacheKey = cacheKey;
2789
+ this._apiKey = apiKey;
2790
+ this._fetch = fetchImpl === globalThis.fetch ? fetchImpl.bind(globalThis) : fetchImpl;
2791
+ this._ready = null;
2792
+ this._exists = false;
2793
+ }
2794
+ /**
2795
+ * Cache identity for createRagPipeline({ cache }): none unless you set
2796
+ * `cacheKey`, since other clients can change the collection.
2797
+ * @param {'retriever'} role
2798
+ * @returns {string|null}
2799
+ */
2800
+ getCacheKey(role) {
2801
+ if (role !== "retriever" || !this.cacheKey) return null;
2802
+ return `weaviate|${this.url}|${this.collection}|${this.cacheKey}`;
2803
+ }
2804
+ /** The message without the API key, should a server ever echo it. */
2805
+ _redact(message) {
2806
+ const text = String(message);
2807
+ return this._apiKey ? text.split(this._apiKey).join("***") : text;
2808
+ }
2809
+ /**
2810
+ * One REST call; resolves the parsed body.
2811
+ * @private
2812
+ * @param {string} method
2813
+ * @param {string} path - After /v1
2814
+ * @param {Object} [body]
2815
+ * @param {{ signal?: AbortSignal, allow404?: boolean }} [options] - allow404 resolves null on 404
2816
+ * @returns {Promise<any>}
2817
+ */
2818
+ async _request(method, path, body, { signal, allow404 = false } = {}) {
2819
+ throwIfAborted(signal);
2820
+ const headers = { "content-type": "application/json" };
2821
+ if (this._apiKey) headers.authorization = `Bearer ${this._apiKey}`;
2822
+ let res;
2823
+ try {
2824
+ res = await this._fetch(`${this.url}/v1${path}`, {
2825
+ method,
2826
+ headers,
2827
+ ...body !== void 0 && { body: JSON.stringify(body) },
2828
+ ...signal && { signal }
2829
+ });
2830
+ } catch (err) {
2831
+ throwIfAborted(signal);
2832
+ throw new Error(
2833
+ this._redact(
2834
+ `WeaviateRetriever: cannot reach Weaviate at ${this.url} (${err.message})`
2835
+ )
2836
+ );
2837
+ }
2838
+ if (res.status === 404 && allow404) return null;
2839
+ const text = await res.text();
2840
+ let json;
2841
+ try {
2842
+ json = text ? JSON.parse(text) : {};
2843
+ } catch {
2844
+ json = {};
2845
+ }
2846
+ if (!res.ok) {
2847
+ if (res.status === 401 || res.status === 403) {
2848
+ throw new Error(
2849
+ `WeaviateRetriever: Weaviate refused the request (${res.status}); check WEAVIATE_API_KEY`
2850
+ );
2851
+ }
2852
+ const detail = errorDetail(json) || text || res.statusText;
2853
+ const where = path.split("?")[0];
2854
+ throw new Error(
2855
+ this._redact(
2856
+ `WeaviateRetriever: ${method} ${where} failed (${res.status}): ${detail}`
2857
+ )
2858
+ );
2859
+ }
2860
+ return json;
2861
+ }
2862
+ /** A GraphQL query; resolves `data`, throws on GraphQL errors. */
2863
+ async _graphql(query, signal) {
2864
+ const json = await this._request("POST", "/graphql", { query }, { signal });
2865
+ if (json.errors && json.errors.length) {
2866
+ throw new Error(
2867
+ this._redact(
2868
+ `WeaviateRetriever: GraphQL query failed: ${errorDetail(json)}`
2869
+ )
2870
+ );
2871
+ }
2872
+ return json.data || {};
2873
+ }
2874
+ /** The collection's schema, or null when it doesn't exist. */
2875
+ _schema(signal) {
2876
+ return this._request("GET", `/schema/${this.collection}`, void 0, {
2877
+ signal,
2878
+ allow404: true
2879
+ });
2880
+ }
2881
+ /** Whether the collection exists; only a yes is remembered. */
2882
+ async _collectionExists(signal) {
2883
+ if (!this._exists) this._exists = Boolean(await this._schema(signal));
2884
+ return this._exists;
2885
+ }
2886
+ _ensureCollection() {
2887
+ if (!this._ready) {
2888
+ this._ready = (async () => {
2889
+ if (this.createCollection) {
2890
+ const schema = await this._schema();
2891
+ if (!schema) {
2892
+ try {
2893
+ await this._request("POST", "/schema", {
2894
+ class: this.collection,
2895
+ vectorizer: "none",
2896
+ vectorIndexType: "hnsw",
2897
+ vectorIndexConfig: { distance: "cosine" },
2898
+ properties: PROPERTIES
2899
+ });
2900
+ } catch (err) {
2901
+ if (!/already exists/i.test(err.message)) throw err;
2902
+ }
2903
+ } else {
2904
+ const have = new Set((schema.properties || []).map((p) => p.name));
2905
+ for (const property of PROPERTIES) {
2906
+ if (!have.has(property.name)) {
2907
+ await this._request(
2908
+ "POST",
2909
+ `/schema/${this.collection}/properties`,
2910
+ property
2911
+ );
2912
+ }
2913
+ }
2914
+ }
2915
+ this._exists = true;
2916
+ }
2917
+ if (!this.dimensions) {
2918
+ const page = await this._request(
2919
+ "GET",
2920
+ `/objects?class=${this.collection}&limit=1&include=vector`,
2921
+ void 0,
2922
+ { allow404: true }
2923
+ );
2924
+ const vector = page && page.objects && page.objects[0] && page.objects[0].vector;
2925
+ if (Array.isArray(vector) && vector.length) {
2926
+ this.dimensions = vector.length;
2927
+ }
2928
+ }
2929
+ })().catch((err) => {
2930
+ this._ready = null;
2931
+ throw err;
2932
+ });
2933
+ }
2934
+ return this._ready;
2935
+ }
2936
+ /**
2937
+ * Inserts or replaces documents.
2938
+ * @param {Array<{ id: string, vector: number[], content?: string, metadata?: Object }>} docs
2939
+ * @returns {Promise<void>}
2940
+ */
2941
+ async store(docs) {
2942
+ if (!Array.isArray(docs))
2943
+ throw new Error("WeaviateRetriever.store() takes an array");
2944
+ if (docs.length === 0) return;
2945
+ let dimensions = null;
2946
+ const objects = docs.map((doc) => {
2947
+ const id = doc && doc.id;
2948
+ if (typeof id !== "string" || id.length === 0) {
2949
+ throw new Error(
2950
+ "WeaviateRetriever.store(): every document needs a string id"
2951
+ );
2952
+ }
2953
+ checkVector(doc.vector, id);
2954
+ if (dimensions === null) dimensions = doc.vector.length;
2955
+ else if (doc.vector.length !== dimensions) {
2956
+ throw new Error(
2957
+ `WeaviateRetriever.store(): document "${id}" has ${doc.vector.length} dimensions, the batch has ${dimensions}`
2958
+ );
2959
+ }
2960
+ const metadata = doc.metadata || {};
2961
+ const properties = {
2962
+ rag_id: id,
2963
+ content: doc.content || "",
2964
+ rag_metadata: JSON.stringify(metadata)
2965
+ };
2966
+ if (metadata.source !== void 0 && metadata.source !== null) {
2967
+ properties.rag_source = String(metadata.source);
2968
+ }
2969
+ return {
2970
+ class: this.collection,
2971
+ id: pointId(id),
2972
+ vector: doc.vector,
2973
+ properties
2974
+ };
2975
+ });
2976
+ await this._ensureCollection();
2977
+ if (this.dimensions && dimensions !== this.dimensions) {
2978
+ throw new Error(
2979
+ `WeaviateRetriever.store(): the documents have ${dimensions} dimensions, the collection has ${this.dimensions}`
2980
+ );
2981
+ }
2982
+ for (let i = 0; i < objects.length; i += UPSERT_BATCH) {
2983
+ const batch = objects.slice(i, i + UPSERT_BATCH);
2984
+ const results = await this._request("POST", "/batch/objects", {
2985
+ objects: batch
2986
+ });
2987
+ for (const [j, item] of (Array.isArray(results) ? results : []).entries()) {
2988
+ const detail = item && item.result && errorDetail(item.result.errors);
2989
+ if (!detail) continue;
2990
+ const id = batch[j] ? batch[j].properties.rag_id : "?";
2991
+ const kind = /vectors? with length/i.test(detail) ? "has the wrong vector dimensions" : "was not stored";
2992
+ throw new Error(
2993
+ this._redact(
2994
+ `WeaviateRetriever.store(): document "${id}" ${kind}: ${detail}`
2995
+ )
2996
+ );
2997
+ }
2998
+ }
2999
+ this._exists = true;
3000
+ if (!this.dimensions) this.dimensions = dimensions;
3001
+ }
3002
+ /**
3003
+ * The documents most similar to a query vector.
3004
+ * @param {number[]|{ queryVector?: number[], topK?: number, signal?: AbortSignal }} request
3005
+ * A vector, or an object with one (required; optional in the type to
3006
+ * match the retriever contract)
3007
+ * @param {number} [k=10] - Results when `request` is a vector
3008
+ * @returns {Promise<Array<{ id: string, score: number, content: string, metadata: Object }>>}
3009
+ */
3010
+ async retrieve(request, k = 10) {
3011
+ const {
3012
+ queryVector,
3013
+ topK = k,
3014
+ signal
3015
+ } = Array.isArray(request) ? { queryVector: request } : request || {};
3016
+ if (!Number.isInteger(topK) || topK < 1) {
3017
+ throw new Error(
3018
+ "WeaviateRetriever.retrieve(): topK must be a positive integer"
3019
+ );
3020
+ }
3021
+ checkVector(queryVector, "query");
3022
+ throwIfAborted(signal);
3023
+ if (!await this._collectionExists(signal)) return [];
3024
+ const data = await this._graphql(
3025
+ `{ Get { ${this.collection}(nearVector: { vector: ${JSON.stringify(queryVector)} }, limit: ${topK}, where: ${OURS_GRAPHQL}) { rag_id content rag_metadata _additional { distance } } } }`,
3026
+ signal
3027
+ );
3028
+ throwIfAborted(signal);
3029
+ const hits = data.Get && data.Get[this.collection] || [];
3030
+ return hits.map((hit) => ({
3031
+ id: hit.rag_id,
3032
+ score: 1 - hit._additional.distance,
3033
+ content: hit.content || "",
3034
+ metadata: parseMetadata(hit.rag_metadata)
3035
+ }));
3036
+ }
3037
+ /**
3038
+ * Deletes the matching objects, a server-limited round at a time.
3039
+ * @private
3040
+ */
3041
+ async _deleteWhere(where) {
3042
+ if (!await this._collectionExists()) return;
3043
+ for (; ; ) {
3044
+ const json = await this._request("DELETE", "/batch/objects", {
3045
+ match: { class: this.collection, where },
3046
+ output: "minimal"
3047
+ });
3048
+ const r = json && json.results || {};
3049
+ if (r.failed > 0) {
3050
+ throw new Error(
3051
+ `WeaviateRetriever: ${r.failed} objects could not be deleted`
3052
+ );
3053
+ }
3054
+ if (!(r.limit > 0 && r.matches > r.limit && r.successful > 0)) return;
3055
+ }
3056
+ }
3057
+ /**
3058
+ * Deletes the chunks of one source (`metadata.source`), for re-ingesting.
3059
+ * @param {string} source
3060
+ */
3061
+ async removeBySource(source) {
3062
+ await this._deleteWhere({
3063
+ path: ["rag_source"],
3064
+ operator: "Equal",
3065
+ valueText: String(source)
3066
+ });
3067
+ }
3068
+ /**
3069
+ * Deletes documents by id.
3070
+ * @param {string[]} ids
3071
+ */
3072
+ async delete(ids) {
3073
+ for (let i = 0; i < ids.length; i += DELETE_BATCH) {
3074
+ await this._deleteWhere({
3075
+ path: ["rag_id"],
3076
+ operator: "ContainsAny",
3077
+ valueTextArray: ids.slice(i, i + DELETE_BATCH).map(String)
3078
+ });
3079
+ }
3080
+ }
3081
+ /** Deletes every document this class stored; the collection stays. */
3082
+ async clear() {
3083
+ await this._deleteWhere(OURS);
3084
+ }
3085
+ /** @returns {Promise<number>} */
3086
+ async size() {
3087
+ if (!await this._collectionExists()) return 0;
3088
+ const data = await this._graphql(
3089
+ `{ Aggregate { ${this.collection}(where: ${OURS_GRAPHQL}) { meta { count } } } }`
3090
+ );
3091
+ const rows = data.Aggregate && data.Aggregate[this.collection] || [];
3092
+ return rows[0] && rows[0].meta ? rows[0].meta.count : 0;
3093
+ }
3094
+ /**
3095
+ * Every document, without vectors.
3096
+ * @param {{ limit?: number }} [options]
3097
+ * @returns {Promise<Array<{ id: string, content: string, metadata: Object }>>}
3098
+ */
3099
+ async list({ limit } = {}) {
3100
+ const out = [];
3101
+ if (!await this._collectionExists()) return out;
3102
+ let after = "";
3103
+ for (; ; ) {
3104
+ const page = await this._request(
3105
+ "GET",
3106
+ `/objects?class=${this.collection}&limit=${PAGE}&after=${encodeURIComponent(after)}`,
3107
+ void 0,
3108
+ { allow404: true }
3109
+ );
3110
+ const objects = page && page.objects || [];
3111
+ for (const o of objects) {
3112
+ const p = o.properties || {};
3113
+ if (typeof p.rag_id !== "string") continue;
3114
+ out.push({
3115
+ id: p.rag_id,
3116
+ content: p.content || "",
3117
+ metadata: parseMetadata(p.rag_metadata)
3118
+ });
3119
+ if (limit && out.length >= limit) return out;
3120
+ }
3121
+ if (objects.length < PAGE) return out;
3122
+ after = objects[objects.length - 1].id;
3123
+ }
3124
+ }
3125
+ };
3126
+ module2.exports = { WeaviateRetriever };
3127
+ }
3128
+ });
3129
+
2543
3130
  // src/adapters/ai-sdk.js
2544
3131
  var require_ai_sdk = __commonJS({
2545
3132
  "src/adapters/ai-sdk.js"(exports2, module2) {
@@ -2700,6 +3287,7 @@ var require_connectors = __commonJS({
2700
3287
  var { QdrantRetriever } = require_qdrant_retriever();
2701
3288
  var { PineconeRetriever } = require_pinecone_retriever();
2702
3289
  var { PgVectorRetriever } = require_pgvector_retriever();
3290
+ var { WeaviateRetriever } = require_weaviate_retriever();
2703
3291
  var { createAiSdkAdapter } = require_ai_sdk();
2704
3292
  module2.exports = {
2705
3293
  OpenAIConnector,
@@ -2710,6 +3298,7 @@ var require_connectors = __commonJS({
2710
3298
  QdrantRetriever,
2711
3299
  PineconeRetriever,
2712
3300
  PgVectorRetriever,
3301
+ WeaviateRetriever,
2713
3302
  createAiSdkAdapter
2714
3303
  };
2715
3304
  }
@@ -2729,7 +3318,8 @@ var modules = {
2729
3318
  "packages/connectors.js": () => require_connectors(),
2730
3319
  "retrieval/pgvector-retriever.js": () => require_pgvector_retriever(),
2731
3320
  "retrieval/pinecone-retriever.js": () => require_pinecone_retriever(),
2732
- "retrieval/qdrant-retriever.js": () => require_qdrant_retriever()
3321
+ "retrieval/qdrant-retriever.js": () => require_qdrant_retriever(),
3322
+ "retrieval/weaviate-retriever.js": () => require_weaviate_retriever()
2733
3323
  };
2734
3324
  Object.defineProperty(mod, Symbol.for("@devilsdev/rag-connectors/internal"), {
2735
3325
  value: (file) => {
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { OpenAIConnector, AnthropicConnector, CohereConnector, OllamaConnector, LocalEmbedder, QdrantRetriever, PineconeRetriever, PgVectorRetriever, createAiSdkAdapter } from "@devilsdev/rag-core/all-types";
1
+ export { OpenAIConnector, AnthropicConnector, CohereConnector, OllamaConnector, LocalEmbedder, QdrantRetriever, PineconeRetriever, PgVectorRetriever, WeaviateRetriever, createAiSdkAdapter } from "@devilsdev/rag-core/all-types";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'module';
2
2
  const require = createRequire(import.meta.url);
3
3
  const mod = require('./index.cjs');
4
- export const { OpenAIConnector, AnthropicConnector, CohereConnector, OllamaConnector, LocalEmbedder, QdrantRetriever, PineconeRetriever, PgVectorRetriever, createAiSdkAdapter } = mod;
4
+ export const { OpenAIConnector, AnthropicConnector, CohereConnector, OllamaConnector, LocalEmbedder, QdrantRetriever, PineconeRetriever, PgVectorRetriever, WeaviateRetriever, createAiSdkAdapter } = mod;
5
5
  export default mod;
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "@devilsdev/rag-connectors",
3
- "version": "3.6.0",
4
- "description": "Embedders, LLMs and vector stores for @devilsdev/rag-core: OpenAI, Anthropic, Cohere, Ollama, the AI SDK, Qdrant, pgvector and Pinecone.",
3
+ "version": "3.7.0",
4
+ "description": "Embedders, LLMs and vector stores for @devilsdev/rag-core: OpenAI, Anthropic, Cohere, Ollama, the AI SDK, Qdrant, pgvector, Pinecone and Weaviate.",
5
5
  "keywords": [
6
6
  "rag",
7
7
  "openai",
8
8
  "anthropic",
9
9
  "ollama",
10
10
  "qdrant",
11
- "pgvector"
11
+ "pgvector",
12
+ "weaviate"
12
13
  ],
13
14
  "license": "MIT",
14
15
  "author": "Ali Kahwaji",
15
16
  "homepage": "https://devilsdev.github.io/rag-pipeline-utils/",
16
17
  "repository": {
17
18
  "type": "git",
18
- "url": "https://github.com/DevilsDev/rag-pipeline-utils.git"
19
+ "url": "git+https://github.com/DevilsDev/rag-pipeline-utils.git"
19
20
  },
20
21
  "bugs": {
21
22
  "url": "https://github.com/DevilsDev/rag-pipeline-utils/issues"
@@ -44,7 +45,7 @@
44
45
  "access": "public"
45
46
  },
46
47
  "dependencies": {
47
- "@devilsdev/rag-core": "3.6.0",
48
+ "@devilsdev/rag-core": "3.7.0",
48
49
  "axios": "^1.21.0"
49
50
  },
50
51
  "peerDependencies": {