karthink / gptel

A simple LLM client for Emacs
GNU General Public License v3.0
1.04k stars 113 forks source link

Export rewrite message #175

Closed daedsidog closed 3 months ago

daedsidog commented 5 months ago

Make it so that the refactor/rewrite/replace message is customizable.

README.org needs to be changed to reflect the changes, as well.

karthink commented 5 months ago

@daedsidog Thanks for the PR. Sorry for the delay -- I'm putting off looking at this because I want to unify the rewrite message and the system directives menus. Ideally I want to get rid of the rewrite menu entirely and make everything accessible from the main gptel-menu itself. I'm not sure yet how it's going to look/work. I'll work on it soon and you can take a look at the new interface and decide if this PR still makes sense.

daedsidog commented 4 months ago

@karthink Have the changes been added?

karthink commented 3 months ago

@karthink Have the changes been added?

@daedsidog I'm planning to remove the refactor menu and commands from gptel -- I think adding them was a mistake. Here is a preview of the unified interface for gptel -- it's flexible enough that you can do refactoring and a lot more from the main gptel-menu. It's not done yet, but please test it out (latest commit, master branch) and let me know what you think.

daedsidog commented 3 months ago

@karthink Looks like you're implementing something I've hodgepodged before. Indeed, the only reason I needed to export the system message was because I wanted to customize the refactor, so there's no reason for this anymore.

I'd like you to take a look at part of my config which I have right now, and tell me if what I have can be shelved away, because it seems the system you're aiming for supersedes it, for example, I just add INSTRUCTION as a comment, and tell GPTEL to address that.

TLDR:

  1. I use kbd-execute-macro to setup GPTEL from the transient menu.
  2. I clean the code returned by GPTEL by removing the markdown fences and indent it (some other things too).
  3. I setup the user directive to ChatGPT based on the buffer, i.e. based on the programming language and major mode.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; ------------------------------ CONTEXTER ------------------------------- ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(use-package contexter
  :commands (contexter-context-string
             contexter-context-substring
             contexter-context-in-region
             contexter-pop-or-push-context
             contexter-remove-context))

(defun my/code-refactoring-prompt-string (code-regions)
  "Create a prompt for an LLM to refactor CODE-REGIONS in the current buffer."
  (let* ((context (contexter-context-string))
         (have-context (not (zerop (length context)))))
    (concat "I need you to refactor and/or write code. Write carefully with\
 great respect to existing syntax, styling, & conventions, this also includes\
 column width."
            (when have-context
              (concat "Listed below is context that may help you. Use it as a\
 read-only reference. You are strictly prohibited from changing it in any\
 way, even if asked directly."
                      "\n\n" context))
            (if have-context
                "\n\nNow, given the above context, do as instructed by the\
 INSTRUCTION comments in the following source:"
              "\n\nDo as instructed by the INSTRUCTION comments in the following\
 source:")
            "\n\n"
            (contexter-context-substring (current-buffer) code-regions)
            "\n\n"
            "When writing code, do not delete existing comments. Do not fix\
 what you may think are bugs, unless explicitly instructed to. Do not refactor\
 things you were not instructed to. You are to remove instruction comments\
 after they are completed.

You have more creative freedom when you are generating code, but you must still\
 follow your instructions closely.

As opposed to the context, the source code you have been given has been erased\
 from its original location. Therefore, you are expected to return it, fully.\
 Of course, you are not to return the ellipses or line numbers. Remember, you\
 may have been given partial code, so be mindful and return it in the same way\
 you found it. Return **ALL** of the modified target code, leave nothing behind.

Return only code, nothing else. When documenting your work, make no reference\
 to the instructions, so that an outside reader will think the code was written\
 in a natural fashion.")))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; --------------------------- END OF CONTEXTER --------------------------- ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; -------------------------------- GPTEL --------------------------------- ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun my/predicated-prog-lang (prog-lang-major-mode)
  "Derive the programming language name from the PROG-LANG-MAJOR-MODE.
Adds a predicate before the name."
  (pcase prog-lang-major-mode
    ('emacs-lisp-mode "an Emacs Lisp")
    ('js-mode "a JavaScript")
    ('c-mode "a C")
    ('c++-mode "a C++")
    ('lisp-mode "a Common Lisp")
    ('twee-mode "a Twee")
    ('web-mode "a Web")
    (_ (concat "a " (substring (symbol-name prog-lang-major-mode) nil -5)))))

(defun my/refactoring-message ()
    "Set a generic refactor/rewrite message for the buffer."
    (if (derived-mode-p 'prog-mode)
        (concat (format "You are %s programmer. Write legible, elegant, and\
 very terse code based on instructions. Do not stray from instructions."
                        (my/predicated-prog-lang major-mode)))
      (format "You are a prose editor. Rewrite the following text to be more\
 professional.")))

(cl-defun my/clean-up-llm-refactored-code (beg end)
  "Clean up the code responses for refactored code in the current buffer.
Current buffer is guaranteed to be the response buffer."
  (save-excursion
    (let* ((res-beg beg)
           (res-end end)
           (contents nil))
      (setq contents (buffer-substring-no-properties res-beg
                                                       res-end))
      (setq contents (replace-regexp-in-string
                      "^\\(```.*\n\\)\\|\n\\(```.*\\)$"
                      ""
                      contents))
      (delete-region res-beg res-end)
      (goto-char res-beg)
      (insert contents)
      (setq res-end (point))
      ;; Indent the code to match the buffer indentation if it's messed up.
      (unless (eq indent-line-function #'indent-relative)
        (indent-region res-beg res-end))
      (pulse-momentary-highlight-region res-beg res-end)
      (setq res-beg (next-single-property-change res-beg 'gptel)))))

(defun my/clean-up-llm-chat-response (beg end)
  (save-excursion
    (goto-char beg)
    (org-latex-preview)
    (my/resize-org-latex-overlays)
    (my/adjust-org-latex-overlay-padding)
    (pulse-momentary-highlight-region beg end)))

(defun my/clean-up-llm-response (beg end)
  (if (bound-and-true-p gptel-mode)
      (my/clean-up-llm-chat-response beg end)
    (my/clean-up-llm-refactored-code beg end)))

;; OpenAI ChatGPT API interface
(use-package gptel-curl
  :commands (gptel gptel-menu gptel-abort)
  :config
  (require 'gptel-transient)
  (customize-set-variable 'gptel-directives
                          (let ((new-directives gptel-directives))
                            (setf (cdr (assoc 'default new-directives))
                                  "You are a large language model living in\
 Emacs and a helpful assistant. Respond as concisely as possible.")
                            new-directives))
  (customize-set-variable 'gptel-api-key
                          "redacted")
  (customize-set-variable 'gptel-default-mode 'org-mode)
  (customize-set-variable
   'gptel-prompt-prefix-alist
   (cons (cons 'org-mode "* Query\n\n")
         (assq-delete-all 'org-mode gptel-prompt-prefix-alist)))
  (customize-set-variable
   'gptel-response-prefix-alist
   (cons (cons 'org-mode "** Response\n\n")
         (assq-delete-all 'org-mode gptel-response-prefix-alist)))
  (customize-set-variable 'gptel-model "gpt-4-0125-preview")
  (customize-set-variable 'gptel-rewrite-message #'my/refactoring-message)
  (add-hook 'gptel-mode-hook
            (lambda () (interactive)
              (display-time-mode)
              (setq gptel--system-message
                    (alist-get 'default gptel-directives))))
  (add-hook 'gptel-post-response-functions #'my/clean-up-llm-response)
  :bind (("C-c g c" . #'gptel)
         ("C-c g m" . #'gptel-menu)
         ("C-c g p" . #'contexter-pop-or-push-context)
         ("C-c g r" . #'my/refactor-using-llm-with-context)
         ("C-c g a" . #'gptel-abort)
         (:map gptel-mode-map
               ;; Insert a prefix when deleting the entire buffer in
               ;; `gptel-mode'.
               (("C-<tab> DEL" . (lambda ()
                            (interactive)
                            (my/delete-entire-buffer)
                            (catch 'finish
                              (dolist (it gptel-prompt-prefix-alist)
                                (let ((mode (car it))
                                      (prefix (cdr it)))
                                  (when (eq major-mode mode)
                                    (insert prefix)
                                    (goto-char (point-max))
                                    (throw 'finish nil)))))))))))

(cl-defun my/refactor-using-llm-with-context ()
  (interactive)
  (unless (use-region-p)
    (cl-return-from my/refactor-using-llm-with-context))
  (let* ((start (region-beginning))
         (end (region-end))
         (context (contexter-context-in-region (current-buffer) start end))
         (prompt)
         (region-substring (buffer-substring-no-properties start end)))
    (when context
      (contexter-remove-context context t))
    ;; Check for the presence of instruction in region.
    (when (not (string-match-p "INSTRUCTION:" region-substring))
      (error "No instruction found within region"))
    (setq prompt (my/code-refactoring-prompt-string (list (list start end))))
    (deactivate-mark)
    (delete-region start end)
    (set-mark (point))
    (insert prompt)
    (activate-mark)
    (gptel-menu)
    (execute-kbd-macro (kbd "rR"))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; ----------------------------- END OF GPTEL ----------------------------- ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
karthink commented 3 months ago

Is contexter a package you wrote? I don't see it on (M)ELPA or Google.

I'm having trouble understanding the non-gptel-post-response-functions part of your code as a result.

daedsidog commented 3 months ago

Is contexter a package you wrote? I don't see it on (M)ELPA or Google.

Yes, but it's local and unpublished. It started out as part of my config, then outgrew to be its own thing.

I'm having trouble understanding the non-gptel-post-response-functions part of your code as a result.

As I've showed you elsewhere before, the contexter is just a way to simultaneously mark different regions of different buffers to construct a context string that can be given to an LLM so it better understands the project.

karthink commented 3 months ago

I'm having trouble understanding the non-gptel-post-response-functions part of your code as a result.

As I've showed you elsewhere before, the contexter is just a way to simultaneously mark different regions of different buffers to construct a context string that can be given to an LLM so it better understands the project.

Okay, I'll take a look at your code again.