branch: elpa/gptel
commit 56623b9889e5b8849ea648110fd5fe0d6db726ac
Author: Psionik K <73710933+psioni...@users.noreply.github.com>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    gptel-org: Tool result handling for Org
    
    * gptel-org.el: (gptel-org--create-prompt,
    gptel--org-strip-tool-headers, gptel--org-unescape-tool-results):
    - Unescape tool results before sending to the backend.  Unescape
    takes place in a buffer containing a clone of the data.
    - Strip tool headers from the prompt before sending to the
    backend.
---
 gptel-org.el | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/gptel-org.el b/gptel-org.el
index 706da1ee8a..6550425310 100644
--- a/gptel-org.el
+++ b/gptel-org.el
@@ -227,6 +227,8 @@ value of `gptel-org-branching-context', which see."
                        do (insert-buffer-substring org-buf start end)
                        (goto-char (point-min)))
               (goto-char (point-max))
+              (gptel--org-unescape-tool-results)
+              (gptel--org-strip-tool-headers)
               (let ((major-mode 'org-mode))
                 (gptel--parse-buffer gptel-backend max-entries)))))
       ;; Create prompt the usual way
@@ -240,9 +242,53 @@ value of `gptel-org-branching-context', which see."
             (set (make-local-variable sym)
                  (buffer-local-value sym org-buf)))
           (insert-buffer-substring org-buf beg end)
+          (gptel--org-unescape-tool-results)
+          (gptel--org-strip-tool-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."
+  (save-excursion
+    (goto-char (point-min))
+    (while (re-search-forward (rx line-start (literal "#+")
+                                  (or (literal "begin") (literal "end"))
+                                  (literal "_tool"))
+                              nil t)
+      (delete-region (match-beginning 0)
+                     (min (point-max) (1+ (line-end-position)))))))
+
+(defun gptel--org-unescape-tool-results ()
+  "Undo escapes done to keep results from escaping blocks.
+Scans backward for gptel tool text property, reads the arguments, then
+unescapes the remainder."
+  (save-excursion
+    (goto-char (point-max))
+    (let ((prev-pt (point)))
+      (while (> prev-pt (point-min))
+        (goto-char
+         (previous-single-char-property-change (point) 'gptel))
+        (let ((prop (get-text-property (point) 'gptel))
+              (backward-progress (point)))
+          (when (eq (car-safe prop) 'tool)
+            ;; User edits to clean up can potentially insert a tool-call header
+            ;; that is propertized.  Tool call headers should not be
+            ;; propertized.
+            (when (looking-at-p "[[:space:]]*#\\+begin_tool")
+              (goto-char (match-end 0)))
+            (condition-case nil
+                (read (current-buffer))
+              ((end-of-file invalid-read-syntax)
+               (message "Could not read tool arguments")))
+            ;; TODO this code is able to put the point behind prev-pt, which
+            ;; makes the region inverted.  The `max' catches this, but really
+            ;; `read' and `looking-at' are the culprits.  Badly formed tool
+            ;; blocks can lead to this being necessary.
+            (org-unescape-code-in-region
+             (min prev-pt (point)) prev-pt))
+          (goto-char (setq prev-pt backward-progress)))))))
+
 ;; Handle media links in the buffer
 (cl-defmethod gptel--parse-media-links ((_mode (eql 'org-mode)) beg end)
   "Parse text and actionable links between BEG and END.

Reply via email to