11. The Prompt Buffer
In this lesson, we will implement the chatgpt command to display and select the prompt buffer at the bottom of the frame. We will also define chatgpt-mode and apply it within the prompt buffer.
Displaying the Prompt Buffer with chatgpt¶
Previously, we utilized the buffer *chatgpt* to enter prompts for OpenAI. We will continue using this buffer but will replace the switch-to-buffer command with our new chatgpt command, which displays the buffer at the bottom of the frame.
(defun chatgpt ()
"Display and select the prompt buffer."
(interactive)
(let ((buff (get-buffer-create "*chatgpt*")))
(select-window
(display-buffer-at-bottom
buff '(display-buffer-below-selected (window-height . 6))))))
The display-buffer-at-bottom function opens the specified buffer at the bottom of the frame and returns the associated window. We then use select-window to select that window.
Defining chatgpt-mode¶
We derive chatgpt-mode from markdown-mode for the prompt buffer:
To activate chatgpt-mode in the prompt buffer, we invoke it within the chatgpt command after selecting the window:
(defun chatgpt ()
"Display and select the prompt buffer."
(interactive)
(let ((buff (get-buffer-create "*chatgpt*")))
(select-window
(display-buffer-at-bottom
buff '(display-buffer-below-selected (window-height . 6))))
(chatgpt-mode)))
Upon calling chatgpt, the *chatgpt* buffer will appear at the bottom of the frame. By evaluating major-mode in the minibuffer (using the eval-expression command) we can verify that the active major mode is chatgpt-mode.
Introducing chatgpt-model Variable¶
Next, we introduce the chatgpt-model variable to use in the chatgpt-request function, replacing the previous hardcoded gpt-4o model string:
(defvar chatgpt-model "gpt-4o"
"The OpenAI model to use.")
(defun chatgpt-request (prompt)
"Return an OpenAI request with PROMPT."
`(:model ,chatgpt-model
:messages ,(vector `(:role "user" :content ,prompt)))
For example:
(let ((chatgpt-model "gpt-4o"))
(chatgpt-request "foo"))
;; (:model "gpt-4o" :messages [(:role "user" :content "foo")])
(let ((chatgpt-model "gpt-4o-mini"))
(chatgpt-request "foo"))
;; (:model "gpt-4o-mini" :messages [(:role "user" :content "foo")])
Mode Line of the Prompt Buffer¶
We enhance the mode line in the prompt buffer to display the currently selected chatgpt-model. To do so, we modify chatgpt-mode to set mode-line-format accordingly:
(define-derived-mode chatgpt-mode markdown-mode "ChatGPT"
"Mode for interacting with ChatGPT."
(setq mode-line-format
'(" "
mode-line-buffer-identification
" "
chatgpt-model
" "
mode-line-misc-info)))
Once chatgpt-mode is activated in the prompt buffer, the mode line will display:
Note that the mode-line-misc-info variable allows other commands or minor modes to append additional information to the mode line via global-mode-string.
Executing chatgpt-mode Once¶
Let's enhance the existing chatgpt command to ensure that chatgpt-mode is invoked only when the *chatgpt* buffer is created for the first time. Currently, chatgpt-mode is executed every time we call the chatgpt function, which is unnecessary.
We modify the function to check for the buffer's existence using get-buffer. If the *chatgpt* buffer does not exist at the time chatgpt is called, we enable chatgpt-mode.
Here's the revised code:
(defun chatgpt ()
"Display and Select the prompt buffer."
(interactive)
(let* ((buff-name "*chatgpt*")
(buff-p (get-buffer buff-name))
(buff (get-buffer-create buff-name)))
(select-window
(display-buffer-at-bottom
buff '(display-buffer-below-selected (window-height . 6))))
(when (not buff-p) (chatgpt-mode))))
This modification ensures that chatgpt-mode is only activated when creating the buffer for the first time.
Creating chatgpt-dir in chatgpt-mode¶
We can further improve our implementation by creating the chatgpt-dir in the chatgpt-mode definition instead of in the chatgpt-send function. This ensures the directory is created only once if it doesn't exist yet. So we remove the call to make-directory in chatgpt-send and add it to chatgpt-mode:
(define-derived-mode chatgpt-mode markdown-mode "ChatGPT"
"ChatGPT mode."
(setq mode-line-format
'(" "
mode-line-buffer-identification
" "
chatgpt-model
" "
mode-line-misc-info))
(make-directory chatgpt-dir t))
Defining chatgpt-mode-map keymap¶
Lastly, we bind the C-c C-c key combination to the chatgpt-send command within the chatgpt-mode-map keymap:
(defvar chatgpt-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") 'chatgpt-send)
map)
"Keymap for `chatgpt-mode'.")
To apply these changes in our current Emacs session, we call the chatgpt command to access the prompt buffer and activate chatgpt-mode.
chatgpt.el¶
The current implementation of the chatgpt.el package is as follows: \begingroup \hyphenpenalty=10000 \exhyphenpenalty=10000
;;; chatgpt.el --- Simple ChatGPT integration -*- lexical-binding: t; -*-
(require 'json)
(require 'markdown-mode)
(defvar chatgpt-api-key
"sk-proj-7pQDxN...w-D40A"
"OpenAI API key.")
(defvar chatgpt-dir "/home/tony/chatgpt-emacs/requests/"
"Request directory.")
(defun chatgpt-json-read ()
(let ((json-key-type 'keyword)
(json-object-type 'plist)
(json-array-type 'vector))
(json-read)))
(defun chatgpt-json-encode (object)
(let ((json-encoding-pretty-print t))
(json-encode object)))
(defun chatgpt-command (req-path)
"Return the curl command."
(format
(concat "curl https://api.openai.com/v1/chat/completions "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer %s' "
"-d @%s")
chatgpt-api-key req-path))
(defvar chatgpt-model "gpt-4o"
"OpenAI model.")
(defun chatgpt-request (prompt)
"Return an OpenAI request with PROMPT."
`(:model ,chatgpt-model
:messages ,(vector `(:role "user" :content ,prompt))))
(defun chatgpt-callback (prompt response req-dir)
"Append PROMPT and RESPONSE to the prompt buffer with a link to REQ-DIR."
(let ((buff (get-buffer-create "*chatgpt[requests]*")))
(with-current-buffer buff
(markdown-mode)
(goto-char (point-max))
(insert "# Request\n\n"
"<!-- [](" req-dir ") -->\n\n"
"## Prompt\n\n" prompt "\n\n"
"## Response\n\n" response "\n\n"))))
(defun chatgpt-send ()
"Send a request to OpenAI."
(interactive)
(make-directory chatgpt-dir t)
(let* ((prompt (buffer-string))
(req (chatgpt-request prompt))
(temporary-file-directory chatgpt-dir)
(req-dir (file-name-as-directory (make-temp-file nil t)))
(req-path (concat req-dir "request.json"))
(command (chatgpt-command req-path)))
(message "chatgpt: %s" req-dir)
(write-region (chatgpt-json-encode req) nil req-path)
(make-process
:name "chatgpt"
:buffer (generate-new-buffer-name "chatgpt")
:command (list "sh" "-c" command)
:sentinel
(lambda (process event)
(if (not (string= event "finished\n"))
(message "Error")
(let* ((resp (with-current-buffer (process-buffer process)
(goto-char (point-min))
(chatgpt-json-read)))
(response (map-nested-elt resp [:choices 0 :message :content]))
(resp-path (concat req-dir "response.json")))
(write-region (chatgpt-json-encode resp) nil resp-path)
(chatgpt-callback prompt response))
(kill-buffer (process-buffer process)))))))
(defvar chatgpt-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") 'chatgpt-send)
map)
"Keymap of `chatgpt-mode'.")
(define-derived-mode chatgpt-mode markdown-mode "ChatGPT"
"ChatGPT mode."
(setq mode-line-format
'(" "
mode-line-buffer-identification
" "
chatgpt-model
" "
mode-line-misc-info))
(make-directory chatgpt-dir t))
(defun chatgpt ()
"Display and Select the prompt buffer."
(interactive)
(let* ((buff-name "*chatgpt*")
(buff-p (get-buffer buff-name))
(buff (get-buffer-create buff-name)))
(select-window
(display-buffer-at-bottom
buff '(display-buffer-below-selected (window-height . 6))))
(when (not buff-p) (chatgpt-mode))))
(provide 'chatgpt)