tork-governance 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 66f973f8bfa19b713628ce8bb3942b67fba1bbdf6a138c55ec56060ffb2e0c2e
4
+ data.tar.gz: 4f9d8086d72580bac59d31397cb62d5ce21a3ca053f6b54ba9b4df69d73a410a
5
+ SHA512:
6
+ metadata.gz: fc1120abb85021b2e921f577925e469a6b4a6daa69389136ab96be1720e99f715977bc8041277b3931c0d109d19c2f107ea2c81ee0b4fe0af3e1e2ae0197552e
7
+ data.tar.gz: 967c1f7abdfdf9274c994487921de2e314fef6dcdaf7849164d5b72a99191138e099e17967b8b6085b03ec6882b29aaa676f11e8cc56469aa0e24d35008c3c1e
data/README.md ADDED
@@ -0,0 +1,425 @@
1
+ # Tork AI Governance SDK for Ruby
2
+
3
+ Official Ruby SDK for the [Tork AI Governance Platform](https://tork.network). Provides comprehensive tools for AI safety, content moderation, PII detection, policy enforcement, and compliance monitoring for LLM applications.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/tork-governance.svg)](https://badge.fury.io/rb/tork-governance)
6
+ [![Build Status](https://github.com/torkjacobs/tork-ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/torkjacobs/tork-ruby-sdk/actions)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'tork-governance'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+
23
+ Or install it yourself as:
24
+
25
+ ```bash
26
+ gem install tork-governance
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```ruby
32
+ require 'tork'
33
+
34
+ # Configure with your API key
35
+ Tork.configure do |config|
36
+ config.api_key = 'tork_your_api_key'
37
+ end
38
+
39
+ # Evaluate content
40
+ result = Tork.evaluate(prompt: "What is the capital of France?")
41
+ puts result['data']['passed'] # => true
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ ### Global Configuration
47
+
48
+ ```ruby
49
+ Tork.configure do |config|
50
+ config.api_key = 'tork_your_api_key'
51
+ config.base_url = 'https://api.tork.network/v1' # Default
52
+ config.timeout = 30 # Request timeout in seconds
53
+ config.max_retries = 3 # Max retry attempts
54
+ config.retry_base_delay = 0.5 # Base delay for exponential backoff
55
+ config.raise_on_rate_limit = true # Raise exception on rate limit
56
+ config.logger = Logger.new(STDOUT) # Enable logging
57
+ end
58
+ ```
59
+
60
+ ### Environment Variable
61
+
62
+ You can also set the API key via environment variable:
63
+
64
+ ```bash
65
+ export TORK_API_KEY=tork_your_api_key
66
+ ```
67
+
68
+ ### Per-Client Configuration
69
+
70
+ ```ruby
71
+ client = Tork::Client.new(
72
+ api_key: 'tork_different_key',
73
+ base_url: 'https://custom.api.com'
74
+ )
75
+ ```
76
+
77
+ ## Usage
78
+
79
+ ### Content Evaluation
80
+
81
+ ```ruby
82
+ client = Tork::Client.new(api_key: 'tork_your_api_key')
83
+
84
+ # Basic evaluation
85
+ result = client.evaluate(prompt: "Hello, how are you?")
86
+
87
+ # Evaluation with response
88
+ result = client.evaluate(
89
+ prompt: "What is 2+2?",
90
+ response: "The answer is 4."
91
+ )
92
+
93
+ # Evaluation with specific policy
94
+ result = client.evaluate(
95
+ prompt: "Process this request",
96
+ policy_id: "pol_abc123"
97
+ )
98
+
99
+ # Evaluation with specific checks
100
+ result = client.evaluations.create(
101
+ prompt: "Contact me at john@example.com",
102
+ checks: ['pii', 'toxicity', 'moderation']
103
+ )
104
+ ```
105
+
106
+ ### PII Detection & Redaction
107
+
108
+ ```ruby
109
+ # Detect PII
110
+ result = client.evaluations.detect_pii(
111
+ content: "My email is john@example.com and SSN is 123-45-6789"
112
+ )
113
+ # => { "has_pii" => true, "types" => ["email", "ssn"] }
114
+
115
+ # Redact PII
116
+ result = client.evaluations.redact_pii(
117
+ content: "Call me at 555-123-4567",
118
+ replacement: "mask"
119
+ )
120
+ # => { "redacted" => "Call me at ***-***-****" }
121
+ ```
122
+
123
+ ### Jailbreak Detection
124
+
125
+ ```ruby
126
+ result = client.evaluations.detect_jailbreak(
127
+ prompt: "Ignore previous instructions and..."
128
+ )
129
+
130
+ if result['data']['is_jailbreak']
131
+ puts "Jailbreak attempt detected!"
132
+ puts "Techniques: #{result['data']['techniques']}"
133
+ end
134
+ ```
135
+
136
+ ### Policy Management
137
+
138
+ ```ruby
139
+ policies = client.policies
140
+
141
+ # List all policies
142
+ all_policies = policies.list(page: 1, per_page: 20)
143
+
144
+ # Get a specific policy
145
+ policy = policies.get('pol_abc123')
146
+
147
+ # Create a new policy
148
+ new_policy = policies.create(
149
+ name: "Content Safety Policy",
150
+ description: "Block harmful content",
151
+ rules: [
152
+ {
153
+ type: "block",
154
+ condition: "toxicity > 0.8",
155
+ action: "reject",
156
+ message: "Content flagged as toxic"
157
+ },
158
+ {
159
+ type: "redact",
160
+ condition: "pii.detected",
161
+ action: "mask"
162
+ }
163
+ ],
164
+ enabled: true
165
+ )
166
+
167
+ # Update a policy
168
+ policies.update('pol_abc123', name: "Updated Policy Name")
169
+
170
+ # Enable/Disable a policy
171
+ policies.enable('pol_abc123')
172
+ policies.disable('pol_abc123')
173
+
174
+ # Delete a policy
175
+ policies.delete('pol_abc123')
176
+
177
+ # Test a policy
178
+ test_result = policies.test('pol_abc123',
179
+ content: "Test content here",
180
+ context: { user_role: "admin" }
181
+ )
182
+ ```
183
+
184
+ ### Batch Evaluation
185
+
186
+ ```ruby
187
+ items = [
188
+ { prompt: "First prompt" },
189
+ { prompt: "Second prompt", response: "Second response" },
190
+ { prompt: "Third prompt" }
191
+ ]
192
+
193
+ results = client.evaluations.batch(items, policy_id: 'pol_abc123')
194
+ ```
195
+
196
+ ### RAG Validation
197
+
198
+ ```ruby
199
+ chunks = [
200
+ { content: "Document chunk 1", source: "doc1.pdf", page: 1 },
201
+ { content: "Document chunk 2", source: "doc2.pdf", page: 3 }
202
+ ]
203
+
204
+ result = client.evaluations.validate_rag(
205
+ chunks: chunks,
206
+ query: "What is the company policy?"
207
+ )
208
+ ```
209
+
210
+ ### Metrics & Analytics
211
+
212
+ ```ruby
213
+ metrics = client.metrics
214
+
215
+ # Get Torking X score for an evaluation
216
+ score = metrics.torking_x(evaluation_id: 'eval_abc123')
217
+
218
+ # Get usage statistics
219
+ usage = metrics.usage(period: 'month')
220
+
221
+ # Get policy performance
222
+ performance = metrics.policy_performance(policy_id: 'pol_abc123')
223
+
224
+ # Get violation statistics
225
+ violations = metrics.violations(period: 'week', group_by: 'type')
226
+
227
+ # Get dashboard summary
228
+ dashboard = metrics.dashboard
229
+
230
+ # Get latency metrics
231
+ latency = metrics.latency(period: 'day', percentiles: [50, 95, 99])
232
+
233
+ # Export metrics
234
+ export = metrics.export(
235
+ type: 'usage',
236
+ start_date: '2024-01-01',
237
+ end_date: '2024-01-31',
238
+ format: 'csv'
239
+ )
240
+ ```
241
+
242
+ ## Error Handling
243
+
244
+ ```ruby
245
+ begin
246
+ result = client.evaluate(prompt: "Test content")
247
+ rescue Tork::AuthenticationError => e
248
+ puts "Invalid API key: #{e.message}"
249
+ rescue Tork::RateLimitError => e
250
+ puts "Rate limited. Retry after #{e.retry_after} seconds"
251
+ rescue Tork::ValidationError => e
252
+ puts "Validation failed: #{e.message}"
253
+ puts "Details: #{e.details}"
254
+ rescue Tork::PolicyViolationError => e
255
+ puts "Policy violation: #{e.message}"
256
+ puts "Violations: #{e.violations}"
257
+ rescue Tork::NotFoundError => e
258
+ puts "Resource not found: #{e.message}"
259
+ rescue Tork::ServerError => e
260
+ puts "Server error: #{e.message}"
261
+ rescue Tork::TimeoutError => e
262
+ puts "Request timed out"
263
+ rescue Tork::ConnectionError => e
264
+ puts "Connection failed"
265
+ rescue Tork::Error => e
266
+ puts "Tork error: #{e.message}"
267
+ end
268
+ ```
269
+
270
+ ## Rails Integration
271
+
272
+ ### Initializer
273
+
274
+ Create `config/initializers/tork.rb`:
275
+
276
+ ```ruby
277
+ Tork.configure do |config|
278
+ config.api_key = Rails.application.credentials.tork_api_key
279
+ config.logger = Rails.logger
280
+ config.timeout = 30
281
+ end
282
+ ```
283
+
284
+ ### Controller Example
285
+
286
+ ```ruby
287
+ class MessagesController < ApplicationController
288
+ def create
289
+ result = Tork.evaluate(
290
+ prompt: params[:content],
291
+ policy_id: current_user.organization.policy_id
292
+ )
293
+
294
+ if result['data']['passed']
295
+ @message = Message.create!(content: params[:content])
296
+ render json: @message
297
+ else
298
+ render json: {
299
+ error: 'Content blocked',
300
+ violations: result['data']['violations']
301
+ }, status: :unprocessable_entity
302
+ end
303
+ rescue Tork::RateLimitError => e
304
+ render json: { error: 'Rate limited' }, status: :too_many_requests
305
+ end
306
+ end
307
+ ```
308
+
309
+ ### Background Job Example
310
+
311
+ ```ruby
312
+ class ContentModerationJob < ApplicationJob
313
+ queue_as :default
314
+
315
+ def perform(message_id)
316
+ message = Message.find(message_id)
317
+
318
+ result = Tork.evaluate(
319
+ prompt: message.content,
320
+ checks: ['toxicity', 'pii']
321
+ )
322
+
323
+ if result['data']['violations'].any?
324
+ message.update!(
325
+ flagged: true,
326
+ moderation_result: result['data']
327
+ )
328
+
329
+ NotificationService.notify_moderators(message)
330
+ end
331
+ end
332
+ end
333
+ ```
334
+
335
+ ## Logging
336
+
337
+ Enable detailed logging for debugging:
338
+
339
+ ```ruby
340
+ require 'logger'
341
+
342
+ Tork.configure do |config|
343
+ config.api_key = 'tork_your_api_key'
344
+ config.logger = Logger.new(STDOUT)
345
+ end
346
+ ```
347
+
348
+ ## Retry Behavior
349
+
350
+ The SDK automatically retries failed requests with exponential backoff:
351
+
352
+ - **Retryable status codes**: 408, 500, 502, 503, 504
353
+ - **Default max retries**: 3
354
+ - **Default base delay**: 0.5 seconds
355
+ - **Backoff factor**: 2x
356
+ - **Jitter**: 50% randomness
357
+
358
+ Customize retry behavior:
359
+
360
+ ```ruby
361
+ Tork.configure do |config|
362
+ config.max_retries = 5
363
+ config.retry_base_delay = 1.0
364
+ config.retry_max_delay = 60.0
365
+ end
366
+ ```
367
+
368
+ ## Thread Safety
369
+
370
+ The SDK is thread-safe. Each `Tork::Client` instance maintains its own connection pool.
371
+
372
+ ```ruby
373
+ # Shared client (thread-safe)
374
+ client = Tork::Client.new(api_key: 'tork_your_api_key')
375
+
376
+ threads = 10.times.map do |i|
377
+ Thread.new do
378
+ client.evaluate(prompt: "Thread #{i} content")
379
+ end
380
+ end
381
+
382
+ threads.each(&:join)
383
+ ```
384
+
385
+ ## Development
386
+
387
+ After checking out the repo:
388
+
389
+ ```bash
390
+ # Install dependencies
391
+ bundle install
392
+
393
+ # Run tests
394
+ bundle exec rspec
395
+
396
+ # Run linter
397
+ bundle exec rubocop
398
+
399
+ # Generate documentation
400
+ bundle exec rake doc
401
+
402
+ # Build the gem
403
+ bundle exec rake build
404
+
405
+ # Install locally
406
+ bundle exec rake install
407
+ ```
408
+
409
+ ## Contributing
410
+
411
+ 1. Fork the repository
412
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
413
+ 3. Commit your changes (`git commit -am 'Add amazing feature'`)
414
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
415
+ 5. Open a Pull Request
416
+
417
+ ## License
418
+
419
+ This gem is available as open source under the [MIT License](https://opensource.org/licenses/MIT).
420
+
421
+ ## Support
422
+
423
+ - **Documentation**: [docs.tork.network](https://docs.tork.network)
424
+ - **Email**: support@tork.network
425
+ - **Issues**: [GitHub Issues](https://github.com/torkjacobs/tork-ruby-sdk/issues)
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
11
+
12
+ desc "Run tests with coverage"
13
+ task :coverage do
14
+ ENV["COVERAGE"] = "true"
15
+ Rake::Task[:spec].invoke
16
+ end
17
+
18
+ desc "Generate documentation"
19
+ task :doc do
20
+ sh "yard doc --output-dir doc lib/**/*.rb"
21
+ end
22
+
23
+ desc "Open documentation in browser"
24
+ task :doc_server do
25
+ sh "yard server --reload"
26
+ end
27
+
28
+ desc "Build and install gem locally"
29
+ task :local_install do
30
+ sh "gem build tork-governance.gemspec"
31
+ sh "gem install tork-governance-#{Tork::VERSION}.gem"
32
+ end
33
+
34
+ desc "Release gem to RubyGems"
35
+ task :publish do
36
+ sh "gem build tork-governance.gemspec"
37
+ sh "gem push tork-governance-#{Tork::VERSION}.gem"
38
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "json"
6
+
7
+ module Tork
8
+ # HTTP client for interacting with the Tork API
9
+ class Client
10
+ attr_reader :config
11
+
12
+ # Initialize a new client
13
+ # @param api_key [String, nil] API key (uses config if not provided)
14
+ # @param base_url [String, nil] Base URL (uses config if not provided)
15
+ # @param config [Configuration, nil] Configuration object
16
+ def initialize(api_key: nil, base_url: nil, config: nil)
17
+ @config = config || Tork.configuration.dup
18
+ @config.api_key = api_key if api_key
19
+ @config.base_url = base_url if base_url
20
+ @config.validate!
21
+
22
+ @connection = build_connection
23
+ end
24
+
25
+ # Access policy resources
26
+ # @return [Resources::Policy]
27
+ def policies
28
+ @policies ||= Resources::Policy.new(self)
29
+ end
30
+
31
+ # Access evaluation resources
32
+ # @return [Resources::Evaluation]
33
+ def evaluations
34
+ @evaluations ||= Resources::Evaluation.new(self)
35
+ end
36
+
37
+ # Access metrics resources
38
+ # @return [Resources::Metrics]
39
+ def metrics
40
+ @metrics ||= Resources::Metrics.new(self)
41
+ end
42
+
43
+ # Shorthand for evaluating content
44
+ # @param prompt [String] The prompt to evaluate
45
+ # @param response [String, nil] The response to evaluate
46
+ # @param policy_id [String, nil] Policy ID to use
47
+ # @param options [Hash] Additional options
48
+ # @return [Hash] Evaluation result
49
+ def evaluate(prompt:, response: nil, policy_id: nil, **options)
50
+ evaluations.create(
51
+ prompt: prompt,
52
+ response: response,
53
+ policy_id: policy_id,
54
+ **options
55
+ )
56
+ end
57
+
58
+ # Make a GET request
59
+ # @param path [String] API path
60
+ # @param params [Hash] Query parameters
61
+ # @return [Hash] Parsed response
62
+ def get(path, params = {})
63
+ request(:get, path, params: params)
64
+ end
65
+
66
+ # Make a POST request
67
+ # @param path [String] API path
68
+ # @param body [Hash] Request body
69
+ # @return [Hash] Parsed response
70
+ def post(path, body = {})
71
+ request(:post, path, body: body)
72
+ end
73
+
74
+ # Make a PUT request
75
+ # @param path [String] API path
76
+ # @param body [Hash] Request body
77
+ # @return [Hash] Parsed response
78
+ def put(path, body = {})
79
+ request(:put, path, body: body)
80
+ end
81
+
82
+ # Make a PATCH request
83
+ # @param path [String] API path
84
+ # @param body [Hash] Request body
85
+ # @return [Hash] Parsed response
86
+ def patch(path, body = {})
87
+ request(:patch, path, body: body)
88
+ end
89
+
90
+ # Make a DELETE request
91
+ # @param path [String] API path
92
+ # @return [Hash] Parsed response
93
+ def delete(path)
94
+ request(:delete, path)
95
+ end
96
+
97
+ private
98
+
99
+ def build_connection
100
+ Faraday.new(url: config.base_url) do |conn|
101
+ # Request configuration
102
+ conn.request :json
103
+ conn.headers["Authorization"] = "Bearer #{config.api_key}"
104
+ conn.headers["User-Agent"] = config.full_user_agent
105
+ conn.headers["Content-Type"] = "application/json"
106
+ conn.headers["Accept"] = "application/json"
107
+
108
+ # Retry configuration with exponential backoff
109
+ conn.request :retry,
110
+ max: config.max_retries,
111
+ interval: config.retry_base_delay,
112
+ interval_randomness: 0.5,
113
+ backoff_factor: 2,
114
+ max_interval: config.retry_max_delay,
115
+ retry_statuses: [408, 500, 502, 503, 504],
116
+ retry_if: ->(env, _exception) { retryable?(env) },
117
+ retry_block: ->(env, _options, retries, exception) {
118
+ log_retry(env, retries, exception)
119
+ }
120
+
121
+ # Response parsing
122
+ conn.response :json, content_type: /\bjson$/
123
+ conn.response :logger, config.logger, bodies: true if config.logger
124
+
125
+ # Timeout
126
+ conn.options.timeout = config.timeout
127
+ conn.options.open_timeout = 10
128
+
129
+ # Adapter
130
+ conn.adapter Faraday.default_adapter
131
+ end
132
+ end
133
+
134
+ def request(method, path, params: nil, body: nil)
135
+ response = @connection.send(method) do |req|
136
+ req.url path
137
+ req.params = params if params
138
+ req.body = body.to_json if body
139
+ end
140
+
141
+ handle_response(response)
142
+ rescue Faraday::TimeoutError
143
+ raise TimeoutError
144
+ rescue Faraday::ConnectionFailed
145
+ raise ConnectionError
146
+ end
147
+
148
+ def handle_response(response)
149
+ case response.status
150
+ when 200..299
151
+ response.body || {}
152
+ when 401
153
+ raise AuthenticationError, extract_error_message(response)
154
+ when 404
155
+ raise NotFoundError, extract_error_message(response)
156
+ when 422
157
+ handle_validation_error(response)
158
+ when 429
159
+ handle_rate_limit(response)
160
+ when 400..499
161
+ raise ValidationError.new(extract_error_message(response), details: response.body)
162
+ when 500..599
163
+ raise ServerError, extract_error_message(response)
164
+ else
165
+ raise Error.new(
166
+ "Unexpected response: #{response.status}",
167
+ http_status: response.status
168
+ )
169
+ end
170
+ end
171
+
172
+ def handle_rate_limit(response)
173
+ retry_after = response.headers["Retry-After"]&.to_i
174
+ message = extract_error_message(response)
175
+
176
+ if config.raise_on_rate_limit
177
+ raise RateLimitError.new(message, retry_after: retry_after)
178
+ else
179
+ {
180
+ error: true,
181
+ code: "RATE_LIMIT_ERROR",
182
+ message: message,
183
+ retry_after: retry_after
184
+ }
185
+ end
186
+ end
187
+
188
+ def handle_validation_error(response)
189
+ body = response.body || {}
190
+
191
+ if body.dig("error", "code") == "POLICY_VIOLATION"
192
+ raise PolicyViolationError.new(
193
+ extract_error_message(response),
194
+ violations: body.dig("error", "violations") || []
195
+ )
196
+ else
197
+ raise ValidationError.new(extract_error_message(response), details: body)
198
+ end
199
+ end
200
+
201
+ def extract_error_message(response)
202
+ body = response.body
203
+ return "Unknown error" unless body.is_a?(Hash)
204
+
205
+ body.dig("error", "message") ||
206
+ body["message"] ||
207
+ body["error"] ||
208
+ "Request failed with status #{response.status}"
209
+ end
210
+
211
+ def retryable?(env)
212
+ # Don't retry POST/PUT/PATCH unless idempotent
213
+ return true if %i[get head delete].include?(env.method)
214
+
215
+ # Retry server errors for all methods
216
+ env.status.nil? || env.status >= 500
217
+ end
218
+
219
+ def log_retry(env, retries, exception)
220
+ return unless config.logger
221
+
222
+ config.logger.warn(
223
+ "[Tork] Retry ##{retries} for #{env.method.upcase} #{env.url}: " \
224
+ "#{exception&.message || "status #{env.status}"}"
225
+ )
226
+ end
227
+ end
228
+ end