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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 29d0a2bc2bf832a2bd4a452e1213d024604d73c3be3406851d2ac77dce20278d
|
4
|
+
data.tar.gz: 973a5a996c8146aa0651dfeb42cb95d27c9a4623c53b1b4f662cefb48f90bc88
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf8a698597f4a53c26a0850ee8afc0552d9280b5ea46ba05b890e5bebaa587e40d666e93349ec1667e1ac438c881b9a1afa79b7482cc970fedc49d7e89d49be5
|
7
|
+
data.tar.gz: c9b77726ec303b37ad0af8f8e16ad3979802f8d995c254dcd2713a4a60563e0cbd7865b4bb433ade118ff2a597c8cc896940004b0abc874b802c63f666d037ba
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# RubyConversations
|
2
|
+
|
3
|
+
A Rails engine for managing AI conversations with support for both local and remote storage modes. Built on top of RubyLLM for AI interactions.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Flexible Storage**: Choose between local database storage or remote API storage
|
8
|
+
- **Built-in Prompt Management**: Version-controlled prompts with placeholder validation
|
9
|
+
- **Conversation History**: Track and manage conversation threads
|
10
|
+
- **Input/Output Storage**: Structured storage for message inputs and responses
|
11
|
+
- **Real-time Updates**: Built-in broadcasting support for real-time applications
|
12
|
+
- **JWT Authentication**: Secure remote mode with JWT authentication
|
13
|
+
- **Easy Integration**: Simple setup with Rails generators
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'ruby_conversations'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
bundle install
|
27
|
+
```
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
gem install ruby_conversations
|
33
|
+
```
|
34
|
+
|
35
|
+
After installation, run the setup generator:
|
36
|
+
|
37
|
+
```bash
|
38
|
+
rails generate ai_conversation_engine:install
|
39
|
+
```
|
40
|
+
|
41
|
+
This will:
|
42
|
+
- Create necessary database migrations
|
43
|
+
- Generate an initializer file
|
44
|
+
- Set up default configurations
|
45
|
+
|
46
|
+
## Configuration
|
47
|
+
|
48
|
+
Configure the engine in `config/initializers/ai_conversation_engine.rb`:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
AiConversationEngine.configure do |config|
|
52
|
+
# Storage mode: :local or :remote
|
53
|
+
config.storage_mode = :local
|
54
|
+
|
55
|
+
# Remote API settings (only needed for remote mode)
|
56
|
+
# config.api_url = ENV['AI_CONVERSATION_API_URL']
|
57
|
+
# config.jwt_secret = ENV['AI_CONVERSATION_JWT_SECRET']
|
58
|
+
|
59
|
+
# Default LLM settings
|
60
|
+
config.default_llm_model = 'gpt-4'
|
61
|
+
|
62
|
+
# Customize model behaviors
|
63
|
+
config.conversation_class = 'AiConversation'
|
64
|
+
config.message_class = 'AiMessage'
|
65
|
+
config.prompt_class = 'Prompt'
|
66
|
+
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
## Usage
|
71
|
+
|
72
|
+
### Basic Conversation
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
# Create a new conversation
|
76
|
+
conversation = AiConversation.create!
|
77
|
+
|
78
|
+
# Ask a question using a predefined prompt
|
79
|
+
conversation.ask("explain_code", inputs: { code: "def hello; end" })
|
80
|
+
```
|
81
|
+
|
82
|
+
### With Associated Objects
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# Create a conversation linked to another object
|
86
|
+
class Project < ApplicationRecord
|
87
|
+
has_many :conversations, as: :conversationable,
|
88
|
+
class_name: 'AiConversationEngine::AiConversation'
|
89
|
+
end
|
90
|
+
|
91
|
+
project = Project.find(1)
|
92
|
+
conversation = project.conversations.create!
|
93
|
+
conversation.ask("analyze_project", inputs: { name: project.name })
|
94
|
+
```
|
95
|
+
|
96
|
+
### Remote Storage Mode
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# Configure for remote storage
|
100
|
+
AiConversationEngine.configure do |config|
|
101
|
+
config.storage_mode = :remote
|
102
|
+
config.api_url = "https://ai-conversation-api.example.com"
|
103
|
+
config.jwt_secret = ENV['JWT_SECRET']
|
104
|
+
end
|
105
|
+
|
106
|
+
# Usage remains the same
|
107
|
+
conversation = AiConversation.create!
|
108
|
+
conversation.ask("explain_code", inputs: { code: "def hello; end" })
|
109
|
+
```
|
110
|
+
|
111
|
+
### Real-time Updates
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# In your view
|
115
|
+
<%= turbo_stream_from "chat_#{@conversation.id}" %>
|
116
|
+
|
117
|
+
# Updates are automatically broadcast when messages are added
|
118
|
+
```
|
119
|
+
|
120
|
+
## Development
|
121
|
+
|
122
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
123
|
+
|
124
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
125
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Provides message-related functionalities for conversations.
|
5
|
+
module Messageable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_many :ai_conversations, as: :conversationable, dependent: :destroy
|
10
|
+
has_many :ai_messages, through: :ai_conversations
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Provides versioning functionalities for models.
|
5
|
+
module Versionable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_many :versions, class_name: 'PromptVersion', dependent: :destroy
|
10
|
+
has_one :latest_version, -> { order(version: :desc) }, class_name: 'PromptVersion'
|
11
|
+
|
12
|
+
delegate :content, :metadata, to: :latest_version, allow_nil: true
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_version!(content:, metadata: {})
|
16
|
+
next_version = versions.maximum(:version).to_i + 1
|
17
|
+
versions.create!(content: content, metadata: metadata, version: next_version)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# An AI conversation is a conversation between an AI and a user.
|
5
|
+
class AiConversation < ActiveRecord::Base
|
6
|
+
# Associations
|
7
|
+
belongs_to :conversationable, polymorphic: true, optional: true, touch: true
|
8
|
+
has_many :ai_messages, class_name: RubyConversations.configuration.message_class, dependent: :destroy
|
9
|
+
|
10
|
+
# Validations
|
11
|
+
validates :ai_messages, presence: { message: 'At least one message is required' }, on: :update
|
12
|
+
validate :validate_messages
|
13
|
+
|
14
|
+
# Scopes
|
15
|
+
scope :recent, -> { order(updated_at: :desc) }
|
16
|
+
scope :with_messages, -> { includes(:ai_messages) }
|
17
|
+
|
18
|
+
# Attributes
|
19
|
+
attr_accessor :model_identifier, :provider, :tool
|
20
|
+
|
21
|
+
def initialize(attributes = nil)
|
22
|
+
attributes ||= {}
|
23
|
+
super
|
24
|
+
@model_identifier ||= attributes[:model_identifier] || RubyConversations.configuration.default_llm_model
|
25
|
+
@provider ||= attributes[:provider] || RubyConversations.configuration.default_llm_provider
|
26
|
+
end
|
27
|
+
|
28
|
+
# Instance Methods
|
29
|
+
def ask_multiple(prompt_inputs, description: nil)
|
30
|
+
MessageBuilder.new(self).build_from_multiple_prompts(prompt_inputs, description: description)
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def ask(name, description: nil, inputs: {})
|
35
|
+
MessageBuilder.new(self).build_from_single_prompt(name, description: description, inputs: inputs)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute(system_message: nil)
|
40
|
+
save!
|
41
|
+
message = ai_messages.last
|
42
|
+
raise ArgumentError, 'Conversation must have at least one message to execute' unless message
|
43
|
+
|
44
|
+
chat = setup_llm_chat(system_message: system_message)
|
45
|
+
response = chat.ask(message.request)
|
46
|
+
update_last_message_response(message, response)
|
47
|
+
response
|
48
|
+
end
|
49
|
+
|
50
|
+
def latest_message
|
51
|
+
ai_messages.order(created_at: :desc).first
|
52
|
+
end
|
53
|
+
|
54
|
+
def message_count
|
55
|
+
ai_messages.count
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def validate_messages
|
61
|
+
ai_messages.each do |message|
|
62
|
+
validate_message_level(message)
|
63
|
+
message.ai_message_prompts.each do |prompt|
|
64
|
+
validate_prompt_level(prompt)
|
65
|
+
prompt.ai_message_inputs.each do |input|
|
66
|
+
validate_input_level(input)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_message_level(message)
|
73
|
+
return if message.valid?
|
74
|
+
|
75
|
+
message.errors.full_messages.each do |msg|
|
76
|
+
errors.add(:base, "Message error: #{msg}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_prompt_level(prompt)
|
81
|
+
return if prompt.valid?
|
82
|
+
|
83
|
+
prompt.errors.full_messages.each do |msg|
|
84
|
+
errors.add(:base, "Message prompt error: #{msg}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_input_level(input)
|
89
|
+
return if input.valid?
|
90
|
+
|
91
|
+
input.errors.full_messages.each do |msg|
|
92
|
+
errors.add(:base, "Message prompt input error: #{msg}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_llm_chat(system_message: nil)
|
97
|
+
chat = RubyLLM.chat(model: model_identifier, provider: provider).with_temperature(0.0)
|
98
|
+
chat.with_tool(tool) if tool.present?
|
99
|
+
chat.add_message(role: :system, content: system_message) if system_message.present?
|
100
|
+
chat
|
101
|
+
end
|
102
|
+
|
103
|
+
def update_last_message_response(message, response)
|
104
|
+
message.update!(response: response.to_h.to_json)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# An AI message is a message in an AI conversation.
|
5
|
+
class AiMessage < ActiveRecord::Base
|
6
|
+
# Associations
|
7
|
+
belongs_to :ai_conversation, class_name: RubyConversations.configuration.conversation_class
|
8
|
+
has_many :ai_message_prompts, class_name: RubyConversations.configuration.message_prompt_class, dependent: :destroy
|
9
|
+
has_many :prompts, through: :ai_message_prompts, class_name: RubyConversations.configuration.prompt_class
|
10
|
+
|
11
|
+
# Validations
|
12
|
+
validates :llm, presence: true
|
13
|
+
validates :request, presence: true
|
14
|
+
|
15
|
+
# Scopes
|
16
|
+
scope :ordered, -> { order(created_at: :desc) }
|
17
|
+
scope :with_tool, ->(tool) { where(tool: tool) }
|
18
|
+
scope :with_prompts, -> { includes(:ai_message_prompts) }
|
19
|
+
|
20
|
+
def prompt_inputs
|
21
|
+
ai_message_prompts.includes(:ai_message_inputs).flat_map(&:ai_message_inputs)
|
22
|
+
end
|
23
|
+
|
24
|
+
def tool?
|
25
|
+
tool.present?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Represents an input provided for a specific prompt placeholder within an AI message.
|
5
|
+
class AiMessageInput < ActiveRecord::Base
|
6
|
+
# Associations
|
7
|
+
belongs_to :ai_message_prompt
|
8
|
+
|
9
|
+
# Validations
|
10
|
+
validates :placeholder_name, presence: true
|
11
|
+
validate :value_not_nil
|
12
|
+
|
13
|
+
# Scopes
|
14
|
+
scope :ordered, -> { order(:placeholder_name) }
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def value_not_nil
|
19
|
+
errors.add(:value, "can't be blank") if value.nil?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Represents the association between an AI message and a specific version of a Prompt.
|
5
|
+
class AiMessagePrompt < ActiveRecord::Base
|
6
|
+
# Associations
|
7
|
+
belongs_to :ai_message
|
8
|
+
belongs_to :prompt, optional: true
|
9
|
+
belongs_to :prompt_version, optional: true
|
10
|
+
has_many :ai_message_inputs, class_name: RubyConversations.configuration.message_input_class, dependent: :destroy
|
11
|
+
|
12
|
+
# Validations
|
13
|
+
validate :prompt_or_inline_data_present
|
14
|
+
|
15
|
+
# Scopes
|
16
|
+
scope :with_inputs, -> { includes(:ai_message_inputs) }
|
17
|
+
scope :for_prompt, ->(prompt) { where(prompt: prompt) }
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def prompt_or_inline_data_present
|
22
|
+
return if prompt_id.present? || prompt_version_id.present?
|
23
|
+
return if name.present? && role.present? && draft.present?
|
24
|
+
|
25
|
+
errors.add(:base, 'Must have either a prompt/version reference or inline prompt data (name, role, draft)')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Builds AiMessage objects with their prompts and inputs based on Prompt templates.
|
5
|
+
class MessageBuilder
|
6
|
+
def initialize(conversation)
|
7
|
+
@conversation = conversation
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_from_single_prompt(name, description: nil, inputs: {})
|
11
|
+
prompt = Prompt.find_by!(name: name)
|
12
|
+
validate_inputs!(prompt, inputs)
|
13
|
+
|
14
|
+
interpolated_message = prompt.interpolate(inputs)
|
15
|
+
message = build_ai_message(interpolated_message, description)
|
16
|
+
build_prompt_association(message, prompt, inputs)
|
17
|
+
|
18
|
+
message # Return the built message
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_from_multiple_prompts(prompt_inputs, description: nil)
|
22
|
+
message = @conversation.ai_messages.build(
|
23
|
+
request: '',
|
24
|
+
change_description: description,
|
25
|
+
llm: @conversation.model_identifier,
|
26
|
+
tool: @conversation.tool
|
27
|
+
)
|
28
|
+
|
29
|
+
prompt_inputs.each do |prompt_name, inputs|
|
30
|
+
process_single_prompt_input(message, prompt_name, inputs)
|
31
|
+
end
|
32
|
+
|
33
|
+
message # Return the built message
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_single_prompt_input(message, prompt_name, inputs)
|
37
|
+
prompt = Prompt.find_by!(name: prompt_name)
|
38
|
+
validate_inputs!(prompt, inputs)
|
39
|
+
|
40
|
+
interpolated_message = prompt.interpolate(inputs)
|
41
|
+
message.request += interpolated_message
|
42
|
+
|
43
|
+
message_prompt = message.ai_message_prompts.build(
|
44
|
+
prompt_version_id: prompt.latest_version_id,
|
45
|
+
draft: prompt.message
|
46
|
+
)
|
47
|
+
build_inputs_for_prompt(message_prompt, inputs)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def validate_inputs!(prompt, inputs)
|
53
|
+
return unless prompt.valid_placeholders.present?
|
54
|
+
|
55
|
+
missing_inputs, extra_inputs = calculate_input_discrepancies(prompt, inputs)
|
56
|
+
|
57
|
+
errors = []
|
58
|
+
errors << "Missing required inputs: #{missing_inputs.join(', ')}" if missing_inputs.any?
|
59
|
+
errors << "Unknown inputs provided: #{extra_inputs.join(', ')}" if extra_inputs.any?
|
60
|
+
|
61
|
+
raise ArgumentError, errors.join("\n") if errors.any?
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_prompt_association(message, prompt, inputs)
|
65
|
+
message_prompt = message.ai_message_prompts.build(
|
66
|
+
prompt: prompt,
|
67
|
+
prompt_version_id: prompt.latest_version_id,
|
68
|
+
draft: prompt.message
|
69
|
+
)
|
70
|
+
build_inputs_for_prompt(message_prompt, inputs)
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_inputs_for_prompt(message_prompt, inputs)
|
74
|
+
inputs.each do |input_name, value|
|
75
|
+
message_prompt.ai_message_inputs.build(
|
76
|
+
placeholder_name: input_name.to_s,
|
77
|
+
value: value.to_s
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Calculates missing and extra inputs compared to prompt placeholders
|
83
|
+
def calculate_input_discrepancies(prompt, inputs)
|
84
|
+
required_placeholders = prompt.valid_placeholders.split(',').map(&:strip)
|
85
|
+
provided_keys = inputs.keys.map(&:to_s)
|
86
|
+
missing_inputs = required_placeholders - provided_keys
|
87
|
+
extra_inputs = provided_keys - required_placeholders
|
88
|
+
[missing_inputs, extra_inputs]
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_ai_message(interpolated_message, description)
|
92
|
+
@conversation.ai_messages.build(
|
93
|
+
request: interpolated_message,
|
94
|
+
change_description: description,
|
95
|
+
llm: @conversation.model_identifier,
|
96
|
+
tool: @conversation.tool
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
7
|
+
|
8
|
+
# Constants
|
9
|
+
VALID_ROLES = %w[system user assistant].freeze
|
10
|
+
|
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
|
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
|
17
|
+
|
18
|
+
# Validations
|
19
|
+
validates :message, presence: true
|
20
|
+
validates :name, presence: true, uniqueness: true
|
21
|
+
validates :temperature, numericality: { greater_than_or_equal_to: 0.0 }, allow_nil: true
|
22
|
+
validates :role, presence: true, inclusion: { in: VALID_ROLES }
|
23
|
+
validate :validate_placeholders_format
|
24
|
+
|
25
|
+
# Scopes
|
26
|
+
scope :active, -> { where(active: true) }
|
27
|
+
scope :by_role, ->(role) { where(role: role) }
|
28
|
+
scope :with_versions, -> { includes(:versions) }
|
29
|
+
scope :ordered_by_name, -> { order(:name) }
|
30
|
+
scope :internal, -> { where(internal: true) }
|
31
|
+
scope :external, -> { where(internal: false) }
|
32
|
+
|
33
|
+
# Class methods
|
34
|
+
def self.find_by_name!(name)
|
35
|
+
find_by!(name: name.to_s.strip)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.roles
|
39
|
+
VALID_ROLES
|
40
|
+
end
|
41
|
+
|
42
|
+
# Instance methods
|
43
|
+
def latest_version_id
|
44
|
+
populate_initial_version!
|
45
|
+
versions.last&.id
|
46
|
+
end
|
47
|
+
|
48
|
+
def populate_initial_version!
|
49
|
+
versions.create!(message:, temperature:, created_at:, updated_at:) if versions.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def interpolate(inputs = {})
|
53
|
+
return message if placeholders.empty?
|
54
|
+
|
55
|
+
validate_required_variables!(inputs)
|
56
|
+
|
57
|
+
interpolated = message.dup
|
58
|
+
format(interpolated, **inputs)
|
59
|
+
end
|
60
|
+
|
61
|
+
def placeholders
|
62
|
+
return [] if message.nil?
|
63
|
+
|
64
|
+
matches = message.scan(/%<([^>]+)>/)
|
65
|
+
matches&.flatten&.uniq || []
|
66
|
+
end
|
67
|
+
|
68
|
+
def active?
|
69
|
+
active
|
70
|
+
end
|
71
|
+
|
72
|
+
def deactivate!
|
73
|
+
update!(active: false)
|
74
|
+
end
|
75
|
+
|
76
|
+
def activate!
|
77
|
+
update!(active: true)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def versioned_attributes
|
83
|
+
%i[content role]
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate_placeholders_format
|
87
|
+
invalid_placeholders = placeholders.grep_v(/\A[a-z_][a-z0-9_]*\z/i)
|
88
|
+
|
89
|
+
return unless invalid_placeholders.any?
|
90
|
+
|
91
|
+
errors.add(:message, "contains invalid placeholders: #{invalid_placeholders.join(', ')}")
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_required_variables!(variables)
|
95
|
+
missing = placeholders - variables.keys.map(&:to_s)
|
96
|
+
raise ArgumentError, "Missing required variables: #{missing.join(', ')}" if missing.any?
|
97
|
+
|
98
|
+
extra = variables.keys.map(&:to_s) - placeholders
|
99
|
+
return unless extra.any?
|
100
|
+
|
101
|
+
raise ArgumentError, "Unknown variables provided: #{extra.join(', ')}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyConversations
|
4
|
+
# Represents a specific version of a Prompt template.
|
5
|
+
class PromptVersion < ActiveRecord::Base
|
6
|
+
# Associations
|
7
|
+
belongs_to :prompt
|
8
|
+
|
9
|
+
# Validations
|
10
|
+
validates :message, presence: true
|
11
|
+
validates :temperature, numericality: { greater_than_or_equal_to: 0.0 }, allow_nil: true
|
12
|
+
|
13
|
+
# Scopes
|
14
|
+
scope :ordered, -> { order(created_at: :desc) }
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/migration'
|
5
|
+
|
6
|
+
module RubyConversations
|
7
|
+
module Generators
|
8
|
+
# Generates the necessary files for installing the ruby_conversations engine.
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
10
|
+
include Rails::Generators::Migration
|
11
|
+
|
12
|
+
source_root File.expand_path('templates', __dir__)
|
13
|
+
|
14
|
+
def self.next_migration_number(path)
|
15
|
+
next_migration_number = current_migration_number(path) + 1
|
16
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_migrations
|
20
|
+
migration_template_details.each do |file, description|
|
21
|
+
migration_template(
|
22
|
+
"migrations/#{file}",
|
23
|
+
"db/migrate/#{file.sub('.erb', '')}",
|
24
|
+
migration_version: migration_version,
|
25
|
+
migration_description: description
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_initializer
|
31
|
+
template 'initializer.rb.erb', 'config/initializers/ruby_conversations.rb'
|
32
|
+
end
|
33
|
+
|
34
|
+
def mount_engine
|
35
|
+
route "mount RubyConversations::Engine => '/ai', as: 'ruby_conversations'"
|
36
|
+
end
|
37
|
+
|
38
|
+
def display_post_install_message
|
39
|
+
readme 'README.md'
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def migration_version
|
45
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
46
|
+
end
|
47
|
+
|
48
|
+
def migration_template_details
|
49
|
+
{
|
50
|
+
'create_prompts.rb.erb' => 'Create prompts table',
|
51
|
+
'create_prompt_versions.rb.erb' => 'Create prompt versions table',
|
52
|
+
'create_ai_conversations.rb.erb' => 'Create conversations table',
|
53
|
+
'create_ai_messages.rb.erb' => 'Create messages table',
|
54
|
+
'create_ai_message_prompts.rb.erb' => 'Create message prompts table',
|
55
|
+
'create_ai_message_inputs.rb.erb' => 'Create message inputs table'
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# RubyConversations Installation
|
2
|
+
|
3
|
+
The gem has been installed! Here are the next steps:
|
4
|
+
|
5
|
+
1. Run the migrations:
|
6
|
+
```bash
|
7
|
+
bin/rails db:migrate
|
8
|
+
```
|
9
|
+
|
10
|
+
2. Configure the gem in `config/initializers/ruby_conversations.rb`
|
11
|
+
- Set your storage mode (:local or :remote)
|
12
|
+
- If using remote mode, set your API URL and JWT secret
|
13
|
+
- Customize model names if needed
|
14
|
+
|
15
|
+
3. For remote mode, ensure your API endpoint is accessible and JWT authentication is configured.
|
16
|
+
|
17
|
+
4. Start using conversations in your models:
|
18
|
+
```ruby
|
19
|
+
class MyModel < ApplicationRecord
|
20
|
+
has_one :conversation, as: :conversationable, class_name: 'AiConversation'
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create a conversation
|
24
|
+
conversation = my_model.create_conversation!
|
25
|
+
|
26
|
+
# Ask a question using a prompt
|
27
|
+
conversation.ask("explain_code", inputs: { code: "def hello; end" })
|
28
|
+
```
|
29
|
+
|
30
|
+
5. The engine is mounted at `/ai`. You can change this in `config/routes.rb` if needed.
|
31
|
+
|
32
|
+
## What's Next?
|
33
|
+
|
34
|
+
- Check out the documentation at https://github.com/yourusername/ruby_conversations
|
35
|
+
- Review the example app at https://github.com/yourusername/ruby_conversations_example
|
36
|
+
- Join our Discord community at https://discord.gg/ruby_conversations
|
37
|
+
|
38
|
+
## Need Help?
|
39
|
+
|
40
|
+
- Open an issue on GitHub
|
41
|
+
- Check our FAQ at https://github.com/yourusername/ruby_conversations/wiki/FAQ
|
42
|
+
- Email support at support@example.com
|