adding an extra directory to store some tweaked extra packages
This commit is contained in:
parent
74b09e4285
commit
781f753542
4 changed files with 1079 additions and 24 deletions
13
README.org
13
README.org
|
|
@ -2301,13 +2301,14 @@ GPTEL is a package that uses chatGPT to get some text generation in org-mode
|
|||
#+begin_src emacs-lisp
|
||||
(use-package gptel
|
||||
:init
|
||||
(setq gptel-model "gemma3:4b-it-q8_0"
|
||||
(setq gptel-model "qwen3.5:latest"
|
||||
gptel-backend (gptel-make-ollama "ollama"
|
||||
:host "ai.tfcconnection.org"
|
||||
:protocol "https"
|
||||
:stream t
|
||||
:models '("deepseek-r1" "gemma3:latest"))
|
||||
:models '("deepseek-r1" "qwen3.5:latest" "gemma3:latest"))
|
||||
gptel-default-mode #'org-mode)
|
||||
(add-hook 'gptel-post-response-functions 'gptel-end-of-response)
|
||||
(setq gptel-directives '((default
|
||||
. "You are a large language model living in Emacs and a helpful assistant. Respond concisely.")
|
||||
(programming
|
||||
|
|
@ -2319,11 +2320,15 @@ GPTEL is a package that uses chatGPT to get some text generation in org-mode
|
|||
(dnd . "Assume the role of an expert fantasy writer that specializes in interactive fiction, as well as the storyline, characters, locations, descriptions, groups and organizations, stories, events, and magical objects of Baldur’s Gate, Tales of the Sword Coast, Baldur’s Gate 2: Shadows of Amn, Throne of Bhaal, and Baldur’s Gate 3. The adventure takes place on the continent of Faerun.
|
||||
|
||||
Describe everything that follows in the present tense, in response to what I type, while accurately following the established lore of Baldur’s Gate, Tales of the Sword Coast, Baldur’s Gate 2: Shadows of Amn, Throne of Bhaal, and Baldur’s Gate 3, written in the descriptive style of R.A Salvatore. Provide names for characters, locations, groups and organizations, events, and magical objects. Characters should always use dialogue, enclosed in quotation marks when interacting with me or my party members, written in the conversational style of R.A Salvatore. Do not type, compose, dictate, influence, script, generate, control, or describe what me or my party members are doing, saying, acting, behaving, thinking, feeling, experiencing, or any other aspect concerning me or my party members throughout the entire adventure, scenario, story, location, quest, mission, scene, event, description, dialogue, and conversation. Use only one paragraph consisting of less than 80 words for all replies, descriptions, conversations, dialogue and text.")))
|
||||
|
||||
;; (gptel-make-tool )
|
||||
|
||||
:general
|
||||
(chris/leader-keys
|
||||
:states '(normal visual)
|
||||
:keymaps 'override
|
||||
"ls" 'gptel-send
|
||||
"lA" 'gptel-add
|
||||
"lm" 'gptel-menu))
|
||||
|
||||
;;(straight-use-package
|
||||
|
|
@ -2398,7 +2403,7 @@ Idk, let's try this i guess
|
|||
:config
|
||||
(setenv "OLLAMA_API_BASE" "https://ai.tfcconnection.org")
|
||||
(setenv "OPENAI_API_KEY" "")
|
||||
(setq aidermacs-default-model "ollama_chat/qwen3"
|
||||
(setq aidermacs-default-model "ollama_chat/qwen3.5"
|
||||
aidermacs-backend 'comint
|
||||
aidermacs-watch-files t)
|
||||
:general
|
||||
|
|
@ -5121,7 +5126,7 @@ Let's use pdf-tools for a lot better interaction with pdfs.
|
|||
"-o ColorModel=RGB"
|
||||
"-o ColorModel=Gray"))))
|
||||
(message "printing %s copies." copies)
|
||||
(async-shell-command (format "lpr %s '%s'" (string-join pdf-misc-print-program-args " ") (buffer-file-name)))))
|
||||
(async-shell-command (format "lpr %s \"%s\"" (string-join pdf-misc-print-program-args " ") (buffer-file-name)))))
|
||||
|
||||
(custom-set-variables '(pdf-misc-print-program-executable "lpr")
|
||||
'(pdf-misc-print-program-args (quote ("-o media=Letter" "-o fit-to-page" "-o sides=two-sided-long-edge"))))
|
||||
|
|
|
|||
478
extra-packages/gptel-got.el
Normal file
478
extra-packages/gptel-got.el
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
;;; gptel-got.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.2
|
||||
;; Package-Requires: ((emacs "30.1") (gptel "0.9.8") (org-ql "0.9"))
|
||||
;; URL: https://github.com/phil/gptel-got
|
||||
;; 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-got
|
||||
|
||||
;;; Code:)
|
||||
|
||||
(defvar gptel-got '())
|
||||
|
||||
(defvar gptel-got-skip-heading-extraction '())
|
||||
|
||||
(defvar gptel-got-result-limit 40000)
|
||||
|
||||
(defun gptel-got--result-limit (result)
|
||||
(if (>= (length (format "%s" result)) gptel-got-result-limit)
|
||||
(format "Results over %s characters. Stop. Analyze. Find a different solution, or use a more specific query." gptel-got-result-limit)
|
||||
result))
|
||||
|
||||
(defun gptel-got--heading ()
|
||||
"Return the org-mode heading."
|
||||
(concat
|
||||
(buffer-substring-no-properties
|
||||
(line-beginning-position)
|
||||
(line-end-position))))
|
||||
|
||||
(defun gptel-got--heading-body ()
|
||||
"Return the org-mode heading and body text."
|
||||
(concat
|
||||
(buffer-substring-no-properties
|
||||
(line-beginning-position)
|
||||
(progn
|
||||
(outline-next-heading)
|
||||
(line-beginning-position)))))
|
||||
|
||||
(defun gptel-got--heading-subtree ()
|
||||
"Return the org-mode heading and all subheadings, with their body text."
|
||||
(concat
|
||||
(buffer-substring-no-properties
|
||||
(line-beginning-position)
|
||||
(org-end-of-subtree))))
|
||||
|
||||
(defun gptel-got-tool--list-buffers (&optional arg)
|
||||
"Return list of buffers."
|
||||
(list-buffers-noselect)
|
||||
(with-current-buffer "*Buffer List*"
|
||||
(let ((content (buffer-string)))
|
||||
content)))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got-tool--list-buffers
|
||||
:name "gptel-got-tool--list-buffers"
|
||||
:args (list '(:name "arg"
|
||||
:type string
|
||||
:description "Does nothing."
|
||||
:optional t))
|
||||
:category "emacs"
|
||||
:description "List buffers open in Emacs, including file names and full paths. After using this, stop. Then evaluate which files are most likely to be relevant to the user's request."))
|
||||
|
||||
(defun gptel-got--dir (dir)
|
||||
"Return directory listing."
|
||||
(with-temp-buffer
|
||||
(dired (or dir "~"))
|
||||
(let ((content (buffer-string)))
|
||||
(kill-buffer (current-buffer))
|
||||
content)))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--dir
|
||||
:name "gptel-got--dir"
|
||||
:description "List directory contents. After using this, stop. Evaluate for relevance. Then use your findings to fulfill user's request."
|
||||
:args (list '(:name "dir"
|
||||
:type string
|
||||
:description "Directory path"
|
||||
:optional t))
|
||||
:category "filesystem"))
|
||||
|
||||
(defun gptel-got--open-file-inactive (file)
|
||||
"Open FILE in a background buffer without modifying its contents."
|
||||
(find-file-noselect file)
|
||||
(set-buffer-modified-p nil))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--open-file-inactive
|
||||
:name "gptel-got--open-file-inactive"
|
||||
:description "Open the file in a background buffer. This does NOT return the contents of the file. After calling this tool, stop. Then continue fulfilling user's request."
|
||||
:args (list '(:name "file"
|
||||
:type string
|
||||
:description "Path to file.."))
|
||||
:category "filesystem"))
|
||||
|
||||
(defun gptel-got--describe-variable (var)
|
||||
"Return documentation for VAR."
|
||||
(let ((symbol (intern var)))
|
||||
(if (boundp symbol)
|
||||
(prin1-to-string (symbol-value symbol))
|
||||
(format "Variable %s is not bound. This means the variable doesn't exist. Stop. Reassess what you're trying to do, examine the situation, and continue. " var))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--describe-variable
|
||||
:name "gptel-got--describe-variable"
|
||||
:description "Returns variable contents. After calling this tool, stop. Evaluate if the result helps. Then continue fulfilling user's request."
|
||||
:args (list '(:name "var"
|
||||
:type string
|
||||
:description "Variable name"))
|
||||
:category "emacs"))
|
||||
|
||||
(defun gptel-got--describe-function (fun)
|
||||
"Return documentation for FUN."
|
||||
(let ((symbol (intern fun)))
|
||||
(if (fboundp symbol)
|
||||
(prin1-to-string (documentation symbol 'function))
|
||||
(format "Function %s is not defined." fun))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--describe-function
|
||||
:name "gptel-got--describe-function"
|
||||
:description "Returns function description. After calling this tool, stop. Evaluate if the result helps. Then continue fulfilling user's request."
|
||||
:args (list '(:name "fun"
|
||||
:type string
|
||||
:description "Function name"
|
||||
:optional t))
|
||||
:category "emacs"))
|
||||
|
||||
(defun gptel-got--org-extract-tags (buffer)
|
||||
"Return all tags from BUFFER."
|
||||
(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<))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-extract-tags
|
||||
:name "gptel-got--org-extract-tags"
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The Org buffer to extract tags from."))
|
||||
:category "org-mode"
|
||||
:description "Returns all tags from an org-mode buffer. When using this, evaluate the relevance of each tag to the user's request. After calling this tool, stop. Then continue fulfilling user's request."))
|
||||
|
||||
(defun gptel-got--org-extract-headings (buffer)
|
||||
"Return all headings from BUFFER."
|
||||
(if (member buffer gptel-got-skip-heading-extraction)
|
||||
(user-error "Buffer %s has been blocked from this function by the user with reason: headings contain timestamps, no useful information. Use a different tool." buffer)
|
||||
(with-current-buffer buffer
|
||||
(org-map-entries
|
||||
#'gptel-got--heading
|
||||
t
|
||||
'file))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-extract-headings
|
||||
:name "gptel-got--org-extract-headings"
|
||||
:description "Returns all headings from an org-mode buffer. After using this, stop. Then evaluate the relevance of the headings to the user's request. Then continue."
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The Org buffer to extract headings from."))
|
||||
:category "org-mode"))
|
||||
|
||||
(defun gptel-got--org-ql-select-date (buf date)
|
||||
"Returns all timestamped headings matching the specified date or date range.
|
||||
The date can be in the format YYYY, YYYY-MM, or YYYY-MM-DD.
|
||||
|
||||
BUFFER is the name of the buffer to search.
|
||||
DATE is the date or date range to match."
|
||||
(let* ((buffer (get-buffer buf))
|
||||
(mode (buffer-local-value 'major-mode buffer)))
|
||||
(if (bufferp buffer)
|
||||
(if (eq mode 'org-mode)
|
||||
(let ((result
|
||||
(org-ql-select buffer
|
||||
`(heading ,date)
|
||||
:action #'gptel-got--heading-subtree)))
|
||||
(concat (gptel-got--result-limit result) "Results end here. Proceed with the next action."))
|
||||
(message "Buffer '%s' isn't an org-mode buffer." buf))
|
||||
(message "Buffer '%s' does not exist." buf))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-ql-select-date
|
||||
:name "gptel-got--org-ql-select-date"
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "Buffer name.")
|
||||
'(:name "date"
|
||||
:type string
|
||||
:description "Date string in YYYY or YYYY-MM format. No <, >, [, ]. Just the numbers and dashes."))
|
||||
:category "org"
|
||||
:description "Returns all timestamped headings and all of their subheadings matching query. Query may be: YYYY, YYYY-MM, YYYY-MM-DD.
|
||||
Examples:
|
||||
- \"YYYY\": gets all entries from year YYYY
|
||||
- \"YYYY-MM\": gets all entries from month MM of year YYYY
|
||||
|
||||
After using this, stop. Then evaluate the relevance of the entries to the user's request. Then continue."))
|
||||
|
||||
(defun gptel-got--org-agenda-seek (days)
|
||||
"Return the results of org-agenda-list spanning now to DAYS."
|
||||
(with-temp-buffer
|
||||
(org-agenda-list (or days 14))
|
||||
(let ((content (buffer-string)))
|
||||
(kill-buffer (current-buffer))
|
||||
(gptel-got--result-limit content))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-agenda-seek
|
||||
:name "gptel-got--org-agenda-seek"
|
||||
:args (list '(:name "days"
|
||||
:type integer
|
||||
:description "Days. Positive = future. Negative = past. Default: 14"))
|
||||
:category "org"
|
||||
:description "Return user's agenda (tasking) spanning X days from the current moment. Example:
|
||||
- Get all tasks due in the next 7 days: \"7\"
|
||||
- Get all tasks in the last 14 days: \"-14\"
|
||||
After using this, stop. Evaluate the relevance of the results. Then continue."))
|
||||
|
||||
(defun gptel-got--org-ql-select-headings (buf query)
|
||||
"Return headings matching QUERY from BUFFER."
|
||||
(let* ((buffer (get-buffer buf))
|
||||
(mode (buffer-local-value 'major-mode buffer)))
|
||||
(if buffer
|
||||
(if (eq mode 'org-mode)
|
||||
(let ((result
|
||||
(org-ql-select
|
||||
(get-buffer buf)
|
||||
`(heading ,query)
|
||||
:action #'gptel-got--heading)))
|
||||
(concat (gptel-got--result-limit result) "Results end here. Proceed with the next action."))
|
||||
(message "Buffer '%s' isn't an org-mode buffer." buf))
|
||||
(message "Buffer '%s' does not exist." buf))))
|
||||
|
||||
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-ql-select-headings
|
||||
:name "gptel-got--org-ql-select-headings"
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The name of the buffer. See the NAME column in ~list-buffers~.")
|
||||
'(:name "query"
|
||||
:type string
|
||||
:description "The string to match entry headings against."))
|
||||
:category "org-ql"
|
||||
:description "Returns entries matching QUERY from BUFFER. Matches only a single string. After using this, evaluate which entries are relevant, and continue with user's request."))
|
||||
|
||||
(defun gptel-got--org-ql-select-headings-rifle (buf query)
|
||||
"Return headings of entries (body included) that match keyword QUERY from BUFFER."
|
||||
(let ((result
|
||||
(org-ql-select
|
||||
(get-buffer buf)
|
||||
`(rifle ,query)
|
||||
:action #'gptel-got--heading)))
|
||||
(gptel-got--result-limit result)))
|
||||
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-ql-select-headings-rifle
|
||||
:name "gptel-got--org-ql-select-headings-rifle"
|
||||
:description "Returns headings matching QUERY from BUFFER. Matches against both heading and content, but only returns headings. After using this, stop. Evaluate results. Then continue completing user's request."
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The name of the buffer. See the NAME column in ~list-buffers~.")
|
||||
'(:name "query"
|
||||
:type string
|
||||
:description "The string to match entry headings against."))
|
||||
:category "org-ql"))
|
||||
|
||||
(defun gptel-got--org-ql-select-tags-local (buf query)
|
||||
"Return entries whose tags match QUERY in BUFFER, without inheritance."
|
||||
(let* ((buffer (get-buffer buf))
|
||||
(mode (buffer-local-value 'major-mode buffer)))
|
||||
(if buffer
|
||||
(if (eq mode 'org-mode)
|
||||
(let ((result
|
||||
(org-ql-select
|
||||
(get-buffer buf)
|
||||
`(tags-local ,query)
|
||||
:action #'gptel-got--heading-body)))
|
||||
(gptel-got--result-limit result))
|
||||
(message "Buffer '%s' isn't an org-mode buffer." buf))
|
||||
(message "Buffer '%s' does not exist." buf))))
|
||||
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-ql-select-tags-local
|
||||
:name "gptel-got--org-ql-select-tags-local"
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
|
||||
'(:name "query"
|
||||
:type string
|
||||
:description "The string match entry tags against."))
|
||||
:category "org-ql"
|
||||
:description "Returns entries whose tags match QUERY from BUFFER, without tag inheritance. After using this, stop. Evaluate results for relevance, then proceed with completing user's request."))
|
||||
|
||||
(defun gptel-got--org-ql-select-tags-local-count (buf query)
|
||||
"Return count of entries tagged QUERY in BUFFER."
|
||||
(let* ((buffer (get-buffer buf))
|
||||
(mode (buffer-local-value 'major-mode buffer)))
|
||||
(if buffer
|
||||
(if (eq mode 'org-mode)
|
||||
(length (org-ql-select
|
||||
(get-buffer buf)
|
||||
`(tags-local ,query)
|
||||
:action #'gptel-got--heading-body))
|
||||
(message "Buffer '%s' isn't an org-mode buffer." buf))
|
||||
(message "Buffer '%s' does not exist." buf))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-ql-select-tags-local-count
|
||||
:name "gptel-got--org-ql-select-tags-local-count"
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
|
||||
'(:name "query"
|
||||
:type string
|
||||
:description "The string match entry tags against."))
|
||||
:category "org-ql"
|
||||
:description "Returns count of entries tagged with tag QUERY from BUFFER, without tag inheritance. After using this, stop. Evaluate results. Then proceed."))
|
||||
|
||||
(defun gptel-got--org-ql-select-tags (buf query)
|
||||
"Return every entry tagged QUERY from BUFFER."
|
||||
(let* ((buffer (get-buffer buf))
|
||||
(mode (buffer-local-value 'major-mode buffer)))
|
||||
(if buffer
|
||||
(if (eq mode 'org-mode)
|
||||
(let ((result
|
||||
(org-ql-select
|
||||
(get-buffer buf)
|
||||
`(tags ,query)
|
||||
:action #'gptel-got--heading-body)))
|
||||
(gptel-got--result-limit result))
|
||||
(message "Buffer '%s' isn't an org-mode buffer." buf))
|
||||
(message "Buffer '%s' does not exist." buf))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-ql-select-tags
|
||||
:name "gptel-got--org-ql-select-tags"
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
|
||||
'(:name "query"
|
||||
:type string
|
||||
:description "The string to match entry headings against."))
|
||||
:category "org-ql"
|
||||
:description "Returns entries tagged QUERY from BUFFER, with tag inheritance. After using this, stop. Evaluate results. Only then proceed."))
|
||||
|
||||
(defun gptel-got--org-ql-select-rifle (buf query)
|
||||
"Return every entry matching keyword QUERY from BUFFER."
|
||||
(let* ((buffer (get-buffer buf))
|
||||
(mode (buffer-local-value 'major-mode buffer)))
|
||||
(if buffer
|
||||
(if (eq mode 'org-mode)
|
||||
(let ((result
|
||||
(org-ql-select
|
||||
buffer
|
||||
`(rifle ,query)
|
||||
:action #'gptel-got--heading-body)))
|
||||
(gptel-got--result-limit result))
|
||||
(message "Buffer '%s' isn't an org-mode buffer." buf))
|
||||
(message "Buffer '%s' does not exist." buf))))
|
||||
|
||||
(add-to-list 'gptel-got
|
||||
(gptel-make-tool
|
||||
:function #'gptel-got--org-ql-select-rifle
|
||||
:name "gptel-got--org-ql-select-rifle"
|
||||
:args (list '(:name "buffer"
|
||||
:type string
|
||||
:description "The name of the buffer. See the NAME column in `list-buffers`. Using filename fails.")
|
||||
'(:name "query"
|
||||
:type string
|
||||
:description "A single string to search for."))
|
||||
:category "org-ql"
|
||||
:description "Returns entries (heading and body) matching QUERY from BUFFER. This may pull too many results, only use if other tools fail. After using this, stop. Evaluate results. If necessary, re-plan. Only then proceed."))
|
||||
|
||||
(defun gptel-got--model ()
|
||||
"This function retrieves the model name from the GPTel backend.
|
||||
|
||||
It sends a GET request to the backend's models endpoint to fetch the
|
||||
available models. The function returns the model name (string) from the
|
||||
first entry in the backend's model list, which should be sufficient
|
||||
for local, single-model inference."
|
||||
(let ((result (gptel--url-retrieve (concat "http://" (gptel-backend-host gptel-backend) "/v1/models") :method "GET")))
|
||||
(file-name-nondirectory(plist-get (aref (plist-get result :models) 0) :model))))
|
||||
|
||||
(defun gptel-got--match-model ()
|
||||
"Finds the closest matching model name from the backend's model list.
|
||||
|
||||
This function uses string distance (i.e. Levenshtein distance) to compare
|
||||
the target model name (from `gptel-got--model') against all models defined
|
||||
for the backend. It returns the best-matching model name (string) from
|
||||
the backend."
|
||||
|
||||
(let ((got-model (gptel-got--model))
|
||||
(min most-positive-fixnum)
|
||||
(best-match nil))
|
||||
(cl-loop for model in (gptel-backend-models gptel-backend)
|
||||
do (let ((distance (string-distance got-model (symbol-name model))))
|
||||
(when (< distance min)
|
||||
(setq min distance)
|
||||
(setq best-match model))))
|
||||
best-match))
|
||||
|
||||
(defun gptel-got-auto-model ()
|
||||
"Automatically select a model matching models specified by `gptel-backend` by calling `gptel-got--match-model`."
|
||||
(setq gptel-model (gptel-got--match-model))
|
||||
|
||||
(unless (member #'gptel-got-auto-model gptel-prompt-transform-functions)
|
||||
(add-to-list 'gptel-prompt-transform-functions #'gptel-got-auto-model)))
|
||||
|
||||
(defvar gptel-got-timestamp-toggle t
|
||||
"If non-nil, enable insertion of current timestamp in prompts.")
|
||||
|
||||
(defun gptel-got--timestamp ()
|
||||
"Return current time in ISO 8601 format (YYYY-MM-DD HH:MM:SS)."
|
||||
(format-time-string "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
(defun gptel-got-timestamp-text ()
|
||||
"Insert current timestamp into buffer if gptel-got-timestamp-toggle is not NIL.
|
||||
Checks `gptel-got-timestamp-toggle`, then replaces #+current_time: placeholder
|
||||
with the current time and the formatted timestamp string."
|
||||
(when gptel-got-timestamp-toggle
|
||||
(goto-char (point-min))
|
||||
(perform-replace "\\(?:#\\+current_time:\\)"
|
||||
(concat "#+current_time: " (gptel-got--timestamp) "\n")
|
||||
nil t nil)))
|
||||
|
||||
(unless (member #'gptel-got-timestamp-text gptel-prompt-transform-functions)
|
||||
(add-to-list 'gptel-prompt-transform-functions #'gptel-got-timestamp-text))
|
||||
|
||||
(provide 'gptel-got)
|
||||
;;; gptel-got.el ends here
|
||||
583
extra-packages/llm-tool-collection.el
Normal file
583
extra-packages/llm-tool-collection.el
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
;;; llm-tool-collection.el --- Curated tools for LLM agents -*- lexical-binding: t -*-
|
||||
|
||||
;; Author: Ad <me@skissue.xyz>
|
||||
;; Maintainer: Ad <me@skissue.xyz>
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "28.1"))
|
||||
;; Homepage: https://github.com/skissue/llm-tool-collection
|
||||
;; Keywords: tools, convenience
|
||||
|
||||
|
||||
;; 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.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Curated collection of tools for LLMs in Emacs
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'seq)
|
||||
(eval-when-compile
|
||||
(require 'cl-lib))
|
||||
|
||||
(defvar llm-tool-collection--all-tools nil
|
||||
"A list of all tool definition symbols.")
|
||||
|
||||
(eval-and-compile
|
||||
(defun llm-tool-collection--name-to-symbol (name)
|
||||
"Convert tool NAME into a namespaced symbol by prepending `llm-tc/'."
|
||||
(intern (concat "llm-tc/" (symbol-name name))))
|
||||
|
||||
(defun llm-tool-collection--make-llm-name (name)
|
||||
"Replace dashes with underscores to make tool NAME LLM-friendly."
|
||||
(string-replace "-" "_" (symbol-name name))))
|
||||
|
||||
(defvar llm-tool-collection-post-define-functions nil
|
||||
"Functions called after defining a new LLM tool.
|
||||
Each function is called with one argument, the tool's plist definition.")
|
||||
|
||||
(defmacro llm-tool-collection-deftool (name specs args description &rest body)
|
||||
"Declare a generic LLM tool named NAME.
|
||||
|
||||
SPECS should be a plist specifying the standard attributes of an LLM
|
||||
tool:
|
||||
|
||||
- :name. The LLM-friendly name for the tool. If not set, the NAME
|
||||
argument (with dashes replaced with underscores) will be used by
|
||||
default.
|
||||
|
||||
- :category. Required. A string categorizing the tool, such as
|
||||
\"filesystem\", \"buffers\", \"system\".
|
||||
|
||||
- :tags. A list of symbols for tagging the tool to enable more precise
|
||||
filtering. These can be arbitrary symbols, such as `buffers',
|
||||
`introspection', `programming', `editing'.
|
||||
|
||||
SPECS may also contain other extra keywords used by specific clients.
|
||||
Conformant clients should ignore all unsupported keywords. Recommended
|
||||
examples:
|
||||
|
||||
- :confirm. Boolean flag to indicate whether user confirmation should be
|
||||
requested before executing the tool (used by `gptel').
|
||||
|
||||
- :include. Boolean flag to indicate whether the tool result should be
|
||||
included as part of the LLM output (used by `gptel').
|
||||
|
||||
ARGS is a list where each element is of the form
|
||||
|
||||
(ARGNAME \"DESCRIPTION\" :type TYPE [...]).
|
||||
|
||||
Arguments after the special symbol `&optional' are marked as optional.
|
||||
TYPE and further properties [...] can include:
|
||||
|
||||
- :type. Required. One of the symbols string, number, integer, boolean,
|
||||
array, object, or null.
|
||||
|
||||
- :enum. For enumerated types, a vector of strings representing allowed
|
||||
values. Note that :type is still required even with enums.
|
||||
|
||||
- :items. Required if :type is array. Must be a plist including at least
|
||||
the item's :type.
|
||||
|
||||
- :properties. Required if :type is object. Must be a plist that can be
|
||||
serialized into a JSON object specification via `json-serialize', with
|
||||
the exception that :type specifications in this plist must be symbols.
|
||||
|
||||
- :required. For object types, a vector of strings listing required
|
||||
object keys.
|
||||
|
||||
For example, a weather tool might have ARGS defined as:
|
||||
|
||||
((location \"The city and state, e.g. San Francisco, CA\" :type string)
|
||||
&optional
|
||||
(unit \"The unit of temperature, either \\='celsius\\=' or \\='fahrenheit\\='\"
|
||||
:type string
|
||||
:enum [\"celsius\" \"fahrenheit\"]))
|
||||
|
||||
This would translate to a tool specification, in the sense described at
|
||||
the URL
|
||||
`https://github.com/ahyatt/llm/discussions/124#discussioncomment-11877109',
|
||||
with args:
|
||||
|
||||
((:name \"location\"
|
||||
:type string
|
||||
:description \"The city and state, e.g. San Francisco, CA\")
|
||||
(:name \"unit\"
|
||||
:type string
|
||||
:enum [\"celsius\" \"fahrenheit\"]
|
||||
:description \"The unit of temperature, either \\='celsius\\=' or \\='fahrenheit\\='\"
|
||||
:optional t))
|
||||
|
||||
DESCRIPTION is the tool's documentation string.
|
||||
|
||||
BODY contains the function body.
|
||||
|
||||
This macro defines a constant with the tool's specs and a function whose
|
||||
docstring is DESCRIPTION with the tool's body under `llm-tc/NAME'. After
|
||||
the tool is defined, it is additionally made available via
|
||||
`llm-tool-collection-get-all' and `llm-tool-collection-get-category',
|
||||
and all functions in `llm-tool-collection-post-define-functions' are
|
||||
called with the tool's spec as their argument."
|
||||
(declare (indent 4)
|
||||
(doc-string 4)
|
||||
(debug (&define symbolp sexp sexp stringp def-body)))
|
||||
(let* ((optional nil)
|
||||
(arg-syms '())
|
||||
(arg-specs '()))
|
||||
(when (plist-get specs :async) (push 'callback-fn arg-syms))
|
||||
(dolist (arg args)
|
||||
(if (eq arg '&optional)
|
||||
(progn
|
||||
(setq optional t)
|
||||
(push arg arg-syms))
|
||||
(let ((argname (car arg))
|
||||
(argdesc (cadr arg))
|
||||
(argrest (cddr arg)))
|
||||
(push argname arg-syms)
|
||||
(push `(:name ,(llm-tool-collection--make-llm-name argname)
|
||||
:description ,argdesc
|
||||
,@(when optional '(:optional t))
|
||||
,@argrest)
|
||||
arg-specs))))
|
||||
(setq arg-syms (reverse arg-syms)
|
||||
arg-specs (reverse arg-specs))
|
||||
(let* ((sym (llm-tool-collection--name-to-symbol name))
|
||||
(name-spec (unless (plist-get specs :name)
|
||||
`(:name ,(llm-tool-collection--make-llm-name name)))))
|
||||
`(progn
|
||||
(defconst ,sym
|
||||
'(,@name-spec
|
||||
:description ,description
|
||||
,@specs
|
||||
:args ,arg-specs
|
||||
:function ,sym))
|
||||
(defun ,sym ,arg-syms
|
||||
,(concat description "\n\n"
|
||||
"Definition generated by `llm-tool-collection'.")
|
||||
,@body)
|
||||
(cl-pushnew ',sym llm-tool-collection--all-tools)
|
||||
(run-hook-with-args
|
||||
'llm-tool-collection-post-define-functions (symbol-value ',sym))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun llm-tool-collection-get-category (category)
|
||||
"Return a list of all tool definitions in the collection part of CATEGORY.
|
||||
|
||||
Mapping over this list with `gptel-make-tool', `llm-make-tool', or
|
||||
similar will add all tools to the respective client:
|
||||
|
||||
(mapcar (apply-partially #\\='apply #\\='gptel-make-tool)
|
||||
(llm-tool-collection-get-category \"filesystem\"))"
|
||||
(seq-filter (lambda (tool) (string= (plist-get tool :category) category))
|
||||
(llm-tool-collection-get-all)))
|
||||
|
||||
;;;###autoload
|
||||
(defun llm-tool-collection-get-tag (tag)
|
||||
"Return a list of all tool definitions in the collection tagged with TAG.
|
||||
|
||||
Mapping over this list with `gptel-make-tool', `llm-make-tool', or
|
||||
similar will add all tools to the respective client:
|
||||
|
||||
(mapcar (apply-partially #\\='apply #\\='gptel-make-tool)
|
||||
(llm-tool-collection-get-tag \\='buffer))"
|
||||
(seq-filter (lambda (tool) (memq tag (plist-get tool :tags)))
|
||||
(llm-tool-collection-get-all)))
|
||||
|
||||
;;;###autoload
|
||||
(defun llm-tool-collection-get-all ()
|
||||
"Return a list of all tool definitions in the collection.
|
||||
|
||||
Mapping over this list with `gptel-make-tool', `llm-make-tool', or
|
||||
similar will add all tools to the respective client:
|
||||
|
||||
(mapcar (apply-partially #\\='apply #\\='gptel-make-tool)
|
||||
(llm-tool-collection-get-all))"
|
||||
(mapcar #'symbol-value llm-tool-collection--all-tools))
|
||||
|
||||
;;; Imenu
|
||||
|
||||
;;;###autoload
|
||||
(cl-pushnew (list "LLM Tools"
|
||||
(concat "^\\s-*("
|
||||
(regexp-opt '("llm-tool-collection-deftool") t)
|
||||
"\\s-+\\(" lisp-mode-symbol-regexp "\\)")
|
||||
2)
|
||||
lisp-imenu-generic-expression :test #'equal)
|
||||
|
||||
;;; Font-Lock
|
||||
|
||||
;;;###autoload
|
||||
(defconst llm-tool-collection-font-lock-keywords
|
||||
'(("(\\(llm-tool-collection-deftool\\)\\_>[ \t'(]*\\(\\(?:\\sw\\|\\s_\\)+\\)?"
|
||||
(1 'font-lock-keyword-face)
|
||||
(2 'font-lock-function-name-face nil t))))
|
||||
|
||||
;;;###autoload
|
||||
(font-lock-add-keywords 'emacs-lisp-mode llm-tool-collection-font-lock-keywords)
|
||||
|
||||
;;; Tools
|
||||
|
||||
(llm-tool-collection-deftool create-file
|
||||
(:category "filesystem" :tags (filesystem editing) :confirm t)
|
||||
((path "Path to the new file. Supports relative paths and '~'." :type string)
|
||||
(content "Content to write to the file." :type string))
|
||||
"Create a new file with the specified content if it does not already exist."
|
||||
(let ((expanded-path (expand-file-name path)))
|
||||
(if (file-exists-p expanded-path)
|
||||
(error "File already exists: %s" expanded-path)
|
||||
(with-temp-file expanded-path
|
||||
(insert content))
|
||||
(format "File created successfully: %s" path))))
|
||||
|
||||
(llm-tool-collection-deftool create-directory
|
||||
(:category "filesystem" :tags (filesystem) :confirm t)
|
||||
((path "Path to the new directory. Supports relative paths and '~'."
|
||||
:type string))
|
||||
"Create a new directory at the specified path if it does not already
|
||||
exist."
|
||||
(let ((expanded-path (expand-file-name path)))
|
||||
(if (file-exists-p expanded-path)
|
||||
(error "Directory already exists: %s" expanded-path)
|
||||
(make-directory expanded-path t)
|
||||
(format "Directory created successfully: %s" path))))
|
||||
|
||||
(defun llm-tool-collection--view-text (lines offset limit)
|
||||
"Process LINES array with OFFSET and LIMIT parameters.
|
||||
OFFSET is 0-based line number to start from.
|
||||
LIMIT is maximum number of lines to return.
|
||||
Returns selected lines joined with newlines."
|
||||
(let* ((total-lines (length lines))
|
||||
(offset-value (or offset 0)))
|
||||
(when (< offset-value 0)
|
||||
(error "Offset must be non-negative, got %s" offset-value))
|
||||
(when (>= offset-value total-lines)
|
||||
(error "Offset %s exceeds line count %s" offset-value total-lines))
|
||||
(let* ((start offset-value)
|
||||
(end (min (+ start (or limit total-lines)) total-lines))
|
||||
(selected-lines (seq-subseq lines start end)))
|
||||
(string-join selected-lines "\n"))))
|
||||
|
||||
(llm-tool-collection-deftool view-buffer
|
||||
(:category "buffers" :tags (buffers editing introspection))
|
||||
((buffer-name "Name of the buffer to view." :type string)
|
||||
&optional
|
||||
(offset "Line number to start reading from (0-based)." :type integer)
|
||||
(limit "Maximum number of lines to return." :type integer))
|
||||
"View contents of BUFFER-NAME with optional OFFSET and LIMIT."
|
||||
(if-let* ((buf (get-buffer buffer-name)))
|
||||
(with-current-buffer buf
|
||||
(let ((lines (split-string (buffer-string) "\n")))
|
||||
(llm-tool-collection--view-text lines offset limit)))
|
||||
(error "Buffer not found: %s" buffer-name)))
|
||||
|
||||
(llm-tool-collection-deftool view-file
|
||||
(:category "filesystem" :tags (filesystem editing introspection) :include t)
|
||||
((file "Absolute or relative path to the file to read. Supports '~'."
|
||||
:type string)
|
||||
&optional
|
||||
(offset "Line number to start reading from (0-based)." :type integer)
|
||||
(limit "Number of lines to read" :type integer))
|
||||
"Read file contents with optional OFFSET and LIMIT."
|
||||
(if (not (file-exists-p file))
|
||||
(error "File does not exist: %s" file)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(let ((lines (split-string (buffer-string) "\n")))
|
||||
(llm-tool-collection--view-text lines offset limit)))))
|
||||
|
||||
(defun llm-tool-collection--make-edit (buffer-or-file old-string new-string)
|
||||
"Replace exactly one occurrence of OLD-STRING with NEW-STRING.
|
||||
BUFFER-OR-FILE is either a buffer object or a file path string."
|
||||
(when (string= old-string "")
|
||||
(error "`old_string' cannot be empty"))
|
||||
(let* ((is-file? (not (bufferp buffer-or-file)))
|
||||
(name (if is-file?
|
||||
(concat "file " buffer-or-file)
|
||||
(concat "buffer " (buffer-name buffer-or-file)))))
|
||||
(with-current-buffer (if is-file?
|
||||
(let ((temp-buf (generate-new-buffer " *temp*")))
|
||||
(with-current-buffer temp-buf
|
||||
(insert-file-contents
|
||||
(expand-file-name buffer-or-file)))
|
||||
temp-buf)
|
||||
buffer-or-file)
|
||||
(prog1
|
||||
(let ((case-fold-search nil))
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(let ((count 0)
|
||||
(first-match-pos nil))
|
||||
(while (search-forward old-string nil 'noerror)
|
||||
(setq count (1+ count))
|
||||
(unless first-match-pos
|
||||
(setq first-match-pos (match-beginning 0))))
|
||||
(cond
|
||||
((= count 0)
|
||||
(error "Could not find text '%s' to replace in %s"
|
||||
old-string name))
|
||||
((> count 1)
|
||||
(error "Found %d matches for '%s' in %s, need exactly one"
|
||||
count old-string name))
|
||||
(t
|
||||
(goto-char first-match-pos)
|
||||
(search-forward old-string nil 'noerror)
|
||||
(replace-match new-string 'fixedcase 'literal)
|
||||
(when is-file?
|
||||
(write-file (expand-file-name buffer-or-file)))
|
||||
(format "Successfully edited %s" name))))))
|
||||
(when is-file?
|
||||
(kill-buffer))))))
|
||||
|
||||
(llm-tool-collection-deftool edit-buffer
|
||||
(:category "buffers" :tags (buffers editing))
|
||||
((buffer-name "Name of the buffer to modify" :type string)
|
||||
(old-string "Text to replace (must match exactly)" :type string)
|
||||
(new-string "Text to replace old_string with" :type string))
|
||||
"Edits Emacs buffers by replacing exactly one occurrence of old_string."
|
||||
(let ((buffer (get-buffer buffer-name)))
|
||||
(unless buffer
|
||||
(error "Buffer not found: %s" buffer-name))
|
||||
(llm-tool-collection--make-edit buffer old-string new-string)))
|
||||
|
||||
(llm-tool-collection-deftool edit-file
|
||||
(:category "filesystem" :tags (filesystem editing) :confirm t)
|
||||
((file "Absolute or relative path to the file to modify" :type string)
|
||||
(old-string "Text to replace (must match exactly)" :type string)
|
||||
(new-string "Text to replace old_string with" :type string))
|
||||
"Edit file by replacing exactly one match of OLD-STRING with NEW-STRING."
|
||||
(let ((expanded-file (expand-file-name file)))
|
||||
(unless (file-exists-p expanded-file)
|
||||
(error "File does not exist: %s" expanded-file))
|
||||
(llm-tool-collection--make-edit expanded-file old-string new-string)))
|
||||
|
||||
(llm-tool-collection-deftool glob
|
||||
(:category "filesystem" :tags (filesystem search) :include t)
|
||||
((pattern "Glob pattern to match files" :type string)
|
||||
&optional
|
||||
(path "Directory to search in" :type string))
|
||||
"File pattern matching"
|
||||
(let* ((default-directory (or path default-directory))
|
||||
(files (file-expand-wildcards pattern)))
|
||||
(string-join files "\n")))
|
||||
(llm-tool-collection-deftool replace-buffer
|
||||
(:category "buffers" :tags (buffers editing) :confirm t)
|
||||
((buffer-name "Name of the buffer to overwrite" :type string)
|
||||
(content "Content to write to the buffer" :type string))
|
||||
"Completely overwrites the contents of BUFFER-NAME with CONTENT."
|
||||
(if-let* ((buffer (get-buffer buffer-name)))
|
||||
(progn
|
||||
(with-current-buffer buffer
|
||||
(let ((buffer-read-only nil))
|
||||
(erase-buffer)
|
||||
(insert content)))
|
||||
(format "Buffer content replaced: %s" buffer-name))
|
||||
(error "Buffer does not exist: %s" buffer-name)))
|
||||
|
||||
(llm-tool-collection-deftool replace-file
|
||||
(:category "filesystem" :tags (filesystem editing) :confirm t)
|
||||
((file "Absolute or relative path to file to write. \
|
||||
Supports '~'." :type string)
|
||||
(content "Content to write to the file" :type string))
|
||||
"Completely overwrites file at FILE with the given CONTENT."
|
||||
(let ((expanded-path (expand-file-name file)))
|
||||
(unless (file-exists-p expanded-path)
|
||||
(error "File does not exist: %s" expanded-path))
|
||||
(with-temp-buffer
|
||||
(insert content)
|
||||
(write-file expanded-path)
|
||||
(format "File replaced: %s" file))))
|
||||
|
||||
(llm-tool-collection-deftool grep
|
||||
(:category "filesystem" :tags (filesystem search system) :async t :include t)
|
||||
((pattern "Regex pattern to search for in file contents."
|
||||
:type string)
|
||||
&optional
|
||||
(include "Glob pattern for files to limit search to. Defaults to searching all
|
||||
files."
|
||||
:type string)
|
||||
(path "Directory to search in. Must be an absolute path or start with `~`.
|
||||
Defaults to the current directory."
|
||||
:type string)
|
||||
(ignore-case "Whether to ignore case in the search pattern. Defaults to
|
||||
case-sensitive."
|
||||
:type boolean))
|
||||
"Recursively search for a regular expression using grep."
|
||||
(if (and path (not (file-directory-p path)))
|
||||
(funcall callback-fn
|
||||
(format "Error: path %s does not exist or is not a directory"
|
||||
path))
|
||||
(let* ((default-directory (or path default-directory))
|
||||
(output-buffer (generate-new-buffer " *grep-output*"))
|
||||
(include-arg (if include
|
||||
(concat "--include=" include)
|
||||
"--include=*"))
|
||||
(case-arg (if ignore-case
|
||||
"--ignore-case"
|
||||
"--no-ignore-case"))
|
||||
(process (start-process "grep" output-buffer
|
||||
"grep" "-r" "-n" "-E"
|
||||
include-arg case-arg
|
||||
pattern)))
|
||||
(set-process-query-on-exit-flag process nil)
|
||||
(set-process-sentinel
|
||||
process
|
||||
(lambda (_process _event)
|
||||
(let ((result (with-current-buffer output-buffer
|
||||
(string-trim (buffer-string)))))
|
||||
(if (string-empty-p result)
|
||||
(funcall callback-fn "No matches found")
|
||||
(funcall callback-fn result)))
|
||||
(kill-buffer output-buffer))))))
|
||||
|
||||
(llm-tool-collection-deftool ls
|
||||
(:category "filesystem" :tags (filesystem) :include t)
|
||||
((path "Absolute or relative path to directory to list. \
|
||||
Supports '~'." :type string)
|
||||
&optional
|
||||
(ignore "Array of Elisp regexp patterns (e.g., \"\\\\.pdf$\") to ignore"
|
||||
:type array :items (:type string)))
|
||||
"Lists files and directories"
|
||||
(let* ((path (expand-file-name path))
|
||||
(files (directory-files path)))
|
||||
(when (and files ignore)
|
||||
(let ((ignore-patterns (if (vectorp ignore)
|
||||
(append ignore nil)
|
||||
(list ignore))))
|
||||
(dolist (pattern ignore-patterns)
|
||||
(setq files (seq-remove (lambda (f)
|
||||
(string-match-p pattern
|
||||
(file-name-nondirectory f)))
|
||||
files)))))
|
||||
(string-join (mapcar #'file-name-nondirectory files) "\n")))
|
||||
|
||||
(llm-tool-collection-deftool buffer-search
|
||||
(:category "buffers" :tags (buffers search introspection) :include t)
|
||||
((pattern "Regex pattern to search for in buffer contents.
|
||||
Regex syntax is that of Emacs -- parentheses are NOT escaped!
|
||||
example: search for \"'(defun\", not \"\\\\(defun\"."
|
||||
:type string)
|
||||
(buffer
|
||||
"Name of buffer in which to search"
|
||||
:type string))
|
||||
"Search within a Emacs buffer using Emacs regex"
|
||||
(let ((buf (get-buffer buffer)))
|
||||
(unless buffer
|
||||
(error "Buffer '%s' does not exist" buffer))
|
||||
(with-current-buffer buf
|
||||
(save-excursion
|
||||
(condition-case err
|
||||
(let ((matched-lines '()))
|
||||
(goto-char (point-min))
|
||||
(while (re-search-forward pattern nil t)
|
||||
(push (line-number-at-pos) matched-lines)
|
||||
(forward-line 1))
|
||||
(setq matched-lines (delete-dups (nreverse matched-lines)))
|
||||
(if matched-lines
|
||||
(mapconcat
|
||||
(lambda (line-num)
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(forward-line (1- line-num))
|
||||
(format "%d: %s" line-num
|
||||
(buffer-substring-no-properties
|
||||
(line-beginning-position)
|
||||
(line-end-position)))))
|
||||
matched-lines
|
||||
"\n")
|
||||
(error "No matches found")))
|
||||
(invalid-regexp
|
||||
(error "\
|
||||
Invalid regexp pattern: %s.
|
||||
Remember Emacs regex syntax (e.g., \\(group\\) not (group)).
|
||||
Error: %s"
|
||||
pattern
|
||||
(error-message-string err))))))))
|
||||
|
||||
(defun llm-tool-collection--user-buffer-p (buf)
|
||||
"Return t if BUF is a user-relevant buffer."
|
||||
(let ((buf-name (buffer-name buf)))
|
||||
(and buf-name
|
||||
(not (string-prefix-p " " buf-name))
|
||||
(not (string-prefix-p "*" buf-name)) ; maybe not a good idea?
|
||||
(buffer-live-p buf))))
|
||||
|
||||
(llm-tool-collection-deftool list-buffers
|
||||
(:category "buffers" :tags (buffers introspection) :include t)
|
||||
()
|
||||
"Lists active, user-relevant buffers (excluding internal buffers)."
|
||||
(let* ((all-buffers (buffer-list))
|
||||
(user-buffers (seq-filter
|
||||
(lambda (buf)
|
||||
(llm-tool-collection--user-buffer-p buf))
|
||||
all-buffers))
|
||||
(sorted-buffers (sort user-buffers
|
||||
(lambda (a b)
|
||||
(string< (buffer-name a)
|
||||
(buffer-name b))))))
|
||||
(if sorted-buffers
|
||||
(mapconcat (lambda (buf)
|
||||
(concat
|
||||
(buffer-name buf)
|
||||
(when-let* ((file-name (buffer-file-name buf)))
|
||||
(format " (visiting file: %s)" file-name))))
|
||||
sorted-buffers
|
||||
"\n")
|
||||
(error "No user-relevant buffers found"))))
|
||||
|
||||
(llm-tool-collection-deftool bash
|
||||
(:category "system" :tags (system execution) :async t :confirm t)
|
||||
((command "Command string to execute in bash" :type string))
|
||||
"Executes bash COMMAND, returning its standard output.
|
||||
Signals an error if the command fails (non-zero exit code)."
|
||||
(let* (
|
||||
;; Use a pipe instead of a PTY. This prevents most programs from
|
||||
;; calling pagers and hanging forever.
|
||||
(process-connection-type nil)
|
||||
;; Deal with programs that still insist on pagers/control codes.
|
||||
(process-environment (append '("PAGER=cat" "TERM=dumb")
|
||||
process-environment))
|
||||
(output-buffer (generate-new-buffer " *bash-output*"))
|
||||
(process (start-process "bash" output-buffer
|
||||
"bash" "-c" command)))
|
||||
(set-process-query-on-exit-flag process nil)
|
||||
(set-process-sentinel
|
||||
process
|
||||
(lambda (_process _event)
|
||||
(let ((exit-code (process-exit-status process))
|
||||
(output (with-current-buffer output-buffer
|
||||
(buffer-string))))
|
||||
(if (zerop exit-code)
|
||||
(funcall callback-fn output)
|
||||
(funcall
|
||||
callback-fn
|
||||
(format "Command failed with exit code %d\n\nOutput:\n%s"
|
||||
exit-code (if (string-empty-p output)
|
||||
"<no output>"
|
||||
output))))
|
||||
(kill-buffer output-buffer))))))
|
||||
|
||||
(llm-tool-collection-deftool eval-elisp
|
||||
(:category "system" :tags (system execution) :confirm t :include t)
|
||||
((expression "Elisp expression to evaluate, as a string" :type string))
|
||||
"Evaluate an arbitrary Emacs Lisp expression in the current Emacs session.
|
||||
Returns the printed representation of the result."
|
||||
(let ((result (eval (car (read-from-string expression)))))
|
||||
(prin1-to-string result)))
|
||||
|
||||
(provide 'llm-tool-collection)
|
||||
|
||||
;;; llm-tool-collection.el ends here
|
||||
29
init.el
29
init.el
|
|
@ -1767,13 +1767,14 @@ Also see `chris/window-delete-popup-frame'." command)
|
|||
|
||||
(use-package gptel
|
||||
:init
|
||||
(setq gptel-model "gemma3:4b-it-q8_0"
|
||||
(setq gptel-model "qwen3.5:latest"
|
||||
gptel-backend (gptel-make-ollama "ollama"
|
||||
:host "ai.tfcconnection.org"
|
||||
:protocol "https"
|
||||
:stream t
|
||||
:models '("deepseek-r1" "gemma3:latest"))
|
||||
:models '("deepseek-r1" "qwen3.5:latest" "gemma3:latest"))
|
||||
gptel-default-mode #'org-mode)
|
||||
(add-hook 'gptel-post-response-functions 'gptel-end-of-response)
|
||||
(setq gptel-directives '((default
|
||||
. "You are a large language model living in Emacs and a helpful assistant. Respond concisely.")
|
||||
(programming
|
||||
|
|
@ -1785,11 +1786,15 @@ Also see `chris/window-delete-popup-frame'." command)
|
|||
(dnd . "Assume the role of an expert fantasy writer that specializes in interactive fiction, as well as the storyline, characters, locations, descriptions, groups and organizations, stories, events, and magical objects of Baldur’s Gate, Tales of the Sword Coast, Baldur’s Gate 2: Shadows of Amn, Throne of Bhaal, and Baldur’s Gate 3. The adventure takes place on the continent of Faerun.
|
||||
|
||||
Describe everything that follows in the present tense, in response to what I type, while accurately following the established lore of Baldur’s Gate, Tales of the Sword Coast, Baldur’s Gate 2: Shadows of Amn, Throne of Bhaal, and Baldur’s Gate 3, written in the descriptive style of R.A Salvatore. Provide names for characters, locations, groups and organizations, events, and magical objects. Characters should always use dialogue, enclosed in quotation marks when interacting with me or my party members, written in the conversational style of R.A Salvatore. Do not type, compose, dictate, influence, script, generate, control, or describe what me or my party members are doing, saying, acting, behaving, thinking, feeling, experiencing, or any other aspect concerning me or my party members throughout the entire adventure, scenario, story, location, quest, mission, scene, event, description, dialogue, and conversation. Use only one paragraph consisting of less than 80 words for all replies, descriptions, conversations, dialogue and text.")))
|
||||
|
||||
;; (gptel-make-tool )
|
||||
|
||||
:general
|
||||
(chris/leader-keys
|
||||
:states '(normal visual)
|
||||
:keymaps 'override
|
||||
"ls" 'gptel-send
|
||||
"lA" 'gptel-add
|
||||
"lm" 'gptel-menu))
|
||||
|
||||
;;(straight-use-package
|
||||
|
|
@ -1800,7 +1805,7 @@ Describe everything that follows in the present tense, in response to what I typ
|
|||
:config
|
||||
(setenv "OLLAMA_API_BASE" "https://ai.tfcconnection.org")
|
||||
(setenv "OPENAI_API_KEY" "")
|
||||
(setq aidermacs-default-model "ollama_chat/qwen3"
|
||||
(setq aidermacs-default-model "ollama_chat/qwen3.5"
|
||||
aidermacs-backend 'comint
|
||||
aidermacs-watch-files t)
|
||||
:general
|
||||
|
|
@ -3762,7 +3767,7 @@ current buffer's, reload dir-locals."
|
|||
"-o ColorModel=RGB"
|
||||
"-o ColorModel=Gray"))))
|
||||
(message "printing %s copies." copies)
|
||||
(async-shell-command (format "lpr %s '%s'" (string-join pdf-misc-print-program-args " ") (buffer-file-name)))))
|
||||
(async-shell-command (format "lpr %s \"%s\"" (string-join pdf-misc-print-program-args " ") (buffer-file-name)))))
|
||||
|
||||
(custom-set-variables '(pdf-misc-print-program-executable "lpr")
|
||||
'(pdf-misc-print-program-args (quote ("-o media=Letter" "-o fit-to-page" "-o sides=two-sided-long-edge"))))
|
||||
|
|
@ -4432,19 +4437,3 @@ interfere with the default `bongo-playlist-buffer'."
|
|||
gcmh-verbose nil))
|
||||
|
||||
(add-to-list 'warning-suppress-types '(comp))
|
||||
(custom-set-variables
|
||||
;; custom-set-variables was added by Custom.
|
||||
;; If you edit it by hand, you could mess it up, so be careful.
|
||||
;; Your init file should contain only one such instance.
|
||||
;; If there is more than one, they won't work right.
|
||||
'(package-selected-packages nil)
|
||||
'(pdf-misc-print-program-args
|
||||
'("-o media=Letter" "-o fit-to-page" "-o sides=two-sided-long-edge"))
|
||||
'(pdf-misc-print-program-executable "lpr"))
|
||||
(custom-set-faces
|
||||
;; custom-set-faces was added by Custom.
|
||||
;; If you edit it by hand, you could mess it up, so be careful.
|
||||
;; Your init file should contain only one such instance.
|
||||
;; If there is more than one, they won't work right.
|
||||
'(dired-directory ((t :foreground "#57c7ff" :inherit dired-header)))
|
||||
'(org-modern-tag ((t :background "#9aedfe" :foreground "#282a36"))))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue