ai_guardrails 1.2.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 +8 -0
- data/CHANGELOG.md +149 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +528 -0
- data/Rakefile +12 -0
- data/lib/ai_guardrails/auto_correction.rb +85 -0
- data/lib/ai_guardrails/auto_fix.rb +85 -0
- data/lib/ai_guardrails/background_job.rb +47 -0
- data/lib/ai_guardrails/cache.rb +50 -0
- data/lib/ai_guardrails/cli.rb +17 -0
- data/lib/ai_guardrails/config.rb +13 -0
- data/lib/ai_guardrails/dsl.rb +101 -0
- data/lib/ai_guardrails/json_repair.rb +234 -0
- data/lib/ai_guardrails/logger.rb +45 -0
- data/lib/ai_guardrails/mock_model_client.rb +34 -0
- data/lib/ai_guardrails/provider/base_client.rb +19 -0
- data/lib/ai_guardrails/provider/factory.rb +20 -0
- data/lib/ai_guardrails/provider/openai_client.rb +43 -0
- data/lib/ai_guardrails/runner.rb +40 -0
- data/lib/ai_guardrails/safety_filter.rb +33 -0
- data/lib/ai_guardrails/schema_validator.rb +57 -0
- data/lib/ai_guardrails/version.rb +5 -0
- data/lib/ai_guardrails.rb +40 -0
- data/sig/ai_guardrails.rbs +4 -0
- metadata +122 -0
data/README.md
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# AiGuardrails
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/ai_guardrails)
|
|
4
|
+
[](https://github.com/logicbunchhq/ai_guardrails/actions)
|
|
5
|
+
[](https://coveralls.io/github/logicbunchhq/ai_guardrails?branch=main)
|
|
6
|
+
|
|
7
|
+
> **AiGuardrails** is a Ruby gem for validating, repairing, and securing AI-generated outputs.
|
|
8
|
+
> It ensures responses from LLMs (like OpenAI or Anthropic) are **valid, safe, and structured**.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
1. [Overview](#overview)
|
|
15
|
+
2. [Installation](#installation)
|
|
16
|
+
3. [Quick Start](#quick-start)
|
|
17
|
+
4. [Features](#features)
|
|
18
|
+
|
|
19
|
+
* [Schema Validation](#schema-validation)
|
|
20
|
+
* [Automatic JSON Repair](#automatic-json-repair)
|
|
21
|
+
* [Unit Test Helpers / Mock Model Client](#unit-test-helpers--mock-model-client)
|
|
22
|
+
* [Provider-Agnostic API](#provider-agnostic-api)
|
|
23
|
+
* [Auto-Correction / Retry Layer](#auto-correction--retry-layer)
|
|
24
|
+
* [Safety & Content Filters](#safety--content-filters)
|
|
25
|
+
* [Easy DSL / Developer-Friendly API](#easy-dsl--developer-friendly-api)
|
|
26
|
+
* [Background Job / CLI Friendly](#background-job--cli-friendly)
|
|
27
|
+
* [Optional Caching](#optional-caching)
|
|
28
|
+
* [JSON + Schema Auto-Fix Hooks](#json--schema-auto-fix-hooks)
|
|
29
|
+
5. [Error Handling](#error-handling)
|
|
30
|
+
6. [Development](#development)
|
|
31
|
+
7. [Contributing](#contributing)
|
|
32
|
+
8. [License](#license)
|
|
33
|
+
9. [Code of Conduct](#code-of-conduct)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Overview
|
|
38
|
+
|
|
39
|
+
AI models often generate invalid JSON, inconsistent data types, or unsafe content.
|
|
40
|
+
**AiGuardrails** helps you handle all of this automatically, providing:
|
|
41
|
+
|
|
42
|
+
* ✅ JSON repair for malformed AI responses
|
|
43
|
+
* ✅ Schema validation for predictable structure
|
|
44
|
+
* ✅ Safety filters for blocked or harmful content
|
|
45
|
+
* ✅ Retry + correction for invalid responses
|
|
46
|
+
* ✅ Easy integration via a single `AiGuardrails.run` call
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
flowchart LR
|
|
52
|
+
A[AI Response] --> B[JSON Parse Attempt]
|
|
53
|
+
B -->|Valid| F[Return Output ✅]
|
|
54
|
+
B -->|Invalid| C[Auto-Fix Hook 🔧]
|
|
55
|
+
C --> D[Revalidate Schema]
|
|
56
|
+
D -->|Fixed| F
|
|
57
|
+
D -->|Still Invalid| E[Retry or Fallback 🚨]
|
|
58
|
+
E --> G[Raise AiGuardrails::SchemaError]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
Add AiGuardrails to your project using Bundler:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bundle add ai_guardrails
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or install manually:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
gem install ai_guardrails
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Requirements
|
|
80
|
+
|
|
81
|
+
- Ruby >= 3.0.0
|
|
82
|
+
- Rails >= 6.0 (if using Rails integration)
|
|
83
|
+
- Optional dependencies:
|
|
84
|
+
- `ruby-openai` for OpenAI provider
|
|
85
|
+
- `ruby-anthropic` gem if using Anthropic provider
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Quick Start
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
require "ai_guardrails"
|
|
93
|
+
|
|
94
|
+
schema = { name: :string, price: :float }
|
|
95
|
+
|
|
96
|
+
# Optional: Helps AI return structured output matching the schema.
|
|
97
|
+
# Can be any type: String, Hash, etc.
|
|
98
|
+
schema_hint = schema
|
|
99
|
+
|
|
100
|
+
result = AiGuardrails::DSL.run(
|
|
101
|
+
prompt: "Generate a product JSON",
|
|
102
|
+
provider: :openai,
|
|
103
|
+
provider_config: { api_key: ENV["OPENAI_API_KEY"] },
|
|
104
|
+
schema: schema,
|
|
105
|
+
schema_hint: schema_hint
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
puts result
|
|
109
|
+
# => { "name" => "Laptop", "price" => 1200.0 }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Features
|
|
115
|
+
|
|
116
|
+
### Schema Validation
|
|
117
|
+
|
|
118
|
+
Validate AI output against a Ruby schema.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
require "ai_guardrails"
|
|
122
|
+
|
|
123
|
+
schema = { name: :string, price: :float, tags: [:string] }
|
|
124
|
+
validator = AiGuardrails::SchemaValidator.new(schema)
|
|
125
|
+
|
|
126
|
+
input = { name: "Laptop", price: 1200.0, tags: ["electronics", "sale"] }
|
|
127
|
+
success, result = validator.validate(input)
|
|
128
|
+
|
|
129
|
+
if success
|
|
130
|
+
puts "Valid output: #{result}"
|
|
131
|
+
else
|
|
132
|
+
puts "Validation errors: #{result}"
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Supported Types
|
|
137
|
+
|
|
138
|
+
| Type | Example |
|
|
139
|
+
| -------------------- | ---------------------- |
|
|
140
|
+
| `:string` | `"Laptop"` |
|
|
141
|
+
| `:integer` | `42` |
|
|
142
|
+
| `:float` | `19.99` |
|
|
143
|
+
| `:boolean` | `true` |
|
|
144
|
+
| `[:string]` | `["a", "b"]` |
|
|
145
|
+
| `[{ key: :string }]` | `[{"key" => "value"}]` |
|
|
146
|
+
|
|
147
|
+
#### Example Invalid Input
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
input = { name: 123, price: "abc", tags: ["electronics", 2] }
|
|
151
|
+
success, errors = validator.validate(input)
|
|
152
|
+
# => { name: ["must be a string"], price: ["must be a float"], tags: ["element 1 must be a string"] }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Automatic JSON Repair
|
|
158
|
+
|
|
159
|
+
LLMs often return invalid JSON.
|
|
160
|
+
`AiGuardrails::JsonRepair` automatically fixes common issues.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
require "ai_guardrails"
|
|
164
|
+
|
|
165
|
+
raw_json = "{name: 'Laptop' price: 1200, tags: ['electronics' 'sale']}"
|
|
166
|
+
fixed = AiGuardrails::JsonRepair.repair(raw_json)
|
|
167
|
+
|
|
168
|
+
puts fixed
|
|
169
|
+
# => { "name" => "Laptop", "price" => 1200, "tags" => ["electronics", "sale"] }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**What It Fixes**
|
|
173
|
+
|
|
174
|
+
* Missing quotes or commas
|
|
175
|
+
* Single → double quotes
|
|
176
|
+
* Trailing commas
|
|
177
|
+
* Unbalanced braces/brackets
|
|
178
|
+
* Nested arrays/objects without separators
|
|
179
|
+
|
|
180
|
+
**Unrepairable JSON Example**
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
begin
|
|
184
|
+
AiGuardrails::JsonRepair.repair("NOT JSON")
|
|
185
|
+
rescue AiGuardrails::JsonRepair::RepairError => e
|
|
186
|
+
puts "Could not repair JSON: #{e.message}"
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Integration with Schema Validation**
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
schema = { name: :string, price: :float }
|
|
194
|
+
fixed = AiGuardrails::JsonRepair.repair("{name: 'Laptop', price: '1200'}")
|
|
195
|
+
validator = AiGuardrails::SchemaValidator.new(schema)
|
|
196
|
+
success, result = validator.validate(fixed)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### Unit Test Helpers / Mock Model Client
|
|
202
|
+
|
|
203
|
+
Simulate AI model responses for testing without API calls.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
mock_client = AiGuardrails::MockModelClient.new(
|
|
207
|
+
"Generate product" => '{"name": "Laptop", "price": 1200}'
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
response = mock_client.call(prompt: "Generate product")
|
|
211
|
+
puts response
|
|
212
|
+
# => '{"name": "Laptop", "price": 1200}'
|
|
213
|
+
|
|
214
|
+
mock_client.add_response("Generate user", '{"name": "Alice"}')
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Simulate API Errors**
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
begin
|
|
221
|
+
mock_client.call(prompt: "error", raise_error: true)
|
|
222
|
+
rescue AiGuardrails::MockModelClient::MockError => e
|
|
223
|
+
puts e.message
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Fallback Example**
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
response = mock_client.call(prompt: "error", default_fallback: "No mock response defined")
|
|
231
|
+
puts response
|
|
232
|
+
# => "No mock response defined"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### Provider-Agnostic API
|
|
238
|
+
|
|
239
|
+
AiGuardrails supports **any LLM provider**, with dynamic loading and zero vendor dependencies.
|
|
240
|
+
|
|
241
|
+
| Provider | Gem | Status |
|
|
242
|
+
| -------------- | ---------------- | ----------- |
|
|
243
|
+
| OpenAI | `ruby-openai` | ✅ Supported |
|
|
244
|
+
| Anthropic | `ruby-anthropic` | 🔜 Planned |
|
|
245
|
+
| Google Gemini | `gemini-ai` | 🔜 Planned |
|
|
246
|
+
| Ollama (local) | `ollama-ai` | 🔜 Planned |
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
client = AiGuardrails::Provider::Factory.build(
|
|
250
|
+
provider: :openai,
|
|
251
|
+
config: { api_key: ENV["OPENAI_API_KEY"], model: "gpt-4o-mini" }
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
puts client.call_model(prompt: "Hello!")
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### Auto-Correction / Retry Layer
|
|
260
|
+
|
|
261
|
+
Automatically repairs and retries AI output until it passes schema validation.
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
schema = { name: :string, price: :float }
|
|
265
|
+
|
|
266
|
+
client = AiGuardrails::Provider::Factory.build(
|
|
267
|
+
provider: :openai,
|
|
268
|
+
config: { api_key: ENV["OPENAI_API_KEY"] }
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
auto = AiGuardrails::AutoCorrection.new(provider: client, schema: schema, max_retries: 3, sleep_time: 1)
|
|
272
|
+
|
|
273
|
+
result = auto.call(prompt: "Generate a product JSON", schema_hint: schema)
|
|
274
|
+
puts result
|
|
275
|
+
# => { "name" => "Laptop", "price" => 1200.0 }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Optional Parameters**
|
|
279
|
+
|
|
280
|
+
* `max_retries`: Maximum retry attempts
|
|
281
|
+
* `sleep_time`: Delay between retries
|
|
282
|
+
* `schema_hint`: Optional guide to help AI produce valid output
|
|
283
|
+
* Raises `AiGuardrails::AutoCorrection::RetryLimitReached` if retries exhausted
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### Safety & Content Filters
|
|
288
|
+
|
|
289
|
+
Detect and block unsafe or prohibited content.
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
filter = AiGuardrails::SafetyFilter.new(blocklist: ["badword", /forbidden/i])
|
|
293
|
+
|
|
294
|
+
filter.safe?("clean text") # => true
|
|
295
|
+
filter.safe?("badword inside") # => false
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Raise exception on violation:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
begin
|
|
302
|
+
filter.check!("This has badword")
|
|
303
|
+
rescue AiGuardrails::SafetyFilter::UnsafeContentError => e
|
|
304
|
+
puts e.message
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### Easy DSL / Developer-Friendly API
|
|
311
|
+
|
|
312
|
+
`AiGuardrails::DSL.run` is a single entry point combining:
|
|
313
|
+
|
|
314
|
+
* JSON repair
|
|
315
|
+
* Schema validation
|
|
316
|
+
* Auto-correction & retries
|
|
317
|
+
* Safety filters
|
|
318
|
+
* Logging & debugging
|
|
319
|
+
* Optional caching
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
result = AiGuardrails::DSL.run(
|
|
323
|
+
prompt: "Generate product",
|
|
324
|
+
schema: { name: :string, price: :float },
|
|
325
|
+
schema_hint: schema, # It could be any other data type eg, string, json etc
|
|
326
|
+
provider: :openai,
|
|
327
|
+
provider_config: { api_key: ENV["OPENAI_API_KEY"] },
|
|
328
|
+
blocklist: ["Forbidden"]
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
puts result
|
|
332
|
+
# => { "name" => "Laptop", "price" => 1200.0 }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Schema vs Schema Hint**
|
|
336
|
+
|
|
337
|
+
* `schema` (required): Full validation for final output.
|
|
338
|
+
* `schema_hint` (optional): Guides AI generation, can be a subset or modified version.
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
schema = { name: :string, price: :float, tags: [:string] }
|
|
342
|
+
hint = "JSON should contain name, price and tags"
|
|
343
|
+
|
|
344
|
+
result = AiGuardrails::DSL.run(
|
|
345
|
+
prompt: "Generate product JSON",
|
|
346
|
+
schema: schema,
|
|
347
|
+
schema_hint: hint,
|
|
348
|
+
provider_config: { api_key: ENV["OPENAI_API_KEY"] },
|
|
349
|
+
blocklist: ["Forbidden"],
|
|
350
|
+
max_retries: 3,
|
|
351
|
+
sleep_time: 1
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
puts result
|
|
355
|
+
# => { "name" => "Laptop", "price" => 1200.0, "tags" => ["electronics", "sale"] }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### Background Job / CLI Friendly
|
|
361
|
+
|
|
362
|
+
Works seamlessly in Rails background jobs, Sidekiq, or standalone CLI scripts.
|
|
363
|
+
|
|
364
|
+
**Background Job Example**
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
require "logger"
|
|
368
|
+
|
|
369
|
+
logger = Logger.new($stdout)
|
|
370
|
+
|
|
371
|
+
AiGuardrails::BackgroundJob.perform(logger: logger, debug: true) do
|
|
372
|
+
AiGuardrails::DSL.run(
|
|
373
|
+
prompt: "Generate product",
|
|
374
|
+
schema: { name: :string, price: :float },
|
|
375
|
+
schema_hint: { name: :string, price: :float },
|
|
376
|
+
provider_config: { api_key: ENV["OPENAI_API_KEY"] }
|
|
377
|
+
)
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**CLI Example (Standalone Script or IRB)**
|
|
382
|
+
Use the CLI for running workflows outside of Rails or for local testing.
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
# If running as a standalone Ruby script, make sure the gem lib path is loaded:
|
|
386
|
+
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
|
|
387
|
+
require "ai_guardrails"
|
|
388
|
+
|
|
389
|
+
AiGuardrails::CLI.run(debug: true) do
|
|
390
|
+
result = AiGuardrails::DSL.run(
|
|
391
|
+
prompt: "Generate product",
|
|
392
|
+
schema: { name: :string, value: :integer },
|
|
393
|
+
schema_hint: { name: :string, value: :integer },
|
|
394
|
+
provider_config: { api_key: ENV["OPENAI_API_KEY"] }
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
puts result
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
**Note**: End-users of the gem in Rails projects do not need the CLI. It is primarily for gem developers or for running workflows outside a Rails app.
|
|
404
|
+
|
|
405
|
+
### Optional Caching
|
|
406
|
+
|
|
407
|
+
Cache AI responses for repeated prompts to reduce cost and latency.
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
AiGuardrails::Cache.configure(enabled: true, store: Rails.cache, expires_in: 300)
|
|
411
|
+
|
|
412
|
+
schema = { name: :string, price: :float }
|
|
413
|
+
|
|
414
|
+
result1 = AiGuardrails::DSL.run(prompt: "Generate product", schema: schema, schema_hint: schema, provider_config: { api_key: ENV["OPENAI_API_KEY"] })
|
|
415
|
+
result2 = AiGuardrails::DSL.run(prompt: "Generate product", schema: schema, schema_hint: schema, provider_config: { api_key: ENV["OPENAI_API_KEY"] })
|
|
416
|
+
|
|
417
|
+
puts result1 == result2 # => true
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Fetch Examples**
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
key = AiGuardrails::Cache.key("prompt", schema)
|
|
424
|
+
|
|
425
|
+
# Using default
|
|
426
|
+
value = AiGuardrails::Cache.fetch(key, "default_value")
|
|
427
|
+
|
|
428
|
+
# Using block
|
|
429
|
+
value = AiGuardrails::Cache.fetch(key) { "computed_result" }
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
### JSON + Schema Auto-Fix Hooks
|
|
435
|
+
|
|
436
|
+
Automatically repair and coerce malformed JSON to match schema.
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
schema = { name: :string, price: :float, available: :boolean }
|
|
440
|
+
raw = '{"name": "Shirt", "price": "19.99", "available": "true"}'
|
|
441
|
+
|
|
442
|
+
fixed = AiGuardrails::AutoFix.fix(raw, schema: schema)
|
|
443
|
+
# => {"name"=>"Shirt", "price"=>19.99, "available"=>true}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Custom Hooks**
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
hooks = [proc { |h| h["price"] *= 2 }]
|
|
450
|
+
fixed = AiGuardrails::AutoFix.fix(raw, schema: schema, hooks: hooks)
|
|
451
|
+
puts fixed["price"] # => 39.98
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Hooks allow:
|
|
455
|
+
|
|
456
|
+
* Setting **default values**
|
|
457
|
+
* Transforming or normalizing data
|
|
458
|
+
* Custom calculations or aggregations
|
|
459
|
+
* Injecting metadata before final output
|
|
460
|
+
|
|
461
|
+
Hooks run after schema validation and JSON repair, ensuring safe, valid, and tailored output.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Error Handling
|
|
466
|
+
|
|
467
|
+
| Exception | When It Occurs |
|
|
468
|
+
| ------------------------------------------------- | ----------------------------------------------- |
|
|
469
|
+
| `AiGuardrails::JsonRepair::RepairError` | Cannot repair invalid JSON input |
|
|
470
|
+
| `AiGuardrails::AutoCorrection::RetryLimitReached` | Maximum retries reached without valid output |
|
|
471
|
+
| `AiGuardrails::SafetyFilter::UnsafeContentError` | Blocked or unsafe content detected in AI output |
|
|
472
|
+
|
|
473
|
+
Example:
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
begin
|
|
477
|
+
result = AiGuardrails::DSL.run(prompt: "Generate product", schema: schema)
|
|
478
|
+
rescue AiGuardrails::AutoCorrection::RetryLimitReached => e
|
|
479
|
+
puts "Retries exceeded: #{e.message}"
|
|
480
|
+
rescue AiGuardrails::SafetyFilter::UnsafeContentError => e
|
|
481
|
+
puts "Blocked content detected: #{e.message}"
|
|
482
|
+
end
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Development
|
|
488
|
+
|
|
489
|
+
Install dependencies:
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
bin/setup
|
|
493
|
+
rake spec
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Interactive console:
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
bin/console
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Release a new version:
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
bundle exec rake release
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## Contributing
|
|
511
|
+
|
|
512
|
+
Bug reports and pull requests are welcome at:
|
|
513
|
+
👉 [https://github.com/logicbunchhq/ai_guardrails](https://github.com/logicbunchhq/ai_guardrails)
|
|
514
|
+
|
|
515
|
+
This project follows the [Contributor Covenant](https://www.contributor-covenant.org/).
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## License
|
|
520
|
+
|
|
521
|
+
Released under the [MIT License](https://opensource.org/licenses/MIT).
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Code of Conduct
|
|
526
|
+
|
|
527
|
+
Everyone interacting with AiGuardrails project is expected to follow the
|
|
528
|
+
[Code of Conduct](https://github.com/logicbunchhq/ai_guardrails/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AiGuardrails
|
|
4
|
+
# AutoCorrection handles retries, JSON repair, and schema validation
|
|
5
|
+
class AutoCorrection
|
|
6
|
+
class RetryLimitReached < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Initialize with a provider client and schema
|
|
9
|
+
# options:
|
|
10
|
+
# max_retries: number of attempts (default: 3)
|
|
11
|
+
# sleep_time: seconds to wait between retries (default: 0)
|
|
12
|
+
def initialize(provider:, schema:, max_retries: 3, sleep_time: 0)
|
|
13
|
+
@provider = provider
|
|
14
|
+
@validator = SchemaValidator.new(schema)
|
|
15
|
+
@max_retries = max_retries
|
|
16
|
+
@sleep_time = sleep_time
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Call the AI model with prompt
|
|
20
|
+
# Returns validated hash (symbolized keys and correct types)
|
|
21
|
+
# rubocop:disable Metrics/MethodLength
|
|
22
|
+
def call(prompt:, schema_hint: nil)
|
|
23
|
+
attempts = 0
|
|
24
|
+
|
|
25
|
+
# Append schema hint to prompt if provided
|
|
26
|
+
prompt = prepare_prompt(prompt, schema_hint)
|
|
27
|
+
|
|
28
|
+
loop do
|
|
29
|
+
attempts += 1
|
|
30
|
+
|
|
31
|
+
# Call AI provider
|
|
32
|
+
raw_output = @provider.call_model(prompt: prompt)
|
|
33
|
+
|
|
34
|
+
# Repair JSON if needed
|
|
35
|
+
input_for_validation = parse_and_repair(raw_output)
|
|
36
|
+
|
|
37
|
+
# Validate against schema
|
|
38
|
+
valid, result = @validator.validate(input_for_validation)
|
|
39
|
+
puts "valid: #{valid}, result: #{result.inspect}"
|
|
40
|
+
|
|
41
|
+
return result if valid
|
|
42
|
+
|
|
43
|
+
# Raise error if max retries reached
|
|
44
|
+
raise RetryLimitReached, "Max retries reached" if attempts >= @max_retries
|
|
45
|
+
|
|
46
|
+
# Log retry attempt
|
|
47
|
+
puts "[AiGuardrails] Attempt #{attempts}: Invalid output, retrying..."
|
|
48
|
+
sleep @sleep_time
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
# rubocop:enable Metrics/MethodLength
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Prepare prompt with schema hint if provided
|
|
56
|
+
def prepare_prompt(prompt, schema_hint)
|
|
57
|
+
return prompt unless schema_hint
|
|
58
|
+
|
|
59
|
+
"#{prompt}\n\nReturn only a valid JSON object matching this schema " \
|
|
60
|
+
"(no explanations, no formatting): #{schema_hint}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Parse and repair raw AI output
|
|
64
|
+
def parse_and_repair(raw_output)
|
|
65
|
+
repaired = repair_json(raw_output)
|
|
66
|
+
parse_json_or_empty(repaired)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Attempt to repair JSON using JsonRepair
|
|
70
|
+
def repair_json(raw)
|
|
71
|
+
JsonRepair.repair(raw)
|
|
72
|
+
rescue JsonRepair::RepairError
|
|
73
|
+
raw
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Parse string JSON or return hash, fallback to empty hash
|
|
77
|
+
def parse_json_or_empty(input)
|
|
78
|
+
return input if input.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
JSON.parse(input)
|
|
81
|
+
rescue JSON::ParserError
|
|
82
|
+
{}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module AiGuardrails
|
|
6
|
+
# Automatically fixes JSON outputs based on schema hints
|
|
7
|
+
module AutoFix
|
|
8
|
+
class << self
|
|
9
|
+
# Fix a JSON string or hash according to schema
|
|
10
|
+
#
|
|
11
|
+
# @param json_input [String, Hash] raw JSON output
|
|
12
|
+
# @param schema [Hash] expected schema (key => type)
|
|
13
|
+
# @param hooks [Array<Proc>] optional hooks for custom fixes
|
|
14
|
+
# @return [Hash] fixed output
|
|
15
|
+
def fix(json_input, schema:, hooks: [])
|
|
16
|
+
data = parse_json(json_input)
|
|
17
|
+
|
|
18
|
+
fixed = apply_schema_fixes(data, schema)
|
|
19
|
+
|
|
20
|
+
hooks.each do |hook|
|
|
21
|
+
fixed = apply_hook(fixed, hook)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
fixed
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def apply_hook(fixed, hook)
|
|
28
|
+
result = hook.call(fixed)
|
|
29
|
+
|
|
30
|
+
# Ensure hook returns a Hash
|
|
31
|
+
if result.is_a?(Hash)
|
|
32
|
+
result
|
|
33
|
+
else
|
|
34
|
+
warn_hook_return_type(hook, result)
|
|
35
|
+
fixed # keep previous hash
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def warn_hook_return_type(hook, result)
|
|
42
|
+
warn(
|
|
43
|
+
"[AiGuardrails::AutoFix] WARNING: hook #{hook} returned " \
|
|
44
|
+
"#{result.class}, expected Hash. Using previous hash instead."
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parse string or return hash
|
|
49
|
+
def parse_json(input)
|
|
50
|
+
case input
|
|
51
|
+
when String
|
|
52
|
+
JSON.parse(input)
|
|
53
|
+
when Hash
|
|
54
|
+
input
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, "Unsupported input type: #{input.class}"
|
|
57
|
+
end
|
|
58
|
+
rescue JSON::ParserError
|
|
59
|
+
{} # fallback empty hash
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Convert/repair values according to schema types
|
|
63
|
+
def apply_schema_fixes(data, schema)
|
|
64
|
+
fixed = {}
|
|
65
|
+
schema.each do |key, type|
|
|
66
|
+
str_key = key.to_s
|
|
67
|
+
value = data[str_key] || data[key.to_sym]
|
|
68
|
+
|
|
69
|
+
fixed[str_key] = convert_value(value, type)
|
|
70
|
+
end
|
|
71
|
+
fixed
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def convert_value(value, type)
|
|
75
|
+
return value.to_s if type == :string
|
|
76
|
+
return value.to_i if type == :integer
|
|
77
|
+
return value.to_s.to_f if type == :float
|
|
78
|
+
return !!value if type == :boolean
|
|
79
|
+
return value.is_a?(Array) ? value : Array(value) if type == Array
|
|
80
|
+
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|