Action
ChatGPT Conversation
UPDATES
10 days ago
- fix error when draft is empty
10 days ago
- fix error when draft is empty
10 days ago
- add new template tag
api-base-url
, allowing for setting the API URL being used, by defaulthttps://api.openai.com/v1/
is used if leave the tag empty - fix response message not trimmed before wrapping into block
17 days ago
Support undo/redo for the new message appended by this action.
18 days ago
Update description
18 days ago
update description
Topic link: https://forums.getdrafts.com/t/chatgpt-conversation-action/14042
Have a conversation with ChatGPT in the note. Any new responses will be appended at the end.
Upon running the action, the OpenAI API key is required, which can be obtained from https://platform.openai.com/account/api-keys.
The simplest way to use it is to create a new draft and type whatever you would like to ask. Then, call the action, and the result will be appended to the end of the draft.
You can also add <!-- block system -->
at the top of the draft to tell ChatGPT what you want it to be. There are some curated prompts for your inspiration in this repo: Awesome ChatGPT Prompts
Note that the messages ChatGPT returns will be wrapped in <!-- block assistant -->
. The action will recognize them as assistant role messages and send them to the ChatGPT API as part of the conversation. So, don’t make the conversation too long; otherwise, the token will be consumed very quickly.
You can change the default behavior of using the whole draft as the context in the conversation by setting the template tag keep-context
to false
. This will make the action only send the system role message and the last user role message to ChatGPT API.
Steps
-
defineTemplateTag
name api-base-url
template -
defineTemplateTag
name extra-tag
template chatgpt
-
defineTemplateTag
name keep-context
template true
-
script
// credentials const credential = Credential.create('ChatGPT Conversation', "Credentials for ChatGPT Conversation"); credential.addPasswordField('openaiAPIKey', "OpenAI API Key"); credential.authorize(); const openaiAPIKey = credential.getValue('openaiAPIKey'); // validate values if (!openaiAPIKey) { alert(`OpenAI API Key must be configured to run this action.`) context.cancel() } // tags let apiBaseUrl = draft.processTemplate("[[api-base-url]]") || 'https://api.openai.com/v1/' let extraTag = draft.processTemplate("[[extra-tag]]") let keepContext = draft.processTemplate("[[keep-context]]") === 'true'
-
script
function main() { const content = draft.content let blocks = parseTextToBlocks(content) if (blocks.length === 0) { alert("Please provide a prompt.") context.cancel() return } // check last block, must be user role message const lastBlock = blocks[blocks.length - 1] if (lastBlock.role !== ROLE_NAME.user) { alert("Please provide a prompt instead of just sending system and assistant messages.") context.cancel() return } // keep context or only use system role message and last user role message if (!keepContext) { const systemBlocks = blocks.filter(block => block.role === ROLE_NAME.system) const userBlocks = blocks.filter(block => block.role === ROLE_NAME.user) blocks = systemBlocks if (userBlocks.length) { blocks.push(userBlocks[userBlocks.length - 1]) } } const res = sendChat(blocksToMessages(blocks)) if (res.success) { const data = res.responseData const resultMessage = data.choices[0].message draft.saveVersion() // save the draft, just in case const textToAppend = '\n' + wrapTextToBlock(resultMessage.content.trim(), resultMessage.role) // draft.append(textToAppend) // Use "editor" enables undo/redo ability whereas "draft" cannot. editor.setTextInRange(draft.content.length, 1, '\n' + textToAppend) draft.addTag(extraTag) draft.update() console.log(`Chat success`) } else { console.log(`Chat failed: ${res.statusCode}, ${res.error}`) context.fail() } } function sendChat(messages) { let http = HTTP.create(); let res = http.request({ url: apiBaseUrl + 'chat/completions', method: 'POST', headers: { Authorization: `Bearer ${openaiAPIKey}`, }, data: { model: "gpt-3.5-turbo", messages, } }) return res } const ROLE_NAME = { system: 'system', assistant: 'assistant', user: 'user', } const blockStartRegex = /^\<\!--\s?block (\w+)\s?--\>$/ const blockEndRegex = /^\<\!--\s?endblock\s?--\>$/ function parseStatement(line) { const obj = { role: null, blockStart: false, blockEnd: false, } let match = line.match(blockStartRegex) if (match) { obj.role = match[1] obj.blockStart = true return obj } match = line.match(blockEndRegex) if (match) { obj.blockEnd = true } return obj } function parseTextToBlocks(text) { let lineno = 0 let currentBlock = null let blocks = [] const createBlock = (role) => { const block = { role, lines: [], } blocks.push(block) return block } text.trim().split('\n').forEach(line => { lineno += 1 const statement = parseStatement(line) if (statement.blockStart) { if (currentBlock && currentBlock.role !== ROLE_NAME.user) { throw `Invalid role-block start at line ${lineno}: ${statement.role} started before ${currentBlock.role} ends` } currentBlock = createBlock(statement.role) } else if (statement.blockEnd) { if (!currentBlock) { throw `Invalid role-block end at line ${lineno}: no role-block exists` } currentBlock = null } else { if (!currentBlock) { currentBlock = createBlock(ROLE_NAME.user) } currentBlock.lines.push(line) } }) if (currentBlock) { if (currentBlock.role !== ROLE_NAME.user) { throw `Invalid role-block at line ${lineno}: ${currentBlock.role} is not ended` } currentBlock = null } // filter empty blocks blocks = blocks.filter(block => { if (block.lines.join('')) { return true } return false }) return blocks } function blocksToMessages(blocks) { return blocks.map(block => ({ role: block.role, content: block.lines.join('\n').trim(), })) } function wrapTextToBlock(text, role) { return `<!-- block ${role} --> ${text} <!-- endblock -->` } // call main main()
Options
-
After Success Default Notification Info Log Level Info