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
This commit is contained in:
Timothy Carambat 2023-07-28 12:05:38 -07:00 committed by GitHub
parent 0a2f837fb2
commit 8929d96ed0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 184 deletions

View File

@ -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,
};

View File

@ -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 = {

View File

@ -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;

View File

@ -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;

View File

@ -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;