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
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:
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:
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:
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)))))))