Clean-up, add timestamps

This commit is contained in:
Phil Bajsicki 2025-06-27 15:58:57 +02:00
commit 3677b889cd
3 changed files with 157 additions and 79 deletions

View file

@ -40,7 +40,6 @@ Therefore (philosophy, I may change my mind later):
- Tool names are documentation. - Tool names are documentation.
- Argument names are documentation. - Argument names are documentation.
- As few arguments per tool as possible. - As few arguments per tool as possible.
** Installation: ** Installation:
I only use Doom Emacs, so here's how I load: I only use Doom Emacs, so here's how I load:
@ -131,22 +130,18 @@ Use ~setq~ in your configuration, with a list of buffer names whose headings you
#+end_src #+end_src
**** result-limit **** result-limit
It is with great disappointment that I admit to a terrible flaw of this package.
When in local use, with a large dataset, a lot of the commands which return entire org-mode entries... end up grabbing way, way too much text. When in local use, with a large dataset, a lot of the commands which return entire org-mode entries... end up grabbing way, way too much text.
This can end up with Emacs freezing as it tries to parse immeasurable amounts of text (for fonts, formatting, etc) at the same time as the LLM is screeching in electric pain trying to actually process all that data. The stop-gap solution is to implement a hard limit on the amount of text a tool can return.
To that end, the stop-gap solution is to implement a hard limit on the amount of text a tool can return. I don't think there's much reason to ever allow partial results, which led me to this:
I don't think there's much reason to ever allow partial results, which leads me to this:
#+begin_src elisp :results none #+begin_src elisp :results none
(defvar gptel-got-result-limit 40000) (defvar gptel-got-result-limit 40000)
(defun gptel-got--result-limit (result) (defun gptel-got--result-limit (result)
(if (>= (length (format "%s" result)) gptel-got-result-limit) (if (>= (length (format "%s" result)) gptel-got-result-limit)
(format "Results over %s character. Stop. Analyze. Find a different solution, or use a more specific query." 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)) result))
@ -303,7 +298,7 @@ Opens a file into an inactive (background) buffer for processing.
#+begin_src elisp #+begin_src elisp
(defun gptel-got--open-file-inactive (file) (defun gptel-got--open-file-inactive (file)
"Open FILE in a background buffer without modifying its contents." "Open FILE in a background buffer without modifying its contents."
(find-file-noselect file) (find-file-noselect file)
(set-buffer-modified-p nil)) (set-buffer-modified-p nil))
(add-to-list 'gptel-got (add-to-list 'gptel-got
@ -475,9 +470,9 @@ QUERY can be any valid org-ql-select query."
My journal is a single file, with a hierarchy like so: My journal is a single file, with a hierarchy like so:
#+begin_src org :tangle no #+begin_src org :tangle no
,* [YYYY] ,* [YYYY])
,** [YYYY-MM] ,** [YYYY-MM]
,*** [YYYY-MM-DD Day HH:MM]) ,*** [YYYY-MM-DD Day HH:MM]
#+end_src #+end_src
This tool /kinda sorta/ works. It has two main failure modes: This tool /kinda sorta/ works. It has two main failure modes:
@ -570,7 +565,7 @@ Retrieve the headings where the heading matches query. This is very much the sam
(org-ql-select (org-ql-select
(get-buffer buf) (get-buffer buf)
`(heading ,query) `(heading ,query)
:action #''gptel-got--heading))) :action #'gptel-got--heading)))
(concat (gptel-got--result-limit result) "Results end here. Proceed with the next action.")) (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' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf)))) (message "Buffer '%s' does not exist." buf))))
@ -887,55 +882,114 @@ This means that /every org-mode file I have/ is part of this search. If you're u
:description "Returns entries matching regexp QUERY from all files. After using this, stop. Evaluate results for relevance, and proceed with completing user's request. The regexp *must* be in Emacs rx format!" )) :description "Returns entries matching regexp QUERY from all files. After using this, stop. Evaluate results for relevance, and proceed with completing user's request. The regexp *must* be in Emacs rx format!" ))
#+end_src #+end_src
*** End ****** Utils
#+begin_src elisp
(provide 'gptel-got)
;;; gptel-got.el ends here
#+end_src
* gptel-got-qol (quality of life)
:PROPERTIES:
:header-args: :elisp :tangle gptel-got-qol.el
:END:
** Notes
These are functions/ hooks/ stuff that I'm building in an effort to improve my experience. These are functions/ hooks/ stuff that I'm building in an effort to improve my experience.
** gptel-got-match-model ******* gptel-got-match-model
Gets the model name from the current gptel-backend's API (should be generally openai-compatible). Gets the model name from the current gptel-backend's API (should be generally openai-compatible).
#+begin_src elisp #+begin_src elisp :results none
(defun gptel-got--model () (defun gptel-got--model ()
"This function retrieves the model name from the GPTel backend. "This function retrieves the model name from the GPTel backend.
It sends a GET request to the backend's models endpoint to fetch the 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 available models. The function returns the model name (string) from the
first entry in the backend's model list, which should be sufficient first entry in the backend's model list, which should be sufficient
when ." for local, single-model inference."
(let ((result (gptel--url-retrieve (concat "http://" (gptel-backend-host gptel-backend) "/v1/models") :method "GET"))) (let ((result (gptel--url-retrieve (concat "http://" (gptel-backend-host gptel-backend) "/v1/models") :method "GET")))
(plist-get (aref (plist-get result :models) 0) :model))) (file-name-nondirectory(plist-get (aref (plist-get result :models) 0) :model))))
#+end_src
Determines which model in your gptel-backend matches the name of the model returned from the LLM API best, using Levenshtein. (defun gptel-got--match-model ()
#+begin_src elisp
(defun gptel-got-match-model ()
"Finds the closest matching model name from the backend's model list. "Finds the closest matching model name from the backend's model list.
This function uses string distance (e.g., Levenshtein distance) to compare This function uses string distance (i.e. Levenshtein distance) to compare
the target model name (from `gptel-got--model') against all available the target model name (from `gptel-got--model') against all models defined
models. It returns the best-matching model name (string) from the backend. for the backend. It returns the best-matching model name (string) from
the backend."
Intended for scenarios where the exact model name is unknown or contains
typos. The function ensures the most similar model name is selected for
operation by minimizing the string distance between names."
(let ((got-model (gptel-got--model)) (let ((got-model (gptel-got--model))
(min most-positive-fixnum) (min most-positive-fixnum)
(best-match nil)) (best-match nil))
(cl-loop for model in (gptel-backend-models gptel-backend) (cl-loop for model in (gptel-backend-models gptel-backend)
do (let ((distance (string-distance got-model (symbol-name model))) do (let ((distance (string-distance got-model (symbol-name model))))
(when (< distance min) (when (< distance min)
(setq min distance) (setq min distance)
(setq best-match model)))) (setq best-match model))))
best-match)) best-match))
#+end_src #+end_src
*** Usage The above works fine, as far as I can tell. Not perfect, but fine.
Currently none, have to hook this into =gptel-prompt-transform-functions=, and then have a model switching mechanism, possibly through presets, or direct. Both would be useful.
******** Example
#+begin_src elisp :tangle no
(gptel-got--model)
#+end_src
#+RESULTS:
: Mistral-Small-3.2-24B-Instruct-2506-UD-Q4_K_XL.gguf
#+begin_src elisp :tangle no
(gptel-got--match-model)
#+end_src
#+RESULTS:
: Mistral
But this... just doesn't, and it's baffling.
#+begin_src elisp :results none
(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)))
#+end_src
******* current_time
I wanted to have a way for my interactions to be timestamped, so I could ask things like "what have I focused on over the last 8 hours."
This function (and variable) lets me do that.
To use, simply add this line to your buffer. The buffer won't change, but the current_time line will have a timestamp appended to it in the text sent to the inference engine.
#+begin_src org :tangle no
#+current_time:
#+end_src
#+begin_src emacs-lisp :results none
(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))
#+end_src
#+begin_src elisp :tangle no
gptel-prompt-transform-functions
#+end_src
#+RESULTS:
| gptel-got-auto-model | gptel-got-timestamp-text | gptel--transform-add-context | gptel--transform-apply-preset |
*** End
#+begin_src elisp
(provide 'gptel-got)
;;; gptel-got.el ends here
#+end_src

View file

@ -1,31 +0,0 @@
(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
when ."
(let ((result (gptel--url-retrieve (concat "http://" (gptel-backend-host gptel-backend) "/v1/models") :method "GET")))
(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 (e.g., Levenshtein distance) to compare
the target model name (from `gptel-got--model') against all available
models. It returns the best-matching model name (string) from the backend.
Intended for scenarios where the exact model name is unknown or contains
typos. The function ensures the most similar model name is selected for
operation by minimizing the string distance between names."
(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))

View file

@ -42,7 +42,7 @@
(defun gptel-got--result-limit (result) (defun gptel-got--result-limit (result)
(if (>= (length (format "%s" result)) gptel-got-result-limit) (if (>= (length (format "%s" result)) gptel-got-result-limit)
(format "Results over %s character. Stop. Analyze. Find a different solution, or use a more specific query." 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)) result))
(defun gptel-got--heading () (defun gptel-got--heading ()
@ -107,7 +107,7 @@
(defun gptel-got--open-file-inactive (file) (defun gptel-got--open-file-inactive (file)
"Open FILE in a background buffer without modifying its contents." "Open FILE in a background buffer without modifying its contents."
(find-file-noselect file) (find-file-noselect file)
(set-buffer-modified-p nil)) (set-buffer-modified-p nil))
(add-to-list 'gptel-got (add-to-list 'gptel-got
@ -265,7 +265,7 @@ After using this, stop. Evaluate the relevance of the results. Then continue."))
(org-ql-select (org-ql-select
(get-buffer buf) (get-buffer buf)
`(heading ,query) `(heading ,query)
:action #''gptel-got--heading))) :action #'gptel-got--heading)))
(concat (gptel-got--result-limit result) "Results end here. Proceed with the next action.")) (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' isn't an org-mode buffer." buf))
(message "Buffer '%s' does not exist." buf)))) (message "Buffer '%s' does not exist." buf))))
@ -419,5 +419,60 @@ After using this, stop. Evaluate the relevance of the results. Then continue."))
:category "org-ql" :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.")) :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) (provide 'gptel-got)
;;; gptel-got.el ends here ;;; gptel-got.el ends here