Skip to content

Commit

Permalink
Add community prompt collection feature
Browse files Browse the repository at this point in the history
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
  • Loading branch information
s-kostyaev committed Feb 22, 2025
1 parent 7ac470c commit 071f49f
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 1 deletion.
14 changes: 14 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
205 changes: 205 additions & 0 deletions ellama-community-prompts.el
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
;; 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.
27 changes: 26 additions & 1 deletion ellama.el
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 071f49f

Please sign in to comment.