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,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Features
|
6
|
+
class Logging
|
7
|
+
attr_reader :name, :provider, :config
|
8
|
+
|
9
|
+
def initialize(provider, config)
|
10
|
+
@name = :logging
|
11
|
+
@provider = provider
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def enabled?
|
16
|
+
@config.feature_enabled?(:logging)
|
17
|
+
end
|
18
|
+
|
19
|
+
def process_response?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_request(env)
|
24
|
+
return { processed: false, reason: "disabled" } unless enabled?
|
25
|
+
|
26
|
+
# Collect request metadata for AI analysis
|
27
|
+
request_summary = build_request_summary(env)
|
28
|
+
|
29
|
+
# Generate AI insights about the request
|
30
|
+
insights = generate_request_insights(request_summary, env)
|
31
|
+
|
32
|
+
{
|
33
|
+
request_summary: request_summary,
|
34
|
+
ai_insights: insights,
|
35
|
+
feature: @name,
|
36
|
+
timestamp: Time.now.iso8601
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_response(env, status, headers, body)
|
41
|
+
return { processed: false, reason: "disabled" } unless enabled?
|
42
|
+
|
43
|
+
# Collect response metadata
|
44
|
+
response_summary = build_response_summary(status, headers, body)
|
45
|
+
|
46
|
+
# Generate AI insights about the complete request-response cycle
|
47
|
+
cycle_insights = generate_cycle_insights(env, response_summary)
|
48
|
+
|
49
|
+
# Log structured data for AI analysis
|
50
|
+
log_structured_data(env, response_summary, cycle_insights)
|
51
|
+
|
52
|
+
{
|
53
|
+
response_summary: response_summary,
|
54
|
+
cycle_insights: cycle_insights,
|
55
|
+
feature: "#{@name}_response",
|
56
|
+
timestamp: Time.now.iso8601
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate_traffic_summary(time_window = 3600)
|
61
|
+
# This would be called periodically to generate AI-powered traffic summaries
|
62
|
+
return { processed: false, reason: "disabled" } unless enabled?
|
63
|
+
|
64
|
+
# In a real implementation, this would analyze stored log data
|
65
|
+
sample_data = build_sample_traffic_data(time_window)
|
66
|
+
|
67
|
+
begin
|
68
|
+
summary = @provider.analyze_patterns(sample_data)
|
69
|
+
|
70
|
+
{
|
71
|
+
time_window: time_window,
|
72
|
+
summary: summary,
|
73
|
+
generated_at: Time.now.iso8601
|
74
|
+
}
|
75
|
+
rescue => e
|
76
|
+
Utils::Logger.error("Failed to generate traffic summary", error: e.message)
|
77
|
+
{ error: e.message, generated_at: Time.now.iso8601 }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def build_request_summary(env)
|
84
|
+
{
|
85
|
+
method: env["REQUEST_METHOD"],
|
86
|
+
path: env["PATH_INFO"],
|
87
|
+
query_params_count: env["QUERY_STRING"]&.split("&")&.size || 0,
|
88
|
+
user_agent: env["HTTP_USER_AGENT"],
|
89
|
+
remote_ip: env["REMOTE_ADDR"],
|
90
|
+
content_length: env["CONTENT_LENGTH"]&.to_i || 0,
|
91
|
+
content_type: env["CONTENT_TYPE"],
|
92
|
+
timestamp: Time.now.iso8601,
|
93
|
+
has_auth: !!(env["HTTP_AUTHORIZATION"] || env["HTTP_X_API_KEY"]),
|
94
|
+
is_ajax: env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest",
|
95
|
+
referer: env["HTTP_REFERER"]
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_response_summary(status, headers, body)
|
100
|
+
body_size = 0
|
101
|
+
if body.respond_to?(:each)
|
102
|
+
body.each { |chunk| body_size += chunk.bytesize }
|
103
|
+
end
|
104
|
+
|
105
|
+
{
|
106
|
+
status: status,
|
107
|
+
content_type: headers["Content-Type"],
|
108
|
+
content_length: headers["Content-Length"]&.to_i || body_size,
|
109
|
+
cache_control: headers["Cache-Control"],
|
110
|
+
has_cookies: !!headers["Set-Cookie"],
|
111
|
+
timestamp: Time.now.iso8601
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def generate_request_insights(request_summary, env)
|
116
|
+
# Simple rule-based insights (in production, could use AI provider)
|
117
|
+
insights = []
|
118
|
+
|
119
|
+
# Analyze request patterns
|
120
|
+
if request_summary[:query_params_count] > 10
|
121
|
+
insights << { type: "performance", message: "High number of query parameters" }
|
122
|
+
end
|
123
|
+
|
124
|
+
if request_summary[:content_length] > 1_000_000
|
125
|
+
insights << { type: "performance", message: "Large request body" }
|
126
|
+
end
|
127
|
+
|
128
|
+
if request_summary[:user_agent].nil? || request_summary[:user_agent].empty?
|
129
|
+
insights << { type: "security", message: "Missing User-Agent header" }
|
130
|
+
end
|
131
|
+
|
132
|
+
if request_summary[:path].include?("..")
|
133
|
+
insights << { type: "security", message: "Potential path traversal attempt" }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Check for API usage patterns
|
137
|
+
if request_summary[:path].start_with?("/api/")
|
138
|
+
insights << { type: "api", message: "API endpoint access" }
|
139
|
+
end
|
140
|
+
|
141
|
+
insights
|
142
|
+
end
|
143
|
+
|
144
|
+
def generate_cycle_insights(env, response_summary)
|
145
|
+
insights = []
|
146
|
+
ai_results = env["rack.ai"][:results] || {}
|
147
|
+
|
148
|
+
# Analyze response patterns
|
149
|
+
if response_summary[:status] >= 400
|
150
|
+
insights << {
|
151
|
+
type: "error",
|
152
|
+
message: "Error response: #{response_summary[:status]}",
|
153
|
+
severity: response_summary[:status] >= 500 ? "high" : "medium"
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
if response_summary[:content_length] > 5_000_000
|
158
|
+
insights << { type: "performance", message: "Large response body" }
|
159
|
+
end
|
160
|
+
|
161
|
+
# Correlate with AI analysis results
|
162
|
+
if ai_results[:classification]
|
163
|
+
classification = ai_results[:classification][:classification]
|
164
|
+
insights << {
|
165
|
+
type: "classification",
|
166
|
+
message: "Request classified as: #{classification}",
|
167
|
+
confidence: ai_results[:classification][:confidence]
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
if ai_results[:security] && ai_results[:security][:threat_level] != :low
|
172
|
+
insights << {
|
173
|
+
type: "security",
|
174
|
+
message: "Security threat detected: #{ai_results[:security][:threat_level]}",
|
175
|
+
threats: ai_results[:security][:injection_detection][:threats]
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
insights
|
180
|
+
end
|
181
|
+
|
182
|
+
def log_structured_data(env, response_summary, insights)
|
183
|
+
log_entry = {
|
184
|
+
timestamp: Time.now.iso8601,
|
185
|
+
request: {
|
186
|
+
method: env["REQUEST_METHOD"],
|
187
|
+
path: env["PATH_INFO"],
|
188
|
+
remote_ip: env["REMOTE_ADDR"],
|
189
|
+
user_agent: env["HTTP_USER_AGENT"]
|
190
|
+
},
|
191
|
+
response: {
|
192
|
+
status: response_summary[:status],
|
193
|
+
content_type: response_summary[:content_type],
|
194
|
+
content_length: response_summary[:content_length]
|
195
|
+
},
|
196
|
+
ai_results: env["rack.ai"][:results],
|
197
|
+
insights: insights,
|
198
|
+
processing_time: Time.now - env["rack.ai"][:start_time]
|
199
|
+
}
|
200
|
+
|
201
|
+
# In production, this would go to structured logging system
|
202
|
+
Utils::Logger.info("AI-Enhanced Request Log", log_entry)
|
203
|
+
end
|
204
|
+
|
205
|
+
def build_sample_traffic_data(time_window)
|
206
|
+
# In a real implementation, this would query actual log storage
|
207
|
+
# For demo purposes, return sample data structure
|
208
|
+
{
|
209
|
+
time_window: time_window,
|
210
|
+
total_requests: 1500,
|
211
|
+
unique_ips: 245,
|
212
|
+
status_codes: {
|
213
|
+
"200" => 1200,
|
214
|
+
"404" => 150,
|
215
|
+
"500" => 50,
|
216
|
+
"403" => 100
|
217
|
+
},
|
218
|
+
top_paths: [
|
219
|
+
{ path: "/api/users", count: 300 },
|
220
|
+
{ path: "/", count: 250 },
|
221
|
+
{ path: "/api/posts", count: 200 }
|
222
|
+
],
|
223
|
+
user_agents: {
|
224
|
+
"browser" => 1000,
|
225
|
+
"bot" => 300,
|
226
|
+
"api_client" => 200
|
227
|
+
},
|
228
|
+
geographic_distribution: {
|
229
|
+
"US" => 800,
|
230
|
+
"EU" => 400,
|
231
|
+
"ASIA" => 300
|
232
|
+
}
|
233
|
+
}
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Features
|
6
|
+
class Moderation
|
7
|
+
attr_reader :name, :provider, :config
|
8
|
+
|
9
|
+
def initialize(provider, config)
|
10
|
+
@name = :moderation
|
11
|
+
@provider = provider
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def enabled?
|
16
|
+
@config.feature_enabled?(:moderation)
|
17
|
+
end
|
18
|
+
|
19
|
+
def process_response?
|
20
|
+
@config.moderation.check_response
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_request(env)
|
24
|
+
content = extract_content_from_request(env)
|
25
|
+
return { processed: false, reason: "no_content" } if content.empty?
|
26
|
+
|
27
|
+
result = @provider.moderate_content(content.join(" "))
|
28
|
+
|
29
|
+
# Apply toxicity threshold
|
30
|
+
threshold = @config.moderation.toxicity_threshold
|
31
|
+
max_score = result[:category_scores]&.values&.max || 0.0
|
32
|
+
|
33
|
+
result[:action] = if result[:flagged] && max_score > threshold
|
34
|
+
@config.moderation.block_on_violation ? :block : :flag
|
35
|
+
else
|
36
|
+
:allow
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add metadata
|
40
|
+
result[:feature] = @name
|
41
|
+
result[:timestamp] = Time.now.iso8601
|
42
|
+
result[:content_analyzed] = content.size
|
43
|
+
result[:threshold] = threshold
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_response(env, status, headers, body)
|
49
|
+
return { processed: false, reason: "disabled" } unless process_response?
|
50
|
+
|
51
|
+
content = extract_content_from_response(body)
|
52
|
+
return { processed: false, reason: "no_content" } if content.empty?
|
53
|
+
|
54
|
+
result = @provider.moderate_content(content)
|
55
|
+
result[:feature] = "#{@name}_response"
|
56
|
+
result[:timestamp] = Time.now.iso8601
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def extract_content_from_request(env)
|
64
|
+
content = []
|
65
|
+
|
66
|
+
# Extract from query parameters
|
67
|
+
if env["QUERY_STRING"] && !env["QUERY_STRING"].empty?
|
68
|
+
content << env["QUERY_STRING"]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Extract from form data (if POST/PUT)
|
72
|
+
if %w[POST PUT PATCH].include?(env["REQUEST_METHOD"])
|
73
|
+
input = env["rack.input"]
|
74
|
+
if input && input.respond_to?(:read)
|
75
|
+
body_content = input.read
|
76
|
+
input.rewind if input.respond_to?(:rewind)
|
77
|
+
content << body_content unless body_content.empty?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Extract from specific headers that might contain user content
|
82
|
+
user_content_headers = %w[HTTP_X_COMMENT HTTP_X_MESSAGE HTTP_X_DESCRIPTION]
|
83
|
+
user_content_headers.each do |header|
|
84
|
+
content << env[header] if env[header]
|
85
|
+
end
|
86
|
+
|
87
|
+
content.compact.reject(&:empty?)
|
88
|
+
end
|
89
|
+
|
90
|
+
def extract_content_from_response(body)
|
91
|
+
return "" unless body.respond_to?(:each)
|
92
|
+
|
93
|
+
content = ""
|
94
|
+
body.each { |chunk| content += chunk.to_s }
|
95
|
+
|
96
|
+
# Only moderate text content
|
97
|
+
return "" unless content.match?(/\A[\s\p{Print}]*\z/)
|
98
|
+
|
99
|
+
content
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Features
|
6
|
+
class Routing
|
7
|
+
attr_reader :name, :provider, :config
|
8
|
+
|
9
|
+
def initialize(provider, config)
|
10
|
+
@name = :routing
|
11
|
+
@provider = provider
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def enabled?
|
16
|
+
@config.feature_enabled?(:routing) && @config.config.routing.smart_routing_enabled
|
17
|
+
end
|
18
|
+
|
19
|
+
def process_response?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_request(env)
|
24
|
+
return { processed: false, reason: "disabled" } unless enabled?
|
25
|
+
|
26
|
+
# Get classification and security results from other features
|
27
|
+
ai_results = env["rack.ai"][:results] || {}
|
28
|
+
classification = ai_results[:classification]
|
29
|
+
security = ai_results[:security]
|
30
|
+
|
31
|
+
routing_decision = make_routing_decision(classification, security, env)
|
32
|
+
|
33
|
+
if routing_decision[:should_route]
|
34
|
+
# Modify the request path for routing
|
35
|
+
original_path = env["PATH_INFO"]
|
36
|
+
new_path = routing_decision[:target_path]
|
37
|
+
|
38
|
+
env["PATH_INFO"] = new_path
|
39
|
+
env["rack.ai.original_path"] = original_path
|
40
|
+
end
|
41
|
+
|
42
|
+
{
|
43
|
+
routing_decision: routing_decision,
|
44
|
+
original_path: env["rack.ai.original_path"],
|
45
|
+
new_path: env["PATH_INFO"],
|
46
|
+
feature: @name,
|
47
|
+
timestamp: Time.now.iso8601
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def make_routing_decision(classification, security, env)
|
54
|
+
# Default: no routing
|
55
|
+
decision = {
|
56
|
+
should_route: false,
|
57
|
+
target_path: env["PATH_INFO"],
|
58
|
+
reason: "no_routing_needed",
|
59
|
+
confidence: 1.0
|
60
|
+
}
|
61
|
+
|
62
|
+
# Route based on classification
|
63
|
+
if classification
|
64
|
+
case classification[:classification]
|
65
|
+
when :suspicious
|
66
|
+
if classification[:confidence] > 0.8
|
67
|
+
decision = {
|
68
|
+
should_route: true,
|
69
|
+
target_path: @config.config.routing.suspicious_route,
|
70
|
+
reason: "suspicious_classification",
|
71
|
+
confidence: classification[:confidence]
|
72
|
+
}
|
73
|
+
end
|
74
|
+
when :bot
|
75
|
+
if classification[:confidence] > 0.9
|
76
|
+
decision = {
|
77
|
+
should_route: true,
|
78
|
+
target_path: @config.config.routing.bot_route,
|
79
|
+
reason: "bot_classification",
|
80
|
+
confidence: classification[:confidence]
|
81
|
+
}
|
82
|
+
end
|
83
|
+
when :spam
|
84
|
+
decision = {
|
85
|
+
should_route: true,
|
86
|
+
target_path: "/blocked",
|
87
|
+
reason: "spam_classification",
|
88
|
+
confidence: classification[:confidence]
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Override with security-based routing
|
94
|
+
if security && security[:threat_level] == :high
|
95
|
+
decision = {
|
96
|
+
should_route: true,
|
97
|
+
target_path: "/security/blocked",
|
98
|
+
reason: "high_security_threat",
|
99
|
+
confidence: 1.0
|
100
|
+
}
|
101
|
+
elsif security && security[:threat_level] == :medium
|
102
|
+
decision = {
|
103
|
+
should_route: true,
|
104
|
+
target_path: "/security/verify",
|
105
|
+
reason: "medium_security_threat",
|
106
|
+
confidence: 0.8
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Add load balancing logic
|
111
|
+
if should_load_balance?(env)
|
112
|
+
decision = apply_load_balancing(decision, env)
|
113
|
+
end
|
114
|
+
|
115
|
+
decision
|
116
|
+
end
|
117
|
+
|
118
|
+
def should_load_balance?(env)
|
119
|
+
# Simple load balancing based on request patterns
|
120
|
+
path = env["PATH_INFO"]
|
121
|
+
method = env["REQUEST_METHOD"]
|
122
|
+
|
123
|
+
# Load balance API endpoints
|
124
|
+
path.start_with?("/api/") && method == "GET"
|
125
|
+
end
|
126
|
+
|
127
|
+
def apply_load_balancing(decision, env)
|
128
|
+
# Simple round-robin or least-connections logic
|
129
|
+
# In production, this would integrate with actual load balancer
|
130
|
+
|
131
|
+
servers = ["server1", "server2", "server3"]
|
132
|
+
selected_server = servers.sample # Random for demo
|
133
|
+
|
134
|
+
decision.merge({
|
135
|
+
load_balanced: true,
|
136
|
+
target_server: selected_server,
|
137
|
+
reason: "#{decision[:reason]}_with_load_balancing"
|
138
|
+
})
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|