From 89a77548b1ca15c1522a6bbf5cff40456c19d889 Mon Sep 17 00:00:00 2001 From: matiss Date: Tue, 16 May 2023 20:32:35 +0100 Subject: [PATCH] feat: using streams for better text generation --- package.json | 4 +- src/main.ts | 221 +++++++++++++++++++++++++++++++++++++++++++++------ yarn.lock | 48 ++++++++++- 3 files changed, 247 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 08dcb32..8544463 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "typescript": "4.7.4" }, "dependencies": { + "axios": "^1.4.0", "dotenv": "^16.0.3", - "openai": "^3.1.0" + "openai": "^3.1.0", + "openai-streams": "^5.3.0" } } diff --git a/src/main.ts b/src/main.ts index 2085d8d..00ab81d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ // @ts-nocheck + import { App, Editor, @@ -13,10 +14,10 @@ import { Configuration, OpenAIApi } from "openai"; import * as dotenv from "dotenv"; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import import { TranslatePrompt, EditPrompt } from "./modals"; import { SettingTab } from "./settings/settings"; +import { OpenAI } from "openai-streams" dotenv.config(); -// Remember to rename these classes and interfaces! - +// interfaces and default settings for the setting tab interface Settings { apiKey: string; summariseTokens: number; @@ -43,6 +44,7 @@ const DEFAULT_SETTINGS: Settings = { export default class GeneAI extends Plugin { settings: Settings; + async onload() { await this.loadSettings(); @@ -51,10 +53,10 @@ export default class GeneAI extends Plugin { }); const openai = new OpenAIApi(configuration); - // This adds a simple command that can be triggered anywhere + // The Completion command this.addCommand({ id: "aicomp", - name: "Complete From Prompt", + name: "Complete From Prooompt", editorCallback: async (editor: Editor, view: MarkdownView) => { // Check if the user has an api key @@ -62,17 +64,29 @@ export default class GeneAI extends Plugin { new Notice("Please set your API key in the settings"); return; } + // Get the prompt from the user selected area const prompt = editor.getSelection(); + + // Set the context to the text above the prompt const context = editor .getRange( { line: 0, ch: 0 }, { line: editor.getCursor("from").line, ch: 0 } ) .trim(); + + const currentCursor = editor.getCursor("from").line; + // The prompt itself let message = `Provided context (which may or may not be relavent): "${context}", Complete the following prompt: "${prompt}", (use markdown to format your text)`; + // Notice to inform the user that the process is going on new Notice("✒️ Writing..."); let completion; - + + // The code below contains the original + // implementation of the chat completion without streams. + // This will be replaced in the future + + // Check to see if the user is using a chat model if (this.settings.model === "gpt-3.5-turbo") { completion = await openai .createChatCompletion({ @@ -85,10 +99,45 @@ export default class GeneAI extends Plugin { ], max_tokens: this.settings.completionTokens, temperature: this.settings.temperature, - }) + stream: true, + }, + { + responseType: 'stream', + onDownloadProgress: (progressEvent) => { + // get the payload + let payload: string = progressEvent.currentTarget.response; + // return if the payload is done + if (payload.includes("[DONE]")) { + return + } + + // Turn the payload into valid JSON + const result = payload + .replace(/data:\s*/g, "") + .replace(/[\r\n\t]/g, "") + .split("}{") + .join("},{"); + const cleanedJsonString = `[${result}]`; + + try { + let parsed = JSON.parse(cleanedJsonString); + if (parsed.length === 1) { + return + } + let last = parsed[parsed.length - 1]; + let content = last.choices[0].delta.content; + editor.replaceSelection(content); + } catch (e) { + console.log(e); + } + } + } + ) .catch((err) => { new Notice(`❗${err}`); }); + + } else { completion = await openai .createCompletion({ @@ -102,11 +151,12 @@ export default class GeneAI extends Plugin { }); } + // Replace selected area with the completion editor.replaceSelection(completion.data.choices[0].message.content.trim()); new Notice("Completed! 🚀"); }, }); - + // Command to summarise highlighted content. this.addCommand({ id: "summarise", name: "Summarise", @@ -139,7 +189,40 @@ export default class GeneAI extends Plugin { ], max_tokens: this.settings.summariseTokens, temperature: this.settings.temperature, - }) + stream: true, + }, + { + responseType: 'stream', + onDownloadProgress: (progressEvent) => { + // get the payload + let payload: string = progressEvent.currentTarget.response; + // return if the payload is done + if (payload.includes("[DONE]")) { + return + } + + // Turn the payload into valid JSON + const result = payload + .replace(/data:\s*/g, "") + .replace(/[\r\n\t]/g, "") + .split("}{") + .join("},{"); + const cleanedJsonString = `[${result}]`; + + try { + let parsed = JSON.parse(cleanedJsonString); + if (parsed.length === 1) { + return + } + let last = parsed[parsed.length - 1]; + let content = last.choices[0].delta.content; + editor.replaceSelection(content); + } catch (e) { + console.log(e); + } + } + } + ) .catch((err) => { new Notice(`❗${err}`); }); @@ -155,14 +238,12 @@ export default class GeneAI extends Plugin { new Notice(`❗${err}`); }); } - editor.replaceSelection( - `## Summary\n\n${completion.data.choices[0].message.content.trim()}` - ); + new Notice("Summarised! 🚀"); }, }); - + // Command to translate highlighted content. this.addCommand({ id: "translate", name: "Translate", @@ -173,12 +254,7 @@ export default class GeneAI extends Plugin { return; } const prompt = editor.getSelection(); - const context = editor - .getRange( - { line: 0, ch: 0 }, - { line: editor.getCursor("from").line, ch: 0 } - ) - .trim(); + let language = "english"; // Use GPT-4 if enabled @@ -187,7 +263,7 @@ export default class GeneAI extends Plugin { new TranslatePrompt(this.app, async (result) => { language = result; - let message = `Provided context (which may or may not be relavent): "${context}", Translate the following: "${prompt}" into ${language}`; + let message = `Translate the following: ${prompt} into ${language}`; new Notice("📖 Translating..."); let completion; @@ -203,7 +279,40 @@ export default class GeneAI extends Plugin { ], max_tokens: this.settings.translateTokens, temperature: this.settings.temperature, - }) + stream: true, + }, + { + responseType: 'stream', + onDownloadProgress: (progressEvent) => { + // get the payload + let payload: string = progressEvent.currentTarget.response; + // return if the payload is done + if (payload.includes("[DONE]")) { + return + } + + // Turn the payload into valid JSON + const result = payload + .replace(/data:\s*/g, "") + .replace(/[\r\n\t]/g, "") + .split("}{") + .join("},{"); + const cleanedJsonString = `[${result}]`; + + try { + let parsed = JSON.parse(cleanedJsonString); + if (parsed.length === 1) { + return + } + let last = parsed[parsed.length - 1]; + let content = last.choices[0].delta.content; + editor.replaceSelection(content); + } catch (e) { + console.log(e); + } + } + } + ) .catch((err) => { new Notice(`❗${err}`); }); @@ -226,7 +335,7 @@ export default class GeneAI extends Plugin { }).open(); }, }); - + // Command to modify highlighted content in a specified way this.addCommand({ id: "modify", name: "Modify", @@ -261,7 +370,39 @@ export default class GeneAI extends Plugin { ], max_tokens: this.settings.modifyTokens, temperature: this.settings.temperature, - }) + stream: true, + }, + { + responseType: 'stream', + onDownloadProgress: (progressEvent) => { + // get the payload + let payload: string = progressEvent.currentTarget.response; + // return if the payload is done + if (payload.includes("[DONE]")) { + return + } + + // Turn the payload into valid JSON + const result = payload + .replace(/data:\s*/g, "") + .replace(/[\r\n\t]/g, "") + .split("}{") + .join("},{"); + const cleanedJsonString = `[${result}]`; + + try { + let parsed = JSON.parse(cleanedJsonString); + if (parsed.length === 1) { + return + } + let last = parsed[parsed.length - 1]; + let content = last.choices[0].delta.content; + editor.replaceSelection(content); + } catch (e) { + console.log(e); + } + } + }) .catch((err) => { new Notice(`❗${err}`); }); @@ -285,7 +426,7 @@ export default class GeneAI extends Plugin { }).open(); }, }); - + // Command to elaborate on highlighted content this.addCommand({ id: "elaborate", name: "Elaborate", @@ -314,7 +455,39 @@ export default class GeneAI extends Plugin { ], max_tokens: this.settings.elaborateTokens, temperature: this.settings.temperature, - }) + stream: true, + }, + { + responseType: 'stream', + onDownloadProgress: (progressEvent) => { + // get the payload + let payload: string = progressEvent.currentTarget.response; + // return if the payload is done + if (payload.includes("[DONE]")) { + return + } + + // Turn the payload into valid JSON + const result = payload + .replace(/data:\s*/g, "") + .replace(/[\r\n\t]/g, "") + .split("}{") + .join("},{"); + const cleanedJsonString = `[${result}]`; + + try { + let parsed = JSON.parse(cleanedJsonString); + if (parsed.length === 1) { + return + } + let last = parsed[parsed.length - 1]; + let content = last.choices[0].delta.content; + editor.replaceSelection(content); + } catch (e) { + console.log(e); + } + } + }) .catch((err) => { new Notice(`❗${err}`); }); diff --git a/yarn.lock b/yarn.lock index d51fb93..50c0261 100644 --- a/yarn.lock +++ b/yarn.lock @@ -337,6 +337,15 @@ axios@^0.26.0: dependencies: follow-redirects "^1.14.8" +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -785,6 +794,11 @@ estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +eventsource-parser@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-1.0.0.tgz#6332e37fd5512e3c8d9df05773b2bf9e152ccc04" + integrity sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g== + fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" @@ -847,7 +861,7 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -follow-redirects@^1.14.8: +follow-redirects@^1.14.8, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -1279,6 +1293,15 @@ obsidian@latest: "@types/codemirror" "0.0.108" moment "2.29.4" +openai-streams@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/openai-streams/-/openai-streams-5.3.0.tgz#1f28b6fa3d8730e4d66ea2aff314937800295a1f" + integrity sha512-481B3eWLQ4P4E7jvF38vRzz2/aQz6CLHx2XSV7J+w83791fhiUxvTNGw8uh/M04N0X5AdCqPaEPqHc+sfxNTUQ== + dependencies: + dotenv "^16.0.3" + eventsource-parser "^1.0.0" + yield-stream "^3.0.0" + openai@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866" @@ -1411,6 +1434,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -1550,6 +1578,11 @@ semver@^7.1.1, semver@^7.3.4, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +shim-streams@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/shim-streams/-/shim-streams-0.0.2.tgz#816a9faf9bc158cbc57b5593f2d4c3c9087b02e6" + integrity sha512-9Otb+FCl13XxRp1nVddtsCbwvB7AEMTjzc3/fixowyzvSVoCzu/VEstblB2SdIDbd61u5D/zpS5u9fGzDdOoZg== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -1776,6 +1809,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +web-streams-polyfill@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -1823,6 +1861,14 @@ yargs@^16.0.0, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yield-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/yield-stream/-/yield-stream-3.0.0.tgz#8211b97e9bdf9dd434e332663816fe678b8dd63f" + integrity sha512-I0fHp7xcR+ilGKgtLP+6d0SxTLfJcLDX/iiTEQZtg0U/cqWKvcHy4D9em6adhs3emSgik8YAZ2Jmv7gc1kyuhA== + dependencies: + shim-streams "^0.0.2" + web-streams-polyfill "^3.2.1" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"