Skip to content
Zen Monk Alain M. Lafon edited this page May 9, 2025 · 14 revisions

This page catalogs user-contributed tools for use with gptel's tool-use feature. These tools allow LLMs to perform various actions within Emacs, access external services, and manipulate data.

Using Tools

To use any of these tools, you need to:

  1. Enable tool-use in gptel: (setq gptel-use-tools t)
  2. Add the tool definitions you want to use to gptel-tools
  3. Copy and evaluate the Elisp code for the tools you want to use

Example:

;; Enable tool use
(setq gptel-use-tools t)

;; Add a tool to gptel-tools
(add-to-list 'gptel-tools
             (gptel-make-tool
              :name "read_url"
              :function (lambda (url) 
                         ;; function implementation
                         )
              :description "Fetch and read the contents of a URL"
              :args (list '(:name "url"
                            :type string
                            :description "The URL to read"))
              :category "web"))

Filesystem Tools

read_file

(gptel-make-tool
 :function (lambda (filepath)
             (with-temp-buffer
               (insert-file-contents (expand-file-name filepath))
               (buffer-string)))
 :name "read_file"
 :description "Read and display the contents of a file"
 :args (list '(:name "filepath"
               :type string
               :description "Path to the file to read. Supports relative paths and ~."))
 :category "filesystem")

list_directory

(gptel-make-tool
 :function (lambda (directory)
             (mapconcat #'identity
                        (directory-files directory)
                        "\n"))
 :name "list_directory"
 :description "List the contents of a given directory"
 :args (list '(:name "directory"
               :type string
               :description "The path to the directory to list"))
 :category "filesystem")

make_directory

(gptel-make-tool
 :function (lambda (parent name)
             (condition-case nil
                 (progn
                   (make-directory (expand-file-name name parent) t)
                   (format "Directory %s created/verified in %s" name parent))
               (error (format "Error creating directory %s in %s" name parent))))
 :name "make_directory"
 :description "Create a new directory with the given name in the specified parent directory"
 :args (list '(:name "parent"
               :type string
               :description "The parent directory where the new directory should be created, e.g. /tmp")
             '(:name "name"
               :type string
               :description "The name of the new directory to create, e.g. testdir"))
 :category "filesystem")

create_file

(gptel-make-tool
 :function (lambda (path filename content)
             (let ((full-path (expand-file-name filename path)))
               (with-temp-buffer
                 (insert content)
                 (write-file full-path))
               (format "Created file %s in %s" filename path)))
 :name "create_file"
 :description "Create a new file with the specified content"
 :args (list '(:name "path"
               :type string
               :description "The directory where to create the file")
             '(:name "filename"
               :type string
               :description "The name of the file to create")
             '(:name "content"
               :type string
               :description "The content to write to the file"))
 :category "filesystem")

edit_file

(defun my-gptel--edit_file (file-path file-edits)
  "In FILE-PATH, apply FILE-EDITS with pattern matching and replacing."
  (if (and file-path (not (string= file-path "")) file-edits)
      (with-current-buffer (get-buffer-create "*edit-file*")
        (insert-file-contents (expand-file-name file-path))
        (let ((inhibit-read-only t)
              (case-fold-search nil)
              (file-name (expand-file-name file-path))
              (edit-success nil))
          ;; apply changes
          (dolist (file-edit (seq-into file-edits 'list))
            (when-let ((line-number (plist-get file-edit :line_number))
                       (old-string (plist-get file-edit :old_string))
                       (new-string (plist-get file-edit :new_string))
                       (is-valid-old-string (not (string= old-string ""))))
              (goto-char (point-min))
              (forward-line (1- line-number))
              (when (search-forward old-string nil t)
                (replace-match new-string t t)
                (setq edit-success t))))
          ;; return result to gptel
          (if edit-success
              (progn
                ;; show diffs
                (ediff-buffers (find-file-noselect file-name) (current-buffer))
                (format "Successfully edited %s" file-name))
            (format "Failed to edited %s" file-name))))
    (format "Failed to edited %s" file-path)))

(gptel-make-tool
   :function #'my-gptel--edit_file
   :name "edit_file"
   :description "Edit file with a list of edits, each edit contains a line-number,
a old-string and a new-string, new-string will replace the old-string at the specified line."
   :args (list '(:name "file-path"
                       :type string
                       :description "The full path of the file to edit")
               '(:name "file-edits"
                       :type array
                       :items (:type object
                                     :properties
                                     (:line_number
                                      (:type integer :description "The line number of the file where edit starts.")
                                      :old_string
                                      (:type string :description "The old-string to be replaced.")
                                      :new_string
                                      (:type string :description "The new-string to replace old-string.")))
                       :description "The list of edits to apply on the file"))
   :category "filesystem")

run_command

    (gptel-make-tool
     :function (lambda (command &optional working_dir)
                 (with-temp-message (format "Executing command: `%s`" command)
                   (let ((default-directory (if (and working_dir (not (string= working_dir "")))
                                                (expand-file-name working_dir)
                                              default-directory)))
                     (shell-command-to-string command))))
     :name "run_command"
     :description "Executes a shell command and returns the output as a string. IMPORTANT: This tool allows execution of arbitrary code; user confirmation will be required before any command is run."
     :args (list
            '(:name "command"
                    :type string
                    :description "The complete shell command to execute.")
            '(:name "working_dir"
                    :type string
                    :description "Optional: The directory in which to run the command. Defaults to the current directory if not specified."))
     :category "command"
     :confirm t
     :include t)

run_async_command

(defun run_async_command (callback command)
  "Run COMMAND asynchronously and pass output to CALLBACK."
  (condition-case error
      (let ((buffer (generate-new-buffer " *async output*")))
        (with-temp-message (format "Running async command: %s" command)
          (async-shell-command command buffer nil))
        (let ((proc (get-buffer-process buffer)))
          (when proc
            (set-process-sentinel
             proc
             (lambda (process _event)
               (unless (process-live-p process)
                 (with-current-buffer (process-buffer process)
                   (let ((output (buffer-substring-no-properties (point-min) (point-max))))
                     (kill-buffer (current-buffer))
                     (funcall callback output)))))))))
    (t
     ;; Handle any kind of error
     (funcall callback (format "An error occurred: %s" error)))))

(gptel-make-tool
 :function #'run_async_command
 :name "run_async_command"
 :description "Run an async command."
 :args (list
        '(:name "command"
                :type "string"
                :description "Command to run."))
 :category "command"
 :async t
 :include t)

Emacs Tools

echo_message

(gptel-make-tool
 :function (lambda (text)
             (message "%s" text)
             (format "Message sent: %s" text))
 :name "echo_message"
 :description "Send a message to the *Messages* buffer"
 :args (list '(:name "text"
               :type string
               :description "The text to send to the messages buffer"))
 :category "emacs")

read_documentation

(defun gptel-read-documentation (symbol)
  "Read the documentation for SYMBOL, which can be a function or variable."
  (let ((sym (intern symbol)))
    (cond
     ((fboundp sym)
      (documentation sym))
     ((boundp sym)
      (documentation-property sym 'variable-documentation))
     (t
      (format "No documentation found for %s" symbol)))))

(gptel-make-tool
 :name "read_documentation"
 :function #'gptel-read-documentation
 :description "Read the documentation for a given function or variable"
 :args (list '(:name "name"
               :type string
               :description "The name of the function or variable whose documentation is to be retrieved"))
 :category "emacs")

Web Tools

read_url

(gptel-make-tool
 :function (lambda (url)
             (with-current-buffer (url-retrieve-synchronously url)
               (goto-char (point-min))
               (forward-paragraph)
               (let ((dom (libxml-parse-html-region (point) (point-max))))
                 (run-at-time 0 nil #'kill-buffer (current-buffer))
                 (with-temp-buffer
                   (shr-insert-document dom)
                   (buffer-substring-no-properties (point-min) (point-max))))))
 :name "read_url"
 :description "Fetch and read the contents of a URL"
 :args (list '(:name "url"
               :type string
               :description "The URL to read"))
 :category "web")

brave_search

(defvar brave-search-api-key "<YOUR API KEY>"
  "API key for accessing the Brave Search API.")

(defun brave-search-query (query)
  "Perform a web search using the Brave Search API with the given QUERY."
  (let ((url-request-method "GET")
        (url-request-extra-headers `(("X-Subscription-Token" . ,brave-search-api-key)))
        (url (format "https://api.search.brave.com/res/v1/web/search?q=%s" (url-encode-url query))))
    (with-current-buffer (url-retrieve-synchronously url)
      (goto-char (point-min))
      (when (re-search-forward "^$" nil 'move)
        (let ((json-object-type 'hash-table)) ; Use hash-table for JSON parsing
          (json-parse-string (buffer-substring-no-properties (point) (point-max))))))))

(gptel-make-tool
 :function #'brave-search-query
 :name "brave_search"
 :description "Perform a web search using the Brave Search API"
 :args (list '(:name "query"
               :type string
               :description "The search query string"))
 :category "web")

yt-dlp

(defun my/gptel-youtube-metadata (callback url)
  (let* ((video-id (and (string-match (concat
                                        "^\\(?:http\\(?:s?://\\)\\)?\\(?:www\\.\\)?\\(?:youtu\\(?:\\(?:\\.be\\|be\\.com\\)/\\)\\)"
                                        "\\(?:watch\\?v=\\)?"
                                        "\\([^?&]+\\)")
                                      url)
                        (match-string 1 url)))
         (dir (file-name-concat temporary-file-directory "yt-dlp" video-id)))
    (if (file-directory-p dir)
        (delete-directory dir t))
    (make-directory dir t)
    (let ((default-directory dir)
          (idx 0)
          (data (list :description nil :transcript nil)))
      (make-process :name "yt-dlp"
                    :command `("yt-dlp" "--write-description" "--skip-download" "--output" "video" ,url)
                    :sentinel (lambda (proc status)
                                (cl-incf idx)
                                (let ((default-directory dir))
                                  (when (file-readable-p "video.description")
                                    (plist-put data :description
                                               (with-temp-buffer
                                                 (insert-file-contents "video.description")
                                                 (buffer-string)))))
                                (when (= idx 2)
                                  (funcall callback (gptel--json-encode data))
                                  (delete-directory dir t))))
      (make-process :name "yt-dlp"
                    :command `("yt-dlp" "--skip-download" "--write-auto-subs" "--sub-langs" "en,-live_chat" "--convert-subs" "srt" "--output" "video" ,url)
                    :sentinel (lambda (proc status)
                                (cl-incf idx)
                                (let ((default-directory dir))
                                  (when (file-readable-p "video.en.srt")
                                    (plist-put data :transcript
                                               (with-temp-buffer
                                                 (insert-file-contents "video.en.srt")
                                                 (buffer-string)))))
                                (when (= idx 2)
                                  (funcall callback (gptel--json-encode data))
                                  (delete-directory dir t)))))))

(gptel-make-tool
 :name "youtube_video_metadata"
 :function #'my/gptel-youtube-metadata
 :description "Find the description and video transcript for a youtube video. Return a JSON object containing two fields:

\"description\": The video description added by the uploader
\"transcript\": The video transcript in SRT format"
 :args '((:name "url"
          :description "The youtube video URL, for example \"https://www.youtube.com/watch?v=H2qJRnV8ZGA\""
          :type string))
 :category "web"
 :async t
 :include t)

SQL Tools

select_from_db

(defun my/ejc-sql-eval-query (query &optional analyze-p connection-name)
  "Evaluate a SQL query using ejc-sql, ensuring a connection exists.
It takes a SQL query and the name of an existing ejc connection.
If no connection name is provided, it defaults to \"Default\".
If ANALYZE-P is non-nil, performs EXPLAIN ANALYZE on the query.
It will create a temporary buffer, connect to the database specified
by CONNECTION-NAME, evaluate the query, and return the result as a string.
It expects the connection CONNECTION-NAME to exist
using `ejc-connect'."
  (interactive)
  (let ((buffer (generate-new-buffer " *temp-ejc-sql-buffer*"))
        (result "")
        (actual-connection-name (or connection-name "Default"))
        (max-wait-time 30) ; Maximum wait time in seconds
        (wait-interval 0.1))
    (with-current-buffer buffer
      (ejc-connect actual-connection-name)
      (if analyze-p
          (insert (concat "EXPLAIN (ANALYZE true, COSTS true, FORMAT json) " query ";"))
        (insert (concat "SELECT json_agg(row_to_json (t)) FROM (" query ") t;")))
      (ejc-eval-user-sql-at-point))
    (let ((wait-time 0))
      (while (and (not (get-buffer "*ejc-sql-output*"))
                  (< wait-time max-wait-time))
        (sit-for wait-interval)
        (setq wait-time (+ wait-time wait-interval))))

    (when (get-buffer "*ejc-sql-output*")
      (with-current-buffer "*ejc-sql-output*"
        (setq result (buffer-string)))
      (kill-buffer "*ejc-sql-output*"))

    (kill-buffer buffer)
    result))

(gptel-make-tool
 :name "select_from_db"
 :function (lambda (query &optional analyze-p connection-name)
             (my/ejc-sql-eval-query query analyze-p connection-name))
 :description "Evaluate a SQL query using ejc-sql, ensuring a connection exists.
It takes a SQL query and the name of an existing ejc connection.
If no connection name is provided, it defaults to \"Default\".
If ANALYZE-P is non-nil, performs EXPLAIN ANALYZE on the query.
It will create a temporary buffer, connect to the database specified
by CONNECTION-NAME, evaluate the query, and return the result as a string."
 :args (list '(:name "query"
               :type string
                :description "The SQL query to evaluate")
             '(:name "analyze_p"
               :type string
               :optional t
               :description "If ANALYZE-P is non-nil, performs EXPLAIN ANALYZE on the query instead of executing it directly, returning query execution plan and statistics.")
             '(:name "connection_name"
               :type string
               :optional t
               :description "The name of the ejc-sql connection to use. This connection must already exist. Defaults to 'Default' if not provided."))
 :category "sql")

Buffer/File Editing Tools

read_buffer

(gptel-make-tool
 :function (lambda (buffer)
             (unless (buffer-live-p (get-buffer buffer))
               (error "Error: buffer %s is not live." buffer))
             (with-current-buffer buffer
               (buffer-substring-no-properties (point-min) (point-max))))
 :name "read_buffer"
 :description "Return the contents of an Emacs buffer"
 :args (list '(:name "buffer"
               :type string
               :description "The name of the buffer whose contents are to be retrieved"))
 :category "emacs")

append_to_buffer

(gptel-make-tool
 :function (lambda (buffer text)
             (with-current-buffer (get-buffer-create buffer)
               (save-excursion
                 (goto-char (point-max))
                 (insert text)))
             (format "Appended text to buffer %s" buffer))
 :name "append_to_buffer"
 :description "Append text to an Emacs buffer. If the buffer does not exist, it will be created."
 :args (list '(:name "buffer"
               :type string
               :description "The name of the buffer to append text to.")
             '(:name "text"
               :type string
               :description "The text to append to the buffer."))
 :category "emacs")

EditBuffer

(defun codel-edit-buffer (buffer-name old-string new-string)
  "In BUFFER-NAME, replace OLD-STRING with NEW-STRING."
  (with-current-buffer buffer-name
    (let ((case-fold-search nil))  ;; Case-sensitive search
      (save-excursion
        (goto-char (point-min))
        (let ((count 0))
          (while (search-forward old-string nil t)
            (setq count (1+ count)))
          (if (= count 0)
              (format "Error: Could not find text to replace in buffer %s" buffer-name)
            (if (> count 1)
                (format "Error: Found %d matches for the text to replace in buffer %s" count buffer-name)
              (goto-char (point-min))
              (search-forward old-string)
              (replace-match new-string t t)
              (format "Successfully edited buffer %s" buffer-name))))))))

(gptel-make-tool
 :name "EditBuffer"
 :function #'codel-edit-buffer
 :description "Edits Emacs buffers"
 :args '((:name "buffer_name"
                :type string
                :description "Name of the buffer to modify"
                :required t)
         (:name "old_string"
                :type string
                :description "Text to replace (must match exactly)"
                :required t)
         (:name "new_string"
                :type string
                :description "Text to replace old_string with"
                :required t))
 :category "edit")

ReplaceBuffer

(defun codel-replace-buffer (buffer-name content)
  "Completely replace contents of BUFFER-NAME with CONTENT."
  (with-current-buffer buffer-name
    (erase-buffer)
    (insert content)
    (format "Buffer replaced: %s" buffer-name)))

(gptel-make-tool
 :name "ReplaceBuffer"
 :function #'codel-replace-buffer
 :description "Completely overwrites buffer contents"
 :args '((:name "buffer_name"
                :type string
                :description "Name of the buffer to overwrite"
                :required t)
         (:name "content"
                :type string
                :description "Content to write to the buffer"
                :required t))
 :category "edit")

External Resources

For more tools and discussions: