diff --git a/.env.local.example b/.env.local.example index 4f824274..c30dd315 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,6 +1,7 @@ # Pick Vector DB VECTOR_DB=pinecone # VECTOR_DB=supabase +# VECTOR_DB=qdrant # Clerk related environment variables NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_**** @@ -34,4 +35,9 @@ TWILIO_ACCOUNT_SID=AC*** TWILIO_AUTH_TOKEN=***** # Steamship related environment variables -STEAMSHIP_API_KEY=**** \ No newline at end of file +STEAMSHIP_API_KEY=**** + +# Qdrant related environment variables +QDRANT_URL="httpS://****" +QDRANT_API_KEY=**** +QDRANT_COLLECTION_NAME=https://**** diff --git a/README.md b/README.md index 47dca440..289e253b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The stack is based on the [AI Getting Started Stack](https://github.com/a16z-inf - Auth: [Clerk](https://clerk.com/) - App logic: [Next.js](https://nextjs.org/) -- VectorDB: [Pinecone](https://www.pinecone.io/) / [Supabase pgvector](https://supabase.com/docs/guides/database/extensions/pgvector) +- VectorDB: [Pinecone](https://www.pinecone.io/) / [Supabase pgvector](https://supabase.com/docs/guides/database/extensions/pgvector) / [Qdrant](https://qdrant.tech/) - LLM orchestration: [Langchain.js](https://js.langchain.com/docs/) - Text model: [OpenAI](https://platform.openai.com/docs/models), [Replicate (Vicuna13b)](https://replicate.com/replicate/vicuna-13b) - Text streaming: [ai sdk](https://github.com/vercel-labs/ai) @@ -93,8 +93,7 @@ c. **Replicate API key** Visit https://replicate.com/account/api-tokens to get your Replicate API key if you're using Vicuna for your language model. - -❗ **_NOTE:_** By default, this template uses Pinecone as vector store, but you can turn on Supabase pgvector easily by uncommenting `VECTOR_DB=supabase` in `.env.local`. This means you only need to fill out either Pinecone API key _or_ Supabase API key. +❗ **_NOTE:_** By default, this template uses Pinecone as a vector store, but you can switch to Supabase pgvector or Qdrant by uncommenting `VECTOR_DB=supabase` or `VECTOR_DB=qdrant` in `.env.local`. This means you only need to fill out either `PINECONE_API_KEY`, `SUPABASE_API_KEY`, or Qdrant API details such as `QDRANT_URL`, `QDRANT_API_KEY`, and `QDRANT_COLLECTION_NAME`. d. **Pinecone API key** @@ -148,6 +147,12 @@ npm run generate-embeddings-pinecone npm run generate-embeddings-supabase ``` +#### If using Qdrant + +```bash +npm run generate-embeddings-qdrant +``` + ### 5. Run app locally Now you are ready to test out the app locally! To do this, simply run `npm run dev` under the project root. diff --git a/package-lock.json b/package-lock.json index 5c3c98d7..146fa2e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@clerk/nextjs": "^4.21.9-snapshot.56dc3e3", "@headlessui/react": "^1.7.15", "@pinecone-database/pinecone": "^0.1.6", + "@qdrant/js-client-rest": "^1.9.0", "@supabase/supabase-js": "^2.25.0", "@tailwindcss/forms": "^0.5.3", "@types/node": "20.2.5", @@ -277,6 +278,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@floating-ui/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", @@ -668,11 +677,42 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@qdrant/js-client-rest": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.9.0.tgz", + "integrity": "sha512-YiX/IskbRCoAY2ujyPDI6FBcO0ygAS4pgkGaJ7DcrJFh4SZV2XHs+u0KM7mO72RWJn1eJQFF2PQwxG+401xxJg==", + "dependencies": { + "@qdrant/openapi-typescript-fetch": "1.2.6", + "@sevinf/maybe": "0.5.0", + "undici": "~5.28.4" + }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8" + }, + "peerDependencies": { + "typescript": ">=4.7" + } + }, + "node_modules/@qdrant/openapi-typescript-fetch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@qdrant/openapi-typescript-fetch/-/openapi-typescript-fetch-1.2.6.tgz", + "integrity": "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==", + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz", "integrity": "sha512-IthPJsJR85GhOkp3Hvp8zFOPK5ynKn6STyHa/WZpioK7E1aYDiBzpqQPrngc14DszIUkIrdd3k9Iu0XSzlP/1w==" }, + "node_modules/@sevinf/maybe": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sevinf/maybe/-/maybe-0.5.0.tgz", + "integrity": "sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg==" + }, "node_modules/@supabase/functions-js": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.1.2.tgz", @@ -6065,6 +6105,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", diff --git a/package.json b/package.json index 43058c1d..460f9a1f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint", "generate-embeddings-pinecone": "node src/scripts/indexPinecone.mjs", "generate-embeddings-supabase": "node src/scripts/indexPGVector.mjs", + "generate-embeddings-qdrant": "node src/scripts/indexQdrant.mjs", "export-to-character": "node src/scripts/exportToCharacter.mjs" }, "dependencies": { @@ -16,6 +17,7 @@ "@clerk/nextjs": "^4.21.9-snapshot.56dc3e3", "@headlessui/react": "^1.7.15", "@pinecone-database/pinecone": "^0.1.6", + "@qdrant/js-client-rest": "^1.9.0", "@supabase/supabase-js": "^2.25.0", "@tailwindcss/forms": "^0.5.3", "@types/node": "20.2.5", diff --git a/src/app/utils/memory.ts b/src/app/utils/memory.ts index 199c560d..9e7e64ba 100644 --- a/src/app/utils/memory.ts +++ b/src/app/utils/memory.ts @@ -3,7 +3,9 @@ import { OpenAIEmbeddings } from "langchain/embeddings/openai"; import { PineconeClient } from "@pinecone-database/pinecone"; import { PineconeStore } from "langchain/vectorstores/pinecone"; import { SupabaseVectorStore } from "langchain/vectorstores/supabase"; +import { QdrantVectorStore } from "langchain/vectorstores/qdrant"; import { SupabaseClient, createClient } from "@supabase/supabase-js"; +import { QdrantClient } from '@qdrant/js-client-rest'; export type CompanionKey = { companionName: string; @@ -14,13 +16,16 @@ export type CompanionKey = { class MemoryManager { private static instance: MemoryManager; private history: Redis; - private vectorDBClient: PineconeClient | SupabaseClient; + private vectorDBClient: PineconeClient | SupabaseClient | QdrantClient; public constructor() { this.history = Redis.fromEnv(); if (process.env.VECTOR_DB === "pinecone") { this.vectorDBClient = new PineconeClient(); - } else { + } else if (process.env.VECTOR_DB === "qdrant") { + this.vectorDBClient = new QdrantClient({ url: process.env.QDRANT_URL!, apiKey: process.env?.QDRANT_API_KEY }); + } + else { const auth = { detectSessionInUrl: false, persistSession: false, @@ -58,6 +63,21 @@ class MemoryManager { { pineconeIndex } ); + const similarDocs = await vectorStore + .similaritySearch(recentChatHistory, 3, { fileName: companionFileName }) + .catch((err) => { + console.log("WARNING: failed to get vector search results.", err); + }); + return similarDocs; + } else if (process.env.VECTOR_DB === "qdrant") { + console.log("INFO: using Qdrant for vector search."); + const qdrantClient = this.vectorDBClient; + + const vectorStore = await QdrantVectorStore.fromExistingCollection(new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY }), { + client: qdrantClient, + collectionName: process.env.QDRANT_COLLECTION_NAME, + }); + const similarDocs = await vectorStore .similaritySearch(recentChatHistory, 3, { fileName: companionFileName }) .catch((err) => { diff --git a/src/scripts/indexQdrant.mjs b/src/scripts/indexQdrant.mjs new file mode 100644 index 00000000..74c7d5ff --- /dev/null +++ b/src/scripts/indexQdrant.mjs @@ -0,0 +1,48 @@ +// Major ref: https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/qdrant +import dotenv from "dotenv"; +import { Document } from "langchain/document"; +import { OpenAIEmbeddings } from "langchain/embeddings/openai"; +import { CharacterTextSplitter } from "langchain/text_splitter"; +import { QdrantVectorStore } from "langchain/vectorstores/qdrant"; +import { QdrantClient } from '@qdrant/js-client-rest'; +import fs from "fs"; +import path from "path"; + +dotenv.config({ path: `.env.local` }); + +const fileNames = fs.readdirSync("companions"); +const splitter = new CharacterTextSplitter({ + separator: " ", + chunkSize: 200, + chunkOverlap: 50, +}); + +const langchainDocs = await Promise.all( + fileNames.map(async (fileName) => { + if (fileName.endsWith(".txt")) { + const filePath = path.join("companions", fileName); + const fileContent = fs.readFileSync(filePath, "utf8"); + // get the last section in the doc for background info + const lastSection = fileContent.split("###ENDSEEDCHAT###").slice(-1)[0]; + const splitDocs = await splitter.createDocuments([lastSection]); + return splitDocs.map((doc) => { + return new Document({ + metadata: { fileName }, + pageContent: doc.pageContent, + }); + }); + } + }) +); + + +const qdrantClient = new QdrantClient({ url: process.env.QDRANT_URL, apiKey: process.env?.QDRANT_API_KEY }); + +await QdrantVectorStore.fromDocuments( + langchainDocs.flat().filter((doc) => doc !== undefined), + new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY }), + { + client: qdrantClient, + collectionName: process.env.QDRANT_COLLECTION_NAME, + } +);