Skip to content

9. Formatting Requests and Responses in Markdown

This lesson discusses how to format requests and responses using Markdown.

Parsing and Returning a JSON Object with json-read

To format responses in Markdown, we first need to parse the JSON received from OpenAI using the json-read function. This function parses and returns the JSON object at the current point in the buffer.

For example, if the point is at the start of a buffer containing the following partial JSON response from OpenAI

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

evaluating (json-read) in the minibuffer (using pp-eval-expression) yields the following output in the *Pp Eval Output* buffer:

((id . "chatcmpl-B5v9nIsqd7sclUBOXJjrsXugrTWs1")
 (model . "gpt-4o-2024-08-06")
 (choices .
          [((index . 0)
            (message
             (role . "assistant")
             (content . "Hi there! How can I assist you today?")
             (refusal))
            (logprobs)
            (finish_reason . "stop"))]))

By adjusting the variable bindings, we can alter the object type returned by json-read. For instance, using the following expression

(let ((json-key-type 'keyword)
      (json-object-type 'plist)
      (json-array-type 'vector))
  (json-read))

produces:

(:id "chatcmpl-B5v9nIsqd7sclUBOXJjrsXugrTWs1"
 :model "gpt-4o-2024-08-06"
 :choices
 [(:index 0
   :message (:role "assistant"
             :content "Hi there! How can I assist you today?"
             :refusal nil)
   :logprobs nil
   :finish_reason "stop")])

We can standardize this parsing with a utility function, chatgpt-json-read:

(defun chatgpt-json-read ()
  (let ((json-key-type 'keyword)
        (json-object-type 'plist)
        (json-array-type 'vector))
    (json-read)))

Accessing Elements in a Nested Structure with map-nested-elt

To retrieve the prompt from the JSON response, we can use the map-nested-elt function as shown below:

(let ((resp '(:id "chatcmpl-B5v9nIsqd7sclUBOXJjrsXugrTWs1"
              :model "gpt-4o-2024-08-06"
              :choices
              [(:index 0
                :message (:role "assistant"
                          :content "Hi there! How can I assist you today?"
                          :refusal nil)
                :logprobs nil
                :finish_reason "stop")])))
  (map-nested-elt resp [:choices 0 :message :content]))
;; "Hi there! How can I assist you today?"

Inserting the Assistant Response instead of the JSON Response

We will modify the chatgpt-send function so that it only appends the content string from the JSON response to the *chatgpt[requests]* buffer, rather than the entire JSON structure.

Previously, the sentinel function would append the JSON response as a raw string:

(lambda (process event)
  (if (not (string= event "finished\n"))
      (message "Error")
    (let ((stdout (with-current-buffer (process-buffer process)
                    (buffer-string))))
      (with-current-buffer (get-buffer-create "*chatgpt[requests]*")
        (goto-char (point-max))
        (insert stdout)))
    (kill-buffer (process-buffer process))))

Now, instead of treating the JSON response as a string, we will parse it using chatgpt-json-read and bind the parsed result to the resp variable:

(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))))
      (with-current-buffer (get-buffer-create "*chatgpt[requests]*")
        (goto-char (point-max))
        (insert (format "%s\n" resp))))
    (kill-buffer (process-buffer process))))

This modification allows us to see the following response in the *chatgpt[requests]* buffer after sending a request to OpenAI:

(:id chatcmpl-B7IV75YO9l840ovOTyUsug9pny371 :object chat.comple
tion :created 1741079113 :model gpt-4o-2024-08-06 :choices [(:i
ndex 0 :message (:role assistant :content Hi there! How can I a
ssist you today? :refusal nil) :logprobs nil :finish_reason sto
p)] :usage (:prompt_tokens 9 :completion_tokens 11 :total_token
s 20 :prompt_tokens_details (:cached_tokens 0 :audio_tokens 0)
:completion_tokens_details (:reasoning_tokens 0 :audio_tokens 0
:accepted_prediction_tokens 0 :rejected_prediction_tokens 0)) :
service_tier default :system_fingerprint fp_eb9dce56a8)

We then extract the assistant's response using map-nested-elt:

(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])))
      (with-current-buffer (get-buffer-create "*chatgpt[requests]*")
        (goto-char (point-max))
        (insert response)))
    (kill-buffer (process-buffer process))))

Now, after sending a request to OpenAI, the *chatgpt[requests]* buffer shows the following:

Hello! How can I assist you today?

Note that in the previous code snippet the let expression has been changed to let* to have resp bound when we use it to bind response variable.

Formatting with markdown-mode

Next, we insert both the prompt and the response in the *chatgpt[requests]* buffer and format it using Markdown.

First, we ensure that we require markdown-mode:

(require 'markdown-mode)

Since markdown-mode is not built-in, install it using your preferred method.

Before appending content to the *chatgpt[requests]* buffer, we activate markdown-mode. Below is the updated sentinel function:

(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])))
      (with-current-buffer (get-buffer-create "*chatgpt[requests]*")
        (markdown-mode)
        (goto-char (point-max))
        (insert "# Request\n\n"
                "## Prompt\n\n" prompt "\n\n"
                "## Response\n\n" response "\n\n")))
    (kill-buffer (process-buffer process))))

After sending a request with the prompt "Hello!", we get the following output appended to the *chatgpt[requests]* buffer in Markdown format:

# Request

## Prompt

Hello!

## Response

Hello! How can I assist you today?

Note that in previous code snippet the prompt variable is bound in an outer let in the chatgpt-send function.

Here is the current definition of the chatgpt-send function:

(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
     (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])))
           (with-current-buffer (get-buffer-create "*chatgpt[requests]*")
             (markdown-mode)
             (goto-char (point-max))
             (insert "# Request\n\n"
                     "## Prompt\n\n" prompt "\n\n"
                     "## Response\n\n" response "\n\n")))
         (kill-buffer (process-buffer process)))))))

Link: https://github.com/jrblevin/markdown-mode