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 +7 -0
- data/README.md +425 -0
- data/Rakefile +38 -0
- data/lib/tork/client.rb +228 -0
- data/lib/tork/configuration.rb +73 -0
- data/lib/tork/errors.rb +84 -0
- data/lib/tork/resources/evaluation.rb +115 -0
- data/lib/tork/resources/metrics.rb +122 -0
- data/lib/tork/resources/policy.rb +115 -0
- data/lib/tork/version.rb +5 -0
- data/lib/tork.rb +85 -0
- data/tork-governance.gemspec +53 -0
- metadata +217 -0
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
|
+
[](https://badge.fury.io/rb/tork-governance)
|
|
6
|
+
[](https://github.com/torkjacobs/tork-ruby-sdk/actions)
|
|
7
|
+
[](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
|
data/lib/tork/client.rb
ADDED
|
@@ -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
|