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
#+auto_tangle: t
#+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
This is a collection of tools I wrote to help me review my life.
To that end, it's proven useful enough.
Summary:
- An explanation of each tool is below.
- 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.
- 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.
@ -24,60 +25,16 @@ Therefore (philosophy, I may change my mind later):
- 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).
- 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.
- Documentation strings are (ideally) for examples.
These are primarily for my own use. Please don't expect quality.
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.
* Installation:
I only use Doom Emacs, so here's how I load:
packages.el:
#+begin_src elisp :tangle no
@ -193,25 +150,24 @@ Use ~setq~ in your configuration, e.g.:
(setq gptel-org-tools-result-limit 12000)
#+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.)
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.
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
(defun gptel-org-tools--heading ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(line-end-position))
'"\n---\n"))
(line-end-position))))
#+end_src
*** Retrieve heading and body (without subheadings)
*** Return heading and body (without subheadings)
#+begin_src elisp
(defun gptel-org-tools--heading-body ()
(concat
@ -219,36 +175,21 @@ These abstract away some of the tool definitions.
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))
"---\n"))
(line-beginning-position)))))
#+end_src
*** Retrieve heading and subheadings (until next same-level heading)
*** Return heading and subheadings (until next same-level heading)
#+begin_src elisp
(defun gptel-org-tools--heading-subtree ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(org-end-of-subtree))
"---\n"))
(org-end-of-subtree))))
#+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
*** Emacs
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...
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
**** 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.
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
Not using ~ibuffer~ to avoid customization differences between users. Argument required by =gptel=.
#+begin_src elisp
(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)))
(let ((content (buffer-string)))
content)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
@ -302,12 +239,10 @@ Seems to be one of the most reliable tools in the basket... mostly because
#+end_src
**** 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.
~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.
I originally wanted to use ~directory-files~ for this, but it turns out that it's much easier to use ~dired~ for this.
#+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
#+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"))
#+end_src
**** find-buffer-visiting
**** find-buffer-visiting [disabled by default]
Disabled for now, as it's causing some issues.
#+begin_src elisp :tangle no :results none
(defun gptel-org-tools--find-buffer-visiting (filename)
@ -353,14 +288,15 @@ Disabled for now, as it's causing some issues.
#+end_src
**** 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
(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))))
(insert-file-contents file)
(concat
(current-buffer))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
: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
**** read-file-contents
This reads file contents,
#+begin_src elisp :tangle no
(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)
"\n---\nTool execution complete. Proceed with next step.")))
(insert-file-contents (expand-file-name filename))
(concat
(buffer-string))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
@ -399,9 +333,9 @@ This reads file contents,
(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))))
(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
@ -419,9 +353,9 @@ This reads file contents,
(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))))
(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
@ -436,34 +370,19 @@ This reads file contents,
#+end_src
*** 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.
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.
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.
#+begin_comment
LLMs are not intelligent, despite claims to the contrary.
#+end_comment
**** 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
(defun gptel-org-tools--org-extract-tags (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"))
#+end_src
**** 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
(defun gptel-org-tools--org-extract-headings (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"))
#+end_src
**** org-ql-select
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.
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
**** 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.
#+begin_src elisp :tangle no
(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."
(let ((result
(org-ql-select (get-buffer buf)
(if (stringp query)
(read query)
query
:action #'gptel-org-tools--heading-body))))
(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
@ -559,7 +461,7 @@ QUERY can be any valid org-ql-select query."
:category "org"))
#+end_src
**** Somewhat working tools
***** org-ql-select-dates
***** org-ql-select-by-date
My journal is a single file, with a hierarchy like so:
#+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.
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.
Testing new version:
#+begin_src elisp
(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.
DATE is the date or date range to match."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(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))
: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"
: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"))
#+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"))
#+end_src
***** 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.
@ -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"))
#+end_src
**** 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
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
(defun gptel-org-tools--org-ql-select-headings (buf query)
"Return headings matching QUERY from BUFFER."
(let ((result
(org-ql-select
(get-buffer buf)
`(heading ,query)
:action #''gptel-org-tools--heading)))
(gptel-org-tools--result-limit result)))
(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
@ -701,11 +578,11 @@ 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)
"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)))
(let ((result
(org-ql-select
(get-buffer buf)
`(rifle ,query)
:action #'gptel-org-tools--heading)))
(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
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)
(defun gptelg-tools--org-ql-select-tags-local (buf query)
"Return entries whose tags match QUERY in BUFFER, without inheritance."
(let ((result
(org-ql-select
(get-buffer buf)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
(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
@ -755,11 +638,16 @@ This pulls all the local tags (without inheritance) from buffer, and returns the
#+begin_src elisp
(defun gptel-org-tools--org-ql-select-tags-local-count (buf query)
"Return count of entries tagged QUERY in BUFFER."
(length (org-ql-select
(get-buffer buf)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(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
@ -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.)
#+begin_src elisp
(defun gptel-org-tools--org-ql-select-tags (buf query)
"Return every entry tagged QUERY from BUFFER."
(let ((result
(org-ql-select
(get-buffer buf)
`(tags ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
"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
@ -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.
#+begin_src elisp
(defun gptel-org-tools--org-ql-select-rifle (buf query)
"Return every entry matching keyword QUERY from BUFFER."
(let ((buffer (get-buffer buf)))
"Return every entry matching keyword QUERY from BUFFER."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(let ((result
(org-ql-select
buffer
`(rifle ,query)
:action #'gptel-org-tools--heading-body)))
(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
@ -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.)
#+begin_src elisp
(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."
(let ((result
(org-ql-select
(org-agenda-files)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(let ((result
(org-ql-select
(org-agenda-files)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(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.)
#+begin_src elisp
(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.
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)))
(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
@ -889,18 +786,18 @@ Note that I define my agenda in this way:
(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.
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
(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)))
(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
@ -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)
"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)))
(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

View file

@ -49,8 +49,7 @@
(concat
(buffer-substring-no-properties
(line-beginning-position)
(line-end-position))
'"\n---\n"))
(line-end-position))))
(defun gptel-org-tools--heading-body ()
(concat
@ -58,22 +57,20 @@
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))
"---\n"))
(line-beginning-position)))))
(defun gptel-org-tools--heading-subtree ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(org-end-of-subtree))
"---\n"))
(org-end-of-subtree))))
(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)))
(let ((content (buffer-string)))
content)))
(add-to-list 'gptel-org-tools
(gptel-make-tool
@ -108,9 +105,10 @@
(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))))
(insert-file-contents file)
(concat
(current-buffer))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--open-file-inactive
@ -124,9 +122,9 @@
(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))))
(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
@ -141,9 +139,9 @@
(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))))
(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
@ -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.
DATE is the date or date range to match."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(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))
: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"
: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"))
:category "org"))
(defun gptel-org-tools--org-agenda-seek (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)
"Return headings matching QUERY from BUFFER."
(let ((result
(org-ql-select
(get-buffer buf)
`(heading ,query)
:action #''gptel-org-tools--heading)))
(gptel-org-tools--result-limit result)))
(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
@ -274,11 +279,11 @@ DATE is the date or date range to match."
(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)))
(let ((result
(org-ql-select
(get-buffer buf)
`(rifle ,query)
:action #'gptel-org-tools--heading)))
(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."))
: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."
(let ((result
(org-ql-select
(get-buffer buf)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
(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
@ -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)
"Return count of entries tagged QUERY in BUFFER."
(length (org-ql-select
(get-buffer buf)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(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
@ -340,13 +356,19 @@ DATE is the date or date range to match."
:category "org-ql"))
(defun gptel-org-tools--org-ql-select-tags (buf query)
"Return every entry tagged QUERY from BUFFER."
(let ((result
(org-ql-select
(get-buffer buf)
`(tags ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
"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
@ -362,15 +384,18 @@ DATE is the date or date range to match."
:category "org-ql"))
(defun gptel-org-tools--org-ql-select-rifle (buf query)
"Return every entry matching keyword QUERY from BUFFER."
(let ((buffer (get-buffer buf)))
"Return every entry matching keyword QUERY from BUFFER."
(let* ((buffer (get-buffer buf))
(mode (buffer-local-value 'major-mode buffer)))
(if buffer
(let ((result
(org-ql-select
buffer
`(rifle ,query)
:action #'gptel-org-tools--heading-body)))
(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
@ -387,13 +412,13 @@ DATE is the date or date range to match."
:category "org-ql"))
(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."
(let ((result
(org-ql-select
(org-agenda-files)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(let ((result
(org-ql-select
(org-agenda-files)
`(tags-local ,query)
:action #'gptel-org-tools--heading-body)))
(gptel-org-tools--result-limit result)))
@ -408,15 +433,15 @@ DATE is the date or date range to match."
:category "org-ql"))
(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.
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)))
(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
@ -431,12 +456,12 @@ with inheritance, in org-agenda-files.
(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)))
(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
@ -451,12 +476,12 @@ with inheritance, in org-agenda-files.
(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)))
(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