gptel: saving and restoring state for Markdown/Text

* gptel.el (gptel--save-state, gptel--restore-state,
gptel-temperature, gptel-model, gptel-max-tokens,
gptel-directives, gptel--always, gptel--button-buttonize,
gptel--system-message, gptel--bounds): Write gptel parameters as
file-local variables when saving chats in Markdown or text files.
The local variable gptel--bounds stores the locations of the
responses from the LLM. This is not a great solution, but the best
I can think to do without adding more syntax to the document.

Chats can be restored by turning on `gptel-mode'.  One of the
problem with this approach is that if the buffer is modified
before `gptel-mode' is turned on, the state data is out of date.
Another problem is that this metadata block as printed in the
buffer can become quite long.  A better approach is needed.

Define helper functions `gptel--always' and
`gptel--button-buttonize' to work around Emacs 27.1 support.

* README.org: Mention saving and restoring chats where
appropriate.
This commit is contained in:
Karthik Chikmagalur 2023-07-28 15:57:08 -07:00
parent e0a7898645
commit 0f161a466b
2 changed files with 61 additions and 9 deletions

View file

@ -13,6 +13,7 @@ https://user-images.githubusercontent.com/8607532/230516816-ae4a613a-4d01-4073-a
- Interact with ChatGPT from anywhere in Emacs (any buffer, shell, minibuffer, wherever) - Interact with ChatGPT from anywhere in Emacs (any buffer, shell, minibuffer, wherever)
- ChatGPT's responses are in Markdown or Org markup. - ChatGPT's responses are in Markdown or Org markup.
- Supports conversations and multiple independent sessions. - Supports conversations and multiple independent sessions.
- Save chats as regular Markdown/Org/Text files and resume them later.
- You can go back and edit your previous prompts, or even ChatGPT's previous responses when continuing a conversation. These will be fed back to ChatGPT. - You can go back and edit your previous prompts, or even ChatGPT's previous responses when continuing a conversation. These will be fed back to ChatGPT.
GPTel uses Curl if available, but falls back to url-retrieve to work without external dependencies. GPTel uses Curl if available, but falls back to url-retrieve to work without external dependencies.
@ -122,6 +123,8 @@ With a region selected, you can also rewrite prose or refactor code from here:
That's it. You can go back and edit previous prompts and responses if you want. That's it. You can go back and edit previous prompts and responses if you want.
4. Save the chat to a file. To resume, open the file and turn on =gptel-mode=.
The default mode is =markdown-mode= if available, else =text-mode=. You can set =gptel-default-mode= to =org-mode= if desired. The default mode is =markdown-mode= if available, else =text-mode=. You can set =gptel-default-mode= to =org-mode= if desired.
** Using it your way ** Using it your way
@ -167,10 +170,11 @@ Maybe, I'd like to experiment a bit more first. Features added since the incept
- GPT-4 support - GPT-4 support
- Response redirection (to the echo area, another buffer, etc) - Response redirection (to the echo area, another buffer, etc)
- A built-in refactor/rewrite prompt - A built-in refactor/rewrite prompt
- Limiting conversation context to Org headings using properties (#58)
- Saving and restoring chats (#17)
Features being considered or in the pipeline: Features being considered or in the pipeline:
- Limiting conversation context to Org headings using properties (#58) - Fully stateless design (#17)
- Stateless design (#17)
** Alternatives ** Alternatives

View file

@ -186,9 +186,34 @@ transient menu interface provided by `gptel-menu'."
:group 'gptel :group 'gptel
:type 'file) :type 'file)
;; FIXME This is convoluted, but it's not worth adding the `compat' dependency
;; just for a couple of helper functions either.
(cl-macrolet
((gptel--compat
() (if (version< "28.1" emacs-version)
(macroexp-progn
`((defalias 'gptel--button-buttonize #'button-buttonize)
(defalias 'gptel--always #'always)))
(macroexp-progn
`((defun gptel--always (&rest _)
"Always return t." t)
(defun gptel--button-buttonize (string callback)
"Make STRING into a button and return it.
When clicked, CALLBACK will be called."
(propertize string
'face 'button
'button t
'follow-link t
'category t
'button-data nil
'keymap button-map
'action callback)))))))
(gptel--compat))
;; Model and interaction parameters ;; Model and interaction parameters
(defvar-local gptel--system-message (defvar-local gptel--system-message
"You are a large language model living in Emacs and a helpful assistant. Respond concisely.") "You are a large language model living in Emacs and a helpful assistant. Respond concisely.")
(put 'gptel--system-message 'safe-local-variable #'gptel--always)
(defcustom gptel-directives (defcustom gptel-directives
`((default . ,gptel--system-message) `((default . ,gptel--system-message)
@ -204,6 +229,7 @@ Each entry in this alist maps a symbol naming the directive to
the string that is sent. To set the directive for a chat session the string that is sent. To set the directive for a chat session
interactively call `gptel-send' with a prefix argument." interactively call `gptel-send' with a prefix argument."
:group 'gptel :group 'gptel
:safe #'gptel--always
:type '(alist :key-type symbol :value-type string)) :type '(alist :key-type symbol :value-type string))
(defcustom gptel-max-tokens nil (defcustom gptel-max-tokens nil
@ -220,6 +246,7 @@ If left unset, ChatGPT will target about 40% of the total token
count of the conversation so far in each message, so messages count of the conversation so far in each message, so messages
will get progressively longer!" will get progressively longer!"
:local t :local t
:safe #'gptel--always
:group 'gptel :group 'gptel
:type '(choice (integer :tag "Specify Token count") :type '(choice (integer :tag "Specify Token count")
(const :tag "Default" nil))) (const :tag "Default" nil)))
@ -232,10 +259,11 @@ The current options are
- \"gpt-3.5-turbo-16k\" - \"gpt-3.5-turbo-16k\"
- \"gpt-4\" (experimental) - \"gpt-4\" (experimental)
- \"gpt-4-32k\" (experimental) - \"gpt-4-32k\" (experimental)
To set the model for a chat session interactively call To set the model for a chat session interactively call
`gptel-send' with a prefix argument." `gptel-send' with a prefix argument."
:local t :local t
:safe #'gptel--always
:group 'gptel :group 'gptel
:type '(choice :type '(choice
(const :tag "GPT 3.5 turbo" "gpt-3.5-turbo") (const :tag "GPT 3.5 turbo" "gpt-3.5-turbo")
@ -252,10 +280,15 @@ of the response, with 2.0 being the most random.
To set the temperature for a chat session interactively call To set the temperature for a chat session interactively call
`gptel-send' with a prefix argument." `gptel-send' with a prefix argument."
:local t :local t
:safe #'gptel--always
:group 'gptel :group 'gptel
:type 'number) :type 'number)
(defvar-local gptel--bounds nil)
(put 'gptel--bounds 'safe-local-variable #'gptel--always)
(defvar-local gptel--num-messages-to-send nil) (defvar-local gptel--num-messages-to-send nil)
(put 'gptel--num-messages-to-send 'safe-local-variable #'gptel--always)
(defvar gptel--debug nil) (defvar gptel--debug nil)
(defun gptel-api-key-from-auth-source (&optional host user) (defun gptel-api-key-from-auth-source (&optional host user)
@ -302,14 +335,20 @@ Currently saving and restoring state is implemented only for
(read (org-entry-get (point-min) "GPTEL_BOUNDS")))) (read (org-entry-get (point-min) "GPTEL_BOUNDS"))))
(mapc (pcase-lambda (`(,beg . ,end)) (mapc (pcase-lambda (`(,beg . ,end))
(put-text-property beg end 'gptel 'response)) (put-text-property beg end 'gptel 'response))
bounds)) bounds)
(message "gptel chat restored."))
(when-let ((model (org-entry-get (point-min) "GPTEL_MODEL"))) (when-let ((model (org-entry-get (point-min) "GPTEL_MODEL")))
(setq-local gptel-model model)) (setq-local gptel-model model))
(when-let ((system (org-entry-get (point-min) "GPTEL_SYSTEM"))) (when-let ((system (org-entry-get (point-min) "GPTEL_SYSTEM")))
(setq-local gptel--system-message system)) (setq-local gptel--system-message system))
(when-let ((temp (org-entry-get (point-min) "GPTEL_TEMPERATURE"))) (when-let ((temp (org-entry-get (point-min) "GPTEL_TEMPERATURE")))
(setq-local gptel-temperature (gptel--numberize temp)))) (setq-local gptel-temperature (gptel--numberize temp))))
(error (message "Could not restore gptel state, sorry!")))))))) (error (message "Could not restore gptel state, sorry!")))))
(_ (when gptel--bounds
(mapc (pcase-lambda (`(,beg . ,end))
(put-text-property beg end 'gptel 'response))
gptel--bounds)
(message "gptel chat restored."))))))
(defun gptel--save-state () (defun gptel--save-state ()
"Write the gptel state to the buffer. "Write the gptel state to the buffer.
@ -346,8 +385,17 @@ opening the file."
(> attempts 0)) (> attempts 0))
(funcall write-bounds (1- attempts))))))) (funcall write-bounds (1- attempts)))))))
(funcall write-bounds 6)))) (funcall write-bounds 6))))
('markdown-mode (_ (save-excursion
(message "Saving gptel state is not implemented for `markdown-mode'.")))) (save-restriction
(add-file-local-variable 'gptel-model gptel-model)
(unless (equal (default-value 'gptel-temperature) gptel-temperature)
(add-file-local-variable 'gptel-temperature gptel-temperature))
(unless (string= (default-value 'gptel--system-message)
gptel--system-message)
(add-file-local-variable 'gptel--system-message gptel--system-message))
(when gptel-max-tokens
(add-file-local-variable 'gptel-max-tokens gptel-max-tokens))
(add-file-local-variable 'gptel--bounds (gptel--get-bounds)))))))
(defun gptel--get-bounds () (defun gptel--get-bounds ()
"Return the gptel response boundaries as an alist." "Return the gptel response boundaries as an alist."
@ -395,14 +443,14 @@ opening the file."
(propertize (propertize
" " 'display `(space :align-to ,(max 1 (- (window-width) (+ 2 l1 l2))))) " " 'display `(space :align-to ,(max 1 (- (window-width) (+ 2 l1 l2)))))
(propertize (propertize
(button-buttonize num-exchanges (gptel--button-buttonize num-exchanges
(lambda (&rest _) (gptel-menu))) (lambda (&rest _) (gptel-menu)))
'mouse-face 'highlight 'mouse-face 'highlight
'help-echo 'help-echo
"Number of past exchanges to include with each request") "Number of past exchanges to include with each request")
" " " "
(propertize (propertize
(button-buttonize (concat "[" gptel-model "]") (gptel--button-buttonize (concat "[" gptel-model "]")
(lambda (&rest _) (gptel-menu))) (lambda (&rest _) (gptel-menu)))
'mouse-face 'highlight 'mouse-face 'highlight
'help-echo "OpenAI GPT model in use"))))))) 'help-echo "OpenAI GPT model in use")))))))