diff options
| author | Craig Jennings <c@cjennings.net> | 2025-08-13 16:49:19 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-08-13 16:49:19 -0500 |
| commit | 6b0aa9a0becafa661a0de6a4e9418352809c17f6 (patch) | |
| tree | f1483800acd331e6eff9931f2717239653478e05 /modules | |
| parent | 20f47901dbce5dd0911299a9af1f1acc98d6cc0b (diff) | |
| download | dotemacs-6b0aa9a0becafa661a0de6a4e9418352809c17f6.tar.gz dotemacs-6b0aa9a0becafa661a0de6a4e9418352809c17f6.zip | |
feat(ai-config): refactor GPTel integration and add features
- Simplify and unify `cj/toggle-gptel` with auto-focus and
- scroll-to-end Introduce `ai-keymap` on C-h g for toggling and
- clearing the AI buffer Use dynamic Org heading prefixes and
- per-response reply headings with timestamps Update default model
- setting, prompt directives, and auth key retrieval
- `gptel-magit` support for Magit integration
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/ai-config.el | 211 |
1 files changed, 99 insertions, 112 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el index 196a2fad..241ee42c 100644 --- a/modules/ai-config.el +++ b/modules/ai-config.el @@ -3,22 +3,19 @@ ;;; Commentary: -;; There are several workflows available. Here are the ones I use most. +;; Here is my basic workflow: + +;; - Launch GPTel via F9 or C-h g t, and chat with the AI in the side window. +;; Remember that sending the message requires C-<return>. -;; - Launch GPTel and chat with the AI in a separate buffer. -;; Chatting is fine, but it can mean cutting and pasting code back and forth -;; between buffers. -;; - Select a region and launch GPTel. -;; The region is automatically inserted into the buffer making it easy to -;; simply ask a question after it's read the code. ;; Note that you can save a file, then turn on gptel-mode to resume your ;; conversation. -;; Remember that sending the message requires C-<return>. -;;; Code: + +;;; ------------------------------- Directives --------------------------------- (defvar default-directive "You are a large language model living in Emacs. You understand philosophy, critical theory, and comparative @@ -34,129 +31,119 @@ (defvar chat-directive "You are a large language model and a funny conversation partner who asks good questions.") +;;; ------------------------------ Toggle GPTel -------------------------------- -;; -------------------------------- Toggle GPTel ------------------------------- -;; Toggle GPTel's buffer in a side window (defun cj/toggle-gptel () - "Toggle the visibility of the ChatGPT buffer without prompting for a name." + "Toggle the visibility of the ChatGPT buffer, and when shown place point at its end." (interactive) - (let ((buffer (get-buffer "*AI-Assistant*"))) - (if (and buffer (get-buffer-window buffer)) - (delete-window (get-buffer-window buffer)) - (if buffer - (display-buffer-in-side-window buffer '((side . right) (window-width . 0.4))) - ;; Call gptel with a fixed buffer name to skip prompt - (gptel "*AI-Assistant*" gptel-model))))) - -;; ;; defer prefixing the prompt until gptel is actually loaded -;; (with-eval-after-load 'gptel -;; ;; Define the var if the package hasn't yet -;; (unless (boundp 'gptel-prompt-prefix-alist) -;; (defvar gptel-prompt-prefix-alist nil -;; "Alist mapping major modes to prompt prefixes for gptel.") -;; (add-to-list 'gptel-prompt-prefix-alist -;; (cons 'org-mode -;; (concat "*** cj " -;; (format-time-string "[%Y-%m-%d %H:%M:%S]") -;; "\n"))))) - -;; ----------------------------------- GPTel ----------------------------------- -;; Emacs integration with large language models + (let* ((buf-name "*AI-Assistant*") + (buffer (get-buffer buf-name)) + (win (and buffer (get-buffer-window buffer)))) + (if win + ;; If it's already visible, just close it + (delete-window win) + ;; Otherwise ensure the buffer exists + (unless buffer + (gptel buf-name gptel-model)) + (setq buffer (get-buffer buf-name)) + ;; Display in a side window, select it, and move point to end + (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)))))) + +;; ------------------------- GPTel Config And AI-Keymap ------------------------ + +(defvar ai-keymap + (let ((map (make-sparse-keymap))) + (define-key map (kbd "t") #'cj/toggle-gptel) + (define-key map (kbd "c") #'cj/gptel-clear-buffer) + map) + "Keymap for AI commands, bound to C-h g…") +(global-set-key (kbd "C-h g") ai-keymap) (use-package gptel - :defer .5 + :defer 0.5 :commands (gptel gptel-send) :bind - (("C-h G" . cj/toggle-gptel) - ("<f9>" . cj/toggle-gptel) - (:map gptel-mode-map - ("C-<return>" . gptel-send))) + ("<f9>" . cj/toggle-gptel) + (:map gptel-mode-map + ("C-<return>" . gptel-send)) :custom (gptel-default-mode 'org-mode) (gptel-expert-commands t) (gptel-track-media t) (gptel-include-reasoning 'ignore) - (gptel-model 'gpt-4o) + (gptel-model 'o4-mini) ;; keep or change to your preferred model (gptel-log-level 'info) (gptel--debug nil) :config + ;; Directives (setq gptel-directives - `((default . ,default-directive) - (code-only . ,code-only-directive) - (writing . ,writing-directive) - (chat . ,chat-directive))) - - ;; fancy prompt - (add-to-list 'gptel-prompt-prefix-alist - (cons 'org-mode - (concat "*** cj " - (format-time-string "[%Y-%m-%d %H:%M:%S]") - "\n"))) - - ;; Grab the secret from the auth file + `((default . ,default-directive) + (code-only . ,code-only-directive) + (writing . ,writing-directive) + (chat . ,chat-directive))) + + ;; Dynamic user prefix for org-mode heading (string, refreshed just before send) + (defun cj/gptel--fresh-org-prefix () + (concat "*** cj " (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)) + + ;; Refresh immediately before each send for accurate timestamp + (defun cj/gptel--refresh-org-prefix (&rest _) + (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. "*** ChatGPT: <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 + (_ "ChatGPT"))) + (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))))) + + (defun cj/gptel-clear-buffer () + "Erase the contents of the *AI-Assistant* buffer, re-insert the org heading, and message." + (interactive) + (let ((buf (get-buffer "*AI-Assistant*"))) + (if (not buf) + (message "No AI buffer found") + (with-current-buffer buf + (erase-buffer) + ;; re-insert the user heading with fresh timestamp + (insert (cj/gptel--fresh-org-prefix)) + (message "AI buffer cleared and heading reset"))))) + + ;; Hook is called with (BEG END); add our per-reply heading + (add-hook 'gptel-post-response-functions #'cj/gptel-insert-model-heading) + + ;; ---- Auth: pick the API key from your auth source (setq auth-sources `((:source ,authinfo-file))) (setq gptel-api-key (auth-source-pick-first-password :host "api.openai.com"))) -;; -------------------------- GPTel Fancy Org Headers -------------------------- -;; each response will be generated under it's own org mode header with backend -;; and model indicated. This is useful for leveraging several LLMs and comparing -;; and saving responses between them. - -(defun cj/gptel-backend-and-model () - "Return gptel backend and model with timestamp in the desired format." - (let ((backend (if (boundp 'gptel-backend) (aref gptel-backend 1))) - (model (if (boundp 'gptel-model) gptel-model)) - (timestamp (format-time-string "[%Y-%m-%d %H:%M:%S]"))) - (format "%s: %s %s" backend model timestamp))) - -(defun cj/gptel-insert-model-in-non-gptel-buffers () - "Add the backend, model, and timestamp in non-dedicated GPTel buffers. -To be used in `gptel-pre-response-hook`." - (unless (member 'gptel-mode local-minor-modes) - (goto-char (point-max)) - (insert (format "\n%s: " (cj/gptel-backend-and-model))) - (goto-char (point-max)))) - -(defun cj/gptel-insert-model-in-chat-buffers (response-begin-pos response-end-pos) - "Add the backend, model, and timestamp in dedicated chat buffers. -Can be used with the `gptel-post-response-functions` hook." - (let* ((gptel-org-prefix (alist-get 'org-mode gptel-prompt-prefix-alist)) - (inserted-string (format "%s %s\n" - (substring gptel-org-prefix 0 (string-match " " gptel-org-prefix)) - (cj/gptel-backend-and-model))) - (len-inserted (length inserted-string))) - (goto-char response-begin-pos) - (insert inserted-string) - (goto-char (+ response-end-pos len-inserted)))) - -;; (defun cj/gptel-backend-and-model () -;; "Return gptel backend and model (if any)." -;; (let ((backend (if (boundp 'gptel-backend) (aref gptel-backend 1))) -;; (model (if (boundp 'gptel-model) gptel-model))) -;; (format "(%s %s)" backend model))) - -;; (defun cj/gptel-insert-model-in-non-gptel-buffers () -;; "This function will add the backend and model in the \"dynamic\" buffers, not in dedicated chat buffers. -;; To be used in `gptel-pre-response-hook'." -;; (unless (member 'gptel-mode local-minor-modes) -;; (goto-char (point-max)) -;; (insert (format "\n%s: " (cj/gptel-backend-and-model))) -;; (goto-char (point-max)))) -;; (add-hook 'gptel-pre-response-hook 'cj/gptel-insert-model-in-non-gptel-buffers) - -;; (defun cj/gptel-insert-model-in-chat-buffers (response-begin-pos response-end-pos) -;; "This function adds the backend and model in dedicated chat buffers. -;; Can be used with the `gptel-post-response-functions' hook." -;; (let* ((gptel-org-prefix (alist-get 'org-mode gptel-prompt-prefix-alist)) -;; (inserted-string (format "%s %s\n" -;; (substring gptel-org-prefix 0 (string-match " " gptel-org-prefix)) -;; (cj/gptel-backend-and-model))) -;; (len-inserted (length inserted-string ))) -;; (goto-char response-begin-pos) -;; (insert inserted-string) -;; (goto-char (+ response-end-pos len-inserted)))) -;; (add-hook 'gptel-post-response-functions 'cj/gptel-insert-model-in-chat-buffers) +;; -------------------------------- GPTel-Magit -------------------------------- + +(use-package gptel-magit + :defer .5 + :hook (magit-mode . gptel-magit-install)) (provide 'ai-config) ;;; ai-config.el ends here |
