Skip to content

Commit a19e04b

Browse files
committed
gptel: Turn context addition into a transform step
Previously, adding context (from gptel-context) to the query payload was a bespoke step, and tied into `gptel-request'. It was carried out by mutating the messages array after buffer parsing. Now that the `gptel-request' pipeline has been inverted, context addition is carried out in the prompt construction buffer, and is handled as a regular transform function in `gptel-prompt-transform-functions'. As a result, the context addition step can now be asynchronous, since the user might want to truncate or augment it depending on its size and content. By default, context addition works by inserting into the prompt construction buffer, or by modifying the system message in the buffer. The only exception to this is handling media files in the context -- there is no universal buffer-oriented method for doing this, so `gptel--wrap-user-prompt' is still used for attaching media from `gptel-context--alist'. TODO: This will be fixed in the future. This deprecates some aspects of the gptel-context API, including `gptel-context-wrap-function'. * gptel-context.el (gptel-context-wrap-function): Obsolete. This is no longer used. Customize `gptel-context-string-function' instead or advise `gptel-context--wrap-in-buffer' if that's insufficient. (gptel-context--wrap): This function serves the same purpose as before, but now works by mutating the prompt construction buffer. It takes a callback and can be asynchronous. (gptel-context--wrap-in-buffer): New function to add the context string to the prompt construction buffer, etiher as text or by modifying the buffer's system message. (gptel-context--wrap-default): Remove, as this is covered by `gptel-context--wrap-in-buffer'. * gptel.el (gptel--transform-add-context): Augmentor for adding context to the request, which calls gptel-context- functions. This will be added to `gptel-prompt-transform-functions' next. (Context addition is currently non-functional in gptel.) (gptel--realize-query): Move context addition step from here back to the transforms/augmentation step. Handling media files in the context is an exception and it still happens here. (gptel--wrap-user-prompt-maybe): Remove, no longer needed.
1 parent 427e8b2 commit a19e04b

File tree

2 files changed

+81
-58
lines changed

2 files changed

+81
-58
lines changed

gptel-context.el

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,25 @@
5656
This is used in gptel context buffers."
5757
:group 'gptel)
5858

59-
(defcustom gptel-context-wrap-function #'gptel-context--wrap-default
60-
"Function to format the context string sent with the gptel request.
59+
(defvar gptel-context-wrap-function nil
60+
"Function to format the context string sent with the gptel request.")
61+
(make-obsolete-variable
62+
'gptel-context-wrap-function
63+
"Custom functions for wrapping context are no longer supported by gptel.\
64+
See `gptel-context--wrap-in-buffer' for details."
65+
"0.9.9")
6166

62-
This function receives two argument, the message to wrap with the
63-
context, and an alist of contexts organized by buffer. It should
64-
return a string containing the message and the context, formatted as
65-
necessary.
67+
(defcustom gptel-context-string-function #'gptel-context--string
68+
"Function to prepare the context string sent with the gptel request.
6669
67-
The message is either the system message or the last user prompt,
68-
as configured by `gptel-use-context'.
70+
This function can be synchronous or asynchronous, and receives one or
71+
two arguments respectively.
6972
70-
The alist of contexts is structured as follows:
73+
Synchronous: An alist of contexts with buffers or files (the context
74+
alist).
75+
Asynchronous: A callback to call with the result, and the context alist.
76+
77+
The context alist is structured as follows:
7178
7279
((buffer1 . (overlay1 overlay2)
7380
(\"path/to/file\")
@@ -293,25 +300,57 @@ ADVANCE controls the overlay boundary behavior."
293300
overlay))
294301

295302
;;;###autoload
296-
(defun gptel-context--wrap (message)
297-
"Wrap MESSAGE with context string."
298-
(funcall gptel-context-wrap-function
299-
message (gptel-context--collect)))
300-
301-
(defun gptel-context--wrap-default (message contexts)
302-
"Add CONTEXTS to MESSAGE.
303-
304-
MESSAGE is usually either the system message or the user prompt.
305-
The accumulated context from CONTEXTS is appended or prepended to
306-
it, respectively."
307-
;; Append context before/after system message.
308-
(let ((context-string (gptel-context--string contexts)))
309-
(if (> (length context-string) 0)
310-
(pcase-exhaustive gptel-use-context
311-
('system (concat message "\n\n" context-string))
312-
('user (concat context-string "\n\n" message))
313-
('nil message))
314-
message)))
303+
(defun gptel-context--wrap (callback data-buf)
304+
"Add request context to DATA-BUF and run CALLBACK.
305+
306+
DATA-BUF is the buffer where the request prompt is constructed."
307+
(if (= (car (func-arity gptel-context-string-function)) 2)
308+
(funcall gptel-context-string-function
309+
(lambda (c) (with-current-buffer data-buf
310+
(gptel-context--wrap-in-buffer c))
311+
(funcall callback))
312+
(gptel-context--collect))
313+
(with-current-buffer data-buf
314+
(thread-last (gptel-context--collect)
315+
(funcall gptel-context-string-function)
316+
(gptel-context--wrap-in-buffer)))
317+
(funcall callback)))
318+
319+
(defun gptel-context--wrap-in-buffer (context-string &optional method)
320+
"Inject CONTEXT-STRING to current buffer using METHOD.
321+
322+
METHOD is either system or user, and defaults to `gptel-use-context'.
323+
This modifies the buffer."
324+
(when (length> context-string 0)
325+
(pcase (or method gptel-use-context)
326+
('system
327+
(if (gptel--model-capable-p 'nosystem)
328+
(gptel-context--wrap-in-buffer context-string 'user)
329+
(if gptel--system-message
330+
(cl-etypecase gptel--system-message
331+
(string
332+
(setq gptel--system-message
333+
(concat gptel--system-message "\n\n" context-string)))
334+
(function
335+
(setq gptel--system-message
336+
(gptel--parse-directive gptel--system-message 'raw))
337+
(gptel-context--wrap-in-buffer context-string))
338+
(list
339+
(setq gptel--system-message ;cons a new list to avoid mutation
340+
(cons (concat (car gptel--system-message) "\n\n" context-string)
341+
(cdr gptel--system-message)))))
342+
(setq gptel--system-message context-string))))
343+
('user
344+
(goto-char (point-max))
345+
(text-property-search-backward 'gptel nil t)
346+
(and gptel-mode
347+
(looking-at
348+
(concat "[\n[:blank:]]*"
349+
(and-let* ((prefix (gptel-prompt-prefix-string))
350+
((not (string-empty-p prefix))))
351+
(concat "\\(?:" (regexp-quote prefix) "\\)?"))))
352+
(delete-region (match-beginning 0) (match-end 0)))
353+
(insert "\n" context-string "\n\n")))))
315354

316355
(defun gptel-context--collect-media (&optional contexts)
317356
"Collect media CONTEXTS.

gptel.el

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,11 @@ in any way.")
11081108
"Curl executable to use."
11091109
(if (stringp gptel-use-curl) gptel-use-curl "curl"))
11101110

1111+
(defun gptel--transform-add-context (callback fsm)
1112+
(if (and gptel-use-context gptel-context--alist)
1113+
(gptel-context--wrap callback (plist-get (gptel-fsm-info fsm) :data))
1114+
(funcall callback)))
1115+
11111116
;;;; Model interface
11121117
;; NOTE: This interface would be simpler to implement as a defstruct. But then
11131118
;; users cannot set `gptel-model' to a symbol/string directly, or we'd need
@@ -2511,12 +2516,7 @@ Initiate the request when done."
25112516
(let* ((directive (gptel--parse-directive gptel--system-message 'raw))
25122517
;; DIRECTIVE contains both the system message and the template prompts
25132518
(gptel--system-message
2514-
;; Add context chunks to system message if required
2515-
(unless (gptel--model-capable-p 'nosystem)
2516-
(if (and gptel-context--alist
2517-
(eq gptel-use-context 'system))
2518-
(gptel-context--wrap (car directive))
2519-
(car directive))))
2519+
(unless (gptel--model-capable-p 'nosystem) (car directive)))
25202520
;; TODO(tool) Limit tool use to capable models after documenting :capabilities
25212521
;; (gptel-use-tools (and (gptel--model-capable-p 'tool-use) gptel-use-tools))
25222522
(stream (and (plist-get info :stream) gptel-use-curl gptel-stream
@@ -2541,7 +2541,14 @@ Initiate the request when done."
25412541
(setq full-prompt (gptel--parse-buffer ;prompt from buffer or explicitly supplied
25422542
gptel-backend (and gptel--num-messages-to-send
25432543
(* 2 gptel--num-messages-to-send))))
2544-
(gptel--wrap-user-prompt-maybe full-prompt)
2544+
;; Inject media chunks into the first user prompt if required. Media
2545+
;; chunks are always included with the first user message,
2546+
;; irrespective of the preference in `gptel-use-context'. This is
2547+
;; because media cannot be included (in general) with system messages.
2548+
;; TODO(augment): Find a way to do this in the prompt-buffer?
2549+
(when (and gptel-context--alist gptel-use-context
2550+
gptel-track-media (gptel--model-capable-p 'media))
2551+
(gptel--wrap-user-prompt gptel-backend full-prompt 'media))
25452552
(unless stream (cl-remf info :stream))
25462553
(plist-put info :backend gptel-backend)
25472554
(when gptel-include-reasoning ;Required for next-request-only scope
@@ -2759,29 +2766,6 @@ Optional RAW disables text properties and transformation."
27592766
(`(tool-result . ,tool-results)
27602767
(gptel--display-tool-results tool-results info)))))
27612768

2762-
(defun gptel--wrap-user-prompt-maybe (prompts)
2763-
"Return PROMPTS wrapped with text and media context.
2764-
2765-
This delegates to backend-specific wrap functions."
2766-
(prog1 prompts
2767-
(when gptel-context--alist
2768-
;; Inject context chunks into the last user prompt if required.
2769-
;; This is also the fallback for when `gptel-use-context' is set to
2770-
;; 'system but the model does not support system messages.
2771-
(when (and gptel-use-context
2772-
(or (eq gptel-use-context 'user)
2773-
(gptel--model-capable-p 'nosystem))
2774-
(> (length prompts) 0)) ;FIXME context should be injected
2775-
;even when there are no prompts
2776-
(gptel--wrap-user-prompt gptel-backend prompts))
2777-
;; Inject media chunks into the first user prompt if required. Media
2778-
;; chunks are always included with the first user message,
2779-
;; irrespective of the preference in `gptel-use-context'. This is
2780-
;; because media cannot be included (in general) with system messages.
2781-
(when (and gptel-use-context gptel-track-media
2782-
(gptel--model-capable-p 'media))
2783-
(gptel--wrap-user-prompt gptel-backend prompts :media)))))
2784-
27852769
(defun gptel--create-prompt-buffer (&optional prompt-end)
27862770
"Return a buffer with the conversation prompt to be sent.
27872771

0 commit comments

Comments
 (0)