.gitignore | ||
gptel-org-tools.el | ||
LICENSE | ||
README.org |
gptel-org-tools
- Disclaimer
- Intro
- Installation:
- Code
Disclaimer
This package is an active privacy risk. It allows the LLM to autonomously expand its context in any direction it chooses.
Only connect to third-party systems if you understand and accept the risk of *any* of your files becoming publicly accessible.
Intro
This is a collection of tools I wrote to help me review my life.
Summary:
- An explanation of each tool is below.
- The tools are tangled into
gptel-org-tools.el
from this file.
Premise:
- LLMs are not very smart and unreliable, but they do okay with basic text comprehension, and can generate completions/ responses which fit the vibe of the request.
- LLMs can then fit the vibe of the request better if there is more relevant and accurate request data in their context window.
- LLMs lose the vibe when there's irrelevant (garbage) data in the context window.
Therefore (philosophy, I may change my mind later):
- Each tool has to limit the garbage data it returns as much as possible.
- Each tool has to have as little LLM-facing documentation as possible (while being enough for the LLM to understand and use the tool.) Extra words is extra garbage.
-
Each tool should handle as wide an range of even remotely valid inputs from an LLM as possible.
- Different models are biased toward different outputs.
user-error
isn't addressable when a model only has 5 minutes worth of memory.- Failure caused by LLM mis-use should be solved in such a way that failure becomes increasingly less likely.
- We never know when an LLM will respond with a string, json, s-exp, or ASCII codes (no, that last one hasn't happened… yet).
-
Each tool should work in harmony with other tools to form a toolbox which serves these goals.
- Avoid tool overlap.
- One tool for one task.
- Tool names are documentation.
- Argument names are documentation.
- As few arguments per tool as possible.
Installation:
I only use Doom Emacs, so here's how I load:
packages.el:
(package! gptel-org-tools
:recipe (:host nil
:repo "https://git.bajsicki.com/phil/gptel-org-tools"))
config.el:
(require 'gptel-org-tools)
(setq gptel-tools gptel-org-tools)
This will overwrite any other tools you have defined before this call takes place.
If you want to just append them to your existing tool list:
config.el:
(require 'gptel-org-tools)
(mapcar (lambda (tool) (cl-pushnew tool gptel-tools)) gptel-org-tools)
Code
Preamble
Stuff, headers, etc.
;;; gptel-org-tools.el --- LLM Tools for org-mode interaction. -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Phil Bajsicki
;; Author: Phil Bajsicki <phil@bajsicki.com>
;; Keywords: extensions, comm, tools, matching, convenience,
;;
;; Author: Phil Bajsicki <phil@bajsicki.com>
;; Version: 0.0.2
;; Package-Requires: ((emacs "30.1") (gptel 0.9.8) (org-ql 0.9))
;; URL: https://github.com/phil/gptel-org-tools
;; SPDX-License-Identifier: GPL-3.0
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, version 3 of the License.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;; This file is NOT part of GNU Emacs.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; All documentation regarding these functions is in the README.org file.
;; Repository: https://git.bajsicki.com/phil/gptel-org-tools
;;; Code:)
gptel-org-tools
Collects into gptel-org-tools
list, distinct from gptel-tools
(defvar gptel-org-tools '())
Variables
skip-heading-extraction
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.
(defvar gptel-org-tools-skip-heading-extraction '())
Use setq
in your configuration, with a list of buffer names whose headings you never want to extract. E.g.
(setq gptel-org-tools-skip-heading-extraction '("journal.org" ".org"))
result-limit
It is with great disappointment that I admit to a terrible flaw of this package.
When in local use, with a large dataset, a lot of the commands which return entire org-mode entries… end up grabbing way, way too much text.
This can end up with Emacs freezing as it tries to parse immeasurable amounts of text (for fonts, formatting, etc) at the same time as the LLM is screeching in electric pain trying to actually process all that data.
To that end, the stop-gap solution is to implement a hard limit on the amount of text a tool can return.
I don't think there's much reason to ever allow partial results, which leads me to this:
(defvar gptel-org-tools-result-limit 20000)
(defun gptel-org-tools--result-limit (result)
(if (>= (length (format "%s" result)) gptel-org-tools-result-limit)
(format "Results over %s character. Please try with a more specific query." gptel-org-tools-result-limit)
result))
Use setq
in your configuration, e.g.:
(setq gptel-org-tools-result-limit 12000)
This will prevent tools from returning results longer than 12,000 characters. Instead, the LLM will receive a message saying it should be much more specific in its queries, which will hopefully guide it to be more specific.
By default the limit is 20k, but for my use 12k seems like a reasonable middle-ground (24GB RAM and long query chains.)
The functionality for withholding results is only applied to select functions that are known to cause issues.
Helper Functions
These abstract away some of the tool definitions. They're called from each function, depending on their intended output.
Return heading (line)
(defun gptel-org-tools--heading ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(line-end-position))))
The tools
Emacs
These tools are primarily concerned with Emacs, Emacs Lisp, and files-or-buffers.
eval [disabled by default]
Dangerous, but occasionally useful for pure chaos and amusement… I would like to say. But in actuality, especially with the 'smarter' models, they can surprise with the varied approaches they have to problem-solving.
E.g. can't find the right org-ql-select
query? Let's use an eldritch abomination of a regular expression while mapping over each heading in the org-mode buffer and do a close enough job.
Highly not recommended, but sometimes an LLM can pull a rabbit out of pure entropy.
(defun gptel-org-tools--eval (elisp)
(unless (stringp elisp) (error "elisp code must be a string"))
(with-temp-buffer
(insert (eval (read elisp)))
(buffer-string)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--eval
:name "gptel-org-tools--eval"
:description "Execute Emacs Lisp code"
:args (list '(:name "eval"
:type string
:description "The Emacs Lisp code to evaluate."))
:category "emacs"
:confirm t))
list-buffers
Not using ibuffer
to avoid customization differences between users. Argument required by gptel
.
(defun gptel-org-tool--list-buffers (&optional arg)
"Return list of buffers."
(list-buffers-noselect)
(with-current-buffer "*Buffer List*"
(let ((content (buffer-string)))
content)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tool--list-buffers
:name "gptel-org-tool--list-buffers"
:description "Access the list of buffers open in Emacs, including file names and full paths."
:args (list '(:name "arg"
:type string
:description "Does nothing."
:optional t))
:category "emacs"))
dired
I originally wanted to use directory-files
for this, but it turns out that it's much easier to use dired
for this.
You can customize the function to point to your org directory, if you wish. I'm not entirely sure if it makes a big difference, but may keep your LLM from getting lost in the sauce.
(defun gptel-org-tools--dir (dir)
"Return directory listing."
(with-temp-buffer
(dired (or dir "~"))
(let ((content (buffer-string)))
(kill-buffer (current-buffer))
content)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--dir
:name "gptel-org-tools--dir"
:description "List directory contents"
:args (list '(:name "dir"
:type string
:description "Directory path"
:optional t))
:category "filesystem"))
find-buffer-visiting [disabled by default]
Disabled for now, as it's causing some issues.
(defun gptel-org-tools--find-buffer-visiting (filename)
"Return the buffer visiting file FILENAME."
(concat
(bufferp
(find-buffer-visiting
(expand-file-name filename)))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--find-buffer-visiting
:name "gptel-org-tools--find-buffer-visiting"
:description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)"
:args (list '(:name "filename"
:type string
:description "The filename to compare to open buffers."))
:category "org-mode"))
open-file-inactive
Opens a file into an inactive (background) buffer for processing.
(defun gptel-org-tools--open-file-inactive (file)
"Open FILE in a buffer."
(with-current-buffer (get-buffer-create file)
(insert-file-contents file)
(concat
(current-buffer))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--open-file-inactive
:name "gptel-org-tools--open-file-inactive"
:description "Open the file in a background buffer. This doesn't interfere with the user."
:args (list '(:name "file"
:type string
:description "Path to file.."))
:category "filesystem"))
read-file-contents
(defun gptel-org-tools--read-file-contents (file)
"Return contents of FILE."
(with-temp-buffer
(insert-file-contents (expand-file-name filename))
(concat
(buffer-string))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--read-file-contents
:name "gptel-org-tools--read-file-contents"
:description "Read and return the contents of a specified file."
:args (list '(:name "filename"
:type string
:description "The filename to read."))
:category "org-mode"
:confirm t))
describe-variable
(defun gptel-org-tools--describe-variable (var)
"Return documentation for VAR."
(let ((symbol (intern var)))
(if (boundp symbol)
(prin1-to-string (symbol-value symbol))
(format "Variable %s is not bound." var))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--describe-variable
:name "gptel-org-tools--describe-variable"
:description "See variable contents"
:args (list '(:name "var"
:type string
:description "Variable name"))
:category "emacs"))
describe-function
(defun gptel-org-tools--describe-function (fun)
"Return documentation for FUN."
(let ((symbol (intern fun)))
(if (fboundp symbol)
(prin1-to-string (documentation symbol 'function))
(format "Function %s is not defined." fun))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--describe-function
:name "gptel-org-tools--describe-function"
:description "See function description"
:args (list '(:name "fun"
:type string
:description "Function name"
:optional t))
:category "emacs"))
Org-mode
Given the complexity of org-mode and org-ql queries, the following points are important:
- Separate tools for each key predicate.
- Separate tools for either single-buffer scans, or
org-agenda-files
.
These tools will still pull garbage in, inevitably, especially for queries such as '(rifle "cat")
, which will then also match "cataclysm", "cataract", "vocation", etc. etc.
Ensuring the functions are easy to call and very specific will help this happen significantly less than if we just give the LLM org-ql-select
and let it go wild.
LLMs are not intelligent, despite claims to the contrary.
org-extract-tags
(defun gptel-org-tools--org-extract-tags (buffer)
"Return all tags from BUFFER."
(with-current-buffer buffer
(let ((tags '()))
(org-map-entries
(lambda ()
(let* ((components (org-heading-components))
(tag-string (car (last components))))
(when tag-string
(dolist (tag (split-string tag-string ":" t))
(push tag tags))))))
(sort (-uniq tags) #'string<))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-extract-tags
:name "gptel-org-tools--org-extract-tags"
:description "Extract all unique tags from an org-mode buffer"
:args (list '(:name "buffer"
:type string
:description "The Org buffer to extract tags from."))
:category "org-mode"))
org-extract-headings
(defun gptel-org-tools--org-extract-headings (buffer)
"Return all headings from BUFFER."
(if (member buffer gptel-org-tools-skip-heading-extraction)
(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
#'gptel-org-tools--heading
t
'file))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-extract-headings
:name "gptel-org-tools--org-extract-headings"
:description "Extract all headings from an org-mode buffer"
:args (list '(:name "buffer"
:type string
:description "The Org buffer to extract headings from."))
:category "org-mode"))
org-ql-select [disabled by default]
Ugly. LLMs sometimes return a sexp, and sometimes a quoted string for query
. So I had to work around that. It works… some of the time.
In general, this is deprecated, but left here for historical reasons. I may resurrect this if I find a good way of prompting the LLM to structure queries well.
(defun gptel-org-tools--org-ql-select (buf query)
"Return entries matching QUERY from BUFFER.
QUERY can be any valid org-ql-select query."
(let ((result
(org-ql-select (get-buffer buf)
(if (stringp query)
(read query)
query
:action #'gptel-org-tools--heading-body))))
(gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select
:name "gptel-org-tools--org-ql-select"
:description "Run org-ql-select against buffer with query. Using filename fails."
:args (list '(:name "buffer"
:type string
:description "The name of the buffer. Can be multiple buffers. See the NAME column in `list-buffers`.")
'(:name "query"
:type string
:description "The query to pass into org-ql-select. See org-ql documentation for syntax. Usually `(tags \"tag1\" \"tag2\")` is sufficient. Possible predicates: `tags` (finds both local and inherited tags), `tags-local` (finds only local tags), `rifle` (matches against both heading and body text). This is a sexp, not a string."))
:category "org"))
Somewhat working tools
org-ql-select-by-date
My journal is a single file, with a hierarchy like so:
* [YYYY]
** [YYYY-MM]
*** [YYYY-MM-DD Day HH:MM])
This tool kinda sorta works. It has two main failure modes:
- the LLM chooses to apply this tool to buffers other than my journal.
- the LLM ignores the existence of this tool and instead uses regexp with terms… and explodes the context window.
The reason for this tool existing (it's the same as select-heading), is that it may help guide the LLM toward higher accuracy. Unclear if that's the case.
(defun gptel-org-tools--org-ql-select-by-date (buf date)
"Returns all timestamped headings matching the specified date or date range.
The date can be in the format YYYY, YYYY-MM, or YYYY-MM-DD.
BUFFER is the name of the buffer to search.
DATE is the date or date range to match."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if (bufferp buffer)
(if (eq mode 'org-mode)
(let ((result
(org-ql-select buffer
`(heading ,date)
:action #'gptel-org-tools--heading-subtree)))
(gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-by-date
:name "gptel-org-tools--org-ql-select-by-date"
:description "Returns all timestamped headings matching query. Query may be: YYYY, YYYY-MM, YYYY-MM-DD."
:args (list '(:name "buffer"
:type string
:description "Buffer name.")
'(:name "date"
:type string
:description "Date string in YYYY or YYYY-MM format. No <, >, [, ]. Just the numbers and dashes."))
:category "org"))
org-agenda-seek
This is still work in progress, the idea is to have the LLM check my calendar and see what my plans are. I have not had time to really dig into this yet.
It works, in principle, but I haven't been able to find a use for it yet. The real challenge is in building a context where the tools integrate with each-other in a way that makes sense. For now, this exists.
(defun gptel-org-tools--org-agenda-seek (days)
"Return the results of org-agenda-list spanning now to DAYS."
(with-temp-buffer
(org-agenda-list (or days 14))
(let ((content (buffer-string)))
(kill-buffer (current-buffer))
(gptel-org-tools--result-limit content))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-agenda-seek
:name "gptel-org-tools--org-agenda-seek"
:description "Get user's agenda (tasking) spanning X days from now. This can be used to get all agenda tasks that are due or scheduled in the next X days, or in the past X days, depending on whether the days argument is positive or negative. Example: get all agenda tasks due in the next 7 days: \"7\""
:args (list '(:name "days"
:type integer
:description "Days. Positive = future. Negative = past. Default: 14"))
:category "org"))
Completely WIP tools
org-ql-select-headings
Retrieve the headings where the heading matches query. This is very much the same as org-ql-select-by-date
, but the descriptions are different to ensure the LLM knows what to do.
(defun gptel-org-tools--org-ql-select-headings (buf query)
"Return headings matching QUERY from BUFFER."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(if (eq mode 'org-mode)
(let ((result
(org-ql-select
(get-buffer buf)
`(heading ,query)
:action #''gptel-org-tools--heading)))
(gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-headings
:name "gptel-org-tools--org-ql-select-headings"
: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 ~list-buffers~.")
'(:name "query"
:type string
:description "The string to match entry headings against."))
:category "org-ql"))
org-ql-select-headings-rifle
Retrieve all the headings where either heading or content matches query.
(defun gptel-org-tools--org-ql-select-headings-rifle (buf query)
"Return headings of entries (body included) that match keyword QUERY from BUFFER."
(let ((result
(org-ql-select
(get-buffer buf)
`(rifle ,query)
:action #'gptel-org-tools--heading)))
(gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-headings-rifle
:name "gptel-org-tools--org-ql-select-headings-rifle"
:description "Retreive headings from buffer using org-ql-select. Matches against both heading and content. Using filename fails."
:args (list '(:name "buffer"
:type string
:description "The name of the buffer. See the NAME column in ~list-buffers~.")
'(:name "query"
:type string
:description "The string to match entry headings against."))
:category "org-ql"))
org-ql-select-tags-local
This pulls all the headings (and their contents) when they match tags (without inheritance.)
(defun gptelg-tools--org-ql-select-tags-local (buf query)
"Return entries whose tags match QUERY in BUFFER, without inheritance."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(if (eq mode 'org-mode)
(let ((result
(org-ql-select
(get-buffer buf)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-tags-local
:name "gptel-org-tools--org-ql-select-tags-local"
:description "Run org-ql-select-tags-local against buffer with query. No tag inheritance."
:args (list '(:name "buffer"
:type string
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
'(:name "query"
:type string
:description "The string match entry tags against."))
:category "org-ql"))
org-ql-select-tags-local-count
This pulls all the local tags (without inheritance) from buffer, and returns the number of these tagged headings.
(defun gptel-org-tools--org-ql-select-tags-local-count (buf query)
"Return count of entries tagged QUERY in BUFFER."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(if (eq mode 'org-mode)
(length (org-ql-select
(get-buffer buf)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-tags-local-count
:name "gptel-org-tools--org-ql-select-tags-local"
:description "Get count of matching tags from buffer. No tag inheritance."
:args (list '(:name "buffer"
:type string
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
'(:name "query"
:type string
:description "The string match entry tags against."))
:category "org-ql"))
org-ql-select-tags
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.)
(defun gptel-org-tools--org-ql-select-tags (buf query)
"Return every entry tagged QUERY from BUFFER."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(if (eq mode 'org-mode)
(let ((result
(org-ql-select
(get-buffer buf)
`(tags ,query)
:action #'gptel-org-tools--heading-body))
(gptel-org-tools--result-limit result)))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-tags
:name "gptel-org-tools--org-ql-select-tags"
:description "Run org-ql-select-tags against buffer with query. Supports tag inheritance."
:args (list '(:name "buffer"
:type string
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
'(:name "query"
:type string
:description "The string to match entry headings against."))
:category "org-ql"))
org-ql-select-rifle
And, the "grab everything that matches" tool.
(defun gptel-org-tools--org-ql-select-rifle (buf query)
"Return every entry matching keyword QUERY from BUFFER."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(if (eq mode 'org-mode)
(let ((result
(org-ql-select
buffer
`(rifle ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-rifle
:name "gptel-org-tools--org-ql-select-rifle"
:description "Run org-ql-select-rifle against buffer with query."
:args (list '(:name "buffer"
:type string
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
'(:name "query"
:type string
:description "A single string to search for."))
:category "org-ql"))
org-ql-select-all-tags-local
This pulls all the headings (and their contents) when they match tags (without inheritance.)
(defun gptel-org-tools--org-ql-select-all-tags-local (query)
"Return entries whose tags match QUERY in org-agenda-files.
QUERY is the tag to search for."
(let ((result
(org-ql-select
(org-agenda-files)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-all-tags-local
:name "gptel-org-tools--org-ql-select-all-tags-local"
:description "Run single string query against all files in (org-agenda-files). WITHOUT tag inheritance, only directly tagged headings."
:args (list '(:name "query"
:type string
:description "A single word to scan for."))
:category "org-ql"))
org-ql-select-all-tags
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.)
(defun gptel-org-tools--org-ql-select-all-tags (query)
"Return entries whose tags match QUERY,
with inheritance, in org-agenda-files.
QUERY is the tag to search for."
(let ((result
(org-ql-select
(org-agenda-files)
`(tags ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-all-tags
:name "gptel-org-tools--org-ql-select-all-tags"
:description "Run single string query against all files in (org-agenda-files). WITH tag inheritance."
:args (list '(:name "query"
:type string
:description "A simple (single) string to scan for."))
:category "org-ql"))
org-ql-select-all-rifle
And for the entire (org-agenda-files)
.
Note that I define my agenda in this way:
(setq org-agenda-files (directory-files-recursively "~/enc/org/" ".org$"))
This means that every org-mode file I have is part of this search. If you're using a different set-up, or want to only use the LLM on specific files, then you should modify this function appropriately.
(defun gptel-org-tools--org-ql-select-all-rifle (query)
"Return entries containing QUERY from org-agenda-files.
QUERY is the keyword to search for."
(let ((result
(org-ql-select
(org-agenda-files)
`(rifle ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-all-rifle
:name "gptel-org-tools--org-ql-select-all-rifle"
:description "Run simple (single) string against ALL org-mode files (notes)."
:args (list '(:name "query"
:type string
:description "The string to match entry headings and content against."))
:category "org-ql"))
org-ql-select-all-regexp
(defun gptel-org-tools--org-ql-select-all-regexp (query)
"Return all entries matching regexp QUERY in org-agenda-files.
QUERY is a regular expression."
(let ((result
(org-ql-select
(org-agenda-files)
`(regexp ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-all-regexp
:name "gptel-org-tools--org-ql-select-all-regexp"
:description "Run regexp on ALL files at once."
:args (list '(:name "query"
:type string
:description "Regexp, Emacs Lisp format. Automatically wrapped in a word boundary by the tool."))
:category "org-ql"))