From f4da3fc9e20a9931bddec4c16db2203fff2bae5b Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Wed, 15 May 2024 13:28:36 -0400 Subject: [PATCH] Add interface to the llm command-line tool --- emacs/.emacs.d/config/init-llm.el | 14 +++ emacs/.emacs.d/init.el | 1 + emacs/.emacs.d/packages/llm/llm.el | 168 +++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 emacs/.emacs.d/config/init-llm.el create mode 100644 emacs/.emacs.d/packages/llm/llm.el diff --git a/emacs/.emacs.d/config/init-llm.el b/emacs/.emacs.d/config/init-llm.el new file mode 100644 index 0000000..9c7522f --- /dev/null +++ b/emacs/.emacs.d/config/init-llm.el @@ -0,0 +1,14 @@ +;; -*- lexical-binding: t; -*- + +(use-package llm + :straight (:type built-in) + :ensure nil + :load-path "packages/llm" + :defer t + :commands (llm-call + llm-set-model + llm-prompt + llm-prompt-buffer + llm-prompt-region)) + +(provide 'init-llm) diff --git a/emacs/.emacs.d/init.el b/emacs/.emacs.d/init.el index eb761c8..23ba4e7 100644 --- a/emacs/.emacs.d/init.el +++ b/emacs/.emacs.d/init.el @@ -131,6 +131,7 @@ (require 'init-games) (require 'handwriting) (require 'init-navi) +(require 'init-llm) (when (string-equal system-type "darwin") (require 'init-mac)) diff --git a/emacs/.emacs.d/packages/llm/llm.el b/emacs/.emacs.d/packages/llm/llm.el new file mode 100644 index 0000000..39c593d --- /dev/null +++ b/emacs/.emacs.d/packages/llm/llm.el @@ -0,0 +1,168 @@ +;;; llm.el --- An Emacs interface to the LLM command-line tool -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Jeremy Isaac Dormitzer + +;; Author: Jeremy Isaac Dormitzer +;; Version: 0.1 +;; Package-Requires: ((emacs "24.3") (s "1.13") (markdown-mode "2.7")) +;; Keywords: tools + +;; 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 . + +;;; Commentary: + +;; This package provides an Emacs interface to the LLM command-line tool. + +;;; Code: +(require 's) +(require 'markdown-mode) + +(defcustom llm-executable "llm" + "Path to the llm executable." + :type 'string + :group 'llm) + +(defcustom llm-model nil + "The llm model to use." + :type 'string + :group 'llm) + +(defcustom llm-max-tokens 5000 + "The maximum number of tokens to generate." + :type 'integer + :group 'llm) + +(defun llm--ensure-executable () + "Ensure that the llm executable is available." + (unless (executable-find llm-executable) + (error + "llm executable not found: see https://llm.datasette.io/en/stable/index.html for installation instructions"))) + +(defun llm--process-filter (proc string) + (let* ((buffer (process-buffer proc)) + (window (get-buffer-window buffer)) + (string (replace-regexp-in-string "\r\n" "\n" string))) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (if (not (mark)) (push-mark)) + (exchange-point-and-mark) ;Use the mark to represent the cursor location + (dolist (char (append string nil)) + (cond ((char-equal char ?\r) + (move-beginning-of-line 1)) + ((char-equal char ?\n) + (move-end-of-line 1) (newline)) + (t + (if (/= (point) (point-max)) ;Overwrite character + (delete-char 1)) + (insert char)))) + (exchange-point-and-mark))) + (if window + (with-selected-window window + (goto-char (point-max)))))) + +(define-derived-mode llm-mode markdown-mode "llm" + "Major mode for LLM output.") + +(define-key llm-mode-map + (kbd "q") #'quit-window) + +(when (fboundp #'evil-define-key) + (evil-define-key 'normal llm-mode-map + (kbd "q") #'quit-window)) + +(defun llm--run-async (name buffer-name &rest llm-args) + "Run llm with LLM-ARGS asynchronously. + +The process is named NAME and runs in BUFFER-NAME." + (llm--ensure-executable) + (when-let ((existing-buffer (get-buffer buffer-name))) + (kill-buffer existing-buffer)) + (let ((proc (make-process :name name + :buffer buffer-name + :command (cons llm-executable llm-args) + :filter #'llm--process-filter))) + (with-current-buffer (process-buffer proc) + (llm-mode)) + (set-process-sentinel proc #'ignore))) + +;;;###autoload +(cl-defun llm-call (callback &rest llm-args) + "Call llm with LLM-ARGS and call CALLBACK with the result." + (when-let ((buf (get-buffer " *llm-call*"))) + (kill-buffer buf)) + (let ((proc (apply #'llm--run-async "llm-call" " *llm-call*" llm-args))) + (set-process-sentinel proc + (lambda (proc event) + (unless (string= event "finished\n") + (error "llm-call failed: %s" (s-trim event))) + (with-current-buffer (process-buffer proc) + (goto-char (point-min)) + (funcall callback (s-trim + (buffer-substring-no-properties + (point) + (point-max))))) + (kill-buffer (process-buffer proc)))))) + +;;;###autoload +(defun llm-set-model (model) + "Set the LLM model to MODEL." + (interactive (list (let* ((model-strings + (split-string (shell-command-to-string + (format "%s models" (executable-find llm-executable))) + "\n" t " ")) + (models (mapcar (lambda (s) + (cons s (cadr (s-match ".*?: \\(.*?\\) -" s)))) + model-strings)) + (selected (completing-read "Model: " models))) + (alist-get selected models nil nil #'equal)))) + (setq llm-model model)) + +(defun llm--prompt-args (query &rest extra-args) + "Return the arguments to prompt LLM with QUERY, appending EXTRA-ARGS." + (let* ((args (list "-o" "max_tokens" (number-to-string llm-max-tokens))) + (args (if llm-model + (append (list "--model" llm-model) args) + args)) + (args (append args extra-args))) + (append (list "prompt") args (list query)))) + +;;;###autoload +(defun llm-prompt (query) + "Prompt llm with the QUERY." + (interactive "sQuery: ") + (apply #'llm--run-async "llm-prompt" "*llm-prompt*" (llm--prompt-args query)) + (switch-to-buffer "*llm-prompt*")) + +;;;###autoload +(defun llm-prompt-buffer (query) + "Prompt llm with the contents of the current buffer and the QUERY." + (interactive "sQuery: ") + (let ((extra-args (list "-s" (buffer-substring-no-properties (point-min) (point-max))))) + (apply #'llm--run-async + "llm-prompt-buffer" + "*llm-prompt-buffer*" + (apply #'llm--prompt-args query extra-args)) + (switch-to-buffer "*llm-prompt-buffer*"))) + +(defun llm-prompt-region (query) + (interactive "sQuery: ") + (let ((extra-args (list "-s" (buffer-substring-no-properties (region-beginning) (region-end))))) + (apply #'llm--run-async + "llm-prompt-region" + "*llm-prompt-region*" + (apply #'llm--prompt-args query extra-args)) + (switch-to-buffer "*llm-prompt-region*"))) + +(provide 'llm) +;;; llm.el ends here