Action

ChatGPT Conversation

Posted by reorx, Last update 10 days ago

UPDATES

10 days ago

  • fix error when draft is empty
show all updates...

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 default https://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
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.