(require 'cl)
(require 'seq)
(require 'dash)
(eval-when-compile
(require 'cl-lib)
(require 'subr-x)
(require 'env)
(require 'json))
(defgroup chatgpt nil
"ChatGPT frontend."
:group 'convenience
:prefix "chatgpt-")
(defcustom chatgpt-max-tokens 300
"Upper limit on the number of tokens the API will return."
:type 'integer)
(defvar chatgpt-buffer "*my/ChatGPT*"
"Title of the buffer used to store the results of an OpenAI API query.")
(define-error 'chatgpt-error "An error related to the ChatGPT emacs package")
(define-error 'chatgpt-parsing-error
"An error caused by a failure to parse an OpenAI API Response")
(defmacro chatgpt-show-results-buffer-if-active ()
"Show the results in other window if necessary."
`(if (and (not ;; visible
(get-buffer-window chatgpt-buffer))
(called-interactively-p 'interactive))
(lambda (&optional buf) (ignore buf)
(with-current-buffer buf
(view-mode t))
(switch-to-buffer-other-window chatgpt-buffer))
#'identity))
;;;###autoload
(defun chatgpt-prompt (prompt callback)
"Query OpenAI with PROMPT calling the CALLBACK function on the resulting buffer.
Returns buffer containing the text from this query"
(interactive (list (read-string "Prompt ChatGPT with: ")
(lambda (buf) (with-current-buffer buf
(view-mode t))
(switch-to-buffer-other-window chatgpt-buffer))))
(chatgpt--query-open-api prompt
(lambda (results)
(with-current-buffer (get-buffer-create chatgpt-buffer)
;; Erase contents of buffer after receiving response
(read-only-mode -1)
(erase-buffer)
(insert results)
;; Return the chatgpt output buffer for non interactive usage
(funcall callback (current-buffer))))))
(defun chatgpt--append-to-prompt (prompt comment-str)
"Append the string COMMENT-STR extra information to a PROMPT as a comment."
(concat prompt
"\n"
comment-start
" "
comment-str))
(defun chatgpt--extract-text-from-query (query-result)
"Extract the resulting text from a given OpenAI response QUERY-RESULT."
(condition-case err
(thread-last query-result
(assoc-default 'choices)
seq-first
(assoc-default 'text)
string-trim)
(error
(signal 'chatgpt-parsing-error err))))
(defun chatgpt--parse-response (status callback)
"Ignoring STATUS and parse the response executing the CALLBACK function on the resulting string."
(ignore status)
;; All this is ran inside the buffer containing the response
(goto-char 0)
(re-search-forward "^$")
(funcall callback (chatgpt--extract-text-from-query (json-read))))
(defun chatgpt--query-open-api (prompt callback)
"Send a string PROMPT to OpenAI API and pass the resulting buffer to CALLBACK.
The environment variable OPENAI_API_KEY is used as your API key
You can register an account here
https://beta.openai.com/docs/introduction/key-concepts"
(let* ((api-key (getenv "OPENAI_API_KEY"))
(url-request-method (encode-coding-string "POST" 'us-ascii))
(url-request-extra-headers `(("Content-Type" . "application/json")
("Authorization" . ,(format "Bearer %s" api-key))))
(url-request-data (json-encode
`(("model" . "text-davinci-003")
("prompt" . ,prompt)
("max_tokens" . ,chatgpt-max-tokens)
("temperature" . 0)))))
(cl-assert (not (string= "" api-key))
t
"Current contents of the environmental variable OPENAI_API_KEY
are '%s' which is not an appropriate OpenAI token please ensure
you have the correctly set the OPENAI_API_KEY variable"
api-key)
(url-retrieve
"https://api.openai.com/v1/completions"
'chatgpt--parse-response
(list callback))))
;; Anak's code
(defvar chatgpt-actions nil)
;; get all keys from plist
(defun get-keys-from-gpt-actions (plist)
;; usage e.g. (get-keys-from-gpt-actions '((:key "value1") (:key "value2") (:key "value3")))
(let ((keys '()))
(while plist
(push (plist-get (car plist) :key) keys)
(setq plist (cdr plist)))
(reverse keys)))
;; (defun get-kvs-from-gpt-actions (plist)
;; ;; usage e.g. (get-keys-from-gpt-actions '((:key "value1") (:key "value2") (:key "value3")))
;; (let ((x '()))
;; (while plist
;; (push '(:key (plist-get (car plist) :key) :value (plist-get (car plist) :value)) x)
;; (setq plist (cdr plist)))
;; (reverse x)))
(defun chatgpt-select-actions (BEG END)
(interactive "r")
(let* ((a (get-keys-from-gpt-actions chatgpt-actions))
(s (completing-read "select an action:" a)))
(cl-do ((d chatgpt-actions (cdr d)))
((if (equal (plist-get (car d) :key) s)
(funcall (plist-get (car d) :value) BEG END))))))
(defun chatgpt-register-action (action)
(let ((-compare-fn (lambda (x y) (equal (plist-get x :key) (plist-get y :key)))))
(setq chatgpt-actions
(-distinct (add-to-list 'chatgpt-actions action)))))
(chatgpt-register-action
'(:key "chatgpt-prompt-region-and-replace"
:value (lambda (b e) (lexical-let ((og-buf (current-buffer))
(BEG b)
(END e))
(chatgpt-prompt (buffer-substring BEG END)
(lambda (buf)
(save-excursion
(with-current-buffer og-buf
(delete-region BEG END)
(goto-char BEG)
(insert (with-current-buffer buf (buffer-string)))))))))))
code above is relatively simple. The confusing part is (chatgpt-register-action ...)
.
To reproduce error
- highlight any section of text
- M-x chatgpt-select-actions
- select "chatgpt-prompt-region-and-replace" from popup window.
- you will get the following error.
error in process filter: save-current-buffer: Symbol’s value as variable is void: og-buf
error in process filter: Symbol’s value as variable is void: og-buf
I know it is dynamic-scoping problem because I can fix the issue by modify (chatgpt-register-action ...)
as followed.
(chatgpt-register-action
'(:key "chatgpt-prompt-region-and-replace"
:value (lambda (b e) (lexical-let ((og-buf (current-buffer))
(BEG b)
(END e))
(chatgpt-prompt (buffer-substring BEG END)
(lambda (buf)
(save-excursion
(with-current-buffer og-buf
(delete-region BEG END)
(goto-char BEG)
(insert (with-current-buffer buf (buffer-string)))))))))))
it becomes clear to me after using edebug that (chatprompt ...)
section in chatgpt-register-action
is execute without considering (let ((og-buf (current-buffer))) ... )
. This can be seen by evaluate (print callback)
while inside (defun chatgpt-prompt ...)
function which show the following code.
(lambda (buf) (save-excursion (with-current-buffer og-buf (delete-region BEG END) (goto-char BEG) (insert (with-current-buffer buf (buffer-string))))))
notice that lambda is closure (My assumption from reading Elisp doc.), so I expect (print callback)
to output something like
(closure ((<value of BEG>)
(<value of END>)
(<value of og-buf))
(lambda (buf) (save-excursion (with-current-buffer og-buf (delete-region BEG END) (goto-char BEG) (insert (with-current-buffer buf (buffer-string))))))
)
My question is
1.why og-buf
value is nil? when running chatgpt-select-actions
follows by selecting "chatgpt-prompt-region-and-replace."
2. why stacktrace doesn't include (let ((og-buf (current-buffer))) ...)
I am aware that order of execution may be the cause of my confusion, but from what I understand, (let ((og-buf (current-buffer))) ...)
should be executed before (chatgpt-prompt ...)
which mean og-buf should appear in stack frame lower, so og-bug
should have (current-buffer)
value and not nil.