diff options
Diffstat (limited to 'modules/ai-config.el')
| -rw-r--r-- | modules/ai-config.el | 372 |
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. |
