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,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
module AI
|
8
|
+
module Utils
|
9
|
+
class Logger
|
10
|
+
class << self
|
11
|
+
def logger
|
12
|
+
@logger ||= build_logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def debug(message, metadata = {})
|
16
|
+
log(:debug, message, metadata)
|
17
|
+
end
|
18
|
+
|
19
|
+
def info(message, metadata = {})
|
20
|
+
log(:info, message, metadata)
|
21
|
+
end
|
22
|
+
|
23
|
+
def warn(message, metadata = {})
|
24
|
+
log(:warn, message, metadata)
|
25
|
+
end
|
26
|
+
|
27
|
+
def error(message, metadata = {})
|
28
|
+
log(:error, message, metadata)
|
29
|
+
end
|
30
|
+
|
31
|
+
def fatal(message, metadata = {})
|
32
|
+
log(:fatal, message, metadata)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def log(level, message, metadata)
|
38
|
+
log_entry = {
|
39
|
+
timestamp: Time.now.iso8601,
|
40
|
+
level: level.to_s.upcase,
|
41
|
+
message: message,
|
42
|
+
component: "rack-ai",
|
43
|
+
metadata: sanitize_metadata(metadata)
|
44
|
+
}
|
45
|
+
|
46
|
+
logger.public_send(level, log_entry.to_json)
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_logger
|
50
|
+
logger = ::Logger.new($stdout)
|
51
|
+
logger.level = log_level_from_env
|
52
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
53
|
+
"#{datetime.iso8601} [#{severity}] #{progname}: #{msg}\n"
|
54
|
+
end
|
55
|
+
logger
|
56
|
+
end
|
57
|
+
|
58
|
+
def log_level_from_env
|
59
|
+
case ENV["RACK_AI_LOG_LEVEL"]&.downcase
|
60
|
+
when "debug"
|
61
|
+
::Logger::DEBUG
|
62
|
+
when "info"
|
63
|
+
::Logger::INFO
|
64
|
+
when "warn"
|
65
|
+
::Logger::WARN
|
66
|
+
when "error"
|
67
|
+
::Logger::ERROR
|
68
|
+
when "fatal"
|
69
|
+
::Logger::FATAL
|
70
|
+
else
|
71
|
+
::Logger::INFO
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def sanitize_metadata(metadata)
|
76
|
+
return {} unless metadata.is_a?(Hash)
|
77
|
+
|
78
|
+
sanitized = {}
|
79
|
+
metadata.each do |key, value|
|
80
|
+
sanitized[key] = sanitize_value(value)
|
81
|
+
end
|
82
|
+
sanitized
|
83
|
+
end
|
84
|
+
|
85
|
+
def sanitize_value(value)
|
86
|
+
case value
|
87
|
+
when String
|
88
|
+
# Truncate very long strings and sanitize sensitive data
|
89
|
+
sanitized = value.length > 1000 ? "#{value[0..997]}..." : value
|
90
|
+
sanitize_sensitive_data(sanitized)
|
91
|
+
when Hash
|
92
|
+
sanitize_metadata(value)
|
93
|
+
when Array
|
94
|
+
value.map { |v| sanitize_value(v) }
|
95
|
+
else
|
96
|
+
value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def sanitize_sensitive_data(text)
|
101
|
+
# Remove potential API keys, tokens, passwords
|
102
|
+
text.gsub(/\b[A-Za-z0-9]{20,}\b/, "[REDACTED]")
|
103
|
+
.gsub(/password[=:]\s*\S+/i, "password=[REDACTED]")
|
104
|
+
.gsub(/token[=:]\s*\S+/i, "token=[REDACTED]")
|
105
|
+
.gsub(/key[=:]\s*\S+/i, "key=[REDACTED]")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Utils
|
6
|
+
class Metrics
|
7
|
+
class << self
|
8
|
+
def initialize_metrics
|
9
|
+
@metrics = {}
|
10
|
+
@counters = {}
|
11
|
+
@histograms = {}
|
12
|
+
@gauges = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def record(metric_name, value, tags = {})
|
16
|
+
initialize_metrics unless @metrics
|
17
|
+
|
18
|
+
@metrics[metric_name] ||= []
|
19
|
+
@metrics[metric_name] << {
|
20
|
+
value: value,
|
21
|
+
timestamp: Time.now.to_f,
|
22
|
+
tags: tags
|
23
|
+
}
|
24
|
+
|
25
|
+
# Keep only last 1000 measurements per metric
|
26
|
+
@metrics[metric_name] = @metrics[metric_name].last(1000)
|
27
|
+
end
|
28
|
+
|
29
|
+
def increment(counter_name, value = 1, tags = {})
|
30
|
+
initialize_metrics unless @counters
|
31
|
+
|
32
|
+
key = build_key(counter_name, tags)
|
33
|
+
@counters[key] ||= 0
|
34
|
+
@counters[key] += value
|
35
|
+
end
|
36
|
+
|
37
|
+
def decrement(counter_name, value = 1, tags = {})
|
38
|
+
increment(counter_name, -value, tags)
|
39
|
+
end
|
40
|
+
|
41
|
+
def histogram(metric_name, value, tags = {})
|
42
|
+
initialize_metrics unless @histograms
|
43
|
+
|
44
|
+
key = build_key(metric_name, tags)
|
45
|
+
@histograms[key] ||= []
|
46
|
+
@histograms[key] << value
|
47
|
+
|
48
|
+
# Keep only last 1000 values per histogram
|
49
|
+
@histograms[key] = @histograms[key].last(1000)
|
50
|
+
end
|
51
|
+
|
52
|
+
def gauge(metric_name, value, tags = {})
|
53
|
+
initialize_metrics unless @gauges
|
54
|
+
|
55
|
+
key = build_key(metric_name, tags)
|
56
|
+
@gauges[key] = {
|
57
|
+
value: value,
|
58
|
+
timestamp: Time.now.to_f,
|
59
|
+
tags: tags
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_counter(counter_name, tags = {})
|
64
|
+
return 0 unless @counters
|
65
|
+
|
66
|
+
key = build_key(counter_name, tags)
|
67
|
+
@counters[key] || 0
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_histogram_stats(metric_name, tags = {})
|
71
|
+
return {} unless @histograms
|
72
|
+
|
73
|
+
key = build_key(metric_name, tags)
|
74
|
+
values = @histograms[key] || []
|
75
|
+
return {} if values.empty?
|
76
|
+
|
77
|
+
sorted_values = values.sort
|
78
|
+
{
|
79
|
+
count: values.length,
|
80
|
+
min: sorted_values.first,
|
81
|
+
max: sorted_values.last,
|
82
|
+
mean: values.sum.to_f / values.length,
|
83
|
+
median: percentile(sorted_values, 50),
|
84
|
+
p95: percentile(sorted_values, 95),
|
85
|
+
p99: percentile(sorted_values, 99)
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_gauge(metric_name, tags = {})
|
90
|
+
return nil unless @gauges
|
91
|
+
|
92
|
+
key = build_key(metric_name, tags)
|
93
|
+
@gauges[key]
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_metrics_summary
|
97
|
+
{
|
98
|
+
counters: @counters&.keys&.length || 0,
|
99
|
+
histograms: @histograms&.keys&.length || 0,
|
100
|
+
gauges: @gauges&.keys&.length || 0,
|
101
|
+
metrics: @metrics&.keys&.length || 0,
|
102
|
+
timestamp: Time.now.iso8601
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def export_prometheus_format
|
107
|
+
lines = []
|
108
|
+
|
109
|
+
# Export counters
|
110
|
+
@counters&.each do |key, value|
|
111
|
+
metric_name, tags = parse_key(key)
|
112
|
+
tags_str = format_prometheus_tags(tags)
|
113
|
+
lines << "# TYPE #{metric_name} counter"
|
114
|
+
lines << "#{metric_name}#{tags_str} #{value}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# Export gauges
|
118
|
+
@gauges&.each do |key, data|
|
119
|
+
metric_name, tags = parse_key(key)
|
120
|
+
tags_str = format_prometheus_tags(tags)
|
121
|
+
lines << "# TYPE #{metric_name} gauge"
|
122
|
+
lines << "#{metric_name}#{tags_str} #{data[:value]}"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Export histogram summaries
|
126
|
+
@histograms&.each do |key, values|
|
127
|
+
next if values.empty?
|
128
|
+
|
129
|
+
metric_name, tags = parse_key(key)
|
130
|
+
stats = calculate_histogram_stats(values)
|
131
|
+
tags_str = format_prometheus_tags(tags)
|
132
|
+
|
133
|
+
lines << "# TYPE #{metric_name} histogram"
|
134
|
+
lines << "#{metric_name}_count#{tags_str} #{stats[:count]}"
|
135
|
+
lines << "#{metric_name}_sum#{tags_str} #{stats[:sum]}"
|
136
|
+
|
137
|
+
[0.5, 0.9, 0.95, 0.99].each do |quantile|
|
138
|
+
quantile_tags = tags.merge(quantile: quantile)
|
139
|
+
quantile_tags_str = format_prometheus_tags(quantile_tags)
|
140
|
+
lines << "#{metric_name}#{quantile_tags_str} #{stats[:"p#{(quantile * 100).to_i}"]}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
lines.join("\n")
|
145
|
+
end
|
146
|
+
|
147
|
+
def reset_metrics!
|
148
|
+
@metrics = {}
|
149
|
+
@counters = {}
|
150
|
+
@histograms = {}
|
151
|
+
@gauges = {}
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def build_key(metric_name, tags)
|
157
|
+
return metric_name.to_s if tags.empty?
|
158
|
+
|
159
|
+
tag_string = tags.map { |k, v| "#{k}=#{v}" }.sort.join(",")
|
160
|
+
"#{metric_name}|#{tag_string}"
|
161
|
+
end
|
162
|
+
|
163
|
+
def parse_key(key)
|
164
|
+
parts = key.split("|", 2)
|
165
|
+
metric_name = parts[0]
|
166
|
+
|
167
|
+
if parts[1]
|
168
|
+
tags = {}
|
169
|
+
parts[1].split(",").each do |tag_pair|
|
170
|
+
k, v = tag_pair.split("=", 2)
|
171
|
+
tags[k.to_sym] = v
|
172
|
+
end
|
173
|
+
[metric_name, tags]
|
174
|
+
else
|
175
|
+
[metric_name, {}]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def percentile(sorted_values, percentile)
|
180
|
+
return 0 if sorted_values.empty?
|
181
|
+
|
182
|
+
index = (percentile / 100.0) * (sorted_values.length - 1)
|
183
|
+
|
184
|
+
if index == index.to_i
|
185
|
+
sorted_values[index.to_i]
|
186
|
+
else
|
187
|
+
lower = sorted_values[index.floor]
|
188
|
+
upper = sorted_values[index.ceil]
|
189
|
+
lower + (upper - lower) * (index - index.floor)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def calculate_histogram_stats(values)
|
194
|
+
sorted_values = values.sort
|
195
|
+
{
|
196
|
+
count: values.length,
|
197
|
+
sum: values.sum,
|
198
|
+
min: sorted_values.first,
|
199
|
+
max: sorted_values.last,
|
200
|
+
p50: percentile(sorted_values, 50),
|
201
|
+
p90: percentile(sorted_values, 90),
|
202
|
+
p95: percentile(sorted_values, 95),
|
203
|
+
p99: percentile(sorted_values, 99)
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
def format_prometheus_tags(tags)
|
208
|
+
return "" if tags.empty?
|
209
|
+
|
210
|
+
tag_pairs = tags.map { |k, v| "#{k}=\"#{v}\"" }
|
211
|
+
"{#{tag_pairs.join(",")}}"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Initialize on first load
|
216
|
+
initialize_metrics
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Utils
|
6
|
+
class Sanitizer
|
7
|
+
SENSITIVE_PATTERNS = [
|
8
|
+
# API Keys and tokens
|
9
|
+
/\b[A-Za-z0-9]{20,}\b/,
|
10
|
+
/sk-[A-Za-z0-9]{48}/, # OpenAI API keys
|
11
|
+
/xoxb-[A-Za-z0-9-]+/, # Slack tokens
|
12
|
+
/ghp_[A-Za-z0-9]{36}/, # GitHub tokens
|
13
|
+
|
14
|
+
# Credit card numbers
|
15
|
+
/\b(?:\d{4}[-\s]?){3}\d{4}\b/,
|
16
|
+
|
17
|
+
# Social Security Numbers
|
18
|
+
/\b\d{3}-\d{2}-\d{4}\b/,
|
19
|
+
|
20
|
+
# Email addresses (partial)
|
21
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
|
22
|
+
|
23
|
+
# Phone numbers
|
24
|
+
/\b\d{3}-\d{3}-\d{4}\b/,
|
25
|
+
/\(\d{3}\)\s*\d{3}-\d{4}/,
|
26
|
+
|
27
|
+
# IP addresses (private ranges)
|
28
|
+
/\b(?:10\.|172\.(?:1[6-9]|2[0-9]|3[01])\.|192\.168\.)\d{1,3}\.\d{1,3}\b/
|
29
|
+
].freeze
|
30
|
+
|
31
|
+
SENSITIVE_HEADERS = %w[
|
32
|
+
authorization
|
33
|
+
x-api-key
|
34
|
+
x-auth-token
|
35
|
+
cookie
|
36
|
+
set-cookie
|
37
|
+
x-session-id
|
38
|
+
x-csrf-token
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def sanitize_request_data(env)
|
43
|
+
sanitized_env = {}
|
44
|
+
|
45
|
+
env.each do |key, value|
|
46
|
+
if sensitive_header?(key)
|
47
|
+
sanitized_env[key] = "[REDACTED]"
|
48
|
+
elsif key == "rack.input" && value.respond_to?(:read)
|
49
|
+
# Don't include request body in sanitized data
|
50
|
+
sanitized_env[key] = "[REQUEST_BODY]"
|
51
|
+
else
|
52
|
+
sanitized_env[key] = sanitize_value(value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
sanitized_env
|
57
|
+
end
|
58
|
+
|
59
|
+
def sanitize_response_data(headers, body)
|
60
|
+
sanitized_headers = {}
|
61
|
+
headers.each do |key, value|
|
62
|
+
if sensitive_header?(key.downcase)
|
63
|
+
sanitized_headers[key] = "[REDACTED]"
|
64
|
+
else
|
65
|
+
sanitized_headers[key] = sanitize_value(value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
sanitized_body = sanitize_body(body)
|
70
|
+
|
71
|
+
{
|
72
|
+
headers: sanitized_headers,
|
73
|
+
body: sanitized_body
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def sanitize_for_ai_processing(data)
|
78
|
+
case data
|
79
|
+
when Hash
|
80
|
+
sanitized = {}
|
81
|
+
data.each do |key, value|
|
82
|
+
sanitized[key] = sanitize_for_ai_processing(value)
|
83
|
+
end
|
84
|
+
sanitized
|
85
|
+
when Array
|
86
|
+
data.map { |item| sanitize_for_ai_processing(item) }
|
87
|
+
when String
|
88
|
+
sanitize_string_for_ai(data)
|
89
|
+
else
|
90
|
+
data
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def extract_safe_content(content, max_length = 1000)
|
95
|
+
return "" if content.nil? || content.empty?
|
96
|
+
|
97
|
+
# Remove sensitive patterns
|
98
|
+
safe_content = sanitize_string_for_ai(content.to_s)
|
99
|
+
|
100
|
+
# Truncate if too long
|
101
|
+
if safe_content.length > max_length
|
102
|
+
safe_content = safe_content[0..max_length-4] + "..."
|
103
|
+
end
|
104
|
+
|
105
|
+
safe_content
|
106
|
+
end
|
107
|
+
|
108
|
+
def redact_pii(text)
|
109
|
+
return text unless text.is_a?(String)
|
110
|
+
|
111
|
+
redacted = text.dup
|
112
|
+
|
113
|
+
# Redact email addresses
|
114
|
+
redacted.gsub!(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, "[EMAIL]")
|
115
|
+
|
116
|
+
# Redact phone numbers
|
117
|
+
redacted.gsub!(/\b\d{3}-\d{3}-\d{4}\b/, "[PHONE]")
|
118
|
+
redacted.gsub!(/\(\d{3}\)\s*\d{3}-\d{4}/, "[PHONE]")
|
119
|
+
|
120
|
+
# Redact SSN
|
121
|
+
redacted.gsub!(/\b\d{3}-\d{2}-\d{4}\b/, "[SSN]")
|
122
|
+
|
123
|
+
# Redact credit card numbers
|
124
|
+
redacted.gsub!(/\b(?:\d{4}[-\s]?){3}\d{4}\b/, "[CREDIT_CARD]")
|
125
|
+
|
126
|
+
redacted
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def sensitive_header?(header_name)
|
132
|
+
normalized = header_name.to_s.downcase.gsub(/^http_/, "").gsub(/_/, "-")
|
133
|
+
SENSITIVE_HEADERS.include?(normalized)
|
134
|
+
end
|
135
|
+
|
136
|
+
def sanitize_value(value)
|
137
|
+
case value
|
138
|
+
when String
|
139
|
+
sanitize_string(value)
|
140
|
+
when Hash
|
141
|
+
value.transform_values { |v| sanitize_value(v) }
|
142
|
+
when Array
|
143
|
+
value.map { |v| sanitize_value(v) }
|
144
|
+
else
|
145
|
+
value
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def sanitize_string(str)
|
150
|
+
return str if str.length < 10 # Skip very short strings
|
151
|
+
|
152
|
+
sanitized = str.dup
|
153
|
+
SENSITIVE_PATTERNS.each do |pattern|
|
154
|
+
sanitized.gsub!(pattern, "[REDACTED]")
|
155
|
+
end
|
156
|
+
sanitized
|
157
|
+
end
|
158
|
+
|
159
|
+
def sanitize_string_for_ai(str)
|
160
|
+
return str if str.length < 10
|
161
|
+
|
162
|
+
# More aggressive sanitization for AI processing
|
163
|
+
sanitized = str.dup
|
164
|
+
|
165
|
+
# Remove all potential sensitive patterns
|
166
|
+
SENSITIVE_PATTERNS.each do |pattern|
|
167
|
+
sanitized.gsub!(pattern, "[REDACTED]")
|
168
|
+
end
|
169
|
+
|
170
|
+
# Remove common password/key patterns
|
171
|
+
sanitized.gsub!(/password[=:]\s*\S+/i, "password=[REDACTED]")
|
172
|
+
sanitized.gsub!(/token[=:]\s*\S+/i, "token=[REDACTED]")
|
173
|
+
sanitized.gsub!(/key[=:]\s*\S+/i, "key=[REDACTED]")
|
174
|
+
sanitized.gsub!(/secret[=:]\s*\S+/i, "secret=[REDACTED]")
|
175
|
+
|
176
|
+
# Remove base64 encoded data (potential tokens)
|
177
|
+
sanitized.gsub!(/[A-Za-z0-9+\/]{40,}={0,2}/, "[BASE64_DATA]")
|
178
|
+
|
179
|
+
sanitized
|
180
|
+
end
|
181
|
+
|
182
|
+
def sanitize_body(body)
|
183
|
+
return "[EMPTY_BODY]" unless body
|
184
|
+
|
185
|
+
if body.respond_to?(:each)
|
186
|
+
content = ""
|
187
|
+
body.each { |chunk| content += chunk.to_s }
|
188
|
+
else
|
189
|
+
content = body.to_s
|
190
|
+
end
|
191
|
+
|
192
|
+
# Only include first 500 chars of body for logging
|
193
|
+
truncated = content.length > 500 ? "#{content[0..497]}..." : content
|
194
|
+
sanitize_string_for_ai(truncated)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/lib/rack/ai.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ai/version"
|
4
|
+
require_relative "ai/configuration"
|
5
|
+
require_relative "ai/middleware"
|
6
|
+
require_relative "ai/providers/base"
|
7
|
+
require_relative "ai/providers/openai"
|
8
|
+
require_relative "ai/providers/huggingface"
|
9
|
+
require_relative "ai/providers/local"
|
10
|
+
require_relative "ai/features/classification"
|
11
|
+
require_relative "ai/features/moderation"
|
12
|
+
require_relative "ai/features/caching"
|
13
|
+
require_relative "ai/features/routing"
|
14
|
+
require_relative "ai/features/logging"
|
15
|
+
require_relative "ai/features/enhancement"
|
16
|
+
require_relative "ai/features/security"
|
17
|
+
require_relative "ai/utils/logger"
|
18
|
+
require_relative "ai/utils/metrics"
|
19
|
+
require_relative "ai/utils/sanitizer"
|
20
|
+
|
21
|
+
module Rack
|
22
|
+
module AI
|
23
|
+
class Error < StandardError; end
|
24
|
+
class ConfigurationError < Error; end
|
25
|
+
class ProviderError < Error; end
|
26
|
+
class FeatureError < Error; end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# Global configuration
|
30
|
+
def configure
|
31
|
+
yield(configuration)
|
32
|
+
end
|
33
|
+
|
34
|
+
def configuration
|
35
|
+
@configuration ||= Configuration.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset_configuration!
|
39
|
+
@configuration = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# Convenience method for creating middleware
|
43
|
+
def middleware(**options)
|
44
|
+
Middleware.new(nil, **options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/rack-ai.gemspec
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/rack/ai/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "rack-ai"
|
7
|
+
spec.version = Rack::AI::VERSION
|
8
|
+
spec.authors = ["Ahmet KAHRAMAN"]
|
9
|
+
spec.email = ["ahmetxhero@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "AI-powered middleware for Rack applications"
|
12
|
+
spec.description = "Extends Rack with AI capabilities including request classification, content moderation, smart caching, and security features"
|
13
|
+
spec.homepage = "https://github.com/ahmetxhero/rack-ai"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/ahmetxhero/rack-ai"
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/ahmetxhero/rack-ai/blob/main/CHANGELOG.md"
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
# Dependencies
|
34
|
+
spec.add_dependency "rack", ">= 2.0", "< 4.0"
|
35
|
+
spec.add_dependency "concurrent-ruby", "~> 1.1"
|
36
|
+
spec.add_dependency "dry-configurable", "~> 1.0"
|
37
|
+
spec.add_dependency "dry-validation", "~> 1.10"
|
38
|
+
spec.add_dependency "faraday", "~> 2.0"
|
39
|
+
spec.add_dependency "faraday-retry", "~> 2.0"
|
40
|
+
spec.add_dependency "redis", "~> 5.0"
|
41
|
+
|
42
|
+
# Development dependencies
|
43
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
44
|
+
spec.add_development_dependency "rack-test", "~> 2.0"
|
45
|
+
spec.add_development_dependency "webmock", "~> 3.18"
|
46
|
+
spec.add_development_dependency "benchmark-ips", "~> 2.12"
|
47
|
+
spec.add_development_dependency "rubocop", "~> 1.50"
|
48
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.20"
|
49
|
+
spec.add_development_dependency "simplecov", "~> 0.22"
|
50
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
51
|
+
end
|