Custom Keywords¶
PromptManager allows you to define custom keywords and parameter patterns beyond the standard [PARAMETER_NAME]
syntax.
Overview¶
Custom keywords enable you to create domain-specific parameter patterns, validation rules, and transformation logic for your prompts.
Defining Custom Keywords¶
Basic Custom Keywords¶
PromptManager.configure do |config|
config.custom_keywords = {
'EMAIL' => {
pattern: /\{EMAIL:([^}]+)\}/,
validator: ->(value) { value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i) },
transformer: ->(value) { value.downcase }
},
'PHONE' => {
pattern: /\{PHONE:([^}]+)\}/,
validator: ->(value) { value.match?(/\A\+?[\d\-\(\)\s]+\z/) },
transformer: ->(value) { value.gsub(/[^\d+]/, '') }
},
'CURRENCY' => {
pattern: /\{CURRENCY:([^}]+):([A-Z]{3})\}/,
transformer: ->(amount, currency) {
formatted = sprintf('%.2f', amount.to_f)
case currency
when 'USD' then "$#{formatted}"
when 'EUR' then "€#{formatted}"
else "#{formatted} #{currency}"
end
}
}
}
end
Usage in Prompts¶
# email_template.txt
Dear Customer,
Your account {EMAIL:customer_email} has been updated.
Please contact us at {PHONE:support_phone} if you have questions.
Your order total is {CURRENCY:order_amount:USD}.
Best regards,
Support Team
prompt = PromptManager::Prompt.new(id: 'email_template')
result = prompt.render(
customer_email: 'JOHN.DOE@EXAMPLE.COM',
support_phone: '1-800-555-0123',
order_amount: 123.45
)
# Result:
# Dear Customer,
# Your account john.doe@example.com has been updated.
# Please contact us at +18005550123 if you have questions.
# Your order total is $123.45.
Advanced Custom Keywords¶
Conditional Keywords¶
config.custom_keywords['IF_PREMIUM'] = {
pattern: /\{IF_PREMIUM:([^}]+)\}/,
processor: ->(content, context) {
user_tier = context.dig(:parameters, :user_tier)
user_tier == 'premium' ? content : ''
}
}
Loop Keywords¶
config.custom_keywords['FOREACH'] = {
pattern: /\{FOREACH:([^:]+):([^}]+)\}/,
processor: ->(array_name, template, context) {
array_data = context.dig(:parameters, array_name.to_sym) || []
array_data.map.with_index do |item, index|
item_template = template.gsub(/\{ITEM\.(\w+)\}/) { item[Regexp.last_match(1).to_sym] }
item_template.gsub(/\{INDEX\}/, index.to_s)
end.join("\n")
}
}
Date/Time Keywords¶
config.custom_keywords['DATE'] = {
pattern: /\{DATE:([^:}]+)(?::([^}]+))?\}/,
processor: ->(format, offset, context) {
base_date = Time.current
if offset
case offset
when /\+(\d+)d/ then base_date += Regexp.last_match(1).to_i.days
when /-(\d+)d/ then base_date -= Regexp.last_match(1).to_i.days
when /\+(\d+)w/ then base_date += Regexp.last_match(1).to_i.weeks
end
end
base_date.strftime(format)
}
}
# Usage in prompt:
Today: {DATE:%B %d, %Y}
Next week: {DATE:%B %d, %Y:+7d}
Last month: {DATE:%B %Y:-1m}
Validation and Error Handling¶
Parameter Validation¶
config.custom_keywords['VALIDATED_EMAIL'] = {
pattern: /\{EMAIL:([^}]+)\}/,
validator: ->(email) {
return false unless email.is_a?(String)
return false unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
# Additional validation
domain = email.split('@').last
!['tempmail.com', 'throwaway.email'].include?(domain)
},
error_message: 'Please provide a valid email address from an allowed domain'
}
Custom Error Handling¶
class CustomKeywordProcessor
def self.process_keyword(keyword, args, context)
case keyword
when 'SECURE_DATA'
return '[REDACTED]' if context[:redact_sensitive_data]
args.first
when 'API_CALL'
begin
api_result = make_api_call(args.first)
api_result['data']
rescue => e
Rails.logger.error "API call failed: #{e.message}"
'[API_ERROR]'
end
else
raise PromptManager::UnknownKeywordError.new("Unknown keyword: #{keyword}")
end
end
end
Dynamic Keywords¶
Runtime Registration¶
class DynamicKeywordManager
def self.register_for_user(user)
PromptManager.configure do |config|
config.custom_keywords ||= {}
# User-specific keywords
config.custom_keywords["USER_#{user.id}_NAME"] = {
pattern: /\{USER_NAME\}/,
processor: ->(*args, context) { user.full_name }
}
# Role-based keywords
if user.admin?
config.custom_keywords['ADMIN_PANEL'] = {
pattern: /\{ADMIN_PANEL:([^}]+)\}/,
processor: ->(content, context) { content }
}
end
end
end
end
# Usage
DynamicKeywordManager.register_for_user(current_user)
Database-Driven Keywords¶
class DatabaseKeywordLoader
def self.load_keywords
CustomKeyword.active.each do |keyword_record|
PromptManager.configure do |config|
config.custom_keywords[keyword_record.name] = {
pattern: Regexp.new(keyword_record.pattern),
processor: eval(keyword_record.processor_code),
description: keyword_record.description
}
end
end
end
end
# Load keywords on application startup
DatabaseKeywordLoader.load_keywords
Integration with ERB¶
ERB-Enhanced Keywords¶
config.custom_keywords['ERB_EVAL'] = {
pattern: /\{ERB:([^}]+)\}/,
processor: ->(erb_code, context) {
template = ERB.new(erb_code)
template.result(binding)
}
}
# Usage in prompt:
Current time: {ERB:<%= Time.current.strftime('%H:%M') %>}
Random number: {ERB:<%= rand(100) %>}
Template Inheritance¶
config.custom_keywords['PARENT'] = {
pattern: /\{PARENT:([^}]+)\}/,
processor: ->(parent_template, context) {
parent_prompt = PromptManager::Prompt.new(id: parent_template)
parent_prompt.render(context[:parameters])
}
}
Performance Optimization¶
Keyword Caching¶
class CachedKeywordProcessor
@cache = {}
def self.process_with_cache(keyword, args, context, cache_ttl: 300)
cache_key = "#{keyword}:#{args.join(':')}:#{context.hash}"
cached_result = @cache[cache_key]
if cached_result && (Time.current - cached_result[:timestamp]) < cache_ttl
return cached_result[:value]
end
result = process_keyword(keyword, args, context)
@cache[cache_key] = {
value: result,
timestamp: Time.current
}
result
end
end
Lazy Evaluation¶
config.custom_keywords['LAZY_LOAD'] = {
pattern: /\{LAZY:([^}]+)\}/,
processor: ->(data_source, context) {
# Only load data when actually needed
-> { expensive_data_load(data_source) }
}
}
Testing Custom Keywords¶
RSpec Examples¶
describe 'Custom Keywords' do
before do
PromptManager.configure do |config|
config.custom_keywords = {
'TEST_UPPER' => {
pattern: /\{UPPER:([^}]+)\}/,
transformer: ->(value) { value.upcase }
}
}
end
end
it 'processes custom keyword' do
prompt = PromptManager::Prompt.new(id: 'test')
allow(prompt.storage).to receive(:read).and_return('Hello {UPPER:world}')
result = prompt.render
expect(result).to eq 'Hello WORLD'
end
it 'validates custom keyword input' do
PromptManager.configure do |config|
config.custom_keywords['VALIDATED'] = {
pattern: /\{VALIDATED:([^}]+)\}/,
validator: ->(value) { value.length > 3 },
error_message: 'Value must be longer than 3 characters'
}
end
prompt = PromptManager::Prompt.new(id: 'test')
allow(prompt.storage).to receive(:read).and_return('Hello {VALIDATED:ab}')
expect {
prompt.render
}.to raise_error(PromptManager::ValidationError, /Value must be longer than 3 characters/)
end
end
Real-World Examples¶
E-commerce Keywords¶
PromptManager.configure do |config|
config.custom_keywords.merge!({
'PRICE' => {
pattern: /\{PRICE:([^:}]+)(?::([A-Z]{3}))?\}/,
processor: ->(amount, currency, context) {
currency ||= 'USD'
user_country = context.dig(:parameters, :user_country)
# Adjust currency based on user location
case user_country
when 'GB' then currency = 'GBP'
when 'DE', 'FR', 'IT' then currency = 'EUR'
end
CurrencyFormatter.format(amount.to_f, currency)
}
},
'INVENTORY_STATUS' => {
pattern: /\{STOCK:([^}]+)\}/,
processor: ->(product_id, context) {
stock_level = InventoryService.check_stock(product_id)
case stock_level
when 0 then '❌ Out of Stock'
when 1..5 then '⚠️ Low Stock'
else '✅ In Stock'
end
}
}
})
end
Localization Keywords¶
config.custom_keywords['TRANSLATE'] = {
pattern: /\{T:([^:}]+)(?::([a-z]{2}))?\}/,
processor: ->(key, locale, context) {
locale ||= context.dig(:parameters, :locale) || 'en'
I18n.with_locale(locale) { I18n.t(key) }
}
}
config.custom_keywords['PLURALIZE'] = {
pattern: /\{PLURAL:([^:]+):([^:]+):([^}]+)\}/,
processor: ->(count, singular, plural, context) {
count_val = context.dig(:parameters, count.to_sym) || 0
count_val.to_i == 1 ? singular : plural
}
}
Best Practices¶
- Descriptive Names: Use clear, descriptive names for custom keywords
- Validation: Always validate input parameters
- Error Handling: Provide meaningful error messages
- Documentation: Document keyword syntax and behavior
- Performance: Cache expensive operations
- Security: Sanitize user input in keyword processors
- Testing: Write comprehensive tests for custom keywords
- Consistency: Follow consistent naming conventions across keywords