index rss mastodon twitter github linkedin email
Álvaro Ramírez
sponsor

Álvaro Ramírez

12 July 2023 ChatGPT visits the Emacs doctor

Emacs is a part-time job. A multi-language development environment. A lisp machine. An email client. A web browser. A zettelkasten. A spreadsheet. A mastodon client. A shell. A ledger. A super agenda. An operating system. Some say it sends ripples into the atmosphere or plays tetris for you. It may even warm your place up during the winter. Can meme with you. It's an ultra-malleable editor with endless possibilities, powered by your life-long customizations. Oh man, no wonder we need to chat to someone from time to time. You know what I mean? "Sir, this is a Wendy's".

Luckily, we also have the built-in Emacs psychotherapist we can chat to, courtesy of M-x doctor. It's powered by elisp, and like all Emacs things, it's basically up for grabs. What I mean is, elisp implements many of these features, but also glues the lot for you. Once you learn a little elisp, you can build new Emacs features but also glue others for that magical compound effect.

got-a-problem.gif

Figure 1: The Emacs doctor

A little while ago, I wanted to give ChatGPT a try, preferably from Emacs (of course). I figured a shell interface would be a great fit for the interaction. Emacs already shipped with a general command interpreter (comint), so I cobbled together a ChatGPT Emacs shell.

cyberpunk.gif

Figure 2: chatgpt-shell

So where am I going with all this? The fine netizens r/emaphis and salgernon both planted a great seed:

I haven't forgotten about you. Let's take chatgpt-shell, M-x doctor, our versatile elisp glue, and let's make them talk:

000026814.jpg

Figure 3: courtesy of thriveth and dr.dk.

There isn't too much to the code, but beware:

  1. If you want to run it, you'll need chatgpt-shell installed and set up.
  2. This was a quick fun hack. No code judging ;)

The snippet is further down… Start with chatgpt-shell-visit-doctor as the entry point, setting things up for us. It creates both the *chatgpt* and *doctor* buffers and arranges the windows next to each other.

We also set a ChatGPT system prompt to guide things a little:

"Pretend to be an overwhelmed Emacs user who is obsessed with configuring their init.el file. You are in a session talking to a psychotherapist. Limit your output to no more than 20 words. In the course of 5 exchanges between you and the therapist, show improvements. On the 8th exchange after therapist speaks, declare you are cured and only output 'Thank you doc, I think I'm cured!'"

ChatGPT and Emacs doctor can go on and on, so we limit ChatGPT responses to 20 words per response and 8 exchanges. We don't want the session to abruptly end without a resolution, so we'll use Thank you doc, I think I'm cured! as our key phrase to end the session.

We register chatgpt-shell--on-chatgpt-patient-response as a hook to receive ChatGPT output, which we feed to the *doctor* buffer. We subsequently get a doctor response that's fed back to ChatGPT via chatgpt-shell--insert-doc-response.

We add some additional freebies like binding Ctrl-c Ctrl-c to chatgpt-shell-leave-doctor, so we can bail out of the exchange from the *chatgpt* buffer.

We also introduced chatgpt-shell--insert-delayed-text as a replacement for insert to slow things down a little. For visual effects, really.

(require 'chatgpt-shell)

(defun chatgpt-shell-visit-doctor ()
  (interactive)
  (setq chatgpt-shell--doctor-in-session t)
  (when (get-buffer "*doctor*")
    (kill-buffer "*doctor*"))
  (delete-other-windows)
  (split-window-horizontally)
  (other-window 1)
  (doctor)
  (visual-line-mode 1)
  (when (fboundp 'accent-menu-mode)
    (accent-menu-mode -1))
  (mapc
   (lambda (shell-buffer)
     (kill-buffer shell-buffer))
   (chatgpt-shell--shell-buffers))
  (other-window 1)
  (setq chatgpt-shell-system-prompts
        '(("Doc" . "Pretend to be an overwhelmed Emacs user who is obsessed with configuring their init.el file. You are in a session talking to a psychotherapist. Limit your output to no more than 20 words. In the course of 5 exchanges between you and the therapist, show improvements. On the 8th exchange after therapist speaks, declare you are cured and only output \"Thank you doc, I think I'm cured!\".")))
  (setq chatgpt-shell-system-prompts nil)
  (setq chatgpt-shell-system-prompt nil)
  (with-current-buffer (chatgpt-shell)
    (define-key chatgpt-shell-mode-map (kbd "C-c C-c")
      'chatgpt-shell-leave-doctor)
    (shell-maker-set-buffer-name (current-buffer)
                                 "*chatgpt*"))
  (chatgpt-shell--insert-doc-response))

(defun chatgpt-shell--doc-conversation ()
  (let ((convo (with-current-buffer "*doctor*"
                 (split-string (buffer-string) "\n\n"))))
    (seq-remove
     (lambda (item)
       (string-empty-p (string-trim item)))
     (append
      ;; Replace first doc line, so it drops "Each time you are finished talking, type RET twice."
      (list "I am the psychotherapist.  Please, describe your problems.")
      (mapcar
       (lambda (item)
         (replace-regexp-in-string "\n" " " item))
       (cdr convo))))))

(defun chatgpt-shell--doc-response ()
  (let* ((conversation (chatgpt-shell--doc-conversation))
         (length (seq-length conversation))
         (doc-response (nth (1- length) conversation)))
    doc-response))

(defun chatgpt-shell--insert-doc-response ()
  (with-current-buffer "*chatgpt*"
    (goto-char (point-max))
    (chatgpt-shell--insert-delayed-text (chatgpt-shell--doc-response))
    (call-interactively 'shell-maker-submit)))

(defun chatgpt-shell--insert-delayed-text (text)
  "Insert TEXT into the current buffer, with a delay between each character."
  (dolist (char (string-to-list text))
    (insert char)
    (sit-for 0.009)))

(defun chatgpt-shell--on-chatgpt-patient-response (command output)
  (if (and chatgpt-shell--doctor-in-session
           (not (string-match-p (regexp-quote "I'm cured") output)))
      (progn
        (with-current-buffer "*chatgpt*"
          (goto-char (point-max)))
        (with-selected-window (get-buffer-window "*doctor*")
          (goto-char (point-max))
          (chatgpt-shell--insert-delayed-text output)
          (call-interactively 'doctor-ret-or-read)
          (chatgpt-shell--insert-doc-response)))
    (message "Bye Bye")))

(defun chatgpt-shell-leave-doctor ()
  (interactive)
  (setq chatgpt-shell--doctor-in-session nil)
  (call-interactively 'chatgpt-shell-ctrl-c-ctrl-c))

(add-hook 'chatgpt-shell-after-command-functions
          #'chatgpt-shell--on-chatgpt-patient-response)

(global-set-key (kbd "M-q") #'chatgpt-shell-visit-doctor)

Ok, enough already. Let's see it in action.

chatgpt-doc_optimized.gif

If you'd like to take a closer look for analysis, here's the official transcript of this fruitful session:

I am the psychotherapist. Please, describe your problems. Each time you are finished talking, type RET twice.

I spend hours tweaking my Emacs config, but it's never perfect. I'm overwhelmed and obsessed with customization.

Is it because it's not ever perfect you are overwhelmed and obsessed with customization that you came to me?

Yes, I can't stop myself from constantly trying to tweak and improve my Emacs configuration. It's consuming me.

Emacs? Hah! I would appreciate it if you would continue.

Sorry about that. Yes, I came to you because I'm overwhelmed by my obsession with customizing my Emacs configuration.

Does it bother you that you are overwhelmed by your obsession with customizing your emacs configuration?

Absolutely! I feel trapped in an endless cycle of tweaking and configuring, unable to achieve satisfaction.

Why do you say that?

I constantly seek perfection in my Emacs config, but it's an unattainable goal that leaves me exhausted and frustrated.

When did you first know that it's an unattainable goal that leaves you exhausted and frustrated?

I've realized it recently after spending countless hours trying to perfect my Emacs configuration without any lasting satisfaction.

Are you sure?

Thanks you doc, I think I'm cured.

While we can work exclusively with the *doctor* buffer making requests directly to the ChatGPT API, where's the fun in that? Getting the buffers to talk to each other enables us to marvel at both the beauty and absurdity of being able to glue anything together in our lovely Emacs world.

Happy Emacsing!