├── docs ├── .keep ├── core-features │ ├── directive-processing.md │ ├── comments.md │ ├── erb-integration.md │ ├── shell-integration.md │ ├── parameter-history.md │ └── error-handling.md ├── assets │ ├── logo.svg │ └── favicon.ico ├── getting-started │ ├── installation.md │ ├── quick-start.md │ └── basic-concepts.md ├── storage │ ├── custom-adapters.md │ ├── filesystem-adapter.md │ └── activerecord-adapter.md ├── index.md ├── api │ ├── prompt-class.md │ ├── configuration.md │ └── directive-processor.md ├── development │ └── roadmap.md └── examples.md ├── test ├── prompts_dir │ ├── test2_prompt.json │ ├── excluded.txt │ ├── included.txt │ ├── todo.json │ ├── new_prompt.json │ ├── new_prompt.txt │ ├── test2_prompt.txt │ ├── also_included.txt │ ├── test_prompt.json │ ├── test_prompt.txt │ ├── hello_prompt.txt │ ├── toy │ │ └── 8-ball.txt │ ├── test_parameters.txt │ ├── test_parameters_and_directives.txt │ ├── test_directives.txt │ └── todo.txt ├── prompt_manager │ ├── storage_test.rb │ ├── directive_processor_test.rb │ ├── storage │ │ ├── test_removed_keywords_bug.rb │ │ ├── active_record_adapter_test.rb │ │ └── file_system_adapter_test.rb │ └── prompt_test.rb ├── prompt_manager_test.rb ├── test_helper.rb └── test_prompt_features.rb ├── .envrc ├── examples ├── prompts_dir │ ├── directive_example.json │ ├── todo.json │ ├── toy │ │ └── 8-ball.txt │ ├── directive_example.txt │ ├── todo.txt │ └── advanced_demo.txt ├── advanced_integrations.rb ├── rgfzf ├── using_search_proc.rb ├── directives.rb └── simple.rb ├── prompt_manager_logo.png ├── lib ├── prompt_manager │ ├── version.rb │ ├── directive_processor.rb │ ├── storage.rb │ ├── storage │ │ ├── active_record_adapter.rb │ │ └── file_system_adapter.rb │ └── prompt.rb └── prompt_manager.rb ├── Gemfile ├── .gitignore ├── Rakefile ├── .irbrc ├── .github └── workflows │ └── docs.yml ├── LICENSE ├── prompt_manager.gemspec ├── CHANGELOG.md ├── Gemfile.lock ├── mkdocs.yml └── COMMITS.md /docs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/prompts_dir/test2_prompt.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/prompts_dir/excluded.txt: -------------------------------------------------------------------------------- 1 | this does not -------------------------------------------------------------------------------- /test/prompts_dir/included.txt: -------------------------------------------------------------------------------- 1 | this contains hello -------------------------------------------------------------------------------- /test/prompts_dir/todo.json: -------------------------------------------------------------------------------- 1 | {"[LANGUAGE]":"ruby"} -------------------------------------------------------------------------------- /test/prompts_dir/new_prompt.json: -------------------------------------------------------------------------------- 1 | {"name":"Rubyist"} -------------------------------------------------------------------------------- /test/prompts_dir/new_prompt.txt: -------------------------------------------------------------------------------- 1 | How are you, [NAME]? -------------------------------------------------------------------------------- /test/prompts_dir/test2_prompt.txt: -------------------------------------------------------------------------------- 1 | Hello, how are you? -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # prompt_manager/.envrc 2 | 3 | export RR=`pwd` 4 | -------------------------------------------------------------------------------- /examples/prompts_dir/directive_example.json: -------------------------------------------------------------------------------- 1 | {"{language}":"French"} -------------------------------------------------------------------------------- /test/prompts_dir/also_included.txt: -------------------------------------------------------------------------------- 1 | Hello Dolly! 2 | Well HELLO Freddy -------------------------------------------------------------------------------- /test/prompts_dir/test_prompt.json: -------------------------------------------------------------------------------- 1 | {"[SIZE]":[20],"[COLOR]":["blue"]} -------------------------------------------------------------------------------- /test/prompts_dir/test_prompt.txt: -------------------------------------------------------------------------------- 1 | This is a prompt with [SIZE] and [COLOR]. -------------------------------------------------------------------------------- /examples/prompts_dir/todo.json: -------------------------------------------------------------------------------- 1 | {"[LANGUAGE]":[],"[KEYWORD_AKA_TODO]":"TODO"} -------------------------------------------------------------------------------- /prompt_manager_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadBomber/prompt_manager/HEAD/prompt_manager_logo.png -------------------------------------------------------------------------------- /lib/prompt_manager/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PromptManager 4 | VERSION = "0.5.8" 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in prompt_manager.gemspec 4 | gemspec 5 | 6 | gem 'rake' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | temp.md 10 | .aigcm_msg 11 | -------------------------------------------------------------------------------- /test/prompt_manager/storage_test.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'test_helper' 3 | 4 | class StorageTest < Minitest::Test 5 | def test_dummy 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/prompts_dir/hello_prompt.txt: -------------------------------------------------------------------------------- 1 | # test/prompts_dir/hello_prompt.txt 2 | # Desc: This prompt says hello 3 | 4 | Hello, how are you? Are you ready to assist me with some research? 5 | -------------------------------------------------------------------------------- /test/prompts_dir/toy/8-ball.txt: -------------------------------------------------------------------------------- 1 | # prompt_manager/examples/prompts_dir/toy/8-ball.txt 2 | # Desc: In the late 1940s the toy "Magic 8 Ball" came to market 3 | 4 | As a magic 8-ball, provide me with a terse answer to what you suspect that I am thinking about. 5 | -------------------------------------------------------------------------------- /examples/prompts_dir/toy/8-ball.txt: -------------------------------------------------------------------------------- 1 | # prompt_manager/examples/prompts_dir/toy/8-ball.txt 2 | # Desc: In the late 1940s the toy "Magic 8 Ball" came to market 3 | 4 | As a magic 8-ball, provide me with a terse answer to what you suspect that I am thinking about. 5 | -------------------------------------------------------------------------------- /test/prompts_dir/test_parameters.txt: -------------------------------------------------------------------------------- 1 | # test/prompts_dir/test_parameters.txt 2 | # Desc: Test parameters for prompt manager 3 | 4 | default prompt regex: [PARAMETER] 5 | user supplied prompt regex: {{parameter two}} 6 | 7 | __END__ 8 | 9 | Testing both the default regex and the example user supplied 10 | regex. 11 | -------------------------------------------------------------------------------- /examples/prompts_dir/directive_example.txt: -------------------------------------------------------------------------------- 1 | # This is a demonstration of directive processing 2 | # Comments like this are ignored when the prompt is processed 3 | 4 | // good_directive param_one param_two 5 | 6 | This is a demonstration of parameters using {language} syntax. 7 | 8 | You can substitute parameters like {language} with values. 9 | -------------------------------------------------------------------------------- /test/prompts_dir/test_parameters_and_directives.txt: -------------------------------------------------------------------------------- 1 | # test/prompts_dir/test_parameters_and_directives.txt 2 | # Desc: Test parameters and directives for prompt manager 3 | 4 | //TextToSpeech [LANGUAGE] [VOICE NAME] 5 | 6 | //model gpt-5 7 | //include path_to_file 8 | 9 | Tell me a few [KIND] jokes about [SUBJECT] 10 | 11 | //[COMMAND] [OPTIONS] 12 | 13 | __END__ 14 | -------------------------------------------------------------------------------- /test/prompts_dir/test_directives.txt: -------------------------------------------------------------------------------- 1 | # test/prompts_dir/test_directives.txt 2 | # Desc: Test directives for prompt manager 3 | 4 | //TextToSpeech English Frank 5 | 6 | Say the lyrics to the song "My Way". Please provide only the lyrics without commentary. 7 | 8 | //model gpt-5 9 | //include path_to_file 10 | 11 | __END__ 12 | Computers will never replace Frank Sinatra 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "tocer/rake/register" 5 | rescue LoadError => error 6 | puts error.message 7 | end 8 | 9 | Tocer::Rake::Register.call 10 | 11 | require "bundler/gem_tasks" 12 | require "rake/testtask" 13 | 14 | Rake::TestTask.new(:test) do |t| 15 | t.libs << "test" 16 | t.libs << "lib" 17 | t.test_files = FileList["test/**/*_test.rb"] 18 | end 19 | 20 | task default: :test 21 | -------------------------------------------------------------------------------- /test/prompts_dir/todo.txt: -------------------------------------------------------------------------------- 1 | # prompts_dir/todo.txt 2 | # Desc: Let the robot fix the TODO items. 3 | # 4 | 5 | As an experienced [LANGUAGE] software engineer write some [LANGUAGE] source code. Consider the following [LANGUAGE] file. For each comment line that contains the word "[KEYWORD_AKA_TODO]" take the text that follows that word as a requirement to be implemented in [LANGUAGE]. Remove the "[KEYWORD_AKA_TODO]" word from the comment line. After the line insert the [LANGUAGE] code that implements the requirement. 6 | 7 | __END__ 8 | -------------------------------------------------------------------------------- /examples/prompts_dir/todo.txt: -------------------------------------------------------------------------------- 1 | # prompts_dir/todo.txt 2 | # Desc: Let the robot fix the TODO items. 3 | # 4 | 5 | As an experienced [LANGUAGE] software engineer write some [LANGUAGE] source code. Consider the following [LANGUAGE] file. For each comment line that contains the word "[KEYWORD_AKA_TODO]" take the text that follows that word as a requirement to be implemented in [LANGUAGE]. Remove the "[KEYWORD_AKA_TODO]" word from the comment line. After the line insert the [LANGUAGE] code that implements the requirement. 6 | 7 | __END__ 8 | -------------------------------------------------------------------------------- /.irbrc: -------------------------------------------------------------------------------- 1 | require_relative 'lib/prompt_manager' 2 | require_relative 'lib/prompt_manager/storage/file_system_adapter' 3 | 4 | HERE = Pathname.new __dir__ 5 | 6 | PromptManager::Storage::FileSystemAdapter.config do |config| 7 | config.prompts_dir = HERE + 'examples/prompts_dir' 8 | # config.search_proc = nil # default 9 | # config.prompt_extension = '.txt' # default 10 | # config.parms+_extension = '.json' # default 11 | end 12 | 13 | PromptManager::Prompt.storage_adapter = PromptManager::Storage::FileSystemAdapter.new 14 | 15 | -------------------------------------------------------------------------------- /test/prompt_manager_test.rb: -------------------------------------------------------------------------------- 1 | # prompt_manager/test/prompt_manager_test.rb 2 | 3 | require 'test_helper' 4 | 5 | class TestPromptManager < Minitest::Test 6 | def test_that_it_has_a_version_number 7 | refute_nil ::PromptManager::VERSION 8 | end 9 | 10 | def test_prompt_manager_error_handling_with_custom_error 11 | assert_raises(PromptManager::Error) do 12 | raise PromptManager::Error, "Custom error message" 13 | end 14 | end 15 | 16 | def test_prompt_manager_error_handling 17 | assert_raises(PromptManager::Error) do 18 | raise PromptManager::Error, "This is a test error" 19 | end 20 | end 21 | end 22 | 23 | __END__ 24 | 25 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # test/test_helper.rb 2 | 3 | require 'simplecov' 4 | SimpleCov.start do 5 | add_filter '/test/' 6 | end 7 | 8 | require 'debug_me' 9 | include DebugMe 10 | 11 | require 'pathname' 12 | 13 | # Add the gem's lib directory to the load path 14 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 15 | 16 | # Define test directory and prompts directory 17 | $TEST_DIR = Pathname.new(__dir__) 18 | $PROMPTS_DIR = $TEST_DIR.join("prompts_dir") 19 | 20 | require "prompt_manager" 21 | 22 | # All non-storage tests are going to use the FileSystemAdapter 23 | PromptManager::Prompt 24 | .storage_adapter = 25 | PromptManager::Storage::FileSystemAdapter 26 | .config do |config| 27 | config.prompts_dir = $PROMPTS_DIR 28 | end.new 29 | 30 | require "minitest/autorun" 31 | require 'minitest/pride' 32 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' 9 | - 'mkdocs.yml' 10 | - '.github/workflows/docs.yml' 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: write 15 | pages: write 16 | id-token: write 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: '3.11' 31 | 32 | - name: Install Python dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install mkdocs-material mkdocs-minify-plugin 36 | 37 | - name: Deploy to GitHub Pages 38 | run: mkdocs gh-deploy --force -------------------------------------------------------------------------------- /docs/core-features/directive-processing.md: -------------------------------------------------------------------------------- 1 | # Directive Processing 2 | 3 | Directives are special instructions in your prompts that begin with `//` and provide powerful prompt composition capabilities. 4 | 5 | ## Overview 6 | 7 | Directives allow you to: 8 | - Include content from other files 9 | - Create modular, reusable prompt components 10 | - Build dynamic prompt structures 11 | - Process commands during prompt generation 12 | 13 | ## Built-in Directives 14 | 15 | ### `//include` (alias: `//import`) 16 | 17 | Include content from other files: 18 | 19 | ```text 20 | //include common/header.txt 21 | //import templates/[TEMPLATE_TYPE].txt 22 | 23 | Your main prompt content here... 24 | ``` 25 | 26 | ## Example 27 | 28 | ```text title="customer_response.txt" 29 | //include common/header.txt 30 | 31 | Dear [CUSTOMER_NAME], 32 | 33 | Thank you for your inquiry about [TOPIC]. 34 | 35 | //include common/footer.txt 36 | ``` 37 | 38 | For detailed examples and advanced usage, see the [Basic Examples](../examples/basic.md). -------------------------------------------------------------------------------- /lib/prompt_manager.rb: -------------------------------------------------------------------------------- 1 | # prompt_manager/lib/prompt_manager.rb 2 | # 3 | # frozen_string_literal: true 4 | 5 | require 'ostruct' 6 | 7 | require_relative "prompt_manager/version" 8 | require_relative "prompt_manager/prompt" 9 | require_relative "prompt_manager/storage" 10 | require_relative "prompt_manager/storage/file_system_adapter" 11 | 12 | # The PromptManager module provides functionality for managing, storing, 13 | # retrieving, and parameterizing text prompts used with generative AI systems. 14 | # It supports different storage backends through adapters and offers features 15 | # like parameter substitution, directives processing, and comment handling. 16 | module PromptManager 17 | # Base error class for all PromptManager-specific errors 18 | class Error < StandardError; end 19 | 20 | # Error class for storage-related issues 21 | class StorageError < Error; end 22 | 23 | # Error class for parameter substitution issues 24 | class ParameterError < Error; end 25 | 26 | # Error class for configuration issues 27 | class ConfigurationError < Error; end 28 | end 29 | -------------------------------------------------------------------------------- /test/test_prompt_features.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'minitest/autorun' 3 | require_relative '../lib/prompt_manager/prompt' 4 | 5 | class TestPromptFeatures < Minitest::Test 6 | def setup 7 | @original_env = ENV.to_hash 8 | end 9 | 10 | def teardown 11 | ENV.replace(@original_env) 12 | end 13 | 14 | def test_env_variable_replacement 15 | ENV['GREETING'] = 'Hello' 16 | prompt_text = 'Say $GREETING to world!' 17 | prompt = PromptManager::Prompt.new(prompt_text) 18 | result = prompt.process 19 | assert_equal 'Say Hello to world!', result 20 | end 21 | 22 | def test_erb_processing 23 | prompt_text = '2+2 is <%= 2+2 %>' 24 | prompt = PromptManager::Prompt.new(prompt_text) 25 | result = prompt.process 26 | assert_equal '2+2 is 4', result 27 | end 28 | 29 | def test_combined_features 30 | ENV['NAME'] = 'Alice' 31 | prompt_text = 'Hi, $NAME! Today, 3*3 equals <%= 3*3 %>.' 32 | prompt = PromptManager::Prompt.new(prompt_text) 33 | result = prompt.process 34 | assert_equal 'Hi, Alice! Today, 3*3 equals 9.', result 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dewayne VanHoozer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/core-features/comments.md: -------------------------------------------------------------------------------- 1 | # Comments and Documentation 2 | 3 | PromptManager supports comprehensive inline documentation through comments and special sections. 4 | 5 | ## Line Comments 6 | 7 | Lines beginning with `#` are treated as comments and ignored during processing: 8 | 9 | ```text 10 | # This is a comment describing the prompt 11 | # Author: Your Name 12 | # Version: 1.0 13 | 14 | Hello [NAME]! This text will be processed. 15 | ``` 16 | 17 | ## Block Comments 18 | 19 | Everything after `__END__` is ignored, creating a documentation section: 20 | 21 | ```text 22 | Your prompt content here... 23 | 24 | __END__ 25 | This section is completely ignored by PromptManager. 26 | 27 | Development notes: 28 | - TODO: Add more parameters 29 | - Version history 30 | - Usage examples 31 | ``` 32 | 33 | ## Documentation Best Practices 34 | 35 | ```text 36 | # Description: Customer service greeting template 37 | # Tags: customer-service, greeting 38 | # Version: 1.2 39 | # Author: Support Team 40 | # Last Updated: 2024-01-15 41 | 42 | //include common/header.txt 43 | 44 | Your prompt content... 45 | 46 | __END__ 47 | Internal notes and documentation go here. 48 | ``` -------------------------------------------------------------------------------- /docs/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/advanced_integrations.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/inline' 5 | 6 | gemfile do 7 | source 'https://rubygems.org' 8 | gem 'prompt_manager' 9 | gem 'ruby-openai' 10 | gem 'tty-spinner' 11 | end 12 | 13 | require 'prompt_manager' 14 | require 'openai' 15 | require 'erb' 16 | require 'time' 17 | require 'tty-spinner' 18 | 19 | # Configure PromptManager with filesystem adapter 20 | PromptManager::Prompt.storage_adapter = PromptManager::Storage::FileSystemAdapter.config do |config| 21 | config.prompts_dir = File.join(__dir__, 'prompts_dir') 22 | end.new 23 | 24 | # Configure OpenAI client 25 | client = OpenAI::Client.new( 26 | access_token: ENV['OPENAI_API_KEY'] 27 | ) 28 | 29 | # Get prompt instance and process with LLM 30 | prompt = PromptManager::Prompt.new( 31 | id: 'advanced_demo', 32 | erb_flag: true, 33 | envar_flag: true 34 | ) 35 | 36 | spinner = TTY::Spinner.new("[:spinner] Waiting for response...") 37 | spinner.auto_spin 38 | 39 | response = client.chat( 40 | parameters: { 41 | model: 'gpt-4o-mini', 42 | messages: [{ role: 'user', content: prompt.to_s }], 43 | stream: proc do |chunk, _bytesize| 44 | spinner.stop 45 | content = chunk.dig("choices", 0, "delta", "content") 46 | print content if content 47 | $stdout.flush 48 | end 49 | } 50 | ) 51 | 52 | puts 53 | -------------------------------------------------------------------------------- /examples/rgfzf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Refactored examples/rgfzf to take search term and directory path as parameters 3 | 4 | search_term="$1" 5 | directory_path="${2:-$(pwd)}" # Use given directory path, or current working directory if not provided 6 | 7 | # Verify that a search term is provided 8 | if [[ -z "$search_term" ]]; then 9 | echo "Usage: $0 search_term [directory_path]" 10 | exit 1 11 | fi 12 | 13 | # Ensure ripgrep (rg) and fzf are installed 14 | if ! command -v rg &> /dev/null || ! command -v fzf &> /dev/null; then 15 | echo "Please ensure ripgrep and fzf are installed before running this script." 16 | exit 1 17 | fi 18 | 19 | # Perform the file search using ripgrep and selection using fzf 20 | selected_file=$( 21 | rg --files-with-matches \ 22 | --no-messages \ 23 | --smart-case "$search_term" "$directory_path" | 24 | sed "s#^$directory_path/##" | # Strip out the directory path 25 | fzf --ansi \ 26 | --color "hl:-1:underline,hl+:-1:underline:reverse" \ 27 | --preview "cat $directory_path/{}" \ 28 | --preview-window "up,60%,border-bottom" \ 29 | --no-multi \ 30 | --exit-0 \ 31 | --query "$search_term" 32 | ) 33 | 34 | 35 | # If no file was selected, exit the script 36 | if [[ -z "$selected_file" ]]; then 37 | # echo "No file selected." 38 | exit 0 39 | fi 40 | 41 | # Remove the file extension 42 | prompt_id=$(echo "$selected_file" | sed "s/\.[^.]*$//") 43 | 44 | echo "$prompt_id" 45 | -------------------------------------------------------------------------------- /lib/prompt_manager/directive_processor.rb: -------------------------------------------------------------------------------- 1 | # lib/prompt_manager/directive_processor.rb 2 | 3 | # This is an example of a directive processor class. 4 | # It only supports the //include directive which is also 5 | # aliased as //import. 6 | 7 | module PromptManager 8 | class DirectiveProcessor 9 | EXCLUDED_METHODS = %w[ run initialize ] 10 | 11 | def initialize 12 | @prefix_size = PromptManager::Prompt::DIRECTIVE_SIGNAL.size 13 | @included_files = [] 14 | end 15 | 16 | def run(directives) 17 | return {} if directives.nil? || directives.empty? 18 | directives.each do |key, _| 19 | sans_prefix = key[@prefix_size..] 20 | args = sans_prefix.split(' ') 21 | method_name = args.shift 22 | 23 | if EXCLUDED_METHODS.include?(method_name) 24 | directives[key] = "Error: #{method_name} is not a valid directive: #{key}" 25 | elsif respond_to?(method_name) 26 | directives[key] = send(method_name, *args) 27 | else 28 | directives[key] = "Error: Unknown directive '#{key}'" 29 | end 30 | end 31 | directives 32 | end 33 | 34 | def include(file_path) 35 | if File.exist?(file_path) && 36 | File.readable?(file_path) && 37 | !@included_files.include?(file_path) 38 | content = File.read(file_path) 39 | @included_files << file_path 40 | content 41 | else 42 | "Error: File '#{file_path}' not accessible" 43 | end 44 | end 45 | alias_method :import, :include 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/prompt_manager/storage.rb: -------------------------------------------------------------------------------- 1 | # prompt_manager/lib/prompt_manager/storage.rb 2 | 3 | # The Storage module provides a namespace for different storage adapters 4 | # that handle persistence of prompts. Each adapter implements a common 5 | # interface for saving, retrieving, searching, and deleting prompts. 6 | # 7 | # Available adapters: 8 | # - FileSystemAdapter: Stores prompts in text files on the local filesystem 9 | # - ActiveRecordAdapter: Stores prompts in a database using ActiveRecord 10 | # 11 | # To use an adapter, configure it before using PromptManager:: 12 | # 13 | # Example with FileSystemAdapter: 14 | # PromptManager::Storage::FileSystemAdapter.config do |config| 15 | # config.prompts_dir = Pathname.new('/path/to/prompts') 16 | # end 17 | # PromptManager::Prompt.storage_adapter = PromptManager::Storage::FileSystemAdapter.new 18 | # 19 | # Example with ActiveRecordAdapter: 20 | # PromptManager::Storage::ActiveRecordAdapter.config do |config| 21 | # config.model = MyPromptModel 22 | # config.id_column = :prompt_id 23 | # config.text_column = :content 24 | # config.parameters_column = :params 25 | # end 26 | # PromptManager::Prompt.storage_adapter = PromptManager::Storage::ActiveRecordAdapter.new 27 | module PromptManager 28 | # The Storage module provides adapters for different storage backends. 29 | # Each adapter implements a common interface for managing prompts. 30 | # Note: PromptManager::Prompt uses one of these adapters as its storage backend to 31 | # perform all CRUD operations on prompt data. 32 | module Storage 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/prompt_manager/directive_processor_test.rb: -------------------------------------------------------------------------------- 1 | # test/prompt_manager/directive_processor_test.rb 2 | 3 | require 'test_helper' 4 | require 'tempfile' 5 | 6 | class DirectiveProcessorTest < Minitest::Test 7 | def setup 8 | @processor = PromptManager::DirectiveProcessor.new 9 | end 10 | 11 | def test_run_with_nil_or_empty 12 | assert_equal({}, @processor.run(nil)) 13 | assert_equal({}, @processor.run({})) 14 | end 15 | 16 | def test_run_with_excluded_method 17 | directives = { "//run something" => "" } 18 | result = @processor.run(directives) 19 | expected = "Error: run is not a valid directive: //run something" 20 | 21 | assert_equal expected, result["//run something"] 22 | end 23 | 24 | def test_run_with_unknown_directive 25 | directives = { "//unknown parameter" => "" } 26 | result = @processor.run(directives) 27 | expected = "Error: Unknown directive '//unknown parameter'" 28 | assert_equal expected, result["//unknown parameter"] 29 | end 30 | 31 | def test_run_include_with_nonexistent_file 32 | directives = { "//include /path/to/nonexistent_file.txt" => "" } 33 | result = @processor.run(directives) 34 | expected = "Error: File '/path/to/nonexistent_file.txt' not accessible" 35 | 36 | assert_equal expected, result["//include /path/to/nonexistent_file.txt"] 37 | end 38 | 39 | def test_alias_import_for_include 40 | directives = { "//import /path/to/nonexistent_file.txt" => "" } 41 | result = @processor.run(directives) 42 | expected = "Error: File '/path/to/nonexistent_file.txt' not accessible" 43 | 44 | assert_equal expected, result["//import /path/to/nonexistent_file.txt"] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /prompt_manager.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/prompt_manager/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "prompt_manager" 7 | spec.version = PromptManager::VERSION 8 | spec.authors = ["Dewayne VanHoozer"] 9 | spec.email = ["dvanhoozer@gmail.com"] 10 | 11 | spec.summary = "Manage prompts for use with gen-AI processes" 12 | 13 | spec.description = <<~EOS 14 | Manage the parameterized prompts (text) used in generative AI (aka chatGPT, 15 | OpenAI, et.al.) using storage adapters such as FileSystemAdapter, 16 | SqliteAdapter, and ActiveRecordAdapter. 17 | EOS 18 | 19 | spec.homepage = "https://github.com/MadBomber/prompt_manager" 20 | spec.license = "MIT" 21 | 22 | spec.required_ruby_version = ">= 3.2.0" 23 | 24 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 25 | spec.metadata["homepage_uri"] = spec.homepage 26 | spec.metadata["source_code_uri"] = spec.homepage 27 | spec.metadata["changelog_uri"] = spec.homepage 28 | 29 | spec.files = Dir.chdir(__dir__) do 30 | `git ls-files -z`.split("\x0").reject do |f| 31 | (File.expand_path(f) == __FILE__) || 32 | f.start_with?(*%w[test/ spec/ features/ .git appveyor Gemfile]) 33 | end 34 | end 35 | 36 | spec.require_paths = ["lib"] 37 | 38 | spec.add_development_dependency 'activerecord' 39 | spec.add_development_dependency 'amazing_print' 40 | spec.add_development_dependency 'debug_me' 41 | spec.add_development_dependency "minitest" 42 | spec.add_development_dependency "ostruct" 43 | spec.add_development_dependency 'tocer' 44 | spec.add_development_dependency 'simplecov' 45 | spec.add_development_dependency 'sqlite3' 46 | 47 | 48 | # Add runtime dependencies if necessary 49 | # spec.add_dependency "some_runtime_dependency" 50 | end -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- 1 | data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A4uLiDOHh4Rjh4eEY4eHhGOHh4Rjh4eEY4eHhGOHh4Rji4uIM////AP///wD///8A////AP///wDY2NgspqamjJmZmbeZmZm3mZmZt5mZmbeZmZm3mZmZt5mZmbeZmZm3pqamjNjY2Cz///8A////AP///wCfn5+MZGRk/1FRUX9RUVF/UVFRX1FRUX9RUVF/UVFRX1FRUX9RUVF/UVFRX2RkZP+fn5+M////AP///wCWlpa3UFBQ/4CAgP+BgYH/gYGB/4GBgf+BgYH/gYGB/4GBgf+BgYH/gYGB/4CAgP9QUFD/lpaWt////wCWlpa3UFBQ/4CAgP//////////////////////////////////////////////////////////////////////////////////////gICA/1BQUP+Wlpa3////AJaWlrdQUFD/gICA//////////////////////////////////////////////////7+/v/+/v7//v7+/v+AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID///////////////////////X19f/Ozs7/zs7O/87Ozv/Ozs7/9fX1///////////////////////AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID///////////////////////X19f/Ozs7/////////////////zs7O/87Ozv/19fX///////////////////////+AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID///////////////////////X19f/Ozs7/zs7O/87Ozv/Ozs7/zs7O/87Ozv/19fX///////////////////////+AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID///////////////////////X19f/Ozs7/////////////////zs7O/87Ozv/19fX///////////////////////+AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID///////////////////////X19f/Ozs7/zs7O/87Ozv/Ozs7/zs7O/87Ozv/19fX///////////////////////+AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID///////////////////////////////////////7+/v/+/v7//v7+/v////////////////////////////////+AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID///////////////////////////////////////////////////////////////////////////////////////+AgID/UFBQ/5aWlrf///8AlpaWt1BQUP+AgID/gYGB/4GBgf+BgYH/gYGB/4GBgf+BgYH/gYGB/4GBgf+BgYH/gICA/1BQUP+Wlpa3////AJ+fn4xkZGT/UVFRX1FRUX9RUVF/UVFRX1FRUX9RUVF/UVFRX1FRUX9RUVF/UVFRX2RkZP+fn5+M////AP///wDY2NgspqamjJmZmbeZmZm3mZmZt5mZmbeZmZm3mZmZt5mZmbeZmZm3pqamjNjY2Cz///8A////AP///wD///8A////AP///wDi4uIM4eHhGOHh4Rjh4eEY4eHhGOHh4Rjh4eEY4eHhGOLi4gz///8A////AP///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wA= -------------------------------------------------------------------------------- /examples/using_search_proc.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | # warn_indent: true 5 | ########################################################## 6 | ### 7 | ## File: using_search_proc.rb 8 | ## Desc: Simple demo of the PromptManager and FileStorageAdapter 9 | ## By: Dewayne VanHoozer (dvanhoozer@gmail.com) 10 | ## 11 | # 12 | 13 | require 'prompt_manager' 14 | require 'prompt_manager/storage/file_system_adapter' 15 | 16 | require 'amazing_print' 17 | require 'pathname' 18 | 19 | HERE = Pathname.new( __dir__ ) 20 | PROMPTS_DIR = HERE + "prompts_dir" 21 | SEARCH_SCRIPT = HERE + 'rgfzf' # a bash script using rg and fzf 22 | 23 | ###################################################### 24 | # Main 25 | 26 | at_exit do 27 | puts 28 | puts "Done." 29 | puts 30 | end 31 | 32 | # Configure the Storage Adapter to use 33 | PromptManager::Storage::FileSystemAdapter.config do |config| 34 | config.prompts_dir = PROMPTS_DIR 35 | config.search_proc = ->(q) {`#{SEARCH_SCRIPT} #{q} #{PROMPTS_DIR}`} # default 36 | # config.prompt_extension = '.txt' # default 37 | # config.parms+_extension = '.json' # default 38 | end 39 | 40 | PromptManager::Prompt.storage_adapter = PromptManager::Storage::FileSystemAdapter.new 41 | 42 | 43 | 44 | puts "Using Custom Search Proc" 45 | puts "========================" 46 | 47 | print "Search Proc Class: " 48 | puts PromptManager::Prompt.storage_adapter.search_proc.class 49 | 50 | search_term = "txt" # some comment lines show the file name example: todo.txt 51 | 52 | puts "Search for '#{search_term}' ..." 53 | 54 | prompt_id = PromptManager::Prompt.search search_term 55 | 56 | # NOTE: the search proc uses fzf as a selection tool. In this 57 | # case only one selected prompt ID that matches the search 58 | # term will be returned. 59 | 60 | puts "Found: '#{prompt_id}' which is a #{prompt_id.class}. empty? #{prompt_id.empty?}" 61 | 62 | puts <<~EOS 63 | 64 | When the rgfzf bash script does not find a prompt ID or if the 65 | ESC key is pressed, the prompt ID that is returned will be an empty String. 66 | 67 | EOS 68 | 69 | -------------------------------------------------------------------------------- /docs/core-features/erb-integration.md: -------------------------------------------------------------------------------- 1 | # ERB Integration 2 | 3 | PromptManager supports ERB (Embedded Ruby) templating for dynamic content generation. 4 | 5 | ## Enabling ERB 6 | 7 | ```ruby 8 | prompt = PromptManager::Prompt.new( 9 | id: 'dynamic_prompt', 10 | erb_flag: true 11 | ) 12 | ``` 13 | 14 | ## Basic Usage 15 | 16 | ```text title="dynamic_prompt.txt" 17 | Current date: <%= Date.today.strftime('%B %d, %Y') %> 18 | 19 | <% if '[PRIORITY]' == 'high' %> 20 | 🚨 URGENT: This requires immediate attention! 21 | <% else %> 22 | 📋 Standard processing request. 23 | <% end %> 24 | 25 | Generated at: <%= Time.now %> 26 | ``` 27 | 28 | ## Advanced Examples 29 | 30 | ### System Information Template 31 | 32 | ```text 33 | **Timestamp**: <%= Time.now.strftime('%A, %B %d, %Y at %I:%M:%S %p %Z') %> 34 | **Analysis Duration**: <%= Time.now - Time.parse('2024-01-01') %> seconds since 2024 began 35 | 36 | <% if RUBY_PLATFORM.include?('darwin') %> 37 | **Platform**: macOS/Darwin System 38 | **Ruby Platform**: <%= RUBY_PLATFORM %> 39 | **Ruby Version**: <%= RUBY_VERSION %> 40 | **Ruby Engine**: <%= RUBY_ENGINE %> 41 | <% elsif RUBY_PLATFORM.include?('linux') %> 42 | **Platform**: Linux System 43 | **Ruby Platform**: <%= RUBY_PLATFORM %> 44 | **Ruby Version**: <%= RUBY_VERSION %> 45 | **Ruby Engine**: <%= RUBY_ENGINE %> 46 | <% else %> 47 | **Platform**: Other Unix-like System 48 | **Ruby Platform**: <%= RUBY_PLATFORM %> 49 | **Ruby Version**: <%= RUBY_VERSION %> 50 | **Ruby Engine**: <%= RUBY_ENGINE %> 51 | <% end %> 52 | 53 | **Performance Context**: <%= `uptime`.strip rescue 'Unable to determine' %> 54 | ``` 55 | 56 | ### Complete Integration Example 57 | 58 | See the complete [advanced_integrations.rb](https://github.com/MadBomber/prompt_manager/blob/main/examples/advanced_integrations.rb) example that demonstrates: 59 | 60 | - ERB templating with system information 61 | - Dynamic timestamp generation 62 | - Platform-specific content rendering 63 | - Integration with OpenAI API streaming 64 | - Professional UI with `tty-spinner` 65 | 66 | This example shows how to create sophisticated prompts that adapt to your system environment and generate technical analysis reports. 67 | 68 | For more comprehensive ERB examples, see the [Examples documentation](../examples.md). -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## System Requirements 4 | 5 | PromptManager requires: 6 | 7 | - **Ruby**: 2.7 or higher (3.0+ recommended) 8 | - **Operating System**: Linux, macOS, or Windows 9 | - **Dependencies**: No additional system dependencies required 10 | 11 | ## Install the Gem 12 | 13 | ### Using Bundler (Recommended) 14 | 15 | Add PromptManager to your project's `Gemfile`: 16 | 17 | ```ruby 18 | # Gemfile 19 | gem 'prompt_manager' 20 | ``` 21 | 22 | Then install: 23 | 24 | ```bash 25 | bundle install 26 | ``` 27 | 28 | ### Using RubyGems 29 | 30 | Install directly with gem: 31 | 32 | ```bash 33 | gem install prompt_manager 34 | ``` 35 | 36 | ### Development Installation 37 | 38 | For development or to get the latest features: 39 | 40 | ```bash 41 | git clone https://github.com/MadBomber/prompt_manager.git 42 | cd prompt_manager 43 | bundle install 44 | ``` 45 | 46 | ## Verify Installation 47 | 48 | Test that PromptManager is installed correctly: 49 | 50 | ```ruby 51 | require 'prompt_manager' 52 | puts PromptManager::VERSION 53 | ``` 54 | 55 | ## Dependencies 56 | 57 | PromptManager has minimal dependencies and automatically installs: 58 | 59 | - **No external system dependencies** 60 | - **Pure Ruby dependencies** only 61 | - **Lightweight footprint** for easy integration 62 | 63 | ## Troubleshooting 64 | 65 | ### Common Issues 66 | 67 | !!! warning "Ruby Version" 68 | 69 | If you see compatibility errors, ensure you're running Ruby 2.7+: 70 | 71 | ```bash 72 | ruby --version 73 | ``` 74 | 75 | !!! tip "Bundler Issues" 76 | 77 | If bundle install fails, try updating bundler: 78 | 79 | ```bash 80 | gem update bundler 81 | bundle install 82 | ``` 83 | 84 | ### Getting Help 85 | 86 | If you encounter installation issues: 87 | 88 | 1. Check the [GitHub Issues](https://github.com/MadBomber/prompt_manager/issues) 89 | 2. Search for similar problems in [Discussions](https://github.com/MadBomber/prompt_manager/discussions) 90 | 3. Create a new issue with: 91 | - Ruby version (`ruby --version`) 92 | - Gem version (`gem list prompt_manager`) 93 | - Error message and full stack trace 94 | 95 | ## Next Steps 96 | 97 | Once installed, continue to the [Quick Start](quick-start.md) guide to begin using PromptManager. -------------------------------------------------------------------------------- /docs/core-features/shell-integration.md: -------------------------------------------------------------------------------- 1 | # Shell Integration 2 | 3 | PromptManager can automatically substitute environment variables and integrate with shell commands. 4 | 5 | ## Environment Variables 6 | 7 | Enable environment variable substitution: 8 | 9 | ```ruby 10 | prompt = PromptManager::Prompt.new( 11 | id: 'system_prompt', 12 | envar_flag: true 13 | ) 14 | ``` 15 | 16 | Environment variables in your prompt text will be automatically replaced. 17 | 18 | ## Example 19 | 20 | ```text title="system_prompt.txt" 21 | System: $USER 22 | Home: $HOME 23 | Path: $PATH 24 | 25 | Working directory: <%= Dir.pwd %> 26 | ``` 27 | 28 | ## Shell Command Execution 29 | 30 | PromptManager also supports shell command substitution using `$(command)` syntax: 31 | 32 | ```text title="system_info.txt" 33 | Current system load: $(uptime) 34 | Disk usage: $(df -h / | tail -1) 35 | Memory info: $(vm_stat | head -5) 36 | ``` 37 | 38 | Commands are executed when the prompt is processed, with output substituted in place. 39 | 40 | ## Advanced Example 41 | 42 | The [advanced_integrations.rb](https://github.com/MadBomber/prompt_manager/blob/main/examples/advanced_integrations.rb) example demonstrates comprehensive shell integration: 43 | 44 | ```text 45 | ### Hardware Platform Details 46 | **Architecture**: $HOSTTYPE$MACHTYPE 47 | **Hostname**: $HOSTNAME 48 | **Operating System**: $OSTYPE 49 | **Shell**: $SHELL (version: $BASH_VERSION) 50 | **User**: $USER 51 | **Home Directory**: $HOME 52 | **Current Path**: $PWD 53 | **Terminal**: $TERM 54 | 55 | ### Environment Configuration 56 | **PATH**: $PATH 57 | **Language**: $LANG 58 | **Editor**: $EDITOR 59 | **Pager**: $PAGER 60 | 61 | ### Performance Context 62 | **Load Average**: <%= `uptime`.strip rescue 'Unable to determine' %> 63 | **Memory Info**: <%= `vm_stat | head -5`.strip rescue 'Unable to determine' if RUBY_PLATFORM.include?('darwin') %> 64 | **Disk Usage**: <%= `df -h / | tail -1`.strip rescue 'Unable to determine' %> 65 | ``` 66 | 67 | This creates dynamic prompts that capture real-time system information for analysis. 68 | 69 | ## Configuration 70 | 71 | Set environment variables that your prompts will use: 72 | 73 | ```bash 74 | export API_KEY="your-api-key" 75 | export ENVIRONMENT="production" 76 | export OPENAI_API_KEY="your-openai-key" 77 | ``` 78 | 79 | For more shell integration examples, see the [Examples documentation](../examples.md). -------------------------------------------------------------------------------- /docs/core-features/parameter-history.md: -------------------------------------------------------------------------------- 1 | # Parameter History 2 | 3 | PromptManager automatically tracks parameter usage history to help you reuse previously entered values and maintain consistency across prompt executions. 4 | 5 | ## Automatic History Tracking 6 | 7 | When you use parameters in your prompts, PromptManager automatically saves the values you provide: 8 | 9 | ```ruby 10 | prompt = PromptManager::Prompt.new(id: 'customer_email') 11 | result = prompt.render(customer_name: 'John Doe', product: 'Pro Plan') 12 | # These values are automatically saved to history 13 | ``` 14 | 15 | ## History File Location 16 | 17 | Parameter history is stored in `~/.prompt_manager/parameters_history.yaml` by default. 18 | 19 | ## Accessing History 20 | 21 | Previous parameter values are automatically suggested when you use the same parameter names in subsequent prompts: 22 | 23 | ```ruby 24 | # First time - you provide values 25 | prompt.render(api_key: 'sk-123', model: 'gpt-4') 26 | 27 | # Second time - previous values are available 28 | prompt.render # Will suggest previously used api_key and model values 29 | ``` 30 | 31 | ## History Management 32 | 33 | ### Viewing History 34 | 35 | ```ruby 36 | # Access stored parameter values 37 | history = PromptManager.configuration.parameter_history 38 | puts history['api_key'] # Shows previously used API keys 39 | ``` 40 | 41 | ### Clearing History 42 | 43 | ```ruby 44 | # Clear all parameter history 45 | PromptManager.configuration.clear_parameter_history 46 | 47 | # Clear specific parameter 48 | PromptManager.configuration.clear_parameter('api_key') 49 | ``` 50 | 51 | ## Configuration 52 | 53 | Configure history behavior in your application: 54 | 55 | ```ruby 56 | PromptManager.configure do |config| 57 | config.save_parameter_history = true # Enable/disable history (default: true) 58 | config.parameter_history_file = 'custom_history.yaml' # Custom file location 59 | config.max_history_entries = 10 # Limit stored values per parameter 60 | end 61 | ``` 62 | 63 | ## Privacy Considerations 64 | 65 | Parameter history is stored locally and never transmitted. However, be mindful of sensitive data: 66 | 67 | - API keys and tokens are stored in plain text 68 | - Consider clearing history for sensitive parameters 69 | - Use environment variables for truly sensitive data instead of parameter history 70 | 71 | ## Best Practices 72 | 73 | 1. **Regular Cleanup**: Periodically clear old parameter values 74 | 2. **Sensitive Data**: Don't rely on history for secrets - use environment variables 75 | 3. **Team Sharing**: History files are user-specific, not shared across team members 76 | 4. **Backup**: Consider backing up important parameter configurations -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## Released 4 | ### [0.5.8] = 2025-09-01 5 | - fixed issue where removed keywords from prompt text were still being included in parameters if they existed in the JSON file (addresses AIA issue #105) 6 | - parameters now only include keywords currently present in the prompt text, while preserving historical values for existing keywords 7 | 8 | ### [0.5.7] = 2025-06-25 9 | - fixed a problem when the value of a parameter is an empty array 10 | - fixed a failing test 11 | 12 | ### [0.5.6] = 2025-06-04 13 | - fixed a problem where shell integration was not working correctly for $(shell command) 14 | 15 | ### [0.5.5] = 2025-05-21 16 | - fixed bug in parameter substitution when value is an Array now uses last entry 17 | 18 | ### [0.5.4] = 2025-05-18 19 | - fixed typo in the Prompt class envvar should have been envar which prevented shell integration from taking place. 20 | 21 | ### [0.5.3] = 2025-05-14 22 | - fixed issue were directives were not getting their content added to the prompt text 23 | - Updated documentation and versioning. 24 | - Added new error classes for better error handling. 25 | - Improved parameter handling and directive processing. 26 | 27 | ### [0.5.0] = 2025-03-29 28 | - Major refactoring of to improve processing of parameters and directives. 29 | - Added PromptManager::DirectiveProcessor as an example of how to implement custom directives. 30 | - Added support for //include directive that protects against loops. 31 | - Added support for embedding system environment variables. 32 | - Added support for ERB processing within a prompt. 33 | - Improved test coverage. 34 | 35 | ### [0.4.2] = 2024-10-26 36 | - Added configurable parameter_regex to customize keyword pattern 37 | 38 | ### [0.4.1] = 2023-12-29 39 | - Changed @directives from Hash to an Array 40 | - Fixed keywords not being substituted in directives 41 | 42 | ### [0.4.0] = 2023-12-19 43 | - Add "//directives param(s)" with keywords just like the prompt text. 44 | 45 | ### [0.3.3] = 2023-12-01 46 | - Added example of using the `search_proc` config parameter with the FileSystemAdapter. 47 | 48 | ### [0.3.2] = 2023-12-01 49 | 50 | - The ActiveRecordAdapter is passing its unit tests 51 | - Dropped the concept of an sqlite3 adapter since active record can be used to access sqlite3 databases as well as the big boys. 52 | 53 | ### [0.3.0] = 2023-11-28 54 | 55 | - **Breaking change** The value of the parameters Hash for a keyword is now an Array instead of a single value. The last value in the Array is always the most recent value used for the given keyword. This was done to support the use of a Readline::History object editing in the [aia](https://github.com/MadBomber/aia) CLI tool 56 | 57 | ### [0.2.0] - 2023-11-21 58 | 59 | - **Breaking change to FileSystemAdapter config process** 60 | - added list and path as extra methods in FileSystemAdapter 61 | 62 | ### [0.1.0] - 2023-11-16 63 | 64 | - Initial release using the FileSystemAdapter 65 | -------------------------------------------------------------------------------- /test/prompt_manager/storage/test_removed_keywords_bug.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../test_helper' 4 | 5 | class TestRemovedKeywordsBug < Minitest::Test 6 | def setup 7 | @temp_dir = Dir.mktmpdir 8 | @adapter = PromptManager::Storage::FileSystemAdapter.config do |config| 9 | config.prompts_dir = @temp_dir 10 | end.new 11 | 12 | PromptManager::Prompt.storage_adapter = @adapter 13 | end 14 | 15 | def teardown 16 | FileUtils.remove_entry(@temp_dir) if @temp_dir && Dir.exist?(@temp_dir) 17 | end 18 | 19 | def test_removed_keywords_are_not_included_in_parameters 20 | prompt_id = 'test_prompt' 21 | 22 | # Step 1: Create a prompt with two keywords 23 | initial_text = "Hello [NAME], you are [AGE] years old." 24 | File.write(File.join(@temp_dir, "#{prompt_id}.txt"), initial_text) 25 | 26 | # Step 2: Save parameters for both keywords (simulating previous usage) 27 | params = { 28 | "[NAME]" => ["Alice", "Bob"], 29 | "[AGE]" => ["25", "30"] 30 | } 31 | File.write( 32 | File.join(@temp_dir, "#{prompt_id}.json"), 33 | JSON.pretty_generate(params) 34 | ) 35 | 36 | # Step 3: Update prompt text to remove [AGE] keyword 37 | updated_text = "Hello [NAME], welcome!" 38 | File.write(File.join(@temp_dir, "#{prompt_id}.txt"), updated_text) 39 | 40 | # Step 4: Load the prompt and check parameters 41 | prompt = PromptManager::Prompt.new(id: prompt_id) 42 | 43 | # The parameters should only include [NAME], not [AGE] 44 | assert_includes prompt.parameters.keys, "[NAME]" 45 | refute_includes prompt.parameters.keys, "[AGE]", 46 | "Removed keyword [AGE] should not be in parameters" 47 | 48 | # Verify the historical values are preserved for existing keywords 49 | assert_equal ["Alice", "Bob"], prompt.parameters["[NAME]"] 50 | end 51 | 52 | def test_new_keywords_start_with_empty_array 53 | prompt_id = 'new_keyword_test' 54 | 55 | # Create a prompt with a keyword that has no JSON history 56 | text = "Testing [NEW_KEYWORD] here." 57 | File.write(File.join(@temp_dir, "#{prompt_id}.txt"), text) 58 | 59 | prompt = PromptManager::Prompt.new(id: prompt_id) 60 | 61 | # New keywords should have empty array as value 62 | assert_equal [], prompt.parameters["[NEW_KEYWORD]"] 63 | end 64 | 65 | def test_keywords_with_existing_history_preserve_values 66 | prompt_id = 'history_test' 67 | 68 | # Create prompt and JSON with history 69 | text = "Hello [NAME]!" 70 | File.write(File.join(@temp_dir, "#{prompt_id}.txt"), text) 71 | 72 | params = { 73 | "[NAME]" => ["Alice", "Bob", "Charlie"] 74 | } 75 | File.write( 76 | File.join(@temp_dir, "#{prompt_id}.json"), 77 | JSON.pretty_generate(params) 78 | ) 79 | 80 | prompt = PromptManager::Prompt.new(id: prompt_id) 81 | 82 | # Should preserve the historical values 83 | assert_equal ["Alice", "Bob", "Charlie"], prompt.parameters["[NAME]"] 84 | end 85 | end -------------------------------------------------------------------------------- /examples/directives.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | # warn_indent: true 5 | ########################################################## 6 | ### 7 | ## File: directives.rb 8 | ## Desc: Demo of the PromptManager and FileStorageAdapter 9 | ## By: Dewayne VanHoozer (dvanhoozer@gmail.com) 10 | ## 11 | # 12 | 13 | param1 = "param_one" 14 | param2 = "param_two" 15 | 16 | 17 | class MyDirectives 18 | def self.good_directive(*args) 19 | puts "inside #{__method__} with these parameters:" 20 | puts args.join(",\n") 21 | end 22 | end 23 | 24 | def concept_break = print "\n------------------------\n\n\n" 25 | 26 | require_relative '../lib/prompt_manager' 27 | require_relative '../lib/prompt_manager/storage/file_system_adapter' 28 | 29 | require 'amazing_print' 30 | require 'pathname' 31 | 32 | require 'debug_me' 33 | include DebugMe 34 | 35 | HERE = Pathname.new( __dir__ ) 36 | PROMPTS_DIR = HERE + "prompts_dir" 37 | 38 | 39 | ###################################################### 40 | # Main 41 | 42 | at_exit do 43 | puts 44 | puts "Done." 45 | puts 46 | end 47 | 48 | # Configure the Storage Adapter to use 49 | PromptManager::Storage::FileSystemAdapter.config do |config| 50 | config.prompts_dir = PROMPTS_DIR 51 | # config.search_proc = nil # default 52 | # config.prompt_extension = '.txt' # default 53 | # config.parms+_extension = '.json' # default 54 | end 55 | 56 | PromptManager::Prompt.storage_adapter = PromptManager::Storage::FileSystemAdapter.new 57 | 58 | # Use {parameter name} brackets to define a parameter 59 | # Note: must include capturing parentheses to make scan return arrays 60 | PromptManager::Prompt.parameter_regex = /(\{[A-Za-z _|]+\})/ 61 | 62 | # Retrieve a prompt 63 | # Note: The 'get' method returns a Hash, not a Prompt object 64 | # Use 'find' instead to get a Prompt object with methods 65 | prompt = PromptManager::Prompt.find(id: 'directive_example') 66 | 67 | # Shows prompt without comments or directives 68 | # It still has its parameter placeholders 69 | puts prompt 70 | concept_break 71 | 72 | puts "Directives are processed automatically when you call to_s on a prompt" 73 | puts "The DirectiveProcessor class handles directives like //include" 74 | puts "You don't need to process them manually" 75 | 76 | puts "Custom directive processing can be done by creating a custom DirectiveProcessor" 77 | puts "and setting it when creating a Prompt instance:" 78 | 79 | concept_break 80 | 81 | 82 | 83 | puts "Parameters in the prompt:" 84 | ap prompt.parameters 85 | puts "-"*16 86 | 87 | # Extract parameters from the prompt text using the parameter_regex 88 | puts "Parameters identified in the prompt text:" 89 | # With a capturing group, scan returns an array of arrays, so we need to flatten 90 | prompt_params = prompt.text.scan(PromptManager::Prompt.parameter_regex).flatten 91 | ap prompt_params 92 | concept_break 93 | 94 | # Set a parameter value (should be a string, not appending to an array) 95 | prompt.parameters['{language}'] = 'French' 96 | 97 | puts "After Substitution" 98 | puts prompt 99 | 100 | # Save the updated parameters 101 | prompt.save 102 | 103 | -------------------------------------------------------------------------------- /examples/prompts_dir/advanced_demo.txt: -------------------------------------------------------------------------------- 1 | # System Analysis and Historical Comparison Report 2 | # Generated with PromptManager - ERB + Shell Integration Demo 3 | 4 | ```markdown 5 | ## Current System Information 6 | 7 | **Timestamp**: <%= Time.now.strftime('%A, %B %d, %Y at %I:%M:%S %p %Z') %> 8 | **Analysis Duration**: <%= Time.now - Time.parse('2024-01-01') %> seconds since 2024 began 9 | 10 | ### Hardware Platform Details 11 | **Architecture**: $HOSTTYPE$MACHTYPE 12 | **Hostname**: $HOSTNAME 13 | **Operating System**: $OSTYPE 14 | **Shell**: $SHELL (version: $BASH_VERSION) 15 | **User**: $USER 16 | **Home Directory**: $HOME 17 | **Current Path**: $PWD 18 | **Terminal**: $TERM 19 | 20 | ### Detailed System Profile 21 | <% if RUBY_PLATFORM.include?('darwin') %> 22 | **Platform**: macOS/Darwin System 23 | **Ruby Platform**: <%= RUBY_PLATFORM %> 24 | **Ruby Version**: <%= RUBY_VERSION %> 25 | **Ruby Engine**: <%= RUBY_ENGINE %> 26 | <% elsif RUBY_PLATFORM.include?('linux') %> 27 | **Platform**: Linux System 28 | **Ruby Platform**: <%= RUBY_PLATFORM %> 29 | **Ruby Version**: <%= RUBY_VERSION %> 30 | **Ruby Engine**: <%= RUBY_ENGINE %> 31 | <% else %> 32 | **Platform**: Other Unix-like System 33 | **Ruby Platform**: <%= RUBY_PLATFORM %> 34 | **Ruby Version**: <%= RUBY_VERSION %> 35 | **Ruby Engine**: <%= RUBY_ENGINE %> 36 | <% end %> 37 | 38 | ### Environment Configuration 39 | **PATH**: $PATH 40 | **Language**: $LANG 41 | **Editor**: $EDITOR 42 | **Pager**: $PAGER 43 | 44 | ### Development Environment 45 | <% if ENV['RBENV_VERSION'] %> 46 | **Ruby Version Manager**: rbenv (version: <%= ENV['RBENV_VERSION'] %>) 47 | <% elsif ENV['RVM_VERSION'] %> 48 | **Ruby Version Manager**: RVM (version: <%= ENV['RVM_VERSION'] %>) 49 | <% else %> 50 | **Ruby Version Manager**: System Ruby or other 51 | <% end %> 52 | 53 | **Gem Home**: $GEM_HOME 54 | **Gem Path**: $GEM_PATH 55 | 56 | ### Performance Context 57 | **Load Average**: <%= `uptime`.strip rescue 'Unable to determine' %> 58 | **Memory Info**: <%= `vm_stat | head -5`.strip rescue 'Unable to determine' if RUBY_PLATFORM.include?('darwin') %> 59 | **Disk Usage**: <%= `df -h / | tail -1`.strip rescue 'Unable to determine' %> 60 | 61 | ## Analysis Request 62 | 63 | You are a technology historian and systems analyst. Please provide a comprehensive comparison between this current system and **the most powerful Apple computer created in the 20th century** (which would be from the 1990s). 64 | 65 | Consider these aspects in your analysis: 66 | 67 | 1. **Processing Power**: Compare the computational capabilities 68 | 2. **Memory and Storage**: Analyze RAM and storage differences 69 | 3. **Architecture Evolution**: Discuss the architectural changes 70 | 4. **Operating System**: Compare the OS sophistication 71 | 5. **Development Environment**: Contrast the programming environments 72 | 6. **Historical Context**: Put both systems in their historical perspective 73 | 7. **Price and Accessibility**: Consider cost and availability differences 74 | 8. **Legacy Impact**: How each system influenced computing 75 | 76 | Please be specific about which Apple computer from the 1990s you're comparing against, and provide concrete numbers where possible. Make the comparison engaging and educational, highlighting the dramatic evolution of computing power over the decades. 77 | 78 | **Format your response as a detailed technical report with clear sections and specific comparisons.** 79 | ``` 80 | -------------------------------------------------------------------------------- /examples/simple.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # frozen_string_literal: true 4 | # warn_indent: true 5 | ########################################################## 6 | ### 7 | ## File: simple.rb 8 | ## Desc: Simple demo of the PromptManager and FileStorageAdapter 9 | ## By: Dewayne VanHoozer (dvanhoozer@gmail.com) 10 | ## 11 | # 12 | 13 | require 'prompt_manager' 14 | require 'prompt_manager/storage/file_system_adapter' 15 | 16 | require 'amazing_print' 17 | require 'pathname' 18 | 19 | HERE = Pathname.new( __dir__ ) 20 | PROMPTS_DIR = HERE + "prompts_dir" 21 | 22 | 23 | ###################################################### 24 | # Main 25 | 26 | at_exit do 27 | puts 28 | puts "Done." 29 | puts 30 | end 31 | 32 | # Configure the Storage Adapter to use 33 | PromptManager::Storage::FileSystemAdapter.config do |config| 34 | config.prompts_dir = PROMPTS_DIR 35 | # config.search_proc = nil # default 36 | # config.prompt_extension = '.txt' # default 37 | # config.parms+_extension = '.json' # default 38 | end 39 | 40 | PromptManager::Prompt.storage_adapter = PromptManager::Storage::FileSystemAdapter.new 41 | 42 | # Get a prompt 43 | # Note: The 'get' method returns a Hash, not a Prompt object 44 | # Use 'find' instead to get a Prompt object with methods 45 | 46 | todo = PromptManager::Prompt.find(id: 'todo') 47 | 48 | # This sequence simulates presenting each of the previously 49 | # used values for each keyword to the user to accept or 50 | # edit. 51 | 52 | # ap todo.keywords 53 | 54 | # This is a new keyword that was added after the current 55 | # todo.json file was created. Simulate the user setting 56 | # its value. 57 | 58 | todo.parameters["[KEYWORD_AKA_TODO]"] = "TODO" 59 | 60 | # When the parameter values change, the prompt must 61 | # be saved to persist the changes 62 | todo.save 63 | 64 | 65 | puts <<~EOS 66 | 67 | Raw Text from Prompt File 68 | includes all lines 69 | ========================= 70 | EOS 71 | 72 | puts todo.text 73 | 74 | 75 | puts <<~EOS 76 | 77 | Last Set of Parameters Used 78 | Includes those recently added 79 | ============================= 80 | EOS 81 | 82 | ap todo.parameters 83 | 84 | 85 | puts <<~EOS 86 | 87 | Prompt Ready to Send to gen-AI 88 | ============================== 89 | EOS 90 | 91 | puts todo.to_s 92 | 93 | puts <<~EOS 94 | 95 | When using the FileSystemAdapter for prompt storage you can have within 96 | the prompts_dir you can have many sub-directories. These sub-directories 97 | act like categories. The prompt ID is composed for the sub-directory name, 98 | a "/" character and then the normal prompt ID. For example "toy/8-ball" 99 | 100 | EOS 101 | 102 | magic = PromptManager::Prompt.find(id: 'toy/8-ball') 103 | 104 | puts "The magic PROMPT is:" 105 | puts magic 106 | puts 107 | puts "Remember if you want to see the full text of the prompt file:" 108 | puts magic.text 109 | 110 | puts "="*64 111 | 112 | puts <<~EOS 113 | 114 | The FileSystemAdapter also adds two new class methods to the Prompt class: 115 | 116 | list - provides an Array of prompt IDs 117 | path(prompt_id) - Returns a Pathname object to the prompt file 118 | 119 | EOS 120 | 121 | puts "List of prompts available" 122 | puts "=========================" 123 | 124 | puts PromptManager::Prompt.list 125 | 126 | puts <<~EOS 127 | 128 | And the path to the "toy/8-ball" prompt file is: 129 | 130 | #{PromptManager::Prompt.path('toy/8-ball')} 131 | 132 | Use "your_prompt.path" for when you want to do something with the 133 | the prompt file like send it to a text editor. 134 | 135 | Your can also use the class method if you supply a prompt_id 136 | like this: 137 | 138 | EOS 139 | 140 | puts PromptManager::Prompt.path('toy/8-ball') 141 | 142 | puts 143 | 144 | puts "Default Search for Prompts" 145 | puts "==========================" 146 | 147 | print "Search Proc Class: " 148 | puts PromptManager::Prompt.storage_adapter.search_proc.class 149 | 150 | search_term = "txt" # some comment lines show the file name example: todo.txt 151 | 152 | puts "Search for '#{search_term}' ..." 153 | 154 | prompt_ids = PromptManager::Prompt.search search_term 155 | 156 | # NOTE: prompt+ids is an Array of prompt IDs even if there is only one entry. 157 | # or and empty array if there are no prompts have the search term. 158 | 159 | puts "Found: #{prompt_ids}" 160 | 161 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | prompt_manager (0.5.8) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | activemodel (8.0.2.1) 10 | activesupport (= 8.0.2.1) 11 | activerecord (8.0.2.1) 12 | activemodel (= 8.0.2.1) 13 | activesupport (= 8.0.2.1) 14 | timeout (>= 0.4.0) 15 | activesupport (8.0.2.1) 16 | base64 17 | benchmark (>= 0.3) 18 | bigdecimal 19 | concurrent-ruby (~> 1.0, >= 1.3.1) 20 | connection_pool (>= 2.2.5) 21 | drb 22 | i18n (>= 1.6, < 2) 23 | logger (>= 1.4.2) 24 | minitest (>= 5.1) 25 | securerandom (>= 0.3) 26 | tzinfo (~> 2.0, >= 2.0.5) 27 | uri (>= 0.13.1) 28 | amazing_print (1.8.1) 29 | base64 (0.3.0) 30 | benchmark (0.4.1) 31 | bigdecimal (3.2.2) 32 | cogger (1.5.0) 33 | core (~> 2.0) 34 | logger (~> 1.7) 35 | refinements (~> 13.5) 36 | tone (~> 2.0) 37 | zeitwerk (~> 2.7) 38 | concurrent-ruby (1.3.5) 39 | connection_pool (2.5.4) 40 | containable (1.3.0) 41 | concurrent-ruby (~> 1.3) 42 | core (2.3.0) 43 | debug_me (1.1.1) 44 | docile (1.4.1) 45 | drb (2.2.3) 46 | dry-configurable (1.3.0) 47 | dry-core (~> 1.1) 48 | zeitwerk (~> 2.6) 49 | dry-core (1.1.0) 50 | concurrent-ruby (~> 1.0) 51 | logger 52 | zeitwerk (~> 2.6) 53 | dry-inflector (1.2.0) 54 | dry-initializer (3.2.0) 55 | dry-logic (1.6.0) 56 | bigdecimal 57 | concurrent-ruby (~> 1.0) 58 | dry-core (~> 1.1) 59 | zeitwerk (~> 2.6) 60 | dry-monads (1.9.0) 61 | concurrent-ruby (~> 1.0) 62 | dry-core (~> 1.1) 63 | zeitwerk (~> 2.6) 64 | dry-schema (1.14.1) 65 | concurrent-ruby (~> 1.0) 66 | dry-configurable (~> 1.0, >= 1.0.1) 67 | dry-core (~> 1.1) 68 | dry-initializer (~> 3.2) 69 | dry-logic (~> 1.5) 70 | dry-types (~> 1.8) 71 | zeitwerk (~> 2.6) 72 | dry-types (1.8.3) 73 | bigdecimal (~> 3.0) 74 | concurrent-ruby (~> 1.0) 75 | dry-core (~> 1.0) 76 | dry-inflector (~> 1.0) 77 | dry-logic (~> 1.4) 78 | zeitwerk (~> 2.6) 79 | etcher (3.3.0) 80 | cogger (~> 1.0) 81 | core (~> 2.0) 82 | dry-monads (~> 1.9) 83 | dry-types (~> 1.7) 84 | refinements (~> 13.3) 85 | versionaire (~> 14.0) 86 | zeitwerk (~> 2.7) 87 | i18n (1.14.7) 88 | concurrent-ruby (~> 1.0) 89 | infusible (4.4.0) 90 | marameters (~> 4.1) 91 | logger (1.7.0) 92 | marameters (4.4.0) 93 | refinements (~> 13.3) 94 | zeitwerk (~> 2.7) 95 | mini_portile2 (2.8.9) 96 | minitest (5.25.5) 97 | optparse (0.6.0) 98 | ostruct (0.6.3) 99 | rake (13.3.0) 100 | refinements (13.5.0) 101 | runcom (12.3.0) 102 | refinements (~> 13.3) 103 | xdg (~> 9.0) 104 | zeitwerk (~> 2.7) 105 | securerandom (0.4.1) 106 | simplecov (0.22.0) 107 | docile (~> 1.1) 108 | simplecov-html (~> 0.11) 109 | simplecov_json_formatter (~> 0.1) 110 | simplecov-html (0.13.2) 111 | simplecov_json_formatter (0.1.4) 112 | sod (1.4.0) 113 | cogger (~> 1.0) 114 | containable (~> 1.1) 115 | infusible (~> 4.0) 116 | optparse (~> 0.6) 117 | refinements (~> 13.5) 118 | tone (~> 2.0) 119 | zeitwerk (~> 2.7) 120 | spek (4.4.0) 121 | core (~> 2.0) 122 | dry-monads (~> 1.9) 123 | refinements (~> 13.3) 124 | versionaire (~> 14.0) 125 | zeitwerk (~> 2.7) 126 | sqlite3 (2.7.3) 127 | mini_portile2 (~> 2.8.0) 128 | sqlite3 (2.7.3-arm64-darwin) 129 | timeout (0.4.3) 130 | tocer (19.3.0) 131 | cogger (~> 1.0) 132 | containable (~> 1.1) 133 | core (~> 2.0) 134 | dry-schema (~> 1.13) 135 | etcher (~> 3.0) 136 | infusible (~> 4.0) 137 | refinements (~> 13.3) 138 | runcom (~> 12.0) 139 | sod (~> 1.0) 140 | spek (~> 4.0) 141 | zeitwerk (~> 2.7) 142 | tone (2.4.0) 143 | refinements (~> 13.3) 144 | zeitwerk (~> 2.7) 145 | tzinfo (2.0.6) 146 | concurrent-ruby (~> 1.0) 147 | uri (1.0.3) 148 | versionaire (14.3.0) 149 | refinements (~> 13.3) 150 | xdg (9.3.0) 151 | zeitwerk (2.7.3) 152 | 153 | PLATFORMS 154 | arm64-darwin-23 155 | ruby 156 | 157 | DEPENDENCIES 158 | activerecord 159 | amazing_print 160 | debug_me 161 | minitest 162 | ostruct 163 | prompt_manager! 164 | rake 165 | simplecov 166 | sqlite3 167 | tocer 168 | 169 | BUNDLED WITH 170 | 2.7.1 171 | -------------------------------------------------------------------------------- /docs/storage/custom-adapters.md: -------------------------------------------------------------------------------- 1 | # Custom Storage Adapters 2 | 3 | Create custom storage adapters to integrate PromptManager with any storage system. 4 | 5 | ## Adapter Interface 6 | 7 | All storage adapters must inherit from `PromptManager::Storage::Base` and implement the required methods: 8 | 9 | ```ruby 10 | class CustomAdapter < PromptManager::Storage::Base 11 | def initialize(**options) 12 | # Initialize your storage connection 13 | super 14 | end 15 | 16 | def read(prompt_id) 17 | # Return the prompt content as a string 18 | # Raise PromptManager::PromptNotFoundError if not found 19 | end 20 | 21 | def write(prompt_id, content) 22 | # Save the prompt content 23 | # Return true on success 24 | end 25 | 26 | def exist?(prompt_id) 27 | # Return true if prompt exists 28 | end 29 | 30 | def delete(prompt_id) 31 | # Remove the prompt 32 | # Return true on success 33 | end 34 | 35 | def list 36 | # Return array of all prompt IDs 37 | end 38 | end 39 | ``` 40 | 41 | ## Example: Redis Adapter 42 | 43 | ```ruby 44 | require 'redis' 45 | 46 | class RedisAdapter < PromptManager::Storage::Base 47 | def initialize(redis_url: 'redis://localhost:6379', key_prefix: 'prompts:', **options) 48 | @redis = Redis.new(url: redis_url) 49 | @key_prefix = key_prefix 50 | super(**options) 51 | end 52 | 53 | def read(prompt_id) 54 | content = @redis.get(redis_key(prompt_id)) 55 | raise PromptManager::PromptNotFoundError.new("Prompt '#{prompt_id}' not found") unless content 56 | content 57 | end 58 | 59 | def write(prompt_id, content) 60 | @redis.set(redis_key(prompt_id), content) 61 | true 62 | end 63 | 64 | def exist?(prompt_id) 65 | @redis.exists?(redis_key(prompt_id)) > 0 66 | end 67 | 68 | def delete(prompt_id) 69 | @redis.del(redis_key(prompt_id)) > 0 70 | end 71 | 72 | def list 73 | keys = @redis.keys("#{@key_prefix}*") 74 | keys.map { |key| key.sub(@key_prefix, '') } 75 | end 76 | 77 | private 78 | 79 | def redis_key(prompt_id) 80 | "#{@key_prefix}#{prompt_id}" 81 | end 82 | end 83 | 84 | # Configure PromptManager to use Redis 85 | PromptManager.configure do |config| 86 | config.storage = RedisAdapter.new( 87 | redis_url: ENV['REDIS_URL'], 88 | key_prefix: 'myapp:prompts:' 89 | ) 90 | end 91 | ``` 92 | 93 | ## Example: S3 Adapter 94 | 95 | ```ruby 96 | require 'aws-sdk-s3' 97 | 98 | class S3Adapter < PromptManager::Storage::Base 99 | def initialize(bucket:, region: 'us-east-1', key_prefix: 'prompts/', **options) 100 | @bucket = bucket 101 | @key_prefix = key_prefix 102 | @s3 = Aws::S3::Client.new(region: region) 103 | super(**options) 104 | end 105 | 106 | def read(prompt_id) 107 | response = @s3.get_object( 108 | bucket: @bucket, 109 | key: s3_key(prompt_id) 110 | ) 111 | response.body.read 112 | rescue Aws::S3::Errors::NoSuchKey 113 | raise PromptManager::PromptNotFoundError.new("Prompt '#{prompt_id}' not found") 114 | end 115 | 116 | def write(prompt_id, content) 117 | @s3.put_object( 118 | bucket: @bucket, 119 | key: s3_key(prompt_id), 120 | body: content, 121 | content_type: 'text/plain' 122 | ) 123 | true 124 | end 125 | 126 | def exist?(prompt_id) 127 | @s3.head_object(bucket: @bucket, key: s3_key(prompt_id)) 128 | true 129 | rescue Aws::S3::Errors::NotFound 130 | false 131 | end 132 | 133 | def delete(prompt_id) 134 | @s3.delete_object(bucket: @bucket, key: s3_key(prompt_id)) 135 | true 136 | end 137 | 138 | def list 139 | response = @s3.list_objects_v2( 140 | bucket: @bucket, 141 | prefix: @key_prefix 142 | ) 143 | 144 | response.contents.map do |object| 145 | object.key.sub(@key_prefix, '') 146 | end 147 | end 148 | 149 | private 150 | 151 | def s3_key(prompt_id) 152 | "#{@key_prefix}#{prompt_id}.txt" 153 | end 154 | end 155 | ``` 156 | 157 | ## Best Practices 158 | 159 | 1. **Error Handling**: Always raise appropriate exceptions 160 | 2. **Connection Management**: Handle connection failures gracefully 161 | 3. **Performance**: Implement connection pooling where appropriate 162 | 4. **Security**: Use proper authentication and encryption 163 | 5. **Testing**: Write comprehensive tests for your adapter 164 | 6. **Documentation**: Document configuration options and requirements 165 | 166 | ## Configuration 167 | 168 | Register your custom adapter: 169 | 170 | ```ruby 171 | PromptManager.configure do |config| 172 | config.storage = CustomAdapter.new( 173 | # Your adapter configuration 174 | ) 175 | end 176 | ``` -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PromptManager Documentation 2 | site_description: Comprehensive documentation for the PromptManager Ruby gem - Manage parameterized prompts for generative AI 3 | site_author: MadBomber 4 | site_url: https://madbomber.github.io/prompt_manager 5 | repo_url: https://github.com/MadBomber/prompt_manager 6 | repo_name: MadBomber/prompt_manager 7 | edit_uri: edit/main/docs/ 8 | 9 | theme: 10 | name: material 11 | logo: assets/logo.svg 12 | favicon: assets/favicon.ico 13 | palette: 14 | - media: "(prefers-color-scheme: light)" 15 | scheme: default 16 | primary: indigo 17 | accent: amber 18 | toggle: 19 | icon: material/brightness-7 20 | name: Switch to dark mode 21 | - media: "(prefers-color-scheme: dark)" 22 | scheme: slate 23 | primary: indigo 24 | accent: amber 25 | toggle: 26 | icon: material/brightness-4 27 | name: Switch to light mode 28 | features: 29 | - navigation.instant 30 | - navigation.tracking 31 | - navigation.tabs 32 | - navigation.tabs.sticky 33 | - navigation.sections 34 | - navigation.expand 35 | - navigation.path 36 | - navigation.prune 37 | - navigation.indexes 38 | - navigation.top 39 | - toc.follow 40 | - toc.integrate 41 | - search.suggest 42 | - search.highlight 43 | - search.share 44 | - header.autohide 45 | - content.code.copy 46 | - content.code.annotate 47 | - content.tabs.link 48 | 49 | plugins: 50 | - search: 51 | separator: '[\s\-,:!=\[\]()"/]+|(?!\b)(?=[A-Z][a-z])|\.(?!\d)|&[lg]t;' 52 | 53 | markdown_extensions: 54 | - abbr 55 | - admonition 56 | - attr_list 57 | - def_list 58 | - footnotes 59 | - md_in_html 60 | - toc: 61 | permalink: true 62 | toc_depth: 3 63 | - pymdownx.arithmatex: 64 | generic: true 65 | - pymdownx.betterem: 66 | smart_enable: all 67 | - pymdownx.caret 68 | - pymdownx.details 69 | - pymdownx.emoji: 70 | emoji_index: !!python/name:material.extensions.emoji.twemoji 71 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 72 | - pymdownx.highlight: 73 | anchor_linenums: true 74 | line_spans: __span 75 | pygments_lang_class: true 76 | - pymdownx.inlinehilite 77 | - pymdownx.keys 78 | - pymdownx.mark 79 | - pymdownx.smartsymbols 80 | - pymdownx.superfences: 81 | custom_fences: 82 | - name: mermaid 83 | class: mermaid 84 | format: !!python/name:pymdownx.superfences.fence_code_format 85 | - pymdownx.tabbed: 86 | alternate_style: true 87 | - pymdownx.tasklist: 88 | custom_checkbox: true 89 | - pymdownx.tilde 90 | 91 | extra: 92 | social: 93 | - icon: fontawesome/brands/github 94 | link: https://github.com/MadBomber/prompt_manager 95 | name: GitHub Repository 96 | - icon: fontawesome/solid/gem 97 | link: https://rubygems.org/gems/prompt_manager 98 | name: RubyGems Package 99 | version: 100 | provider: mike 101 | default: latest 102 | 103 | nav: 104 | - Home: index.md 105 | - Getting Started: 106 | - Installation: getting-started/installation.md 107 | - Quick Start: getting-started/quick-start.md 108 | - Basic Concepts: getting-started/basic-concepts.md 109 | - Core Features: 110 | - Parameterized Prompts: core-features/parameterized-prompts.md 111 | - Directive Processing: core-features/directive-processing.md 112 | - ERB Integration: core-features/erb-integration.md 113 | - Shell Integration: core-features/shell-integration.md 114 | - Comments & Documentation: core-features/comments.md 115 | - Parameter History: core-features/parameter-history.md 116 | - Error Handling: core-features/error-handling.md 117 | - Storage Adapters: 118 | - Overview: storage/overview.md 119 | - FileSystemAdapter: storage/filesystem-adapter.md 120 | - ActiveRecordAdapter: storage/activerecord-adapter.md 121 | - Custom Adapters: storage/custom-adapters.md 122 | - API Reference: 123 | - Prompt Class: api/prompt-class.md 124 | - Storage Adapters: api/storage-adapters.md 125 | - Directive Processor: api/directive-processor.md 126 | - Configuration: api/configuration.md 127 | - Advanced Usage: 128 | - Custom Keywords: advanced/custom-keywords.md 129 | - Dynamic Directives: advanced/dynamic-directives.md 130 | - Search Integration: advanced/search-integration.md 131 | - Performance Tips: advanced/performance.md 132 | - Examples: 133 | - Overview: examples.md 134 | - Basic Examples: examples/basic.md 135 | - Advanced Examples: examples/advanced.md 136 | - Real World Use Cases: examples/real-world.md 137 | - Development: 138 | - Architecture: development/architecture.md 139 | - Contributing: development/contributing.md 140 | - Testing: development/testing.md 141 | - Roadmap: development/roadmap.md 142 | - Migration Guides: 143 | - Version 0.9.0: migration/v0.9.0.md 144 | - Version 1.0.0: migration/v1.0.0.md 145 | 146 | copyright: Copyright © 2024 MadBomber - MIT License -------------------------------------------------------------------------------- /docs/core-features/error-handling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | PromptManager provides comprehensive error handling to help you identify and resolve issues during prompt processing. 4 | 5 | ## Common Exceptions 6 | 7 | ### `PromptNotFoundError` 8 | 9 | Raised when a prompt cannot be located: 10 | 11 | ```ruby 12 | begin 13 | prompt = PromptManager::Prompt.new(id: 'nonexistent_prompt') 14 | rescue PromptManager::PromptNotFoundError => e 15 | puts "Prompt not found: #{e.message}" 16 | # Handle gracefully - perhaps show available prompts 17 | end 18 | ``` 19 | 20 | ### `MissingParametersError` 21 | 22 | Raised when required parameters are not provided: 23 | 24 | ```ruby 25 | begin 26 | result = prompt.render # Missing required parameters 27 | rescue PromptManager::MissingParametersError => e 28 | puts "Missing parameters: #{e.missing_parameters.join(', ')}" 29 | # Prompt user for missing values 30 | end 31 | ``` 32 | 33 | ### `DirectiveProcessingError` 34 | 35 | Raised when directive processing fails: 36 | 37 | ```ruby 38 | begin 39 | result = prompt.render 40 | rescue PromptManager::DirectiveProcessingError => e 41 | puts "Directive error: #{e.message}" 42 | puts "Line: #{e.line_number}" if e.respond_to?(:line_number) 43 | end 44 | ``` 45 | 46 | ### `StorageError` 47 | 48 | Raised when storage operations fail: 49 | 50 | ```ruby 51 | begin 52 | prompt = PromptManager::Prompt.new(id: 'my_prompt') 53 | rescue PromptManager::StorageError => e 54 | puts "Storage error: #{e.message}" 55 | # Check file permissions, disk space, etc. 56 | end 57 | ``` 58 | 59 | ## Error Recovery Strategies 60 | 61 | ### Graceful Degradation 62 | 63 | ```ruby 64 | def safe_render_prompt(prompt_id, params = {}) 65 | begin 66 | prompt = PromptManager::Prompt.new(id: prompt_id) 67 | prompt.render(params) 68 | rescue PromptManager::PromptNotFoundError 69 | "Default response when prompt is unavailable" 70 | rescue PromptManager::MissingParametersError => e 71 | "Please provide: #{e.missing_parameters.join(', ')}" 72 | rescue => e 73 | logger.error "Unexpected error rendering prompt: #{e.message}" 74 | "An error occurred processing your request" 75 | end 76 | end 77 | ``` 78 | 79 | ### Retry Logic 80 | 81 | ```ruby 82 | def render_with_retry(prompt, params, max_retries: 3) 83 | retries = 0 84 | 85 | begin 86 | prompt.render(params) 87 | rescue PromptManager::StorageError => e 88 | retries += 1 89 | if retries <= max_retries 90 | sleep(0.5 * retries) # Exponential backoff 91 | retry 92 | else 93 | raise e 94 | end 95 | end 96 | end 97 | ``` 98 | 99 | ## Validation and Prevention 100 | 101 | ### Parameter Validation 102 | 103 | ```ruby 104 | def validate_parameters(params, required_params) 105 | missing = required_params - params.keys 106 | 107 | unless missing.empty? 108 | raise PromptManager::MissingParametersError.new( 109 | "Missing required parameters: #{missing.join(', ')}", 110 | missing_parameters: missing 111 | ) 112 | end 113 | end 114 | 115 | # Usage 116 | validate_parameters(user_params, [:customer_name, :order_id]) 117 | ``` 118 | 119 | ### Pre-flight Checks 120 | 121 | ```ruby 122 | def preflight_check(prompt_id) 123 | # Check if prompt exists 124 | unless PromptManager.storage.exist?(prompt_id) 125 | raise PromptManager::PromptNotFoundError, "Prompt '#{prompt_id}' not found" 126 | end 127 | 128 | # Check for circular includes 129 | check_circular_includes(prompt_id) 130 | 131 | # Validate syntax 132 | validate_prompt_syntax(prompt_id) 133 | end 134 | ``` 135 | 136 | ## Logging and Debugging 137 | 138 | ### Enable Debug Logging 139 | 140 | ```ruby 141 | PromptManager.configure do |config| 142 | config.debug = true 143 | config.logger = Logger.new(STDOUT) 144 | end 145 | ``` 146 | 147 | ### Custom Error Handlers 148 | 149 | ```ruby 150 | PromptManager.configure do |config| 151 | config.error_handler = ->(error, context) { 152 | # Custom error handling 153 | ErrorReporter.notify(error, context: context) 154 | 155 | # Return fallback response 156 | case error 157 | when PromptManager::PromptNotFoundError 158 | "Prompt temporarily unavailable" 159 | when PromptManager::MissingParametersError 160 | "Please check your input parameters" 161 | else 162 | "Service temporarily unavailable" 163 | end 164 | } 165 | end 166 | ``` 167 | 168 | ## Testing Error Conditions 169 | 170 | ### RSpec Examples 171 | 172 | ```ruby 173 | describe "Error handling" do 174 | it "handles missing prompts gracefully" do 175 | expect { 176 | PromptManager::Prompt.new(id: 'nonexistent') 177 | }.to raise_error(PromptManager::PromptNotFoundError) 178 | end 179 | 180 | it "validates required parameters" do 181 | prompt = PromptManager::Prompt.new(id: 'test_prompt') 182 | 183 | expect { 184 | prompt.render # No parameters provided 185 | }.to raise_error(PromptManager::MissingParametersError) 186 | end 187 | end 188 | ``` 189 | 190 | ## Best Practices 191 | 192 | 1. **Always Handle Exceptions**: Never let PromptManager exceptions bubble up unhandled 193 | 2. **Provide Meaningful Fallbacks**: Return sensible defaults when prompts fail 194 | 3. **Log Errors**: Capture error details for debugging and monitoring 195 | 4. **Validate Early**: Check parameters and conditions before processing 196 | 5. **Test Error Paths**: Include error scenarios in your test suite 197 | 6. **Monitor in Production**: Set up alerts for prompt processing failures -------------------------------------------------------------------------------- /test/prompt_manager/storage/active_record_adapter_test.rb: -------------------------------------------------------------------------------- 1 | # test/test_prompt_manager_storage_active_record_adapter.rb 2 | 3 | require 'test_helper' 4 | 5 | require 'active_record' 6 | require 'json' 7 | 8 | 9 | require 'prompt_manager/storage/active_record_adapter' 10 | 11 | ############################################################ 12 | ### 13 | ## Setup the database from the application's point of view 14 | # 15 | 16 | ActiveRecord::Base 17 | .establish_connection( 18 | adapter: 'sqlite3', 19 | database: ':memory:' 20 | ) 21 | 22 | ActiveRecord::Schema.define do 23 | create_table :db_prompts do |t| 24 | t.string :prompt_name 25 | t.string :prompt_text 26 | t.text :prompt_params 27 | end 28 | end 29 | 30 | 31 | # Within a Rails application this could be ApplicationRecord 32 | class DbPrompt < ActiveRecord::Base 33 | serialize :prompt_params 34 | end 35 | 36 | # 37 | ## database setyo frin aookucatuib's POV 38 | ### 39 | ############################################################ 40 | 41 | 42 | class TestActiveRecordAdapter < Minitest::Test 43 | def setup 44 | # The @storage_adapter object used by the PromptManager::Prompt class 45 | # is an instance of the storage adapter class. 46 | @adapter = PromptManager::Storage::ActiveRecordAdapter.config do |config| 47 | config.model = DbPrompt 48 | config.id_column = :prompt_name 49 | config.text_column = :prompt_text 50 | config.parameters_column = :prompt_params 51 | end.new 52 | end 53 | 54 | 55 | ################################################# 56 | 57 | def test_config 58 | assert_equal DbPrompt, @adapter.model 59 | assert_equal :prompt_name, @adapter.id_column 60 | assert_equal :prompt_text, @adapter.text_column 61 | assert_equal :prompt_params, @adapter.parameters_column 62 | end 63 | 64 | 65 | def test_save 66 | @adapter.save(id: 'example_name', text: 'Example prompt', parameters: { size: 'large' }) 67 | 68 | prompt_record = DbPrompt.find_by(prompt_name: 'example_name') 69 | 70 | assert prompt_record 71 | assert_equal 'Example prompt', prompt_record.prompt_text 72 | assert_equal({size: 'large'}, prompt_record.prompt_params) # Updated expectation to match ActiveRecord's behavior 73 | end 74 | 75 | 76 | def test_get 77 | prompt_id = "example_name_#{rand(10000)}" 78 | 79 | DbPrompt.destroy(id: prompt_id) rescue 80 | 81 | DbPrompt.create( 82 | prompt_name: prompt_id, 83 | prompt_text: 'Updated prompt', 84 | prompt_params: { size: 'large' }.to_json 85 | ) 86 | 87 | # The result is a Hash having the three keys expected 88 | # by the PromptManager::Prompt class 89 | result = @adapter.get(id: prompt_id) 90 | 91 | expected_parameters = { size: 'large' } 92 | 93 | assert_equal Hash, result.class 94 | assert_equal prompt_id, result[:id] 95 | assert_equal 'Updated prompt', result[:text] 96 | assert_equal expected_parameters, result[:parameters].symbolize_keys 97 | end 98 | 99 | 100 | def test_list 101 | DbPrompt.create(prompt_name: 'example_name_1', prompt_text: 'Example prompt 1') 102 | DbPrompt.create(prompt_name: 'example_name_2', prompt_text: 'Example prompt 2') 103 | 104 | ids = @adapter.list 105 | assert_includes ids, 'example_name_1' 106 | assert_includes ids, 'example_name_2' 107 | end 108 | 109 | 110 | def test_delete 111 | DbPrompt.find_or_initialize_by( 112 | prompt_name: 'delete_me', 113 | prompt_text: 'Example prompt to be deleted' 114 | ).save 115 | 116 | assert @adapter.get(id: 'delete_me') 117 | 118 | @adapter.delete(id: 'delete_me') 119 | 120 | assert_raises ArgumentError do 121 | @adapter.get(id: 'delete_me') 122 | end 123 | end 124 | 125 | 126 | def test_save_with_existing_record 127 | @adapter.save(id: 'example_name', text: 'Updated prompt', parameters: { size: 'small' }) 128 | prompt_record = DbPrompt.find_by(prompt_name: 'example_name') 129 | assert_equal 'Updated prompt', prompt_record.prompt_text 130 | assert_equal({ size: 'small' }, prompt_record.prompt_params) 131 | end 132 | 133 | 134 | def test_create 135 | DbPrompt.create(prompt_name: 'example_name_1', prompt_text: 'Example prompt 1') 136 | DbPrompt.create(prompt_name: 'example_name_2', prompt_text: 'Another example 2') 137 | 138 | search_result = @adapter.search('Another') 139 | assert_includes search_result, 'example_name_2' 140 | refute_includes search_result, 'example_name_1' 141 | end 142 | 143 | # Add tests to cover missing method behavior and validation of configuration. 144 | 145 | # Add test for method_missing 146 | def test_method_missing_delegates_to_record 147 | # DbPrompt.create(prompt_name: 'example_name', prompt_text: 'Example prompt') 148 | # @adapter.get(id: 'example_name') 149 | 150 | assert_respond_to @adapter, :where # Assuming `where` is the method missing 151 | # assert_equal 'Example prompt', @adapter.prompt_text 152 | end 153 | 154 | 155 | def test_respond_to_missing_handles_record_methods 156 | assert_respond_to @adapter, :find_by_prompt_name # Assuming record will have a `find_by_prompt_name` method 157 | end 158 | 159 | 160 | def test_validate_configuration 161 | assert_raises(ArgumentError) do 162 | PromptManager::Storage::ActiveRecordAdapter.config do |config| 163 | config.model = nil 164 | end 165 | end 166 | end 167 | end 168 | 169 | -------------------------------------------------------------------------------- /lib/prompt_manager/storage/active_record_adapter.rb: -------------------------------------------------------------------------------- 1 | # prompt_manager/lib/prompt_manager/storage/active_record_adapter.rb 2 | 3 | # This class acts as an adapter for interacting with an ActiveRecord model 4 | require 'active_record' 5 | # to manage storage operations for PromptManager::Prompt instances. It defines 6 | # methods that allow for saving, searching, retrieving by ID, and deleting 7 | # prompts. 8 | # 9 | # To use this adapter, you must configure it with an ActiveRecord model and 10 | # the column names for ID, text content, and parameters. The adapter will 11 | # handle serialization and deserialization of parameters. 12 | # 13 | # This adapter is used by PromptManager::Prompt as its storage backend, enabling CRUD operations on persistent prompt data. 14 | 15 | class PromptManager::Storage::ActiveRecordAdapter 16 | 17 | class << self 18 | # Configure the ActiveRecord model and column mappings 19 | attr_accessor :model, 20 | :id_column, 21 | :text_column, 22 | :parameters_column 23 | 24 | # Configure the adapter with the required settings 25 | # Must be called with a block before using the adapter 26 | def config 27 | if block_given? 28 | yield self 29 | validate_configuration 30 | else 31 | raise ArgumentError, "No block given to config" 32 | end 33 | 34 | self 35 | end 36 | 37 | # Validate that all required configuration is present and valid 38 | def validate_configuration 39 | validate_model 40 | validate_columns 41 | end 42 | 43 | # Ensure the provided model is a valid ActiveRecord model 44 | def validate_model 45 | raise ArgumentError, "AR Model not set" unless model 46 | raise ArgumentError, "AR Model is not an ActiveRecord model" unless model < ActiveRecord::Base 47 | end 48 | 49 | # Verify that all required columns exist in the model 50 | def validate_columns 51 | columns = model.column_names # Array of Strings 52 | [id_column, text_column, parameters_column].each do |column| 53 | raise ArgumentError, "#{column} is not a valid column for model #{model}" unless columns.include?(column.to_s) 54 | end 55 | end 56 | 57 | # Delegate unknown methods to the ActiveRecord model 58 | def method_missing(method_name, *args, &block) 59 | if model.respond_to?(method_name) 60 | model.send(method_name, *args, &block) 61 | else 62 | super 63 | end 64 | end 65 | 66 | # Support respond_to? for delegated methods 67 | def respond_to_missing?(method_name, include_private = false) 68 | model.respond_to?(method_name, include_private) || super 69 | end 70 | end 71 | 72 | 73 | ############################################## 74 | # The ActiveRecord object representing the current prompt 75 | attr_accessor :record 76 | 77 | # Accessor methods to avoid repeated self.class prefixes 78 | def model = self.class.model 79 | def id_column = self.class.id_column 80 | def text_column = self.class.text_column 81 | def parameters_column = self.class.parameters_column 82 | 83 | # Initialize the adapter and validate configuration 84 | def initialize 85 | self.class.send(:validate_configuration) # send gets around private designations of a method 86 | @record = model.first 87 | end 88 | 89 | # Retrieve a prompt by its ID 90 | # Returns a hash with id, text, and parameters 91 | def get(id:) 92 | @record = model.find_by(id_column => id) 93 | raise ArgumentError, "Prompt not found with id: #{id}" unless @record 94 | 95 | # Handle case where parameters might be stored as a JSON string 96 | # instead of a native Hash 97 | parameters = @record[parameters_column] 98 | 99 | if parameters.is_a? String 100 | parameters = JSON.parse parameters 101 | end 102 | 103 | { 104 | id: id, 105 | text: @record[text_column], 106 | parameters: parameters 107 | } 108 | end 109 | 110 | # Save a prompt with the given ID, text, and parameters 111 | # Creates a new record if one doesn't exist, otherwise updates existing record 112 | def save(id:, text: "", parameters: {}) 113 | @record = model.find_or_initialize_by(id_column => id) 114 | 115 | @record[text_column] = text 116 | @record[parameters_column] = parameters 117 | @record.save! 118 | end 119 | 120 | # Delete a prompt with the given ID 121 | def delete(id:) 122 | @record = model.find_by(id_column => id) 123 | @record&.destroy 124 | end 125 | 126 | # Return an array of all prompt IDs 127 | def list(*) 128 | model.all.pluck(id_column) 129 | end 130 | 131 | # Search for prompts containing the given text 132 | # Returns an array of matching prompt IDs 133 | def search(for_what) 134 | model.where("#{text_column} LIKE ?", "%#{for_what}%").pluck(id_column) 135 | end 136 | 137 | ############################################## 138 | private 139 | 140 | # Delegate unknown methods to the current record 141 | def method_missing(method_name, *args, &block) 142 | if @record && @record.respond_to?(method_name) 143 | @record.send(method_name, *args, &block) 144 | elsif model.respond_to?(method_name) 145 | model.send(method_name, *args, &block) 146 | else 147 | super 148 | end 149 | end 150 | 151 | # Support respond_to? for delegated methods 152 | def respond_to_missing?(method_name, include_private = false) 153 | (model.respond_to?(method_name, include_private) || 154 | (@record && @record.respond_to?(method_name, include_private)) || 155 | super) 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /docs/storage/filesystem-adapter.md: -------------------------------------------------------------------------------- 1 | # FileSystemAdapter 2 | 3 | The FileSystemAdapter is the default storage adapter for PromptManager, storing prompts as files in a directory structure. 4 | 5 | ## Overview 6 | 7 | The FileSystemAdapter stores prompts as individual text files in a configurable directory. This is the simplest and most common storage method. 8 | 9 | ## Configuration 10 | 11 | ### Basic Setup 12 | 13 | ```ruby 14 | require 'prompt_manager' 15 | 16 | # Use default filesystem storage (~/prompts_dir/) 17 | prompt = PromptManager::Prompt.new(id: 'welcome_message') 18 | ``` 19 | 20 | ### Custom Directory 21 | 22 | ```ruby 23 | PromptManager.configure do |config| 24 | config.storage = PromptManager::Storage::FileSystemAdapter.new( 25 | prompts_dir: '/path/to/your/prompts' 26 | ) 27 | end 28 | ``` 29 | 30 | ### Multiple Directories 31 | 32 | ```ruby 33 | # Search multiple directories in order 34 | PromptManager.configure do |config| 35 | config.storage = PromptManager::Storage::FileSystemAdapter.new( 36 | prompts_dir: [ 37 | '/home/user/project_prompts', 38 | '/shared/common_prompts', 39 | '/system/default_prompts' 40 | ] 41 | ) 42 | end 43 | ``` 44 | 45 | ## Directory Structure 46 | 47 | ### Basic Structure 48 | 49 | ``` 50 | prompts_dir/ 51 | ├── welcome_message.txt 52 | ├── error_response.txt 53 | ├── customer_service/ 54 | │ ├── greeting.txt 55 | │ └── farewell.txt 56 | └── templates/ 57 | ├── email_header.txt 58 | └── email_footer.txt 59 | ``` 60 | 61 | ### Organizing Prompts 62 | 63 | ```ruby 64 | # Access nested prompts using path separators 65 | customer_greeting = PromptManager::Prompt.new(id: 'customer_service/greeting') 66 | email_header = PromptManager::Prompt.new(id: 'templates/email_header') 67 | ``` 68 | 69 | ## File Operations 70 | 71 | ### Creating Prompts 72 | 73 | ```ruby 74 | # Prompts are created as .txt files 75 | prompt = PromptManager::Prompt.new(id: 'new_prompt') 76 | prompt.save("Your prompt content here...") 77 | # Creates: prompts_dir/new_prompt.txt 78 | ``` 79 | 80 | ### Reading Prompts 81 | 82 | ```ruby 83 | # Automatically reads from filesystem 84 | prompt = PromptManager::Prompt.new(id: 'existing_prompt') 85 | content = prompt.render 86 | ``` 87 | 88 | ### Updating Prompts 89 | 90 | ```ruby 91 | # Modify the file directly or use save method 92 | prompt = PromptManager::Prompt.new(id: 'existing_prompt') 93 | prompt.save("Updated content...") 94 | ``` 95 | 96 | ### Deleting Prompts 97 | 98 | ```ruby 99 | # Remove the prompt file 100 | prompt = PromptManager::Prompt.new(id: 'old_prompt') 101 | prompt.delete 102 | ``` 103 | 104 | ## Advanced Features 105 | 106 | ### File Extensions 107 | 108 | The adapter supports different file extensions: 109 | 110 | ```ruby 111 | # These all work: 112 | # - welcome.txt 113 | # - welcome.md 114 | # - welcome.prompt 115 | # - welcome (no extension) 116 | 117 | prompt = PromptManager::Prompt.new(id: 'welcome') 118 | # Searches for welcome.txt, then welcome.md, etc. 119 | ``` 120 | 121 | ### Directory Search Order 122 | 123 | When using multiple directories, the adapter searches in order: 124 | 125 | ```ruby 126 | config.storage = PromptManager::Storage::FileSystemAdapter.new( 127 | prompts_dir: [ 128 | './project_prompts', # First priority 129 | '~/shared_prompts', # Second priority 130 | '/system/prompts' # Last resort 131 | ] 132 | ) 133 | ``` 134 | 135 | ### Permissions and Security 136 | 137 | ```ruby 138 | # Set directory permissions 139 | config.storage = PromptManager::Storage::FileSystemAdapter.new( 140 | prompts_dir: '/secure/prompts', 141 | file_mode: 0600, # Read/write for owner only 142 | dir_mode: 0700 # Access for owner only 143 | ) 144 | ``` 145 | 146 | ## Error Handling 147 | 148 | ### Common Issues 149 | 150 | ```ruby 151 | begin 152 | prompt = PromptManager::Prompt.new(id: 'missing_prompt') 153 | rescue PromptManager::PromptNotFoundError => e 154 | puts "Prompt file not found: #{e.message}" 155 | end 156 | 157 | begin 158 | prompt.save("content") 159 | rescue PromptManager::StorageError => e 160 | puts "Cannot write file: #{e.message}" 161 | # Check permissions, disk space, etc. 162 | end 163 | ``` 164 | 165 | ### File System Monitoring 166 | 167 | ```ruby 168 | # Watch for file changes (requires additional gems) 169 | require 'listen' 170 | 171 | listener = Listen.to('/path/to/prompts') do |modified, added, removed| 172 | puts "Prompts changed: #{modified + added + removed}" 173 | # Reload prompts if needed 174 | end 175 | 176 | listener.start 177 | ``` 178 | 179 | ## Performance Considerations 180 | 181 | ### Caching 182 | 183 | ```ruby 184 | # Enable file content caching 185 | PromptManager.configure do |config| 186 | config.cache_prompts = true 187 | config.cache_ttl = 300 # 5 minutes 188 | end 189 | ``` 190 | 191 | ### Large Directories 192 | 193 | For directories with many prompts: 194 | 195 | ```ruby 196 | # Use indexing for faster lookups 197 | config.storage = PromptManager::Storage::FileSystemAdapter.new( 198 | prompts_dir: '/large/prompt/directory', 199 | enable_indexing: true, 200 | index_file: '.prompt_index' 201 | ) 202 | ``` 203 | 204 | ## Best Practices 205 | 206 | 1. **Organize by Purpose**: Use subdirectories to group related prompts 207 | 2. **Consistent Naming**: Use clear, descriptive prompt IDs 208 | 3. **Version Control**: Store your prompts directory in git 209 | 4. **Backup Strategy**: Regular backups of your prompts directory 210 | 5. **File Permissions**: Secure sensitive prompts with appropriate permissions 211 | 6. **Documentation**: Use comments in prompt files to document purpose and usage 212 | 213 | ## Migration from Other Storage 214 | 215 | ### From Database 216 | 217 | ```ruby 218 | # Export database prompts to filesystem 219 | database_adapter.all_prompts.each do |prompt_id, content| 220 | file_path = File.join(prompts_dir, "#{prompt_id}.txt") 221 | File.write(file_path, content) 222 | end 223 | ``` 224 | 225 | ### Bulk Import 226 | 227 | ```ruby 228 | # Import multiple files 229 | Dir.glob('/old/prompts/*.txt').each do |file_path| 230 | prompt_id = File.basename(file_path, '.txt') 231 | content = File.read(file_path) 232 | 233 | prompt = PromptManager::Prompt.new(id: prompt_id) 234 | prompt.save(content) 235 | end 236 | ``` -------------------------------------------------------------------------------- /lib/prompt_manager/prompt.rb: -------------------------------------------------------------------------------- 1 | # prompt_manager/lib/prompt_manager/prompt.rb 2 | 3 | require_relative "directive_processor" 4 | 5 | class PromptManager::Prompt 6 | COMMENT_SIGNAL = '#' # lines beginning with this are a comment 7 | DIRECTIVE_SIGNAL = '//' # Like the old IBM JCL 8 | DEFAULT_PARAMETER_REGEX = /(\[[A-Z _|]+\])/ 9 | @parameter_regex = DEFAULT_PARAMETER_REGEX 10 | 11 | ############################################## 12 | ## Public class methods 13 | 14 | class << self 15 | attr_accessor :storage_adapter, :parameter_regex 16 | 17 | def get(id:) 18 | storage_adapter.get(id: id) # Return the hash directly from storage 19 | end 20 | 21 | def create(id:, text: "", parameters: {}) 22 | storage_adapter.save( 23 | id: id, 24 | text: text, 25 | parameters: parameters 26 | ) 27 | 28 | ::PromptManager::Prompt.new(id: id, context: [], directives_processor: PromptManager::DirectiveProcessor.new) 29 | end 30 | 31 | def find(id:) 32 | ::PromptManager::Prompt.new(id: id, context: [], directives_processor: PromptManager::DirectiveProcessor.new) 33 | end 34 | 35 | def destroy(id:) 36 | prompt = find(id: id) 37 | prompt.delete 38 | end 39 | 40 | def search(for_what) 41 | storage_adapter.search(for_what) 42 | end 43 | 44 | def method_missing(method_name, *args, &block) 45 | if storage_adapter.respond_to?(method_name) 46 | storage_adapter.send(method_name, *args, &block) 47 | else 48 | super 49 | end 50 | end 51 | 52 | def respond_to_missing?(method_name, include_private = false) 53 | storage_adapter.respond_to?(method_name, include_private) || super 54 | end 55 | end 56 | 57 | ############################################## 58 | ## Public Instance Methods 59 | 60 | attr_accessor :id, # String name for the prompt 61 | :text, # String, full text of the prompt 62 | :parameters # Hash, Key and Value are Strings 63 | 64 | 65 | def initialize( 66 | id: nil, # A String name for the prompt 67 | context: [], # TODO: Array of Strings or Pathname? 68 | directives_processor: PromptManager::DirectiveProcessor.new, 69 | external_binding: binding, 70 | erb_flag: false, # replace $ENVAR and ${ENVAR} when true 71 | envar_flag: false # process ERB against the external_binding when true 72 | ) 73 | 74 | @id = id 75 | @directives_processor = directives_processor 76 | 77 | validate_arguments(@id) 78 | 79 | @record = db.get(id: id) 80 | @text = @record[:text] || "" 81 | @parameters = @record[:parameters] || {} 82 | @directives = {} 83 | @external_binding = external_binding 84 | @erb_flag = erb_flag 85 | @envar_flag = envar_flag 86 | end 87 | 88 | def validate_arguments(prompt_id, prompts_db=db) 89 | raise ArgumentError, 'id cannot be blank' if prompt_id.nil? || prompt_id.strip.empty? 90 | raise(ArgumentError, 'storage_adapter is not set') if prompts_db.nil? 91 | end 92 | 93 | def to_s 94 | processed_text = remove_comments 95 | processed_text = substitute_values(processed_text, @parameters) 96 | processed_text = substitute_env_vars(processed_text) 97 | processed_text = process_directives(processed_text) 98 | process_erb(processed_text) 99 | end 100 | 101 | def save 102 | db.save( 103 | id: id, 104 | text: text, # Save the original text 105 | parameters: parameters 106 | ) 107 | end 108 | 109 | def delete = db.delete(id: id) 110 | 111 | 112 | ###################################### 113 | private 114 | 115 | def db = self.class.storage_adapter 116 | 117 | 118 | def remove_comments 119 | lines = @text.gsub(//m, '').lines(chomp: true) 120 | markdown_block_depth = 0 121 | filtered_lines = [] 122 | end_index = lines.index("__END__") || lines.size 123 | 124 | lines[0...end_index].each do |line| 125 | trimmed_line = line.strip 126 | 127 | if trimmed_line.start_with?('```') 128 | if trimmed_line == '```markdown' 129 | markdown_block_depth += 1 130 | elsif markdown_block_depth > 0 131 | markdown_block_depth -= 1 132 | end 133 | end 134 | 135 | if markdown_block_depth > 0 || !trimmed_line.start_with?(COMMENT_SIGNAL) 136 | filtered_lines << line 137 | end 138 | end 139 | 140 | filtered_lines.join("\n") 141 | end 142 | 143 | 144 | 145 | 146 | def substitute_values(input_text, values_hash) 147 | if values_hash.is_a?(Hash) && !values_hash.empty? 148 | values_hash.each do |key, value| 149 | value = value.last if value.is_a?(Array) 150 | input_text = input_text.gsub(key, value.nil? ? '' : value) 151 | end 152 | end 153 | input_text 154 | end 155 | 156 | def erb? = @erb_flag 157 | def envar? = @envar_flag 158 | 159 | def substitute_env_vars(input_text) 160 | return input_text unless envar? 161 | 162 | # First, handle shell command substitution $(command) 163 | input_text = input_text.gsub(/\$\(([^\)]+)\)/) do |match| 164 | cmd = $1.strip 165 | begin 166 | # Execute the shell command and capture its output 167 | result = `#{cmd}`.chomp 168 | result.empty? ? match : result 169 | rescue => e 170 | # If command execution fails, log the error and keep the original text 171 | warn "Shell command execution failed: #{e.message}" 172 | match 173 | end 174 | end 175 | 176 | # Then handle environment variables as before 177 | input_text.gsub(/\$(\w+)|\$\{(\w+)\}/) do |match| 178 | env_var = $1 || $2 179 | ENV[env_var] || match 180 | end 181 | end 182 | 183 | def process_directives(input_text) 184 | directive_lines = input_text.split("\n").select { |line| line.strip.start_with?(DIRECTIVE_SIGNAL) } 185 | @directives = directive_lines.each_with_object({}) { |line, hash| hash[line.strip] = "" } 186 | @directives = @directives_processor.run(@directives) 187 | substitute_values(input_text, @directives) 188 | end 189 | 190 | def process_erb(input_text) 191 | return input_text unless erb? 192 | 193 | ERB.new(input_text).result(@external_binding) 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /test/prompt_manager/prompt_test.rb: -------------------------------------------------------------------------------- 1 | # prompt_manager/test/prompt_manager/prompt_test.rb 2 | 3 | require 'test_helper' 4 | require 'fileutils' 5 | require 'tmpdir' 6 | require 'pathname' 7 | 8 | debug_me{[ 9 | "PromptManager::Prompt.storage_adapter" 10 | ]} 11 | 12 | 13 | class PromptTest < Minitest::Test 14 | # Save original adapter settings to restore them after tests 15 | def setup 16 | # Save original settings 17 | @original_prompts_dir = PromptManager::Storage::FileSystemAdapter.prompts_dir 18 | @original_env = ENV.to_hash 19 | 20 | # Create a dedicated test directory for each test 21 | @test_dir = Dir.mktmpdir('prompt_test_') 22 | @test_prompts_dir = Pathname.new(File.join(@test_dir, 'prompts')) 23 | FileUtils.mkdir_p(@test_prompts_dir) 24 | 25 | # Temporarily change the prompts_dir for all tests in this run 26 | # We're using the same adapter class, just pointing it to a different directory 27 | PromptManager::Storage::FileSystemAdapter.prompts_dir = @test_prompts_dir 28 | end 29 | 30 | def teardown 31 | # Restore original adapter settings and clean up test directory 32 | PromptManager::Storage::FileSystemAdapter.prompts_dir = @original_prompts_dir 33 | ENV.replace(@original_env) 34 | 35 | FileUtils.remove_entry(@test_dir) if @test_dir && File.exist?(@test_dir) 36 | end 37 | 38 | def test_prompt_initialization_with_invalid_id 39 | assert_raises ArgumentError do 40 | PromptManager::Prompt.new(id: nil, context: [], directives_processor: PromptManager::DirectiveProcessor.new) 41 | end 42 | end 43 | 44 | def test_class_constants 45 | assert_equal '#', PromptManager::Prompt::COMMENT_SIGNAL 46 | assert_equal '//', PromptManager::Prompt::DIRECTIVE_SIGNAL 47 | end 48 | 49 | def test_prompt_initialization_raises_argument_error_when_id_blank 50 | assert_raises ArgumentError do 51 | PromptManager::Prompt.new(id: '', context: [], directives_processor: PromptManager::DirectiveProcessor.new) 52 | end 53 | end 54 | 55 | def test_prompt_initialization_raises_argument_error_when_no_storage_adapter_set 56 | original_adapter = PromptManager::Prompt.storage_adapter 57 | begin 58 | PromptManager::Prompt.storage_adapter = nil 59 | assert_raises(ArgumentError, 'storage_adapter is not set') do 60 | PromptManager::Prompt.new(id: 'test_prompt', context: [], directives_processor: PromptManager::DirectiveProcessor.new) 61 | end 62 | ensure 63 | PromptManager::Prompt.storage_adapter = original_adapter 64 | end 65 | end 66 | 67 | def test_prompt_initialization_with_valid_id 68 | # Create the test prompt files first 69 | create_test_prompt('test_prompt', 'Hello, World!', {}) 70 | 71 | prompt = PromptManager::Prompt.new(id: 'test_prompt', context: [], directives_processor: PromptManager::DirectiveProcessor.new) 72 | assert_equal 'test_prompt', prompt.id 73 | end 74 | 75 | def test_prompt_to_s_method 76 | # Create the test prompt files first 77 | create_test_prompt('test_prompt', 'Hello, World!', {}) 78 | 79 | prompt = PromptManager::Prompt.new(id: 'test_prompt', context: [], directives_processor: PromptManager::DirectiveProcessor.new) 80 | # Build removes comments and directives, but keeps __END__ etc. 81 | # EDIT: Now removes __END__ and subsequent lines. 82 | expected = "Hello, World!" 83 | assert_equal expected, prompt.to_s 84 | end 85 | 86 | def test_prompt_saves_to_storage 87 | new_prompt_id = 'new_prompt' 88 | new_prompt_text = "How are you, [NAME]?" 89 | new_prompt_parameters = { '[NAME]' => ['Rubyist'] } 90 | 91 | PromptManager::Prompt.create( 92 | id: new_prompt_id, 93 | text: new_prompt_text, 94 | parameters: new_prompt_parameters 95 | ) 96 | 97 | prompt_from_storage = PromptManager::Prompt.get(id: 'new_prompt') 98 | 99 | assert_equal new_prompt_text, prompt_from_storage[:text] 100 | assert_equal new_prompt_parameters, prompt_from_storage[:parameters] 101 | end 102 | 103 | def test_prompt_deletes_from_storage 104 | # Create a prompt first 105 | test_id = 'delete_test_prompt' 106 | create_test_prompt(test_id, 'Hello, I will be deleted', {}) 107 | 108 | prompt = PromptManager::Prompt.find(id: test_id) 109 | assert_equal test_id, prompt.id # Verify it exists 110 | 111 | prompt.delete 112 | 113 | assert_raises(ArgumentError) do 114 | PromptManager::Prompt.get(id: test_id) # Should raise error when prompt not found 115 | end 116 | end 117 | 118 | def test_prompt_searches_storage 119 | # Create multiple prompts to search through 120 | create_test_prompt('search_test_1', 'Hello, this is the first test prompt', {}) 121 | create_test_prompt('search_test_2', 'Goodbye, this is the second test prompt', {}) 122 | create_test_prompt('search_test_3', 'Hello again, this is the third test prompt', {}) 123 | 124 | # Search for a term that should match two prompts 125 | search_results = PromptManager::Prompt.search('Hello') 126 | 127 | refute_empty search_results 128 | assert_includes search_results, 'search_test_1' 129 | assert_includes search_results, 'search_test_3' 130 | refute_includes search_results, 'search_test_2' 131 | end 132 | 133 | private 134 | 135 | # Helper method to create test prompt files 136 | def create_test_prompt(id, text, parameters) 137 | # Get file extensions from the adapter 138 | prompt_ext = PromptManager::Storage::FileSystemAdapter.prompt_extension 139 | params_ext = PromptManager::Storage::FileSystemAdapter.params_extension 140 | 141 | # Create the prompt and parameter files in the test directory 142 | prompt_path = @test_prompts_dir.join("#{id}#{prompt_ext}") 143 | params_path = @test_prompts_dir.join("#{id}#{params_ext}") 144 | 145 | File.write(prompt_path, text) 146 | File.write(params_path, parameters.to_json) 147 | end 148 | 149 | 150 | def test_env_variable_replacement 151 | ENV['GREETING'] = 'Hello' 152 | prompt_text = 'Say $GREETING to world!' 153 | prompt = PromptManager::Prompt.new(prompt_text) 154 | result = prompt.process 155 | assert_equal 'Say Hello to world!', result 156 | end 157 | 158 | def test_erb_processing 159 | prompt_text = '2+2 is <%= 2+2 %>' 160 | prompt = PromptManager::Prompt.new(prompt_text) 161 | result = prompt.process 162 | assert_equal '2+2 is 4', result 163 | end 164 | 165 | def test_combined_features 166 | ENV['NAME'] = 'Alice' 167 | prompt_text = 'Hi, $NAME! Today, 3*3 equals <%= 3*3 %>.' 168 | prompt = PromptManager::Prompt.new(prompt_text) 169 | result = prompt.process 170 | assert_equal 'Hi, Alice! Today, 3*3 equals 9.', result 171 | end 172 | 173 | end 174 | 175 | __END__ 176 | -------------------------------------------------------------------------------- /test/prompt_manager/storage/file_system_adapter_test.rb: -------------------------------------------------------------------------------- 1 | # prompt_manager/test/prompt_manager/storage/file_system_adapter_test.rb 2 | 3 | require 'test_helper' 4 | 5 | require 'prompt_manager/storage/file_system_adapter' 6 | 7 | # Lets create a shortcut ... 8 | FSA = PromptManager::Storage::FileSystemAdapter 9 | 10 | 11 | class FileSystemAdapterTest < Minitest::Test 12 | def setup 13 | @prompts_dir = $PROMPTS_DIR # defined in test_helper 14 | @prompt_id = 'test_prompt' 15 | 16 | # An instance pf a stprage adapter class 17 | @adapter = FSA.config do |o| 18 | o.prompts_dir = $PROMPTS_DIR 19 | end.new 20 | directive_example_filename = 'directive_example' + PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION 21 | directive_example_file = File.join(@prompts_dir, directive_example_filename) 22 | File.delete(directive_example_file) if File.exist?(directive_example_file) 23 | end 24 | 25 | def test_get_non_existent_prompt 26 | assert_raises ArgumentError do 27 | @adapter.get(id: 'non_existent') 28 | end 29 | end 30 | 31 | def test_delete_non_existent_prompt 32 | assert_raises Errno::ENOENT do 33 | @adapter.delete(id: 'non_existent') 34 | end 35 | end 36 | 37 | def test_save_with_invalid_id 38 | assert_raises Errno::ENOENT do 39 | @adapter.save(id: 'invalid/id', text: 'text', parameters: {}) 40 | end 41 | end 42 | 43 | 44 | def teardown 45 | # what should be torn down? 46 | end 47 | 48 | ############################################ 49 | def test_config 50 | assert_equal FSA, PromptManager::Storage::FileSystemAdapter 51 | 52 | assert FSA.respond_to? :config 53 | 54 | assert_equal FSA.prompts_dir, $PROMPTS_DIR 55 | # SMELL: assert_equal FSA.search_proc, FSA::SEARCH_PROC 56 | assert_equal FSA.prompt_extension, FSA::PROMPT_EXTENSION 57 | assert_equal FSA.params_extension, FSA::PARAMS_EXTENSION 58 | end 59 | 60 | 61 | def test_config_without_a_block 62 | assert_raises ArgumentError do 63 | FSA.config 64 | end 65 | end 66 | 67 | 68 | ############################################ 69 | def test_list 70 | assert FSA.respond_to? :list 71 | assert @adapter.respond_to? :list 72 | 73 | result = @adapter.list 74 | 75 | assert result.is_a?(Array) 76 | assert result.first.is_a?(String) 77 | assert result.include?('todo') 78 | assert result.include?('toy/8-ball') 79 | 80 | class_result = FSA.list 81 | 82 | assert class_result.is_a?(Array) 83 | assert class_result.first.is_a?(String) 84 | assert class_result.include?('todo') 85 | assert class_result.include?('toy/8-ball') 86 | end 87 | 88 | 89 | ############################################ 90 | def test_path 91 | assert FSA.respond_to? :path 92 | assert @adapter.respond_to? :path 93 | 94 | class_result = FSA.path('todo') 95 | result = @adapter.path('todo') 96 | 97 | assert_equal class_result, result 98 | 99 | assert_equal result.parent, @prompts_dir 100 | assert_equal result.extname.to_s, FSA.prompt_extension 101 | assert_equal result.basename.to_s.split('.').first, 'todo' 102 | end 103 | 104 | 105 | ############################################ 106 | def test_get 107 | # Setup 108 | expected_text = 'This is a prompt with [SIZE] and [COLOR].' 109 | expected_params = { 110 | '[SIZE]' => [20], 111 | '[COLOR]' => ['blue'] 112 | } 113 | 114 | prompt_path = @prompts_dir + (@prompt_id + FSA.prompt_extension) 115 | params_path = @prompts_dir + (@prompt_id + FSA.params_extension) 116 | 117 | prompt_path.write(expected_text) 118 | params_path.write(expected_params.to_json) 119 | 120 | # Exercise 121 | result = @adapter.get(id: @prompt_id) 122 | 123 | # Verify 124 | assert_equal expected_text, result[:text] 125 | assert_equal expected_params, result[:parameters] 126 | end 127 | 128 | 129 | ############################################ 130 | def test_save 131 | # Setup 132 | text = 'New prompt text' 133 | parameters = {difficulty: 'hard', time: 30} 134 | 135 | # Exercise 136 | @adapter.save(id: @prompt_id, text: text, parameters: parameters) 137 | 138 | # Verify 139 | assert File.exist?(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION)) 140 | assert File.exist?(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PARAMS_EXTENSION)) 141 | assert_equal text, File.read(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION)) 142 | assert_equal parameters, JSON.parse(File.read(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PARAMS_EXTENSION)), symbolize_names: true) 143 | end 144 | 145 | 146 | ############################################ 147 | def test_delete 148 | # Setup 149 | # Creating the files to be deleted 150 | File.write(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION), 'To be deleted') 151 | File.write(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PARAMS_EXTENSION), {to_be: 'deleted'}.to_json) 152 | 153 | # Exercise 154 | @adapter.delete(id: @prompt_id) 155 | 156 | # Verify 157 | refute File.exist?(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION)) 158 | refute File.exist?(File.join(@prompts_dir, @prompt_id + PromptManager::Storage::FileSystemAdapter::PARAMS_EXTENSION)) 159 | end 160 | 161 | 162 | ############################################ 163 | def test_search_proc 164 | search_term = "MadBomber" 165 | saved_search_proc = @adapter.class.search_proc 166 | 167 | # search_proc is a way to use command line tools like 168 | # grep, rg, aq, ack, etc or anything else that makes 169 | # sense that will return a list of prompt IDs. 170 | # In the case of the FileSystemAdapter the ID is 171 | # the basename of the file snns its extension. 172 | @adapter.class.search_proc = ->(q) { ["hello #{q}"] } 173 | 174 | expected = ["hello madbomber"] # NOTE: query term is all lowercase 175 | results = @adapter.search(search_term) 176 | 177 | assert_equal expected, results 178 | 179 | @adapter.class.search_proc = saved_search_proc 180 | end 181 | 182 | 183 | ############################################ 184 | def test_search 185 | search_term = "hello" 186 | 187 | expected = %w[ 188 | also_included 189 | hello_prompt 190 | included 191 | test2_prompt 192 | ].sort 193 | 194 | # Exercise 195 | results = @adapter.search(search_term) 196 | 197 | # Verify 198 | assert_equal expected, results 199 | refute_includes results, 'excluded' 200 | end 201 | 202 | # Add more tests for exceptional cases and edge conditions 203 | end 204 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | This guide will get you up and running with PromptManager in just a few minutes. 4 | 5 | ## 1. Install PromptManager 6 | 7 | ```bash 8 | gem install prompt_manager 9 | ``` 10 | 11 | ## 2. Set Up Your First Prompt 12 | 13 | Create a directory for your prompts and your first prompt file: 14 | 15 | ```bash 16 | mkdir ~/.prompts 17 | ``` 18 | 19 | Create your first prompt file: 20 | 21 | === "~/.prompts/greeting.txt" 22 | 23 | ```text 24 | # Description: A friendly greeting prompt 25 | # Keywords: NAME, LANGUAGE 26 | 27 | Hello [NAME]! 28 | 29 | I'm here to help you today. Please let me know how I can assist you, 30 | and I'll respond in [LANGUAGE]. 31 | 32 | What would you like to know about? 33 | ``` 34 | 35 | === "~/.prompts/greeting.json" 36 | 37 | ```json 38 | { 39 | "[NAME]": ["Alice", "Bob", "Charlie"], 40 | "[LANGUAGE]": ["English", "Spanish", "French"] 41 | } 42 | ``` 43 | 44 | ## 3. Basic Usage 45 | 46 | Create a simple Ruby script to use your prompt: 47 | 48 | ```ruby title="quick_example.rb" 49 | #!/usr/bin/env ruby 50 | 51 | require 'prompt_manager' 52 | 53 | # Configure the FileSystem storage adapter 54 | PromptManager::Prompt.storage_adapter = 55 | PromptManager::Storage::FileSystemAdapter.config do |config| 56 | config.prompts_dir = File.expand_path('~/.prompts') 57 | end.new 58 | 59 | # Load your prompt 60 | prompt = PromptManager::Prompt.new(id: 'greeting') 61 | 62 | # Set parameter values 63 | prompt.parameters = { 64 | "[NAME]" => "Alice", 65 | "[LANGUAGE]" => "English" 66 | } 67 | 68 | # Generate the final prompt text 69 | puts "=== Generated Prompt ===" 70 | puts prompt.to_s 71 | 72 | # Save any parameter changes 73 | prompt.save 74 | ``` 75 | 76 | Run it: 77 | 78 | ```bash 79 | ruby quick_example.rb 80 | ``` 81 | 82 | Expected output: 83 | ``` 84 | === Generated Prompt === 85 | Hello Alice! 86 | 87 | I'm here to help you today. Please let me know how I can assist you, 88 | and I'll respond in English. 89 | 90 | What would you like to know about? 91 | ``` 92 | 93 | ## 4. Understanding the Workflow 94 | 95 | The basic PromptManager workflow involves: 96 | 97 | ```mermaid 98 | graph LR 99 | A[Create Prompt File] --> B[Configure Storage] 100 | B --> C[Load Prompt] 101 | C --> D[Set Parameters] 102 | D --> E[Generate Text] 103 | E --> F[Save Changes] 104 | ``` 105 | 106 | ### Step by Step: 107 | 108 | 1. **Create Prompt File**: Write your template with `[KEYWORDS]` 109 | 2. **Configure Storage**: Choose FileSystem or ActiveRecord adapter 110 | 3. **Load Prompt**: Create a Prompt instance with an ID 111 | 4. **Set Parameters**: Provide values for your keywords 112 | 5. **Generate Text**: Call `to_s` to get the final prompt 113 | 6. **Save Changes**: Persist parameter updates 114 | 115 | ## 5. Advanced Quick Start 116 | 117 | Here's a more advanced example showing multiple features: 118 | 119 | ```ruby title="advanced_example.rb" 120 | require 'prompt_manager' 121 | 122 | # Configure storage 123 | PromptManager::Prompt.storage_adapter = 124 | PromptManager::Storage::FileSystemAdapter.config do |config| 125 | config.prompts_dir = File.expand_path('~/.prompts') 126 | end.new 127 | 128 | # Create a prompt with directives and ERB 129 | prompt = PromptManager::Prompt.new( 130 | id: 'advanced_greeting', 131 | erb_flag: true, 132 | envar_flag: true 133 | ) 134 | 135 | # Set parameters 136 | prompt.parameters = { 137 | "[USER_NAME]" => "Alice", 138 | "[TASK_TYPE]" => "translation", 139 | "[URGENCY]" => "high" 140 | } 141 | 142 | # Display available keywords 143 | puts "Available keywords: #{prompt.keywords.join(', ')}" 144 | 145 | # Generate and display the result 146 | puts "\n=== Final Prompt ===" 147 | puts prompt.to_s 148 | 149 | # Save changes 150 | prompt.save 151 | puts "\nPrompt saved successfully!" 152 | ``` 153 | 154 | === "~/.prompts/advanced_greeting.txt" 155 | 156 | ```text 157 | # Advanced greeting with directives and ERB 158 | //include common/header.txt 159 | 160 | Dear [USER_NAME], 161 | 162 | <% if '[URGENCY]' == 'high' %> 163 | 🚨 URGENT: This [TASK_TYPE] request requires immediate attention. 164 | <% else %> 165 | 📋 Standard [TASK_TYPE] request for processing. 166 | <% end %> 167 | 168 | Current system time: <%= Time.now.strftime('%Y-%m-%d %H:%M:%S') %> 169 | Working directory: <%= Dir.pwd %> 170 | 171 | __END__ 172 | This section is ignored - useful for notes and documentation. 173 | ``` 174 | 175 | ## 6. Next Steps 176 | 177 | Now that you have PromptManager working, explore these areas: 178 | 179 | ### Learn Core Features 180 | - [Parameterized Prompts](../core-features/parameterized-prompts.md) - Master keyword substitution 181 | - [Directive Processing](../core-features/directive-processing.md) - Include files and process commands 182 | - [ERB Integration](../core-features/erb-integration.md) - Dynamic templating 183 | 184 | ### Storage Options 185 | - [FileSystem Adapter](../storage/filesystem-adapter.md) - File-based storage 186 | - [ActiveRecord Adapter](../storage/activerecord-adapter.md) - Database storage 187 | - [Custom Adapters](../storage/custom-adapters.md) - Build your own 188 | 189 | ### Advanced Usage 190 | - [Custom Keywords](../advanced/custom-keywords.md) - Define your own keyword patterns 191 | - [Search Integration](../advanced/search-integration.md) - Find prompts quickly 192 | - [Performance Tips](../advanced/performance.md) - Optimize for large collections 193 | 194 | ### Real Examples 195 | - [Basic Examples](../examples/basic.md) - Simple use cases 196 | - [Advanced Examples](../examples/advanced.md) - Complex scenarios 197 | - [Real World Cases](../examples/real-world.md) - Production examples 198 | 199 | ## Common Patterns 200 | 201 | Here are some common patterns you'll use frequently: 202 | 203 | ### Parameter History 204 | ```ruby 205 | # Access parameter history (since v0.3.0) 206 | prompt.parameters["[NAME]"] # Returns ["Alice", "Bob", "Charlie"] 207 | latest_name = prompt.parameters["[NAME]"].last # "Charlie" 208 | ``` 209 | 210 | ### Error Handling 211 | ```ruby 212 | begin 213 | prompt = PromptManager::Prompt.new(id: 'missing') 214 | rescue PromptManager::StorageError => e 215 | puts "Storage error: #{e.message}" 216 | rescue PromptManager::ParameterError => e 217 | puts "Parameter error: #{e.message}" 218 | end 219 | ``` 220 | 221 | ### Search and Discovery 222 | ```ruby 223 | # List all available prompts 224 | prompts = PromptManager::Prompt.list 225 | puts "Available prompts: #{prompts.join(', ')}" 226 | 227 | # Search for prompts (requires search_proc configuration) 228 | results = PromptManager::Prompt.search('greeting') 229 | ``` 230 | 231 | ## Troubleshooting 232 | 233 | ### File Not Found 234 | If you get "file not found" errors, check: 235 | 236 | 1. **Prompt directory exists**: `ls ~/.prompts` 237 | 2. **File has correct extension**: Should be `.txt` by default 238 | 3. **Prompt ID matches filename**: `greeting` looks for `greeting.txt` 239 | 240 | ### Parameter Errors 241 | If parameters aren't substituting: 242 | 243 | 1. **Check keyword format**: Must be `[UPPERCASE]` by default 244 | 2. **Verify parameter keys match**: Case-sensitive matching 245 | 3. **Ensure parameters are set**: Call `prompt.parameters = {...}` 246 | 247 | ### Permission Issues 248 | If you can't write to the prompts directory: 249 | 250 | ```bash 251 | chmod 755 ~/.prompts 252 | chmod 644 ~/.prompts/*.txt 253 | chmod 644 ~/.prompts/*.json 254 | ``` 255 | 256 | Need help? Check our [testing guide](../development/testing.md) or [open an issue](https://github.com/MadBomber/prompt_manager/issues). -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PromptManager Documentation 2 | 3 |
8 | 9 |
13 |
14 | |
15 |
16 | Like an enchanted librarian organizing floating books of knowledge, PromptManager helps you masterfully orchestrate and organize your AI prompts through wisdom and experience. 17 | 18 |Each prompt becomes a living entity that can be categorized, parameterized, and interconnected with golden threads of relationships. 19 | 20 |Key Features21 |
|
33 |