Skip to content

Commit 995647d

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 fd880f9 commit 995647d

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\")
@@ -301,25 +308,57 @@ ADVANCE controls the overlay boundary behavior."
301308
overlay))
302309

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

324363
(defun gptel-context--collect-media (&optional contexts)
325364
"Collect media CONTEXTS.

gptel.el

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

1092+
(defun gptel--transform-add-context (callback fsm)
1093+
(if (and gptel-use-context gptel-context--alist)
1094+
(gptel-context--wrap callback (plist-get (gptel-fsm-info fsm) :data))
1095+
(funcall callback)))
1096+
10921097
;;;; Model interface
10931098
;; NOTE: This interface would be simpler to implement as a defstruct. But then
10941099
;; users cannot set `gptel-model' to a symbol/string directly, or we'd need
@@ -2492,12 +2497,7 @@ Initiate the request when done."
24922497
(let* ((directive (gptel--parse-directive gptel--system-message 'raw))
24932498
;; DIRECTIVE contains both the system message and the template prompts
24942499
(gptel--system-message
2495-
;; Add context chunks to system message if required
2496-
(unless (gptel--model-capable-p 'nosystem)
2497-
(if (and gptel-context--alist
2498-
(eq gptel-use-context 'system))
2499-
(gptel-context--wrap (car directive))
2500-
(car directive))))
2500+
(unless (gptel--model-capable-p 'nosystem) (car directive)))
25012501
;; TODO(tool) Limit tool use to capable models after documenting :capabilities
25022502
;; (gptel-use-tools (and (gptel--model-capable-p 'tool-use) gptel-use-tools))
25032503
(stream (and (plist-get info :stream) gptel-use-curl gptel-stream
@@ -2522,7 +2522,14 @@ Initiate the request when done."
25222522
(setq full-prompt (gptel--parse-buffer ;prompt from buffer or explicitly supplied
25232523
gptel-backend (and gptel--num-messages-to-send
25242524
(* 2 gptel--num-messages-to-send))))
2525-
(gptel--wrap-user-prompt-maybe full-prompt)
2525+
;; Inject media chunks into the first user prompt if required. Media
2526+
;; chunks are always included with the first user message,
2527+
;; irrespective of the preference in `gptel-use-context'. This is
2528+
;; because media cannot be included (in general) with system messages.
2529+
;; TODO(augment): Find a way to do this in the prompt-buffer?
2530+
(when (and gptel-context--alist gptel-use-context
2531+
gptel-track-media (gptel--model-capable-p 'media))
2532+
(gptel--wrap-user-prompt gptel-backend full-prompt 'media))
25262533
(unless stream (cl-remf info :stream))
25272534
(plist-put info :backend gptel-backend)
25282535
(when gptel-include-reasoning ;Required for next-request-only scope
@@ -2740,29 +2747,6 @@ Optional RAW disables text properties and transformation."
27402747
(`(tool-result . ,tool-results)
27412748
(gptel--display-tool-results tool-results info)))))
27422749

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

0 commit comments

Comments
 (0)