gptel: Simplify response API

* gptel.el (gptel--url-parse-response, gptel--url-get-response,
gptel--insert-response, gptel-send):
- Use shorter keys for passing the info plist,
- record errors in the info plist,
- separate user messaging from the callback and more.
- Make the API more functional (i.e. less imperative)

This is in preparation for adding `gptel-request', an API for
defining custom commands.

Note: The streaming filter and callback are mostly unchanged.
Streaming is not planned to be accessible via `gptel-request'.

* gptel-curl.el (gptel-curl--parse-response, gptel-curl--sentinel,
gptel-curl--stream-filter, gptel-curl--stream-insert-response,
gptel-curl--stream-cleanup, gptel-curl-get-response): Ditto.
This commit is contained in:
Karthik Chikmagalur 2023-04-08 16:57:38 -07:00
parent 36051b15d5
commit f0953d569e
2 changed files with 62 additions and 66 deletions

View file

@ -67,8 +67,8 @@ PROMPTS is the data to send, TOKEN is a unique identifier."
INFO is a plist with the following keys: INFO is a plist with the following keys:
- :prompt (the prompt being sent) - :prompt (the prompt being sent)
- :gptel-buffer (the gptel buffer) - :buffer (the gptel buffer)
- :start-marker (marker at which to insert the response). - :position (marker at which to insert the response).
Call CALLBACK with the response and INFO afterwards. If omitted Call CALLBACK with the response and INFO afterwards. If omitted
the response is inserted into the current buffer after point." the response is inserted into the current buffer after point."
@ -89,7 +89,7 @@ the response is inserted into the current buffer after point."
:transformer (when (or (eq gptel-default-mode 'org-mode) :transformer (when (or (eq gptel-default-mode 'org-mode)
(eq (buffer-local-value (eq (buffer-local-value
'major-mode 'major-mode
(plist-get info :gptel-buffer)) (plist-get info :buffer))
'org-mode)) 'org-mode))
(gptel--stream-convert-markdown->org))) (gptel--stream-convert-markdown->org)))
info)) info))
@ -98,6 +98,7 @@ the response is inserted into the current buffer after point."
(set-process-filter process #'gptel-curl--stream-filter)) (set-process-filter process #'gptel-curl--stream-filter))
(set-process-sentinel process #'gptel-curl--sentinel))))) (set-process-sentinel process #'gptel-curl--sentinel)))))
;; TODO: Separate user-messaging from this function
(defun gptel-curl--stream-cleanup (process status) (defun gptel-curl--stream-cleanup (process status)
"Process sentinel for GPTel curl requests. "Process sentinel for GPTel curl requests.
@ -107,11 +108,11 @@ PROCESS and STATUS are process parameters."
(with-current-buffer proc-buf (with-current-buffer proc-buf
(clone-buffer "*gptel-error*" 'show))) (clone-buffer "*gptel-error*" 'show)))
(let* ((info (alist-get process gptel-curl--process-alist)) (let* ((info (alist-get process gptel-curl--process-alist))
(gptel-buffer (plist-get info :gptel-buffer)) (gptel-buffer (plist-get info :buffer))
(tracking-marker (plist-get info :tracking-marker)) (tracking-marker (plist-get info :tracking-marker))
(start-marker (plist-get info :start-marker)) (start-marker (plist-get info :position))
(http-status (plist-get info :http-status)) (http-status (plist-get info :http-status))
(http-msg (plist-get info :http-msg))) (http-msg (plist-get info :status)))
(if (equal http-status "200") (if (equal http-status "200")
;; Finish handling response ;; Finish handling response
(with-current-buffer gptel-buffer (with-current-buffer gptel-buffer
@ -153,13 +154,12 @@ PROCESS and STATUS are process parameters."
INFO is a mutable plist containing information relevant to this buffer. INFO is a mutable plist containing information relevant to this buffer.
See `gptel--url-get-response' for details." See `gptel--url-get-response' for details."
(let ((content-str (plist-get response :content)) (let ((status-str (plist-get response :status))
(status-str (plist-get response :status)) (gptel-buffer (plist-get info :buffer))
(gptel-buffer (plist-get info :gptel-buffer)) (start-marker (plist-get info :position))
(start-marker (plist-get info :start-marker))
(tracking-marker (plist-get info :tracking-marker)) (tracking-marker (plist-get info :tracking-marker))
(transformer (plist-get info :transformer))) (transformer (plist-get info :transformer)))
(if content-str (when response
(with-current-buffer gptel-buffer (with-current-buffer gptel-buffer
(save-excursion (save-excursion
(unless tracking-marker (unless tracking-marker
@ -171,13 +171,11 @@ See `gptel--url-get-response' for details."
(plist-put info :tracking-marker tracking-marker)) (plist-put info :tracking-marker tracking-marker))
(when transformer (when transformer
(setq content-str (funcall transformer content-str))) (setq response (funcall transformer response)))
(put-text-property 0 (length content-str) 'gptel 'response content-str) (put-text-property 0 (length response) 'gptel 'response response)
(goto-char tracking-marker) (goto-char tracking-marker)
(insert content-str))) (insert response))))))
(gptel--update-header-line
(format " Response Error: %s" status-str) 'error))))
(defun gptel-curl--stream-filter (process output) (defun gptel-curl--stream-filter (process output)
(let* ((content-strs) (let* ((content-strs)
@ -201,9 +199,9 @@ See `gptel--url-get-response' for details."
(and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg) (and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg)
(match-string 1 http-msg))))) (match-string 1 http-msg)))))
(plist-put proc-info :http-status http-status) (plist-put proc-info :http-status http-status)
(plist-put proc-info :http-msg (string-trim http-msg))))) (plist-put proc-info :status (string-trim http-msg)))))
(when-let ((http-msg (plist-get proc-info :http-msg)) (when-let ((http-msg (plist-get proc-info :status))
(http-status (plist-get proc-info :http-status))) (http-status (plist-get proc-info :http-status)))
;; Find data chunk(s) and run callback ;; Find data chunk(s) and run callback
(when (equal http-status "200") (when (equal http-status "200")
@ -222,7 +220,7 @@ See `gptel--url-get-response' for details."
(push content content-strs))))) (push content content-strs)))))
(error (error
(goto-char (match-beginning 0)))) (goto-char (match-beginning 0))))
(list :content (apply #'concat (nreverse content-strs)) :status http-msg)) (apply #'concat (nreverse content-strs)))
proc-info)))))) proc-info))))))
(defun gptel-curl--sentinel (process status) (defun gptel-curl--sentinel (process status)
@ -233,14 +231,15 @@ PROCESS and STATUS are process parameters."
(when gptel--debug (when gptel--debug
(with-current-buffer proc-buf (with-current-buffer proc-buf
(clone-buffer "*gptel-error*" 'show))) (clone-buffer "*gptel-error*" 'show)))
(if-let* (((eq (process-status process) 'exit)) (when-let* (((eq (process-status process) 'exit))
(proc-info (alist-get process gptel-curl--process-alist)) (proc-info (alist-get process gptel-curl--process-alist))
(proc-token (plist-get proc-info :token)) (proc-token (plist-get proc-info :token))
(proc-callback (plist-get proc-info :callback)) (proc-callback (plist-get proc-info :callback)))
(response (gptel-curl--parse-response proc-buf proc-token))) (pcase-let ((`(,response ,http-msg ,error)
(funcall proc-callback response proc-info) (gptel-curl--parse-response proc-buf proc-token)))
;; Failed (plist-put proc-info :status http-msg)
(funcall proc-callback (list :content nil :status status) proc-info)) (when error (plist-put proc-info :error error))
(funcall proc-callback response proc-info)))
(setf (alist-get process gptel-curl--process-alist nil 'remove) nil) (setf (alist-get process gptel-curl--process-alist nil 'remove) nil)
(kill-buffer proc-buf))) (kill-buffer proc-buf)))
@ -274,24 +273,21 @@ buffer."
(json-readtable-error 'json-read-error))))) (json-readtable-error 'json-read-error)))))
(cond (cond
((equal http-status "200") ((equal http-status "200")
(list :content (list (string-trim
(string-trim
(map-nested-elt response '(:choices 0 :message :content))) (map-nested-elt response '(:choices 0 :message :content)))
:status http-msg)) http-msg))
((plist-get response :error) ((plist-get response :error)
(let* ((error-plist (plist-get response :error)) (let* ((error-plist (plist-get response :error))
(error-msg (plist-get error-plist :message)) (error-msg (plist-get error-plist :message))
(error-type (plist-get error-plist :type))) (error-type (plist-get error-plist :type)))
(message "ChatGPT error: (%s) %s" http-msg error-msg) (list nil (concat "(" http-msg ") " (string-trim error-type)) error-msg)))
(list :content nil :status (concat "(" http-msg ") " (string-trim error-type)))))
((eq response 'json-read-error) ((eq response 'json-read-error)
(message "ChatGPT error: (%s) Malformed JSON in response." http-msg) (list nil (concat "(" http-msg ") Malformed JSON in response.")
(list :content nil :status (concat "(" http-msg ") Malformed JSON in response."))) "Malformed JSON in response"))
(t (message "ChatGPT error (%s): Could not parse HTTP response." http-msg) (t (list nil (concat "(" http-msg ") Could not parse HTTP response.")
(list :content nil :status (concat "(" http-msg ") Could not parse HTTP response.")))) "Could not parse HTTP response.")))
(message "ChatGPT error: (%s) Could not parse HTTP response." http-msg) (list nil (concat "(" http-msg ") Could not parse HTTP response.")
(list :content nil "Could not parse HTTP response."))))))
:status (concat "(" http-msg ") Could not parse HTTP response.")))))))
(provide 'gptel-curl) (provide 'gptel-curl)
;;; gptel-curl.el ends here ;;; gptel-curl.el ends here

View file

@ -304,8 +304,8 @@ instead."
(if gptel-use-curl (if gptel-use-curl
#'gptel-curl-get-response #'gptel--url-get-response) #'gptel-curl-get-response #'gptel--url-get-response)
(list :prompt full-prompt (list :prompt full-prompt
:gptel-buffer gptel-buffer :buffer gptel-buffer
:start-marker response-pt))) :position response-pt)))
(gptel--update-header-line " Waiting..." 'warning))) (gptel--update-header-line " Waiting..." 'warning)))
(defun gptel--insert-response (response info) (defun gptel--insert-response (response info)
@ -313,28 +313,29 @@ instead."
INFO is a plist containing information relevant to this buffer. INFO is a plist containing information relevant to this buffer.
See `gptel--url-get-response' for details." See `gptel--url-get-response' for details."
(let* ((content-str (plist-get response :content)) (let* ((status-str (plist-get info :status))
(status-str (plist-get response :status)) (gptel-buffer (plist-get info :buffer))
(gptel-buffer (plist-get info :gptel-buffer)) (start-marker (plist-get info :position)))
(start-marker (plist-get info :start-marker)))
(with-current-buffer gptel-buffer (with-current-buffer gptel-buffer
(if content-str (if response
(progn (progn
(setq content-str (gptel--transform-response (setq response (gptel--transform-response
content-str gptel-buffer)) response gptel-buffer))
(save-excursion (save-excursion
(put-text-property 0 (length content-str) 'gptel 'response content-str) (put-text-property 0 (length response) 'gptel 'response response)
(message "Querying ChatGPT... done.") (message "Querying ChatGPT... done.")
(goto-char start-marker) (goto-char start-marker)
(unless (bobp) (insert "\n\n")) (unless (bobp) (insert "\n\n"))
(let ((p (point))) (let ((p (point)))
(insert content-str) (insert response)
(pulse-momentary-highlight-region p (point))) (pulse-momentary-highlight-region p (point)))
(when gptel-mode (when gptel-mode
(insert "\n\n" (gptel-prompt-string)) (insert "\n\n" (gptel-prompt-string))
(gptel--update-header-line " Ready" 'success)))) (gptel--update-header-line " Ready" 'success))))
(gptel--update-header-line (gptel--update-header-line
(format " Response Error: %s" status-str) 'error)) (format " Response Error: %s" status-str) 'error)
(message "ChatGPT response error: (%s) %s"
status-str (plist-get info :error)))
(run-hooks 'gptel-post-response-hook)))) (run-hooks 'gptel-post-response-hook))))
(defun gptel--create-prompt (&optional prompt-end) (defun gptel--create-prompt (&optional prompt-end)
@ -422,8 +423,8 @@ BUFFER is the interaction buffer for ChatGPT."
INFO is a plist with the following keys: INFO is a plist with the following keys:
- :prompt (the prompt being sent) - :prompt (the prompt being sent)
- :gptel-buffer (the gptel buffer) - :buffer (the gptel buffer)
- :start-marker (marker at which to insert the response). - :position (marker at which to insert the response).
Call CALLBACK with the response and INFO afterwards. If omitted Call CALLBACK with the response and INFO afterwards. If omitted
the response is inserted into the current buffer after point." the response is inserted into the current buffer after point."
@ -439,8 +440,10 @@ the response is inserted into the current buffer after point."
'utf-8))) 'utf-8)))
(url-retrieve "https://api.openai.com/v1/chat/completions" (url-retrieve "https://api.openai.com/v1/chat/completions"
(lambda (_) (lambda (_)
(let ((response (pcase-let ((`(,response ,http-msg ,error)
(gptel--url-parse-response (current-buffer)))) (gptel--url-parse-response (current-buffer))))
(plist-put info :status http-msg)
(when error (plist-put info :error error))
(funcall (or callback #'gptel--insert-response) (funcall (or callback #'gptel--insert-response)
response info) response info)
(kill-buffer))) (kill-buffer)))
@ -465,22 +468,19 @@ the response is inserted into the current buffer after point."
(json-readtable-error 'json-read-error)))))) (json-readtable-error 'json-read-error))))))
(cond (cond
((string-match-p "200 OK" http-msg) ((string-match-p "200 OK" http-msg)
(list :content (string-trim (map-nested-elt response '(:choices 0 :message :content))) (list (string-trim (map-nested-elt response '(:choices 0 :message :content)))
:status http-msg)) http-msg))
((plist-get response :error) ((plist-get response :error)
(let* ((error-plist (plist-get response :error)) (let* ((error-plist (plist-get response :error))
(error-msg (plist-get error-plist :message)) (error-msg (plist-get error-plist :message))
(error-type (plist-get error-plist :type))) (error-type (plist-get error-plist :type)))
(message "ChatGPT error: (%s) %s" http-msg error-msg) (list nil (concat "(" http-msg ") " error-type) error-msg)))
(list :content nil :status (concat "(" http-msg ") " error-type))))
((eq response 'json-read-error) ((eq response 'json-read-error)
(message "ChatGPT error: (%s) Malformed JSON in response." http-msg) (list nil (concat "(" http-msg ") Malformed JSON in response.") "json-read-error"))
(list :content nil :status (concat http-msg ": Malformed JSON in response."))) (t (list nil (concat "(" http-msg ") Could not parse HTTP response.")
(t (message "ChatGPT error: (%s) Could not parse HTTP response." http-msg) "Could not parse HTTP response.")))
(list :content nil :status (concat "(" http-msg ") Could not parse HTTP response.")))) (list nil (concat "(" http-msg ") Could not parse HTTP response.")
(message "ChatGPT error: (%s) Could not parse HTTP response." http-msg) "Could not parse HTTP response.")))))
(list :content nil
:status (concat "(" http-msg ") Could not parse HTTP response."))))))
;;;###autoload ;;;###autoload
(defun gptel (name &optional api-key initial) (defun gptel (name &optional api-key initial)