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
@@ -0,0 +1,348 @@
|
|
1
|
+
# ActiveRecordAdapter
|
2
|
+
|
3
|
+
The ActiveRecordAdapter allows you to store prompts in a relational database using ActiveRecord, perfect for Rails applications and scenarios requiring database-backed prompt storage.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Store prompts in your application's database alongside other application data. This adapter provides full CRUD operations, query capabilities, and integration with ActiveRecord models.
|
8
|
+
|
9
|
+
## Setup
|
10
|
+
|
11
|
+
### Migration
|
12
|
+
|
13
|
+
Create the prompts table:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
# Generate migration
|
17
|
+
rails generate migration CreatePrompts
|
18
|
+
|
19
|
+
# In the migration file:
|
20
|
+
class CreatePrompts < ActiveRecord::Migration[7.0]
|
21
|
+
def change
|
22
|
+
create_table :prompts do |t|
|
23
|
+
t.string :prompt_id, null: false, index: { unique: true }
|
24
|
+
t.text :content, null: false
|
25
|
+
t.text :description
|
26
|
+
t.json :metadata, default: {}
|
27
|
+
t.timestamps
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
### Model
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
# app/models/prompt.rb
|
37
|
+
class Prompt < ApplicationRecord
|
38
|
+
validates :prompt_id, presence: true, uniqueness: true
|
39
|
+
validates :content, presence: true
|
40
|
+
|
41
|
+
# Optional: Add scopes for organization
|
42
|
+
scope :by_category, ->(category) { where("metadata->>'category' = ?", category) }
|
43
|
+
scope :recent, -> { order(updated_at: :desc) }
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
### Configuration
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# Configure PromptManager to use ActiveRecord
|
51
|
+
PromptManager.configure do |config|
|
52
|
+
config.storage = PromptManager::Storage::ActiveRecordAdapter.new(
|
53
|
+
model_class: Prompt,
|
54
|
+
id_column: :prompt_id,
|
55
|
+
content_column: :content
|
56
|
+
)
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
## Basic Usage
|
61
|
+
|
62
|
+
### Creating Prompts
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# Create via PromptManager
|
66
|
+
prompt = PromptManager::Prompt.new(id: 'welcome_email')
|
67
|
+
prompt.save(
|
68
|
+
content: 'Welcome to our service, [USER_NAME]!',
|
69
|
+
metadata: {
|
70
|
+
category: 'email',
|
71
|
+
author: 'marketing_team',
|
72
|
+
version: '1.0'
|
73
|
+
}
|
74
|
+
)
|
75
|
+
|
76
|
+
# Or create via ActiveRecord
|
77
|
+
Prompt.create!(
|
78
|
+
prompt_id: 'goodbye_email',
|
79
|
+
content: 'Thanks for using our service, [USER_NAME]!',
|
80
|
+
description: 'Farewell email template',
|
81
|
+
metadata: { category: 'email', priority: 'low' }
|
82
|
+
)
|
83
|
+
```
|
84
|
+
|
85
|
+
### Reading Prompts
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# Via PromptManager (recommended)
|
89
|
+
prompt = PromptManager::Prompt.new(id: 'welcome_email')
|
90
|
+
result = prompt.render(user_name: 'Alice')
|
91
|
+
|
92
|
+
# Via ActiveRecord
|
93
|
+
prompt_record = Prompt.find_by(prompt_id: 'welcome_email')
|
94
|
+
puts prompt_record.content
|
95
|
+
```
|
96
|
+
|
97
|
+
### Updating Prompts
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# Via PromptManager
|
101
|
+
prompt = PromptManager::Prompt.new(id: 'welcome_email')
|
102
|
+
prompt.save('Updated content: Welcome [USER_NAME] to our platform!')
|
103
|
+
|
104
|
+
# Via ActiveRecord
|
105
|
+
prompt_record = Prompt.find_by(prompt_id: 'welcome_email')
|
106
|
+
prompt_record.update!(
|
107
|
+
content: 'Updated content...',
|
108
|
+
metadata: prompt_record.metadata.merge(version: '1.1')
|
109
|
+
)
|
110
|
+
```
|
111
|
+
|
112
|
+
## Advanced Features
|
113
|
+
|
114
|
+
### Query Interface
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
# Configure with query support
|
118
|
+
PromptManager.configure do |config|
|
119
|
+
config.storage = PromptManager::Storage::ActiveRecordAdapter.new(
|
120
|
+
model_class: Prompt,
|
121
|
+
id_column: :prompt_id,
|
122
|
+
content_column: :content,
|
123
|
+
enable_queries: true
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Search prompts
|
128
|
+
results = PromptManager.storage.search(
|
129
|
+
category: 'email',
|
130
|
+
content_contains: 'welcome'
|
131
|
+
)
|
132
|
+
|
133
|
+
# List by metadata
|
134
|
+
email_prompts = PromptManager.storage.where(
|
135
|
+
"metadata->>'category' = ?", 'email'
|
136
|
+
)
|
137
|
+
```
|
138
|
+
|
139
|
+
### Versioning
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
# Add versioning to your model
|
143
|
+
class Prompt < ApplicationRecord
|
144
|
+
has_many :prompt_versions, dependent: :destroy
|
145
|
+
|
146
|
+
before_update :create_version
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def create_version
|
151
|
+
if content_changed?
|
152
|
+
prompt_versions.create!(
|
153
|
+
content: content_was,
|
154
|
+
version_number: (prompt_versions.maximum(:version_number) || 0) + 1,
|
155
|
+
created_at: updated_at_was
|
156
|
+
)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Version model
|
162
|
+
class PromptVersion < ApplicationRecord
|
163
|
+
belongs_to :prompt
|
164
|
+
end
|
165
|
+
|
166
|
+
# Migration for versions
|
167
|
+
class CreatePromptVersions < ActiveRecord::Migration[7.0]
|
168
|
+
def change
|
169
|
+
create_table :prompt_versions do |t|
|
170
|
+
t.references :prompt, null: false, foreign_key: true
|
171
|
+
t.text :content, null: false
|
172
|
+
t.integer :version_number, null: false
|
173
|
+
t.timestamps
|
174
|
+
end
|
175
|
+
|
176
|
+
add_index :prompt_versions, [:prompt_id, :version_number], unique: true
|
177
|
+
end
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
### Multi-tenancy
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
# Add tenant support
|
185
|
+
class Prompt < ApplicationRecord
|
186
|
+
belongs_to :tenant
|
187
|
+
|
188
|
+
validates :prompt_id, uniqueness: { scope: :tenant_id }
|
189
|
+
|
190
|
+
scope :for_tenant, ->(tenant) { where(tenant: tenant) }
|
191
|
+
end
|
192
|
+
|
193
|
+
# Configure with tenant scope
|
194
|
+
PromptManager.configure do |config|
|
195
|
+
config.storage = PromptManager::Storage::ActiveRecordAdapter.new(
|
196
|
+
model_class: Prompt,
|
197
|
+
id_column: :prompt_id,
|
198
|
+
content_column: :content,
|
199
|
+
scope: -> { Prompt.for_tenant(Current.tenant) }
|
200
|
+
)
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
## Performance Optimization
|
205
|
+
|
206
|
+
### Indexing
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
# Add performance indexes
|
210
|
+
class AddPromptIndexes < ActiveRecord::Migration[7.0]
|
211
|
+
def change
|
212
|
+
add_index :prompts, :prompt_id, unique: true
|
213
|
+
add_index :prompts, :updated_at
|
214
|
+
add_index :prompts, "((metadata->>'category'))", name: 'index_prompts_on_category'
|
215
|
+
add_index :prompts, :content, type: :gin # For full-text search (PostgreSQL)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
### Caching
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
# Configure caching
|
224
|
+
PromptManager.configure do |config|
|
225
|
+
config.storage = PromptManager::Storage::ActiveRecordAdapter.new(
|
226
|
+
model_class: Prompt,
|
227
|
+
id_column: :prompt_id,
|
228
|
+
content_column: :content,
|
229
|
+
cache_queries: true,
|
230
|
+
cache_ttl: 300 # 5 minutes
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Add caching to your model
|
235
|
+
class Prompt < ApplicationRecord
|
236
|
+
after_update :clear_cache
|
237
|
+
after_destroy :clear_cache
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def clear_cache
|
242
|
+
Rails.cache.delete("prompt:#{prompt_id}")
|
243
|
+
end
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
### Connection Pooling
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
# For high-traffic applications
|
251
|
+
class PromptReadOnlyRecord < ApplicationRecord
|
252
|
+
self.abstract_class = true
|
253
|
+
connects_to database: { reading: :prompt_replica }
|
254
|
+
end
|
255
|
+
|
256
|
+
class Prompt < PromptReadOnlyRecord
|
257
|
+
# Read operations use replica database
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
## Integration Examples
|
262
|
+
|
263
|
+
### Rails Controller
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class PromptsController < ApplicationController
|
267
|
+
def show
|
268
|
+
prompt = PromptManager::Prompt.new(id: params[:id])
|
269
|
+
@content = prompt.render(params.permit(:user_name, :product_name))
|
270
|
+
rescue PromptManager::PromptNotFoundError
|
271
|
+
render json: { error: 'Prompt not found' }, status: 404
|
272
|
+
end
|
273
|
+
|
274
|
+
def create
|
275
|
+
prompt = PromptManager::Prompt.new(id: prompt_params[:id])
|
276
|
+
prompt.save(prompt_params[:content])
|
277
|
+
|
278
|
+
render json: { message: 'Prompt created successfully' }
|
279
|
+
rescue => e
|
280
|
+
render json: { error: e.message }, status: 422
|
281
|
+
end
|
282
|
+
|
283
|
+
private
|
284
|
+
|
285
|
+
def prompt_params
|
286
|
+
params.require(:prompt).permit(:id, :content, :description, metadata: {})
|
287
|
+
end
|
288
|
+
end
|
289
|
+
```
|
290
|
+
|
291
|
+
### Background Jobs
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
class ProcessPromptJob < ApplicationJob
|
295
|
+
def perform(prompt_id, parameters)
|
296
|
+
prompt = PromptManager::Prompt.new(id: prompt_id)
|
297
|
+
result = prompt.render(parameters)
|
298
|
+
|
299
|
+
# Process the result
|
300
|
+
NotificationService.send_message(result)
|
301
|
+
rescue PromptManager::PromptNotFoundError => e
|
302
|
+
logger.error "Prompt not found: #{prompt_id}"
|
303
|
+
# Handle gracefully
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Usage
|
308
|
+
ProcessPromptJob.perform_later('daily_report', user_id: user.id)
|
309
|
+
```
|
310
|
+
|
311
|
+
## Best Practices
|
312
|
+
|
313
|
+
1. **Use Transactions**: Wrap prompt operations in database transactions
|
314
|
+
2. **Validate Content**: Add model validations for prompt content
|
315
|
+
3. **Index Strategically**: Index frequently queried fields
|
316
|
+
4. **Cache Wisely**: Cache frequently accessed prompts
|
317
|
+
5. **Monitor Performance**: Track database query performance
|
318
|
+
6. **Backup Regularly**: Include prompts in your database backup strategy
|
319
|
+
7. **Version Control**: Consider versioning for important prompts
|
320
|
+
8. **Secure Access**: Use database-level permissions and encryption
|
321
|
+
|
322
|
+
## Migration from FileSystem
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
# Migrate from filesystem to database
|
326
|
+
class MigratePromptsToDatabase
|
327
|
+
def self.perform(prompts_dir)
|
328
|
+
Dir.glob(File.join(prompts_dir, '**/*.txt')).each do |file_path|
|
329
|
+
relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(prompts_dir))
|
330
|
+
prompt_id = relative_path.sub_ext('').to_s
|
331
|
+
content = File.read(file_path)
|
332
|
+
|
333
|
+
Prompt.create!(
|
334
|
+
prompt_id: prompt_id,
|
335
|
+
content: content,
|
336
|
+
description: "Migrated from #{file_path}",
|
337
|
+
metadata: {
|
338
|
+
original_file: file_path,
|
339
|
+
migrated_at: Time.current
|
340
|
+
}
|
341
|
+
)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Run migration
|
347
|
+
MigratePromptsToDatabase.perform('/path/to/prompts')
|
348
|
+
```
|
@@ -0,0 +1,176 @@
|
|
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
|
+
```
|