ragdoll-cli 0.1.8 → 0.1.10

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.
data/lib/ragdoll/cli.rb CHANGED
@@ -14,6 +14,8 @@ require_relative 'cli/commands/search'
14
14
  require_relative 'cli/commands/config'
15
15
  require_relative 'cli/commands/delete'
16
16
  require_relative 'cli/commands/update'
17
+ require_relative 'cli/commands/analytics'
18
+ require_relative 'cli/commands/keywords'
17
19
 
18
20
  module Ragdoll
19
21
  module CLI
@@ -37,16 +39,32 @@ module Ragdoll
37
39
  desc 'search QUERY', 'Search for documents matching the query'
38
40
  method_option :limit, type: :numeric, default: 10, aliases: '-l',
39
41
  desc: 'Maximum number of results to return'
42
+ method_option :threshold, type: :numeric,
43
+ desc: 'Similarity threshold (0.0-1.0, lower = more results)'
40
44
  method_option :content_type, type: :string, aliases: '-c',
41
45
  desc: 'Filter by content type (text, image, audio)'
42
46
  method_option :classification, type: :string, aliases: '-C',
43
47
  desc: 'Filter by classification'
44
48
  method_option :keywords, type: :string, aliases: '-k',
45
- desc: 'Filter by keywords (comma-separated)'
49
+ desc: 'Filter by keywords (comma-separated). Use ragdoll keywords for keyword-only search'
50
+ method_option :keywords_all, type: :boolean, default: false, aliases: '-K',
51
+ desc: 'Require ALL keywords to match (default: any keyword matches)'
46
52
  method_option :tags, type: :string, aliases: '-T',
47
53
  desc: 'Filter by tags (comma-separated)'
48
54
  method_option :format, type: :string, default: 'table', aliases: '-f',
49
55
  desc: 'Output format (table, json, plain)'
56
+ method_option :session_id, type: :string, aliases: '-s',
57
+ desc: 'Session ID for search tracking'
58
+ method_option :user_id, type: :string, aliases: '-u',
59
+ desc: 'User ID for search tracking'
60
+ method_option :track_search, type: :boolean, default: true, aliases: '-t',
61
+ desc: 'Enable search tracking (default: true)'
62
+ method_option :search_type, type: :string, default: 'semantic', aliases: '-S',
63
+ desc: 'Search type: semantic, hybrid, fulltext (default: semantic)'
64
+ method_option :semantic_weight, type: :numeric, aliases: '-w',
65
+ desc: 'Weight for semantic search in hybrid mode (0.0-1.0, default: 0.7)'
66
+ method_option :text_weight, type: :numeric, aliases: '-W',
67
+ desc: 'Weight for text search in hybrid mode (0.0-1.0, default: 0.3)'
50
68
  def search(query)
51
69
  Search.new.call(query, options)
52
70
  end
@@ -54,6 +72,12 @@ module Ragdoll
54
72
  desc 'config SUBCOMMAND', 'Manage configuration'
55
73
  subcommand 'config', Config
56
74
 
75
+ desc 'analytics SUBCOMMAND', 'Search analytics and reporting'
76
+ subcommand 'analytics', Analytics
77
+
78
+ desc 'keywords SUBCOMMAND', 'Manage and search by document keywords'
79
+ subcommand 'keywords', Keywords
80
+
57
81
  desc 'stats', 'Show document and embedding statistics'
58
82
  def stats
59
83
  client = StandaloneClient.new
@@ -78,11 +102,36 @@ module Ragdoll
78
102
  end
79
103
  end
80
104
 
81
- return unless stats[:content_types]
105
+ if stats[:content_types]
106
+ puts "\nContent Types:"
107
+ stats[:content_types].each do |type, count|
108
+ puts " #{type}: #{count}"
109
+ end
110
+ end
82
111
 
83
- puts "\nContent Types:"
84
- stats[:content_types].each do |type, count|
85
- puts " #{type}: #{count}"
112
+ # Add search analytics if available
113
+ begin
114
+ search_analytics = client.search_analytics(days: 30)
115
+ if search_analytics && !search_analytics.empty?
116
+ puts "\nSearch Analytics (last 30 days):"
117
+ puts " Total searches: #{search_analytics[:total_searches] || 0}"
118
+ puts " Unique queries: #{search_analytics[:unique_queries] || 0}"
119
+ puts " Avg results per search: #{search_analytics[:avg_results_per_search] || 0}"
120
+ puts " Avg execution time: #{search_analytics[:avg_execution_time] || 0}ms"
121
+
122
+ if search_analytics[:search_types]
123
+ puts " Search types:"
124
+ search_analytics[:search_types].each do |type, count|
125
+ puts " #{type}: #{count}"
126
+ end
127
+ end
128
+
129
+ puts " Searches with results: #{search_analytics[:searches_with_results] || 0}"
130
+ puts " Avg click-through rate: #{search_analytics[:avg_click_through_rate] || 0}%"
131
+ end
132
+ rescue StandardError => e
133
+ # Search analytics not available - silently continue
134
+ puts "\nSearch analytics: Not available (#{e.message})"
86
135
  end
87
136
  end
88
137
 
@@ -120,12 +169,22 @@ module Ragdoll
120
169
  puts " Status: #{document[:status]}"
121
170
  puts " Embeddings Count: #{document[:embeddings_count]}"
122
171
  puts " Content Length: #{document[:content_length]} characters"
172
+
173
+ # Show keywords prominently
174
+ keywords = document[:keywords] || document['keywords'] || []
175
+ if keywords.any?
176
+ puts " Keywords: #{keywords.join(', ')}"
177
+ else
178
+ puts " Keywords: (none)"
179
+ end
180
+
123
181
  puts " Created: #{document[:created_at]}"
124
182
  puts " Updated: #{document[:updated_at]}"
125
183
 
126
- if document[:metadata]
184
+ if document[:metadata] && document[:metadata].any?
127
185
  puts "\nMetadata:"
128
186
  document[:metadata].each do |key, value|
187
+ next if key == 'keywords' # Already displayed above
129
188
  puts " #{key}: #{value}"
130
189
  end
131
190
  end
@@ -154,9 +213,25 @@ module Ragdoll
154
213
  desc: 'Maximum number of documents to list'
155
214
  method_option :format, type: :string, default: 'table', aliases: '-f',
156
215
  desc: 'Output format (table, json, plain)'
216
+ method_option :keywords, type: :string, aliases: '-k',
217
+ desc: 'Filter by keywords (comma-separated)'
218
+ method_option :keywords_all, type: :boolean, default: false, aliases: '-K',
219
+ desc: 'Require ALL keywords to match (default: any keyword matches)'
157
220
  def list
158
221
  client = StandaloneClient.new
159
- documents = client.list_documents(limit: options[:limit])
222
+
223
+ # Handle keyword filtering if provided
224
+ if options[:keywords]
225
+ keywords_array = options[:keywords].split(',').map(&:strip)
226
+ search_method = options[:keywords_all] ? :search_by_keywords_all : :search_by_keywords
227
+ documents = client.public_send(search_method, keywords_array, limit: options[:limit])
228
+
229
+ puts "Listing documents with keywords: #{keywords_array.join(', ')}"
230
+ puts "Mode: #{options[:keywords_all] ? 'ALL keywords (AND)' : 'ANY keywords (OR)'}"
231
+ puts
232
+ else
233
+ documents = client.list_documents(limit: options[:limit])
234
+ end
160
235
 
161
236
  # Get accurate embeddings count for all documents
162
237
  documents.each do |doc|
@@ -176,16 +251,30 @@ module Ragdoll
176
251
  puts "#{doc[:id]}: #{doc[:title] || 'Untitled'}"
177
252
  end
178
253
  else
179
- # Table format
180
- puts 'ID'.ljust(10) + 'Title'.ljust(40) + 'Status'.ljust(12) + 'Embeddings'
181
- puts '-' * 80
182
- documents.each do |doc|
183
- id = (doc[:id] || doc['id'] || '')[0..9].ljust(10)
184
- title = (doc[:title] || doc['title'] || 'Untitled')[0..39].ljust(40)
185
- status = (doc[:status] || doc['status'] || 'unknown')[0..11].ljust(12)
186
- embeddings = (doc[:embeddings_count] || doc['embeddings_count'] || 0).to_s
187
-
188
- puts "#{id}#{title}#{status}#{embeddings}"
254
+ # Table format - show keywords if keyword filtering is being used
255
+ if options[:keywords]
256
+ puts 'ID'.ljust(10) + 'Title'.ljust(30) + 'Keywords'.ljust(35) + 'Status'.ljust(10) + 'Emb'
257
+ puts '-' * 90
258
+ documents.each do |doc|
259
+ id = (doc[:id] || doc['id'] || '')[0..9].ljust(10)
260
+ title = (doc[:title] || doc['title'] || 'Untitled')[0..29].ljust(30)
261
+ keywords = (doc[:keywords] || doc['keywords'] || []).join(', ')[0..34].ljust(35)
262
+ status = (doc[:status] || doc['status'] || 'unknown')[0..9].ljust(10)
263
+ embeddings = (doc[:embeddings_count] || doc['embeddings_count'] || 0).to_s
264
+
265
+ puts "#{id}#{title}#{keywords}#{status}#{embeddings}"
266
+ end
267
+ else
268
+ puts 'ID'.ljust(10) + 'Title'.ljust(40) + 'Status'.ljust(12) + 'Embeddings'
269
+ puts '-' * 80
270
+ documents.each do |doc|
271
+ id = (doc[:id] || doc['id'] || '')[0..9].ljust(10)
272
+ title = (doc[:title] || doc['title'] || 'Untitled')[0..39].ljust(40)
273
+ status = (doc[:status] || doc['status'] || 'unknown')[0..11].ljust(12)
274
+ embeddings = (doc[:embeddings_count] || doc['embeddings_count'] || 0).to_s
275
+
276
+ puts "#{id}#{title}#{status}#{embeddings}"
277
+ end
189
278
  end
190
279
  end
191
280
  end
@@ -354,27 +443,150 @@ module Ragdoll
354
443
  end
355
444
 
356
445
  desc 'context QUERY', 'Get context for RAG applications'
357
- method_option :limit, type: :numeric, default: 5, aliases: '-l', desc: 'Maximum number of context chunks'
446
+ method_option :limit, type: :numeric, default: 10, aliases: '-l', desc: 'Maximum number of context chunks'
447
+ method_option :threshold, type: :numeric, desc: 'Similarity threshold (0.0-1.0, lower = more results)'
358
448
  def context(query)
359
449
  client = StandaloneClient.new
360
- ctx = client.get_context(query, limit: options[:limit])
361
- puts JSON.pretty_generate(ctx)
450
+ context_options = { limit: options[:limit] }
451
+ context_options[:threshold] = options[:threshold] if options[:threshold]
452
+ ctx = client.get_context(query, **context_options)
453
+
454
+ # Check if no context was found and provide enhanced feedback
455
+ if ctx[:context_chunks].empty?
456
+ # Get the underlying search response for statistics
457
+ search_response = client.search(query, **context_options)
458
+ display_no_results_feedback(query, search_response, 'context')
459
+ else
460
+ puts JSON.pretty_generate(ctx)
461
+ end
362
462
  end
363
463
 
364
464
  desc 'enhance PROMPT', 'Enhance a prompt with context'
365
- method_option :context_limit, type: :numeric, default: 5, aliases: '-l', desc: 'Number of context chunks to include'
465
+ method_option :limit, type: :numeric, default: 10, aliases: '-l', desc: 'Maximum number of context chunks to include'
466
+ method_option :threshold, type: :numeric, desc: 'Similarity threshold (0.0-1.0, lower = more results)'
366
467
  def enhance(prompt)
367
468
  client = StandaloneClient.new
368
- enhanced = client.enhance_prompt(prompt, context_limit: options[:context_limit])
369
- puts enhanced
469
+ enhance_options = { context_limit: options[:limit] }
470
+ enhance_options[:threshold] = options[:threshold] if options[:threshold]
471
+ enhanced = client.enhance_prompt(prompt, **enhance_options)
472
+
473
+ # Check if no context was found and provide enhanced feedback
474
+ if enhanced[:context_count] == 0
475
+ # Get the underlying search response for statistics
476
+ search_response = client.search(prompt, limit: enhance_options[:context_limit], threshold: enhance_options[:threshold])
477
+ display_no_results_feedback(prompt, search_response, 'enhance')
478
+ else
479
+ puts enhanced[:enhanced_prompt]
480
+ end
370
481
  end
371
482
 
483
+ desc 'search-history', 'Show recent search history'
484
+ method_option :limit, type: :numeric, default: 20, aliases: '-l',
485
+ desc: 'Number of searches to show (default: 20)'
486
+ method_option :user_id, type: :string, aliases: '-u',
487
+ desc: 'Filter by user ID'
488
+ method_option :session_id, type: :string, aliases: '-s',
489
+ desc: 'Filter by session ID'
490
+ method_option :format, type: :string, default: 'table', aliases: '-f',
491
+ desc: 'Output format (table, json, plain)'
492
+ def search_history
493
+ analytics = Analytics.new
494
+ analytics.options = options
495
+ analytics.history
496
+ end
497
+
498
+ desc 'search-stats', 'Show detailed search analytics'
499
+ method_option :days, type: :numeric, default: 30, aliases: '-d',
500
+ desc: 'Number of days to analyze (default: 30)'
501
+ method_option :format, type: :string, default: 'table', aliases: '-f',
502
+ desc: 'Output format (table, json)'
503
+ def search_stats
504
+ analytics = Analytics.new
505
+ analytics.options = options
506
+ analytics.overview
507
+ end
508
+
509
+ desc 'trending', 'Show trending search queries'
510
+ method_option :limit, type: :numeric, default: 10, aliases: '-l',
511
+ desc: 'Number of queries to show (default: 10)'
512
+ method_option :days, type: :numeric, default: 7, aliases: '-d',
513
+ desc: 'Time period in days (default: 7)'
514
+ method_option :format, type: :string, default: 'table', aliases: '-f',
515
+ desc: 'Output format (table, json)'
516
+ def trending
517
+ analytics = Analytics.new
518
+ analytics.options = options
519
+ analytics.trending
520
+ end
521
+
522
+ desc 'cleanup-searches', 'Cleanup old search records'
523
+ method_option :days, type: :numeric, default: 30, aliases: '-d',
524
+ desc: 'Remove searches older than N days (default: 30)'
525
+ method_option :dry_run, type: :boolean, default: true, aliases: '-n',
526
+ desc: 'Show what would be deleted without actually deleting (default: true)'
527
+ method_option :force, type: :boolean, default: false, aliases: '-f',
528
+ desc: 'Actually perform the cleanup (overrides dry_run)'
529
+ def cleanup_searches
530
+ analytics = Analytics.new
531
+ analytics.options = options
532
+ analytics.cleanup
533
+ end
372
534
 
373
535
  private
374
536
 
375
537
  def load_configuration
376
538
  ConfigurationLoader.new.load
377
539
  end
540
+
541
+ def display_no_results_feedback(query, search_response, command_type)
542
+ puts "No results found for '#{query}'"
543
+ puts
544
+
545
+ # Get statistics for better feedback
546
+ statistics = search_response[:statistics] || search_response['statistics']
547
+ execution_time = search_response[:execution_time_ms] || search_response['execution_time_ms']
548
+
549
+ if statistics
550
+ threshold = statistics[:threshold_used] || statistics['threshold_used']
551
+ highest = statistics[:highest_similarity] || statistics['highest_similarity']
552
+ lowest = statistics[:lowest_similarity] || statistics['lowest_similarity']
553
+ average = statistics[:average_similarity] || statistics['average_similarity']
554
+ above_threshold = statistics[:similarities_above_threshold] || statistics['similarities_above_threshold']
555
+ total_checked = statistics[:total_embeddings_checked] || statistics['total_embeddings_checked']
556
+
557
+ puts "Search Analysis:"
558
+ puts " • Similarity threshold: #{threshold&.round(3) || 'N/A'}"
559
+ puts " • Embeddings analyzed: #{total_checked || 0}"
560
+ if highest && lowest && average
561
+ puts " • Similarity range: #{lowest.round(3)} - #{highest.round(3)} (avg: #{average.round(3)})"
562
+ end
563
+ puts " • Results above threshold: #{above_threshold || 0}"
564
+ puts " • Search time: #{execution_time || 0}ms"
565
+ puts
566
+
567
+ # Provide actionable suggestions
568
+ if highest && threshold
569
+ if highest < threshold
570
+ suggested_threshold = (highest * 0.9).round(3)
571
+ puts "💡 Suggestions:"
572
+ puts " • Lower the similarity threshold (highest found: #{highest.round(3)})"
573
+ puts " • Try: ragdoll #{command_type} '#{query}' --threshold=#{suggested_threshold}"
574
+ if highest < 0.3
575
+ puts " • Your query might not match the document content well"
576
+ puts " • Try different or more specific search terms"
577
+ puts " • Try keyword-based search: ragdoll keywords search KEYWORD"
578
+ puts " • List available keywords: ragdoll keywords list"
579
+ end
580
+ elsif above_threshold > 0
581
+ puts "💡 Note: Found #{above_threshold} similar content above threshold #{threshold}"
582
+ puts " This suggests an issue with result processing."
583
+ end
584
+ end
585
+ else
586
+ puts "No similarity statistics available."
587
+ puts "💡 Try lowering the similarity threshold with --threshold=0.5"
588
+ end
589
+ end
378
590
  end
379
591
  end
380
592
  end
metadata CHANGED
@@ -1,15 +1,184 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ragdoll-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- description: Under development. Contributors welcome.
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ragdoll
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.10
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.10
26
+ - !ruby/object:Gem::Dependency
27
+ name: ruby-progressbar
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: thor
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: debug_me
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: minitest
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubocop
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rubocop-minitest
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rubocop-rake
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: simplecov
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: undercover
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ description: Command-line interface for Ragdoll RAG system with semantic, full-text,
181
+ and hybrid search capabilities. Under development. Contributors welcome.
13
182
  email:
14
183
  - dvanhoozer@gmail.com
15
184
  executables:
@@ -21,9 +190,11 @@ files:
21
190
  - Rakefile
22
191
  - bin/ragdoll
23
192
  - lib/ragdoll/cli.rb
193
+ - lib/ragdoll/cli/commands/analytics.rb
24
194
  - lib/ragdoll/cli/commands/config.rb
25
195
  - lib/ragdoll/cli/commands/delete.rb
26
196
  - lib/ragdoll/cli/commands/health.rb
197
+ - lib/ragdoll/cli/commands/keywords.rb
27
198
  - lib/ragdoll/cli/commands/list.rb
28
199
  - lib/ragdoll/cli/commands/search.rb
29
200
  - lib/ragdoll/cli/commands/stats.rb