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"