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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/ragdoll/application.js +129 -0
- data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
- data/app/assets/stylesheets/ragdoll/application.css +84 -0
- data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
- data/app/channels/application_cable/channel.rb +6 -0
- data/app/channels/application_cable/connection.rb +6 -0
- data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
- data/app/channels/ragdoll/file_processing_channel.rb +26 -0
- data/app/components/ragdoll/alert_component.html.erb +4 -0
- data/app/components/ragdoll/alert_component.rb +32 -0
- data/app/components/ragdoll/application_component.rb +6 -0
- data/app/components/ragdoll/card_component.html.erb +15 -0
- data/app/components/ragdoll/card_component.rb +21 -0
- data/app/components/ragdoll/document_list_component.html.erb +41 -0
- data/app/components/ragdoll/document_list_component.rb +13 -0
- data/app/components/ragdoll/document_table_component.html.erb +76 -0
- data/app/components/ragdoll/document_table_component.rb +13 -0
- data/app/components/ragdoll/empty_state_component.html.erb +12 -0
- data/app/components/ragdoll/empty_state_component.rb +17 -0
- data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
- data/app/components/ragdoll/flash_messages_component.rb +37 -0
- data/app/components/ragdoll/navbar_component.html.erb +24 -0
- data/app/components/ragdoll/navbar_component.rb +31 -0
- data/app/components/ragdoll/page_header_component.html.erb +13 -0
- data/app/components/ragdoll/page_header_component.rb +15 -0
- data/app/components/ragdoll/stats_card_component.html.erb +11 -0
- data/app/components/ragdoll/stats_card_component.rb +17 -0
- data/app/components/ragdoll/status_badge_component.html.erb +3 -0
- data/app/components/ragdoll/status_badge_component.rb +30 -0
- data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
- data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
- data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
- data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
- data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
- data/app/controllers/ragdoll/application_controller.rb +17 -0
- data/app/controllers/ragdoll/configuration_controller.rb +82 -0
- data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
- data/app/controllers/ragdoll/documents_controller.rb +460 -0
- data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
- data/app/controllers/ragdoll/jobs_controller.rb +116 -0
- data/app/controllers/ragdoll/search_controller.rb +368 -0
- data/app/jobs/application_job.rb +9 -0
- data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
- data/app/jobs/ragdoll/process_file_job.rb +166 -0
- data/app/services/ragdoll/worker_health_service.rb +111 -0
- data/app/views/layouts/ragdoll/application.html.erb +162 -0
- data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
- data/app/views/ragdoll/dashboard/index.html.erb +208 -0
- data/app/views/ragdoll/documents/edit.html.erb +91 -0
- data/app/views/ragdoll/documents/index.html.erb +302 -0
- data/app/views/ragdoll/documents/new.html.erb +1518 -0
- data/app/views/ragdoll/documents/show.html.erb +188 -0
- data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
- data/app/views/ragdoll/jobs/index.html.erb +669 -0
- data/app/views/ragdoll/jobs/show.html.erb +129 -0
- data/app/views/ragdoll/search/index.html.erb +324 -0
- data/config/cable.yml +12 -0
- data/config/routes.rb +56 -1
- data/lib/ragdoll/rails/engine.rb +32 -1
- data/lib/ragdoll/rails/version.rb +1 -1
- 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,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,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
|