Merge pull request #5 from Beim/feat/hash-file

Incremental update embedding data
This commit is contained in:
Mohammad Iskandarany 2023-05-21 20:08:21 +03:00 committed by Mohammad Iskandarani
commit a7f510f9c3
5 changed files with 136 additions and 29 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "gpt-assistant", "id": "gpt-assistant",
"name": "GPT Assistant", "name": "GPT Assistant",
"version": "0.1.2", "version": "0.1.3",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Use a GPT-3 based model on your notes and get personalized answers from your knowledge base.", "description": "Use a GPT-3 based model on your notes and get personalized answers from your knowledge base.",
"author": "M7mdisk", "author": "M7mdisk",

View file

@ -5,10 +5,16 @@ import { cosineSimilarity } from "./utils";
export interface chunkData { export interface chunkData {
text: string; text: string;
embeddings: number[]; embeddings: number[];
sha1: string;
} }
export type EmbeddedData = chunkData[]; export type EmbeddedData = chunkData[];
export interface CachedData {
searchable: EmbeddedData;
sha: Array<string>;
}
export type Answer = { error: boolean; text: string }; export type Answer = { error: boolean; text: string };
export class Assistant { export class Assistant {
MAX_TOKENS = 500; MAX_TOKENS = 500;
@ -78,13 +84,20 @@ export class Assistant {
}; };
} }
prepareTexts(texts: string[]): string[] { prepareTexts(texts: EmbeddedData): EmbeddedData {
let shortened: string[] = []; let shortened: EmbeddedData = [];
texts.forEach((text) => { texts.forEach((item) => {
const text = item.text;
if (this.tokenizer.encode(text).bpe.length > this.MAX_TOKENS) { if (this.tokenizer.encode(text).bpe.length > this.MAX_TOKENS) {
shortened = shortened.concat(this.splitIntoMany(text)); shortened = shortened.concat(this.splitIntoMany(text).map(t => {
return {
text: t,
embeddings: [],
sha1: item.sha1,
}
}));
} else { } else {
shortened.push(text); shortened.push(item);
} }
}); });
return shortened; return shortened;
@ -112,14 +125,15 @@ export class Assistant {
}); });
return chunks; return chunks;
} }
async createEmbeddings(data: string[]): Promise<EmbeddedData> { async createEmbeddings(data: EmbeddedData): Promise<EmbeddedData> {
const embeddings = await this.openai.createEmbedding({ const embeddings = await this.openai.createEmbedding({
input: data, input: data.map((d) => d.text),
model: "text-embedding-ada-002", model: "text-embedding-ada-002",
}); });
return data.map((text, idx) => ({ return data.map((d, idx) => ({
text, text: d.text,
embeddings: embeddings.data.data[idx].embedding, embeddings: embeddings.data.data[idx].embedding,
sha1: d.sha1,
})); }));
} }
} }

View file

@ -1,4 +1,5 @@
import { Answer, Assistant } from "./assistant"; import { Answer, Assistant, EmbeddedData, CachedData } from "./assistant";
import { sha1File } from "./utils";
import { import {
App, App,
MarkdownRenderer, MarkdownRenderer,
@ -12,10 +13,12 @@ import {
interface PluginSettings { interface PluginSettings {
apiKey: string; apiKey: string;
autoUpdate: boolean;
} }
const DEFAULT_SETTINGS: PluginSettings = { const DEFAULT_SETTINGS: PluginSettings = {
apiKey: "", apiKey: "",
autoUpdate: false,
}; };
export default class GPTAssistantPlugin extends Plugin { export default class GPTAssistantPlugin extends Plugin {
@ -28,7 +31,6 @@ export default class GPTAssistantPlugin extends Plugin {
this.assistant = new Assistant(this.settings.apiKey); this.assistant = new Assistant(this.settings.apiKey);
if (await this.hasCachedData()) { if (await this.hasCachedData()) {
const { searchable } = await this.loadData(); const { searchable } = await this.loadData();
this.saveNamedData("searchable", searchable);
this.assistant.setData(searchable); this.assistant.setData(searchable);
} }
@ -46,14 +48,42 @@ export default class GPTAssistantPlugin extends Plugin {
new Notice("Please provide an API Key in the settings"); new Notice("Please provide an API Key in the settings");
return; return;
} }
if (this.settings.autoUpdate) {
this.loadEmbeddingsToAssistant(); // async update embedding
}
new AskAssistantModal(this.app, async (question) => { new AskAssistantModal(this.app, async (question) => {
try {
const answer = await this.assistant.answerQuestion( const answer = await this.assistant.answerQuestion(
question question
); );
return answer ?? ""; return answer;
} catch (e) {
if (e.response) {
console.error(e.response)
new Notice("❌ " + e.response.data.error.message)
}
return { error: true, text: "" }
}
}).open(); }).open();
}, },
}); });
this.addCommand({
id: "update-assistant",
name: "Update assistant",
callback: async () => {
if (!this.settings.apiKey) {
new Notice("Please provide an API Key in the settings");
return;
}
new Notice(
"Loading data into model. this could take a while..."
);
await this.loadEmbeddingsToAssistant();
new Notice("Your data has been loaded into the model.");
},
});
} }
private async hasCachedData(): Promise<boolean> { private async hasCachedData(): Promise<boolean> {
@ -61,18 +91,64 @@ export default class GPTAssistantPlugin extends Plugin {
return data && data.searchable && data.searchable.length; return data && data.searchable && data.searchable.length;
} }
private async loadCachedData(): Promise<CachedData> {
const data = await this.loadData();
if (data && data.searchable && data.searchable.length &&
data.sha && data.sha.length) {
return {
searchable: data.searchable,
sha: data.sha,
}
}
return {
searchable: [],
sha: [],
}
}
async loadEmbeddingsToAssistant() { async loadEmbeddingsToAssistant() {
const { vault } = this.app; const { vault } = this.app;
const fileContents: string[] = await Promise.all( const cachedData = await this.loadCachedData();
const oldSearchable = cachedData.searchable;
const oldSha = new Set<string>(cachedData.sha);
const newSha = new Set<string>();
// Load new/updated file contents
const fileContents: EmbeddedData = (await Promise.all(
vault vault
.getMarkdownFiles() .getMarkdownFiles()
.map((file) => .map((file) => {
vault.cachedRead(file).then((res) => file.name + res) const sha1 = sha1File(file);
) newSha.add(sha1);
); if (oldSha.has(sha1)) { // file doesn't change
const chunks = await this.assistant.prepareTexts(fileContents); return { text: '', embeddings: [], sha1: sha1 };
const searchable = await this.assistant.createEmbeddings(chunks); }
this.saveNamedData("searchable", searchable); return vault.cachedRead(file).then((res) => {
return { text: file.name + res, embeddings: [], sha1: sha1 }
})
})
)).filter(f => f.text.length);
let searchable = oldSearchable.filter((e) => oldSha.has(e.sha1) && newSha.has(e.sha1));
if (fileContents.length) { // create embeddings for new/updated files
const chunks = this.assistant.prepareTexts(fileContents);
try {
const newSearchable = await this.assistant.createEmbeddings(chunks);
searchable = newSearchable.concat(searchable);
new Notice("Your data has been loaded into the model.");
} catch (e) {
if (e.response) {
console.error(e.response)
new Notice("❌ " + e.response.data.error.message)
}
}
}
this.saveNamedData({
"searchable": searchable,
"sha": Array.from(newSha),
});
this.assistant.setData(searchable); this.assistant.setData(searchable);
} }
@ -94,8 +170,8 @@ export default class GPTAssistantPlugin extends Plugin {
}); });
} }
async saveNamedData(name: string, data: unknown) { async saveNamedData(data: CachedData) {
await this.saveData({ ...(await this.loadData()), [name]: data }); await this.saveData({ ...(await this.loadData()), ...data });
} }
} }
@ -178,6 +254,17 @@ class AssistantSettings extends PluginSettingTab {
}) })
); );
new Setting(containerEl)
.setName("Automatically update")
.setDesc("Automatically load new notes into the assistant")
.addToggle((tg) => {
tg.setValue(this.plugin.settings.autoUpdate);
tg.onChange(async (value) => {
this.plugin.settings.autoUpdate = value;
await this.plugin.saveSettings();
});
})
new Setting(containerEl) new Setting(containerEl)
.setName("Process notes") .setName("Process notes")
.setDesc("Load all your notes into the assistant") .setDesc("Load all your notes into the assistant")
@ -194,7 +281,6 @@ class AssistantSettings extends PluginSettingTab {
); );
await this.plugin.loadEmbeddingsToAssistant(); await this.plugin.loadEmbeddingsToAssistant();
new Notice("Your data has been loaded into the model.");
}); });
}); });
} }

View file

@ -1,3 +1,6 @@
import { createHash } from 'crypto'
import { TFile } from "obsidian";
function dotProduct(vecA: number[], vecB: number[]) { function dotProduct(vecA: number[], vecB: number[]) {
let product = 0; let product = 0;
for (let i = 0; i < vecA.length; i++) { for (let i = 0; i < vecA.length; i++) {
@ -17,3 +20,7 @@ function magnitude(vec: number[]) {
export function cosineSimilarity(vecA: number[], vecB: number[]) { export function cosineSimilarity(vecA: number[], vecB: number[]) {
return dotProduct(vecA, vecB) / (magnitude(vecA) * magnitude(vecB)); return dotProduct(vecA, vecB) / (magnitude(vecA) * magnitude(vecB));
} }
export function sha1File(file: TFile) {
return createHash('sha1').update(`${file.path}-${file.stat.ctime}-${file.stat.mtime}-${file.stat.size}`).digest('hex')
}

View file

@ -1,3 +1,3 @@
{ {
"1.0.0": "0.15.0" "0.1.3": "0.15.0"
} }