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)