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:
parent
e0a7898645
commit
0f161a466b
2 changed files with 61 additions and 9 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
62
gptel.el
62
gptel.el
|
@ -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")))))))
|
||||||
|
|
Loading…
Add table
Reference in a new issue