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.
@@ -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