rails_ai_promptable 0.1.0 → 0.1.2
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/.rubocop.yml +25 -0
- data/CHANGELOG.md +18 -0
- data/README.md +1 -1
- data/lib/generators/rails_ai_promptable/install_generator.rb +21 -23
- data/lib/rails_ai_promptable/background_job.rb +2 -4
- data/lib/rails_ai_promptable/configuration.rb +26 -26
- data/lib/rails_ai_promptable/logger.rb +2 -1
- data/lib/rails_ai_promptable/promptable.rb +12 -8
- data/lib/rails_ai_promptable/providers/anthropic_provider.rb +13 -13
- data/lib/rails_ai_promptable/providers/azure_openai_provider.rb +14 -14
- data/lib/rails_ai_promptable/providers/cohere_provider.rb +10 -10
- data/lib/rails_ai_promptable/providers/gemini_provider.rb +9 -9
- data/lib/rails_ai_promptable/providers/mistral_provider.rb +11 -11
- data/lib/rails_ai_promptable/providers/openai_provider.rb +10 -10
- data/lib/rails_ai_promptable/providers/openrouter_provider.rb +12 -12
- data/lib/rails_ai_promptable/providers.rb +11 -10
- data/lib/rails_ai_promptable/template_registry.rb +5 -5
- data/lib/rails_ai_promptable/version.rb +1 -1
- data/lib/rails_ai_promptable.rb +8 -8
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff190c064892809b494cf10ccba89de83af0a15b2427268b187f3f6e5fcac52e
|
|
4
|
+
data.tar.gz: 12c92260228ab4a8dc81b625e7952273a10b940e0dd52e2c392b6cb332b9ec6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 331cc36fe43dac4d8a8f949e2465f24e6b39a9030de82cc3662e28080e6ab686929f47e3d83c5e3cdee63c69200872bd179ac0746446ab50ced359946185aea1
|
|
7
|
+
data.tar.gz: 85110e55a94e6ce2e74da9a31c46fcf88ac5679d9b81a4052f77113f7caac63c3766a67ac70b6214dea9dac3700c185766fed9333963191555d79e2c9ce7c7fb
|
data/.rubocop.yml
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
3
5
|
|
|
4
6
|
Style/StringLiterals:
|
|
5
7
|
EnforcedStyle: double_quotes
|
|
6
8
|
|
|
7
9
|
Style/StringLiteralsInInterpolation:
|
|
8
10
|
EnforcedStyle: double_quotes
|
|
11
|
+
|
|
12
|
+
Style/Documentation:
|
|
13
|
+
Enabled: false
|
|
14
|
+
|
|
15
|
+
Metrics/BlockLength:
|
|
16
|
+
Exclude:
|
|
17
|
+
- "spec/**/*_spec.rb"
|
|
18
|
+
- "*.gemspec"
|
|
19
|
+
|
|
20
|
+
Metrics/MethodLength:
|
|
21
|
+
Max: 30
|
|
22
|
+
|
|
23
|
+
Metrics/AbcSize:
|
|
24
|
+
Max: 30
|
|
25
|
+
|
|
26
|
+
Metrics/CyclomaticComplexity:
|
|
27
|
+
Max: 10
|
|
28
|
+
|
|
29
|
+
Layout/LineLength:
|
|
30
|
+
Max: 150
|
|
31
|
+
|
|
32
|
+
Lint/UnusedMethodArgument:
|
|
33
|
+
AllowUnusedKeywordArguments: true
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.2] - 2025-11-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fixed GitHub Actions badge URL in README to display correctly
|
|
7
|
+
- Fixed gem version badge display on RubyGems page
|
|
8
|
+
|
|
9
|
+
## [0.1.1] - 2025-11-06
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Fixed render_template to handle both %{key} and %<key>s string interpolation formats
|
|
13
|
+
- Fixed gemspec validation error by excluding gem files from repository
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Applied RuboCop auto-corrections and style improvements
|
|
17
|
+
- Updated RuboCop configuration with NewCops enabled
|
|
18
|
+
- Improved code formatting and consistency across all files
|
|
19
|
+
- Updated ENV variable access to use ENV.fetch for better nil handling
|
|
20
|
+
|
|
3
21
|
## [0.1.0] - 2025-11-05
|
|
4
22
|
|
|
5
23
|
- Initial release
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A powerful and flexible gem to integrate AI capabilities into your Rails applications. Support for multiple AI providers including OpenAI, Anthropic (Claude), Google Gemini, Cohere, Azure OpenAI, Mistral AI, and OpenRouter.
|
|
4
4
|
|
|
5
5
|
[](https://badge.fury.io/rb/rails_ai_promptable)
|
|
6
|
-
[](https://github.com/shoaibmalik786/rails_ai_promptable/actions)
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
@@ -1,48 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "rails/generators"
|
|
4
4
|
|
|
5
5
|
module RailsAiPromptable
|
|
6
6
|
module Generators
|
|
7
7
|
class InstallGenerator < Rails::Generators::Base
|
|
8
|
-
source_root File.expand_path(
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
9
|
|
|
10
|
-
desc
|
|
10
|
+
desc "Creates a RailsAIPromptable initializer file"
|
|
11
11
|
|
|
12
12
|
class_option :provider,
|
|
13
13
|
type: :string,
|
|
14
|
-
default:
|
|
15
|
-
desc:
|
|
14
|
+
default: "openai",
|
|
15
|
+
desc: "AI provider to use (openai, anthropic, gemini, cohere, azure_openai, mistral, openrouter)"
|
|
16
16
|
|
|
17
17
|
def copy_initializer_file
|
|
18
|
-
template
|
|
18
|
+
template "rails_ai_promptable.rb.tt", "config/initializers/rails_ai_promptable.rb"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def show_readme
|
|
22
|
-
readme
|
|
22
|
+
readme "POST_INSTALL" if behavior == :invoke
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
def provider_name
|
|
28
|
-
options[
|
|
28
|
+
options["provider"].to_s
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def provider_config
|
|
32
32
|
case provider_name
|
|
33
|
-
when
|
|
34
|
-
openai_config
|
|
35
|
-
when 'anthropic', 'claude'
|
|
33
|
+
when "anthropic", "claude"
|
|
36
34
|
anthropic_config
|
|
37
|
-
when
|
|
35
|
+
when "gemini", "google"
|
|
38
36
|
gemini_config
|
|
39
|
-
when
|
|
37
|
+
when "cohere"
|
|
40
38
|
cohere_config
|
|
41
|
-
when
|
|
39
|
+
when "azure_openai", "azure"
|
|
42
40
|
azure_config
|
|
43
|
-
when
|
|
41
|
+
when "mistral"
|
|
44
42
|
mistral_config
|
|
45
|
-
when
|
|
43
|
+
when "openrouter"
|
|
46
44
|
openrouter_config
|
|
47
45
|
else
|
|
48
46
|
openai_config
|
|
@@ -51,7 +49,7 @@ module RailsAiPromptable
|
|
|
51
49
|
|
|
52
50
|
def openai_config
|
|
53
51
|
{
|
|
54
|
-
provider:
|
|
52
|
+
provider: ":openai",
|
|
55
53
|
api_key: "ENV['OPENAI_API_KEY']",
|
|
56
54
|
default_model: "'gpt-4o-mini'",
|
|
57
55
|
additional_config: " # config.openai_base_url = 'https://api.openai.com/v1' # Optional: custom endpoint"
|
|
@@ -60,7 +58,7 @@ module RailsAiPromptable
|
|
|
60
58
|
|
|
61
59
|
def anthropic_config
|
|
62
60
|
{
|
|
63
|
-
provider:
|
|
61
|
+
provider: ":anthropic",
|
|
64
62
|
api_key: "ENV['ANTHROPIC_API_KEY']",
|
|
65
63
|
default_model: "'claude-3-5-sonnet-20241022'",
|
|
66
64
|
additional_config: " # config.anthropic_base_url = 'https://api.anthropic.com/v1' # Optional: custom endpoint"
|
|
@@ -69,7 +67,7 @@ module RailsAiPromptable
|
|
|
69
67
|
|
|
70
68
|
def gemini_config
|
|
71
69
|
{
|
|
72
|
-
provider:
|
|
70
|
+
provider: ":gemini",
|
|
73
71
|
api_key: "ENV['GEMINI_API_KEY']",
|
|
74
72
|
default_model: "'gemini-pro'",
|
|
75
73
|
additional_config: " # config.gemini_base_url = 'https://generativelanguage.googleapis.com/v1beta' # Optional: custom endpoint"
|
|
@@ -78,7 +76,7 @@ module RailsAiPromptable
|
|
|
78
76
|
|
|
79
77
|
def cohere_config
|
|
80
78
|
{
|
|
81
|
-
provider:
|
|
79
|
+
provider: ":cohere",
|
|
82
80
|
api_key: "ENV['COHERE_API_KEY']",
|
|
83
81
|
default_model: "'command'",
|
|
84
82
|
additional_config: " # config.cohere_base_url = 'https://api.cohere.ai/v1' # Optional: custom endpoint"
|
|
@@ -87,7 +85,7 @@ module RailsAiPromptable
|
|
|
87
85
|
|
|
88
86
|
def azure_config
|
|
89
87
|
{
|
|
90
|
-
provider:
|
|
88
|
+
provider: ":azure_openai",
|
|
91
89
|
api_key: "ENV['AZURE_OPENAI_API_KEY']",
|
|
92
90
|
default_model: "'gpt-4'",
|
|
93
91
|
additional_config: <<~CONFIG.chomp
|
|
@@ -100,7 +98,7 @@ module RailsAiPromptable
|
|
|
100
98
|
|
|
101
99
|
def mistral_config
|
|
102
100
|
{
|
|
103
|
-
provider:
|
|
101
|
+
provider: ":mistral",
|
|
104
102
|
api_key: "ENV['MISTRAL_API_KEY']",
|
|
105
103
|
default_model: "'mistral-small-latest'",
|
|
106
104
|
additional_config: " # config.mistral_base_url = 'https://api.mistral.ai/v1' # Optional: custom endpoint"
|
|
@@ -109,7 +107,7 @@ module RailsAiPromptable
|
|
|
109
107
|
|
|
110
108
|
def openrouter_config
|
|
111
109
|
{
|
|
112
|
-
provider:
|
|
110
|
+
provider: ":openrouter",
|
|
113
111
|
api_key: "ENV['OPENROUTER_API_KEY']",
|
|
114
112
|
default_model: "'openai/gpt-3.5-turbo'",
|
|
115
113
|
additional_config: <<~CONFIG.chomp
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
begin
|
|
4
|
-
require
|
|
4
|
+
require "active_job"
|
|
5
5
|
rescue LoadError
|
|
6
6
|
# ActiveJob not available, skip loading BackgroundJob
|
|
7
7
|
end
|
|
@@ -19,9 +19,7 @@ if defined?(ActiveJob)
|
|
|
19
19
|
result = record.ai_generate(context: context, **(kwargs || {}))
|
|
20
20
|
|
|
21
21
|
# Call the callback method if defined on the record
|
|
22
|
-
if record.respond_to?(:ai_generation_completed)
|
|
23
|
-
record.ai_generation_completed(result)
|
|
24
|
-
end
|
|
22
|
+
record.ai_generation_completed(result) if record.respond_to?(:ai_generation_completed)
|
|
25
23
|
|
|
26
24
|
# Store result in ai_generated_content attribute if it exists
|
|
27
25
|
if record.respond_to?(:ai_generated_content=)
|
|
@@ -20,60 +20,60 @@ module RailsAIPromptable
|
|
|
20
20
|
|
|
21
21
|
def initialize
|
|
22
22
|
@provider = :openai
|
|
23
|
-
@api_key = ENV
|
|
24
|
-
@default_model =
|
|
23
|
+
@api_key = ENV.fetch("OPENAI_API_KEY", nil)
|
|
24
|
+
@default_model = "gpt-4o-mini"
|
|
25
25
|
@timeout = 30
|
|
26
26
|
@logger = Logger.new($stdout)
|
|
27
27
|
|
|
28
28
|
# OpenAI settings
|
|
29
|
-
@openai_base_url =
|
|
29
|
+
@openai_base_url = "https://api.openai.com/v1"
|
|
30
30
|
|
|
31
31
|
# Anthropic settings
|
|
32
|
-
@anthropic_api_key = ENV
|
|
33
|
-
@anthropic_base_url =
|
|
32
|
+
@anthropic_api_key = ENV.fetch("ANTHROPIC_API_KEY", nil)
|
|
33
|
+
@anthropic_base_url = "https://api.anthropic.com/v1"
|
|
34
34
|
|
|
35
35
|
# Gemini settings
|
|
36
|
-
@gemini_api_key = ENV
|
|
37
|
-
@gemini_base_url =
|
|
36
|
+
@gemini_api_key = ENV.fetch("GEMINI_API_KEY", nil)
|
|
37
|
+
@gemini_base_url = "https://generativelanguage.googleapis.com/v1beta"
|
|
38
38
|
|
|
39
39
|
# Cohere settings
|
|
40
|
-
@cohere_api_key = ENV
|
|
41
|
-
@cohere_base_url =
|
|
40
|
+
@cohere_api_key = ENV.fetch("COHERE_API_KEY", nil)
|
|
41
|
+
@cohere_base_url = "https://api.cohere.ai/v1"
|
|
42
42
|
|
|
43
43
|
# Azure OpenAI settings
|
|
44
|
-
@azure_api_key = ENV
|
|
45
|
-
@azure_base_url = ENV
|
|
46
|
-
@azure_api_version =
|
|
47
|
-
@azure_deployment_name = ENV
|
|
44
|
+
@azure_api_key = ENV.fetch("AZURE_OPENAI_API_KEY", nil)
|
|
45
|
+
@azure_base_url = ENV.fetch("AZURE_OPENAI_BASE_URL", nil)
|
|
46
|
+
@azure_api_version = "2024-02-15-preview"
|
|
47
|
+
@azure_deployment_name = ENV.fetch("AZURE_OPENAI_DEPLOYMENT_NAME", nil)
|
|
48
48
|
|
|
49
49
|
# Mistral AI settings
|
|
50
|
-
@mistral_api_key = ENV
|
|
51
|
-
@mistral_base_url =
|
|
50
|
+
@mistral_api_key = ENV.fetch("MISTRAL_API_KEY", nil)
|
|
51
|
+
@mistral_base_url = "https://api.mistral.ai/v1"
|
|
52
52
|
|
|
53
53
|
# OpenRouter settings
|
|
54
|
-
@openrouter_api_key = ENV
|
|
55
|
-
@openrouter_base_url =
|
|
56
|
-
@openrouter_app_name = ENV
|
|
57
|
-
@openrouter_site_url = ENV
|
|
54
|
+
@openrouter_api_key = ENV.fetch("OPENROUTER_API_KEY", nil)
|
|
55
|
+
@openrouter_base_url = "https://openrouter.ai/api/v1"
|
|
56
|
+
@openrouter_app_name = ENV.fetch("OPENROUTER_APP_NAME", nil)
|
|
57
|
+
@openrouter_site_url = ENV.fetch("OPENROUTER_SITE_URL", nil)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Helper method to get the appropriate default model for the current provider
|
|
61
61
|
def model_for_provider
|
|
62
62
|
case provider.to_sym
|
|
63
63
|
when :openai
|
|
64
|
-
|
|
64
|
+
"gpt-4o-mini"
|
|
65
65
|
when :anthropic
|
|
66
|
-
|
|
66
|
+
"claude-3-5-sonnet-20241022"
|
|
67
67
|
when :gemini
|
|
68
|
-
|
|
68
|
+
"gemini-pro"
|
|
69
69
|
when :cohere
|
|
70
|
-
|
|
70
|
+
"command"
|
|
71
71
|
when :azure_openai
|
|
72
|
-
azure_deployment_name ||
|
|
72
|
+
azure_deployment_name || "gpt-4"
|
|
73
73
|
when :mistral
|
|
74
|
-
|
|
74
|
+
"mistral-small-latest"
|
|
75
75
|
when :openrouter
|
|
76
|
-
|
|
76
|
+
"openai/gpt-3.5-turbo"
|
|
77
77
|
else
|
|
78
78
|
default_model
|
|
79
79
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "active_support/concern"
|
|
4
4
|
|
|
5
5
|
module RailsAIPromptable
|
|
6
6
|
module Promptable
|
|
@@ -13,6 +13,7 @@ module RailsAIPromptable
|
|
|
13
13
|
class_methods do
|
|
14
14
|
def prompt_template(template = nil)
|
|
15
15
|
return ai_prompt_template if template.nil?
|
|
16
|
+
|
|
16
17
|
self.ai_prompt_template = template
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -21,7 +22,8 @@ module RailsAIPromptable
|
|
|
21
22
|
template = RailsAIPromptable::TemplateRegistry.get(name)
|
|
22
23
|
|
|
23
24
|
if template.nil?
|
|
24
|
-
raise ArgumentError,
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"Template '#{name}' not found. Available templates: #{RailsAIPromptable::TemplateRegistry.list.join(", ")}"
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
self.ai_prompt_template = template
|
|
@@ -29,12 +31,12 @@ module RailsAIPromptable
|
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def ai_generate(context: {}, model: nil, temperature: nil, format: :text)
|
|
32
|
-
template = self.class.ai_prompt_template ||
|
|
34
|
+
template = self.class.ai_prompt_template || ""
|
|
33
35
|
prompt = render_template(template, context)
|
|
34
36
|
|
|
35
37
|
RailsAIPromptable.configuration.logger.info("[rails_ai_promptable] prompt: ")
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
RailsAIPromptable.client.generate(
|
|
38
40
|
prompt: prompt,
|
|
39
41
|
model: model || RailsAIPromptable.configuration.default_model,
|
|
40
42
|
temperature: temperature || 0.7,
|
|
@@ -42,11 +44,10 @@ module RailsAIPromptable
|
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
# basic parsing
|
|
45
|
-
response
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def ai_generate_later(context: {}, **kwargs)
|
|
49
|
-
RailsAIPromptable.configuration.logger.info(
|
|
50
|
+
RailsAIPromptable.configuration.logger.info("[rails_ai_promptable] enqueuing ai_generate_later")
|
|
50
51
|
# Use ActiveJob to enqueue. We'll provide a default job class in later steps.
|
|
51
52
|
RailsAIPromptable::BackgroundJob.perform_later(self.class.name, id, context, kwargs)
|
|
52
53
|
end
|
|
@@ -56,9 +57,12 @@ module RailsAIPromptable
|
|
|
56
57
|
def render_template(template, context)
|
|
57
58
|
template % context.transform_keys(&:to_sym)
|
|
58
59
|
rescue KeyError
|
|
59
|
-
# fallback: simple interpolation using gsub
|
|
60
|
+
# fallback: simple interpolation using gsub for both %{key} and %<key>s formats
|
|
60
61
|
result = template.dup
|
|
61
|
-
context.each
|
|
62
|
+
context.each do |k, v|
|
|
63
|
+
result.gsub!("%{#{k}}", v.to_s)
|
|
64
|
+
result.gsub!("%<#{k}>s", v.to_s)
|
|
65
|
+
end
|
|
62
66
|
result
|
|
63
67
|
end
|
|
64
68
|
end
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module RailsAIPromptable
|
|
7
7
|
module Providers
|
|
8
8
|
class AnthropicProvider < BaseProvider
|
|
9
|
-
API_VERSION =
|
|
9
|
+
API_VERSION = "2023-06-01"
|
|
10
10
|
|
|
11
11
|
def initialize(configuration)
|
|
12
12
|
super
|
|
13
13
|
@api_key = configuration.anthropic_api_key || configuration.api_key
|
|
14
|
-
@base_url = configuration.anthropic_base_url ||
|
|
14
|
+
@base_url = configuration.anthropic_base_url || "https://api.anthropic.com/v1"
|
|
15
15
|
@timeout = configuration.timeout
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def generate(prompt:, model:, temperature:, format:)
|
|
19
19
|
uri = URI.parse("#{@base_url}/messages")
|
|
20
20
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
21
|
-
http.use_ssl = uri.scheme ==
|
|
21
|
+
http.use_ssl = uri.scheme == "https"
|
|
22
22
|
http.read_timeout = @timeout
|
|
23
23
|
|
|
24
24
|
request = Net::HTTP::Post.new(uri.request_uri, {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
"Content-Type" => "application/json",
|
|
26
|
+
"x-api-key" => @api_key,
|
|
27
|
+
"anthropic-version" => API_VERSION
|
|
28
|
+
})
|
|
29
29
|
|
|
30
30
|
body = {
|
|
31
31
|
model: model,
|
|
32
|
-
messages: [{ role:
|
|
32
|
+
messages: [{ role: "user", content: prompt }],
|
|
33
33
|
temperature: temperature,
|
|
34
34
|
max_tokens: 4096
|
|
35
35
|
}
|
|
@@ -40,13 +40,13 @@ module RailsAIPromptable
|
|
|
40
40
|
parsed = JSON.parse(response.body)
|
|
41
41
|
|
|
42
42
|
if response.code.to_i >= 400
|
|
43
|
-
error_message = parsed.dig(
|
|
43
|
+
error_message = parsed.dig("error", "message") || "Unknown error"
|
|
44
44
|
raise "Anthropic API error: #{error_message}"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Extract content from Anthropic response
|
|
48
|
-
parsed.dig(
|
|
49
|
-
rescue => e
|
|
48
|
+
parsed.dig("content", 0, "text")
|
|
49
|
+
rescue StandardError => e
|
|
50
50
|
RailsAIPromptable.configuration.logger.error("[rails_ai_promptable] anthropic error: #{e.message}")
|
|
51
51
|
nil
|
|
52
52
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module RailsAIPromptable
|
|
7
7
|
module Providers
|
|
@@ -10,7 +10,7 @@ module RailsAIPromptable
|
|
|
10
10
|
super
|
|
11
11
|
@api_key = configuration.azure_api_key || configuration.api_key
|
|
12
12
|
@base_url = configuration.azure_base_url
|
|
13
|
-
@api_version = configuration.azure_api_version ||
|
|
13
|
+
@api_version = configuration.azure_api_version || "2024-02-15-preview"
|
|
14
14
|
@timeout = configuration.timeout
|
|
15
15
|
@deployment_name = configuration.azure_deployment_name
|
|
16
16
|
|
|
@@ -22,16 +22,16 @@ module RailsAIPromptable
|
|
|
22
22
|
deployment = @deployment_name || model
|
|
23
23
|
uri = URI.parse("#{@base_url}/openai/deployments/#{deployment}/chat/completions?api-version=#{@api_version}")
|
|
24
24
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
25
|
-
http.use_ssl = uri.scheme ==
|
|
25
|
+
http.use_ssl = uri.scheme == "https"
|
|
26
26
|
http.read_timeout = @timeout
|
|
27
27
|
|
|
28
28
|
request = Net::HTTP::Post.new(uri.request_uri, {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
"Content-Type" => "application/json",
|
|
30
|
+
"api-key" => @api_key
|
|
31
|
+
})
|
|
32
32
|
|
|
33
33
|
body = {
|
|
34
|
-
messages: [{ role:
|
|
34
|
+
messages: [{ role: "user", content: prompt }],
|
|
35
35
|
temperature: temperature,
|
|
36
36
|
max_tokens: 2048
|
|
37
37
|
}
|
|
@@ -42,13 +42,13 @@ module RailsAIPromptable
|
|
|
42
42
|
parsed = JSON.parse(response.body)
|
|
43
43
|
|
|
44
44
|
if response.code.to_i >= 400
|
|
45
|
-
error_message = parsed.dig(
|
|
45
|
+
error_message = parsed.dig("error", "message") || "Unknown error"
|
|
46
46
|
raise "Azure OpenAI API error: #{error_message}"
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# Extract content (same structure as OpenAI)
|
|
50
|
-
parsed.dig(
|
|
51
|
-
rescue => e
|
|
50
|
+
parsed.dig("choices", 0, "message", "content")
|
|
51
|
+
rescue StandardError => e
|
|
52
52
|
RailsAIPromptable.configuration.logger.error("[rails_ai_promptable] azure_openai error: #{e.message}")
|
|
53
53
|
nil
|
|
54
54
|
end
|
|
@@ -56,9 +56,9 @@ module RailsAIPromptable
|
|
|
56
56
|
private
|
|
57
57
|
|
|
58
58
|
def validate_azure_configuration!
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
return if @base_url
|
|
60
|
+
|
|
61
|
+
raise ArgumentError, "Azure OpenAI requires azure_base_url to be set (e.g., https://your-resource.openai.azure.com)"
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module RailsAIPromptable
|
|
7
7
|
module Providers
|
|
@@ -9,20 +9,20 @@ module RailsAIPromptable
|
|
|
9
9
|
def initialize(configuration)
|
|
10
10
|
super
|
|
11
11
|
@api_key = configuration.cohere_api_key || configuration.api_key
|
|
12
|
-
@base_url = configuration.cohere_base_url ||
|
|
12
|
+
@base_url = configuration.cohere_base_url || "https://api.cohere.ai/v1"
|
|
13
13
|
@timeout = configuration.timeout
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def generate(prompt:, model:, temperature:, format:)
|
|
17
17
|
uri = URI.parse("#{@base_url}/generate")
|
|
18
18
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
19
|
-
http.use_ssl = uri.scheme ==
|
|
19
|
+
http.use_ssl = uri.scheme == "https"
|
|
20
20
|
http.read_timeout = @timeout
|
|
21
21
|
|
|
22
22
|
request = Net::HTTP::Post.new(uri.request_uri, {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
"Content-Type" => "application/json",
|
|
24
|
+
"Authorization" => "Bearer #{@api_key}"
|
|
25
|
+
})
|
|
26
26
|
|
|
27
27
|
body = {
|
|
28
28
|
model: model,
|
|
@@ -37,13 +37,13 @@ module RailsAIPromptable
|
|
|
37
37
|
parsed = JSON.parse(response.body)
|
|
38
38
|
|
|
39
39
|
if response.code.to_i >= 400
|
|
40
|
-
error_message = parsed[
|
|
40
|
+
error_message = parsed["message"] || "Unknown error"
|
|
41
41
|
raise "Cohere API error: #{error_message}"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Extract content from Cohere response
|
|
45
|
-
parsed.dig(
|
|
46
|
-
rescue => e
|
|
45
|
+
parsed.dig("generations", 0, "text")
|
|
46
|
+
rescue StandardError => e
|
|
47
47
|
RailsAIPromptable.configuration.logger.error("[rails_ai_promptable] cohere error: #{e.message}")
|
|
48
48
|
nil
|
|
49
49
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module RailsAIPromptable
|
|
7
7
|
module Providers
|
|
@@ -9,7 +9,7 @@ module RailsAIPromptable
|
|
|
9
9
|
def initialize(configuration)
|
|
10
10
|
super
|
|
11
11
|
@api_key = configuration.gemini_api_key || configuration.api_key
|
|
12
|
-
@base_url = configuration.gemini_base_url ||
|
|
12
|
+
@base_url = configuration.gemini_base_url || "https://generativelanguage.googleapis.com/v1beta"
|
|
13
13
|
@timeout = configuration.timeout
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -17,12 +17,12 @@ module RailsAIPromptable
|
|
|
17
17
|
# Gemini uses the API key as a query parameter
|
|
18
18
|
uri = URI.parse("#{@base_url}/models/#{model}:generateContent?key=#{@api_key}")
|
|
19
19
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
20
|
-
http.use_ssl = uri.scheme ==
|
|
20
|
+
http.use_ssl = uri.scheme == "https"
|
|
21
21
|
http.read_timeout = @timeout
|
|
22
22
|
|
|
23
23
|
request = Net::HTTP::Post.new(uri.request_uri, {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
"Content-Type" => "application/json"
|
|
25
|
+
})
|
|
26
26
|
|
|
27
27
|
body = {
|
|
28
28
|
contents: [{
|
|
@@ -40,13 +40,13 @@ module RailsAIPromptable
|
|
|
40
40
|
parsed = JSON.parse(response.body)
|
|
41
41
|
|
|
42
42
|
if response.code.to_i >= 400
|
|
43
|
-
error_message = parsed.dig(
|
|
43
|
+
error_message = parsed.dig("error", "message") || "Unknown error"
|
|
44
44
|
raise "Gemini API error: #{error_message}"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Extract content from Gemini response
|
|
48
|
-
parsed.dig(
|
|
49
|
-
rescue => e
|
|
48
|
+
parsed.dig("candidates", 0, "content", "parts", 0, "text")
|
|
49
|
+
rescue StandardError => e
|
|
50
50
|
RailsAIPromptable.configuration.logger.error("[rails_ai_promptable] gemini error: #{e.message}")
|
|
51
51
|
nil
|
|
52
52
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module RailsAIPromptable
|
|
7
7
|
module Providers
|
|
@@ -9,24 +9,24 @@ module RailsAIPromptable
|
|
|
9
9
|
def initialize(configuration)
|
|
10
10
|
super
|
|
11
11
|
@api_key = configuration.mistral_api_key || configuration.api_key
|
|
12
|
-
@base_url = configuration.mistral_base_url ||
|
|
12
|
+
@base_url = configuration.mistral_base_url || "https://api.mistral.ai/v1"
|
|
13
13
|
@timeout = configuration.timeout
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def generate(prompt:, model:, temperature:, format:)
|
|
17
17
|
uri = URI.parse("#{@base_url}/chat/completions")
|
|
18
18
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
19
|
-
http.use_ssl = uri.scheme ==
|
|
19
|
+
http.use_ssl = uri.scheme == "https"
|
|
20
20
|
http.read_timeout = @timeout
|
|
21
21
|
|
|
22
22
|
request = Net::HTTP::Post.new(uri.request_uri, {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
"Content-Type" => "application/json",
|
|
24
|
+
"Authorization" => "Bearer #{@api_key}"
|
|
25
|
+
})
|
|
26
26
|
|
|
27
27
|
body = {
|
|
28
28
|
model: model,
|
|
29
|
-
messages: [{ role:
|
|
29
|
+
messages: [{ role: "user", content: prompt }],
|
|
30
30
|
temperature: temperature,
|
|
31
31
|
max_tokens: 2048
|
|
32
32
|
}
|
|
@@ -37,13 +37,13 @@ module RailsAIPromptable
|
|
|
37
37
|
parsed = JSON.parse(response.body)
|
|
38
38
|
|
|
39
39
|
if response.code.to_i >= 400
|
|
40
|
-
error_message = parsed
|
|
40
|
+
error_message = parsed["message"] || "Unknown error"
|
|
41
41
|
raise "Mistral API error: #{error_message}"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Extract content (OpenAI-compatible format)
|
|
45
|
-
parsed.dig(
|
|
46
|
-
rescue => e
|
|
45
|
+
parsed.dig("choices", 0, "message", "content")
|
|
46
|
+
rescue StandardError => e
|
|
47
47
|
RailsAIPromptable.configuration.logger.error("[rails_ai_promptable] mistral error: #{e.message}")
|
|
48
48
|
nil
|
|
49
49
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module RailsAIPromptable
|
|
7
7
|
module Providers
|
|
@@ -16,15 +16,15 @@ module RailsAIPromptable
|
|
|
16
16
|
def generate(prompt:, model:, temperature:, format:)
|
|
17
17
|
uri = URI.parse("#{@base_url}/chat/completions")
|
|
18
18
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
19
|
-
http.use_ssl = uri.scheme ==
|
|
20
|
-
request = Net::HTTP::Post.new(uri.request_uri,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
http.use_ssl = uri.scheme == "https"
|
|
20
|
+
request = Net::HTTP::Post.new(uri.request_uri, {
|
|
21
|
+
"Content-Type" => "application/json",
|
|
22
|
+
"Authorization" => "Bearer #{@api_key}"
|
|
23
|
+
})
|
|
24
24
|
|
|
25
25
|
body = {
|
|
26
26
|
model: model,
|
|
27
|
-
messages: [{ role:
|
|
27
|
+
messages: [{ role: "user", content: prompt }],
|
|
28
28
|
temperature: temperature
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -34,8 +34,8 @@ module RailsAIPromptable
|
|
|
34
34
|
parsed = JSON.parse(response.body)
|
|
35
35
|
|
|
36
36
|
# naive extraction
|
|
37
|
-
parsed.dig(
|
|
38
|
-
rescue => e
|
|
37
|
+
parsed.dig("choices", 0, "message", "content")
|
|
38
|
+
rescue StandardError => e
|
|
39
39
|
RailsAIPromptable.configuration.logger.error("[rails_ai_promptable] openai error: #{e.message}")
|
|
40
40
|
nil
|
|
41
41
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
5
|
|
|
6
6
|
module RailsAIPromptable
|
|
7
7
|
module Providers
|
|
@@ -9,7 +9,7 @@ module RailsAIPromptable
|
|
|
9
9
|
def initialize(configuration)
|
|
10
10
|
super
|
|
11
11
|
@api_key = configuration.openrouter_api_key || configuration.api_key
|
|
12
|
-
@base_url = configuration.openrouter_base_url ||
|
|
12
|
+
@base_url = configuration.openrouter_base_url || "https://openrouter.ai/api/v1"
|
|
13
13
|
@timeout = configuration.timeout
|
|
14
14
|
@app_name = configuration.openrouter_app_name
|
|
15
15
|
@site_url = configuration.openrouter_site_url
|
|
@@ -18,23 +18,23 @@ module RailsAIPromptable
|
|
|
18
18
|
def generate(prompt:, model:, temperature:, format:)
|
|
19
19
|
uri = URI.parse("#{@base_url}/chat/completions")
|
|
20
20
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
21
|
-
http.use_ssl = uri.scheme ==
|
|
21
|
+
http.use_ssl = uri.scheme == "https"
|
|
22
22
|
http.read_timeout = @timeout
|
|
23
23
|
|
|
24
24
|
headers = {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
"Content-Type" => "application/json",
|
|
26
|
+
"Authorization" => "Bearer #{@api_key}"
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
# OpenRouter requires these headers for tracking/attribution
|
|
30
|
-
headers[
|
|
31
|
-
headers[
|
|
30
|
+
headers["HTTP-Referer"] = @site_url if @site_url
|
|
31
|
+
headers["X-Title"] = @app_name if @app_name
|
|
32
32
|
|
|
33
33
|
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
34
34
|
|
|
35
35
|
body = {
|
|
36
36
|
model: model,
|
|
37
|
-
messages: [{ role:
|
|
37
|
+
messages: [{ role: "user", content: prompt }],
|
|
38
38
|
temperature: temperature,
|
|
39
39
|
max_tokens: 2048
|
|
40
40
|
}
|
|
@@ -45,13 +45,13 @@ module RailsAIPromptable
|
|
|
45
45
|
parsed = JSON.parse(response.body)
|
|
46
46
|
|
|
47
47
|
if response.code.to_i >= 400
|
|
48
|
-
error_message = parsed.dig(
|
|
48
|
+
error_message = parsed.dig("error", "message") || "Unknown error"
|
|
49
49
|
raise "OpenRouter API error: #{error_message}"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Extract content (OpenAI-compatible format)
|
|
53
|
-
parsed.dig(
|
|
54
|
-
rescue => e
|
|
53
|
+
parsed.dig("choices", 0, "message", "content")
|
|
54
|
+
rescue StandardError => e
|
|
55
55
|
RailsAIPromptable.configuration.logger.error("[rails_ai_promptable] openrouter error: #{e.message}")
|
|
56
56
|
nil
|
|
57
57
|
end
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsAIPromptable
|
|
4
4
|
module Providers
|
|
5
|
-
autoload :BaseProvider,
|
|
6
|
-
autoload :OpenAIProvider,
|
|
7
|
-
autoload :AnthropicProvider,
|
|
8
|
-
autoload :GeminiProvider,
|
|
9
|
-
autoload :CohereProvider,
|
|
10
|
-
autoload :AzureOpenAIProvider,
|
|
11
|
-
autoload :MistralProvider,
|
|
12
|
-
autoload :OpenRouterProvider,
|
|
5
|
+
autoload :BaseProvider, "rails_ai_promptable/providers/base_provider"
|
|
6
|
+
autoload :OpenAIProvider, "rails_ai_promptable/providers/openai_provider"
|
|
7
|
+
autoload :AnthropicProvider, "rails_ai_promptable/providers/anthropic_provider"
|
|
8
|
+
autoload :GeminiProvider, "rails_ai_promptable/providers/gemini_provider"
|
|
9
|
+
autoload :CohereProvider, "rails_ai_promptable/providers/cohere_provider"
|
|
10
|
+
autoload :AzureOpenAIProvider, "rails_ai_promptable/providers/azure_openai_provider"
|
|
11
|
+
autoload :MistralProvider, "rails_ai_promptable/providers/mistral_provider"
|
|
12
|
+
autoload :OpenRouterProvider, "rails_ai_promptable/providers/openrouter_provider"
|
|
13
13
|
|
|
14
14
|
def self.for(provider_sym, configuration)
|
|
15
15
|
case provider_sym.to_sym
|
|
@@ -28,13 +28,14 @@ module RailsAIPromptable
|
|
|
28
28
|
when :openrouter
|
|
29
29
|
OpenRouterProvider.new(configuration)
|
|
30
30
|
else
|
|
31
|
-
raise ArgumentError,
|
|
31
|
+
raise ArgumentError,
|
|
32
|
+
"Unknown provider: #{provider_sym}. Supported providers: :openai, :anthropic, :gemini, :cohere, :azure_openai, :mistral, :openrouter"
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
# Helper method to list all available providers
|
|
36
37
|
def self.available_providers
|
|
37
|
-
[
|
|
38
|
+
%i[openai anthropic gemini cohere azure_openai mistral openrouter]
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "yaml"
|
|
4
4
|
|
|
5
5
|
module RailsAIPromptable
|
|
6
6
|
class TemplateRegistry
|
|
@@ -43,14 +43,14 @@ module RailsAIPromptable
|
|
|
43
43
|
def load_from_directory(directory_path)
|
|
44
44
|
return unless Dir.exist?(directory_path)
|
|
45
45
|
|
|
46
|
-
Dir.glob(File.join(directory_path,
|
|
46
|
+
Dir.glob(File.join(directory_path, "*")).each do |file_path|
|
|
47
47
|
next unless File.file?(file_path)
|
|
48
48
|
|
|
49
|
-
name = File.basename(file_path,
|
|
49
|
+
name = File.basename(file_path, ".*")
|
|
50
50
|
|
|
51
|
-
if file_path.end_with?(
|
|
51
|
+
if file_path.end_with?(".yml", ".yaml")
|
|
52
52
|
content = YAML.load_file(file_path)
|
|
53
|
-
template = content.is_a?(Hash) ? content[
|
|
53
|
+
template = content.is_a?(Hash) ? content["template"] : content.to_s
|
|
54
54
|
else
|
|
55
55
|
template = File.read(file_path)
|
|
56
56
|
end
|
data/lib/rails_ai_promptable.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
3
|
+
require "rails_ai_promptable/version"
|
|
4
|
+
require "rails_ai_promptable/configuration"
|
|
5
|
+
require "rails_ai_promptable/template_registry"
|
|
6
|
+
require "rails_ai_promptable/promptable"
|
|
7
|
+
require "rails_ai_promptable/providers"
|
|
8
|
+
require "rails_ai_promptable/logger"
|
|
9
|
+
require "rails_ai_promptable/background_job"
|
|
10
10
|
|
|
11
11
|
module RailsAIPromptable
|
|
12
12
|
class << self
|
|
@@ -25,4 +25,4 @@ module RailsAIPromptable
|
|
|
25
25
|
@client = nil
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
|
-
end
|
|
28
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_ai_promptable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shoaib Malik
|
|
@@ -11,33 +11,33 @@ cert_chain: []
|
|
|
11
11
|
date: 2025-11-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: httparty
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '0.20'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '0.20'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: rails
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0
|
|
33
|
+
version: '7.0'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0
|
|
40
|
+
version: '7.0'
|
|
41
41
|
description: rails_ai_promptable makes it easy to integrate AI-driven features into
|
|
42
42
|
your Rails application. It allows you to define promptable methods, chain context,
|
|
43
43
|
and connect with AI APIs like OpenAI, Anthropic, or local LLMs with minimal setup.
|