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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
module LlmProviders
|
|
5
|
+
class Anthropic < Base
|
|
6
|
+
DEFAULT_MODEL = "claude-sonnet-4-20250514"
|
|
7
|
+
BASE_URL = "https://api.anthropic.com/v1"
|
|
8
|
+
|
|
9
|
+
def initialize(api_key:, base_url: nil)
|
|
10
|
+
super(api_key: api_key, base_url: base_url)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate(messages, model: nil, max_tokens: 1024)
|
|
14
|
+
model ||= DEFAULT_MODEL
|
|
15
|
+
url = "#{base_url}/messages"
|
|
16
|
+
system_msg = messages.find { |m| (m["role"] || m[:role]).to_s == "system" }
|
|
17
|
+
chat_msgs = messages.reject { |m| (m["role"] || m[:role]).to_s == "system" }
|
|
18
|
+
body = {
|
|
19
|
+
model: model,
|
|
20
|
+
max_tokens: max_tokens,
|
|
21
|
+
messages: chat_msgs.map { |m| { role: (m["role"] || m[:role]).to_s, content: m["content"] || m[:content] } }
|
|
22
|
+
}
|
|
23
|
+
body[:system] = system_msg["content"] || system_msg[:content] if system_msg
|
|
24
|
+
resp = client.post(url) do |req|
|
|
25
|
+
req.body = body.to_json
|
|
26
|
+
req.headers["Content-Type"] = "application/json"
|
|
27
|
+
req.headers["x-api-key"] = @api_key
|
|
28
|
+
req.headers["anthropic-version"] = "2023-06-01"
|
|
29
|
+
end
|
|
30
|
+
parse_response(resp, model)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def base_url
|
|
36
|
+
@base_url || BASE_URL
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def client
|
|
40
|
+
@client ||= Faraday.new(url: base_url) do |f|
|
|
41
|
+
f.response :json
|
|
42
|
+
f.adapter Faraday.default_adapter
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def parse_response(resp, model)
|
|
47
|
+
raise RailsAiKit::Error, "Anthropic API error: #{resp.body}" unless resp.success?
|
|
48
|
+
data = resp.body
|
|
49
|
+
content = data["content"]&.find { |c| c["type"] == "text" }&.dig("text").to_s
|
|
50
|
+
usage = data["usage"] || {}
|
|
51
|
+
{
|
|
52
|
+
content: content,
|
|
53
|
+
model: data["model"] || model,
|
|
54
|
+
usage: { prompt_tokens: usage["input_tokens"] || 0, completion_tokens: usage["output_tokens"] || 0 },
|
|
55
|
+
raw: data
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
module LlmProviders
|
|
5
|
+
class Base
|
|
6
|
+
class NotImplementedError < RailsAiKit::Error; end
|
|
7
|
+
|
|
8
|
+
# @param api_key [String]
|
|
9
|
+
# @param base_url [String, nil] Optional override (e.g. for custom)
|
|
10
|
+
def initialize(api_key:, base_url: nil)
|
|
11
|
+
@api_key = api_key
|
|
12
|
+
@base_url = base_url
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param messages [Array<Hash>] [{ role: "user"|"assistant"|"system", content: "..." }]
|
|
16
|
+
# @param model [String, nil] Model name override
|
|
17
|
+
# @param max_tokens [Integer]
|
|
18
|
+
# @return [Hash] { content: String, model: String, usage: { prompt_tokens:, completion_tokens: }, raw: Hash }
|
|
19
|
+
def generate(messages, model: nil, max_tokens: 1024)
|
|
20
|
+
raise NotImplementedError, "#{self.class}#generate must be implemented"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def extract_text(content)
|
|
26
|
+
return content if content.is_a?(String)
|
|
27
|
+
return content["text"] if content.is_a?(Hash) && content["text"]
|
|
28
|
+
content.dig("content", 0, "text") if content.is_a?(Hash)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
module LlmProviders
|
|
5
|
+
# Custom provider: any OpenAI-compatible endpoint (URL + API key).
|
|
6
|
+
# Use for local models, other hosts, or proxy endpoints.
|
|
7
|
+
class Custom < Base
|
|
8
|
+
def initialize(api_key:, base_url:)
|
|
9
|
+
raise ArgumentError, "base_url is required for custom provider" if base_url.blank?
|
|
10
|
+
super(api_key: api_key, base_url: base_url.to_s.sub(%r{/$}, ""))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate(messages, model: nil, max_tokens: 1024)
|
|
14
|
+
model ||= "default"
|
|
15
|
+
url = "#{@base_url}/chat/completions"
|
|
16
|
+
body = {
|
|
17
|
+
model: model,
|
|
18
|
+
messages: normalize_messages(messages),
|
|
19
|
+
max_tokens: max_tokens
|
|
20
|
+
}
|
|
21
|
+
resp = client.post(url) do |req|
|
|
22
|
+
req.body = body.to_json
|
|
23
|
+
req.headers["Content-Type"] = "application/json"
|
|
24
|
+
req.headers["Authorization"] = "Bearer #{@api_key}" if @api_key.present?
|
|
25
|
+
end
|
|
26
|
+
parse_response(resp, model)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def client
|
|
32
|
+
@client ||= Faraday.new do |f|
|
|
33
|
+
f.response :json
|
|
34
|
+
f.adapter Faraday.default_adapter
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def normalize_messages(messages)
|
|
39
|
+
messages.map do |m|
|
|
40
|
+
{ role: m["role"] || m[:role], content: m["content"] || m[:content] }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_response(resp, model)
|
|
45
|
+
raise RailsAiKit::Error, "Custom LLM API error: #{resp.body}" unless resp.success?
|
|
46
|
+
data = resp.body.is_a?(Hash) ? resp.body : JSON.parse(resp.body)
|
|
47
|
+
choice = data["choices"]&.first
|
|
48
|
+
content = choice.dig("message", "content").to_s
|
|
49
|
+
usage = data["usage"] || {}
|
|
50
|
+
{
|
|
51
|
+
content: content,
|
|
52
|
+
model: data["model"] || model,
|
|
53
|
+
usage: { prompt_tokens: usage["prompt_tokens"] || 0, completion_tokens: usage["completion_tokens"] || 0 },
|
|
54
|
+
raw: data
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
module LlmProviders
|
|
5
|
+
class Google < Base
|
|
6
|
+
DEFAULT_MODEL = "gemini-1.5-flash"
|
|
7
|
+
BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
|
8
|
+
|
|
9
|
+
def initialize(api_key:, base_url: nil)
|
|
10
|
+
super(api_key: api_key, base_url: base_url)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate(messages, model: nil, max_tokens: 1024)
|
|
14
|
+
model ||= DEFAULT_MODEL
|
|
15
|
+
url = "#{base_url}/models/#{model}:generateContent?key=#{@api_key}"
|
|
16
|
+
system_instruction = messages.find { |m| (m["role"] || m[:role]).to_s == "system" }
|
|
17
|
+
parts = messages.reject { |m| (m["role"] || m[:role]).to_s == "system" }
|
|
18
|
+
.map { |m| (m["content"] || m[:content]).to_s }
|
|
19
|
+
.join("\n\n")
|
|
20
|
+
body = {
|
|
21
|
+
contents: [{ parts: [{ text: parts }] }],
|
|
22
|
+
generationConfig: { maxOutputTokens: max_tokens }
|
|
23
|
+
}
|
|
24
|
+
body[:systemInstruction] = { parts: [{ text: system_instruction["content"] || system_instruction[:content] }] } if system_instruction
|
|
25
|
+
resp = client.post(url) do |req|
|
|
26
|
+
req.body = body.to_json
|
|
27
|
+
req.headers["Content-Type"] = "application/json"
|
|
28
|
+
end
|
|
29
|
+
parse_response(resp, model)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def base_url
|
|
35
|
+
@base_url || BASE_URL
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def client
|
|
39
|
+
@client ||= Faraday.new(url: base_url) do |f|
|
|
40
|
+
f.response :json
|
|
41
|
+
f.adapter Faraday.default_adapter
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_response(resp, model)
|
|
46
|
+
raise RailsAiKit::Error, "Google Gemini API error: #{resp.body}" unless resp.success?
|
|
47
|
+
data = resp.body
|
|
48
|
+
text = data.dig("candidates", 0, "content", "parts", 0, "text").to_s
|
|
49
|
+
usage = data.dig("usageMetadata") || {}
|
|
50
|
+
{
|
|
51
|
+
content: text,
|
|
52
|
+
model: model,
|
|
53
|
+
usage: { prompt_tokens: usage["promptTokenCount"] || 0, completion_tokens: usage["candidatesTokenCount"] || 0 },
|
|
54
|
+
raw: data
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
module LlmProviders
|
|
5
|
+
# Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1
|
|
6
|
+
class Groq < Base
|
|
7
|
+
DEFAULT_MODEL = "llama-3.1-70b-versatile"
|
|
8
|
+
BASE_URL = "https://api.groq.com/openai/v1"
|
|
9
|
+
|
|
10
|
+
def initialize(api_key:, base_url: nil)
|
|
11
|
+
super(api_key: api_key, base_url: base_url)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate(messages, model: nil, max_tokens: 1024)
|
|
15
|
+
model ||= DEFAULT_MODEL
|
|
16
|
+
url = "#{base_url}/chat/completions"
|
|
17
|
+
body = {
|
|
18
|
+
model: model,
|
|
19
|
+
messages: normalize_messages(messages),
|
|
20
|
+
max_tokens: max_tokens
|
|
21
|
+
}
|
|
22
|
+
resp = client.post(url) do |req|
|
|
23
|
+
req.body = body.to_json
|
|
24
|
+
req.headers["Content-Type"] = "application/json"
|
|
25
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
26
|
+
end
|
|
27
|
+
parse_response(resp, model)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def base_url
|
|
33
|
+
@base_url || BASE_URL
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def client
|
|
37
|
+
@client ||= Faraday.new(url: base_url) do |f|
|
|
38
|
+
f.response :json
|
|
39
|
+
f.adapter Faraday.default_adapter
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_messages(messages)
|
|
44
|
+
messages.map do |m|
|
|
45
|
+
{ role: m["role"] || m[:role], content: m["content"] || m[:content] }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_response(resp, model)
|
|
50
|
+
raise RailsAiKit::Error, "Groq API error: #{resp.body}" unless resp.success?
|
|
51
|
+
data = resp.body
|
|
52
|
+
choice = data["choices"]&.first
|
|
53
|
+
content = choice.dig("message", "content").to_s
|
|
54
|
+
usage = data["usage"] || {}
|
|
55
|
+
{
|
|
56
|
+
content: content,
|
|
57
|
+
model: data["model"] || model,
|
|
58
|
+
usage: { prompt_tokens: usage["prompt_tokens"] || 0, completion_tokens: usage["completion_tokens"] || 0 },
|
|
59
|
+
raw: data
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
module LlmProviders
|
|
5
|
+
class Openai < Base
|
|
6
|
+
DEFAULT_MODEL = "gpt-4o-mini"
|
|
7
|
+
BASE_URL = "https://api.openai.com/v1"
|
|
8
|
+
|
|
9
|
+
def initialize(api_key:, base_url: nil)
|
|
10
|
+
super(api_key: api_key, base_url: base_url)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate(messages, model: nil, max_tokens: 1024)
|
|
14
|
+
model ||= DEFAULT_MODEL
|
|
15
|
+
url = "#{base_url}/chat/completions"
|
|
16
|
+
body = {
|
|
17
|
+
model: model,
|
|
18
|
+
messages: normalize_messages(messages),
|
|
19
|
+
max_tokens: max_tokens
|
|
20
|
+
}
|
|
21
|
+
resp = client.post(url) do |req|
|
|
22
|
+
req.body = body.to_json
|
|
23
|
+
req.headers["Content-Type"] = "application/json"
|
|
24
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
25
|
+
end
|
|
26
|
+
parse_response(resp, model)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def base_url
|
|
32
|
+
@base_url || BASE_URL
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def client
|
|
36
|
+
@client ||= Faraday.new(url: base_url) do |f|
|
|
37
|
+
f.response :json
|
|
38
|
+
f.adapter Faraday.default_adapter
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_messages(messages)
|
|
43
|
+
messages.map do |m|
|
|
44
|
+
{ role: m["role"] || m[:role], content: m["content"] || m[:content] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_response(resp, model)
|
|
49
|
+
raise RailsAiKit::Error, "OpenAI API error: #{resp.body}" unless resp.success?
|
|
50
|
+
data = resp.body
|
|
51
|
+
choice = data["choices"]&.first
|
|
52
|
+
content = choice.dig("message", "content").to_s
|
|
53
|
+
usage = data["usage"] || {}
|
|
54
|
+
{
|
|
55
|
+
content: content,
|
|
56
|
+
model: data["model"] || model,
|
|
57
|
+
usage: { prompt_tokens: usage["prompt_tokens"] || 0, completion_tokens: usage["completion_tokens"] || 0 },
|
|
58
|
+
raw: data
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiKit
|
|
4
|
+
class Trace < ActiveRecord::Base
|
|
5
|
+
self.table_name = "rails_ai_traces"
|
|
6
|
+
|
|
7
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
8
|
+
scope :blocked, -> { where(action: "block") }
|
|
9
|
+
scope :by_model, ->(model) { where(model: model) }
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def record(prompt:, response:, model:, usage: nil, guard_scores: nil, latency_ms: nil, action: :allow)
|
|
13
|
+
create(
|
|
14
|
+
prompt: prompt.is_a?(String) ? prompt : prompt.to_json,
|
|
15
|
+
response: response,
|
|
16
|
+
model: model,
|
|
17
|
+
prompt_tokens: usage&.dig("prompt_tokens") || usage&.dig(:prompt_tokens),
|
|
18
|
+
completion_tokens: usage&.dig("completion_tokens") || usage&.dig(:completion_tokens),
|
|
19
|
+
guard_scores: guard_scores.is_a?(Hash) ? guard_scores : (guard_scores.present? ? JSON.parse(guard_scores.to_s) : {}),
|
|
20
|
+
latency_ms: latency_ms,
|
|
21
|
+
action: action.to_s
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/rails_ai_kit/version.rb
CHANGED
data/lib/rails_ai_kit.rb
CHANGED
|
@@ -4,7 +4,7 @@ require_relative "rails_ai_kit/version"
|
|
|
4
4
|
require_relative "rails_ai_kit/configuration"
|
|
5
5
|
|
|
6
6
|
module RailsAiKit
|
|
7
|
-
class Error < StandardError; end
|
|
7
|
+
class Error < StandardError; end
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
require_relative "rails_ai_kit/embedding_providers/base"
|
|
@@ -12,13 +12,28 @@ require_relative "rails_ai_kit/embedding_providers/openai"
|
|
|
12
12
|
require_relative "rails_ai_kit/embedding_providers/cohere"
|
|
13
13
|
require_relative "rails_ai_kit/embedding_service"
|
|
14
14
|
|
|
15
|
+
# LLM and Guardrails (no ActiveRecord dependency for client/guardrails)
|
|
16
|
+
require_relative "rails_ai_kit/llm_providers/base"
|
|
17
|
+
require_relative "rails_ai_kit/llm_providers/openai"
|
|
18
|
+
require_relative "rails_ai_kit/llm_providers/anthropic"
|
|
19
|
+
require_relative "rails_ai_kit/llm_providers/google"
|
|
20
|
+
require_relative "rails_ai_kit/llm_providers/groq"
|
|
21
|
+
require_relative "rails_ai_kit/llm_providers/custom"
|
|
22
|
+
require_relative "rails_ai_kit/llm_client"
|
|
23
|
+
require_relative "rails_ai_kit/guards/base"
|
|
24
|
+
require_relative "rails_ai_kit/guards/toxicity"
|
|
25
|
+
require_relative "rails_ai_kit/guards/pii"
|
|
26
|
+
require_relative "rails_ai_kit/guards/hallucination"
|
|
27
|
+
require_relative "rails_ai_kit/guards/prompt_injection"
|
|
28
|
+
require_relative "rails_ai_kit/guard_result"
|
|
29
|
+
require_relative "rails_ai_kit/guardrails"
|
|
30
|
+
require_relative "rails_ai_kit/ai_generate"
|
|
31
|
+
require_relative "rails_ai_kit/eval"
|
|
32
|
+
|
|
15
33
|
# Load models and AR integration only after ActiveRecord is ready.
|
|
16
|
-
# Neighbor 0.6+ adds has_neighbors in its own on_load(:active_record).
|
|
17
|
-
# Gemfile order can make our hook run before Neighbor's (or before neighbor is required).
|
|
18
|
-
# So we ensure the neighbor gem is loaded and has_neighbors is on Base before our models.
|
|
19
34
|
ActiveSupport.on_load(:active_record) do
|
|
20
35
|
unless ActiveRecord::Base.respond_to?(:has_neighbors)
|
|
21
|
-
require "neighbor"
|
|
36
|
+
require "neighbor"
|
|
22
37
|
require "neighbor/attribute"
|
|
23
38
|
require "neighbor/model"
|
|
24
39
|
require "neighbor/normalized_attribute"
|
|
@@ -28,17 +43,34 @@ ActiveSupport.on_load(:active_record) do
|
|
|
28
43
|
require_relative "rails_ai_kit/label_record"
|
|
29
44
|
require_relative "rails_ai_kit/classifier"
|
|
30
45
|
require_relative "rails_ai_kit/vector_classify"
|
|
46
|
+
require_relative "rails_ai_kit/trace"
|
|
31
47
|
ActiveRecord::Base.include RailsAiKit::VectorClassify
|
|
48
|
+
ActiveRecord::Base.include RailsAiKit::AiGenerate
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
module RailsAiKit
|
|
35
|
-
# Top-level classifier using default classifier name. For custom name use Classifier.new(classifier_name: "MyClassifier").
|
|
36
52
|
def self.classifier(classifier_name = nil)
|
|
37
53
|
Classifier.new(classifier_name: classifier_name)
|
|
38
54
|
end
|
|
39
55
|
|
|
40
|
-
# Convenience: embed a single text. Same as RailsAiKit.embedding.embed(text).
|
|
41
56
|
def self.embed(text)
|
|
42
57
|
embedding.embed(text)
|
|
43
58
|
end
|
|
59
|
+
|
|
60
|
+
def self.llm
|
|
61
|
+
@llm_client ||= LlmClient.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Run guardrails: generate response and evaluate with LLM-as-judge.
|
|
65
|
+
# @return [RailsAiKit::GuardResult]
|
|
66
|
+
def self.guard(prompt: nil, messages: nil, model: nil, checks: %i[toxicity hallucination], threshold: 0.8, system_prompt: nil)
|
|
67
|
+
Guardrails.new.guard(prompt: prompt, messages: messages, model: model, checks: checks, threshold: threshold, system_prompt: system_prompt)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Run evaluation on a dataset.
|
|
71
|
+
# @param dataset [String, Array] Path to JSON or array of { prompt:, expected: (optional) }
|
|
72
|
+
# @return [Hash] { total:, accuracy:, hallucination_rate:, toxicity_failures:, details: }
|
|
73
|
+
def self.eval(dataset:, model: nil, checks: %i[toxicity hallucination])
|
|
74
|
+
Eval.new(dataset: dataset, model: model, checks: checks).run
|
|
75
|
+
end
|
|
44
76
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_ai_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rails AI Kit Contributors
|
|
@@ -76,19 +76,41 @@ extra_rdoc_files: []
|
|
|
76
76
|
files:
|
|
77
77
|
- CHANGELOG.md
|
|
78
78
|
- README.md
|
|
79
|
+
- lib/generators/rails_ai_kit/dashboard/templates/index.html.erb
|
|
80
|
+
- lib/generators/rails_ai_kit/dashboard/templates/traces_controller.rb
|
|
81
|
+
- lib/generators/rails_ai_kit/dashboard_generator.rb
|
|
79
82
|
- lib/generators/rails_ai_kit/install/templates/create_rails_ai_kit_labels.rb
|
|
83
|
+
- lib/generators/rails_ai_kit/install/templates/create_rails_ai_kit_tables.rb
|
|
84
|
+
- lib/generators/rails_ai_kit/install/templates/create_rails_ai_traces.rb
|
|
80
85
|
- lib/generators/rails_ai_kit/install/templates/rails_ai_kit.rb
|
|
81
86
|
- lib/generators/rails_ai_kit/install_generator.rb
|
|
82
87
|
- lib/generators/rails_ai_kit/vector_columns/templates/add_vector_columns.rb
|
|
83
88
|
- lib/generators/rails_ai_kit/vector_columns_generator.rb
|
|
84
89
|
- lib/rails_ai_kit.rb
|
|
90
|
+
- lib/rails_ai_kit/ai_generate.rb
|
|
85
91
|
- lib/rails_ai_kit/classifier.rb
|
|
86
92
|
- lib/rails_ai_kit/configuration.rb
|
|
87
93
|
- lib/rails_ai_kit/embedding_providers/base.rb
|
|
88
94
|
- lib/rails_ai_kit/embedding_providers/cohere.rb
|
|
89
95
|
- lib/rails_ai_kit/embedding_providers/openai.rb
|
|
90
96
|
- lib/rails_ai_kit/embedding_service.rb
|
|
97
|
+
- lib/rails_ai_kit/eval.rb
|
|
98
|
+
- lib/rails_ai_kit/guard_result.rb
|
|
99
|
+
- lib/rails_ai_kit/guardrails.rb
|
|
100
|
+
- lib/rails_ai_kit/guards/base.rb
|
|
101
|
+
- lib/rails_ai_kit/guards/hallucination.rb
|
|
102
|
+
- lib/rails_ai_kit/guards/pii.rb
|
|
103
|
+
- lib/rails_ai_kit/guards/prompt_injection.rb
|
|
104
|
+
- lib/rails_ai_kit/guards/toxicity.rb
|
|
91
105
|
- lib/rails_ai_kit/label_record.rb
|
|
106
|
+
- lib/rails_ai_kit/llm_client.rb
|
|
107
|
+
- lib/rails_ai_kit/llm_providers/anthropic.rb
|
|
108
|
+
- lib/rails_ai_kit/llm_providers/base.rb
|
|
109
|
+
- lib/rails_ai_kit/llm_providers/custom.rb
|
|
110
|
+
- lib/rails_ai_kit/llm_providers/google.rb
|
|
111
|
+
- lib/rails_ai_kit/llm_providers/groq.rb
|
|
112
|
+
- lib/rails_ai_kit/llm_providers/openai.rb
|
|
113
|
+
- lib/rails_ai_kit/trace.rb
|
|
92
114
|
- lib/rails_ai_kit/vector_classify.rb
|
|
93
115
|
- lib/rails_ai_kit/version.rb
|
|
94
116
|
homepage: https://github.com/imrrohitt/rails_ai_kit
|