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,635 @@
1
+ # Search Integration
2
+
3
+ PromptManager provides powerful search capabilities to find, filter, and organize prompts across your entire prompt library.
4
+
5
+ ## Basic Search
6
+
7
+ ### Simple Text Search
8
+
9
+ ```ruby
10
+ # Search by prompt content
11
+ results = PromptManager.search("customer service")
12
+ results.each do |prompt_id|
13
+ puts "Found: #{prompt_id}"
14
+ end
15
+
16
+ # Search with options
17
+ results = PromptManager.search(
18
+ query: "email template",
19
+ limit: 10,
20
+ include_content: true
21
+ )
22
+
23
+ results.each do |result|
24
+ puts "ID: #{result[:id]}"
25
+ puts "Content: #{result[:content][0..100]}..."
26
+ end
27
+ ```
28
+
29
+ ### Search by Metadata
30
+
31
+ ```ruby
32
+ # Search by prompt ID pattern
33
+ email_prompts = PromptManager.search(id_pattern: /email/)
34
+
35
+ # Search by file path (FileSystem adapter)
36
+ marketing_prompts = PromptManager.search(path_pattern: /marketing/)
37
+
38
+ # Search by tags (if using metadata)
39
+ customer_service = PromptManager.search(tags: ['customer-service', 'support'])
40
+ ```
41
+
42
+ ## Advanced Search Features
43
+
44
+ ### Full-Text Search with Elasticsearch
45
+
46
+ ```ruby
47
+ # config/initializers/prompt_manager.rb
48
+ PromptManager.configure do |config|
49
+ config.search_backend = PromptManager::Search::ElasticsearchBackend.new(
50
+ host: ENV['ELASTICSEARCH_URL'],
51
+ index: 'prompt_manager_prompts'
52
+ )
53
+ end
54
+
55
+ class PromptManager::Search::ElasticsearchBackend
56
+ def initialize(host:, index:)
57
+ @client = Elasticsearch::Client.new(hosts: host)
58
+ @index = index
59
+ setup_index
60
+ end
61
+
62
+ def search(query, options = {})
63
+ search_body = build_search_query(query, options)
64
+
65
+ response = @client.search(
66
+ index: @index,
67
+ body: search_body
68
+ )
69
+
70
+ parse_search_results(response)
71
+ end
72
+
73
+ def index_prompt(prompt_id, content, metadata = {})
74
+ document = {
75
+ id: prompt_id,
76
+ content: content,
77
+ metadata: metadata,
78
+ indexed_at: Time.current.iso8601,
79
+ parameters: extract_parameters(content),
80
+ directives: extract_directives(content)
81
+ }
82
+
83
+ @client.index(
84
+ index: @index,
85
+ id: prompt_id,
86
+ body: document
87
+ )
88
+ end
89
+
90
+ private
91
+
92
+ def build_search_query(query, options)
93
+ {
94
+ query: {
95
+ bool: {
96
+ should: [
97
+ {
98
+ match: {
99
+ content: {
100
+ query: query,
101
+ boost: 2.0
102
+ }
103
+ }
104
+ },
105
+ {
106
+ match: {
107
+ id: {
108
+ query: query,
109
+ boost: 1.5
110
+ }
111
+ }
112
+ },
113
+ {
114
+ nested: {
115
+ path: 'metadata',
116
+ query: {
117
+ match: {
118
+ 'metadata.description': query
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ],
124
+ filter: build_filters(options)
125
+ }
126
+ },
127
+ highlight: {
128
+ fields: {
129
+ content: {},
130
+ id: {}
131
+ }
132
+ },
133
+ size: options[:limit] || 20,
134
+ from: options[:offset] || 0
135
+ }
136
+ end
137
+ end
138
+ ```
139
+
140
+ ### Faceted Search
141
+
142
+ ```ruby
143
+ search_results = PromptManager.search(
144
+ query: "email",
145
+ facets: {
146
+ tags: {},
147
+ category: {},
148
+ last_modified: {
149
+ ranges: [
150
+ { to: "now-1d", label: "Last 24 hours" },
151
+ { from: "now-7d", to: "now-1d", label: "Last week" },
152
+ { from: "now-30d", to: "now-7d", label: "Last month" }
153
+ ]
154
+ }
155
+ }
156
+ )
157
+
158
+ puts "Results: #{search_results[:total]}"
159
+ puts "Facets:"
160
+ search_results[:facets].each do |facet_name, facet_data|
161
+ puts " #{facet_name}:"
162
+ facet_data[:buckets].each do |bucket|
163
+ puts " #{bucket[:label]}: #{bucket[:count]}"
164
+ end
165
+ end
166
+ ```
167
+
168
+ ### Semantic Search with Vector Embeddings
169
+
170
+ ```ruby
171
+ class PromptManager::Search::VectorBackend
172
+ def initialize(embedding_model: 'text-embedding-ada-002')
173
+ @openai = OpenAI::Client.new
174
+ @embedding_model = embedding_model
175
+ @vector_db = Pinecone::Client.new
176
+ end
177
+
178
+ def index_prompt(prompt_id, content, metadata = {})
179
+ # Generate embedding
180
+ embedding_response = @openai.embeddings(
181
+ parameters: {
182
+ model: @embedding_model,
183
+ input: content
184
+ }
185
+ )
186
+
187
+ embedding = embedding_response['data'][0]['embedding']
188
+
189
+ # Store in vector database
190
+ @vector_db.upsert(
191
+ namespace: 'prompts',
192
+ vectors: [{
193
+ id: prompt_id,
194
+ values: embedding,
195
+ metadata: {
196
+ content: content,
197
+ **metadata
198
+ }
199
+ }]
200
+ )
201
+ end
202
+
203
+ def semantic_search(query, limit: 10, similarity_threshold: 0.8)
204
+ # Generate query embedding
205
+ query_embedding = @openai.embeddings(
206
+ parameters: {
207
+ model: @embedding_model,
208
+ input: query
209
+ }
210
+ )['data'][0]['embedding']
211
+
212
+ # Search for similar vectors
213
+ results = @vector_db.query(
214
+ namespace: 'prompts',
215
+ vector: query_embedding,
216
+ top_k: limit,
217
+ include_metadata: true
218
+ )
219
+
220
+ # Filter by similarity threshold
221
+ results['matches'].select do |match|
222
+ match['score'] >= similarity_threshold
223
+ end
224
+ end
225
+ end
226
+
227
+ # Usage
228
+ PromptManager.configure do |config|
229
+ config.search_backend = PromptManager::Search::VectorBackend.new
230
+ end
231
+
232
+ # Find semantically similar prompts
233
+ similar_prompts = PromptManager.semantic_search(
234
+ "greeting message for new customers",
235
+ limit: 5
236
+ )
237
+
238
+ similar_prompts.each do |result|
239
+ puts "#{result['id']} (similarity: #{result['score']})"
240
+ puts result['metadata']['content'][0..100]
241
+ puts "---"
242
+ end
243
+ ```
244
+
245
+ ## Search Integration Patterns
246
+
247
+ ### Auto-completion and Suggestions
248
+
249
+ ```ruby
250
+ class PromptSearchController < ApplicationController
251
+ def autocomplete
252
+ query = params[:q]
253
+ suggestions = PromptManager.search(
254
+ query: query,
255
+ type: :autocomplete,
256
+ limit: 10
257
+ )
258
+
259
+ render json: {
260
+ suggestions: suggestions.map do |result|
261
+ {
262
+ id: result[:id],
263
+ title: result[:title] || result[:id].humanize,
264
+ description: result[:content][0..100],
265
+ category: result[:metadata][:category]
266
+ }
267
+ end
268
+ }
269
+ end
270
+
271
+ def search
272
+ results = PromptManager.search(
273
+ query: params[:q],
274
+ filters: search_filters,
275
+ facets: search_facets,
276
+ page: params[:page] || 1,
277
+ per_page: 20
278
+ )
279
+
280
+ render json: {
281
+ results: results[:items],
282
+ total: results[:total],
283
+ facets: results[:facets],
284
+ pagination: {
285
+ page: params[:page]&.to_i || 1,
286
+ total_pages: (results[:total] / 20.0).ceil
287
+ }
288
+ }
289
+ end
290
+
291
+ private
292
+
293
+ def search_filters
294
+ filters = {}
295
+ filters[:category] = params[:category] if params[:category].present?
296
+ filters[:tags] = params[:tags].split(',') if params[:tags].present?
297
+ filters[:date_range] = params[:date_range] if params[:date_range].present?
298
+ filters
299
+ end
300
+ end
301
+ ```
302
+
303
+ ### Search Analytics
304
+
305
+ ```ruby
306
+ class SearchAnalytics
307
+ def self.track_search(query, user_id, results_count)
308
+ SearchLog.create!(
309
+ query: query,
310
+ user_id: user_id,
311
+ results_count: results_count,
312
+ searched_at: Time.current
313
+ )
314
+ end
315
+
316
+ def self.popular_searches(limit: 10)
317
+ SearchLog
318
+ .where('searched_at > ?', 30.days.ago)
319
+ .group(:query)
320
+ .order('count_all DESC')
321
+ .limit(limit)
322
+ .count
323
+ end
324
+
325
+ def self.search_trends
326
+ SearchLog
327
+ .where('searched_at > ?', 7.days.ago)
328
+ .group('DATE(searched_at)')
329
+ .count
330
+ end
331
+
332
+ def self.no_results_queries
333
+ SearchLog
334
+ .where(results_count: 0)
335
+ .where('searched_at > ?', 7.days.ago)
336
+ .group(:query)
337
+ .order('count_all DESC')
338
+ .limit(20)
339
+ .count
340
+ end
341
+ end
342
+
343
+ # Usage in search
344
+ results = PromptManager.search(params[:q])
345
+ SearchAnalytics.track_search(params[:q], current_user.id, results.count)
346
+ ```
347
+
348
+ ### Search Result Ranking
349
+
350
+ ```ruby
351
+ class PromptRankingService
352
+ RANKING_FACTORS = {
353
+ exact_match: 3.0,
354
+ title_match: 2.0,
355
+ content_relevance: 1.0,
356
+ recency: 0.5,
357
+ usage_frequency: 1.5,
358
+ user_preference: 2.0
359
+ }.freeze
360
+
361
+ def self.rank_results(results, query, user_context = {})
362
+ scored_results = results.map do |result|
363
+ score = calculate_score(result, query, user_context)
364
+ result.merge(relevance_score: score)
365
+ end
366
+
367
+ scored_results.sort_by { |r| -r[:relevance_score] }
368
+ end
369
+
370
+ private
371
+
372
+ def self.calculate_score(result, query, user_context)
373
+ score = 0.0
374
+
375
+ # Exact match bonus
376
+ if result[:id].downcase.include?(query.downcase)
377
+ score += RANKING_FACTORS[:exact_match]
378
+ end
379
+
380
+ # Content relevance
381
+ content_matches = result[:content].downcase.scan(query.downcase).size
382
+ score += content_matches * RANKING_FACTORS[:content_relevance]
383
+
384
+ # Recency bonus
385
+ days_old = (Time.current - result[:updated_at]).to_f / 1.day
386
+ recency_factor = [1.0 - (days_old / 365.0), 0.0].max
387
+ score += recency_factor * RANKING_FACTORS[:recency]
388
+
389
+ # Usage frequency
390
+ usage_count = PromptUsageLog.where(prompt_id: result[:id])
391
+ .where('used_at > ?', 30.days.ago)
392
+ .count
393
+ score += Math.log(usage_count + 1) * RANKING_FACTORS[:usage_frequency]
394
+
395
+ # User preference (based on past usage)
396
+ if user_context[:user_id]
397
+ user_usage = PromptUsageLog.where(
398
+ prompt_id: result[:id],
399
+ user_id: user_context[:user_id]
400
+ ).count
401
+ score += Math.log(user_usage + 1) * RANKING_FACTORS[:user_preference]
402
+ end
403
+
404
+ score
405
+ end
406
+ end
407
+ ```
408
+
409
+ ## Search UI Components
410
+
411
+ ### React Search Component
412
+
413
+ ```jsx
414
+ // components/PromptSearch.jsx
415
+ import React, { useState, useEffect, useMemo } from 'react';
416
+ import { debounce } from 'lodash';
417
+
418
+ const PromptSearch = () => {
419
+ const [query, setQuery] = useState('');
420
+ const [results, setResults] = useState([]);
421
+ const [facets, setFacets] = useState({});
422
+ const [selectedFilters, setSelectedFilters] = useState({});
423
+ const [loading, setLoading] = useState(false);
424
+
425
+ const debouncedSearch = useMemo(
426
+ () => debounce(async (searchQuery, filters) => {
427
+ setLoading(true);
428
+ try {
429
+ const response = await fetch('/api/prompts/search', {
430
+ method: 'POST',
431
+ headers: {
432
+ 'Content-Type': 'application/json',
433
+ },
434
+ body: JSON.stringify({
435
+ query: searchQuery,
436
+ filters: filters
437
+ }),
438
+ });
439
+
440
+ const data = await response.json();
441
+ setResults(data.results);
442
+ setFacets(data.facets);
443
+ } catch (error) {
444
+ console.error('Search error:', error);
445
+ } finally {
446
+ setLoading(false);
447
+ }
448
+ }, 300),
449
+ []
450
+ );
451
+
452
+ useEffect(() => {
453
+ if (query.length > 2) {
454
+ debouncedSearch(query, selectedFilters);
455
+ } else {
456
+ setResults([]);
457
+ }
458
+ }, [query, selectedFilters, debouncedSearch]);
459
+
460
+ return (
461
+ <div className="prompt-search">
462
+ <div className="search-input">
463
+ <input
464
+ type="text"
465
+ value={query}
466
+ onChange={(e) => setQuery(e.target.value)}
467
+ placeholder="Search prompts..."
468
+ className="search-field"
469
+ />
470
+ {loading && <div className="search-spinner">Loading...</div>}
471
+ </div>
472
+
473
+ <div className="search-content">
474
+ <div className="search-filters">
475
+ {Object.entries(facets).map(([facetName, facetData]) => (
476
+ <div key={facetName} className="facet-group">
477
+ <h4>{facetName.charAt(0).toUpperCase() + facetName.slice(1)}</h4>
478
+ {facetData.buckets.map(bucket => (
479
+ <label key={bucket.key} className="facet-option">
480
+ <input
481
+ type="checkbox"
482
+ checked={selectedFilters[facetName]?.includes(bucket.key) || false}
483
+ onChange={(e) => handleFilterChange(facetName, bucket.key, e.target.checked)}
484
+ />
485
+ {bucket.label} ({bucket.count})
486
+ </label>
487
+ ))}
488
+ </div>
489
+ ))}
490
+ </div>
491
+
492
+ <div className="search-results">
493
+ {results.map(result => (
494
+ <div key={result.id} className="search-result">
495
+ <h3 className="result-title">{result.title || result.id}</h3>
496
+ <p className="result-content">{result.snippet}</p>
497
+ <div className="result-metadata">
498
+ <span className="result-category">{result.category}</span>
499
+ <span className="result-score">Score: {result.relevance_score?.toFixed(2)}</span>
500
+ </div>
501
+ </div>
502
+ ))}
503
+ </div>
504
+ </div>
505
+ </div>
506
+ );
507
+ };
508
+
509
+ export default PromptSearch;
510
+ ```
511
+
512
+ ### Search API Implementation
513
+
514
+ ```ruby
515
+ class Api::PromptsController < ApplicationController
516
+ def search
517
+ search_params = params.require(:search).permit(
518
+ :query, :page, :per_page,
519
+ filters: {},
520
+ facets: []
521
+ )
522
+
523
+ results = PromptManager.search(
524
+ query: search_params[:query],
525
+ filters: search_params[:filters] || {},
526
+ facets: search_params[:facets] || [],
527
+ page: search_params[:page]&.to_i || 1,
528
+ per_page: [search_params[:per_page]&.to_i || 20, 100].min
529
+ )
530
+
531
+ # Apply custom ranking
532
+ ranked_results = PromptRankingService.rank_results(
533
+ results[:items],
534
+ search_params[:query],
535
+ user_context: { user_id: current_user&.id }
536
+ )
537
+
538
+ render json: {
539
+ results: ranked_results,
540
+ total: results[:total],
541
+ facets: results[:facets],
542
+ query: search_params[:query]
543
+ }
544
+ end
545
+
546
+ def suggestions
547
+ query = params[:q]
548
+
549
+ suggestions = PromptManager.search(
550
+ query: query,
551
+ type: :suggestions,
552
+ limit: 8
553
+ )
554
+
555
+ render json: {
556
+ suggestions: suggestions.map do |s|
557
+ {
558
+ text: s[:id].humanize,
559
+ value: s[:id],
560
+ category: s[:metadata][:category]
561
+ }
562
+ end
563
+ }
564
+ end
565
+ end
566
+ ```
567
+
568
+ ## Performance Optimization
569
+
570
+ ### Search Indexing Strategy
571
+
572
+ ```ruby
573
+ class PromptIndexer
574
+ def self.reindex_all
575
+ total_prompts = PromptManager.list.count
576
+
577
+ PromptManager.list.each_with_index do |prompt_id, index|
578
+ begin
579
+ prompt = PromptManager::Prompt.new(id: prompt_id)
580
+ content = prompt.content
581
+ metadata = extract_metadata(prompt)
582
+
583
+ PromptManager.search_backend.index_prompt(
584
+ prompt_id,
585
+ content,
586
+ metadata
587
+ )
588
+
589
+ puts "Indexed #{index + 1}/#{total_prompts}: #{prompt_id}"
590
+ rescue => e
591
+ Rails.logger.error "Failed to index #{prompt_id}: #{e.message}"
592
+ end
593
+ end
594
+ end
595
+
596
+ def self.incremental_index
597
+ # Index only recently modified prompts
598
+ recently_modified = PromptManager.list.select do |prompt_id|
599
+ prompt = PromptManager::Prompt.new(id: prompt_id)
600
+ last_modified = File.mtime(prompt.file_path) rescue Time.at(0)
601
+ last_indexed = IndexLog.where(prompt_id: prompt_id).maximum(:indexed_at) || Time.at(0)
602
+
603
+ last_modified > last_indexed
604
+ end
605
+
606
+ recently_modified.each do |prompt_id|
607
+ index_single_prompt(prompt_id)
608
+ end
609
+ end
610
+ end
611
+
612
+ # Schedule regular reindexing
613
+ class PromptReindexJob < ApplicationJob
614
+ def perform
615
+ PromptIndexer.incremental_index
616
+ end
617
+ end
618
+
619
+ # Run every hour
620
+ # schedule.rb or similar
621
+ every 1.hour do
622
+ PromptReindexJob.perform_later
623
+ end
624
+ ```
625
+
626
+ ## Best Practices
627
+
628
+ 1. **Index Management**: Keep search indexes up to date with prompt changes
629
+ 2. **Query Optimization**: Use proper filters and pagination to improve performance
630
+ 3. **Result Ranking**: Implement relevance scoring based on user behavior
631
+ 4. **Analytics**: Track search patterns to improve the search experience
632
+ 5. **Faceted Navigation**: Provide filters to help users narrow down results
633
+ 6. **Error Handling**: Gracefully handle search backend failures
634
+ 7. **Caching**: Cache frequent searches and autocomplete suggestions
635
+ 8. **Security**: Ensure search queries are properly sanitized and authorized