-
Notifications
You must be signed in to change notification settings - Fork 241
Tools collection
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.
To use any of these tools, you need to:
- Enable tool-use in gptel:
(setq gptel-use-tools t)
- Add the tool definitions you want to use to
gptel-tools
- 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"))
(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")
(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")
(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")
(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")
(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")
(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)
(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)
(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")
(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")
(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")
(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")
(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)
(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")
(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")
(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")
(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")
(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")
For more tools and discussions:
- llm-tool-collection - A community collection of tools for use with LLM clients in Emacs. To discuss tools, check out the discussions page
- codel.el - A collection of code editing tools
- mcp.el - Model Context Protocol implementation for Emacs