Add (org-agenda-files): tags, tags-local, rifle

This commit is contained in:
Phil Bajsicki 2025-04-16 12:36:02 +02:00
parent 5c39f9348d
commit c781b00be4
2 changed files with 194 additions and 20 deletions

View file

@ -62,9 +62,9 @@ I change models a lot, and this /just works/ for most models, even if some aren'
:stream nil :stream nil
:models '("llamacpp" :models '("llamacpp"
:capabilities (reasoning)) :capabilities (reasoning))
:request-params '(:thinking t :request-params '(:thinking nil
:enable_thinking t :enable_thinking nil
:include_reasoning t :include_reasoning nil
:parallel_tool_calls t))) :parallel_tool_calls t)))
(setf (alist-get 'org-mode gptel-prompt-prefix-alist) "@user\n") (setf (alist-get 'org-mode gptel-prompt-prefix-alist) "@user\n")
@ -76,6 +76,9 @@ I change models a lot, and this /just works/ for most models, even if some aren'
(require 'gptel) (require 'gptel)
#+end_src #+end_src
#+RESULTS:
: gptel
With that out of the way, let's get to the tools. With that out of the way, let's get to the tools.
* Usage options: * Usage options:
I only use Doom Emacs, so here's how I do it. I only use Doom Emacs, so here's how I do it.
@ -102,8 +105,20 @@ config.el:
(require 'gptel-org-tools) (require 'gptel-org-tools)
(mapcar (lambda (tool) (cl-pushnew tool gptel-tools)) gptel-org-tools) (mapcar (lambda (tool) (cl-pushnew tool gptel-tools)) gptel-org-tools)
#+end_src #+end_src
** Variables
If you're like me, you may have structured headings based on timestamps in your org-mode notes.
To that end, there are a few variables that may help you from blowing out the context, specifically for the ~org-extract-headings~ tool.
#+begin_src elisp
(defvar gptel-org-tools-skip-heading-extraction '())
#+end_src
Use ~setq~ in your configuration, with a list of buffer names whose headings you *never* want to extract. E.g.
#+begin_src elisp :tangle no
(setq gptel-org-tools-skip-heading-extraction '("journal.org" ".org"))
#+end_src
* Code * Code
** Preamble ** Preamble
@ -169,7 +184,7 @@ Highly not recommended, but sometimes an LLM can pull a rabbit out of pure entro
#+begin_src elisp :tangle no #+begin_src elisp :tangle no
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function (lambda (elisp) :function ((lambda () (interactive) ) (elisp)
(unless (stringp elisp) (error "elisp code must be a string")) (unless (stringp elisp) (error "elisp code must be a string"))
(with-temp-buffer (with-temp-buffer
(insert (eval (read elisp))) (insert (eval (read elisp)))
@ -193,19 +208,17 @@ Seems to be one of the most reliable tools in the basket... mostly because
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function (lambda (arg) :function (lambda (arg)
(with-temp-buffer (list-buffers-noselect)
(list-buffers) (with-current-buffer "*Buffer List*"
(let ((content (buffer-string))) (buffer-string)))
(kill-buffer (current-buffer))
content)))
:name "list-buffers" :name "list-buffers"
:description "Access the list of buffers open in Emacs, including file names and full paths." :description "Access the list of buffers open in Emacs, including file names and full paths."
:args (list '(:name "arg" :args (list '(:name "arg"
:type string :type string
:description "Does nothing." :description "Does nothing."))
:optional t))
:category "emacs")) :category "emacs"))
#+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. 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.
@ -375,13 +388,15 @@ Then we need to pull /some/ information from the buffer, without dragging the en
Therefore, headings. A reasonable amount of information, and still keeping the signal-to-noise ratio pretty decent. 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)
(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 (with-current-buffer buffer
(org-map-entries (org-map-entries
#'(buffer-substring-no-properties #'(buffer-substring-no-properties
(line-beginning-position) (line-beginning-position)
(line-end-position)) (line-end-position))
t t
'file))) 'file))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -655,6 +670,96 @@ And, the "grab everything that matches" tool.
#+end_src #+end_src
***** org-ql-select-agenda-tags-local
This pulls all the headings (and their contents) when they match tags (without inheritance.)
#+begin_src elisp
(defun gptel-org-tools--org-ql-select-agenda-tags (query)
(org-ql-select
(org-agenda-files)
`(tags-local ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-agenda-tags-local
:name "org-ql-select-agenda-tags-local"
:description "Run simple word query against all files in (org-agenda-files). WITHOUT tag inheritance, only directly tagged headings."
:args (list '(:name "query"
:type string
:description "Plain list of strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
:category "org-ql"))
#+end_src
***** org-ql-select-agenda-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.)
#+begin_src elisp
(defun gptel-org-tools--org-ql-select-agenda-tags (query)
(org-ql-select
(org-agenda-files)
`(tags ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-agenda-tags
:name "org-ql-select-agenda-tags"
:description "Run simple word query against all files in (org-agenda-files). WITH tag inheritance."
:args (list '(:name "query"
:type string
:description "Plain list of strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
:category "org-ql"))
#+end_src
***** org-ql-select-agenda-rifle
And for the entire =(org-agenda-files)=.
Note that I define my agenda in this way:
#+begin_src elisp :tangle no
(setq org-agenda-files (directory-files-recursively "~/enc/org/" ".org$"))1
#+end_src
This means that /every org-mode file I have/ is part of this search.
#+begin_src elisp
(defun gptel-org-tools--org-ql-select-agenda-rifle (query)
(org-ql-select
(org-agenda-files)
`(rifle ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-agenda-rifle
:name "org-ql-select-agenda-rifle"
:description "Run simple word query against all files in (org-agenda-files)"
:args (list '(:name "query"
:type string
:description "Plain list of strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
:category "org-ql"))
#+end_src
** End ** End
:PROPERTIES: :PROPERTIES:
:CREATED: <2025-04-14 Mon 22:46> :CREATED: <2025-04-14 Mon 22:46>

View file

@ -1,3 +1,5 @@
(defvar gptel-org-tools-skip-heading-extraction '())
;;; gptel-org-tools.el --- LLM Tools for org-mode interaction. -*- lexical-binding: t; -*- ;;; gptel-org-tools.el --- LLM Tools for org-mode interaction. -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Phil Bajsicki ;; Copyright (C) 2025 Phil Bajsicki
@ -39,17 +41,14 @@
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
:function (lambda (arg) :function (lambda (arg)
(with-temp-buffer (list-buffers-noselect)
(list-buffers) (with-current-buffer "*Buffer List*"
(let ((content (buffer-string))) (buffer-string)))
(kill-buffer (current-buffer))
content)))
:name "list-buffers" :name "list-buffers"
:description "Access the list of buffers open in Emacs, including file names and full paths." :description "Access the list of buffers open in Emacs, including file names and full paths."
:args (list '(:name "arg" :args (list '(:name "arg"
:type string :type string
:description "Does nothing." :description "Does nothing."))
:optional t))
:category "emacs")) :category "emacs"))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
@ -144,13 +143,15 @@
:category "org-mode")) :category "org-mode"))
(defun gptel-org-tools--org-extract-headings (buffer) (defun gptel-org-tools--org-extract-headings (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 (with-current-buffer buffer
(org-map-entries (org-map-entries
#'(buffer-substring-no-properties #'(buffer-substring-no-properties
(line-beginning-position) (line-beginning-position)
(line-end-position)) (line-end-position))
t t
'file))) 'file))))
(add-to-list 'gptel-org-tools (add-to-list 'gptel-org-tools
(gptel-make-tool (gptel-make-tool
@ -328,5 +329,73 @@
:description "The strings to match entry headings and content against. Example: \"tag1\" \"tag2\"")) :description "The strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
:category "org-ql")) :category "org-ql"))
(defun gptel-org-tools--org-ql-select-agenda-tags (query)
(org-ql-select
(org-agenda-files)
`(tags-local ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-agenda-tags-local
:name "org-ql-select-agenda-tags-local"
:description "Run simple word query against all files in (org-agenda-files). WITHOUT tag inheritance, only directly tagged headings."
:args (list '(:name "query"
:type string
:description "Plain list of strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
:category "org-ql"))
(defun gptel-org-tools--org-ql-select-agenda-tags (query)
(org-ql-select
(org-agenda-files)
`(tags ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-agenda-tags
:name "org-ql-select-agenda-tags"
:description "Run simple word query against all files in (org-agenda-files). WITH tag inheritance."
:args (list '(:name "query"
:type string
:description "Plain list of strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
:category "org-ql"))
(defun gptel-org-tools--org-ql-select-agenda-rifle (query)
(org-ql-select
(org-agenda-files)
`(rifle ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(add-to-list 'gptel-org-tools
(gptel-make-tool
:function #'gptel-org-tools--org-ql-select-agenda-rifle
:name "org-ql-select-agenda-rifle"
:description "Run simple word query against all files in (org-agenda-files)"
:args (list '(:name "query"
:type string
:description "Plain list of strings to match entry headings and content against. Example: \"tag1\" \"tag2\""))
:category "org-ql"))
(provide 'gptel-org-tools) (provide 'gptel-org-tools)
;;; gptel-org-tools.el ends here ;;; gptel-org-tools.el ends here