glancer 1.0.0
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 +7 -0
- data/.github/workflows/ci.yml +96 -0
- data/.rubocop.yml +54 -0
- data/CHANGELOG.md +88 -0
- data/CLAUDE.md +115 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +354 -0
- data/app/assets/config/glancer_manifest.js +1 -0
- data/app/assets/javascripts/glancer/application.js +15 -0
- data/app/assets/javascripts/glancer/controllers/chat_controller.js +101 -0
- data/app/assets/javascripts/glancer/controllers/message_controller.js +1052 -0
- data/app/assets/javascripts/glancer/controllers/toast_controller.js +63 -0
- data/app/assets/stylesheets/glancer/application.css +350 -0
- data/app/assets/stylesheets/glancer/code-blocks.css +6 -0
- data/app/assets/stylesheets/glancer/list.css +31 -0
- data/app/assets/stylesheets/glancer/scrollbar.css +16 -0
- data/app/assets/stylesheets/glancer/table.css +97 -0
- data/app/controllers/glancer/application_controller.rb +33 -0
- data/app/controllers/glancer/chats_controller.rb +49 -0
- data/app/controllers/glancer/messages_controller.rb +144 -0
- data/app/controllers/glancer/schema_controller.rb +29 -0
- data/app/controllers/glancer/settings_controller.rb +23 -0
- data/app/helpers/glancer/application_helper.rb +17 -0
- data/app/jobs/glancer/application_job.rb +6 -0
- data/app/jobs/glancer/process_message_job.rb +38 -0
- data/app/models/glancer/audit.rb +12 -0
- data/app/models/glancer/chat.rb +8 -0
- data/app/models/glancer/code_version.rb +12 -0
- data/app/models/glancer/embedding.rb +6 -0
- data/app/models/glancer/message.rb +25 -0
- data/app/models/glancer/setting.rb +23 -0
- data/app/models/glancer/sql_version.rb +6 -0
- data/app/views/glancer/_data/_importmap.json.erb +7 -0
- data/app/views/glancer/chats/_chat_sidebar.html.erb +2 -0
- data/app/views/glancer/chats/_show.html.erb +52 -0
- data/app/views/glancer/chats/_sidebar_chat_list.html.erb +30 -0
- data/app/views/glancer/chats/index.html.erb +10 -0
- data/app/views/glancer/chats/show.html.erb +1 -0
- data/app/views/glancer/messages/_data_table.html.erb +268 -0
- data/app/views/glancer/messages/_execution_error.html.erb +26 -0
- data/app/views/glancer/messages/_form.html.erb +93 -0
- data/app/views/glancer/messages/_message.html.erb +206 -0
- data/app/views/glancer/messages/_message_info.html.erb +176 -0
- data/app/views/glancer/messages/_temp_form.html.erb +100 -0
- data/app/views/glancer/messages/create.turbo_stream.erb +25 -0
- data/app/views/glancer/schema/show.html.erb +123 -0
- data/app/views/glancer/settings/show.html.erb +306 -0
- data/app/views/glancer/shared/_icons.html.erb +126 -0
- data/app/views/layouts/glancer/application.html.erb +234 -0
- data/config/locales/glancer.en.yml +90 -0
- data/config/locales/glancer.es.yml +90 -0
- data/config/locales/glancer.pt-BR.yml +90 -0
- data/config/routes.rb +20 -0
- data/db/migrate/20250629212642_create_glancer_audits.rb +19 -0
- data/db/migrate/20250629212643_create_glancer_chats.rb +10 -0
- data/db/migrate/20250629212645_create_glancer_embeddings.rb +17 -0
- data/db/migrate/20250629212647_create_glancer_messages.rb +29 -0
- data/db/migrate/20260513204129_add_user_edited_sql_to_glancer_messages.rb +11 -0
- data/db/migrate/20260513210647_create_glancer_sql_versions.rb +18 -0
- data/db/migrate/20260513210648_add_message_id_to_glancer_audits.rb +8 -0
- data/db/migrate/20260513220000_create_glancer_settings.rb +12 -0
- data/db/migrate/20260514083509_add_llm_model_to_glancer_messages.rb +9 -0
- data/db/migrate/20260523120000_rename_code_columns_in_glancer_messages.rb +8 -0
- data/db/migrate/20260523120001_rename_code_column_in_glancer_audits.rb +7 -0
- data/db/migrate/20260523120002_add_code_type_to_glancer_tables.rb +10 -0
- data/db/migrate/20260523120003_rename_glancer_sql_versions_to_code_versions.rb +8 -0
- data/db/migrate/20260523130000_add_enriched_question_to_glancer_messages.rb +7 -0
- data/db/migrate/20260524100000_add_status_to_glancer_messages.rb +9 -0
- data/lib/generators/glancer/install/install_generator.rb +74 -0
- data/lib/generators/glancer/install/templates/glancer.rb +227 -0
- data/lib/generators/glancer/install/templates/llm_context.glancer.md +51 -0
- data/lib/glancer/async_runner.rb +50 -0
- data/lib/glancer/chart_analyzer.rb +230 -0
- data/lib/glancer/configuration.rb +372 -0
- data/lib/glancer/engine.rb +90 -0
- data/lib/glancer/indexer/context_indexer.rb +58 -0
- data/lib/glancer/indexer/model_indexer.rb +64 -0
- data/lib/glancer/indexer/schema_indexer.rb +171 -0
- data/lib/glancer/indexer.rb +50 -0
- data/lib/glancer/retriever.rb +114 -0
- data/lib/glancer/utils/logger.rb +83 -0
- data/lib/glancer/utils/markdown_helper.rb +56 -0
- data/lib/glancer/utils/result_formatter.rb +25 -0
- data/lib/glancer/utils/table_stats.rb +18 -0
- data/lib/glancer/utils/transaction.rb +59 -0
- data/lib/glancer/version.rb +5 -0
- data/lib/glancer/workflow/ar_executor.rb +104 -0
- data/lib/glancer/workflow/ar_extractor.rb +25 -0
- data/lib/glancer/workflow/ar_prompt_builder.rb +64 -0
- data/lib/glancer/workflow/ar_sanitizer.rb +88 -0
- data/lib/glancer/workflow/builder.rb +129 -0
- data/lib/glancer/workflow/cache.rb +55 -0
- data/lib/glancer/workflow/executor.rb +72 -0
- data/lib/glancer/workflow/llm.rb +123 -0
- data/lib/glancer/workflow/prompt_builder.rb +143 -0
- data/lib/glancer/workflow/query_enricher.rb +117 -0
- data/lib/glancer/workflow/sql_extractor.rb +42 -0
- data/lib/glancer/workflow/sql_sanitizer.rb +42 -0
- data/lib/glancer/workflow/sql_validator.rb +67 -0
- data/lib/glancer/workflow.rb +158 -0
- data/lib/glancer.rb +50 -0
- data/lib/tasks/glancer/tailwind.rake +8 -0
- data/lib/tasks/glancer.rake +99 -0
- data/spec/glancer_spec.rb +62 -0
- data/spec/lib/glancer/async_runner_spec.rb +133 -0
- data/spec/lib/glancer/chart_analyzer_spec.rb +296 -0
- data/spec/lib/glancer/configuration_spec.rb +858 -0
- data/spec/lib/glancer/engine_spec.rb +209 -0
- data/spec/lib/glancer/indexer/context_indexer_spec.rb +96 -0
- data/spec/lib/glancer/indexer/model_indexer_spec.rb +103 -0
- data/spec/lib/glancer/indexer/schema_indexer_spec.rb +382 -0
- data/spec/lib/glancer/indexer_spec.rb +95 -0
- data/spec/lib/glancer/retriever_spec.rb +179 -0
- data/spec/lib/glancer/utils/logger_spec.rb +85 -0
- data/spec/lib/glancer/utils/markdown_helper_spec.rb +92 -0
- data/spec/lib/glancer/utils/result_formatter_spec.rb +73 -0
- data/spec/lib/glancer/utils/table_stats_spec.rb +34 -0
- data/spec/lib/glancer/utils/transaction_spec.rb +73 -0
- data/spec/lib/glancer/workflow/ar_executor_spec.rb +155 -0
- data/spec/lib/glancer/workflow/ar_extractor_spec.rb +50 -0
- data/spec/lib/glancer/workflow/ar_prompt_builder_spec.rb +79 -0
- data/spec/lib/glancer/workflow/ar_sanitizer_spec.rb +175 -0
- data/spec/lib/glancer/workflow/builder_spec.rb +204 -0
- data/spec/lib/glancer/workflow/cache_spec.rb +142 -0
- data/spec/lib/glancer/workflow/executor_spec.rb +149 -0
- data/spec/lib/glancer/workflow/llm_spec.rb +124 -0
- data/spec/lib/glancer/workflow/prompt_builder_spec.rb +196 -0
- data/spec/lib/glancer/workflow/query_enricher_spec.rb +184 -0
- data/spec/lib/glancer/workflow/sql_extractor_spec.rb +82 -0
- data/spec/lib/glancer/workflow/sql_sanitizer_spec.rb +98 -0
- data/spec/lib/glancer/workflow/sql_validator_spec.rb +166 -0
- data/spec/lib/glancer/workflow_spec.rb +308 -0
- data/spec/models/glancer/audit_spec.rb +82 -0
- data/spec/models/glancer/chat_spec.rb +60 -0
- data/spec/models/glancer/code_version_spec.rb +71 -0
- data/spec/models/glancer/embedding_spec.rb +73 -0
- data/spec/models/glancer/message_spec.rb +144 -0
- data/spec/models/glancer/setting_spec.rb +88 -0
- data/spec/models/glancer/sql_version_spec.rb +4 -0
- data/spec/spec_helper.rb +128 -0
- data/spec/support/schema.rb +55 -0
- metadata +255 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glancer
|
|
4
|
+
module Workflow
|
|
5
|
+
def self.run(chat_id, question, cache: true, &status_callback)
|
|
6
|
+
Glancer::Utils::Logger.info("Workflow",
|
|
7
|
+
"Running workflow for chat_id: #{chat_id.inspect}, " \
|
|
8
|
+
"question: #{question.inspect}, " \
|
|
9
|
+
"mode: #{Glancer.configuration.query_mode}, cache: #{cache}")
|
|
10
|
+
|
|
11
|
+
if cache && (cached = Workflow::Cache.fetch(question))
|
|
12
|
+
Glancer::Utils::Logger.info("Workflow", "Using cached result for question: #{question.inspect}")
|
|
13
|
+
return cached.merge(from_cache: true)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
chat = Glancer::Chat.find(chat_id)
|
|
17
|
+
history = chat.messages.order(created_at: :desc).limit(Glancer.configuration.history_limit).reverse
|
|
18
|
+
|
|
19
|
+
enrichment_enabled = Glancer.configuration.query_enrichment_enabled
|
|
20
|
+
adapter = Glancer.configuration.query_mode
|
|
21
|
+
|
|
22
|
+
status_callback&.call(:enriching) if enrichment_enabled
|
|
23
|
+
effective_question = enrich_question(question, history, adapter: adapter)
|
|
24
|
+
|
|
25
|
+
status_callback&.call(:retrieving_context)
|
|
26
|
+
embeddings = Retriever.search(effective_question)
|
|
27
|
+
Glancer::Utils::Logger.debug("Workflow", "Retrieved #{embeddings.size} relevant document(s) for context")
|
|
28
|
+
|
|
29
|
+
result = if adapter == :activerecord
|
|
30
|
+
run_activerecord(question, effective_question, embeddings, history, status_callback)
|
|
31
|
+
else
|
|
32
|
+
run_sql(question, effective_question, embeddings, history, status_callback)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Always persist the enriched question so the info panel can show it on every message
|
|
36
|
+
result[:enriched_question] = effective_question if enrichment_enabled
|
|
37
|
+
|
|
38
|
+
if cache && result[:successful]
|
|
39
|
+
Workflow::Cache.write(question, result)
|
|
40
|
+
Glancer::Utils::Logger.info("Workflow", "Result cached for question: #{question.inspect}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
result
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
Glancer::Utils::Logger.error("Workflow", "Workflow execution failed: #{e.class} - #{e.message}")
|
|
46
|
+
Glancer::Utils::Logger.debug("Workflow", "Backtrace:\n#{e.backtrace.join("\n")}")
|
|
47
|
+
raise Glancer::Error, "Workflow failed: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.enrich_question(question, history = [], adapter: nil)
|
|
51
|
+
return question unless Glancer.configuration.query_enrichment_enabled
|
|
52
|
+
|
|
53
|
+
Glancer::Utils::Logger.info("Workflow", "Enriching question before retrieval...")
|
|
54
|
+
table_names = Glancer::Workflow::QueryEnricher.known_table_names
|
|
55
|
+
enriched = Glancer::Workflow::QueryEnricher.enrich(question, table_names, history: history,
|
|
56
|
+
adapter: adapter)
|
|
57
|
+
Glancer::Utils::Logger.info("Workflow", "Enriched question: #{enriched.inspect}")
|
|
58
|
+
enriched
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
Glancer::Utils::Logger.warn("Workflow", "Question enrichment failed, using original: #{e.message}")
|
|
61
|
+
question
|
|
62
|
+
end
|
|
63
|
+
private_class_method :enrich_question
|
|
64
|
+
|
|
65
|
+
def self.run_sql(question, effective_question, embeddings, history, status_callback = nil)
|
|
66
|
+
Glancer::Utils::Logger.info("Workflow", "Running SQL code generation mode...")
|
|
67
|
+
|
|
68
|
+
status_callback&.call(:generating_code)
|
|
69
|
+
sql = Workflow::Builder.build_sql(effective_question, embeddings, history: history)
|
|
70
|
+
Glancer::Utils::Logger.debug("Workflow", "Generated raw SQL:\n#{sql}")
|
|
71
|
+
|
|
72
|
+
sql = Workflow::SQLExtractor.extract(sql)
|
|
73
|
+
Glancer::Utils::Logger.debug("Workflow", "Extracted SQL:\n#{sql}")
|
|
74
|
+
|
|
75
|
+
Workflow::SQLSanitizer.ensure_safe!(sql)
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
status_callback&.call(:validating)
|
|
79
|
+
Workflow::SQLValidator.validate_tables_exist!(sql)
|
|
80
|
+
rescue Glancer::Error => e
|
|
81
|
+
Glancer::Utils::Logger.warn("Workflow", "Table validation failed: #{e.message}. Returning friendly response.")
|
|
82
|
+
explanation = Glancer::Workflow::LLM.explain_missing_tables(question, e.message)
|
|
83
|
+
return {
|
|
84
|
+
question: question,
|
|
85
|
+
content: explanation,
|
|
86
|
+
code: sql,
|
|
87
|
+
code_type: "sql",
|
|
88
|
+
successful: false
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
status_callback&.call(:executing)
|
|
93
|
+
raw_data = Workflow::Executor.execute(sql, original_question: question)
|
|
94
|
+
|
|
95
|
+
if raw_data.is_a?(Hash) && raw_data[:error]
|
|
96
|
+
explanation = Glancer::Workflow::LLM.explain_error(question, raw_data[:message], raw_data[:last_code])
|
|
97
|
+
return {
|
|
98
|
+
question: question,
|
|
99
|
+
content: explanation,
|
|
100
|
+
code: raw_data[:last_code],
|
|
101
|
+
code_type: "sql",
|
|
102
|
+
successful: false
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
status_callback&.call(:humanizing)
|
|
107
|
+
{
|
|
108
|
+
question: question,
|
|
109
|
+
content: Glancer::Workflow::LLM.humanized_response(question, raw_data, sql),
|
|
110
|
+
code: sql,
|
|
111
|
+
code_type: "sql",
|
|
112
|
+
successful: true,
|
|
113
|
+
sources: embeddings.map { |e| { id: e.id, type: e.source_type, path: e.source_path, score: e.score } }
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
private_class_method :run_sql
|
|
117
|
+
|
|
118
|
+
def self.run_activerecord(question, effective_question, embeddings, history, status_callback = nil)
|
|
119
|
+
Glancer::Utils::Logger.info("Workflow", "Running ActiveRecord mode...")
|
|
120
|
+
|
|
121
|
+
status_callback&.call(:generating_code)
|
|
122
|
+
code = Workflow::Builder.build_ar_code(effective_question, embeddings, history: history)
|
|
123
|
+
Glancer::Utils::Logger.debug("Workflow", "Generated raw AR code:\n#{code}")
|
|
124
|
+
|
|
125
|
+
code = Workflow::ARExtractor.extract(code)
|
|
126
|
+
Glancer::Utils::Logger.debug("Workflow", "Extracted AR code:\n#{code}")
|
|
127
|
+
|
|
128
|
+
Workflow::ARSanitizer.ensure_safe!(code)
|
|
129
|
+
|
|
130
|
+
status_callback&.call(:executing)
|
|
131
|
+
raw_data = Workflow::ARExecutor.execute(code, original_question: question)
|
|
132
|
+
|
|
133
|
+
if raw_data.is_a?(Hash) && raw_data[:error]
|
|
134
|
+
explanation = Glancer::Workflow::LLM.explain_error(
|
|
135
|
+
question, raw_data[:message], raw_data[:last_code], mode: :activerecord
|
|
136
|
+
)
|
|
137
|
+
return {
|
|
138
|
+
question: question,
|
|
139
|
+
content: explanation,
|
|
140
|
+
code: raw_data[:last_code],
|
|
141
|
+
code_type: "activerecord",
|
|
142
|
+
successful: false
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
status_callback&.call(:humanizing)
|
|
147
|
+
{
|
|
148
|
+
question: question,
|
|
149
|
+
content: Glancer::Workflow::LLM.humanized_response(question, raw_data, code, mode: :activerecord),
|
|
150
|
+
code: code,
|
|
151
|
+
code_type: "activerecord",
|
|
152
|
+
successful: true,
|
|
153
|
+
sources: embeddings.map { |e| { id: e.id, type: e.source_type, path: e.source_path, score: e.score } }
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
private_class_method :run_activerecord
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/glancer.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "glancer/version"
|
|
4
|
+
require "glancer/configuration"
|
|
5
|
+
|
|
6
|
+
module Glancer
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :configuration
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.configure
|
|
14
|
+
self.configuration ||= Configuration.new
|
|
15
|
+
yield(configuration)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require "glancer/utils/logger" # Glancer::Utils::Logger
|
|
20
|
+
require "glancer/utils/markdown_helper" # Glancer::Utils::MarkdownHelper
|
|
21
|
+
require "glancer/utils/result_formatter" # Glancer::Utils::ResultFormatter
|
|
22
|
+
require "glancer/utils/table_stats" # Glancer::Utils::TableStats
|
|
23
|
+
require "glancer/utils/transaction" # Glancer::Utils::Transaction
|
|
24
|
+
|
|
25
|
+
require "glancer/engine" # Glancer::Engine
|
|
26
|
+
|
|
27
|
+
require "glancer/indexer" # Glancer::Indexer
|
|
28
|
+
require "glancer/indexer/context_indexer" # Glancer::Indexer::ContextIndexer
|
|
29
|
+
require "glancer/indexer/model_indexer" # Glancer::Indexer::ModelIndexer
|
|
30
|
+
require "glancer/indexer/schema_indexer" # Glancer::Indexer::SchemaIndexer
|
|
31
|
+
|
|
32
|
+
require "glancer/retriever" # Glancer::Retriever
|
|
33
|
+
|
|
34
|
+
require "glancer/workflow" # Glancer::Workflow
|
|
35
|
+
require "glancer/workflow/builder" # Glancer::Workflow::Builder
|
|
36
|
+
require "glancer/workflow/cache" # Glancer::Workflow::Cache
|
|
37
|
+
require "glancer/workflow/executor" # Glancer::Workflow::Executor
|
|
38
|
+
require "glancer/workflow/prompt_builder" # Glancer::Workflow::PromptBuilder
|
|
39
|
+
require "glancer/workflow/sql_extractor" # Glancer::Workflow::SQLExtractor
|
|
40
|
+
require "glancer/workflow/sql_sanitizer" # Glancer::Workflow::SQLSanitizer
|
|
41
|
+
require "glancer/workflow/sql_validator" # Glancer::Workflow::SQLValidator
|
|
42
|
+
require "glancer/workflow/llm" # Glancer::Workflow::LLM
|
|
43
|
+
require "glancer/workflow/ar_prompt_builder" # Glancer::Workflow::ARPromptBuilder
|
|
44
|
+
require "glancer/workflow/ar_extractor" # Glancer::Workflow::ARExtractor
|
|
45
|
+
require "glancer/workflow/ar_sanitizer" # Glancer::Workflow::ARSanitizer
|
|
46
|
+
require "glancer/workflow/ar_executor" # Glancer::Workflow::ARExecutor
|
|
47
|
+
require "glancer/workflow/query_enricher" # Glancer::Workflow::QueryEnricher
|
|
48
|
+
|
|
49
|
+
require "glancer/chart_analyzer" # Glancer::ChartAnalyzer
|
|
50
|
+
require "glancer/async_runner" # Glancer::AsyncRunner
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :glancer do
|
|
4
|
+
desc "Show current Glancer and RubyLLM versions"
|
|
5
|
+
task :version do
|
|
6
|
+
puts "\n\e[36m✱ glancer@v#{Glancer::VERSION}\e[0m\n"
|
|
7
|
+
puts
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
namespace :index do
|
|
11
|
+
desc "Rebuild all Glancer indexes"
|
|
12
|
+
task all: :environment do
|
|
13
|
+
existing = Glancer::Embedding.count
|
|
14
|
+
if existing.positive?
|
|
15
|
+
puts "\n\e[33m✱ There are currently #{existing} embeddings stored.\e[0m"
|
|
16
|
+
puts "\e[31m└→ This operation will delete all existing embeddings and reindex everything.\e[0m"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Glancer::Utils::Transaction.make do
|
|
20
|
+
Glancer::Embedding.where(source_type: %w[schema models context]).delete_all
|
|
21
|
+
|
|
22
|
+
Glancer::Utils::Logger.with_debug_logs do
|
|
23
|
+
Glancer::Indexer.rebuild_all!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
puts "\e[32m✔ All indexes rebuilt!\e[0m"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "Rebuild schema index only"
|
|
31
|
+
task schema: :environment do
|
|
32
|
+
if confirm_rebuild?(:schema)
|
|
33
|
+
Glancer::Utils::Transaction.make do
|
|
34
|
+
Glancer::Embedding.where(source_type: "schema").delete_all
|
|
35
|
+
|
|
36
|
+
Glancer::Utils::Logger.with_debug_logs do
|
|
37
|
+
chunks = Glancer::Indexer::SchemaIndexer.index!
|
|
38
|
+
Glancer::Retriever.store_documents(chunks)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
puts "\e[32m✔ Schema index rebuilt!\e[0m"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "Rebuild models index only"
|
|
47
|
+
task models: :environment do
|
|
48
|
+
if confirm_rebuild?(:models)
|
|
49
|
+
Glancer::Utils::Transaction.make do
|
|
50
|
+
Glancer::Embedding.where(source_type: "models").delete_all
|
|
51
|
+
|
|
52
|
+
Glancer::Utils::Logger.with_debug_logs do
|
|
53
|
+
chunks = Glancer::Indexer::ModelIndexer.index!
|
|
54
|
+
Glancer::Retriever.store_documents(chunks)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
puts "\e[32m✔ Models index rebuilt!\e[0m"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "Rebuild context index only"
|
|
63
|
+
task context: :environment do
|
|
64
|
+
if confirm_rebuild?(:context)
|
|
65
|
+
Glancer::Utils::Transaction.make do
|
|
66
|
+
Glancer::Embedding.where(source_type: "context").delete_all
|
|
67
|
+
|
|
68
|
+
Glancer::Utils::Logger.with_debug_logs do
|
|
69
|
+
chunks = Glancer::Indexer::ContextIndexer.index!
|
|
70
|
+
Glancer::Retriever.store_documents(chunks)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts "\e[32m✔ Context index rebuilt!\e[0m"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def confirm_rebuild?(type)
|
|
80
|
+
existing = Glancer::Embedding.where(source_type: type.to_s)
|
|
81
|
+
if existing.exists?
|
|
82
|
+
last = existing.order(created_at: :desc).first.created_at
|
|
83
|
+
puts "\n\e[33m✱ Existing #{existing.count} '#{type}' embeddings found. Last updated: #{last.strftime("%Y-%m-%d %H:%M:%S")}.\e[0m"
|
|
84
|
+
print "\e[31m└→ Do you want to delete and reindex? [y/N]: \e[0m"
|
|
85
|
+
input = $stdin.gets.strip.upcase
|
|
86
|
+
|
|
87
|
+
puts "\n\e[31m✖ Operation cancelled.\e[0m\n" unless input == "Y"
|
|
88
|
+
|
|
89
|
+
return input == "Y"
|
|
90
|
+
end
|
|
91
|
+
true
|
|
92
|
+
rescue Interrupt
|
|
93
|
+
puts "\n\n\e[31m✖ Operation cancelled\e[0m\n"
|
|
94
|
+
false
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
puts "\n\n\e[31m✖ Error: #{e.message}\e[0m\n"
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer do
|
|
6
|
+
describe "::VERSION" do
|
|
7
|
+
it "is defined and non-nil" do
|
|
8
|
+
expect(Glancer::VERSION).not_to be_nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "is a String" do
|
|
12
|
+
expect(Glancer::VERSION).to be_a(String)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "follows semantic versioning format" do
|
|
16
|
+
expect(Glancer::VERSION).to match(/\A\d+\.\d+\.\d+/)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "::Error" do
|
|
21
|
+
it "is a subclass of StandardError" do
|
|
22
|
+
expect(Glancer::Error.ancestors).to include(StandardError)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "can be raised and rescued" do
|
|
26
|
+
expect { raise Glancer::Error, "test" }.to raise_error(Glancer::Error, "test")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe ".configure" do
|
|
31
|
+
it "yields the configuration object" do
|
|
32
|
+
Glancer.configure do |c|
|
|
33
|
+
expect(c).to be_a(Glancer::Configuration)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "persists changes made inside the block" do
|
|
38
|
+
Glancer.configure { |c| c.k = 42 }
|
|
39
|
+
expect(Glancer.configuration.k).to eq(42)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "re-uses the existing configuration on repeated calls" do
|
|
43
|
+
Glancer.configure { |c| c.k = 7 }
|
|
44
|
+
Glancer.configure { |c| c.min_score = 0.5 }
|
|
45
|
+
expect(Glancer.configuration.k).to eq(7)
|
|
46
|
+
expect(Glancer.configuration.min_score).to eq(0.5)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe ".configuration" do
|
|
51
|
+
it "is accessible after configure" do
|
|
52
|
+
expect(Glancer.configuration).to be_a(Glancer::Configuration)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "can be replaced by assignment" do
|
|
56
|
+
new_config = Glancer::Configuration.new
|
|
57
|
+
new_config.k = 99
|
|
58
|
+
Glancer.configuration = new_config
|
|
59
|
+
expect(Glancer.configuration.k).to eq(99)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::AsyncRunner do
|
|
6
|
+
let(:chat) { Glancer::Chat.create!(title: "Test") }
|
|
7
|
+
let(:message) do
|
|
8
|
+
Glancer::Message.create!(
|
|
9
|
+
chat: chat, role: :assistant, content: "", code_type: "sql", status: :processing
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
let(:question) { "How many users?" }
|
|
13
|
+
|
|
14
|
+
let(:workflow_result) do
|
|
15
|
+
{
|
|
16
|
+
content: "There are 5 users.",
|
|
17
|
+
code: "SELECT COUNT(*) FROM users",
|
|
18
|
+
code_type: "sql",
|
|
19
|
+
successful: true,
|
|
20
|
+
enriched_question: nil
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
before do
|
|
25
|
+
allow(Glancer::Workflow).to receive(:run).and_return(workflow_result)
|
|
26
|
+
allow(Glancer::Workflow::LLM).to receive(:generate_title).and_return("User count")
|
|
27
|
+
# Stub resolved_chat_provider/model so the model string builds without config errors
|
|
28
|
+
allow(Glancer.configuration).to receive(:resolved_chat_provider).and_return("gemini")
|
|
29
|
+
allow(Glancer.configuration).to receive(:resolved_chat_model).and_return("gemini-2.0-flash")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ── .run ──────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
describe ".run" do
|
|
35
|
+
it "updates the message status to :processing then :complete" do
|
|
36
|
+
described_class.run(message.id, question)
|
|
37
|
+
expect(message.reload.status).to eq("complete")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "stores the workflow content on the message" do
|
|
41
|
+
described_class.run(message.id, question)
|
|
42
|
+
expect(message.reload.content).to eq("There are 5 users.")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "stores the generated code on the message" do
|
|
46
|
+
described_class.run(message.id, question)
|
|
47
|
+
expect(message.reload.code).to eq("SELECT COUNT(*) FROM users")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "marks the message as successful" do
|
|
51
|
+
described_class.run(message.id, question)
|
|
52
|
+
expect(message.reload.successful).to be(true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "creates a code_version record for the generated code" do
|
|
56
|
+
expect { described_class.run(message.id, question) }
|
|
57
|
+
.to change(Glancer::CodeVersion, :count).by(1)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "does not create a code_version when the result has no code" do
|
|
61
|
+
allow(Glancer::Workflow).to receive(:run).and_return(workflow_result.merge(code: nil))
|
|
62
|
+
expect { described_class.run(message.id, question) }
|
|
63
|
+
.not_to change(Glancer::CodeVersion, :count)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "updates the chat title on the first user message" do
|
|
67
|
+
chat.messages.create!(role: :user, content: question)
|
|
68
|
+
described_class.run(message.id, question)
|
|
69
|
+
expect(chat.reload.title).to eq("User count")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "does not update the chat title when multiple user messages exist" do
|
|
73
|
+
2.times { chat.messages.create!(role: :user, content: question) }
|
|
74
|
+
expect(Glancer::Workflow::LLM).not_to receive(:generate_title)
|
|
75
|
+
described_class.run(message.id, question)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context "when the workflow raises" do
|
|
79
|
+
before { allow(Glancer::Workflow).to receive(:run).and_raise(StandardError, "LLM down") }
|
|
80
|
+
|
|
81
|
+
it "marks the message as failed" do
|
|
82
|
+
described_class.run(message.id, question)
|
|
83
|
+
expect(message.reload.status).to eq("failed")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "stores the error message as content" do
|
|
87
|
+
described_class.run(message.id, question)
|
|
88
|
+
expect(message.reload.content).to eq("LLM down")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "marks the message as not successful" do
|
|
92
|
+
described_class.run(message.id, question)
|
|
93
|
+
expect(message.reload.successful).to be(false)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
context "when both workflow and the rescue update raise" do
|
|
98
|
+
before do
|
|
99
|
+
allow(Glancer::Workflow).to receive(:run).and_raise(StandardError, "boom")
|
|
100
|
+
allow_any_instance_of(Glancer::Message).to receive(:update!).and_call_original
|
|
101
|
+
allow_any_instance_of(Glancer::Message).to receive(:update!)
|
|
102
|
+
.with(hash_including(status: :failed))
|
|
103
|
+
.and_raise(StandardError, "db gone")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "does not propagate the secondary error" do
|
|
107
|
+
expect { described_class.run(message.id, question) }.not_to raise_error
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ── .call ─────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
describe ".call" do
|
|
115
|
+
it "returns a Thread" do
|
|
116
|
+
t = described_class.call(message.id, question)
|
|
117
|
+
t.join
|
|
118
|
+
expect(t).to be_a(Thread)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "executes .run inside the thread" do
|
|
122
|
+
expect(described_class).to receive(:run).with(message.id, question)
|
|
123
|
+
described_class.call(message.id, question).join
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "does not raise when the thread encounters an error outside with_connection" do
|
|
127
|
+
msg_id = message.id # force lazy let evaluation before stubbing connection_pool
|
|
128
|
+
allow(ActiveRecord::Base).to receive(:connection_pool)
|
|
129
|
+
.and_raise(StandardError, "pool error")
|
|
130
|
+
expect { described_class.call(msg_id, question).join }.not_to raise_error
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|