summaryrefslogtreecommitdiff
path: root/modules/ai-config.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-10-23 00:15:14 -0500
committerCraig Jennings <c@cjennings.net>2025-10-23 00:15:14 -0500
commitd22d004ed9bdc11dbc21be368f90384ed8b1b795 (patch)
treea8ac1f44845bdd11c442cca3c69a51cfde9c1f8b /modules/ai-config.el
parentdb5f40e21b3015c2ce09c9b94e1d14c4ae951b92 (diff)
refactor: unify and simplify key binding setups
Optimized key binding configurations across modules for consistency and reduced redundancy. Improved conditional requiring to handle errors gracefully in `music-config.el`, ensuring robustness across different machine environments. Eliminated comments clutter and adjusted function definitions to adhere to revised standards.
Diffstat (limited to 'modules/ai-config.el')
-rw-r--r--modules/ai-config.el372
1 files changed, 186 insertions, 186 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el
index 32a5d53f..004750b6 100644
--- a/modules/ai-config.el
+++ b/modules/ai-config.el
@@ -32,12 +32,14 @@
(autoload 'cj/gptel-load-conversation "ai-conversations" "Load a saved AI conversation." t)
(autoload 'cj/gptel-delete-conversation "ai-conversations" "Delete a saved AI conversation." t)
+
+
(with-eval-after-load 'gptel
(require 'ai-conversations))
;;; ------------------------- AI Config Helper Functions ------------------------
-;; Define all our variables upfront
+;; Define variables upfront
(defvar cj/anthropic-api-key-cached nil "Cached Anthropic API key.")
(defvar cj/openai-api-key-cached nil "Cached OpenAI API key.")
(defvar gptel-claude-backend nil "Claude backend, lazy-initialized.")
@@ -49,70 +51,69 @@
HOST and USER must be strings that identify the credential to return."
(let* ((found (auth-source-search :host host :user user :require '(:secret) :max 1))
- (secret (plist-get (car found) :secret)))
- (cond
- ((functionp secret) (funcall secret))
- ((stringp secret) secret)
- (t (error "No usable secret found for host %s and user %s" host user)))))
+ (secret (plist-get (car found) :secret)))
+ (cond
+ ((functionp secret) (funcall secret))
+ ((stringp secret) secret)
+ (t (error "No usable secret found for host %s and user %s" host user)))))
(defun cj/anthropic-api-key ()
"Return the Anthropic API key, caching the result after first retrieval."
(or cj/anthropic-api-key-cached
- (setq cj/anthropic-api-key-cached
- (cj/auth-source-secret "api.anthropic.com" "apikey"))))
+ (setq cj/anthropic-api-key-cached
+ (cj/auth-source-secret "api.anthropic.com" "apikey"))))
(defun cj/openai-api-key ()
"Return the OpenAI API key, caching the result after first retrieval."
(or cj/openai-api-key-cached
- (setq cj/openai-api-key-cached
- (cj/auth-source-secret "api.openai.com" "apikey"))))
+ (setq cj/openai-api-key-cached
+ (cj/auth-source-secret "api.openai.com" "apikey"))))
(defun cj/ensure-gptel-backends ()
"Initialize GPTel backends if they are not already available.
-
Call this only after loading 'gptel' so the backend constructors exist."
(unless gptel-claude-backend
- (setq gptel-claude-backend
- (gptel-make-anthropic
- "Claude"
- :key (cj/anthropic-api-key)
- :models '(
- "claude-opus-4-1-20250805"
- "claude-3-5-sonnet-20241022"
- "claude-3-opus-20240229"
- "claude-3-5-haiku-20241022"
- )
- :stream t)))
+ (setq gptel-claude-backend
+ (gptel-make-anthropic
+ "Claude"
+ :key (cj/anthropic-api-key)
+ :models '(
+ "claude-opus-4-1-20250805"
+ "claude-3-5-sonnet-20241022"
+ "claude-3-opus-20240229"
+ "claude-3-5-haiku-20241022"
+ )
+ :stream t)))
(unless gptel-chatgpt-backend
- (setq gptel-chatgpt-backend
- (gptel-make-openai
- "ChatGPT"
- :key (cj/openai-api-key)
- :models '(
- "gpt-4o"
- "gpt-5"
- "gpt-4.1"
- "o1"
- )
- :stream t)))
+ (setq gptel-chatgpt-backend
+ (gptel-make-openai
+ "ChatGPT"
+ :key (cj/openai-api-key)
+ :models '(
+ "gpt-4o"
+ "gpt-5"
+ "gpt-4.1"
+ "o1"
+ )
+ :stream t)))
;; Set default backend
(unless gptel-backend
- (setq gptel-backend (or gptel-chatgpt-backend gptel-claude-backend))))
+ (setq gptel-backend (or gptel-chatgpt-backend gptel-claude-backend))))
(autoload 'cj/toggle-gptel "ai-config" "Toggle the AI-Assistant window" t)
-;; ------------------ Gptel Conversation And Utility Commands ------------------
+;; ------------------ GPTel Conversation And Utility Commands ------------------
(defun cj/gptel--available-backends ()
"Return an alist of (NAME . BACKEND), ensuring gptel and backends are initialized."
(unless (featurep 'gptel)
- (require 'gptel))
+ (require 'gptel))
(cj/ensure-gptel-backends)
(delq nil
- (list (and (bound-and-true-p gptel-claude-backend)
- (cons "Anthropic - Claude" gptel-claude-backend))
- (and (bound-and-true-p gptel-chatgpt-backend)
- (cons "OpenAI - ChatGPT" gptel-chatgpt-backend)))))
+ (list (and (bound-and-true-p gptel-claude-backend)
+ (cons "Anthropic - Claude" gptel-claude-backend))
+ (and (bound-and-true-p gptel-chatgpt-backend)
+ (cons "OpenAI - ChatGPT" gptel-chatgpt-backend)))))
(defun cj/gptel--model->string (m)
(cond
@@ -123,62 +124,61 @@ Call this only after loading 'gptel' so the backend constructors exist."
;; Backend/model switching commands (moved out of use-package so they are commandp)
(defun cj/gptel-change-model ()
"Change the GPTel backend and select a model from that backend.
-
Present all available models from every backend, switching backends when
necessary. Prompt for whether to apply the selection globally or buffer-locally."
(interactive)
(let* ((backends (cj/gptel--available-backends))
- (all-models
- (mapcan
- (lambda (pair)
- (let* ((backend-name (car pair))
- (backend (cdr pair))
- (models (when (fboundp 'gptel-backend-models)
- (gptel-backend-models backend))))
- (mapcar (lambda (m)
- (list (format "%s: %s" backend-name (cj/gptel--model->string m))
- backend
- (cj/gptel--model->string m)
- backend-name))
- models)))
- backends))
- (current-backend-name (car (rassoc (bound-and-true-p gptel-backend) backends)))
- (current-selection (format "%s: %s"
- (or current-backend-name "AI")
- (cj/gptel--model->string (bound-and-true-p gptel-model))))
- (scope (completing-read "Set model for: " '("buffer" "global") nil t))
- (selected (completing-read
- (format "Select model (current: %s): " current-selection)
- (mapcar #'car all-models) nil t nil nil current-selection)))
- (let* ((model-info (assoc selected all-models))
- (backend (nth 1 model-info))
- (model (intern (nth 2 model-info))) ;; Convert string to symbol
- (backend-name (nth 3 model-info)))
- (if (string= scope "global")
- (progn
- (setq gptel-backend backend)
- (setq gptel-model model)
- (message "Changed to %s model: %s (global)" backend-name model))
- (setq-local gptel-backend backend)
- (setq-local gptel-model (if (stringp model) (intern model) model))
- (message "Changed to %s model: %s (buffer-local)" backend-name model)))))
+ (all-models
+ (mapcan
+ (lambda (pair)
+ (let* ((backend-name (car pair))
+ (backend (cdr pair))
+ (models (when (fboundp 'gptel-backend-models)
+ (gptel-backend-models backend))))
+ (mapcar (lambda (m)
+ (list (format "%s: %s" backend-name (cj/gptel--model->string m))
+ backend
+ (cj/gptel--model->string m)
+ backend-name))
+ models)))
+ backends))
+ (current-backend-name (car (rassoc (bound-and-true-p gptel-backend) backends)))
+ (current-selection (format "%s: %s"
+ (or current-backend-name "AI")
+ (cj/gptel--model->string (bound-and-true-p gptel-model))))
+ (scope (completing-read "Set model for: " '("buffer" "global") nil t))
+ (selected (completing-read
+ (format "Select model (current: %s): " current-selection)
+ (mapcar #'car all-models) nil t nil nil current-selection)))
+ (let* ((model-info (assoc selected all-models))
+ (backend (nth 1 model-info))
+ (model (intern (nth 2 model-info))) ;; Convert string to symbol
+ (backend-name (nth 3 model-info)))
+ (if (string= scope "global")
+ (progn
+ (setq gptel-backend backend)
+ (setq gptel-model model)
+ (message "Changed to %s model: %s (global)" backend-name model))
+ (setq-local gptel-backend backend)
+ (setq-local gptel-model (if (stringp model) (intern model) model))
+ (message "Changed to %s model: %s (buffer-local)" backend-name model)))))
(defun cj/gptel-switch-backend ()
"Switch the GPTel backend and then choose one of its models."
(interactive)
(let* ((backends (cj/gptel--available-backends))
- (choice (completing-read "Select GPTel backend: " (mapcar #'car backends) nil t))
- (backend (cdr (assoc choice backends))))
- (unless backend
- (user-error "Invalid GPTel backend: %s" choice))
- (let* ((models (when (fboundp 'gptel-backend-models)
- (gptel-backend-models backend)))
- (model (completing-read (format "Select %s model: " choice)
- (mapcar #'cj/gptel--model->string models)
- nil t nil nil (cj/gptel--model->string (bound-and-true-p gptel-model)))))
- (setq gptel-backend backend
- gptel-model model)
- (message "Switched to %s with model: %s" choice model))))
+ (choice (completing-read "Select GPTel backend: " (mapcar #'car backends) nil t))
+ (backend (cdr (assoc choice backends))))
+ (unless backend
+ (user-error "Invalid GPTel backend: %s" choice))
+ (let* ((models (when (fboundp 'gptel-backend-models)
+ (gptel-backend-models backend)))
+ (model (completing-read (format "Select %s model: " choice)
+ (mapcar #'cj/gptel--model->string models)
+ nil t nil nil (cj/gptel--model->string (bound-and-true-p gptel-model)))))
+ (setq gptel-backend backend
+ gptel-model model)
+ (message "Switched to %s with model: %s" choice model))))
;; Clear assistant buffer (moved out so it's always available)
(defun cj/gptel-clear-buffer ()
@@ -188,14 +188,14 @@ Operate only when `gptel-mode' is active in an Org buffer so the heading
can be reinserted."
(interactive)
(let ((is-gptel (bound-and-true-p gptel-mode))
- (is-org (derived-mode-p 'org-mode)))
- (if (and is-gptel is-org)
- (progn
- (erase-buffer)
- (when (fboundp 'cj/gptel--fresh-org-prefix)
- (insert (cj/gptel--fresh-org-prefix)))
- (message "GPTel buffer cleared and heading reset"))
- (message "Not a GPTel buffer in org-mode. Nothing cleared."))))
+ (is-org (derived-mode-p 'org-mode)))
+ (if (and is-gptel is-org)
+ (progn
+ (erase-buffer)
+ (when (fboundp 'cj/gptel--fresh-org-prefix)
+ (insert (cj/gptel--fresh-org-prefix)))
+ (message "GPTel buffer cleared and heading reset"))
+ (message "Not a GPTel buffer in org-mode. Nothing cleared."))))
;; ----------------------------- Context Management ----------------------------
@@ -205,14 +205,14 @@ can be reinserted."
Returns t on success, nil on failure.
Provides consistent user feedback about the context state."
(when (and file-path (file-exists-p file-path))
- (gptel-add-file file-path)
- (let ((context-count (if (boundp 'gptel-context--alist)
- (length gptel-context--alist)
- 0)))
+ (gptel-add-file file-path)
+ (let ((context-count (if (boundp 'gptel-context--alist)
+ (length gptel-context--alist)
+ 0)))
(message "Added %s to GPTel context (%d sources total)"
- (file-name-nondirectory file-path)
- context-count))
- t))
+ (file-name-nondirectory file-path)
+ context-count))
+ t))
(defun cj/gptel-add-file ()
"Add a file to the GPTel context.
@@ -221,19 +221,19 @@ If inside a Projectile project, prompt from that project's file list.
Otherwise, prompt with `read-file-name'."
(interactive)
(let* ((in-proj (and (featurep 'projectile)
- (fboundp 'projectile-project-p)
- (projectile-project-p)))
- (file-name (if in-proj
- (let ((cands (projectile-current-project-files)))
- (if (fboundp 'projectile-completing-read)
- (projectile-completing-read "GPTel add file: " cands)
- (completing-read "GPTel add file: " cands nil t)))
- (read-file-name "GPTel add file: ")))
- (file-path (if in-proj
- (expand-file-name file-name (projectile-project-root))
- file-name)))
- (unless (cj/gptel--add-file-to-context file-path)
- (error "Failed to add file: %s" file-path))))
+ (fboundp 'projectile-project-p)
+ (projectile-project-p)))
+ (file-name (if in-proj
+ (let ((cands (projectile-current-project-files)))
+ (if (fboundp 'projectile-completing-read)
+ (projectile-completing-read "GPTel add file: " cands)
+ (completing-read "GPTel add file: " cands nil t)))
+ (read-file-name "GPTel add file: ")))
+ (file-path (if in-proj
+ (expand-file-name file-name (projectile-project-root))
+ file-name)))
+ (unless (cj/gptel--add-file-to-context file-path)
+ (error "Failed to add file: %s" file-path))))
(defun cj/gptel-add-buffer-file ()
"Select a buffer and add its associated file to the GPTel context.
@@ -243,13 +243,13 @@ a file, that file is added to the GPTel context. Otherwise, an error
message is displayed."
(interactive)
(let* ((buffers (mapcar #'buffer-name (buffer-list)))
- (selected-buffer-name (completing-read "Add file from buffer: " buffers nil t))
- (selected-buffer (get-buffer selected-buffer-name))
- (file-path (and selected-buffer
- (buffer-file-name selected-buffer))))
- (if file-path
- (cj/gptel--add-file-to-context file-path)
- (message "Buffer '%s' is not visiting a file" selected-buffer-name))))
+ (selected-buffer-name (completing-read "Add file from buffer: " buffers nil t))
+ (selected-buffer (get-buffer selected-buffer-name))
+ (file-path (and selected-buffer
+ (buffer-file-name selected-buffer))))
+ (if file-path
+ (cj/gptel--add-file-to-context file-path)
+ (message "Buffer '%s' is not visiting a file" selected-buffer-name))))
(defun cj/gptel-add-this-buffer ()
"Add the current buffer to the GPTel context.
@@ -258,7 +258,7 @@ Works for any buffer, whether it's visiting a file or not."
(interactive)
;; Load gptel-context if needed
(unless (featurep 'gptel-context)
- (require 'gptel-context))
+ (require 'gptel-context))
;; Use gptel-add with prefix arg '(4) to add current buffer
(gptel-add '(4))
(message "Added buffer '%s' to GPTel context" (buffer-name)))
@@ -287,9 +287,9 @@ Works for any buffer, whether it's visiting a file or not."
;; Named backend list for switching
(defvar cj/gptel-backends
- `(("Anthropic - Claude" . ,gptel-claude-backend)
- ("OpenAI - ChatGPT" . ,gptel-chatgpt-backend))
- "Alist of GPTel backends for interactive switching.")
+ `(("Anthropic - Claude" . ,gptel-claude-backend)
+ ("OpenAI - ChatGPT" . ,gptel-chatgpt-backend))
+ "Alist of GPTel backends for interactive switching.")
(setq gptel-confirm-tool-calls nil) ;; allow tool access by default
;;; ---------------------------- Backend Management ---------------------------
@@ -301,35 +301,35 @@ Works for any buffer, whether it's visiting a file or not."
;; Dynamic user prefix for org-mode heading (string, refreshed just before send)
(defun cj/gptel--fresh-org-prefix ()
- "Generate a fresh org-mode header with current timestamp for user messages."
- (concat "* " user-login-name " " (format-time-string "[%Y-%m-%d %H:%M:%S]") "\n"))
+ "Generate a fresh org-mode header with current timestamp for user messages."
+ (concat "* " user-login-name " " (format-time-string "[%Y-%m-%d %H:%M:%S]") "\n"))
;; Initialize as a string (GPTel expectation)
(setf (alist-get 'org-mode gptel-prompt-prefix-alist)
- (cj/gptel--fresh-org-prefix))
+ (cj/gptel--fresh-org-prefix))
;; Refresh immediately before each send for accurate timestamp
(defun cj/gptel--refresh-org-prefix (&rest _)
- "Update the org-mode prefix with fresh timestamp before sending message."
- (setf (alist-get 'org-mode gptel-prompt-prefix-alist)
- (cj/gptel--fresh-org-prefix)))
+ "Update the org-mode prefix with fresh timestamp before sending message."
+ (setf (alist-get 'org-mode gptel-prompt-prefix-alist)
+ (cj/gptel--fresh-org-prefix)))
(advice-add 'gptel-send :before #'cj/gptel--refresh-org-prefix)
;; AI header on each reply: (e.g. "*** AI: <model> [timestamp]")
(defun cj/gptel-backend-and-model ()
- "Return backend, model, and timestamp as a single string."
- (let* ((backend (pcase (bound-and-true-p gptel-backend)
- ((and v (pred vectorp)) (aref v 1)) ;; display name if vector
- (_ "AI")))
- (model (format "%s" (or (bound-and-true-p gptel-model) "")))
- (ts (format-time-string "[%Y-%m-%d %H:%M:%S]")))
- (format "%s: %s %s" backend model ts)))
+ "Return backend, model, and timestamp as a single string."
+ (let* ((backend (pcase (bound-and-true-p gptel-backend)
+ ((and v (pred vectorp)) (aref v 1)) ;; display name if vector
+ (_ "AI")))
+ (model (format "%s" (or (bound-and-true-p gptel-model) "")))
+ (ts (format-time-string "[%Y-%m-%d %H:%M:%S]")))
+ (format "%s: %s %s" backend model ts)))
(defun cj/gptel-insert-model-heading (response-begin-pos _response-end-pos)
- "Insert an Org heading for the AI reply at RESPONSE-BEGIN-POS."
- (save-excursion
- (goto-char response-begin-pos)
- (insert (format "* %s\n" (cj/gptel-backend-and-model)))))
+ "Insert an Org heading for the AI reply at RESPONSE-BEGIN-POS."
+ (save-excursion
+ (goto-char response-begin-pos)
+ (insert (format "* %s\n" (cj/gptel-backend-and-model)))))
(add-hook 'gptel-post-response-functions #'cj/gptel-insert-model-heading))
@@ -339,26 +339,26 @@ Works for any buffer, whether it's visiting a file or not."
"Toggle the visibility of the AI-Assistant buffer, and place point at its end."
(interactive)
(let* ((buf-name "*AI-Assistant*")
- (buffer (get-buffer buf-name))
- (win (and buffer (get-buffer-window buffer))))
- (if win
- (delete-window win)
- ;; Ensure GPTel and our backends are initialized before creating the buffer
- (unless (featurep 'gptel)
- (require 'gptel))
- (cj/ensure-gptel-backends)
- (unless buffer
- ;; Pass backend, not model
- (gptel buf-name gptel-backend))
- (setq buffer (get-buffer buf-name))
- (setq win
- (display-buffer-in-side-window
- buffer
- '((side . right)
- (window-width . 0.4))))
- (select-window win)
- (with-current-buffer buffer
- (goto-char (point-max))))))
+ (buffer (get-buffer buf-name))
+ (win (and buffer (get-buffer-window buffer))))
+ (if win
+ (delete-window win)
+ ;; Ensure GPTel and our backends are initialized before creating the buffer
+ (unless (featurep 'gptel)
+ (require 'gptel))
+ (cj/ensure-gptel-backends)
+ (unless buffer
+ ;; Pass backend, not model
+ (gptel buf-name gptel-backend))
+ (setq buffer (get-buffer buf-name))
+ (setq win
+ (display-buffer-in-side-window
+ buffer
+ '((side . right)
+ (window-width . 0.4))))
+ (select-window win)
+ (with-current-buffer buffer
+ (goto-char (point-max))))))
;; ------------------------------- Clear Context -------------------------------
@@ -367,16 +367,16 @@ Works for any buffer, whether it's visiting a file or not."
(interactive)
(cond
((fboundp 'gptel-context-remove-all)
- (call-interactively 'gptel-context-remove-all)
- (message "GPTel context cleared"))
+ (call-interactively 'gptel-context-remove-all)
+ (message "GPTel context cleared"))
((fboundp 'gptel-context-clear)
- (call-interactively 'gptel-context-clear)
- (message "GPTel context cleared"))
+ (call-interactively 'gptel-context-clear)
+ (message "GPTel context cleared"))
((boundp 'gptel-context--alist)
- (setq gptel-context--alist nil)
- (message "GPTel context cleared"))
+ (setq gptel-context--alist nil)
+ (message "GPTel context cleared"))
(t
- (message "No known GPTel context clearing function available"))))
+ (message "No known GPTel context clearing function available"))))
;;; -------------------------------- GPTel-Magit --------------------------------
@@ -398,22 +398,22 @@ Works for any buffer, whether it's visiting a file or not."
;;; --------------------------------- AI Keymap ---------------------------------
-(define-prefix-command 'cj/ai-keymap nil
- "Keymap for AI operations.")
-(keymap-set cj/custom-keymap "a" #'cj/ai-keymap)
-(keymap-set cj/ai-keymap "B" #'cj/gptel-switch-backend) ;; change the backend (OpenAI, Anthropic, etc.)
-(keymap-set cj/ai-keymap "M" #'gptel-menu) ;; gptel's transient menu
-(keymap-set cj/ai-keymap "d" #'cj/gptel-delete-conversation) ;; delete conversation
-(keymap-set cj/ai-keymap "." #'cj/gptel-add-this-buffer) ;; add buffer to context
-(keymap-set cj/ai-keymap "f" #'cj/gptel-add-file) ;; add a file to context
-(keymap-set cj/ai-keymap "l" #'cj/gptel-load-conversation) ;; load and continue conversation
-(keymap-set cj/ai-keymap "m" #'cj/gptel-change-model) ;; change the LLM model
-(keymap-set cj/ai-keymap "p" #'gptel-system-prompt) ;; change prompt
-(keymap-set cj/ai-keymap "&" #'gptel-rewrite) ;; rewrite a region of code/text
-(keymap-set cj/ai-keymap "r" #'cj/gptel-context-clear) ;; remove all context
-(keymap-set cj/ai-keymap "s" #'cj/gptel-save-conversation) ;; save conversation
-(keymap-set cj/ai-keymap "t" #'cj/toggle-gptel) ;; toggles the ai-assistant window
-(keymap-set cj/ai-keymap "x" #'cj/gptel-clear-buffer) ;; clears the assistant buffer
+(defvar-keymap cj/ai-keymap
+ :doc "Keymap for gptel and other AI operations."
+ "B" #'cj/gptel-switch-backend ;; change the backend (OpenAI, Anthropic, etc.
+ "M" #'gptel-menu ;; gptel's transient menu
+ "d" #'cj/gptel-delete-conversation ;; delete conversation
+ "." #'cj/gptel-add-this-buffer ;; add buffer to context
+ "f" #'cj/gptel-add-file ;; add a file to context
+ "l" #'cj/gptel-load-conversation ;; load and continue conversation
+ "m" #'cj/gptel-change-model ;; change the LLM model
+ "p" #'gptel-system-prompt ;; change prompt
+ "&" #'gptel-rewrite ;; rewrite a region of code/text
+ "r" #'cj/gptel-context-clear ;; remove all context
+ "s" #'cj/gptel-save-conversation ;; save conversation
+ "t" #'cj/toggle-gptel ;; toggles the ai-assistant window
+ "x" #'cj/gptel-clear-buffer) ;; clears the assistant buffer
+(keymap-set cj/custom-keymap "a" cj/ai-keymap)
(provide 'ai-config)
;;; ai-config.el ends here.