branch: externals/ellama commit 6e5372b015c63abaf18df9a9b53c9efe38b77bda Author: Sergey Kostyaev <sskosty...@gmail.com> Commit: Sergey Kostyaev <sskosty...@gmail.com>
Add ephemeral context functionality Implemented ephemeral context elements that are cleared after a single LLM request. Added corresponding functions and transient suffixes to manage ephemeral context elements. Updated existing functions to handle both global and ephemeral contexts. Fix #309 --- ellama-context.el | 169 ++++++++++++++++++++++++++++++++++++---------------- ellama-transient.el | 52 ++++++++++++++-- 2 files changed, 163 insertions(+), 58 deletions(-) diff --git a/ellama-context.el b/ellama-context.el index 76dc55f318..ff0e82ff24 100644 --- a/ellama-context.el +++ b/ellama-context.el @@ -62,6 +62,9 @@ (defvar ellama-context-global nil "Global context.") +(defvar ellama-context-ephemeral nil + "Ephemeral context elements for a single LLM request.") + (defvar ellama--context-buffer " *ellama-context*") (defvar ellama-context-buffer "*ellama-context*") @@ -71,6 +74,7 @@ "Clear global context." (interactive) (setq ellama-context-global nil) + (setq ellama-context-ephemeral nil) (with-current-buffer ellama--context-buffer (erase-buffer)) (ellama-context-update-show)) @@ -80,7 +84,11 @@ (setq ellama-context-global (cl-remove-if (lambda (el) (string= name (ellama-context-element-display el))) - ellama-context-global))) + ellama-context-global)) + (setq ellama-context-ephemeral + (cl-remove-if (lambda (el) + (string= name (ellama-context-element-display el))) + ellama-context-ephemeral))) ;;;###autoload (defun ellama-context-element-remove-by-name () @@ -93,7 +101,8 @@ the context." (ellama-context--element-remove-by-name (completing-read "Remove context element: " - (seq-uniq (mapcar #'ellama-context-element-display ellama-context-global)))) + (seq-uniq (mapcar #'ellama-context-element-display (append ellama-context-global + ellama-context-ephemeral))))) (ellama-context-update-show)) (defun ellama-context-update-show () @@ -102,19 +111,22 @@ the context." (declare-function posframe-hide "ext:posframe") (with-current-buffer (get-buffer-create ellama--context-buffer) (erase-buffer) - (if ellama-context-global + (if (or ellama-context-global + ellama-context-ephemeral) (insert (format " ellama ctx: %s" (string-join (mapcar (lambda (el) (ellama-context-element-display el)) - ellama-context-global) + (append ellama-context-global + ellama-context-ephemeral)) " "))) (insert " ellama ctx"))) (when ellama-context-posframe-enabled (require 'posframe) - (if ellama-context-global + (if (or ellama-context-global + ellama-context-ephemeral) (posframe-show ellama--context-buffer :poshandler ellama-context-poshandler @@ -159,7 +171,8 @@ the context." (when (listp header-line-format) (if (and ellama-context-header-line-mode (or ellama-context-line-always-visible - ellama-context-global)) + ellama-context-global + ellama-context-ephemeral)) (add-to-list 'header-line-format '(:eval (ellama-context-line)) t) (setq header-line-format (delete '(:eval (ellama-context-line)) header-line-format))))) @@ -183,7 +196,8 @@ the context." "Update and display context information in the mode line." (if (and ellama-context-mode-line-mode (or ellama-context-line-always-visible - ellama-context-global)) + ellama-context-global + ellama-context-ephemeral)) (add-to-list 'mode-line-format '(:eval (ellama-context-line)) t) (setq mode-line-format (delete '(:eval (ellama-context-line)) mode-line-format)))) @@ -219,7 +233,8 @@ the context." (read-only-mode +1) (ellama-context-mode) (erase-buffer) - (dolist (el ellama-context-global) + (dolist (el (append ellama-context-global + ellama-context-ephemeral)) (insert (ellama-context-element-display el)) (put-text-property (pos-bol) (pos-eol) 'context-element el) (insert "\n")) @@ -277,7 +292,9 @@ the context." (defun ellama-context-remove-element (element) "Remove context ELEMENT from global context." (setf ellama-context-global - (cl-remove element ellama-context-global :test #'equal-including-properties))) + (cl-remove element ellama-context-global :test #'equal-including-properties)) + (setf ellama-context-ephemeral + (cl-remove element ellama-context-ephemeral :test #'equal-including-properties))) ;;;###autoload (defun ellama-context-preview-element-at-point () @@ -303,6 +320,9 @@ the context." (cl-defgeneric ellama-context-element-add (element) "Add the ELEMENT to the Ellama context.") +(cl-defgeneric ellama-context-ephemeral-element-add (element) + "Add the ephemeral ELEMENT to the Ellama context.") + (cl-defgeneric ellama-context-element-extract (element) "Extract the content of the context ELEMENT.") @@ -321,6 +341,15 @@ the context." (get-buffer-create ellama--context-buffer t) (ellama-context-update-show)) +(cl-defmethod ellama-context-ephemeral-element-add ((element ellama-context-element)) + "Add the ephemeral ELEMENT to the Ellama context." + (setf ellama-context-ephemeral (nreverse ellama-context-ephemeral)) + (cl-pushnew element ellama-context-ephemeral + :test #'equal-including-properties) + (setf ellama-context-ephemeral (nreverse ellama-context-ephemeral)) + (get-buffer-create ellama--context-buffer t) + (ellama-context-update-show)) + ;; Buffer context element (defclass ellama-context-element-buffer (ellama-context-element) @@ -683,22 +712,29 @@ the context." ;;;###autoload -(defun ellama-context-add-file () - "Add file to context." +(defun ellama-context-add-file (&optional ephemeral) + "Add file to context. +For one request only if EPHEMERAL." (interactive) (let* ((file-name (read-file-name "Select file: " nil nil t)) (element (ellama-context-element-file :name file-name))) - (ellama-context-element-add element))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))) -(defun ellama-context-add-file-quote-noninteractive (path content) - "Add file with PATH quote CONTENT to context." +(defun ellama-context-add-file-quote-noninteractive (path content &optional ephemeral) + "Add file with PATH quote CONTENT to context. +For one request only if EPHEMERAL." (let ((element (ellama-context-element-file-quote :path path :content content))) - (ellama-context-element-add element))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))) ;;;###autoload -(defun ellama-context-add-file-quote () - "Add file quote to context interactively." +(defun ellama-context-add-file-quote (&optional ephemeral) + "Add file quote to context interactively. +For one request only if EPHEMERAL." (interactive) (let ((path (buffer-file-name (current-buffer))) (content (if (region-active-p) @@ -710,30 +746,37 @@ the context." (point-max))))) (if (not path) (warn "should be called from buffer associated with file") - (ellama-context-add-file-quote-noninteractive path content)))) + (ellama-context-add-file-quote-noninteractive path content ephemeral)))) ;;;###autoload -(defun ellama-context-add-buffer (buf) - "Add BUF to context." +(defun ellama-context-add-buffer (buf &optional ephemeral) + "Add BUF to context. +For one request only if EPHEMERAL." (interactive "bSelect buffer: ") (let* ((buffer-name (if (stringp buf) buf (buffer-name buf))) (element (ellama-context-element-buffer :name buffer-name))) - (ellama-context-element-add element))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))) ;;;###autoload -(defun ellama-context-add-directory (dir) - "Add all files in DIR to the context." +(defun ellama-context-add-directory (dir &optional ephemeral) + "Add all files in DIR to the context. +For one request only if EPHEMERAL." (interactive "DSelect directory: ") (dolist (file-name (directory-files dir t "^[^\.].*")) (unless (file-directory-p file-name) (let ((element (ellama-context-element-file :name file-name))) - (ellama-context-element-add element))))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))))) ;;;###autoload -(defun ellama-context-add-selection () - "Add active region to context." +(defun ellama-context-add-selection (&optional ephemeral) + "Add active region to context. +For one request only if EPHEMERAL." (interactive) (if (region-active-p) (let* ((data (buffer-substring-no-properties (region-beginning) (region-end))) @@ -746,32 +789,44 @@ the context." (ellama-context-element-file-quote :path file-name :content content) (ellama-context-element-buffer-quote :name buffer-name :content content)))) - (ellama-context-element-add element)) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element))) (warn "No active region"))) -(defun ellama-context-add-text (text) - "Add TEXT to context." +(defun ellama-context-add-text (text &optional ephemeral) + "Add TEXT to context. +For one request only if EPHEMERAL." (let ((element (ellama-context-element-text :content text))) - (ellama-context-element-add element))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))) (declare-function Info-copy-current-node-name "info") ;;;###autoload -(defun ellama-context-add-info-node (node) - "Add info NODE to context." +(defun ellama-context-add-info-node (node &optional ephemeral) + "Add info NODE to context. +For one request only if EPHEMERAL." (interactive (list (Info-copy-current-node-name))) (let ((element (ellama-context-element-info-node :name node))) - (ellama-context-element-add element))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))) -(defun ellama-context-add-info-node-quote-noninteractive (name content) - "Add info node with NAME quote CONTENT to context." +(defun ellama-context-add-info-node-quote-noninteractive (name content &optional ephemeral) + "Add info node with NAME quote CONTENT to context. +For one request only if EPHEMERAL." (let ((element (ellama-context-element-info-node-quote :name name :content content))) - (ellama-context-element-add element))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))) ;;;###autoload -(defun ellama-context-add-info-node-quote () - "Add info node quote to context interactively." +(defun ellama-context-add-info-node-quote (&optional ephemeral) + "Add info node quote to context interactively. +For one request only if EPHEMERAL." (interactive) (let ((name (Info-copy-current-node-name)) (content (if (region-active-p) @@ -783,17 +838,21 @@ the context." (point-max))))) (if (not name) (warn "should be called from `info' buffer") - (ellama-context-add-info-node-quote-noninteractive name content)))) + (ellama-context-add-info-node-quote-noninteractive name content ephemeral)))) -(defun ellama-context-add-webpage-quote-noninteractive (name url content) - "Add webpage with NAME and URL quote CONTENT to context." +(defun ellama-context-add-webpage-quote-noninteractive (name url content &optional ephemeral) + "Add webpage with NAME and URL quote CONTENT to context. +For one request only if EPHEMERAL." (let ((element (ellama-context-element-webpage-quote :name name :url url :content content))) - (ellama-context-element-add element))) + (if ephemeral + (ellama-context-ephemeral-element-add element) + (ellama-context-element-add element)))) ;;;###autoload -(defun ellama-context-add-webpage-quote-eww () - "Add webpage quote to context interactively from `eww'." +(defun ellama-context-add-webpage-quote-eww (&optional ephemeral) + "Add webpage quote to context interactively from `eww'. +For one request only if EPHEMERAL." (interactive) (defvar eww-data) (declare-function eww-current-url "eww") @@ -807,14 +866,15 @@ the context." (buffer-substring-no-properties (point-min) (point-max))))) - (ellama-context-add-webpage-quote-noninteractive name url content)) + (ellama-context-add-webpage-quote-noninteractive name url content ephemeral)) (warn "Should be called from `eww'."))) ;;;###autoload (defun ellama-context-format (_) "Format context for chat buffer." (let ((mode (if (derived-mode-p 'org-mode) 'org-mode 'markdown-mode))) - (if-let* ((context ellama-context-global)) + (if-let* ((context (append ellama-context-global + ellama-context-ephemeral))) (concat (string-join (cons "Context:" (mapcar (lambda (elt) @@ -827,14 +887,17 @@ the context." ;;;###autoload (defun ellama-context-prompt-with-context (prompt) "Add context to PROMPT for sending to llm." - (let* ((context ellama-context-global)) + (let* ((context (append ellama-context-global + ellama-context-ephemeral))) (if context - (concat (string-join - (cons "Context:" - (mapcar #'ellama-context-element-extract context)) - "\n") - "\n\n" - prompt) + (prog1 + (concat (string-join + (cons "Context:" + (mapcar #'ellama-context-element-extract context)) + "\n") + "\n\n" + prompt) + (setq ellama-context-ephemeral nil)) prompt))) (provide 'ellama-context) diff --git a/ellama-transient.el b/ellama-transient.el index c2431297ac..36016fec50 100644 --- a/ellama-transient.el +++ b/ellama-transient.el @@ -31,6 +31,7 @@ ;;; Code: (require 'ellama) (require 'transient) +(require 'ellama-context) (defcustom ellama-transient-system-show-limit 45 "Maximum length of system message to show." @@ -302,6 +303,45 @@ Otherwise, prompt the user to enter a system message." (declare-function ellama-context-update-buffer "ellama-context") (defvar ellama-context-buffer) +(transient-define-suffix ellama-transient-add-buffer (&optional args) + "Add current buffer to context. +ARGS used for transient arguments." + (interactive (list (transient-args transient-current-command))) + (ellama-context-add-buffer + (read-buffer "Buffer: ") + (transient-arg-value "--ephemeral" args))) + +(transient-define-suffix ellama-transient-add-directory (&optional args) + "Add directory to context. +ARGS used for transient arguments." + (interactive (list (transient-args transient-current-command))) + (let ((directory (read-directory-name "Directory: "))) + (ellama-context-add-directory + directory + (transient-arg-value "--ephemeral" args)))) + +(transient-define-suffix ellama-transient-add-file (&optional args) + "Add file to context. +ARGS used for transient arguments." + (interactive (list (transient-args transient-current-command))) + (ellama-context-add-file (transient-arg-value "--ephemeral" args))) + +(transient-define-suffix ellama-transient-add-selection (&optional args) + "Add current selection to context. +ARGS used for transient arguments." + (interactive (list (transient-args transient-current-command))) + (when (region-active-p) + (ellama-context-add-selection (transient-arg-value "--ephemeral" args)))) + +(transient-define-suffix ellama-transient-add-info-node (&optional args) + "Add Info Node to context. +ARGS used for transient arguments." + (interactive (list (transient-args transient-current-command))) + (let ((info-node (Info-copy-current-node-name))) + (ellama-context-add-info-node + info-node + (transient-arg-value "--ephemeral" args)))) + ;;;###autoload (autoload 'ellama-transient-context-menu "ellama-transient" nil t) (transient-define-prefix ellama-transient-context-menu () "Context Commands." @@ -312,11 +352,13 @@ Otherwise, prompt the user to enter a system message." %s" (with-current-buffer ellama-context-buffer (buffer-substring (point-min) (point-max))))) ["Add" - ("b" "Add Buffer" ellama-context-add-buffer) - ("d" "Add Directory" ellama-context-add-directory) - ("f" "Add File" ellama-context-add-file) - ("s" "Add Selection" ellama-context-add-selection) - ("i" "Add Info Node" ellama-context-add-info-node)] + ("b" "Add Buffer" ellama-transient-add-buffer) + ("d" "Add Directory" ellama-transient-add-directory) + ("f" "Add File" ellama-transient-add-file) + ("s" "Add Selection" ellama-transient-add-selection) + ("i" "Add Info Node" ellama-transient-add-info-node)] + ["Options" + ("-e" "Use Ephemeral Context" "--ephemeral")] ["Manage" ("m" "Manage context" ellama-context-manage) ("D" "Delete element" ellama-context-element-remove-by-name)