branch: elpa/gptel
commit cbb49f92d38100e32c3307466964e63f8a395080
Author: Karthik Chikmagalur <[email protected]>
Commit: karthink <[email protected]>

    gptel-context: Add gptel-context--wrap
    
    Also rename `gptel--context-overlay-alist` to
    `gptel-context--overlay-alist`, and apply the context to the
    system message in `gptel-request` correctly.
    
    gptel-contexter.el (gptel-context-preamble,
    gptel-context-postamble): Remove `gptel-context-preamble` and
    `gptel-context-postamble`.
    
    * gptel-contexter.el (gptel--wrap-in-context,
    gptel-context--wrap, gptel-context--overlay-alist,
    gptel-context-injection-destination): Rename
    gptel--wrap-in-context to gptel-context--wrap.
    
    * gptel-contexter.el (gptel-context--wrap,
    gptel-context-injection-destination): Modify `gptel-context--wrap`
    for the new allowed values of `gptel-use-context`.
    
    * gptel-contexter.el (gptel-context--wrap,
    gptel-use-context-in-chat): Remove gptel-use-context-in-chat.
    
    * gptel-transient.el: Remove the menu item for
    gptel-use-context-in-chat.
    
    * gptel-contexter.el (gptel-context-buffer-mode-map): Ensure that
    gptel-context-buffer-mode-map is loaded correctly.
    
    * gptel.el (gptel-context--overlay-alist, gptel--create-prompt,
    gptel-request, gptel--wrap-user-prompt): Move
    gptel-context--overlay-alist here.  Move the check before
    attaching the context to the user message to
    `gptel--create-prompt`.  Simplify the check for attaching the
    context to the system message.  New defgeneric
    `gptel--wrap-user-prompt` for backend-specific context wrapping
    code.
    
    * gptel.el (gptel--create-prompt, gptel-request,
    gptel-use-context): Rename `gptel-context-injection-destination`
    to `gptel-use-context` and simplify its allowed values.  Move the
    variable to gptel.el.
    
    * gptel-openai.el (gptel--wrap-user-prompt): Add method to wrap
    last user prompt with the context string.
    
    * gptel-ollama.el (gptel--wrap-user-prompt): Add method to wrap
    last user prompt with the context string.
    
    * gptel-kagi.el (gptel--wrap-user-prompt): Add method to wrap last
    user prompt with the context string.
    
    * gptel-gemini.el (gptel--wrap-user-prompt): Add method to wrap
    last user prompt with the context string.
    
    * gptel-anthropic.el (gptel--wrap-user-prompt): Add method to wrap
    last user prompt with the context string.
    
    * gptel-transient.el (gptel--infix-context-destination,
    gptel--infix-use-context, gptel-menu): Adjust menu for new
    variable.
---
 gptel-anthropic.el |   4 ++
 gptel-contexter.el | 125 ++++++++++++++++++++++-------------------------------
 gptel-gemini.el    |   5 +++
 gptel-kagi.el      |   9 ++++
 gptel-ollama.el    |   4 ++
 gptel-openai.el    |  12 ++---
 gptel-transient.el |  33 ++++----------
 gptel.el           |  96 +++++++++++++++++++++++++++++-----------
 8 files changed, 156 insertions(+), 132 deletions(-)

diff --git a/gptel-anthropic.el b/gptel-anthropic.el
index 27767dd065..3768570ccf 100644
--- a/gptel-anthropic.el
+++ b/gptel-anthropic.el
@@ -100,6 +100,10 @@
             prompts))
     prompts))
 
+(cl-defmethod gptel--wrap-user-prompt ((_backend gptel-anthropic) prompts)
+  "Wrap the last user prompt in PROMPTS with the context string."
+  (cl-callf gptel-context--wrap (plist-get (car (last prompts)) :content)))
+
 ;;;###autoload
 (cl-defun gptel-make-anthropic
     (name &key curl-args stream key
diff --git a/gptel-contexter.el b/gptel-contexter.el
index ec2bd30b94..15b90c8f0e 100644
--- a/gptel-contexter.el
+++ b/gptel-contexter.el
@@ -36,35 +36,26 @@
   :group 'gptel
   :type 'symbol)
 
-(defcustom gptel-use-context-in-chat nil
-  "Determines if context should be injected when using the dedicated chat 
buffer.
-If non-nil, then the model will use the context in the chat buffer."
-  :group 'gptel
-  :type 'symbol)
+(defcustom gptel-context-string-function #'gptel-context-string--default
+  "Function to format the context string sent with the gptel request.
 
-(defcustom gptel-context-injection-destination :nowhere
-  "Where to inject the context.  Currently supported options are:
+This function receives one argument, an alist of context overlays
+organized by buffer.  It should return a string containing the
+formatted context and any additional comments you wish to
+include.
 
-    :nowhere               - Do not use the context at all.
-    :before-system-message - Inject the context right before the system 
message.
-    :after-system-message  - Inject the context right after the system emssage.
-    :before-user-prompt    - Inject the context right before the user prompt.
-    :after-user-prompt     - Inject the context right after the user prompt."
-  :group 'gptel
-  :type 'symbol)
+The alist of context overlays is structured as follows:
 
-(defcustom gptel-context-preamble "Request context:"
-  "A string to be prepended to the context."
-  :group 'gptel
-  :type 'string)
+((buffer1 . (overlay1 overlay2)
+ (buffer2 . (overlay3 overlay4 overlay5))))
 
-(defcustom gptel-context-postamble ""
-  "A string to be appended to the context."
-  :group 'gptel
-  :type 'string)
+Each overlay covers a buffer region containing the
+context chunk.  This is accessible as, for example:
 
-(defvar gptel--context-overlay-alist nil
-  "Alist of buffers and their corresponding context chunks.")
+(with-current-buffer buffer1
+  (buffer-substring (overlay-start overlay1)
+                    (overlay-end   overlay1)))"
+  :type 'function)
 
 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;; ------------------------------ FUNCTIONS ------------------------------- 
;;;
@@ -123,38 +114,22 @@ If there is a context under point, it is removed when 
called without a prefix."
     (overlay-put overlay 'face gptel-context-highlight-face)
     (overlay-put overlay 'gptel-context t)
     (push overlay (alist-get (current-buffer)
-                             gptel--context-overlay-alist))
+                             gptel-context--overlay-alist))
     overlay))
 
-(defun gptel--wrap-in-context (message)
+;;;###autoload
+(defun gptel-context--wrap (message)
   "Wrap MESSAGE with context.
+
 The message is usually either a system message or user prompt."
   ;; Append context before/after system message.
-  (if (and (bound-and-true-p gptel-mode)
-           (not gptel-use-context-in-chat))
-      ;; If we are in the dedicated chat buffer, we would like to consult
-      ;; `gptel-use-context-in-chat' to see if context should be used inside.
-      message
-    (let ((context (gptel-context-string)))
-      (if (> (length context) 0)
-          (if (memq gptel-context-injection-destination
-                    '(:before-system-message
-                      :before-user-prompt))
-              (concat gptel-context-preamble
-                      (when (not (zerop (length gptel-context-preamble))) 
"\n\n")
-                      context
-                      "\n\n"
-                      gptel-context-postamble
-                      (when (not (zerop (length gptel-context-postamble))) 
"\n\n")
-                      message)
-            (concat message
-                    "\n\n"
-                    gptel-context-preamble
-                    (when (not (zerop (length gptel-context-preamble))) "\n\n")
-                    context
-                    (when (not (zerop (length gptel-context-postamble))) 
"\n\n")
-                    gptel-context-postamble))
-        message))))
+  (let ((context (gptel-context-string)))
+    (if (> (length context) 0)
+        (pcase-exhaustive gptel-use-context
+          ('system (concat message "\n\n" context))
+          ('user   (concat context "\n\n" message))
+          ('nil    message))
+      message)))
 
 (cl-defun gptel--add-region-as-context (buffer region-beginning region-end)
   "Add region delimited by REGION-BEGINNING, REGION-END in BUFFER as context."
@@ -203,14 +178,14 @@ If selection is active, removes all contexts within 
selection."
   "Get the list of all active context overlays."
   ;; Get only the non-degenerate overlays, collect them, and update the 
overlays variable.
   (let ((overlay-alist
-         (cl-loop for (buf . ovs) in gptel--context-overlay-alist
+         (cl-loop for (buf . ovs) in gptel-context--overlay-alist
                   when (buffer-live-p buf)
                   for updated-ovs = (cl-loop for ov in ovs
                                              when (overlay-start ov)
                                              collect ov)
                   when updated-ovs
                   collect (cons buf updated-ovs))))
-    (setq gptel--context-overlay-alist overlay-alist)))
+    (setq gptel-context--overlay-alist overlay-alist)))
 
 ;;;###autoload
 (defun gptel-contexts-in-buffer (buffer)
@@ -277,7 +252,7 @@ representthe regions' boundaries within BUFFER."
     (let ((is-top-snippet t)
           (previous-line 1)
           prog-lang-tag
-          (contexts (alist-get buffer gptel--context-overlay-alist)))
+          (contexts (alist-get buffer gptel-context--overlay-alist)))
       (setq prog-lang-tag (gptel-major-mode-md-prog-lang
                              (buffer-local-value 'major-mode buffer)))
       (insert (format "In buffer `%s`:" (buffer-name buffer)))
@@ -313,18 +288,29 @@ representthe regions' boundaries within BUFFER."
         (insert "\n..."))
       (insert "\n```")))
 
+(defun gptel-context-string--default (context-alist)
+  (with-temp-buffer
+    (insert "Request context:\n\n")
+    (cl-loop for (buf . ovs) in context-alist
+             do (gptel-buffer-insert-context-string buf)
+             (insert "\n\n")
+             finally return (buffer-string))))
+
 ;;;###autoload
 (defun gptel-context-string ()
-  "Return the context string of all aggregated contexts.
-If PROPERTIZE is non-nil, keep the text properties."
-  (without-restriction
-    (with-temp-buffer
-      (cl-loop for (buf . ovs) in (gptel-contexts)
-               do (gptel-buffer-insert-context-string buf)
-               (insert "\n\n")
-               finally return (buffer-string)))))
+  "Return a string containing the aggregated gptel context."
+  (funcall gptel-context-string-function
+           (gptel-contexts)))
 
 ;;; Major mode for context inspection buffers
+(defvar-keymap gptel-context-buffer-mode-map
+  "C-c C-c" #'gptel-context-confirm
+  "C-c C-k" #'gptel-context-quit
+  "RET"     #'gptel-context-visit
+  "n"       #'gptel-context-next
+  "p"       #'gptel-context-previous
+  "d"       #'gptel-context-flag-deletion)
+
 (define-derived-mode gptel-context-buffer-mode special-mode "gptel-context"
   "Major-mode for inspecting context used by gptel."
   :group 'gptel
@@ -351,7 +337,7 @@ If PROPERTIZE is non-nil, keep the text properties."
              (propertize "C-c C-k" 'face 'help-key-binding)
              " to abort."))
       (save-excursion
-        (let ((contexts gptel--context-overlay-alist))
+        (let ((contexts gptel-context--overlay-alist))
           (if (length> contexts 0)
               (let (beg ov l1 l2)
                 (pcase-dolist (`(,buf . ,ovs) contexts)
@@ -366,10 +352,10 @@ If PROPERTIZE is non-nil, keep the text properties."
                     (setq beg (point))
                     (insert-buffer-substring
                      buf (overlay-start source-ov) (overlay-end source-ov))
-                    (insert "\n")
                     (setq ov (make-overlay beg (point)))
                     (overlay-put ov 'gptel-context source-ov)
-                    (overlay-put ov 'gptel-overlay t)))
+                    (overlay-put ov 'gptel-overlay t)
+                    (insert "\n")))
                 (goto-char (point-min)))
             (insert "There are no active contexts in any buffer.")))))
     (display-buffer (current-buffer)
@@ -471,14 +457,5 @@ If non-nil, indicates backward movement.")
                           (overlays-in (point-min) (point-max)))))
   (gptel-context-quit))
 
-(defvar-keymap gptel-context-buffer-mode-map
-  :parent special-mode-map
-  "C-c C-c" #'gptel-context-confirm
-  "C-c C-k" #'gptel-context-quit
-  "RET"     #'gptel-context-visit
-  "n"       #'gptel-context-next
-  "p"       #'gptel-context-previous
-  "d"       #'gptel-context-flag-deletion)
-
 (provide 'gptel-contexter)
 ;;; gptel-contexter.el ends here.
diff --git a/gptel-gemini.el b/gptel-gemini.el
index 58d8404a37..75b319aa3b 100644
--- a/gptel-gemini.el
+++ b/gptel-gemini.el
@@ -115,6 +115,11 @@
                       (plist-get :text)))
     prompts))
 
+(cl-defmethod gptel--wrap-user-prompt ((_backend gptel-gemini) prompts)
+  "Wrap the last user prompt in PROMPTS with the context string."
+  (cl-callf gptel-context--wrap
+      (plist-get (plist-get (car (last prompts)) :parts) :text)))
+
 ;;;###autoload
 (cl-defun gptel-make-gemini
     (name &key curl-args header key (stream nil)
diff --git a/gptel-kagi.el b/gptel-kagi.el
index 412256c350..796fb7a2e9 100644
--- a/gptel-kagi.el
+++ b/gptel-kagi.el
@@ -121,6 +121,15 @@
                      ""))))
           prompts)))))
 
+(cl-defmethod gptel--wrap-user-prompt ((_backend gptel-kagi) prompts)
+  (cond
+   ((plist-get prompts :url)
+    (message "Ignoring gptel context for URL summary request."))
+   ((plist-get prompts :query)
+    (cl-callf gptel-context--wrap (plist-get prompts :query)))
+   ((plist-get prompts :text)
+    (cl-callf gptel-context--wrap (plist-get prompts :text)))))
+
 ;;;###autoload
 (cl-defun gptel-make-kagi
     (name &key curl-args stream key
diff --git a/gptel-ollama.el b/gptel-ollama.el
index db1150cf16..86147bf30a 100644
--- a/gptel-ollama.el
+++ b/gptel-ollama.el
@@ -121,6 +121,10 @@ Intended for internal use only.")
                 :content gptel--system-message)
           prompts)))
 
+(cl-defmethod gptel--wrap-user-prompt ((_backend gptel-ollama) prompts)
+  "Wrap the last user prompt in PROMPTS with the context string."
+  (cl-callf gptel-context--wrap (plist-get (car (last prompts)) :content)))
+
 ;;;###autoload
 (cl-defun gptel-make-ollama
     (name &key curl-args header key models stream
diff --git a/gptel-openai.el b/gptel-openai.el
index b89d3e747a..7250ad5455 100644
--- a/gptel-openai.el
+++ b/gptel-openai.el
@@ -152,18 +152,14 @@ with differing settings.")
                            (regexp-quote (gptel-response-prefix-string)))))
             prompts)
       (and max-entries (cl-decf max-entries)))
-    (when (and (memq gptel-context-injection-destination '(:before-user-prompt 
:after-user-prompt))
-               (> (length prompts) 0))
-      ;; Add context to final user prompt.
-      (let* ((last-prompt (last prompts))
-             (last-plist (car last-prompt)))
-        (setf (car last-prompt) (plist-put last-plist :content
-                                           (gptel--wrap-in-context (plist-get 
(car last-prompt)
-                                                                              
:content))))))
     (cons (list :role "system"
                 :content gptel--system-message)
           prompts)))
 
+(cl-defmethod gptel--wrap-user-prompt ((_backend gptel-openai) prompts)
+  "Wrap the last user prompt in PROMPTS with the context string."
+  (cl-callf gptel-context--wrap (plist-get (car (last prompts)) :content)))
+
 ;;;###autoload
 (cl-defun gptel-make-openai
     (name &key curl-args models stream key
diff --git a/gptel-transient.el b/gptel-transient.el
index d082fd0b78..d0ae66a943 100644
--- a/gptel-transient.el
+++ b/gptel-transient.el
@@ -295,8 +295,7 @@ Also format its value in the Transient menu."
     (gptel--infix-add-directive)]]
   [["Context" :if (lambda () gptel-expert-commands)
     (gptel--suffix-context-buffer)
-    (gptel--infix-context-destination)
-    (gptel--infix-use-context-in-chat :if (lambda () gptel-mode))]]
+    (gptel--infix-use-context)]]
   [["Model Parameters"
     :pad-keys t
     (gptel--infix-variable-scope)
@@ -479,38 +478,24 @@ Customize `gptel-directives' for task-specific prompts."
 
 ;; ** Infixes for context aggregation
 
-(transient-define-infix gptel--infix-context-destination ()
+(transient-define-infix gptel--infix-use-context ()
   "Describe target destination for context injection."
   :description "Include context"
   :class 'gptel-lisp-variable
-  :variable 'gptel-context-injection-destination
+  :variable 'gptel-use-context
   :set-value #'gptel--set-with-scope
   :display-nil "No"
-  :display-map '((:nowhere               . "No")
-                 (:before-system-message . "before system message")
-                 (:after-system-message  . "after system message")
-                 (:before-user-prompt    . "before user prompt")
-                 (:after-user-prompt     . "after user prompt"))
+  :display-map '((nil    . "No")
+                 (system . "with system message")
+                 (user   . "with user prompt"))
   :key "-xd"
   :reader (lambda (prompt &rest _)
-            (let* ((choices '(("No"                    . :nowhere)
-                              ("before system message" . 
:before-system-message)
-                              ("after system message"  . :after-system-message)
-                              ("before user prompt"    . :before-user-prompt)
-                              ("after user prompt"     . :after-user-prompt)))
+            (let* ((choices '(("No"                  . nil)
+                              ("with system message" . system)
+                              ("with user prompt"    . user)))
                    (destination (completing-read prompt choices nil t)))
               (cdr (assoc destination choices)))))
 
-(transient-define-infix gptel--infix-use-context-in-chat ()
-  "Determine if context should be passed to the LLM during the chat."
-  :description "Use in chat"
-  :class 'gptel--switches
-  :variable 'gptel-use-context-in-chat
-  :set-value #'gptel--set-with-scope
-  :display-if-true "Yes"
-  :display-if-false "No"
-  :key "-xc")
-
 ;; ** Infixes for model parameters
 
 (transient-define-infix gptel--infix-variable-scope ()
diff --git a/gptel.el b/gptel.el
index 335d9e5e99..c612e12a3a 100644
--- a/gptel.el
+++ b/gptel.el
@@ -515,8 +515,34 @@ with `gptel-mode' enabled), where user prompts and 
responses are
 always handled separately."
   :type 'boolean)
 
+(defcustom gptel-use-context 'system
+  "Where in the request to inject gptel's additional context.
+
+gptel always includes the active region or the buffer up to the
+cursor in the request to the LLM.  Additionally, you can add
+other buffers or their regions to the context with
+`gptel-add-context', or from gptel's menu.  This data will be
+sent with every request.
+
+This option controls whether and where this additional context is
+included in the request.
+
+Currently supported options are:
+
+    nil     - Do not use the context.
+    system  - Include the context with the system message.
+    user    - Include the context with the user prompt."
+  :group 'gptel
+  :type '(choice
+          (const :tag "Don't include context" nil)
+          (const :tag "With system message" system)
+          (const :tag "With user prompt" user)))
+
 (defvar-local gptel--old-header-line nil)
 
+(defvar gptel-context--overlay-alist nil
+  "Alist of buffers and their corresponding context chunks.")
+
 
 ;; Utility functions
 
@@ -795,6 +821,8 @@ file."
         (message (propertize msg 'face face))))
     (force-mode-line-update)))
 
+(declare-function gptel-context--wrap "gptel-contexter")
+
 
 ;; Send queries, handle responses
 (cl-defun gptel-request
@@ -890,7 +918,12 @@ query data as usual, but do not send the request.
 
 Model parameters can be let-bound around calls to this function."
   (declare (indent 1))
-  (let* ((gptel--system-message system)
+  (let* ((gptel--system-message
+          ;Add context chunks to system message if required
+          (if (and gptel-context--overlay-alist
+                   (eq gptel-use-context 'system))
+              (gptel-context--wrap system)
+            system))
          (gptel-stream stream)
          (start-marker
           (cond
@@ -904,21 +937,11 @@ Model parameters can be let-bound around calls to this 
function."
          (full-prompt
           (cond
            ((null prompt)
-            (let ((gptel--system-message (if (memq 
gptel-context-injection-destination
-                                                   '(:before-system-message
-                                                     :after-system-message))
-                                             (gptel--wrap-in-context system)
-                                           system)))
-              (gptel--create-prompt start-marker)))
+            (gptel--create-prompt start-marker))
            ((stringp prompt)
             ;; FIXME Dear reader, welcome to Jank City:
             (with-temp-buffer
-              (let ((gptel--system-message (if (memq 
gptel-context-injection-destination
-                                                     '(:before-system-message
-                                                       :after-system-message))
-                                               (gptel--wrap-in-context system)
-                                             system))
-                    (gptel-model (buffer-local-value 'gptel-model buffer))
+              (let ((gptel-model (buffer-local-value 'gptel-model buffer))
                     (gptel-backend (buffer-local-value 'gptel-backend buffer)))
                 (insert prompt)
                 (gptel--create-prompt))))
@@ -1048,23 +1071,34 @@ recent exchanges.
 If the region is active limit the prompt to the region contents
 instead.
 
+If `gptel-context--overlay-alist' is non-nil and the additional
+context needs to be included with the user prompt, add it.
+
 If PROMPT-END (a marker) is provided, end the prompt contents
 there."
   (save-excursion
     (save-restriction
-      (let ((max-entries (and gptel--num-messages-to-send
-                              (* 2 gptel--num-messages-to-send))))
-        (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))
-         ((derived-mode-p 'org-mode)
-          (require 'gptel-org)
-          (gptel-org--create-prompt (or prompt-end (point-max))))
-         (t (goto-char (or prompt-end (point-max)))
-            (gptel--parse-buffer gptel-backend max-entries)))))))
+      (let* ((max-entries (and gptel--num-messages-to-send
+                               (* 2 gptel--num-messages-to-send)))
+             (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))
+               ((derived-mode-p 'org-mode)
+                (require 'gptel-org)
+                (gptel-org--create-prompt (or prompt-end (point-max))))
+               (t (goto-char (or prompt-end (point-max)))
+                  (gptel--parse-buffer gptel-backend max-entries)))))
+        ;; Inject context chunks into the last user prompt if required
+        ;; NOTE: prompts is modified in place
+        (when (and gptel-context--overlay-alist
+                   (eq gptel-use-context 'user)
+                   (> (length prompts) 0))
+          (gptel--wrap-user-prompt gptel-backend prompts))
+        prompts))))
 
 (cl-defgeneric gptel--parse-buffer (backend max-entries)
   "Parse current buffer backwards from point and return a list of prompts.
@@ -1074,6 +1108,16 @@ BACKEND is the LLM backend in use.
 MAX-ENTRIES is the number of queries/responses to include for
 contexbt.")
 
+(cl-defgeneric gptel--wrap-user-prompt (backend prompts)
+  "Wrap the last prompt in PROMPTS with gptel's context.
+
+PROMPTS is a structure as returned by `gptel--parse-buffer'.
+Typically this is a list of plists."
+  (display-warning
+   '(gptel context)
+   (format "Context support not implemented for backend %s, ignoring context"
+           (gptel-backend-name backend))))
+
 (cl-defgeneric gptel--request-data (backend prompts)
   "Generate a plist of all data for an LLM query.
 

Reply via email to