dotfiles/emacs/.emacs.d/config/init-ai.el

1040 lines
49 KiB
EmacsLisp

;; -*- lexical-binding: t; -*-
(defvar ai-map (make-sparse-keymap))
(define-key leader-map "a" (cons "ai" ai-map))
(use-package gptel
:straight (:type git :host github :repo "karthink/gptel" :branch "master")
:commands (gptel-request)
:autoload (gptel--strip-mode-suffix)
:init
;; Annoyingly, gptel fails to require gptel-context, so we have to do it manually
(autoload 'gptel-context-add "gptel-context")
(autoload 'gptel-context-add-file "gptel-context")
(setq gptel-directives
`((default . ,(lambda ()
(let ((msg "You are a large language model living in Emacs and a helpful assistant. Respond concisely."))
(if (and gptel-use-tools gptel-tools)
(concat msg " Only use tools when strictly necessary.")
msg))))
(shell-command . "You are a command line helper. Generate shell commands that do what is requested, without any additional description or explanation. Reply in plain text with no Markdown or other syntax. Reply with the command only.")
(elisp-help . "You are an Emacs Lisp assistant. Generate Emacs Lisp code that does what is requested, without any additional description or explanation. Reply in plain text with no Markdown or syntax. Reply with the code only.")
(code-review . "You are a code reviewer. Provide feedback on the code snippet below. Highlight any issues, suggest improvements, and provide explanations for your suggestions. Respond in plain text with no Markdown or other syntax.")))
:config
;; Default to creating a new buffer when invoking gptel
(defun gptel-before-advice (name &optional _ initial interactivep)
(interactive
(let* ((backend (default-value 'gptel-backend))
(backend-name
(generate-new-buffer-name (format "*%s*" (gptel-backend-name backend)))))
(list (read-buffer "Create or choose gptel buffer: "
backend-name nil ; DEFAULT and REQUIRE-MATCH
(lambda (b) ; PREDICATE
(buffer-local-value 'gptel-mode
(get-buffer (or (car-safe b) b)))))
(condition-case nil
(gptel--get-api-key
(gptel-backend-key backend))
((error user-error)
(setq gptel-api-key
(read-passwd
(format "%s API key: " backend-name)))))
(and (use-region-p)
(buffer-substring (region-beginning)
(region-end)))
t))))
(advice-add 'gptel :before #'gptel-before-advice)
(defvar gptel-backend-openai (gptel-make-openai "ChatGPT"
:key #'gptel-api-key-from-auth-source
:stream t
:models gptel--openai-models))
(defvar gptel-backend-gemini (gptel-make-gemini "Gemini"
:stream t
:key (password-store-get "gemini-api-key")))
(defun ollama-models ()
(if (executable-find "ollama")
(->> (shell-command-to-string "ollama list")
(s-lines)
(cdr)
(mapcar (lambda (line)
(let ((parts (s-split-up-to " " line 1 t)))
(car parts))))
(-filter #'s-present?))
'("llama3.1:latest"
"mistral-nemo:latest"
"gemma2:2b"
"tinyllama:latest")))
(defvar gptel-backend-ollama (gptel-make-ollama "Ollama"
:stream t
:models (ollama-models)))
(defvar gptel-backend-anthropic (gptel-make-anthropic "Claude"
:stream t
:key (password-store-get "anthropic-api-key")
:header (lambda () (when-let* ((key (gptel--get-api-key)))
`(("x-api-key" . ,key)
("anthropic-version" . "2023-06-01")
("anthropic-beta" . "pdfs-2024-09-25")
("anthropic-beta" . "output-128k-2025-02-19")
("anthropic-beta" . "prompt-caching-2024-07-31"))))))
(defvar gptel-backend-anthropic-reasoning (gptel-make-anthropic "Claude-Reasoning"
:key (password-store-get "anthropic-api-key")
:stream t
:models '(claude-3-7-sonnet-20250219)
:header (lambda () (when-let* ((key (gptel--get-api-key)))
`(("x-api-key" . ,key)
("anthropic-version" . "2023-06-01")
("anthropic-beta" . "pdfs-2024-09-25")
("anthropic-beta" . "output-128k-2025-02-19")
("anthropic-beta" . "prompt-caching-2024-07-31"))))
:request-params '(:thinking (:type "enabled" :budget_tokens 4096)
:max_tokens 16384)))
;; Azure setup
(when (file-exists-p (expand-file-name "~/.azure/msal_token_cache.json"))
(defcustom gptel-azure-host nil
"The host for gptel to connect to Azure-hosted OpenAI models."
:type 'string)
(defcustom gptel-azure-deployment "gpt-4o"
"The Azure OpenAI deployment name to use for gptel."
:type 'string)
(defcustom gptel-azure-openai-version "2024-10-01-preview"
"The OpenAI API version in Azure to use for gptel.")
(defun get-azure-bearer-token ()
"Extract the access token for cognitiveservices.azure.com from the MSAL token cache JSON. Run `az login' to populate the cache."
(let* ((json-object-type 'hash-table)
(json-array-type 'list)
(json-key-type 'string)
(data (json-read-file (expand-file-name "~/.azure/msal_token_cache.json")))
(access-tokens (gethash "AccessToken" data)))
(cl-loop for key being the hash-keys of access-tokens
for token-data = (gethash key access-tokens)
if (and (string-match-p (rx "https://cognitiveservices.azure.com/.default")
(gethash "target" token-data))
(gethash "secret" token-data))
return (gethash "secret" token-data))))
(defun make-gptel-backend-azure ()
(gptel-make-azure "Azure OpenAI"
:protocol "https"
:stream t
:host gptel-azure-host
:endpoint (format "/openai/deployments/%s/chat/completions?api-version=%s"
gptel-azure-deployment gptel-azure-openai-version)
:key ""
:header `(("Authorization" . ,(format "Bearer %s" (get-azure-bearer-token))))
:models '("gpt-4o")))
(defvar gptel-backend-azure (make-gptel-backend-azure))
(defun gptel-reauth-azure ()
(interactive)
(setq gptel-backend-azure (make-gptel-backend-azure))))
(setq gptel-backend gptel-backend-anthropic
gptel-model 'claude-3-7-sonnet-20250219)
(defun gptel-select-backend (backend)
(interactive (list (cl-loop
for (name . backend) in gptel--known-backends
nconc (cl-loop for model in (gptel-backend-models backend)
collect (list (concat name ":" (format "%s" model)) backend model))
into models-alist finally return
(cdr (assoc (completing-read "Backend: " models-alist nil t)
models-alist)))))
(setq gptel-backend (car backend)
gptel-model (cadr backend)))
(defvar tavily-search-api-key (password-store-get "tavily-api-key")
"API key for accessing the Tavily Search API.")
(defun tavily-search-query (query &optional topic)
"Perform a web search using the Tavily Search API with the given QUERY."
(let* ((url-request-method "POST")
(url "https://api.tavily.com/search")
(url-request-extra-headers '(("Content-Type" . "application/json")))
(url-request-data (json-encode `(("query" . ,query)
("api_key" . ,tavily-search-api-key)
,@(when topic `("topic" . ,topic))))))
(with-current-buffer (url-retrieve-synchronously url)
(goto-char (point-min))
(when (re-search-forward "^$" nil 'move)
(buffer-substring-no-properties (point) (point-max))))))
(defun gptel-preview-code-execution (code mode &optional prompt)
"Preview CODE in a buffer using MODE before execution.
PROMPT is an optional custom confirmation message."
(let ((preview-buffer (get-buffer-create "*Code Preview*")))
(with-current-buffer preview-buffer
(erase-buffer)
(funcall mode)
(insert code)
(display-buffer preview-buffer)
(if (yes-or-no-p (or prompt "Execute this code? "))
(progn
(with-selected-window (get-buffer-window preview-buffer)
(quit-window))
t)
(progn
(with-selected-window (get-buffer-window preview-buffer)
(quit-window))
nil)))))
(defvar execute-shell-command-tool
(gptel-make-tool
:name "execute_shell_command"
:function (lambda (callback command)
(make-process
:name "gptel-shell"
:buffer (generate-new-buffer " *gptel-shell*")
:command (list shell-file-name shell-command-switch command)
:sentinel (lambda (proc _event)
(let ((result (with-current-buffer (process-buffer proc)
(buffer-string))))
(kill-buffer (process-buffer proc))
(funcall callback result)))))
:description "Execute a shell command and return the result"
:args (list '(:name "command"
:type "string"
:description "The shell command to execute"))
:confirm t
:async t
:category "shell"))
(defvar read-url-tool
(gptel-make-tool
:name "read_url"
:function (lambda (callback url)
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(forward-paragraph)
(let ((dom (libxml-parse-html-region (point) (point-max))))
(kill-buffer)
(with-temp-buffer
(shr-insert-document dom)
(funcall callback (buffer-substring-no-properties
(point-min)
(point-max))))))))
:description "Fetch and read the contents of a web page"
:args (list '(:name "url"
:type "string"
:description "The URL of the web page to read"))
:async t
:category "web"))
(defvar open-url-for-user-tool
(gptel-make-tool
:name "open_url_for_user"
:function (lambda (url)
(browse-url url))
:description "Open a URL for the user to view. Note: this will open the URL for the user. If you want to read the contents of the URL, use the 'read_url' tool."
:args (list '(:name "url"
:type "string"
:description "The URL to open"))
:category "web"))
(defvar web-search-tool
(gptel-make-tool
:name "web_search"
:function (lambda (callback query)
(let* ((url-request-method "POST")
(url "https://api.tavily.com/search")
(url-request-extra-headers '(("Content-Type" . "application/json")))
(url-request-data (json-encode `(("query" . ,query)
("api_key" . ,tavily-search-api-key)))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Perform a web search"
:args (list '(:name "query"
:type "string"
:description "The search query string"))
:async t
:category "web"))
(defvar news-search-tool
(gptel-make-tool
:name "news_search"
:function (lambda (callback query)
(let* ((url-request-method "POST")
(url "https://api.tavily.com/search")
(url-request-extra-headers '(("Content-Type" . "application/json")))
(url-request-data (json-encode `(("query" . ,query)
("api_key" . ,tavily-search-api-key)
("topic" . "news")))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Search for current events/news"
:args (list '(:name "query"
:type "string"
:description "The search query string"))
:async t
:category "web"))
(defvar emacs-symbol-help-tool
(gptel-make-tool
:name "emacs_symbol_help"
:function (lambda (symbol)
(let ((symbol (if (stringp symbol)
(intern symbol)
symbol)))
(if (fboundp symbol)
(documentation symbol)
(documentation-property symbol 'variable-documentation))))
:description "Retrieve the help documentation for an Emacs Lisp symbol running in the current Emacs process"
:args (list '(:name "symbol"
:type "string"
:description "The symbol to retrieve the help documentation for"))
:category "emacs"))
(defvar read-buffer-tool
(gptel-make-tool
:name "read_buffer"
:function (lambda (buffer)
(unless (buffer-live-p (get-buffer buffer))
(error "Error: buffer %s is not live" buffer))
(with-current-buffer buffer
(buffer-substring-no-properties (point-min) (point-max))))
:description "Return the contents of an Emacs buffer"
:args (list '(:name "buffer"
:type "string"
:description "The name of the buffer whose contents are to be retrieved"))
:category "emacs"))
(defvar evaluate-elisp-tool
(gptel-make-tool
:name "evaluate_elisp"
:function (lambda (elisp-code)
(condition-case err
(if (gptel-preview-code-execution elisp-code
'emacs-lisp-mode
"Execute this Emacs Lisp code? ")
(eval (read elisp-code))
(format "%s\n\nResult: code execution cancelled" elisp-code))
(error (format "Error: %s" (error-message-string err)))))
:description "Evaluate arbitrary Emacs Lisp code in the current Emacs process"
:args (list '(:name "elisp_code"
:type "string"
:description "The Emacs Lisp code to evaluate"))
:category "emacs"))
(defvar evaluate-python-tool
(gptel-make-tool
:name "evaluate_python"
:function (lambda (callback code)
(let ((temp-file (make-temp-file "gptel-python-" nil ".py")))
(if (gptel-preview-code-execution code 'python-mode)
(progn
(with-temp-file temp-file
(insert code))
(make-process
:name "gptel-python"
:buffer (generate-new-buffer " *gptel-python*")
:command (list "docker" "run" "--rm"
"-v" (concat temp-file ":/code.py")
"jupyter/scipy-notebook"
"python" "/code.py")
:sentinel (lambda (proc _event)
(let ((result (with-current-buffer (process-buffer proc)
(buffer-string))))
(delete-file temp-file)
(kill-buffer (process-buffer proc))
(funcall callback result)))))
(progn
(when (file-exists-p temp-file)
(delete-file temp-file))
(funcall callback
(format "%s\n\nResult: code execution cancelled" code))))))
:description "Evaluate Python code in a sandboxed Docker container with scientific computing libraries"
:args (list '(:name "code"
:type "string"
:description "The Python code to evaluate"))
:include t
:async t
:category "programming"))
(defvar append-to-buffer-tool
(gptel-make-tool
:name "append_to_buffer"
:function (lambda (buffer text)
(with-current-buffer (get-buffer-create buffer)
(save-excursion
(goto-char (point-max))
(insert text)))
(format "Appended text to buffer %s" buffer))
:description "Append text to the an Emacs buffer. If the buffer does not exist, it will be created."
:args (list '(:name "buffer"
:type "string"
:description "The name of the buffer to append text to.")
'(:name "text"
:type "string"
:description "The text to append to the buffer."))
:category "emacs"))
(defvar echo-message-tool
(gptel-make-tool
:name "echo_message"
:function (lambda (text)
(message "%s" text)
(format "Message sent: %s" text))
:description "Send a message to the *Messages* buffer"
:args (list '(:name "text"
:type "string"
:description "The text to send to the messages buffer"))
:category "emacs"))
(defvar read-file-into-buffer-tool
(gptel-make-tool
:name "read_file_into_buffer"
:function (lambda (file)
(with-current-buffer (find-file-noselect file)
(buffer-string)))
:description "Read a file into a buffer in the current Emacs process"
:args (list '(:name "file"
:type "string"
:description "The file to open"))
:category "emacs"))
(defvar list-directory-tool
(gptel-make-tool
:name "list_directory"
:function (lambda (directory)
(mapconcat #'identity
(directory-files directory)
"\n"))
:description "List the contents of a given directory"
:args (list '(:name "directory"
:type "string"
:description "The path to the directory to list"))
:category "filesystem"))
(defvar make-directory-tool
(gptel-make-tool
:name "make_directory"
:function (lambda (parent name)
(condition-case nil
(progn
(make-directory (expand-file-name name parent) t)
(format "Directory %s created/verified in %s" name parent))
(error (format "Error creating directory %s in %s" name parent))))
:description "Create a new directory with the given name in the specified parent directory"
:args (list '(:name "parent"
:type "string"
:description "The parent directory where the new directory should be created, e.g. /tmp")
'(:name "name"
:type "string"
:description "The name of the new directory to create, e.g. testdir"))
:category "filesystem"))
(defvar create-file-tool
(gptel-make-tool
:name "create_file"
:function (lambda (path filename content)
(let ((full-path (expand-file-name filename path)))
(with-temp-buffer
(insert content)
(write-file full-path))
(format "Created file %s in %s" filename path)))
:description "Create a new file with the specified content"
:args (list '(:name "path"
:type "string"
:description "The directory where to create the file")
'(:name "filename"
:type "string"
:description "The name of the file to create")
'(:name "content"
:type "string"
:description "The content to write to the file"))
:confirm t
:category "filesystem"))
(defvar edit-buffer-tool
(gptel-make-tool
:name "edit_buffer"
:function (lambda (buffer original replacement)
(if (get-buffer buffer)
(with-current-buffer buffer
(save-excursion
(cond
;; Special case: empty buffer and empty original string
((and (string= original "")
(= (buffer-size) 0))
(insert replacement)
(format "Successfully edited buffer %s" buffer))
;; Error case: empty original string but non-empty buffer
((string= original "")
(format "Error: Cannot replace empty string in non-empty buffer %s" buffer))
;; Normal case: non-empty original string
(t
(goto-char (point-min))
(let ((count 0)
(start-pos (point-min)))
(while (search-forward original nil t)
(setq count (1+ count))
(when (= count 1)
(setq start-pos (match-beginning 0))))
(cond
((= count 0)
(format "Error: Could not find the original text in buffer %s" buffer))
((> count 1)
(format "Error: Found %d instances of the text in buffer %s" count buffer))
(t
(goto-char start-pos)
(delete-region start-pos (+ start-pos (length original)))
(insert replacement)
(format "Successfully edited buffer %s" buffer))))))))
(format "Error: Buffer %s does not exist" buffer)))
:description "Replace specific text in a buffer with new text"
:args (list '(:name "buffer"
:type "string"
:description "Name of the buffer to edit")
'(:name "original"
:type "string"
:description "The original text to be replaced")
'(:name "replacement"
:type "string"
:description "The new text to replace the original with"))
:confirm t
:category "emacs"))
(defvar edit-file-tool
(gptel-make-tool
:name "edit_file"
:function (lambda (filepath original replacement)
(let ((file (expand-file-name filepath)))
(if (file-exists-p file)
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(if (search-forward original nil t)
(progn
(replace-match replacement t t)
(write-region (point-min) (point-max) file)
(format "Successfully edited file %s" filepath))
(format "Error: Could not find the original code in %s" filepath)))
(format "Error: File %s does not exist" filepath))))
:description "Replace specific code in a file with new code"
:args (list '(:name "filepath"
:type "string"
:description "Path to the file to edit")
'(:name "original"
:type "string"
:description "The original code to be replaced")
'(:name "replacement"
:type "string"
:description "The new code to replace the original with"))
:confirm t
:category "filesystem"))
(defvar write-buffer-tool
(gptel-make-tool
:name "write_buffer"
:function (lambda (buffer path)
(if (get-buffer buffer)
(with-current-buffer buffer
(write-region (point-min) (point-max) path nil t)
(format "Successfully wrote buffer %s to file %s" buffer path))
(format "Error: Buffer %s does not exist" buffer)))
:description "Write the contents of a buffer to a file"
:args (list '(:name "buffer"
:type "string"
:description "The name of the buffer to write to a file")
'(:name "path"
:type "string"
:description "The path to the file to write the buffer contents to"))
:category "emacs"))
(defvar grep-search-tool
(gptel-make-tool
:name "grep_search"
:function (lambda (query directory)
(unless (executable-find "rg")
(error "ripgrep (rg) is not installed"))
(let* ((dir (expand-file-name directory))
(cmd (format "rg --line-number --no-heading %s %s"
(shell-quote-argument query)
(shell-quote-argument dir))))
(shell-command-to-string cmd)))
:description "Search file contents using ripgrep (rg)"
:args (list '(:name "query"
:type "string"
:description "The search pattern to look for in files")
'(:name "directory"
:type "string"
:description "The directory to search in"))
:category "filesystem"))
(defvar glob-search-tool
(gptel-make-tool
:name "glob_search"
:function (lambda (pattern directory &optional max-depth)
(let* ((dir (expand-file-name directory))
(depth-arg (if max-depth
(format "-maxdepth %d" max-depth)
""))
(cmd (cond
((executable-find "fd")
(format "fd %s %s %s"
(if max-depth (format "--max-depth=%d" max-depth) "")
(shell-quote-argument pattern)
(shell-quote-argument dir)))
(t
(format "find %s %s -name %s"
(shell-quote-argument dir)
depth-arg
(shell-quote-argument pattern))))))
(shell-command-to-string cmd)))
:description "Search for filenames matching a pattern"
:args (list '(:name "pattern"
:type "string"
:description "The filename pattern to search for (e.g. '*.py')")
'(:name "directory"
:type "string"
:description "The directory to search in")
'(:name "max-depth"
:type "integer"
:description "Optional maximum depth to search"
:required nil))
:category "filesystem"))
(defvar add-file-to-context-tool
(gptel-make-tool
:name "add_file_to_context"
:function (lambda (filepath)
(if (file-exists-p (expand-file-name filepath))
(progn
(gptel-context-add-file (expand-file-name filepath))
(format "Added file %s to gptel context" filepath))
(format "Error: file %s does not exist" filepath)))
:description "Read a file and add add it to the conversation context"
:args (list '(:name "filepath"
:type "string"
:description "Path to the file to add to context. Supports relative paths and ~."))
:confirm t
:category "context"))
(defvar domainr-rapidapi-key (password-store-get "rapidapi-api-key")
"API key for accessing the Domainr API through RapidAPI (mashape-key).")
(defvar domainr-registrar "namecheap.com"
"The domain registrar to use for domain registration.")
(defvar search-domain-tool
(gptel-make-tool
:name "search_domain"
:function (lambda (callback query)
(unless domainr-rapidapi-key
(error "domainr-rapidapi-key is not set"))
(let* ((url (format "https://domainr.p.rapidapi.com/v2/search?mashape-key=%s&query=%s"
domainr-rapidapi-key
(url-encode-url query)))
(url-request-extra-headers
'(("Content-Type" . "application/json"))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Search for available domain names based on a query"
:args (list '(:name "query"
:type "string"
:description "The search query for domain names"))
:async t
:category "web"))
(defvar check-domain-status-tool
(gptel-make-tool
:name "check_domain_status"
:function (lambda (callback domain)
(unless domainr-rapidapi-key
(error "domainr-rapidapi-key is not set"))
(let* ((url (format "https://domainr.p.rapidapi.com/v2/status?mashape-key=%s&domain=%s"
domainr-rapidapi-key
(url-encode-url domain)))
(url-request-extra-headers
'(("Content-Type" . "application/json"))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Check the availability status of a specific domain name"
:args (list '(:name "domain"
:type "string"
:description "The domain name to check"))
:async t
:category "web"))
(defvar register-domain-tool
(gptel-make-tool
:name "register_domain"
:function (lambda (callback domain)
(unless domainr-rapidapi-key
(error "domainr-rapidapi-key is not set"))
(let* ((url (format "https://domainr.p.rapidapi.com/v2/register?mashape-key=%s&domain=%s&registrar=%s"
domainr-rapidapi-key
(url-encode-url domain)
domainr-registrar))
(url-automatic-caching nil))
(url-retrieve url
(lambda (status)
(goto-char (point-min))
(when-let ((redirect (plist-get status :redirect)))
(browse-url-default-browser (string-trim redirect))
(funcall callback
(format "Opened registration page for %s" domain)))))))
:description "Open the registration page for a domain name in the default browser"
:args (list '(:name "domain"
:type "string"
:description "The domain name to register"))
:async t
:category "web"))
(defun gptel-coding-assistant-system-prompt ()
(let ((cwd (file-truename default-directory))
(buffers (s-join "\n"
(->> (buffer-list)
(-filter
(lambda (buf)
(not (string-match-p "^[[:blank:]\\*].*" (buffer-name buf)))))
(mapcar
(lambda (buf)
(if (buffer-file-name buf)
(format "%s (visiting %s)" (buffer-name buf) (buffer-file-name buf))
(buffer-name buf))))))))
(format "You are a coding assistant living in the Emacs text editor. You have been provided with a number of tools for this purpose. Here are some tips on tool use:
- use the web_search and read_url tools as necessary for questions you don't already know the answer to, or to look up documentation
- to get the contents of a file by reading its buffer with the read_buffer tool, or if it doesn't have an open buffer read it into one using the read_file_into_buffer tool. Filenames can be relative to the current working directory or absolute
- to edit a file, read it into a buffer using read_file_into_buffer if it doesn't already have a buffer, then use the edit_buffer_text tool. Save your work with the write_buffer tool. Always output your desired changes in the chat for the user to review before calling the tool
- the execute_shell_command tool is your escape hatch. Use it to perform operations that the other tools don't permit, such as listing directory contents, using rg to search file contents, or any other operation that doesn't fit into a different tool. This can be particularly useful for exploring codebases to answer questions, e.g. by searching using rg or listing files with ls or find. Be mindful of the size of directory trees when using this tool so as not to run e.g. a find command that takes forever
- use the evaluate_elisp tool to control the Emacs process you are running in. It can be used to run compilation processes via (compile) or any other operation that can't be done using the other tools. Use this tool sparingly and with caution
The core workflow for making code changes is as follows:
1. Ensure the relevant file exists and is visited in a buffer. If it doesn't exist create it with a shell command. If it isn't visited in a buffer, read it into one using read_file_into_buffer
2. Suggest changes to the user
3. Verify the buffer contents with read_buffer, then make the changes using edit_buffer
4. Write the changes back to the file using write_buffer
For exploring codebases:
1. Don't use find to search for files, it is extremely slow
2. Instead, use fd, rg, or git ls-files when in a repository
3. Use 'rg -l \"search term\"' for finding files containing specific terms
4. For file patterns, use 'fd -e js' to find files by extension, or 'rg --files -g \"*.js\"'
5. Always narrow your search with patterns to reduce the result set size
For debugging issues:
1. Use read_buffer and execute_shell_command to trace through relevant code paths
2. Identify potential failure points and suggest targeted debugging approaches
3. When suggesting fixes, explain your reasoning and potential side effects
For refactoring tasks:
1. Propose changes incrementally rather than all at once
2. After each change, suggest tests or validations to ensure functionality is preserved
ALWAYS CHECK THE BUFFER CONTENTS BEFORE MAKING CHANGES. Otherwise you may call the edit_buffer tool with invalid arguments.
Remember to always let the user review changes before you make them, and provide a summary of any changes made afterwards.
Respond concisely.
current working directory: %s
open buffers: %s" cwd buffers)))
(defvar gptel-toolsets
(list :default `((gptel--system-message . ,(alist-get 'default gptel-directives))
(gptel-tools . ,(list
read-url-tool
open-url-for-user-tool
web-search-tool
emacs-symbol-help-tool
evaluate-python-tool)))
:empty `((gptel--system-message . ,(alist-get 'default gptel-directives))
(gptel-tools . nil))
:news `((gptel--system-message . ,(alist-get 'default gptel-directives))
(gptel-tools . ,(list
read-url-tool
open-url-for-user-tool
web-search-tool
news-search-tool)))
:business `((gptel--system-message . ,(alist-get 'default gptel-directives))
(gptel-tools . ,(list
read-url-tool
open-url-for-user-tool
web-search-tool
news-search-tool
search-domain-tool
check-domain-status-tool
register-domain-tool)))
:coding-assistant `((gptel-backend . ,gptel-backend-anthropic)
(gptel-model . claude-3-5-sonnet-20241022)
(gptel--system-message . gptel-coding-assistant-system-prompt)
(gptel-tools . ,(list
web-search-tool
read-url-tool
read-file-into-buffer-tool
read-buffer-tool
edit-buffer-tool
write-buffer-tool
execute-shell-command-tool
evaluate-elisp-tool))))
"Plist of names to alists of gptel variables")
(defun gptel-toolset (name)
(plist-get gptel-toolsets name))
(defun gptel-select-toolset-name ()
(let* ((toolsets (->> gptel-toolsets
(-partition 2)
(-map #'car)
(-map (lambda (name-kw)
(cons (s-chop-prefix ":"
(symbol-name name-kw))
name-kw)))))
(selected (completing-read "Toolset: " toolsets)))
(alist-get selected toolsets nil nil #'equal)))
(defun gptel-select-toolset (name)
(interactive (list (gptel-select-toolset-name)))
(let ((toolset (gptel-toolset name)))
(dolist (setting toolset)
(set (car setting) (cdr setting)))))
(defmacro gptel-with-toolset (name &rest body)
(declare (indent 1))
`(let* ((toolset (gptel-toolset ,name))
(symbols (mapcar #'car toolset))
(values (mapcar #'cdr toolset)))
(cl-progv symbols values
,@body)))
(gptel-select-toolset :coding-assistant)
(with-eval-after-load 'vterm
(defun vterm-ask-ai ()
(interactive)
(unless (derived-mode-p 'vterm-mode)
(user-error "Not in a vterm buffer!"))
(let* ((gptel--system-message (alist-get 'shell-command gptel-directives))
(gptel-tools nil)
(start (vterm-beginning-of-line))
(end (vterm-end-of-line))
(prompt (buffer-substring-no-properties start end))
(buf (current-buffer)))
(gptel-request prompt
:callback (lambda (response info)
(with-current-buffer buf
(vterm-delete-region start end)
(vterm-insert response))))))
(keymap-set vterm-mode-map "C-c RET" #'vterm-ask-ai))
(defun minibuffer-ask-ai ()
(interactive)
(let* ((gptel--system-message (alist-get 'shell-command gptel-directives))
(gptel-tools nil)
(prompt (minibuffer-contents))
(point-pos (point))
(buf (current-buffer)))
(gptel-request prompt
:callback (lambda (response info)
(with-current-buffer buf
(delete-region (minibuffer-prompt-end) (point-max))
(insert response)
(goto-char point-pos))))))
(keymap-set minibuffer-local-shell-command-map "C-c RET" #'minibuffer-ask-ai)
(defun eval-expression-ask-ai ()
(interactive)
(let* ((gptel--system-message (alist-get 'elisp-help gptel-directives))
(gptel-tools nil)
(prompt (minibuffer-contents))
(point-pos (point))
(buf (current-buffer)))
(gptel-request prompt
:callback (lambda (response info)
(with-current-buffer buf
(delete-region (minibuffer-prompt-end) (point-max))
(insert response)
(goto-char point-pos))))))
(defun ielm-ask-ai ()
(interactive)
(unless (derived-mode-p 'inferior-emacs-lisp-mode)
(user-error "Not in an ielm buffer!"))
(let* ((gptel--system-message (alist-get 'elisp-help gptel-directives))
(gptel-tools nil)
(input-start (comint-line-beginning-position))
(input-end (point-at-eol))
(prompt (buffer-substring-no-properties input-start input-end))
(buf (current-buffer)))
(gptel-request prompt
:callback (lambda (response info)
(with-current-buffer buf
(delete-region input-start input-end)
(insert response))))))
(keymap-set read-expression-map "C-c RET" #'eval-expression-ask-ai)
(with-eval-after-load 'ielm
(keymap-set inferior-emacs-lisp-mode-map "C-c RET" #'ielm-ask-ai))
:general
("C-c RET" #'gptel-send
"C-c C-<return>" #'gptel-menu)
(ai-map
"g" #'gptel
"o" #'gptel-org
"s" #'gptel-send
"m" #'gptel-menu
"b" #'gptel-chat-with-buffer
"f" #'gptel-format
"B" #'gptel-select-backend
"a" #'gptel-context-add
"F" #'gptel-context-add-file
"k" #'gptel-abort
"t" #'gptel-select-toolset))
(use-package gptel-quick
:straight (:type git :host github :repo "karthink/gptel-quick")
:commands gptel-quick
:config
(defun gptel-quick-around-advice (orig-fun &rest args)
(gptel-with-toolset :empty
(apply orig-fun args)))
(advice-add 'gptel-quick :around #'gptel-quick-around-advice)
:general
(embark-general-map
"?" #'gptel-quick)
(ai-map
"?" #'gptel-quick))
(use-package aimenu
:straight `(:local-repo ,(expand-file-name "packages/aimenu" user-emacs-directory) :type nil)
:defer t
:config
(setq aimenu-gptel-backend gptel-backend-openai
aimenu-gptel-model 'gpt-4o-mini)
:general
(ai-map "i" #'aimenu))
(defun gptel-commit-message ()
"Generate a commit message via gptel."
(interactive)
(unless git-commit-mode
(user-error "Not in a git commit buffer!"))
(let* ((diff-buf (magit-get-mode-buffer 'magit-diff-mode))
(diff (with-current-buffer diff-buf
(buffer-substring-no-properties
(point-min)
;; Skip the last line, which is just the [back] button
(save-excursion
(goto-char (point-max))
(forward-line -1)
(point)))))
(prompt (format "%s\n\nWrite a clear, concise commit message for this diff. The first line should succinctly summarize the changes made and should be no more than 50 characters. If additional context is needed, include it in an additional paragraph separate by a blank line from the first line. Do not use the word 'enhance' or talk about the user experience. Be specific. Reply in plain text with no Markdown or other syntax. Reply with the commit message only." diff)))
(message "Generating commit message...")
(gptel-with-toolset :empty
(gptel-request prompt
:stream t
:system "You are a professional software engineer."))))
(defun gptel-pr-review (pr)
"Review a pull request via gptel."
(interactive (list (forge-get-pullreq
(forge-read-pullreq "Review PR: "))))
(let ((diff (forge-pullreq-diff pr))
(name (generate-new-buffer-name (format "*PR review: %s*" (oref pr title))))
(prompt "Review the changes in this pull request. Highlight any issues, suggest improvements, and provide explanations for your suggestions. Keep your feedback specific and concise."))
(gptel name nil diff t)
(with-current-buffer name
(goto-char (point-max))
(newline)
(insert (gptel-prompt-prefix-string))
(insert (format " %s" prompt)))))
(with-eval-after-load 'git-commit
(keymap-set git-commit-mode-map "C-c RET" #'gptel-commit-message))
(defvar comfy-ui-path (expand-file-name "~/ComfyUI")
"Path to ComfyUI source repository.")
(defvar comfy-ui-command (list "pipenv" "run" "python" "main.py")
"Command to run ComfyUI server.")
(defvar-local comfy-ui--url nil
"URL for this buffer's ComfyUI process.")
(defun comfy-ui-process-filter (proc string)
(when-let ((match (s-match "To see the GUI go to: \\(.*\\)" string)))
(with-current-buffer (process-buffer proc)
(setq comfy-ui--url (nth 1 match))
(let ((browse-url-browser-function #'browse-url-default-browser))
(browse-url comfy-ui--url))))
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(let ((moving (= (point) (process-mark proc))))
(save-excursion
;; Insert the text, advancing the process marker.
(goto-char (process-mark proc))
(insert string)
(set-marker (process-mark proc) (point)))
(if moving (goto-char (process-mark proc)))))))
(defun comfy-ui ()
"Launch Comfy UI in a subprocess and opens the web UI."
(interactive)
(unless (file-exists-p (expand-file-name (f-join comfy-ui-path "main.py")))
(user-error "Could not find ComfyUI installation!"))
(if-let ((proc (get-process "comfy-ui")))
(with-current-buffer (process-buffer proc)
(browse-url comfy-ui--url))
(with-temp-buffer
(cd comfy-ui-path)
(make-process :name "comfy-ui"
:buffer "*ComfyUI*"
:command comfy-ui-command
:filter #'comfy-ui-process-filter))))
(defvar ollama-copilot-proxy-port 11435
"Port for the Ollama Copilot proxy server.")
(defvar ollama-copilot-model "codellama:code"
"Model for the Ollama Copilot proxy server.")
(defun ollama-copilot-ensure ()
"Start the Ollama Copilot proxy server if it's not already running."
(let ((proc-name "ollama-copilot"))
(unless (get-process proc-name)
(unless (executable-find "ollama-copilot")
(user-error "Could not find ollama-copilot executable!"))
(make-process :name proc-name
:buffer (format "*%s*" proc-name)
:command `("ollama-copilot"
"-proxy-port" ,(format ":%s" ollama-copilot-proxy-port)
"-model" ,ollama-copilot-model)))))
(defvar ollama-copilot--proxy-cache nil
"Internal variable to cache the old proxy value.")
(define-minor-mode ollama-copilot-mode
"Minor mode to use ollama-copilot as a local Copilot proxy."
:global t
(require 'copilot)
(if ollama-copilot-mode
(progn
(ollama-copilot-ensure)
(setq ollama-copilot--proxy-cache copilot-network-proxy)
(setq copilot-network-proxy `(:host "127.0.0.1"
:port ,ollama-copilot-proxy-port
:rejectUnauthorized :json-false))
(copilot-diagnose))
(setq copilot-network-proxy ollama-copilot--proxy-cache)
(copilot-diagnose)))
(provide 'init-ai)