diff --git a/app/views/settings/assistants/_form.html.erb b/app/views/settings/assistants/_form.html.erb
index 9455e26d6..1f5c977fe 100644
--- a/app/views/settings/assistants/_form.html.erb
+++ b/app/views/settings/assistants/_form.html.erb
@@ -13,7 +13,31 @@
<%= form.label :name %>
- <%= form.text_field :name, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full dark:text-black" %>
+ <%= form.text_field :name,
+ autofocus: assistant.new_record?,
+ class: %|
+ block w-full
+ border border-gray-200 outline-none
+ shadow rounded-md
+ px-3 py-2 mt-2
+ dark:text-black
+ | %>
+
+
+
+ <%= form.label :language_model_id %>
+ <%= form.select :language_model_id, + LanguageModel.ordered.pluck(:description, :id), + {}, + class: %| + block + border border-gray-200 outline-none + shadow rounded-md + px-3 py-2 mt-2 + w-full + dark:text-black + | + %>
+ <%= form.select :language_model_id, + LanguageModel.ordered.pluck(:description, :id), + {}, + class: %| + block + border border-gray-200 outline-none + shadow rounded-md + px-3 py-2 mt-2 + w-full + dark:text-black + | + %>
@@ -22,9 +46,17 @@
- <%= form.label :instructions %>
+ <%= form.label :instructions, "Custom Instructions" %>
<%= form.text_area :instructions,
- class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full dark:text-black h-18",
+ autofocus: assistant.persisted?,
+ class: %|
+ block w-full
+ border border-gray-200 outline-none
+ shadow rounded-md
+ px-3 py-2 mt-2
+ dark:text-black
+ h-18
+ |,
data: {
controller: "textarea-autogrow",
action: %|
diff --git a/app/views/settings/assistants/edit.html.erb b/app/views/settings/assistants/edit.html.erb
index 3ee8e807e..ba76627b8 100644
--- a/app/views/settings/assistants/edit.html.erb
+++ b/app/views/settings/assistants/edit.html.erb
@@ -3,21 +3,20 @@
<%= render "form", assistant: @assistant %>
- <%# TODO: Messages are connected to assistants. When an assistant is deleted,
- what should happen to the messages?
-
- button_to "Delete",
- settings_assistant_path(@assistant),
- method: :delete,
- data: { turbo_confirm: "Are you sure?" },
- form: { class: "inline-block" },
- class: %|
- ml-5 mt-2 py-3 px-5
- bg-white dark:bg-gray-500
- border border-gray-300 dark:border-gray-500
- rounded-lg
- font-medium
- |
+ <%= button_to "Delete",
+ settings_assistant_path(@assistant),
+ method: :delete,
+ data: { turbo_confirm: "Are you sure?" },
+ form: { class: "inline-block" },
+ class: %|
+ ml-5 py-3 px-5
+ bg-white dark:bg-gray-500
+ border border-gray-300 dark:border-gray-500
+ rounded-lg
+ font-medium
+ #{@last_assistant && 'hidden'}
+ |
%>
+
<%= link_to "Cancel", root_path, class: "float-right inline-block ml-5 py-3" %>
diff --git a/app/views/settings/assistants/new.html.erb b/app/views/settings/assistants/new.html.erb
index 4122ed1f9..7a49677b0 100644
--- a/app/views/settings/assistants/new.html.erb
+++ b/app/views/settings/assistants/new.html.erb
@@ -1,5 +1,5 @@
-
diff --git a/config/database.yml b/config/database.yml
index 03998056f..4abe0cd3f 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -1,17 +1,3 @@
-# PostgreSQL. Versions 9.3 and up are supported.
-#
-# Install the pg driver:
-# gem install pg
-# On macOS with Homebrew:
-# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
-# On Windows:
-# gem install pg
-# Choose the win32 build.
-# Install PostgreSQL and put its /bin directory on your path.
-#
-# Configure Using Gemfile
-# gem "pg"
-#
default: &default
adapter: postgresql
encoding: unicode
@@ -23,64 +9,14 @@ default: &default
development:
<<: *default
- database: hostedgpt_development
+ database: <%= ENV['HOSTEDGPT_DEV_DB'] || "hostedgpt_development" %>
- # The specified database role being used to connect to PostgreSQL.
- # To create additional roles in PostgreSQL see `$ createuser --help`.
- # When left blank, PostgreSQL will use the default role. This is
- # the same name as the operating system user running Rails.
- #username: hostedgpt
-
- # The password associated with the PostgreSQL role (username).
- #password:
-
- # Connect on a TCP socket. Omitted by default since the client uses a
- # domain socket that doesn't need configuration. Windows does not have
- # domain sockets, so uncomment these lines.
- #host: localhost
-
- # The TCP port the server listens on. Defaults to 5432.
- # If your server runs on a different port number, change accordingly.
- #port: 5432
-
- # Schema search path. The server defaults to $user,public
- #schema_search_path: myapp,sharedapp,public
-
- # Minimum log levels, in increasing order:
- # debug5, debug4, debug3, debug2, debug1,
- # log, notice, warning, error, fatal, and panic
- # Defaults to warning.
- #min_messages: notice
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
test:
<<: *default
- database: hostedgpt_test
+ database: <%= ENV['HOSTEDGPT_TEST_DB'] || "hostedgpt_test" %>
-# As with config/credentials.yml, you never want to store sensitive information,
-# like your database password, in your source code. If your source code is
-# ever seen by anyone, they now have access to your database.
-#
-# Instead, provide the password or a full connection URL as an environment
-# variable when you boot the app. For example:
-#
-# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
-#
-# If the connection URL is provided in the special DATABASE_URL environment
-# variable, Rails will automatically merge its configuration values on top of
-# the values provided in this file. Alternatively, you can specify a connection
-# URL environment variable explicitly:
-#
-# production:
-# url: <%= ENV["MY_APP_DATABASE_URL"] %>
-#
-# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
-# for a full overview on how database connection configuration can be specified.
-#
production:
<<: *default
database: hostedgpt_production
username: hostedgpt
- password: <%= ENV["HOSTEDGPT_DATABASE_PASSWORD"] %>
+ password: <%= ENV["HOSTEDGPT_DATABASE_PASSWORD"] %>=
\ No newline at end of file
diff --git a/db/migrate/20240523080700_create_language_models.rb b/db/migrate/20240523080700_create_language_models.rb
new file mode 100644
index 000000000..59f1c899c
--- /dev/null
+++ b/db/migrate/20240523080700_create_language_models.rb
@@ -0,0 +1,76 @@
+class CreateLanguageModels < ActiveRecord::Migration[7.1]
+ def up
+ create_table :language_models do |t|
+ t.integer :position, null: false
+ t.string :name, null: false
+ t.text :description, null: false
+ t.boolean :supports_images, null: false
+
+ t.timestamps
+ end
+
+ # Initially supported models
+ [
+ [1, 'gpt-best', 'Best OpenAI Model', true],
+ [2, 'claude-best', 'Best Anthropic Model', true],
+
+ [3, 'gpt-4o', 'GPT-4o (latest)', true],
+ [4, 'gpt-4o-2024-05-13', 'GPT-4o Omni Multimodal (2024-05-13)', true],
+
+ [5, 'gpt-4-turbo', 'GPT-4 Turbo with Vision (latest)', true],
+ [6, 'gpt-4-turbo-2024-04-09', 'GPT-4 Turbo with Vision (2024-04-09)', true],
+ [7, 'gpt-4-turbo-preview', 'GPT-4 Turbo Preview', false],
+ [8, 'gpt-4-0125-preview', 'GPT-4 Turbo Preview (2024-01-25)', false],
+ [9, 'gpt-4-1106-preview', 'GPT-4 Turbo Preview (2023-11-06)', false],
+ [10, 'gpt-4-vision-preview', 'GPT-4 Turbo with Vision Preview (2023-11-06)', true],
+ [11, 'gpt-4-1106-vision-preview', 'GPT-4 Turbo with Vision Preview (2023-11-06)', true],
+
+ [12, 'gpt-4', 'GPT-4 (latest)', false],
+ [13, 'gpt-4-0613', 'GPT-4 Snapshot improved function calling (2023-06-13)', false],
+
+ [14, 'gpt-3.5-turbo', 'GPT-3.5 Turbo (latest)', false],
+ [15, 'gpt-3.5-turbo-16k-0613', 'GPT-3.5 Turbo (2022-06-13)', false],
+ [16, 'gpt-3.5-turbo-0125', 'GPT-3.5 Turbo (2022-01-25)', false],
+ [17, 'gpt-3.5-turbo-1106', 'GPT-3.5 Turbo (2022-11-06)', false],
+ [18, 'gpt-3.5-turbo-instruct', 'GPT-3.5 Turbo Instruct', false],
+
+ [19, 'claude-3-opus-20240229', 'Claude 3 Opus (2024-02-29)', true],
+ [20, 'claude-3-sonnet-20240229', 'Claude 3 Sonnet (2024-02-29)', true],
+ [21, 'claude-3-haiku-20240307', 'Claude 3 Haiku (2024-03-07)', true],
+ [22, 'claude-2.1', 'Claude 2.1', false],
+ [23, 'claude-2.0', 'Claude 2.0', false],
+ [24, 'claude-instant-1.2', 'Claude Instant 1.2', false]
+ ].each do |position, name, description, supports_images|
+ LanguageModel.create!(position: position, name: name, description: description, supports_images: supports_images)
+ end
+
+ max_position = 24
+
+ # Respect some users who may have added their own model values in the assistants table
+ (Assistant.all.pluck(:model).uniq - LanguageModel.all.pluck(:name)).each do |model_name|
+ Rails.logger.info "Create language_models record from assistants column value: #{model_name.inspect}. Setting supports_images to false, update manually if it has support"
+ LanguageModel.create!(name: model_name, description: model_name, position: max_position += 1, supports_images: false)
+ end
+
+ add_reference :assistants, :language_model, null: true, foreign_key: { to_table: :language_models}
+
+ Assistant.all.each do |a|
+ Rails.logger.info "Have assistant #{a.id} with model #{a.model}"
+ end
+ ActiveRecord::Base.connection.execute "update assistants a set language_model_id = (select id from language_models lm where lm.name = a.model)"
+
+ remove_column :assistants, :model
+ remove_column :assistants, :images
+ end
+
+ def down
+ add_column :assistants, :images, :boolean
+ add_column :assistants, :model, :string
+
+ ActiveRecord::Base.connection.execute "update assistants a set images = (select supports_images from language_models lm where lm.id=a.language_model_id)"
+ ActiveRecord::Base.connection.execute "update assistants a set model = (select name from language_models lm where lm.id=a.language_model_id)"
+
+ remove_column :assistants, :language_model_id
+ drop_table :language_models
+ end
+end
diff --git a/db/migrate/20240524144314_add_deleted_at_to_assistants.rb b/db/migrate/20240524144314_add_deleted_at_to_assistants.rb
new file mode 100644
index 000000000..b267c0eaa
--- /dev/null
+++ b/db/migrate/20240524144314_add_deleted_at_to_assistants.rb
@@ -0,0 +1,6 @@
+class AddDeletedAtToAssistants < ActiveRecord::Migration[7.1]
+ def change
+ add_column :assistants, :deleted_at, :timestamp, default: nil
+ add_index :assistants, [:user_id, :deleted_at]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c0ec3c009..8898daf97 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_05_22_231839) do
+ActiveRecord::Schema[7.1].define(version: 2024_05_24_144314) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -50,14 +50,16 @@
create_table "assistants", force: :cascade do |t|
t.bigint "user_id", null: false
- t.string "model"
t.string "name"
t.string "description"
t.string "instructions"
t.jsonb "tools", default: [], null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.boolean "images", default: false, null: false
+ t.datetime "deleted_at", precision: nil
+ t.bigint "language_model_id"
+ t.index ["language_model_id"], name: "index_assistants_on_language_model_id"
+ t.index ["user_id", "deleted_at"], name: "index_assistants_on_user_id_and_deleted_at"
t.index ["user_id"], name: "index_assistants_on_user_id"
end
@@ -96,6 +98,15 @@
t.index ["user_id"], name: "index_documents_on_user_id"
end
+ create_table "language_models", force: :cascade do |t|
+ t.integer "position", null: false
+ t.string "name", null: false
+ t.text "description", null: false
+ t.boolean "supports_images", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "messages", force: :cascade do |t|
t.bigint "conversation_id", null: false
t.string "role", null: false
@@ -105,10 +116,10 @@
t.bigint "content_document_id"
t.bigint "run_id"
t.bigint "assistant_id", null: false
+ t.datetime "cancelled_at"
t.datetime "processed_at", precision: nil
t.integer "index", null: false
t.integer "version", null: false
- t.datetime "cancelled_at"
t.boolean "branched", default: false, null: false
t.integer "branched_from_version"
t.jsonb "content_tool_calls"
@@ -304,6 +315,7 @@
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "assistants", "language_models"
add_foreign_key "assistants", "users"
add_foreign_key "chats", "users"
add_foreign_key "conversations", "assistants"
diff --git a/test/controllers/assistants_controller_test.rb b/test/controllers/assistants_controller_test.rb
index 2224e98f6..806461069 100644
--- a/test/controllers/assistants_controller_test.rb
+++ b/test/controllers/assistants_controller_test.rb
@@ -12,6 +12,8 @@ class AssistantsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to new_assistant_message_path(@assistant)
end
+ # TODO: Delete this duplicate functionality since it's been moved to settings.
+
test "should get new" do
get new_assistant_url
assert_response :success
@@ -19,7 +21,7 @@ class AssistantsControllerTest < ActionDispatch::IntegrationTest
test "should create assistant" do
assert_difference("Assistant.count") do
- post assistants_url, params: {assistant: {description: @assistant.description, instructions: @assistant.instructions, model: @assistant.model, name: @assistant.name, tools: @assistant.tools, user_id: @assistant.user_id}}
+ post assistants_url, params: {assistant: {description: @assistant.description, instructions: @assistant.instructions, language_model_id: @assistant.language_model_id, name: @assistant.name, tools: @assistant.tools, user_id: @assistant.user_id}}
end
assert_redirected_to assistant_url(Assistant.last)
@@ -36,18 +38,13 @@ class AssistantsControllerTest < ActionDispatch::IntegrationTest
end
test "should update assistant" do
- patch assistant_url(@assistant), params: {assistant: {description: @assistant.description, instructions: @assistant.instructions, model: @assistant.model, name: @assistant.name, tools: @assistant.tools, user_id: @assistant.user_id}}
+ patch assistant_url(@assistant), params: {assistant: {description: "new description", instructions: "new instructions", language_model_id: language_models(:claude_3_opus).id, name: 'new name', tools: @assistant.tools, user_id: @assistant.user_id}}
assert_redirected_to assistant_url(@assistant)
+ @assistant.reload
+ assert_equal "new description", @assistant.description
+ assert_equal "new instructions", @assistant.instructions
+ assert_equal "claude-3-opus-20240229", @assistant.language_model.name
+ assert_equal "new name", @assistant.name
end
- # TODO: Messages are connected to assistants. When an assistant is deleted,
- # what should happen to the messages?
- #
- # test "should destroy assistant" do
- # assert_difference("Assistant.count", -1) do
- # delete assistant_url(@assistant)
- # end
-
- # assert_redirected_to assistants_url
- # end
end
diff --git a/test/controllers/messages_controller_test.rb b/test/controllers/messages_controller_test.rb
index 37c0f7c22..6047d1900 100644
--- a/test/controllers/messages_controller_test.rb
+++ b/test/controllers/messages_controller_test.rb
@@ -1,6 +1,8 @@
require "test_helper"
class MessagesControllerTest < ActionDispatch::IntegrationTest
+ include ActionDispatch::TestProcess::FixtureFile
+
setup do
@message = messages(:hear_me)
@conversation = @message.conversation
@@ -79,6 +81,20 @@ class MessagesControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to conversation_messages_url(@conversation, version: 2)
end
+ test "should create message with image attachment" do
+ test_file = fixture_file_upload("cat.png", "image/png")
+ assert_difference "Conversation.count", 1 do
+ assert_difference "Document.count", 1 do
+ assert_difference "Message.count", 2 do
+ post assistant_messages_url(@assistant), params: { message: { documents_attributes: {"0": {file: test_file}}, content_text: @message.content_text } }
+ end
+ end
+ end
+
+ (user_msg, asst_msg) = Message.last(2)
+ assert_equal Document.last, user_msg.documents.first
+ end
+
test "should fail to create message when there is no content_text" do
post assistant_messages_url(@assistant), params: { message: { content_text: nil } }
assert_response :unprocessable_entity
@@ -112,4 +128,69 @@ class MessagesControllerTest < ActionDispatch::IntegrationTest
patch message_url(message, version: 2), params: { message: { id: message.id } }
assert_redirected_to conversation_messages_url(message.conversation, version: 2)
end
+
+ test "messages can still be viewed when attached to a soft-deleted assistant" do
+ assert @assistant.soft_delete
+ get conversation_messages_url(@conversation, version: 1)
+ assert @conversation.messages.count > 0
+ assert_select 'div[data-role="message"]', count: @conversation.messages.count
+ end
+
+ test "when assistant is not deleted the deleted-blurb is hidden but the composer is visible" do
+ get conversation_messages_url(@conversation, version: 1)
+ assert_response :success
+ assert_contains_text "main footer", "Samantha has been deleted and cannot assist any longer."
+ assert_select "div#composer"
+ assert_select "div#composer.hidden", false
+ end
+
+ test "when assistant supports images the image upload function is available" do
+ get conversation_messages_url(@conversation, version: 1)
+ assert_select "div#composer"
+ assert_select "div#composer.relationship", false
+ end
+
+ test "when assistant doesn't support images the image upload function is not available" do
+ get conversation_messages_url(conversations(:trees), version: 1)
+ assert_select "div#composer.relationship"
+ end
+
+ test "the composer is hidden when viewing a list of messages attached to an assistant that has been soft-deleted" do
+ @assistant.soft_delete
+ get conversation_messages_url(@conversation, version: 1)
+ assert_response :success
+ assert_contains_text "main footer", "Samantha has been deleted and cannot assist any longer."
+ assert_select "footer div.hidden p.text-center", false
+ assert_select "div#composer.hidden"
+ end
+
+ test "viewing messages in a conversation which has history but the assistant has been soft deleted, the conversation history can still be viewed" do
+ @assistant.soft_delete
+ message = messages(:message2_v1)
+
+ patch message_url(message, version: 2), params: { message: { id: message.id } }
+ assert_redirected_to conversation_messages_url(message.conversation, version: 2)
+
+ get conversation_messages_url(message.conversation, version: 2)
+ assert_response :success
+ assert_contains_text "main", "Where were you born"
+ end
+
+ test "when there are many assistants only a few are shown in the nav bar" do
+ 5.times do |x|
+ @user.assistants.create! name: "New assistant #{x+1}", language_model: LanguageModel.find_by(name: 'gpt-3.5-turbo')
+ end
+ get conversation_messages_url(@conversation, version: 1)
+ @user.assistants.each do |assistant|
+ assert_select %{div[data-radio-behavior-id-param="#{assistant.id}"] a[data-role="name"]}
+ end
+ @user.assistants.each_with_index do |assistant, index|
+ if index>5
+ assert_select %{div.hidden[data-role="assistant"][data-radio-behavior-id-param="#{assistant.id}"] a[data-role="name"]}
+ else
+ assert_select %{div[data-role="assistant"][data-radio-behavior-id-param="#{assistant.id}"] a[data-role="name"]}
+ assert_select %{div.hiden[data-role="assistant"][data-radio-behavior-id-param="#{assistant.id}"] a[data-role="name"]}, false
+ end
+ end
+ end
end
diff --git a/test/controllers/settings/assistants_controller_test.rb b/test/controllers/settings/assistants_controller_test.rb
index f5eb13e02..62b415399 100644
--- a/test/controllers/settings/assistants_controller_test.rb
+++ b/test/controllers/settings/assistants_controller_test.rb
@@ -3,7 +3,8 @@
class Settings::AssistantsControllerTest < ActionDispatch::IntegrationTest
setup do
@assistant = assistants(:samantha)
- login_as @assistant.user
+ @user = @assistant.user
+ login_as @user
end
test "should get new" do
@@ -12,7 +13,7 @@ class Settings::AssistantsControllerTest < ActionDispatch::IntegrationTest
end
test "should create assistant" do
- params = assistants(:samantha).slice(:name, :description, :instructions)
+ params = assistants(:samantha).slice(:name, :description, :instructions, :language_model_id)
assert_difference("Assistant.count") do
post settings_assistants_url, params: { assistant: params }
@@ -20,12 +21,41 @@ class Settings::AssistantsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to edit_settings_assistant_url(Assistant.last)
assert_nil flash[:error]
- assert_equal params, Assistant.last.slice(:name, :description, :instructions)
+ assert_equal params, Assistant.last.slice(:name, :description, :instructions, :language_model_id)
end
test "should get edit" do
get edit_settings_assistant_url(@assistant)
assert_response :success
+ assert_contains_text "div#nav-container", "Your Account"
+ assert_contains_text "div#nav-container", "New Assistant"
+ end
+
+ test "form should display a DELETE button if this is not your last assistant" do
+ assert @user.assistants.length > 1, "User needs to have more than one assistant"
+ get edit_settings_assistant_url(@assistant)
+ assert_response :success
+ assert_contains_text "main", "Delete"
+ end
+
+ test "form should NOT display a DELETE button if this is your last assistant" do
+ @user.assistants.where.not(id: @assistant.id).map(&:destroy)
+ assert @user.assistants.length == 1, "User needs to have more than one assistant"
+ get edit_settings_assistant_url(@user.assistants.first)
+ assert_response :success
+ assert_contains_text "main", "Delete"
+ end
+
+ test "should allow language_model selection" do
+ get edit_settings_assistant_url(@assistant)
+ assert_select 'label', 'Language model'
+ assert_select 'select#assistant_language_model_id'
+ end
+
+ test "should update language_model" do
+ params = {language_model_id: language_models(:claude_best).id}
+ patch settings_assistant_url(@assistant), params: { assistant: params }
+ assert_equal language_models(:claude_best), @assistant.reload.language_model
end
test "should update assistant" do
@@ -37,11 +67,28 @@ class Settings::AssistantsControllerTest < ActionDispatch::IntegrationTest
assert_equal params, @assistant.reload.slice(:name, :description, :instructions)
end
- test "should destroy assistant" do
- assert_difference("Assistant.count", -1) do
+ test "destroy should soft-delete assistant" do
+ assert_difference "Assistant.count", 0 do
+ delete settings_assistant_url(@assistant)
+ end
+
+ assert @assistant.reload.deleted?
+ assert_redirected_to new_settings_assistant_url
+ assert flash[:notice].present?, "There should have been a success message"
+ refute flash[:alert].present?, "There should NOT have been an error message"
+ end
+
+ test "destroy on the last assistant should not delete it" do
+ user = @assistant.user
+ user.assistants.where.not(id: @assistant.id).map(&:destroy)
+
+ assert_no_difference "Assistant.count" do
delete settings_assistant_url(@assistant)
end
+ refute @assistant.reload.deleted?
assert_redirected_to new_settings_assistant_url
+ refute flash[:notice].present?, "There should NOT have been a success message"
+ assert flash[:alert].present?, "There should have been an error message"
end
end
diff --git a/test/fixtures/assistants.yml b/test/fixtures/assistants.yml
index 3a0fc916c..c60ee050f 100644
--- a/test/fixtures/assistants.yml
+++ b/test/fixtures/assistants.yml
@@ -1,35 +1,39 @@
samantha:
user: keith
- model: gpt-4
+ language_model: gpt_4
name: Samantha
description: My personal assistant
instructions: You are a helpful assistant
tools: [{"type": "code_interpreter"}, {"type": "retrieval"}, {"type": "function"}]
- images: true
keith_gpt4:
user: keith
- model: gpt-4
+ language_model: gpt_4
name: OpenAI's GPT-4
description: OpenAI's GPT-4
instructions:
tools: [{"type": "code_interpreter"}, {"type": "retrieval"}, {"type": "function"}]
- images: true
keith_claude3:
user: keith
- model: claude-3-opus-20240229
+ language_model: claude_3_opus
name: Claude 3 Opus
description: Claude 3, Opus version
instructions:
tools: []
- images: true
+
+keith_gpt3:
+ user: keith
+ language_model: gpt_3_5_turbo
+ name: GPT 3.5
+ description: OpenAI's GPT-3 but with Zen
+ instructions: Include a Zen perspective when possilbe
+ tools: [{"type": "code_interpreter"}, {"type": "retrieval"}, {"type": "function"}]
rob_gpt4:
user: rob
- model: gpt-4
+ language_model: gpt_4
name: GPT-4
description: OpenAI's GPT-4
instructions:
tools: [{"type": "code_interpreter"}, {"type": "retrieval"}, {"type": "function"}]
- images: true
diff --git a/test/fixtures/conversations.yml b/test/fixtures/conversations.yml
index 65b7d2e7c..b0b77859c 100644
--- a/test/fixtures/conversations.yml
+++ b/test/fixtures/conversations.yml
@@ -75,3 +75,9 @@ weather:
assistant: samantha
title: Weather
last_assistant_message: weather_explained
+
+trees:
+ user: keith
+ assistant: keith_gpt3
+ title: Trees
+ last_assistant_message: trees_explained
diff --git a/test/fixtures/language_models.yml b/test/fixtures/language_models.yml
new file mode 100644
index 000000000..839d9f206
--- /dev/null
+++ b/test/fixtures/language_models.yml
@@ -0,0 +1,47 @@
+gpt_best:
+ position: 1
+ supports_images: true
+ name: gpt-best
+ description: gpt-best from fixtures
+
+claude_best:
+ position: 2
+ name: best-claude
+ supports_images: true
+ description: claude-best from fixtures
+
+gpt_4o:
+ position: 3
+ name: gpt-4o
+ supports_images: true
+ description: gpt-4ot from fixtures
+
+gpt_3_5_turbo:
+ position: 4
+ name: gpt-3.5-turbo
+ supports_images: false
+ description: gpt-3.5-turbo from fixtures
+
+claude_3_sonnet:
+ position: 18
+ name: claude-3-sonnet-20240229
+ supports_images: true
+ description: claude-3-sonnet-20240229 from fixtures
+
+claude_3_opus:
+ position: 17
+ supports_images: true
+ name: claude-3-opus-20240229
+ description: claude-3-opus-20240229 from fixtures
+
+gpt_4:
+ position: 3
+ supports_images: true
+ name: gpt-4
+ description: gpt-4 from fixtures
+
+gpt_3_5_turbo_0125:
+ position: 12
+ name: gpt-3.5-turbo
+ description: gpt-3.5-turbo from fixtures
+ supports_images: false
diff --git a/test/fixtures/messages.yml b/test/fixtures/messages.yml
index 97b850d58..3dda98f7c 100644
--- a/test/fixtures/messages.yml
+++ b/test/fixtures/messages.yml
@@ -658,3 +658,19 @@ weather_explained:
processed_at: 2023-12-30 1:03:00
index: 3
version: 1
+
+
+# Next conversation
+
+trees_explained:
+ assistant: keith_gpt3
+ conversation: trees
+ role: assistant
+ tool_call_id:
+ content_text: The trees in Australia are
+ content_tool_calls:
+ content_document:
+ created_at: 2024-05-25 1:03:00
+ processed_at: 2024-05-25 1:03:00
+ index: 0
+ version: 1
diff --git a/test/jobs/get_next_ai_message_job_anthropic_test.rb b/test/jobs/get_next_ai_message_job_anthropic_test.rb
index e1212ff35..469827728 100644
--- a/test/jobs/get_next_ai_message_job_anthropic_test.rb
+++ b/test/jobs/get_next_ai_message_job_anthropic_test.rb
@@ -14,7 +14,7 @@ class GetNextAIMessageJobAnthropicTest < ActiveJob::TestCase
assert GetNextAIMessageJob.perform_now(@user.id, @message.id, @conversation.assistant.id)
end
- message_text = @test_client.messages
+ message_text = @test_client.messages(model: "claude-3-opus-20240229")
assert_equal message_text, @conversation.latest_message_for_version(:latest).content_text
end
diff --git a/test/jobs/get_next_ai_message_job_openai_test.rb b/test/jobs/get_next_ai_message_job_openai_test.rb
index 7ea2c5b69..9bd2d205a 100644
--- a/test/jobs/get_next_ai_message_job_openai_test.rb
+++ b/test/jobs/get_next_ai_message_job_openai_test.rb
@@ -12,7 +12,7 @@ class GetNextAIMessageJobOpenaiTest < ActiveJob::TestCase
test "populates the latest message from the assistant" do
assert_no_difference "@conversation.messages.reload.length" do
TestClient::OpenAI.stub :text, "Hello" do
- TestClient::OpenAI.stub :api_response, TestClient::OpenAI.api_text_response do
+ TestClient::OpenAI.stub :api_response, -> { TestClient::OpenAI.api_text_response } do
assert GetNextAIMessageJob.perform_now(@user.id, @message.id, @conversation.assistant.id)
end
end
@@ -84,7 +84,7 @@ class GetNextAIMessageJobOpenaiTest < ActiveJob::TestCase
test "when API response key is missing, a nice error message is displayed" do
TestClient::OpenAI.stub :text, "" do
- TestClient::OpenAI.stub :api_response, TestClient::OpenAI.api_text_response do
+ TestClient::OpenAI.stub :api_response, -> { TestClient::OpenAI.api_text_response } do
assert GetNextAIMessageJob.perform_now(@user.id, @message.id, @conversation.assistant.id)
assert_includes @conversation.latest_message_for_version(:latest).content_text, "a blank response"
end
diff --git a/test/jobs/get_next_ai_message_job_test.rb b/test/jobs/get_next_ai_message_job_test.rb
index 6048edc4f..117c3306d 100644
--- a/test/jobs/get_next_ai_message_job_test.rb
+++ b/test/jobs/get_next_ai_message_job_test.rb
@@ -34,7 +34,7 @@ class GetNextAIMessageJobOpenaiTest < ActiveJob::TestCase
job = GetNextAIMessageJob.new
job.stub(:message_cancelled?, -> { false_on_first_run += 1; false_on_first_run != 1 }) do
TestClient::OpenAI.stub :text, "Hello" do
- TestClient::OpenAI.stub :api_response, TestClient::OpenAI.api_text_response do
+ TestClient::OpenAI.stub :api_response, -> { TestClient::OpenAI.api_text_response }do
assert_changes "@message.content_text", from: nil, to: "Hello" do
assert_changes "@message.reload.cancelled_at", from: nil do
diff --git a/test/models/assistant_test.rb b/test/models/assistant_test.rb
index 7bc113641..b018c4f3d 100644
--- a/test/models/assistant_test.rb
+++ b/test/models/assistant_test.rb
@@ -9,8 +9,9 @@ class AssistantTest < ActiveSupport::TestCase
assert_instance_of Conversation, assistants(:samantha).conversations.first
end
- test "has associated messages (through conversations)" do
- assert_instance_of Message, assistants(:samantha).messages.first
+ test "has supports_images?" do
+ assert assistants(:samantha).supports_images?
+ refute assistants(:keith_gpt3).supports_images?
end
test "has associated documents" do
@@ -25,33 +26,59 @@ class AssistantTest < ActiveSupport::TestCase
assert_instance_of Step, assistants(:samantha).steps.first
end
+ test "has associated messages (through conversations)" do
+ assert_instance_of Message, assistants(:samantha).messages.first
+ end
+
+ test "has associated language_model" do
+ assert_instance_of LanguageModel, assistants(:samantha).language_model
+ end
+
test "tools is an array of objects" do
assert_instance_of Array, assistants(:samantha).tools
end
- test "simple create works" do
+ test "simple create works and tool defaults to empty array" do
+ a = nil
assert_nothing_raised do
- Assistant.create!(user: users(:keith))
+ a = Assistant.create!(
+ user: users(:keith),
+ language_model: language_models(:gpt_4),
+ name: 'abc'
+ )
end
- end
-
- test "tools defaults to empty array on create" do
- a = Assistant.create!(user: users(:keith))
assert_equal [], a.tools
end
test "associations are deleted upon destroy" do
assistant = assistants(:samantha)
conversation_count = assistant.conversations.count * -1
+ message_count = assistant.conversations.map { |c| c.messages.length }.sum * -1
document_count = (assistant.documents.count+assistant.conversations.sum { |c| c.messages.sum { |m| m.documents.count }}) * -1
run_count = assistant.runs.count * -1
step_count = assistant.steps.count * -1
- assert_difference "Conversation.count", conversation_count do
- assert_difference "Document.count", document_count do
- assert_difference "Run.count", run_count do
- assert_difference "Step.count", step_count do
- assistant.destroy
+ assert_difference "Message.count", message_count do
+ assert_difference "Conversation.count", conversation_count do
+ assert_difference "Document.count", document_count do
+ assert_difference "Run.count", run_count do
+ assert_difference "Step.count", step_count do
+ assistant.destroy
+ end
+ end
+ end
+ end
+ end
+ end
+
+ test "associations are not deleted upon soft delete" do
+ assert_no_difference "Message.count" do
+ assert_no_difference "Conversation.count" do
+ assert_no_difference "Document.count" do
+ assert_no_difference "Run.count" do
+ assert_no_difference "Step.count" do
+ assistants(:samantha).soft_delete
+ end
end
end
end
@@ -70,4 +97,28 @@ class AssistantTest < ActiveSupport::TestCase
test "initials will split on - and return two characters" do
assert_equal "G4", assistants(:rob_gpt4).initials
end
+
+ test "language model validated" do
+ record = Assistant.new
+ refute record.valid?
+ assert record.errors[:language_model].present?
+ end
+
+ test "name validated" do
+ record = Assistant.new
+ refute record.valid?
+ assert record.errors[:name].present?
+ end
+
+ test "cannot soft_delete last assistant of a user" do
+ assert_raise do
+ users(:rob).assistants.first.soft_delete!
+ end
+ end
+
+ test "can soft_delete assistant of a user if they have more than one" do
+ assert_nothing_raised do
+ users(:keith).assistants.first.destroy
+ end
+ end
end
diff --git a/test/models/conversation_test.rb b/test/models/conversation_test.rb
index ce3549c66..71366312c 100644
--- a/test/models/conversation_test.rb
+++ b/test/models/conversation_test.rb
@@ -63,7 +63,7 @@ class ConversationTest < ActiveSupport::TestCase
perform_enqueued_jobs do
ChatCompletionAPI.stub :get_next_response, {"topic" => "Hear me"} do
TestClient::OpenAI.stub :text, "Hello" do
- TestClient::OpenAI.stub :api_response, TestClient::OpenAI.api_text_response do
+ TestClient::OpenAI.stub :api_response, -> { TestClient::OpenAI.api_text_response } do
conversation = users(:keith).conversations.create!(assistant: assistants(:samantha))
assert_nil conversation.title
diff --git a/test/models/language_model_test.rb b/test/models/language_model_test.rb
new file mode 100644
index 000000000..5dd5bb519
--- /dev/null
+++ b/test/models/language_model_test.rb
@@ -0,0 +1,42 @@
+require "test_helper"
+
+class LanguageModelTest < ActiveSupport::TestCase
+ test "has associated assistant" do
+ assert_instance_of Assistant, language_models(:gpt_4).assistants.first
+ end
+
+ test "is readonly?" do
+ assert language_models(:gpt_best).readonly?
+ assert language_models(:claude_3_sonnet).readonly?
+ end
+
+ test "supports_images?" do
+ assert language_models(:gpt_best).supports_images?
+ refute language_models(:gpt_3_5_turbo).supports_images?
+ end
+
+ test "ai_backend for best models" do
+ assert_equal AIBackend::OpenAI, language_models(:gpt_best).ai_backend
+ assert_equal AIBackend::Anthropic, language_models(:claude_best).ai_backend
+ end
+
+ test "ai_backend for Anthropic models" do
+ assert_equal AIBackend::Anthropic, language_models(:claude_3_sonnet).ai_backend
+ assert_equal AIBackend::Anthropic, language_models(:claude_3_opus).ai_backend
+ end
+
+ test "provider_name for Anthropic models" do
+ assert_equal "claude-3-sonnet-20240229", language_models(:claude_3_sonnet).provider_name
+ assert_equal "claude-3-opus-20240229", language_models(:claude_3_opus).provider_name
+ end
+
+ test "provider_name for OpenAI models" do
+ assert_equal "gpt-3.5-turbo", language_models(:gpt_3_5_turbo_0125).provider_name
+ assert_equal "gpt-4o", language_models(:gpt_4o).provider_name
+ end
+
+ test "ai_backend for OpenAI models" do
+ assert_equal AIBackend::OpenAI, language_models(:gpt_4o).ai_backend
+ assert_equal AIBackend::OpenAI, language_models(:gpt_3_5_turbo).ai_backend
+ end
+end
diff --git a/test/models/message_test.rb b/test/models/message_test.rb
index 1699f4394..a1780f1c0 100644
--- a/test/models/message_test.rb
+++ b/test/models/message_test.rb
@@ -1,6 +1,8 @@
require "test_helper"
class MessageTest < ActiveSupport::TestCase
+ include ActionDispatch::TestProcess::FixtureFile
+
test "has an associated assistant (it's through conversation)" do
assert_instance_of Assistant, messages(:hear_me).assistant
end
@@ -17,6 +19,15 @@ class MessageTest < ActiveSupport::TestCase
# assert_instance_of Document, messages(:examine_this).content_document
# end
+ test "handles documents_attributes" do
+ Current.user = users(:keith)
+ test_file = fixture_file_upload("cat.png", "image/png")
+ message = Message.create(assistant: assistants(:samantha), documents_attributes: {"0": {file: test_file}}, content_text: "Nice file")
+ assert_equal 1, message.documents.length
+ document = message.documents.first
+ assert_equal 'cat.png', document.filename
+ end
+
test "has an associated run" do
assert_instance_of Run, messages(:yes_i_do).run
end
diff --git a/test/models/run_test.rb b/test/models/run_test.rb
index 82486c226..96ba7a59c 100644
--- a/test/models/run_test.rb
+++ b/test/models/run_test.rb
@@ -22,7 +22,6 @@ class RunTest < ActiveSupport::TestCase
Run.create!(
assistant: assistants(:samantha),
conversation: conversations(:greeting),
- model: assistants(:samantha).model,
instructions: assistants(:samantha).instructions,
status: "queued",
expired_at: 1.minute.from_now
@@ -30,6 +29,17 @@ class RunTest < ActiveSupport::TestCase
end
end
+ test "model populated from assistant" do
+ r = Run.create!(
+ assistant: assistants(:keith_claude3),
+ conversation: conversations(:greeting),
+ instructions: "Some instructions",
+ status: "queued",
+ expired_at: 1.minute.from_now
+ )
+ assert_equal 'claude-3-opus-20240229', r.model
+ end
+
test "associations are deleted upon destroy" do
assert_difference "Step.count", -1 do
runs(:hear_me_response).destroy
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index 8e5a9e9d6..d490bf0ce 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -149,4 +149,13 @@ class UserTest < ActiveSupport::TestCase
new_user.update!(preferences: { dark_mode: "system" })
assert_equal "system", new_user.preferences[:dark_mode]
end
+
+ test "assistants scope filters out deleted vs assistants_including_deleted" do
+ assert_difference "users(:keith).assistants.length", -1 do
+ assert_no_difference "users(:keith).assistants_including_deleted.length" do
+ users(:keith).assistants.first.soft_delete
+ users(:keith).reload
+ end
+ end
+ end
end
diff --git a/test/services/ai_backend/anthropic_test.rb b/test/services/ai_backend/anthropic_test.rb
index d5a5440bf..21fe401b8 100644
--- a/test/services/ai_backend/anthropic_test.rb
+++ b/test/services/ai_backend/anthropic_test.rb
@@ -16,7 +16,7 @@ class AIBackend::AnthropicTest < ActiveSupport::TestCase
end
test "get_next_chat_message works" do
- assert_equal @test_client.messages, @anthropic.get_next_chat_message
+ assert_equal @test_client.messages(model: "gpt-4", system: "You are a helpful assistant"), @anthropic.get_next_chat_message
end
test "preceding_messages constructs a proper response and pivots on images" do
diff --git a/test/services/ai_backend/open_ai_test.rb b/test/services/ai_backend/open_ai_test.rb
index 3867fe9ab..c97e86f74 100644
--- a/test/services/ai_backend/open_ai_test.rb
+++ b/test/services/ai_backend/open_ai_test.rb
@@ -3,9 +3,10 @@
class AIBackend::OpenAITest < ActiveSupport::TestCase
setup do
@conversation = conversations(:attachments)
+ @assistant = assistants(:keith_claude3)
@openai = AIBackend::OpenAI.new(
users(:keith),
- assistants(:samantha),
+ @assistant,
@conversation,
@conversation.latest_message_for_version(:latest)
)
@@ -16,6 +17,19 @@ class AIBackend::OpenAITest < ActiveSupport::TestCase
assert @openai.client.present?
end
+ test "get_next_chat_message works to stream text and uses model from assistant" do
+ assert_not_equal @assistant, @conversation.assistant,
+ "We want to force this next message to use a different assistant so these should not match"
+
+ TestClient::OpenAI.stub :text, nil do # this forces it to fall back to default text
+ TestClient::OpenAI.stub :api_response, -> { TestClient::OpenAI.api_text_response }do
+ streamed_text = ""
+ @openai.get_next_chat_message { |chunk| streamed_text += chunk }
+ assert_equal "Hello this is model claude-3-opus-20240229 with instruction nil! How can I assist you today?", streamed_text
+ end
+ end
+ end
+
test "get_tool_messages_by_calling properly executes tools" do
tool_message = {
role: "tool",
@@ -47,18 +61,6 @@ class AIBackend::OpenAITest < ActiveSupport::TestCase
assert msg[:content].starts_with?('"An unexpected error occurred. You')
end
- test "get_next_chat_message works to stream text" do
- text = "Hello! How can I assist you today?"
-
- TestClient::OpenAI.stub :text, text do
- TestClient::OpenAI.stub :api_response, TestClient::OpenAI.api_text_response do
- streamed_text = ""
- @openai.get_next_chat_message { |chunk| streamed_text += chunk }
- assert_equal text, streamed_text
- end
- end
- end
-
test "get_next_chat_message works to get a function call" do
function = "openmeteo_get_current_and_todays_weather"
diff --git a/test/support/view_helpers.rb b/test/support/view_helpers.rb
new file mode 100644
index 000000000..f9ba24570
--- /dev/null
+++ b/test/support/view_helpers.rb
@@ -0,0 +1,10 @@
+module ViewHelpers
+
+ private
+
+ def assert_contains_text(selector, text)
+ assert_select selector, 1, "#{selector} was not found" do |element|
+ assert element.text.include?(text), "Element #{selector} did not contain '#{text}' (#{element.text.remove("\n")})"
+ end
+ end
+end
diff --git a/test/system/assistants_test.rb b/test/system/assistants_test.rb
index 2f4de479b..0639dc46f 100644
--- a/test/system/assistants_test.rb
+++ b/test/system/assistants_test.rb
@@ -11,32 +11,33 @@ class AssistantsTest < ApplicationSystemTestCase
# assert_selector "h1", text: "Assistants"
# end
- test "should create assistant" do
+ test "should create Assistant" do
visit new_assistant_url
fill_in "Description", with: @assistant.description
fill_in "Instructions", with: @assistant.instructions
- fill_in "Model", with: @assistant.model
+ find('#assistant_language_model_id').find(:xpath, 'option[1]').select_option
fill_in "Name", with: @assistant.name
fill_in "User", with: @assistant.user_id
click_text "Create Assistant"
assert_text "Assistant was successfully created"
+ assert_text "claude-3-opus-20240229 from fixtures"
click_text "Back"
end
test "should update Assistant" do
visit assistant_url(@assistant)
click_text "Edit this assistant", match: :first
-
fill_in "Description", with: @assistant.description
fill_in "Instructions", with: @assistant.instructions
- fill_in "Model", with: @assistant.model
+ find('#assistant_language_model_id').find(:xpath, 'option[2]').select_option
fill_in "Name", with: @assistant.name
fill_in "User", with: @assistant.user_id
click_text "Update Assistant"
assert_text "Assistant was successfully updated"
+ assert_text "claude-3-sonnet-20240229 from fixtures"
click_text "Back"
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index d5366843b..965052ae8 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -48,6 +48,7 @@ class TestCase
include ActiveJob::TestHelper
include FeatureHelpers
include PostgresqlHelper
+ include ViewHelpers
parallelize(workers: :number_of_processors)
fixtures :all