branch: externals/ellama commit c20394170bcaf4333d44b64d96a015b57113a6bb Merge: 6fb94007b9 6279b2f2a6 Author: Sergey Kostyaev <s-kosty...@users.noreply.github.com> Commit: GitHub <nore...@github.com>
Merge pull request #210 from s-kostyaev/improve-working-with-context Improve context management and add new commands --- NEWS.org | 4 ++ README.org | 23 +++++++ ellama.el | 190 +++++++++++++++++++++++++++++++++++++++++++++------ tests/test-ellama.el | 30 ++++++++ 4 files changed, 225 insertions(+), 22 deletions(-) diff --git a/NEWS.org b/NEWS.org index a24c1ebbcb..6d6904f524 100644 --- a/NEWS.org +++ b/NEWS.org @@ -1,3 +1,7 @@ +* Version 1.0.0 +- Added ~ellama-write~ command. +- Added ~ellama-proofread~ command. +- Added global context management, including functions to reset context. * Version 0.13.11 - Add function ~ellama-make-semantic-similar-p-with-context~ that return test function for checking if two provided texts are meaning diff --git a/README.org b/README.org index d58465f66c..8fa8a8ffc5 100644 --- a/README.org +++ b/README.org @@ -108,6 +108,13 @@ buffer and continue conversation. If called with universal argument (~C-u~) will start new session with llm model interactive selection. [[imgs/ellama-ask.gif]] +*** ellama-write + +This command allows you to generate text using an LLM. When called +interactively, it prompts for an instruction that is then used to +generate text based on the context. If a region is active, the +selected text is added to the context before generating the response. + *** ellama-chat-send-last-message Send last user message extracted from current ellama chat buffer. @@ -202,6 +209,10 @@ provided change using Ellama. Generate commit message based on diff. +*** ellama-proofread + +Proofread selected text. + *** ellama-improve-wording Enhance the wording in the currently selected region or buffer using Ellama. @@ -253,6 +264,10 @@ Add selected region to context. Add info node to context. +*** ellama-context-reset + +Clear global context. + *** ellama-chat-translation-enable Chat translation enable. @@ -280,6 +295,7 @@ Ellama, using the ~ellama-keymap-prefix~ prefix (not set by default): | Keymap | Function | Description | |--------+---------------------------------+------------------------------| +| "w" | ellama-write | Write | | "c c" | ellama-code-complete | Code complete | | "c a" | ellama-code-add | Code add | | "c e" | ellama-code-edit | Code edit | @@ -293,6 +309,7 @@ Ellama, using the ~ellama-keymap-prefix~ prefix (not set by default): | "s r" | ellama-session-rename | Session rename | | "s d" | ellama-session-remove | Session delete | | "s a" | ellama-session-switch | Session activate | +| "P" | ellama-proofread | Proofread | | "i w" | ellama-improve-wording | Improve wording | | "i g" | ellama-improve-grammar | Improve grammar and spelling | | "i c" | ellama-improve-conciseness | Improve conciseness | @@ -313,6 +330,7 @@ Ellama, using the ~ellama-keymap-prefix~ prefix (not set by default): | "x f" | ellama-context-add-file | Context add file | | "x s" | ellama-context-add-selection | Context add selection | | "x i" | ellama-context-add-info-node | Context add info node | +| "x r" | ellama-context-reset | Context reset | | "p s" | ellama-provider-select | Provider select | ** Configuration @@ -371,6 +389,11 @@ argument generated text string. - ~ellama-translate-italic~: Translate italic during markdown to org transformations. Enabled by default. - ~ellama-extraction-provider~: LLM provider for data extraction. +- ~ellama-text-display-limit~: Limit for text display in context elements. +- ~ellama-context-poshandler~: Position handler for displaying context buffer. + ~posframe-poshandler-frame-top-center~ will be used if not set. +- ~ellama-context-border-width~: Border width for the context buffer. +- ~ellama-context-element-padding-size~: Padding size for context elements. ** Acknowledgments diff --git a/ellama.el b/ellama.el index 3b6d95fca7..2ad36970f7 100644 --- a/ellama.el +++ b/ellama.el @@ -5,8 +5,8 @@ ;; Author: Sergey Kostyaev <sskosty...@gmail.com> ;; URL: http://github.com/s-kostyaev/ellama ;; Keywords: help local tools -;; Package-Requires: ((emacs "28.1") (llm "0.22.0") (spinner "1.7.4") (transient "0.7") (compat "29.1")) -;; Version: 0.13.11 +;; Package-Requires: ((emacs "28.1") (llm "0.22.0") (spinner "1.7.4") (transient "0.7") (compat "29.1") (posframe "1.4.0")) +;; Version: 1.0.0 ;; SPDX-License-Identifier: GPL-3.0-or-later ;; Created: 8th Oct 2023 @@ -41,6 +41,7 @@ (require 'spinner) (require 'transient) (require 'compat) +(require 'posframe) (eval-when-compile (require 'rx)) (defgroup ellama nil @@ -133,6 +134,7 @@ (define-key map (kbd "i w") 'ellama-improve-wording) (define-key map (kbd "i g") 'ellama-improve-grammar) (define-key map (kbd "i c") 'ellama-improve-conciseness) + (define-key map (kbd "P") 'ellama-proofread) ;; make (define-key map (kbd "m l") 'ellama-make-list) (define-key map (kbd "m t") 'ellama-make-table) @@ -143,6 +145,7 @@ (define-key map (kbd "a l") 'ellama-ask-line) (define-key map (kbd "a s") 'ellama-ask-selection) ;; text + (define-key map (kbd "w") 'ellama-write) (define-key map (kbd "t t") 'ellama-translate) (define-key map (kbd "t b") 'ellama-translate-buffer) (define-key map (kbd "t c") 'ellama-complete) @@ -155,6 +158,7 @@ (define-key map (kbd "x f") 'ellama-context-add-file) (define-key map (kbd "x s") 'ellama-context-add-selection) (define-key map (kbd "x i") 'ellama-context-add-info-node) + (define-key map (kbd "x r") 'ellama-context-reset) ;; provider (define-key map (kbd "p s") 'ellama-provider-select) map) @@ -256,6 +260,16 @@ You are a summarizer. You write a summary of the input **IN THE SAME LANGUAGE AS :group 'ellama :type 'string) +(defcustom ellama-write-prompt-template "<SYSTEM> +Write text, based on provided context and instruction. Do not add any explanation or acknowledgement, just follow instruction. +</SYSTEM> +<INSTRUCTION> +%s +</INSTRUCTION>" + "Prompt template for `ellama-write'." + :group 'ellama + :type 'string) + (defcustom ellama-improve-grammar-prompt-template "improve grammar and spelling" "Prompt template for `ellama-improve-grammar'." :group 'ellama @@ -266,6 +280,11 @@ You are a summarizer. You write a summary of the input **IN THE SAME LANGUAGE AS :group 'ellama :type 'string) +(defcustom ellama-proofread-prompt-template "proofread" + "Prompt template for `ellama-proofread'." + :group 'ellama + :type 'string) + (defcustom ellama-improve-conciseness-prompt-template "make it as simple and concise as possible" "Prompt template for `ellama-improve-conciseness'." :group 'ellama @@ -738,8 +757,6 @@ EXTRA contains additional information." "Generate name for ellama ACTION by PROVIDER according to PROMPT." (ellama--fix-file-name (funcall ellama-naming-scheme provider action prompt))) -(defvar ellama--new-session-context nil) - (defun ellama-get-nick-prefix-for-mode () "Return preferred header prefix char based om the current mode. Defaults to #, but supports `org-mode'. Depends on `ellama-major-mode'." @@ -779,15 +796,14 @@ If EPHEMERAL non nil new session will not be associated with any file." ellama--current-session))) (session (make-ellama-session :id id :provider provider :file file-name - :context (if previous-session - (ellama-session-context previous-session) - ellama--new-session-context))) + :context (or (when previous-session + (ellama-session-context previous-session)) + ellama--global-context))) (buffer (if file-name (progn (make-directory ellama-sessions-directory t) (find-file-noselect file-name)) (get-buffer-create id)))) - (setq ellama--new-session-context nil) (setq ellama--current-session-id id) (puthash id buffer ellama--active-sessions) (with-current-buffer buffer @@ -915,9 +931,8 @@ If EPHEMERAL non nil new session will not be associated with any file." :provider (ellama-session-provider session) :file (ellama-session-file session) :prompt (ellama-session-prompt session) - :context ellama--new-session-context + :context ellama--global-context :extra extra))) - (setq ellama--new-session-context nil) (setq ellama--current-session-id (ellama-session-id ellama--current-session)) (puthash (ellama-session-id ellama--current-session) buffer ellama--active-sessions) @@ -1000,6 +1015,20 @@ If EPHEMERAL non nil new session will not be associated with any file." (remhash id ellama--active-sessions) (puthash new-id buffer ellama--active-sessions))) +(defvar ellama--global-context nil + "Global context.") + +(defvar ellama--context-buffer " *ellama-context*") + +;;;###autoload +(defun ellama-context-reset () + "Clear global context." + (interactive) + (setq ellama--global-context nil) + (with-current-buffer ellama--context-buffer + (erase-buffer)) + (posframe-hide ellama--context-buffer)) + ;; Context elements (defclass ellama-context-element () () @@ -1011,16 +1040,51 @@ If EPHEMERAL non nil new session will not be associated with any file." (cl-defgeneric ellama-context-element-extract (element) "Extract the content of the context ELEMENT.") +(cl-defgeneric ellama-context-element-display (element) + "Display the context ELEMENT.") + (cl-defgeneric ellama-context-element-format (element mode) "Format the context ELEMENT for the major MODE.") +(defcustom ellama-context-poshandler 'posframe-poshandler-frame-top-center + "Position handler for displaying context buffer." + :group 'ellama + :type 'function) + +(defcustom ellama-context-border-width 1 + "Border width for the context buffer." + :group 'ellama + :type 'integer) + +(defcustom ellama-context-element-padding-size 20 + "Padding size for context elements." + :group 'ellama + :type 'integer) + (cl-defmethod ellama-context-element-add ((element ellama-context-element)) "Add the ELEMENT to the Ellama context." (if-let* ((id ellama--current-session-id) (session (with-current-buffer (ellama-get-session-buffer id) ellama--current-session))) - (push element (ellama-session-context session)) - (push element ellama--new-session-context))) + (push element (ellama-session-context session))) + (push element ellama--global-context) + (get-buffer-create ellama--context-buffer t) + (with-current-buffer ellama--context-buffer + (erase-buffer) + (insert (format + "context: %s" + (string-join + (mapcar + (lambda (el) + (string-pad + (ellama-context-element-display el) ellama-context-element-padding-size)) + ellama--global-context) + " ")))) + (posframe-show + ellama--context-buffer + :poshandler ellama-context-poshandler + :internal-border-width ellama-context-border-width)) + ;; Buffer context element @@ -1035,6 +1099,12 @@ If EPHEMERAL non nil new session will not be associated with any file." (with-current-buffer name (buffer-substring-no-properties (point-min) (point-max))))) +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-buffer)) + "Display the context ELEMENT." + (with-slots (name) element + name)) + (cl-defmethod ellama-context-element-format ((element ellama-context-element-buffer) (mode (eql 'markdown-mode))) "Format the context ELEMENT for the major MODE." @@ -1063,6 +1133,12 @@ If EPHEMERAL non nil new session will not be associated with any file." (insert-file-contents name) (buffer-substring-no-properties (point-min) (point-max))))) +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-file)) + "Display the context ELEMENT." + (with-slots (name) element + (file-name-nondirectory name))) + (cl-defmethod ellama-context-element-format ((element ellama-context-element-file) (mode (eql 'markdown-mode))) "Format the context ELEMENT for the major MODE." @@ -1091,6 +1167,12 @@ If EPHEMERAL non nil new session will not be associated with any file." (info name (current-buffer)) (buffer-substring-no-properties (point-min) (point-max))))) +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-info-node)) + "Display the context ELEMENT." + (with-slots (name) element + (format "(info \"%s\")" name))) + (cl-defmethod ellama-context-element-format ((element ellama-context-element-info-node) (mode (eql 'markdown-mode))) "Format the context ELEMENT for the major MODE." @@ -1122,6 +1204,21 @@ If EPHEMERAL non nil new session will not be associated with any file." "Extract the content of the context ELEMENT." (oref element content)) +(defcustom ellama-text-display-limit 15 + "Limit for text display in context elements." + :group 'ellama + :type 'integer) + +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-text)) + "Display the context ELEMENT." + (with-slots (content) element + (format "\"%s\"" (concat + (string-limit + content + ellama-text-display-limit) + "...")))) + (cl-defmethod ellama-context-element-format ((element ellama-context-element-text) (mode (eql 'markdown-mode))) "Format the context ELEMENT for the major MODE." @@ -1147,6 +1244,18 @@ If EPHEMERAL non nil new session will not be associated with any file." "Extract the content of the context ELEMENT." (oref element content)) +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-webpage-quote)) + "Display the context ELEMENT." + (with-slots (name) element + name)) + +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-webpage-quote)) + "Display the context ELEMENT." + (with-slots (name) element + name)) + (defun ellama--quote-buffer (quote) "Return buffer name for QUOTE." (let* ((buf-name (concat (make-temp-name "*ellama-quote-") "*")) @@ -1209,6 +1318,12 @@ If EPHEMERAL non nil new session will not be associated with any file." "Extract the content of the context ELEMENT." (oref element content)) +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-info-node-quote)) + "Display the context ELEMENT." + (with-slots (name) element + (format "(info \"%s\")" name))) + (cl-defmethod ellama-context-element-format ((element ellama-context-element-info-node-quote) (mode (eql 'markdown-mode))) "Format the context ELEMENT for the major MODE." @@ -1255,6 +1370,12 @@ If EPHEMERAL non nil new session will not be associated with any file." "Extract the content of the context ELEMENT." (oref element content)) +(cl-defmethod ellama-context-element-display + ((element ellama-context-element-file-quote)) + "Display the context ELEMENT." + (with-slots (path) element + (file-name-nondirectory path))) + (cl-defmethod ellama-context-element-format ((element ellama-context-element-file-quote) (mode (eql 'markdown-mode))) "Format the context ELEMENT for the major MODE." @@ -1415,15 +1536,18 @@ If EPHEMERAL non nil new session will not be associated with any file." (defun ellama--prompt-with-context (prompt) "Add context to PROMPT for sending to llm." - (if-let* ((session ellama--current-session) - (context (ellama-session-context session))) - (concat (string-join - (cons "Context:" - (mapcar #'ellama-context-element-extract context)) - "\n") - "\n\n" - prompt) - prompt)) + (let* ((session ellama--current-session) + (context (or (when session + (ellama-session-context session)) + ellama--global-context))) + (if context + (concat (string-join + (cons "Context:" + (mapcar #'ellama-context-element-extract context)) + "\n") + "\n\n" + prompt) + prompt))) (defun ellama-chat-buffer-p (buffer) "Return non-nil if BUFFER is an ellama chat buffer." @@ -2080,6 +2204,17 @@ ARGS contains keys for fine control. (ellama-context-add-buffer (current-buffer))) (ellama-chat ellama-code-review-prompt-template nil :provider ellama-coding-provider)) +;;;###autoload +(defun ellama-write (instruction) + "Write text based on context and INSTRUCTION at point." + (interactive "sInstruction: ") + (when (region-active-p) + (ellama-context-add-selection)) + (ellama-stream (format ellama-write-prompt-template instruction) + :point (point) + :filter (when (derived-mode-p 'org-mode) + #'ellama--translate-markdown-to-org-filter))) + ;;;###autoload (defun ellama-change (change &optional edit-template) "Change selected text or text in current buffer according to provided CHANGE. @@ -2118,6 +2253,14 @@ prefix (\\[universal-argument]), prompt the user to amend the template." (interactive "p") (ellama-change ellama-improve-wording-prompt-template edit-template)) +;;;###autoload +(defun ellama-proofread (&optional edit-template) + "Proofread the currently selected region or buffer. +When the value of EDIT-TEMPLATE is 4, or with one `universal-argument' as +prefix (\\[universal-argument]), prompt the user to amend the template." + (interactive "p") + (ellama-change ellama-proofread-prompt-template edit-template)) + ;;;###autoload (defun ellama-improve-conciseness (&optional edit-template) "Make the text of the currently selected region or buffer concise and simple. @@ -2434,13 +2577,16 @@ Call CALLBACK on result list of strings. ARGS contains keys for fine control. ("b" "Add Buffer" ellama-context-add-buffer) ("f" "Add File" ellama-context-add-file) ("s" "Add Selection" ellama-context-add-selection) - ("i" "Add Info Node" ellama-context-add-info-node)] + ("i" "Add Info Node" ellama-context-add-info-node) + ("r" "Context reset" ellama-context-reset)] ["Quit" ("q" "Quit" transient-quit-one)]]) (transient-define-prefix ellama-transient-main-menu () "Main Menu." [["Main" ("c" "Chat" ellama-chat) + ("w" "Write" ellama-write) + ("P" "Proofread" ellama-proofread) ("a" "Ask Commands" ellama-transient-ask-menu) ("C" "Code Commands" ellama-transient-code-menu)]] [["Text" diff --git a/tests/test-ellama.el b/tests/test-ellama.el index d4d4c878f5..0944050965 100644 --- a/tests/test-ellama.el +++ b/tests/test-ellama.el @@ -205,6 +205,36 @@ (let ((element (ellama-context-element-file-quote :content "123"))) (should (equal "123" (ellama-context-element-extract element))))) +(ert-deftest test-ellama-context-element-display-buffer () + (with-temp-buffer + (let ((element (ellama-context-element-buffer :name (buffer-name)))) + (should (equal (buffer-name) (ellama-context-element-display element)))))) + +(ert-deftest test-ellama-context-element-display-file () + (let* ((filename (expand-file-name "LICENSE" (locate-dominating-file "." ".git"))) + (element (ellama-context-element-file :name filename))) + (should (equal (file-name-nondirectory filename) (ellama-context-element-display element))))) + +(ert-deftest test-ellama-context-element-display-info-node () + (let ((element (ellama-context-element-info-node :name "(dir)Top"))) + (should (equal "(info \"(dir)Top\")" (ellama-context-element-display element))))) + +(ert-deftest test-ellama-context-element-display-text () + (let ((element (ellama-context-element-text :content "123"))) + (should (equal "\"123...\"" (ellama-context-element-display element))))) + +(ert-deftest test-ellama-context-element-display-webpage-quote () + (let ((element (ellama-context-element-webpage-quote :name "Example" :url "http://example.com" :content "123"))) + (should (equal "Example" (ellama-context-element-display element))))) + +(ert-deftest test-ellama-context-element-display-info-node-quote () + (let ((element (ellama-context-element-info-node-quote :name "Example" :content "123"))) + (should (equal "(info \"Example\")" (ellama-context-element-display element))))) + +(ert-deftest test-ellama-context-element-display-file-quote () + (let ((element (ellama-context-element-file-quote :path "/path/to/file" :content "123"))) + (should (equal "file" (ellama-context-element-display element))))) + (ert-deftest test-ellama-md-to-org-code-simple () (let ((result (ellama--translate-markdown-to-org-filter "Here is your TikZ code for a blue rectangle: ```tex