diff --git a/common.ts b/common.ts index 1c783fd..0a905e0 100644 --- a/common.ts +++ b/common.ts @@ -1,4 +1,4 @@ -export const PROVIDERS = ["cohere", "textsynth", "ocp", "openai", "openai-chat", "azure", "azure-chat"]; +export const PROVIDERS = ["cohere", "textsynth", "ocp", "openai", "openai-chat", "azure", "azure-chat", "anthropic"]; export type Provider = (typeof PROVIDERS)[number]; type ProviderProps = { @@ -7,6 +7,7 @@ type ProviderProps = { "ocp": { url: string }; "azure": { url: string }; "azure-chat": { url: string }; + "anthropic": { url: string }; }; type SharedPresetSettings = { diff --git a/main.ts b/main.ts index cbb5ec6..109078a 100644 --- a/main.ts +++ b/main.ts @@ -15,6 +15,10 @@ import { getPreset, } from './common'; +// import { +// claudeCompletion, +// } from './claude'; + import { App, Editor, @@ -32,6 +36,8 @@ import { ViewPlugin } from "@codemirror/view"; import { Configuration as AzureConfiguration, OpenAIApi as AzureOpenAIApi} from "azure-openai"; import { Configuration, OpenAIApi } from "openai"; import * as cohere from "cohere-ai"; +import Anthropic from '@anthropic-ai/sdk'; + import cl100k from "gpt-tokenizer"; import p50k from "gpt-tokenizer/esm/model/text-davinci-003"; @@ -92,6 +98,8 @@ export default class LoomPlugin extends Plugin { openai: OpenAIApi; azure: AzureOpenAIApi; + anthropic: Anthropic; + anthropicApiKey: string; rendering = false; @@ -137,29 +145,47 @@ export default class LoomPlugin extends Plugin { initializeProviders() { const preset = getPreset(this.settings); - if (preset === undefined) return; - - if (preset.provider === "openai") { - this.openai = new OpenAIApi(new Configuration({ - apiKey: preset.apiKey, - // @ts-expect-error TODO - organization: preset.organization, - })); - } else if (preset.provider == "cohere") - cohere.init(preset.apiKey); - else if (preset.provider == "azure") { - // @ts-expect-error TODO - const url = preset.url; + if (preset === undefined) return; + + if (preset.provider === "openai") { + this.openai = new OpenAIApi(new Configuration({ + apiKey: preset.apiKey, + // @ts-expect-error TODO + organization: preset.organization, + })); + } else if (preset.provider == "cohere") + cohere.init(preset.apiKey); + else if (preset.provider == "azure") { + // @ts-expect-error TODO + const url = preset.url; - if (!preset.apiKey || !url) return; - this.azure = new AzureOpenAIApi(new AzureConfiguration({ - apiKey: preset.apiKey, - azure: { - apiKey: preset.apiKey, - endpoint: url, - }, - })); - } + if (!preset.apiKey || !url) return; + this.azure = new AzureOpenAIApi(new AzureConfiguration({ + apiKey: preset.apiKey, + azure: { + apiKey: preset.apiKey, + endpoint: url, + }, + })); + } else if (preset.provider == "anthropic") { + //(property) ClientOptions.fetch?: Fetch | undefined + //Specify a custom fetch function implementation. + //If not provided, we use node-fetch on Node.js and otherwise expect that fetch is defined globally. + // expects Promise as return value + this.anthropicApiKey = preset.apiKey; + this.anthropic = new Anthropic({ + apiKey: preset.apiKey, + // fetch: + defaultHeaders: { + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'messages-2023-12-15', + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Credentials": "true", + }, + }); + } } apiKeySet() { @@ -310,6 +336,17 @@ export default class LoomPlugin extends Plugin { hotkeys: [{ modifiers: ["Ctrl", "Shift"], key: " " }], }); + this.addCommand({ + id: "bookmark", + name: "Bookmark current node", + checkCallback: (checking: boolean) => + withState(checking, (state) => { + this.app.workspace.trigger("loom:toggle-bookmark", state.current); + }), + hotkeys: [{ modifiers: ["Ctrl"], key: "b" }], + }); + + const withState = (checking: boolean, callback: (state: NoteState) => void) => { const file = this.app.workspace.getActiveFile(); if (!file || file.extension !== "md") return false; @@ -1232,6 +1269,9 @@ export default class LoomPlugin extends Plugin { // the tokenization and completion depend on the provider, // so call a different method depending on the provider + + // console.log("prompt", prompt); + const completionMethods: Record Promise> = { cohere: this.completeCohere, textsynth: this.completeTextSynth, @@ -1240,20 +1280,31 @@ export default class LoomPlugin extends Plugin { "openai-chat": this.completeOpenAIChat, azure: this.completeAzure, "azure-chat": this.completeAzureChat, + anthropic: this.completeAnthropic, }; let result; try { result = await completionMethods[getPreset(this.settings).provider].bind(this)(prompt); } catch (e) { new Notice(`Error: ${e}`); + this.state[file.path].generating = null; + this.saveAndRender(); + this.statusBarItem.style.display = "none"; + return; } if (!result.ok) { new Notice(`Error ${result.status}: ${result.message}`); + this.state[file.path].generating = null; + this.saveAndRender(); + this.statusBarItem.style.display = "none"; + return; } const rawCompletions = result.completions; + // console.log("rawCompletions", rawCompletions); + // escape and clean up the completions const completions = rawCompletions.map((completion: string) => { if (!completion) completion = ""; // empty completions are null, apparently @@ -1273,7 +1324,7 @@ export default class LoomPlugin extends Plugin { // create a child of the current node for each completion let ids = []; for (let completion of completions) { - const [id, node] = this.newNode(completion, state.generating, true); + const [id, node] = this.newNode(completion, rootNode, true); state.nodes[id] = node; ids.push(id); } @@ -1473,6 +1524,48 @@ export default class LoomPlugin extends Plugin { return result; } + async completeAnthropic(prompt: string) { + prompt = this.trimOpenAIPrompt(prompt); + // let result: CompletionResult; + + + try { + // console.log("prompt", prompt); + const response = await requestUrl({ + url: "https://api.anthropic.com/v1/messages", + method: "POST", + headers: { + "content-type": "application/json", + "anthropic-version": "2023-06-01", + 'x-api-key': this.anthropicApiKey, + }, + body: JSON.stringify({ + model: getPreset(this.settings).model, + max_tokens: this.settings.maxTokens, + temperature: this.settings.temperature, + system: "The assistant is in CLI simulation mode, and responds to the user's CLI commands only with the output of the command.", + messages: [ + {"role": "user", "content": `cat untitled.txt`}, + {"role": "assistant", "content": `${prompt}`} + ], + }), + }); + + + const result: CompletionResult = response.status === 200 + ? { ok: true, completions: [response.json.content[0]?.text || ""] } + : { ok: false, status: response.status, message: "" }; + + // console.log("response", result); + + return result; + } catch (e) { + console.error(e) + const result: CompletionResult = { ok: false, status: 400, message: "an error was encountered" }; + return result; + } + } + async loadSettings() { const settings = (await this.loadData())?.settings || {}; this.settings = Object.assign({}, DEFAULT_SETTINGS, settings); @@ -1578,6 +1671,7 @@ class LoomSettingTab extends PluginSettingTab { fillInModelDropdown.createEl("option", { text: "code-davinci-002", attr: { value: "code-davinci-002" } }); fillInModelDropdown.createEl("option", { text: "code-davinci-002 (Proxy)", attr: { value: "code-davinci-002-proxy" } }); fillInModelDropdown.createEl("option", { text: "gpt-4-base", attr: { value: "gpt-4-base" } }); + fillInModelDropdown.createEl("option", { text: "claude-3-opus", attr: { value: "claude-3-opus" } }); fillInModelDropdown.addEventListener("change", (event) => { const value = (event.target as HTMLSelectElement).value; @@ -1606,6 +1700,12 @@ class LoomSettingTab extends PluginSettingTab { this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = 8192; break; } + case "claude-3-opus": { + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider = "anthropic"; + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].model = "claude-3-opus-20240229"; + this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].contextLength = 20000; + break; + } } this.plugin.save(); updatePresetFields(); @@ -1624,6 +1724,7 @@ class LoomSettingTab extends PluginSettingTab { restoreApiKeyDropdown.createEl("option", { text: "Cohere", attr: { value: "cohere" } }); restoreApiKeyDropdown.createEl("option", { text: "TextSynth", attr: { value: "textsynth" } }); restoreApiKeyDropdown.createEl("option", { text: "Azure", attr: { value: "azure" } }); + restoreApiKeyDropdown.createEl("option", { text: "Anthropic", attr: { value: "anthropic" } }); restoreApiKeyDropdown.addEventListener("change", (event) => { const provider = (event.target as HTMLSelectElement).value as Provider; @@ -1675,6 +1776,14 @@ class LoomSettingTab extends PluginSettingTab { }; break; } + case "anthropic": { + preset = { + ...preset, + // @ts-expect-error + apiKey: this.plugin.settings.anthropicApiKey || "", + }; + break; + } default: { throw new Error(`Unknown provider: ${provider}`); } @@ -1714,6 +1823,7 @@ class LoomSettingTab extends PluginSettingTab { "openai-chat": "OpenAI (Chat)", azure: "Azure", "azure-chat": "Azure (Chat)", + anthropic: "Anthropic", }; dropdown.addOptions(options); dropdown.setValue(this.plugin.settings.modelPresets[this.plugin.settings.modelPreset].provider); diff --git a/package-lock.json b/package-lock.json index 4063bf6..ce56436 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "obsidian-loom", - "version": "1.17.0", + "version": "1.20.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-loom", - "version": "1.17.0", + "version": "1.20.3", "license": "MIT", "dependencies": { + "@anthropic-ai/sdk": "^0.20.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.3", "@types/lodash": "^4.14.191", @@ -34,6 +35,29 @@ "typescript": "4.7.4" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.20.0.tgz", + "integrity": "sha512-VpkVetFHQ31cendkcAGnZo08yWDJeo4jPRRtsNEgVKoF+NkzZDVktGvK4ZB7mhPrUQlCcjd4DzjijiWnZ4qIog==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz", + "integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@codemirror/state": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", @@ -194,8 +218,16 @@ "node_modules/@types/node": { "version": "16.18.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.12.tgz", - "integrity": "sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==", - "dev": true + "integrity": "sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } }, "node_modules/@types/roman-numerals": { "version": "0.3.0", @@ -411,6 +443,17 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -432,6 +475,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -937,6 +991,14 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1083,6 +1145,31 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1179,6 +1266,14 @@ "node": ">=8" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1419,8 +1514,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -1434,6 +1528,43 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/obsidian": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.1.1.tgz", @@ -1794,6 +1925,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -1858,6 +1994,11 @@ "node": ">=4.2.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -1888,6 +2029,28 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index a85ed34..3c50c9d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "typescript": "4.7.4" }, "dependencies": { + "@anthropic-ai/sdk": "^0.20.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.3", "@types/lodash": "^4.14.191",