0
(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

  1. highlight any section of text
  2. M-x chatgpt-select-actions
  3. select "chatgpt-prompt-region-and-replace" from popup window.
  4. 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.

  • AFAIK, you should not use the `cl` package anymore, just use the `cl-lib` one. Also, the default is to set/use lexical bidning via the file-variable (see `M-x elisp-enable-lexical-binding`). Finally, I can not evaluate your code because some line starts with `rom plist...`, please correct that error. – dalanicolai Apr 10 '23 at 06:53
  • @dalanicolai: May I ask why "*should not use the `cl` package*"? – shynur Apr 10 '23 at 07:56
  • 1
    @Shynur It's deprecated in favour of `cl-lib` (the latter having been created specifically for the purpose of name-spacing all of the CL functionality). – phils Apr 10 '23 at 08:00
  • 2
    You have an absolute wall of code there. Please try to reduce your question to a minimal use-case, as you're currently asking people to do a lot of work to understand your question. At a glance I note you have a *quoted* lambda: `'(..... (lambda...) ...)` so maybe that's your problem. A quoted form is data, so while a quoted lambda form can be called as a function, it is not a closure. – phils Apr 10 '23 at 08:05
  • Your subject "dynamic scoping doesn't perform as expected" is also confusing, as my impression is that you were *expecting* lexical scoping. – phils Apr 10 '23 at 08:08
  • 1
    @shynur regarding `cl`/`cl-lib` see [the answer here](https://emacs.stackexchange.com/a/48115/26163) – dalanicolai Apr 10 '23 at 10:31
  • @phils No i am not expecting lexical scoping. To better word it is that I think either dynamic or lexical scoping should behave the same way given my code. I obviously wrong, so I want to know why I was wrong. – A-nak Wannapaschaiyong Apr 10 '23 at 15:13
  • @dalanicolai I fix the code. – A-nak Wannapaschaiyong Apr 10 '23 at 15:21
  • @phils I tried to simplify the code so it can replicate code behavior, but I couldn't, so there must be something I overlook, but some function that behave implicitly. – A-nak Wannapaschaiyong Apr 10 '23 at 15:23
  • 1
    I cannot tell what your question is. You say you're expecting dynamic scope but at the same time talk about expecting there to be *closures*. You talk about `(let ((og-buf (current-buffer))) ...)` which doesn't appear in the code at all. – phils Apr 10 '23 at 22:39
  • Given `(defvar og-buf) (defun foo () og-buf)` then `(let ((og-buf (current-buffer))) (foo))` will return a buffer. If you're using lexical-binding (which per earlier comments I cannot tell) then that `defvar` is critical, as the variable will not have dynamic scope otherwise. – phils Apr 11 '23 at 01:41

0 Answers0