6. First Request To OpenAI From Emacs Lisp
Until now, we have explored the use of the OpenAI Chat Completion API with curl and how to execute command line operations in Emacs Lisp using the make-process function. Leveraging this knowledge, we can now create our first Emacs command chatgtp-send which sends the prompt "Hello!" to OpenAI and appends the JSON response to a buffer.
Review of The Last Lesson¶
Specifically, in the previous lesson, we implemented an expression that invokes the make-process function. This function starts an asynchronous process to execute a command that outputs foo to the standard output and completes after 1 second.
We also defined the associated sentinel function such that if the process completes successfully, its standard output is appended to the buffer bar. Conversely, if the process is terminated prematurely, such as being killed, the message Error will be displayed in the echo area.
(make-process
:name "foo-name"
:buffer "foo-buff"
:command (list "sh" "-c" "sleep 1 | echo foo")
:sentinel
(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 "bar")
(insert stdout))))))
Renaming The Process Name and Process Buffers¶
To rename the process from foo-name to chatgpt, the name of our package, we use the following code:
Note that if a process named chatgpt already exists, the make-process function will generate a unique identifier for the new process to avoid naming collisions.
Currently, we are using the same process buffer foo-buff for all requests. This single buffer approach poses challenges for sending concurrent requests and managing JSON responses from OpenAI. To address this, we will generate unique buffer names for each request using generate-new-buffer-name:
Next, instead of redirecting OpenAI JSON responses to the buffer bar (currently, responses are the output foo of the command sleep 1 | echo foo) we will direct them to the buffer *chatgpt[requests]*. Additionally, rather than inserting the response at the current point, we will append it by moving the point to the end of the buffer using (goto-char (point-max)) before any insertion:
(make-process
:name "chatgpt"
:buffer (generate-new-buffer-name "chatgpt")
:command (list "sh" "-c" "sleep 1 | echo foo")
:sentinel
(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))))))
This implementation allows us to effectively manage multiple requests and handle the responses in an orderly manner.
Killing The Process Buffers¶
In our implementation, we utilize a unique process buffer for each invocation of make-process, which can lead to the accumulation of numerous inactive buffers if not properly managed. To prevent this, we ensure that once processing is complete, we kill these buffers using the kill-buffer function with the expression (kill-buffer (process-buffer process)).
(make-process
:name "chatgpt"
:buffer (generate-new-buffer-name "chatgpt")
:command (list "sh" "-c" "sleep 1 | echo foo")
:sentinel
(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)))))
Sending our First OpenAI Request from Emacs Lisp¶
In this section, we'll send our first request to the OpenAI API using Emacs Lisp. To do so, we modify the previous make-process calls by replacing the original command sleep 1 | echo foo with the curl command we wrote in the lesson 2 that sends the JSON request found in the file /home/tony/chatgpt-emacs/request.json:
Here's the updated Emacs Lisp code:
(let ((command (concat "curl https://api.openai.com/v1/chat/completions "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer sk-proj-7pQDxN...w-D40A "
"-d @/home/tony/chatgpt-emacs/request.json")))
(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 ((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))))))
Evaluating the previous expression appends the following JSON response from OpenAI in *chatgpt[requests]* buffer:
{
"id": "chatcmpl-B59xiKIxwA1xwxTytSsrcTXBO7CkH",
"object": "chat.completion",
"created": 1740569634,
"model": "gpt-4o-2024-08-06",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I assist you today?",
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 10,
"total_tokens": 19,
"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"
}
Defining chatgpt-send Command in chatgpt.el File¶
Finally, we write the first command for our chatgpt package in chatgpt.el file, naming it chatgpt-send. This command sends a request to OpenAI with the prompt "Hello!" fetched from the file /home/tony/chatgpt-emacs/request.json.
;;; chatgpt.el --- Simple ChatGPT integration -*- lexical-binding: t; -*-
(defun chatgpt-send ()
"Send the request \"Hello!\" to OpenAI."
(interactive)
(let ((command (concat "curl https://api.openai.com/v1/chat/completions "
"-H 'Content-Type: application/json' "
"-H 'Authorization: Bearer sk-proj-7pQDxN...w-D40A "
"-d @/home/tony/chatgpt-emacs/request.json")))
(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 ((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)))))))
(provide 'chatgpt)
Next, we will refactor the curl command into a separate function for better organization.