ruby_conversations 1.0.7 → 1.0.9

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -41
  3. data/app/models/concerns/ruby_conversations/conversation_chat.rb +49 -0
  4. data/app/models/concerns/ruby_conversations/conversation_messages.rb +18 -0
  5. data/app/models/{ruby_conversations/concerns → concerns/ruby_conversations}/llm_credentials.rb +1 -0
  6. data/app/models/concerns/ruby_conversations/message_api_attributes.rb +39 -0
  7. data/app/models/concerns/ruby_conversations/message_attributes.rb +44 -0
  8. data/app/models/concerns/ruby_conversations/message_processing.rb +48 -0
  9. data/app/models/{ruby_conversations/concerns → concerns/ruby_conversations}/message_validation.rb +2 -2
  10. data/app/models/concerns/ruby_conversations/storable.rb +13 -0
  11. data/app/models/ruby_conversations/conversation.rb +102 -0
  12. data/app/models/ruby_conversations/message.rb +108 -0
  13. data/app/models/ruby_conversations/message_builder.rb +45 -20
  14. data/app/models/ruby_conversations/message_input.rb +69 -0
  15. data/app/models/ruby_conversations/message_prompt.rb +110 -0
  16. data/app/models/ruby_conversations/prompt.rb +48 -39
  17. data/lib/ruby_conversations/aws_credential_provider.rb +11 -5
  18. data/lib/ruby_conversations/client.rb +90 -0
  19. data/lib/ruby_conversations/configuration.rb +3 -13
  20. data/lib/ruby_conversations/engine.rb +2 -0
  21. data/lib/ruby_conversations/errors.rb +9 -0
  22. data/lib/ruby_conversations/version.rb +1 -1
  23. data/lib/ruby_conversations.rb +15 -42
  24. metadata +43 -81
  25. data/Rakefile +0 -12
  26. data/app/models/concerns/ruby_conversations/messageable.rb +0 -13
  27. data/app/models/concerns/ruby_conversations/versionable.rb +0 -20
  28. data/app/models/ruby_conversations/ai_conversation.rb +0 -91
  29. data/app/models/ruby_conversations/ai_message.rb +0 -28
  30. data/app/models/ruby_conversations/ai_message_input.rb +0 -22
  31. data/app/models/ruby_conversations/ai_message_prompt.rb +0 -28
  32. data/app/models/ruby_conversations/prompt_version.rb +0 -16
  33. data/lib/generators/ruby_conversations/install/install_generator.rb +0 -60
  34. data/lib/generators/ruby_conversations/install/templates/README.md +0 -42
  35. data/lib/generators/ruby_conversations/install/templates/initializer.rb.erb +0 -18
  36. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_conversations.rb.erb +0 -13
  37. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_inputs.rb.erb +0 -13
  38. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_message_prompts.rb.erb +0 -18
  39. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_messages.rb.erb +0 -18
  40. data/lib/generators/ruby_conversations/install/templates/migrations/create_ai_tool_calls.rb.erb +0 -15
  41. data/lib/generators/ruby_conversations/install/templates/migrations/create_prompt_versions.rb.erb +0 -14
  42. data/lib/generators/ruby_conversations/install/templates/migrations/create_prompts.rb.erb +0 -16
  43. data/lib/ruby_conversations/jwt_client.rb +0 -23
  44. data/lib/ruby_conversations/storage/base.rb +0 -28
  45. data/lib/ruby_conversations/storage/local.rb +0 -30
  46. data/lib/ruby_conversations/storage/remote.rb +0 -93
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ module RubyConversations
6
+ # Represents an input value for a message prompt
7
+ class MessageInput
8
+ include ActiveModel::Model
9
+
10
+ # Define attributes
11
+ attr_accessor :id, :message_prompt_id, :placeholder_name, :value,
12
+ :created_at, :updated_at
13
+
14
+ # Validations
15
+ validates :placeholder_name, presence: true
16
+ validate :value_not_nil
17
+
18
+ # Association-like methods
19
+ def message_prompt=(prompt)
20
+ @message_prompt = prompt
21
+ @message_prompt_id = prompt&.id
22
+ end
23
+
24
+ attr_reader :message_prompt
25
+
26
+ # Scopes
27
+ def self.ordered
28
+ all.sort_by(&:placeholder_name)
29
+ end
30
+
31
+ def self.all
32
+ @all ||= []
33
+ end
34
+
35
+ # Attributes method for serialization/logging
36
+ def attributes
37
+ base_attributes
38
+ end
39
+
40
+ # Method for API serialization
41
+ def attributes_for_api
42
+ {
43
+ id: id,
44
+ placeholder_name: placeholder_name,
45
+ value: value
46
+ }.compact
47
+ end
48
+
49
+ private
50
+
51
+ def value_not_nil
52
+ errors.add(:value, "can't be blank") if value.nil?
53
+ end
54
+
55
+ def base_attributes
56
+ {
57
+ 'id' => id,
58
+ 'message_prompt_id' => message_prompt_id,
59
+ 'placeholder_name' => placeholder_name,
60
+ 'value' => value,
61
+ 'created_at' => created_at,
62
+ 'updated_at' => updated_at
63
+ }
64
+ end
65
+ end
66
+
67
+ # Alias for compatibility with Zeitwerk
68
+ AIMessageInput = MessageInput
69
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require_relative 'message_input'
5
+
6
+ module RubyConversations
7
+ # Represents a prompt used to generate a message in a conversation
8
+ class MessagePrompt
9
+ include ActiveModel::Model
10
+
11
+ # Define attributes
12
+ attr_accessor :id, :message_id, :prompt_version_id, :name, :role, :content, :metadata,
13
+ :created_at, :updated_at, :message_inputs, :draft, :message
14
+
15
+ # Define nested attributes writer
16
+ def message_inputs_attributes=(attributes)
17
+ @message_inputs ||= []
18
+ attributes.each do |attrs|
19
+ @message_inputs << RubyConversations::MessageInput.new(attrs)
20
+ end
21
+ end
22
+
23
+ # Initialization
24
+ def initialize(attributes = {})
25
+ @message_inputs = [] # Initialize inputs array
26
+ # Extract nested inputs before super
27
+ inputs_attributes = extract_nested_attributes!(attributes, :message_inputs)
28
+
29
+ super
30
+ initialize_message_inputs(inputs_attributes)
31
+ end
32
+
33
+ # Handle nested inputs initialization
34
+ def initialize_message_inputs(attributes_array)
35
+ (attributes_array || []).each do |attrs|
36
+ next if attrs.blank?
37
+
38
+ @message_inputs << RubyConversations::MessageInput.new(attrs)
39
+ end
40
+ end
41
+
42
+ # Attributes method for serialization/logging
43
+ def attributes
44
+ base_attributes.merge(message_inputs_attributes_hash)
45
+ end
46
+
47
+ # Method for API serialization
48
+ def attributes_for_api
49
+ {
50
+ id: id,
51
+ prompt_version_id: prompt_version_id,
52
+ name: name,
53
+ role: role,
54
+ content: content,
55
+ metadata: metadata,
56
+ draft: draft,
57
+ ai_message_inputs_attributes: message_inputs.map(&:attributes_for_api)
58
+ }.compact
59
+ end
60
+
61
+ private
62
+
63
+ def base_attributes
64
+ identity_attributes.merge(content_attributes).merge(timestamp_attributes)
65
+ end
66
+
67
+ def identity_attributes
68
+ {
69
+ 'id' => id,
70
+ 'message_id' => message_id,
71
+ 'prompt_version_id' => prompt_version_id,
72
+ 'message' => message
73
+ }
74
+ end
75
+
76
+ def content_attributes
77
+ {
78
+ 'name' => name,
79
+ 'role' => role,
80
+ 'content' => content,
81
+ 'metadata' => metadata,
82
+ 'draft' => draft
83
+ }
84
+ end
85
+
86
+ def timestamp_attributes
87
+ {
88
+ 'created_at' => created_at,
89
+ 'updated_at' => updated_at
90
+ }
91
+ end
92
+
93
+ def message_inputs_attributes_hash
94
+ {
95
+ 'message_inputs' => message_inputs.map(&:attributes)
96
+ }
97
+ end
98
+
99
+ # Helper method to extract nested attributes
100
+ def extract_nested_attributes!(attributes, key)
101
+ nested = extract_key_attributes(attributes, key)
102
+ nested_attributes = extract_key_attributes(attributes, "#{key}_attributes")
103
+ nested.concat(nested_attributes)
104
+ end
105
+
106
+ def extract_key_attributes(attributes, key)
107
+ attributes.delete(key) || attributes.delete(key.to_s) || attributes.delete(key.to_sym) || []
108
+ end
109
+ end
110
+ end
@@ -1,61 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyConversations
4
- # A prompt is a message template that can be used to generate messages for an AI conversation.
5
- class Prompt < ActiveRecord::Base
6
- include Versionable
3
+ require 'active_model'
7
4
 
8
- # Constants
9
- VALID_ROLES = %w[system user assistant].freeze
5
+ module RubyConversations
6
+ # Represents a prompt template used to generate AI messages.
7
+ class Prompt
8
+ include ActiveModel::Model
10
9
 
11
- # Associations
12
- has_many :versions, class_name: RubyConversations.configuration.prompt_version_class
13
- has_many :message_prompts, class_name: RubyConversations.configuration.message_prompt_class, dependent: :nullify
14
- has_many :messages, through: :message_prompts, class_name: RubyConversations.configuration.message_class,
15
- source: :ai_message, dependent: :nullify
16
- belongs_to :organization, optional: true
10
+ # Define attributes
11
+ attr_accessor :id, :name, :role, :message, :valid_placeholders, :temperature, :metadata, :created_at, :updated_at,
12
+ :latest_version_id
17
13
 
18
- # Validations
19
- validates :name, presence: true, uniqueness: true
20
- validates :temperature, numericality: { greater_than_or_equal_to: 0.0 }, allow_nil: true
21
- validates :role, presence: true, inclusion: { in: VALID_ROLES }
22
-
23
- # Scopes
24
- scope :active, -> { where(active: true) }
25
- scope :by_role, ->(role) { where(role: role) }
26
- scope :with_versions, -> { includes(:versions) }
27
- scope :ordered_by_name, -> { order(:name) }
28
- scope :internal, -> { where(internal: true) }
29
- scope :external, -> { where(internal: false) }
14
+ # Constants
15
+ ROLES = %w[system user assistant].freeze
30
16
 
31
17
  # Class methods
18
+ def self.roles
19
+ ROLES
20
+ end
21
+
32
22
  def self.find_by_name!(name)
33
- find_by!(name: name.to_s.strip)
23
+ prompt_data = RubyConversations.client.fetch_prompt(name)
24
+ raise "Prompt not found: #{name}" unless prompt_data
25
+
26
+ new(prompt_data)
34
27
  end
35
28
 
36
- def self.roles
37
- VALID_ROLES
29
+ def self.find_by!(conditions)
30
+ name = conditions[:name]
31
+ find_by_name!(name)
38
32
  end
39
33
 
40
- # Instance methods
41
- def latest_version_id
42
- populate_initial_version!
43
- versions.last&.id
34
+ # Validations
35
+ validates :name, presence: true
36
+ validates :role, presence: true, inclusion: { in: ROLES }
37
+ validates :message, presence: true
38
+
39
+ # Initialization
40
+ def initialize(attributes = {})
41
+ super
44
42
  end
45
43
 
46
- def populate_initial_version!
47
- versions.create!(message:, temperature:, created_at:, updated_at:) if versions.empty?
44
+ # Basic attributes method
45
+ def attributes
46
+ instance_values
48
47
  end
49
48
 
50
- def interpolate(inputs = {})
51
- interpolated = message.dup
52
- format(interpolated, **inputs)
49
+ # Method for API serialization
50
+ def attributes_for_api
51
+ {
52
+ id: id,
53
+ name: name,
54
+ role: role,
55
+ message: message,
56
+ valid_placeholders: valid_placeholders,
57
+ temperature: temperature,
58
+ metadata: metadata,
59
+ latest_version_id: latest_version_id
60
+ }.compact
53
61
  end
54
62
 
55
- private
63
+ # Interpolate placeholders in the message
64
+ def interpolate(variables = {})
65
+ return message if message.nil? || variables.empty?
56
66
 
57
- def versioned_attributes
58
- %i[content role]
67
+ format(message, **variables)
59
68
  end
60
69
  end
61
70
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aws-sdk-core'
4
+ require_relative 'errors'
4
5
 
5
6
  module RubyConversations
6
7
  # Manages AWS credentials with automatic refresh capability
@@ -53,7 +54,7 @@ module RubyConversations
53
54
  end
54
55
 
55
56
  def use_mock_credentials?
56
- !Rails.env.production?
57
+ ENV['RAILS_ENV'] != 'production'
57
58
  end
58
59
 
59
60
  def set_mock_credentials
@@ -62,7 +63,7 @@ module RubyConversations
62
63
  'mock_secret_access_key',
63
64
  'mock_session_token'
64
65
  )
65
- @expiration = Time.now + 1.hour
66
+ @expiration = Time.now + 3600 # 1 hour
66
67
  end
67
68
 
68
69
  def fetch_and_set_real_credentials
@@ -78,13 +79,18 @@ module RubyConversations
78
79
  end
79
80
 
80
81
  def assign_credentials(ecs_credentials)
81
- @credentials = ecs_credentials.credentials
82
- @expiration = ecs_credentials.expiration if ecs_credentials.respond_to?(:expiration)
82
+ if ecs_credentials.respond_to?(:credentials)
83
+ @credentials = ecs_credentials.credentials
84
+ @expiration = ecs_credentials.expiration if ecs_credentials.respond_to?(:expiration)
85
+ else
86
+ @credentials = ecs_credentials
87
+ @expiration = nil
88
+ end
83
89
  end
84
90
 
85
91
  def expired?
86
- return true if @credentials.nil?
87
92
  return false if @expiration.nil?
93
+ return true if @credentials.nil?
88
94
 
89
95
  # Refresh if we're within 5 minutes of expiration
90
96
  @expiration < Time.now + 300
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'jwt'
5
+
6
+ module RubyConversations
7
+ # HTTP client for interacting with the conversations API
8
+ class Client
9
+ PROMPT_ATTRIBUTES = %w[
10
+ id name message role temperature valid_placeholders created_at updated_at latest_version_id
11
+ ].freeze
12
+
13
+ def initialize(url:, jwt_secret:)
14
+ @url = url
15
+ @jwt_secret = jwt_secret
16
+ @jwt_token = nil
17
+ @jwt_expiration = nil
18
+ end
19
+
20
+ def store_conversation(conversation)
21
+ response = client.post('api/ai_conversations', conversation.conversation_attributes_for_storage)
22
+ handle_response(response)
23
+ end
24
+
25
+ def fetch_prompt(name)
26
+ response = client.get("api/prompts/#{name}")
27
+ data = handle_response(response)
28
+ map_prompt_attributes(data)
29
+ end
30
+
31
+ private
32
+
33
+ def map_prompt_attributes(data)
34
+ PROMPT_ATTRIBUTES.each_with_object({}) do |attr_name, attrs|
35
+ attrs[attr_name.to_sym] = data[attr_name]
36
+ end
37
+ end
38
+
39
+ def client
40
+ @client ||= build_client
41
+ end
42
+
43
+ def build_client
44
+ Faraday.new(url: @url) do |faraday|
45
+ faraday.request :json
46
+ faraday.response :json
47
+ faraday.request :authorization, 'Bearer', -> { current_jwt }
48
+ faraday.adapter Faraday.default_adapter
49
+ end
50
+ end
51
+
52
+ def current_jwt
53
+ refresh_jwt if token_expired?
54
+ @jwt_token
55
+ end
56
+
57
+ def token_expired?
58
+ @jwt_token.nil? || @jwt_expiration.nil? || Time.now.to_i >= @jwt_expiration
59
+ end
60
+
61
+ def refresh_jwt
62
+ if @jwt_secret.respond_to?(:call)
63
+ refresh_callable_jwt
64
+ else
65
+ refresh_static_jwt
66
+ end
67
+ end
68
+
69
+ def refresh_callable_jwt
70
+ @jwt_token = @jwt_secret.call
71
+ decoded_token = JWT.decode(@jwt_token, nil, false).first
72
+ @jwt_expiration = decoded_token['exp']
73
+ end
74
+
75
+ def refresh_static_jwt
76
+ @jwt_expiration = Time.now.to_i + 3600 # 1 hour
77
+ payload = { exp: @jwt_expiration, iat: Time.now.to_i }
78
+ @jwt_token = JWT.encode(payload, @jwt_secret, 'HS256')
79
+ end
80
+
81
+ def handle_response(response)
82
+ case response.status
83
+ when 200, 201
84
+ response.body
85
+ else
86
+ raise Error, "API request failed: #{response.body}"
87
+ end
88
+ end
89
+ end
90
+ end
@@ -3,26 +3,16 @@
3
3
  module RubyConversations
4
4
  # Configuration options for RubyConversations
5
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
6
+ attr_accessor :api_url, :jwt_secret, :default_llm_model, :default_llm_provider
9
7
 
10
8
  def initialize
11
- @storage_mode = :local
12
9
  @default_llm_model = 'claude-3-7-sonnet'
13
10
  @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
11
  end
21
12
 
22
13
  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
14
+ raise ConfigurationError, 'api_url is required' unless api_url
15
+ raise ConfigurationError, 'jwt_secret is required' unless jwt_secret
26
16
  end
27
17
  end
28
18
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'strongmind/auth'
4
+
3
5
  module RubyConversations
4
6
  # Defines the Rails engine for RubyConversations.
5
7
  class Engine < ::Rails::Engine
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyConversations
4
+ # Base error class for the gem
5
+ class Error < StandardError; end
6
+
7
+ # Error raised when there is a configuration issue
8
+ class ConfigurationError < Error; end
9
+ end
@@ -3,7 +3,7 @@
3
3
  module RubyConversations
4
4
  MAJOR = 1
5
5
  MINOR = 0
6
- PATCH = 7
6
+ PATCH = 9
7
7
 
8
8
  VERSION = "#{RubyConversations::MAJOR}.#{RubyConversations::MINOR}.#{RubyConversations::PATCH}".freeze
9
9
  end
@@ -1,22 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rails'
4
- require 'active_record'
5
- require 'ruby_llm'
6
- require 'faraday'
7
- require 'jwt'
8
3
  require 'zeitwerk'
4
+ require 'ruby_conversations/version'
9
5
  require 'ruby_conversations/configuration'
6
+ require 'ruby_conversations/errors'
7
+ require 'ruby_conversations/client'
10
8
  require 'ruby_conversations/aws_credential_provider'
9
+ require 'ruby_llm'
11
10
 
12
- loader = Zeitwerk::Loader.for_gem
13
- loader.ignore("#{__dir__}/generators")
14
-
15
- # Main module for the RubyConversations engine.
11
+ # Main module for RubyConversations
16
12
  module RubyConversations
17
- class Error < StandardError; end
18
- class ConfigurationError < Error; end
19
-
20
13
  class << self
21
14
  attr_writer :configuration
22
15
 
@@ -26,50 +19,30 @@ module RubyConversations
26
19
 
27
20
  def configure
28
21
  yield(configuration)
29
- configuration.validate!
30
- configure_ruby_llm if configuration.default_llm_provider == 'bedrock'
31
22
  end
32
23
 
33
- def storage
34
- @storage ||= case configuration.storage_mode
35
- when :local
36
- build_local_storage
37
- when :remote
38
- build_remote_storage
39
- else
40
- raise ConfigurationError, "Unknown storage mode: #{configuration.storage_mode}"
41
- end
24
+ def client
25
+ @client ||= build_client
42
26
  end
43
27
 
44
28
  def reset!
45
- @configuration = Configuration.new
46
- @storage = nil
29
+ @configuration = nil
30
+ @client = nil
47
31
  end
48
32
 
49
33
  private
50
34
 
51
- def build_local_storage
52
- Storage::Local.new
53
- end
54
-
55
- def build_remote_storage
56
- Storage::Remote.new(
35
+ def build_client
36
+ configuration.validate!
37
+ Client.new(
57
38
  url: configuration.api_url,
58
39
  jwt_secret: configuration.jwt_secret
59
40
  )
60
41
  end
61
-
62
- def configure_ruby_llm
63
- credential_provider = AwsCredentialProvider.instance
64
-
65
- RubyLLM.configure do |config|
66
- config.bedrock_region = ENV.fetch('AWS_REGION', '')
67
- config.bedrock_api_key = credential_provider.access_key_id
68
- config.bedrock_secret_key = credential_provider.secret_access_key
69
- config.bedrock_session_token = credential_provider.session_token
70
- end
71
- end
72
42
  end
73
43
  end
74
44
 
45
+ # Set up Zeitwerk autoloading
46
+ loader = Zeitwerk::Loader.new
47
+ loader.push_dir("#{__dir__}/../app/models")
75
48
  loader.setup