contai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/README.md +159 -0
- data/Rakefile +12 -0
- data/app/controllers/contai/generations_controller.rb +25 -0
- data/app/helpers/contai_helper.rb +39 -0
- data/app/jobs/contai_generation_job.rb +7 -0
- data/config/routes.rb +3 -0
- data/contai.gemspec +38 -0
- data/docs/usage.md +70 -0
- data/lib/contai/configuration.rb +11 -0
- data/lib/contai/engine.rb +9 -0
- data/lib/contai/generatabale.rb +102 -0
- data/lib/contai/providers/base.rb +37 -0
- data/lib/contai/providers/claude.rb +38 -0
- data/lib/contai/providers/http.rb +57 -0
- data/lib/contai/providers/n8n.rb +17 -0
- data/lib/contai/providers/openai.rb +37 -0
- data/lib/contai/providers.rb +24 -0
- data/lib/contai/version.rb +5 -0
- data/lib/contai.rb +21 -0
- data/lib/generators/contai/install_generator.rb +68 -0
- data/lib/generators/contai/model_generator.rb +55 -0
- data/lib/generators/contai/templates/contai.css +53 -0
- data/lib/generators/contai/templates/contai.js +82 -0
- data/lib/generators/contai/templates/contai_controller.js +109 -0
- data/lib/generators/contai/templates/contai_generation_job.rb +7 -0
- data/lib/generators/contai/templates/initializer.rb +34 -0
- data/sig/contai.rbs +4 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1ea8b64ee261ac1e205aec107d9011fcb2a90194856143eb25c73a79af70f982
|
4
|
+
data.tar.gz: b91bd9e6138f0b4980ed0f2651f15083de0ae275524d3a749308f6af900c04b3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 596f44c2e09f5ada2af464d9f7f656569bc396a8b3343cf7415e53d89fa1a7c81457907995c263689468a8238df2dce43f871cd13ff7f26bef289ebd84c0ee30
|
7
|
+
data.tar.gz: da7f09f708f026f5bb5766d18a1d7e3125bd327fb0b2494a39e8575d6ffb9be51556896967dbec38ba0ce2d3e827efa419c2ac5893f237e6dcef8e11c522f63e
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# Contai
|
2
|
+
|
3
|
+
AI content generation for Rails models. Integrate external AI APIs seamlessly into your Rails applications.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'contai'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
### Basic Setup
|
20
|
+
|
21
|
+
Include `Contai::Generatable` in your model and configure it:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Article < ApplicationRecord
|
25
|
+
include Contai::Generatable
|
26
|
+
|
27
|
+
contai do
|
28
|
+
prompt_from :title, :description
|
29
|
+
output_to :body
|
30
|
+
provider :openai, api_key: ENV["OPENAI_API_KEY"]
|
31
|
+
template "Write a blog post about: {{title}} - {{description}}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
### Generate Content
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
article = Article.new(title: "Ruby on Rails", description: "Web framework")
|
40
|
+
article.generate_ai_content!
|
41
|
+
# Content will be generated and saved to article.body
|
42
|
+
|
43
|
+
# Async generation
|
44
|
+
article.generate_ai_content!(async: true)
|
45
|
+
```
|
46
|
+
### Supported Providers
|
47
|
+
|
48
|
+
#### OpenAI
|
49
|
+
```ruby
|
50
|
+
provider :openai,
|
51
|
+
api_key: ENV["OPENAI_API_KEY"],
|
52
|
+
model: "gpt-4",
|
53
|
+
max_tokens: 1000
|
54
|
+
```
|
55
|
+
|
56
|
+
#### Claude
|
57
|
+
```ruby
|
58
|
+
provider :claude,
|
59
|
+
api_key: ENV["CLAUDE_API_KEY"],
|
60
|
+
model: "claude-3-sonnet-20240229"
|
61
|
+
```
|
62
|
+
|
63
|
+
#### N8N Webhook
|
64
|
+
```ruby
|
65
|
+
provider :n8n,
|
66
|
+
webhook_url: "https://your-n8n-instance.com/webhook/your-webhook-id",
|
67
|
+
response_path: "output"
|
68
|
+
```
|
69
|
+
|
70
|
+
#### Custom HTTP
|
71
|
+
```ruby
|
72
|
+
provider :http,
|
73
|
+
url: "https://api.example.com/generate",
|
74
|
+
method: :post,
|
75
|
+
headers: { "Authorization" => "Bearer #{ENV['API_KEY']}" },
|
76
|
+
body_template: { prompt: "{{prompt}}", model_name: "custom-model" },
|
77
|
+
response_path: "result.text"
|
78
|
+
```
|
79
|
+
|
80
|
+
### Configuration
|
81
|
+
|
82
|
+
Configure global defaults:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# config/initializers/contai.rb
|
86
|
+
Contai.configure do |config|
|
87
|
+
config.default_provider = :openai
|
88
|
+
config.default_template = "Generate content based on: {{prompt}}"
|
89
|
+
config.timeout = 30
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
### Routes
|
94
|
+
|
95
|
+
Add to your routes file:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# config/routes.rb
|
99
|
+
Rails.application.routes.draw do
|
100
|
+
post '/contai/generate', to: 'contai/generations#create', as: 'contai_generation'
|
101
|
+
end
|
102
|
+
```
|
103
|
+
## Error Handling
|
104
|
+
|
105
|
+
The gem provides comprehensive error handling:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
article = Article.new(title: "Test")
|
109
|
+
result = article.generate_ai_content!
|
110
|
+
|
111
|
+
unless result
|
112
|
+
puts article.errors.full_messages
|
113
|
+
# => ["AI generation failed: API error"]
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
## Testing
|
118
|
+
|
119
|
+
Test your models with Contai:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# spec/models/article_spec.rb
|
123
|
+
RSpec.describe Article do
|
124
|
+
describe '#generate_ai_content!' do
|
125
|
+
let(:article) { create(:article, title: "Test", description: "Test desc") }
|
126
|
+
|
127
|
+
before do
|
128
|
+
allow_any_instance_of(Contai::Providers::OpenAI).to receive(:generate)
|
129
|
+
.and_return(double(success?: true, content: "Generated content"))
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'generates content' do
|
133
|
+
expect { article.generate_ai_content! }.to change { article.body }
|
134
|
+
.from(nil).to("Generated content")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
## Development
|
141
|
+
|
142
|
+
After checking out the repo, run:
|
143
|
+
|
144
|
+
```bash
|
145
|
+
bundle install
|
146
|
+
rake spec
|
147
|
+
```
|
148
|
+
|
149
|
+
## Contributing
|
150
|
+
|
151
|
+
1. Fork it
|
152
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
153
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
154
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
155
|
+
5. Create new Pull Request
|
156
|
+
|
157
|
+
## License
|
158
|
+
|
159
|
+
The gem is available as open source under the terms of the [CAPAA License](https://capaal.com).
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Contai
|
2
|
+
class GenerationsController < ApplicationController
|
3
|
+
def create
|
4
|
+
model_class = params[:model].constantize
|
5
|
+
record = model_class.find(params[:id])
|
6
|
+
|
7
|
+
if record.generate_ai_content!
|
8
|
+
render json: {
|
9
|
+
success: true,
|
10
|
+
content: record.send(record.class.contai_config[:output_field])
|
11
|
+
}
|
12
|
+
else
|
13
|
+
render json: {
|
14
|
+
success: false,
|
15
|
+
errors: record.errors.full_messages
|
16
|
+
}, status: 422
|
17
|
+
end
|
18
|
+
rescue => e
|
19
|
+
render json: {
|
20
|
+
success: false,
|
21
|
+
error: e.message
|
22
|
+
}, status: 500
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ContaiHelper
|
2
|
+
def contai_button(record, text: "Contai!", css_class: "btn btn-secondary contai-btn")
|
3
|
+
return unless record.class.respond_to?(:contai_config) && record.class.contai_config.any?
|
4
|
+
|
5
|
+
button_to text, contai_generation_path,
|
6
|
+
params: { model: record.class.name, id: record.id },
|
7
|
+
method: :post,
|
8
|
+
remote: true,
|
9
|
+
class: css_class,
|
10
|
+
data: {
|
11
|
+
contai_target: record.class.contai_config[:output_field],
|
12
|
+
contai_record_id: record.id,
|
13
|
+
contai_model: record.class.name
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def contai_field_with_button(form, field, options = {})
|
18
|
+
record = form.object
|
19
|
+
return form.text_area(field, options) unless record.class.respond_to?(:contai_config)
|
20
|
+
|
21
|
+
config = record.class.contai_config
|
22
|
+
return form.text_area(field, options) unless config[:output_field] == field
|
23
|
+
|
24
|
+
content_tag :div, class: "contai-field-wrapper" do
|
25
|
+
form.text_area(field, options.merge(
|
26
|
+
data: { contai_target: "output" }
|
27
|
+
)) +
|
28
|
+
content_tag(:button, "Contai!",
|
29
|
+
type: "button",
|
30
|
+
class: "btn btn-secondary contai-generate-btn",
|
31
|
+
data: {
|
32
|
+
controller: "contai",
|
33
|
+
contai_model_value: record.class.name,
|
34
|
+
contai_record_id_value: record.id,
|
35
|
+
contai_output_target: field
|
36
|
+
})
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/config/routes.rb
ADDED
data/contai.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/contai/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "contai"
|
7
|
+
spec.version = Contai::VERSION
|
8
|
+
spec.authors = ["Panasenkov A."]
|
9
|
+
spec.email = ["apanasenkov@capaa.ru"]
|
10
|
+
|
11
|
+
spec.summary = "Gem for generating AI content for rails models"
|
12
|
+
spec.description = "A Ruby on Rails gem that provides seamless integration with AI services to automatically generate content for your Rails models. It offers a simple interface to enhance your models with AI-generated content, supporting various content types and customization options."
|
13
|
+
spec.homepage = "https://github.com/capaas/contai"
|
14
|
+
spec.license = "Unlicense"
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/capaas/contai"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/capaas/contai/blob/main/CHANGELOG.md"
|
22
|
+
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_dependency "rails", "~> 6.0"
|
34
|
+
spec.add_dependency "httparty", "~> 0.20"
|
35
|
+
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
37
|
+
spec.add_development_dependency "sqlite3", "~> 1.4"
|
38
|
+
end
|
data/docs/usage.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# Contai Gem - Полное руководство по использованию
|
2
|
+
|
3
|
+
## Быстрый старт
|
4
|
+
|
5
|
+
### 1. Установка
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# Gemfile
|
9
|
+
gem 'contai'
|
10
|
+
```
|
11
|
+
|
12
|
+
```bash
|
13
|
+
bundle install
|
14
|
+
rails generate contai:install
|
15
|
+
```
|
16
|
+
|
17
|
+
### 2. Настройка модели
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# app/models/article.rb
|
21
|
+
class Article < ApplicationRecord
|
22
|
+
include Contai::Generatable
|
23
|
+
|
24
|
+
contai do
|
25
|
+
prompt_from :title, :description
|
26
|
+
output_to :body
|
27
|
+
provider :openai, api_key: ENV['OPENAI_API_KEY']
|
28
|
+
template "Напиши подробную статью на тему: {{title}}. Описание: {{description}}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
### 3. Использование в контроллере
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
# app/controllers/articles_controller.rb
|
37
|
+
class ArticlesController < ApplicationController
|
38
|
+
def create
|
39
|
+
@article = Article.new(article_params)
|
40
|
+
|
41
|
+
if @article.save
|
42
|
+
# Синхронная генерация
|
43
|
+
@article.generate_ai_content!
|
44
|
+
|
45
|
+
# Или асинхронная
|
46
|
+
# @article.generate_ai_content!(async: true)
|
47
|
+
|
48
|
+
redirect_to @article
|
49
|
+
else
|
50
|
+
render :new
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_content
|
55
|
+
@article = Article.find(params[:id])
|
56
|
+
|
57
|
+
if @article.generate_ai_content!
|
58
|
+
redirect_to @article, notice: 'Контент успешно сгенерирован!'
|
59
|
+
else
|
60
|
+
redirect_to @article, alert: 'Ошибка генерации контента'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def article_params
|
67
|
+
params.require(:article).permit(:title, :description)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Contai
|
2
|
+
module Generatable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :contai_config, default: {}
|
7
|
+
end
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def contai(&block)
|
11
|
+
config = ContaiConfig.new
|
12
|
+
config.instance_eval(&block) if block_given?
|
13
|
+
self.contai_config = config.to_h
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_ai_content!(async: false)
|
18
|
+
if async
|
19
|
+
ContaiGenerationJob.perform_later(self)
|
20
|
+
else
|
21
|
+
perform_generation
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def perform_generation
|
28
|
+
config = self.class.contai_config
|
29
|
+
return false if config.empty?
|
30
|
+
|
31
|
+
prompt = build_prompt(config)
|
32
|
+
provider = Providers.build(config[:provider], config[:provider_options])
|
33
|
+
|
34
|
+
result = provider.generate(prompt)
|
35
|
+
|
36
|
+
if result.success?
|
37
|
+
update!(config[:output_field] => result.content)
|
38
|
+
true
|
39
|
+
else
|
40
|
+
errors.add(:base, "AI generation failed: #{result.error}")
|
41
|
+
false
|
42
|
+
end
|
43
|
+
rescue => e
|
44
|
+
errors.add(:base, "AI generation error: #{e.message}")
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_prompt(config)
|
49
|
+
template = config[:template] || Contai.configuration.default_template
|
50
|
+
data = {}
|
51
|
+
|
52
|
+
config[:prompt_fields].each do |field|
|
53
|
+
data[field] = send(field)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Simple template substitution
|
57
|
+
template.gsub(/\{\{(\w+)\}\}/) do |match|
|
58
|
+
field = $1.to_sym
|
59
|
+
data[field] || match
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class ContaiConfig
|
65
|
+
attr_accessor :prompt_fields, :output_field, :provider_type, :provider_options, :template
|
66
|
+
|
67
|
+
def initialize
|
68
|
+
@prompt_fields = [:prompt]
|
69
|
+
@output_field = :body
|
70
|
+
@provider_type = :http
|
71
|
+
@provider_options = {}
|
72
|
+
@template = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def prompt_from(*fields)
|
76
|
+
@prompt_fields = fields
|
77
|
+
end
|
78
|
+
|
79
|
+
def output_to(field)
|
80
|
+
@output_field = field
|
81
|
+
end
|
82
|
+
|
83
|
+
def provider(type, options = {})
|
84
|
+
@provider_type = type
|
85
|
+
@provider_options = options
|
86
|
+
end
|
87
|
+
|
88
|
+
def template(tmpl)
|
89
|
+
@template = tmpl
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_h
|
93
|
+
{
|
94
|
+
prompt_fields: @prompt_fields,
|
95
|
+
output_field: @output_field,
|
96
|
+
provider: @provider_type,
|
97
|
+
provider_options: @provider_options,
|
98
|
+
template: @template
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Contai
|
2
|
+
module Providers
|
3
|
+
class Base
|
4
|
+
def initialize(options = {})
|
5
|
+
@options = options
|
6
|
+
end
|
7
|
+
|
8
|
+
def generate(prompt)
|
9
|
+
raise NotImplementedError, "Subclasses must implement #generate"
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def success_result(content)
|
15
|
+
Result.new(success: true, content: content)
|
16
|
+
end
|
17
|
+
|
18
|
+
def error_result(error)
|
19
|
+
Result.new(success: false, error: error)
|
20
|
+
end
|
21
|
+
|
22
|
+
class Result
|
23
|
+
attr_reader :content, :error
|
24
|
+
|
25
|
+
def initialize(success:, content: nil, error: nil)
|
26
|
+
@success = success
|
27
|
+
@content = content
|
28
|
+
@error = error
|
29
|
+
end
|
30
|
+
|
31
|
+
def success?
|
32
|
+
@success
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "httparty"
|
2
|
+
|
3
|
+
module Contai
|
4
|
+
module Providers
|
5
|
+
class Claude < Base
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
base_uri "https://api.anthropic.com/v1"
|
9
|
+
|
10
|
+
def generate(prompt)
|
11
|
+
response = self.class.post("/messages",
|
12
|
+
headers: {
|
13
|
+
"x-api-key" => @options[:api_key],
|
14
|
+
"Content-Type" => "application/json",
|
15
|
+
"anthropic-version" => "2023-06-01"
|
16
|
+
},
|
17
|
+
body: {
|
18
|
+
model: @options[:model] || "claude-3-sonnet-20240229",
|
19
|
+
max_tokens: @options[:max_tokens] || 1000,
|
20
|
+
messages: [
|
21
|
+
{ role: "user", content: prompt }
|
22
|
+
]
|
23
|
+
}.to_json,
|
24
|
+
timeout: @options[:timeout] || Contai.configuration.timeout
|
25
|
+
)
|
26
|
+
|
27
|
+
if response.success?
|
28
|
+
content = response.parsed_response.dig("content", 0, "text")
|
29
|
+
success_result(content)
|
30
|
+
else
|
31
|
+
error_result("Claude API error: #{response.code} #{response.message}")
|
32
|
+
end
|
33
|
+
rescue => e
|
34
|
+
error_result(e.message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "httparty"
|
2
|
+
|
3
|
+
module Contai
|
4
|
+
module Providers
|
5
|
+
class HTTP < Base
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
def generate(prompt)
|
9
|
+
url = @options[:url] || raise(ArgumentError, "HTTP provider requires :url option")
|
10
|
+
method = (@options[:method] || :post).to_s.downcase
|
11
|
+
|
12
|
+
request_options = {
|
13
|
+
headers: @options[:headers] || { "Content-Type" => "application/json" },
|
14
|
+
timeout: @options[:timeout] || Contai.configuration.timeout
|
15
|
+
}
|
16
|
+
|
17
|
+
body_data = @options[:body_template] || { prompt: prompt }
|
18
|
+
if body_data.is_a?(Hash)
|
19
|
+
body_data = body_data.transform_values { |v| v.is_a?(String) ? v.gsub("{{prompt}}", prompt) : v }
|
20
|
+
request_options[:body] = body_data.to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
response = case method
|
24
|
+
when "get"
|
25
|
+
self.class.get(url, request_options)
|
26
|
+
when "post"
|
27
|
+
self.class.post(url, request_options)
|
28
|
+
when "put"
|
29
|
+
self.class.put(url, request_options)
|
30
|
+
else
|
31
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
32
|
+
end
|
33
|
+
|
34
|
+
if response.success?
|
35
|
+
content = extract_content(response, @options[:response_path])
|
36
|
+
success_result(content)
|
37
|
+
else
|
38
|
+
error_result("HTTP API error: #{response.code} #{response.message}")
|
39
|
+
end
|
40
|
+
rescue => e
|
41
|
+
error_result(e.message)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def extract_content(response, path)
|
47
|
+
return response.parsed_response.to_s unless path
|
48
|
+
|
49
|
+
content = response.parsed_response
|
50
|
+
path.split(".").each do |key|
|
51
|
+
content = content[key] if content.respond_to?(:[])
|
52
|
+
end
|
53
|
+
content.to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Contai
|
2
|
+
module Providers
|
3
|
+
class N8N < HTTP
|
4
|
+
def initialize(options = {})
|
5
|
+
webhook_url = options[:webhook_url] || raise(ArgumentError, "N8N provider requires :webhook_url")
|
6
|
+
|
7
|
+
super({
|
8
|
+
url: webhook_url,
|
9
|
+
method: :post,
|
10
|
+
headers: { "Content-Type" => "application/json" },
|
11
|
+
body_template: { prompt: "{{prompt}}" },
|
12
|
+
response_path: options[:response_path] || "output"
|
13
|
+
}.merge(options))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "httparty"
|
2
|
+
|
3
|
+
module Contai
|
4
|
+
module Providers
|
5
|
+
class OpenAI < Base
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
base_uri "https://api.openai.com/v1"
|
9
|
+
|
10
|
+
def generate(prompt)
|
11
|
+
response = self.class.post("/chat/completions",
|
12
|
+
headers: {
|
13
|
+
"Authorization" => "Bearer #{@options[:api_key]}",
|
14
|
+
"Content-Type" => "application/json"
|
15
|
+
},
|
16
|
+
body: {
|
17
|
+
model: @options[:model] || "gpt-3.5-turbo",
|
18
|
+
messages: [
|
19
|
+
{ role: "user", content: prompt }
|
20
|
+
],
|
21
|
+
max_tokens: @options[:max_tokens] || 1000
|
22
|
+
}.to_json,
|
23
|
+
timeout: @options[:timeout] || Contai.configuration.timeout
|
24
|
+
)
|
25
|
+
|
26
|
+
if response.success?
|
27
|
+
content = response.parsed_response.dig("choices", 0, "message", "content")
|
28
|
+
success_result(content)
|
29
|
+
else
|
30
|
+
error_result("OpenAI API error: #{response.code} #{response.message}")
|
31
|
+
end
|
32
|
+
rescue => e
|
33
|
+
error_result(e.message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "contai/providers/base"
|
2
|
+
require "contai/providers/openai"
|
3
|
+
require "contai/providers/claude"
|
4
|
+
require "contai/providers/http"
|
5
|
+
require "contai/providers/n8n"
|
6
|
+
|
7
|
+
module Contai
|
8
|
+
module Providers
|
9
|
+
def self.build(type, options = {})
|
10
|
+
case type.to_sym
|
11
|
+
when :openai
|
12
|
+
OpenAI.new(options)
|
13
|
+
when :claude
|
14
|
+
Claude.new(options)
|
15
|
+
when :n8n
|
16
|
+
N8N.new(options)
|
17
|
+
when :http
|
18
|
+
HTTP.new(options)
|
19
|
+
else
|
20
|
+
raise ArgumentError, "Unknown provider: #{type}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/contai.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "contai/version"
|
4
|
+
require "contai/configuration"
|
5
|
+
require "contai/generatable"
|
6
|
+
require "contai/providers"
|
7
|
+
require "contai/engine" if defined?(Rails)
|
8
|
+
|
9
|
+
module Contai
|
10
|
+
class << self
|
11
|
+
attr_accessor :configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configuration
|
15
|
+
@configuration ||= Configuration.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.configure
|
19
|
+
yield(configuration)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Contai
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
|
6
|
+
desc "Install Contai into your Rails application"
|
7
|
+
|
8
|
+
def create_initializer
|
9
|
+
template 'initializer.rb', 'config/initializers/contai.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_routes
|
13
|
+
route 'post "/contai/generate", to: "contai/generations#create", as: "contai_generation"'
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_stimulus_controller
|
17
|
+
if stimulus_detected?
|
18
|
+
template 'contai_controller.js', 'app/javascript/controllers/contai_controller.js'
|
19
|
+
|
20
|
+
say "Stimulus controller created. Make sure to register it in your application:", :green
|
21
|
+
say "// app/javascript/controllers/application.js"
|
22
|
+
say 'import ContaiController from "./contai_controller"'
|
23
|
+
say 'application.register("contai", ContaiController)'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def copy_assets
|
28
|
+
template 'contai.js', 'app/assets/javascripts/contai.js'
|
29
|
+
template 'contai.css', 'app/assets/stylesheets/contai.css'
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_job
|
33
|
+
template 'contai_generation_job.rb', 'app/jobs/contai_generation_job.rb'
|
34
|
+
end
|
35
|
+
|
36
|
+
def show_instructions
|
37
|
+
say "\n" + "="*60
|
38
|
+
say "Contai has been installed successfully!", :green
|
39
|
+
say "="*60
|
40
|
+
say "\nNext steps:"
|
41
|
+
say "1. Configure your AI providers in config/initializers/contai.rb"
|
42
|
+
say "2. Add 'include Contai::Generatable' to your models"
|
43
|
+
say "3. Configure your models with the contai DSL"
|
44
|
+
say "4. Use contai_button or contai_field_with_button helpers in your views"
|
45
|
+
say "\nExample model configuration:"
|
46
|
+
say <<~EXAMPLE
|
47
|
+
class Article < ApplicationRecord
|
48
|
+
include Contai::Generatable
|
49
|
+
|
50
|
+
contai do
|
51
|
+
prompt_from :title, :description
|
52
|
+
output_to :body
|
53
|
+
provider :openai, api_key: ENV['OPENAI_API_KEY']
|
54
|
+
template "Write about: {{title}} - {{description}}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
EXAMPLE
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def stimulus_detected?
|
63
|
+
File.exist?(Rails.root.join('app/javascript/controllers/application.js'))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Contai
|
2
|
+
module Generators
|
3
|
+
class ModelGenerator < Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
|
6
|
+
desc "Add Contai configuration to an existing model"
|
7
|
+
|
8
|
+
class_option :prompt_fields, type: :array, default: ['title'],
|
9
|
+
desc: "Fields to use for prompt generation"
|
10
|
+
class_option :output_field, type: :string, default: 'body',
|
11
|
+
desc: "Field to store generated content"
|
12
|
+
class_option :provider, type: :string, default: 'openai',
|
13
|
+
desc: "AI provider to use"
|
14
|
+
class_option :template, type: :string,
|
15
|
+
desc: "Custom template for prompt generation"
|
16
|
+
|
17
|
+
def add_contai_to_model
|
18
|
+
model_file = "app/models/#{file_name}.rb"
|
19
|
+
|
20
|
+
unless File.exist?(model_file)
|
21
|
+
say "Model #{class_name} not found at #{model_file}", :red
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
inject_into_class model_file, class_name do
|
26
|
+
contai_configuration
|
27
|
+
end
|
28
|
+
|
29
|
+
say "Added Contai configuration to #{class_name}", :green
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def contai_configuration
|
35
|
+
prompt_fields = options[:prompt_fields].map(&:to_sym)
|
36
|
+
output_field = options[:output_field].to_sym
|
37
|
+
provider = options[:provider].to_sym
|
38
|
+
template = options[:template]
|
39
|
+
|
40
|
+
config = []
|
41
|
+
config << " include Contai::Generatable"
|
42
|
+
config << ""
|
43
|
+
config << " contai do"
|
44
|
+
config << " prompt_from #{prompt_fields.map(&:inspect).join(', ')}"
|
45
|
+
config << " output_to :#{output_field}"
|
46
|
+
config << " provider :#{provider}"
|
47
|
+
config << " template \"#{template}\"" if template
|
48
|
+
config << " end"
|
49
|
+
config << ""
|
50
|
+
|
51
|
+
config.join("\n")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
.contai-field-wrapper {
|
2
|
+
position: relative;
|
3
|
+
}
|
4
|
+
|
5
|
+
.contai-generate-btn {
|
6
|
+
margin-top: 5px;
|
7
|
+
font-size: 0.875rem;
|
8
|
+
}
|
9
|
+
|
10
|
+
.contai-btn {
|
11
|
+
background-color: #6366f1;
|
12
|
+
border-color: #6366f1;
|
13
|
+
color: white;
|
14
|
+
padding: 0.375rem 0.75rem;
|
15
|
+
border-radius: 0.25rem;
|
16
|
+
border: 1px solid transparent;
|
17
|
+
font-size: 0.875rem;
|
18
|
+
line-height: 1.5;
|
19
|
+
cursor: pointer;
|
20
|
+
text-decoration: none;
|
21
|
+
display: inline-block;
|
22
|
+
text-align: center;
|
23
|
+
vertical-align: middle;
|
24
|
+
user-select: none;
|
25
|
+
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
26
|
+
}
|
27
|
+
|
28
|
+
.contai-btn:hover {
|
29
|
+
background-color: #4f46e5;
|
30
|
+
border-color: #4f46e5;
|
31
|
+
color: white;
|
32
|
+
}
|
33
|
+
|
34
|
+
.contai-btn:disabled {
|
35
|
+
opacity: 0.6;
|
36
|
+
cursor: not-allowed;
|
37
|
+
}
|
38
|
+
|
39
|
+
.contai-notification {
|
40
|
+
animation: slideIn 0.3s ease-out;
|
41
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
42
|
+
}
|
43
|
+
|
44
|
+
@keyframes slideIn {
|
45
|
+
from {
|
46
|
+
transform: translateX(100%);
|
47
|
+
opacity: 0;
|
48
|
+
}
|
49
|
+
to {
|
50
|
+
transform: translateX(0);
|
51
|
+
opacity: 1;
|
52
|
+
}
|
53
|
+
}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
(() => {
|
2
|
+
'use strict';
|
3
|
+
|
4
|
+
document.addEventListener('click', (e) => {
|
5
|
+
const button = e.target.closest('[data-contai-model]');
|
6
|
+
if (!button) return;
|
7
|
+
|
8
|
+
e.preventDefault();
|
9
|
+
|
10
|
+
const model = button.dataset.contaiModel;
|
11
|
+
const recordId = button.dataset.contaiRecordId;
|
12
|
+
const outputTarget = button.dataset.contaiOutputTarget;
|
13
|
+
|
14
|
+
generateContaiContent(button, model, recordId, outputTarget);
|
15
|
+
});
|
16
|
+
|
17
|
+
function generateContaiContent(button, model, recordId, outputTarget) {
|
18
|
+
const originalText = button.textContent;
|
19
|
+
|
20
|
+
button.disabled = true;
|
21
|
+
button.textContent = 'Generating...';
|
22
|
+
|
23
|
+
fetch('/contai/generate', {
|
24
|
+
method: 'POST',
|
25
|
+
headers: {
|
26
|
+
'Content-Type': 'application/json',
|
27
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
28
|
+
},
|
29
|
+
body: JSON.stringify({
|
30
|
+
model: model,
|
31
|
+
id: recordId
|
32
|
+
})
|
33
|
+
})
|
34
|
+
.then(response => response.json())
|
35
|
+
.then(data => {
|
36
|
+
if (data.success) {
|
37
|
+
updateOutput(outputTarget, data.content);
|
38
|
+
showNotification('Content generated successfully!', 'success');
|
39
|
+
} else {
|
40
|
+
showNotification('Error: ' + (data.errors || data.error), 'error');
|
41
|
+
}
|
42
|
+
})
|
43
|
+
.catch(() => {
|
44
|
+
showNotification('Network error occurred', 'error');
|
45
|
+
})
|
46
|
+
.finally(() => {
|
47
|
+
button.disabled = false;
|
48
|
+
button.textContent = originalText;
|
49
|
+
});
|
50
|
+
}
|
51
|
+
|
52
|
+
function updateOutput(fieldName, content) {
|
53
|
+
const field = document.querySelector(`[name*="${fieldName}"]`);
|
54
|
+
if (field) {
|
55
|
+
field.value = content;
|
56
|
+
field.dispatchEvent(new Event('input'));
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
function showNotification(message, type) {
|
61
|
+
const notification = document.createElement('div');
|
62
|
+
notification.className = `contai-notification contai-${type}`;
|
63
|
+
notification.textContent = message;
|
64
|
+
|
65
|
+
Object.assign(notification.style, {
|
66
|
+
position: 'fixed',
|
67
|
+
top: '20px',
|
68
|
+
right: '20px',
|
69
|
+
padding: '10px 15px',
|
70
|
+
borderRadius: '4px',
|
71
|
+
color: 'white',
|
72
|
+
zIndex: '1000',
|
73
|
+
backgroundColor: type === 'success' ? '#28a745' : '#dc3545'
|
74
|
+
});
|
75
|
+
|
76
|
+
document.body.appendChild(notification);
|
77
|
+
|
78
|
+
setTimeout(() => {
|
79
|
+
notification.remove();
|
80
|
+
}, 3000);
|
81
|
+
}
|
82
|
+
})();
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["output"]
|
5
|
+
static values = {
|
6
|
+
model: String,
|
7
|
+
recordId: Number,
|
8
|
+
outputTarget: String
|
9
|
+
}
|
10
|
+
|
11
|
+
connect() {
|
12
|
+
this.setupButton()
|
13
|
+
}
|
14
|
+
|
15
|
+
setupButton() {
|
16
|
+
const button = this.element.querySelector('.contai-generate-btn')
|
17
|
+
if (button) {
|
18
|
+
button.addEventListener('click', this.generate.bind(this))
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
async generate(event) {
|
23
|
+
event.preventDefault()
|
24
|
+
|
25
|
+
const button = event.target
|
26
|
+
const originalText = button.textContent
|
27
|
+
|
28
|
+
try {
|
29
|
+
button.disabled = true
|
30
|
+
button.textContent = 'Generating...'
|
31
|
+
|
32
|
+
const response = await fetch('/contai/generate', {
|
33
|
+
method: 'POST',
|
34
|
+
headers: {
|
35
|
+
'Content-Type': 'application/json',
|
36
|
+
'X-CSRF-Token': this.getCSRFToken()
|
37
|
+
},
|
38
|
+
body: JSON.stringify({
|
39
|
+
model: this.modelValue,
|
40
|
+
id: this.recordIdValue
|
41
|
+
})
|
42
|
+
})
|
43
|
+
|
44
|
+
const data = await response.json()
|
45
|
+
|
46
|
+
if (data.success) {
|
47
|
+
this.updateOutput(data.content)
|
48
|
+
this.showSuccess()
|
49
|
+
} else {
|
50
|
+
this.showError(data.errors || [data.error])
|
51
|
+
}
|
52
|
+
} catch (error) {
|
53
|
+
this.showError(['Network error occurred'])
|
54
|
+
} finally {
|
55
|
+
button.disabled = false
|
56
|
+
button.textContent = originalText
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
updateOutput(content) {
|
61
|
+
if (this.hasOutputTarget) {
|
62
|
+
this.outputTarget.value = content
|
63
|
+
this.outputTarget.dispatchEvent(new Event('input', { bubbles: true }))
|
64
|
+
} else {
|
65
|
+
const fieldName = this.outputTargetValue || 'body'
|
66
|
+
const field = this.element.querySelector(`[name*="${fieldName}"]`)
|
67
|
+
if (field) {
|
68
|
+
field.value = content
|
69
|
+
field.dispatchEvent(new Event('input', { bubbles: true }))
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
showSuccess() {
|
75
|
+
this.showNotification('Content generated successfully!', 'success')
|
76
|
+
}
|
77
|
+
|
78
|
+
showError(errors) {
|
79
|
+
const message = Array.isArray(errors) ? errors.join(', ') : errors
|
80
|
+
this.showNotification(`Error: ${message}`, 'error')
|
81
|
+
}
|
82
|
+
|
83
|
+
showNotification(message, type) {
|
84
|
+
const notification = document.createElement('div')
|
85
|
+
notification.className = `contai-notification contai-${type}`
|
86
|
+
notification.textContent = message
|
87
|
+
notification.style.cssText = `
|
88
|
+
position: fixed;
|
89
|
+
top: 20px;
|
90
|
+
right: 20px;
|
91
|
+
padding: 10px 15px;
|
92
|
+
border-radius: 4px;
|
93
|
+
color: white;
|
94
|
+
z-index: 1000;
|
95
|
+
background-color: ${type === 'success' ? '#28a745' : '#dc3545'};
|
96
|
+
`
|
97
|
+
|
98
|
+
document.body.appendChild(notification)
|
99
|
+
|
100
|
+
setTimeout(() => {
|
101
|
+
notification.remove()
|
102
|
+
}, 3000)
|
103
|
+
}
|
104
|
+
|
105
|
+
getCSRFToken() {
|
106
|
+
const token = document.querySelector('meta[name="csrf-token"]')
|
107
|
+
return token ? token.getAttribute('content') : ''
|
108
|
+
}
|
109
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
Contai.configure do |config|
|
2
|
+
# Default provider for all models (can be overridden per model)
|
3
|
+
config.default_provider = :http
|
4
|
+
|
5
|
+
# Default template for prompt generation
|
6
|
+
config.default_template = "Generate content based on: {{prompt}}"
|
7
|
+
|
8
|
+
# Request timeout in seconds
|
9
|
+
config.timeout = 30
|
10
|
+
end
|
11
|
+
|
12
|
+
# Configure your AI providers here:
|
13
|
+
|
14
|
+
# OpenAI Configuration
|
15
|
+
# Contai::Providers::OpenAI.configure do |config|
|
16
|
+
# config.api_key = ENV['OPENAI_API_KEY']
|
17
|
+
# config.default_model = 'gpt-3.5-turbo'
|
18
|
+
# config.default_max_tokens = 1000
|
19
|
+
# end
|
20
|
+
|
21
|
+
# Claude Configuration
|
22
|
+
# Contai::Providers::Claude.configure do |config|
|
23
|
+
# config.api_key = ENV['CLAUDE_API_KEY']
|
24
|
+
# config.default_model = 'claude-3-sonnet-20240229'
|
25
|
+
# end
|
26
|
+
|
27
|
+
# Custom HTTP API Configuration
|
28
|
+
# Contai::Providers::HTTP.configure do |config|
|
29
|
+
# config.default_url = ENV['AI_API_URL']
|
30
|
+
# config.default_headers = {
|
31
|
+
# 'Authorization' => "Bearer #{ENV['AI_API_KEY']}",
|
32
|
+
# 'Content-Type' => 'application/json'
|
33
|
+
# }
|
34
|
+
# end
|
data/sig/contai.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: contai
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Panasenkov A.
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-06-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httparty
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.20'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.20'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.4'
|
69
|
+
description: A Ruby on Rails gem that provides seamless integration with AI services
|
70
|
+
to automatically generate content for your Rails models. It offers a simple interface
|
71
|
+
to enhance your models with AI-generated content, supporting various content types
|
72
|
+
and customization options.
|
73
|
+
email:
|
74
|
+
- apanasenkov@capaa.ru
|
75
|
+
executables: []
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- ".rspec"
|
80
|
+
- ".rubocop.yml"
|
81
|
+
- CHANGELOG.md
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- app/controllers/contai/generations_controller.rb
|
85
|
+
- app/helpers/contai_helper.rb
|
86
|
+
- app/jobs/contai_generation_job.rb
|
87
|
+
- config/routes.rb
|
88
|
+
- contai.gemspec
|
89
|
+
- docs/usage.md
|
90
|
+
- lib/contai.rb
|
91
|
+
- lib/contai/configuration.rb
|
92
|
+
- lib/contai/engine.rb
|
93
|
+
- lib/contai/generatabale.rb
|
94
|
+
- lib/contai/providers.rb
|
95
|
+
- lib/contai/providers/base.rb
|
96
|
+
- lib/contai/providers/claude.rb
|
97
|
+
- lib/contai/providers/http.rb
|
98
|
+
- lib/contai/providers/n8n.rb
|
99
|
+
- lib/contai/providers/openai.rb
|
100
|
+
- lib/contai/version.rb
|
101
|
+
- lib/generators/contai/install_generator.rb
|
102
|
+
- lib/generators/contai/model_generator.rb
|
103
|
+
- lib/generators/contai/templates/contai.css
|
104
|
+
- lib/generators/contai/templates/contai.js
|
105
|
+
- lib/generators/contai/templates/contai_controller.js
|
106
|
+
- lib/generators/contai/templates/contai_generation_job.rb
|
107
|
+
- lib/generators/contai/templates/initializer.rb
|
108
|
+
- sig/contai.rbs
|
109
|
+
homepage: https://github.com/capaas/contai
|
110
|
+
licenses:
|
111
|
+
- Unlicense
|
112
|
+
metadata:
|
113
|
+
allowed_push_host: https://rubygems.org
|
114
|
+
homepage_uri: https://github.com/capaas/contai
|
115
|
+
source_code_uri: https://github.com/capaas/contai
|
116
|
+
changelog_uri: https://github.com/capaas/contai/blob/main/CHANGELOG.md
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 3.0.0
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubygems_version: 3.2.22
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: Gem for generating AI content for rails models
|
136
|
+
test_files: []
|