Skip to content
karthink edited this page Jan 13, 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."))))))))

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 ()
  "Clean up the code responses for refactored code in the current buffer.
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))
  (save-excursion
    (let* ((res-beg (point-min))
           (res-end nil)
           (contents nil))
      (unless (get-text-property res-beg 'gptel)
        (setq res-beg (next-single-property-change res-beg 'gptel)))
      (while res-beg
        (setq res-end (next-single-property-change res-beg 'gptel))
        (unless res-end
          (setq res-end (point-max)))
        (setq contents (buffer-substring-no-properties res-beg
                                                       res-end))
        (setq contents (replace-regexp-in-string "\n*``.*\n*"
                                                 ""
                                                 contents))
        (delete-region res-beg res-end)
        (goto-char res-beg)
        (insert contents)
        (setq res-end (point))
        ;; Indent the code to match the buffer indentation if it's messed up.
        (indent-region res-beg res-end)
        (pulse-momentary-highlight-region res-beg res-end)
        (setq res-beg (next-single-property-change res-beg 'gptel))))))

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