sql-chatbot-rails 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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +20 -0
  4. data/app/controllers/sql_chatbot/chatbot_controller.rb +158 -0
  5. data/config/routes.rb +11 -0
  6. data/lib/generators/sql_chatbot/install_generator.rb +25 -0
  7. data/lib/generators/sql_chatbot/templates/initializer.rb +22 -0
  8. data/lib/sql_chatbot/auth/cors.rb +35 -0
  9. data/lib/sql_chatbot/auth/jwt.rb +34 -0
  10. data/lib/sql_chatbot/configuration.rb +58 -0
  11. data/lib/sql_chatbot/engine.rb +23 -0
  12. data/lib/sql_chatbot/grammar/count_renderer.rb +113 -0
  13. data/lib/sql_chatbot/grammar/entity_candidates.rb +210 -0
  14. data/lib/sql_chatbot/grammar/intent_extractor.rb +191 -0
  15. data/lib/sql_chatbot/grammar/list_renderer.rb +50 -0
  16. data/lib/sql_chatbot/grammar/miss_logger.rb +17 -0
  17. data/lib/sql_chatbot/grammar/modifiers.rb +145 -0
  18. data/lib/sql_chatbot/grammar/primitives.rb +69 -0
  19. data/lib/sql_chatbot/grammar/programmatic_renderer.rb +258 -0
  20. data/lib/sql_chatbot/grammar/registry.rb +66 -0
  21. data/lib/sql_chatbot/grammar/sanity_check.rb +37 -0
  22. data/lib/sql_chatbot/grammar/template_compiler.rb +179 -0
  23. data/lib/sql_chatbot/llm/client.rb +87 -0
  24. data/lib/sql_chatbot/prompts/answer.rb +157 -0
  25. data/lib/sql_chatbot/prompts/classify.rb +59 -0
  26. data/lib/sql_chatbot/prompts/generate_sql.rb +88 -0
  27. data/lib/sql_chatbot/services/code_indexer.rb +337 -0
  28. data/lib/sql_chatbot/services/grammar_pipeline.rb +45 -0
  29. data/lib/sql_chatbot/services/model_introspector.rb +152 -0
  30. data/lib/sql_chatbot/services/orchestrator.rb +635 -0
  31. data/lib/sql_chatbot/services/registry_builder.rb +385 -0
  32. data/lib/sql_chatbot/services/route_introspector.rb +118 -0
  33. data/lib/sql_chatbot/services/schema_service.rb +884 -0
  34. data/lib/sql_chatbot/services/sql_executor.rb +81 -0
  35. data/lib/sql_chatbot/version.rb +5 -0
  36. data/lib/sql_chatbot_rails.rb +91 -0
  37. data/vendor/assets/widget.js +53 -0
  38. metadata +180 -0
@@ -0,0 +1,81 @@
1
+ module SqlChatbot
2
+ module Services
3
+ module SqlExecutor
4
+ KEYWORD_BLOCKLIST = %w[INSERT UPDATE DELETE DROP ALTER CREATE GRANT TRUNCATE EXECUTE REVOKE COPY INTO].freeze
5
+ FUNCTION_BLOCKLIST = %w[pg_read_file pg_read_binary_file dblink pg_terminate_backend lo_import lo_export pg_sleep set_config current_setting].freeze
6
+ CATALOG_BLOCKLIST = %w[pg_shadow pg_roles pg_authid pg_user information_schema].freeze
7
+ AGGREGATE_PATTERN = /\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i
8
+
9
+ def self.validate_sql(sql)
10
+ trimmed = sql.gsub(/^\s+/, "")
11
+ trimmed = trimmed.gsub(/--[^\n]*/, "").gsub(/\/\*[\s\S]*?\*\//, "")
12
+ trimmed = trimmed.strip
13
+
14
+ # Fix missing space after SELECT
15
+ trimmed = trimmed.sub(/^SELECT(?=[A-Z])/i, "SELECT ")
16
+
17
+ # Single statement check
18
+ parts = trimmed.split(";").select { |p| p.strip.length > 0 }
19
+ if parts.length > 1
20
+ return { valid: false, reason: "Only a single statement is allowed" }
21
+ end
22
+
23
+ working_sql = trimmed.sub(/;\s*$/, "").strip
24
+
25
+ # Must start with SELECT
26
+ unless working_sql.match?(/^SELECT\b/i)
27
+ return { valid: false, reason: "Only SELECT queries are allowed" }
28
+ end
29
+
30
+ # Keyword blocklist
31
+ KEYWORD_BLOCKLIST.each do |keyword|
32
+ if working_sql.match?(/\b#{keyword}\b/i)
33
+ return { valid: false, reason: "Blocked keyword: #{keyword}" }
34
+ end
35
+ end
36
+
37
+ # Function blocklist
38
+ FUNCTION_BLOCKLIST.each do |fn|
39
+ if working_sql.match?(/\b#{fn}\b/i)
40
+ return { valid: false, reason: "Blocked function: #{fn}" }
41
+ end
42
+ end
43
+
44
+ # Catalog blocklist
45
+ CATALOG_BLOCKLIST.each do |catalog|
46
+ if working_sql.match?(/\b#{catalog}\b/i)
47
+ return { valid: false, reason: "Blocked system catalog: #{catalog}" }
48
+ end
49
+ end
50
+
51
+ # Auto-add LIMIT
52
+ has_limit = working_sql.match?(/\bLIMIT\b/i)
53
+ is_aggregate = AGGREGATE_PATTERN.match?(working_sql)
54
+ working_sql += " LIMIT 500" if !has_limit && !is_aggregate
55
+
56
+ { valid: true, sql: working_sql }
57
+ end
58
+
59
+ def self.execute_sql(sql)
60
+ connection = ActiveRecord::Base.connection
61
+ connection.execute("SET statement_timeout = '10s'")
62
+ connection.execute("BEGIN")
63
+ connection.execute("SET TRANSACTION READ ONLY")
64
+
65
+ result = connection.execute(sql)
66
+ rows = result.to_a
67
+ columns = rows.first&.keys || []
68
+
69
+ connection.execute("COMMIT")
70
+
71
+ { columns: columns, rows: rows, row_count: rows.length }
72
+ rescue => e
73
+ connection.execute("ROLLBACK") rescue nil
74
+ raise e
75
+ ensure
76
+ # Reset statement_timeout on shared AR connection
77
+ connection.execute("SET statement_timeout = '0'") rescue nil
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlChatbot
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sql_chatbot/version"
4
+ require "sql_chatbot/configuration"
5
+ require "sql_chatbot/llm/client"
6
+ require "sql_chatbot/prompts/classify"
7
+ require "sql_chatbot/prompts/generate_sql"
8
+ require "sql_chatbot/prompts/answer"
9
+ require "sql_chatbot/services/sql_executor"
10
+ require "sql_chatbot/services/schema_service"
11
+ require "sql_chatbot/services/code_indexer"
12
+ require "sql_chatbot/services/orchestrator"
13
+ require "sql_chatbot/services/model_introspector"
14
+ require "sql_chatbot/services/route_introspector"
15
+ require "sql_chatbot/auth/jwt"
16
+ require "sql_chatbot/auth/cors"
17
+ require "sql_chatbot/engine" if defined?(Rails)
18
+
19
+ module SqlChatbot
20
+ class << self
21
+ attr_accessor :config, :schema_service, :code_indexer, :orchestrator, :registry
22
+
23
+ def configure
24
+ self.config ||= Configuration.new
25
+ yield(config) if block_given?
26
+ end
27
+
28
+ def reset!
29
+ self.config = Configuration.new
30
+ @schema_service = nil
31
+ @code_indexer = nil
32
+ @orchestrator = nil
33
+ @registry = nil
34
+ @initialized = false
35
+ @init_mutex = Mutex.new
36
+ end
37
+
38
+ def ensure_initialized!
39
+ return if @initialized
40
+ @init_mutex ||= Mutex.new
41
+ @init_mutex.synchronize do
42
+ return if @initialized
43
+ cfg = config || Configuration.new
44
+
45
+ @schema_service = Services::SchemaService.new
46
+ @schema_service.discover
47
+
48
+ # Introspect Rails models for enums, non-standard FKs, and soft delete gems
49
+ introspector = Services::ModelIntrospector.new
50
+ introspection = introspector.introspect
51
+
52
+ # Apply soft delete annotations conditionally (gem-based vs enum-based)
53
+ @schema_service.apply_soft_delete_annotations(
54
+ soft_delete_tables: introspection.soft_delete_tables,
55
+ enum_soft_delete_tables: introspection.enum_soft_delete_tables,
56
+ )
57
+
58
+ # Inject model annotations (enums, FKs)
59
+ @schema_service.append_model_annotations(introspection.annotations)
60
+
61
+ # Move lookup values from referenced tables to FK columns
62
+ @schema_service.relocate_lookup_annotations
63
+
64
+ # Introspect Rails routes for navigation context
65
+ route_introspector = Services::RouteIntrospector.new
66
+ route_data = route_introspector.introspect
67
+
68
+ @code_indexer = Services::CodeIndexer.new
69
+ @code_indexer.index(cfg.code_paths)
70
+
71
+ llm_client = LLM::Client.new(
72
+ api_key: cfg.resolved_api_key,
73
+ base_url: cfg.resolved_base_url,
74
+ model: cfg.resolved_model,
75
+ )
76
+
77
+ @orchestrator = Services::Orchestrator.new(
78
+ llm_client: llm_client,
79
+ schema_service: @schema_service,
80
+ code_indexer: @code_indexer,
81
+ route_introspector_data: route_data,
82
+ )
83
+
84
+ @initialized = true
85
+ end
86
+ rescue => e
87
+ @initialized = false
88
+ raise e
89
+ end
90
+ end
91
+ end