blazer-ai 0.1.0 → 0.2.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 +4 -4
- data/app/controllers/blazer/ai/queries_controller.rb +8 -8
- data/app/overrides/blazer/queries/_form.html.erb +1 -1
- data/app/views/blazer/ai/queries/_generate_sql_button.html.erb +1 -1
- data/lib/blazer/ai/configuration.rb +2 -2
- data/lib/blazer/ai/engine.rb +6 -3
- data/lib/blazer/ai/middleware.rb +219 -0
- data/lib/blazer/ai/railtie.rb +5 -19
- data/lib/blazer/ai/schema_cache.rb +1 -1
- data/lib/blazer/ai/sql_generator.rb +3 -3
- data/lib/blazer/ai/url_helper.rb +6 -1
- data/lib/blazer/ai/version.rb +1 -1
- data/lib/generators/blazer_ai/install_generator.rb +18 -1
- data/lib/generators/blazer_ai/templates/blazer_ai.rb +0 -4
- data/lib/generators/blazer_ai/templates/ruby_llm.rb +3 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae37ca646c57731bc74109540de81eba74164b71f4f057f98060c54127167848
|
|
4
|
+
data.tar.gz: 649ebd6b0667fa5a41035dabf3b94e8d077c9627c491f05e01642f67e8bf53fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7d1f3812b5e8b159e6dab2782134587f8c23b3faf4797a95fbcff4f3e3af330bee39b8dfaec80a57d6e4eea00d8de9b896bc7c5c49dbc99da17d0293589f0fb
|
|
7
|
+
data.tar.gz: dc6ab6edd63c9f76dc498b2ebf55108b108729052ad136f291c722b5f248d95268f53e5878fdb4a23a52f0dd77d2fe4692266be999df4d2cc1d1b270c8d2862a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Handles AI-powered SQL generation requests.
|
|
2
2
|
# Validates rate limits, sanitizes input, and returns generated SQL.
|
|
3
3
|
class Blazer::Ai::QueriesController < Blazer::Ai::ApplicationController
|
|
4
|
-
before_action :ensure_ai_enabled, only: [
|
|
4
|
+
before_action :ensure_ai_enabled, only: [:create]
|
|
5
5
|
|
|
6
6
|
def create
|
|
7
7
|
rate_limiter.check_and_track!(identifier: current_identifier)
|
|
@@ -12,17 +12,17 @@ class Blazer::Ai::QueriesController < Blazer::Ai::ApplicationController
|
|
|
12
12
|
sql = generator.call
|
|
13
13
|
log_generation(query_params, sql)
|
|
14
14
|
|
|
15
|
-
render json: {
|
|
15
|
+
render json: {sql: sql}
|
|
16
16
|
rescue SqlValidator::ValidationError
|
|
17
|
-
render json: {
|
|
17
|
+
render json: {error: "Generated SQL failed safety validation"}, status: :unprocessable_entity
|
|
18
18
|
rescue SqlGenerator::GenerationError => e
|
|
19
|
-
render json: {
|
|
19
|
+
render json: {error: e.message}, status: :unprocessable_entity
|
|
20
20
|
rescue RateLimiter::RateLimitExceeded => e
|
|
21
|
-
render json: {
|
|
22
|
-
rescue
|
|
21
|
+
render json: {error: e.message, retry_after: e.retry_after}, status: :too_many_requests
|
|
22
|
+
rescue => e
|
|
23
23
|
Rails.logger.error("[BlazerAI] Generation error: #{e.class}: #{e.message}")
|
|
24
24
|
Rails.logger.error(e.backtrace.first(10).join("\n")) if e.backtrace
|
|
25
|
-
render json: {
|
|
25
|
+
render json: {error: "An error occurred while generating SQL. Please try again."}, status: :unprocessable_entity
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
private
|
|
@@ -58,7 +58,7 @@ class Blazer::Ai::QueriesController < Blazer::Ai::ApplicationController
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def ensure_ai_enabled
|
|
61
|
-
render json: {
|
|
61
|
+
render json: {error: "AI features are disabled"}, status: :forbidden unless Blazer::Ai.configuration.enabled?
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def rate_limiter
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Configuration for Blazer AI.
|
|
2
2
|
class Blazer::Ai::Configuration
|
|
3
3
|
attr_accessor :enabled, :default_model, :temperature, :rate_limit_per_minute,
|
|
4
|
-
|
|
4
|
+
:schema_cache_ttl, :max_prompt_length, :max_sql_length
|
|
5
5
|
|
|
6
6
|
def initialize
|
|
7
7
|
@enabled = true
|
|
8
|
-
@default_model = "gpt-
|
|
8
|
+
@default_model = "gpt-4o-mini"
|
|
9
9
|
@temperature = 0.2
|
|
10
10
|
@rate_limit_per_minute = 20
|
|
11
11
|
@schema_cache_ttl = 12.hours
|
data/lib/blazer/ai/engine.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Rails engine for Blazer AI.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
module Blazer
|
|
3
|
+
module Ai
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Blazer::Ai
|
|
6
|
+
end
|
|
7
|
+
end
|
|
5
8
|
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Rack middleware that injects the AI button into Blazer query pages
|
|
2
|
+
# and handles the AI generation API endpoint.
|
|
3
|
+
module Blazer
|
|
4
|
+
module Ai
|
|
5
|
+
class Middleware
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
# Handle AI generation endpoint
|
|
12
|
+
if ai_generate_request?(env)
|
|
13
|
+
return handle_ai_generate(env)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
status, headers, response = @app.call(env)
|
|
17
|
+
|
|
18
|
+
# Only inject into HTML responses for Blazer query pages
|
|
19
|
+
if inject_button?(env, headers)
|
|
20
|
+
body = extract_body(response)
|
|
21
|
+
if body.include?("id=\"editor\"") || body.include?("id=\"editor-container\"")
|
|
22
|
+
body = inject_ai_script(body)
|
|
23
|
+
headers["Content-Length"] = body.bytesize.to_s
|
|
24
|
+
response = [body]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
[status, headers, response]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def ai_generate_request?(env)
|
|
34
|
+
env["REQUEST_METHOD"] == "POST" &&
|
|
35
|
+
env["PATH_INFO"].to_s.end_with?("/ai/generate_sql")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_ai_generate(env)
|
|
39
|
+
require "json"
|
|
40
|
+
|
|
41
|
+
request = Rack::Request.new(env)
|
|
42
|
+
params = begin
|
|
43
|
+
JSON.parse(request.body.read)
|
|
44
|
+
rescue
|
|
45
|
+
{}
|
|
46
|
+
end
|
|
47
|
+
query_params = params["query"] || {}
|
|
48
|
+
|
|
49
|
+
# Basic validation
|
|
50
|
+
name = query_params["name"].to_s.strip
|
|
51
|
+
description = query_params["description"].to_s.strip
|
|
52
|
+
|
|
53
|
+
if name.empty? && description.empty?
|
|
54
|
+
return json_response({error: "Name or description required"}, 422)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if AI is enabled
|
|
58
|
+
unless Blazer::Ai.configuration.enabled?
|
|
59
|
+
return json_response({error: "AI features are disabled"}, 403)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Rate limiting
|
|
63
|
+
identifier = "ip:#{request.ip}"
|
|
64
|
+
rate_limiter = RateLimiter.new
|
|
65
|
+
begin
|
|
66
|
+
rate_limiter.check_and_track!(identifier: identifier)
|
|
67
|
+
rescue RateLimiter::RateLimitExceeded => e
|
|
68
|
+
return json_response({error: e.message, retry_after: e.retry_after}, 429)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Find data source
|
|
72
|
+
data_source = find_data_source(query_params["data_source"])
|
|
73
|
+
|
|
74
|
+
# Generate SQL
|
|
75
|
+
generator = SqlGenerator.new(
|
|
76
|
+
params: {name: name, description: description},
|
|
77
|
+
data_source: data_source
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
begin
|
|
81
|
+
sql = generator.call
|
|
82
|
+
json_response({sql: sql}, 200)
|
|
83
|
+
rescue SqlValidator::ValidationError
|
|
84
|
+
json_response({error: "Generated SQL failed safety validation"}, 422)
|
|
85
|
+
rescue SqlGenerator::GenerationError => e
|
|
86
|
+
json_response({error: e.message}, 422)
|
|
87
|
+
rescue => e
|
|
88
|
+
Rails.logger.error("[BlazerAI] Generation error: #{e.class}: #{e.message}") if defined?(Rails.logger)
|
|
89
|
+
json_response({error: "An error occurred while generating SQL"}, 422)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def find_data_source(data_source_id)
|
|
94
|
+
return nil if data_source_id.to_s.empty?
|
|
95
|
+
return nil unless defined?(Blazer) && Blazer.respond_to?(:data_sources)
|
|
96
|
+
Blazer.data_sources[data_source_id]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def json_response(data, status)
|
|
100
|
+
body = JSON.generate(data)
|
|
101
|
+
[
|
|
102
|
+
status,
|
|
103
|
+
{"Content-Type" => "application/json", "Content-Length" => body.bytesize.to_s},
|
|
104
|
+
[body]
|
|
105
|
+
]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def inject_button?(env, headers)
|
|
109
|
+
return false unless headers["Content-Type"]&.include?("text/html")
|
|
110
|
+
path = env["PATH_INFO"].to_s
|
|
111
|
+
path.match?(%r{/queries(/new|/\d+(/edit)?)?$})
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def extract_body(response)
|
|
115
|
+
body = +""
|
|
116
|
+
response.each { |part| body << part }
|
|
117
|
+
response.close if response.respond_to?(:close)
|
|
118
|
+
body
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def inject_ai_script(body)
|
|
122
|
+
generate_path = Blazer::Ai::UrlHelper.blazer_ai_generate_sql_path
|
|
123
|
+
script = build_script(generate_path)
|
|
124
|
+
|
|
125
|
+
# Find the last </body> tag using rindex to avoid regex issues
|
|
126
|
+
closing_body_index = body.rindex("</body>")
|
|
127
|
+
return body unless closing_body_index
|
|
128
|
+
|
|
129
|
+
# Insert script before the closing body tag by manual string slicing
|
|
130
|
+
body[0...closing_body_index] + script + body[closing_body_index..-1]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_script(generate_path)
|
|
134
|
+
<<~HTML
|
|
135
|
+
<style>
|
|
136
|
+
#blazer-ai-btn { margin-right: 5px; cursor: pointer; }
|
|
137
|
+
#blazer-ai-btn.loading { opacity: 0.7; }
|
|
138
|
+
#blazer-ai-btn .spinner { display: none; width: 12px; height: 12px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: bas 0.8s linear infinite; margin-right: 4px; vertical-align: middle; }
|
|
139
|
+
#blazer-ai-btn.loading .spinner { display: inline-block; }
|
|
140
|
+
@keyframes bas { to { transform: rotate(360deg); } }
|
|
141
|
+
#blazer-ai-error { display: none; margin-top: 8px; padding: 8px 12px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24; font-size: 13px; }
|
|
142
|
+
#blazer-ai-error.show { display: block; }
|
|
143
|
+
</style>
|
|
144
|
+
<script>
|
|
145
|
+
(function() {
|
|
146
|
+
var BA = {
|
|
147
|
+
loading: false,
|
|
148
|
+
path: "#{generate_path}",
|
|
149
|
+
init: function() {
|
|
150
|
+
var self = this, attempts = 0;
|
|
151
|
+
var iv = setInterval(function() {
|
|
152
|
+
var btn = document.querySelector('input.btn-success[type="submit"][value="Create"], input.btn-success[type="submit"][value="Update"]');
|
|
153
|
+
if (btn && !document.getElementById('blazer-ai-btn')) {
|
|
154
|
+
clearInterval(iv);
|
|
155
|
+
self.inject(btn);
|
|
156
|
+
} else if (++attempts > 50) clearInterval(iv);
|
|
157
|
+
}, 100);
|
|
158
|
+
document.addEventListener('keydown', function(e) {
|
|
159
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'G') { e.preventDefault(); BA.generate(); }
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
inject: function(createBtn) {
|
|
163
|
+
var btn = document.createElement('a');
|
|
164
|
+
btn.id = 'blazer-ai-btn';
|
|
165
|
+
btn.className = 'btn btn-info';
|
|
166
|
+
btn.innerHTML = '<span class="spinner"></span>AI Generate';
|
|
167
|
+
btn.onclick = function(e) { e.preventDefault(); BA.generate(); };
|
|
168
|
+
createBtn.parentNode.insertBefore(btn, createBtn);
|
|
169
|
+
var err = document.createElement('div');
|
|
170
|
+
err.id = 'blazer-ai-error';
|
|
171
|
+
var closeBtn = document.createElement('span');
|
|
172
|
+
closeBtn.style.cssText = 'float:right;cursor:pointer';
|
|
173
|
+
closeBtn.textContent = 'x';
|
|
174
|
+
closeBtn.onclick = function() { err.classList.remove('show'); };
|
|
175
|
+
var msgSpan = document.createElement('span');
|
|
176
|
+
msgSpan.className = 'msg';
|
|
177
|
+
err.appendChild(closeBtn);
|
|
178
|
+
err.appendChild(msgSpan);
|
|
179
|
+
createBtn.parentNode.parentNode.appendChild(err);
|
|
180
|
+
},
|
|
181
|
+
generate: function() {
|
|
182
|
+
if (this.loading) return;
|
|
183
|
+
var name = (document.querySelector('input[name="query[name]"]') || {}).value || '';
|
|
184
|
+
var desc = (document.querySelector('textarea[name="query[description]"]') || {}).value || '';
|
|
185
|
+
var ds = (document.querySelector('select[name="query[data_source]"]') || {}).value || '';
|
|
186
|
+
if (!name.trim() && !desc.trim()) { this.error('Enter a query name or description first.'); return; }
|
|
187
|
+
this.setLoading(true);
|
|
188
|
+
this.hideError();
|
|
189
|
+
fetch(this.path, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': (document.querySelector('meta[name="csrf-token"]') || {}).content || '' },
|
|
192
|
+
body: JSON.stringify({ query: { name: name, description: desc, data_source: ds } })
|
|
193
|
+
})
|
|
194
|
+
.then(function(r) { return r.json().then(function(d) { return { status: r.status, data: d }; }); })
|
|
195
|
+
.then(function(r) {
|
|
196
|
+
BA.setLoading(false);
|
|
197
|
+
if (r.status === 429) BA.error('Rate limit. Wait ' + (r.data.retry_after || 60) + 's.');
|
|
198
|
+
else if (r.status !== 200 || r.data.error) BA.error(r.data.error || 'Generation failed.');
|
|
199
|
+
else if (r.data.sql) BA.insertSQL(r.data.sql);
|
|
200
|
+
})
|
|
201
|
+
.catch(function() { BA.setLoading(false); BA.error('Network error.'); });
|
|
202
|
+
},
|
|
203
|
+
insertSQL: function(sql) {
|
|
204
|
+
if (typeof editor !== 'undefined') { editor.setValue(sql, 1); editor.focus(); }
|
|
205
|
+
else { var ta = document.querySelector('textarea[name="query[statement]"]'); if (ta) { ta.value = sql; ta.focus(); } }
|
|
206
|
+
},
|
|
207
|
+
setLoading: function(v) { this.loading = v; var b = document.getElementById('blazer-ai-btn'); if (b) b.classList.toggle('loading', v); },
|
|
208
|
+
error: function(m) { var e = document.getElementById('blazer-ai-error'); if (e) { e.querySelector('.msg').textContent = m; e.classList.add('show'); } },
|
|
209
|
+
hideError: function() { var e = document.getElementById('blazer-ai-error'); if (e) e.classList.remove('show'); }
|
|
210
|
+
};
|
|
211
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function() { BA.init(); });
|
|
212
|
+
else BA.init();
|
|
213
|
+
})();
|
|
214
|
+
</script>
|
|
215
|
+
HTML
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/blazer/ai/railtie.rb
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
# Integrates Blazer AI into Rails applications.
|
|
2
|
-
# Injects routes into Blazer::Engine and sets up view paths.
|
|
3
2
|
class Blazer::Ai::Railtie < ::Rails::Railtie
|
|
4
|
-
initializer "blazer_ai.load"
|
|
3
|
+
initializer "blazer_ai.load" do
|
|
5
4
|
require "blazer/ai/engine"
|
|
5
|
+
require "blazer/ai/middleware"
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Inject routes directly into Blazer::Engine to avoid nested engine routing issues
|
|
15
|
-
initializer "blazer_ai.extend_routes", after: "blazer.routes" do |app|
|
|
16
|
-
if defined?(Blazer::Engine)
|
|
17
|
-
Blazer::Engine.routes.prepend do
|
|
18
|
-
post "/ai/generate_sql" => "ai/queries#create", as: "blazer_ai_generate_sql"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
ActiveSupport.on_load(:action_controller) do
|
|
22
|
-
helper Blazer::Ai::UrlHelper
|
|
23
|
-
end
|
|
24
|
-
end
|
|
8
|
+
# Middleware handles both UI injection and API endpoint
|
|
9
|
+
initializer "blazer_ai.middleware" do |app|
|
|
10
|
+
app.middleware.use Blazer::Ai::Middleware
|
|
25
11
|
end
|
|
26
12
|
end
|
|
@@ -73,9 +73,9 @@ module Blazer::Ai
|
|
|
73
73
|
# Timeout to prevent hung requests if LLM provider is slow/unresponsive
|
|
74
74
|
Timeout.timeout(30, GenerationError, "SQL generation timed out. Please try again.") do
|
|
75
75
|
RubyLLM.chat(model: model)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
.with_temperature(temperature)
|
|
77
|
+
.ask(prompt)
|
|
78
|
+
.content
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
data/lib/blazer/ai/url_helper.rb
CHANGED
|
@@ -8,7 +8,12 @@ module Blazer::Ai::UrlHelper
|
|
|
8
8
|
path = "/ai/generate_sql"
|
|
9
9
|
|
|
10
10
|
if defined?(Rails.application)
|
|
11
|
-
|
|
11
|
+
# Find the route that mounts Blazer::Engine
|
|
12
|
+
# The route name could be 'blazer', 'admin_blazer', etc.
|
|
13
|
+
main_route = Rails.application.routes.routes.find do |r|
|
|
14
|
+
r.app.respond_to?(:app) && r.app.app == Blazer::Engine
|
|
15
|
+
end
|
|
16
|
+
|
|
12
17
|
if main_route
|
|
13
18
|
blazer_mount = main_route.path.spec.to_s.gsub("(.:format)", "")
|
|
14
19
|
path = blazer_mount + path
|
data/lib/blazer/ai/version.rb
CHANGED
|
@@ -5,9 +5,26 @@ module BlazerAi
|
|
|
5
5
|
class InstallGenerator < Rails::Generators::Base
|
|
6
6
|
source_root File.expand_path("templates", __dir__)
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def copy_blazer_ai_initializer
|
|
9
9
|
copy_file "blazer_ai.rb", "config/initializers/blazer_ai.rb"
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
def copy_ruby_llm_initializer
|
|
13
|
+
return if ruby_llm_configured?
|
|
14
|
+
|
|
15
|
+
copy_file "ruby_llm.rb", "config/initializers/ruby_llm.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def ruby_llm_configured?
|
|
21
|
+
initializers_path = File.join(destination_root, "config/initializers")
|
|
22
|
+
return false unless File.directory?(initializers_path)
|
|
23
|
+
|
|
24
|
+
Dir.glob(File.join(initializers_path, "*.rb")).any? do |file|
|
|
25
|
+
File.read(file).include?("RubyLLM.configure")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
11
28
|
end
|
|
12
29
|
end
|
|
13
30
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: blazer-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kieran Klaassen
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- lib/blazer/ai.rb
|
|
75
75
|
- lib/blazer/ai/configuration.rb
|
|
76
76
|
- lib/blazer/ai/engine.rb
|
|
77
|
+
- lib/blazer/ai/middleware.rb
|
|
77
78
|
- lib/blazer/ai/prompt_sanitizer.rb
|
|
78
79
|
- lib/blazer/ai/railtie.rb
|
|
79
80
|
- lib/blazer/ai/rate_limiter.rb
|
|
@@ -85,6 +86,7 @@ files:
|
|
|
85
86
|
- lib/generators/blazer_ai/USAGE
|
|
86
87
|
- lib/generators/blazer_ai/install_generator.rb
|
|
87
88
|
- lib/generators/blazer_ai/templates/blazer_ai.rb
|
|
89
|
+
- lib/generators/blazer_ai/templates/ruby_llm.rb
|
|
88
90
|
homepage: https://github.com/kieranklaassen/blazer-ai
|
|
89
91
|
licenses:
|
|
90
92
|
- MIT
|