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,278 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
module AI
|
8
|
+
module Features
|
9
|
+
class Caching
|
10
|
+
attr_reader :name, :provider, :config
|
11
|
+
|
12
|
+
def initialize(provider, config)
|
13
|
+
@name = :caching
|
14
|
+
@provider = provider
|
15
|
+
@config = config
|
16
|
+
@redis = build_redis_client if @config.config.caching.predictive_enabled
|
17
|
+
end
|
18
|
+
|
19
|
+
def enabled?
|
20
|
+
@config.feature_enabled?(:caching) && @config.config.cache_enabled
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_response?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_request(env)
|
28
|
+
return { processed: false, reason: "disabled" } unless enabled?
|
29
|
+
|
30
|
+
cache_key = generate_cache_key(env)
|
31
|
+
|
32
|
+
# Check if we have cached AI analysis for this request pattern
|
33
|
+
cached_result = get_cached_analysis(cache_key)
|
34
|
+
if cached_result
|
35
|
+
return {
|
36
|
+
cache_hit: true,
|
37
|
+
cached_result: cached_result,
|
38
|
+
feature: @name,
|
39
|
+
timestamp: Time.now.iso8601
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Analyze request patterns for predictive caching
|
44
|
+
request_pattern = extract_request_pattern(env)
|
45
|
+
pattern_analysis = analyze_access_patterns(request_pattern)
|
46
|
+
|
47
|
+
result = {
|
48
|
+
cache_hit: false,
|
49
|
+
pattern_analysis: pattern_analysis,
|
50
|
+
should_prefetch: should_prefetch?(pattern_analysis),
|
51
|
+
feature: @name,
|
52
|
+
timestamp: Time.now.iso8601
|
53
|
+
}
|
54
|
+
|
55
|
+
# Store analysis for future use
|
56
|
+
cache_analysis(cache_key, result)
|
57
|
+
|
58
|
+
# Trigger prefetching if recommended
|
59
|
+
if result[:should_prefetch]
|
60
|
+
schedule_prefetch(request_pattern)
|
61
|
+
end
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def process_response(env, status, headers, body)
|
67
|
+
return { processed: false, reason: "disabled" } unless enabled?
|
68
|
+
|
69
|
+
# Analyze response for caching recommendations
|
70
|
+
response_analysis = analyze_response_cachability(env, status, headers, body)
|
71
|
+
|
72
|
+
# Update access patterns
|
73
|
+
update_access_patterns(env, status)
|
74
|
+
|
75
|
+
{
|
76
|
+
response_analysis: response_analysis,
|
77
|
+
feature: "#{@name}_response",
|
78
|
+
timestamp: Time.now.iso8601
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def build_redis_client
|
85
|
+
Redis.new(url: @config.config.caching.redis_url)
|
86
|
+
rescue => e
|
87
|
+
Utils::Logger.warn("Failed to connect to Redis for caching", error: e.message)
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def generate_cache_key(env)
|
92
|
+
path = env["PATH_INFO"]
|
93
|
+
method = env["REQUEST_METHOD"]
|
94
|
+
query = env["QUERY_STRING"]
|
95
|
+
user_agent_hash = Digest::MD5.hexdigest(env["HTTP_USER_AGENT"] || "")
|
96
|
+
|
97
|
+
"rack_ai:cache:#{method}:#{path}:#{Digest::MD5.hexdigest(query)}:#{user_agent_hash}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_cached_analysis(cache_key)
|
101
|
+
return nil unless @redis
|
102
|
+
|
103
|
+
cached = @redis.get(cache_key)
|
104
|
+
cached ? JSON.parse(cached) : nil
|
105
|
+
rescue => e
|
106
|
+
Utils::Logger.warn("Cache read error", error: e.message, key: cache_key)
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def cache_analysis(cache_key, result)
|
111
|
+
return unless @redis
|
112
|
+
|
113
|
+
@redis.setex(cache_key, @config.config.cache_ttl, result.to_json)
|
114
|
+
rescue => e
|
115
|
+
Utils::Logger.warn("Cache write error", error: e.message, key: cache_key)
|
116
|
+
end
|
117
|
+
|
118
|
+
def extract_request_pattern(env)
|
119
|
+
{
|
120
|
+
path: env["PATH_INFO"],
|
121
|
+
method: env["REQUEST_METHOD"],
|
122
|
+
user_agent: env["HTTP_USER_AGENT"],
|
123
|
+
referer: env["HTTP_REFERER"],
|
124
|
+
timestamp: Time.now.to_i,
|
125
|
+
hour: Time.now.hour,
|
126
|
+
day_of_week: Time.now.wday
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def analyze_access_patterns(pattern)
|
131
|
+
return { confidence: 0.0, patterns: [] } unless @redis
|
132
|
+
|
133
|
+
# Get recent access patterns for this path
|
134
|
+
pattern_key = "rack_ai:patterns:#{pattern[:path]}"
|
135
|
+
recent_accesses = @redis.lrange(pattern_key, 0, 99)
|
136
|
+
|
137
|
+
return { confidence: 0.0, patterns: [] } if recent_accesses.empty?
|
138
|
+
|
139
|
+
# Analyze patterns
|
140
|
+
access_data = recent_accesses.map { |data| JSON.parse(data) }
|
141
|
+
|
142
|
+
{
|
143
|
+
confidence: calculate_pattern_confidence(access_data),
|
144
|
+
patterns: identify_patterns(access_data),
|
145
|
+
frequency: calculate_frequency(access_data),
|
146
|
+
peak_hours: identify_peak_hours(access_data)
|
147
|
+
}
|
148
|
+
rescue => e
|
149
|
+
Utils::Logger.warn("Pattern analysis error", error: e.message)
|
150
|
+
{ confidence: 0.0, patterns: [] }
|
151
|
+
end
|
152
|
+
|
153
|
+
def should_prefetch?(pattern_analysis)
|
154
|
+
return false unless pattern_analysis[:confidence]
|
155
|
+
|
156
|
+
confidence_threshold = @config.config.caching.prefetch_threshold
|
157
|
+
pattern_analysis[:confidence] > confidence_threshold
|
158
|
+
end
|
159
|
+
|
160
|
+
def schedule_prefetch(request_pattern)
|
161
|
+
# In a real implementation, this would queue prefetch jobs
|
162
|
+
Utils::Logger.info("Prefetch scheduled", pattern: request_pattern[:path])
|
163
|
+
end
|
164
|
+
|
165
|
+
def analyze_response_cachability(env, status, headers, body)
|
166
|
+
cacheable = determine_cachability(status, headers)
|
167
|
+
content_type = headers["Content-Type"] || ""
|
168
|
+
|
169
|
+
{
|
170
|
+
cacheable: cacheable,
|
171
|
+
content_type: content_type,
|
172
|
+
status: status,
|
173
|
+
has_cache_headers: has_cache_control_headers?(headers),
|
174
|
+
recommended_ttl: recommend_ttl(content_type, status)
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def update_access_patterns(env, status)
|
179
|
+
return unless @redis
|
180
|
+
|
181
|
+
pattern_key = "rack_ai:patterns:#{env['PATH_INFO']}"
|
182
|
+
access_data = {
|
183
|
+
timestamp: Time.now.to_i,
|
184
|
+
status: status,
|
185
|
+
method: env["REQUEST_METHOD"],
|
186
|
+
user_agent: env["HTTP_USER_AGENT"],
|
187
|
+
hour: Time.now.hour,
|
188
|
+
day_of_week: Time.now.wday
|
189
|
+
}
|
190
|
+
|
191
|
+
@redis.lpush(pattern_key, access_data.to_json)
|
192
|
+
@redis.ltrim(pattern_key, 0, 999) # Keep last 1000 accesses
|
193
|
+
@redis.expire(pattern_key, 86400 * 7) # Expire after 7 days
|
194
|
+
rescue => e
|
195
|
+
Utils::Logger.warn("Pattern update error", error: e.message)
|
196
|
+
end
|
197
|
+
|
198
|
+
def calculate_pattern_confidence(access_data)
|
199
|
+
return 0.0 if access_data.size < 5
|
200
|
+
|
201
|
+
# Simple confidence based on access frequency and regularity
|
202
|
+
time_intervals = access_data.each_cons(2).map do |curr, prev|
|
203
|
+
curr["timestamp"] - prev["timestamp"]
|
204
|
+
end
|
205
|
+
|
206
|
+
return 0.0 if time_intervals.empty?
|
207
|
+
|
208
|
+
# Calculate coefficient of variation (lower = more regular)
|
209
|
+
mean_interval = time_intervals.sum.to_f / time_intervals.size
|
210
|
+
variance = time_intervals.map { |i| (i - mean_interval) ** 2 }.sum / time_intervals.size
|
211
|
+
cv = Math.sqrt(variance) / mean_interval
|
212
|
+
|
213
|
+
# Convert to confidence (0-1, where lower CV = higher confidence)
|
214
|
+
[1.0 - (cv / 2.0), 0.0].max
|
215
|
+
end
|
216
|
+
|
217
|
+
def identify_patterns(access_data)
|
218
|
+
patterns = []
|
219
|
+
|
220
|
+
# Check for hourly patterns
|
221
|
+
hourly_counts = access_data.group_by { |d| d["hour"] }.transform_values(&:count)
|
222
|
+
if hourly_counts.values.max > hourly_counts.values.min * 2
|
223
|
+
patterns << "hourly_variation"
|
224
|
+
end
|
225
|
+
|
226
|
+
# Check for daily patterns
|
227
|
+
daily_counts = access_data.group_by { |d| d["day_of_week"] }.transform_values(&:count)
|
228
|
+
if daily_counts.values.max > daily_counts.values.min * 2
|
229
|
+
patterns << "daily_variation"
|
230
|
+
end
|
231
|
+
|
232
|
+
patterns
|
233
|
+
end
|
234
|
+
|
235
|
+
def calculate_frequency(access_data)
|
236
|
+
return 0.0 if access_data.size < 2
|
237
|
+
|
238
|
+
time_span = access_data.first["timestamp"] - access_data.last["timestamp"]
|
239
|
+
return 0.0 if time_span <= 0
|
240
|
+
|
241
|
+
access_data.size.to_f / time_span * 3600 # requests per hour
|
242
|
+
end
|
243
|
+
|
244
|
+
def identify_peak_hours(access_data)
|
245
|
+
hourly_counts = access_data.group_by { |d| d["hour"] }.transform_values(&:count)
|
246
|
+
avg_count = hourly_counts.values.sum.to_f / hourly_counts.size
|
247
|
+
|
248
|
+
hourly_counts.select { |hour, count| count > avg_count * 1.5 }.keys.sort
|
249
|
+
end
|
250
|
+
|
251
|
+
def determine_cachability(status, headers)
|
252
|
+
return false unless (200..299).include?(status)
|
253
|
+
return false if headers["Set-Cookie"]
|
254
|
+
return false if headers["Cache-Control"]&.include?("no-cache")
|
255
|
+
|
256
|
+
true
|
257
|
+
end
|
258
|
+
|
259
|
+
def has_cache_control_headers?(headers)
|
260
|
+
!!(headers["Cache-Control"] || headers["Expires"] || headers["ETag"])
|
261
|
+
end
|
262
|
+
|
263
|
+
def recommend_ttl(content_type, status)
|
264
|
+
case content_type
|
265
|
+
when /image|css|javascript/
|
266
|
+
3600 # 1 hour for static assets
|
267
|
+
when /json|xml/
|
268
|
+
300 # 5 minutes for API responses
|
269
|
+
when /html/
|
270
|
+
600 # 10 minutes for HTML
|
271
|
+
else
|
272
|
+
300 # Default 5 minutes
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Features
|
6
|
+
class Classification
|
7
|
+
attr_reader :name, :provider, :config
|
8
|
+
|
9
|
+
def initialize(provider, config)
|
10
|
+
@name = :classification
|
11
|
+
@provider = provider
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def enabled?
|
16
|
+
@config.feature_enabled?(:classification)
|
17
|
+
end
|
18
|
+
|
19
|
+
def process_response?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_request(env)
|
24
|
+
request_data = @provider.build_request_data(env)
|
25
|
+
result = @provider.classify_request(request_data)
|
26
|
+
|
27
|
+
# Apply confidence threshold
|
28
|
+
threshold = @config.classification.confidence_threshold
|
29
|
+
if result[:confidence] < threshold
|
30
|
+
result[:classification] = :uncertain
|
31
|
+
result[:action] = :allow
|
32
|
+
else
|
33
|
+
result[:action] = determine_action(result[:classification])
|
34
|
+
end
|
35
|
+
|
36
|
+
# Add metadata
|
37
|
+
result[:feature] = @name
|
38
|
+
result[:timestamp] = Time.now.iso8601
|
39
|
+
result[:request_id] = generate_request_id(env)
|
40
|
+
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def determine_action(classification)
|
47
|
+
case classification
|
48
|
+
when :spam
|
49
|
+
:block
|
50
|
+
when :suspicious
|
51
|
+
@config.routing.smart_routing_enabled ? :route : :allow
|
52
|
+
when :bot
|
53
|
+
@config.routing.smart_routing_enabled ? :route : :allow
|
54
|
+
when :human
|
55
|
+
:allow
|
56
|
+
else
|
57
|
+
:allow
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def generate_request_id(env)
|
62
|
+
"#{Time.now.to_i}-#{env.object_id}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Features
|
6
|
+
class Enhancement
|
7
|
+
attr_reader :name, :provider, :config
|
8
|
+
|
9
|
+
def initialize(provider, config)
|
10
|
+
@name = :enhancement
|
11
|
+
@provider = provider
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def enabled?
|
16
|
+
@config.feature_enabled?(:enhancement)
|
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
|
+
# Enhancement is primarily a response-processing feature
|
27
|
+
{
|
28
|
+
feature: @name,
|
29
|
+
timestamp: Time.now.iso8601,
|
30
|
+
enhancement_ready: true
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_response(env, status, headers, body)
|
35
|
+
return { processed: false, reason: "disabled" } unless enabled?
|
36
|
+
return { processed: false, reason: "non_success_status" } unless (200..299).include?(status)
|
37
|
+
|
38
|
+
content_type = headers["Content-Type"] || ""
|
39
|
+
return { processed: false, reason: "unsupported_content_type" } unless enhanceable_content?(content_type)
|
40
|
+
|
41
|
+
# Extract content from response body
|
42
|
+
content = extract_content(body)
|
43
|
+
return { processed: false, reason: "no_content" } if content.empty?
|
44
|
+
|
45
|
+
# Determine enhancement type based on content and request context
|
46
|
+
enhancement_type = determine_enhancement_type(env, content_type, content)
|
47
|
+
return { processed: false, reason: "no_enhancement_needed" } unless enhancement_type
|
48
|
+
|
49
|
+
begin
|
50
|
+
# Apply AI enhancement
|
51
|
+
enhancement_result = @provider.enhance_content(content, enhancement_type)
|
52
|
+
|
53
|
+
# Update response body if enhancement was successful
|
54
|
+
if enhancement_result[:enhanced_content] &&
|
55
|
+
enhancement_result[:enhanced_content] != content
|
56
|
+
update_response_body(body, enhancement_result[:enhanced_content])
|
57
|
+
headers["X-AI-Enhanced"] = "true"
|
58
|
+
headers["X-AI-Enhancement-Type"] = enhancement_type.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
{
|
62
|
+
enhancement_applied: true,
|
63
|
+
enhancement_type: enhancement_type,
|
64
|
+
original_length: content.length,
|
65
|
+
enhanced_length: enhancement_result[:enhanced_content]&.length || content.length,
|
66
|
+
improvement_ratio: calculate_improvement_ratio(content, enhancement_result[:enhanced_content]),
|
67
|
+
feature: "#{@name}_response",
|
68
|
+
timestamp: Time.now.iso8601
|
69
|
+
}
|
70
|
+
rescue => e
|
71
|
+
Utils::Logger.warn("Content enhancement failed", error: e.message, enhancement_type: enhancement_type)
|
72
|
+
{
|
73
|
+
enhancement_applied: false,
|
74
|
+
error: e.message,
|
75
|
+
feature: "#{@name}_response",
|
76
|
+
timestamp: Time.now.iso8601
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def enhanceable_content?(content_type)
|
84
|
+
enhanceable_types = [
|
85
|
+
"text/html",
|
86
|
+
"text/plain",
|
87
|
+
"application/json",
|
88
|
+
"text/markdown"
|
89
|
+
]
|
90
|
+
|
91
|
+
enhanceable_types.any? { |type| content_type.include?(type) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def extract_content(body)
|
95
|
+
content = ""
|
96
|
+
if body.respond_to?(:each)
|
97
|
+
body.each { |chunk| content += chunk.to_s }
|
98
|
+
elsif body.respond_to?(:read)
|
99
|
+
content = body.read
|
100
|
+
body.rewind if body.respond_to?(:rewind)
|
101
|
+
else
|
102
|
+
content = body.to_s
|
103
|
+
end
|
104
|
+
content
|
105
|
+
end
|
106
|
+
|
107
|
+
def determine_enhancement_type(env, content_type, content)
|
108
|
+
path = env["PATH_INFO"] || ""
|
109
|
+
user_agent = env["HTTP_USER_AGENT"] || ""
|
110
|
+
|
111
|
+
# SEO enhancement for HTML pages
|
112
|
+
if content_type.include?("text/html") && needs_seo_enhancement?(content)
|
113
|
+
return :seo
|
114
|
+
end
|
115
|
+
|
116
|
+
# Readability enhancement for long text content
|
117
|
+
if content.length > 1000 && needs_readability_improvement?(content)
|
118
|
+
return :readability
|
119
|
+
end
|
120
|
+
|
121
|
+
# Accessibility enhancement for HTML
|
122
|
+
if content_type.include?("text/html") && needs_accessibility_improvement?(content)
|
123
|
+
return :accessibility
|
124
|
+
end
|
125
|
+
|
126
|
+
# API response optimization
|
127
|
+
if path.start_with?("/api/") && content_type.include?("application/json")
|
128
|
+
return :api_optimization
|
129
|
+
end
|
130
|
+
|
131
|
+
# Mobile optimization based on user agent
|
132
|
+
if mobile_user_agent?(user_agent) && content_type.include?("text/html")
|
133
|
+
return :mobile_optimization
|
134
|
+
end
|
135
|
+
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
|
139
|
+
def needs_seo_enhancement?(content)
|
140
|
+
# Simple heuristics for SEO improvement needs
|
141
|
+
return true if content.scan(/<title>/).length != 1
|
142
|
+
return true if content.scan(/<meta name="description"/).empty?
|
143
|
+
return true if content.scan(/<h1>/).length != 1
|
144
|
+
return true if content.scan(/<img[^>]*alt=/).length < content.scan(/<img/).length
|
145
|
+
|
146
|
+
false
|
147
|
+
end
|
148
|
+
|
149
|
+
def needs_readability_improvement?(content)
|
150
|
+
# Simple readability checks
|
151
|
+
sentences = content.split(/[.!?]+/)
|
152
|
+
return true if sentences.any? { |s| s.split.length > 25 } # Long sentences
|
153
|
+
|
154
|
+
# Check for complex words (more than 3 syllables - simplified)
|
155
|
+
words = content.downcase.scan(/\b[a-z]+\b/)
|
156
|
+
complex_words = words.select { |word| word.length > 10 }
|
157
|
+
return true if complex_words.length > words.length * 0.1
|
158
|
+
|
159
|
+
false
|
160
|
+
end
|
161
|
+
|
162
|
+
def needs_accessibility_improvement?(content)
|
163
|
+
# Basic accessibility checks
|
164
|
+
return true if content.scan(/<img[^>]*alt=/).length < content.scan(/<img/).length
|
165
|
+
return true if content.scan(/<input[^>]*aria-label=/).length < content.scan(/<input/).length
|
166
|
+
return true if content.scan(/<button[^>]*aria-label=/).length < content.scan(/<button/).length
|
167
|
+
|
168
|
+
false
|
169
|
+
end
|
170
|
+
|
171
|
+
def mobile_user_agent?(user_agent)
|
172
|
+
mobile_patterns = [
|
173
|
+
/Mobile/i,
|
174
|
+
/Android/i,
|
175
|
+
/iPhone/i,
|
176
|
+
/iPad/i,
|
177
|
+
/Windows Phone/i
|
178
|
+
]
|
179
|
+
|
180
|
+
mobile_patterns.any? { |pattern| user_agent.match?(pattern) }
|
181
|
+
end
|
182
|
+
|
183
|
+
def update_response_body(body, enhanced_content)
|
184
|
+
# Update the response body with enhanced content
|
185
|
+
if body.respond_to?(:clear) && body.respond_to?(:<<)
|
186
|
+
body.clear
|
187
|
+
body << enhanced_content
|
188
|
+
elsif body.is_a?(Array)
|
189
|
+
body.clear
|
190
|
+
body << enhanced_content
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def calculate_improvement_ratio(original, enhanced)
|
195
|
+
return 1.0 unless enhanced && original != enhanced
|
196
|
+
|
197
|
+
# Simple improvement metric based on length and complexity
|
198
|
+
original_complexity = calculate_complexity(original)
|
199
|
+
enhanced_complexity = calculate_complexity(enhanced)
|
200
|
+
|
201
|
+
return 1.0 if original_complexity == 0
|
202
|
+
|
203
|
+
enhanced_complexity.to_f / original_complexity
|
204
|
+
end
|
205
|
+
|
206
|
+
def calculate_complexity(text)
|
207
|
+
# Simple complexity score based on sentence length and word complexity
|
208
|
+
sentences = text.split(/[.!?]+/)
|
209
|
+
return 1 if sentences.empty?
|
210
|
+
|
211
|
+
avg_sentence_length = sentences.map(&:split).map(&:length).sum.to_f / sentences.length
|
212
|
+
long_words = text.scan(/\b\w{7,}\b/).length
|
213
|
+
|
214
|
+
(avg_sentence_length + long_words).to_i
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|