summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-08-13 16:49:19 -0500
committerCraig Jennings <c@cjennings.net>2025-08-13 16:49:19 -0500
commit6b0aa9a0becafa661a0de6a4e9418352809c17f6 (patch)
treef1483800acd331e6eff9931f2717239653478e05 /modules
parent20f47901dbce5dd0911299a9af1f1acc98d6cc0b (diff)
downloaddotemacs-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.el211
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