gptel: saving and restoring state, and limiting context
* gptel.el (gptel-mode, gptel-set-topic, gptel--create-prompt, gptel-set-topic, gptel--get-topic-start, gptel--get-bounds, gptel--save-state, gptel--restore-state): Add support for saving and restoring gptel state for Org buffers. Support for Markdown buffers is not yet implemented. `gptel--save-state' and `gptel--restore-state' save and restores state using Org properties. With `gptel-mode' active, these are run automatically when saving the buffer or enabling `gptel-mode' respectively. The command `gptel-set-topic' can be used to set a topic for the current heading, which is stored as an Org property. The topic name is unused (as of now), but the presence of this property limits the text context sent to ChatGPT to the heading text up to the cursor position. Autload `gptel-mode' since the user may want to enable this (to restore sessions) without having loaded gptel.el.
This commit is contained in:
parent
4356f6fbec
commit
cc6c5e7321
1 changed files with 155 additions and 31 deletions
132
gptel.el
132
gptel.el
|
@ -57,6 +57,16 @@
|
|||
(declare-function gptel-menu "gptel-transient")
|
||||
(declare-function pulse-momentary-highlight-region "pulse")
|
||||
|
||||
;; Functions used for saving/restoring gptel state in Org buffers
|
||||
(defvar org-entry-property-inherited-from)
|
||||
(declare-function org-entry-get "org")
|
||||
(declare-function org-entry-put "org")
|
||||
(declare-function org-with-wide-buffer "org-macs")
|
||||
(declare-function org-set-property "org")
|
||||
(declare-function org-property-values "org")
|
||||
(declare-function org-open-line "org")
|
||||
(declare-function org-at-heading-p "org")
|
||||
|
||||
(eval-when-compile
|
||||
(require 'subr-x)
|
||||
(require 'cl-lib))
|
||||
|
@ -268,7 +278,85 @@ By default, `gptel-host' is used as HOST and \"apikey\" as USER."
|
|||
(defun gptel-prompt-string ()
|
||||
(or (alist-get major-mode gptel-prompt-prefix-alist) ""))
|
||||
|
||||
(defun gptel--restore-state ()
|
||||
"Restore gptel state when turning on `gptel-mode'.
|
||||
|
||||
Currently saving and restoring state is implemented only for
|
||||
`org-mode' buffers."
|
||||
(when (buffer-file-name)
|
||||
(pcase major-mode
|
||||
('org-mode
|
||||
(save-restriction
|
||||
(widen)
|
||||
(condition-case-unless-debug nil
|
||||
(progn
|
||||
(when-let ((bounds
|
||||
(read (org-entry-get (point-min) "GPTEL_BOUNDS"))))
|
||||
(mapc (pcase-lambda (`(,beg . ,end))
|
||||
(put-text-property beg end 'gptel 'response))
|
||||
bounds))
|
||||
(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!"))))))))
|
||||
|
||||
(defun gptel--save-state ()
|
||||
"Write the gptel state to the buffer.
|
||||
|
||||
This enables saving the chat session when writing the buffer to
|
||||
disk. To restore a chat session, turn on `gptel-mode' after
|
||||
opening the file."
|
||||
(pcase major-mode
|
||||
('org-mode
|
||||
(org-with-wide-buffer
|
||||
(goto-char (point-min))
|
||||
(when (org-at-heading-p)
|
||||
(org-open-line 1))
|
||||
(org-entry-put (point-min) "GPTEL_MODEL" gptel-model)
|
||||
(org-entry-put (point-min) "GPTEL_TEMPERATURE"
|
||||
(number-to-string gptel-temperature))
|
||||
(unless (string=
|
||||
(default-value 'gptel--system-message)
|
||||
gptel--system-message)
|
||||
(org-entry-put (point-min) "GPTEL_SYSTEM"
|
||||
gptel--system-message))
|
||||
(when gptel-max-tokens
|
||||
(org-entry-put
|
||||
(point-min) "GPTEL_MAX_TOKENS" gptel-max-tokens))
|
||||
;; Save response boundaries
|
||||
(letrec ((write-bounds
|
||||
(lambda (attempts)
|
||||
(let* ((bounds (gptel--get-bounds))
|
||||
(offset (caar bounds))
|
||||
(offset-marker (set-marker (make-marker) offset)))
|
||||
(org-entry-put (point-min) "GPTEL_BOUNDS"
|
||||
(prin1-to-string (gptel--get-bounds)))
|
||||
(when (and (not (= (marker-position offset-marker) offset))
|
||||
(> attempts 0))
|
||||
(funcall write-bounds (1- attempts)))))))
|
||||
(funcall write-bounds 6))))
|
||||
('markdown-mode
|
||||
(message "Saving gptel state is not implemented for `markdown-mode'."))))
|
||||
|
||||
(defun gptel--get-bounds ()
|
||||
"Return the gptel response boundaries as an alist."
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(widen)
|
||||
(goto-char (point-max))
|
||||
(let ((prop) (bounds))
|
||||
(while (setq prop (text-property-search-backward
|
||||
'gptel 'response t))
|
||||
(push (cons (prop-match-beginning prop)
|
||||
(prop-match-end prop))
|
||||
bounds))
|
||||
bounds))))
|
||||
|
||||
(defvar-local gptel--old-header-line nil)
|
||||
;;;###autoload
|
||||
(define-minor-mode gptel-mode
|
||||
"Minor mode for interacting with ChatGPT."
|
||||
:lighter " GPT"
|
||||
|
@ -277,6 +365,12 @@ By default, `gptel-host' is used as HOST and \"apikey\" as USER."
|
|||
(define-key map (kbd "C-c RET") #'gptel-send)
|
||||
map)
|
||||
(if gptel-mode
|
||||
(progn
|
||||
(unless (memq major-mode '(org-mode markdown-mode text-mode))
|
||||
(gptel-mode -1)
|
||||
(user-error (format "`gptel-mode' is not supported in `%s'." major-mode)))
|
||||
(add-hook 'before-save-hook #'gptel--save-state nil t)
|
||||
(gptel--restore-state)
|
||||
(setq gptel--old-header-line header-line-format
|
||||
header-line-format
|
||||
(list (concat (propertize " " 'display '(space :align-to 0))
|
||||
|
@ -303,7 +397,7 @@ By default, `gptel-host' is used as HOST and \"apikey\" as USER."
|
|||
(button-buttonize (concat "[" gptel-model "]")
|
||||
(lambda (&rest _) (gptel-menu)))
|
||||
'mouse-face 'highlight
|
||||
'help-echo "OpenAI GPT model in use"))))))
|
||||
'help-echo "OpenAI GPT model in use")))))))
|
||||
(setq header-line-format gptel--old-header-line)))
|
||||
|
||||
(defun gptel--update-header-line (msg face)
|
||||
|
@ -488,6 +582,30 @@ See `gptel--url-get-response' for details."
|
|||
status-str (plist-get info :error)))
|
||||
(run-hooks 'gptel-post-response-hook))))
|
||||
|
||||
(defun gptel-set-topic ()
|
||||
"Set a topic and limit this conversation to the current heading.
|
||||
|
||||
This limits the context sent to ChatGPT to the text between the
|
||||
current heading and the cursor position."
|
||||
(interactive)
|
||||
(pcase major-mode
|
||||
('org-mode
|
||||
(org-set-property
|
||||
"GPTEL_TOPIC"
|
||||
(completing-read "Set topic as: "
|
||||
(org-property-values "GPTEL_TOPIC"))))
|
||||
('markdown-mode
|
||||
(message
|
||||
"Support for multiple topics per buffer is not implemented for `markdown-mode'."))))
|
||||
|
||||
(defun gptel--get-topic-start ()
|
||||
"If a conversation topic is set, return it."
|
||||
(pcase major-mode
|
||||
('org-mode
|
||||
(when (org-entry-get (point) "GPTEL_TOPIC" 'inherit)
|
||||
(marker-position org-entry-property-inherited-from)))
|
||||
('markdown-mode nil)))
|
||||
|
||||
(defun gptel--create-prompt (&optional prompt-end)
|
||||
"Return a full conversation prompt from the contents of this buffer.
|
||||
|
||||
|
@ -501,10 +619,16 @@ If PROMPT-END (a marker) is provided, end the prompt contents
|
|||
there."
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(if (use-region-p)
|
||||
(progn (narrow-to-region (region-beginning) (region-end))
|
||||
(cond
|
||||
((use-region-p)
|
||||
;; Narrow to region
|
||||
(narrow-to-region (region-beginning) (region-end))
|
||||
(goto-char (point-max)))
|
||||
(goto-char (or prompt-end (point-max))))
|
||||
((when-let ((topic-start (gptel--get-topic-start)))
|
||||
;; Narrow to topic
|
||||
(narrow-to-region topic-start (or prompt-end (point-max)))
|
||||
(goto-char (point-max))))
|
||||
(t (goto-char (or prompt-end (point-max)))))
|
||||
(let ((max-entries (and gptel--num-messages-to-send
|
||||
(* 2 (gptel--numberize
|
||||
gptel--num-messages-to-send))))
|
||||
|
|
Loading…
Add table
Reference in a new issue