ragdoll-cli 0.0.2 → 0.1.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f19f7c89cad761c7da655010fdc868b7124ab164d0481cf2d97dea485df58317
4
- data.tar.gz: c2c7be844cc5addcd4386c4ea0596c75abe9cbbb29167b218ee95826a9c98ace
3
+ metadata.gz: 843beb22244ce386dadbe959585717710940767c066307b2a92d9cfeafa6c2c9
4
+ data.tar.gz: 3a47b7c95155ab97644063746998f3e2f9888f0ad7e757af1a35240baed67537
5
5
  SHA512:
6
- metadata.gz: 27c20b905c6fa11a5941a8c1343b01b37affd6d84fd4e3884733988031c2e91f7336c351fc7da90474c68bc4b951cae37b3b3d2d6f85e25f010d19d3110ef43a
7
- data.tar.gz: 121b9df6c28f2ed96ddfaf2f00468b7f53aced41b04ed77dca015ec8690e801e3fd10dfc8567737b3448835bb7010234b11630e53a68a494064b8f089a3654ba
6
+ metadata.gz: d15dd454cec4f80ce738ffa100c6a0c00e8642e7881443aca0247436dcb9e321761467b7554439324b3ba5c9b1b232f76d87ce18a4a90547b88e899ea2e196b4
7
+ data.tar.gz: 14d92726298d5d0419075bc8d9f87bc0be0d0a34d2128c20da4fb768d68770cf0ded31735c588347cce462c94c705dc8252734317db7c09fafe25d73b19f29c6
data/Rakefile CHANGED
@@ -16,6 +16,12 @@ Rake::TestTask.new(:test) do |t|
16
16
  end
17
17
 
18
18
  # Load annotate tasks
19
- Dir.glob("lib/tasks/*.rake").each { |r| load r }
19
+ Dir.glob("lib/tasks/*.rake").each do |r|
20
+ begin
21
+ load r
22
+ rescue LoadError => e
23
+ puts "Skipping #{r}: #{e.message}" if ENV['DEBUG']
24
+ end
25
+ end
20
26
 
21
27
  task default: :test
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'thor'
5
+
6
+ module Ragdoll
7
+ module CLI
8
+ class Analytics < Thor
9
+ desc 'overview', 'Show search analytics overview'
10
+ method_option :days, type: :numeric, default: 30, aliases: '-d',
11
+ desc: 'Number of days to analyze (default: 30)'
12
+ method_option :format, type: :string, default: 'table', aliases: '-f',
13
+ desc: 'Output format (table, json)'
14
+ def overview
15
+ client = StandaloneClient.new
16
+ days = (options && options[:days]) || 30
17
+ analytics = client.search_analytics(days: days)
18
+
19
+ case (options && options[:format]) || 'table'
20
+ when 'json'
21
+ puts JSON.pretty_generate(analytics)
22
+ else
23
+ puts "Search Analytics (last #{days} days):"
24
+ puts
25
+ puts 'Metric'.ljust(30) + 'Value'
26
+ puts '-' * 50
27
+
28
+ analytics.each do |key, value|
29
+ metric = format_metric_name(key).ljust(30)
30
+ formatted_value = format_metric_value(key, value)
31
+ puts "#{metric}#{formatted_value}"
32
+ end
33
+ end
34
+ rescue StandardError => e
35
+ puts "Error retrieving analytics: #{e.message}"
36
+ end
37
+
38
+ desc 'history', 'Show recent search history'
39
+ method_option :limit, type: :numeric, default: 20, aliases: '-l',
40
+ desc: 'Number of searches to show (default: 20)'
41
+ method_option :user_id, type: :string, aliases: '-u',
42
+ desc: 'Filter by user ID'
43
+ method_option :session_id, type: :string, aliases: '-s',
44
+ desc: 'Filter by session ID'
45
+ method_option :format, type: :string, default: 'table', aliases: '-f',
46
+ desc: 'Output format (table, json, plain)'
47
+ def history
48
+ client = StandaloneClient.new
49
+ limit = (options && options[:limit]) || 20
50
+ filter_options = {}
51
+ filter_options[:user_id] = options[:user_id] if options && options[:user_id]
52
+ filter_options[:session_id] = options[:session_id] if options && options[:session_id]
53
+
54
+ searches = client.search_history(limit: limit, **filter_options)
55
+
56
+ case (options && options[:format]) || 'table'
57
+ when 'json'
58
+ puts JSON.pretty_generate(searches || [])
59
+ when 'plain'
60
+ if searches.nil? || searches.empty?
61
+ puts 'No search history found.'
62
+ return
63
+ end
64
+ searches.each_with_index do |search, index|
65
+ puts "#{index + 1}. #{search[:query]} (#{search[:search_type]})"
66
+ puts " Time: #{search[:created_at]}"
67
+ puts " Results: #{search[:results_count]}"
68
+ puts " Execution: #{search[:execution_time_ms]}ms"
69
+ puts " Session: #{search[:session_id]}" if search[:session_id]
70
+ puts " User: #{search[:user_id]}" if search[:user_id]
71
+ puts
72
+ end
73
+ else
74
+ # Table format
75
+ if searches.nil? || searches.empty?
76
+ puts 'No search history found.'
77
+ return
78
+ end
79
+
80
+ puts "Recent Search History (#{searches.length} searches):"
81
+ puts
82
+ puts 'Time'.ljust(20) + 'Query'.ljust(30) + 'Type'.ljust(10) + 'Results'.ljust(8) + 'Time(ms)'
83
+ puts '-' * 80
84
+
85
+ searches.each do |search|
86
+ time = format_time(search[:created_at]).ljust(20)
87
+ query = (search[:query] || 'N/A')[0..29].ljust(30)
88
+ type = (search[:search_type] || 'N/A')[0..9].ljust(10)
89
+ results = (search[:results_count] || 0).to_s.ljust(8)
90
+ exec_time = (search[:execution_time_ms] || 0).to_s
91
+
92
+ puts "#{time}#{query}#{type}#{results}#{exec_time}"
93
+ end
94
+ end
95
+ rescue StandardError => e
96
+ puts "Error retrieving search history: #{e.message}"
97
+ end
98
+
99
+ desc 'trending', 'Show trending search queries'
100
+ method_option :limit, type: :numeric, default: 10, aliases: '-l',
101
+ desc: 'Number of queries to show (default: 10)'
102
+ method_option :days, type: :numeric, default: 7, aliases: '-d',
103
+ desc: 'Time period in days (default: 7)'
104
+ method_option :format, type: :string, default: 'table', aliases: '-f',
105
+ desc: 'Output format (table, json)'
106
+ def trending
107
+ client = StandaloneClient.new
108
+ limit = (options && options[:limit]) || 10
109
+ days = (options && options[:days]) || 7
110
+ trending_queries = client.trending_queries(limit: limit, days: days)
111
+
112
+ case (options && options[:format]) || 'table'
113
+ when 'json'
114
+ puts JSON.pretty_generate(trending_queries || [])
115
+ else
116
+ if trending_queries.nil? || trending_queries.empty?
117
+ puts "No trending queries found for the last #{days} days."
118
+ return
119
+ end
120
+
121
+ puts "Trending Search Queries (last #{days} days):"
122
+ puts
123
+ puts 'Rank'.ljust(5) + 'Query'.ljust(40) + 'Count'.ljust(8) + 'Avg Results'
124
+ puts '-' * 80
125
+
126
+ trending_queries.each_with_index do |query_data, index|
127
+ rank = (index + 1).to_s.ljust(5)
128
+ query = (query_data[:query] || 'N/A')[0..39].ljust(40)
129
+ count = (query_data[:count] || 0).to_s.ljust(8)
130
+ avg_results = (query_data[:avg_results] || 0).round(1).to_s
131
+
132
+ puts "#{rank}#{query}#{count}#{avg_results}"
133
+ end
134
+ end
135
+ rescue StandardError => e
136
+ puts "Error retrieving trending queries: #{e.message}"
137
+ end
138
+
139
+ desc 'cleanup', 'Cleanup old search records'
140
+ method_option :days, type: :numeric, default: 30, aliases: '-d',
141
+ desc: 'Remove searches older than N days (default: 30)'
142
+ method_option :dry_run, type: :boolean, default: true, aliases: '-n',
143
+ desc: 'Show what would be deleted without actually deleting (default: true)'
144
+ method_option :force, type: :boolean, default: false, aliases: '-f',
145
+ desc: 'Actually perform the cleanup (overrides dry_run)'
146
+ def cleanup
147
+ client = StandaloneClient.new
148
+
149
+ days = (options && options[:days]) || 30
150
+ dry_run = if options && options.key?(:dry_run)
151
+ options[:dry_run]
152
+ else
153
+ true # Default to true
154
+ end
155
+ force = (options && options[:force]) == true
156
+
157
+ cleanup_options = {
158
+ days: days,
159
+ dry_run: dry_run && !force
160
+ }
161
+
162
+ if cleanup_options[:dry_run]
163
+ puts "DRY RUN: Showing what would be cleaned up (use --force to actually clean up)"
164
+ puts
165
+ else
166
+ puts "Performing actual cleanup of search records older than #{days} days..."
167
+ puts
168
+ end
169
+
170
+ result = client.cleanup_searches(**cleanup_options)
171
+
172
+ if result.is_a?(Hash)
173
+ puts "Cleanup Results:"
174
+ puts " Orphaned searches: #{result[:orphaned_count] || 0}"
175
+ puts " Old unused searches: #{result[:unused_count] || 0}"
176
+ puts " Total cleaned: #{(result[:orphaned_count] || 0) + (result[:unused_count] || 0)}"
177
+ else
178
+ puts "Searches cleaned up: #{result}"
179
+ end
180
+
181
+ if cleanup_options[:dry_run]
182
+ puts
183
+ puts "Use --force to actually perform the cleanup"
184
+ end
185
+ rescue StandardError => e
186
+ puts "Error during cleanup: #{e.message}"
187
+ end
188
+
189
+ private
190
+
191
+ def format_metric_name(key)
192
+ key.to_s.tr('_', ' ').split.map(&:capitalize).join(' ')
193
+ end
194
+
195
+ def format_metric_value(key, value)
196
+ case key.to_s
197
+ when /time/
198
+ "#{value}ms"
199
+ when /rate/
200
+ "#{value}%"
201
+ when /count/
202
+ value.to_s
203
+ else
204
+ value.to_s
205
+ end
206
+ end
207
+
208
+ def format_time(timestamp)
209
+ return 'N/A' unless timestamp
210
+
211
+ if timestamp.respond_to?(:strftime)
212
+ timestamp.strftime('%m/%d %H:%M')
213
+ else
214
+ # Handle string timestamps
215
+ Time.parse(timestamp.to_s).strftime('%m/%d %H:%M')
216
+ end
217
+ rescue
218
+ 'N/A'
219
+ end
220
+ end
221
+ end
222
+ end
@@ -15,7 +15,7 @@ module Ragdoll
15
15
  return unless yes?('Confirm deletion?')
16
16
  end
17
17
 
18
- result = client.delete_document(id: id)
18
+ result = client.delete_document(id)
19
19
 
20
20
  if result[:success]
21
21
  puts "Document ID #{id} deleted successfully."
@@ -29,8 +29,9 @@ module Ragdoll
29
29
  private
30
30
 
31
31
  def yes?(question)
32
- require 'highline/import'
33
- agree("#{question} (y/n) ")
32
+ print "#{question} (y/n) "
33
+ response = $stdin.gets.chomp.downcase
34
+ response == 'y' || response == 'yes'
34
35
  end
35
36
  end
36
37
  end
@@ -14,21 +14,79 @@ module Ragdoll
14
14
 
15
15
  search_options = {}
16
16
  search_options[:limit] = options[:limit] if options[:limit]
17
+ search_options[:threshold] = options[:threshold] if options[:threshold]
17
18
  search_options[:content_type] = options[:content_type] if options[:content_type]
18
19
  search_options[:classification] = options[:classification] if options[:classification]
19
20
  search_options[:keywords] = options[:keywords].split(',').map(&:strip) if options[:keywords]
20
21
  search_options[:tags] = options[:tags].split(',').map(&:strip) if options[:tags]
22
+
23
+ # Add search tracking options
24
+ search_options[:session_id] = options[:session_id] if options[:session_id]
25
+ search_options[:user_id] = options[:user_id] if options[:user_id]
26
+ search_options[:track_search] = options[:track_search] if options.respond_to?(:key?) ? options.key?(:track_search) : options.track_search
21
27
 
22
- search_response = client.search(query, **search_options)
28
+ # Select search method based on search_type
29
+ search_response = case options[:search_type]
30
+ when 'hybrid'
31
+ client.hybrid_search(query: query, **search_options)
32
+ when 'fulltext'
33
+ # Note: fulltext search would need to be implemented in client
34
+ client.search(query: query, **search_options)
35
+ else
36
+ client.search(query: query, **search_options)
37
+ end
23
38
 
24
39
  # Extract the actual results array from the response
25
40
  results = search_response[:results] || search_response['results'] || []
26
41
 
27
42
  if results.empty?
43
+ # Get statistics for better feedback
44
+ statistics = search_response[:statistics] || search_response['statistics']
45
+ execution_time = search_response[:execution_time_ms] || search_response['execution_time_ms']
28
46
  total = search_response[:total_results] || search_response['total_results'] || 0
47
+
29
48
  puts "No results found for '#{query}'"
30
- puts "(Total documents in system: #{total})" if total > 0
31
- puts "Try adjusting your search terms or check if documents have been processed."
49
+ puts
50
+
51
+ if statistics
52
+ threshold = statistics[:threshold_used] || statistics['threshold_used']
53
+ highest = statistics[:highest_similarity] || statistics['highest_similarity']
54
+ lowest = statistics[:lowest_similarity] || statistics['lowest_similarity']
55
+ average = statistics[:average_similarity] || statistics['average_similarity']
56
+ above_threshold = statistics[:similarities_above_threshold] || statistics['similarities_above_threshold']
57
+ total_checked = statistics[:total_embeddings_checked] || statistics['total_embeddings_checked']
58
+
59
+ puts "Search Analysis:"
60
+ puts " • Similarity threshold: #{threshold&.round(3) || 'N/A'}"
61
+ puts " • Embeddings analyzed: #{total_checked || 0}"
62
+ if highest && lowest && average
63
+ puts " • Similarity range: #{lowest.round(3)} - #{highest.round(3)} (avg: #{average.round(3)})"
64
+ end
65
+ puts " • Results above threshold: #{above_threshold || 0}"
66
+ puts " • Search time: #{execution_time || 0}ms"
67
+ puts
68
+
69
+ # Provide actionable suggestions
70
+ if highest && threshold
71
+ if highest < threshold
72
+ suggested_threshold = (highest * 0.9).round(3)
73
+ puts "💡 Suggestions:"
74
+ puts " • Lower the similarity threshold (highest found: #{highest.round(3)})"
75
+ puts " • Try: ragdoll search '#{query}' --threshold=#{suggested_threshold}"
76
+ if highest < 0.3
77
+ puts " • Your query might not match the document content well"
78
+ puts " • Try different or more specific search terms"
79
+ end
80
+ elsif above_threshold > 0
81
+ puts "💡 Note: Found #{above_threshold} similar content above threshold #{threshold}"
82
+ puts " This suggests an issue with result processing."
83
+ end
84
+ end
85
+ else
86
+ puts "(Total documents in system: #{total})" if total > 0
87
+ puts "Try adjusting your search terms or check if documents have been processed."
88
+ end
89
+
32
90
  return
33
91
  end
34
92
 
@@ -18,7 +18,7 @@ module Ragdoll
18
18
  return
19
19
  end
20
20
 
21
- result = client.update_document(id: id, **update_options)
21
+ result = client.update_document(id, **update_options)
22
22
 
23
23
  if result[:success]
24
24
  puts "Document ID #{id} updated successfully."
@@ -6,10 +6,8 @@ require 'fileutils'
6
6
  module Ragdoll
7
7
  module CLI
8
8
  class ConfigurationLoader
9
- DEFAULT_CONFIG_PATH = File.expand_path('~/.ragdoll/config.yml')
10
-
11
9
  def initialize
12
- @config_path = ENV['RAGDOLL_CONFIG'] || DEFAULT_CONFIG_PATH
10
+ @config_path = nil
13
11
  end
14
12
 
15
13
 
@@ -41,25 +39,27 @@ module Ragdoll
41
39
  'log_file' => File.expand_path('~/.ragdoll/ragdoll.log')
42
40
  }
43
41
 
44
- File.write(@config_path, YAML.dump(default_config))
42
+ File.write(config_path, YAML.dump(default_config))
45
43
  default_config
46
44
  end
47
45
 
48
46
 
49
47
  def config_exists?
50
- File.exist?(@config_path)
48
+ File.exist?(config_path)
51
49
  end
52
50
 
53
- attr_reader :config_path
51
+ def config_path
52
+ @config_path ||= ENV['RAGDOLL_CONFIG'] || File.expand_path('~/.ragdoll/config.yml')
53
+ end
54
54
 
55
55
  private
56
56
 
57
57
  def load_config_file
58
58
  return create_default_config unless config_exists?
59
59
 
60
- YAML.load_file(@config_path)
60
+ YAML.load_file(config_path)
61
61
  rescue StandardError => e
62
- puts "Warning: Could not load config file #{@config_path}: #{e.message}"
62
+ puts "Warning: Could not load config file #{config_path}: #{e.message}"
63
63
  puts 'Using default configuration.'
64
64
  create_default_config
65
65
  end
@@ -136,7 +136,7 @@ module Ragdoll
136
136
 
137
137
 
138
138
  def ensure_config_directory
139
- config_dir = File.dirname(@config_path)
139
+ config_dir = File.dirname(config_path)
140
140
  FileUtils.mkdir_p(config_dir) unless Dir.exist?(config_dir)
141
141
  end
142
142
  end
@@ -4,6 +4,7 @@ module Ragdoll
4
4
  module CLI
5
5
  class StandaloneClient
6
6
  include DebugMe
7
+
7
8
  def add_document(path, **options)
8
9
  Ragdoll.add_document(path: path, **options)
9
10
  end
@@ -34,8 +35,12 @@ module Ragdoll
34
35
  end
35
36
 
36
37
 
37
- def search(query, **options)
38
- Ragdoll.search(query: query, **options)
38
+ def search(query = nil, **options)
39
+ if query
40
+ Ragdoll.search(query: query, **options)
41
+ else
42
+ Ragdoll.search(**options)
43
+ end
39
44
  end
40
45
 
41
46
 
@@ -53,6 +58,68 @@ module Ragdoll
53
58
  Ragdoll.stats
54
59
  end
55
60
 
61
+ def search_analytics(days: 30)
62
+ # TODO: This will delegate to Ragdoll core when analytics are implemented
63
+ if defined?(Ragdoll) && Ragdoll.respond_to?(:search_analytics)
64
+ Ragdoll.search_analytics(days: days)
65
+ else
66
+ # Placeholder response for now
67
+ {
68
+ total_searches: 0,
69
+ unique_queries: 0,
70
+ avg_results_per_search: 0.0,
71
+ avg_execution_time: 0.0,
72
+ search_types: {},
73
+ searches_with_results: 0,
74
+ avg_click_through_rate: 0.0
75
+ }
76
+ end
77
+ end
78
+
79
+ def search_history(limit: 20, **options)
80
+ # TODO: This will delegate to Ragdoll core when analytics are implemented
81
+ if defined?(Ragdoll) && Ragdoll.respond_to?(:search_history)
82
+ Ragdoll.search_history(limit: limit, **options)
83
+ else
84
+ # Placeholder response for now
85
+ []
86
+ end
87
+ end
88
+
89
+ def trending_queries(limit: 10, days: 7)
90
+ # TODO: This will delegate to Ragdoll core when analytics are implemented
91
+ if defined?(Ragdoll) && Ragdoll.respond_to?(:trending_queries)
92
+ Ragdoll.trending_queries(limit: limit, days: days)
93
+ else
94
+ # Placeholder response for now
95
+ []
96
+ end
97
+ end
98
+
99
+ def cleanup_searches(**options)
100
+ # TODO: This will delegate to Ragdoll core when analytics are implemented
101
+ if defined?(Ragdoll) && Ragdoll.respond_to?(:cleanup_searches)
102
+ Ragdoll.cleanup_searches(**options)
103
+ else
104
+ # Placeholder response for now
105
+ { orphaned_count: 0, unused_count: 0 }
106
+ end
107
+ end
108
+
109
+ def hybrid_search(query = nil, **options)
110
+ # TODO: This will delegate to Ragdoll core when hybrid search is implemented
111
+ if defined?(Ragdoll) && Ragdoll.respond_to?(:hybrid_search)
112
+ if query
113
+ Ragdoll.hybrid_search(query: query, **options)
114
+ else
115
+ Ragdoll.hybrid_search(**options)
116
+ end
117
+ else
118
+ # Fallback to regular search for now
119
+ result = search(query, **options)
120
+ result.is_a?(Hash) ? result.merge(search_type: 'hybrid') : { search_type: 'hybrid', results: [] }
121
+ end
122
+ end
56
123
 
57
124
  def healthy?
58
125
  Ragdoll.healthy?
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Ragdoll
5
5
  module CLI
6
- VERSION = "0.0.2"
6
+ VERSION = "0.1.9"
7
7
  end
8
8
  end
data/lib/ragdoll/cli.rb CHANGED
@@ -14,6 +14,7 @@ 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'
17
18
 
18
19
  module Ragdoll
19
20
  module CLI
@@ -37,6 +38,8 @@ module Ragdoll
37
38
  desc 'search QUERY', 'Search for documents matching the query'
38
39
  method_option :limit, type: :numeric, default: 10, aliases: '-l',
39
40
  desc: 'Maximum number of results to return'
41
+ method_option :threshold, type: :numeric,
42
+ desc: 'Similarity threshold (0.0-1.0, lower = more results)'
40
43
  method_option :content_type, type: :string, aliases: '-c',
41
44
  desc: 'Filter by content type (text, image, audio)'
42
45
  method_option :classification, type: :string, aliases: '-C',
@@ -47,6 +50,14 @@ module Ragdoll
47
50
  desc: 'Filter by tags (comma-separated)'
48
51
  method_option :format, type: :string, default: 'table', aliases: '-f',
49
52
  desc: 'Output format (table, json, plain)'
53
+ method_option :session_id, type: :string, aliases: '-s',
54
+ desc: 'Session ID for search tracking'
55
+ method_option :user_id, type: :string, aliases: '-u',
56
+ desc: 'User ID for search tracking'
57
+ method_option :track_search, type: :boolean, default: true, aliases: '-t',
58
+ desc: 'Enable search tracking (default: true)'
59
+ method_option :search_type, type: :string, default: 'semantic', aliases: '-S',
60
+ desc: 'Search type: semantic, hybrid, fulltext (default: semantic)'
50
61
  def search(query)
51
62
  Search.new.call(query, options)
52
63
  end
@@ -54,6 +65,9 @@ module Ragdoll
54
65
  desc 'config SUBCOMMAND', 'Manage configuration'
55
66
  subcommand 'config', Config
56
67
 
68
+ desc 'analytics SUBCOMMAND', 'Search analytics and reporting'
69
+ subcommand 'analytics', Analytics
70
+
57
71
  desc 'stats', 'Show document and embedding statistics'
58
72
  def stats
59
73
  client = StandaloneClient.new
@@ -78,11 +92,36 @@ module Ragdoll
78
92
  end
79
93
  end
80
94
 
81
- return unless stats[:content_types]
95
+ if stats[:content_types]
96
+ puts "\nContent Types:"
97
+ stats[:content_types].each do |type, count|
98
+ puts " #{type}: #{count}"
99
+ end
100
+ end
82
101
 
83
- puts "\nContent Types:"
84
- stats[:content_types].each do |type, count|
85
- puts " #{type}: #{count}"
102
+ # Add search analytics if available
103
+ begin
104
+ search_analytics = client.search_analytics(days: 30)
105
+ if search_analytics && !search_analytics.empty?
106
+ puts "\nSearch Analytics (last 30 days):"
107
+ puts " Total searches: #{search_analytics[:total_searches] || 0}"
108
+ puts " Unique queries: #{search_analytics[:unique_queries] || 0}"
109
+ puts " Avg results per search: #{search_analytics[:avg_results_per_search] || 0}"
110
+ puts " Avg execution time: #{search_analytics[:avg_execution_time] || 0}ms"
111
+
112
+ if search_analytics[:search_types]
113
+ puts " Search types:"
114
+ search_analytics[:search_types].each do |type, count|
115
+ puts " #{type}: #{count}"
116
+ end
117
+ end
118
+
119
+ puts " Searches with results: #{search_analytics[:searches_with_results] || 0}"
120
+ puts " Avg click-through rate: #{search_analytics[:avg_click_through_rate] || 0}%"
121
+ end
122
+ rescue StandardError => e
123
+ # Search analytics not available - silently continue
124
+ puts "\nSearch analytics: Not available (#{e.message})"
86
125
  end
87
126
  end
88
127
 
@@ -354,27 +393,152 @@ module Ragdoll
354
393
  end
355
394
 
356
395
  desc 'context QUERY', 'Get context for RAG applications'
357
- method_option :limit, type: :numeric, default: 5, aliases: '-l', desc: 'Maximum number of context chunks'
396
+ method_option :limit, type: :numeric, default: 10, aliases: '-l', desc: 'Maximum number of context chunks'
397
+ method_option :threshold, type: :numeric, desc: 'Similarity threshold (0.0-1.0, lower = more results)'
358
398
  def context(query)
359
399
  client = StandaloneClient.new
360
- ctx = client.get_context(query, limit: options[:limit])
361
- puts JSON.pretty_generate(ctx)
400
+ context_options = { limit: options[:limit] }
401
+ context_options[:threshold] = options[:threshold] if options[:threshold]
402
+ ctx = client.get_context(query, **context_options)
403
+
404
+ # Check if no context was found and provide enhanced feedback
405
+ if ctx[:context_chunks].empty?
406
+ # Get the underlying search response for statistics
407
+ search_response = client.search(query, **context_options)
408
+ display_no_results_feedback(query, search_response, 'context')
409
+ else
410
+ puts JSON.pretty_generate(ctx)
411
+ end
362
412
  end
363
413
 
364
414
  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'
415
+ method_option :limit, type: :numeric, default: 10, aliases: '-l', desc: 'Maximum number of context chunks to include'
416
+ method_option :threshold, type: :numeric, desc: 'Similarity threshold (0.0-1.0, lower = more results)'
366
417
  def enhance(prompt)
367
418
  client = StandaloneClient.new
368
- enhanced = client.enhance_prompt(prompt, context_limit: options[:context_limit])
369
- puts enhanced
419
+ enhance_options = { context_limit: options[:limit] }
420
+ enhance_options[:threshold] = options[:threshold] if options[:threshold]
421
+ enhanced = client.enhance_prompt(prompt, **enhance_options)
422
+
423
+ # Check if no context was found and provide enhanced feedback
424
+ if enhanced[:context_count] == 0
425
+ # Get the underlying search response for statistics
426
+ search_response = client.search(prompt, limit: enhance_options[:context_limit], threshold: enhance_options[:threshold])
427
+ display_no_results_feedback(prompt, search_response, 'enhance')
428
+ else
429
+ puts enhanced[:enhanced_prompt]
430
+ end
431
+ end
432
+
433
+ desc 'search-history', 'Show recent search history'
434
+ method_option :limit, type: :numeric, default: 20, aliases: '-l',
435
+ desc: 'Number of searches to show (default: 20)'
436
+ method_option :user_id, type: :string, aliases: '-u',
437
+ desc: 'Filter by user ID'
438
+ method_option :session_id, type: :string, aliases: '-s',
439
+ desc: 'Filter by session ID'
440
+ method_option :format, type: :string, default: 'table', aliases: '-f',
441
+ desc: 'Output format (table, json, plain)'
442
+ def search_history
443
+ analytics = Analytics.new
444
+ analytics.options = options
445
+ analytics.history
446
+ end
447
+
448
+ desc 'search-stats', 'Show detailed search analytics'
449
+ method_option :days, type: :numeric, default: 30, aliases: '-d',
450
+ desc: 'Number of days to analyze (default: 30)'
451
+ method_option :format, type: :string, default: 'table', aliases: '-f',
452
+ desc: 'Output format (table, json)'
453
+ def search_stats
454
+ analytics = Analytics.new
455
+ analytics.options = options
456
+ analytics.overview
370
457
  end
371
458
 
459
+ desc 'trending', 'Show trending search queries'
460
+ method_option :limit, type: :numeric, default: 10, aliases: '-l',
461
+ desc: 'Number of queries to show (default: 10)'
462
+ method_option :days, type: :numeric, default: 7, aliases: '-d',
463
+ desc: 'Time period in days (default: 7)'
464
+ method_option :format, type: :string, default: 'table', aliases: '-f',
465
+ desc: 'Output format (table, json)'
466
+ def trending
467
+ analytics = Analytics.new
468
+ analytics.options = options
469
+ analytics.trending
470
+ end
471
+
472
+ desc 'cleanup-searches', 'Cleanup old search records'
473
+ method_option :days, type: :numeric, default: 30, aliases: '-d',
474
+ desc: 'Remove searches older than N days (default: 30)'
475
+ method_option :dry_run, type: :boolean, default: true, aliases: '-n',
476
+ desc: 'Show what would be deleted without actually deleting (default: true)'
477
+ method_option :force, type: :boolean, default: false, aliases: '-f',
478
+ desc: 'Actually perform the cleanup (overrides dry_run)'
479
+ def cleanup_searches
480
+ analytics = Analytics.new
481
+ analytics.options = options
482
+ analytics.cleanup
483
+ end
372
484
 
373
485
  private
374
486
 
375
487
  def load_configuration
376
488
  ConfigurationLoader.new.load
377
489
  end
490
+
491
+ def display_no_results_feedback(query, search_response, command_type)
492
+ # Extract the actual results array from the response
493
+ results = search_response[:results] || search_response['results'] || []
494
+
495
+ puts "No results found for '#{query}'"
496
+ puts
497
+
498
+ # Get statistics for better feedback
499
+ statistics = search_response[:statistics] || search_response['statistics']
500
+ execution_time = search_response[:execution_time_ms] || search_response['execution_time_ms']
501
+ total = search_response[:total_results] || search_response['total_results'] || 0
502
+
503
+ if statistics
504
+ threshold = statistics[:threshold_used] || statistics['threshold_used']
505
+ highest = statistics[:highest_similarity] || statistics['highest_similarity']
506
+ lowest = statistics[:lowest_similarity] || statistics['lowest_similarity']
507
+ average = statistics[:average_similarity] || statistics['average_similarity']
508
+ above_threshold = statistics[:similarities_above_threshold] || statistics['similarities_above_threshold']
509
+ total_checked = statistics[:total_embeddings_checked] || statistics['total_embeddings_checked']
510
+
511
+ puts "Search Analysis:"
512
+ puts " • Similarity threshold: #{threshold&.round(3) || 'N/A'}"
513
+ puts " • Embeddings analyzed: #{total_checked || 0}"
514
+ if highest && lowest && average
515
+ puts " • Similarity range: #{lowest.round(3)} - #{highest.round(3)} (avg: #{average.round(3)})"
516
+ end
517
+ puts " • Results above threshold: #{above_threshold || 0}"
518
+ puts " • Search time: #{execution_time || 0}ms"
519
+ puts
520
+
521
+ # Provide actionable suggestions
522
+ if highest && threshold
523
+ if highest < threshold
524
+ suggested_threshold = (highest * 0.9).round(3)
525
+ puts "💡 Suggestions:"
526
+ puts " • Lower the similarity threshold (highest found: #{highest.round(3)})"
527
+ puts " • Try: ragdoll #{command_type} '#{query}' --threshold=#{suggested_threshold}"
528
+ if highest < 0.3
529
+ puts " • Your query might not match the document content well"
530
+ puts " • Try different or more specific search terms"
531
+ end
532
+ elsif above_threshold > 0
533
+ puts "💡 Note: Found #{above_threshold} similar content above threshold #{threshold}"
534
+ puts " This suggests an issue with result processing."
535
+ end
536
+ end
537
+ else
538
+ puts "No similarity statistics available."
539
+ puts "💡 Try lowering the similarity threshold with --threshold=0.5"
540
+ end
541
+ end
378
542
  end
379
543
  end
380
544
  end
metadata CHANGED
@@ -1,14 +1,182 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ragdoll-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.9
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: []
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'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
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'
12
180
  description: Under development. Contributors welcome.
13
181
  email:
14
182
  - dvanhoozer@gmail.com
@@ -21,6 +189,7 @@ files:
21
189
  - Rakefile
22
190
  - bin/ragdoll
23
191
  - lib/ragdoll/cli.rb
192
+ - lib/ragdoll/cli/commands/analytics.rb
24
193
  - lib/ragdoll/cli/commands/config.rb
25
194
  - lib/ragdoll/cli/commands/delete.rb
26
195
  - lib/ragdoll/cli/commands/health.rb