branch: elpa/gptel commit 4ab198a904f1706a8daede1145386db4dc960aa1 Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com> Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
gptel: Change buffer parser property-search method Parse the buffer using `previous-single-property-change' instead of `text-property-search-backward'. This is for compatibility with overlay-based response bounds tracking. In the future, we want to be able to use both overlays and text-properties to track responses since they can compensate for each other's weaknesses. Specifically, overlays handle insertions in the middle of response regions correctly, but are fragile under text deletion. Text properties are robust but cannot "swallow" changes to regions consistently without adding to expensive hooks like `yank-transform-functions' and `after-change-functions'. * gptel-openai.el (gptel--parse-buffer): Above described changes. * gptel-ollama.el (gptel--parse-buffer): Above described changes. * gptel-gemini.el (gptel--parse-buffer): Above described changes. * gptel-anthropic.el (gptel--parse-buffer): Above described changes. --- gptel-anthropic.el | 51 +++++++++++++++++++++++---------------------------- gptel-gemini.el | 49 +++++++++++++++++++++++-------------------------- gptel-ollama.el | 47 ++++++++++++++++++++++------------------------- gptel-openai.el | 53 +++++++++++++++++++++++++---------------------------- 4 files changed, 93 insertions(+), 107 deletions(-) diff --git a/gptel-anthropic.el b/gptel-anthropic.el index 50842e8c06..bc610929f3 100644 --- a/gptel-anthropic.el +++ b/gptel-anthropic.el @@ -269,42 +269,37 @@ TOOL-USE is a list of plists containing tool names, arguments and call results." :content `[(:type "text" :text ,text)]))) (cl-defmethod gptel--parse-buffer ((_backend gptel-anthropic) &optional max-entries) - (let ((prompts) (prop) (prev-pt (point)) + (let ((prompts) (prev-pt (point)) (include-media (and gptel-track-media (or (gptel--model-capable-p 'media) - (gptel--model-capable-p 'url))))) + (gptel--model-capable-p 'url))))) (if (or gptel-mode gptel-track-response) - (while (and - (or (not max-entries) (>= max-entries 0)) - (setq prop (text-property-search-backward - 'gptel 'response - (when (get-char-property (max (point-min) (1- (point))) - 'gptel) - t)))) + (while (and (or (not max-entries) (>= max-entries 0)) + (goto-char (previous-single-property-change + (point) 'gptel nil (point-min))) + (not (= (point) prev-pt))) ;; HACK Until we can find a more robust solution for editing ;; responses, ignore prompts containing only whitespace, as the ;; Anthropic API can't handle it. See #452, #409, #406, #351 and #321 ;; We check for blank prompts by skipping whitespace and comparing ;; point against the previous. (unless (save-excursion (skip-syntax-forward " ") (>= (point) prev-pt)) - (if (prop-match-value prop) ; assistant role - (push (list :role "assistant" - :content - (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop))) - prompts) - (if include-media ; user role: possibly with media - (push (list :role "user" - :content - (gptel--anthropic-parse-multipart - (gptel--parse-media-links - major-mode (prop-match-beginning prop) (prop-match-end prop)))) - prompts) - (push (list :role "user" - :content - (gptel--trim-prefixes - (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop)))) - prompts)))) + (pcase (get-char-property (point) 'gptel) + ('response + (push (list :role "assistant" + :content (buffer-substring-no-properties (point) prev-pt)) + prompts)) + ('nil ; user role: possibly with media + (if include-media + (push (list :role "user" + :content + (gptel--anthropic-parse-multipart + (gptel--parse-media-links major-mode (point) prev-pt))) + prompts) + (push (list :role "user" + :content + (gptel--trim-prefixes + (buffer-substring-no-properties (point) prev-pt))) + prompts))))) (setq prev-pt (point)) (and max-entries (cl-decf max-entries))) (push (list :role "user" diff --git a/gptel-gemini.el b/gptel-gemini.el index 1e69e82a75..81dc78c53d 100644 --- a/gptel-gemini.el +++ b/gptel-gemini.el @@ -226,35 +226,32 @@ See generic implementation for full documentation." finally return prompts)) (cl-defmethod gptel--parse-buffer ((_backend gptel-gemini) &optional max-entries) - (let ((prompts) (prop) + (let ((prompts) (prev-pt (point)) (include-media (and gptel-track-media (or (gptel--model-capable-p 'media) (gptel--model-capable-p 'url))))) (if (or gptel-mode gptel-track-response) - (while (and - (or (not max-entries) (>= max-entries 0)) - (setq prop (text-property-search-backward - 'gptel 'response - (when (get-char-property (max (point-min) (1- (point))) - 'gptel) - t)))) - (if (prop-match-value prop) ;assistant role - (push (list :role "model" - :parts - (list :text (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop)))) - prompts) - (if include-media - (push (list :role "user" - :parts (gptel--gemini-parse-multipart - (gptel--parse-media-links - major-mode (prop-match-beginning prop) (prop-match-end prop)))) - prompts) - (push (list :role "user" - :parts - `[(:text ,(gptel--trim-prefixes - (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop))))]) - prompts))) + (while (and (or (not max-entries) (>= max-entries 0)) + (goto-char (previous-single-property-change + (point) 'gptel nil (point-min))) + (not (= (point) prev-pt))) + (pcase (get-char-property (point) 'gptel) + ('response + (push (list :role "model" + :parts + (list :text (buffer-substring-no-properties (point) prev-pt))) + prompts)) + ('nil + (if include-media + (push (list :role "user" + :parts (gptel--gemini-parse-multipart + (gptel--parse-media-links major-mode (point) prev-pt))) + prompts) + (push (list :role "user" + :parts + `[(:text ,(gptel--trim-prefixes + (buffer-substring-no-properties (point) prev-pt)))]) + prompts)))) + (setq prev-pt (point)) (and max-entries (cl-decf max-entries))) (push (list :role "user" :parts diff --git a/gptel-ollama.el b/gptel-ollama.el index 97d3363942..9061786dc1 100644 --- a/gptel-ollama.el +++ b/gptel-ollama.el @@ -142,34 +142,31 @@ Store response metadata in state INFO." (list :role (if role "user" "assistant") :content text))) (cl-defmethod gptel--parse-buffer ((_backend gptel-ollama) &optional max-entries) - (let ((prompts) (prop) + (let ((prompts) (prev-pt (point)) (include-media (and gptel-track-media (or (gptel--model-capable-p 'media) (gptel--model-capable-p 'url))))) (if (or gptel-mode gptel-track-response) - (while (and - (or (not max-entries) (>= max-entries 0)) - (setq prop (text-property-search-backward - 'gptel 'response - (when (get-char-property (max (point-min) (1- (point))) - 'gptel) - t)))) - (if (prop-match-value prop) ;assistant role - (push (list :role "assistant" - :content (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop))) - prompts) - (if include-media - (push (append '(:role "user") - (gptel--ollama-parse-multipart - (gptel--parse-media-links - major-mode (prop-match-beginning prop) (prop-match-end prop)))) - prompts) - (push (list :role "user" - :content - (gptel--trim-prefixes - (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop)))) - prompts))) + (while (and (or (not max-entries) (>= max-entries 0)) + (goto-char (previous-single-property-change + (point) 'gptel nil (point-min))) + (not (= (point) prev-pt))) + (pcase (get-char-property (point) 'gptel) + ('response + (push (list :role "assistant" + :content (buffer-substring-no-properties (point) prev-pt)) + prompts)) + ('nil + (if include-media + (push (append '(:role "user") + (gptel--ollama-parse-multipart + (gptel--parse-media-links major-mode (point) prev-pt))) + prompts) + (push (list :role "user" + :content + (gptel--trim-prefixes + (buffer-substring-no-properties (point) prev-pt))) + prompts)))) + (setq prev-pt (point)) (and max-entries (cl-decf max-entries))) (push (list :role "user" :content diff --git a/gptel-openai.el b/gptel-openai.el index 5126941de3..34ef05c78d 100644 --- a/gptel-openai.el +++ b/gptel-openai.el @@ -319,41 +319,38 @@ Mutate state INFO with response metadata." (list :role (if role "user" "assistant") :content text))) (cl-defmethod gptel--parse-buffer ((_backend gptel-openai) &optional max-entries) - (let ((prompts) (prop) + (let ((prompts) (prev-pt (point)) (include-media (and gptel-track-media (or (gptel--model-capable-p 'media) (gptel--model-capable-p 'url))))) (if (or gptel-mode gptel-track-response) - (while (and - (or (not max-entries) (>= max-entries 0)) - (setq prop (text-property-search-backward - 'gptel 'response - (when (get-char-property (max (point-min) (1- (point))) - 'gptel) - t)))) - (if (prop-match-value prop) ;assistant role - (push (list :role "assistant" - :content - (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop))) - prompts) - (if include-media - (push (list :role "user" - :content - (gptel--openai-parse-multipart - (gptel--parse-media-links - major-mode (prop-match-beginning prop) (prop-match-end prop)))) - prompts) - (push (list :role "user" - :content - (gptel--trim-prefixes - (buffer-substring-no-properties (prop-match-beginning prop) - (prop-match-end prop)))) - prompts))) + (while (and (or (not max-entries) (>= max-entries 0)) + (/= prev-pt (point-min)) + (goto-char (previous-single-property-change + (point) 'gptel nil (point-min)))) + (pcase (get-char-property (point) 'gptel) + ('response + (push (list :role "assistant" + :content (buffer-substring-no-properties (point) prev-pt)) + prompts)) + ('nil + (if include-media + (push (list :role "user" + :content + (gptel--openai-parse-multipart + (gptel--parse-media-links major-mode (point) prev-pt))) + prompts) + (push (list :role "user" + :content + (gptel--trim-prefixes + (buffer-substring-no-properties (point) prev-pt))) + prompts)))) + (setq prev-pt (point)) (and max-entries (cl-decf max-entries))) (push (list :role "user" :content - (gptel--trim-prefixes (buffer-substring-no-properties (point-min) (point-max)))) + (gptel--trim-prefixes (buffer-substring-no-properties + (point-min) (point-max)))) prompts)) prompts))