Skip to content

Commit

Permalink
Create new & manage Assistants (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephan-buckmaster authored May 28, 2024
1 parent b9dd783 commit ca13237
Show file tree
Hide file tree
Showing 51 changed files with 709 additions and 205 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ If you encountered an error while waiting for the services to be deployed on Ren

# Deploy the app on Fly

Deploying to Fly.io is another great option. It's not quite one-click like Render and it's not 100% free. But we've made the configuration really easy for you and the cost should be about $2 per month, and Render costs $7 per month after 90 days of free servie so Fly is actually less expensive over the long term.
Deploying to Fly.io is another great option. It's not quite one-click like Render and it's not 100% free. But we've made the configuration really easy for you and the cost should be about $2 per month, and Render costs $7 per month after 90 days of free service so Fly is actually less expensive over the long term.

1. Click Fork > Create New Fork at the top of this repository. Pull your forked repository down to your computer (the usual git clone ...).
1. Install the Fly command-line tool [view instructions](https://fly.io/docs/hands-on/install-flyctl/)
1. In the root directory of the repoistory you pulled down, run `fly launch --build-only` and say `Yes` to copy the existing fly.toml, but note that it will generate the wrong settings.
1. In the root directory of the repository you pulled down, run `fly launch --build-only` and say `Yes` to copy the existing fly.toml, but note that it will generate the wrong settings.
1. **The settings it shows are INCORRECT** so tell it you want to make changes
1. When it opens your browser, change the Database to `Fly Postgres` with a name such as `hostedgpt-db` and you can set the configuration to `Development`.
1. Click `Confirm Settings` at the bottom of the page and close the browser.
Expand All @@ -81,7 +81,7 @@ Deploying to Fly.io is another great option. It's not quite one-click like Rende

# Deploy the app on Heroku

Heroku is a one-click option that will cost $10/monnth for the compute (dyno) and database. By default, apps use Eco dynos ($5) if you are subscribed to Eco. Otherwise, it defaults to Basic dynos ($7). The Eco dynos plan is shared across all Eco dynos in your account and is recommended if you plan on deploying many small apps to Heroku. Eco dynos "sleep" after 30 minutes of inactivity and take a few seconds to wake up. Basic dynos do not sleep.
Heroku is a one-click option that will cost $10/month for the compute (dyno) and database. By default, apps use Eco dynos ($5) if you are subscribed to Eco. Otherwise, it defaults to Basic dynos ($7). The Eco dynos plan is shared across all Eco dynos in your account and is recommended if you plan on deploying many small apps to Heroku. Eco dynos "sleep" after 30 minutes of inactivity and take a few seconds to wake up. Basic dynos do not sleep.

Eligible students can apply for Heroku platform credits through [Heroku for GitHub Students program](https://blog.heroku.com/github-student-developer-program).

Expand All @@ -94,7 +94,7 @@ Eligible students can apply for Heroku platform credits through [Heroku for GitH

# Contribute as a developer

We welcome contributors! After you get your developoment environment setup, review the list of Issues. We organize the issues into Milestones and are currently working on v0.7. [View 0.7 Milestone](https://github.com/allyourbot/hostedgpt/milestone/6). Look for any issues tagged with **Good first issue** and add a comment so we know you're working on it.
We welcome contributors! After you get your development environment setup, review the list of Issues. We organize the issues into Milestones and are currently working on v0.7. [View 0.7 Milestone](https://github.com/allyourbot/hostedgpt/milestone/6). Look for any issues tagged with **Good first issue** and add a comment so we know you're working on it.

## Setting up development

Expand Down
4 changes: 2 additions & 2 deletions app/controllers/assistants_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def update
end

def destroy
@assistant.destroy!
@assistant.soft_delete
redirect_to assistants_url, notice: "Assistant was successfully destroyed.", status: :see_other
end

Expand All @@ -46,6 +46,6 @@ def set_assistant
end

def assistant_params
params.require(:assistant).permit(:user_id, :model, :name, :description, :instructions, :tools)
params.require(:assistant).permit(:user_id, :language_model_id, :name, :description, :instructions, :tools)
end
end
4 changes: 1 addition & 3 deletions app/controllers/messages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ def create
end

def update
# Clicking edit beneath a message actually submits to create and not here. This action is only used for next/prev conversation.
# In order to force a morph we PATCH to here and redirect.
if @message.update(message_params)
redirect_to conversation_messages_path(@message.conversation, version: @version || @message.version)
else
Expand All @@ -76,7 +74,7 @@ def set_conversation
end

def set_assistant
@assistant = Current.user.assistants.find_by(id: params[:assistant_id])
@assistant = Current.user.assistants_including_deleted.find_by(id: params[:assistant_id])
@assistant ||= @conversation.latest_message_for_version(@version).assistant
end

Expand Down
11 changes: 6 additions & 5 deletions app/controllers/settings/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ class Settings::ApplicationController < ApplicationController
def set_settings_menu
# controller_name => array of items
@settings_menu = {
people: {
"Your Account": edit_settings_person_path
},

assistants: Current.user.assistants.ordered.map {
|assistant| [ assistant, edit_settings_assistant_path(assistant) ]
}.to_h.merge({
#'New Assistant': new_settings_assistant_path(assistant)
}),
"New Assistant": new_settings_assistant_path
})

people: {
'Account': edit_settings_person_path
}
}
end
end
19 changes: 15 additions & 4 deletions app/controllers/settings/assistants_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class Settings::AssistantsController < Settings::ApplicationController
before_action :set_assistant, only: [:edit, :update, :destroy]
before_action :set_last_assistant, except: [:destroy]

def new
@assistant = Assistant.new
Expand Down Expand Up @@ -27,17 +28,27 @@ def update
end

def destroy
@assistant.destroy!
redirect_to new_settings_assistant_url, notice: "Deleted", status: :see_other
if @assistant.soft_delete
redirect_to new_settings_assistant_url, notice: "Deleted", status: :see_other
else
redirect_to new_settings_assistant_url, alert: "Cannot delete your last assistant", status: :see_other
end
end

private

def set_assistant
@assistant = Current.user.assistants.find(params[:id])
@assistant = Current.user.assistants.find_by(id: params[:id])
if @assistant.nil?
redirect_to new_settings_assistant_url, notice: "The assistant was deleted", status: :see_other
end
end

def set_last_assistant
@last_assistant = Current.user.assistants.count <= 1
end

def assistant_params
params.require(:assistant).permit(:name, :description, :instructions)
params.require(:assistant).permit(:name, :description, :instructions, :language_model_id)
end
end
6 changes: 1 addition & 5 deletions app/jobs/get_next_ai_message_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ class WaitForPrevious < StandardError; end
retry_on WaitForPrevious, wait: ->(run) { (2**run - 1).seconds }, attempts: 3

def ai_backend
if @assistant.model.starts_with?('gpt-')
AIBackend::OpenAI
else
AIBackend::Anthropic
end
@assistant.language_model.ai_backend
end

def perform(user_id, message_id, assistant_id, attempt = 1)
Expand Down
19 changes: 18 additions & 1 deletion app/models/assistant.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
class Assistant < ApplicationRecord
MAX_LIST_DISPLAY = 5

belongs_to :user

has_many :conversations, dependent: :destroy
has_many :documents, dependent: :destroy
has_many :runs, dependent: :destroy
has_many :steps, dependent: :destroy
has_many :messages # TODO: What should happen if an assistant is deleted?
has_many :messages, dependent: :destroy

delegate :supports_images?, to: :language_model

belongs_to :language_model

validates :tools, presence: true, allow_blank: true
validates :name, presence: true

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

Expand All @@ -20,6 +27,16 @@ def initials
parts[1]&.try(:[], 0)&.capitalize.to_s
end

def soft_delete
return false if user.assistants.count <= 1
update!(deleted_at: Time.now)
return true
end

def soft_delete!
raise "Can't delete user's last assistant" if !soft_delete
end

def to_s
name
end
Expand Down
8 changes: 4 additions & 4 deletions app/models/concerns/user/registerable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ module User::Registerable
private

def create_initial_assistants
assistants.create! name: "GPT-4", model: "gpt-4o-2024-05-13", images: true
assistants.create! name: "GPT-3.5", model: "gpt-3.5-turbo-0125", images: false
assistants.create! name: "Claude 3 Opus", model: "claude-3-opus-20240229", images: true
assistants.create! name: "Claude 3 Sonnet", model: "claude-3-sonnet-20240229", images: true
assistants.create! name: "GPT-4o", language_model: LanguageModel.find_by(name: 'gpt-4o')
assistants.create! name: "GPT-3.5", language_model: LanguageModel.find_by(name: 'gpt-3.5-turbo')
assistants.create! name: "Claude 3 Opus", language_model: LanguageModel.find_by(name: 'claude-3-opus-20240229')
assistants.create! name: "Claude 3 Sonnet", language_model: LanguageModel.find_by(name: 'claude-3-sonnet-20240229')
end
end
27 changes: 27 additions & 0 deletions app/models/language_model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# We don't care about large or not
class LanguageModel < ApplicationRecord
BEST_MODELS = {
'gpt-best' => 'gpt-4o-2024-05-13',
'claude-best' => 'claude-3-opus-20240229'
}

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

has_many :assistants

def readonly?
!new_record?
end

def provider_name
BEST_MODELS[name] || name
end

def ai_backend
if name.starts_with?('gpt-')
AIBackend::OpenAI
else
AIBackend::Anthropic
end
end
end
7 changes: 7 additions & 0 deletions app/models/run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ class Run < ApplicationRecord

enum status: %w[queued in_progress requires_action cancelling cancelled failed completed expired].index_by(&:to_sym)

before_validation :set_model, on: :create
validates :status, :expired_at, :model, :instructions, presence: true
validates :tools, :file_ids, presence: true, allow_blank: true

private

def set_model
self.model = assistant&.language_model&.name
end
end
16 changes: 15 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class User < ApplicationRecord
validates :first_name, presence: true
validates :last_name, presence: true, on: :create

has_many :assistants, dependent: :destroy
has_many :assistants, -> { not_deleted }
has_many :assistants_including_deleted, class_name: "Assistant", dependent: :destroy
has_many :conversations, dependent: :destroy
belongs_to :last_cancelled_message, class_name: "Message", optional: true

Expand All @@ -19,4 +20,17 @@ class User < ApplicationRecord
def preferences
attributes["preferences"].with_defaults(dark_mode: "system")
end

def destroy_in_progress?
@destroy_in_progress
end

def destroy
@destroy_in_progress = true
begin
super
ensure
@destroy_in_progress = false
end
end
end
4 changes: 2 additions & 2 deletions app/services/ai_backend/anthropic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def get_next_chat_message(&chunk_received_handler)

begin
response = @client.messages(
model: @assistant.model,
model: @assistant.language_model.provider_name,
system: @assistant.instructions,
messages: preceding_messages,
parameters: {
Expand Down Expand Up @@ -76,7 +76,7 @@ def get_next_chat_message(&chunk_received_handler)

def preceding_messages
@conversation.messages.for_conversation_version(@message.version).where("messages.index < ?", @message.index).collect do |message|
if @assistant.images && message.documents.present?
if @assistant.supports_images? && message.documents.present?

content = [{ type: "text", text: message.content_text }]
content += message.documents.collect do |document|
Expand Down
4 changes: 2 additions & 2 deletions app/services/ai_backend/open_ai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def get_next_chat_message(&chunk_handler)

begin
response = @client.chat(parameters: {
model: @assistant.model,
model: @assistant.language_model.provider_name,
messages: system_message + preceding_messages,
tools: Toolbox.tools,
stream: response_handler,
Expand Down Expand Up @@ -124,7 +124,7 @@ def system_message

def preceding_messages
@conversation.messages.for_conversation_version(@message.version).where("messages.index < ?", @message.index).collect do |message|
if @assistant.images && message.documents.present?
if @assistant.supports_images? && message.documents.present?

content_with_images = [{ type: "text", text: message.content_text }]
content_with_images += message.documents.collect do |document|
Expand Down
10 changes: 6 additions & 4 deletions app/services/test_client/anthropic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ def self.text
#
# Stub this method to respond with something more specific if needed.
def messages(**args)
model = args.dig(:model) || "no model"
system_message = args.dig(:system)
if proc = args.dig(:parameters, :stream)
proc.call({
"id"=>"msg_01LtHY4sJVd7WBdPCsYb8kHQ",
"type"=>"message",
"role"=>"assistant",
"delta"=>
{"type"=>"text",
"text"=> self.class.text || "Hello! It's nice to meet you. How can I assist you today?"},
"model"=>"claude-3-opus-20240229",
"text"=> self.class.text || "Hello this is model #{model} with instruction #{system_message.to_s.inspect}! How can I assist you today?"},
"model"=>model,
"stop_reason"=>"end_turn",
"stop_sequence"=>nil,
"usage"=>{"input_tokens"=>10, "output_tokens"=>19}
Expand All @@ -30,8 +32,8 @@ def messages(**args)
"role"=>"assistant",
"content"=>
[{"type"=>"text",
"text"=> self.class.text || "Hello! It's nice to meet you. How can I assist you today?"}],
"model"=>"claude-3-opus-20240229",
"text"=> self.class.text || "Hello this is model #{model} with instruction #{system_message.to_s.inspect}! How can I assist you today?"}],
"model"=> model,
"stop_reason"=>"end_turn",
"stop_sequence"=>nil,
"usage"=>{"input_tokens"=>10, "output_tokens"=>19}
Expand Down
10 changes: 8 additions & 2 deletions app/services/test_client/open_ai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ def self.text
raise "When using the OpenAI test client for api_text_response you need to stub the .text method"
end

def self.default_text
"Hello this is model #{@@model} with instruction nil! How can I assist you today?"
end

def self.api_text_response
{
"id"=> "chatcmpl-abc123abc123abc123abc123abc12",
"object"=>"chat.completion",
"created"=>1707429030,
"model"=>"gpt-3.5-turbo-0613",
"model"=> @@model,
"choices"=> [
{
"index"=>0,
"delta"=>{
"role"=>"assistant",
"content"=> text
"content"=> text || default_text
},
"logprobs"=>nil,
"finish_reason"=>"stop"
Expand Down Expand Up @@ -63,6 +67,8 @@ def self.api_function_response
end

def chat(**args)
@@model = args.dig(:parameters, :model) || "no model"

proc = args.dig(:parameters, :stream)
raise "No stream proc provided. When calling get_next_chat_message in tests be sure to include a block" if proc.nil?
proc.call(self.class.api_response)
Expand Down
5 changes: 4 additions & 1 deletion app/views/assistants/_assistant.html.erb
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
<%# locals: (assistant:, settings: true, assistant_counter: -1) %>
<% selected = assistant == @assistant %>
<% first = assistant_counter.zero? %>
<% visible = assistant_counter <= Assistant::MAX_LIST_DISPLAY-1 || selected %>

<div class="bg-gray-50 dark:bg-gray-900 <%= first && 'absolute top-[17px] left-0 pl-3 w-full z-10' %>">
<%# This extra div ^ is needed because of the absolute positioning. It doesn't lay out properly if added to the div below. %>
<div class="
flex justify-between items-center
mb-1 p-1 pl-2 pr-2 mr-5
hover:bg-gray-100 dark:hover:bg-gray-700
bg-gray-50 dark:bg-transparent
bg-gray-50 dark:bg-transparent
dark:bg-gray-700
group cursor-pointer
text-sm rounded-lg
<%= selected && 'relationship' %>
<%= !visible && 'hidden' %>
"
data-role="assistant"
data-radio-behavior-target="radio"
data-action="radio-changed@window->radio-behavior#select"
data-radio-behavior-id-param="<%= assistant.id %>"
data-transition-target="<%= !visible && 'transitionable' %>"
>
<%= link_to new_assistant_message_path(assistant), class: "flex-1 flex items-center text-gray-950 dark:text-gray-100 font-medium truncate", data: { role: "name" } do %>
<%= render partial: "layouts/assistant_avatar", locals: { assistant: assistant, size: 7, classes: "mr-2" } %>
Expand Down
Loading

0 comments on commit ca13237

Please sign in to comment.