Skip to content

Commit 665bce9

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 27f4397 commit 665bce9

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\")
@@ -286,25 +293,57 @@ ADVANCE controls the overlay boundary behavior."
286293
overlay))
287294

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

309348
(defun gptel-context--collect-media (&optional contexts)
310349
"Collect media CONTEXTS.

gptel.el

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,11 @@ Note: Changing this variable does not affect gptel\\='s behavior
10801080
in any way.")
10811081
(put 'gptel--backend-name 'safe-local-variable #'always)
10821082

1083+
(defun gptel--transform-add-context (callback fsm)
1084+
(if (and gptel-use-context gptel-context--alist)
1085+
(gptel-context--wrap callback (plist-get (gptel-fsm-info fsm) :data))
1086+
(funcall callback)))
1087+
10831088
;;;; Model interface
10841089
;; NOTE: This interface would be simpler to implement as a defstruct. But then
10851090
;; users cannot set `gptel-model' to a symbol/string directly, or we'd need
@@ -2483,12 +2488,7 @@ Initiate the request when done."
24832488
(let* ((directive (gptel--parse-directive gptel--system-message 'raw))
24842489
;; DIRECTIVE contains both the system message and the template prompts
24852490
(gptel--system-message
2486-
;; Add context chunks to system message if required
2487-
(unless (gptel--model-capable-p 'nosystem)
2488-
(if (and gptel-context--alist
2489-
(eq gptel-use-context 'system))
2490-
(gptel-context--wrap (car directive))
2491-
(car directive))))
2491+
(unless (gptel--model-capable-p 'nosystem) (car directive)))
24922492
;; TODO(tool) Limit tool use to capable models after documenting :capabilities
24932493
;; (gptel-use-tools (and (gptel--model-capable-p 'tool-use) gptel-use-tools))
24942494
(stream (and (plist-get info :stream) gptel-use-curl gptel-stream
@@ -2513,7 +2513,14 @@ Initiate the request when done."
25132513
(setq full-prompt (gptel--parse-buffer ;prompt from buffer or explicitly supplied
25142514
gptel-backend (and gptel--num-messages-to-send
25152515
(* 2 gptel--num-messages-to-send))))
2516-
(gptel--wrap-user-prompt-maybe full-prompt)
2516+
;; Inject media chunks into the first user prompt if required. Media
2517+
;; chunks are always included with the first user message,
2518+
;; irrespective of the preference in `gptel-use-context'. This is
2519+
;; because media cannot be included (in general) with system messages.
2520+
;; TODO(augment): Find a way to do this in the prompt-buffer?
2521+
(when (and gptel-context--alist gptel-use-context
2522+
gptel-track-media (gptel--model-capable-p 'media))
2523+
(gptel--wrap-user-prompt gptel-backend full-prompt 'media))
25172524
(unless stream (cl-remf info :stream))
25182525
(plist-put info :backend gptel-backend)
25192526
(when gptel-include-reasoning ;Required for next-request-only scope
@@ -2731,29 +2738,6 @@ Optional RAW disables text properties and transformation."
27312738
(`(tool-result . ,tool-results)
27322739
(gptel--display-tool-results tool-results info)))))
27332740

2734-
(defun gptel--wrap-user-prompt-maybe (prompts)
2735-
"Return PROMPTS wrapped with text and media context.
2736-
2737-
This delegates to backend-specific wrap functions."
2738-
(prog1 prompts
2739-
(when gptel-context--alist
2740-
;; Inject context chunks into the last user prompt if required.
2741-
;; This is also the fallback for when `gptel-use-context' is set to
2742-
;; 'system but the model does not support system messages.
2743-
(when (and gptel-use-context
2744-
(or (eq gptel-use-context 'user)
2745-
(gptel--model-capable-p 'nosystem))
2746-
(> (length prompts) 0)) ;FIXME context should be injected
2747-
;even when there are no prompts
2748-
(gptel--wrap-user-prompt gptel-backend prompts))
2749-
;; Inject media chunks into the first user prompt if required. Media
2750-
;; chunks are always included with the first user message,
2751-
;; irrespective of the preference in `gptel-use-context'. This is
2752-
;; because media cannot be included (in general) with system messages.
2753-
(when (and gptel-use-context gptel-track-media
2754-
(gptel--model-capable-p 'media))
2755-
(gptel--wrap-user-prompt gptel-backend prompts :media)))))
2756-
27572741
(defun gptel--create-prompt-buffer (&optional prompt-end)
27582742
"Return a buffer with the conversation prompt to be sent.
27592743

0 commit comments

Comments
 (0)