m7mdisk_obsidian-gpt/assistant.ts
Mohammad Iskandarani dd2b20f183 Basic MVP working
2023-03-10 18:22:57 +03:00

125 lines
3.5 KiB
TypeScript

import { Configuration, OpenAIApi } from "openai";
import GPT3Tokenizer from "gpt3-tokenizer";
import { cosineSimilarity } from "utils";
export interface chunkData {
text: string;
embeddings: number[];
}
export type EmbeddedData = chunkData[];
export type Answer = { error: boolean; text: string };
export class Assistant {
MAX_TOKENS = 500;
openai: OpenAIApi;
tokenizer: GPT3Tokenizer;
searchable?: EmbeddedData;
constructor(apiKey: string) {
const configuration = new Configuration({
apiKey: apiKey,
});
this.openai = new OpenAIApi(configuration);
this.tokenizer = new GPT3Tokenizer({ type: "gpt3" });
}
setData(emb: EmbeddedData) {
this.searchable = emb;
}
private async createContext(question: string, maxLength = 1800) {
if (this.searchable == null) throw new Error("NO SEARCHABLE DATA");
const questionEmbeddings = (
await this.openai.createEmbedding({
input: question,
model: "text-embedding-ada-002",
})
).data.data[0].embedding;
const sortedData = this.searchable.sort(
(a, b) =>
cosineSimilarity(questionEmbeddings, b.embeddings) -
cosineSimilarity(questionEmbeddings, a.embeddings)
);
const returns = [];
let cur_len = 0;
for (const row of sortedData) {
const n_tokens = this.tokenizer.encode(row.text).bpe.length;
cur_len += n_tokens + 4;
if (cur_len > maxLength) break;
returns.push(row.text);
}
return returns.join("\n\n###\n\n");
}
async answerQuestion(
question: string,
maxLength = 1800,
maxTokens = 150,
stopSequence = null
): Promise<Answer> {
if (this.searchable == null)
return {
error: true,
text: "Data not loaded. Please load it from settings",
};
const context = await this.createContext(question, maxLength);
const response = await this.openai.createCompletion({
prompt: `Answer the question based on the context below, and if the question can't be answered based on the context, say "I don't know, I couldn't find anything related to this in your notes."\n\nContext: ${context}\n\n---\n\nQuestion: ${question}\nAnswer:`,
temperature: 0,
max_tokens: maxTokens,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
stop: stopSequence,
model: "text-davinci-003",
});
return {
error: false,
text: response.data.choices[0].text ?? "Something went wrong.",
};
}
prepareTexts(texts: string[]): string[] {
let shortened: string[] = [];
texts.forEach((text) => {
if (this.tokenizer.encode(text).bpe.length > this.MAX_TOKENS) {
shortened = shortened.concat(this.splitIntoMany(text));
} else {
shortened.push(text);
}
});
return shortened;
}
private splitIntoMany(text: string, max_tokens = 500): string[] {
const sentences = text.split(". ");
const n_tokens = sentences.map(
(sentence) => this.tokenizer.encode(" " + sentence).bpe.length
);
let tokens_so_far = 0;
const chunks: string[] = [];
const chunk: string[] = [];
sentences.forEach((sentence, idx) => {
const token = n_tokens[idx];
if (token + tokens_so_far > max_tokens) {
chunks.push(chunk.join(". ") + ".");
chunk.length = 0;
tokens_so_far = 0;
}
if (!(token > max_tokens)) {
chunk.push(sentence);
tokens_so_far += token + 1;
}
});
return chunks;
}
async createEmbeddings(data: string[]): Promise<EmbeddedData> {
const embeddings = await this.openai.createEmbedding({
input: data,
model: "text-embedding-ada-002",
});
return data.map((text, idx) => ({
text,
embeddings: embeddings.data.data[idx].embedding,
}));
}
}