branch: externals/ellama
commit 071f49f8319faa3d80a0824719dad8c1c3184786
Author: Sergey Kostyaev <sskosty...@gmail.com>
Commit: Sergey Kostyaev <sskosty...@gmail.com>

    Add community prompt collection feature
    
    Added a new file `ellama-community-prompts.el` to include functionality for
    managing and using community prompts. This includes downloading, parsing, 
and
    selecting prompts from a CSV file. Also updated `ellama.el` to support 
sending
    buffers to new chat sessions and added a new mode `ellama-blueprint-mode` 
with
    specific keybindings for handling blueprint buffers.
    
    Fix #98
---
 README.org                  |  14 +++
 ellama-community-prompts.el | 205 ++++++++++++++++++++++++++++++++++++++++++++
 ellama.el                   |  27 +++++-
 3 files changed, 245 insertions(+), 1 deletion(-)

diff --git a/README.org b/README.org
index 6e5c2a6ad5..9b491ff40c 100644
--- a/README.org
+++ b/README.org
@@ -320,6 +320,16 @@ provides much better results on reasoning tasks using AoT.
 Solve domain specific problem with simple chain. It makes LLMs act
 like a professional and adds a planning step.
 
+*** ellama-community-prompts-select-blueprint
+
+Select a prompt from the community prompt collection.
+The user is prompted to choose a role, and then a
+corresponding prompt is inserted into a blueprint buffer.
+
+*** ellama-community-prompts-update-variables
+
+Prompt user for values of variables found in current buffer and update them.
+
 ** Keymap
 
 In any buffer where there is active ellama streaming, you can press
@@ -446,6 +456,10 @@ argument generated text string.
   action function for ~ellama-preview-context-element~.
 - ~ellama-context-line-always-visible~: Make context header or mode line always
   visible, even with empty context.
+- ~ellama-community-prompts-url~: The URL of the community prompts collection.
+- ~ellama-community-prompts-file~: Path to the CSV file containing community 
prompts.
+  This file is expected to be located inside an ~ellama~ subdirectory
+  within your ~user-emacs-directory~.
 
 ** Minor modes
 
diff --git a/ellama-community-prompts.el b/ellama-community-prompts.el
new file mode 100644
index 0000000000..0899dae268
--- /dev/null
+++ b/ellama-community-prompts.el
@@ -0,0 +1,205 @@
+;;; ellama-community-prompts.el --- Community prompt collection -*- 
lexical-binding: t -*-
+
+;; Copyright (C) 2023-2025  Free Software Foundation, Inc.
+
+;; 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") (transient "0.7") (compat 
"29.1"))
+;; Version: 1.3.0
+;; SPDX-License-Identifier: GPL-3.0-or-later
+;; Created: 8th Oct 2023
+
+;; This file 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, or (at your option)
+;; any later version.
+
+;; This file 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Ellama is a tool for interacting with large language models from Emacs.
+;; It allows you to ask questions and receive responses from the
+;; LLMs.  Ellama can perform various tasks such as translation, code
+;; review, summarization, enhancing grammar/spelling or wording and
+;; more through the Emacs interface.  Ellama natively supports streaming
+;; output, making it effortless to use with your preferred text editor.
+;;
+
+;;; Code:
+(require 'plz)
+(require 'ellama)
+
+(defcustom ellama-community-prompts-url 
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
+  "The URL of the community prompts collection."
+  :type 'string
+  :group 'ellama)
+
+(defcustom ellama-community-prompts-file (expand-file-name
+                                         "community-prompts.csv"
+                                         (file-name-concat
+                                          user-emacs-directory
+                                          "ellama"))
+  "Path to the CSV file containing community prompts.
+This file is expected to be located inside an `ellama' subdirectory
+within your `user-emacs-directory'."
+  :type 'file
+  :group 'ellama)
+
+(defun ellama-community-prompts-ensure-file ()
+  "Ensure that the community prompt collection file is downloaded.
+Downloads the file from `ellama-community-prompts-url` if it does
+not already exist."
+  (unless (file-exists-p ellama-community-prompts-file)
+    (let* ((directory (file-name-directory ellama-community-prompts-file))
+           (response (plz 'get ellama-community-prompts-url
+                       :as 'file
+                       :then (lambda (filename)
+                               (rename-file filename 
ellama-community-prompts-file t))
+                       :else (lambda (error)
+                               (message "Failed to download community prompts: 
%s" error)))))
+      (when (and response (not (file-directory-p directory)))
+        (make-directory directory t))
+      (when response
+        (message "Community prompts file downloaded successfully.")))))
+
+(defun ellama-community-prompts-parse-csv-line (line)
+  "Parse a single CSV LINE into a list of fields, handling quotes.
+LINE is the string to be parsed."
+  (let ((i 0)
+        (len (length line)))
+    (cl-loop
+     with fields = '()
+     with current-field = ""
+     with inside-quotes = nil
+     while (< i len)
+     do (let ((char (aref line i)))
+          (cond
+           ;; Opening quote (start of field)
+           ((and (eq char ?\") (not inside-quotes))
+           (setq inside-quotes t)
+           (cl-incf i))
+           ;; Closing quote (end of field or escaped quote)
+           ((and (eq char ?\") inside-quotes)
+           (if (and (< (1+ i) len) (eq (aref line (1+ i)) ?\"))
+                (progn  ; Escaped quote: add single quote, skip next character
+                  (setq current-field (concat current-field "\""))
+                  (cl-incf i 2))
+             (setq inside-quotes nil)  ; End of quoted field
+             (cl-incf i)))
+           ;; Comma separator (outside quotes)
+           ((and (eq char ?,) (not inside-quotes))
+           (push current-field fields)
+           (setq current-field "")
+           (cl-incf i))
+           ;; Regular character
+           (t
+           (setq current-field (concat current-field (string char)))
+           (cl-incf i))))
+     ;; Add the last field after loop ends
+     finally return (nreverse (cons current-field fields)))))
+
+(defun ellama-community-prompts-convert-to-plist (parsed-line)
+  "Convert PARSED-LINE to plist.
+PARSED-LINE is expected to be a list with three elements: :act,
+:prompt, and :for-devs."
+  (let ((act (cl-first parsed-line))
+       (prompt (cl-second parsed-line))
+       (for-devs (string= "TRUE" (cl-third parsed-line))))
+    `(:act ,act :prompt ,prompt :for-devs ,for-devs)))
+
+(defvar ellama-community-prompts-collection nil
+  "Community prompts collection.")
+
+(defun ellama-community-prompts-ensure ()
+  "Ensure that the community prompt collection are loaded and available.
+This function ensures that the file specified by 
`ellama-community-prompts-file'
+is read and parsed, and the resulting collection of prompts is stored in
+`ellama-community-prompts-collection'. If the collection is already populated,
+this function does nothing.
+
+Returns the collection of community prompts."
+  (ellama-community-prompts-ensure-file)
+  (unless ellama-community-prompts-collection
+    (setq ellama-community-prompts-collection
+         (let ((buf (find-file-noselect ellama-community-prompts-file)))
+           (with-current-buffer buf
+             (mapcar (lambda (line)
+                       (ellama-community-prompts-convert-to-plist
+                        (ellama-community-prompts-parse-csv-line
+                         line)))
+                     (cdr (string-lines
+                           (buffer-substring-no-properties
+                            (point-min) (point-max)))))))))
+  ellama-community-prompts-collection)
+
+(defvar ellama-community-prompts-blurpint-buffer " 
*ellama-community-prompts-blueprint-buffer*"
+  "Buffer for community prompt blueprint.")
+
+;;;###autoload
+(defun ellama-community-prompts-select-blueprint (&optional for-devs)
+  "Select a prompt from the community prompt collection.
+The user is prompted to choose a role, and then a
+corresponding prompt is inserted into a blueprint buffer.
+
+Optional argument FOR-DEVS filters prompts for developers."
+  (interactive "P")
+  (let ((acts '())
+        selected-act selected-prompt)
+    ;; Collect unique acts from the filtered collection
+    (dolist (prompt ellama-community-prompts-collection)
+      (when (or (not for-devs) (eq for-devs (plist-get prompt :for-devs)))
+        (cl-pushnew (plist-get prompt :act) acts)))
+    ;; Prompt user to select an act
+    (setq selected-act (completing-read "Select Act: " acts))
+    ;; Find the corresponding prompt
+    (catch 'found-prompt
+      (dolist (prompt ellama-community-prompts-collection)
+        (when (and (string= selected-act (plist-get prompt :act))
+                   (or (not for-devs) (eq for-devs (plist-get prompt 
:for-devs))))
+          (setq selected-prompt (plist-get prompt :prompt))
+          (throw 'found-prompt nil))))
+    ;; Create a new buffer and insert the selected prompt
+    (with-current-buffer (get-buffer-create 
ellama-community-prompts-blurpint-buffer)
+      (erase-buffer)
+      (let ((hard-newline t))
+        (insert selected-prompt)
+        (fill-region (point-min) (point-max))
+        (ellama-blueprint-mode))
+      (switch-to-buffer (current-buffer))
+      (ellama-community-prompts-update-variables))))
+
+(defun ellama-community-prompts-get-variable-list ()
+  "Return a deduplicated list of variables found in the current buffer."
+  (save-excursion
+    (let ((vars '()))
+      (goto-char (point-min))
+      (while (re-search-forward "\{\\([^}]+\\)}" nil t)
+       (push (match-string 1) vars))
+      (seq-uniq vars))))
+
+(defun ellama-community-prompts-set-variable (var value)
+  "Replace VAR with VALUE in blueprint buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (while (search-forward (format "{%s}" var) nil t)
+      (replace-match value))))
+
+;;;###autoload
+(defun ellama-community-prompts-update-variables ()
+  "Prompt user for values of variables found in current buffer and update 
them."
+  (interactive)
+  (let ((vars (ellama-community-prompts-get-variable-list)))
+    (dolist (var vars)
+      (let ((value (read-string (format "Enter value for %s: " var))))
+        (ellama-community-prompts-set-variable var value)))))
+
+(provide 'ellama-community-prompts)
+;;; ellama-community-prompts.el ends here.
diff --git a/ellama.el b/ellama.el
index 6fdbc3ca6d..92bdd3cd9e 100644
--- a/ellama.el
+++ b/ellama.el
@@ -1237,6 +1237,30 @@ the context."
   :keymap ellama-context-mode-map
   :group 'ellama)
 
+;;;###autoload
+(defun ellama-send-buffer-to-new-chat ()
+  "Send current buffer to new chat session."
+  (interactive)
+  (ellama-chat
+   (buffer-substring-no-properties (point-min) (point-max))
+   t))
+
+(defvar-keymap ellama-blueprint-mode-map
+  :doc "Local keymap for Ellama blueprint mode buffers."
+  :parent global-map
+  "C-c C-c" #'ellama-send-buffer-to-new-chat
+  "C-c C-k" (lambda () (interactive) (kill-buffer (current-buffer))))
+
+;;;###autoload
+(define-derived-mode ellama-blueprint-mode
+  fundamental-mode
+  "ellama-blueprint"
+  "Toggle Ellama Blueprint mode."
+  :keymap ellama-blueprint-mode-map
+  :group 'ellama
+  (setq header-line-format
+       "'C-c C-c' to send  'C-c C-k' to cancel"))
+
 (defun ellama-update-context-buffer ()
   "Update ellama context buffer."
   (let* ((buf (get-buffer-create ellama-context-buffer))
@@ -3121,7 +3145,8 @@ Call CALLBACK on result list of strings.  ARGS contains 
keys for fine control.
 (transient-define-prefix ellama-transient-main-menu ()
   "Main Menu."
   ["Main"
-   [("c" "Chat" ellama-chat)]
+   [("c" "Chat" ellama-chat)
+    ("B" "Community blueprint" ellama-community-prompts-select-blueprint)]
    [("a" "Ask Commands" ellama-transient-ask-menu)
     ("C" "Code Commands" ellama-transient-code-menu)]]
   ["Text"

Reply via email to