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.

Reply via email to