prompt_manager 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +46 -20
- data/lib/prompt_manager/directive_processor.rb +47 -0
- data/lib/prompt_manager/prompt.rb +78 -151
- data/lib/prompt_manager/storage/active_record_adapter.rb +46 -35
- data/lib/prompt_manager/storage/file_system_adapter.rb +58 -53
- data/lib/prompt_manager/storage.rb +28 -1
- data/lib/prompt_manager/version.rb +1 -1
- data/lib/prompt_manager.rb +12 -2
- metadata +4 -7
- data/examples/prompts_dir/directive_example.txt +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 267e48a65e377f2fc3390d3bbff69a6e1f241c96951fc86437f5ab02d2e93daa
|
4
|
+
data.tar.gz: db8af7d8515e72bf12f847e3544a7413a0acb0707a8321bcae7f13e1ffa1ff86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 259cb1494a9390f94d40c669bd87960d9257a9126c3d71cbc6fdcdbaa82dae3e0209f23acc322d019e781abe48090a070fba20761a2ffe8a1818a303c8ef2741
|
7
|
+
data.tar.gz: 97e6ce3eb0743b02804b44c745a2710f15e4048957c7a039c6313b2d061d65660f9d4435465fd0139ff8e348ba841cb4ae543d06a7709391fab19a6419ea1e55
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
## Unreleased
|
2
2
|
|
3
3
|
## Released
|
4
|
+
### [0.5.0] = 2025-03-29
|
5
|
+
- Major refactoring of to improve processing of parameters and directives.
|
6
|
+
- Added PromptManager::DirectiveProcessor as an example of how to implement custom directives.
|
7
|
+
- Added support for //include directive that protects against loops.
|
8
|
+
- Added support for embedding system environment variables.
|
9
|
+
- Added support for ERB processing within a prompt.
|
10
|
+
- Improved test coverage.
|
4
11
|
|
5
12
|
### [0.4.2] = 2024-10-26
|
6
13
|
- Added configurable parameter_regex to customize keyword pattern
|
data/README.md
CHANGED
@@ -9,6 +9,8 @@ Manage the parameterized prompts (text) used in generative AI (aka chatGPT, Open
|
|
9
9
|
|
10
10
|
## Table of Contents
|
11
11
|
|
12
|
+
- [PromptManager](#promptmanager)
|
13
|
+
- [Table of Contents](#table-of-contents)
|
12
14
|
- [Installation](#installation)
|
13
15
|
- [Usage](#usage)
|
14
16
|
- [Overview](#overview)
|
@@ -16,15 +18,15 @@ Manage the parameterized prompts (text) used in generative AI (aka chatGPT, Open
|
|
16
18
|
- [What does a keyword look like?](#what-does-a-keyword-look-like)
|
17
19
|
- [All about directives](#all-about-directives)
|
18
20
|
- [Example Prompt with Directives](#example-prompt-with-directives)
|
19
|
-
- [Accessing Directives](#accessing-directives)
|
21
|
+
- [Accessing Directives and Setting Parameter Values](#accessing-directives-and-setting-parameter-values)
|
20
22
|
- [Dynamic Directives](#dynamic-directives)
|
21
23
|
- [Executing Directives](#executing-directives)
|
22
24
|
- [Comments Are Ignored](#comments-are-ignored)
|
23
25
|
- [Storage Adapters](#storage-adapters)
|
24
26
|
- [FileSystemAdapter](#filesystemadapter)
|
25
27
|
- [Configuration](#configuration)
|
26
|
-
- [
|
27
|
-
- [
|
28
|
+
- [prompts\_dir](#prompts_dir)
|
29
|
+
- [search\_proc](#search_proc)
|
28
30
|
- [File Extensions](#file-extensions)
|
29
31
|
- [Example Prompt Text File](#example-prompt-text-file)
|
30
32
|
- [Example Prompt Parameters JSON File](#example-prompt-parameters-json-file)
|
@@ -32,15 +34,21 @@ Manage the parameterized prompts (text) used in generative AI (aka chatGPT, Open
|
|
32
34
|
- [ActiveRecordAdapter](#activerecordadapter)
|
33
35
|
- [Configuration](#configuration-1)
|
34
36
|
- [model](#model)
|
35
|
-
- [
|
36
|
-
- [
|
37
|
-
- [
|
37
|
+
- [id\_column](#id_column)
|
38
|
+
- [text\_column](#text_column)
|
39
|
+
- [parameters\_column](#parameters_column)
|
38
40
|
- [Other Potential Storage Adapters](#other-potential-storage-adapters)
|
39
41
|
- [Development](#development)
|
40
42
|
- [Contributing](#contributing)
|
41
43
|
- [License](#license)
|
42
44
|
|
43
45
|
<!-- Tocer[finish]: Auto-generated, don't remove. -->
|
46
|
+
### Latest Capabilities
|
47
|
+
- **Directive Processing:** Processes directives such as `//include` (aliased as `//import`) with loop protection.
|
48
|
+
- **ERB Processing:** Supports ERB templating within prompts.
|
49
|
+
- **Environment Variable Embedding:** Automatically substitutes system environment variables in prompts.
|
50
|
+
- **Improved Parameter Handling:** Refactored to maintain a history of parameter values.
|
51
|
+
- **ActiveRecord Adapter:** Facilitates storing and retrieving prompts via an ActiveRecord model.
|
44
52
|
|
45
53
|
## Installation
|
46
54
|
|
@@ -60,11 +68,19 @@ See also [examples/using_search_proc.rb](examples/using_search_proc.rb)
|
|
60
68
|
|
61
69
|
## Overview
|
62
70
|
|
71
|
+
### Prompt Initialization Options
|
72
|
+
- `id`: A String name for the prompt.
|
73
|
+
- `context`: An Array for additional context.
|
74
|
+
- `directives_processor`: An instance of PromptManager::DirectiveProcessor (default), can be customized.
|
75
|
+
- `external_binding`: A Ruby binding to be used for ERB processing.
|
76
|
+
- `erb_flag`: Boolean flag to enable ERB processing in the prompt text.
|
77
|
+
- `envar_flag`: Boolean flag to enable environment variable substitution in the prompt text.
|
78
|
+
|
63
79
|
The `prompt_manager` gem provides functionality to manage prompts that have keywords and directives for use with generative AI processes.
|
64
80
|
|
65
81
|
### Generative AI (gen-AI)
|
66
82
|
|
67
|
-
Gen-AI deals with the conversion (some would say execution) of a human natural language text (the "prompt") into
|
83
|
+
Gen-AI deals with the conversion (some would say execution) of a human natural language text (the "prompt") into something else using what are known as large language models (LLM) such as those available from OpenAI. A parameterized prompt is one in which there are embedded keywords (parameters) which are place holders for other text to be inserted into the prompt.
|
68
84
|
|
69
85
|
The prompt_manager uses a regular expression to identify these keywords within the prompt. It uses the keywords as keys in a `parameters` Hash which is stored with the prompt text in a serialized form - for example as JSON.
|
70
86
|
|
@@ -84,7 +100,7 @@ The regex must include capturing parentheses () to extract the keyword. The defa
|
|
84
100
|
|
85
101
|
A directive is a line in the prompt text that starts with the two characters '//' - slash slash - just like in the old days of IBM JCL - Job Control Language. A prompt can have zero or more directives. Directives can have parameters and can make use of keywords.
|
86
102
|
|
87
|
-
The `prompt_manager` only collects directives. It extracts keywords from directive lines and provides the substitution of those keywords with other text just like it does for the prompt.
|
103
|
+
The `prompt_manager` only collects directives. It extracts keywords from directive lines and provides the substitution of those keywords with other text just like it does for the prompt.
|
88
104
|
|
89
105
|
##### Example Prompt with Directives
|
90
106
|
|
@@ -102,20 +118,30 @@ __END__
|
|
102
118
|
Computers will never replace Frank Sinatra
|
103
119
|
```
|
104
120
|
|
105
|
-
##### Accessing Directives
|
121
|
+
##### Accessing Directives and Setting Parameter Values
|
106
122
|
|
107
|
-
Getting directives from a prompt is
|
123
|
+
Getting directives and keywords from a prompt is straightforward:
|
108
124
|
|
109
125
|
```ruby
|
110
|
-
prompt = PromptManager::Prompt.new(
|
111
|
-
prompt.keywords #=> an Array
|
126
|
+
prompt = PromptManager::Prompt.new(id: 'some_id')
|
127
|
+
prompt.keywords #=> an Array of keywords found in the prompt text
|
112
128
|
prompt.directives #=> an Array of entries like: ['directive', 'parameters']
|
113
129
|
|
130
|
+
# Or directly update the parameters hash
|
131
|
+
prompt.parameters = {
|
132
|
+
"[KEYWORD1]" => "value1",
|
133
|
+
"[KEYWORD2]" => "value2"
|
134
|
+
}
|
135
|
+
|
136
|
+
# After setting parameter values, call to_s to build the final prompt
|
114
137
|
# to_s builds the prompt by substituting
|
115
|
-
# values for keywords
|
138
|
+
# values for keywords and removing comments.
|
116
139
|
# The resulting text contains directives and
|
117
140
|
# prompt text ready for the LLM process.
|
118
|
-
|
141
|
+
final_text = prompt.to_s
|
142
|
+
|
143
|
+
# To persist any parameter changes to storage
|
144
|
+
prompt.save
|
119
145
|
```
|
120
146
|
|
121
147
|
The entries in the Array returned by the `prompt.directives` method is in the order that the directives were defined within the prompt. Each entry has two elements:
|
@@ -138,7 +164,7 @@ Since directies are collected after the keywords in the prompt have been substit
|
|
138
164
|
|
139
165
|
The `prompt_manager` gem only collects directives. Executing those directives is left up to some down stream process. Here are some ideas on how directives could be used in prompt downstream process:
|
140
166
|
|
141
|
-
- "//model gpt-5" could be used to set the LLM model to be used for a specific prompt.
|
167
|
+
- "//model gpt-5" could be used to set the LLM model to be used for a specific prompt.
|
142
168
|
- "//backend mods" could be used to set the backend prompt processor on the command line to be the `mods` utility.
|
143
169
|
- "//include path_to_file" could be used to add the contents of a file to the prompt.
|
144
170
|
- "//chat" could be used to send the prompts and then start up a chat session about the prompt and its response.
|
@@ -172,8 +198,8 @@ Use a `config` block to establish the configuration for the class.
|
|
172
198
|
|
173
199
|
```ruby
|
174
200
|
PromptManager::Storage::FileSystemAdapter.config do |o|
|
175
|
-
o.prompts_dir = "path/to/prompts_directory"
|
176
|
-
o.search_proc = nil # default
|
201
|
+
o.prompts_dir = "path/to/prompts_directory"
|
202
|
+
o.search_proc = nil # default
|
177
203
|
o.prompt_extension = '.txt' # default
|
178
204
|
o.params_extension = '.json' # default
|
179
205
|
end
|
@@ -183,7 +209,7 @@ The `config` block returns `self` so that means you can do this to setup the sto
|
|
183
209
|
|
184
210
|
```ruby
|
185
211
|
PromptManager::Prompt
|
186
|
-
.storage_adapter =
|
212
|
+
.storage_adapter =
|
187
213
|
PromptManager::Storage::FileSystemAdapter
|
188
214
|
.config do |config|
|
189
215
|
config.prompts_dir = 'path/to/prompts_dir'
|
@@ -198,7 +224,7 @@ An `ArgumentError` will be raised when `prompts_dir` does not exist or if it is
|
|
198
224
|
|
199
225
|
##### search_proc
|
200
226
|
|
201
|
-
The default for `search_proc` is nil which means that the search will be preformed by a default `search` method which is basically reading all the prompt files to see which ones contain the search term. It will return an Array of prompt IDs for each prompt file found that contains the search term. Its up to the application to select which returned prompt ID to use.
|
227
|
+
The default for `search_proc` is nil which means that the search will be preformed by a default `search` method which is basically reading all the prompt files to see which ones contain the search term. It will return an Array of prompt IDs for each prompt file found that contains the search term. Its up to the application to select which returned prompt ID to use.
|
202
228
|
|
203
229
|
There are faster ways to search and select files. For example there are specialized search and selection utilities that are available for the command line. The `examples` directory contains a `bash` script named `rgfzf` that uses `rg` (aka `ripgrep`) to do the searching and `fzf` to do the selecting.
|
204
230
|
|
@@ -273,7 +299,7 @@ The `PromptManager::Prompt` class expects an instance of a storage adapter class
|
|
273
299
|
|
274
300
|
```ruby
|
275
301
|
PromptManager::Prompt
|
276
|
-
.storage_adapter =
|
302
|
+
.storage_adapter =
|
277
303
|
PromptManager::Storage::ActiveRecordAdapter.config do |config|
|
278
304
|
config.model = DbPromptModel # any ActiveRecord::Base model
|
279
305
|
config.id_column = :prompt_name
|
@@ -0,0 +1,47 @@
|
|
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
|
@@ -1,34 +1,22 @@
|
|
1
1
|
# prompt_manager/lib/prompt_manager/prompt.rb
|
2
2
|
|
3
|
-
|
4
|
-
# generative AI processes. This includes creation, retrieval, storage management,
|
5
|
-
# as well as building prompts with replacement of parameterized values and
|
6
|
-
# comment removal. It communicates with a storage system through a storage
|
7
|
-
# adapter.
|
8
|
-
#
|
9
|
-
# Directives are collected into an Array where each entry is an Array
|
10
|
-
# of two elements. The first is the directive name as a String. The
|
11
|
-
# second is a string of parameters used by the directive.
|
12
|
-
#
|
13
|
-
# Directives are collected from the prompt after keyword
|
14
|
-
# substitution has occured. This means that directives within a
|
15
|
-
# prompt can be dynamic.
|
16
|
-
#
|
17
|
-
# PromptManager does not execute directives. They
|
18
|
-
# are made available to be passed on to down stream
|
19
|
-
# process.
|
3
|
+
require_relative "directive_processor"
|
20
4
|
|
21
5
|
class PromptManager::Prompt
|
22
|
-
COMMENT_SIGNAL
|
23
|
-
DIRECTIVE_SIGNAL
|
6
|
+
COMMENT_SIGNAL = '#' # lines beginning with this are a comment
|
7
|
+
DIRECTIVE_SIGNAL = '//' # Like the old IBM JCL
|
24
8
|
DEFAULT_PARAMETER_REGEX = /(\[[A-Z _|]+\])/
|
25
|
-
@
|
26
|
-
|
9
|
+
@parameter_regex = DEFAULT_PARAMETER_REGEX
|
10
|
+
|
11
|
+
##############################################
|
12
|
+
## Public class methods
|
27
13
|
|
28
14
|
class << self
|
29
15
|
attr_accessor :storage_adapter, :parameter_regex
|
30
16
|
|
31
|
-
|
17
|
+
def get(id:)
|
18
|
+
storage_adapter.get(id: id) # Return the hash directly from storage
|
19
|
+
end
|
32
20
|
|
33
21
|
def create(id:, text: "", parameters: {})
|
34
22
|
storage_adapter.save(
|
@@ -37,15 +25,22 @@ class PromptManager::Prompt
|
|
37
25
|
parameters: parameters
|
38
26
|
)
|
39
27
|
|
40
|
-
new(id: id)
|
28
|
+
::PromptManager::Prompt.new(id: id, context: [], directives_processor: PromptManager::DirectiveProcessor.new)
|
41
29
|
end
|
42
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
|
43
39
|
|
44
40
|
def search(for_what)
|
45
41
|
storage_adapter.search(for_what)
|
46
42
|
end
|
47
43
|
|
48
|
-
|
49
44
|
def method_missing(method_name, *args, &block)
|
50
45
|
if storage_adapter.respond_to?(method_name)
|
51
46
|
storage_adapter.send(method_name, *args, &block)
|
@@ -54,176 +49,108 @@ class PromptManager::Prompt
|
|
54
49
|
end
|
55
50
|
end
|
56
51
|
|
57
|
-
|
58
52
|
def respond_to_missing?(method_name, include_private = false)
|
59
53
|
storage_adapter.respond_to?(method_name, include_private) || super
|
60
54
|
end
|
61
55
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
66
63
|
|
67
64
|
|
68
|
-
# Retrieve the specific prompt ID from the Storage system.
|
69
65
|
def initialize(
|
70
|
-
id: nil,
|
71
|
-
context: []
|
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
72
|
)
|
73
73
|
|
74
74
|
@id = id
|
75
|
-
@
|
76
|
-
|
77
|
-
validate_arguments(@id, @db)
|
75
|
+
@directives_processor = directives_processor
|
78
76
|
|
79
|
-
@
|
80
|
-
@text = @record[:text]
|
81
|
-
@parameters = @record[:parameters]
|
82
|
-
@keywords = [] # Array of String
|
83
|
-
@directives = [] # Array of arrays. directive is first entry, rest are parameters
|
77
|
+
validate_arguments(@id)
|
84
78
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
88
86
|
end
|
89
87
|
|
90
|
-
|
91
|
-
|
92
|
-
def validate_arguments(prompt_id, prompts_db)
|
93
|
-
raise ArgumentError, 'id cannot be blank' if prompt_id.nil? || id.strip.empty?
|
88
|
+
def validate_arguments(prompt_id, prompts_db=db)
|
89
|
+
raise ArgumentError, 'id cannot be blank' if prompt_id.nil? || prompt_id.strip.empty?
|
94
90
|
raise(ArgumentError, 'storage_adapter is not set') if prompts_db.nil?
|
95
91
|
end
|
96
92
|
|
97
|
-
|
98
|
-
# Return tje prompt text suitable for passing to a
|
99
|
-
# gen-AI process.
|
100
93
|
def to_s
|
101
|
-
|
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)
|
102
99
|
end
|
103
|
-
alias_method :prompt, :to_s
|
104
|
-
|
105
100
|
|
106
|
-
# Save the prompt to the Storage system
|
107
101
|
def save
|
108
102
|
db.save(
|
109
|
-
id: id,
|
110
|
-
text: text,
|
103
|
+
id: id,
|
104
|
+
text: text, # Save the original text
|
111
105
|
parameters: parameters
|
112
|
-
)
|
113
|
-
end
|
114
|
-
|
115
|
-
|
116
|
-
# Delete this prompt from the Storage system
|
117
|
-
def delete
|
118
|
-
db.delete(id: id)
|
119
|
-
end
|
120
|
-
|
121
|
-
|
122
|
-
# Build the @prompt String by replacing the keywords
|
123
|
-
# with there parameterized values and removing all
|
124
|
-
# the comments.
|
125
|
-
#
|
126
|
-
def build
|
127
|
-
@prompt = text.gsub(self.class.parameter_regex) do |match|
|
128
|
-
param_name = match
|
129
|
-
Array(parameters[param_name]).last || match
|
130
|
-
end
|
131
|
-
|
132
|
-
save_directives(@prompt)
|
133
|
-
remove_comments
|
106
|
+
)
|
134
107
|
end
|
135
108
|
|
136
|
-
|
137
|
-
def keywords
|
138
|
-
update_keywords
|
139
|
-
end
|
109
|
+
def delete = db.delete(id: id)
|
140
110
|
|
141
111
|
|
142
112
|
######################################
|
143
113
|
private
|
144
114
|
|
145
|
-
def
|
146
|
-
@keywords = @text.scan(self.class.parameter_regex).flatten.uniq
|
147
|
-
@keywords.each do |kw|
|
148
|
-
@parameters[kw] = [] unless @parameters.has_key?(kw)
|
149
|
-
end
|
115
|
+
def db = self.class.storage_adapter
|
150
116
|
|
151
|
-
|
117
|
+
def remove_comments
|
118
|
+
lines = @text.split("\n")
|
119
|
+
end_index = lines.index("__END__") || lines.size
|
120
|
+
lines[0...end_index].reject { |line| line.strip.start_with?(COMMENT_SIGNAL) }.join("\n")
|
152
121
|
end
|
153
122
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
line = a_line.strip
|
160
|
-
next unless line.start_with?(DIRECTIVE_SIGNAL)
|
161
|
-
|
162
|
-
parts = line.split(' ')
|
163
|
-
directive = parts.shift[DIRECTIVE_SIGNAL.length..] # drop the directive signal
|
164
|
-
@directives << [directive, parts.join(' ')]
|
123
|
+
def substitute_values(input_text, values_hash)
|
124
|
+
if values_hash.is_a?(Hash) && !values_hash.empty?
|
125
|
+
values_hash.each do |key, value|
|
126
|
+
input_text = input_text.gsub(key, value)
|
127
|
+
end
|
165
128
|
end
|
166
|
-
|
167
|
-
@directives
|
129
|
+
input_text
|
168
130
|
end
|
169
131
|
|
132
|
+
def erb? = @erb_flag
|
133
|
+
def envvar? = @envvar_flag
|
170
134
|
|
171
|
-
def
|
172
|
-
|
173
|
-
.split("\n")
|
174
|
-
.reject{|a_line|
|
175
|
-
a_line.strip.start_with?(COMMENT_SIGNAL) ||
|
176
|
-
a_line.strip.start_with?(DIRECTIVE_SIGNAL)
|
177
|
-
}
|
178
|
-
|
179
|
-
# Remove empty lines at the start of the prompt
|
180
|
-
#
|
181
|
-
lines = lines.drop_while(&:empty?)
|
182
|
-
|
183
|
-
# Drop all the lines at __END__ and after
|
184
|
-
#
|
185
|
-
logical_end_inx = lines.index("__END__")
|
186
|
-
|
187
|
-
@prompt = if logical_end_inx
|
188
|
-
lines[0...logical_end_inx] # NOTE: ... means to not include last index
|
189
|
-
else
|
190
|
-
lines
|
191
|
-
end.join("\n")
|
192
|
-
end
|
193
|
-
|
194
|
-
|
195
|
-
# Handle storage errors
|
196
|
-
# SMELL: Just raise them or get out of their way and let the
|
197
|
-
# main program do tje job.
|
198
|
-
def handle_storage_error(error)
|
199
|
-
# Log the error message, notify, or take appropriate action
|
200
|
-
log_error("Storage operation failed: #{error.message}")
|
201
|
-
# Re-raise the error if necessary, or define recovery steps
|
202
|
-
raise error
|
203
|
-
end
|
204
|
-
|
135
|
+
def substitute_env_vars(input_text)
|
136
|
+
return input_text unless envvar?
|
205
137
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
def method_missing(method_name, *args, &block)
|
210
|
-
if db.respond_to?(method_name)
|
211
|
-
db.send(method_name, id, &block)
|
212
|
-
else
|
213
|
-
super
|
138
|
+
input_text.gsub(/\$(\w+)|\$\{(\w+)\}/) do |match|
|
139
|
+
env_var = $1 || $2
|
140
|
+
ENV[env_var] || match
|
214
141
|
end
|
215
142
|
end
|
216
143
|
|
217
|
-
|
218
|
-
|
219
|
-
|
144
|
+
def process_directives(input_text)
|
145
|
+
directive_lines = input_text.split("\n").select { |line| line.strip.start_with?(DIRECTIVE_SIGNAL) }
|
146
|
+
@directives = directive_lines.each_with_object({}) { |line, hash| hash[line.strip] = "" }
|
147
|
+
@directives = @directives_processor.run(@directives)
|
148
|
+
substitute_values(input_text, @directives)
|
220
149
|
end
|
221
150
|
|
151
|
+
def process_erb(input_text)
|
152
|
+
return input_text unless erb?
|
222
153
|
|
223
|
-
|
224
|
-
# main program? I believe its the main program's job.
|
225
|
-
def log_error(message)
|
226
|
-
puts "ERROR: #{message}"
|
154
|
+
ERB.new(input_text).result(@external_binding)
|
227
155
|
end
|
228
156
|
end
|
229
|
-
|
@@ -1,18 +1,28 @@
|
|
1
1
|
# prompt_manager/lib/prompt_manager/storage/active_record_adapter.rb
|
2
2
|
|
3
3
|
# This class acts as an adapter for interacting with an ActiveRecord model
|
4
|
+
require 'active_record'
|
4
5
|
# to manage storage operations for PromptManager::Prompt instances. It defines
|
5
6
|
# methods that allow for saving, searching, retrieving by ID, and deleting
|
6
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.
|
7
14
|
|
8
15
|
class PromptManager::Storage::ActiveRecordAdapter
|
9
16
|
|
10
17
|
class << self
|
11
|
-
|
12
|
-
|
13
|
-
:
|
18
|
+
# Configure the ActiveRecord model and column mappings
|
19
|
+
attr_accessor :model,
|
20
|
+
:id_column,
|
21
|
+
:text_column,
|
14
22
|
:parameters_column
|
15
23
|
|
24
|
+
# Configure the adapter with the required settings
|
25
|
+
# Must be called with a block before using the adapter
|
16
26
|
def config
|
17
27
|
if block_given?
|
18
28
|
yield self
|
@@ -24,19 +34,19 @@ class PromptManager::Storage::ActiveRecordAdapter
|
|
24
34
|
self
|
25
35
|
end
|
26
36
|
|
27
|
-
|
37
|
+
# Validate that all required configuration is present and valid
|
28
38
|
def validate_configuration
|
29
39
|
validate_model
|
30
40
|
validate_columns
|
31
41
|
end
|
32
42
|
|
33
|
-
|
43
|
+
# Ensure the provided model is a valid ActiveRecord model
|
34
44
|
def validate_model
|
35
45
|
raise ArgumentError, "AR Model not set" unless model
|
36
46
|
raise ArgumentError, "AR Model is not an ActiveRecord model" unless model < ActiveRecord::Base
|
37
47
|
end
|
38
48
|
|
39
|
-
|
49
|
+
# Verify that all required columns exist in the model
|
40
50
|
def validate_columns
|
41
51
|
columns = model.column_names # Array of Strings
|
42
52
|
[id_column, text_column, parameters_column].each do |column|
|
@@ -44,8 +54,7 @@ class PromptManager::Storage::ActiveRecordAdapter
|
|
44
54
|
end
|
45
55
|
end
|
46
56
|
|
47
|
-
|
48
|
-
|
57
|
+
# Delegate unknown methods to the ActiveRecord model
|
49
58
|
def method_missing(method_name, *args, &block)
|
50
59
|
if model.respond_to?(method_name)
|
51
60
|
model.send(method_name, *args, &block)
|
@@ -54,7 +63,7 @@ class PromptManager::Storage::ActiveRecordAdapter
|
|
54
63
|
end
|
55
64
|
end
|
56
65
|
|
57
|
-
|
66
|
+
# Support respond_to? for delegated methods
|
58
67
|
def respond_to_missing?(method_name, include_private = false)
|
59
68
|
model.respond_to?(method_name, include_private) || super
|
60
69
|
end
|
@@ -62,30 +71,29 @@ class PromptManager::Storage::ActiveRecordAdapter
|
|
62
71
|
|
63
72
|
|
64
73
|
##############################################
|
74
|
+
# The ActiveRecord object representing the current prompt
|
65
75
|
attr_accessor :record
|
66
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
|
67
82
|
|
68
|
-
#
|
69
|
-
def model = self.class.model
|
70
|
-
def id_column = self.class.id_column
|
71
|
-
def text_column = self.class.text_column
|
72
|
-
def parameters_column = self.class.parameters_column
|
73
|
-
|
74
|
-
|
83
|
+
# Initialize the adapter and validate configuration
|
75
84
|
def initialize
|
76
85
|
self.class.send(:validate_configuration) # send gets around private designations of a method
|
77
86
|
@record = model.first
|
78
87
|
end
|
79
88
|
|
80
|
-
|
89
|
+
# Retrieve a prompt by its ID
|
90
|
+
# Returns a hash with id, text, and parameters
|
81
91
|
def get(id:)
|
82
92
|
@record = model.find_by(id_column => id)
|
83
93
|
raise ArgumentError, "Prompt not found with id: #{id}" unless @record
|
84
94
|
|
85
|
-
#
|
86
|
-
#
|
87
|
-
# there something else going on?
|
88
|
-
# FIXME: expected the parameters_column to be a HAsh after de-serialization
|
95
|
+
# Handle case where parameters might be stored as a JSON string
|
96
|
+
# instead of a native Hash
|
89
97
|
parameters = @record[parameters_column]
|
90
98
|
|
91
99
|
if parameters.is_a? String
|
@@ -93,54 +101,57 @@ class PromptManager::Storage::ActiveRecordAdapter
|
|
93
101
|
end
|
94
102
|
|
95
103
|
{
|
96
|
-
id: id,
|
104
|
+
id: id,
|
97
105
|
text: @record[text_column],
|
98
106
|
parameters: parameters
|
99
107
|
}
|
100
108
|
end
|
101
109
|
|
102
|
-
|
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
|
103
112
|
def save(id:, text: "", parameters: {})
|
104
113
|
@record = model.find_or_initialize_by(id_column => id)
|
105
114
|
|
106
115
|
@record[text_column] = text
|
107
|
-
@record[parameters_column]
|
116
|
+
@record[parameters_column] = parameters
|
108
117
|
@record.save!
|
109
118
|
end
|
110
119
|
|
111
|
-
|
120
|
+
# Delete a prompt with the given ID
|
112
121
|
def delete(id:)
|
113
122
|
@record = model.find_by(id_column => id)
|
114
123
|
@record&.destroy
|
115
124
|
end
|
116
125
|
|
117
|
-
|
118
|
-
|
126
|
+
# Return an array of all prompt IDs
|
119
127
|
def list(*)
|
120
128
|
model.all.pluck(id_column)
|
121
129
|
end
|
122
130
|
|
123
|
-
|
131
|
+
# Search for prompts containing the given text
|
132
|
+
# Returns an array of matching prompt IDs
|
124
133
|
def search(for_what)
|
125
134
|
model.where("#{text_column} LIKE ?", "%#{for_what}%").pluck(id_column)
|
126
135
|
end
|
127
136
|
|
128
|
-
|
129
137
|
##############################################
|
130
138
|
private
|
131
139
|
|
132
|
-
|
140
|
+
# Delegate unknown methods to the current record
|
133
141
|
def method_missing(method_name, *args, &block)
|
134
|
-
if @record.respond_to?(method_name)
|
135
|
-
|
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)
|
136
146
|
else
|
137
147
|
super
|
138
148
|
end
|
139
149
|
end
|
140
150
|
|
141
|
-
|
151
|
+
# Support respond_to? for delegated methods
|
142
152
|
def respond_to_missing?(method_name, include_private = false)
|
143
|
-
model.respond_to?(method_name, include_private) ||
|
153
|
+
(model.respond_to?(method_name, include_private) ||
|
154
|
+
(@record && @record.respond_to?(method_name, include_private)) ||
|
155
|
+
super)
|
144
156
|
end
|
145
157
|
end
|
146
|
-
|
@@ -3,36 +3,45 @@
|
|
3
3
|
# Use the local (or remote) file system as a place to
|
4
4
|
# store and access prompts.
|
5
5
|
#
|
6
|
-
# Adds two additional methods to the
|
6
|
+
# Adds two additional methods to the Prompt class:
|
7
7
|
# list - returns Array of prompt IDs
|
8
|
-
# path
|
8
|
+
# path - returns a Pathname object to the prompt's text file
|
9
9
|
# path(prompt_id) - same as path on the prompt instance
|
10
10
|
#
|
11
11
|
# Allows sub-directories of the prompts_dir to be
|
12
|
-
# used like categories.
|
12
|
+
# used like categories. For example the prompt_id "toy/magic"
|
13
13
|
# is found in the `magic.txt` file inside the `toy` sub-directory
|
14
14
|
# of the prompts_dir.
|
15
15
|
#
|
16
|
-
# There can
|
16
|
+
# There can be many layers of categories (sub-directories)
|
17
17
|
#
|
18
|
+
# This adapter serves as the file-based storage backend for PromptManager::Prompt,
|
19
|
+
# enabling prompt retrieval and persistence using file system operations.
|
18
20
|
|
19
21
|
require 'json' # basic serialization of parameters
|
20
22
|
require 'pathname'
|
23
|
+
require 'fileutils'
|
21
24
|
|
22
25
|
class PromptManager::Storage::FileSystemAdapter
|
23
|
-
|
26
|
+
# Placeholder for search proc
|
27
|
+
SEARCH_PROC = nil
|
28
|
+
# File extension for parameters
|
24
29
|
PARAMS_EXTENSION = '.json'.freeze
|
30
|
+
# File extension for prompts
|
25
31
|
PROMPT_EXTENSION = '.txt'.freeze
|
32
|
+
# Regular expression for valid prompt IDs
|
26
33
|
PROMPT_ID_FORMAT = /^[a-zA-Z0-9\-\/_]+$/
|
27
34
|
|
28
35
|
class << self
|
29
|
-
|
36
|
+
# Accessors for configuration options
|
37
|
+
attr_accessor :prompts_dir, :search_proc,
|
30
38
|
:params_extension, :prompt_extension
|
31
|
-
|
39
|
+
|
40
|
+
# Configure the adapter
|
32
41
|
def config
|
33
42
|
if block_given?
|
34
43
|
yield self
|
35
|
-
validate_configuration
|
44
|
+
validate_configuration
|
36
45
|
else
|
37
46
|
raise ArgumentError, "No block given to config"
|
38
47
|
end
|
@@ -48,7 +57,6 @@ class PromptManager::Storage::FileSystemAdapter
|
|
48
57
|
new.list
|
49
58
|
end
|
50
59
|
|
51
|
-
|
52
60
|
def path(prompt_id)
|
53
61
|
new.path(prompt_id)
|
54
62
|
end
|
@@ -56,6 +64,7 @@ class PromptManager::Storage::FileSystemAdapter
|
|
56
64
|
#################################################
|
57
65
|
private
|
58
66
|
|
67
|
+
# Validate the configuration
|
59
68
|
def validate_configuration
|
60
69
|
validate_prompts_dir
|
61
70
|
validate_search_proc
|
@@ -63,28 +72,33 @@ class PromptManager::Storage::FileSystemAdapter
|
|
63
72
|
validate_params_extension
|
64
73
|
end
|
65
74
|
|
66
|
-
|
75
|
+
# Validate the prompts directory
|
67
76
|
def validate_prompts_dir
|
68
|
-
# This is a work around for a Ruby scope issue where the
|
69
|
-
# class getter/setter method is becoming confused with a
|
70
|
-
# local variable when anything other than plain 'ol get and
|
71
|
-
# set are used.
|
77
|
+
# This is a work around for a Ruby scope issue where the
|
78
|
+
# class getter/setter method is becoming confused with a
|
79
|
+
# local variable when anything other than plain 'ol get and
|
80
|
+
# set are used. This error is in both Ruby v3.2.2 and
|
72
81
|
# v3.3.0-preview3.
|
73
82
|
#
|
74
83
|
prompts_dir_local = self.prompts_dir
|
75
84
|
|
85
|
+
raise ArgumentError, "prompts_dir must be set" if prompts_dir_local.nil? || prompts_dir_local.to_s.strip.empty?
|
86
|
+
|
76
87
|
unless prompts_dir_local.is_a?(Pathname)
|
77
|
-
prompts_dir_local = Pathname.new(prompts_dir_local)
|
88
|
+
prompts_dir_local = Pathname.new(prompts_dir_local)
|
78
89
|
end
|
79
90
|
|
80
91
|
prompts_dir_local = prompts_dir_local.expand_path
|
81
92
|
|
82
|
-
|
83
|
-
|
93
|
+
unless prompts_dir_local.exist?
|
94
|
+
FileUtils.mkdir_p(prompts_dir_local)
|
95
|
+
end
|
96
|
+
raise(ArgumentError, "prompts_dir: #{prompts_dir_local} is not a directory") unless prompts_dir_local.directory?
|
97
|
+
|
84
98
|
self.prompts_dir = prompts_dir_local
|
85
99
|
end
|
86
100
|
|
87
|
-
|
101
|
+
# Validate the search proc
|
88
102
|
def validate_search_proc
|
89
103
|
search_proc_local = self.search_proc
|
90
104
|
|
@@ -97,7 +111,7 @@ class PromptManager::Storage::FileSystemAdapter
|
|
97
111
|
self.search_proc = search_proc_local
|
98
112
|
end
|
99
113
|
|
100
|
-
|
114
|
+
# Validate the prompt extension
|
101
115
|
def validate_prompt_extension
|
102
116
|
prompt_extension_local = self.prompt_extension
|
103
117
|
|
@@ -113,7 +127,7 @@ class PromptManager::Storage::FileSystemAdapter
|
|
113
127
|
self.prompt_extension = prompt_extension_local
|
114
128
|
end
|
115
129
|
|
116
|
-
|
130
|
+
# Validate the params extension
|
117
131
|
def validate_params_extension
|
118
132
|
params_extension_local = self.params_extension
|
119
133
|
|
@@ -130,25 +144,25 @@ class PromptManager::Storage::FileSystemAdapter
|
|
130
144
|
end
|
131
145
|
end
|
132
146
|
|
133
|
-
|
134
147
|
##################################################
|
135
148
|
###
|
136
149
|
## Instance
|
137
150
|
#
|
138
151
|
|
152
|
+
# Accessors for instance variables
|
139
153
|
def prompts_dir = self.class.prompts_dir
|
140
154
|
def search_proc = self.class.search_proc
|
141
155
|
def prompt_extension = self.class.prompt_extension
|
142
156
|
def params_extension = self.class.params_extension
|
143
157
|
|
144
|
-
|
158
|
+
# Initialize the adapter
|
145
159
|
def initialize
|
146
160
|
# NOTE: validate because main program may have made
|
147
161
|
# changes outside of the config block
|
148
162
|
self.class.send(:validate_configuration) # send gets around private designations of a method
|
149
163
|
end
|
150
164
|
|
151
|
-
|
165
|
+
# Get a prompt by ID
|
152
166
|
def get(id:)
|
153
167
|
validate_id(id)
|
154
168
|
verify_id(id)
|
@@ -160,17 +174,15 @@ class PromptManager::Storage::FileSystemAdapter
|
|
160
174
|
}
|
161
175
|
end
|
162
176
|
|
163
|
-
|
164
177
|
# Retrieve prompt text by its id
|
165
178
|
def prompt_text(prompt_id)
|
166
179
|
read_file(file_path(prompt_id, prompt_extension))
|
167
180
|
end
|
168
181
|
|
169
|
-
|
170
182
|
# Retrieve parameter values by its id
|
171
183
|
def parameter_values(prompt_id)
|
172
184
|
params_path = file_path(prompt_id, params_extension)
|
173
|
-
|
185
|
+
|
174
186
|
if params_path.exist?
|
175
187
|
parms_content = read_file(params_path)
|
176
188
|
deserialize(parms_content)
|
@@ -179,35 +191,33 @@ class PromptManager::Storage::FileSystemAdapter
|
|
179
191
|
end
|
180
192
|
end
|
181
193
|
|
182
|
-
|
183
194
|
# Save prompt text and parameter values to corresponding files
|
184
195
|
def save(
|
185
|
-
id:,
|
186
|
-
text: "",
|
196
|
+
id:,
|
197
|
+
text: "",
|
187
198
|
parameters: {}
|
188
199
|
)
|
189
200
|
validate_id(id)
|
190
201
|
|
191
202
|
prompt_filepath = file_path(id, prompt_extension)
|
192
203
|
params_filepath = file_path(id, params_extension)
|
193
|
-
|
204
|
+
|
194
205
|
write_with_error_handling(prompt_filepath, text)
|
195
206
|
write_with_error_handling(params_filepath, serialize(parameters))
|
196
207
|
end
|
197
208
|
|
198
|
-
|
199
|
-
# Delete prompted text and parameter values files
|
209
|
+
# Delete prompt text and parameter values files
|
200
210
|
def delete(id:)
|
201
211
|
validate_id(id)
|
202
212
|
|
203
213
|
prompt_filepath = file_path(id, prompt_extension)
|
204
214
|
params_filepath = file_path(id, params_extension)
|
205
|
-
|
215
|
+
|
206
216
|
delete_with_error_handling(prompt_filepath)
|
207
217
|
delete_with_error_handling(params_filepath)
|
208
218
|
end
|
209
219
|
|
210
|
-
|
220
|
+
# Search for prompts
|
211
221
|
def search(for_what)
|
212
222
|
search_term = for_what.downcase
|
213
223
|
|
@@ -218,25 +228,23 @@ class PromptManager::Storage::FileSystemAdapter
|
|
218
228
|
end
|
219
229
|
end
|
220
230
|
|
221
|
-
|
222
231
|
# Return an Array of prompt IDs
|
223
232
|
def list(*)
|
224
233
|
prompt_ids = []
|
225
|
-
|
234
|
+
|
226
235
|
Pathname.glob(prompts_dir.join("**/*#{prompt_extension}")).each do |file_path|
|
227
236
|
prompt_id = file_path.relative_path_from(prompts_dir).to_s.gsub(prompt_extension, '')
|
228
237
|
prompt_ids << prompt_id
|
229
238
|
end
|
230
239
|
|
231
|
-
prompt_ids
|
240
|
+
prompt_ids.sort
|
232
241
|
end
|
233
242
|
|
234
|
-
|
235
243
|
# Returns a Pathname object for a prompt ID text file
|
236
244
|
# However, it is possible that the file does not exist.
|
237
245
|
def path(id)
|
238
246
|
validate_id(id)
|
239
|
-
file_path(id, prompt_extension)
|
247
|
+
file_path(id, prompt_extension)
|
240
248
|
end
|
241
249
|
|
242
250
|
##########################################
|
@@ -247,14 +255,14 @@ class PromptManager::Storage::FileSystemAdapter
|
|
247
255
|
raise ArgumentError, "Invalid ID format id: #{id}" unless id =~ PROMPT_ID_FORMAT
|
248
256
|
end
|
249
257
|
|
250
|
-
|
258
|
+
# Verify that the ID exists
|
251
259
|
def verify_id(id)
|
252
260
|
unless file_path(id, prompt_extension).exist?
|
253
261
|
raise ArgumentError, "Invalid prompt_id: #{id}"
|
254
262
|
end
|
255
263
|
end
|
256
264
|
|
257
|
-
|
265
|
+
# Write to a file with error handling
|
258
266
|
def write_with_error_handling(file_path, content)
|
259
267
|
begin
|
260
268
|
file_path.write content
|
@@ -264,8 +272,7 @@ class PromptManager::Storage::FileSystemAdapter
|
|
264
272
|
end
|
265
273
|
end
|
266
274
|
|
267
|
-
|
268
|
-
# file_path (Pathname)
|
275
|
+
# Delete a file with error handling
|
269
276
|
def delete_with_error_handling(file_path)
|
270
277
|
begin
|
271
278
|
file_path.delete
|
@@ -275,39 +282,37 @@ class PromptManager::Storage::FileSystemAdapter
|
|
275
282
|
end
|
276
283
|
end
|
277
284
|
|
278
|
-
|
285
|
+
# Get the file path for a prompt ID and extension
|
279
286
|
def file_path(id, extension)
|
280
287
|
prompts_dir + "#{id}#{extension}"
|
281
288
|
end
|
282
289
|
|
283
|
-
|
290
|
+
# Read a file
|
284
291
|
def read_file(full_path)
|
285
292
|
raise IOError, 'File does not exist' unless File.exist?(full_path)
|
286
293
|
File.read(full_path)
|
287
294
|
end
|
288
295
|
|
289
|
-
|
296
|
+
# Search for prompts
|
290
297
|
def search_prompts(search_term)
|
291
298
|
prompt_ids = []
|
292
|
-
|
299
|
+
|
293
300
|
Pathname.glob(prompts_dir.join("**/*#{prompt_extension}")).each do |prompt_path|
|
294
301
|
if prompt_path.read.downcase.include?(search_term)
|
295
|
-
prompt_id = prompt_path.relative_path_from(prompts_dir).to_s.gsub(prompt_extension, '')
|
302
|
+
prompt_id = prompt_path.relative_path_from(prompts_dir).to_s.gsub(prompt_extension, '')
|
296
303
|
prompt_ids << prompt_id
|
297
304
|
end
|
298
305
|
end
|
299
306
|
|
300
|
-
prompt_ids
|
307
|
+
prompt_ids.sort
|
301
308
|
end
|
302
309
|
|
303
|
-
|
304
|
-
# TODO: Should the serializer be generic?
|
305
|
-
|
310
|
+
# Serialize data to JSON
|
306
311
|
def serialize(data)
|
307
312
|
data.to_json
|
308
313
|
end
|
309
314
|
|
310
|
-
|
315
|
+
# Deserialize JSON data
|
311
316
|
def deserialize(data)
|
312
317
|
JSON.parse(data)
|
313
318
|
end
|
@@ -1,7 +1,34 @@
|
|
1
1
|
# prompt_manager/lib/prompt_manager/storage.rb
|
2
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
|
3
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.
|
4
32
|
module Storage
|
5
33
|
end
|
6
34
|
end
|
7
|
-
|
data/lib/prompt_manager.rb
CHANGED
@@ -5,10 +5,20 @@
|
|
5
5
|
require 'ostruct'
|
6
6
|
|
7
7
|
require_relative "prompt_manager/version"
|
8
|
-
require_relative "prompt_manager/storage"
|
9
8
|
require_relative "prompt_manager/prompt"
|
9
|
+
require_relative "prompt_manager/storage"
|
10
|
+
require_relative "prompt_manager/storage/file_system_adapter"
|
10
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.
|
11
16
|
module PromptManager
|
17
|
+
# Base error class for all PromptManager-specific errors
|
12
18
|
class Error < StandardError; end
|
13
|
-
|
19
|
+
|
20
|
+
# TODO: Add additional module-specific error classes such as:
|
21
|
+
# - StorageError - For issues with storing or retrieving prompts
|
22
|
+
# - ParameterError - For issues with parameter substitution
|
23
|
+
# - ConfigurationError - For setup and configuration issues
|
14
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prompt_manager
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-03-30 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activerecord
|
@@ -139,7 +138,6 @@ files:
|
|
139
138
|
- README.md
|
140
139
|
- Rakefile
|
141
140
|
- examples/directives.rb
|
142
|
-
- examples/prompts_dir/directive_example.txt
|
143
141
|
- examples/prompts_dir/todo.json
|
144
142
|
- examples/prompts_dir/todo.txt
|
145
143
|
- examples/prompts_dir/toy/8-ball.txt
|
@@ -147,6 +145,7 @@ files:
|
|
147
145
|
- examples/simple.rb
|
148
146
|
- examples/using_search_proc.rb
|
149
147
|
- lib/prompt_manager.rb
|
148
|
+
- lib/prompt_manager/directive_processor.rb
|
150
149
|
- lib/prompt_manager/prompt.rb
|
151
150
|
- lib/prompt_manager/storage.rb
|
152
151
|
- lib/prompt_manager/storage/active_record_adapter.rb
|
@@ -160,7 +159,6 @@ metadata:
|
|
160
159
|
homepage_uri: https://github.com/MadBomber/prompt_manager
|
161
160
|
source_code_uri: https://github.com/MadBomber/prompt_manager
|
162
161
|
changelog_uri: https://github.com/MadBomber/prompt_manager
|
163
|
-
post_install_message:
|
164
162
|
rdoc_options: []
|
165
163
|
require_paths:
|
166
164
|
- lib
|
@@ -175,8 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
175
173
|
- !ruby/object:Gem::Version
|
176
174
|
version: '0'
|
177
175
|
requirements: []
|
178
|
-
rubygems_version: 3.
|
179
|
-
signing_key:
|
176
|
+
rubygems_version: 3.6.6
|
180
177
|
specification_version: 4
|
181
178
|
summary: Manage prompts for use with gen-AI processes
|
182
179
|
test_files: []
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# directive_example.txt
|
2
|
-
# Desc: Shows how directives work
|
3
|
-
|
4
|
-
//good_directive param1 param2
|
5
|
-
//bad_directive param3 param4
|
6
|
-
|
7
|
-
say hello to me in {language} as well as English.
|
8
|
-
|
9
|
-
# The default parameter is delimited by square brackets
|
10
|
-
# and is all uppercase
|
11
|
-
|
12
|
-
write a [PROGRAMMING LANGUAGE] program that predicts the lottery.
|
13
|
-
|
14
|
-
|