From 6240a40ff0551969190d74e7f9aabbdda75cc67e Mon Sep 17 00:00:00 2001
From: Phil Bajsicki <phil@bajsicki.com>
Date: Wed, 16 Apr 2025 17:00:18 +0200
Subject: [PATCH] Improve org-ql tools' argument descriptions

---
 README.org         | 172 +++++++++++++++++++--------------------------
 gptel-org-tools.el | 118 ++++++++++---------------------
 2 files changed, 110 insertions(+), 180 deletions(-)

diff --git a/README.org b/README.org
index 9eebfb8..2432eb2 100644
--- a/README.org
+++ b/README.org
@@ -102,21 +102,6 @@ config.el:
 (require 'gptel-org-tools)
 (mapcar (lambda (tool) (cl-pushnew tool gptel-tools)) gptel-org-tools)
 #+end_src
-** Variables
-If you're like me, you may have structured headings based on timestamps in your org-mode notes.
-
-To that end, there are a few variables that may help you from blowing out the context, specifically for the ~org-extract-headings~ tool.
-
-#+begin_src elisp
-(defvar gptel-org-tools-skip-heading-extraction '())
-#+end_src
-
-Use ~setq~ in your configuration, with a list of buffer names whose headings you *never* want to extract. E.g.
-
-#+begin_src elisp :tangle no
-(setq gptel-org-tools-skip-heading-extraction '("journal.org" ".org"))
-#+end_src
-
 * Code
 ** Preamble
 :PROPERTIES:
@@ -165,10 +150,34 @@ Collects into =gptel-org-tools= list, distinct from =gptel-tools=
 #+begin_src elisp
 (defvar gptel-org-tools '())
 #+end_src
+** Variables
+If you're like me, you may have structured headings based on timestamps in your org-mode notes.
+
+To that end, there are a few variables that may help you from blowing out the context, specifically for the ~org-extract-headings~ tool.
+
+#+begin_src elisp
+(defvar gptel-org-tools-skip-heading-extraction '())
+#+end_src
+
+Use ~setq~ in your configuration, with a list of buffer names whose headings you *never* want to extract. E.g.
+
+#+begin_src elisp :tangle no
+(setq gptel-org-tools-skip-heading-extraction '("journal.org" ".org"))
+#+end_src
+
 ** Helper Functions
 These abstract away some of the tool definitions.
 
-Both of these clear the org-ql-cache to work around issues where a file may be updated between tool calls.
+*** Retrieve heading (line])
+#+begin_src elisp
+(defun gptel-org-tools--heading ()
+  (concat
+   (buffer-substring-no-properties
+    (line-beginning-position)
+    (line-end-position))
+   '"\n---\n"))
+#+end_src
+
 *** Retrieve heading and body (without subheadings)
 #+begin_src elisp
 (defun gptel-org-tools--heading-body ()
@@ -190,15 +199,19 @@ Both of these clear the org-ql-cache to work around issues where a file may be u
       (org-end-of-subtree))
      "---\n"))
 #+end_src
-** Note on org-ql
-Given that there isn't (yet?) a built-in way of disabling caching, every (org-ql-select) call is wrapped like so.
+** Note on org-ql (caching)
+There isn't (yet?) a good way to disable caching. This means that repeated queries will return the same output, until org-ql-cache is cleared.
 
 See [[https://github.com/alphapapa/org-ql/issues/437][this issue]] for details.
 
-#+begin_src elisp :tangle no
-  (let ((org-ql-cache (make-hash-table)))
-    (org-ql-select ...))
+The problem is that trying the solution given in the GitHub issue throws errors, which make LLMs freak out.
+
+So for now, you can manually re-set the cache like so:
+
+#+begin_src elisp :tangle no :results none
+(setq org-ql-cache (make-hash-table))
 #+end_src
+
 ** The tools
 *** Emacs
 These tools are primarily concerned with Emacs, Emacs Lisp, and files-or-buffers.
@@ -245,7 +258,8 @@ Seems to be one of the most reliable tools in the basket... mostly because
 	      :description "Access the list of buffers open in Emacs, including file names and full paths."
 	      :args (list '(:name "arg"
 			    :type string
-			    :description "Does nothing."))
+			    :description "Does nothing."
+			    :optional t))
 	      :category "emacs"))
 #+end_src
 
@@ -277,11 +291,14 @@ Be sure to customize the function to point to your org directory, if you wish. I
 #+end_src
 
 **** find-buffer-visiting
+Disabled for now, as it's causing some issues.
 #+begin_src elisp
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
 	      :function (lambda (filename)
-			  (bufferp (find-buffer-visiting (expand-file-name filename))))
+			  (concat
+			   (bufferp (find-buffer-visiting (expand-file-name filename)))
+			   "\n---\nTool execution complete. Proceed with next step."))
 	      :name "find-buffer-visiting"
 	      :description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)"
 	      :args (list '(:name "filename"
@@ -289,6 +306,7 @@ Be sure to customize the function to point to your org directory, if you wish. I
 			    :description "The filename to compare to open buffers."))
 	      :category "org-mode"))
 #+end_src
+
 **** open-file-inactive
 Continuation from above. Open a file into a buffer for processing, once it's found by dired-list.
 #+begin_src elisp
@@ -297,7 +315,9 @@ Continuation from above. Open a file into a buffer for processing, once it's fou
 	      :function (lambda (file)
 			   (with-current-buffer (get-buffer-create file)
 			     (insert-file-contents file)
-			     (current-buffer)))
+			     (concat
+			      (current-buffer)
+			      "\n---\nTool execution complete. Proceed with next step.")))
 	      :name "open-file-inactive"
 	      :description "Open the file in a background buffer. This doesn't interfere with the user."
 	      :args (list '(:name "file"
@@ -314,7 +334,9 @@ This reads file contents,
               :function (lambda (filename)
                           (with-temp-buffer
                             (insert-file-contents (expand-file-name filename))
-                            (buffer-string)))
+                            (concat
+			     (buffer-string)
+			     "\n---\nTool execution complete. Proceed with next step.")))
               :name "read-file-contents"
               :description "Read and return the contents of a specified file."
               :args (list '(:name "filename"
@@ -422,9 +444,7 @@ Therefore, headings. A reasonable amount of information, and still keeping the s
       (user-error "Buffer %s has too many headings, use org-extract-tags or org-ql-select-rifle." buffer)
   (with-current-buffer buffer
     (org-map-entries
-     #'(buffer-substring-no-properties
-	(line-beginning-position)
-	(line-end-position))
+     #'gptel-org-tools--heading
      t
      'file))))
 
@@ -460,7 +480,7 @@ Currently *not* tangled, as I'm testing breaking out each type of query into its
     (if (stringp query)
 	(read query)
       query)
-    :action #'gptel-org-tools--heading-body
+    :action #'gptel-org-tools--heading-body))
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
@@ -546,7 +566,6 @@ The following tools are still very much WIP, and I think they're self-explanator
 Retrieve the headings where the heading matches query..
 #+begin_src elisp
 (defun gptel-org-tools--org-ql-select-headings (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
     (get-buffer buf)
     `(heading ,query)
@@ -554,20 +573,20 @@ Retrieve the headings where the heading matches query..
 		(concat
 		 (buffer-substring-no-properties
 		  (line-beginning-position)
-		  (line-end-position)))))))
+		  (line-end-position))))))
 
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
 	      :function #'gptel-org-tools--org-ql-select-headings
 	      :name "org-ql-select-headings"
-	      :description "Retreive matching headings from buffer using org-ql-select. Matches only against heading. Using filename fails."
+	      :description "Retreive matching headings from buffer. Matches only a single string. Using filename fails."
 	      :args (list '(:name "buffer"
 			    :type string
 			    :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings against."))
+			    :description "The keyword to match entry headings against."))
 	      :category "org-ql"))
 #+end_src
 
@@ -575,15 +594,10 @@ Retrieve the headings where the heading matches query..
 Retrieve all the headings where either heading or content matches query.
 #+begin_src elisp
 (defun gptel-org-tools--org-ql-select-headings-rifle (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
-    (get-buffer buf)
-    `(rifle ,query)
-    :action #'(lambda ()
-			(concat
-			 (buffer-substring-no-properties
-			  (line-beginning-position)
-			  (line-end-position)))))))
+      (get-buffer buf)
+      `(rifle ,query)
+      :action #'gptel-org-tools--heading))
 
 
 (add-to-list 'gptel-org-tools
@@ -596,7 +610,7 @@ Retrieve all the headings where either heading or content matches query.
 			    :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings against."))
+			    :description "The keyword to match entry headings against."))
 	      :category "org-ql"))
 #+end_src
 
@@ -604,17 +618,10 @@ Retrieve all the headings where either heading or content matches query.
 This pulls all the headings (and their contents) when they match tags (without inheritance.)
 #+begin_src elisp
 (defun gptel-org-tools--org-ql-select-tags-local (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
     (get-buffer buf)
     `(tags-local ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
+    :action #'gptel-org-tools-heading-body))
 
 
 (add-to-list 'gptel-org-tools
@@ -635,18 +642,10 @@ This pulls all the headings (and their contents) when they match tags (without i
 This pulls all the headings (and their contents) when they match tags (with inheritance; if a parent entry has the tag, descendant entries do, too.)
 #+begin_src elisp
 (defun gptel-org-tools--org-ql-select-tags (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
     (get-buffer buf)
     `(tags ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
-
+    :action #'gptel-org-tools--heading-body))
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
@@ -658,27 +657,20 @@ This pulls all the headings (and their contents) when they match tags (with inhe
 			    :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings against."))
+			    :description "The keyword to match entry headings against."))
 	      :category "org-ql"))
 #+end_src
 
 ***** org-ql-select-rifle
 And, the "grab everything that matches" tool.
 #+begin_src elisp
-(defun hash-gptl-org-tools--org-ql-select-rifle ()
-    (let ((org-ql-cache (make-hash-table))
-	  (buffer (get-buffer buf)))
+(defun gptel-org-tools--org-ql-select-rifle (buf query)
+    (let ((buffer (get-buffer buf)))
     (if buffer
         (org-ql-select
           buffer
           `(rifle ,query)
-          :action #'(lambda ()
-                       (concat
-                        (buffer-substring-no-properties
-                         (line-beginning-position)
-                         (progn
-                           (outline-next-heading)
-                           (line-beginning-position))))))
+          :action #'gptel-org-tools--heading-subtree)
       (message "Buffer '%s' does not exist." buf))))
 
 (add-to-list 'gptel-org-tools
@@ -691,25 +683,18 @@ And, the "grab everything that matches" tool.
 			    :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings and content against."))
+			    :description "A single keyword to search for."))
 	      :category "org-ql"))
 #+end_src
 
 ***** org-ql-select-agenda-tags-local
 This pulls all the headings (and their contents) when they match tags (without inheritance.)
 #+begin_src elisp
-(defun gptel-org-tools--org-ql-select-agenda-tags (query)
-  (let ((org-ql-cache (make-hash-table)))
+(defun gptel-org-tools--org-ql-select-agenda-tags-local (query)
   (org-ql-select
     (org-agenda-files)
     `(tags-local ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
+    :action #'gptel-org-tools--heading-body))
 
 
 (add-to-list 'gptel-org-tools
@@ -719,7 +704,7 @@ This pulls all the headings (and their contents) when they match tags (without i
 	      :description "Run simple word query  against all files in (org-agenda-files). WITHOUT tag inheritance, only directly tagged headings."
 	      :args (list '(:name "query"
 			    :type string
-			    :description "The string to match entry tags against."))
+			    :description "A single word to scan for."))
 	      :category "org-ql"))
 #+end_src
 
@@ -727,18 +712,10 @@ This pulls all the headings (and their contents) when they match tags (without i
 This pulls all the headings (and their contents) when they match tags (with inheritance; if a parent entry has the tag, descendant entries do, too.)
 #+begin_src elisp
 (defun gptel-org-tools--org-ql-select-agenda-tags (query)
-  (let ((org-ql-cache (make-hash-table)))
   (org-ql-select
     (org-agenda-files)
     `(tags ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
-
+    :action #'gptel-org-tools--heading-subtree))
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
@@ -747,7 +724,7 @@ This pulls all the headings (and their contents) when they match tags (with inhe
 	      :description "Run simple word query  against all files in (org-agenda-files). WITH tag inheritance."
 	      :args (list '(:name "query"
 			    :type string
-			    :description "The string to match entry tags against."))
+			    :description "A single word to scan for."))
 	      :category "org-ql"))
 #+end_src
 
@@ -757,24 +734,17 @@ And for the entire =(org-agenda-files)=.
 Note that I define my agenda in this way:
 
 #+begin_src elisp :tangle no
-(setq org-agenda-files (directory-files-recursively "~/enc/org/" ".org$"))1
+(setq org-agenda-files (directory-files-recursively "~/enc/org/" ".org$"))
 #+end_src
 
 This means that /every org-mode file I have/ is part of this search.
 
 #+begin_src elisp
 (defun gptel-org-tools--org-ql-select-agenda-rifle (query)
-  (let ((org-ql-cache (make-hash-table)))
   (org-ql-select
     (org-agenda-files)
     `(rifle ,query)
-    :action #'(lambda ()
-                (concat
-                 (buffer-substring-no-properties
-                  (line-beginning-position)
-                  (progn
-                    (outline-next-heading)
-                    (line-beginning-position))))))))
+    :action #'gptel-org-tools--heading-subtree))
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
@@ -783,7 +753,7 @@ This means that /every org-mode file I have/ is part of this search.
 	      :description "Run simple word query against all files in (org-agenda-files)"
 	      :args (list '(:name "query"
 			    :type string
-			    :description "The string to match entry headings and content against."))
+			    :description "The keyword to match entry headings and content against."))
 	      :category "org-ql"))
 #+end_src
 
diff --git a/gptel-org-tools.el b/gptel-org-tools.el
index 800c57b..22dc33e 100644
--- a/gptel-org-tools.el
+++ b/gptel-org-tools.el
@@ -1,5 +1,3 @@
-(defvar gptel-org-tools-skip-heading-extraction '())
-
 ;;; gptel-org-tools.el --- LLM Tools for org-mode interaction.  -*- lexical-binding: t; -*-
 
 ;; Copyright (C) 2025  Phil Bajsicki
@@ -38,6 +36,15 @@
 
 (defvar gptel-org-tools '())
 
+(defvar gptel-org-tools-skip-heading-extraction '())
+
+(defun gptel-org-tools--heading ()
+  (concat
+   (buffer-substring-no-properties
+    (line-beginning-position)
+    (line-end-position))
+   '"\n---\n"))
+
 (defun gptel-org-tools--heading-body ()
     (concat
      (buffer-substring-no-properties
@@ -64,7 +71,8 @@
 	      :description "Access the list of buffers open in Emacs, including file names and full paths."
 	      :args (list '(:name "arg"
 			    :type string
-			    :description "Does nothing."))
+			    :description "Does nothing."
+			    :optional t))
 	      :category "emacs"))
 
 (add-to-list 'gptel-org-tools
@@ -86,7 +94,9 @@
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
 	      :function (lambda (filename)
-			  (bufferp (find-buffer-visiting (expand-file-name filename))))
+			  (concat
+			   (bufferp (find-buffer-visiting (expand-file-name filename)))
+			   "\n---\nTool execution complete. Proceed with next step."))
 	      :name "find-buffer-visiting"
 	      :description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)"
 	      :args (list '(:name "filename"
@@ -99,7 +109,9 @@
 	      :function (lambda (file)
 			   (with-current-buffer (get-buffer-create file)
 			     (insert-file-contents file)
-			     (current-buffer)))
+			     (concat
+			      (current-buffer)
+			      "\n---\nTool execution complete. Proceed with next step.")))
 	      :name "open-file-inactive"
 	      :description "Open the file in a background buffer. This doesn't interfere with the user."
 	      :args (list '(:name "file"
@@ -163,9 +175,7 @@
       (user-error "Buffer %s has too many headings, use org-extract-tags or org-ql-select-rifle." buffer)
   (with-current-buffer buffer
     (org-map-entries
-     #'(buffer-substring-no-properties
-	(line-beginning-position)
-	(line-end-position))
+     #'gptel-org-tools--heading
      t
      'file))))
 
@@ -215,7 +225,6 @@
 	      :category "org"))
 
 (defun gptel-org-tools--org-ql-select-headings (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
     (get-buffer buf)
     `(heading ,query)
@@ -223,32 +232,27 @@
 		(concat
 		 (buffer-substring-no-properties
 		  (line-beginning-position)
-		  (line-end-position)))))))
+		  (line-end-position))))))
 
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
 	      :function #'gptel-org-tools--org-ql-select-headings
 	      :name "org-ql-select-headings"
-	      :description "Retreive matching headings from buffer using org-ql-select. Matches only against heading. Using filename fails."
+	      :description "Retreive matching headings from buffer. Matches only a single string. Using filename fails."
 	      :args (list '(:name "buffer"
 			    :type string
 			    :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings against."))
+			    :description "The keyword to match entry headings against."))
 	      :category "org-ql"))
 
 (defun gptel-org-tools--org-ql-select-headings-rifle (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
-    (get-buffer buf)
-    `(rifle ,query)
-    :action #'(lambda ()
-			(concat
-			 (buffer-substring-no-properties
-			  (line-beginning-position)
-			  (line-end-position)))))))
+      (get-buffer buf)
+      `(rifle ,query)
+      :action #'gptel-org-tools--heading))
 
 
 (add-to-list 'gptel-org-tools
@@ -261,21 +265,14 @@
 			    :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings against."))
+			    :description "The keyword to match entry headings against."))
 	      :category "org-ql"))
 
 (defun gptel-org-tools--org-ql-select-tags-local (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
     (get-buffer buf)
     `(tags-local ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
+    :action #'gptel-org-tools-heading-body))
 
 
 (add-to-list 'gptel-org-tools
@@ -292,18 +289,10 @@
 	      :category "org-ql"))
 
 (defun gptel-org-tools--org-ql-select-tags (buf query)
-  (let ((org-ql-cache (make-hash-table)))
     (org-ql-select
     (get-buffer buf)
     `(tags ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
-
+    :action #'gptel-org-tools--heading-body))
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
@@ -315,23 +304,16 @@
 			    :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings against."))
+			    :description "The keyword to match entry headings against."))
 	      :category "org-ql"))
 
-(defun hash-gptl-org-tools--org-ql-select-rifle ()
-    (let ((org-ql-cache (make-hash-table))
-	  (buffer (get-buffer buf)))
+(defun gptel-org-tools--org-ql-select-rifle (buf query)
+    (let ((buffer (get-buffer buf)))
     (if buffer
         (org-ql-select
           buffer
           `(rifle ,query)
-          :action #'(lambda ()
-                       (concat
-                        (buffer-substring-no-properties
-                         (line-beginning-position)
-                         (progn
-                           (outline-next-heading)
-                           (line-beginning-position))))))
+          :action #'gptel-org-tools--heading-subtree)
       (message "Buffer '%s' does not exist." buf))))
 
 (add-to-list 'gptel-org-tools
@@ -344,21 +326,14 @@
 			    :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.")
 			  '(:name "query"
 			    :type string
-			    :description "The string to match entry headings and content against."))
+			    :description "A single keyword to search for."))
 	      :category "org-ql"))
 
-(defun gptel-org-tools--org-ql-select-agenda-tags (query)
-  (let ((org-ql-cache (make-hash-table)))
+(defun gptel-org-tools--org-ql-select-agenda-tags-local (query)
   (org-ql-select
     (org-agenda-files)
     `(tags-local ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
+    :action #'gptel-org-tools--heading-body))
 
 
 (add-to-list 'gptel-org-tools
@@ -368,22 +343,14 @@
 	      :description "Run simple word query  against all files in (org-agenda-files). WITHOUT tag inheritance, only directly tagged headings."
 	      :args (list '(:name "query"
 			    :type string
-			    :description "The string to match entry tags against."))
+			    :description "A single word to scan for."))
 	      :category "org-ql"))
 
 (defun gptel-org-tools--org-ql-select-agenda-tags (query)
-  (let ((org-ql-cache (make-hash-table)))
   (org-ql-select
     (org-agenda-files)
     `(tags ,query)
-    :action #'(lambda ()
-		(concat
-		 (buffer-substring-no-properties
-		  (line-beginning-position)
-		  (progn
-		    (outline-next-heading)
-		    (line-beginning-position))))))))
-
+    :action #'gptel-org-tools--heading-subtree))
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
@@ -392,21 +359,14 @@
 	      :description "Run simple word query  against all files in (org-agenda-files). WITH tag inheritance."
 	      :args (list '(:name "query"
 			    :type string
-			    :description "The string to match entry tags against."))
+			    :description "A single word to scan for."))
 	      :category "org-ql"))
 
 (defun gptel-org-tools--org-ql-select-agenda-rifle (query)
-  (let ((org-ql-cache (make-hash-table)))
   (org-ql-select
     (org-agenda-files)
     `(rifle ,query)
-    :action #'(lambda ()
-                (concat
-                 (buffer-substring-no-properties
-                  (line-beginning-position)
-                  (progn
-                    (outline-next-heading)
-                    (line-beginning-position))))))))
+    :action #'gptel-org-tools--heading-subtree))
 
 (add-to-list 'gptel-org-tools
 	     (gptel-make-tool
@@ -415,7 +375,7 @@
 	      :description "Run simple word query against all files in (org-agenda-files)"
 	      :args (list '(:name "query"
 			    :type string
-			    :description "The string to match entry headings and content against."))
+			    :description "The keyword to match entry headings and content against."))
 	      :category "org-ql"))
 
 (provide 'gptel-org-tools)