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

Reply via email to