Plugin Example: OpenAI Plugin

To get an OpenAI API key, sign up for OpenAI, then visit OpenAI's API keys page.


View this plugin in the Amplenote Plugin Directory | Install this plugin | View this plugin as a public note


name

OpenAI

description

Experimental OpenAI prompts to revise and generate note content.

icon

psychology

setting

API Key

setting

OpenAI model (e.g., 'gpt-4'. Leave blank for gpt-3.5-turbo)

instructions

There are currently three ways to invoke this plugin:

1. Select a range of text, choose one of the options in the dropdown menu for the rightmost icon
2. Click the triple-dot for a note and choose one of the options that has the AI logo
3. Insert evaluation brackets and start typing, e.g., {answer

This plugin sends requests to the OpenAI model specified (defaults to ChatGPT aka GPT-3.5, but can be swapped to gpt-4 if you have access) endpoint and requires an OpenAI account with an active subscription. To get an OpenAI API key, sign up for OpenAI (https://platform.openai.com/signup), then visit OpenAI's API keys page (https://platform.openai.com/account/api-keys).

If you find that all API calls fail, confirm that you have credits in your OpenAI account.

Use AI Plugin to:
* Summarize a long blog post or essay
* Answer a question
* Continue a sentence or paragraph in progress
* Revise or insert note content
* Configure setup with GPT-4
* Insert a rhyming word


{
insertText: {
"Answer": async function(app) {
const instruction = await app.prompt("What question would you like answered?");
if (!instruction) return null;
 
const answer = await this._noteOptionInsertContent(app, app.context.noteUUID, "answer", { instruction });
if (answer) {
return answer;
} else {
app.alert("Could not determine an answer to the provided question")
return null;
}
},
"Continue": async function(app) {
const answer = await this._noteOptionInsertContent(app, app.context.noteUUID, "continue");
if (answer) {
app.context.replaceSelection(answer);
} else {
app.alert("Could not determine an answer to the provided question")
return null;
}
},
},
 
// --------------------------------------------------------------------------
// https://www.amplenote.com/help/developing_amplenote_plugins#noteOption
noteOption: {
"Answer": async function(app, noteUUID) {
const instruction = await app.prompt("What question should be answered?");
if (!instruction) return;
 
await this._noteOptionInsertContent(app, noteUUID, "answer", { instruction }, true);
},
"Revise": async function(app, noteUUID) {
const instruction = await app.prompt("How should this note be revised?");
if (!instruction) return;
 
await this._noteOptionInsertContent(app, noteUUID, "reviseContent", { instruction }, true);
},
"Summarize": async function(app, noteUUID) {
await this._noteOptionInsertContent(app, noteUUID, "summarize", {}, true);
},
},
 
// --------------------------------------------------------------------------
// https://www.amplenote.com/help/developing_amplenote_plugins#replaceText
replaceText: {
"Complete": async function(app, text) {
const result = await this._callOpenAI(app, "complete", text);
if (result === null) return null;
 
return text + " " + result;
},
"Revise": async function(app, text) {
const instruction = await app.prompt("How should this text be revised?");
if (!instruction) return null;
 
const result = await this._callOpenAI(app, "reviseText", [ instruction, text ]);
if (result === null) return null;
 
app.alert(result);
return null;
},
"Rhymes": async function(app, text) {
const noteUUID = app.context.noteUUID;
const note = await app.notes.find(noteUUID);
const noteContent = await note.content();
 
const result = await this._noteOptionInsertContent(app, app.context.noteUUID, "rhyming", { text });
const optionList = result?.split("\n")?.map(word => word.replace(/^[\d]+\.?[\s]?/g, ""))
if (optionList?.length) {
const selectedValue = await app.prompt(`Choose a replacement for "${ text }"`, {
inputs: [ {
type: "radio",
label: `${ optionList.length } synonym${ optionList.length === 1 ? "" : "s" } found`,
options: optionList.map(option => ({ label: option.toLowerCase(), value: option.toLowerCase() }))
} ]
});
if (selectedValue) return selectedValue;
} else {
app.alert("Got no rhymes");
}
return null;
},
},
 
// --------------------------------------------------------------------------
// Private methods
// --------------------------------------------------------------------------
 
// --------------------------------------------------------------------------
// `promptKey` is a key that should be present among this._userPrompts, below
async _noteOptionInsertContent(app, noteUUID, promptKey, { instruction = null, text = null } = {}, confirmInsert = null) {
const note = await app.notes.find(noteUUID);
const noteContent = await note.content();
 
const result = await this._callOpenAI(app, promptKey, { instruction, noteContent, text });
if (result === null) return;
 
if (confirmInsert) {
const actionIndex = await app.alert(result, {
actions: [ { icon: "post_add", label: "Insert in note" } ]
});
if (actionIndex === 0) {
note.insertContent(result);
}
} else {
return result;
}
},
 
// --------------------------------------------------------------------------
// `promptParams` is an object consisting of `noteContent` key and an optional `instructions` key
async _callOpenAI(app, promptType, promptParams) {
let messages = [];
 
const systemPrompt = this._systemPrompts[promptType] || this._systemPrompts.default;
messages.push({ role: "system", content: systemPrompt });
 
const userPrompt = this._userPrompts[promptType](promptParams);
if (Array.isArray(userPrompt)) {
userPrompt.forEach(content => {
messages.push({ role: "user", content: this._truncate(content) });
});
} else {
messages.push({ role: "user", content: this._truncate(userPrompt) });
}
 
try {
const settingModel = app.settings["OpenAI model (e.g., 'gpt-4'. Leave blank for gpt-3.5-turbo)"];
const model = settingModel && settingModel.trim().length ? settingModel : "gpt-3.5-turbo";
 
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${ app.settings["API Key"] }`,
"Content-Type": "application/json"
},
body: JSON.stringify({ model, messages })
});
 
if (response.ok) {
const result = await response.json();
 
const { choices: [ { message: { content } } ] } = result;
return content;
} else if (response.status === 401) {
app.alert("Invalid OpenAI key. Please configure your OpenAI key in plugin settings.");
return null;
} else {
const result = await response.json();
if (result && result.error) {
app.alert("Failed to call OpenAI: " + result.error.message);
return null;
}
}
} catch (error) {
app.alert("Failed to call OpenAI: " + error);
return null;
}
},
 
// --------------------------------------------------------------------------
// GPT-3.5 has a 4097 token limit, so very much approximating that by limiting to 10k characters
_truncate(text, limit = 10000) {
return text.length > limit ? text.slice(0, limit) : text;
},
 
// --------------------------------------------------------------------------
_systemPrompts: {
default: "You are a helpful assistant helping continue writing markdown-formatted content.",
reviseContent: "You are a helpful assistant that revises markdown-formatted content, as instructed.",
reviseText: "You are a helpful assistant that revises text, as instructed.",
summarize: "You are a helpful assistant that summarizes notes that are markdown-formatted.",
},
 
// --------------------------------------------------------------------------
_userPrompts: {
answer: ({ instruction }) => ([ `Succinctly answer the following question: ${ instruction }`, "Do not explain your answer. Do not mention the question that was asked. Do not include unnecessary punctuation." ]),
complete: ({ noteContent }) => `Continue the following markdown-formatted content:\n\n${ noteContent }`,
continue: ({ noteContent }) => ([
`Respond with text that could be used to replace the token in the following input markdown document, which begins and ends with triple tildes:`,
`~~~\n${ noteContent.replace("{OpenAI: Continue}", "") }\n~~~`,
`The resulting text should be grammatically correct and make sense in context. Do not explain how you derived your answer. Do not explain why you chose your answer. Do not respond with the token itself. Do not repeat the note content.`,
]),
reviseContent: ({ noteContent, instruction }) => [ instruction, noteContent ],
reviseText: ({ instruction, text }) => [ instruction, text ],
rhyming: ({ noteContent, text }) => ([
`You are a rhyming word generator. Respond only with a numbered list of the 10 best rhymes to replace the word "${ text }"`,
`The suggested replacements will be inserted in place of the ${ text } token in the following markdown document:\n~~~\n${ noteContent.replace(text, `${ text }`) }\n~~~`,
`Respond with up to 10 rhyming words that can be inserted into the document, each of which is 3 or less words. Do not repeat the input content. Do not explain how you derived your answer. Do not explain why you chose your answer. Do not respond with the token itself.`
]),
summarize: ({ noteContent }) => `Summarize the following markdown-formatted note:\n\n${ noteContent }`,
},
}


linkVersion History

May 24, 2023. Added "Continue," "Rhyming," and "Answer" options

April 12, 2023. Added gpt-4 as option

March 29, 2023. Initial implementation