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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/COMMITS.md +196 -0
  4. data/README.md +485 -203
  5. data/docs/.keep +0 -0
  6. data/docs/advanced/custom-keywords.md +421 -0
  7. data/docs/advanced/dynamic-directives.md +535 -0
  8. data/docs/advanced/performance.md +612 -0
  9. data/docs/advanced/search-integration.md +635 -0
  10. data/docs/api/configuration.md +355 -0
  11. data/docs/api/directive-processor.md +431 -0
  12. data/docs/api/prompt-class.md +354 -0
  13. data/docs/api/storage-adapters.md +462 -0
  14. data/docs/assets/favicon.ico +1 -0
  15. data/docs/assets/logo.svg +24 -0
  16. data/docs/core-features/comments.md +48 -0
  17. data/docs/core-features/directive-processing.md +38 -0
  18. data/docs/core-features/erb-integration.md +68 -0
  19. data/docs/core-features/error-handling.md +197 -0
  20. data/docs/core-features/parameter-history.md +76 -0
  21. data/docs/core-features/parameterized-prompts.md +500 -0
  22. data/docs/core-features/shell-integration.md +79 -0
  23. data/docs/development/architecture.md +544 -0
  24. data/docs/development/contributing.md +425 -0
  25. data/docs/development/roadmap.md +234 -0
  26. data/docs/development/testing.md +822 -0
  27. data/docs/examples/advanced.md +523 -0
  28. data/docs/examples/basic.md +688 -0
  29. data/docs/examples/real-world.md +776 -0
  30. data/docs/examples.md +337 -0
  31. data/docs/getting-started/basic-concepts.md +318 -0
  32. data/docs/getting-started/installation.md +97 -0
  33. data/docs/getting-started/quick-start.md +256 -0
  34. data/docs/index.md +230 -0
  35. data/docs/migration/v0.9.0.md +459 -0
  36. data/docs/migration/v1.0.0.md +591 -0
  37. data/docs/storage/activerecord-adapter.md +348 -0
  38. data/docs/storage/custom-adapters.md +176 -0
  39. data/docs/storage/filesystem-adapter.md +236 -0
  40. data/docs/storage/overview.md +427 -0
  41. data/examples/advanced_integrations.rb +52 -0
  42. data/examples/prompts_dir/advanced_demo.txt +79 -0
  43. data/examples/prompts_dir/directive_example.json +1 -0
  44. data/examples/prompts_dir/directive_example.txt +8 -0
  45. data/examples/prompts_dir/todo.json +1 -1
  46. data/improvement_plan.md +996 -0
  47. data/lib/prompt_manager/storage/file_system_adapter.rb +8 -2
  48. data/lib/prompt_manager/version.rb +1 -1
  49. data/mkdocs.yml +146 -0
  50. data/prompt_manager_logo.png +0 -0
  51. metadata +46 -3
  52. data/LICENSE.txt +0 -21
@@ -0,0 +1,612 @@
1
+ # Performance Optimization
2
+
3
+ This guide covers techniques and best practices for optimizing PromptManager performance in production environments.
4
+
5
+ ## Caching Strategies
6
+
7
+ ### Prompt Content Caching
8
+
9
+ ```ruby
10
+ # Enable built-in caching
11
+ PromptManager.configure do |config|
12
+ config.cache_prompts = true
13
+ config.cache_ttl = 3600 # 1 hour
14
+ config.cache_store = ActiveSupport::Cache::RedisStore.new(
15
+ url: ENV['REDIS_URL'],
16
+ namespace: 'prompt_manager'
17
+ )
18
+ end
19
+ ```
20
+
21
+ ### Custom Caching Layer
22
+
23
+ ```ruby
24
+ class CachedPromptManager
25
+ def self.render(prompt_id, parameters = {}, cache_options = {})
26
+ cache_key = generate_cache_key(prompt_id, parameters)
27
+
28
+ Rails.cache.fetch(cache_key, cache_options) do
29
+ prompt = PromptManager::Prompt.new(id: prompt_id)
30
+ prompt.render(parameters)
31
+ end
32
+ end
33
+
34
+ def self.invalidate_cache(prompt_id, parameters = nil)
35
+ if parameters
36
+ cache_key = generate_cache_key(prompt_id, parameters)
37
+ Rails.cache.delete(cache_key)
38
+ else
39
+ # Invalidate all cached versions of this prompt
40
+ Rails.cache.delete_matched("prompt:#{prompt_id}:*")
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def self.generate_cache_key(prompt_id, parameters)
47
+ param_hash = Digest::MD5.hexdigest(parameters.to_json)
48
+ "prompt:#{prompt_id}:#{param_hash}"
49
+ end
50
+ end
51
+
52
+ # Usage
53
+ result = CachedPromptManager.render('welcome_email', { name: 'John' }, expires_in: 30.minutes)
54
+ ```
55
+
56
+ ### Multi-level Caching
57
+
58
+ ```ruby
59
+ class HierarchicalPromptCache
60
+ def initialize
61
+ @l1_cache = ActiveSupport::Cache::MemoryStore.new(size: 100) # Fast, small
62
+ @l2_cache = Rails.cache # Redis, larger but slower
63
+ end
64
+
65
+ def fetch(key, options = {}, &block)
66
+ # Try L1 cache first
67
+ result = @l1_cache.read(key)
68
+ return result if result
69
+
70
+ # Try L2 cache
71
+ result = @l2_cache.fetch(key, options, &block)
72
+
73
+ # Store in L1 cache for next time
74
+ @l1_cache.write(key, result, expires_in: 5.minutes) if result
75
+
76
+ result
77
+ end
78
+
79
+ def invalidate(key_pattern)
80
+ @l1_cache.clear
81
+ @l2_cache.delete_matched(key_pattern)
82
+ end
83
+ end
84
+ ```
85
+
86
+ ## Storage Optimization
87
+
88
+ ### Connection Pooling
89
+
90
+ ```ruby
91
+ class PooledDatabaseAdapter < PromptManager::Storage::ActiveRecordAdapter
92
+ def initialize(pool_size: 10, **options)
93
+ super(**options)
94
+ @connection_pool = ConnectionPool.new(size: pool_size) do
95
+ model_class.connection_pool.checkout
96
+ end
97
+ end
98
+
99
+ def read(prompt_id)
100
+ @connection_pool.with do |connection|
101
+ result = connection.exec_query(
102
+ "SELECT content FROM prompts WHERE prompt_id = ?",
103
+ 'PromptManager::Read',
104
+ [prompt_id]
105
+ )
106
+
107
+ raise PromptNotFoundError unless result.any?
108
+ result.first['content']
109
+ end
110
+ end
111
+
112
+ def write(prompt_id, content)
113
+ @connection_pool.with do |connection|
114
+ connection.exec_insert(
115
+ "INSERT INTO prompts (prompt_id, content, updated_at) VALUES (?, ?, ?) " \
116
+ "ON CONFLICT (prompt_id) DO UPDATE SET content = ?, updated_at = ?",
117
+ 'PromptManager::Write',
118
+ [prompt_id, content, Time.current, content, Time.current]
119
+ )
120
+ end
121
+ true
122
+ end
123
+ end
124
+ ```
125
+
126
+ ### Bulk Operations
127
+
128
+ ```ruby
129
+ class BulkPromptOperations
130
+ def self.bulk_render(prompt_configs, batch_size: 100)
131
+ results = {}
132
+
133
+ prompt_configs.each_slice(batch_size) do |batch|
134
+ # Pre-load all prompts in the batch
135
+ prompt_contents = preload_prompts(batch.map { |config| config[:prompt_id] })
136
+
137
+ # Process batch in parallel
138
+ batch_results = Parallel.map(batch, in_threads: 4) do |config|
139
+ begin
140
+ content = prompt_contents[config[:prompt_id]]
141
+ next unless content
142
+
143
+ processor = PromptManager::DirectiveProcessor.new
144
+ result = processor.process(content, config[:parameters])
145
+
146
+ [config[:prompt_id], { success: true, result: result }]
147
+ rescue => e
148
+ [config[:prompt_id], { success: false, error: e.message }]
149
+ end
150
+ end.compact
151
+
152
+ batch_results.each do |prompt_id, result|
153
+ results[prompt_id] = result
154
+ end
155
+ end
156
+
157
+ results
158
+ end
159
+
160
+ private
161
+
162
+ def self.preload_prompts(prompt_ids)
163
+ # Batch load all prompts at once
164
+ if PromptManager.storage.respond_to?(:bulk_read)
165
+ PromptManager.storage.bulk_read(prompt_ids)
166
+ else
167
+ prompt_ids.each_with_object({}) do |id, hash|
168
+ begin
169
+ hash[id] = PromptManager.storage.read(id)
170
+ rescue PromptNotFoundError
171
+ # Skip missing prompts
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ # Usage
179
+ configs = [
180
+ { prompt_id: 'welcome', parameters: { name: 'Alice' } },
181
+ { prompt_id: 'welcome', parameters: { name: 'Bob' } },
182
+ { prompt_id: 'reminder', parameters: { task: 'Meeting' } }
183
+ ]
184
+
185
+ results = BulkPromptOperations.bulk_render(configs)
186
+ ```
187
+
188
+ ## Directive Processing Optimization
189
+
190
+ ### Lazy Evaluation
191
+
192
+ ```ruby
193
+ class LazyDirectiveProcessor < PromptManager::DirectiveProcessor
194
+ def process(content, context = {})
195
+ # Only process directives that are actually needed
196
+ lazy_content = LazyContent.new(content, context)
197
+ lazy_content.to_s
198
+ end
199
+ end
200
+
201
+ class LazyContent
202
+ def initialize(content, context)
203
+ @content = content
204
+ @context = context
205
+ @processed = false
206
+ @result = nil
207
+ end
208
+
209
+ def to_s
210
+ return @result if @processed
211
+
212
+ # Process only when needed
213
+ @result = process_directives
214
+ @processed = true
215
+ @result
216
+ end
217
+
218
+ private
219
+
220
+ def process_directives
221
+ # Only process directives that appear in the content
222
+ directive_pattern = %r{^//(\w+)\s+(.*)$}
223
+
224
+ @content.gsub(directive_pattern) do |match|
225
+ directive_name = Regexp.last_match(1)
226
+ directive_args = Regexp.last_match(2)
227
+
228
+ # Skip processing if directive handler doesn't exist
229
+ next match unless directive_handlers.key?(directive_name)
230
+
231
+ # Process directive
232
+ handler = directive_handlers[directive_name]
233
+ handler.call(directive_args, @context)
234
+ end
235
+ end
236
+ end
237
+ ```
238
+
239
+ ### Directive Compilation
240
+
241
+ ```ruby
242
+ class CompiledDirectiveProcessor
243
+ def initialize
244
+ @compiled_templates = {}
245
+ end
246
+
247
+ def compile(content)
248
+ template_id = Digest::MD5.hexdigest(content)
249
+
250
+ @compiled_templates[template_id] ||= compile_template(content)
251
+ end
252
+
253
+ def render(template_id, context)
254
+ compiled_template = @compiled_templates[template_id]
255
+ return nil unless compiled_template
256
+
257
+ compiled_template.call(context)
258
+ end
259
+
260
+ private
261
+
262
+ def compile_template(content)
263
+ # Pre-compile template into executable code
264
+ ruby_code = convert_to_ruby(content)
265
+
266
+ # Create a proc that can be called with context
267
+ eval("lambda { |context| #{ruby_code} }")
268
+ end
269
+
270
+ def convert_to_ruby(content)
271
+ # Convert directive syntax to Ruby code
272
+ content.gsub(%r{//include\s+(.+)}) do |match|
273
+ file_path = Regexp.last_match(1).strip
274
+ %{PromptManager.storage.read("#{file_path}")}
275
+ end.gsub(/\[(\w+)\]/) do |match|
276
+ param_name = Regexp.last_match(1).downcase
277
+ %{context[:parameters][:#{param_name}]}
278
+ end
279
+ end
280
+ end
281
+ ```
282
+
283
+ ## Memory Management
284
+
285
+ ### Memory-Efficient Prompt Loading
286
+
287
+ ```ruby
288
+ class StreamingPromptProcessor
289
+ def process_large_prompt(prompt_id, parameters = {})
290
+ prompt_file = PromptManager.storage.file_path(prompt_id)
291
+
292
+ Enumerator.new do |yielder|
293
+ File.foreach(prompt_file) do |line|
294
+ processed_line = process_line(line, parameters)
295
+ yielder << processed_line unless processed_line.empty?
296
+ end
297
+ end
298
+ end
299
+
300
+ private
301
+
302
+ def process_line(line, parameters)
303
+ # Process parameters in this line
304
+ line.gsub(/\[(\w+)\]/) do |match|
305
+ param_name = Regexp.last_match(1).downcase.to_sym
306
+ parameters[param_name] || match
307
+ end
308
+ end
309
+ end
310
+
311
+ # Usage for large prompts
312
+ processor = StreamingPromptProcessor.new
313
+ prompt_stream = processor.process_large_prompt('huge_prompt', user_id: 123)
314
+
315
+ prompt_stream.each do |line|
316
+ # Process line by line without loading entire prompt into memory
317
+ output_stream.puts line
318
+ end
319
+ ```
320
+
321
+ ### Object Pool Pattern
322
+
323
+ ```ruby
324
+ class PromptProcessorPool
325
+ def initialize(size: 10)
326
+ @pool = Queue.new
327
+ @size = size
328
+
329
+ size.times do
330
+ @pool << PromptManager::DirectiveProcessor.new
331
+ end
332
+ end
333
+
334
+ def with_processor
335
+ processor = @pool.pop
336
+ begin
337
+ yield processor
338
+ ensure
339
+ # Reset processor state
340
+ processor.reset_state if processor.respond_to?(:reset_state)
341
+ @pool << processor
342
+ end
343
+ end
344
+ end
345
+
346
+ # Global pool
347
+ PROCESSOR_POOL = PromptProcessorPool.new(size: 20)
348
+
349
+ # Usage
350
+ PROCESSOR_POOL.with_processor do |processor|
351
+ result = processor.process(content, context)
352
+ end
353
+ ```
354
+
355
+ ## Database Query Optimization
356
+
357
+ ### Query Optimization for ActiveRecord Adapter
358
+
359
+ ```ruby
360
+ class OptimizedActiveRecordAdapter < PromptManager::Storage::ActiveRecordAdapter
361
+ def bulk_read(prompt_ids)
362
+ # Single query instead of N+1
363
+ prompts = model_class.where(id_column => prompt_ids)
364
+ .pluck(id_column, content_column)
365
+ .to_h
366
+
367
+ # Ensure all requested IDs are present
368
+ missing_ids = prompt_ids - prompts.keys
369
+ missing_ids.each { |id| prompts[id] = nil }
370
+
371
+ prompts
372
+ end
373
+
374
+ def read_with_metadata(prompt_id)
375
+ # Fetch prompt and metadata in single query
376
+ prompt = model_class.select(:id, :content, :metadata, :updated_at)
377
+ .find_by(id_column => prompt_id)
378
+
379
+ raise PromptNotFoundError unless prompt
380
+
381
+ {
382
+ content: prompt.send(content_column),
383
+ metadata: prompt.metadata,
384
+ last_modified: prompt.updated_at
385
+ }
386
+ end
387
+
388
+ def frequently_used_prompts(limit: 100)
389
+ # Cache frequently accessed prompts
390
+ model_class.joins(:usage_logs)
391
+ .group(id_column)
392
+ .order('COUNT(usage_logs.id) DESC')
393
+ .limit(limit)
394
+ .pluck(id_column, content_column)
395
+ .to_h
396
+ end
397
+ end
398
+ ```
399
+
400
+ ### Index Optimization
401
+
402
+ ```sql
403
+ -- Optimize database indexes for prompt queries
404
+
405
+ -- Primary lookup index
406
+ CREATE INDEX CONCURRENTLY idx_prompts_id_active
407
+ ON prompts(prompt_id) WHERE active = true;
408
+
409
+ -- Content search index (PostgreSQL)
410
+ CREATE INDEX CONCURRENTLY idx_prompts_content_gin
411
+ ON prompts USING gin(to_tsvector('english', content));
412
+
413
+ -- Metadata search index (PostgreSQL with JSONB)
414
+ CREATE INDEX CONCURRENTLY idx_prompts_metadata_gin
415
+ ON prompts USING gin(metadata);
416
+
417
+ -- Usage-based queries
418
+ CREATE INDEX CONCURRENTLY idx_prompts_usage_updated
419
+ ON prompts(usage_count DESC, updated_at DESC);
420
+
421
+ -- Composite index for filtered queries
422
+ CREATE INDEX CONCURRENTLY idx_prompts_category_status_updated
423
+ ON prompts(category, status, updated_at DESC);
424
+ ```
425
+
426
+ ## Monitoring and Profiling
427
+
428
+ ### Performance Monitoring
429
+
430
+ ```ruby
431
+ class PromptPerformanceMonitor
432
+ def self.monitor_render(prompt_id, parameters = {})
433
+ start_time = Time.current
434
+ memory_before = get_memory_usage
435
+
436
+ begin
437
+ result = yield
438
+
439
+ duration = Time.current - start_time
440
+ memory_after = get_memory_usage
441
+ memory_used = memory_after - memory_before
442
+
443
+ log_performance_metrics(prompt_id, {
444
+ duration: duration,
445
+ memory_used: memory_used,
446
+ parameters_count: parameters.size,
447
+ result_size: result.bytesize,
448
+ success: true
449
+ })
450
+
451
+ result
452
+ rescue => e
453
+ duration = Time.current - start_time
454
+
455
+ log_performance_metrics(prompt_id, {
456
+ duration: duration,
457
+ error: e.class.name,
458
+ success: false
459
+ })
460
+
461
+ raise e
462
+ end
463
+ end
464
+
465
+ private
466
+
467
+ def self.get_memory_usage
468
+ GC.stat[:heap_allocated_pages] * GC::INTERNAL_CONSTANTS[:HEAP_PAGE_SIZE]
469
+ end
470
+
471
+ def self.log_performance_metrics(prompt_id, metrics)
472
+ Rails.logger.info "PromptManager Performance: #{prompt_id}", metrics
473
+
474
+ # Send to monitoring service
475
+ if defined?(StatsD)
476
+ StatsD.histogram('prompt_manager.render_duration', metrics[:duration])
477
+ StatsD.histogram('prompt_manager.memory_usage', metrics[:memory_used]) if metrics[:memory_used]
478
+ StatsD.increment('prompt_manager.renders', tags: ["success:#{metrics[:success]}"])
479
+ end
480
+ end
481
+ end
482
+
483
+ # Usage
484
+ result = PromptPerformanceMonitor.monitor_render('welcome_email', name: 'John') do
485
+ prompt = PromptManager::Prompt.new(id: 'welcome_email')
486
+ prompt.render(name: 'John')
487
+ end
488
+ ```
489
+
490
+ ### Custom Profiling
491
+
492
+ ```ruby
493
+ class PromptProfiler
494
+ def self.profile_render(prompt_id, parameters = {})
495
+ profiler = RubyProf.profile do
496
+ prompt = PromptManager::Prompt.new(id: prompt_id)
497
+ prompt.render(parameters)
498
+ end
499
+
500
+ # Generate reports
501
+ printer = RubyProf::GraphHtmlPrinter.new(profiler)
502
+ File.open("tmp/profile_#{prompt_id}_#{Time.current.to_i}.html", 'w') do |file|
503
+ printer.print(file)
504
+ end
505
+ end
506
+
507
+ def self.benchmark_operations(iterations: 100)
508
+ Benchmark.bmbm do |x|
509
+ x.report("File read:") do
510
+ iterations.times { PromptManager.storage.read('test_prompt') }
511
+ end
512
+
513
+ x.report("Template render:") do
514
+ prompt = PromptManager::Prompt.new(id: 'test_prompt')
515
+ iterations.times { prompt.render(name: 'test') }
516
+ end
517
+
518
+ x.report("Cached render:") do
519
+ iterations.times { CachedPromptManager.render('test_prompt', name: 'test') }
520
+ end
521
+ end
522
+ end
523
+ end
524
+ ```
525
+
526
+ ## Production Deployment Optimization
527
+
528
+ ### Preloading and Warmup
529
+
530
+ ```ruby
531
+ class PromptPreloader
532
+ def self.preload_critical_prompts
533
+ critical_prompts = %w[
534
+ welcome_email
535
+ password_reset
536
+ order_confirmation
537
+ error_notification
538
+ ]
539
+
540
+ critical_prompts.each do |prompt_id|
541
+ begin
542
+ prompt = PromptManager::Prompt.new(id: prompt_id)
543
+
544
+ # Preload into cache
545
+ CachedPromptManager.render(prompt_id, {}, expires_in: 1.hour)
546
+
547
+ Rails.logger.info "Preloaded prompt: #{prompt_id}"
548
+ rescue => e
549
+ Rails.logger.error "Failed to preload #{prompt_id}: #{e.message}"
550
+ end
551
+ end
552
+ end
553
+
554
+ def self.warmup_processor_pool
555
+ # Initialize processor pool
556
+ PROCESSOR_POOL.with_processor do |processor|
557
+ processor.process("//include test\nWarmup content [TEST]",
558
+ parameters: { test: 'value' })
559
+ end
560
+
561
+ Rails.logger.info "Processor pool warmed up"
562
+ end
563
+ end
564
+
565
+ # In Rails initializer or deployment script
566
+ Rails.application.config.after_initialize do
567
+ PromptPreloader.preload_critical_prompts
568
+ PromptPreloader.warmup_processor_pool
569
+ end
570
+ ```
571
+
572
+ ### Configuration for Production
573
+
574
+ ```ruby
575
+ # config/environments/production.rb
576
+ PromptManager.configure do |config|
577
+ # Use optimized storage adapter
578
+ config.storage = OptimizedActiveRecordAdapter.new
579
+
580
+ # Enable aggressive caching
581
+ config.cache_prompts = true
582
+ config.cache_ttl = 3600 # 1 hour
583
+ config.cache_store = ActiveSupport::Cache::RedisStore.new(
584
+ url: ENV['REDIS_URL'],
585
+ pool_size: 10,
586
+ pool_timeout: 5
587
+ )
588
+
589
+ # Optimize processing
590
+ config.max_include_depth = 5 # Reduce for performance
591
+ config.directive_timeout = 10 # Shorter timeout
592
+
593
+ # Error handling
594
+ config.raise_on_missing_prompts = false
595
+ config.error_handler = ->(error, context) {
596
+ Rails.logger.error "Prompt error: #{error.message}"
597
+ ErrorTracker.notify(error, context)
598
+ 'Content temporarily unavailable'
599
+ }
600
+ end
601
+ ```
602
+
603
+ ## Best Practices Summary
604
+
605
+ 1. **Cache Aggressively**: Cache rendered prompts and frequently accessed content
606
+ 2. **Batch Operations**: Process multiple prompts together when possible
607
+ 3. **Monitor Performance**: Track render times, memory usage, and error rates
608
+ 4. **Optimize Queries**: Use proper indexes and minimize database roundtrips
609
+ 5. **Pool Resources**: Reuse expensive objects like processors and connections
610
+ 6. **Profile Regularly**: Identify bottlenecks in production workloads
611
+ 7. **Preload Critical Content**: Warm up caches with important prompts
612
+ 8. **Handle Errors Gracefully**: Provide fallbacks when performance degrades