ruby_conversations 1.0.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/README.md +125 -0
- data/Rakefile +12 -0
- data/app/models/concerns/ruby_conversations/messageable.rb +13 -0
- data/app/models/concerns/ruby_conversations/versionable.rb +20 -0
- data/app/models/ruby_conversations/ai_conversation.rb +107 -0
- data/app/models/ruby_conversations/ai_message.rb +28 -0
- data/app/models/ruby_conversations/ai_message_input.rb +22 -0
- data/app/models/ruby_conversations/ai_message_prompt.rb +28 -0
- data/app/models/ruby_conversations/message_builder.rb +100 -0
- data/app/models/ruby_conversations/prompt.rb +104 -0
- data/app/models/ruby_conversations/prompt_version.rb +16 -0
- data/lib/generators/ruby_conversations/install/install_generator.rb +60 -0
- data/lib/generators/ruby_conversations/install/templates/README.md +42 -0
- data/lib/generators/ruby_conversations/install/templates/initializer.rb.erb +18 -0
- data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_conversations.rb.erb +13 -0
- data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_inputs.rb.erb +13 -0
- data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_prompts.rb.erb +18 -0
- data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_messages.rb.erb +18 -0
- data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_tool_calls.rb.erb +15 -0
- data/lib/generators/ruby_conversations/install/templates/migrations/create_prompt_versions.rb.erb +14 -0
- data/lib/generators/ruby_conversations/install/templates/migrations/create_prompts.rb.erb +16 -0
- data/lib/ruby_conversations/configuration.rb +28 -0
- data/lib/ruby_conversations/engine.rb +41 -0
- data/lib/ruby_conversations/jwt_client.rb +23 -0
- data/lib/ruby_conversations/storage/base.rb +28 -0
- data/lib/ruby_conversations/storage/local.rb +30 -0
- data/lib/ruby_conversations/storage/remote.rb +93 -0
- data/lib/ruby_conversations/version.rb +9 -0
- data/lib/ruby_conversations.rb +62 -0
- metadata +314 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RubyConversations.configure do |config|
|
4
|
+
# Storage mode: :local or :remote
|
5
|
+
config.storage_mode = :local
|
6
|
+
|
7
|
+
# Remote API settings (only needed for remote mode)
|
8
|
+
# config.api_url = ENV['RUBY_CONVERSATIONS_API_URL']
|
9
|
+
# config.jwt_secret = ENV['RUBY_CONVERSATIONS_JWT_SECRET']
|
10
|
+
|
11
|
+
# Default LLM settings
|
12
|
+
config.default_llm_model = 'gpt-4'
|
13
|
+
|
14
|
+
# Customize model behaviors
|
15
|
+
config.conversation_class = 'AiConversation'
|
16
|
+
config.message_class = 'AiMessage'
|
17
|
+
config.prompt_class = 'Prompt'
|
18
|
+
end
|
data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_conversations.rb.erb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateAiConversations < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :ai_conversations do |t|
|
4
|
+
t.string :conversationable_type
|
5
|
+
t.bigint :conversationable_id
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :ai_conversations, [:conversationable_type, :conversationable_id],
|
11
|
+
name: 'index_ai_conversations_on_conversationable'
|
12
|
+
end
|
13
|
+
end
|
data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_inputs.rb.erb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateAiMessageInputs < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :ai_message_inputs do |t|
|
4
|
+
t.string :placeholder_name
|
5
|
+
t.text :value
|
6
|
+
t.bigint :ai_message_prompt_id, null: false
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :ai_message_inputs, :ai_message_prompt_id
|
12
|
+
end
|
13
|
+
end
|
data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_prompts.rb.erb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateAiMessagePrompts < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :ai_message_prompts do |t|
|
4
|
+
t.bigint :ai_message_id, null: false
|
5
|
+
t.bigint :prompt_id
|
6
|
+
t.text :draft
|
7
|
+
t.bigint :prompt_version_id
|
8
|
+
t.string :name
|
9
|
+
t.string :role
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :ai_message_prompts, :ai_message_id
|
15
|
+
add_index :ai_message_prompts, :prompt_id
|
16
|
+
add_index :ai_message_prompts, :prompt_version_id
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateAiMessages < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :ai_messages do |t|
|
4
|
+
t.bigint :ai_conversation_id, null: false
|
5
|
+
t.jsonb :request
|
6
|
+
t.text :response
|
7
|
+
t.string :model_identifier
|
8
|
+
t.string :tool
|
9
|
+
t.text :change_description
|
10
|
+
t.string :llm, default: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', null: false
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :ai_messages, :ai_conversation_id
|
16
|
+
add_index :ai_messages, :llm
|
17
|
+
end
|
18
|
+
end
|
data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_tool_calls.rb.erb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateAiToolCalls < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :ai_tool_calls do |t|
|
4
|
+
t.references :ai_message, null: false, foreign_key: true
|
5
|
+
t.string :name, null: false
|
6
|
+
t.jsonb :arguments, null: false, default: {}
|
7
|
+
t.text :result
|
8
|
+
t.text :error
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :ai_tool_calls, :name
|
14
|
+
end
|
15
|
+
end
|
data/lib/generators/ruby_conversations/install/templates/migrations/create_prompt_versions.rb.erb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreatePromptVersions < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :prompt_versions do |t|
|
4
|
+
t.bigint :prompt_id, null: false
|
5
|
+
t.text :message
|
6
|
+
t.float :temperature
|
7
|
+
t.text :change_description
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index :prompt_versions, :prompt_id
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreatePrompts < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :prompts do |t|
|
4
|
+
t.text :message
|
5
|
+
t.string :name
|
6
|
+
t.string :role
|
7
|
+
t.float :temperature, default: 0.0
|
8
|
+
t.string :valid_placeholders
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :prompts, :name, unique: true
|
14
|
+
add_index :prompts, :role
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Configuration options for RubyConversations
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :storage_mode, :api_url, :jwt_secret, :default_llm_model, :default_llm_provider,
|
7
|
+
:conversation_class, :message_class, :message_input_class, :message_prompt_class,
|
8
|
+
:prompt_class, :prompt_version_class
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@storage_mode = :local
|
12
|
+
@default_llm_model = 'claude-3-7-sonnet'
|
13
|
+
@default_llm_provider = 'bedrock'
|
14
|
+
@conversation_class = 'AiConversation'
|
15
|
+
@message_class = 'AiMessage'
|
16
|
+
@message_input_class = 'AiMessageInput'
|
17
|
+
@message_prompt_class = 'AiMessagePrompt'
|
18
|
+
@prompt_class = 'Prompt'
|
19
|
+
@prompt_version_class = 'PromptVersion'
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate!
|
23
|
+
return unless storage_mode == :remote
|
24
|
+
raise ConfigurationError, 'api_url is required for remote storage' unless api_url
|
25
|
+
raise ConfigurationError, 'jwt_secret is required for remote storage' unless jwt_secret
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Defines the Rails engine for RubyConversations.
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
config.generators do |g|
|
7
|
+
g.test_framework :rspec
|
8
|
+
end
|
9
|
+
|
10
|
+
config.autoload_paths += Dir[
|
11
|
+
root.join('app', 'models', 'concerns', '**', '*.rb'),
|
12
|
+
root.join('app', 'models', '**', '*.rb')
|
13
|
+
]
|
14
|
+
|
15
|
+
initializer 'ruby_conversations.zeitwerk' do |_app|
|
16
|
+
Rails.autoloaders.main.push_dir(root.join('app', 'models', 'concerns'))
|
17
|
+
Rails.autoloaders.main.push_dir(root.join('app', 'models'))
|
18
|
+
end
|
19
|
+
|
20
|
+
initializer 'ruby_conversations.assets' do |app|
|
21
|
+
# Future UI components will be added here
|
22
|
+
end
|
23
|
+
|
24
|
+
config.to_prepare do
|
25
|
+
Dir.glob(Engine.root.join('app', 'models', 'concerns', 'ruby_conversations', '*.rb')).each do |c|
|
26
|
+
require_dependency(c)
|
27
|
+
end
|
28
|
+
|
29
|
+
Dir.glob(Engine.root.join('app', 'models', 'ruby_conversations', '*.rb')).each do |m|
|
30
|
+
require_dependency(m)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
initializer 'ruby_conversations.factories', after: 'factory_bot.set_factory_paths' do
|
35
|
+
if defined?(FactoryBot)
|
36
|
+
factory_path = File.expand_path('../../spec/factories', __dir__)
|
37
|
+
FactoryBot.definition_file_paths << factory_path unless FactoryBot.definition_file_paths.include?(factory_path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Handles JWT encoding and decoding for secure communication.
|
5
|
+
class JwtClient
|
6
|
+
def initialize(secret)
|
7
|
+
@secret = secret
|
8
|
+
end
|
9
|
+
|
10
|
+
def token
|
11
|
+
JWT.encode(payload, @secret, 'HS256')
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def payload
|
17
|
+
{
|
18
|
+
exp: 5.minutes.from_now.to_i,
|
19
|
+
iat: Time.current.to_i
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
module Storage
|
5
|
+
# Abstract base class for storage adapters.
|
6
|
+
class Base
|
7
|
+
def remote?
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def store_conversation(_conversation)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_conversation(id)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch_prompt(name)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def store_prompt(prompt)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
module Storage
|
5
|
+
# Storage adapter for handling conversations locally using ActiveRecord.
|
6
|
+
class Local < Base
|
7
|
+
def remote?
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
11
|
+
def store_conversation(conversation)
|
12
|
+
# No-op for local storage - ActiveRecord handles it
|
13
|
+
conversation
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch_conversation(id)
|
17
|
+
RubyConversations.configuration.conversation_class.constantize.find(id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch_prompt(name)
|
21
|
+
RubyConversations.configuration.prompt_class.constantize.find_by!(name: name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def store_prompt(prompt)
|
25
|
+
# No-op for local storage - ActiveRecord handles it
|
26
|
+
prompt
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
module Storage
|
5
|
+
# Storage adapter for handling conversations via a remote API.
|
6
|
+
class Remote < Base
|
7
|
+
def initialize(url:, jwt_secret:)
|
8
|
+
super()
|
9
|
+
@url = url
|
10
|
+
@jwt_client = JwtClient.new(jwt_secret)
|
11
|
+
end
|
12
|
+
|
13
|
+
def remote?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def store_conversation(conversation)
|
18
|
+
response = client.post(
|
19
|
+
"#{@url}/api/conversations",
|
20
|
+
conversation.as_json(conversation_json_options)
|
21
|
+
)
|
22
|
+
|
23
|
+
update_conversation_from_response(conversation, response.body)
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch_conversation(id)
|
27
|
+
response = client.get("#{@url}/api/conversations/#{id}")
|
28
|
+
build_conversation_from_response(response.body)
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch_prompt(name)
|
32
|
+
response = client.get("#{@url}/api/prompts/#{name}")
|
33
|
+
build_prompt_from_response(response.body)
|
34
|
+
end
|
35
|
+
|
36
|
+
def store_prompt(prompt)
|
37
|
+
response = client.post(
|
38
|
+
"#{@url}/api/prompts",
|
39
|
+
prompt.as_json(include: :versions)
|
40
|
+
)
|
41
|
+
build_prompt_from_response(response.body)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def client
|
47
|
+
@client ||= Faraday.new do |f|
|
48
|
+
f.request :json
|
49
|
+
f.response :json
|
50
|
+
f.adapter Faraday.default_adapter
|
51
|
+
f.headers['Authorization'] = "Bearer #{@jwt_client.token}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_conversation_from_response(conversation, response)
|
56
|
+
conversation.assign_attributes(
|
57
|
+
response.except('ai_messages')
|
58
|
+
)
|
59
|
+
|
60
|
+
response['ai_messages']&.each do |msg_data|
|
61
|
+
msg = conversation.ai_messages.find { |m| m.id == msg_data['id'] }
|
62
|
+
msg&.assign_attributes(msg_data.except('ai_message_prompts'))
|
63
|
+
end
|
64
|
+
|
65
|
+
conversation
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_conversation_from_response(response)
|
69
|
+
RubyConversations.configuration.conversation_class.constantize.new(response)
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_prompt_from_response(response)
|
73
|
+
RubyConversations.configuration.prompt_class.constantize.new(response)
|
74
|
+
end
|
75
|
+
|
76
|
+
# rubocop:disable Metrics/MethodLength
|
77
|
+
def conversation_json_options
|
78
|
+
{
|
79
|
+
include: {
|
80
|
+
ai_messages: {
|
81
|
+
include: {
|
82
|
+
ai_message_prompts: {
|
83
|
+
include: :ai_message_inputs
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
end
|
90
|
+
# rubocop:enable Metrics/MethodLength
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
MAJOR = 1
|
5
|
+
MINOR = 0
|
6
|
+
PATCH = 0 # this is automatically incremented by the build process
|
7
|
+
|
8
|
+
VERSION = "#{RubyConversations::MAJOR}.#{RubyConversations::MINOR}.#{RubyConversations::PATCH}".freeze
|
9
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'active_record'
|
5
|
+
require 'ruby_llm'
|
6
|
+
require 'faraday'
|
7
|
+
require 'jwt'
|
8
|
+
require 'zeitwerk'
|
9
|
+
require 'ruby_conversations/configuration'
|
10
|
+
|
11
|
+
loader = Zeitwerk::Loader.for_gem
|
12
|
+
loader.ignore("#{__dir__}/generators")
|
13
|
+
|
14
|
+
# Main module for the RubyConversations engine.
|
15
|
+
module RubyConversations
|
16
|
+
class Error < StandardError; end
|
17
|
+
class ConfigurationError < Error; end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
attr_writer :configuration
|
21
|
+
|
22
|
+
def configuration
|
23
|
+
@configuration ||= Configuration.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def configure
|
27
|
+
yield(configuration)
|
28
|
+
configuration.validate!
|
29
|
+
end
|
30
|
+
|
31
|
+
def storage
|
32
|
+
@storage ||= case configuration.storage_mode
|
33
|
+
when :local
|
34
|
+
build_local_storage
|
35
|
+
when :remote
|
36
|
+
build_remote_storage
|
37
|
+
else
|
38
|
+
raise ConfigurationError, "Unknown storage mode: #{configuration.storage_mode}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def reset!
|
43
|
+
@configuration = Configuration.new
|
44
|
+
@storage = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def build_local_storage
|
50
|
+
Storage::Local.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_remote_storage
|
54
|
+
Storage::Remote.new(
|
55
|
+
url: configuration.api_url,
|
56
|
+
jwt_secret: configuration.jwt_secret
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
loader.setup
|