gptel-got/gptel-org-tools.el
2025-04-15 15:42:37 +02:00

312 lines
10 KiB
EmacsLisp

;;; gptel-org-tools.el --- LLM Tools for org-mode interaction. -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Phil Bajsicki
;; Author: Phil Bajsicki <phil@bajsicki.com>
;; Keywords: extensions, comm, tools, matching, convenience,
;;
;; Author: Phil Bajsicki <phil@bajsicki.com>
;; Version: 0.0.1
;; Package-Requires: ((emacs "30.1") (gptel 0.9.8) (org-ql 0.9))
;; URL: https://github.com/phil/gptel-org-tools
;; SPDX-License-Identifier: GPL-3.0
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, version 3 of the License.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;; This file is NOT part of GNU Emacs.
;; This file is not part of GNU Emacs.
;;; Commentary:
;; All documentation regarding these functions is in the README.org file.
;; Repository: https://git.bajsicki.com/phil/gptel-org-tools
;;; Code:
(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")
(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")
(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")
(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")
(defun gptel-org-tools-org-extract-tags (buffer)
(interactive "bBuffer: ")
(with-current-buffer buffer
(let ((tags '()))
(org-map-entries
(lambda ()
(let* ((components (org-heading-components))
(tag-string (car (last components))))
(when tag-string
(dolist (tag (split-string tag-string ":" t))
(push tag tags))))))
(sort (-uniq tags) #'string<))))
(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
(line-beginning-position)
(line-end-position))
t
'file)))
(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
(get-buffer buf)
(if (stringp query)
(read query)
query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(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)
:action #'(lambda ()
(buffer-substring-no-properties
(line-beginning-position)
(org-end-of-subtree)))))
(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")
(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
(get-buffer buf)
`(heading ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(line-end-position))))))
(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))))))
(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
(get-buffer buf)
`(tags-local ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(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
(get-buffer buf)
`(tags ,query)
:action #'(lambda ()
(concat
(buffer-substring-no-properties
(line-beginning-position)
(progn
(outline-next-heading)
(line-beginning-position)))))))
(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)))))))
(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