From 44d56bdefd34c40e7c9428dc1d42ca595b641082 Mon Sep 17 00:00:00 2001 From: Phil Bajsicki <phil@bajsicki.com> Date: Tue, 15 Apr 2025 21:17:23 +0200 Subject: [PATCH] Fix org-ql-select-rifle --- README.org | 471 +++++++++++++++++++++++---------------------- gptel-org-tools.el | 412 +++++++++++++++++++-------------------- 2 files changed, 451 insertions(+), 432 deletions(-) diff --git a/README.org b/README.org index f94b764..25a3680 100644 --- a/README.org +++ b/README.org @@ -57,15 +57,15 @@ I change models a lot, and this /just works/ for most models, even if some aren' (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 t - :enable_thinking t - :include_reasoning t - :parallel_tool_calls t))) + :host "localhost:8080" + :protocol "http" + :stream nil + :models '("llamacpp" + :capabilities (reasoning)) + :request-params '(:thinking t + :enable_thinking t + :include_reasoning t + :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") @@ -120,6 +120,12 @@ Stuff, headers, etc. ;;; Code:) #+end_src +** gptel-org-tools +Collects into =gptel-org-tools= list, distinct from =gptel-tools= +#+begin_src elisp +(defvar gptel-org-tools '()) +#+end_src + ** The tools *** Emacs These tools are primarily concerned with Emacs, Emacs Lisp, and files-or-buffers. @@ -133,19 +139,20 @@ E.g. can't find the right ~org-ql-select~ query? Let's use an eldritch abominati Highly not recommended, but sometimes an LLM can pull a rabbit out of pure entropy. #+begin_src elisp :tangle no -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (elisp) - (unless (stringp elisp) (error "elisp code must be a string")) - (with-temp-buffer - (insert (eval (read elisp))) - (buffer-string))) - :name "eval" - :description "Execute arbitrary Emacs Lisp code" - :args (list '(:name "eval" - :type string - :description "The Emacs Lisp code to evaluate.")) - :category "emacs")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (elisp) + (unless (stringp elisp) (error "elisp code must be a string")) + (with-temp-buffer + (insert (eval (read elisp))) + (buffer-string))) + :name "eval" + :description "Execute arbitrary Emacs Lisp code" + :args (list '(:name "eval" + :type string + :description "The Emacs Lisp code to evaluate.")) + :category "emacs" + :confirm t)) #+end_src **** list-buffers @@ -155,21 +162,21 @@ The rationale behind using ~ibuffer~ is the same as with dired. They both displa Seems to be one of the most reliable tools in the basket... mostly because #+begin_src elisp -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (arg) - (with-temp-buffer - (ibuffer) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) - :name "list-buffers" - :description "Access the list of buffers open in Emacs, including file names and full paths." - :args (list '(:name "arg" - :type string - :description "Does nothing." - :optional t)) - :category "emacs")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (arg) + (with-temp-buffer + (ibuffer) + (let ((content (buffer-string))) + (kill-buffer (current-buffer)) + content))) + :name "list-buffers" + :description "Access the list of buffers open in Emacs, including file names and full paths." + :args (list '(:name "arg" + :type string + :description "Does nothing." + :optional t)) + :category "emacs")) #+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. @@ -181,54 +188,57 @@ Be sure to customize the function to point to your org directory, if you wish. I #+end_comment #+begin_src elisp -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (dir) - (with-temp-buffer - (dired (or dir "~")) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) - :name "dired" - :description "List directory contents" - :args (list '(:name "dir" - :type string - :description "Directory path" - :optional t)) - :category "filesystem")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (dir) + (with-temp-buffer + (dired (or dir "~")) + (let ((content (buffer-string))) + (kill-buffer (current-buffer)) + content))) + :name "dired" + :description "List directory contents" + :args (list '(:name "dir" + :type string + :description "Directory path" + :optional t)) + :category "filesystem")) #+end_src **** find-buffer-visiting #+begin_src elisp -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (filename) - (bufferp (find-buffer-visiting (expand-file-name filename)))) - :name "find-buffer-visiting" - :description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)" - :args (list '(:name "filename" - :type string - :description "The filename to compare to open buffers.")) - :category "org-mode")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (filename) + (bufferp (find-buffer-visiting (expand-file-name filename)))) + :name "find-buffer-visiting" + :description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)" + :args (list '(:name "filename" + :type string + :description "The filename to compare to open buffers.")) + :category "org-mode")) #+end_src -**** find-file-noselect -Continuation from above. Open a file into a buffer for processing. Onec it's found by dired-list. +**** open-file-inactive +Continuation from above. Open a file into a buffer for processing, once it's found by dired-list. #+begin_src elisp -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (file) - (find-file-noselect file)) - :name "find-file-noselect" - :description "Open the file in a buffer. This doesn't interfere with the user." - :args (list '(:name "file" - :type string - :description "Path to file..")) - :category "filesystem")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (file) + (with-current-buffer (get-buffer-create file) + (insert-file-contents file) + (current-buffer))) + :name "open-file-inactive" + :description "Open the file in a background buffer. This doesn't interfere with the user." + :args (list '(:name "file" + :type string + :description "Path to file..")) + :category "filesystem")) #+end_src + **** read-file-contents This reads file contents, #+begin_src elisp -(add-to-list 'gptel-tools +(add-to-list 'gptel-org-tools (gptel-make-tool :function (lambda (filename) (with-temp-buffer @@ -243,13 +253,13 @@ This reads file contents, #+end_src **** describe-variable #+begin_src elisp -(add-to-list 'gptel-tools +(add-to-list 'gptel-org-tools (gptel-make-tool :function (lambda (var) - (let ((symbol (intern var))) - (if (boundp symbol) - (prin1-to-string (symbol-value symbol)) - (format "Variable %s is not bound." var)))) + (let ((symbol (intern var))) + (if (boundp symbol) + (prin1-to-string (symbol-value symbol)) + (format "Variable %s is not bound." var)))) :name "describe-variable" :description "See variable contents" :args (list '(:name "var" @@ -260,13 +270,13 @@ This reads file contents, **** describe-function #+begin_src elisp -(add-to-list 'gptel-tools +(add-to-list 'gptel-org-tools (gptel-make-tool :function (lambda (fun) - (let ((symbol (intern fun))) - (if (fboundp symbol) - (prin1-to-string (documentation symbol 'function)) - (format "Function %s is not defined." fun)))) + (let ((symbol (intern fun))) + (if (fboundp symbol) + (prin1-to-string (documentation symbol 'function)) + (format "Function %s is not defined." fun)))) :name "describe-function" :description "See function description" :args (list '(:name "fun" @@ -307,7 +317,6 @@ Pretty simple, does what it says on the tin. It gets all the tags from the =buff 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) - (interactive "bBuffer: ") (with-current-buffer buffer (let ((tags '())) (org-map-entries @@ -319,15 +328,15 @@ This is not, by any means, sufficient, but I do tag people and specific events f (push tag tags)))))) (sort (-uniq tags) #'string<)))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-extract-tags - :name "org-extract-tags" - :description "Extract all unique tags from an org-mode buffer" - :args (list '(:name "buffer" - :type string - :description "The Org buffer to extract tags from.")) - :category "org-mode")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-extract-tags + :name "org-extract-tags" + :description "Extract all unique tags from an org-mode buffer" + :args (list '(:name "buffer" + :type string + :description "The Org buffer to extract tags from.")) + :category "org-mode")) #+end_src **** org-extract-headings But what if there's no tags related to the topic? @@ -337,7 +346,6 @@ 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. #+begin_src elisp (defun gptel-org-tools--org-extract-headings (buffer) - (interactive "bBuffer: ") (with-current-buffer buffer (org-map-entries #'(buffer-substring-no-properties @@ -346,15 +354,15 @@ Therefore, headings. A reasonable amount of information, and still keeping the s t 'file))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-extract-headings - :name "org-extract-headings" - :description "Extract all headings from an org-mode buffer" - :args (list '(:name "buffer" - :type string - :description "The Org buffer to extract headings from.")) - :category "org-mode")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-extract-headings + :name "org-extract-headings" + :description "Extract all headings from an org-mode buffer" + :args (list '(:name "buffer" + :type string + :description "The Org buffer to extract headings from.")) + :category "org-mode")) #+end_src **** org-ql-select @@ -366,6 +374,11 @@ My current goal is to replace this monstrosity with individual functions for eac 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 (defun gptel-org-tools--org-ql-select (buf query) (org-ql-select @@ -378,21 +391,21 @@ But in the interim, this works. Kind of. (buffer-substring-no-properties (line-beginning-position) (progn - (outline-next-heading) - (line-beginning-position))))))) + (outline-next-heading) + (line-beginning-position))))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select - :name "org-ql-select" - :description "Run org-ql-select against buffer with query. Using filename fails." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. Can be multiple buffers. See the NAME column in `emacs-list-buffers`.") - '(:name "query" - :type string - :description "The query to pass into org-ql-select. See org-ql documentation for syntax. Usually `(tags \"tag1\" \"tag2\")` is sufficient. Possible predicates: `tags` (finds both local and inherited tags), `tags-local` (finds only local tags), `rifle` (matches against both heading and body text). This is a sexp, not a string.")) - :category "org")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select + :name "org-ql-select" + :description "Run org-ql-select against buffer with query. Using filename fails." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. Can be multiple buffers. See the NAME column in `emacs-list-buffers`.") + '(:name "query" + :type string + :description "The query to pass into org-ql-select. See org-ql documentation for syntax. Usually `(tags \"tag1\" \"tag2\")` is sufficient. Possible predicates: `tags` (finds both local and inherited tags), `tags-local` (finds only local tags), `rifle` (matches against both heading and body text). This is a sexp, not a string.")) + :category "org")) #+end_src **** Somewhat working tools ***** org-ql-select-dates @@ -417,7 +430,6 @@ But, any customizations to tweak this is left to the user, as everyone has their #+begin_src elisp (defun gptel-org-tools--org-ql-select-dates (buf date) - (interactive "fBuffer: \nsDate (YYYY or YYYY-MM): ") (org-ql-select (get-buffer buf) `(heading ,date) @@ -427,38 +439,38 @@ But, any customizations to tweak this is left to the user, as everyone has their (org-end-of-subtree))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-dates - :name "org-ql-select-dates" - :description "Extract org subtree by date in YYYY or YYYY-MM format" - :args (list '(:name "buffer" - :type string - :description "Buffer name.") - '(:name "date" - :type string - :description "Date in YYYY or YYYY-MM format. Can add multiple like so: \"YYYY-MM\" \"YYYY-MM\"")) - :category "org")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-dates + :name "org-ql-select-dates" + :description "Extract org subtree by date in YYYY or YYYY-MM format" + :args (list '(:name "buffer" + :type string + :description "Buffer name.") + '(:name "date" + :type string + :description "Date in YYYY or YYYY-MM format. Can add multiple like so: \"YYYY-MM\" \"YYYY-MM\"")) + :category "org")) #+end_src ***** org-agenda-fortnight This is still work in progress, the idea is to have the LLM check my calendar and see what my plans are. I have not had time to really dig into this yet. It works, in principle, but I haven't been able to find a use for it yet. The real challenge is in building a context where the tools integrate with each-other in a way that makes sense. For now, this exists. #+begin_src elisp -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (days) - (with-temp-buffer - (org-agenda-list (or days 14)) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) - :name "org-agenda-fortnight" - :description "Get the next 14 days of user's org-agenda." - :args (list '(:name "days" - :type integer - :description "The number of days to look ahead. Default: 14")) - :category "org")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (days) + (with-temp-buffer + (org-agenda-list (or days 14)) + (let ((content (buffer-string))) + (kill-buffer (current-buffer)) + content))) + :name "org-agenda-fortnight" + :description "Get the next 14 days of user's org-agenda." + :args (list '(:name "days" + :type integer + :description "The number of days to look ahead. Default: 14")) + :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. @@ -479,46 +491,46 @@ Retrieve the headings where the heading matches query.. (line-end-position)))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-headings - :name "org-ql-select-headings" - :description "Retreive matching headings from buffer using org-ql-select. Matches only against heading. Using filename fails." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") - '(:name "query" - :type string - :description "The string to pass into org-ql-select-headings. This is a bare string. Example: \"searchterm\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-headings + :name "org-ql-select-headings" + :description "Retreive matching headings from buffer using org-ql-select. Matches only against heading. Using filename fails." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") + '(:name "query" + :type string + :description "The string to pass into org-ql-select-headings. This is a bare string. Example: \"searchterm\"")) + :category "org-ql")) #+end_src ***** org-ql-select-headings-rifle 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) - (org-ql-select - (get-buffer buf) - `(rifle ,query) - :action :action #'(lambda () - (concat - (buffer-substring-no-properties - (line-beginning-position) - (line-end-position)))))) + (org-ql-select + (get-buffer buf) + `(rifle ,query) + :action #'(lambda () + (concat + (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)))))) - (add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-headings-rifle - :name "org-ql-select-headings-rifle" - :description "Retreive headings from buffer using org-ql-select. Matches against both heading and content. Using filename fails." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") - '(:name "query" - :type string - :description "The string to pass into org-ql-select-headings-rifle. This is a bare string. Example: \"searchterm\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-headings-rifle + :name "org-ql-select-headings-rifle" + :description "Retreive headings from buffer using org-ql-select. Matches against both heading and content. Using filename fails." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") + '(:name "query" + :type string + :description "The string to pass into org-ql-select-headings-rifle. This is a bare string. Example: \"searchterm\"")) + :category "org-ql")) #+end_src ***** org-ql-select-tags-local @@ -533,22 +545,22 @@ This pulls all the headings (and their contents) when they match tags (without i (buffer-substring-no-properties (line-beginning-position) (progn - (outline-next-heading) - (line-beginning-position))))))) + (outline-next-heading) + (line-beginning-position))))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-tags-local - :name "org-ql-select-tags-local" - :description "Run org-ql-select-tags-local against buffer with query. No tag inheritance." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") - '(:name "query" - :type string - :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-tags-local + :name "org-ql-select-tags-local" + :description "Run org-ql-select-tags-local against buffer with query. No tag inheritance." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") + '(:name "query" + :type string + :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) + :category "org-ql")) #+end_src ***** org-ql-select-tags @@ -563,51 +575,54 @@ This pulls all the headings (and their contents) when they match tags (with inhe (buffer-substring-no-properties (line-beginning-position) (progn - (outline-next-heading) - (line-beginning-position))))))) + (outline-next-heading) + (line-beginning-position))))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-tags - :name "org-ql-select-tags" - :description "Run org-ql-select-tags against buffer with query. Supports tag inheritance." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") - '(:name "query" - :type string - :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-tags + :name "org-ql-select-tags" + :description "Run org-ql-select-tags against buffer with query. Supports tag inheritance." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") + '(:name "query" + :type string + :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) + :category "org-ql")) #+end_src ***** org-ql-select-rifle And, the "grab everything that matches" tool. #+begin_src elisp (defun gptel-org-tools--org-ql-select-rifle (buf query) - (org-ql-select - (get-buffer buf) - `(rifle ,query) - :action #'(lambda () - (concat - (buffer-substring-no-properties - (line-beginning-position) - (progn - (outline-next-heading) - (line-beginning-position))))))) + (let ((buffer (get-buffer buf))) + (if buffer + (org-ql-select + buffer + `(rifle ,query) + :action #'(lambda () + (concat + (buffer-substring-no-properties + (line-beginning-position) + (progn + (outline-next-heading) + (line-beginning-position)))))) + (message "Buffer '%s' does not exist." buf)))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-rifle - :name "org-ql-select-rifle" - :description "Run org-ql-select-rifle against buffer with query." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") - '(:name "query" - :type string - :description "The strings to match entry headings and content against. Example: \"tag1\" \"tag2\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-rifle + :name "org-ql-select-rifle" + :description "Run org-ql-select-rifle against buffer with query." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") + '(:name "query" + :type string + :description "The strings to match entry headings and content against. Example: \"tag1\" \"tag2\"")) + :category "org-ql")) #+end_src diff --git a/gptel-org-tools.el b/gptel-org-tools.el index a5845ef..704a59d 100644 --- a/gptel-org-tools.el +++ b/gptel-org-tools.el @@ -34,61 +34,65 @@ ;;; Code:) -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (arg) - (with-temp-buffer - (ibuffer) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) - :name "list-buffers" - :description "Access the list of buffers open in Emacs, including file names and full paths." - :args (list '(:name "arg" - :type string - :description "Does nothing." - :optional t)) - :category "emacs")) +(defvar gptel-org-tools '()) -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (dir) - (with-temp-buffer - (dired (or dir "~")) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) - :name "dired" - :description "List directory contents" - :args (list '(:name "dir" - :type string - :description "Directory path" - :optional t)) - :category "filesystem")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (arg) + (with-temp-buffer + (ibuffer) + (let ((content (buffer-string))) + (kill-buffer (current-buffer)) + content))) + :name "list-buffers" + :description "Access the list of buffers open in Emacs, including file names and full paths." + :args (list '(:name "arg" + :type string + :description "Does nothing." + :optional t)) + :category "emacs")) -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (filename) - (bufferp (find-buffer-visiting (expand-file-name filename)))) - :name "find-buffer-visiting" - :description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)" - :args (list '(:name "filename" - :type string - :description "The filename to compare to open buffers.")) - :category "org-mode")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (dir) + (with-temp-buffer + (dired (or dir "~")) + (let ((content (buffer-string))) + (kill-buffer (current-buffer)) + content))) + :name "dired" + :description "List directory contents" + :args (list '(:name "dir" + :type string + :description "Directory path" + :optional t)) + :category "filesystem")) -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (file) - (find-file-noselect file)) - :name "find-file-noselect" - :description "Open the file in a buffer. This doesn't interfere with the user." - :args (list '(:name "file" - :type string - :description "Path to file..")) - :category "filesystem")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (filename) + (bufferp (find-buffer-visiting (expand-file-name filename)))) + :name "find-buffer-visiting" + :description "Check if the file is open in a buffer. Usage (find-buffer-visiting filename)" + :args (list '(:name "filename" + :type string + :description "The filename to compare to open buffers.")) + :category "org-mode")) -(add-to-list 'gptel-tools +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (file) + (with-current-buffer (get-buffer-create file) + (insert-file-contents file) + (current-buffer))) + :name "open-file-inactive" + :description "Open the file in a background buffer. This doesn't interfere with the user." + :args (list '(:name "file" + :type string + :description "Path to file..")) + :category "filesystem")) + +(add-to-list 'gptel-org-tools (gptel-make-tool :function (lambda (filename) (with-temp-buffer @@ -101,13 +105,13 @@ :description "The filename to read.")) :category "org-mode")) -(add-to-list 'gptel-tools +(add-to-list 'gptel-org-tools (gptel-make-tool :function (lambda (var) - (let ((symbol (intern var))) - (if (boundp symbol) - (prin1-to-string (symbol-value symbol)) - (format "Variable %s is not bound." var)))) + (let ((symbol (intern var))) + (if (boundp symbol) + (prin1-to-string (symbol-value symbol)) + (format "Variable %s is not bound." var)))) :name "describe-variable" :description "See variable contents" :args (list '(:name "var" @@ -115,13 +119,13 @@ :description "Variable name")) :category "emacs")) -(add-to-list 'gptel-tools +(add-to-list 'gptel-org-tools (gptel-make-tool :function (lambda (fun) - (let ((symbol (intern fun))) - (if (fboundp symbol) - (prin1-to-string (documentation symbol 'function)) - (format "Function %s is not defined." fun)))) + (let ((symbol (intern fun))) + (if (fboundp symbol) + (prin1-to-string (documentation symbol 'function)) + (format "Function %s is not defined." fun)))) :name "describe-function" :description "See function description" :args (list '(:name "fun" @@ -131,7 +135,6 @@ :category "emacs")) (defun gptel-org-tools--org-extract-tags (buffer) - (interactive "bBuffer: ") (with-current-buffer buffer (let ((tags '())) (org-map-entries @@ -143,18 +146,17 @@ (push tag tags)))))) (sort (-uniq tags) #'string<)))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-extract-tags - :name "org-extract-tags" - :description "Extract all unique tags from an org-mode buffer" - :args (list '(:name "buffer" - :type string - :description "The Org buffer to extract tags from.")) - :category "org-mode")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-extract-tags + :name "org-extract-tags" + :description "Extract all unique tags from an org-mode buffer" + :args (list '(:name "buffer" + :type string + :description "The Org buffer to extract tags from.")) + :category "org-mode")) (defun gptel-org-tools--org-extract-headings (buffer) - (interactive "bBuffer: ") (with-current-buffer buffer (org-map-entries #'(buffer-substring-no-properties @@ -163,15 +165,15 @@ t 'file))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-extract-headings - :name "org-extract-headings" - :description "Extract all headings from an org-mode buffer" - :args (list '(:name "buffer" - :type string - :description "The Org buffer to extract headings from.")) - :category "org-mode")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-extract-headings + :name "org-extract-headings" + :description "Extract all headings from an org-mode buffer" + :args (list '(:name "buffer" + :type string + :description "The Org buffer to extract headings from.")) + :category "org-mode")) (defun gptel-org-tools--org-ql-select (buf query) (org-ql-select @@ -184,24 +186,23 @@ (buffer-substring-no-properties (line-beginning-position) (progn - (outline-next-heading) - (line-beginning-position))))))) + (outline-next-heading) + (line-beginning-position))))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select - :name "org-ql-select" - :description "Run org-ql-select against buffer with query. Using filename fails." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. Can be multiple buffers. See the NAME column in `emacs-list-buffers`.") - '(:name "query" - :type string - :description "The query to pass into org-ql-select. See org-ql documentation for syntax. Usually `(tags \"tag1\" \"tag2\")` is sufficient. Possible predicates: `tags` (finds both local and inherited tags), `tags-local` (finds only local tags), `rifle` (matches against both heading and body text). This is a sexp, not a string.")) - :category "org")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select + :name "org-ql-select" + :description "Run org-ql-select against buffer with query. Using filename fails." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. Can be multiple buffers. See the NAME column in `emacs-list-buffers`.") + '(:name "query" + :type string + :description "The query to pass into org-ql-select. See org-ql documentation for syntax. Usually `(tags \"tag1\" \"tag2\")` is sufficient. Possible predicates: `tags` (finds both local and inherited tags), `tags-local` (finds only local tags), `rifle` (matches against both heading and body text). This is a sexp, not a string.")) + :category "org")) (defun gptel-org-tools--org-ql-select-dates (buf date) - (interactive "fBuffer: \nsDate (YYYY or YYYY-MM): ") (org-ql-select (get-buffer buf) `(heading ,date) @@ -211,33 +212,33 @@ (org-end-of-subtree))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-dates - :name "org-ql-select-dates" - :description "Extract org subtree by date in YYYY or YYYY-MM format" - :args (list '(:name "buffer" - :type string - :description "Buffer name.") - '(:name "date" - :type string - :description "Date in YYYY or YYYY-MM format. Can add multiple like so: \"YYYY-MM\" \"YYYY-MM\"")) - :category "org")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-dates + :name "org-ql-select-dates" + :description "Extract org subtree by date in YYYY or YYYY-MM format" + :args (list '(:name "buffer" + :type string + :description "Buffer name.") + '(:name "date" + :type string + :description "Date in YYYY or YYYY-MM format. Can add multiple like so: \"YYYY-MM\" \"YYYY-MM\"")) + :category "org")) -(add-to-list 'gptel-tools -(gptel-make-tool - :function (lambda (days) - (with-temp-buffer - (org-agenda-list (or days 14)) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) - :name "org-agenda-fortnight" - :description "Get the next 14 days of user's org-agenda." - :args (list '(:name "days" - :type integer - :description "The number of days to look ahead. Default: 14")) - :category "org")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function (lambda (days) + (with-temp-buffer + (org-agenda-list (or days 14)) + (let ((content (buffer-string))) + (kill-buffer (current-buffer)) + content))) + :name "org-agenda-fortnight" + :description "Get the next 14 days of user's org-agenda." + :args (list '(:name "days" + :type integer + :description "The number of days to look ahead. Default: 14")) + :category "org")) (defun gptel-org-tools--org-ql-select-headings (buf query) (org-ql-select @@ -250,42 +251,42 @@ (line-end-position)))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-headings - :name "org-ql-select-headings" - :description "Retreive matching headings from buffer using org-ql-select. Matches only against heading. Using filename fails." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") - '(:name "query" - :type string - :description "The string to pass into org-ql-select-headings. This is a bare string. Example: \"searchterm\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-headings + :name "org-ql-select-headings" + :description "Retreive matching headings from buffer using org-ql-select. Matches only against heading. Using filename fails." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") + '(:name "query" + :type string + :description "The string to pass into org-ql-select-headings. This is a bare string. Example: \"searchterm\"")) + :category "org-ql")) (defun gptel-org-tools--org-ql-select-headings-rifle (buf query) - (org-ql-select - (get-buffer buf) - `(rifle ,query) - :action :action #'(lambda () - (concat - (buffer-substring-no-properties - (line-beginning-position) - (line-end-position)))))) + (org-ql-select + (get-buffer buf) + `(rifle ,query) + :action #'(lambda () + (concat + (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)))))) - (add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-headings-rifle - :name "org-ql-select-headings-rifle" - :description "Retreive headings from buffer using org-ql-select. Matches against both heading and content. Using filename fails." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") - '(:name "query" - :type string - :description "The string to pass into org-ql-select-headings-rifle. This is a bare string. Example: \"searchterm\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-headings-rifle + :name "org-ql-select-headings-rifle" + :description "Retreive headings from buffer using org-ql-select. Matches against both heading and content. Using filename fails." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in ~emacs-list-buffers~.") + '(:name "query" + :type string + :description "The string to pass into org-ql-select-headings-rifle. This is a bare string. Example: \"searchterm\"")) + :category "org-ql")) (defun gptel-org-tools--org-ql-select-tags-local (buf query) (org-ql-select @@ -296,22 +297,22 @@ (buffer-substring-no-properties (line-beginning-position) (progn - (outline-next-heading) - (line-beginning-position))))))) + (outline-next-heading) + (line-beginning-position))))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-tags-local - :name "org-ql-select-tags-local" - :description "Run org-ql-select-tags-local against buffer with query. No tag inheritance." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") - '(:name "query" - :type string - :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-tags-local + :name "org-ql-select-tags-local" + :description "Run org-ql-select-tags-local against buffer with query. No tag inheritance." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") + '(:name "query" + :type string + :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) + :category "org-ql")) (defun gptel-org-tools--org-ql-select-tags (buf query) (org-ql-select @@ -322,47 +323,50 @@ (buffer-substring-no-properties (line-beginning-position) (progn - (outline-next-heading) - (line-beginning-position))))))) + (outline-next-heading) + (line-beginning-position))))))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-tags - :name "org-ql-select-tags" - :description "Run org-ql-select-tags against buffer with query. Supports tag inheritance." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") - '(:name "query" - :type string - :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-tags + :name "org-ql-select-tags" + :description "Run org-ql-select-tags against buffer with query. Supports tag inheritance." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") + '(:name "query" + :type string + :description "The tags to match entry headings against. Example: \"tag1\" \"tag2\"")) + :category "org-ql")) (defun gptel-org-tools--org-ql-select-rifle (buf query) - (org-ql-select - (get-buffer buf) - `(rifle ,query) - :action #'(lambda () - (concat - (buffer-substring-no-properties - (line-beginning-position) - (progn - (outline-next-heading) - (line-beginning-position))))))) + (let ((buffer (get-buffer buf))) + (if buffer + (org-ql-select + buffer + `(rifle ,query) + :action #'(lambda () + (concat + (buffer-substring-no-properties + (line-beginning-position) + (progn + (outline-next-heading) + (line-beginning-position)))))) + (message "Buffer '%s' does not exist." buf)))) -(add-to-list 'gptel-tools -(gptel-make-tool - :function #'gptel-org-tools--org-ql-select-rifle - :name "org-ql-select-rifle" - :description "Run org-ql-select-rifle against buffer with query." - :args (list '(:name "buffer" - :type string - :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") - '(:name "query" - :type string - :description "The strings to match entry headings and content against. Example: \"tag1\" \"tag2\"")) - :category "org-ql")) +(add-to-list 'gptel-org-tools + (gptel-make-tool + :function #'gptel-org-tools--org-ql-select-rifle + :name "org-ql-select-rifle" + :description "Run org-ql-select-rifle against buffer with query." + :args (list '(:name "buffer" + :type string + :description "The name of the buffer. See the NAME column in `emacs-list-buffers`. Using filename fails.") + '(:name "query" + :type string + :description "The strings to match entry headings and content against. Example: \"tag1\" \"tag2\"")) + :category "org-ql")) (provide 'gptel-org-tools) ;;; gptel-org-tools.el ends here