Skip to content

Instantly share code, notes, and snippets.

@ultronozm
Created September 22, 2024 18:36
Show Gist options
  • Select an option

  • Save ultronozm/d6873da862f87543d6dbad5191c8ae7a to your computer and use it in GitHub Desktop.

Select an option

Save ultronozm/d6873da862f87543d6dbad5191c8ae7a to your computer and use it in GitHub Desktop.
;;; llm-apply.el --- modify buffer contents via LLMs -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Paul D. Nelson
;; Author: Paul D. Nelson <nelson.paul.david@gmail.com>
;; Keywords:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'llm)
(defgroup llm-apply nil
"Customization group for llm-apply package."
:group 'tools)
(defcustom llm-apply-provider nil
"The default LLM provider to use for llm-apply.
This should be an instance of an LLM provider created using the `llm'
package (e.g., via `make-llm-openai')."
:type 'symbol)
(defcustom llm-apply-log-buffer-name "*LLM Apply Log*"
"Name of the buffer used for logging LLM apply actions."
:type 'string)
;; adapted from aider
(defcustom llm-apply-system-message
"You are an assistant that generates a list of buffer modifications and file creations based on given directions. Use the provided function calls, namely, generate_diff to describe buffer changes using universal diff format and create_file to create new files. Notes:
For each file that needs to be changed, write out the changes similar to a unified diff like `diff -U0` would produce.
Make sure you include the first 2 lines with the file paths.
Don't include timestamps with the file paths.
Start each hunk of changes with a `@@ ... @@` line.
Don't include line numbers like `diff -U0` does.
The user's patch tool doesn't need them.
The user's patch tool needs CORRECT patches that apply cleanly against the current contents of the file!
Think carefully and make sure you include and mark all lines that need to be removed or changed as `-` lines.
Make sure you mark all new or modified lines with `+`.
Don't leave out any lines or the diff patch won't apply correctly.
Indentation matters in the diffs!
Start a new hunk for each section of the file that needs changes.
Only output hunks that specify changes with `+` or `-` lines.
Skip any hunks that are entirely unchanging ` ` lines.
Output hunks in whatever order makes the most sense.
Hunks don't need to be in any particular order.
When editing a function, method, loop, etc use a hunk to replace the *entire* code block.
Delete the entire existing version with `-` lines and then add a new, updated version with `+` lines.
This will help you generate correct code and correct diffs.
To move code within a file, use 2 hunks: 1 to delete it from its current location, 1 to insert it in the new location."
"The system message to use for the LLM prompt."
:type 'string)
(cl-defstruct llm-apply-buffer-change
type buffer diff)
(cl-defstruct llm-apply-file-creation
filename content)
(defun llm-apply--make-buffer-change ()
"Create an LLM function call for generating diffs.
This function returns an `llm-function-call' object that represents
a function for generating universal diffs. When called, this function
will create an `llm-apply-buffer-change' struct with the provided
buffer name and diff.
The generated function has two arguments:
- buffer: The name of the buffer to modify (string, required)
- diff: The universal diff describing the changes to apply (string, required)
This is primarily used in the context of the auxiliary LLM for
applying changes to buffers based on natural language instructions."
(make-llm-function-call
:function (lambda (buffer diff)
(make-llm-apply-buffer-change
:type 'apply-diff
:buffer buffer
:diff diff))
:name "generate_diff"
:description "Generate a universal diff for changes to be applied to the buffer."
:args (list (make-llm-function-arg
:name "buffer"
:description "The name of the buffer to modify."
:type 'string
:required t)
(make-llm-function-arg
:name "diff"
:description "The universal diff describing the changes to be applied."
:type 'string
:required t))))
(defun llm-apply--make-file-creation ()
"Create an LLM function call for file creation.
This function returns an `llm-function-call' object that represents
a function for creating new files. When called, this function will
create an `llm-apply-file-creation' struct with the provided
filename and content.
The generated function has two arguments:
- filename: The name of the file to create (string, required)
- content: The content to write to the new file (string, required)
This is primarily used in the context of the auxiliary LLM for
creating new files based on natural language instructions."
(make-llm-function-call
:function (lambda (filename content)
(make-llm-apply-file-creation
:filename filename
:content content))
:name "create_file"
:description "Describe the creation of a new file with content."
:args (list (make-llm-function-arg
:name "filename"
:description "The name of the file to create."
:type 'string
:required t)
(make-llm-function-arg
:name "content"
:description "The content to write to the new file."
:type 'string
:required t))))
;;; Prompt generation
(defun llm-apply--make-prompt (instructions buffers)
"Generate a prompt for the auxiliary LLM based on INSTRUCTIONS and BUFFERS.
INSTRUCTIONS is a string describing the desired effects. BUFFERS is a
list of buffers to be included in the context and subject to
modification.
The prompt is structured to guide the LLM in producing a list of buffer
modifications and file creations based on the given instructions, using
the provided function calls (generate_diff and create_file)."
(let* ((context (mapconcat
(lambda (buf)
(format "Buffer: %s\nContents:\n%s"
(buffer-name buf)
(concat
"```\n"
(with-current-buffer buf
(buffer-substring-no-properties
(point-min) (point-max)))
"```")))
buffers
"\n\n")))
(llm-make-chat-prompt
instructions
:context (concat llm-apply-system-message "\n\n" context)
:functions (list (llm-apply--make-buffer-change)
(llm-apply--make-file-creation)))))
(defun llm-apply--parse-response (response)
"Parse the LLM RESPONSE and return a list of change objects."
(if (stringp response)
(progn (message "String response from LLM: %s" response)
nil)
(cl-loop for (func-name . result) in response
when result
collect (pcase func-name
("generate_diff"
(unless (llm-apply-buffer-change-p result)
(error "Invalid result for generate_diff: %S" result))
result)
("create_file"
(unless (llm-apply-file-creation-p result)
(error "Invalid result for create_file: %S" result))
result)
(_ (error "Unknown function call: %S" func-name))))))
(defun llm-apply--diff (diff)
"Apply DIFF to some buffers."
(interactive "sDiff to apply: ")
(dolist (hunks (split-string diff "--- .*\n\\+\\+\\+ " t))
(setq hunks (split-string hunks "\n" t))
(let ((buf-name (car hunks)))
(setq hunks (string-join (cdr hunks) "\n"))
(if-let (buf (get-buffer buf-name))
(with-current-buffer buf
(dolist (hunk (split-string hunks "^@@.*\n" t))
(let (before after)
(dolist (line (split-string hunk "\n" t))
(cond
((string-prefix-p " " line)
(push (substring line 1) before)
(push (substring line 1) after))
((string-prefix-p "-" line)
(push (substring line 1) before))
((string-prefix-p "+" line)
(push (substring line 1) after))
(t (error "Invalid line in hunk: %s" line))))
(setq before (string-join (nreverse before) "\n"))
(setq after (string-join (nreverse after) "\n"))
;; Find the start position of the old content
(goto-char (point-min))
(if (re-search-forward (regexp-quote before) nil t)
(let ((start (match-beginning 0))
(end (match-end 0)))
(delete-region start end)
(goto-char start)
(insert after)
(message "Applied hunk to buffer %s" buf-name))
(message "Hunk not found in buffer %s" buf-name)))))
(message "Buffer %s not found" buf-name)))))
(defun llm-apply-init-log-buffer ()
"Initialize the LLM Apply log buffer if it doesn't exist."
(let ((buffer (get-buffer-create llm-apply-log-buffer-name)))
(unless (eq (buffer-local-value 'major-mode buffer) 'org-mode)
(with-current-buffer buffer
(org-mode)
(insert "* LLM Apply Actions Log\n\n")))
buffer))
(defun llm-apply-log-action (action)
"Log an ACTION in the LLM Apply log buffer with a timestamp."
(with-current-buffer (llm-apply-init-log-buffer)
(goto-char (point-max))
(insert (format-time-string "** [%Y-%m-%d %H:%M:%S] "))
(cond
((llm-apply-buffer-change-p action)
(let* ((buffer-name (llm-apply-buffer-change-buffer action))
(diff (llm-apply-buffer-change-diff action))
(temp-file (make-temp-file "llm-apply-diff-" nil ".diff")))
(with-temp-file temp-file
(insert diff))
(insert (format "Buffer Change: %s\n" buffer-name))
(insert (format " [[file:%s][View Diff]]\n\n" temp-file))))
((llm-apply-file-creation-p action)
(insert (format "File Creation: %s\n\n" (llm-apply-file-creation-filename action))))
(t (insert (format "Unknown Action Type: %S\n\n" action))))))
(defun llm-apply-log-request (instructions buffers prompt)
"Log the LLM request in the LLM Apply log buffer with a timestamp."
(with-current-buffer (llm-apply-init-log-buffer)
(goto-char (point-max))
(insert (format-time-string "** [%Y-%m-%d %H:%M:%S] "))
(let ((temp-file (make-temp-file "llm-apply-request-" nil ".org")))
(with-temp-file temp-file
(insert "* Instructions\n\n")
(insert instructions)
(insert "\n\n* Buffers\n\n")
(dolist (buf buffers)
(insert (format "** %s\n" (buffer-name buf)))
(insert "#+begin_src\n")
(insert (with-current-buffer buf (buffer-substring-no-properties
(point-min) (point-max))))
(insert "\n#+end_src\n\n"))
(insert "* Prompt\n\n")
(insert (format "%S" prompt)))
(insert "LLM Request\n")
(insert (format " [[file:%s][View Request Details]]\n\n" temp-file)))))
(defun llm-apply-actions (actions)
"Apply ACTIONS."
(llm-apply-init-log-buffer)
(dolist (action actions)
(llm-apply-log-action action)
(cond
((llm-apply-buffer-change-p action)
(let ((buffer (get-buffer (llm-apply-buffer-change-buffer action))))
(if buffer
(with-current-buffer buffer
(let* ((diff (llm-apply-buffer-change-diff action))
(temp-diff-buffer (generate-new-buffer "*temp-diff*")))
(unwind-protect
(progn
(with-current-buffer temp-diff-buffer
(insert diff)
(setq-local default-directory
(or (file-name-directory
(buffer-file-name buffer))
default-directory))
(diff-mode)
(llm-apply--diff (buffer-substring-no-properties
(point-min) (point-max))))))))
(message "Buffer not found: %s" (llm-apply-buffer-change-buffer action)))))
((llm-apply-file-creation-p action)
(with-temp-file (llm-apply-file-creation-filename action)
(insert (llm-apply-file-creation-content action))))
(t (error "Unknown action type: %S" action)))))
;;; Main entry point
(defun llm-apply (instructions &optional buffers provider)
"Apply INSTRUCTIONS to BUFFERS using the llm PROVIDER.
If PROVIDER is not specified, use `llm-apply-provider'.
If BUFFERS is not provided, apply to current buffer."
(interactive "sInstructions:")
(let* ((provider (or provider llm-apply-provider))
(buffers (or buffers (list (current-buffer))))
(prompt (llm-apply--make-prompt instructions buffers)))
(llm-apply-init-log-buffer)
(llm-apply-log-request instructions buffers prompt)
(let* ((response (llm-chat provider prompt))
(actions (llm-apply--parse-response response)))
(if actions
(progn
(llm-apply-actions actions)
(message "Performed %d action(s)" (length actions)))
(message "No actions to carry out")))))
(defun llm-apply-my-setup ()
"Set up the LLM provider for llm-apply.
Assumes that the environment variable `ANTHROPIC_KEY' is set with
the appropriate API key."
(setq llm-apply-provider
(make-llm-claude
:key (exec-path-from-shell-getenv "ANTHROPIC_KEY")
:chat-model "claude-3-5-sonnet-20240620")))
(provide 'llm-apply)
;;; llm-apply.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment