From cf0b24af0275bf8cb821be15462976c9149fe024 Mon Sep 17 00:00:00 2001
From: Timothy Carambat <rambat1010@gmail.com>
Date: Tue, 15 Aug 2023 15:26:44 -0700
Subject: [PATCH] Add Qdrant support for embedding, chat, and conversation
 (#192)

* Add Qdrant support for embedding, chat, and conversation

* Change comments
---
 .vscode/settings.json                         |   1 +
 docker/.env.example                           |   5 +
 .../Modals/Settings/VectorDbs/index.jsx       |  45 ++
 frontend/src/media/vectordbs/qdrant.png       | Bin 0 -> 15073 bytes
 server/.env.example                           |   5 +
 server/endpoints/system.js                    |   6 +
 server/package.json                           |   1 +
 server/utils/helpers/index.js                 |   3 +
 server/utils/helpers/updateENV.js             |  10 +-
 .../utils/vectorDbProviders/qdrant/index.js   | 397 ++++++++++++++++++
 server/yarn.lock                              |  28 +-
 11 files changed, 499 insertions(+), 2 deletions(-)
 create mode 100644 frontend/src/media/vectordbs/qdrant.png
 create mode 100644 server/utils/vectorDbProviders/qdrant/index.js

diff --git a/.vscode/settings.json b/.vscode/settings.json
index c8c7ea995..dde2d134b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,7 @@
 {
   "cSpell.words": [
     "openai",
+    "Qdrant",
     "Weaviate"
   ]
 }
\ No newline at end of file
diff --git a/docker/.env.example b/docker/.env.example
index 77550b6f0..70c61ef96 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -37,6 +37,11 @@ PINECONE_INDEX=
 # WEAVIATE_ENDPOINT="http://localhost:8080"
 # WEAVIATE_API_KEY=
 
+# Enable all below if you are using vector database: Qdrant.
+# VECTOR_DB="qdrant"
+# QDRANT_ENDPOINT="http://localhost:6333"
+# QDRANT_API_KEY=
+
 # CLOUD DEPLOYMENT VARIRABLES ONLY
 # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
 # NO_DEBUG="true"
diff --git a/frontend/src/components/Modals/Settings/VectorDbs/index.jsx b/frontend/src/components/Modals/Settings/VectorDbs/index.jsx
index b1a5a97b5..ec25e0a7e 100644
--- a/frontend/src/components/Modals/Settings/VectorDbs/index.jsx
+++ b/frontend/src/components/Modals/Settings/VectorDbs/index.jsx
@@ -4,6 +4,7 @@ import ChromaLogo from "../../../../media/vectordbs/chroma.png";
 import PineconeLogo from "../../../../media/vectordbs/pinecone.png";
 import LanceDbLogo from "../../../../media/vectordbs/lancedb.png";
 import WeaviateLogo from "../../../../media/vectordbs/weaviate.png";
+import QDrantLogo from "../../../../media/vectordbs/qdrant.png";
 
 const noop = () => false;
 export default function VectorDBSelection({
@@ -80,6 +81,15 @@ export default function VectorDBSelection({
                   image={PineconeLogo}
                   onClick={updateVectorChoice}
                 />
+                <VectorDBOption
+                  name="QDrant"
+                  value="qdrant"
+                  link="qdrant.tech"
+                  description="Open source local and distributed cloud vector database."
+                  checked={vectorDB === "qdrant"}
+                  image={QDrantLogo}
+                  onClick={updateVectorChoice}
+                />
                 <VectorDBOption
                   name="Weaviate"
                   value="weaviate"
@@ -181,6 +191,41 @@ export default function VectorDBSelection({
                   </p>
                 </div>
               )}
+              {vectorDB === "qdrant" && (
+                <>
+                  <div>
+                    <label className="block mb-2 text-sm font-medium text-gray-800 dark:text-slate-200">
+                      QDrant API Endpoint
+                    </label>
+                    <input
+                      type="url"
+                      name="QdrantEndpoint"
+                      disabled={!canDebug}
+                      className="bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block w-full p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200"
+                      placeholder="http://localhost:6633"
+                      defaultValue={settings?.QdrantEndpoint}
+                      required={true}
+                      autoComplete="off"
+                      spellCheck={false}
+                    />
+                  </div>
+                  <div>
+                    <label className="block mb-2 text-sm font-medium text-gray-800 dark:text-slate-200">
+                      Api Key
+                    </label>
+                    <input
+                      type="password"
+                      name="QdrantApiKey"
+                      disabled={!canDebug}
+                      className="bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block w-full p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200"
+                      placeholder="wOeqxsYP4....1244sba"
+                      defaultValue={settings?.QdrantApiKey}
+                      autoComplete="off"
+                      spellCheck={false}
+                    />
+                  </div>
+                </>
+              )}
               {vectorDB === "weaviate" && (
                 <>
                   <div>
diff --git a/frontend/src/media/vectordbs/qdrant.png b/frontend/src/media/vectordbs/qdrant.png
new file mode 100644
index 0000000000000000000000000000000000000000..d63e720c5402f3abd7246ffdd0ad6f853063c2a5
GIT binary patch
literal 15073
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

literal 0
HcmV?d00001

diff --git a/server/.env.example b/server/.env.example
index 606dd8988..ff92295ec 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -36,6 +36,11 @@ PINECONE_INDEX=
 # WEAVIATE_ENDPOINT="http://localhost:8080"
 # WEAVIATE_API_KEY=
 
+# Enable all below if you are using vector database: Qdrant.
+# VECTOR_DB="qdrant"
+# QDRANT_ENDPOINT="http://localhost:6333"
+# QDRANT_API_KEY=
+
 
 # CLOUD DEPLOYMENT VARIRABLES ONLY
 # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 73041e183..d0b48cc62 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -78,6 +78,12 @@ function systemEndpoints(app) {
               WeaviateApiKey: process.env.WEAVIATE_API_KEY,
             }
           : {}),
+        ...(vectorDB === "qdrant"
+          ? {
+              QdrantEndpoint: process.env.QDRANT_ENDPOINT,
+              QdrantApiKey: process.env.QDRANT_API_KEY,
+            }
+          : {}),
         LLMProvider: llmProvider,
         ...(llmProvider === "openai"
           ? {
diff --git a/server/package.json b/server/package.json
index b2f5bdc8a..15bbff6f6 100644
--- a/server/package.json
+++ b/server/package.json
@@ -18,6 +18,7 @@
     "@azure/openai": "^1.0.0-beta.3",
     "@googleapis/youtube": "^9.0.0",
     "@pinecone-database/pinecone": "^0.1.6",
+    "@qdrant/js-client-rest": "^1.4.0",
     "archiver": "^5.3.1",
     "bcrypt": "^5.1.0",
     "body-parser": "^1.20.2",
diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js
index b7fb5ae00..b077606ad 100644
--- a/server/utils/helpers/index.js
+++ b/server/utils/helpers/index.js
@@ -13,6 +13,9 @@ function getVectorDbClass() {
     case "weaviate":
       const { Weaviate } = require("../vectorDbProviders/weaviate");
       return Weaviate;
+    case "qdrant":
+      const { QDrant } = require("../vectorDbProviders/qdrant");
+      return QDrant;
     default:
       throw new Error("ENV: No VECTOR_DB value found in environment!");
   }
diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js
index 9f00ec423..d08f25c7a 100644
--- a/server/utils/helpers/updateENV.js
+++ b/server/utils/helpers/updateENV.js
@@ -47,6 +47,14 @@ const KEY_MAPPING = {
     envKey: "WEAVIATE_API_KEY",
     checks: [],
   },
+  QdrantEndpoint: {
+    envKey: "QDRANT_ENDPOINT",
+    checks: [isValidURL],
+  },
+  QdrantApiKey: {
+    envKey: "QDRANT_API_KEY",
+    checks: [],
+  },
 
   PineConeEnvironment: {
     envKey: "PINECONE_ENVIRONMENT",
@@ -112,7 +120,7 @@ function validOpenAIModel(input = "") {
 }
 
 function supportedVectorDB(input = "") {
-  const supported = ["chroma", "pinecone", "lancedb", "weaviate"];
+  const supported = ["chroma", "pinecone", "lancedb", "weaviate", "qdrant"];
   return supported.includes(input)
     ? null
     : `Invalid VectorDB type. Must be one of ${supported.join(", ")}.`;
diff --git a/server/utils/vectorDbProviders/qdrant/index.js b/server/utils/vectorDbProviders/qdrant/index.js
new file mode 100644
index 000000000..0dc39e790
--- /dev/null
+++ b/server/utils/vectorDbProviders/qdrant/index.js
@@ -0,0 +1,397 @@
+const { QdrantClient } = require("@qdrant/js-client-rest");
+const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter");
+const { storeVectorResult, cachedVectorInformation } = require("../../files");
+const { v4: uuidv4 } = require("uuid");
+const { toChunks, getLLMProvider } = require("../../helpers");
+const { chatPrompt } = require("../../chats");
+
+const QDrant = {
+  name: "QDrant",
+  connect: async function () {
+    if (process.env.VECTOR_DB !== "qdrant")
+      throw new Error("QDrant::Invalid ENV settings");
+
+    const client = new QdrantClient({
+      url: process.env.QDRANT_ENDPOINT,
+      ...(process.env.QDRANT_API_KEY
+        ? { apiKey: process.env.QDRANT_API_KEY }
+        : {}),
+    });
+
+    const isAlive = (await client.api("cluster")?.clusterStatus())?.ok || false;
+    if (!isAlive)
+      throw new Error(
+        "QDrant::Invalid Heartbeat received - is the instance online?"
+      );
+
+    return { client };
+  },
+  heartbeat: async function () {
+    await this.connect();
+    return { heartbeat: Number(new Date()) };
+  },
+  totalIndicies: async function () {
+    const { client } = await this.connect();
+    const { collections } = await client.getCollections();
+    var totalVectors = 0;
+    for (const collection of collections) {
+      if (!collection || !collection.name) continue;
+      totalVectors +=
+        (await this.namespace(client, collection.name))?.vectorCount || 0;
+    }
+    return totalVectors;
+  },
+  namespaceCount: async function (_namespace = null) {
+    const { client } = await this.connect();
+    const namespace = await this.namespace(client, _namespace);
+    return namespace?.vectorCount || 0;
+  },
+  similarityResponse: async function (_client, namespace, queryVector) {
+    const { client } = await this.connect();
+    const result = {
+      contextTexts: [],
+      sourceDocuments: [],
+    };
+
+    const responses = await client.search(namespace, {
+      vector: queryVector,
+      limit: 4,
+    });
+
+    responses.forEach((response) => {
+      result.contextTexts.push(response?.payload?.text || "");
+      result.sourceDocuments.push({
+        ...(response?.payload || {}),
+        id: response.id,
+      });
+    });
+
+    return result;
+  },
+  namespace: async function (client, namespace = null) {
+    if (!namespace) throw new Error("No namespace value provided.");
+    const collection = await client.getCollection(namespace).catch(() => null);
+    if (!collection) return null;
+
+    return {
+      name: namespace,
+      ...collection,
+      vectorCount: collection.vectors_count,
+    };
+  },
+  hasNamespace: async function (namespace = null) {
+    if (!namespace) return false;
+    const { client } = await this.connect();
+    return await this.namespaceExists(client, namespace);
+  },
+  namespaceExists: async function (client, namespace = null) {
+    if (!namespace) throw new Error("No namespace value provided.");
+    const collection = await client.getCollection(namespace).catch((e) => {
+      console.error("QDrant::namespaceExists", e.message);
+      return null;
+    });
+    return !!collection;
+  },
+  deleteVectorsInNamespace: async function (client, namespace = null) {
+    await client.deleteCollection(namespace);
+    return true;
+  },
+  getOrCreateCollection: async function (client, namespace) {
+    if (await this.namespaceExists(client, namespace)) {
+      return await client.getCollection(namespace);
+    }
+    await client.createCollection(namespace, {
+      vectors: {
+        size: 1536, //TODO: Fixed to OpenAI models - when other embeddings exist make variable.
+        distance: "Cosine",
+      },
+    });
+    return await client.getCollection(namespace);
+  },
+  addDocumentToNamespace: async function (
+    namespace,
+    documentData = {},
+    fullFilePath = null
+  ) {
+    const { DocumentVectors } = require("../../../models/vectors");
+    try {
+      const { pageContent, docId, ...metadata } = documentData;
+      if (!pageContent || pageContent.length == 0) return false;
+
+      console.log("Adding new vectorized document into namespace", namespace);
+      const cacheResult = await cachedVectorInformation(fullFilePath);
+      if (cacheResult.exists) {
+        const { client } = await this.connect();
+        const collection = await this.getOrCreateCollection(client, namespace);
+        if (!collection)
+          throw new Error("Failed to create new QDrant collection!", {
+            namespace,
+          });
+
+        const { chunks } = cacheResult;
+        const documentVectors = [];
+
+        for (const chunk of chunks) {
+          const submission = {
+            ids: [],
+            vectors: [],
+            payloads: [],
+          };
+
+          // Before sending to Qdrant and saving the records to our db
+          // we need to assign the id of each chunk that is stored in the cached file.
+          chunk.forEach((chunk) => {
+            const id = uuidv4();
+            const { id: _id, ...payload } = chunk.payload;
+            documentVectors.push({ docId, vectorId: id });
+            submission.ids.push(id);
+            submission.vectors.push(chunk.vector);
+            submission.payloads.push(payload);
+          });
+
+          const additionResult = await client.upsert(namespace, {
+            wait: true,
+            batch: { ...submission },
+          });
+          if (additionResult?.status !== "completed")
+            throw new Error("Error embedding into QDrant", additionResult);
+        }
+
+        await DocumentVectors.bulkInsert(documentVectors);
+        return true;
+      }
+
+      // If we are here then we are going to embed and store a novel document.
+      // We have to do this manually as opposed to using LangChains `Qdrant.fromDocuments`
+      // because we then cannot atomically control our namespace to granularly find/remove documents
+      // from vectordb.
+      const textSplitter = new RecursiveCharacterTextSplitter({
+        chunkSize: 1000,
+        chunkOverlap: 20,
+      });
+      const textChunks = await textSplitter.splitText(pageContent);
+
+      console.log("Chunks created from document:", textChunks.length);
+      const LLMConnector = getLLMProvider();
+      const documentVectors = [];
+      const vectors = [];
+      const vectorValues = await LLMConnector.embedChunks(textChunks);
+      const submission = {
+        ids: [],
+        vectors: [],
+        payloads: [],
+      };
+
+      if (!!vectorValues && vectorValues.length > 0) {
+        for (const [i, vector] of vectorValues.entries()) {
+          const vectorRecord = {
+            id: uuidv4(),
+            vector: vector,
+            // [DO NOT REMOVE]
+            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.
+            // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64
+            payload: { ...metadata, text: textChunks[i] },
+          };
+
+          submission.ids.push(vectorRecord.id);
+          submission.vectors.push(vectorRecord.vector);
+          submission.payloads.push(vectorRecord.payload);
+
+          vectors.push(vectorRecord);
+          documentVectors.push({ docId, vectorId: vectorRecord.id });
+        }
+      } else {
+        console.error(
+          "Could not use OpenAI to embed document chunks! This document will not be recorded."
+        );
+      }
+
+      const { client } = await this.connect();
+      const collection = await this.getOrCreateCollection(client, namespace);
+      if (!collection)
+        throw new Error("Failed to create new QDrant collection!", {
+          namespace,
+        });
+
+      if (vectors.length > 0) {
+        const chunks = [];
+
+        console.log("Inserting vectorized chunks into QDrant collection.");
+        for (const chunk of toChunks(vectors, 500)) chunks.push(chunk);
+
+        const additionResult = await client.upsert(namespace, {
+          wait: true,
+          batch: {
+            ids: submission.ids,
+            vectors: submission.vectors,
+            payloads: submission.payloads,
+          },
+        });
+        if (additionResult?.status !== "completed")
+          throw new Error("Error embedding into QDrant", additionResult);
+
+        await storeVectorResult(chunks, fullFilePath);
+      }
+
+      await DocumentVectors.bulkInsert(documentVectors);
+      return true;
+    } catch (e) {
+      console.error("addDocumentToNamespace", e.message);
+      return false;
+    }
+  },
+  deleteDocumentFromNamespace: async function (namespace, docId) {
+    const { DocumentVectors } = require("../../../models/vectors");
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace))) return;
+
+    const knownDocuments = await DocumentVectors.where(`docId = '${docId}'`);
+    if (knownDocuments.length === 0) return;
+
+    const vectorIds = knownDocuments.map((doc) => doc.vectorId);
+    await client.delete(namespace, {
+      wait: true,
+      points: vectorIds,
+    });
+
+    const indexes = knownDocuments.map((doc) => doc.id);
+    await DocumentVectors.deleteIds(indexes);
+    return true;
+  },
+  query: async function (reqBody = {}) {
+    const { namespace = null, input, workspace = {} } = reqBody;
+    if (!namespace || !input) throw new Error("Invalid request body");
+
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace))) {
+      return {
+        response: null,
+        sources: [],
+        message: "Invalid query - no documents found for workspace!",
+      };
+    }
+
+    const LLMConnector = getLLMProvider();
+    const queryVector = await LLMConnector.embedTextInput(input);
+    const { contextTexts, sourceDocuments } = await this.similarityResponse(
+      client,
+      namespace,
+      queryVector
+    );
+    const prompt = {
+      role: "system",
+      content: `${chatPrompt(workspace)}
+    Context:
+    ${contextTexts
+      .map((text, i) => {
+        return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`;
+      })
+      .join("")}`,
+    };
+    const memory = [prompt, { role: "user", content: input }];
+    const responseText = await LLMConnector.getChatCompletion(memory, {
+      temperature: workspace?.openAiTemp ?? 0.7,
+    });
+
+    return {
+      response: responseText,
+      sources: this.curateSources(sourceDocuments),
+      message: false,
+    };
+  },
+  // This implementation of chat uses the chat history and modifies the system prompt at execution
+  // this is improved over the regular langchain implementation so that chats do not directly modify embeddings
+  // because then multi-user support will have all conversations mutating the base vector collection to which then
+  // the only solution is replicating entire vector databases per user - which will very quickly consume space on VectorDbs
+  chat: async function (reqBody = {}) {
+    const {
+      namespace = null,
+      input,
+      workspace = {},
+      chatHistory = [],
+    } = reqBody;
+    if (!namespace || !input) throw new Error("Invalid request body");
+
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace))) {
+      return {
+        response: null,
+        sources: [],
+        message: "Invalid query - no documents found for workspace!",
+      };
+    }
+
+    const LLMConnector = getLLMProvider();
+    const queryVector = await LLMConnector.embedTextInput(input);
+    const { contextTexts, sourceDocuments } = await this.similarityResponse(
+      client,
+      namespace,
+      queryVector
+    );
+    const prompt = {
+      role: "system",
+      content: `${chatPrompt(workspace)}
+    Context:
+    ${contextTexts
+      .map((text, i) => {
+        return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`;
+      })
+      .join("")}`,
+    };
+    const memory = [prompt, ...chatHistory, { role: "user", content: input }];
+    const responseText = await LLMConnector.getChatCompletion(memory, {
+      temperature: workspace?.openAiTemp ?? 0.7,
+    });
+
+    return {
+      response: responseText,
+      sources: this.curateSources(sourceDocuments),
+      message: false,
+    };
+  },
+  "namespace-stats": async function (reqBody = {}) {
+    const { namespace = null } = reqBody;
+    if (!namespace) throw new Error("namespace required");
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace)))
+      throw new Error("Namespace by that name does not exist.");
+    const stats = await this.namespace(client, namespace);
+    return stats
+      ? stats
+      : { message: "No stats were able to be fetched from DB for namespace" };
+  },
+  "delete-namespace": async function (reqBody = {}) {
+    const { namespace = null } = reqBody;
+    const { client } = await this.connect();
+    if (!(await this.namespaceExists(client, namespace)))
+      throw new Error("Namespace by that name does not exist.");
+
+    const details = await this.namespace(client, namespace);
+    await this.deleteVectorsInNamespace(client, namespace);
+    return {
+      message: `Namespace ${namespace} was deleted along with ${details?.vectorCount} vectors.`,
+    };
+  },
+  reset: async function () {
+    const { client } = await this.connect();
+    const response = await client.getCollections();
+    for (const collection of response.collections) {
+      await client.deleteCollection(collection.name);
+    }
+    return { reset: true };
+  },
+  curateSources: function (sources = []) {
+    const documents = [];
+    for (const source of sources) {
+      if (Object.keys(source).length > 0) {
+        documents.push({
+          ...source,
+        });
+      }
+    }
+
+    return documents;
+  },
+};
+
+module.exports.QDrant = QDrant;
diff --git a/server/yarn.lock b/server/yarn.lock
index 2ff2aec4b..6a9e1669e 100644
--- a/server/yarn.lock
+++ b/server/yarn.lock
@@ -173,6 +173,25 @@
   dependencies:
     cross-fetch "^3.1.5"
 
+"@qdrant/js-client-rest@^1.4.0":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@qdrant/js-client-rest/-/js-client-rest-1.4.0.tgz#efd341a9a30b241e7e11f773b581b3102db1adc6"
+  integrity sha512-I3pCKnaVdqiVpZ9+XtEjCx7IQSJnerXffD/g8mj/fZsOOJH3IFM+nF2izOfVIByufAArW+drGcAPrxHedba99w==
+  dependencies:
+    "@qdrant/openapi-typescript-fetch" "^1.2.1"
+    "@sevinf/maybe" "^0.5.0"
+    undici "^5.22.1"
+
+"@qdrant/openapi-typescript-fetch@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@qdrant/openapi-typescript-fetch/-/openapi-typescript-fetch-1.2.1.tgz#6e232899ca0a7fbc769f0c3a229b56f93da39f19"
+  integrity sha512-oiBJRN1ME7orFZocgE25jrM3knIF/OKJfMsZPBbtMMKfgNVYfps0MokGvSJkBmecj6bf8QoLXWIGlIoaTM4Zmw==
+
+"@sevinf/maybe@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@sevinf/maybe/-/maybe-0.5.0.tgz#e59fcea028df615fe87d708bb30e1f338e46bb44"
+  integrity sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg==
+
 "@tootallnate/once@1":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -526,7 +545,7 @@ buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
-busboy@^1.0.0:
+busboy@^1.0.0, busboy@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
   integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
@@ -2505,6 +2524,13 @@ undefsafe@^2.0.5:
   resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
   integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
 
+undici@^5.22.1:
+  version "5.23.0"
+  resolved "https://registry.yarnpkg.com/undici/-/undici-5.23.0.tgz#e7bdb0ed42cebe7b7aca87ced53e6eaafb8f8ca0"
+  integrity sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg==
+  dependencies:
+    busboy "^1.6.0"
+
 unique-filename@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
-- 
GitLab