Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create & manage LanguageModels and APIService (+add Llama 3) #389

Merged
merged 72 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
d2f16fc
Can view/edit/add language models
stephan-buckmaster May 29, 2024
c6ce6ea
Can use other OpenAI flavour services
stephan-buckmaster May 30, 2024
43a2194
Can administer API services
stephan-buckmaster May 30, 2024
5544f1c
Can associate API Service with assistants
stephan-buckmaster May 31, 2024
1a751a8
Can assign driver to API Services
stephan-buckmaster May 31, 2024
1d5eb1b
Can connect to OpenAI backend
stephan-buckmaster May 31, 2024
d7ad4f7
Fix bug, use language_model name, not assistant's name
stephan-buckmaster Jun 1, 2024
34ceb46
API Service is optional, can view API Service's access token when edi…
stephan-buckmaster Jun 2, 2024
e4ed8f2
rake test: all pass
stephan-buckmaster Jun 5, 2024
c6ea9a6
Undo changes to unrelated files
stephan-buckmaster Jun 5, 2024
d6cb747
Merge from upstream
stephan-buckmaster Jun 6, 2024
f3a9503
Recreate db/schema.rb
stephan-buckmaster Jun 10, 2024
46b6806
Review comments
stephan-buckmaster Jun 11, 2024
c9bab8d
Merge upstream
stephan-buckmaster Jun 11, 2024
8b51f04
Add hints to language model form fields
stephan-buckmaster Jun 11, 2024
b36f264
Display API services and language models in a table
stephan-buckmaster Jun 11, 2024
fa52677
Sidebar styling: API Services / Language Models links
stephan-buckmaster Jun 11, 2024
640a8f2
Styling of buttons for language models and API services
stephan-buckmaster Jun 11, 2024
9651eb6
Show language model's owner
stephan-buckmaster Jun 11, 2024
685dda6
Fix indentation
stephan-buckmaster Jun 11, 2024
2e4f6ac
Fix test failures
stephan-buckmaster Jun 12, 2024
8594d60
Merge from upstream
stephan-buckmaster Jun 12, 2024
7abc447
Add test for API Services model + controller (including updates to vi…
stephan-buckmaster Jun 12, 2024
752e115
Add test for Language Model controller (including updates to views/co…
stephan-buckmaster Jun 12, 2024
407f757
Expand LanguageModelTest
stephan-buckmaster Jun 13, 2024
23b901d
Extend soft-deletes across APIService/LanguageModel/Assistant
stephan-buckmaster Jun 13, 2024
d1fab25
Tests for APIService
stephan-buckmaster Jun 13, 2024
228dcb4
Move the TestClients out of the app directory, to test/support
stephan-buckmaster Jun 13, 2024
7e1512c
Fix lint/rubocop issues
stephan-buckmaster Jun 13, 2024
a13efbb
Merge branch 'main' into pr/stephan-buckmaster/389
krschacht Jun 13, 2024
ad1cd40
Move encrypts & normalizes to be near other attribute details
krschacht Jun 13, 2024
f6e9fb7
Reorder language_model methods
krschacht Jun 13, 2024
90f8491
Merge from upstream
stephan-buckmaster Jun 15, 2024
8061ccf
Review comments regarding db schema
stephan-buckmaster Jun 15, 2024
b8a3652
Review comments re. APIServices controller
stephan-buckmaster Jun 15, 2024
ccd8176
More review comments
stephan-buckmaster Jun 16, 2024
8c902d9
Review comment / assert_instance_of User
stephan-buckmaster Jun 16, 2024
f0190b0
Review comment / destroy vs delete
stephan-buckmaster Jun 16, 2024
8b9a7d9
Use singular for add_reference :language_models, :user in the db-migr…
stephan-buckmaster Jun 18, 2024
2d37035
Merge from upstream
stephan-buckmaster Jun 19, 2024
236eaa6
Another merge from upstream
stephan-buckmaster Jun 19, 2024
3c1b59b
Correct a bug re. access_token vs. token
stephan-buckmaster Jun 19, 2024
5c3ae61
Allow running migration 20240523080700_create_language_models.rb with…
stephan-buckmaster Jun 19, 2024
c215eec
db/schema.rb had memories table twice
stephan-buckmaster Jun 19, 2024
c6dad30
Move system language models to user level
stephan-buckmaster Jun 19, 2024
d6105c0
Fix / extend tests
stephan-buckmaster Jun 19, 2024
3594426
Fix lint/rubocop issue
stephan-buckmaster Jun 19, 2024
5e55b5d
Merge from upstream
stephan-buckmaster Jun 21, 2024
4fd47bd
Merge branch '341-manage-llms-2' into 341-manage-llms-3. More changes…
stephan-buckmaster Jun 22, 2024
c371ff9
Merge branch 'main' into pr/stephan-buckmaster/389
krschacht Jun 23, 2024
3ea3c9a
Merge branch 'main' into pr/stephan-buckmaster/425
krschacht Jun 23, 2024
55f971e
Update app/controllers/settings/language_models_controller.rb
krschacht Jun 23, 2024
1c0e70f
Merge branch 'pr/stephan-buckmaster/425' into pr/stephan-buckmaster/389
krschacht Jun 23, 2024
b7ccb75
wip
krschacht Jun 23, 2024
75ab8ff
migrations
krschacht Jun 23, 2024
fb2bb9f
Model tests all pass
krschacht Jun 23, 2024
4e19856
More model fixes
krschacht Jun 23, 2024
992160e
Get tests working for AI backends
krschacht Jun 24, 2024
4d625c1
Controller tests passing
krschacht Jun 24, 2024
885f17a
Claned up language model
krschacht Jun 24, 2024
c5e7d73
finish tweaking API services forms
krschacht Jun 24, 2024
d5a8882
Final pass changes
krschacht Jun 24, 2024
86c91f2
fix tests
krschacht Jun 24, 2024
a72c947
Fix test
krschacht Jun 24, 2024
0b673c2
correct merge misses
krschacht Jun 24, 2024
4859770
Anthropic and OpenAI are "official"
stephan-buckmaster Jun 24, 2024
361cb25
Make sure the sequence rails db:drop; rails db:create; rails db:migra…
stephan-buckmaster Jun 24, 2024
c24c8d4
flip logic of instruction link & add tests
krschacht Jun 24, 2024
042261e
correct test
krschacht Jun 24, 2024
685614d
Update the new-user pre-populate to include Groq
krschacht Jun 24, 2024
09dee26
Improve user experience for Groq & fix some bugs
krschacht Jun 24, 2024
a8a84da
Add Groq for all old users
krschacht Jun 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions app/controllers/settings/api_services_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
class Settings::APIServicesController < Settings::ApplicationController
before_action :set_api_service, only: [:edit, :update, :destroy]

def index
@api_services = Current.user.api_services.ordered
end

def edit
end

def new
@api_service = APIService.new
end

def create
@api_service = Current.user.api_services.new(api_service_params)

if @api_service.save
redirect_to settings_api_services_path, notice: "Saved", status: :see_other
else
render :new, status: :unprocessable_entity
end
end

def update
if @api_service.update(api_service_params)
redirect_to settings_api_services_path, notice: "Saved", status: :see_other
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@api_service.deleted!
redirect_to settings_api_services_path, notice: "Deleted", status: :see_other
end

private

def set_api_service
@api_service = Current.user.api_services.find_by(id: params[:id])
if @api_service.nil?
redirect_to settings_api_services_path, alert: "The API Service could not be found", status: :see_other
end
end

def api_service_params
params.require(:api_service).permit(:name, :url, :token, :driver)
end
end
9 changes: 8 additions & 1 deletion app/controllers/settings/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ def set_settings_menu
|assistant| [ assistant, edit_settings_assistant_path(assistant) ]
}.to_h.merge({
"New Assistant": new_settings_assistant_path
})
}),

language_models: {
"Language Models": settings_language_models_path,
},

api_services: {
"API Services": settings_api_services_path,
},
}
end
end
58 changes: 58 additions & 0 deletions app/controllers/settings/language_models_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
class Settings::LanguageModelsController < Settings::ApplicationController
before_action :set_users_language_model, only: [:edit, :update, :destroy]
before_action :set_system_language_model, only: [:show]

def index
@language_models = LanguageModel.for_user(Current.user).order(updated_at: :desc)
end

def edit
end

def show
end

def new
@language_model = LanguageModel.new
end

def create
@language_model = Current.user.language_models.new(language_model_params)

if @language_model.save
redirect_to settings_language_models_path, notice: "Saved", status: :see_other
else
render :new, status: :unprocessable_entity
end
end

def update
if @language_model.update(language_model_params)
redirect_to settings_language_models_path, notice: "Saved", status: :see_other
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@language_model.deleted!
redirect_to settings_language_models_path, notice: "Deleted", status: :see_other
end

private

def set_users_language_model
@language_model = Current.user.language_models.find_by(id: params[:id])
if @language_model.nil?
redirect_to settings_language_models_path, status: :see_other, alert: "The Language Model could not be found"
end
end

def set_system_language_model
@language_model = LanguageModel.where(user_id: nil).find_by(id: params[:id])
end

def language_model_params
params.require(:language_model).permit(:api_name, :name, :supports_images, :api_service_id)
end
end
2 changes: 1 addition & 1 deletion app/controllers/settings/people_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def update

def person_params
h = params.require(:person).permit(:email, personable_attributes: [
:id, :first_name, :last_name, :password, :openai_key, :anthropic_key, preferences: [:dark_mode],
:id, :first_name, :last_name, :password, preferences: [:dark_mode],
credentials_attributes: [ :id, :type, :password ]
]).to_h
format_and_strip_all_but_first_valid_credential(h)
Expand Down
3 changes: 3 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,7 @@ def viewport_tag(content)
tag.meta(name: 'viewport', content: content)
end

def n_a_if_blank(value, n_a = "Not Available")
value.blank? ? n_a : value.to_s
end
end
15 changes: 15 additions & 0 deletions app/helpers/settings/api_services_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Settings
module APIServicesHelper
def official?(model)
openai?(model) && anthropic?(model)
end

def openai?(api_service)
api_service.url == APIService::URL_OPEN_AI
end

def anthropic?(api_service)
api_service.url == APIService::URL_ANTHROPIC
end
end
end
7 changes: 7 additions & 0 deletions app/helpers/settings/language_models_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Settings
module LanguageModelsHelper
def display_boolean(value)
ActiveModel::Type::Boolean.new.cast(value) ? 'Yes' : 'No'
end
end
end
3 changes: 1 addition & 2 deletions app/jobs/autotitle_conversation_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ class ConversationNotReady < StandardError; end

def perform(conversation_id)
conversation = Conversation.find(conversation_id)
Current.user = conversation.user
return false if Current.user.preferred_openai_key.blank? # should we use anthropic key if that's all the user has?
return false if conversation.assistant.api_service.effective_token.blank? # should we use anthropic key if that's all the user has?

messages = conversation.messages.ordered.limit(4)
raise ConversationNotReady if messages.empty?
Expand Down
6 changes: 3 additions & 3 deletions app/jobs/get_next_ai_message_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@ def self.broadcast_updated_message(message, locals = {})
private

def set_openai_error
@message.content_text = "(You need to enter a valid API key for OpenAI to use GPT-3.5 or GPT-4. Click your Profile in the bottom " +
"left and then Settings. You will find OpenAI Key instructions.)"
@message.content_text = "(You need to enter a valid API key for OpenAI to use GPT. Click your Profile in the bottom " +
"left and then Settings and then **API Services**. You will find OpenAI Key instructions.)"
end

def set_anthropic_error
@message.content_text = "(You need to enter a valid API key for Anthropic to use Claude. Click your Profile in the bottom " +
"left and then Settings. You will find Anthropic Key instructions.)"
"left and then Settings and then **API Services**. You will find Anthropic Key instructions.)"
end

def set_response_error
Expand Down
44 changes: 44 additions & 0 deletions app/models/api_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class APIService < ApplicationRecord
URL_OPEN_AI = "https://api.openai.com/"
URL_ANTHROPIC = "https://api.anthropic.com/"

belongs_to :user

has_many :language_models, -> { not_deleted }

enum driver: %w[ openai anthropic ].index_by(&:to_sym)

validates :url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]), if: -> { url.present? }
validates :name, :url, presence: true

normalizes :url, with: -> url { url.strip }
encrypts :token

before_save :soft_delete_language_models, if: -> { deleted_at && deleted_at_changed? && deleted_at_was.nil? }

scope :ordered, -> { order(:name) }

def ai_backend
openai? ? AIBackend::OpenAI : AIBackend::Anthropic
end

def requires_token?
[URL_OPEN_AI, URL_ANTHROPIC].include?(url) # other services may require it but we don't always know
end

def effective_token
token.presence || default_llm_key
end

private

def default_llm_key
return nil unless Feature.default_llm_keys?
return Setting.default_openai_key if url == URL_OPEN_AI
return Setting.default_anthropic_key if url == URL_ANTHROPIC
end

def soft_delete_language_models
language_models.each { |language_model| language_model.deleted! }
end
end
5 changes: 5 additions & 0 deletions app/models/assistant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Assistant < ApplicationRecord
has_many :messages, dependent: :destroy

delegate :supports_images?, to: :language_model
delegate :api_service, to: :language_model

belongs_to :language_model

Expand All @@ -18,6 +19,10 @@ class Assistant < ApplicationRecord

scope :ordered, -> { order(:id) }

def delete!
update!(deleted_at: Time.now)
end

def initials
return nil if name.blank?

Expand Down
3 changes: 2 additions & 1 deletion app/models/credential.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
class Credential < ApplicationRecord
belongs_to :user
encrypts :external_id, :oauth_email, :oauth_token, :oauth_refresh_token, deterministic: true

has_many :authentications, -> { not_deleted }
has_many :authentications_including_deleted, class_name: "Authentication", inverse_of: :credential, dependent: :destroy

encrypts :external_id, :oauth_email, :oauth_token, :oauth_refresh_token, deterministic: true

serialize :properties, coder: JsonSerializer
end
44 changes: 31 additions & 13 deletions app/models/language_model.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
# We don"t care about large or not
class LanguageModel < ApplicationRecord
BEST_GPT = "gpt-best"
BEST_CLAUDE = "claude-best"

BEST_MODELS = {
"gpt-best" => "gpt-4o-2024-05-13",
"claude-best" => "claude-3-5-sonnet-20240620"
BEST_GPT => "gpt-4o-2024-05-13",
BEST_CLAUDE => "claude-3-5-sonnet-20240620"
}

belongs_to :user
belongs_to :api_service

has_many :assistants, -> { not_deleted }
has_many :assistants_including_deleted, class_name: "Assistant", dependent: :destroy

before_validation :populate_position, unless: :position

validates :api_name, :name, :position, presence: true

before_save :soft_delete_assistants, if: -> { deleted_at && deleted_at_changed? && deleted_at_was.nil? }

scope :ordered, -> { order(:position) }
scope :for_user, ->(user) { where(user_id: user.id).not_deleted }

has_many :assistants
delegate :ai_backend, to: :api_service

def readonly?
!new_record?
def provider_name
BEST_MODELS[api_name] || api_name
end

def provider_name
BEST_MODELS[name] || name
def created_by_current_user?
user == Current.user
end

private

def populate_position
self.position = (user&.language_models&.maximum(:position) || 0) + 1
end

def ai_backend
if name.starts_with?("gpt-")
AIBackend::OpenAI
else
AIBackend::Anthropic
end
def soft_delete_assistants
assistants.update_all(deleted_at: Time.current)
end
end
4 changes: 2 additions & 2 deletions app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ class Person < ApplicationRecord
delegated_type :personable, types: %w[ User Tombstone ], dependent: :destroy
has_many :clients, dependent: :destroy

encrypts :email, deterministic: true

accepts_nested_attributes_for :personable

validate :personable_id_unchanged, on: :update
validates_associated :personable
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validate :proper_personable_id, on: :update

encrypts :email, deterministic: true

scope :ordered, -> { order(:created_at) }

normalizes :email, with: -> email { email.downcase.strip }
Expand Down
2 changes: 1 addition & 1 deletion app/models/run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ class Run < ApplicationRecord
private

def set_model
self.model = assistant&.language_model&.name
self.model = assistant&.language_model&.provider_name
end
end
19 changes: 7 additions & 12 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
class User < ApplicationRecord
include Personable, Registerable
encrypts :openai_key, :anthropic_key

has_secure_password validations: false
has_person_name

validates :first_name, presence: true
validates :last_name, presence: true, on: :create

has_many :assistants, -> { not_deleted }
has_many :assistants_including_deleted, class_name: "Assistant", inverse_of: :user, dependent: :destroy
has_many :language_models, -> { not_deleted }
has_many :language_models_including_deleted, class_name: "LanguageModel", dependent: :destroy
has_many :api_services, -> { not_deleted }
has_many :api_services_including_deleted, class_name: "APIService", dependent: :destroy
has_many :conversations, dependent: :destroy
has_many :credentials, dependent: :destroy
has_many :memories, dependent: :destroy
Expand All @@ -22,18 +22,13 @@ class User < ApplicationRecord

belongs_to :last_cancelled_message, class_name: "Message", optional: true

validates :first_name, presence: true
validates :last_name, presence: true, on: :create

accepts_nested_attributes_for :credentials
serialize :preferences, coder: JsonSerializer

def preferences
attributes["preferences"].with_defaults(dark_mode: "system")
end

def preferred_openai_key
self.openai_key.presence || (Feature.default_llm_keys? ? Setting.default_openai_key : nil)
end

def preferred_anthropic_key
self.anthropic_key.presence || (Feature.default_llm_keys? ? Setting.default_anthropic_key : nil)
end
end
Loading
Loading