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

    gptel: Switch to prompt-buffer centric parsing
    
    Previously, gptel's prompt construction worked as follows:
    
    1. If the prompt is nil or a string, copy the text to be sent to a
    temporary buffer, and use it to create the messages array.  If the
    prompt is a list of strings, create the messages array from it
    directly (with no intermediate buffer).
    
    2. Feed this array along with the specified or environmental
    parameters like the backend, model, system message and other
    parameters to `gptel--request-data', which produces the full
    payload.
    
    gptel is now inverting this flow:
    
    1. Whatever the prompt, copy it to a temporary buffer (the "prompt
    buffer") using `gptel--create-prompt-buffer' or directly with
    `gptel--with-buffer-copy'.  Use the new function
    `gptel--parse-list-and-insert' to insert a list-of-strings prompt
    into the prompt buffer.
    
    2. Also copy the environment to this temporary buffer as
    buffer-local variables.
    
    3. Apply any augmentations/transformations/context injections to
    this prompt buffer.  This can involve changing the buffer contents
    and/or changing the environment, i.e. buffer-local variables such
    as the model, backend or system prompt.  The ability to do this
    is the main reason for this inversion.  One example of this is
    the upcoming just-in-time preset feature.
    
    NOTE: transformations are not yet implemented.
    
    4. Construct the full payload using `gptel--request-data' inside
    the prompt buffer.
    
    * gptel.el:
    (gptel--parse-directive): Add FIXME for later
    (gptel--parse-list-and-insert): Inserts prompts, a list of
    strings, into the current buffer with user/llm/tool roles applied.
    (gptel-request, gptel--realize-query): Split the work of
    `gptel-request' into two functions.  `gptel-request' now handles
    only the creation of the prompt buffer.  `gptel--realize-query' runs
    in the prompt buffer and does the actual payload realization.
    (gptel--create-prompt): Remove function
    (gptel--create-prompt-buffer): Renamed from
    `gptel--create-prompt', now returns the prompt buffer instead of a
    messages array.  No longer handles context injection.
    
    * gptel-org.el (gptel-org--create-prompt): Remove function
    (gptel-org--create-prompt-buffer): Renamed from
    `gptel-org--create-prompt', now returns a buffer instead of a
    messages array.
---
 gptel-org.el |  30 ++++-----
 gptel.el     | 213 +++++++++++++++++++++++++++++++++++++++--------------------
 2 files changed, 154 insertions(+), 89 deletions(-)

diff --git a/gptel-org.el b/gptel-org.el
index 87eecec95b..3d32024f09 100644
--- a/gptel-org.el
+++ b/gptel-org.el
@@ -189,26 +189,22 @@ current heading and the cursor position."
                                  50))))))
   (when (stringp topic) (org-set-property "GPTEL_TOPIC" topic)))
 
-;; NOTE: This can be converted to a cl-defmethod for `gptel--parse-buffer'
-;; (conceptually cleaner), but will cause load-order issues in gptel.el and
-;; might be harder to debug.
-(defun gptel-org--create-prompt (&optional prompt-end)
-  "Return a full conversation prompt from the contents of this Org buffer.
-
-If `gptel--num-messages-to-send' is set, limit to that many
-recent exchanges.
-
-The prompt is constructed from the contents of the buffer up to
-point, or PROMPT-END if provided.  Its contents depend on the
-value of `gptel-org-branching-context', which see."
+;; NOTE: This can be converted to a cl-defmethod for
+;; `gptel--create-prompt-buffer' (conceptually cleaner), but will cause
+;; load-order issues in gptel.el and might be harder to debug.
+(defun gptel-org--create-prompt-buffer (&optional prompt-end)
+  "Return a buffer with the conversation prompt to be sent.
+
+If the region is active limit the prompt text to the region contents.
+Otherwise the prompt text is constructed from the contents of the
+current buffer up to point, or PROMPT-END if provided.  Its contents
+depend on the value of `gptel-org-branching-context', which see."
   (when (use-region-p)
     (narrow-to-region (region-beginning) (region-end)))
   (if prompt-end
       (goto-char prompt-end)
     (setq prompt-end (point)))
-  (let ((max-entries (and gptel--num-messages-to-send
-                          (* 2 gptel--num-messages-to-send)))
-        (topic-start (gptel-org--get-topic-start)))
+  (let ((topic-start (gptel-org--get-topic-start)))
     (when topic-start
       ;; narrow to GPTEL_TOPIC property scope
       (narrow-to-region topic-start prompt-end))
@@ -253,7 +249,7 @@ value of `gptel-org-branching-context', which see."
               (gptel-org--strip-block-headers)
               (when gptel-org-ignore-elements (gptel-org--strip-elements))
               (save-excursion (run-hooks 'gptel-prompt-filter-hook))
-              (gptel--parse-buffer gptel-backend max-entries))))
+              (current-buffer))))
       ;; Create prompt the usual way
       (let ((org-buf (current-buffer))
             (beg (point-min)))
@@ -262,7 +258,7 @@ value of `gptel-org-branching-context', which see."
           (gptel-org--strip-block-headers)
           (when gptel-org-ignore-elements (gptel-org--strip-elements))
           (save-excursion (run-hooks 'gptel-prompt-filter-hook))
-          (gptel--parse-buffer gptel-backend max-entries))))))
+          (current-buffer))))))
 
 (defun gptel-org--strip-elements ()
   "Remove all elements in `gptel-org-ignore-elements' from the
diff --git a/gptel.el b/gptel.el
index 7ec4bd27b1..ac3c044f9c 100644
--- a/gptel.el
+++ b/gptel.el
@@ -193,7 +193,7 @@
 (declare-function hl-line-highlight "hl-line")
 
 (declare-function org-escape-code-in-string "org-src")
-(declare-function gptel-org--create-prompt "gptel-org")
+(declare-function gptel-org--create-prompt-buffer "gptel-org")
 (declare-function gptel-org-set-topic "gptel-org")
 (declare-function gptel-org--save-state "gptel-org")
 (declare-function gptel-org--restore-state "gptel-org")
@@ -1264,6 +1264,7 @@ returned as a list of strings."
          (function (gptel--parse-directive (funcall directive) raw))
          (cons     (if raw directive
                      (cons (car directive)
+                           ;; FIXME(augment) do this elsewhere
                            (gptel--parse-list
                             gptel-backend (cdr directive))))))))
 
@@ -2389,32 +2390,7 @@ be used to rerun or continue the request at a later 
time."
   (declare (indent 1))
   ;; TODO Remove this check in version 1.0
   (gptel--sanitize-model)
-  (let* ((directive (gptel--parse-directive system))
-         ;; DIRECTIVE contains both the system message and the template prompts
-         (gptel--system-message
-          ;; Add context chunks to system message if required
-          (unless (gptel--model-capable-p 'nosystem)
-            (if (and gptel-context--alist
-                     (eq gptel-use-context 'system))
-                (gptel-context--wrap (car directive))
-              (car directive))))
-         ;; TODO(tool) Limit tool use to capable models after documenting 
:capabilities
-         ;; (gptel-use-tools (and (gptel--model-capable-p 'tool-use) 
gptel-use-tools))
-         (stream (and stream gptel-use-curl
-                      ;; HACK(tool): no stream if Ollama + tools.  Need to 
find a better way
-                      (not (and (eq (type-of gptel-backend) 'gptel-ollama)
-                                gptel-tools gptel-use-tools))
-                      ;; Check model-specific request-params for streaming 
preference
-                      (let* ((model-params (gptel--model-request-params 
gptel-model))
-                             (stream-spec (plist-get model-params :stream)))
-                        ;; If not present, there is no model-specific 
preference
-                        (or (not (memq :stream model-params))
-                            ;; If present, it must not be :json-false or nil
-                            (and stream-spec (not (eq stream-spec 
:json-false)))))
-                      ;; Check backend-specific streaming settings
-                      (gptel-backend-stream gptel-backend)))
-         (gptel-stream stream)
-         (start-marker
+  (let* ((start-marker
           (cond
            ((null position)
             (if (use-region-p)
@@ -2423,38 +2399,89 @@ be used to rerun or continue the request at a later 
time."
            ((markerp position) position)
            ((integerp position)
             (set-marker (make-marker) position buffer))))
-         (full-prompt
-          (nconc
-           (cdr directive)           ;prompt constructed from 
directive/template
-           (cond                     ;prompt from buffer or explicitly supplied
-            ((null prompt)
-             (gptel--create-prompt start-marker))
-            ((stringp 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)
+         (prompt-buffer
+          (cond                       ;prompt from buffer or explicitly 
supplied
+           ((null prompt)
+            (gptel--create-prompt-buffer start-marker))
+           ((stringp prompt)
+            (gptel--with-buffer-copy buffer nil nil
+              (insert prompt)
+              (save-excursion (run-hooks 'gptel-prompt-filter-hook))
+              (current-buffer)))
+           ((consp prompt)
+            ;; (gptel--parse-list gptel-backend prompt)
+            (gptel--with-buffer-copy buffer nil nil
+              (gptel--parse-list-and-insert prompt)
+              (save-excursion (run-hooks 'gptel-prompt-filter-hook))
+              (current-buffer)))))
+         (info (list :data prompt-buffer
                      :buffer buffer
-                     :position start-marker
-                     :backend gptel-backend)))
-    (when stream (plist-put info :stream t))
+                     :position start-marker)))
+    (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!
     (when callback (plist-put info :callback callback))
     (when context (plist-put info :context context))
     (when in-place (plist-put info :in-place in-place))
-    (when gptel-include-reasoning       ;Required for next-request-only scope
-      (plist-put info :include-reasoning gptel-include-reasoning))
-    (when (and gptel-use-tools gptel-tools)
-      (plist-put info :tools gptel-tools))
     ;; Add info to state machine context
+    (when dry-run (plist-put info :dry-run dry-run))
     (setf (gptel-fsm-info fsm) info))
-  (unless dry-run (gptel--fsm-transition fsm)) ;INIT -> WAIT
-  fsm)
+  ;; (gptel--fsm-transition fsm)           ;INIT -> AUGMENT
+  (gptel--realize-query fsm))
+
+(defun gptel--realize-query (fsm)
+  "Realize the query payload for FSM from its prompt buffer.
+
+Initiate the request when done."
+  (let ((info (gptel-fsm-info fsm)))
+    (with-current-buffer (plist-get info :data)
+      (let* ((directive (gptel--parse-directive gptel--system-message 'raw))
+             ;; DIRECTIVE contains both the system message and the template 
prompts
+             (gptel--system-message
+              ;; Add context chunks to system message if required
+              (unless (gptel--model-capable-p 'nosystem)
+                (if (and gptel-context--alist
+                         (eq gptel-use-context 'system))
+                    (gptel-context--wrap (car directive))
+                  (car directive))))
+             ;; TODO(tool) Limit tool use to capable models after documenting 
:capabilities
+             ;; (gptel-use-tools (and (gptel--model-capable-p 'tool-use) 
gptel-use-tools))
+             (stream (and (plist-get info :stream) gptel-use-curl gptel-stream
+                          ;; HACK(tool): no stream if Ollama + tools.  Need to 
find a better way
+                          (not (and (eq (type-of gptel-backend) 'gptel-ollama)
+                                    gptel-tools gptel-use-tools))
+                          ;; Check model-specific request-params for streaming 
preference
+                          (let* ((model-params (gptel--model-request-params 
gptel-model))
+                                 (stream-spec (plist-get model-params 
:stream)))
+                            ;; If not present, there is no model-specific 
preference
+                            (or (not (memq :stream model-params))
+                                ;; If present, it must not be :json-false or 
nil
+                                (and stream-spec (not (eq stream-spec 
:json-false)))))
+                          ;; Check backend-specific streaming settings
+                          (gptel-backend-stream gptel-backend)))
+             (gptel-stream stream)
+             (full-prompt))
+        (when (cdr directive)       ; prompt constructed from 
directive/template
+          (save-excursion (goto-char (point-min))
+                          (gptel--parse-list-and-insert (cdr directive))))
+        (goto-char (point-max))
+        (setq full-prompt (gptel--parse-buffer ;prompt from buffer or 
explicitly supplied
+                           gptel-backend (and gptel--num-messages-to-send
+                                              (* 2 
gptel--num-messages-to-send))))
+        (gptel--wrap-user-prompt-maybe full-prompt)
+        (unless stream (cl-remf info :stream))
+        (plist-put info :backend gptel-backend)
+        (when gptel-include-reasoning   ;Required for next-request-only scope
+          (plist-put info :include-reasoning gptel-include-reasoning))
+        (when (and gptel-use-tools gptel-tools)
+          (plist-put info :tools gptel-tools))
+        (plist-put info :data
+                   (gptel--request-data gptel-backend full-prompt))
+        (run-hooks 'gptel-augment-post-modify-hook))
+      (kill-buffer (current-buffer)))
+    ;; INIT -> WAIT
+    (unless (plist-get info :dry-run) (gptel--fsm-transition fsm))
+    fsm))
 
 (defun gptel-abort (buf)
   "Stop any active gptel process associated with buffer BUF.
@@ -2671,42 +2698,50 @@ This delegates to backend-specific wrap functions."
                  (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.
-
-If `gptel--num-messages-to-send' is set, limit to that many
-recent exchanges.
-
-If the region is active limit the prompt to the region contents
-instead.
-
-If `gptel-context--alist' is non-nil and the additional
-context needs to be included with the user prompt, add it.
+(defun gptel--create-prompt-buffer (&optional prompt-end)
+  "Return a buffer with the conversation prompt to be sent.
 
-If PROMPT-END (a marker) is provided, end the prompt contents
-there.  This defaults to (point)."
+If the region is active limit the prompt text to the region contents.
+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* ((max-entries (and gptel--num-messages-to-send
-                               (* 2 gptel--num-messages-to-send)))
-             (buf (current-buffer))
+      (let* ((buf (current-buffer))
              (prompts
               (cond
                ((derived-mode-p 'org-mode)
                 (require 'gptel-org)
                 ;; Also handles regions in Org mode
-                (gptel-org--create-prompt prompt-end))
+                (gptel-org--create-prompt-buffer prompt-end))
                ((use-region-p)
                 (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))))
+                    (current-buffer))))
                (t (unless prompt-end (setq prompt-end (point)))
                   (gptel--with-buffer-copy buf (point-min) prompt-end
                     (save-excursion (run-hooks 'gptel-prompt-filter-hook))
-                    (gptel--parse-buffer gptel-backend max-entries))))))
+                    (current-buffer))))))
         ;; NOTE: prompts is modified in place here
-        (gptel--wrap-user-prompt-maybe prompts)))))
+        ;; (gptel--wrap-user-prompt-maybe prompts)
+        prompts))))
+
+(defun gptel--create-prompt (&optional prompt-end)
+  "Return a full conversation prompt from the contents of this buffer.
+
+If `gptel--num-messages-to-send' is set, limit to that many
+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))))
+
+(make-obsolete 'gptel--create-prompt 'gptel--create-prompt-buffer
+               "0.9.9")
 
 (cl-defgeneric gptel--parse-buffer (backend max-entries)
   "Parse current buffer backwards from point and return a list of prompts.
@@ -2716,6 +2751,40 @@ BACKEND is the LLM backend in use.
 MAX-ENTRIES is the number of queries/responses to include for
 contexbt.")
 
+(defun gptel--parse-list-and-insert (prompts)
+  "Insert PROMPTS, a list of messages into the current buffer.
+
+Propertize the insertions in a format gptel can parse into a
+conversation.
+
+PROMPTS is typically the input to `gptel-request', either a list of strings
+representing a conversation with alternate prompt/response turns, or a list of
+lists with explicit roles (prompt/response/tool).  See the documentation of
+`gptel-request' for the latter."
+  (if (stringp (car prompts))         ; Simple format, list of strings
+      (cl-loop for text in prompts
+               for response = nil then (not response)
+               when text
+               if response
+               do (insert gptel-response-separator
+                          (propertize text 'gptel 'response)
+                          gptel-response-separator)
+               else do (insert text))
+    (dolist (entry prompts)             ; Advanced format, list of lists
+      (pcase entry
+        (`(prompt . ,msg) (insert (or (car-safe msg) msg)))
+        (`(response . ,msg)
+         (insert gptel-response-separator
+                 (propertize (or (car-safe msg) msg) 'gptel 'response)))
+        (`(tool . ,call)
+         (insert gptel-response-separator
+                 (propertize
+                  (concat
+                   "(:name " (plist-get call :name) " :args "
+                   (prin1-to-string (plist-get call :args)) ")\n\n"
+                   (plist-get call :result))
+                  'gptel `(tool . ,(plist-get call :id)))))))))
+
 (cl-defgeneric gptel--parse-list (backend prompt-list)
   "Parse PROMPT-LIST and return a list of prompts suitable for
 BACKEND.

Reply via email to