Skip to content

8. Making the Prompt Dynamic in Requests

In this lesson, we will enhance the chatgpt-send function to facilitate sending requests with prompts entered in a buffer.

Writing a JSON Object to a File

In the following sections, we will learn how to create a request.json file containing the JSON request sent to OpenAI.

First, we ensure to require the built-in json package:

(require 'json)

Next, we use the json-encode function to encode an Emacs Lisp object into a JSON string:

(json-encode '(:foo "bar")) ;; "{\"foo\":\"bar\"}"

We Utilize the write-region function to write the JSON string to a file:

(write-region (json-encode '(:foo "bar"))
              nil "/home/tony/chatgpt-emacs/request-1.json")

This will create the file /home/tony/chatgpt-emacs/request-1.json containing:

{"foo":"bar"}

Writing the OpenAI Request to a File

We generate the following JSON request

{
  "model": "gpt-4o",
  "messages": [
    {
      "role": "user",
      "content": "Hello!"
    }
  ]
}

from Emacs Lisp. We bind it to a variable req, and write it to disk using json-encode and write-region:

(let ((req `(:model "gpt-4o"
             :messages ,(vector `(:role "user" :content "Hello!")))))
  (write-region (json-encode req)
                nil "/home/tony/chatgpt-emacs/request-1.json"))

Evaluating this expression updates the file /home/tony/chatgpt-emacs/request-1.json with:

{"model":"gpt-4o","messages":[{"role":"user","content":"Hello!"}]}

To pretty-print the JSON request, we set json-encoding-pretty-print to t:

(let ((json-encoding-pretty-print t)
      (req `(:model "gpt-4o"
             :messages ,(vector `(:role "user" :content "Hello!")))))
  (write-region (json-encode req)
                nil "/home/tony/chatgpt-emacs/request-1.json"))

Evaluating this expression yields the contents:

{
  "model": "gpt-4o",
  "messages": [
    {
      "role": "user",
      "content": "Hello!"
    }
  ]
}

We can further enhance our code's flexibility by allowing prompts to change. Let's set prompt to "foo bar baz":

(let* ((json-encoding-pretty-print t)
       (prompt "foo bar baz")
       (req `(:model "gpt-4o"
              :messages ,(vector `(:role "user" :content ,prompt)))))
  (write-region (json-encode req)
                nil "/home/tony/chatgpt-emacs/request-1.json"))

This writes the following to /home/tony/chatgpt-emacs/request-1.json:

{
  "model": "gpt-4o",
  "messages": [
    {
      "role": "user",
      "content": "foo bar baz"
    }
  ]
}

Lastly, we introduce req-path to replace the hard-coded file path:

(let* ((json-encoding-pretty-print t)
       (prompt "Hello!")
       (req `(:model "gpt-4o"
              :messages ,(vector `(:role "user" :content ,prompt))))
       (req-path "/home/tony/chatgpt-emacs/request-1.json"))
  (write-region (json-encode req) nil req-path))

Evaluating this will again result in the file being updated to:

{
  "model": "gpt-4o",
  "messages": [
    {
      "role": "user",
      "content": "Hello!"
    }
  ]
}

Updating chatgpt-send

Now we can integrate the above functionality into chatgpt-send:

(defun chatgpt-send ()
  "Send a request to OpenAI."
  (interactive)
  (let* ((json-encoding-pretty-print t)
         (prompt "Hello!")
         (req `(:model "gpt-4o"
                :messages ,(vector `(:role "user" :content ,prompt))))
         (req-path "/home/tony/chatgpt-emacs/request.json")
         (command (chatgpt-command req-path)))
    (write-region (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) ...))))

After redefining chatgpt-send, calling it sends the JSON request in /home/tony/chatgpt-emacs/request.json to OpenAI, resulting in the following response in the *chatgpt[requests]* buffer:

{
  "id": "chatcmpl-B5v9nIsqd7sclUBOXJjrsXugrTWs1",
  "object": "chat.completion",
  "created": 1740751051,
  "model": "gpt-4o-2024-08-06",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hi there! How can I assist you today?",
        "refusal": null
      },
      ...
    }
  ],
  ...
}

Entering the Prompt from a Buffer

Finally, we modify chatgpt-send to use the current buffer content as the prompt. We replace the hardcoded prompt "Hello!" with a call to buffer-string:

(defun chatgpt-send ()
  "Send a request to OpenAI."
  (interactive)
  (let* ((json-encoding-pretty-print t)
         (prompt (buffer-string))
         (req `(:model "gpt-4o"
                :messages ,(vector `(:role "user" :content ,prompt))))
         (req-path "/home/tony/chatgpt-emacs/request.json")
         (command (chatgpt-command req-path)))
    (write-region (json-encode req) nil req-path)
    (make-process
     :name "chatgpt"
     :buffer (generate-new-buffer-name "chatgpt")
     :command (list "sh" "-c" command)
     :sentinel ...)))

Now, in a new buffer named *chatgpt*, if we enter the prompt I'm hungry and call chatgpt-send, we received the following response from OpenAI:

{
  "id": "chatcmpl-B5vaCM7XPe9sCdfeE0iNDjU0RiMuf",
  "object": "chat.completion",
  "created": 1740752688,
  "model": "gpt-4o-2024-08-06",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "What are you in the mood for? I can suggest
recipes,  snacks, or nearby restaurants if you let me know your
preferences!",
        "refusal": null
      },
      ...
    }
  ],
  ...
}

By following these steps, we have successfully made the prompt dynamic, enhancing the functionality of chatgpt-send.