From 8929d96ed03bce1bb46987b27e5175e607b7c429 Mon Sep 17 00:00:00 2001 From: Timothy Carambat Date: Fri, 28 Jul 2023 12:05:38 -0700 Subject: [PATCH] Move OpenAI api calls into its own interface/Class (#162) * Move OpenAI api calls into its own interface/Class move curate sources to be specific for each vectorDBs response for chat/query * remove comment --- server/utils/helpers/index.js | 23 ----- server/utils/openAi/index.js | 33 +++++++ .../utils/vectorDbProviders/chroma/index.js | 82 ++++++++--------- server/utils/vectorDbProviders/lance/index.js | 87 +++++-------------- .../utils/vectorDbProviders/pinecone/index.js | 74 ++++++---------- 5 files changed, 115 insertions(+), 184 deletions(-) diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 672bddea..1a5aac86 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -22,30 +22,7 @@ function toChunks(arr, size) { ); } -function curateSources(sources = []) { - const documents = []; - - // Sometimes the source may or may not have a metadata property - // in the response so we search for it explicitly or just spread the entire - // source and check to see if at least title exists. - for (const source of sources) { - if (source.hasOwnProperty("metadata")) { - const { metadata = {} } = source; - if (Object.keys(metadata).length > 0) { - documents.push({ ...metadata }); - } - } else { - if (Object.keys(source).length > 0) { - documents.push({ ...source }); - } - } - } - - return documents; -} - module.exports = { getVectorDbClass, toChunks, - curateSources, }; diff --git a/server/utils/openAi/index.js b/server/utils/openAi/index.js index b4ce75ba..64b64bdc 100644 --- a/server/utils/openAi/index.js +++ b/server/utils/openAi/index.js @@ -1,4 +1,5 @@ const { Configuration, OpenAIApi } = require("openai"); + class OpenAi { constructor() { const config = new Configuration({ @@ -7,6 +8,7 @@ class OpenAi { const openai = new OpenAIApi(config); this.openai = openai; } + isValidChatModel(modelName = "") { const validModels = ["gpt-4", "gpt-3.5-turbo"]; return validModels.includes(modelName); @@ -79,6 +81,37 @@ class OpenAi { return textResponse; } + + async getChatCompletion(messages = [], { temperature = 0.7 }) { + const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; + const { data } = await this.openai.createChatCompletion({ + model, + messages, + temperature, + }); + + if (!data.hasOwnProperty("choices")) return null; + return data.choices[0].message.content; + } + + async embedTextInput(textInput) { + const result = await this.embedChunks(textInput); + return result?.[0] || []; + } + + async embedChunks(textChunks = []) { + const { + data: { data }, + } = await this.openai.createEmbedding({ + model: "text-embedding-ada-002", + input: textChunks, + }); + + return data.length > 0 && + data.every((embd) => embd.hasOwnProperty("embedding")) + ? data.map((embd) => embd.embedding) + : null; + } } module.exports = { diff --git a/server/utils/vectorDbProviders/chroma/index.js b/server/utils/vectorDbProviders/chroma/index.js index 801a41db..d66a669a 100644 --- a/server/utils/vectorDbProviders/chroma/index.js +++ b/server/utils/vectorDbProviders/chroma/index.js @@ -5,10 +5,10 @@ const { VectorDBQAChain } = require("langchain/chains"); const { OpenAIEmbeddings } = require("langchain/embeddings/openai"); const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); const { storeVectorResult, cachedVectorInformation } = require("../../files"); -const { Configuration, OpenAIApi } = require("openai"); const { v4: uuidv4 } = require("uuid"); -const { toChunks, curateSources } = require("../../helpers"); +const { toChunks } = require("../../helpers"); const { chatPrompt } = require("../../chats"); +const { OpenAi } = require("../../openAi"); const Chroma = { name: "Chroma", @@ -57,26 +57,6 @@ const Chroma = { embedder: function () { return new OpenAIEmbeddings({ openAIApiKey: process.env.OPEN_AI_KEY }); }, - openai: function () { - const config = new Configuration({ apiKey: process.env.OPEN_AI_KEY }); - const openai = new OpenAIApi(config); - return openai; - }, - getChatCompletion: async function ( - openai, - messages = [], - { temperature = 0.7 } - ) { - const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; - const { data } = await openai.createChatCompletion({ - model, - messages, - temperature, - }); - - if (!data.hasOwnProperty("choices")) return null; - return data.choices[0].message.content; - }, llm: function ({ temperature = 0.7 }) { const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; return new OpenAI({ @@ -85,22 +65,6 @@ const Chroma = { temperature, }); }, - embedTextInput: async function (openai, textInput) { - const result = await this.embedChunks(openai, textInput); - return result?.[0] || []; - }, - embedChunks: async function (openai, chunks = []) { - const { - data: { data }, - } = await openai.createEmbedding({ - model: "text-embedding-ada-002", - input: chunks, - }); - return data.length > 0 && - data.every((embd) => embd.hasOwnProperty("embedding")) - ? data.map((embd) => embd.embedding) - : null; - }, similarityResponse: async function (client, namespace, queryVector) { const collection = await client.getCollection({ name: namespace }); const result = { @@ -212,10 +176,10 @@ const Chroma = { const textChunks = await textSplitter.splitText(pageContent); console.log("Chunks created from document:", textChunks.length); + const openAiConnector = new OpenAi(); const documentVectors = []; const vectors = []; - const openai = this.openai(); - const vectorValues = await this.embedChunks(openai, textChunks); + const vectorValues = await openAiConnector.embedChunks(textChunks); const submission = { ids: [], embeddings: [], @@ -322,7 +286,7 @@ const Chroma = { const response = await chain.call({ query: input }); return { response: response.text, - sources: curateSources(response.sourceDocuments), + sources: this.curateSources(response.sourceDocuments), message: false, }; }, @@ -348,7 +312,8 @@ const Chroma = { }; } - const queryVector = await this.embedTextInput(this.openai(), input); + const openAiConnector = new OpenAi(); + const queryVector = await openAiConnector.embedTextInput(input); const { contextTexts, sourceDocuments } = await this.similarityResponse( client, namespace, @@ -359,19 +324,24 @@ const Chroma = { content: `${chatPrompt(workspace)} Context: ${contextTexts - .map((text, i) => { - return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; - }) - .join("")}`, + .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 this.getChatCompletion(this.openai(), memory, { + const responseText = await openAiConnector.getChatCompletion(memory, { temperature: workspace?.openAiTemp ?? 0.7, }); + // When we roll out own response we have separate metadata and texts, + // so for source collection we need to combine them. + const sources = sourceDocuments.map((metadata, i) => { + return { metadata: { ...metadata, text: contextTexts[i] } }; + }); return { response: responseText, - sources: curateSources(sourceDocuments), + sources: this.curateSources(sources), message: false, }; }, @@ -403,6 +373,22 @@ const Chroma = { await client.reset(); return { reset: true }; }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { metadata = {} } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ + ...metadata, + ...(source.hasOwnProperty("pageContent") + ? { text: source.pageContent } + : {}), + }); + } + } + + return documents; + }, }; module.exports.Chroma = Chroma; diff --git a/server/utils/vectorDbProviders/lance/index.js b/server/utils/vectorDbProviders/lance/index.js index bf29d17f..3fc0f01c 100644 --- a/server/utils/vectorDbProviders/lance/index.js +++ b/server/utils/vectorDbProviders/lance/index.js @@ -3,23 +3,9 @@ const { toChunks } = require("../../helpers"); const { OpenAIEmbeddings } = require("langchain/embeddings/openai"); const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); const { storeVectorResult, cachedVectorInformation } = require("../../files"); -const { Configuration, OpenAIApi } = require("openai"); const { v4: uuidv4 } = require("uuid"); const { chatPrompt } = require("../../chats"); - -// Since we roll our own results for prompting we -// have to manually curate sources as well. -function curateLanceSources(sources = []) { - const documents = []; - for (const source of sources) { - const { text, vector: _v, score: _s, ...metadata } = source; - if (Object.keys(metadata).length > 0) { - documents.push({ ...metadata, text }); - } - } - - return documents; -} +const { OpenAi } = require("../../openAi"); const LanceDb = { uri: `${ @@ -61,51 +47,9 @@ const LanceDb = { const table = await client.openTable(_namespace); return (await table.countRows()) || 0; }, - embeddingFunc: function () { - return new lancedb.OpenAIEmbeddingFunction( - "context", - process.env.OPEN_AI_KEY - ); - }, - embedTextInput: async function (openai, textInput) { - const result = await this.embedChunks(openai, textInput); - return result?.[0] || []; - }, - embedChunks: async function (openai, chunks = []) { - const { - data: { data }, - } = await openai.createEmbedding({ - model: "text-embedding-ada-002", - input: chunks, - }); - return data.length > 0 && - data.every((embd) => embd.hasOwnProperty("embedding")) - ? data.map((embd) => embd.embedding) - : null; - }, embedder: function () { return new OpenAIEmbeddings({ openAIApiKey: process.env.OPEN_AI_KEY }); }, - openai: function () { - const config = new Configuration({ apiKey: process.env.OPEN_AI_KEY }); - const openai = new OpenAIApi(config); - return openai; - }, - getChatCompletion: async function ( - openai, - messages = [], - { temperature = 0.7 } - ) { - const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; - const { data } = await openai.createChatCompletion({ - model, - messages, - temperature, - }); - - if (!data.hasOwnProperty("choices")) return null; - return data.choices[0].message.content; - }, similarityResponse: async function (client, namespace, queryVector) { const collection = await client.openTable(namespace); const result = { @@ -225,11 +169,11 @@ const LanceDb = { const textChunks = await textSplitter.splitText(pageContent); console.log("Chunks created from document:", textChunks.length); + const openAiConnector = new OpenAi(); const documentVectors = []; const vectors = []; const submissions = []; - const openai = this.openai(); - const vectorValues = await this.embedChunks(openai, textChunks); + const vectorValues = await openAiConnector.embedChunks(textChunks); if (!!vectorValues && vectorValues.length > 0) { for (const [i, vector] of vectorValues.entries()) { @@ -287,7 +231,8 @@ const LanceDb = { } // LanceDB does not have langchainJS support so we roll our own here. - const queryVector = await this.embedTextInput(this.openai(), input); + const openAiConnector = new OpenAi(); + const queryVector = await openAiConnector.embedTextInput(input); const { contextTexts, sourceDocuments } = await this.similarityResponse( client, namespace, @@ -304,13 +249,13 @@ const LanceDb = { .join("")}`, }; const memory = [prompt, { role: "user", content: input }]; - const responseText = await this.getChatCompletion(this.openai(), memory, { + const responseText = await openAiConnector.getChatCompletion(memory, { temperature: workspace?.openAiTemp ?? 0.7, }); return { response: responseText, - sources: curateLanceSources(sourceDocuments), + sources: this.curateSources(sourceDocuments), message: false, }; }, @@ -336,7 +281,8 @@ const LanceDb = { }; } - const queryVector = await this.embedTextInput(this.openai(), input); + const openAiConnector = new OpenAi(); + const queryVector = await openAiConnector.embedTextInput(input); const { contextTexts, sourceDocuments } = await this.similarityResponse( client, namespace, @@ -353,13 +299,13 @@ const LanceDb = { .join("")}`, }; const memory = [prompt, ...chatHistory, { role: "user", content: input }]; - const responseText = await this.getChatCompletion(this.openai(), memory, { + const responseText = await openAiConnector.getChatCompletion(memory, { temperature: workspace?.openAiTemp ?? 0.7, }); return { response: responseText, - sources: curateLanceSources(sourceDocuments), + sources: this.curateSources(sourceDocuments), message: false, }; }, @@ -391,6 +337,17 @@ const LanceDb = { fs.rm(`${client.uri}`, { recursive: true }, () => null); return { reset: true }; }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { text, vector: _v, score: _s, ...metadata } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ ...metadata, text }); + } + } + + return documents; + }, }; module.exports.LanceDb = LanceDb; diff --git a/server/utils/vectorDbProviders/pinecone/index.js b/server/utils/vectorDbProviders/pinecone/index.js index e34391b1..a392dc40 100644 --- a/server/utils/vectorDbProviders/pinecone/index.js +++ b/server/utils/vectorDbProviders/pinecone/index.js @@ -1,16 +1,14 @@ const { PineconeClient } = require("@pinecone-database/pinecone"); const { PineconeStore } = require("langchain/vectorstores/pinecone"); const { OpenAI } = require("langchain/llms/openai"); -const { VectorDBQAChain, LLMChain } = require("langchain/chains"); +const { VectorDBQAChain } = require("langchain/chains"); const { OpenAIEmbeddings } = require("langchain/embeddings/openai"); -const { VectorStoreRetrieverMemory } = require("langchain/memory"); -const { PromptTemplate } = require("langchain/prompts"); const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); const { storeVectorResult, cachedVectorInformation } = require("../../files"); -const { Configuration, OpenAIApi } = require("openai"); const { v4: uuidv4 } = require("uuid"); -const { toChunks, curateSources } = require("../../helpers"); +const { toChunks } = require("../../helpers"); const { chatPrompt } = require("../../chats"); +const { OpenAi } = require("../../openAi"); const Pinecone = { name: "Pinecone", @@ -34,42 +32,6 @@ const Pinecone = { embedder: function () { return new OpenAIEmbeddings({ openAIApiKey: process.env.OPEN_AI_KEY }); }, - openai: function () { - const config = new Configuration({ apiKey: process.env.OPEN_AI_KEY }); - const openai = new OpenAIApi(config); - return openai; - }, - getChatCompletion: async function ( - openai, - messages = [], - { temperature = 0.7 } - ) { - const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; - const { data } = await openai.createChatCompletion({ - model, - messages, - temperature, - }); - - if (!data.hasOwnProperty("choices")) return null; - return data.choices[0].message.content; - }, - embedTextInput: async function (openai, textInput) { - const result = await this.embedChunks(openai, textInput); - return result?.[0] || []; - }, - embedChunks: async function (openai, chunks = []) { - const { - data: { data }, - } = await openai.createEmbedding({ - model: "text-embedding-ada-002", - input: chunks, - }); - return data.length > 0 && - data.every((embd) => embd.hasOwnProperty("embedding")) - ? data.map((embd) => embd.embedding) - : null; - }, llm: function ({ temperature = 0.7 }) { const model = process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo"; return new OpenAI({ @@ -182,10 +144,10 @@ const Pinecone = { const textChunks = await textSplitter.splitText(pageContent); console.log("Chunks created from document:", textChunks.length); + const openAiConnector = new OpenAi(); const documentVectors = []; const vectors = []; - const openai = this.openai(); - const vectorValues = await this.embedChunks(openai, textChunks); + const vectorValues = await openAiConnector.embedChunks(textChunks); if (!!vectorValues && vectorValues.length > 0) { for (const [i, vector] of vectorValues.entries()) { @@ -299,7 +261,7 @@ const Pinecone = { const response = await chain.call({ query: input }); return { response: response.text, - sources: curateSources(response.sourceDocuments), + sources: this.curateSources(response.sourceDocuments), message: false, }; }, @@ -322,7 +284,8 @@ const Pinecone = { "Invalid namespace - has it been collected and seeded yet?" ); - const queryVector = await this.embedTextInput(this.openai(), input); + const openAiConnector = new OpenAi(); + const queryVector = await openAiConnector.embedTextInput(input); const { contextTexts, sourceDocuments } = await this.similarityResponse( pineconeIndex, namespace, @@ -340,17 +303,32 @@ const Pinecone = { }; const memory = [prompt, ...chatHistory, { role: "user", content: input }]; - - const responseText = await this.getChatCompletion(this.openai(), memory, { + const responseText = await openAiConnector.getChatCompletion(memory, { temperature: workspace?.openAiTemp ?? 0.7, }); return { response: responseText, - sources: curateSources(sourceDocuments), + sources: this.curateSources(sourceDocuments), message: false, }; }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { metadata = {} } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ + ...metadata, + ...(source.hasOwnProperty("pageContent") + ? { text: source.pageContent } + : {}), + }); + } + } + + return documents; + }, }; module.exports.Pinecone = Pinecone;