Action
ChatGPT Conversation
UPDATES
3 months ago
Add support for new models including: gpt-4o, gp4-4o-mini, o1-preview, o1-mini
3 months ago
Add support for new models including: gpt-4o, gp4-4o-mini, o1-preview, o1-mini
10 months ago
- Use Drafts builtin OpenAI integration instead of manually written HTTP calls
- Update models pricing to the latest
- Change default model from gpt-3.5 to gpt-4
over 1 year ago
display tokens usage and cost after running the action, provide an option in template tags to enable this feature or not
over 1 year ago
remove api-base-url template tag, use credential to store apiBaseUrl
over 1 year ago
enhance error handling, show alert with error and HTTP info to help user debugging
almost 2 years ago
- Copy the result content to clipboard after running the action
almost 2 years ago
- fix error when draft is empty
almost 2 years 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
almost 2 years ago
Support undo/redo for the new message appended by this action.
almost 2 years ago
Update description
almost 2 years 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 gpt-model
template gpt-4o
-
defineTemplateTag
name extra-tag
template chatgpt
-
defineTemplateTag
name keep-context
template true
-
defineTemplateTag
name show-tokens-usage
template true
-
script
// credentials const credential = Credential.create('ChatGPT Conversation', "Credentials for ChatGPT Conversation"); credential.addPasswordField('openaiAPIKey', "OpenAI API Key"); credential.addURLField('openaiAPIBaseUrl', "OpenAI API Base URL (optional)"); credential.authorize(); const apiKey = credential.getValue('openaiAPIKey'); const apiBaseUrl = credential.getValue('openaiAPIBaseUrl') || 'https://api.openai.com/v1'; // validate values if (!apiKey) { alert(`OpenAI API Key must be configured to run this action.`) context.cancel() } // tags let gptModel = draft.processTemplate("[[gpt-model]]") || 'gpt-4' let extraTag = draft.processTemplate("[[extra-tag]]") let keepContext = draft.processTemplate("[[keep-context]]") === 'true' let showTokensUsage = draft.processTemplate("[[show-tokens-usage]]") === 'true'
-
script
/** * params: * - gptModel: string * - extraTag: string * - keepContext: boolean * - showTokensUsage: boolean **/ 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)) const handleData = (data) => { const resultMessage = data.choices[0].message const resultContent = resultMessage.content.trim() draft.saveVersion() // save the draft, just in case const textToAppend = '\n' + wrapTextToBlock(resultContent, resultMessage.role) // Use "editor" object enables undo/redo ability whereas "draft.append" cannot. editor.setTextInRange(draft.content.length, 1, '\n' + textToAppend) draft.addTag(extraTag) draft.update() app.setClipboard(resultContent) // display token usage if (showTokensUsage) { const { tokens, cost } = calUsage(data, gptModel) const msg = `Used ${tokens} tokens of $${cost}` console.log(msg) app.displayInfoMessage(msg) } } if (res.success) { try { handleData(res.responseData) console.log(`Chat success`) } catch (err) { const msg = `Chat failed when handling response data.\n\nError:${err}\n\nBody:\n${res.responseText}` console.log(msg) alert(msg) context.fail() } } else { const msg = `Chat failed with HTTP status ${res.statusCode}.\n\nBody:\n${res.responseText}` console.log(msg) alert(msg) context.fail() } } // https://openai.com/pricing // https://platform.openai.com/docs/models // price is per 1k token // last update: 2024-02-29 const modelPriceRegexMap = { // o1-mini 'o1-mini(-[-\d]+)?$': { prompt: 0.003, completion: 0.012, }, // o1-preview 'o1-preview(-[-\d]+)?$': { prompt: 0.015, completion: 0.06, }, // gpt-4o-mini 'gpt-4o-mini(-[-\d]+)?$': { prompt: 0.00015, completion: 0.0006, }, // gpt-4o 'gpt-4o(-[-\d]+)?$': { prompt: 0.005, completion: 0.015, }, // gpt-4 turbo 'gpt-4-(\w+)-preview$': { prompt: 0.01, completion: 0.03, }, // gpt-4 'gpt-4(-\d+)?$': { prompt: 0.03, completion: 0.06, }, // gpt-4 32k 'gpt-4-32k(-\d+)?$': { prompt: 0.06, completion: 0.12, }, // gpt-3.5 turbo 'gpt-3.5-turbo(-\d+)?$': { prompt: 0.0005, completion: 0.0015, } } function calUsage(data, model) { // 'usage': {'prompt_tokens': 56, 'completion_tokens': 31, 'total_tokens': 87}, const { prompt_tokens, completion_tokens, total_tokens } = data.usage let priceObj for (let key in modelPriceRegexMap) { const re = new RegExp(key) if (re.test(model)) { priceObj = modelPriceRegexMap[key] break } } if (!priceObj) { throw Error(`could not get price from model ${model}`) } const cost = (prompt_tokens * priceObj.prompt / 1000) + (completion_tokens * priceObj.completion / 1000) const costFixed = cost.toFixed(6) console.log(`calUsage: usage=${JSON.stringify(data.usage)} model=${model} priceObj=${JSON.stringify(priceObj)} cost=${cost} costFixed=${costFixed}`) return { tokens: total_tokens, cost: costFixed, } } function sendChat(messages) { let ai = OpenAI.create(apiKey, apiBaseUrl) ai.model = gptModel let res = ai.request({ path: '/chat/completions', method: 'POST', data: { model: gptModel, 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 Error Log Level Info