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,535 @@
1
+ # Dynamic Directives
2
+
3
+ Dynamic directives allow you to create sophisticated, runtime-configurable prompt processing behaviors that go beyond static `//include` statements.
4
+
5
+ ## Overview
6
+
7
+ Dynamic directives are custom processing instructions that can modify prompt content based on runtime conditions, external data sources, user context, and complex business logic.
8
+
9
+ ## Creating Dynamic Directives
10
+
11
+ ### Basic Dynamic Directive
12
+
13
+ ```ruby
14
+ PromptManager.configure do |config|
15
+ config.directive_processor.register_directive('current_time') do |args, context|
16
+ format = args.strip.empty? ? '%Y-%m-%d %H:%M:%S' : args.strip
17
+ Time.current.strftime(format)
18
+ end
19
+ end
20
+ ```
21
+
22
+ ```text
23
+ # Usage in prompts:
24
+ Generated at: //current_time
25
+ Custom format: //current_time %B %d, %Y at %I:%M %p
26
+ ```
27
+
28
+ ### Context-Aware Directives
29
+
30
+ ```ruby
31
+ config.directive_processor.register_directive('user_greeting') do |args, context|
32
+ user = context.dig(:parameters, :user)
33
+ return 'Hello there!' unless user
34
+
35
+ time_of_day = Time.current.hour
36
+ greeting = case time_of_day
37
+ when 0..11 then 'Good morning'
38
+ when 12..17 then 'Good afternoon'
39
+ else 'Good evening'
40
+ end
41
+
42
+ "#{greeting}, #{user[:name]}!"
43
+ end
44
+ ```
45
+
46
+ ## Advanced Directive Patterns
47
+
48
+ ### Data Loading Directives
49
+
50
+ ```ruby
51
+ config.directive_processor.register_directive('load_user_data') do |user_id, context|
52
+ begin
53
+ user = UserService.find(user_id)
54
+ context[:loaded_user] = user
55
+
56
+ # Return user summary
57
+ <<~USER_INFO
58
+ User: #{user.name}
59
+ Email: #{user.email}
60
+ Account Type: #{user.account_type}
61
+ USER_INFO
62
+ rescue UserNotFoundError
63
+ 'User information unavailable'
64
+ end
65
+ end
66
+ ```
67
+
68
+ ### API Integration Directives
69
+
70
+ ```ruby
71
+ config.directive_processor.register_directive('weather') do |location, context|
72
+ begin
73
+ response = HTTParty.get(
74
+ 'https://api.weather.example.com/current',
75
+ query: {
76
+ location: location,
77
+ api_key: ENV['WEATHER_API_KEY']
78
+ }
79
+ )
80
+
81
+ if response.success?
82
+ data = response.parsed_response
83
+ "Weather in #{location}: #{data['temperature']}°F, #{data['condition']}"
84
+ else
85
+ 'Weather information unavailable'
86
+ end
87
+ rescue => e
88
+ Rails.logger.error "Weather API error: #{e.message}"
89
+ 'Weather service temporarily unavailable'
90
+ end
91
+ end
92
+ ```
93
+
94
+ ### Database Query Directives
95
+
96
+ ```ruby
97
+ config.directive_processor.register_directive('recent_orders') do |user_id, context|
98
+ user = User.find(user_id)
99
+ recent_orders = user.orders.recent.limit(5)
100
+
101
+ if recent_orders.any?
102
+ orders_list = recent_orders.map do |order|
103
+ "- Order ##{order.id}: #{order.total_formatted} (#{order.status})"
104
+ end.join("\n")
105
+
106
+ "Your recent orders:\n#{orders_list}"
107
+ else
108
+ 'You have no recent orders.'
109
+ end
110
+ rescue ActiveRecord::RecordNotFound
111
+ 'User not found'
112
+ end
113
+ ```
114
+
115
+ ## Conditional Logic Directives
116
+
117
+ ### IF/ELSE/ENDIF Directive System
118
+
119
+ ```ruby
120
+ class ConditionalDirectiveProcessor
121
+ def self.register_conditionals(processor)
122
+ processor.register_directive('if') do |condition, context|
123
+ context[:if_stack] ||= []
124
+
125
+ result = evaluate_condition(condition, context)
126
+ context[:if_stack].push({
127
+ condition_met: result,
128
+ in_else: false,
129
+ content: ''
130
+ })
131
+
132
+ '' # Don't output anything for the if directive itself
133
+ end
134
+
135
+ processor.register_directive('else') do |args, context|
136
+ return '' unless context[:if_stack]&.any?
137
+
138
+ current_if = context[:if_stack].last
139
+ current_if[:in_else] = true
140
+
141
+ '' # Don't output anything for the else directive
142
+ end
143
+
144
+ processor.register_directive('endif') do |args, context|
145
+ return '' unless context[:if_stack]&.any?
146
+
147
+ if_block = context[:if_stack].pop
148
+
149
+ # Process the accumulated content based on conditions
150
+ if (if_block[:condition_met] && !if_block[:in_else]) ||
151
+ (!if_block[:condition_met] && if_block[:in_else])
152
+ processor.process(if_block[:content], context)
153
+ else
154
+ ''
155
+ end
156
+ end
157
+ end
158
+
159
+ def self.evaluate_condition(condition, context)
160
+ # Simple condition evaluation
161
+ # In production, use a proper expression evaluator
162
+ case condition.strip
163
+ when /\[(\w+)\]\s*(==|!=|>|<|>=|<=)\s*(.+)/
164
+ param_name = Regexp.last_match(1).downcase.to_sym
165
+ operator = Regexp.last_match(2)
166
+ expected_value = Regexp.last_match(3).strip.gsub(/['"]/, '')
167
+
168
+ actual_value = context.dig(:parameters, param_name).to_s
169
+
170
+ case operator
171
+ when '==' then actual_value == expected_value
172
+ when '!=' then actual_value != expected_value
173
+ when '>' then actual_value.to_f > expected_value.to_f
174
+ when '<' then actual_value.to_f < expected_value.to_f
175
+ when '>=' then actual_value.to_f >= expected_value.to_f
176
+ when '<=' then actual_value.to_f <= expected_value.to_f
177
+ end
178
+ else
179
+ false
180
+ end
181
+ end
182
+ end
183
+
184
+ # Register the conditional directives
185
+ ConditionalDirectiveProcessor.register_conditionals(
186
+ PromptManager.configuration.directive_processor
187
+ )
188
+ ```
189
+
190
+ ```text
191
+ # Usage in prompts:
192
+ //if [USER_TYPE] == 'premium'
193
+ 🌟 Welcome to Premium features!
194
+ //else
195
+ Upgrade to Premium for additional benefits.
196
+ //endif
197
+
198
+ //if [ORDER_TOTAL] >= 100
199
+ 🚚 Free shipping applied!
200
+ //endif
201
+ ```
202
+
203
+ ### Loop Directives
204
+
205
+ ```ruby
206
+ config.directive_processor.register_directive('foreach') do |args, context|
207
+ array_name, template = args.split(':', 2)
208
+ array_data = context.dig(:parameters, array_name.strip.to_sym) || []
209
+
210
+ array_data.map.with_index do |item, index|
211
+ # Create item context
212
+ item_context = context.deep_dup
213
+ item_context[:parameters].merge!({
214
+ item: item,
215
+ item_index: index,
216
+ is_first: index == 0,
217
+ is_last: index == array_data.length - 1,
218
+ total_count: array_data.length
219
+ })
220
+
221
+ # Process template with item context
222
+ processed_template = template.gsub(/\[ITEM\.(\w+)\]/i) do |match|
223
+ property = Regexp.last_match(1).downcase
224
+ item.is_a?(Hash) ? item[property.to_sym] : item.send(property)
225
+ end
226
+
227
+ PromptManager::DirectiveProcessor.new.process(processed_template, item_context)
228
+ end.join("\n")
229
+ end
230
+ ```
231
+
232
+ ```text
233
+ # Usage in prompts:
234
+ Your order items:
235
+ //foreach order_items: - [ITEM.name]: $[ITEM.price] (Qty: [ITEM.quantity])
236
+
237
+ //foreach products:
238
+ **[ITEM.name]** - $[ITEM.price]
239
+ //if [ITEM.on_sale] == 'true'
240
+ 🏷️ ON SALE!
241
+ //endif
242
+ ```
243
+
244
+ ## Template System Directives
245
+
246
+ ### Layout System
247
+
248
+ ```ruby
249
+ class LayoutDirectiveProcessor
250
+ def self.register_layout_directives(processor)
251
+ processor.register_directive('layout') do |layout_name, context|
252
+ layout_content = processor.storage.read("layouts/#{layout_name}")
253
+ context[:current_layout] = layout_content
254
+ context[:sections] ||= {}
255
+ '' # Layout directive doesn't output content directly
256
+ end
257
+
258
+ processor.register_directive('section') do |args, context|
259
+ section_name, content = args.split(':', 2)
260
+ context[:sections] ||= {}
261
+ context[:sections][section_name.strip] = content.strip
262
+ '' # Section directive stores content for later use
263
+ end
264
+
265
+ processor.register_directive('yield') do |section_name, context|
266
+ section_content = context.dig(:sections, section_name.strip) || ''
267
+ processor.process(section_content, context)
268
+ end
269
+
270
+ processor.register_directive('render_layout') do |args, context|
271
+ return '' unless context[:current_layout]
272
+
273
+ processor.process(context[:current_layout], context)
274
+ end
275
+ end
276
+ end
277
+
278
+ LayoutDirectiveProcessor.register_layout_directives(
279
+ PromptManager.configuration.directive_processor
280
+ )
281
+ ```
282
+
283
+ ```text
284
+ # child_template.txt:
285
+ //layout email_layout
286
+ //section title: Important Account Update
287
+ //section content: Your account settings have been updated successfully.
288
+ //render_layout
289
+
290
+ # layouts/email_layout.txt:
291
+ Subject: //yield title
292
+
293
+ Dear [CUSTOMER_NAME],
294
+
295
+ //yield content
296
+
297
+ Best regards,
298
+ The Support Team
299
+ ```
300
+
301
+ ### Component System
302
+
303
+ ```ruby
304
+ config.directive_processor.register_directive('component') do |args, context|
305
+ component_name, props_str = args.split(':', 2)
306
+
307
+ # Parse component props
308
+ props = {}
309
+ if props_str
310
+ props_str.scan(/(\w+)="([^"]*)"/) do |key, value|
311
+ props[key.to_sym] = value
312
+ end
313
+ end
314
+
315
+ # Load component template
316
+ component_template = processor.storage.read("components/#{component_name}")
317
+
318
+ # Create component context
319
+ component_context = context.deep_dup
320
+ component_context[:parameters].merge!(props)
321
+
322
+ # Render component
323
+ processor.process(component_template, component_context)
324
+ rescue PromptManager::PromptNotFoundError
325
+ "<!-- Component '#{component_name}' not found -->"
326
+ end
327
+ ```
328
+
329
+ ```text
330
+ # Using components:
331
+ //component button: text="Click Here" url="https://example.com" style="primary"
332
+ //component user_card: name="[USER_NAME]" email="[USER_EMAIL]"
333
+
334
+ # components/button.txt:
335
+ [Click here]([URL]) <!-- [TEXT] -->
336
+
337
+ # components/user_card.txt:
338
+ **[NAME]**
339
+ Email: [EMAIL]
340
+ ```
341
+
342
+ ## External Service Integration
343
+
344
+ ### CRM Integration Directive
345
+
346
+ ```ruby
347
+ config.directive_processor.register_directive('crm_data') do |args, context|
348
+ data_type, customer_id = args.split(':', 2)
349
+
350
+ case data_type.strip
351
+ when 'customer_info'
352
+ customer = CRMService.get_customer(customer_id)
353
+ <<~INFO
354
+ Customer: #{customer.name}
355
+ Account Value: #{customer.lifetime_value}
356
+ Support Tier: #{customer.support_tier}
357
+ INFO
358
+
359
+ when 'recent_interactions'
360
+ interactions = CRMService.get_recent_interactions(customer_id, limit: 5)
361
+ interactions.map do |interaction|
362
+ "- #{interaction.date}: #{interaction.type} - #{interaction.summary}"
363
+ end.join("\n")
364
+
365
+ else
366
+ 'Unknown CRM data type'
367
+ end
368
+ rescue => e
369
+ Rails.logger.error "CRM integration error: #{e.message}"
370
+ 'CRM data temporarily unavailable'
371
+ end
372
+ ```
373
+
374
+ ### Analytics Directive
375
+
376
+ ```ruby
377
+ config.directive_processor.register_directive('analytics') do |args, context|
378
+ metric_type, time_period = args.split(':', 2)
379
+ time_period ||= '30d'
380
+
381
+ case metric_type.strip
382
+ when 'user_activity'
383
+ user_id = context.dig(:parameters, :user_id)
384
+ activity = AnalyticsService.get_user_activity(user_id, time_period)
385
+ "Active #{activity[:active_days]} days in the last #{time_period}"
386
+
387
+ when 'feature_usage'
388
+ feature_usage = AnalyticsService.get_feature_usage(time_period)
389
+ top_features = feature_usage.first(3)
390
+ "Top features: #{top_features.map(&:name).join(', ')}"
391
+
392
+ else
393
+ 'Unknown analytics metric'
394
+ end
395
+ end
396
+ ```
397
+
398
+ ## Performance Optimization
399
+
400
+ ### Caching Dynamic Directives
401
+
402
+ ```ruby
403
+ class CachedDirectiveProcessor < PromptManager::DirectiveProcessor
404
+ def initialize(**options)
405
+ super(**options)
406
+ @directive_cache = Rails.cache
407
+ end
408
+
409
+ def register_cached_directive(name, cache_ttl: 300, &handler)
410
+ cached_handler = lambda do |args, context|
411
+ cache_key = generate_cache_key(name, args, context)
412
+
413
+ @directive_cache.fetch(cache_key, expires_in: cache_ttl) do
414
+ handler.call(args, context)
415
+ end
416
+ end
417
+
418
+ register_directive(name, &cached_handler)
419
+ end
420
+
421
+ private
422
+
423
+ def generate_cache_key(directive_name, args, context)
424
+ relevant_params = context[:parameters].slice(:user_id, :tenant_id)
425
+ "directive:#{directive_name}:#{args}:#{relevant_params.hash}"
426
+ end
427
+ end
428
+ ```
429
+
430
+ ### Async Directive Processing
431
+
432
+ ```ruby
433
+ config.directive_processor.register_directive('async_load') do |args, context|
434
+ data_source = args.strip
435
+ cache_key = "async_data:#{data_source}:#{context[:parameters][:user_id]}"
436
+
437
+ # Try to get cached result first
438
+ cached_result = Rails.cache.read(cache_key)
439
+ return cached_result if cached_result
440
+
441
+ # Start async job if not cached
442
+ AsyncDataLoadJob.perform_later(data_source, cache_key, context[:parameters])
443
+
444
+ # Return placeholder
445
+ "Loading #{data_source} data..."
446
+ end
447
+
448
+ class AsyncDataLoadJob < ApplicationJob
449
+ def perform(data_source, cache_key, parameters)
450
+ result = load_data_from_source(data_source, parameters)
451
+ Rails.cache.write(cache_key, result, expires_in: 1.hour)
452
+ end
453
+ end
454
+ ```
455
+
456
+ ## Testing Dynamic Directives
457
+
458
+ ### RSpec Examples
459
+
460
+ ```ruby
461
+ describe 'Dynamic Directives' do
462
+ let(:processor) { PromptManager::DirectiveProcessor.new(storage: storage) }
463
+ let(:storage) { instance_double(PromptManager::Storage::Base) }
464
+
465
+ before do
466
+ processor.register_directive('test_directive') do |args, context|
467
+ "processed: #{args} with #{context.dig(:parameters, :test_param)}"
468
+ end
469
+ end
470
+
471
+ it 'processes directive with context' do
472
+ content = "//test_directive hello world"
473
+ context = { parameters: { test_param: 'value' } }
474
+
475
+ result = processor.process(content, context)
476
+ expect(result).to eq "processed: hello world with value"
477
+ end
478
+
479
+ it 'handles directive errors' do
480
+ processor.register_directive('error_directive') do |args, context|
481
+ raise StandardError, 'test error'
482
+ end
483
+
484
+ expect {
485
+ processor.process("//error_directive test")
486
+ }.to raise_error(PromptManager::DirectiveProcessingError)
487
+ end
488
+ end
489
+ ```
490
+
491
+ ### Integration Tests
492
+
493
+ ```ruby
494
+ describe 'Dynamic Directive Integration' do
495
+ it 'processes complex directive chains' do
496
+ storage = PromptManager::Storage::FileSystemAdapter.new(prompts_dir: 'spec/fixtures')
497
+ processor = PromptManager::DirectiveProcessor.new(storage: storage)
498
+
499
+ # Register test directives
500
+ processor.register_directive('if') { |condition, context| ... }
501
+ processor.register_directive('foreach') { |args, context| ... }
502
+
503
+ prompt_content = <<~PROMPT
504
+ //if [USER_TYPE] == 'premium'
505
+ Premium Features:
506
+ //foreach features: - [ITEM.name]: [ITEM.description]
507
+ //endif
508
+ PROMPT
509
+
510
+ result = processor.process(prompt_content, {
511
+ parameters: {
512
+ user_type: 'premium',
513
+ features: [
514
+ { name: 'Feature 1', description: 'Description 1' },
515
+ { name: 'Feature 2', description: 'Description 2' }
516
+ ]
517
+ }
518
+ })
519
+
520
+ expect(result).to include('Feature 1: Description 1')
521
+ expect(result).to include('Feature 2: Description 2')
522
+ end
523
+ end
524
+ ```
525
+
526
+ ## Best Practices
527
+
528
+ 1. **Error Handling**: Always handle errors gracefully in directive processors
529
+ 2. **Performance**: Cache expensive operations and use async processing when appropriate
530
+ 3. **Security**: Validate and sanitize all directive arguments
531
+ 4. **Context Isolation**: Be careful when modifying context to avoid side effects
532
+ 5. **Documentation**: Document directive syntax, parameters, and behavior
533
+ 6. **Testing**: Write comprehensive tests including error cases
534
+ 7. **Naming**: Use clear, descriptive names that indicate the directive's purpose
535
+ 8. **Resource Management**: Clean up resources and connections properly