branch: elpa/gptel commit 7400df6d27ac4311d1eca431335ca44b457180f2 Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com> Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
gptel: Create all prompts in temp buffer Do not load gptel-org.el via `with-eval-after-load', since we require it anyway when sending commands. Require gptel when compiling gptel-org.el. * gptel.el: (gptel--create-prompt, gptel--wrap-user-prompt-maybe, gptel-request, gptel--with-buffer-copy, gptel-prompt-filter-hook): All prompts are now created from a temp buffer. This includes Org mode (from before), markdown and strings passed to `gptel-request'. New macro `gptel--with-buffer-copy' to copy over relevant local variables and help create prompts from temp buffers. This makes it possible to do arbitrary text transformations to the buffer before sending the request, paving the way for the new hook `gptel-prompt-filter-hook'. The name is tentative. This hook is intended for transformations like deleting parts of the buffer the user doesn't want to send, or replacing shell commands with shell output (#328). * gptel-org.el (gptel-org--create-prompt): Adjust for `gptel--with-buffer-copy' and run `gptel-prompt-filter-hook'. * README (Additional Customization): Mention `gptel-prompt-filter-hook'. Document `gptel-post-request-hook' from before -- this was an omission. --- README.org | 2 ++ gptel-org.el | 33 ++++++++----------- gptel.el | 101 ++++++++++++++++++++++++++++++++++++++++------------------- 3 files changed, 83 insertions(+), 53 deletions(-) diff --git a/README.org b/README.org index 663d550b7c..e5a6abd301 100644 --- a/README.org +++ b/README.org @@ -1347,6 +1347,8 @@ Other Emacs clients for LLMs prescribe the format of the interaction (a comint s | *Hooks for customization* | | |---------------------------------+-------------------------------------------------------------| | =gptel-save-state-hook= | Runs before saving the chat state to a file on disk | +| =gptel-prompt-filter-hook= | Runs in a temp buffer to transform text before sending | +| =gptel-post-request-hook= | Runs immediately after dispatching a =gptel-request=. | | =gptel-pre-response-hook= | Runs before inserting the LLM response into the buffer | | =gptel-post-response-functions= | Runs after inserting the full LLM response into the buffer | | =gptel-post-stream-hook= | Runs after each streaming insertion | diff --git a/gptel-org.el b/gptel-org.el index fc20baf6e5..0745463993 100644 --- a/gptel-org.el +++ b/gptel-org.el @@ -23,7 +23,9 @@ ;; ;;; Code: -(eval-when-compile (require 'cl-lib)) +(eval-when-compile + (require 'cl-lib) + (require 'gptel)) (require 'org-element) (require 'outline) @@ -51,6 +53,7 @@ (declare-function gptel--parse-buffer "gptel") (declare-function gptel--parse-directive "gptel") (declare-function gptel--restore-props "gptel") +(declare-function gptel--with-buffer-copy "gptel") (declare-function org-entry-get "org") (declare-function org-entry-put "org") (declare-function org-with-wide-buffer "org-macs") @@ -236,12 +239,7 @@ value of `gptel-org-branching-context', which see." do (outline-next-heading) collect (point) into ends finally return (cons prompt-end ends)))) - (with-temp-buffer - ;; TODO(org) duplicated below - (dolist (sym '( gptel-backend gptel--system-message gptel-model - gptel-mode gptel-track-response gptel-track-media)) - (set (make-local-variable sym) - (buffer-local-value sym org-buf))) + (gptel--with-buffer-copy org-buf nil nil (cl-loop for start in start-bounds for end in end-bounds do (insert-buffer-substring org-buf start end) @@ -250,24 +248,19 @@ value of `gptel-org-branching-context', which see." (gptel-org--unescape-tool-results) (gptel-org--strip-elements) (gptel-org--strip-block-headers) - (let ((major-mode 'org-mode)) - (gptel--parse-buffer gptel-backend max-entries))))) + (when gptel-org-ignore-elements (gptel-org--strip-elements)) + (save-excursion (run-hooks 'gptel-prompt-filter-hook)) + (gptel--parse-buffer gptel-backend max-entries)))) ;; Create prompt the usual way (let ((org-buf (current-buffer)) - (beg (point-min)) - (end (point-max))) - (with-temp-buffer - ;; TODO(org) duplicated above - (dolist (sym '( gptel-backend gptel--system-message gptel-model - gptel-mode gptel-track-response gptel-track-media)) - (set (make-local-variable sym) - (buffer-local-value sym org-buf))) - (insert-buffer-substring org-buf beg end) + (beg (point-min)) (end (point-max))) + (gptel--with-buffer-copy org-buf beg end (gptel-org--unescape-tool-results) (gptel-org--strip-elements) (gptel-org--strip-block-headers) - (let ((major-mode 'org-mode)) - (gptel--parse-buffer gptel-backend max-entries))))))) + (when gptel-org-ignore-elements (gptel-org--strip-elements)) + (save-excursion (run-hooks 'gptel-prompt-filter-hook)) + (gptel--parse-buffer gptel-backend max-entries)))))) (defun gptel-org--strip-elements () "Remove all elements in `gptel-org-ignore-elements' from the diff --git a/gptel.el b/gptel.el index d1a25d0ed2..8afd57929a 100644 --- a/gptel.el +++ b/gptel.el @@ -191,9 +191,6 @@ (require 'cl-generic) (require 'gptel-openai) -(with-eval-after-load 'org - (require 'gptel-org)) - ;;; User options @@ -273,6 +270,19 @@ The default for windows comes from Microsoft documentation located here: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa" :type 'natnum) +(defcustom gptel-prompt-filter-hook nil + "Hook run to modify the buffer before sending. + +This hook is called in a temporary buffer containing the text to +be sent, with the cursor at the end of the prompt. You can use +it to modify the buffer as required. + +Example: A typical use case might be to search for occurrences of +$(cmd) and replace it with the output of the shell command cmd, +making it easy to send the output of shell commands to the LLM." + :group 'gptel + :type 'hook) + (defcustom gptel-post-request-hook nil "Hook run after sending a gptel request. @@ -961,6 +971,22 @@ Note: This will move the cursor." (skip-syntax-forward "w.") ,(macroexp-progn body))) +(defmacro gptel--with-buffer-copy (buf start end &rest body) + "Copy gptel's local variables from BUF to a temp buffer and run BODY. + +If positions START and END are provided, insert that part of BUF first." + (declare (indent 3)) + `(with-temp-buffer + (dolist (sym '( gptel-backend gptel--system-message gptel-model + gptel-mode gptel-track-response gptel-track-media + gptel-prompt-filter-hook)) + (set (make-local-variable sym) + (buffer-local-value sym ,buf))) + ,(when (and start end) + `(insert-buffer-substring ,buf ,start ,end)) + (let ((major-mode (buffer-local-value 'major-mode ,buf))) + ,@body))) + (defun gptel-prompt-prefix-string () "Prefix before user prompts in `gptel-mode'." (or (alist-get major-mode gptel-prompt-prefix-alist) "")) @@ -2315,12 +2341,13 @@ be used to rerun or continue the request at a later time." ((null prompt) (gptel--create-prompt start-marker)) ((stringp prompt) - ;; FIXME Dear reader, welcome to Jank City: - (with-temp-buffer - (let ((gptel-model (buffer-local-value 'gptel-model buffer)) - (gptel-backend (buffer-local-value 'gptel-backend buffer))) - (insert prompt) - (gptel--create-prompt)))) + (gptel--with-buffer-copy buffer nil nil + (insert prompt) + (save-excursion (run-hooks 'gptel-prompt-filter-hook)) + (gptel--wrap-user-prompt-maybe + (gptel--parse-buffer + gptel-backend (and gptel--num-messages-to-send + (* 2 gptel--num-messages-to-send)))))) ((consp prompt) (gptel--parse-list gptel-backend prompt))))) (info (list :data (gptel--request-data gptel-backend full-prompt) :buffer buffer @@ -2532,6 +2559,29 @@ Optional RAW disables text properties and transformation." (`(tool-result . ,tool-results) (gptel--display-tool-results tool-results info))))) +(defun gptel--wrap-user-prompt-maybe (prompts) + "Return PROMPTS wrapped with text and media context. + +This delegates to backend-specific wrap functions." + (prog1 prompts + (when gptel-context--alist + ;; Inject context chunks into the last user prompt if required. + ;; This is also the fallback for when `gptel-use-context' is set to + ;; 'system but the model does not support system messages. + (when (and gptel-use-context + (or (eq gptel-use-context 'user) + (gptel--model-capable-p 'nosystem)) + (> (length prompts) 0)) ;FIXME context should be injected + ;even when there are no prompts + (gptel--wrap-user-prompt gptel-backend prompts)) + ;; Inject media chunks into the first user prompt if required. Media + ;; chunks are always included with the first user message, + ;; irrespective of the preference in `gptel-use-context'. This is + ;; because media cannot be included (in general) with system messages. + (when (and gptel-use-context gptel-track-media + (gptel--model-capable-p 'media)) + (gptel--wrap-user-prompt gptel-backend prompts :media))))) + (defun gptel--create-prompt (&optional prompt-end) "Return a full conversation prompt from the contents of this buffer. @@ -2551,38 +2601,23 @@ there." (let* ((max-entries (and gptel--num-messages-to-send (* 2 gptel--num-messages-to-send))) (prompt-end (or prompt-end (point-max))) + (buf (current-buffer)) (prompts (cond ((use-region-p) - ;; Narrow to region - (narrow-to-region (region-beginning) (region-end)) - (goto-char (point-max)) - (gptel--parse-buffer gptel-backend max-entries)) + (let ((rb (region-beginning)) (re (region-end))) + (gptel--with-buffer-copy buf rb re + (save-excursion (run-hooks 'gptel-prompt-filter-hook)) + (gptel--parse-buffer gptel-backend max-entries)))) ((derived-mode-p 'org-mode) (require 'gptel-org) (goto-char prompt-end) (gptel-org--create-prompt prompt-end)) - (t (goto-char prompt-end) - (gptel--parse-buffer gptel-backend max-entries))))) + (t (gptel--with-buffer-copy buf (point-min) prompt-end + (save-excursion (run-hooks 'gptel-prompt-filter-hook)) + (gptel--parse-buffer gptel-backend max-entries)))))) ;; NOTE: prompts is modified in place here - (when gptel-context--alist - ;; Inject context chunks into the last user prompt if required. - ;; This is also the fallback for when `gptel-use-context' is set to - ;; 'system but the model does not support system messages. - (when (and gptel-use-context - (or (eq gptel-use-context 'user) - (gptel--model-capable-p 'nosystem)) - (> (length prompts) 0)) ;FIXME context should be injected - ;even when there are no prompts - (gptel--wrap-user-prompt gptel-backend prompts)) - ;; Inject media chunks into the first user prompt if required. Media - ;; chunks are always included with the first user message, - ;; irrespective of the preference in `gptel-use-context'. This is - ;; because media cannot be included (in general) with system messages. - (when (and gptel-use-context gptel-track-media - (gptel--model-capable-p 'media)) - (gptel--wrap-user-prompt gptel-backend prompts :media))) - prompts)))) + (gptel--wrap-user-prompt-maybe prompts))))) (cl-defgeneric gptel--parse-buffer (backend max-entries) "Parse current buffer backwards from point and return a list of prompts.