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)
- ChatGPT's responses are in Markdown or Org markup.
- 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.
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.
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.
** 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
- Response redirection (to the echo area, another buffer, etc)
- 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:
- Limiting conversation context to Org headings using properties (#58)
- Stateless design (#17)
- Fully stateless design (#17)
** Alternatives

View file

@ -186,9 +186,34 @@ transient menu interface provided by `gptel-menu'."
:group 'gptel
: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
(defvar-local gptel--system-message
"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
`((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
interactively call `gptel-send' with a prefix argument."
:group 'gptel
:safe #'gptel--always
:type '(alist :key-type symbol :value-type string))
(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
will get progressively longer!"
:local t
:safe #'gptel--always
:group 'gptel
:type '(choice (integer :tag "Specify Token count")
(const :tag "Default" nil)))
@ -232,10 +259,11 @@ The current options are
- \"gpt-3.5-turbo-16k\"
- \"gpt-4\" (experimental)
- \"gpt-4-32k\" (experimental)
To set the model for a chat session interactively call
`gptel-send' with a prefix argument."
:local t
:safe #'gptel--always
:group 'gptel
:type '(choice
(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
`gptel-send' with a prefix argument."
:local t
:safe #'gptel--always
:group 'gptel
:type 'number)
(defvar-local gptel--bounds nil)
(put 'gptel--bounds 'safe-local-variable #'gptel--always)
(defvar-local gptel--num-messages-to-send nil)
(put 'gptel--num-messages-to-send 'safe-local-variable #'gptel--always)
(defvar gptel--debug nil)
(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"))))
(mapc (pcase-lambda (`(,beg . ,end))
(put-text-property beg end 'gptel 'response))
bounds))
bounds)
(message "gptel chat restored."))
(when-let ((model (org-entry-get (point-min) "GPTEL_MODEL")))
(setq-local gptel-model model))
(when-let ((system (org-entry-get (point-min) "GPTEL_SYSTEM")))
(setq-local gptel--system-message system))
(when-let ((temp (org-entry-get (point-min) "GPTEL_TEMPERATURE")))
(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 ()
"Write the gptel state to the buffer.
@ -346,8 +385,17 @@ opening the file."
(> attempts 0))
(funcall write-bounds (1- attempts)))))))
(funcall write-bounds 6))))
('markdown-mode
(message "Saving gptel state is not implemented for `markdown-mode'."))))
(_ (save-excursion
(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 ()
"Return the gptel response boundaries as an alist."
@ -395,14 +443,14 @@ opening the file."
(propertize
" " 'display `(space :align-to ,(max 1 (- (window-width) (+ 2 l1 l2)))))
(propertize
(button-buttonize num-exchanges
(gptel--button-buttonize num-exchanges
(lambda (&rest _) (gptel-menu)))
'mouse-face 'highlight
'help-echo
"Number of past exchanges to include with each request")
" "
(propertize
(button-buttonize (concat "[" gptel-model "]")
(gptel--button-buttonize (concat "[" gptel-model "]")
(lambda (&rest _) (gptel-menu)))
'mouse-face 'highlight
'help-echo "OpenAI GPT model in use")))))))