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.