aidantilgner_AutogenObsidia.../main.ts

448 lines
11 KiB
TypeScript
Raw Normal View History

2024-02-01 21:24:18 +00:00
import {
App,
Editor,
Modal,
Plugin,
PluginSettingTab,
Setting,
2024-02-02 05:06:20 +00:00
EditorSuggest,
EditorPosition,
EditorSuggestTriggerInfo,
TFile,
EditorSuggestContext,
2024-02-01 21:24:18 +00:00
} from "obsidian";
import OpenAI from "openai";
import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions";
2024-02-02 05:06:20 +00:00
import { getClient, getGeneration } from "utils/openai";
2024-02-01 19:20:00 +00:00
2024-02-01 21:24:18 +00:00
interface AutogenSettings {
openaiApiKey: string;
2024-02-19 23:34:24 +00:00
customURL: string;
2024-02-01 21:24:18 +00:00
model: ChatCompletionCreateParamsBase["model"];
2024-02-01 22:28:47 +00:00
triggerRegex: string;
windowSize: number;
2024-02-02 05:06:20 +00:00
systemPrompt: string;
2024-02-01 19:20:00 +00:00
}
2024-02-01 21:24:18 +00:00
const DEFAULT_SETTINGS: AutogenSettings = {
openaiApiKey: "",
2024-02-19 23:34:24 +00:00
customURL: "",
2024-02-01 21:24:18 +00:00
model: "gpt-3.5-turbo",
2024-02-01 22:28:47 +00:00
triggerRegex: "@\\[(.*?)\\]",
windowSize: 8000,
2024-02-02 05:06:20 +00:00
systemPrompt: `# Identity:
You are a helpful content generator. Given a selection of text, you are tasked with generating a replacement for the selection.
# Your Role
Based on the context of the full text, and the selection itself, you are to generate a replacement for the given selection.
The selection might take the form of an instruction, something to elaborate on, a transformation of some other part of the text, or some other prompt.
The goal is to provide the best completion for the given selection, based on the context of the full text and the intent of the author.
# Things to remember:
- Markdown is supported
- This is an Obsidian plugin, so you can use Obsidian-specific syntax
`,
2024-02-01 21:24:18 +00:00
};
2024-02-01 19:20:00 +00:00
2024-02-01 21:24:18 +00:00
export default class Autogen extends Plugin {
settings: AutogenSettings;
openaiClient: OpenAI | null = null;
2024-02-01 19:20:00 +00:00
async onload() {
await this.loadSettings();
2024-02-01 21:24:18 +00:00
this.addSettingTab(new AutogenSettingTab(this.app, this));
2024-02-01 19:20:00 +00:00
2024-02-01 21:24:18 +00:00
this.initOpenAIClient();
2024-02-02 05:06:20 +00:00
// add the suggester
this.registerEditorSuggest(new AutogenSuggest(this));
2024-02-01 21:24:18 +00:00
}
initOpenAIClient() {
if (this.settings.openaiApiKey) {
if (this.settings.customURL) {
2024-02-19 23:34:24 +00:00
this.openaiClient = getClient(
this.settings.openaiApiKey,
this.settings.customURL
);
} else {
this.openaiClient = getClient(this.settings.openaiApiKey);
}
2024-02-01 21:24:18 +00:00
}
}
2024-02-02 05:06:20 +00:00
matchRegex(editor: Editor) {
const cursor = editor.getCursor();
const content = editor.getLine(cursor.line);
const match = content.match(this.settings.triggerRegex);
return match;
2024-02-01 21:24:18 +00:00
}
2024-02-02 05:06:20 +00:00
async triggerGeneration(editor: Editor) {
2024-02-01 22:28:47 +00:00
const pattern = new RegExp(this.settings.triggerRegex, "g");
2024-02-01 21:24:18 +00:00
const content = editor.getValue();
const match = pattern.exec(content);
2024-02-02 05:06:20 +00:00
const windowSize = this.settings.windowSize;
2024-02-01 21:24:18 +00:00
if (match !== null) {
const start = Math.max(match.index - windowSize / 2, 0);
const end = Math.min(
match.index + match[0].length + windowSize / 2,
content.length
);
const textWindow = content.substring(start, end);
2024-02-02 05:06:20 +00:00
const modal = new AutogenModal(this, match[0], textWindow, editor);
2024-02-01 21:24:18 +00:00
modal.open();
}
2024-02-01 19:20:00 +00:00
}
2024-02-01 21:24:18 +00:00
async generateReplacementText(window: string, match: string) {
if (this.openaiClient === null) {
this.initOpenAIClient();
}
if (this.openaiClient !== null) {
2024-02-02 05:06:20 +00:00
const replacement = await getGeneration({
client: this.openaiClient,
model: this.settings.model,
systemPrompt: this.settings.systemPrompt,
textWindow: window,
match,
});
2024-02-01 21:24:18 +00:00
if (replacement) {
return replacement;
} else {
console.error("Error generating replacement text");
return "Error: Error generating replacement text";
}
} else {
console.error("OpenAI client not initialized");
return "Error: OpenAI client not initialized. Please make sure there is an API key set in the settings.";
}
}
2024-02-01 19:20:00 +00:00
2024-02-01 21:24:18 +00:00
replaceText(editor: Editor, match: string, replacement: string) {
const content = editor.getValue();
const newContent = content.replace(match, replacement);
editor.setValue(newContent);
2024-02-02 22:29:03 +00:00
// find the line number of the replacement string
const matchIndex = newContent.indexOf(replacement);
const lineNumber = newContent.substr(0, matchIndex).split("\n").length;
// find the character position of the replacement string
const line = editor.getLine(lineNumber);
const charPosition = line.indexOf(replacement);
editor.setCursor({
line: lineNumber,
ch: charPosition,
});
2024-02-01 19:20:00 +00:00
}
2024-02-01 21:24:18 +00:00
onunload() {}
2024-02-01 19:20:00 +00:00
async loadSettings() {
2024-02-01 21:24:18 +00:00
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
2024-02-01 19:20:00 +00:00
}
async saveSettings() {
await this.saveData(this.settings);
}
}
2024-02-02 05:06:20 +00:00
interface Suggestion {
label: string;
id: string;
}
2024-02-01 21:24:18 +00:00
2024-02-02 05:06:20 +00:00
class AutogenSuggest extends EditorSuggest<Suggestion> {
private editor: Editor;
constructor(public plugin: Autogen) {
super(plugin.app);
this.setInstructions([
{
command: "Press Enter",
purpose: "to create generation",
},
]);
}
onTrigger(
cursor: EditorPosition,
editor: Editor,
file: TFile | null
): EditorSuggestTriggerInfo | null {
if (this.plugin.matchRegex(editor) == null) {
return null;
}
this.editor = editor;
return {
start: cursor,
end: cursor,
query: "",
};
}
getSuggestions(
context: EditorSuggestContext
): Suggestion[] | Promise<Suggestion[]> {
return [
{
label: "Get suggestions",
2024-02-02 05:06:20 +00:00
id: "getSuggestions",
},
];
}
selectSuggestion(value: Suggestion, evt: MouseEvent | KeyboardEvent): void {
switch (value.id) {
case "getSuggestions":
this.plugin.triggerGeneration(this.editor);
break;
default:
break;
}
}
renderSuggestion(value: Suggestion, el: HTMLElement): void {
el.setText(value.label);
}
}
class AutogenModal extends Modal {
public match = "";
public plugin: Autogen;
public textWindow = "";
public editor: Editor;
constructor(
plugin: Autogen,
match: string,
textWindow: string,
editor: Editor
) {
super(plugin.app);
2024-02-01 21:24:18 +00:00
this.match = match;
2024-02-02 05:06:20 +00:00
this.plugin = plugin;
this.textWindow = textWindow;
this.editor = editor;
2024-02-01 19:20:00 +00:00
}
2024-02-02 05:06:20 +00:00
async onOpen() {
2024-02-01 21:24:18 +00:00
const { contentEl, titleEl } = this;
2024-02-02 05:06:20 +00:00
contentEl.empty();
titleEl.setText("Generating replacement...");
const loadingEl = createEl("p", {
text: "Loading...",
});
contentEl.appendChild(loadingEl);
2024-02-01 22:28:47 +00:00
2024-02-02 05:06:20 +00:00
const replacement = await this.plugin.generateReplacementText(
this.textWindow,
this.match
);
contentEl.empty();
titleEl.setText("Replace selection with:");
2024-02-01 21:24:18 +00:00
const textEl = createEl("p", {
2024-02-02 05:06:20 +00:00
text: replacement,
2024-02-01 21:24:18 +00:00
});
contentEl.appendChild(textEl);
contentEl.createEl("br");
2024-02-01 22:28:47 +00:00
const buttonContainer = createEl("div");
buttonContainer.addClass("autogen-modal-button-container");
2024-02-01 21:24:18 +00:00
2024-02-02 05:06:20 +00:00
const confirmButton = createEl("button", { text: "Confirm" });
const cancelButton = createEl("button", { text: "Cancel" });
confirmButton.addEventListener("click", () => {
this.plugin.replaceText(this.editor, this.match, replacement);
this.close();
});
2024-02-01 21:24:18 +00:00
cancelButton.addEventListener("click", () => {
this.close();
});
2024-02-01 22:28:47 +00:00
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(confirmButton);
2024-02-02 05:06:20 +00:00
this.contentEl.appendChild(buttonContainer);
confirmButton.focus();
2024-02-01 19:20:00 +00:00
}
onClose() {
2024-02-01 21:24:18 +00:00
const { contentEl } = this;
2024-02-01 19:20:00 +00:00
contentEl.empty();
}
}
2024-02-01 21:24:18 +00:00
class AutogenSettingTab extends PluginSettingTab {
plugin: Autogen;
2024-02-01 19:20:00 +00:00
2024-02-01 21:24:18 +00:00
constructor(app: App, plugin: Autogen) {
2024-02-01 19:20:00 +00:00
super(app, plugin);
this.plugin = plugin;
}
display(): void {
2024-02-01 21:24:18 +00:00
const { containerEl } = this;
2024-02-01 19:20:00 +00:00
containerEl.empty();
2024-02-02 05:06:20 +00:00
const apiKeyDesc = document.createDocumentFragment();
// Create a span element to hold the text
const span = document.createElement("span");
span.textContent = "Your OpenAI API Key (find or create one ";
// Create the anchor element for the link
const link = document.createElement("a");
link.href = "https://platform.openai.com/api-keys";
link.textContent = "here";
2024-02-19 23:34:24 +00:00
link.target = "_blank";
2024-02-02 05:06:20 +00:00
apiKeyDesc.appendChild(span);
apiKeyDesc.appendChild(link);
2024-02-19 23:34:24 +00:00
apiKeyDesc.appendChild(document.createTextNode(")."));
2024-02-02 05:06:20 +00:00
2024-02-01 19:20:00 +00:00
new Setting(containerEl)
.setName("OpenAI API key")
2024-02-02 05:06:20 +00:00
.setDesc(apiKeyDesc)
2024-02-01 21:24:18 +00:00
.addText((text) =>
text
.setPlaceholder("Enter your API Key")
.setValue(this.plugin.settings.openaiApiKey)
.onChange(async (value) => {
this.plugin.settings.openaiApiKey = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Model")
2024-02-19 23:34:24 +00:00
.setDesc("The model to use for generating text.")
2024-02-01 21:24:18 +00:00
.addDropdown((dropdown) => {
dropdown
.addOption("gpt-3.5-turbo", "GPT-3.5 Turbo")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
dropdown
.addOption("gpt-3.5-turbo-16k", "GPT-3.5 Turbo 16k")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
dropdown
.addOption("gpt-4", "GPT-4")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
dropdown
.addOption("gpt-4-32k", "GPT-4 32k")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
2024-02-19 23:34:24 +00:00
dropdown
.addOption("custom", "Custom")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
2024-02-01 21:24:18 +00:00
});
2024-02-01 22:28:47 +00:00
2024-02-19 23:34:24 +00:00
new Setting(containerEl)
.setName("Custom URL")
.setDesc(
"Set a custom URL (e.g. for proxy or local models with OpenAI-compatible API). Leave blank for OpenAI default."
)
.addText((text) =>
text
.setPlaceholder(
"Custom URL (leave blank for OpenAI default)"
)
.setValue(this.plugin.settings.customURL)
.onChange(async (value) => {
this.plugin.settings.customURL = value;
await this.plugin.saveSettings();
})
);
2024-02-01 22:28:47 +00:00
new Setting(containerEl)
.setName("Trigger regex")
2024-02-19 23:34:24 +00:00
.setDesc("The regex pattern to trigger the autogen.")
2024-02-01 22:28:47 +00:00
.addText((text) =>
text
.setPlaceholder("Enter the regex pattern")
.setValue("@\\[(.*?)\\]")
.onChange(async (value) => {
this.plugin.settings.triggerRegex = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Window size")
2024-02-01 22:28:47 +00:00
.setDesc(
"The max number of characters, not including the prompt, to be sent for the generation. This affects token usage and performance."
)
.addText((text) =>
text
.setPlaceholder("Enter the window size")
.setValue(this.plugin.settings.windowSize.toString())
.onChange(async (value) => {
this.plugin.settings.windowSize = parseInt(value);
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("System prompt")
2024-02-19 23:34:24 +00:00
.setDesc(
"The system prompt to be used by the chosen model, sent to the API."
)
2024-02-02 05:06:20 +00:00
.addTextArea((text) => {
text.setPlaceholder("Enter the system prompt")
.setValue(this.plugin.settings.systemPrompt)
2024-02-01 22:28:47 +00:00
.onChange(async (value) => {
2024-02-02 05:06:20 +00:00
this.plugin.settings.systemPrompt = value;
2024-02-01 22:28:47 +00:00
await this.plugin.saveSettings();
2024-02-02 05:06:20 +00:00
});
text.inputEl.addClass("autogen-systemprompt-textarea");
2024-02-02 05:06:20 +00:00
});
2024-02-01 19:20:00 +00:00
}
}