branch: elpa/gptel commit c23cba5ac284a35adb9addb122ce9aa529ad2362 Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com> Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
gptel: Add blocks around reasoning content * gptel.el: (gptel--display-reasoning-stream, gptel--insert-response): Extract the reasoning block in the default stream callback into a new function `gptel--display-reasoning-stream'. When inserting the reasoning block into the chat buffer, add blocks (#+begin_reasoning...#+end-reasoning, markdown backticks) around the reasoning chunk. In Org mode, fold these blocks automatically. This behavior is identical to how tool results are handled. This behavior is not currently customizable. * gptel-curl.el (gptel-curl--stream-insert-response): Remove reasoning block handling code and call `gptel--display-reasoning-stream' instead. * gptel-org.el: (gptel-org--create-prompt, gptel-org--strip-tool-headers): When building the query payload, remove (#+begin_reasoning...#+end_reasoning) block headers from the prompt in Org mode, along with #+begin_tool...#+end_tool block headers. As with tool results, this is to avoid auto-mimicry by the LLMs. Rename `gptel-org--strip-tool-headers' -> `gptel-org--strip-block-headers'. * README.org: Mention the new behavior of enclosing the reasoning content in Org blocks/Markdown backticks. --- README.org | 2 ++ gptel-curl.el | 20 +------------ gptel-org.el | 18 ++++++++---- gptel.el | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 96 insertions(+), 37 deletions(-) diff --git a/README.org b/README.org index 421186a0b4..2986056bf4 100644 --- a/README.org +++ b/README.org @@ -1009,6 +1009,8 @@ And then browse through or remove context from the context buffer: Some LLMs include in their response a "thinking" or "reasoning" block. This text improves the quality of the LLM’s final output, but may not be interesting to you by itself. You can decide how you would like this "reasoning" content to be handled by gptel by setting the user option =gptel-include-reasoning=. You can include it in the LLM response (the default), omit it entirely, include it in the buffer but ignore it on subsequent conversation turns, or redirect it to another buffer. [...] +When included with the response, reasoning content will be delimited by Org blocks or markdown backticks. + *** Tool use (experimental) gptel can provide the LLM with client-side elisp "tools", or function specifications, along with the request. If the LLM decides to run the tool, it supplies the tool call arguments, which gptel uses to run the tool in your Emacs session. The result is optionally returned to the LLM to complete the task. diff --git a/gptel-curl.el b/gptel-curl.el index ad9713b3af..de6904fcf9 100644 --- a/gptel-curl.el +++ b/gptel-curl.el @@ -259,25 +259,7 @@ Optional RAW disables text properties and transformation." (insert response) (run-hooks 'gptel-post-stream-hook))))) (`(reasoning . ,text) - (pcase (plist-get info :include-reasoning) - ('nil) - ('t - (if (eq text t) - (gptel-curl--stream-insert-response - gptel-response-separator info t) - (gptel-curl--stream-insert-response text info))) - ('ignore - (if (eq text t) - (setq text gptel-response-separator) - (add-text-properties - 0 (length text) '(gptel ignore front-sticky (gptel)) text)) - (gptel-curl--stream-insert-response text info t)) - ((pred stringp) - (unless (eq text t) - (with-current-buffer (get-buffer-create - (plist-get info :include-reasoning)) - (save-excursion (goto-char (point-max)) - (insert text))))))) + (gptel--display-reasoning-stream text info)) (`(tool-call . ,tool-calls) (gptel--display-tool-calls tool-calls info)) (`(tool-result . ,tool-results) diff --git a/gptel-org.el b/gptel-org.el index 94d2c678de..0ff0b70877 100644 --- a/gptel-org.el +++ b/gptel-org.el @@ -229,7 +229,7 @@ value of `gptel-org-branching-context', which see." (goto-char (point-min))) (goto-char (point-max)) (gptel-org--unescape-tool-results) - (gptel-org--strip-tool-headers) + (gptel-org--strip-block-headers) (let ((major-mode 'org-mode)) (gptel--parse-buffer gptel-backend max-entries))))) ;; Create prompt the usual way @@ -244,19 +244,25 @@ value of `gptel-org-branching-context', which see." (buffer-local-value sym org-buf))) (insert-buffer-substring org-buf beg end) (gptel-org--unescape-tool-results) - (gptel-org--strip-tool-headers) + (gptel-org--strip-block-headers) (let ((major-mode 'org-mode)) (gptel--parse-buffer gptel-backend max-entries))))))) (defun gptel-org--strip-tool-headers () "Remove all tool_call block headers and footers. Every line that matches will be removed entirely." +(defun gptel-org--strip-block-headers () + "Remove all gptel-specific block headers and footers. +Every line that matches will be removed entirely. + +This removal is necessary to avoid auto-mimicry by LLMs." (save-excursion (goto-char (point-min)) - (while (re-search-forward (rx line-start (literal "#+") - (or (literal "begin") (literal "end")) - (literal "_tool")) - nil t) + (while (re-search-forward + (rx line-start (literal "#+") + (or (literal "begin") (literal "end")) + (or (literal "_tool") (literal "_reasoning"))) + nil t) (delete-region (match-beginning 0) (min (point-max) (1+ (line-end-position))))))) diff --git a/gptel.el b/gptel.el index 3ab34448f8..798bcb02c3 100644 --- a/gptel.el +++ b/gptel.el @@ -2465,18 +2465,34 @@ Optional RAW disables text properties and transformation." ;; for uniformity with streaming responses (set-marker-insertion-type tracking-marker t))))) (`(reasoning . ,text) - (pcase (plist-get info :include-reasoning) - ('t (gptel--insert-response text info)) - ('nil) - ('ignore - (add-text-properties - 0 (length text) '(gptel ignore front-sticky (gptel)) text) - (gptel--insert-response text info t)) - ((pred stringp) - (with-current-buffer (get-buffer-create - (plist-get info :include-reasoning)) - (save-excursion (goto-char (point-max)) - (insert text)))))) + (when-let* ((include (plist-get info :include-reasoning))) + (if (stringp include) + (with-current-buffer (get-buffer-create + (plist-get info :include-reasoning)) + (save-excursion (goto-char (point-max)) (insert text))) + (with-current-buffer (marker-buffer start-marker) + (let ((blocks (if (derived-mode-p 'org-mode) + `("#+begin_reasoning\n" . ,(concat "\n#+end_reasoning" + gptel-response-separator)) + ;; TODO(reasoning) remove properties and strip instead + (cons (propertize "``` reasoning\n" 'gptel 'ignore) + (concat (propertize "\n```" 'gptel 'ignore) + gptel-response-separator))))) + (if (eq include 'ignore) + (progn + (add-text-properties + 0 (length text) '(gptel ignore front-sticky (gptel)) text) + (gptel--insert-response + (concat (car blocks) text (cdr blocks)) info t)) + (gptel--insert-response (car blocks) info t) + (gptel--insert-response text info) + (gptel--insert-response (cdr blocks) info t)) + (when (derived-mode-p 'org-mode) ;fold block + (save-excursion + (goto-char (plist-get info :tracking-marker)) + (search-backward "#+end_reasoning" start-marker t) + (when (looking-at "^#\\+end_reasoning") + (org-cycle))))))))) (`(tool-call . ,tool-calls) (gptel--display-tool-calls tool-calls info)) (`(tool-result . ,tool-results) @@ -2835,6 +2851,59 @@ INTERACTIVEP is t when gptel is called interactively." (current-buffer))) +;;; Reasoning content UI +(defun gptel--display-reasoning-stream (text info) + "Show reasoning TEXT in an appropriate location. + +INFO is the request INFO, see `gptel--url-get-response'. This is +for streaming responses only." + (when-let* ((include (plist-get info :include-reasoning))) + (if (stringp include) + (unless (eq text t) + (with-current-buffer (get-buffer-create include) + (save-excursion (goto-char (point-max)) + (insert text)))) + (let* ((reasoning-marker (plist-get info :reasoning-marker)) + (tracking-marker (plist-get info :tracking-marker)) + (start-marker (plist-get info :position))) + (with-current-buffer (marker-buffer start-marker) + (if (eq text t) ;end of stream + (progn + (gptel-curl--stream-insert-response + (concat (if (derived-mode-p 'org-mode) + "\n#+end_reasoning" + ;; TODO(reasoning) remove properties and strip instead + (propertize "\n```" 'gptel 'ignore)) + gptel-response-separator) + info t) + (when (derived-mode-p 'org-mode) ;fold block + (ignore-errors + (save-excursion + (goto-char tracking-marker) + (search-backward "#+end_reasoning" start-marker t) + (when (looking-at "^#\\+end_reasoning") + (org-cycle)))))) + (unless (and reasoning-marker tracking-marker + (= reasoning-marker tracking-marker)) + (gptel-curl--stream-insert-response + (if (derived-mode-p 'org-mode) + "#+begin_reasoning\n" + ;; TODO(reasoning) remove properties and strip instead + (propertize "``` reasoning\n" 'gptel 'ignore)) + info t)) + (if (eq include 'ignore) + (progn + (add-text-properties + 0 (length text) '(gptel ignore front-sticky (gptel)) text) + (gptel-curl--stream-insert-response text info t)) + (gptel-curl--stream-insert-response text info))) + (setq tracking-marker (plist-get info :tracking-marker)) + (if reasoning-marker + (move-marker reasoning-marker tracking-marker) + (plist-put info :reasoning-marker + (copy-marker tracking-marker nil)))))))) + + ;;; Tool use UI (defun gptel--display-tool-calls (tool-calls info &optional use-minibuffer) "Handle tool call confirmation.