branch: elpa/gptel
commit 079d9cd6462179215e4b8f55e4c8d289467e59b1
Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    gptel: Apply prompt-transform-functions in gptel-request
    
    Use `gptel-prompt-transform-functions' in `gptel-request',
    enabling a prompt transformation/augmentation pipeline.  This
    pipeline will be used for just-in-time presets, adding gptel's context
    to the request and other operations that are critical to
    `gptel-send'.
    
    `gptel-prompt-transform-functions' is not automatically applied to
    `gptel-request' calls, it must be passed explicitly via the
    `:transforms' argument.  This is so that gptel-request stays
    closer to a 'pure' function, for use as a library.
    
    `gptel-send' passes the transforms explicitly to `gptel-request'.
    
    * gptel.el (gptel--create-prompt-buffer, gptel--create-prompt):
    New function `gptel--create-prompt-buffer', used to create a
    prompt buffer with the appropriate text to be
    sent (pre-transform).  `gptel--create-prompt' is no longer used
    but retained for compatibility and testing.
    
    (gptel-request): Add the `:transforms' argument and docstring.
    
    Run `gptel-prompt-transform-functions' here.  In short, create the
    prompt construction buffer with `gptel--create-prompt-buffer',
    then run through the transforms.  The preset transform is applied
    first, since it can change the value of
    `gptel-prompt-transform-functions'.  The rest are applied via a
    wrapped hook that distinguishes between sync and async transforms
    via function arity, and uses a "gather" method to track
    completion.
    
    TODO: Async transforms end up running in parallel, so there is a
    race condition involving buffer mutation when there is more than
    one.  This will be fixed in the future.
    
    * gptel-transient (gptel--suffix-send): Add `:transforms' to the
    `gptel-request' call.
    
    * gptel-rewrite (gptel--suffix-rewrite): Add `:transforms' to the
    `gptel-request' call.
---
 gptel-rewrite.el   |   1 +
 gptel-transient.el |   1 +
 gptel.el           | 106 +++++++++++++++++++++++++++++++++++++++++------------
 3 files changed, 84 insertions(+), 24 deletions(-)

diff --git a/gptel-rewrite.el b/gptel-rewrite.el
index b5e8cfcb58..0ed0819cc1 100644
--- a/gptel-rewrite.el
+++ b/gptel-rewrite.el
@@ -642,6 +642,7 @@ generated from functions."
                (overlay-put ov 'evaporate t)
                ;; NOTE: Switch to `generate-new-buffer' after we drop Emacs 
27.1 (#724)
                (cons ov (gptel--temp-buffer " *gptel-rewrite*")))
+             :transforms gptel-prompt-transform-functions
              :callback #'gptel--rewrite-callback)
       ;; Move back so that the cursor is on the overlay when done.
       (unless (get-char-property (point) 'gptel-rewrite)
diff --git a/gptel-transient.el b/gptel-transient.el
index fbdfa83fc6..7040761b68 100644
--- a/gptel-transient.el
+++ b/gptel-transient.el
@@ -1498,6 +1498,7 @@ This sets the variable `gptel-include-tool-results', 
which see."
                  (gptel--merge-additional-directive system-extra)
                gptel--system-message)
              :callback callback
+             :transforms gptel-prompt-transform-functions
              :fsm (gptel-make-fsm :handlers gptel-send--handlers)
              :dry-run dry-run)
 
diff --git a/gptel.el b/gptel.el
index ff09431da5..6981dfd5a4 100644
--- a/gptel.el
+++ b/gptel.el
@@ -2255,7 +2255,7 @@ Run post-response hooks."
                position context dry-run
                (stream nil) (in-place nil)
                (system gptel--system-message)
-               (fsm (gptel-make-fsm)))
+               transforms (fsm (gptel-make-fsm)))
   "Request a response from the `gptel-backend' for PROMPT.
 
 The request is asynchronous, this function returns immediately.
@@ -2390,6 +2390,22 @@ If DRY-RUN is non-nil, do not send the request.  
Construct and
 return a state machine object that can be introspected and
 resumed.
 
+TRANSFORMS is a list of functions used to transform the prompt or query
+parameters dynamically.  Each function is called in a temporary buffer
+containing the prompt to be sent, and can conditionally modify this
+buffer.  This can include changing the (buffer-local) values of the
+model, backend or system prompt, or augmenting the prompt with
+additional information (such as from a RAG engine).
+
+- Synchronous transformers are called with zero or one argument, the
+  INFO plist for the request.
+
+- Asynchronous transformers are called with two arguments, a callback
+  and the state machine.  It should run the callback after finishing its
+  transformation.
+
+See `gptel-prompt-transform-functions' for more.
+
 FSM is the state machine driving the request.  This can be used
 to define a custom request control flow, see `gptel-fsm' for
 details.  You can safely ignore this -- FSM is an unstable
@@ -2432,6 +2448,7 @@ be used to rerun or continue the request at a later time."
          (info (list :data prompt-buffer
                      :buffer buffer
                      :position start-marker)))
+    (when transforms (plist-put info :transforms transforms))
     (with-current-buffer prompt-buffer (setq gptel--system-message system))
     (when stream (plist-put info :stream stream))
     ;; This context should not be confused with the context aggregation 
context!
@@ -2441,8 +2458,51 @@ be used to rerun or continue the request at a later 
time."
     ;; Add info to state machine context
     (when dry-run (plist-put info :dry-run dry-run))
     (setf (gptel-fsm-info fsm) info))
-  ;; (gptel--fsm-transition fsm)           ;INIT -> AUGMENT
-  (gptel--realize-query fsm))
+
+  ;; TEMP: Augment in separate let block to avoid overcapturing
+  ;; FIXME(augment) Call augmentors with INFO, not FSM
+  (let ((info (gptel-fsm-info fsm)))
+    (with-current-buffer (plist-get info :data)
+      (setq-local gptel-prompt-transform-functions (plist-get info 
:transforms))
+      ;; Preset has highest priority because it can change 
prompt-transform-functions
+      (when (memq 'gptel--transform-apply-preset 
gptel-prompt-transform-functions)
+        (gptel--transform-apply-preset fsm)
+        (setq gptel-prompt-transform-functions ;avoid mutation, copy transforms
+              (remq 'gptel--transform-apply-preset 
gptel-prompt-transform-functions)))
+      (let ((augment-total              ;act like a hook, count total
+             (if (memq t gptel-prompt-transform-functions)
+                 (length
+                  (setq gptel-prompt-transform-functions
+                        (nconc (remq t gptel-prompt-transform-functions)
+                               (default-value 
'gptel-prompt-transform-functions))))
+               (length gptel-prompt-transform-functions)))
+            (augment-idx 0))
+        (if (null gptel-prompt-transform-functions)
+            (gptel--realize-query fsm)
+          (with-current-buffer (plist-get info :buffer) ;Apply prompt 
transformations
+            (gptel--update-status " Augmenting..." 'mode-line-emphasis))
+          ;; TODO(augment): This needs to be converted into a linear callback
+          ;; chain to avoid race conditions with multiple async augmentors.
+          (run-hook-wrapped
+           'gptel-prompt-transform-functions
+           (lambda (func fsm-arg)
+             (with-current-buffer (plist-get info :data)
+               (goto-char (point-max))
+               (if (= (car (func-arity func)) 2) ;async augmentor
+                   (funcall func (lambda ()
+                                   (cl-incf augment-idx)
+                                   (when (>= augment-idx augment-total) ;All 
augmentors have run
+                                     (gptel--realize-query fsm-arg)))
+                            fsm-arg)
+                 (if (= (car (func-arity func)) 0)
+                     (funcall func)
+                   (funcall func fsm-arg)) ;sync augmentor
+                 (cl-incf augment-idx)
+                 (when (>= augment-idx augment-total) ;All augmentors have run
+                   (gptel--realize-query fsm-arg))))
+             nil)           ;always return nil so run-hook-wrapped doesn't 
abort
+           fsm)))))
+  fsm)
 
 (defun gptel--realize-query (fsm)
   "Realize the query payload for FSM from its prompt buffer.
@@ -2550,6 +2610,7 @@ waiting for the response."
     (gptel--sanitize-model)
     (gptel-request nil
       :stream gptel-stream
+      :transforms gptel-prompt-transform-functions
       :fsm (gptel-make-fsm :handlers gptel-send--handlers))
     (gptel--update-status " Waiting..." 'warning)))
 
@@ -2721,23 +2782,19 @@ Otherwise the prompt text is constructed from the 
contents of the
 current buffer up to point, or PROMPT-END if provided."
   (save-excursion
     (save-restriction
-      (let* ((buf (current-buffer))
-             (prompts
-              (cond
-               ((derived-mode-p 'org-mode)
-                (require 'gptel-org)
-                ;; Also handles regions in Org mode
-                (gptel-org--create-prompt-buffer prompt-end))
-               ((use-region-p)
-                (let ((rb (region-beginning)) (re (region-end)))
-                  (gptel--with-buffer-copy buf rb re
-                    (current-buffer))))
-               (t (unless prompt-end (setq prompt-end (point)))
-                  (gptel--with-buffer-copy buf (point-min) prompt-end
-                    (current-buffer))))))
-        ;; NOTE: prompts is modified in place here
-        ;; (gptel--wrap-user-prompt-maybe prompts)
-        prompts))))
+      (let ((buf (current-buffer)))
+        (cond
+         ((derived-mode-p 'org-mode)
+          (require 'gptel-org)
+          ;; Also handles regions in Org mode
+          (gptel-org--create-prompt-buffer prompt-end))
+         ((use-region-p)
+          (let ((rb (region-beginning)) (re (region-end)))
+            (gptel--with-buffer-copy buf rb re
+              (current-buffer))))
+         (t (unless prompt-end (setq prompt-end (point)))
+            (gptel--with-buffer-copy buf (point-min) prompt-end
+              (current-buffer))))))))
 
 (defun gptel--create-prompt (&optional prompt-end)
   "Return a full conversation prompt from the contents of this buffer.
@@ -2748,10 +2805,11 @@ recent exchanges.
 If PROMPT-END (a marker) is provided, end the prompt contents
 there.  This defaults to (point)."
   (with-current-buffer (gptel--create-prompt-buffer prompt-end)
-    (gptel--parse-buffer
-     gptel-backend (and gptel--num-messages-to-send
-                        (* 2 gptel--num-messages-to-send)))
-    (kill-buffer (current-buffer))))
+    (unwind-protect
+        (gptel--parse-buffer
+         gptel-backend (and gptel--num-messages-to-send
+                            (* 2 gptel--num-messages-to-send)))
+      (kill-buffer (current-buffer)))))
 
 (make-obsolete 'gptel--create-prompt 'gptel--create-prompt-buffer
                "0.9.9")

Reply via email to