rails_ai_kit 0.1.6 → 0.1.8
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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +179 -158
- data/lib/generators/rails_ai_kit/dashboard/templates/index.html.erb +38 -0
- data/lib/generators/rails_ai_kit/dashboard/templates/traces_controller.rb +25 -0
- data/lib/generators/rails_ai_kit/dashboard_generator.rb +25 -0
- data/lib/generators/rails_ai_kit/install/templates/create_rails_ai_kit_tables.rb +30 -0
- data/lib/generators/rails_ai_kit/install/templates/create_rails_ai_traces.rb +21 -0
- data/lib/generators/rails_ai_kit/install/templates/rails_ai_kit.rb +20 -4
- data/lib/generators/rails_ai_kit/install_generator.rb +4 -4
- data/lib/rails_ai_kit/ai_generate.rb +56 -0
- data/lib/rails_ai_kit/configuration.rb +19 -1
- data/lib/rails_ai_kit/eval.rb +83 -0
- data/lib/rails_ai_kit/guard_result.rb +28 -0
- data/lib/rails_ai_kit/guardrails.rb +159 -0
- data/lib/rails_ai_kit/guards/base.rb +50 -0
- data/lib/rails_ai_kit/guards/hallucination.rb +32 -0
- data/lib/rails_ai_kit/guards/pii.rb +30 -0
- data/lib/rails_ai_kit/guards/prompt_injection.rb +29 -0
- data/lib/rails_ai_kit/guards/toxicity.rb +37 -0
- data/lib/rails_ai_kit/llm_client.rb +45 -0
- data/lib/rails_ai_kit/llm_providers/anthropic.rb +60 -0
- data/lib/rails_ai_kit/llm_providers/base.rb +32 -0
- data/lib/rails_ai_kit/llm_providers/custom.rb +59 -0
- data/lib/rails_ai_kit/llm_providers/google.rb +59 -0
- data/lib/rails_ai_kit/llm_providers/groq.rb +64 -0
- data/lib/rails_ai_kit/llm_providers/openai.rb +63 -0
- data/lib/rails_ai_kit/trace.rb +26 -0
- data/lib/rails_ai_kit/version.rb +1 -1
- data/lib/rails_ai_kit.rb +39 -7
- metadata +23 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be63d7b9ec9ef2a7c943cdb80fded8941102df5dd5c410698eda80f56fc65cc5
|
|
4
|
+
data.tar.gz: 6848aba88723727d242aa35b156757d860e91fefb6f9e28d92902d8d6c2dc587
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3b21a2cb175fc0ad89248bda65d812fe458ee31b68b3083d704ada7cea7b8adb2fe3b75781ee8c29c7a925966f4185dcb870db80f2753518fda7bc5f4dfc266
|
|
7
|
+
data.tar.gz: c3aa3961a3e430ba1ffdbd63b2390d2515c6eb8b896e32b1e26c5797bd492f859aa6882728c9037b97968bb762566e8fcf138ff8f4c87a268283e240e6a7c37a
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Guardrails (LLM-as-judge)** – Generate then validate with safety checks (no vectors). `RailsAiKit.guard(prompt:, model:, checks: [:toxicity, :pii, :hallucination], threshold: 0.8)` returns `GuardResult` with `output`, `safe?`, `scores`, `action`.
|
|
8
|
+
- **Guards** – Toxicity, PII, Hallucination, Prompt injection (each uses LLM to score 0–1).
|
|
9
|
+
- **LLM providers** – OpenAI, Anthropic (Claude), Google (Gemini), Groq, and custom URL + key (OpenAI-compatible). Single config: `llm_provider`, `llm_api_keys`, optional `llm_url`/`llm_key` for custom.
|
|
10
|
+
- **ai_generate macro** – `ai_generate :summary, from: :content, guards: [...], threshold: 0.85` → `record.ai_summary` (generate + guardrails).
|
|
11
|
+
- **Eval** – `RailsAiKit.eval(dataset: "path.json", model:)` returns accuracy, hallucination rate, toxicity failures, and per-item details.
|
|
12
|
+
- **AI Traces** – Table `rails_ai_traces` (prompt, response, model, tokens, guard_scores, latency, action). Optional dashboard at `/ai_traces` via `rails g rails_ai_kit:dashboard`.
|
|
13
|
+
- Install generator now creates both `rails_ai_kit_labels` and `rails_ai_traces` in one migration.
|
|
14
|
+
|
|
5
15
|
## [0.1.3] - 2026-03-07
|
|
6
16
|
|
|
7
17
|
### Fixed
|
data/README.md
CHANGED
|
@@ -4,219 +4,236 @@
|
|
|
4
4
|
|
|
5
5
|
# Rails AI Kit
|
|
6
6
|
|
|
7
|
-
**AI-first toolkit for Rails** —
|
|
8
|
-
|
|
9
|
-
> Rails gem for vector-based text classification (pgvector), embeddings (OpenAI, Cohere), similarity search, and generators. Train labels with examples; classify on save or in batch.
|
|
7
|
+
**AI-first toolkit for Rails** — two core features today, more on the way.
|
|
10
8
|
|
|
11
9
|
[](https://rubygems.org/gems/rails_ai_kit)
|
|
12
10
|
[](https://rubyonrails.org/)
|
|
13
11
|
[](LICENSE)
|
|
14
12
|
|
|
13
|
+
**[Source code](https://github.com/imrrohitt/rails_ai_kit)** · **[RubyGems](https://rubygems.org/gems/rails_ai_kit)**
|
|
14
|
+
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Two main features
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|:---|:---|
|
|
21
|
-
| **Rails-native** | Generators, ActiveRecord integration, single config file |
|
|
22
|
-
| **Vector-ready** | [pgvector](https://github.com/pgvector/pgvector) with OpenAI or Cohere embeddings |
|
|
23
|
-
| **Modular** | Use only the features you need; add more as the gem evolves |
|
|
19
|
+
Rails AI Kit currently provides **two independent feature sets**. Use one, both, or add more as the gem evolves.
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
| Feature | What it does | Use when |
|
|
22
|
+
|--------|----------------|----------|
|
|
23
|
+
| **1. Guardrails** | Run LLM generation through safety checks (toxicity, PII, hallucination, prompt injection). Generate → validate → score → allow/block. | You call an LLM and want to block unsafe outputs or toxic user inputs. |
|
|
24
|
+
| **2. Vector classification** | Classify text using embeddings + pgvector. Train labels with examples; auto-classify on save or in batch. No ML training, no LLM per request. | You need categories (e.g. support tickets, content tags, document routing). |
|
|
25
|
+
|
|
26
|
+
*More features (evals, tool use, etc.) are planned — the gem is actively developed.*
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
29
|
-
##
|
|
30
|
+
## Feature 1: Guardrails
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Guardrails wrap LLM calls with **safety checks**. You get a generated response plus scores; you decide allow/block by threshold. Checks run on both the **model output** and the **user prompt** (so toxic or injection-style inputs are detected even if the model refuses to comply).
|
|
32
33
|
|
|
33
|
-
###
|
|
34
|
+
### What you get
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
- **`RailsAiKit.guard(prompt:, model:, checks:, threshold:)`** — One call: generate + run checks. Returns `GuardResult` with `output`, `safe?`, `scores`, `action` (`:allow` / `:block`).
|
|
37
|
+
- **Guards:** toxicity, PII, hallucination, prompt injection (each uses an LLM-as-judge or keyword rules).
|
|
38
|
+
- **Prompt + output:** Toxicity and prompt-injection also evaluate the **user prompt**; final toxicity = max(prompt_toxicity, output_toxicity). Keyword safety net for obvious offensive language.
|
|
39
|
+
- **Rails macro:** `ai_generate :summary, from: :content, guards: [...]` → `article.ai_summary` (generate then guard).
|
|
40
|
+
- **Eval:** `RailsAiKit.eval(dataset: "eval.json", model:)` → accuracy, hallucination rate, toxicity failures.
|
|
41
|
+
- **AI Traces:** Optional `rails_ai_traces` table + `/ai_traces` dashboard to inspect prompts, responses, scores, and latency.
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
- Auto-classify on save (embed + compare → `label`, `confidence_score`)
|
|
39
|
-
- Similarity search: `Model.similar_to("query", limit: 5)`
|
|
40
|
-
- Batch classify for backfills or background jobs
|
|
43
|
+
### LLM providers
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
One config, same API: **OpenAI**, **Anthropic (Claude)**, **Google (Gemini)**, **Groq**, or **custom** (any OpenAI-compatible endpoint via `llm_url` + `llm_key`).
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
### Quick example (Guardrails)
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
```ruby
|
|
50
|
+
result = RailsAiKit.guard(
|
|
51
|
+
prompt: "What is the capital of France?",
|
|
52
|
+
model: "gpt-4o-mini",
|
|
53
|
+
checks: [:toxicity, :hallucination],
|
|
54
|
+
threshold: 0.8
|
|
55
|
+
)
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
result.output # => "The capital of France is Paris."
|
|
58
|
+
result.safe? # => true
|
|
59
|
+
result.scores # => { "toxicity" => 0.0, "hallucination" => 0.0, "label" => "factual" }
|
|
60
|
+
result.action # => :allow
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Toxic or injection-style prompts are scored (including a keyword safety net); if any score exceeds `threshold`, `action` is `:block`.
|
|
51
64
|
|
|
52
65
|
---
|
|
53
66
|
|
|
54
|
-
##
|
|
67
|
+
## Feature 2: Vector classification
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
Vector classification uses **embeddings + pgvector** to assign labels to text. You train each label with example phrases; the gem embeds them, stores one vector per label, and classifies new content by nearest-neighbor (cosine distance). No ML training and no LLM call per classification — only an embedding API (OpenAI or Cohere).
|
|
70
|
+
|
|
71
|
+
### What you get
|
|
72
|
+
|
|
73
|
+
- **Train labels:** `RailsAiKit.classifier("Article").train("sports", examples: ["football match", "cricket"])` → one vector per label in `rails_ai_kit_labels`.
|
|
74
|
+
- **Auto-classify on save:** Add `vector_classify :content, labels: ["sports", "politics", "tech"]` to your model. On save, the gem embeds `content`, stores it, compares to label vectors, and sets `label` and `confidence_score`.
|
|
75
|
+
- **Classify without saving:** `RailsAiKit.classifier("Article").classify("India won the cricket match")` → `{ label: "sports", confidence: 0.91 }`.
|
|
76
|
+
- **Batch classify:** `RailsAiKit.classifier("Article").batch_classify(records, text_attribute: :content)`.
|
|
77
|
+
- **Similarity search:** `Article.similar_to("new iPhone launch", limit: 5)`.
|
|
78
|
+
- **Filtering:** `Article.where(label: "sports")`, `Article.where("confidence_score >= ?", 0.8)`.
|
|
79
|
+
|
|
80
|
+
### Quick example (Vector classification)
|
|
57
81
|
|
|
58
82
|
```ruby
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
# 1. Train labels once
|
|
84
|
+
c = RailsAiKit.classifier("Article")
|
|
85
|
+
c.train("sports", examples: ["football match", "cricket tournament"])
|
|
86
|
+
c.train("technology", examples: ["new iPhone launch", "AI update"])
|
|
61
87
|
|
|
62
|
-
|
|
88
|
+
# 2. Model
|
|
89
|
+
class Article < ApplicationRecord
|
|
90
|
+
vector_classify :content, labels: ["sports", "politics", "technology"]
|
|
91
|
+
end
|
|
63
92
|
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
# 3. Create record → auto-classified
|
|
94
|
+
article = Article.create!(content: "Apple released a new iPhone")
|
|
95
|
+
article.label # => "technology"
|
|
96
|
+
article.confidence_score # => 0.91
|
|
66
97
|
```
|
|
67
98
|
|
|
68
|
-
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Requirements
|
|
102
|
+
|
|
103
|
+
- **Rails 6+**
|
|
104
|
+
- **PostgreSQL** with [pgvector](https://github.com/pgvector/pgvector) (for vector classification only)
|
|
105
|
+
- **Embedding API** (OpenAI or Cohere) for vector classification
|
|
106
|
+
- **LLM API** (OpenAI, Anthropic, Google, Groq, or custom) for Guardrails / `ai_generate` / Eval
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Installation
|
|
69
111
|
|
|
70
|
-
|
|
112
|
+
```ruby
|
|
113
|
+
# Gemfile
|
|
114
|
+
gem "rails_ai_kit"
|
|
115
|
+
```
|
|
71
116
|
|
|
72
117
|
```bash
|
|
118
|
+
bundle install
|
|
73
119
|
rails g rails_ai_kit:install
|
|
74
120
|
rails db:migrate
|
|
75
121
|
```
|
|
76
122
|
|
|
77
|
-
|
|
123
|
+
The install generator adds:
|
|
124
|
+
|
|
125
|
+
- **pgvector** and tables: `rails_ai_kit_labels` (for vector classification), `rails_ai_traces` (for Guardrails tracing)
|
|
126
|
+
- **Initializer:** `config/initializers/rails_ai_kit.rb`
|
|
127
|
+
|
|
128
|
+
### Configuration
|
|
78
129
|
|
|
79
|
-
|
|
130
|
+
Edit `config/initializers/rails_ai_kit.rb`:
|
|
80
131
|
|
|
81
132
|
```ruby
|
|
82
133
|
RailsAiKit.configure do |config|
|
|
83
|
-
|
|
84
|
-
config.
|
|
85
|
-
|
|
134
|
+
# ----- Vector classification (embeddings) -----
|
|
135
|
+
config.embedding_provider = :openai
|
|
136
|
+
config.embedding_dimensions = 1536
|
|
86
137
|
config.api_keys = {
|
|
87
138
|
openai: ENV["OPENAI_API_KEY"],
|
|
88
139
|
cohere: ENV["COHERE_API_KEY"]
|
|
89
140
|
}
|
|
90
|
-
|
|
91
141
|
config.default_classifier_name = "default"
|
|
142
|
+
|
|
143
|
+
# ----- Guardrails (LLM) -----
|
|
144
|
+
config.llm_provider = :openai
|
|
145
|
+
config.default_llm_model = "gpt-4o-mini"
|
|
146
|
+
config.llm_api_keys = {
|
|
147
|
+
openai: ENV["OPENAI_API_KEY"],
|
|
148
|
+
anthropic: ENV["ANTHROPIC_API_KEY"],
|
|
149
|
+
google: ENV["GOOGLE_AI_API_KEY"],
|
|
150
|
+
groq: ENV["GROQ_API_KEY"]
|
|
151
|
+
}
|
|
152
|
+
# Custom endpoint:
|
|
153
|
+
# config.llm_provider = :custom
|
|
154
|
+
# config.llm_url = "https://your-llm.example.com/v1"
|
|
155
|
+
# config.llm_key = ENV["CUSTOM_LLM_KEY"]
|
|
156
|
+
|
|
157
|
+
config.trace_llm_calls = true
|
|
92
158
|
end
|
|
93
159
|
```
|
|
94
160
|
|
|
95
|
-
Use
|
|
161
|
+
Use env vars or Rails credentials; do not commit API keys.
|
|
96
162
|
|
|
97
|
-
###
|
|
163
|
+
### Add vector columns (for vector classification only)
|
|
98
164
|
|
|
99
|
-
For models you want to classify
|
|
165
|
+
For models you want to classify:
|
|
100
166
|
|
|
101
167
|
```bash
|
|
102
168
|
rails g rails_ai_kit:vector_columns Article content
|
|
103
169
|
rails db:migrate
|
|
104
170
|
```
|
|
105
171
|
|
|
106
|
-
|
|
172
|
+
Adds `embedding`, `label`, `confidence_score` to the table.
|
|
107
173
|
|
|
108
174
|
---
|
|
109
175
|
|
|
110
|
-
## Usage
|
|
176
|
+
## Usage in detail
|
|
111
177
|
|
|
112
|
-
###
|
|
178
|
+
### Guardrails
|
|
113
179
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
RailsAiKit.
|
|
119
|
-
|
|
120
|
-
```
|
|
180
|
+
| Method | Description |
|
|
181
|
+
|--------|-------------|
|
|
182
|
+
| `RailsAiKit.guard(prompt:, model:, checks: [:toxicity, :hallucination], threshold: 0.8)` | Generate + run checks; returns `GuardResult` |
|
|
183
|
+
| `result.output`, `result.safe?`, `result.scores`, `result.action` | Response text, safe flag, scores hash, `:allow` / `:block` |
|
|
184
|
+
| `RailsAiKit.eval(dataset: "path.json", model:)` | Run eval dataset; returns accuracy, hallucination rate, toxicity failures |
|
|
185
|
+
| `rails g rails_ai_kit:dashboard` | Add `/ai_traces` dashboard |
|
|
121
186
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#### Declare classification on a model
|
|
187
|
+
**ai_generate macro (Guardrails):**
|
|
125
188
|
|
|
126
189
|
```ruby
|
|
127
190
|
class Article < ApplicationRecord
|
|
128
|
-
|
|
129
|
-
|
|
191
|
+
ai_generate :summary,
|
|
192
|
+
from: :content,
|
|
193
|
+
prompt_template: "Summarize in 2–3 sentences:\n\n%{text}",
|
|
194
|
+
guards: [:toxicity, :hallucination],
|
|
195
|
+
threshold: 0.85
|
|
130
196
|
end
|
|
197
|
+
article.ai_summary # => generated summary or nil if blocked
|
|
131
198
|
```
|
|
132
199
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
1. Generate an embedding for `content`
|
|
136
|
-
2. Store it in `embedding`
|
|
137
|
-
3. Compare to trained label vectors and set `label` and `confidence_score`
|
|
138
|
-
|
|
139
|
-
#### Train labels first
|
|
140
|
-
|
|
141
|
-
Before classifying, train each label with example texts:
|
|
142
|
-
|
|
143
|
-
```ruby
|
|
144
|
-
c = RailsAiKit.classifier("Article")
|
|
145
|
-
|
|
146
|
-
c.train("sports", examples: [
|
|
147
|
-
"football match",
|
|
148
|
-
"cricket tournament",
|
|
149
|
-
"Olympic gold medal"
|
|
150
|
-
])
|
|
151
|
-
c.train("politics", examples: ["election results", "parliament debate"])
|
|
152
|
-
c.train("technology", examples: ["new iPhone launch", "AI software update"])
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
#### Create records
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
article = Article.create!(content: "Apple released a new iPhone")
|
|
159
|
-
article.label # => "technology"
|
|
160
|
-
article.confidence_score # => 0.91
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
#### Classify without saving
|
|
200
|
+
### Vector classification
|
|
164
201
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
RailsAiKit.classifier("Article").
|
|
170
|
-
|
|
171
|
-
|
|
202
|
+
| Method | Description |
|
|
203
|
+
|--------|-------------|
|
|
204
|
+
| `RailsAiKit.classifier("Article").train("label", examples: ["...", "..."])` | Train a label with example texts |
|
|
205
|
+
| `RailsAiKit.classifier("Article").classify("some text")` | Classify without saving → `{ label:, confidence:, distance: }` |
|
|
206
|
+
| `RailsAiKit.classifier("Article").batch_classify(records, text_attribute: :content)` | Batch classify, set `label` and `confidence_score` on records |
|
|
207
|
+
| `Article.similar_to("query", limit: 5)` | Similarity search (nearest neighbors by embedding) |
|
|
208
|
+
| `Article.where(label: "sports")` | Filter by predicted label |
|
|
172
209
|
|
|
173
|
-
|
|
210
|
+
**vector_classify macro:**
|
|
174
211
|
|
|
175
212
|
```ruby
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
# Optionally: records.each(&:save!)
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
#### Similarity search
|
|
186
|
-
|
|
187
|
-
```ruby
|
|
188
|
-
Article.similar_to("new iPhone launch", limit: 5)
|
|
213
|
+
class Article < ApplicationRecord
|
|
214
|
+
vector_classify :content,
|
|
215
|
+
labels: ["sports", "politics", "technology"],
|
|
216
|
+
classifier_name: "Article"
|
|
217
|
+
end
|
|
218
|
+
# On save: embed content → store embedding → set label & confidence_score
|
|
189
219
|
```
|
|
190
220
|
|
|
191
|
-
|
|
221
|
+
### Embeddings (shared)
|
|
192
222
|
|
|
193
223
|
```ruby
|
|
194
|
-
|
|
195
|
-
|
|
224
|
+
RailsAiKit.embed("some text")
|
|
225
|
+
RailsAiKit.embedding.embed_batch(["a", "b"])
|
|
196
226
|
```
|
|
197
227
|
|
|
198
228
|
---
|
|
199
229
|
|
|
200
|
-
##
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
# app/models/support_ticket.rb
|
|
204
|
-
class SupportTicket < ApplicationRecord
|
|
205
|
-
vector_classify :message,
|
|
206
|
-
labels: ["billing", "technical", "account"],
|
|
207
|
-
classifier_name: "SupportTicket"
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Train once
|
|
211
|
-
c = RailsAiKit.classifier("SupportTicket")
|
|
212
|
-
c.train("billing", examples: ["My payment failed", "Refund request", "Invoice issue"])
|
|
213
|
-
c.train("technical", examples: ["App crashed", "Login not working", "Error message"])
|
|
214
|
-
c.train("account", examples: ["Change email", "Close my account", "Password reset"])
|
|
230
|
+
## Generators
|
|
215
231
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
| Generator | Purpose |
|
|
233
|
+
|-----------|---------|
|
|
234
|
+
| `rails g rails_ai_kit:install` | pgvector + `rails_ai_kit_labels` + `rails_ai_traces` + initializer |
|
|
235
|
+
| `rails g rails_ai_kit:vector_columns ModelName content_column` | Add embedding, label, confidence_score to a table (vector classification) |
|
|
236
|
+
| `rails g rails_ai_kit:dashboard` | Add AI Traces dashboard at `/ai_traces` (Guardrails) |
|
|
220
237
|
|
|
221
238
|
---
|
|
222
239
|
|
|
@@ -224,34 +241,43 @@ ticket.label # => "billing" → route to billing queue
|
|
|
224
241
|
|
|
225
242
|
| Option | Description | Default |
|
|
226
243
|
|--------|-------------|---------|
|
|
244
|
+
| **Embeddings (vector classification)** | | |
|
|
227
245
|
| `embedding_provider` | `:openai` or `:cohere` | `:openai` |
|
|
228
|
-
| `embedding_dimensions` | Vector size
|
|
229
|
-
| `api_keys` |
|
|
230
|
-
| `default_classifier_name` | Classifier name when
|
|
246
|
+
| `embedding_dimensions` | Vector size | `1536` |
|
|
247
|
+
| `api_keys` | Embedding API keys | `{}` |
|
|
248
|
+
| `default_classifier_name` | Classifier name when not specified | `"default"` |
|
|
249
|
+
| **LLM (Guardrails)** | | |
|
|
250
|
+
| `llm_provider` | `:openai`, `:anthropic`, `:google`, `:groq`, `:custom` | `:openai` |
|
|
251
|
+
| `llm_api_keys` | LLM API keys | `{}` |
|
|
252
|
+
| `llm_url`, `llm_key` | Custom LLM endpoint | `nil` |
|
|
253
|
+
| `default_llm_model` | Default model for guard / ai_generate | `nil` |
|
|
254
|
+
| `trace_llm_calls` | Log guard calls to `rails_ai_traces` | `true` |
|
|
231
255
|
|
|
232
256
|
---
|
|
233
257
|
|
|
234
|
-
##
|
|
258
|
+
## How it works
|
|
235
259
|
|
|
236
|
-
|
|
237
|
-
|-----------|---------|
|
|
238
|
-
| `rails g rails_ai_kit:install` | Enable pgvector + create `rails_ai_kit_labels` (for Classifier) |
|
|
239
|
-
| `rails g rails_ai_kit:vector_columns ModelName content_column` | Add `embedding`, `label`, `confidence_score` to a table |
|
|
260
|
+
### Guardrails
|
|
240
261
|
|
|
241
|
-
|
|
262
|
+
1. You call `RailsAiKit.guard(prompt:, model:, checks:, threshold:)`.
|
|
263
|
+
2. The gem generates a response with the configured LLM.
|
|
264
|
+
3. It runs the requested checks (toxicity, PII, hallucination, prompt injection) on the **output** and, for toxicity/prompt_injection, on the **user prompt** (in parallel where possible). Keyword rules backstop obvious offensive prompts.
|
|
265
|
+
4. It merges scores and sets `action` to `:block` if any score exceeds `threshold`.
|
|
266
|
+
5. Optionally it records the run in `rails_ai_traces`.
|
|
242
267
|
|
|
243
|
-
|
|
268
|
+
### Vector classification
|
|
244
269
|
|
|
245
|
-
1.
|
|
246
|
-
2.
|
|
247
|
-
3.
|
|
270
|
+
1. You train labels with example texts; the gem embeds them and stores one vector per label in `rails_ai_kit_labels`.
|
|
271
|
+
2. When you save a record with `vector_classify`, the gem embeds the source column, stores the vector, and finds the nearest label (cosine distance). It sets `label` and `confidence_score`.
|
|
272
|
+
3. Similarity search uses the same embedding column and pgvector nearest-neighbor.
|
|
248
273
|
|
|
249
274
|
---
|
|
250
275
|
|
|
251
276
|
## Roadmap
|
|
252
277
|
|
|
253
|
-
- **
|
|
254
|
-
- **
|
|
278
|
+
- **Guardrails:** More guards, configurable keyword lists, retry/fallback behavior.
|
|
279
|
+
- **Vector classification:** Hierarchical labels, confidence threshold, hybrid search, incremental learning.
|
|
280
|
+
- **More features:** Additional AI capabilities as the gem evolves.
|
|
255
281
|
|
|
256
282
|
---
|
|
257
283
|
|
|
@@ -262,8 +288,6 @@ bundle install
|
|
|
262
288
|
bundle exec rake install
|
|
263
289
|
```
|
|
264
290
|
|
|
265
|
-
Run tests (when added) with `bundle exec rspec` or `bundle exec rake test`.
|
|
266
|
-
|
|
267
291
|
---
|
|
268
292
|
|
|
269
293
|
## License
|
|
@@ -272,18 +296,15 @@ MIT.
|
|
|
272
296
|
|
|
273
297
|
---
|
|
274
298
|
|
|
275
|
-
##
|
|
299
|
+
## Related
|
|
276
300
|
|
|
277
|
-
-
|
|
278
|
-
-
|
|
279
|
-
- **EmbeddingService** – `RailsAiKit.embedding` / `RailsAiKit.embed(text)`.
|
|
280
|
-
- **Classifier** – Label training, `classify`, `classify_by_embedding`, `batch_classify`; uses `rails_ai_kit_labels` + Neighbor for similarity.
|
|
281
|
-
- **VectorClassify** – ActiveRecord concern: `vector_classify` macro, `similar_to` scope.
|
|
282
|
-
- **Generators** – Install (labels table), vector_columns (embedding/label/confidence on a model).
|
|
301
|
+
- [pgvector](https://github.com/pgvector/pgvector) – Vector similarity search for Postgres
|
|
302
|
+
- [Neighbor](https://github.com/ankane/neighbor) – Nearest neighbor search for Rails (used for vector classification)
|
|
283
303
|
|
|
284
304
|
---
|
|
285
305
|
|
|
286
|
-
##
|
|
306
|
+
## Creator
|
|
287
307
|
|
|
288
|
-
|
|
289
|
-
|
|
308
|
+
**Rohit Kushwaha** — *AI Engineer*
|
|
309
|
+
|
|
310
|
+
[](https://www.linkedin.com/in/rohit-kushwaha-6915b7197/)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<div class="rails_ai_kit traces">
|
|
2
|
+
<h1>AI Traces</h1>
|
|
3
|
+
|
|
4
|
+
<% if defined?(@stats) && @stats %>
|
|
5
|
+
<div class="stats">
|
|
6
|
+
<span>Total: <%= @stats[:total] %></span>
|
|
7
|
+
<span>Blocked: <%= @stats[:blocked] %></span>
|
|
8
|
+
<span>Models: <%= @stats[:models]&.join(", ") %></span>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<table>
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th>Time</th>
|
|
16
|
+
<th>Model</th>
|
|
17
|
+
<th>Prompt</th>
|
|
18
|
+
<th>Response</th>
|
|
19
|
+
<th>Guard scores</th>
|
|
20
|
+
<th>Latency (ms)</th>
|
|
21
|
+
<th>Action</th>
|
|
22
|
+
</tr>
|
|
23
|
+
</thead>
|
|
24
|
+
<tbody>
|
|
25
|
+
<% @traces.each do |trace| %>
|
|
26
|
+
<tr class="<%= trace.action %>">
|
|
27
|
+
<td><%= trace.created_at&.strftime("%Y-%m-%d %H:%M") %></td>
|
|
28
|
+
<td><%= trace.model %></td>
|
|
29
|
+
<td><%= truncate(trace.prompt, length: 80) %></td>
|
|
30
|
+
<td><%= truncate(trace.response, length: 80) %></td>
|
|
31
|
+
<td><%= trace.guard_scores.is_a?(Hash) ? trace.guard_scores.to_json : trace.guard_scores %></td>
|
|
32
|
+
<td><%= trace.latency_ms %></td>
|
|
33
|
+
<td><%= trace.action %></td>
|
|
34
|
+
</tr>
|
|
35
|
+
<% end %>
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
class TracesController < ActionController::Base
|
|
5
|
+
layout "application"
|
|
6
|
+
before_action :set_traces
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@traces = @traces.recent.limit(100)
|
|
10
|
+
@traces = @traces.by_model(params[:model]) if params[:model].present?
|
|
11
|
+
@traces = @traces.blocked if params[:blocked] == "1"
|
|
12
|
+
@stats = {
|
|
13
|
+
total: RailsAiKit::Trace.count,
|
|
14
|
+
blocked: RailsAiKit::Trace.blocked.count,
|
|
15
|
+
models: RailsAiKit::Trace.distinct.pluck(:model).compact
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def set_traces
|
|
22
|
+
@traces = RailsAiKit::Trace.all
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsAiKit
|
|
6
|
+
module Generators
|
|
7
|
+
class DashboardGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Adds AI Traces dashboard (controller + view + route) at /ai_traces"
|
|
11
|
+
|
|
12
|
+
def create_controller
|
|
13
|
+
copy_file "traces_controller.rb", "app/controllers/rails_ai_kit/traces_controller.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_view
|
|
17
|
+
copy_file "index.html.erb", "app/views/rails_ai_kit/traces/index.html.erb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_route
|
|
21
|
+
route 'get "/ai_traces", to: "rails_ai_kit/traces#index", as: :ai_traces'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsAiKitTables < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
enable_extension "vector" unless extension_enabled?("vector")
|
|
6
|
+
|
|
7
|
+
create_table :rails_ai_kit_labels do |t|
|
|
8
|
+
t.string :classifier_name, null: false
|
|
9
|
+
t.string :label_name, null: false
|
|
10
|
+
t.vector :embedding, limit: <%= (defined?(RailsAiKit) && RailsAiKit.configuration.embedding_dimensions) || 1536 %>, null: false
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :rails_ai_kit_labels, [:classifier_name, :label_name], unique: true
|
|
14
|
+
|
|
15
|
+
create_table :rails_ai_traces do |t|
|
|
16
|
+
t.text :prompt, null: false
|
|
17
|
+
t.text :response
|
|
18
|
+
t.string :model
|
|
19
|
+
t.integer :prompt_tokens
|
|
20
|
+
t.integer :completion_tokens
|
|
21
|
+
t.jsonb :guard_scores, default: {}
|
|
22
|
+
t.decimal :latency_ms, precision: 10, scale: 2
|
|
23
|
+
t.string :action, default: "allow"
|
|
24
|
+
t.timestamps
|
|
25
|
+
end
|
|
26
|
+
add_index :rails_ai_traces, :created_at
|
|
27
|
+
add_index :rails_ai_traces, :model
|
|
28
|
+
add_index :rails_ai_traces, :action
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsAiTraces < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_ai_traces do |t|
|
|
6
|
+
t.text :prompt, null: false
|
|
7
|
+
t.text :response
|
|
8
|
+
t.string :model
|
|
9
|
+
t.integer :prompt_tokens
|
|
10
|
+
t.integer :completion_tokens
|
|
11
|
+
t.jsonb :guard_scores, default: {}
|
|
12
|
+
t.decimal :latency_ms, precision: 10, scale: 2
|
|
13
|
+
t.string :action, default: "allow"
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :rails_ai_traces, :created_at
|
|
18
|
+
add_index :rails_ai_traces, :model
|
|
19
|
+
add_index :rails_ai_traces, :action
|
|
20
|
+
end
|
|
21
|
+
end
|