prompt_manager 0.5.7 → 0.5.8
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 +4 -0
- data/COMMITS.md +196 -0
- data/README.md +485 -203
- data/docs/.keep +0 -0
- data/docs/advanced/custom-keywords.md +421 -0
- data/docs/advanced/dynamic-directives.md +535 -0
- data/docs/advanced/performance.md +612 -0
- data/docs/advanced/search-integration.md +635 -0
- data/docs/api/configuration.md +355 -0
- data/docs/api/directive-processor.md +431 -0
- data/docs/api/prompt-class.md +354 -0
- data/docs/api/storage-adapters.md +462 -0
- data/docs/assets/favicon.ico +1 -0
- data/docs/assets/logo.svg +24 -0
- data/docs/core-features/comments.md +48 -0
- data/docs/core-features/directive-processing.md +38 -0
- data/docs/core-features/erb-integration.md +68 -0
- data/docs/core-features/error-handling.md +197 -0
- data/docs/core-features/parameter-history.md +76 -0
- data/docs/core-features/parameterized-prompts.md +500 -0
- data/docs/core-features/shell-integration.md +79 -0
- data/docs/development/architecture.md +544 -0
- data/docs/development/contributing.md +425 -0
- data/docs/development/roadmap.md +234 -0
- data/docs/development/testing.md +822 -0
- data/docs/examples/advanced.md +523 -0
- data/docs/examples/basic.md +688 -0
- data/docs/examples/real-world.md +776 -0
- data/docs/examples.md +337 -0
- data/docs/getting-started/basic-concepts.md +318 -0
- data/docs/getting-started/installation.md +97 -0
- data/docs/getting-started/quick-start.md +256 -0
- data/docs/index.md +230 -0
- data/docs/migration/v0.9.0.md +459 -0
- data/docs/migration/v1.0.0.md +591 -0
- data/docs/storage/activerecord-adapter.md +348 -0
- data/docs/storage/custom-adapters.md +176 -0
- data/docs/storage/filesystem-adapter.md +236 -0
- data/docs/storage/overview.md +427 -0
- data/examples/advanced_integrations.rb +52 -0
- data/examples/prompts_dir/advanced_demo.txt +79 -0
- data/examples/prompts_dir/directive_example.json +1 -0
- data/examples/prompts_dir/directive_example.txt +8 -0
- data/examples/prompts_dir/todo.json +1 -1
- data/improvement_plan.md +996 -0
- data/lib/prompt_manager/storage/file_system_adapter.rb +8 -2
- data/lib/prompt_manager/version.rb +1 -1
- data/mkdocs.yml +146 -0
- data/prompt_manager_logo.png +0 -0
- metadata +46 -3
- data/LICENSE.txt +0 -21
data/docs/.keep
ADDED
File without changes
|
@@ -0,0 +1,421 @@
|
|
1
|
+
# Custom Keywords
|
2
|
+
|
3
|
+
PromptManager allows you to define custom keywords and parameter patterns beyond the standard `[PARAMETER_NAME]` syntax.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Custom keywords enable you to create domain-specific parameter patterns, validation rules, and transformation logic for your prompts.
|
8
|
+
|
9
|
+
## Defining Custom Keywords
|
10
|
+
|
11
|
+
### Basic Custom Keywords
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
PromptManager.configure do |config|
|
15
|
+
config.custom_keywords = {
|
16
|
+
'EMAIL' => {
|
17
|
+
pattern: /\{EMAIL:([^}]+)\}/,
|
18
|
+
validator: ->(value) { value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i) },
|
19
|
+
transformer: ->(value) { value.downcase }
|
20
|
+
},
|
21
|
+
|
22
|
+
'PHONE' => {
|
23
|
+
pattern: /\{PHONE:([^}]+)\}/,
|
24
|
+
validator: ->(value) { value.match?(/\A\+?[\d\-\(\)\s]+\z/) },
|
25
|
+
transformer: ->(value) { value.gsub(/[^\d+]/, '') }
|
26
|
+
},
|
27
|
+
|
28
|
+
'CURRENCY' => {
|
29
|
+
pattern: /\{CURRENCY:([^}]+):([A-Z]{3})\}/,
|
30
|
+
transformer: ->(amount, currency) {
|
31
|
+
formatted = sprintf('%.2f', amount.to_f)
|
32
|
+
case currency
|
33
|
+
when 'USD' then "$#{formatted}"
|
34
|
+
when 'EUR' then "€#{formatted}"
|
35
|
+
else "#{formatted} #{currency}"
|
36
|
+
end
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
### Usage in Prompts
|
44
|
+
|
45
|
+
```text
|
46
|
+
# email_template.txt
|
47
|
+
Dear Customer,
|
48
|
+
|
49
|
+
Your account {EMAIL:customer_email} has been updated.
|
50
|
+
Please contact us at {PHONE:support_phone} if you have questions.
|
51
|
+
Your order total is {CURRENCY:order_amount:USD}.
|
52
|
+
|
53
|
+
Best regards,
|
54
|
+
Support Team
|
55
|
+
```
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
prompt = PromptManager::Prompt.new(id: 'email_template')
|
59
|
+
result = prompt.render(
|
60
|
+
customer_email: 'JOHN.DOE@EXAMPLE.COM',
|
61
|
+
support_phone: '1-800-555-0123',
|
62
|
+
order_amount: 123.45
|
63
|
+
)
|
64
|
+
|
65
|
+
# Result:
|
66
|
+
# Dear Customer,
|
67
|
+
# Your account john.doe@example.com has been updated.
|
68
|
+
# Please contact us at +18005550123 if you have questions.
|
69
|
+
# Your order total is $123.45.
|
70
|
+
```
|
71
|
+
|
72
|
+
## Advanced Custom Keywords
|
73
|
+
|
74
|
+
### Conditional Keywords
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
config.custom_keywords['IF_PREMIUM'] = {
|
78
|
+
pattern: /\{IF_PREMIUM:([^}]+)\}/,
|
79
|
+
processor: ->(content, context) {
|
80
|
+
user_tier = context.dig(:parameters, :user_tier)
|
81
|
+
user_tier == 'premium' ? content : ''
|
82
|
+
}
|
83
|
+
}
|
84
|
+
```
|
85
|
+
|
86
|
+
```text
|
87
|
+
# Usage in prompt:
|
88
|
+
{IF_PREMIUM:🌟 Thank you for being a Premium member!}
|
89
|
+
```
|
90
|
+
|
91
|
+
### Loop Keywords
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
config.custom_keywords['FOREACH'] = {
|
95
|
+
pattern: /\{FOREACH:([^:]+):([^}]+)\}/,
|
96
|
+
processor: ->(array_name, template, context) {
|
97
|
+
array_data = context.dig(:parameters, array_name.to_sym) || []
|
98
|
+
|
99
|
+
array_data.map.with_index do |item, index|
|
100
|
+
item_template = template.gsub(/\{ITEM\.(\w+)\}/) { item[Regexp.last_match(1).to_sym] }
|
101
|
+
item_template.gsub(/\{INDEX\}/, index.to_s)
|
102
|
+
end.join("\n")
|
103
|
+
}
|
104
|
+
}
|
105
|
+
```
|
106
|
+
|
107
|
+
```text
|
108
|
+
# Usage in prompt:
|
109
|
+
Your order items:
|
110
|
+
{FOREACH:order_items:- {ITEM.name}: ${ITEM.price}}
|
111
|
+
```
|
112
|
+
|
113
|
+
### Date/Time Keywords
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
config.custom_keywords['DATE'] = {
|
117
|
+
pattern: /\{DATE:([^:}]+)(?::([^}]+))?\}/,
|
118
|
+
processor: ->(format, offset, context) {
|
119
|
+
base_date = Time.current
|
120
|
+
|
121
|
+
if offset
|
122
|
+
case offset
|
123
|
+
when /\+(\d+)d/ then base_date += Regexp.last_match(1).to_i.days
|
124
|
+
when /-(\d+)d/ then base_date -= Regexp.last_match(1).to_i.days
|
125
|
+
when /\+(\d+)w/ then base_date += Regexp.last_match(1).to_i.weeks
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
base_date.strftime(format)
|
130
|
+
}
|
131
|
+
}
|
132
|
+
```
|
133
|
+
|
134
|
+
```text
|
135
|
+
# Usage in prompt:
|
136
|
+
Today: {DATE:%B %d, %Y}
|
137
|
+
Next week: {DATE:%B %d, %Y:+7d}
|
138
|
+
Last month: {DATE:%B %Y:-1m}
|
139
|
+
```
|
140
|
+
|
141
|
+
## Validation and Error Handling
|
142
|
+
|
143
|
+
### Parameter Validation
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
config.custom_keywords['VALIDATED_EMAIL'] = {
|
147
|
+
pattern: /\{EMAIL:([^}]+)\}/,
|
148
|
+
validator: ->(email) {
|
149
|
+
return false unless email.is_a?(String)
|
150
|
+
return false unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
151
|
+
|
152
|
+
# Additional validation
|
153
|
+
domain = email.split('@').last
|
154
|
+
!['tempmail.com', 'throwaway.email'].include?(domain)
|
155
|
+
},
|
156
|
+
error_message: 'Please provide a valid email address from an allowed domain'
|
157
|
+
}
|
158
|
+
```
|
159
|
+
|
160
|
+
### Custom Error Handling
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class CustomKeywordProcessor
|
164
|
+
def self.process_keyword(keyword, args, context)
|
165
|
+
case keyword
|
166
|
+
when 'SECURE_DATA'
|
167
|
+
return '[REDACTED]' if context[:redact_sensitive_data]
|
168
|
+
args.first
|
169
|
+
|
170
|
+
when 'API_CALL'
|
171
|
+
begin
|
172
|
+
api_result = make_api_call(args.first)
|
173
|
+
api_result['data']
|
174
|
+
rescue => e
|
175
|
+
Rails.logger.error "API call failed: #{e.message}"
|
176
|
+
'[API_ERROR]'
|
177
|
+
end
|
178
|
+
|
179
|
+
else
|
180
|
+
raise PromptManager::UnknownKeywordError.new("Unknown keyword: #{keyword}")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
## Dynamic Keywords
|
187
|
+
|
188
|
+
### Runtime Registration
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
class DynamicKeywordManager
|
192
|
+
def self.register_for_user(user)
|
193
|
+
PromptManager.configure do |config|
|
194
|
+
config.custom_keywords ||= {}
|
195
|
+
|
196
|
+
# User-specific keywords
|
197
|
+
config.custom_keywords["USER_#{user.id}_NAME"] = {
|
198
|
+
pattern: /\{USER_NAME\}/,
|
199
|
+
processor: ->(*args, context) { user.full_name }
|
200
|
+
}
|
201
|
+
|
202
|
+
# Role-based keywords
|
203
|
+
if user.admin?
|
204
|
+
config.custom_keywords['ADMIN_PANEL'] = {
|
205
|
+
pattern: /\{ADMIN_PANEL:([^}]+)\}/,
|
206
|
+
processor: ->(content, context) { content }
|
207
|
+
}
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Usage
|
214
|
+
DynamicKeywordManager.register_for_user(current_user)
|
215
|
+
```
|
216
|
+
|
217
|
+
### Database-Driven Keywords
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
class DatabaseKeywordLoader
|
221
|
+
def self.load_keywords
|
222
|
+
CustomKeyword.active.each do |keyword_record|
|
223
|
+
PromptManager.configure do |config|
|
224
|
+
config.custom_keywords[keyword_record.name] = {
|
225
|
+
pattern: Regexp.new(keyword_record.pattern),
|
226
|
+
processor: eval(keyword_record.processor_code),
|
227
|
+
description: keyword_record.description
|
228
|
+
}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Load keywords on application startup
|
235
|
+
DatabaseKeywordLoader.load_keywords
|
236
|
+
```
|
237
|
+
|
238
|
+
## Integration with ERB
|
239
|
+
|
240
|
+
### ERB-Enhanced Keywords
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
config.custom_keywords['ERB_EVAL'] = {
|
244
|
+
pattern: /\{ERB:([^}]+)\}/,
|
245
|
+
processor: ->(erb_code, context) {
|
246
|
+
template = ERB.new(erb_code)
|
247
|
+
template.result(binding)
|
248
|
+
}
|
249
|
+
}
|
250
|
+
```
|
251
|
+
|
252
|
+
```text
|
253
|
+
# Usage in prompt:
|
254
|
+
Current time: {ERB:<%= Time.current.strftime('%H:%M') %>}
|
255
|
+
Random number: {ERB:<%= rand(100) %>}
|
256
|
+
```
|
257
|
+
|
258
|
+
### Template Inheritance
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
config.custom_keywords['PARENT'] = {
|
262
|
+
pattern: /\{PARENT:([^}]+)\}/,
|
263
|
+
processor: ->(parent_template, context) {
|
264
|
+
parent_prompt = PromptManager::Prompt.new(id: parent_template)
|
265
|
+
parent_prompt.render(context[:parameters])
|
266
|
+
}
|
267
|
+
}
|
268
|
+
```
|
269
|
+
|
270
|
+
## Performance Optimization
|
271
|
+
|
272
|
+
### Keyword Caching
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
class CachedKeywordProcessor
|
276
|
+
@cache = {}
|
277
|
+
|
278
|
+
def self.process_with_cache(keyword, args, context, cache_ttl: 300)
|
279
|
+
cache_key = "#{keyword}:#{args.join(':')}:#{context.hash}"
|
280
|
+
|
281
|
+
cached_result = @cache[cache_key]
|
282
|
+
if cached_result && (Time.current - cached_result[:timestamp]) < cache_ttl
|
283
|
+
return cached_result[:value]
|
284
|
+
end
|
285
|
+
|
286
|
+
result = process_keyword(keyword, args, context)
|
287
|
+
@cache[cache_key] = {
|
288
|
+
value: result,
|
289
|
+
timestamp: Time.current
|
290
|
+
}
|
291
|
+
|
292
|
+
result
|
293
|
+
end
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
### Lazy Evaluation
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
config.custom_keywords['LAZY_LOAD'] = {
|
301
|
+
pattern: /\{LAZY:([^}]+)\}/,
|
302
|
+
processor: ->(data_source, context) {
|
303
|
+
# Only load data when actually needed
|
304
|
+
-> { expensive_data_load(data_source) }
|
305
|
+
}
|
306
|
+
}
|
307
|
+
```
|
308
|
+
|
309
|
+
## Testing Custom Keywords
|
310
|
+
|
311
|
+
### RSpec Examples
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
describe 'Custom Keywords' do
|
315
|
+
before do
|
316
|
+
PromptManager.configure do |config|
|
317
|
+
config.custom_keywords = {
|
318
|
+
'TEST_UPPER' => {
|
319
|
+
pattern: /\{UPPER:([^}]+)\}/,
|
320
|
+
transformer: ->(value) { value.upcase }
|
321
|
+
}
|
322
|
+
}
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'processes custom keyword' do
|
327
|
+
prompt = PromptManager::Prompt.new(id: 'test')
|
328
|
+
allow(prompt.storage).to receive(:read).and_return('Hello {UPPER:world}')
|
329
|
+
|
330
|
+
result = prompt.render
|
331
|
+
expect(result).to eq 'Hello WORLD'
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'validates custom keyword input' do
|
335
|
+
PromptManager.configure do |config|
|
336
|
+
config.custom_keywords['VALIDATED'] = {
|
337
|
+
pattern: /\{VALIDATED:([^}]+)\}/,
|
338
|
+
validator: ->(value) { value.length > 3 },
|
339
|
+
error_message: 'Value must be longer than 3 characters'
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
prompt = PromptManager::Prompt.new(id: 'test')
|
344
|
+
allow(prompt.storage).to receive(:read).and_return('Hello {VALIDATED:ab}')
|
345
|
+
|
346
|
+
expect {
|
347
|
+
prompt.render
|
348
|
+
}.to raise_error(PromptManager::ValidationError, /Value must be longer than 3 characters/)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
```
|
352
|
+
|
353
|
+
## Real-World Examples
|
354
|
+
|
355
|
+
### E-commerce Keywords
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
PromptManager.configure do |config|
|
359
|
+
config.custom_keywords.merge!({
|
360
|
+
'PRICE' => {
|
361
|
+
pattern: /\{PRICE:([^:}]+)(?::([A-Z]{3}))?\}/,
|
362
|
+
processor: ->(amount, currency, context) {
|
363
|
+
currency ||= 'USD'
|
364
|
+
user_country = context.dig(:parameters, :user_country)
|
365
|
+
|
366
|
+
# Adjust currency based on user location
|
367
|
+
case user_country
|
368
|
+
when 'GB' then currency = 'GBP'
|
369
|
+
when 'DE', 'FR', 'IT' then currency = 'EUR'
|
370
|
+
end
|
371
|
+
|
372
|
+
CurrencyFormatter.format(amount.to_f, currency)
|
373
|
+
}
|
374
|
+
},
|
375
|
+
|
376
|
+
'INVENTORY_STATUS' => {
|
377
|
+
pattern: /\{STOCK:([^}]+)\}/,
|
378
|
+
processor: ->(product_id, context) {
|
379
|
+
stock_level = InventoryService.check_stock(product_id)
|
380
|
+
|
381
|
+
case stock_level
|
382
|
+
when 0 then '❌ Out of Stock'
|
383
|
+
when 1..5 then '⚠️ Low Stock'
|
384
|
+
else '✅ In Stock'
|
385
|
+
end
|
386
|
+
}
|
387
|
+
}
|
388
|
+
})
|
389
|
+
end
|
390
|
+
```
|
391
|
+
|
392
|
+
### Localization Keywords
|
393
|
+
|
394
|
+
```ruby
|
395
|
+
config.custom_keywords['TRANSLATE'] = {
|
396
|
+
pattern: /\{T:([^:}]+)(?::([a-z]{2}))?\}/,
|
397
|
+
processor: ->(key, locale, context) {
|
398
|
+
locale ||= context.dig(:parameters, :locale) || 'en'
|
399
|
+
I18n.with_locale(locale) { I18n.t(key) }
|
400
|
+
}
|
401
|
+
}
|
402
|
+
|
403
|
+
config.custom_keywords['PLURALIZE'] = {
|
404
|
+
pattern: /\{PLURAL:([^:]+):([^:]+):([^}]+)\}/,
|
405
|
+
processor: ->(count, singular, plural, context) {
|
406
|
+
count_val = context.dig(:parameters, count.to_sym) || 0
|
407
|
+
count_val.to_i == 1 ? singular : plural
|
408
|
+
}
|
409
|
+
}
|
410
|
+
```
|
411
|
+
|
412
|
+
## Best Practices
|
413
|
+
|
414
|
+
1. **Descriptive Names**: Use clear, descriptive names for custom keywords
|
415
|
+
2. **Validation**: Always validate input parameters
|
416
|
+
3. **Error Handling**: Provide meaningful error messages
|
417
|
+
4. **Documentation**: Document keyword syntax and behavior
|
418
|
+
5. **Performance**: Cache expensive operations
|
419
|
+
6. **Security**: Sanitize user input in keyword processors
|
420
|
+
7. **Testing**: Write comprehensive tests for custom keywords
|
421
|
+
8. **Consistency**: Follow consistent naming conventions across keywords
|