;;; llm-tools.el --- Emacs tools for GPTel models -*- lexical-binding: t -*- ;; Copyright (C) 2024 ;; Author: Your Name ;; Keywords: tools, convenience, gptel ;; This file is NOT part of GNU Emacs. ;;; Commentary: ;; This file provides GPTel tools that allow LLM models to look up ;; information in Emacs, including function names and documentation, ;; variable names and documentation, key bindings, and more. ;;; Code: (require 'gptel) ;; ============================================================================ ;; Documentation Lookup Tools ;; ============================================================================ (gptel-make-tool :name "get_function_doc" :category "emacs" :description "Get the documentation string for an Emacs Lisp function. Returns the function's docstring, argument list, and source file location." :function (lambda (function-name) (let ((sym (intern-soft function-name))) (if (and sym (fboundp sym)) (let* ((doc (documentation sym t)) (arglist (help-function-arglist sym)) (file (find-lisp-object-file-name sym 'defun))) (format "Function: %s\nArguments: %s\nFile: %s\n\nDocumentation:\n%s" sym (if arglist (prin1-to-string arglist) "(unknown)") (or file "built-in/unknown") (or doc "No documentation available"))) (format "Error: Function '%s' not found" function-name)))) :args '((:name "function-name" :type string :description "The name of the function (symbol) to look up" :optional nil))) (gptel-make-tool :name "get_variable_doc" :category "emacs" :description "Get the documentation string for an Emacs Lisp variable. Returns the variable's docstring, current value (if safe to display), and source file location." :function (lambda (variable-name) (let ((sym (intern-soft variable-name))) (if (and sym (boundp sym)) (let* ((doc (documentation-property sym 'variable-documentation)) (file (find-lisp-object-file-name sym 'defvar)) (value (bound-and-true-p sym)) (value-str (if (and value (or (numberp value) (stringp value) (symbolp value) (and (listp value) (< (length (format "%s" value)) 200)))) (format "%S" value) "(complex value omitted)"))) (format "Variable: %s\nCurrent Value: %s\nFile: %s\n\nDocumentation:\n%s" sym value-str (or file "built-in/unknown") (or doc "No documentation available"))) (format "Error: Variable '%s' not found" variable-name)))) :args '((:name "variable-name" :type string :description "The name of the variable (symbol) to look up" :optional nil))) (gptel-make-tool :name "get_face_doc" :category "emacs" :description "Get information about an Emacs face including its documentation and attributes." :function (lambda (face-name) (let ((sym (intern-soft face-name))) (if (and sym (facep sym)) (let* ((doc (documentation-property sym 'face-documentation)) (attrs (face-all-attributes sym (selected-frame)))) (format "Face: %s\n\nDocumentation:\n%s\n\nAttributes:\n%s" sym (or doc "No documentation available") (mapconcat (lambda (attr) (format " %s: %S" (car attr) (cdr attr))) attrs "\n"))) (format "Error: Face '%s' not found" face-name)))) :args '((:name "face-name" :type string :description "The name of the face to look up" :optional nil))) (gptel-make-tool :name "describe_key" :category "emacs" :description "Get the command bound to a specific key sequence in Emacs." :function (lambda (key-sequence) (condition-case err (let* ((keys (read-kbd-macro key-sequence)) (binding (key-binding keys)) (cmd (if (arrayp binding) (format "Keymap: %S" binding) binding))) (if (and cmd (not (eq cmd 'nil))) (format "Key: %s\nBound to: %s\n\nDocumentation:\n%s" key-sequence (if (symbolp cmd) cmd (format "%S" cmd)) (if (symbolp cmd) (or (documentation cmd t) "No documentation") "Not a command")) (format "Key '%s' is not bound to any command" key-sequence))) (error (format "Error parsing key sequence '%s': %s" key-sequence (error-message-string err))))) :args '((:name "key-sequence" :type string :description "The key sequence as a string (e.g., 'C-x C-f')" :optional nil))) (gptel-make-tool :name "apropos_lookup" :category "emacs" :description "Look up Emacs symbols using apropos. Searches for symbols matching a pattern and returns their names, types, and documentation snippets." :function (lambda (pattern type) (let* ((symbols (apropos-internal pattern (lambda (s) (fboundp s)))) (vars (apropos-internal pattern (lambda (s) (boundp s)))) (faces (apropos-internal pattern (lambda (s) (facep s)))) (filter-fn (cond ((string= type "functions") (lambda (s) (memq s symbols))) ((string= type "variables") (lambda (s) (memq s vars))) ((string= type "faces") (lambda (s) (memq s faces))) (t (lambda (s) t)))) (all-symbols (delete-dups (append symbols vars faces))) (filtered (seq-filter filter-fn all-symbols))) (if filtered (format "Found %d symbols matching '%s':\n\n%s" (length filtered) pattern (mapconcat (lambda (sym) (format "%s (%s)\n %s" sym (cond ((fboundp sym) "function") ((boundp sym) "variable") ((facep sym) "face") (t "symbol")) (let ((doc (or (documentation sym t) (documentation-property sym 'variable-documentation) (documentation-property sym 'face-documentation) "No documentation"))) (if (> (length doc) 100) (concat (substring doc 0 100) "...") doc)))) filtered "\n\n")) (format "No symbols matching pattern '%s' found" pattern)))) :args '((:name "pattern" :type string :description "The regex pattern to search for in symbol names" :optional nil) (:name "type" :type string :description "Filter by type: 'functions', 'variables', 'faces', or 'all' (default 'all')" :optional t :default "all"))) ;; ============================================================================ ;; Buffer/List Tools ;; ============================================================================ (gptel-make-tool :name "list_modes" :category "emacs" :description "List major and minor modes available in Emacs." :function (lambda (mode-type pattern) (let ((major-modes (apropos-internal "-mode\\'" (lambda (s) (and (boundp s) (get s 'derived-mode-parent))))) (minor-modes (apropos-internal "-mode\\'" (lambda (s) (and (boundp s) (get s 'minor-mode-function))))) (filter (when pattern (lambda (s) (string-match-p pattern (symbol-name s)))))) (when filter (setq major-modes (seq-filter filter major-modes)) (setq minor-modes (seq-filter filter minor-modes))) (let ((results (cond ((string= mode-type "major") (cons "Major Modes" major-modes)) ((string= mode-type "minor") (cons "Minor Modes" minor-modes)) (t `(("Major Modes" . ,major-modes) ("Minor Modes" . ,minor-modes)))))) (if (consp (car results)) (format "%s\n\n%s" (format "%s (%d):\n%s" (caar results) (length (cdar results)) (mapconcat (lambda (m) (format " - %s" m)) (cdar results) "\n")) (format "%s (%d):\n%s" (cadr results) (length (cddr results)) (mapconcat (lambda (m) (format " - %s" m)) (cddr results) "\n"))) (format "%s (%d):\n%s" (car results) (length (cdr results)) (mapconcat (lambda (m) (format " - %s" m)) (cdr results) "\n")))))) :args '((:name "mode-type" :type string :description "Type of mode: 'major', 'minor', or 'all' (default 'all')" :optional t :default "all") (:name "pattern" :type string :description "Optional regex pattern to filter mode names" :optional t))) ;; ============================================================================ ;; Source Location Tools ;; ============================================================================ (gptel-make-tool :name "find_function_source" :category "emacs" :description "Find the file where an Emacs Lisp function is defined. Returns the file path and line number if available." :function (lambda (function-name) (let ((sym (intern-soft function-name))) (if (and sym (fboundp sym)) (let* ((file (find-lisp-object-file-name sym 'defun)) (source (when file (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) (re-search-forward (format "^(defun\\s +%s" (regexp-quote function-name)) nil t) (when (re-search-backward "^(defun\\s +" nil t) (cons file (line-number-at-pos))))))) (if source (format "Function '%s' is defined in:\n %s:%d" function-name (car source) (cdr source)) (format "Function '%s' source location: %s" function-name (or file "built-in/unknown")))) (format "Error: Function '%s' not found" function-name)))) :args '((:name "function-name" :type string :description "The name of the function to locate" :optional nil))) (gptel-make-tool :name "find_variable_source" :category "emacs" :description "Find the file where an Emacs Lisp variable is defined. Returns the file path if available." :function (lambda (variable-name) (let ((sym (intern-soft variable-name))) (if (and sym (boundp sym)) (let ((file (or (find-lisp-object-file-name sym 'defvar) (find-lisp-object-file-name sym 'defcustom) (find-lisp-object-file-name sym 'defconst)))) (format "Variable '%s' is defined in: %s" variable-name (or file "built-in/unknown"))) (format "Error: Variable '%s' not found" variable-name)))) :args '((:name "variable-name" :type string :description "The name of the variable to locate" :optional nil))) ;; ============================================================================ ;; Current Context Tools ;; ============================================================================ (gptel-make-tool :name "get_buffer_context" :category "emacs" :description "Get information about the current buffer including its name, major mode, minor modes, and file path if applicable." :function (lambda () (format "Buffer Context:\n Name: %s\n File: %s\n Major Mode: %s\n Minor Modes: %s\n Read-only: %s\n Modified: %s" (buffer-name (current-buffer)) (or (buffer-file-name (current-buffer)) "N/A") major-mode (mapconcat (lambda (m) (format "%s" m)) (seq-filter (lambda (m) (and (boundp m) (symbol-value m))) minor-mode-list) ", ") (if buffer-read-only "yes" "no") (if (buffer-modified-p) "yes" "no"))) :args nil) (gptel-make-tool :name "get_active_features" :category "emacs" :description "Get information about active Emacs features and packages." :function (lambda () (format "Active Features (loaded packages/modules):\n%s" (mapconcat (lambda (f) (format " - %s" f)) features "\n"))) :args nil) ;; ============================================================================ ;; Tool Registry for Easy Enable/Disable ;; ============================================================================ (defconst llm-tools-tool-list '("get_function_doc" "get_variable_doc" "get_face_doc" "describe_key" "apropos_lookup" "list_modes" "find_function_source" "find_variable_source" "get_buffer_context" "get_active_features") "List of all LLM tool symbols defined in this file.") ;;;###autoload (defun llm-tools-enable-all () "Enable all LLM Emacs tools for GPTel." (interactive) (dolist (tool llm-tools-tool-list) (when (and tool (gptel-get-tool tool)) (add-to-list 'gptel-tools (gptel-get-tool tool)))) (message "LLM Emacs tools enabled for GPTel (%d tools)" (length gptel-tools))) ;;;###autoload (defun llm-tools-disable-all () "Disable all LLM Emacs tools from GPTel." (interactive) (dolist (tool llm-tools-tool-list) (let ((tool-obj (gptel-get-tool tool))) (when tool-obj (setq gptel-tools (delete tool-obj gptel-tools))))) (message "LLM Emacs tools disabled from GPTel")) (provide 'llm-tools) ;;; llm-tools.el ends here