diff --git a/README.md b/README.md index 88bb02a..c0b4fe5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Obsidian Companion -Companion is an Obsidian plugin that adds an **AI-powered autocomplete** feature to your note-taking and personal knowledge management platform. With Companion, you can write notes more quickly and easily by receiving suggestions for completing words, phrases, and even entire sentences based on the context of your writing. The autocomplete feature uses OpenAI's state-of-the-art **GPT-3 and GPT-3.5 models, including ChatGPT**, to generate smart suggestions that are tailored to your specific writing style and preferences. Support for more models is planned, too. +Companion is an Obsidian plugin that adds an **AI-powered autocomplete** feature to your note-taking and personal knowledge management platform. With Companion, you can write notes more quickly and easily by receiving suggestions for completing words, phrases, and even entire sentences based on the context of your writing. The autocomplete feature uses OpenAI's state-of-the-art **GPT-3 and GPT-3.5 models, including ChatGPT**, among others, to generate smart suggestions that are tailored to your specific writing style and preferences. Support for more models is planned, too. Companion's autocomplete feature is designed to be unobtrusive, providing suggestions in ghost text that can be accepted or ignored by the you as you see fit, similar to what github copilot does. With Companion, you can write notes more efficiently and effectively, leveraging the power of AI to enhance your productivity and creativity. Whether you're a student, a researcher, or a knowledge worker, Companion can help you to take your note-taking and knowledge management to the next level. @@ -50,6 +50,16 @@ To use the Presets feature, follow these steps: You can create multiple presets with different settings and enable them as global editor commands, making it easy to switch between different configurations as you work. With the Presets feature, you can customize your Companion experience to suit your needs and work more efficiently with AI-powered autocomplete suggestions. +# Completion providers + +This plugin can use more than one source of completions, with more on the way. Currently it can: + +- Ask **ChatGPT** to "Continue the following" +- Use the usual **GPT-3** models +- Use **AI21's Jurassic-2** models + +If there are any sources you'd like to suggest, feel free to open an issue. + # Say Thank You Thanks to all those using my plugin! I made this project as a passion project, and I don't expect to receive any financial compensation for it. However, if you find my work useful and want to support me, feel free to Buy Me A Coffee diff --git a/src/complete/completers.sass b/src/complete/completers.sass index 47feb21..14b969d 100644 --- a/src/complete/completers.sass +++ b/src/complete/completers.sass @@ -1,2 +1,3 @@ @import "completers/openai/openai.sass" -@import "completers/chatgpt/chatgpt.sass" \ No newline at end of file +@import "completers/chatgpt/chatgpt.sass" +@import "completers/ai21/ai21.sass" diff --git a/src/complete/completers.ts b/src/complete/completers.ts index c97fa1c..5f7c631 100644 --- a/src/complete/completers.ts +++ b/src/complete/completers.ts @@ -1,8 +1,10 @@ import { Completer } from "./complete"; import { OpenAIComplete } from "./completers/openai/openai"; import { ChatGPTComplete } from "./completers/chatgpt/chatgpt"; +import { JurassicJ2Complete } from "./completers/ai21/ai21"; export const available: Completer[] = [ new ChatGPTComplete(), new OpenAIComplete(), + new JurassicJ2Complete(), ]; diff --git a/src/complete/completers/ai21/ai21.sass b/src/complete/completers/ai21/ai21.sass new file mode 100644 index 0000000..f2a287b --- /dev/null +++ b/src/complete/completers/ai21/ai21.sass @@ -0,0 +1,5 @@ +.ai-complete-jurassic-expandable + display: flex + flex-direction: row + align-items: center + gap: 0.5rem diff --git a/src/complete/completers/ai21/ai21.ts b/src/complete/completers/ai21/ai21.ts new file mode 100644 index 0000000..a88036d --- /dev/null +++ b/src/complete/completers/ai21/ai21.ts @@ -0,0 +1,72 @@ +import { Completer, Model, Prompt } from "../../complete"; +import available_models from "./models.json"; +import { + SettingsUI as ProviderSettingsUI, + Settings, + parse_settings, +} from "./provider_settings"; + +export default class J2Model implements Model { + id: string; + name: string; + description: string; + + provider_settings: Settings; + + constructor( + id: string, + name: string, + description: string, + provider_settings: string + ) { + this.id = id; + this.name = name; + this.description = description; + this.provider_settings = parse_settings(provider_settings); + } + + async complete(prompt: Prompt): Promise { + if (this.provider_settings.api_key === "") { + throw new Error("API Key not set"); + } + const response = await fetch( + `https://api.ai21.com/studio/v1/j2-${this.id}/complete`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.provider_settings.api_key}`, + }, + body: JSON.stringify({ + prompt: prompt.prefix, + numResults: 1, + ...this.provider_settings.generation_settings, + }), + } + ); + if (!response.ok) { + throw new Error( + `Jurassic-j2 API returned ${response.status} ${ + (await response.json()).detail + }` + ); + } + const data = await response.json(); + return data.completions[0].data.text; + } +} + +export class JurassicJ2Complete implements Completer { + id: string = "jurassic"; + name: string = "AI21 Jurassic"; + description: string = "AI21's Jurassic-j2 models"; + + async get_models(settings: string) { + return available_models.map( + (model) => + new J2Model(model.id, model.name, model.description, settings) + ); + } + + Settings = ProviderSettingsUI; +} diff --git a/src/complete/completers/ai21/models.json b/src/complete/completers/ai21/models.json new file mode 100644 index 0000000..ef230d5 --- /dev/null +++ b/src/complete/completers/ai21/models.json @@ -0,0 +1,17 @@ +[ + { + "id": "large", + "name": "J2-Large", + "description": "Designed for fast responses." + }, + { + "id": "grande", + "name": "J2-Grande", + "description": "Offers enhanced text generation capabilities, making it well-suited to language tasks with a greater degree of complexity." + }, + { + "id": "jumbo", + "name": "J2-Jumbo", + "description": "As the largest and most powerful model in the Jurassic series, J2-Jumbo is an ideal choice for the most complex language processing tasks and generative text applications." + } +] diff --git a/src/complete/completers/ai21/provider_settings.tsx b/src/complete/completers/ai21/provider_settings.tsx new file mode 100644 index 0000000..7612fbb --- /dev/null +++ b/src/complete/completers/ai21/provider_settings.tsx @@ -0,0 +1,349 @@ +import * as React from "react"; +import { useState } from "react"; +import SettingsItem from "../../../components/SettingsItem"; + +export interface PenaltySettings { + scale: number; + applyToWhitespaces?: boolean; + applyToPunctuations?: boolean; + applyToNumbers?: boolean; + applyToStopwords?: boolean; + applyToEmojis?: boolean; +} + +export interface GenerationSettings { + maxTokens?: number; + minTokens?: number; + temperature?: number; + topP?: number; + stopSequences?: string[]; + topKReturn?: number; + frequencyPenalty?: PenaltySettings; + presencePenalty?: PenaltySettings; + countPenalty?: PenaltySettings; +} + +type KeysOfType = keyof { + [P in keyof T as T[P] extends V ? P : never]: any; +}; + +export interface Settings { + api_key: string; + + generation_settings: GenerationSettings; +} + +export const parse_settings = (data: string | null): Settings => { + if (data === null) { + return { api_key: "", generation_settings: {} }; + } + try { + const settings = JSON.parse(data); + if (typeof settings.api_key !== "string") { + return { api_key: "", generation_settings: {} }; + } + return settings; + } catch (e) { + return { api_key: "", generation_settings: {} }; + } +}; + +function GenerationSettingsItem({ + id, + description, + settings, + saveSettings, + parseFn = parseInt, +}: { + id: KeysOfType; + description: string; + settings: string | null; + saveSettings: (settings: string) => void; + parseFn?: (s: string) => number; +}) { + const parsed_settings = parse_settings(settings); + return ( + + + saveSettings( + JSON.stringify({ + ...parsed_settings, + generation_settings: isNaN(parseFn(e.target.value)) + ? (({ [id]: _, ...rest }) => rest)( + parsed_settings.generation_settings + ) + : { + ...parsed_settings.generation_settings, + [id]: parseFn(e.target.value), + }, + }) + ) + } + /> + + ); +} + +function PenaltySettingsBooleanItem({ + id, + item, + description, + settings, + saveSettings, +}: { + id: KeysOfType; + item: KeysOfType; + description: string; + settings: string | null; + saveSettings: (settings: string) => void; +}) { + const parsed_settings = parse_settings(settings); + let state = parsed_settings.generation_settings?.[id]?.[item]; + if (typeof state !== "boolean") { + state = true; + } + return ( + +
+ saveSettings( + JSON.stringify({ + ...parsed_settings, + generation_settings: + parsed_settings.generation_settings && { + ...parsed_settings.generation_settings, + [id]: { + ...parsed_settings + .generation_settings?.[id], + [item]: !state, + }, + }, + }) + ) + } + /> + + ); +} + +function PenaltySettings({ + id, + default_scale, + settings, + saveSettings, +}: { + id: KeysOfType; + default_scale: number; + settings: string | null; + saveSettings: (settings: string) => void; +}) { + const [expanded, setExpanded] = useState(false); + const parsed_settings = parse_settings(settings); + return ( + <> + setExpanded(!expanded)} + > + {expanded ? "▾" : "▸"} + {id} + + } + /> + {expanded && ( + <> + + { + saveSettings( + JSON.stringify({ + ...parsed_settings, + generation_settings: + typeof parseInt(e.target.value) === + "number" && + !isNaN(parseInt(e.target.value)) + ? { + ...parsed_settings.generation_settings, + [id]: { + ...parsed_settings + .generation_settings?.[ + id + ], + scale: parseInt( + e.target.value + ), + }, + } + : (({ [id]: _, ...rest }) => + rest)( + parsed_settings.generation_settings + ), + }) + ); + }} + /> + + {typeof parsed_settings.generation_settings?.[id]?.scale === + "number" && ( + <> + + + + + + + )} + + )} + + ); +} + +export function SettingsUI({ + settings, + saveSettings, +}: { + settings: string | null; + saveSettings: (settings: string) => void; +}) { + const [expanded, setExpanded] = useState(false); + + return ( + <> + + Your{" "} + + AI21 API key + + + } + > + + saveSettings( + JSON.stringify({ api_key: e.target.value }) + ) + } + /> + + setExpanded(!expanded)} + > + {expanded ? "▾" : "▸"} + Advanced + + } + description={ + <> + You can learn more{" "} + + here + + . + + } + /> + {expanded && ( + <> + + + + + + + + + )} + + ); +}