diff options
Diffstat (limited to 'gptel-tools/read_text_file.el')
| -rw-r--r-- | gptel-tools/read_text_file.el | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/gptel-tools/read_text_file.el b/gptel-tools/read_text_file.el new file mode 100644 index 00000000..8e0433a9 --- /dev/null +++ b/gptel-tools/read_text_file.el @@ -0,0 +1,144 @@ +;;; read_text_file.el --- Read text files for GPTel -*- coding: utf-8; lexical-binding: t; -*- + +;; Copyright (C) 2025 + +;; Author: gptel-tool-writer +;; Keywords: convenience, tools +;; Package-Requires: ((emacs "27.1") (gptel "0.9.0")) + +;; This file is not part of GNU Emacs. + +;; 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, either version 3 of the License, or +;; (at your option) any later version. + +;; 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. + +;;; Commentary: + +;;; Code: + +;; Helper functions for read_text_file tool +(defun cj/validate-file-path (path) + "Validate PATH is within home directory and exists." + (let ((full-path (expand-file-name path "~"))) + (unless (string-prefix-p (expand-file-name "~") full-path) + (error "Path must be within home directory")) + (unless (file-exists-p full-path) + (error "File not found: %s" full-path)) + (when (file-directory-p full-path) + (error "Path is a directory, not a file: %s" full-path)) + (unless (file-readable-p full-path) + (error "No read permission for file: %s" full-path)) + ;; Follow symlinks + (if (file-symlink-p full-path) + (file-truename full-path) + full-path))) + +(defun cj/get-file-metadata (path) + "Return formatted metadata string for file at PATH." + (let* ((attributes (file-attributes path)) + (size (file-attribute-size attributes)) + (modes (file-attribute-modes attributes)) + (modtime (format-time-string "%Y-%m-%d" + (file-attribute-modification-time attributes)))) + (list :size size + :string (format "File: %s (%s, %s, modified %s)" + path modes + (file-size-human-readable size) + modtime)))) + +(defun cj/check-file-size-limits (size no-confirm) + "Check file SIZE against limits, prompting user unless NO-CONFIRM." + (let ((size-warning-limit (* 10 1024 1024)) ; 10MB + (size-hard-limit (* 100 1024 1024))) ; 100MB + (when (> size size-hard-limit) + (error "File too large (%s): exceeds 100MB limit" + (file-size-human-readable size))) + (when (and (> size size-warning-limit) + (not no-confirm)) + (unless (y-or-n-p (format "File is large (%s). Continue? " + (file-size-human-readable size))) + (error "File read cancelled: size exceeds 10MB"))))) + +(defun cj/detect-binary-file (path) + "Check if file at PATH appears to be binary." + (with-temp-buffer + (insert-file-contents path nil 0 1024) + (goto-char (point-min)) + (search-forward "\0" nil t))) + +(defun cj/handle-special-file-types (path no-confirm) + "Handle PDF, EPUB, and other binary files at PATH." + (cond + ((string-match-p "\\.pdf\\'" path) + (when (and (not no-confirm) + (not (y-or-n-p "This is a PDF file. Extract text for LLM (y) or cancel (n)? "))) + (error "PDF file read cancelled")) + ;; Extract text from PDF + (let ((text (shell-command-to-string + (format "pdftotext '%s' -" path)))) + (if (string-empty-p text) + (error "Could not extract text from PDF: %s" path) + text))) + ((string-match-p "\\.epub\\'" path) + (when (and (not no-confirm) + (not (y-or-n-p "This is an EPUB file. Extract text for LLM (y) or cancel (n)? "))) + (error "EPUB file read cancelled")) + (error "EPUB text extraction not yet implemented")) + (t + (when (and (not no-confirm) + (not (y-or-n-p "This appears to be a binary file. Read anyway? "))) + (error "Binary file read cancelled")) + nil))) ; Return nil to indicate normal read + +;; Main tool function using the helpers +(gptel-make-tool + :name "read_text_file" + :function (lambda (path &optional no-confirm) + (let* ((full-path (cj/validate-file-path path)) + (metadata (cj/get-file-metadata full-path)) + (size (plist-get metadata :size)) + (metadata-string (plist-get metadata :string))) + ;; Show metadata and confirm + (unless no-confirm + (unless (y-or-n-p (format "%s\nRead this file? " metadata-string)) + (error "File read cancelled by user"))) + ;; Check size limits + (cj/check-file-size-limits size no-confirm) + ;; Handle binary/special files + (let ((content + (if (cj/detect-binary-file full-path) + (or (cj/handle-special-file-types full-path no-confirm) + ;; If not a special type or user wants to read anyway + (with-temp-buffer + (insert-file-contents full-path) + (buffer-string))) + ;; Normal text file + (with-temp-buffer + (insert-file-contents full-path) + (buffer-string))))) + (format "Read %d bytes from %s\n\n%s" + (length content) full-path content)))) + :description "Read text content from a file within the user's home directory. Shows file metadata and requests confirmation before reading. Handles large files, binary detection, and PDF text extraction." + :args (list '(:name "path" + :type string + :description "File path relative to home directory, e.g., 'documents/myfile.txt' or '~/documents/myfile.txt'") + '(:name "no_confirm" + :type boolean + :description "If true, skip confirmation prompts and read immediately" + :optional t)) + :category "filesystem" + :confirm t + :include t) + +;; Automatically add to gptel-tools on load +(add-to-list 'gptel-tools (gptel-get-tool '("filesystem" "read_text_file"))) + + +(provide 'read_text_file) +;;; read_text_file.el ends here. |
