diff --git a/README.org b/README.org index 1e87390..af3b63b 100644 --- a/README.org +++ b/README.org @@ -62,9 +62,9 @@ I change models a lot, and this /just works/ for most models, even if some aren' :stream nil :models '("llamacpp" :capabilities (reasoning)) - :request-params '(:thinking t - :enable_thinking t - :include_reasoning t + :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") @@ -76,6 +76,9 @@ I change models a lot, and this /just works/ for most models, even if some aren' (require 'gptel) #+end_src +#+RESULTS: +: gptel + 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. @@ -102,8 +105,20 @@ config.el: (require 'gptel-org-tools) (mapcar (lambda (tool) (cl-pushnew tool gptel-tools)) gptel-org-tools) #+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 ** 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 (add-to-list 'gptel-org-tools (gptel-make-tool - :function (lambda (elisp) + :function ((lambda () (interactive) ) (elisp) (unless (stringp elisp) (error "elisp code must be a string")) (with-temp-buffer (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 (gptel-make-tool :function (lambda (arg) - (with-temp-buffer - (list-buffers) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) + (list-buffers-noselect) + (with-current-buffer "*Buffer List*" + (buffer-string))) :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)) + :description "Does nothing.")) :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. @@ -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. #+begin_src elisp (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 (org-map-entries #'(buffer-substring-no-properties (line-beginning-position) (line-end-position)) t - 'file))) + 'file)))) (add-to-list 'gptel-org-tools (gptel-make-tool @@ -655,6 +670,96 @@ And, the "grab everything that matches" tool. #+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 :PROPERTIES: :CREATED: <2025-04-14 Mon 22:46> diff --git a/gptel-org-tools.el b/gptel-org-tools.el index 8dc6db8..5abd253 100644 --- a/gptel-org-tools.el +++ b/gptel-org-tools.el @@ -1,3 +1,5 @@ +(defvar gptel-org-tools-skip-heading-extraction '()) + ;;; gptel-org-tools.el --- LLM Tools for org-mode interaction. -*- lexical-binding: t; -*- ;; Copyright (C) 2025 Phil Bajsicki @@ -39,17 +41,14 @@ (add-to-list 'gptel-org-tools (gptel-make-tool :function (lambda (arg) - (with-temp-buffer - (list-buffers) - (let ((content (buffer-string))) - (kill-buffer (current-buffer)) - content))) + (list-buffers-noselect) + (with-current-buffer "*Buffer List*" + (buffer-string))) :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)) + :description "Does nothing.")) :category "emacs")) (add-to-list 'gptel-org-tools @@ -144,13 +143,15 @@ :category "org-mode")) (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 (org-map-entries #'(buffer-substring-no-properties (line-beginning-position) (line-end-position)) t - 'file))) + 'file)))) (add-to-list 'gptel-org-tools (gptel-make-tool @@ -328,5 +329,73 @@ :description "The 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-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) ;;; gptel-org-tools.el ends here