Skip to content

Defining custom gptel commands

karthink edited this page Dec 26, 2024 · 3 revisions

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: A one-shot prompt from the minibuffer

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-lookup--history nil)

(defun gptel-lookup (prompt)
  (interactive (list (read-string "Ask ChatGPT: " nil gptel-lookup--history)))
  (when (string= prompt "") (user-error "A prompt is required."))
  (gptel-request
   prompt
   :callback
   (lambda (response info)
     (if (not response)
         (message "gptel-lookup failed with message: %s" (plist-get info :status))
       (with-current-buffer (get-buffer-create "*gptel-lookup*")
         (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: Rewrite a region

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. (Note that gptel includes a refactoring interface, so this is purely for demonstration.)

(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."))))))))

Example 3: Quickly explain the thing or region at point

A slightly more comprehensive use of gptel-request can be found in the gptel-quick package. The included gptel-quick command shows a short summary or explanation of the word at point, or an active region, in a popup. This is useful for quickly looking up names, words, phrases, or summarizing/explaining prose or snippets of code, with minimal friction:

gptel-quick-simple.webm

And you can tweak the detail in the response on the fly:

gptel-quick-make-longer.webm

Example 4: Get the LLM to name your chat buffer (#520)

Feed the LLM the text of your chat buffer and get it to suggest a name. Prompt the user and rename the buffer/file appropriately.

This shows and advanced gptel-request feature: basically, you construct a conversation consisting of this sequence:

system message: "I will provide a transcript of a chat with an LLM.  Suggest a short and informative name for a file to store this chat in..."

user: <no message>

assistant: What is the chat content?

user: <contents of buffer here>

The system message is supplied to gptel-request via the :system keywrod argument. The series of exchanges is supplied as a list:

(nil                                    ;no user message
 "What is the chat content?"            ;llm response, prefilled
 (buffer-substring-no-properties ...))  ;user message

In the :callback argument, we collect the response string and use it to rename the buffer or file.

Here is gptel-rename-chat:

(defun gptel-rename-chat ()
  (interactive)
  (unless gptel-mode
    (user-error "This command is intended to be used in gptel chat buffers."))
  (let ((gptel-model 'gpt-4o-mini))
    (gptel-request
        (list nil                                    ;user
              "What is the chat content?"            ;llm
              (concat "```" (if (eq major-mode 'org-mode) "org" "markdown") "\n"
                      (buffer-substring-no-properties (point-min) (point-max))
                      "\n```"))                      ;user
      :system
      (list (format                                  ;system message
             "I will provide a transcript of a chat with an LLM.  \
Suggest a short and informative name for a file to store this chat in.  \
Use the following guidelines:
- be very concise, one very short sentence at most
- no spaces, use underscores if required
- return ONLY the title, no explanation or summary
- append the extension .%s"
             (if (eq major-mode 'org-mode) "org" "md")))
      :callback
      (lambda (resp info)                           ;callback called with response and request info
        (if (stringp resp)
            (let ((buf (plist-get info :buffer)))
              (when (and (buffer-live-p buf)
                         (y-or-n-p (format "Rename buffer %s to %s? " (buffer-name buf) resp)))
                (with-current-buffer buf (rename-visited-file resp))))
          (message "Error(%s): did not receive a response from the LLM."
                   (plist-get info :status)))))))

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.