rack-ai 0.1.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/.rspec +3 -0
- data/.rubocop.yml +55 -0
- data/CHANGELOG.md +65 -0
- data/LICENSE +21 -0
- data/README.md +687 -0
- data/ROADMAP.md +203 -0
- data/Rakefile +40 -0
- data/benchmarks/performance_benchmark.rb +283 -0
- data/examples/rails_integration.rb +301 -0
- data/examples/sinatra_integration.rb +458 -0
- data/lib/rack/ai/configuration.rb +208 -0
- data/lib/rack/ai/features/caching.rb +278 -0
- data/lib/rack/ai/features/classification.rb +67 -0
- data/lib/rack/ai/features/enhancement.rb +219 -0
- data/lib/rack/ai/features/logging.rb +238 -0
- data/lib/rack/ai/features/moderation.rb +104 -0
- data/lib/rack/ai/features/routing.rb +143 -0
- data/lib/rack/ai/features/security.rb +275 -0
- data/lib/rack/ai/middleware.rb +268 -0
- data/lib/rack/ai/providers/base.rb +107 -0
- data/lib/rack/ai/providers/huggingface.rb +259 -0
- data/lib/rack/ai/providers/local.rb +152 -0
- data/lib/rack/ai/providers/openai.rb +246 -0
- data/lib/rack/ai/utils/logger.rb +111 -0
- data/lib/rack/ai/utils/metrics.rb +220 -0
- data/lib/rack/ai/utils/sanitizer.rb +200 -0
- data/lib/rack/ai/version.rb +7 -0
- data/lib/rack/ai.rb +48 -0
- data/rack-ai.gemspec +51 -0
- metadata +290 -0
@@ -0,0 +1,458 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sinatra Integration Example for Rack::AI
|
4
|
+
#
|
5
|
+
# This example shows how to integrate Rack::AI with a Sinatra application
|
6
|
+
# for lightweight AI-powered request processing and security.
|
7
|
+
|
8
|
+
require "sinatra/base"
|
9
|
+
require "rack/ai"
|
10
|
+
require "json"
|
11
|
+
|
12
|
+
class AIEnhancedApp < Sinatra::Base
|
13
|
+
# Configure Rack::AI middleware
|
14
|
+
use Rack::AI::Middleware,
|
15
|
+
provider: :openai,
|
16
|
+
api_key: ENV["OPENAI_API_KEY"],
|
17
|
+
features: [:classification, :security, :moderation],
|
18
|
+
fail_safe: true,
|
19
|
+
async_processing: false # Synchronous for simpler Sinatra apps
|
20
|
+
|
21
|
+
# Global configuration
|
22
|
+
configure do
|
23
|
+
Rack::AI.configure do |config|
|
24
|
+
config.provider = :openai
|
25
|
+
config.api_key = ENV["OPENAI_API_KEY"]
|
26
|
+
config.features = [:classification, :security, :moderation, :logging]
|
27
|
+
config.fail_safe = true
|
28
|
+
config.async_processing = false
|
29
|
+
|
30
|
+
# Simplified configuration for Sinatra
|
31
|
+
config.classification.confidence_threshold = 0.7
|
32
|
+
config.moderation.toxicity_threshold = 0.8
|
33
|
+
config.sanitize_logs = true
|
34
|
+
config.log_level = :info
|
35
|
+
end
|
36
|
+
|
37
|
+
# Enable JSON parsing
|
38
|
+
set :parse_json_body, true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Helper methods for AI results
|
42
|
+
helpers do
|
43
|
+
def ai_results
|
44
|
+
@ai_results ||= request.env["rack.ai"]&.dig(:results) || {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def classified_as?(type)
|
48
|
+
ai_results[:classification]&.dig(:classification) == type.to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
def threat_level
|
52
|
+
ai_results[:security]&.dig(:threat_level) || :low
|
53
|
+
end
|
54
|
+
|
55
|
+
def content_flagged?
|
56
|
+
ai_results[:moderation]&.dig(:flagged) || false
|
57
|
+
end
|
58
|
+
|
59
|
+
def ai_confidence(feature)
|
60
|
+
ai_results[feature.to_sym]&.dig(:confidence) || 0.0
|
61
|
+
end
|
62
|
+
|
63
|
+
def block_if_threat!
|
64
|
+
case threat_level
|
65
|
+
when :high
|
66
|
+
halt 403, json({ error: "Security threat detected", code: "HIGH_THREAT" })
|
67
|
+
when :medium
|
68
|
+
headers["X-Security-Warning"] = "Medium threat detected"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def block_if_spam!
|
73
|
+
if classified_as?(:spam) && ai_confidence(:classification) > 0.8
|
74
|
+
halt 403, json({ error: "Request blocked as spam", code: "SPAM_DETECTED" })
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def json(data)
|
79
|
+
content_type :json
|
80
|
+
data.to_json
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Before filter to check AI results
|
85
|
+
before do
|
86
|
+
# Block high-security threats immediately
|
87
|
+
block_if_threat!
|
88
|
+
|
89
|
+
# Block obvious spam
|
90
|
+
block_if_spam!
|
91
|
+
|
92
|
+
# Log suspicious activity
|
93
|
+
if classified_as?(:suspicious)
|
94
|
+
logger.warn "Suspicious request: #{request.ip} -> #{request.path}"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Add AI headers for debugging
|
98
|
+
if settings.development?
|
99
|
+
headers["X-AI-Classification"] = ai_results[:classification]&.dig(:classification).to_s
|
100
|
+
headers["X-AI-Threat-Level"] = threat_level.to_s
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Routes
|
105
|
+
|
106
|
+
# Home page with AI-enhanced content
|
107
|
+
get "/" do
|
108
|
+
erb :index, locals: {
|
109
|
+
classification: ai_results[:classification],
|
110
|
+
is_bot: classified_as?(:bot)
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
# API endpoint with AI protection
|
115
|
+
get "/api/data" do
|
116
|
+
# Different responses based on classification
|
117
|
+
case ai_results[:classification]&.dig(:classification)
|
118
|
+
when :bot
|
119
|
+
# Simplified response for bots
|
120
|
+
json({
|
121
|
+
message: "Bot-optimized response",
|
122
|
+
data: { items: 10, cached: true },
|
123
|
+
timestamp: Time.now.iso8601
|
124
|
+
})
|
125
|
+
when :human
|
126
|
+
# Full response for humans
|
127
|
+
json({
|
128
|
+
message: "Welcome human user!",
|
129
|
+
data: generate_full_data,
|
130
|
+
metadata: {
|
131
|
+
classification: "human",
|
132
|
+
confidence: ai_confidence(:classification)
|
133
|
+
},
|
134
|
+
timestamp: Time.now.iso8601
|
135
|
+
})
|
136
|
+
else
|
137
|
+
# Default response
|
138
|
+
json({
|
139
|
+
message: "Standard API response",
|
140
|
+
data: generate_standard_data,
|
141
|
+
timestamp: Time.now.iso8601
|
142
|
+
})
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Content submission with AI moderation
|
147
|
+
post "/api/content" do
|
148
|
+
# Check for content moderation violations
|
149
|
+
if content_flagged?
|
150
|
+
moderation = ai_results[:moderation]
|
151
|
+
halt 400, json({
|
152
|
+
error: "Content moderation violation",
|
153
|
+
categories: moderation[:categories],
|
154
|
+
code: "CONTENT_FLAGGED"
|
155
|
+
})
|
156
|
+
end
|
157
|
+
|
158
|
+
# Process the content
|
159
|
+
content = params[:content] || request.body.read
|
160
|
+
|
161
|
+
# Store with AI metadata
|
162
|
+
result = store_content(content, ai_results)
|
163
|
+
|
164
|
+
json({
|
165
|
+
message: "Content stored successfully",
|
166
|
+
id: result[:id],
|
167
|
+
ai_analysis: {
|
168
|
+
classification: ai_results[:classification]&.dig(:classification),
|
169
|
+
moderation_passed: !content_flagged?
|
170
|
+
}
|
171
|
+
})
|
172
|
+
end
|
173
|
+
|
174
|
+
# Admin endpoint with enhanced security
|
175
|
+
get "/admin/*" do
|
176
|
+
# Extra security for admin routes
|
177
|
+
if threat_level != :low
|
178
|
+
halt 403, json({ error: "Access denied due to security concerns" })
|
179
|
+
end
|
180
|
+
|
181
|
+
if classified_as?(:bot) && ai_confidence(:classification) > 0.9
|
182
|
+
halt 403, json({ error: "Bot access not allowed to admin area" })
|
183
|
+
end
|
184
|
+
|
185
|
+
# Admin functionality here
|
186
|
+
json({ message: "Admin access granted", path: params[:splat].first })
|
187
|
+
end
|
188
|
+
|
189
|
+
# AI insights endpoint
|
190
|
+
get "/ai/insights" do
|
191
|
+
content_type :json
|
192
|
+
|
193
|
+
insights = {
|
194
|
+
current_request: {
|
195
|
+
classification: ai_results[:classification],
|
196
|
+
security: ai_results[:security],
|
197
|
+
moderation: ai_results[:moderation]
|
198
|
+
},
|
199
|
+
processing_time: Time.now - request.env["rack.ai"][:start_time],
|
200
|
+
features_enabled: Rack::AI.configuration.features
|
201
|
+
}
|
202
|
+
|
203
|
+
insights.to_json
|
204
|
+
end
|
205
|
+
|
206
|
+
# Health check endpoint (bypasses AI processing)
|
207
|
+
get "/health" do
|
208
|
+
json({ status: "ok", timestamp: Time.now.iso8601 })
|
209
|
+
end
|
210
|
+
|
211
|
+
# Error handlers with AI context
|
212
|
+
error 403 do
|
213
|
+
ai_context = {
|
214
|
+
classification: ai_results[:classification]&.dig(:classification),
|
215
|
+
threat_level: threat_level,
|
216
|
+
request_id: request.env["rack.ai"]&.dig(:metadata, :request_id)
|
217
|
+
}
|
218
|
+
|
219
|
+
json({
|
220
|
+
error: "Forbidden",
|
221
|
+
message: "Access denied by AI security system",
|
222
|
+
ai_context: ai_context,
|
223
|
+
timestamp: Time.now.iso8601
|
224
|
+
})
|
225
|
+
end
|
226
|
+
|
227
|
+
error 500 do
|
228
|
+
logger.error "Server error with AI context: #{ai_results}"
|
229
|
+
|
230
|
+
json({
|
231
|
+
error: "Internal Server Error",
|
232
|
+
message: "An error occurred while processing your request",
|
233
|
+
timestamp: Time.now.iso8601
|
234
|
+
})
|
235
|
+
end
|
236
|
+
|
237
|
+
# Private helper methods
|
238
|
+
private
|
239
|
+
|
240
|
+
def generate_full_data
|
241
|
+
{
|
242
|
+
users: [
|
243
|
+
{ id: 1, name: "Alice", role: "admin" },
|
244
|
+
{ id: 2, name: "Bob", role: "user" }
|
245
|
+
],
|
246
|
+
posts: [
|
247
|
+
{ id: 1, title: "Welcome Post", author_id: 1 },
|
248
|
+
{ id: 2, title: "Getting Started", author_id: 2 }
|
249
|
+
],
|
250
|
+
metadata: {
|
251
|
+
total_users: 2,
|
252
|
+
total_posts: 2,
|
253
|
+
generated_at: Time.now.iso8601
|
254
|
+
}
|
255
|
+
}
|
256
|
+
end
|
257
|
+
|
258
|
+
def generate_standard_data
|
259
|
+
{
|
260
|
+
summary: "Basic data summary",
|
261
|
+
count: 42,
|
262
|
+
status: "active"
|
263
|
+
}
|
264
|
+
end
|
265
|
+
|
266
|
+
def store_content(content, ai_metadata)
|
267
|
+
# Simulate content storage with AI metadata
|
268
|
+
{
|
269
|
+
id: SecureRandom.uuid,
|
270
|
+
content: content[0..100], # Store first 100 chars for demo
|
271
|
+
ai_metadata: {
|
272
|
+
classification: ai_metadata[:classification]&.dig(:classification),
|
273
|
+
confidence: ai_metadata[:classification]&.dig(:confidence),
|
274
|
+
moderation_passed: !ai_metadata[:moderation]&.dig(:flagged),
|
275
|
+
stored_at: Time.now.iso8601
|
276
|
+
}
|
277
|
+
}
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Example of a specialized AI-powered API
|
282
|
+
class AISecurityAPI < Sinatra::Base
|
283
|
+
# More restrictive AI configuration for security API
|
284
|
+
use Rack::AI::Middleware,
|
285
|
+
provider: :openai,
|
286
|
+
api_key: ENV["OPENAI_API_KEY"],
|
287
|
+
features: [:security, :classification],
|
288
|
+
fail_safe: false, # Strict mode - fail if AI is unavailable
|
289
|
+
async_processing: false
|
290
|
+
|
291
|
+
configure do
|
292
|
+
Rack::AI.configure do |config|
|
293
|
+
config.classification.confidence_threshold = 0.9 # Higher threshold
|
294
|
+
config.security.rate_limit = 100 # Lower rate limit
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
helpers do
|
299
|
+
def require_high_confidence!
|
300
|
+
classification = request.env["rack.ai"]&.dig(:results, :classification)
|
301
|
+
confidence = classification&.dig(:confidence) || 0.0
|
302
|
+
|
303
|
+
if confidence < 0.9
|
304
|
+
halt 403, { error: "High confidence classification required" }.to_json
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def security_analysis
|
309
|
+
request.env["rack.ai"]&.dig(:results, :security) || {}
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
before do
|
314
|
+
content_type :json
|
315
|
+
require_high_confidence!
|
316
|
+
end
|
317
|
+
|
318
|
+
get "/security/analyze" do
|
319
|
+
{
|
320
|
+
security_analysis: security_analysis,
|
321
|
+
threat_assessment: {
|
322
|
+
level: security_analysis[:threat_level],
|
323
|
+
confidence: security_analysis[:confidence],
|
324
|
+
recommendations: generate_security_recommendations
|
325
|
+
},
|
326
|
+
timestamp: Time.now.iso8601
|
327
|
+
}.to_json
|
328
|
+
end
|
329
|
+
|
330
|
+
post "/security/report" do
|
331
|
+
incident_data = JSON.parse(request.body.read)
|
332
|
+
|
333
|
+
# Enhanced incident analysis with AI
|
334
|
+
analysis = {
|
335
|
+
incident: incident_data,
|
336
|
+
ai_analysis: security_analysis,
|
337
|
+
severity: calculate_severity(incident_data, security_analysis),
|
338
|
+
response_required: security_analysis[:threat_level] == :high
|
339
|
+
}
|
340
|
+
|
341
|
+
# Store incident report
|
342
|
+
incident_id = store_security_incident(analysis)
|
343
|
+
|
344
|
+
{
|
345
|
+
incident_id: incident_id,
|
346
|
+
analysis: analysis,
|
347
|
+
timestamp: Time.now.iso8601
|
348
|
+
}.to_json
|
349
|
+
end
|
350
|
+
|
351
|
+
private
|
352
|
+
|
353
|
+
def generate_security_recommendations
|
354
|
+
case security_analysis[:threat_level]
|
355
|
+
when :high
|
356
|
+
["Block IP immediately", "Review access logs", "Alert security team"]
|
357
|
+
when :medium
|
358
|
+
["Monitor closely", "Require additional authentication"]
|
359
|
+
else
|
360
|
+
["Continue normal monitoring"]
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def calculate_severity(incident_data, ai_analysis)
|
365
|
+
base_severity = incident_data["severity"] || "low"
|
366
|
+
ai_threat = ai_analysis[:threat_level] || :low
|
367
|
+
|
368
|
+
return "critical" if ai_threat == :high
|
369
|
+
return "high" if ai_threat == :medium && base_severity != "low"
|
370
|
+
|
371
|
+
base_severity
|
372
|
+
end
|
373
|
+
|
374
|
+
def store_security_incident(analysis)
|
375
|
+
# Simulate storing security incident
|
376
|
+
SecureRandom.uuid
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# Example Rack application with custom AI middleware
|
381
|
+
class CustomAIApp
|
382
|
+
def initialize
|
383
|
+
@app = Rack::Builder.new do
|
384
|
+
# Custom AI configuration
|
385
|
+
use Rack::AI::Middleware,
|
386
|
+
provider: :huggingface, # Using HuggingFace instead of OpenAI
|
387
|
+
api_key: ENV["HUGGINGFACE_API_KEY"],
|
388
|
+
features: [:classification, :moderation],
|
389
|
+
fail_safe: true
|
390
|
+
|
391
|
+
# Custom middleware that uses AI results
|
392
|
+
use CustomAIProcessor
|
393
|
+
|
394
|
+
# Main application
|
395
|
+
run lambda { |env|
|
396
|
+
ai_results = env["rack.ai"]&.dig(:results) || {}
|
397
|
+
|
398
|
+
response_body = {
|
399
|
+
message: "Hello from Custom AI App!",
|
400
|
+
ai_results: ai_results,
|
401
|
+
timestamp: Time.now.iso8601
|
402
|
+
}.to_json
|
403
|
+
|
404
|
+
[200, { "Content-Type" => "application/json" }, [response_body]]
|
405
|
+
}
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def call(env)
|
410
|
+
@app.call(env)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# Custom middleware that processes AI results
|
415
|
+
class CustomAIProcessor
|
416
|
+
def initialize(app)
|
417
|
+
@app = app
|
418
|
+
end
|
419
|
+
|
420
|
+
def call(env)
|
421
|
+
status, headers, body = @app.call(env)
|
422
|
+
|
423
|
+
# Add custom AI processing
|
424
|
+
ai_results = env["rack.ai"]&.dig(:results) || {}
|
425
|
+
|
426
|
+
# Custom header based on AI classification
|
427
|
+
if ai_results[:classification]
|
428
|
+
classification = ai_results[:classification][:classification]
|
429
|
+
headers["X-Custom-AI-Class"] = classification.to_s.upcase
|
430
|
+
|
431
|
+
# Custom rate limiting based on classification
|
432
|
+
if classification == :bot
|
433
|
+
headers["X-RateLimit-Remaining"] = "100"
|
434
|
+
elsif classification == :human
|
435
|
+
headers["X-RateLimit-Remaining"] = "1000"
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
[status, headers, body]
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Run the application
|
444
|
+
if __FILE__ == $0
|
445
|
+
# Choose which app to run based on environment variable
|
446
|
+
app_type = ENV["APP_TYPE"] || "main"
|
447
|
+
|
448
|
+
app = case app_type
|
449
|
+
when "security"
|
450
|
+
AISecurityAPI
|
451
|
+
when "custom"
|
452
|
+
CustomAIApp.new
|
453
|
+
else
|
454
|
+
AIEnhancedApp
|
455
|
+
end
|
456
|
+
|
457
|
+
Rack::Handler::WEBrick.run app, Port: 4567
|
458
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-configurable"
|
4
|
+
require "dry-validation"
|
5
|
+
require "ostruct"
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
module AI
|
9
|
+
class Configuration
|
10
|
+
extend Dry::Configurable
|
11
|
+
|
12
|
+
# AI Provider settings
|
13
|
+
setting :provider, default: :openai
|
14
|
+
setting :api_key, default: nil
|
15
|
+
setting :api_url, default: nil
|
16
|
+
setting :timeout, default: 30
|
17
|
+
setting :retries, default: 3
|
18
|
+
|
19
|
+
# Feature toggles
|
20
|
+
setting :features, default: [:classification, :moderation]
|
21
|
+
setting :fail_safe, default: true
|
22
|
+
setting :async_processing, default: true
|
23
|
+
|
24
|
+
# Security settings
|
25
|
+
setting :sanitize_logs, default: true
|
26
|
+
setting :allowed_data_types, default: [:headers, :query_params]
|
27
|
+
setting :blocked_data_types, default: [:body, :cookies]
|
28
|
+
|
29
|
+
# Performance settings
|
30
|
+
setting :cache_enabled, default: true
|
31
|
+
setting :cache_ttl, default: 3600
|
32
|
+
setting :rate_limit, default: 1000
|
33
|
+
setting :batch_size, default: 10
|
34
|
+
|
35
|
+
# Logging and monitoring
|
36
|
+
setting :log_level, default: :info
|
37
|
+
setting :metrics_enabled, default: true
|
38
|
+
setting :explain_decisions, default: false
|
39
|
+
|
40
|
+
# Custom configurations per feature
|
41
|
+
setting :classification do
|
42
|
+
setting :confidence_threshold, default: 0.8
|
43
|
+
setting :categories, default: [:spam, :bot, :human, :suspicious]
|
44
|
+
end
|
45
|
+
|
46
|
+
setting :moderation do
|
47
|
+
setting :toxicity_threshold, default: 0.7
|
48
|
+
setting :check_response, default: false
|
49
|
+
setting :block_on_violation, default: true
|
50
|
+
end
|
51
|
+
|
52
|
+
setting :caching do
|
53
|
+
setting :predictive_enabled, default: true
|
54
|
+
setting :prefetch_threshold, default: 0.9
|
55
|
+
setting :redis_url, default: "redis://localhost:6379"
|
56
|
+
end
|
57
|
+
|
58
|
+
setting :routing do
|
59
|
+
setting :smart_routing_enabled, default: true
|
60
|
+
setting :suspicious_route, default: "/captcha"
|
61
|
+
setting :bot_route, default: "/api/bot"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Instance methods for configuration access
|
65
|
+
attr_accessor :provider, :api_key, :api_url, :timeout, :retries, :features,
|
66
|
+
:fail_safe, :async_processing, :sanitize_logs, :allowed_data_types,
|
67
|
+
:blocked_data_types, :cache_enabled, :cache_ttl, :rate_limit,
|
68
|
+
:batch_size, :log_level, :metrics_enabled, :explain_decisions
|
69
|
+
|
70
|
+
def initialize
|
71
|
+
# Set default values
|
72
|
+
@provider = :openai
|
73
|
+
@api_key = nil
|
74
|
+
@api_url = nil
|
75
|
+
@timeout = 30
|
76
|
+
@retries = 3
|
77
|
+
@features = [:classification, :moderation]
|
78
|
+
@fail_safe = true
|
79
|
+
@async_processing = true
|
80
|
+
@sanitize_logs = true
|
81
|
+
@allowed_data_types = [:headers, :query_params]
|
82
|
+
@blocked_data_types = [:body, :cookies]
|
83
|
+
@cache_enabled = true
|
84
|
+
@cache_ttl = 3600
|
85
|
+
@rate_limit = 1000
|
86
|
+
@batch_size = 10
|
87
|
+
@log_level = :info
|
88
|
+
@metrics_enabled = true
|
89
|
+
@explain_decisions = false
|
90
|
+
|
91
|
+
# Initialize nested configurations
|
92
|
+
@classification = OpenStruct.new(
|
93
|
+
confidence_threshold: 0.8,
|
94
|
+
categories: [:spam, :bot, :human, :suspicious]
|
95
|
+
)
|
96
|
+
|
97
|
+
@moderation = OpenStruct.new(
|
98
|
+
toxicity_threshold: 0.7,
|
99
|
+
check_response: false,
|
100
|
+
block_on_violation: true
|
101
|
+
)
|
102
|
+
|
103
|
+
@caching = OpenStruct.new(
|
104
|
+
predictive_enabled: true,
|
105
|
+
prefetch_threshold: 0.9,
|
106
|
+
redis_url: "redis://localhost:6379"
|
107
|
+
)
|
108
|
+
|
109
|
+
@routing = OpenStruct.new(
|
110
|
+
smart_routing_enabled: true,
|
111
|
+
suspicious_route: "/captcha",
|
112
|
+
bot_route: "/api/bot"
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def classification
|
117
|
+
@classification
|
118
|
+
end
|
119
|
+
|
120
|
+
def moderation
|
121
|
+
@moderation
|
122
|
+
end
|
123
|
+
|
124
|
+
def caching
|
125
|
+
@caching
|
126
|
+
end
|
127
|
+
|
128
|
+
def routing
|
129
|
+
@routing
|
130
|
+
end
|
131
|
+
|
132
|
+
# For compatibility with middleware
|
133
|
+
def config
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
# Validation schema
|
138
|
+
ConfigSchema = Dry::Validation.Contract do
|
139
|
+
params do
|
140
|
+
required(:provider).filled(:symbol)
|
141
|
+
optional(:api_key).maybe(:string)
|
142
|
+
optional(:api_url).maybe(:string)
|
143
|
+
required(:timeout).filled(:integer)
|
144
|
+
required(:retries).filled(:integer)
|
145
|
+
required(:features).filled(:array)
|
146
|
+
required(:fail_safe).filled(:bool)
|
147
|
+
end
|
148
|
+
|
149
|
+
rule(:provider) do
|
150
|
+
key.failure("must be one of: openai, huggingface, local") unless [:openai, :huggingface, :local].include?(value)
|
151
|
+
end
|
152
|
+
|
153
|
+
rule(:timeout) do
|
154
|
+
key.failure("must be positive") if value <= 0
|
155
|
+
end
|
156
|
+
|
157
|
+
rule(:retries) do
|
158
|
+
key.failure("must be non-negative") if value < 0
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def validate!
|
163
|
+
result = ConfigSchema.call(to_h)
|
164
|
+
return true if result.success?
|
165
|
+
|
166
|
+
errors = result.errors.to_h
|
167
|
+
raise ConfigurationError, "Invalid configuration: #{errors}"
|
168
|
+
end
|
169
|
+
|
170
|
+
def to_h
|
171
|
+
{
|
172
|
+
provider: @provider,
|
173
|
+
api_key: @api_key,
|
174
|
+
api_url: @api_url,
|
175
|
+
timeout: @timeout,
|
176
|
+
retries: @retries,
|
177
|
+
features: @features,
|
178
|
+
fail_safe: @fail_safe,
|
179
|
+
async_processing: @async_processing,
|
180
|
+
sanitize_logs: @sanitize_logs,
|
181
|
+
allowed_data_types: @allowed_data_types,
|
182
|
+
blocked_data_types: @blocked_data_types,
|
183
|
+
cache_enabled: @cache_enabled,
|
184
|
+
cache_ttl: @cache_ttl,
|
185
|
+
rate_limit: @rate_limit,
|
186
|
+
batch_size: @batch_size,
|
187
|
+
log_level: @log_level,
|
188
|
+
metrics_enabled: @metrics_enabled,
|
189
|
+
explain_decisions: @explain_decisions
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
def feature_enabled?(feature)
|
194
|
+
@features.include?(feature.to_sym)
|
195
|
+
end
|
196
|
+
|
197
|
+
def provider_config
|
198
|
+
{
|
199
|
+
provider: @provider,
|
200
|
+
api_key: @api_key,
|
201
|
+
api_url: @api_url,
|
202
|
+
timeout: @timeout,
|
203
|
+
retries: @retries
|
204
|
+
}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|