Fix select-by-date

This commit is contained in:
Phil Bajsicki 2025-04-28 20:04:46 +02:00
parent 3191b0c887
commit d8f64b9d89
2 changed files with 264 additions and 342 deletions

View file

@ -2,15 +2,16 @@
#+author: Phil Bajsicki #+author: Phil Bajsicki
#+auto_tangle: t #+auto_tangle: t
#+PROPERTY: header-args :elisp :tangle gptel-org-tools.el #+PROPERTY: header-args :elisp :tangle gptel-org-tools.el
* 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 * Intro
This is a collection of tools I wrote to help me review my life. This is a collection of tools I wrote to help me review my life.
To that end, it's proven useful enough.
Summary: Summary:
- An explanation of each tool is below. - An explanation of each tool is below.
- The tools are tangled into =gptel-org-tools.el= from this file. - The tools are tangled into =gptel-org-tools.el= *from this file*.
- There are no docstrings unless there are, in which case that's a mistake and I apologize.
Premise: 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 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.
@ -24,60 +25,16 @@ Therefore (philosophy, I may change my mind later):
- Different models are biased toward different outputs. - Different models are biased toward different outputs.
- ~user-error~ isn't addressable when a model only has 5 minutes worth of memory. - ~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. - 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). - 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. - Each tool should work in harmony with other tools to form a toolbox which serves these goals.
- Avoid tool overlap. - Avoid tool overlap.
- One tool for one task. - One tool for one task.
- Tool names are documentation. - Tool names are documentation.
- Argument names are documentation. - Argument names are documentation.
- As few arguments per tool as possible. - As few arguments per tool as possible.
- Documentation strings are (ideally) for examples.
These are primarily for my own use. Please don't expect quality. * Installation:
I only use Doom Emacs, so here's how I load:
This forge is for my personal use, and as such: [[https://bajsicki.com/contact/][contact]], just in case there's questions, issues, whatnot.
* My system
I'm running LLMs exclusively locally, on the smaller side. I'm getting reasonably good results with the following set-up:
- Hardware: RX 7900XTX
- Software:
- [[https://github.com/ggml-org/llama.cpp][llama.cpp]] compiled with ROCm 6.4.
- Emacs: [[https://github.com/doomemacs/doomemacs][Doom Emacs]], [[https://github.com/karthink/gptel][gptel]], [[https://github.com/alphapapa/org-ql/][org-ql]].
** My set-up
These are the settings I use, for reproducibility.
Yes, this is somewhat odd, that I would use the deepseek option, but I have found that it handles reasoning a little bit better than gptel's openai backend.
I change models a lot, and this /just works/ for most models, even if some aren't compatible outright.
#+begin_src elisp :tangle no
(use-package! gptel)
(setq gptel-model 'llamacpp)
(setq gptel-include-reasoning t)
(setq gptel-backend (gptel-make-deepseek "llamacpp"
:host "localhost:8080"
:protocol "http"
:stream nil
:models '("llamacpp"
:capabilities (reasoning))
:request-params '(:thinking nil
:enable_thinking nil
:include_reasoning nil
:parallel_tool_calls t)))
(setf (alist-get 'org-mode gptel-prompt-prefix-alist) "@user\n")
(setf (alist-get 'org-mode gptel-response-prefix-alist) "@assistant\n")
(setq gptel-default-mode 'org-mode)
(setq gptel-use-tools t)
(setq gptel-log-level 'debug)
(setq gptel--debug t)
(require 'gptel)
#+end_src
With that out of the way, let's get to the tools.
* Usage options:
I only use Doom Emacs, so here's how I do it.
packages.el: packages.el:
#+begin_src elisp :tangle no #+begin_src elisp :tangle no
@ -193,25 +150,24 @@ Use ~setq~ in your configuration, e.g.:
(setq gptel-org-tools-result-limit 12000) (setq gptel-org-tools-result-limit 12000)
#+end_src #+end_src
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 alleviate the issue. 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.) 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. The functionality for withholding results is only applied to select functions that are known to cause issues.
** Helper Functions ** Helper Functions
These abstract away some of the tool definitions. These abstract away some of the tool definitions. They're called from each function, depending on their intended output.
*** Retrieve heading (line]) *** Return heading (line)
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--heading () (defun gptel-org-tools--heading ()
(concat (concat
(buffer-substring-no-properties (buffer-substring-no-properties
(line-beginning-position) (line-beginning-position)
(line-end-position)) (line-end-position))))
'"\n---\n"))
#+end_src #+end_src
*** Retrieve heading and body (without subheadings) *** Return heading and body (without subheadings)
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--heading-body () (defun gptel-org-tools--heading-body ()
(concat (concat
@ -219,36 +175,21 @@ These abstract away some of the tool definitions.
(line-beginning-position) (line-beginning-position)
(progn (progn
(outline-next-heading) (outline-next-heading)
(line-beginning-position))) (line-beginning-position)))))
"---\n"))
#+end_src #+end_src
*** Retrieve heading and subheadings (until next same-level heading) *** Return heading and subheadings (until next same-level heading)
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--heading-subtree () (defun gptel-org-tools--heading-subtree ()
(concat (concat
(buffer-substring-no-properties (buffer-substring-no-properties
(line-beginning-position) (line-beginning-position)
(org-end-of-subtree)) (org-end-of-subtree))))
"---\n"))
#+end_src #+end_src
** 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.
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 ** The tools
*** Emacs *** Emacs
These tools are primarily concerned with Emacs, Emacs Lisp, and files-or-buffers. These tools are primarily concerned with Emacs, Emacs Lisp, and files-or-buffers.
**** eval **** eval [disabled by default]
Dangerous, but occasionally useful for pure chaos and amusement... 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. 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.
@ -276,18 +217,14 @@ Highly not recommended, but sometimes an LLM can pull a rabbit out of pure entro
#+end_src #+end_src
**** list-buffers **** list-buffers
I wanted the assistant to have an easier time finding my files and buffers, and this has proven to be a great choice. I have yet to manage to get rid of the =:args=, but having them optional/ do nothing works well enough. Not using ~ibuffer~ to avoid customization differences between users. Argument required by =gptel=.
The rationale behind using ~list-buffers~ is the same as with dired. They both display a lot of data, densely. So instead of trying to use some workaround with ~buffer-file-name~ or other functions, I'd rather just grab a 'text capture' of the same UI I'm looking at, and call it a day.
Seems to be one of the most reliable tools in the basket... mostly because
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tool--list-buffers (&optional arg) (defun gptel-org-tool--list-buffers (&optional arg)
"Return list of buffers." "Return list of buffers."
(list-buffers-noselect) (list-buffers-noselect)
(with-current-buffer "*Buffer List*" (with-current-buffer "*Buffer List*"
(let ((content (buffer-string))) (let ((content (buffer-string)))
content))) content)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -302,12 +239,10 @@ Seems to be one of the most reliable tools in the basket... mostly because
#+end_src #+end_src
**** dired **** dired
See above, same reasoning. There's very little reason to use the ~directory-files~ function for this (as in another tool I saw.) The reason is, ~directory-files~ doesn't provide nearly as much information about the items in that directory. Not even a distinction between files and directories. I originally wanted to use ~directory-files~ for this, but it turns out that it's much easier to use ~dired~ for this.
~directory-files-and-attributes~ might, but I personally found its output horrendous to read, and still somehow more expensive context-wise than just plain ol' dired.
#+begin_comment #+begin_comment
Be sure to customize the function to point to your org directory, if you wish. I find it makes a big difference. Having the argument be optional also helps when the LLM starts stumbling, as it gives it a /reset point/ so it can re-orient itself (although by that time it has usually forgotten what it was supposed to be doing...) 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.
#+end_comment #+end_comment
#+begin_src elisp #+begin_src elisp
@ -331,7 +266,7 @@ Be sure to customize the function to point to your org directory, if you wish. I
:category "filesystem")) :category "filesystem"))
#+end_src #+end_src
**** find-buffer-visiting **** find-buffer-visiting [disabled by default]
Disabled for now, as it's causing some issues. Disabled for now, as it's causing some issues.
#+begin_src elisp :tangle no :results none #+begin_src elisp :tangle no :results none
(defun gptel-org-tools--find-buffer-visiting (filename) (defun gptel-org-tools--find-buffer-visiting (filename)
@ -353,14 +288,15 @@ Disabled for now, as it's causing some issues.
#+end_src #+end_src
**** open-file-inactive **** open-file-inactive
Continuation from above. Open a file into a buffer for processing, once it's found by dired-list. Opens a file into an inactive (background) buffer for processing.
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--open-file-inactive (file) (defun gptel-org-tools--open-file-inactive (file)
"Open FILE in a buffer." "Open FILE in a buffer."
(with-current-buffer (get-buffer-create file) (with-current-buffer (get-buffer-create file)
(insert-file-contents file) (insert-file-contents file)
(concat (concat
(current-buffer)))) (current-buffer))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function #'gptel-org-tools--open-file-inactive :function #'gptel-org-tools--open-file-inactive
@ -373,15 +309,13 @@ Continuation from above. Open a file into a buffer for processing, once it's fou
#+end_src #+end_src
**** read-file-contents **** read-file-contents
This reads file contents,
#+begin_src elisp :tangle no #+begin_src elisp :tangle no
(defun gptel-org-tools--read-file-contents (file) (defun gptel-org-tools--read-file-contents (file)
"Return contents of FILE." "Return contents of FILE."
(with-temp-buffer (with-temp-buffer
(insert-file-contents (expand-file-name filename)) (insert-file-contents (expand-file-name filename))
(concat (concat
(buffer-string) (buffer-string))))
"\n---\nTool execution complete. Proceed with next step.")))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -399,9 +333,9 @@ This reads file contents,
(defun gptel-org-tools--describe-variable (var) (defun gptel-org-tools--describe-variable (var)
"Return documentation for VAR." "Return documentation for VAR."
(let ((symbol (intern var))) (let ((symbol (intern var)))
(if (boundp symbol) (if (boundp symbol)
(prin1-to-string (symbol-value symbol)) (prin1-to-string (symbol-value symbol))
(format "Variable %s is not bound." var)))) (format "Variable %s is not bound." var))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -419,9 +353,9 @@ This reads file contents,
(defun gptel-org-tools--describe-function (fun) (defun gptel-org-tools--describe-function (fun)
"Return documentation for FUN." "Return documentation for FUN."
(let ((symbol (intern fun))) (let ((symbol (intern fun)))
(if (fboundp symbol) (if (fboundp symbol)
(prin1-to-string (documentation symbol 'function)) (prin1-to-string (documentation symbol 'function))
(format "Function %s is not defined." fun)))) (format "Function %s is not defined." fun))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -436,34 +370,19 @@ This reads file contents,
#+end_src #+end_src
*** Org-mode *** Org-mode
And here we start getting into the weeds. 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=.
The core of these functions is to ensure that the LLM doesn't go astray. 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.
It will, inevitably. 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.
Then, the core of these functions is to ensure it can be gently prodded into doing what it's asked, instead of being "asked politely" until it complies.
We want to minimize the effect of entropy on the use of these tools. To that end, we're making /separate tools for separate purposes/.
This isn't proven, by any means, but given what little I know about LLMs, I believe it's easier for them to choose between many single-purpose tools, than from few multi-purpose tools.
Even us humans experience less cognitive load when we're given clear options up-front, instead of being given a multi-tool that we then need to learn our way around to get the job done.
With that secondary purpose in mind, the real aim of these tools is to /limit/ the information that the LLM has access to /only/ to that which is relevant. In the ideal.
In the real world, we'll still see garbage being pulled in, and the LLM being led astray by an unfortunate sentence, or an ~org-ql~ query that explodes the context.
At the same time, that will happen /significantly/ less than if we were to give it more freedom.
#+begin_comment #+begin_comment
LLMs are not intelligent, despite claims to the contrary. LLMs are not intelligent, despite claims to the contrary.
#+end_comment #+end_comment
**** org-extract-tags **** org-extract-tags
Pretty simple, does what it says on the tin. It gets all the tags from the =buffer=. This is useful as a first line of research for the LLM, as it can then get a surface-level understanding of the contents of a file.
This is not, by any means, sufficient, but I do tag people and specific events frequently enough that it helps save on the context window.
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-extract-tags (buffer) (defun gptel-org-tools--org-extract-tags (buffer)
"Return all tags from BUFFER." "Return all tags from BUFFER."
@ -489,11 +408,6 @@ This is not, by any means, sufficient, but I do tag people and specific events f
:category "org-mode")) :category "org-mode"))
#+end_src #+end_src
**** org-extract-headings **** org-extract-headings
But what if there's no tags related to the topic?
Then we need to pull /some/ information from the buffer, without dragging the entire 500kb in and exploding the context window.
Therefore, headings. A reasonable amount of information, and still keeping the signal-to-noise ratio pretty decent.
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-extract-headings (buffer) (defun gptel-org-tools--org-extract-headings (buffer)
"Return all headings from BUFFER." "Return all headings from BUFFER."
@ -516,19 +430,9 @@ Therefore, headings. A reasonable amount of information, and still keeping the s
:category "org-mode")) :category "org-mode"))
#+end_src #+end_src
**** org-ql-select **** org-ql-select [disabled by default]
This is the big one. It works, and it's ugly, and I'm in the middle of replacing it. The tool argument descriptions do /a lot/ of lifting here, and even then the models stumble and fall on their face very often. 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.
They also sometimes return a sexp, and sometimes a quoted string. So I had to work around that. It works... some of the time.
My current goal is to replace this monstrosity with individual functions for each of the main ~org-ql~ predicates, such that the LLMs have an easier time with the syntax, so they can just choose their desired query instead of having to both choose and properly form the syntax.
But in the interim, this works. Kind of.
#+begin_comment
Currently *not* tangled, as I'm testing breaking out each type of query into its own individual tool.
#+end_comment
#+begin_src elisp :tangle no #+begin_src elisp :tangle no
(defun gptel-org-tools--org-ql-select (buf query) (defun gptel-org-tools--org-ql-select (buf query)
@ -537,14 +441,12 @@ Currently *not* tangled, as I'm testing breaking out each type of query into its
QUERY can be any valid org-ql-select query." QUERY can be any valid org-ql-select query."
(let ((result (let ((result
(org-ql-select (get-buffer buf) (org-ql-select (get-buffer buf)
(if (stringp query) (if (stringp query)
(read query) (read query)
query query
:action #'gptel-org-tools--heading-body)))) :action #'gptel-org-tools--heading-body))))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function #'gptel-org-tools--org-ql-select :function #'gptel-org-tools--org-ql-select
@ -559,7 +461,7 @@ QUERY can be any valid org-ql-select query."
:category "org")) :category "org"))
#+end_src #+end_src
**** Somewhat working tools **** Somewhat working tools
***** org-ql-select-dates ***** org-ql-select-by-date
My journal is a single file, with a hierarchy like so: My journal is a single file, with a hierarchy like so:
#+begin_src org :tangle no #+begin_src org :tangle no
@ -574,11 +476,10 @@ This /kinda sorta/ works. The only time I have seen this fail is when the LLM ch
I doubt it'll be useful for anyone else, but it's here and fairly easy to adapt to other needs. I doubt it'll be useful for anyone else, but it's here and fairly easy to adapt to other needs.
Notice it pulls to the end of the subtree. So for months, that's at least 28 entries in a year, and during busy months, possibly hundreds. Notice it pulls to the end of the subtree. So for months, that's at least 28 entries, and during busy months, possibly hundreds.
But, any customizations to tweak this is left to the user, as everyone has their own conventions. But, any customizations to tweak this is left to the user, as everyone has their own conventions.
Testing new version: Testing new version:
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-by-date (buf date) (defun gptel-org-tools--org-ql-select-by-date (buf date)
@ -588,59 +489,32 @@ The date can be in the format YYYY, YYYY-MM, or YYYY-MM-DD.
BUFFER is the name of the buffer to search. BUFFER is the name of the buffer to search.
DATE is the date or date range to match." DATE is the date or date range to match."
(let* ((buffer (get-buffer buf)) (let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer))) (mode (buffer-local-value 'major-mode buffer)))
(if buffer (if (bufferp buffer)
(if (eq mode 'org-mode) (if (eq mode 'org-mode)
(let ((result (let ((result
(org-ql-select buffer (org-ql-select buffer
`(heading ,date) `(heading ,date)
:action #'gptel-org-tools--heading-subtree))) :action #'gptel-org-tools--heading-subtree)))
(gptel-org-tools--result-limit result)) (gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf)) (message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf)))) (message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function #'gptel-org-tools--org-ql-select-by-date :function #'gptel-org-tools--org-ql-select-by-date
:name "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." :description "Returns all timestamped headings matching query. Query may be: YYYY, YYYY-MM, YYYY-MM-DD."
:args (list '(:name "buffer" :args (list '(:name "buffer"
:type string :type string
:description "Buffer name.") :description "Buffer name.")
'(:name "date" '(:name "date"
:type string :type string
:description "Date string in YYYY or YYYY-MM format. No <, >, [, ]. Just the numbers and dashes.")) :description "Date string in YYYY or YYYY-MM format. No <, >, [, ]. Just the numbers and dashes."))
:category "org"))
#+end_src
Original (works but not ideal).
#+begin_src elisp :tangle no
(defun gptel-org-tools--org-ql-select-by-date (buf date)
"Return headings from BUFFER matching DATE.
DATE must be in the form of YYYY, YYYY-MM, or YYYY-MM-DD."
(org-ql-select
(get-buffer buf)
`(heading ,date)
:action #'gptel-org-tools--heading-subtree))
(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. Prefer using this first when request specifies any time periods. Example: get all headings matching March 2025: \"2025-03\""
:args (list '(:name "buffer"
:type string
:description "Buffer name.")
'(:name "date"
:type string
:description "Date in YYYY or YYYY-MM format."))
:category "org")) :category "org"))
#+end_src #+end_src
***** org-agenda-seek ***** 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. 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.
@ -665,21 +539,24 @@ It works, in principle, but I haven't been able to find a use for it yet. The re
:category "org")) :category "org"))
#+end_src #+end_src
**** Completely WIP tools **** Completely WIP tools
The following tools are still very much WIP, and I think they're self-explanatory enough. They have /NOT/ been tested in any way, shape, form, or capacity.
(I mean, /some/ of them work... but they don't sometimes, too.)
***** org-ql-select-headings ***** org-ql-select-headings
Retrieve the headings where the heading matches query.. 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.
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-headings (buf query) (defun gptel-org-tools--org-ql-select-headings (buf query)
"Return headings matching QUERY from BUFFER." "Return headings matching QUERY from BUFFER."
(let ((result (let* ((buffer (get-buffer buf))
(org-ql-select (mode (buffer-local-value 'major-mode buffer)))
(get-buffer buf) (if buffer
`(heading ,query) (if (eq mode 'org-mode)
:action #''gptel-org-tools--heading))) (let ((result
(gptel-org-tools--result-limit 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 (add-to-list 'gptel-org-tools
@ -701,11 +578,11 @@ Retrieve all the headings where either heading or content matches query.
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-headings-rifle (buf query) (defun gptel-org-tools--org-ql-select-headings-rifle (buf query)
"Return headings of entries (body included) that match keyword QUERY from BUFFER." "Return headings of entries (body included) that match keyword QUERY from BUFFER."
(let ((result (let ((result
(org-ql-select (org-ql-select
(get-buffer buf) (get-buffer buf)
`(rifle ,query) `(rifle ,query)
:action #'gptel-org-tools--heading))) :action #'gptel-org-tools--heading)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
@ -726,14 +603,20 @@ Retrieve all the headings where either heading or content matches query.
***** org-ql-select-tags-local ***** org-ql-select-tags-local
This pulls all the headings (and their contents) when they match tags (without inheritance.) This pulls all the headings (and their contents) when they match tags (without inheritance.)
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-tags-local (buf query) (defun gptelg-tools--org-ql-select-tags-local (buf query)
"Return entries whose tags match QUERY in BUFFER, without inheritance." "Return entries whose tags match QUERY in BUFFER, without inheritance."
(let ((result (let* ((buffer (get-buffer buf))
(org-ql-select (mode (buffer-local-value 'major-mode buffer)))
(get-buffer buf) (if buffer
`(tags-local ,query) (if (eq mode 'org-mode)
:action #'gptel-org-tools--heading-body))) (let ((result
(gptel-org-tools--result-limit 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 (add-to-list 'gptel-org-tools
@ -755,11 +638,16 @@ This pulls all the local tags (without inheritance) from buffer, and returns the
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-tags-local-count (buf query) (defun gptel-org-tools--org-ql-select-tags-local-count (buf query)
"Return count of entries tagged QUERY in BUFFER." "Return count of entries tagged QUERY in BUFFER."
(length (org-ql-select (let* ((buffer (get-buffer buf))
(get-buffer buf) (mode (buffer-local-value 'major-mode buffer)))
`(tags-local ,query) (if buffer
:action #'gptel-org-tools--heading-body))) (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 (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -779,13 +667,19 @@ This pulls all the local tags (without inheritance) from buffer, and returns the
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.) 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 #+begin_src elisp
(defun gptel-org-tools--org-ql-select-tags (buf query) (defun gptel-org-tools--org-ql-select-tags (buf query)
"Return every entry tagged QUERY from BUFFER." "Return every entry tagged QUERY from BUFFER."
(let ((result (let* ((buffer (get-buffer buf))
(org-ql-select (mode (buffer-local-value 'major-mode buffer)))
(get-buffer buf) (if buffer
`(tags ,query) (if (eq mode 'org-mode)
:action #'gptel-org-tools--heading-body))) (let ((result
(gptel-org-tools--result-limit 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 (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -805,15 +699,18 @@ This pulls all the headings (and their contents) when they match tags (with inhe
And, the "grab everything that matches" tool. And, the "grab everything that matches" tool.
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-rifle (buf query) (defun gptel-org-tools--org-ql-select-rifle (buf query)
"Return every entry matching keyword QUERY from BUFFER." "Return every entry matching keyword QUERY from BUFFER."
(let ((buffer (get-buffer buf))) (let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer (if buffer
(let ((result (if (eq mode 'org-mode)
(org-ql-select (let ((result
buffer (org-ql-select
`(rifle ,query) buffer
:action #'gptel-org-tools--heading-body))) `(rifle ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)) (gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf)))) (message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
@ -834,13 +731,13 @@ And, the "grab everything that matches" tool.
This pulls all the headings (and their contents) when they match tags (without inheritance.) This pulls all the headings (and their contents) when they match tags (without inheritance.)
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-all-tags-local (query) (defun gptel-org-tools--org-ql-select-all-tags-local (query)
"Return entries whose tags match QUERY in org-agenda-files. "Return entries whose tags match QUERY in org-agenda-files.
QUERY is the tag to search for." QUERY is the tag to search for."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(tags-local ,query) `(tags-local ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
@ -859,15 +756,15 @@ 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.) 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 #+begin_src elisp
(defun gptel-org-tools--org-ql-select-all-tags (query) (defun gptel-org-tools--org-ql-select-all-tags (query)
"Return entries whose tags match QUERY, "Return entries whose tags match QUERY,
with inheritance, in org-agenda-files. with inheritance, in org-agenda-files.
QUERY is the tag to search for." QUERY is the tag to search for."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(tags ,query) `(tags ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -889,18 +786,18 @@ Note that I define my agenda in this way:
(setq org-agenda-files (directory-files-recursively "~/enc/org/" ".org$")) (setq org-agenda-files (directory-files-recursively "~/enc/org/" ".org$"))
#+end_src #+end_src
This means that /every org-mode file I have/ is part of this search. 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.
#+begin_src elisp #+begin_src elisp
(defun gptel-org-tools--org-ql-select-all-rifle (query) (defun gptel-org-tools--org-ql-select-all-rifle (query)
"Return entries containing QUERY from org-agenda-files. "Return entries containing QUERY from org-agenda-files.
QUERY is the keyword to search for." QUERY is the keyword to search for."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(rifle ,query) `(rifle ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -919,12 +816,12 @@ This means that /every org-mode file I have/ is part of this search.
(defun gptel-org-tools--org-ql-select-all-regexp (query) (defun gptel-org-tools--org-ql-select-all-regexp (query)
"Return all entries matching regexp QUERY in org-agenda-files. "Return all entries matching regexp QUERY in org-agenda-files.
QUERY is a regular expression." QUERY is a regular expression."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(regexp ,query) `(regexp ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool

View file

@ -49,8 +49,7 @@
(concat (concat
(buffer-substring-no-properties (buffer-substring-no-properties
(line-beginning-position) (line-beginning-position)
(line-end-position)) (line-end-position))))
'"\n---\n"))
(defun gptel-org-tools--heading-body () (defun gptel-org-tools--heading-body ()
(concat (concat
@ -58,22 +57,20 @@
(line-beginning-position) (line-beginning-position)
(progn (progn
(outline-next-heading) (outline-next-heading)
(line-beginning-position))) (line-beginning-position)))))
"---\n"))
(defun gptel-org-tools--heading-subtree () (defun gptel-org-tools--heading-subtree ()
(concat (concat
(buffer-substring-no-properties (buffer-substring-no-properties
(line-beginning-position) (line-beginning-position)
(org-end-of-subtree)) (org-end-of-subtree))))
"---\n"))
(defun gptel-org-tool--list-buffers (&optional arg) (defun gptel-org-tool--list-buffers (&optional arg)
"Return list of buffers." "Return list of buffers."
(list-buffers-noselect) (list-buffers-noselect)
(with-current-buffer "*Buffer List*" (with-current-buffer "*Buffer List*"
(let ((content (buffer-string))) (let ((content (buffer-string)))
content))) content)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -108,9 +105,10 @@
(defun gptel-org-tools--open-file-inactive (file) (defun gptel-org-tools--open-file-inactive (file)
"Open FILE in a buffer." "Open FILE in a buffer."
(with-current-buffer (get-buffer-create file) (with-current-buffer (get-buffer-create file)
(insert-file-contents file) (insert-file-contents file)
(concat (concat
(current-buffer)))) (current-buffer))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function #'gptel-org-tools--open-file-inactive :function #'gptel-org-tools--open-file-inactive
@ -124,9 +122,9 @@
(defun gptel-org-tools--describe-variable (var) (defun gptel-org-tools--describe-variable (var)
"Return documentation for VAR." "Return documentation for VAR."
(let ((symbol (intern var))) (let ((symbol (intern var)))
(if (boundp symbol) (if (boundp symbol)
(prin1-to-string (symbol-value symbol)) (prin1-to-string (symbol-value symbol))
(format "Variable %s is not bound." var)))) (format "Variable %s is not bound." var))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -141,9 +139,9 @@
(defun gptel-org-tools--describe-function (fun) (defun gptel-org-tools--describe-function (fun)
"Return documentation for FUN." "Return documentation for FUN."
(let ((symbol (intern fun))) (let ((symbol (intern fun)))
(if (fboundp symbol) (if (fboundp symbol)
(prin1-to-string (documentation symbol 'function)) (prin1-to-string (documentation symbol 'function))
(format "Function %s is not defined." fun)))) (format "Function %s is not defined." fun))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -206,30 +204,30 @@ The date can be in the format YYYY, YYYY-MM, or YYYY-MM-DD.
BUFFER is the name of the buffer to search. BUFFER is the name of the buffer to search.
DATE is the date or date range to match." DATE is the date or date range to match."
(let* ((buffer (get-buffer buf)) (let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer))) (mode (buffer-local-value 'major-mode buffer)))
(if buffer (if (bufferp buffer)
(if (eq mode 'org-mode) (if (eq mode 'org-mode)
(let ((result (let ((result
(org-ql-select buffer (org-ql-select buffer
`(heading ,date) `(heading ,date)
:action #'gptel-org-tools--heading-subtree))) :action #'gptel-org-tools--heading-subtree)))
(gptel-org-tools--result-limit result)) (gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf)) (message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf)))) (message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function #'gptel-org-tools--org-ql-select-by-date :function #'gptel-org-tools--org-ql-select-by-date
:name "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." :description "Returns all timestamped headings matching query. Query may be: YYYY, YYYY-MM, YYYY-MM-DD."
:args (list '(:name "buffer" :args (list '(:name "buffer"
:type string :type string
:description "Buffer name.") :description "Buffer name.")
'(:name "date" '(:name "date"
:type string :type string
:description "Date string in YYYY or YYYY-MM format. No <, >, [, ]. Just the numbers and dashes.")) :description "Date string in YYYY or YYYY-MM format. No <, >, [, ]. Just the numbers and dashes."))
:category "org")) :category "org"))
(defun gptel-org-tools--org-agenda-seek (days) (defun gptel-org-tools--org-agenda-seek (days)
"Return the results of org-agenda-list spanning now to DAYS." "Return the results of org-agenda-list spanning now to DAYS."
@ -251,12 +249,19 @@ DATE is the date or date range to match."
(defun gptel-org-tools--org-ql-select-headings (buf query) (defun gptel-org-tools--org-ql-select-headings (buf query)
"Return headings matching QUERY from BUFFER." "Return headings matching QUERY from BUFFER."
(let ((result (let* ((buffer (get-buffer buf))
(org-ql-select (mode (buffer-local-value 'major-mode buffer)))
(get-buffer buf) (if buffer
`(heading ,query) (if (eq mode 'org-mode)
:action #''gptel-org-tools--heading))) (let ((result
(gptel-org-tools--result-limit 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 (add-to-list 'gptel-org-tools
@ -274,11 +279,11 @@ DATE is the date or date range to match."
(defun gptel-org-tools--org-ql-select-headings-rifle (buf query) (defun gptel-org-tools--org-ql-select-headings-rifle (buf query)
"Return headings of entries (body included) that match keyword QUERY from BUFFER." "Return headings of entries (body included) that match keyword QUERY from BUFFER."
(let ((result (let ((result
(org-ql-select (org-ql-select
(get-buffer buf) (get-buffer buf)
`(rifle ,query) `(rifle ,query)
:action #'gptel-org-tools--heading))) :action #'gptel-org-tools--heading)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
@ -295,14 +300,20 @@ DATE is the date or date range to match."
:description "The string to match entry headings against.")) :description "The string to match entry headings against."))
:category "org-ql")) :category "org-ql"))
(defun gptel-org-tools--org-ql-select-tags-local (buf query) (defun gptelg-tools--org-ql-select-tags-local (buf query)
"Return entries whose tags match QUERY in BUFFER, without inheritance." "Return entries whose tags match QUERY in BUFFER, without inheritance."
(let ((result (let* ((buffer (get-buffer buf))
(org-ql-select (mode (buffer-local-value 'major-mode buffer)))
(get-buffer buf) (if buffer
`(tags-local ,query) (if (eq mode 'org-mode)
:action #'gptel-org-tools--heading-body))) (let ((result
(gptel-org-tools--result-limit 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 (add-to-list 'gptel-org-tools
@ -320,11 +331,16 @@ DATE is the date or date range to match."
(defun gptel-org-tools--org-ql-select-tags-local-count (buf query) (defun gptel-org-tools--org-ql-select-tags-local-count (buf query)
"Return count of entries tagged QUERY in BUFFER." "Return count of entries tagged QUERY in BUFFER."
(length (org-ql-select (let* ((buffer (get-buffer buf))
(get-buffer buf) (mode (buffer-local-value 'major-mode buffer)))
`(tags-local ,query) (if buffer
:action #'gptel-org-tools--heading-body))) (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 (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -340,13 +356,19 @@ DATE is the date or date range to match."
:category "org-ql")) :category "org-ql"))
(defun gptel-org-tools--org-ql-select-tags (buf query) (defun gptel-org-tools--org-ql-select-tags (buf query)
"Return every entry tagged QUERY from BUFFER." "Return every entry tagged QUERY from BUFFER."
(let ((result (let* ((buffer (get-buffer buf))
(org-ql-select (mode (buffer-local-value 'major-mode buffer)))
(get-buffer buf) (if buffer
`(tags ,query) (if (eq mode 'org-mode)
:action #'gptel-org-tools--heading-body))) (let ((result
(gptel-org-tools--result-limit 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 (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -362,15 +384,18 @@ DATE is the date or date range to match."
:category "org-ql")) :category "org-ql"))
(defun gptel-org-tools--org-ql-select-rifle (buf query) (defun gptel-org-tools--org-ql-select-rifle (buf query)
"Return every entry matching keyword QUERY from BUFFER." "Return every entry matching keyword QUERY from BUFFER."
(let ((buffer (get-buffer buf))) (let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer (if buffer
(let ((result (if (eq mode 'org-mode)
(org-ql-select (let ((result
buffer (org-ql-select
`(rifle ,query) buffer
:action #'gptel-org-tools--heading-body))) `(rifle ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)) (gptel-org-tools--result-limit result))
(message "Buffer '%s' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf)))) (message "Buffer '%s' does not exist." buf))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
@ -387,13 +412,13 @@ DATE is the date or date range to match."
:category "org-ql")) :category "org-ql"))
(defun gptel-org-tools--org-ql-select-all-tags-local (query) (defun gptel-org-tools--org-ql-select-all-tags-local (query)
"Return entries whose tags match QUERY in org-agenda-files. "Return entries whose tags match QUERY in org-agenda-files.
QUERY is the tag to search for." QUERY is the tag to search for."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(tags-local ,query) `(tags-local ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
@ -408,15 +433,15 @@ DATE is the date or date range to match."
:category "org-ql")) :category "org-ql"))
(defun gptel-org-tools--org-ql-select-all-tags (query) (defun gptel-org-tools--org-ql-select-all-tags (query)
"Return entries whose tags match QUERY, "Return entries whose tags match QUERY,
with inheritance, in org-agenda-files. with inheritance, in org-agenda-files.
QUERY is the tag to search for." QUERY is the tag to search for."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(tags ,query) `(tags ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -431,12 +456,12 @@ with inheritance, in org-agenda-files.
(defun gptel-org-tools--org-ql-select-all-rifle (query) (defun gptel-org-tools--org-ql-select-all-rifle (query)
"Return entries containing QUERY from org-agenda-files. "Return entries containing QUERY from org-agenda-files.
QUERY is the keyword to search for." QUERY is the keyword to search for."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(rifle ,query) `(rifle ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -451,12 +476,12 @@ with inheritance, in org-agenda-files.
(defun gptel-org-tools--org-ql-select-all-regexp (query) (defun gptel-org-tools--org-ql-select-all-regexp (query)
"Return all entries matching regexp QUERY in org-agenda-files. "Return all entries matching regexp QUERY in org-agenda-files.
QUERY is a regular expression." QUERY is a regular expression."
(let ((result (let ((result
(org-ql-select (org-ql-select
(org-agenda-files) (org-agenda-files)
`(regexp ,query) `(regexp ,query)
:action #'gptel-org-tools--heading-body))) :action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result))) (gptel-org-tools--result-limit result)))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool