ragdoll-rails 0.1.9 → 0.1.11

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/ragdoll/application.js +129 -0
  3. data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
  4. data/app/assets/stylesheets/ragdoll/application.css +84 -0
  5. data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
  6. data/app/channels/application_cable/channel.rb +6 -0
  7. data/app/channels/application_cable/connection.rb +6 -0
  8. data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
  9. data/app/channels/ragdoll/file_processing_channel.rb +26 -0
  10. data/app/components/ragdoll/alert_component.html.erb +4 -0
  11. data/app/components/ragdoll/alert_component.rb +32 -0
  12. data/app/components/ragdoll/application_component.rb +6 -0
  13. data/app/components/ragdoll/card_component.html.erb +15 -0
  14. data/app/components/ragdoll/card_component.rb +21 -0
  15. data/app/components/ragdoll/document_list_component.html.erb +41 -0
  16. data/app/components/ragdoll/document_list_component.rb +13 -0
  17. data/app/components/ragdoll/document_table_component.html.erb +76 -0
  18. data/app/components/ragdoll/document_table_component.rb +13 -0
  19. data/app/components/ragdoll/empty_state_component.html.erb +12 -0
  20. data/app/components/ragdoll/empty_state_component.rb +17 -0
  21. data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
  22. data/app/components/ragdoll/flash_messages_component.rb +37 -0
  23. data/app/components/ragdoll/navbar_component.html.erb +24 -0
  24. data/app/components/ragdoll/navbar_component.rb +31 -0
  25. data/app/components/ragdoll/page_header_component.html.erb +13 -0
  26. data/app/components/ragdoll/page_header_component.rb +15 -0
  27. data/app/components/ragdoll/stats_card_component.html.erb +11 -0
  28. data/app/components/ragdoll/stats_card_component.rb +17 -0
  29. data/app/components/ragdoll/status_badge_component.html.erb +3 -0
  30. data/app/components/ragdoll/status_badge_component.rb +30 -0
  31. data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
  32. data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
  33. data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
  34. data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
  35. data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
  36. data/app/controllers/ragdoll/application_controller.rb +17 -0
  37. data/app/controllers/ragdoll/configuration_controller.rb +82 -0
  38. data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
  39. data/app/controllers/ragdoll/documents_controller.rb +460 -0
  40. data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
  41. data/app/controllers/ragdoll/jobs_controller.rb +116 -0
  42. data/app/controllers/ragdoll/search_controller.rb +368 -0
  43. data/app/jobs/application_job.rb +9 -0
  44. data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
  45. data/app/jobs/ragdoll/process_file_job.rb +166 -0
  46. data/app/services/ragdoll/worker_health_service.rb +111 -0
  47. data/app/views/layouts/ragdoll/application.html.erb +162 -0
  48. data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
  49. data/app/views/ragdoll/dashboard/index.html.erb +208 -0
  50. data/app/views/ragdoll/documents/edit.html.erb +91 -0
  51. data/app/views/ragdoll/documents/index.html.erb +302 -0
  52. data/app/views/ragdoll/documents/new.html.erb +1518 -0
  53. data/app/views/ragdoll/documents/show.html.erb +188 -0
  54. data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
  55. data/app/views/ragdoll/jobs/index.html.erb +669 -0
  56. data/app/views/ragdoll/jobs/show.html.erb +129 -0
  57. data/app/views/ragdoll/search/index.html.erb +324 -0
  58. data/config/cable.yml +12 -0
  59. data/config/routes.rb +56 -1
  60. data/lib/ragdoll/rails/engine.rb +32 -1
  61. data/lib/ragdoll/rails/version.rb +1 -1
  62. metadata +86 -1
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class EmptyStateComponent < ApplicationComponent
5
+ def initialize(title:, message:, icon: nil, action_path: nil, action_text: nil)
6
+ @title = title
7
+ @message = message
8
+ @icon = icon
9
+ @action_path = action_path
10
+ @action_text = action_text
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :title, :message, :icon, :action_path, :action_text
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ <% flash_messages.each do |flash_msg| %>
2
+ <%= render Ragdoll::AlertComponent.new(message: flash_msg[:message], type: flash_msg[:type]) %>
3
+ <% end %>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class FlashMessagesComponent < ApplicationComponent
5
+ def initialize(flash:)
6
+ @flash = flash
7
+ end
8
+
9
+ private
10
+
11
+ attr_reader :flash
12
+
13
+ def flash_type_to_alert_type(type)
14
+ case type.to_s
15
+ when 'notice'
16
+ 'success'
17
+ when 'alert'
18
+ 'danger'
19
+ when 'error'
20
+ 'danger'
21
+ when 'warning'
22
+ 'warning'
23
+ else
24
+ 'info'
25
+ end
26
+ end
27
+
28
+ def flash_messages
29
+ flash.map do |type, message|
30
+ {
31
+ type: flash_type_to_alert_type(type),
32
+ message: message
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
2
+ <div class="container">
3
+ <%= link_to brand_path, class: "navbar-brand d-flex align-items-center" do %>
4
+ <i class="fas fa-robot me-2"></i>
5
+ <%= brand_text %>
6
+ <% end %>
7
+
8
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
9
+ <span class="navbar-toggler-icon"></span>
10
+ </button>
11
+
12
+ <div class="collapse navbar-collapse" id="navbarNav">
13
+ <ul class="navbar-nav me-auto">
14
+ <% nav_items.each do |item| %>
15
+ <li class="nav-item">
16
+ <%= link_to item[:path], class: nav_link_classes(item[:path]) do %>
17
+ <i class="<%= item[:icon] %>"></i> <%= item[:text] %>
18
+ <% end %>
19
+ </li>
20
+ <% end %>
21
+ </ul>
22
+ </div>
23
+ </div>
24
+ </nav>
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class NavbarComponent < ApplicationComponent
5
+ def initialize(brand_text: 'Ragdoll Engine', brand_path: nil)
6
+ @brand_text = brand_text
7
+ @brand_path = brand_path || main_app.root_path
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :brand_text, :brand_path
13
+
14
+ def nav_items
15
+ [
16
+ { text: 'Dashboard', path: ragdoll.dashboard_index_path, icon: 'fas fa-tachometer-alt' },
17
+ { text: 'Documents', path: ragdoll.documents_path, icon: 'fas fa-file-alt' },
18
+ { text: 'Search', path: ragdoll.search_index_path, icon: 'fas fa-search' },
19
+ { text: 'Jobs', path: ragdoll.jobs_path, icon: 'fas fa-tasks' },
20
+ { text: 'Analytics', path: ragdoll.analytics_path, icon: 'fas fa-chart-line' },
21
+ { text: 'Configuration', path: ragdoll.configuration_path, icon: 'fas fa-cog' }
22
+ ]
23
+ end
24
+
25
+ def nav_link_classes(path)
26
+ base_classes = ['nav-link']
27
+ base_classes << 'active' if current_page?(path)
28
+ base_classes.join(' ')
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ <div class="d-flex justify-content-between align-items-center mb-4">
2
+ <div>
3
+ <h1>
4
+ <% if icon %>
5
+ <i class="<%= icon %>"></i>
6
+ <% end %>
7
+ <%= title %>
8
+ </h1>
9
+ <% if subtitle %>
10
+ <p class="text-muted"><%= subtitle %></p>
11
+ <% end %>
12
+ </div>
13
+ </div>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class PageHeaderComponent < ApplicationComponent
5
+ def initialize(title:, icon: nil, subtitle: nil)
6
+ @title = title
7
+ @icon = icon
8
+ @subtitle = subtitle
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :title, :icon, :subtitle
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ <div class="card border-<%= color %>">
2
+ <div class="card-body text-center">
3
+ <h5 class="card-title text-<%= color %>">
4
+ <i class="<%= icon %>"></i> <%= title %>
5
+ </h5>
6
+ <h2 class="text-<%= color %>"><%= value %></h2>
7
+ <% if description %>
8
+ <p class="text-muted mb-0"><%= description %></p>
9
+ <% end %>
10
+ </div>
11
+ </div>
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class StatsCardComponent < ApplicationComponent
5
+ def initialize(title:, value:, icon:, color: 'primary', description: nil)
6
+ @title = title
7
+ @value = value
8
+ @icon = icon
9
+ @color = color
10
+ @description = description
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :title, :value, :icon, :color, :description
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ <span class="badge <%= badge_class %>">
2
+ <%= badge_text %>
3
+ </span>
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class StatusBadgeComponent < ApplicationComponent
5
+ def initialize(status:)
6
+ @status = status
7
+ end
8
+
9
+ private
10
+
11
+ attr_reader :status
12
+
13
+ def badge_class
14
+ case status.to_s.downcase
15
+ when 'processed', 'completed', 'success'
16
+ 'bg-success'
17
+ when 'failed', 'error'
18
+ 'bg-danger'
19
+ when 'processing', 'pending', 'queued'
20
+ 'bg-warning'
21
+ else
22
+ 'bg-secondary'
23
+ end
24
+ end
25
+
26
+ def badge_text
27
+ status.to_s.humanize
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ module Api
5
+ module V1
6
+ class AnalyticsController < BaseController
7
+ def index
8
+ analytics_data = {
9
+ document_stats: {
10
+ total_documents: ::Ragdoll::Document.count,
11
+ processed_documents: ::Ragdoll::Document.where(status: 'processed').count,
12
+ failed_documents: ::Ragdoll::Document.where(status: 'failed').count,
13
+ pending_documents: ::Ragdoll::Document.where(status: 'pending').count,
14
+ total_embeddings: ::Ragdoll::Embedding.count
15
+ },
16
+
17
+ search_stats: {
18
+ total_searches: ::Ragdoll::Search.count,
19
+ unique_queries: ::Ragdoll::Search.distinct.count(:query),
20
+ searches_today: ::Ragdoll::Search.where(created_at: Date.current.beginning_of_day..Date.current.end_of_day).count,
21
+ searches_this_week: ::Ragdoll::Search.where(created_at: Date.current.beginning_of_week..Date.current.end_of_day).count,
22
+ average_similarity: ::Ragdoll::Search.where.not(avg_similarity_score: nil).average(:avg_similarity_score)&.round(3) || 0
23
+ },
24
+
25
+ popular_queries: ::Ragdoll::Search.group(:query).count.sort_by { |query, count| -count }.first(10).to_h,
26
+
27
+ document_types: ::Ragdoll::Document.group(:document_type).count,
28
+
29
+ top_documents: ::Ragdoll::Embedding
30
+ .joins("JOIN ragdoll_contents ON ragdoll_contents.id = ragdoll_embeddings.embeddable_id")
31
+ .joins("JOIN ragdoll_documents ON ragdoll_documents.id = ragdoll_contents.document_id")
32
+ .group('ragdoll_documents.title')
33
+ .order('SUM(ragdoll_embeddings.usage_count) DESC')
34
+ .limit(10)
35
+ .sum(:usage_count),
36
+
37
+ search_trends: (6.days.ago.to_date..Date.current).map do |date|
38
+ count = ::Ragdoll::Search.where(created_at: date.beginning_of_day..date.end_of_day).count
39
+ [date.strftime('%m/%d'), count]
40
+ end.to_h,
41
+
42
+ embedding_usage: ::Ragdoll::Embedding
43
+ .joins("JOIN ragdoll_contents ON ragdoll_contents.id = ragdoll_embeddings.embeddable_id")
44
+ .joins("JOIN ragdoll_documents ON ragdoll_documents.id = ragdoll_contents.document_id")
45
+ .group('ragdoll_documents.title')
46
+ .order('SUM(ragdoll_embeddings.usage_count) DESC')
47
+ .limit(10)
48
+ .sum(:usage_count),
49
+
50
+ similarity_distribution: build_similarity_distribution
51
+ }
52
+
53
+ render json: analytics_data
54
+ end
55
+
56
+ private
57
+
58
+ def build_similarity_distribution
59
+ similarity_scores = ::Ragdoll::Search.where.not(avg_similarity_score: nil).pluck(:avg_similarity_score)
60
+ {
61
+ "0.9-1.0" => similarity_scores.count { |s| s >= 0.9 },
62
+ "0.8-0.9" => similarity_scores.count { |s| s >= 0.8 && s < 0.9 },
63
+ "0.7-0.8" => similarity_scores.count { |s| s >= 0.7 && s < 0.8 },
64
+ "0.6-0.7" => similarity_scores.count { |s| s >= 0.6 && s < 0.7 },
65
+ "0.5-0.6" => similarity_scores.count { |s| s >= 0.5 && s < 0.6 },
66
+ "< 0.5" => similarity_scores.count { |s| s < 0.5 }
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ module Api
5
+ module V1
6
+ class BaseController < ActionController::API
7
+ protect_from_forgery with: :null_session
8
+ before_action :set_default_response_format
9
+
10
+ private
11
+
12
+ def set_default_response_format
13
+ request.format = :json
14
+ end
15
+
16
+ def render_error(message, status = :unprocessable_entity)
17
+ render json: { error: message }, status: status
18
+ end
19
+
20
+ def render_success(data = {}, message = nil)
21
+ response = { success: true }
22
+ response[:message] = message if message
23
+ response[:data] = data unless data.empty?
24
+ render json: response
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ module Api
5
+ module V1
6
+ class DocumentsController < BaseController
7
+ before_action :set_document, only: [:show, :update, :destroy, :reprocess]
8
+
9
+ def index
10
+ documents = ::Ragdoll::Document.all
11
+ documents = documents.where(status: params[:status]) if params[:status].present?
12
+ documents = documents.where(document_type: params[:document_type]) if params[:document_type].present?
13
+ documents = documents.order(created_at: :desc)
14
+
15
+ render json: {
16
+ documents: documents.map(&method(:document_json)),
17
+ total: documents.count
18
+ }
19
+ end
20
+
21
+ def show
22
+ render json: {
23
+ document: document_json(@document),
24
+ embeddings: @document.all_embeddings.map(&method(:embedding_json))
25
+ }
26
+ end
27
+
28
+ def create
29
+ begin
30
+ if params[:file].present?
31
+ # Handle file upload
32
+ temp_path = Rails.root.join('tmp', 'uploads', params[:file].original_filename)
33
+ FileUtils.mkdir_p(File.dirname(temp_path))
34
+ File.binwrite(temp_path, params[:file].read)
35
+
36
+ result = ::Ragdoll.add_document(path: temp_path.to_s)
37
+
38
+ if result[:success] && result[:document_id]
39
+ document = ::Ragdoll::Document.find(result[:document_id])
40
+ File.delete(temp_path) if File.exist?(temp_path)
41
+ render json: { document: document_json(document) }, status: :created
42
+ else
43
+ File.delete(temp_path) if File.exist?(temp_path)
44
+ return render_error(result[:error] || "Failed to create document")
45
+ end
46
+ elsif params[:content].present?
47
+ # Handle text content
48
+ temp_path = Rails.root.join('tmp', 'uploads', "#{SecureRandom.hex(8)}.txt")
49
+ FileUtils.mkdir_p(File.dirname(temp_path))
50
+ File.write(temp_path, params[:content])
51
+
52
+ result = ::Ragdoll.add_document(path: temp_path.to_s)
53
+
54
+ if result[:success] && result[:document_id]
55
+ document = ::Ragdoll::Document.find(result[:document_id])
56
+ File.delete(temp_path) if File.exist?(temp_path)
57
+ render json: { document: document_json(document) }, status: :created
58
+ else
59
+ File.delete(temp_path) if File.exist?(temp_path)
60
+ return render_error(result[:error] || "Failed to create document")
61
+ end
62
+ else
63
+ return render_error("Either file or content must be provided")
64
+ end
65
+
66
+ rescue => e
67
+ render_error(e.message)
68
+ end
69
+ end
70
+
71
+ def update
72
+ begin
73
+ if @document.update(document_params)
74
+ render json: { document: document_json(@document) }
75
+ else
76
+ render_error(@document.errors.full_messages.join(', '))
77
+ end
78
+ rescue => e
79
+ render_error(e.message)
80
+ end
81
+ end
82
+
83
+ def destroy
84
+ begin
85
+ @document.destroy
86
+ render_success({}, "Document deleted successfully")
87
+ rescue => e
88
+ render_error(e.message)
89
+ end
90
+ end
91
+
92
+ def reprocess
93
+ begin
94
+ @document.all_embeddings.destroy_all
95
+ @document.update(status: 'pending')
96
+ ::Ragdoll::GenerateEmbeddingsJob.perform_later(@document.id)
97
+
98
+ render_success({}, "Document reprocessing initiated")
99
+ rescue => e
100
+ render_error(e.message)
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def set_document
107
+ @document = ::Ragdoll::Document.find(params[:id])
108
+ rescue ActiveRecord::RecordNotFound
109
+ render_error("Document not found", :not_found)
110
+ end
111
+
112
+ def document_params
113
+ params.require(:document).permit(:title, :content, :metadata, :status)
114
+ end
115
+
116
+ def document_json(document)
117
+ {
118
+ id: document.id,
119
+ title: document.title,
120
+ content: document.content,
121
+ document_type: document.document_type,
122
+ location: document.location,
123
+ metadata: document.metadata,
124
+ status: document.status,
125
+ character_count: document.total_character_count,
126
+ word_count: document.total_word_count,
127
+ embedding_count: document.all_embeddings.count,
128
+ created_at: document.created_at,
129
+ updated_at: document.updated_at
130
+ }
131
+ end
132
+
133
+ def embedding_json(embedding)
134
+ {
135
+ id: embedding.id,
136
+ content: embedding.content,
137
+ chunk_index: embedding.chunk_index,
138
+ model_name: embedding.model_name,
139
+ vector_dimensions: embedding.embedding_dimensions,
140
+ usage_count: embedding.usage_count,
141
+ last_used_at: embedding.returned_at,
142
+ created_at: embedding.created_at
143
+ }
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ module Api
5
+ module V1
6
+ class SearchController < BaseController
7
+ def search
8
+ query = params[:query]
9
+
10
+ if query.blank?
11
+ return render_error("Query parameter is required")
12
+ end
13
+
14
+ begin
15
+ search_options = {
16
+ limit: params[:limit]&.to_i || 10,
17
+ threshold: params[:threshold]&.to_f || 0.7,
18
+ use_usage_ranking: params[:use_usage_ranking] == 'true'
19
+ }
20
+
21
+ # Add document type filter if specified
22
+ if params[:document_type].present?
23
+ search_options[:document_type] = params[:document_type]
24
+ end
25
+
26
+ # Add status filter if specified
27
+ if params[:status].present?
28
+ search_options[:status] = params[:status]
29
+ end
30
+
31
+ search_response = ::Ragdoll.search(search_options.merge(query: query))
32
+ results = search_response.is_a?(Hash) ? search_response[:results] || [] : []
33
+
34
+ # Format results with additional metadata
35
+ formatted_results = results.map do |result|
36
+ if result[:embedding_id] && result[:document_id]
37
+ embedding = ::Ragdoll::Embedding.find(result[:embedding_id])
38
+ document = ::Ragdoll::Document.find(result[:document_id])
39
+ {
40
+ embedding_id: result[:embedding_id],
41
+ document_id: result[:document_id],
42
+ document_title: document.title,
43
+ content: result[:content],
44
+ similarity: result[:similarity],
45
+ usage_count: embedding.usage_count,
46
+ last_used_at: embedding.returned_at,
47
+ chunk_index: embedding.chunk_index,
48
+ document_type: document.document_type
49
+ }
50
+ end
51
+ end.compact
52
+
53
+ # Save search for analytics
54
+ begin
55
+ similarities = formatted_results.map { |r| r[:similarity] }.compact
56
+ ::Ragdoll::Search.create!(
57
+ query: query,
58
+ search_type: 'api_semantic',
59
+ results_count: formatted_results.count,
60
+ max_similarity_score: similarities.any? ? similarities.max : nil,
61
+ min_similarity_score: similarities.any? ? similarities.min : nil,
62
+ avg_similarity_score: similarities.any? ? (similarities.sum / similarities.size.to_f) : nil,
63
+ search_filters: search_options.to_json,
64
+ search_options: {
65
+ api_request: true,
66
+ threshold_used: search_options[:threshold]
67
+ }.to_json
68
+ )
69
+ rescue => e
70
+ Rails.logger.error "Failed to save API search: #{e.message}"
71
+ end
72
+
73
+ render json: {
74
+ query: query,
75
+ results: formatted_results,
76
+ total_results: formatted_results.count,
77
+ search_options: search_options
78
+ }
79
+
80
+ rescue => e
81
+ render_error(e.message)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ module Api
5
+ module V1
6
+ class SystemController < BaseController
7
+ def stats
8
+ begin
9
+ client = ::Ragdoll::Client.new
10
+ ragdoll_stats = client.stats
11
+
12
+ system_stats = {
13
+ ragdoll_version: ::Ragdoll::VERSION,
14
+ rails_version: Rails.version,
15
+ ruby_version: RUBY_VERSION,
16
+
17
+ database_stats: {
18
+ documents: ::Ragdoll::Document.count,
19
+ embeddings: ::Ragdoll::Embedding.count,
20
+ searches: ::Ragdoll::Search.count,
21
+ database_size: calculate_database_size
22
+ },
23
+
24
+ configuration: {
25
+ llm_provider: ::Ragdoll.configuration.llm_provider,
26
+ embedding_provider: ::Ragdoll.configuration.embedding_provider,
27
+ embedding_model: ::Ragdoll.configuration.embedding_model,
28
+ chunk_size: ::Ragdoll.configuration.chunk_size,
29
+ chunk_overlap: ::Ragdoll.configuration.chunk_overlap,
30
+ max_search_results: ::Ragdoll.configuration.max_search_results,
31
+ search_similarity_threshold: ::Ragdoll.configuration.search_similarity_threshold,
32
+ enable_search_analytics: ::Ragdoll.configuration.enable_search_analytics,
33
+ enable_document_summarization: ::Ragdoll.configuration.enable_document_summarization,
34
+ enable_usage_tracking: ::Ragdoll.configuration.enable_usage_tracking,
35
+ usage_ranking_enabled: ::Ragdoll.configuration.usage_ranking_enabled
36
+ },
37
+
38
+ performance_metrics: {
39
+ average_search_time: calculate_average_search_time,
40
+ embedding_dimensions: ::Ragdoll::Embedding.first&.embedding_dimensions || 0,
41
+ average_document_size: ::Ragdoll::Document.average('LENGTH(summary)')&.round || 0,
42
+ average_chunks_per_document: ::Ragdoll::Document.count > 0 ? (::Ragdoll::Embedding.count.to_f / ::Ragdoll::Document.count).round || 0 : 0
43
+ },
44
+
45
+ health_check: {
46
+ database_connection: database_healthy?,
47
+ embedding_service: embedding_service_healthy?
48
+ }
49
+ }.merge(ragdoll_stats)
50
+
51
+ render json: system_stats
52
+ rescue => e
53
+ render_error("Error retrieving system stats: #{e.message}")
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def calculate_database_size
60
+ # Simple approximation - in production you might want more sophisticated calculation
61
+ result = ActiveRecord::Base.connection.execute(
62
+ "SELECT pg_size_pretty(pg_database_size(current_database()))"
63
+ )
64
+ result.first['pg_size_pretty']
65
+ rescue
66
+ "Unknown"
67
+ end
68
+
69
+ def calculate_average_search_time
70
+ # Calculate from actual search execution times if available
71
+ avg_time = ::Ragdoll::Search.where.not(execution_time_ms: nil).average(:execution_time_ms)
72
+ if avg_time
73
+ "#{avg_time.round}ms"
74
+ else
75
+ "< 100ms"
76
+ end
77
+ end
78
+
79
+ def database_healthy?
80
+ ActiveRecord::Base.connection.active?
81
+ rescue
82
+ false
83
+ end
84
+
85
+ def embedding_service_healthy?
86
+ begin
87
+ client = ::Ragdoll::Client.new
88
+ # Try a simple test to see if the embedding service is available
89
+ true
90
+ rescue
91
+ false
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+ before_action :set_current_user
7
+
8
+ layout 'ragdoll/application'
9
+
10
+ private
11
+
12
+ def set_current_user
13
+ # This can be overridden in the host application
14
+ # to set the current user for the Ragdoll engine
15
+ end
16
+ end
17
+ end