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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +125 -0
  3. data/Rakefile +12 -0
  4. data/app/models/concerns/ruby_conversations/messageable.rb +13 -0
  5. data/app/models/concerns/ruby_conversations/versionable.rb +20 -0
  6. data/app/models/ruby_conversations/ai_conversation.rb +107 -0
  7. data/app/models/ruby_conversations/ai_message.rb +28 -0
  8. data/app/models/ruby_conversations/ai_message_input.rb +22 -0
  9. data/app/models/ruby_conversations/ai_message_prompt.rb +28 -0
  10. data/app/models/ruby_conversations/message_builder.rb +100 -0
  11. data/app/models/ruby_conversations/prompt.rb +104 -0
  12. data/app/models/ruby_conversations/prompt_version.rb +16 -0
  13. data/lib/generators/ruby_conversations/install/install_generator.rb +60 -0
  14. data/lib/generators/ruby_conversations/install/templates/README.md +42 -0
  15. data/lib/generators/ruby_conversations/install/templates/initializer.rb.erb +18 -0
  16. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_conversations.rb.erb +13 -0
  17. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_inputs.rb.erb +13 -0
  18. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_prompts.rb.erb +18 -0
  19. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_messages.rb.erb +18 -0
  20. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_tool_calls.rb.erb +15 -0
  21. data/lib/generators/ruby_conversations/install/templates/migrations/create_prompt_versions.rb.erb +14 -0
  22. data/lib/generators/ruby_conversations/install/templates/migrations/create_prompts.rb.erb +16 -0
  23. data/lib/ruby_conversations/configuration.rb +28 -0
  24. data/lib/ruby_conversations/engine.rb +41 -0
  25. data/lib/ruby_conversations/jwt_client.rb +23 -0
  26. data/lib/ruby_conversations/storage/base.rb +28 -0
  27. data/lib/ruby_conversations/storage/local.rb +30 -0
  28. data/lib/ruby_conversations/storage/remote.rb +93 -0
  29. data/lib/ruby_conversations/version.rb +9 -0
  30. data/lib/ruby_conversations.rb +62 -0
  31. 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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