Improve org-ql tools' argument descriptions

This commit is contained in:
Phil Bajsicki 2025-04-16 17:00:18 +02:00
parent de14a8d066
commit 6240a40ff0
2 changed files with 110 additions and 180 deletions

View file

@ -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

View file

@ -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)