Skip to content
karthink edited this page Apr 9, 2024 · 27 revisions

Save transient flags

Any options you set from gptel's transient menu (except for the model parameters) are only active for the next query. If you want these flags to be persistent, you can press C-x s when in the transient menu:

gptel-save

Now options you choose will remain set until you change them. Pressing C-x C-s will save them across Emacs sessions.

However, you still have to bring up the menu to send a query with these saved options. You can replace your usage of gptel-send with the following command if you want these options to be applied without having to bring up the menu:

(defun gptel-send-with-options (&optional arg)
  "Send query.  With prefix ARG open gptel's menu instead."
  (interactive "P")
  (if arg
      (call-interactively 'gptel-menu)
    (gptel--suffix-send (transient-args 'gptel-menu))))

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

;; Create a kagi backend if you don't have one defined
(defvar gptel--kagi
  (gptel-make-kagi "Kagi" :key "YOUR_KAGI_KEY")) ;or function that returns a key

;; Function that requests kagi for a url summary and shows it in a side-window
(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)))))))

;; Make this function available to Embark
(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.

Rendering LaTeX in dedicated chat buffer

latex

Sometimes the chat model likes to talk in LaTeX when you ask it math questions. Most online LLM interfaces support rendering of LaTeX through MathJax. Below is some code that will configure Org mode to allow rendering LaTeX, even if your GPTel buffer does not use Org mode. This is achieved by switching to Org, rendering the LaTeX (if any are detected) and then switching back to the previous mode.

This is quite experimental, and might have some side effects with certain modes.

;; If you already have an Org config, then you need to adjust it so that it has
;; the below adjustments.
(use-package org
  :config
  ;; Adjust the colors to fit the Org buffer. This only applies if your
  ;; dedicated GPTel buffer is Org mode.
  (plist-put org-format-latex-options :foreground nil)
  (plist-put org-format-latex-options :background nil)
  ;; Use a different process for generating LaTeX so that we can scale it.
  (customize-set-variable 'org-preview-latex-default-process 'dvisvgm)
  ;; Make it so that the LaTeX scales when the text scale is adjusted.
  (add-hook 'org-mode-hook (lambda ()
                             (add-hook 'text-scale-mode-hook
                                       (lambda ()
                                         (my/resize-org-latex-overlays)
                                         (my/adjust-org-latex-overlay-padding))
                                       nil t))))

;; If you are using Markdown for your dedicated GPTel buffer, then this is
;; required.
(use-package markdown
  :config
  (add-hook 'markdown-mode-hook
            (lambda ()
              (add-hook 'text-scale-mode-hook
                        (lambda ()
                          ;; Even though this is for Org mode, we can use it in
                          ;; other buffers.
                          (my/resize-org-latex-overlays)
                          (my/adjust-org-latex-overlay-padding))
                        nil t))))

;; Add the hook so that the below functions will run once the LLM response has
;; finished.
(add-hook 'gptel-post-response-functions #'my/clean-up-llm-response)

(defun my/adjust-org-latex-overlay-padding ()
  (save-excursion
    (goto-char (point-min))
    (while (search-forward "\\[" nil t)
      (let* ((o (car (overlays-at (point))))
             (display-plist (cdr (overlay-get o 'display))))
        (when (eq (overlay-get o 'org-overlay-type) 'org-latex-overlay)
          (plist-put display-plist :margin
                     (cons 0 (default-line-height))))))))

(defun my/resize-org-latex-overlays ()
  (defvar text-scale-mode-step)
  (defvar text-scale-mode-amount)
  (cl-loop for o in (car (overlay-lists))
           if (eq (overlay-get o 'org-overlay-type) 'org-latex-overlay)
           do (plist-put (cdr (overlay-get o 'display))
		         :scale (expt text-scale-mode-step
				      text-scale-mode-amount))))

(defun my/clean-up-llm-chat-response (beg end)
  (when (save-excursion
          (goto-char beg)
          (re-search-forward (rx (or (group "\\" (zero-or-more any) "\\")
                                     (group "[" (zero-or-more any) "]")))
                             end t))
    (goto-char beg)
    (let ((original-mode major-mode)
          (text-scale-amount text-scale-mode-amount))
      (unless (eq major-mode 'org-mode)
        (org-mode))
      (org-latex-preview)
      (unless (eq original-mode 'org-mode)
        (funcall original-mode)
        (gptel-mode)
        (text-scale-set text-scale-amount))
      (my/resize-org-latex-overlays)
      (my/adjust-org-latex-overlay-padding)
      (pulse-momentary-highlight-region beg end))))

(defun my/clean-up-llm-response (beg end)
  (if (bound-and-true-p gptel-mode) ; Make sure we are in the dedicated buffer.
      (my/clean-up-llm-chat-response beg end))