Skip to content
karthink edited this page Jan 16, 2024 · 30 revisions

Defining custom gptel commands

GPTel provides gptel-request, a lower level function, to query ChatGPT with custom behavior.

Its signature is as follows:

(gptel-request
 "my prompt"                                 ;the prompt to send to ChatGPT
 ;; The below keys are all optional
 :buffer   some-buffer-or-name              ;defaults to (current-buffer)
 :system   "Chat directive here"            ;defaults to gptel--system-message
 :position some-pt                          ;defaults to (point)
 :context  (list "any other info")          ;will be available to the callback
 :callback (lambda (response info) ...))    ;called with the response and an info plist
                                            ;defaults to inserting the response at :position

See its documentation for details.

Example 1

For example, to define a command that accepts a prompt in the minibuffer and pops up a window with the response, you could define the following:

(defvar gptel-quick--history nil)
(defun gptel-quick (prompt)
  (interactive (list (read-string "Ask ChatGPT: " nil gptel-quick--history)))
  (when (string= prompt "") (user-error "A prompt is required."))
  (gptel-request
   prompt
   :callback
   (lambda (response info)
     (if (not response)
         (message "gptel-quick failed with message: %s" (plist-get info :status))
       (with-current-buffer (get-buffer-create "*gptel-quick*")
         (let ((inhibit-read-only t))
           (erase-buffer)
           (insert response))
         (special-mode)
         (display-buffer (current-buffer)
                         `((display-buffer-in-side-window)
                           (side . bottom)
                           (window-height . ,#'fit-window-to-buffer))))))))

Example 2

A command that asks ChatGPT to rewrite and replace the current region, sentence or line. Calling with a prefix-arg will query the user for the instructions to include with the text.

(defun gptel-rewrite-and-replace (bounds &optional directive)
  (interactive
   (list
    (cond
     ((use-region-p) (cons (region-beginning) (region-end)))
     ((derived-mode-p 'text-mode)
      (list (bounds-of-thing-at-point 'sentence)))
     (t (cons (line-beginning-position) (line-end-position))))
    (and current-prefix-arg
         (read-string "ChatGPT Directive: "
                      "You are a prose editor. Rewrite my prompt more professionally."))))
  (gptel-request
   (buffer-substring-no-properties (car bounds) (cdr bounds)) ;the prompt
   :system (or directive "You are a prose editor. Rewrite my prompt more professionally.")
   :buffer (current-buffer)
   :context (cons (set-marker (make-marker) (car bounds))
                  (set-marker (make-marker) (cdr bounds)))
   :callback
   (lambda (response info)
     (if (not response)
         (message "ChatGPT response failed with: %s" (plist-get info :status))
       (let* ((bounds (plist-get info :context))
              (beg (car bounds))
              (end (cdr bounds))
              (buf (plist-get info :buffer)))
         (with-current-buffer buf
           (save-excursion
             (goto-char beg)
             (kill-region beg end)
             (insert response)
             (set-marker beg nil)
             (set-marker end nil)
             (message "Rewrote line. Original line saved to kill-ring."))))))))

Embark actions using gptel-request

Generate a summary of a URL with Kagi

(defun my/kagi-summarize (url)
  (let ((gptel-backend gptel--kagi)
        (gptel-model "summarize:agnes")) ;or summarize:cecil, summarize:daphne, summarize:muriel
    (gptel-request
     url
     :callback
     (lambda (response info)
       (if response
           (with-current-buffer (get-buffer-create "*Kagi Summary*")
             (let ((inhibit-read-only t))
               (erase-buffer)
               (visual-line-mode 1)
               (insert response)
               (display-buffer
                (current-buffer)
                '((display-buffer-in-side-window
                   display-buffer-at-bottom)
                  (side . bottom))))
             (special-mode 1))
         (message "gptel-request failed with message: %s"
                  (plist-get info :status)))))))

(keymap-set embark-url-map "=" #'my/kagi-summarize)

Running embark-act on a (text or video) link followed by = will pop up a summary of the link contents at the bottom of the screen.

Formatting in-buffer refactored code

Depending on your model or prompt, sometimes returned in-buffer refactored code would not be perfect, i.e. it would have incorrect indentation or even include Markdown formatting. The below is an example for how to add a hook to automatically fix such cases.

(cl-defun my/clean-up-gptel-refactored-code (beg end)
  "Clean up the code responses for refactored code in the current buffer.

The response is placed between BEG and END.  The current buffer is
guaranteed to be the response buffer."
  (when gptel-mode          ; Don't want this to happen in the dedicated buffer.
    (cl-return-from my/clean-up-gptel-refactored-code))
  (when (and beg end)
    (save-excursion
      (let ((contents
             (replace-regexp-in-string
              "\n*``.*\n*" ""
              (buffer-substring-no-properties beg end))))
        (delete-region beg end)
        (goto-char beg)
        (insert contents))
      ;; Indent the code to match the buffer indentation if it's messed up.
      (indent-region beg end)
      (pulse-momentary-highlight-region beg end))))

Then, where you config gptel, add:

(add-hook 'gptel-post-response-functions #'my/clean-up-gptel-refactored-code)

What this does is remove the Markdown tags and automatically indent the code with respect to the buffer. When you send the prompt, you can see the streamed-in bad code, and after the streaming is complete, it refactors it.

Clone this wiki locally