Skip to content

Commit

Permalink
Merge branch 'AllYourBot:main' into add-support-for-gemini
Browse files Browse the repository at this point in the history
  • Loading branch information
papayalabs authored Nov 18, 2024
2 parents d404260 + 751d103 commit 76038a2
Show file tree
Hide file tree
Showing 48 changed files with 646 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.5
3.3.6
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ruby 3.3.5
ruby 3.3.6
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
### START of FLY ####

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.3.5
ARG RUBY_VERSION=3.3.6
FROM quay.io/evl.ms/fullstaq-ruby:${RUBY_VERSION}-jemalloc-slim as base-for-fly

LABEL fly_launch_runtime="rails"
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ gem "postmark-rails"

gem "omniauth", "~> 2.1"
gem "omniauth-google-oauth2", "~> 1.1"
gem "omniauth-microsoft_graph", "~> 2.0"
gem "omniauth-rails_csrf_protection", "~> 1.0.2"

group :development, :test do
Expand Down
10 changes: 8 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ GEM
oauth2 (~> 2.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)
omniauth-microsoft_graph (2.0.1)
jwt (~> 2.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8.0)
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
Expand Down Expand Up @@ -437,6 +441,7 @@ GEM

PLATFORMS
arm64-darwin-23
arm64-darwin-24
x86_64-linux

DEPENDENCIES
Expand All @@ -457,6 +462,7 @@ DEPENDENCIES
name_of_person
omniauth (~> 2.1)
omniauth-google-oauth2 (~> 1.1)
omniauth-microsoft_graph (~> 2.0)
omniauth-rails_csrf_protection (~> 1.0.2)
ostruct
pg (~> 1.1)
Expand Down Expand Up @@ -487,7 +493,7 @@ DEPENDENCIES
web-console

RUBY VERSION
ruby 3.3.5p100
ruby 3.3.6p108

BUNDLED WITH
2.5.1
2.5.22
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ This project is led by an experienced rails developer, but I'm actively looking
- [Authentication](#authentication)
- [Password authentication](#password-authentication)
- [Google OAuth authentication](#google-oauth-authentication)
- [Microsoft Graph OAuth authentication](#microsoft-graph-oauth-authentication)
- [HTTP header authentication](#http-header-authentication)
- [Contribute as a developer](#contribute-as-a-developer)
- [Running locally](#Running-locally)
Expand Down Expand Up @@ -196,6 +197,7 @@ HostedGPT supports multiple authentication methods:

- [Password authentication](#password-authentication)
- [Google OAuth authentication](#google-oauth-authentication)
- [Microsoft Graph OAuth authentication](#microsoft-graph-oauth-authentication)

#### Password authentication

Expand All @@ -210,6 +212,14 @@ To enable Google OAuth authentication, you need to set up Google OAuth in the Go
- `GOOGLE_AUTH_CLIENT_ID` - Google OAuth client ID
- `GOOGLE_AUTH_CLIENT_SECRET` - Google OAuth client secret

Alternately, add the following to your encrypted credentials file:

```yaml
google:
auth_client_id: <your client id>
auth_client_secret: <your client secret>
```
**Steps to set up:**
1. **Go to the Google Cloud Console and Create a New Project:**
Expand Down Expand Up @@ -248,6 +258,52 @@ To enable Google OAuth authentication, you need to set up Google OAuth in the Go
- `GOOGLE_AUTH_CLIENT_ID`: Your Client ID
- `GOOGLE_AUTH_CLIENT_SECRET`: Your Client Secret

#### Microsoft Graph OAuth authentication

Microsoft Graph OAuth authentication is disabled by default. You can enable it by setting `MICROSOFT_GRAPH_AUTHENTICATION_FEATURE` to `true`.

To enable Microsoft Graph OAuth authentication, you need to set up Microsoft Graph OAuth in the Microsoft Azure portal. It's a bit involved but we've outlined the steps below. After you follow these steps you will set the following environment variables:

- `MICROSOFT_GRAPH_AUTH_CLIENT_ID` - Microsoft Graph OAuth client ID
- `MICROSOFT_GRAPH_AUTH_CLIENT_SECRET` - Microsoft Graph OAuth client secret
- `MICROSOFT_GRAPH_SCOPE` - Space separated list of scopes to request. This defaults to `openid profile email offline_access user.read mailboxsettings.read`.

Alternately, add the following to your encrypted credentials file:

```yaml
microsoft_graph:
auth_client_id: <your client id>
auth_client_secret: <your client secret>
scope: openid profile email offline_access user.read mailboxsettings.read
```

Users will need to have setup their full name in their Microsoft account before they can use this authentication method, via <https://profile.live.com/>, otherwise they will see a login/registration error like "First name can't be blank and last name can't be blank".

Users can remotely remove the connection between their Microsoft account and HostedGPT by going to <https://account.microsoft.com/privacy/app-access> and clicking "Don't Allow" on the corresponding application. However, this will not sign out the user from HostedGPT until the session expires.

**Steps to set up:**

1. **Go to the Microsoft Azure portal and create a new application:**

- Navigate to [Register an application](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/CreateApplicationBlade/quickStartType)
- Give it a name
- Select the Supported account types
- Select the Redirect URI for "Web" (e.g., `https://example.com/auth/microsoft/callback` or `http://localhost:3000/auth/microsoft/callback`)
- Click Register

2. **Create OAuth Credentials:**

- The client ID ("Application (client) ID") is displayed on the Overview page
- To generate a client secret, click on "Add a certificate or secret" > "New client secret"
- Give it a name and pick an expiration date
- Back on the "Certificates & secrets" page, the new client secret will be listed under "Value"

3. **Set Environment Variables:**
- Set the Client ID and Client Secret as environment variables in your application:
- `MICROSOFT_GRAPH_AUTH_CLIENT_ID`: Your Client ID
- `MICROSOFT_GRAPH_AUTH_CLIENT_SECRET`: Your Client Secret
- `MICROSOFT_GRAPH_SCOPE` - Space separated list of scopes to request. This defaults to `openid profile email offline_access user.read mailboxsettings.read`.

#### HTTP header authentication

Note: Enabling this automatically disables Password-based and Google-auth based authentication.
Expand Down
106 changes: 106 additions & 0 deletions app/controllers/authentications/microsoft_graph_oauth_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
class Authentications::MicrosoftGraphOauthController < ApplicationController
allow_unauthenticated_access

# GET /auth/microsoft_graph/callback
def create
if params[:error]
if Current.user
redirect_to(edit_settings_person_path, alert: params[:error_description])
else
redirect_to(login_path, alert: params[:error_description])
end
return
end

if Current.user
Current.user.microsoft_graph_credential&.destroy
_, cred = add_person_credentials("MicrosoftGraphCredential")
cred.save! && redirect_to(edit_settings_person_path, notice: "Saved") && return

elsif (credential = MicrosoftGraphCredential.find_by(oauth_id: auth[:uid]))
@person = credential.user.person

elsif Feature.disabled?(:registration)
redirect_to(root_path, alert: "Registration is disabled") && return

elsif auth_email && (user = Person.find_by(email: auth_email)&.user)
@person = init_for_user(user)

elsif auth_email && (@person = Person.find_by(email: auth_email))
@person = init_for_person(@person)

else
@person = initialize_microsoft_person
end

if @person&.save
login_as(@person, credential: @person.user.reload.microsoft_graph_credential)
redirect_to root_path
else
@person&.errors&.delete :personable
msg = @person.errors.full_messages.map { |m| m.gsub(/Personable |credentials /, "") }.to_sentence.capitalize
if msg.downcase.include?("oauth refresh token can't be blank")
msg += " " + helpers.link_to("Microsoft third-party connections", "https://account.microsoft.com/privacy/app-access", class: "underline") + " search for website, and delete all it's connections. Then try again."
end

redirect_to new_user_path, alert: msg
end
rescue => e
warn e.message
warn e.backtrace.join("\n")
redirect_to edit_settings_person_path, alert: "Error. #{e.message}", status: :see_other
end

private

def auth
request.env["omniauth.auth"]&.deep_symbolize_keys || {}
end

def auth_email
auth.dig(:info, :email)
end

def init_for_user(user)
user.microsoft_graph_credential&.destroy

user.first_name = auth[:info][:first_name]
user.last_name = auth[:info][:last_name]
@person = user.person
add_person_credentials("MicrosoftGraphCredential").first
end

def init_for_person(person)
@person.personable_type = "User"
@person.personable_attributes = {
first_name: auth[:info][:first_name],
last_name: auth[:info][:last_name]
}
add_person_credentials("MicrosoftGraphCredential").first
end

def initialize_microsoft_person
@person = Person.new({
personable_type: "User",
email: auth_email,
personable_attributes: {
first_name: auth[:info][:first_name],
last_name: auth[:info][:last_name],
}
})
add_person_credentials("MicrosoftGraphCredential").first
end

def add_person_credentials(type)
p = Current.person || @person
c = p.user.credentials.build(
type: type,
oauth_id: auth[:uid],
oauth_email: auth[:info][:email],
oauth_token: auth[:credentials][:token],
oauth_refresh_token: auth[:credentials][:refresh_token],
properties: auth[:credentials].except(:token, :refresh_token)
)
[p, c]
end
end
2 changes: 1 addition & 1 deletion app/controllers/concerns/authenticate/login_logout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ def reset_authentication
end

def manual_login_allowed?
Feature.password_authentication? || Feature.google_authentication?
Feature.password_authentication? || Feature.google_authentication? || Feature.microsoft_graph_authentication?
end
end
2 changes: 1 addition & 1 deletion app/controllers/settings/language_models_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ def set_system_language_model
end

def language_model_params
params.require(:language_model).permit(:api_name, :name, :best, :supports_images, :supports_tools, :api_service_id)
params.require(:language_model).permit(:api_name, :name, :best, :supports_images, :supports_tools, :api_service_id, :supports_system_message)
end
end
10 changes: 10 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
module ApplicationHelper

def truncate_long_name(name)
truncate(name, length: 20)
end

def at_most_two_initials(initials)
return initials if initials.nil? || initials.length <= 2
initials[0] + initials[-1]
end

def spinner(opts = {})
html = <<~HTML
<svg class="animate-spin -ml-1 mr-3 h-#{opts[:size]} w-#{opts[:size]} #{opts[:class]}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
Expand Down
16 changes: 8 additions & 8 deletions app/jobs/get_next_ai_message_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def ai_backend
end

def perform(user_id, message_id, assistant_id, attempt = 1)
puts "\n### GetNextAIMessageJob.perform(#{user_id}, #{message_id}, #{assistant_id}, #{attempt})" unless Rails.env.test?
Rails.logger.info "### GetNextAIMessageJob.perform(#{user_id}, #{message_id}, #{assistant_id}, #{attempt})" unless Rails.env.test?

@user = User.find(user_id)
@message = Message.find(message_id)
Expand All @@ -29,7 +29,7 @@ def perform(user_id, message_id, assistant_id, attempt = 1)
@message.update!(processed_at: Time.current, content_text: "")
GetNextAIMessageJob.broadcast_updated_message(@message, thinking: true) # thinking shows dot, signaling to user that we're waiting now on ai_backend

puts "\n### Wait for reply" unless Rails.env.test?
Rails.logger.info "\n### Wait for reply" unless Rails.env.test?

response = Current.set(user: @user, message: @message) do
ai_backend.new(@conversation.user, @assistant, @conversation, @message)
Expand Down Expand Up @@ -59,7 +59,7 @@ def perform(user_id, message_id, assistant_id, attempt = 1)
return true

rescue ResponseCancelled => e
puts "\n### Response cancelled in GetNextAIMessageJob(#{message_id})" unless Rails.env.test?
Rails.logger.info "\n### Response cancelled in GetNextAIMessageJob(#{message_id})" unless Rails.env.test?
wrap_up_the_message
return true
rescue OpenAI::ConfigurationError => e
Expand Down Expand Up @@ -90,14 +90,14 @@ def perform(user_id, message_id, assistant_id, attempt = 1)
wrap_up_the_message
return true
rescue WaitForPrevious
puts "\n### WaitForPrevious in GetNextAIMessageJob(#{message_id})" unless Rails.env.test?
Rails.logger.info "\n### WaitForPrevious in GetNextAIMessageJob(#{message_id})" unless Rails.env.test?
raise WaitForPrevious
rescue => e
msg = e.inspect.gsub(/(sk-)[\w\-]{40}/, '\1' + "*" * 40)

unless Rails.env.test?
puts "\n### Finished GetNextAIMessageJob attempt ##{attempt} with ERROR: #{msg}" unless Rails.env.test?
puts e.backtrace.join("\n") if Rails.env.development?
Rails.logger.info "\n### Finished GetNextAIMessageJob attempt ##{attempt} with ERROR: #{msg}" unless Rails.env.test?
Rails.logger.info e.backtrace.join("\n") if Rails.env.development?

if attempt < 3
GetNextAIMessageJob.broadcast_updated_message(@message, thinking: false)
Expand Down Expand Up @@ -178,11 +178,11 @@ def wrap_up_the_message
@message.save!
@message.conversation.touch # updated_at change will bump it up your list + ensures it will be auto-titled

puts "\n### Finished GetNextAIMessageJob.perform(#{@user.id}, #{@message.id}, #{@message.assistant_id}, #{@attempt})" unless Rails.env.test?
Rails.logger.info "\n### Finished GetNextAIMessageJob.perform(#{@user.id}, #{@message.id}, #{@message.assistant_id}, #{@attempt})" unless Rails.env.test?
end

def call_tools_before_wrapping_up
puts "\n### Calling tools" unless Rails.env.test?
Rails.logger.info "\n### Calling tools" unless Rails.env.test?

msgs = []
Current.set(user: @user, message: @message) do
Expand Down
1 change: 1 addition & 0 deletions app/models/feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def features
if @@features_hash[:http_header_authentication]
@@features_hash[:password_authentication] = false
@@features_hash[:google_authentication] = false
@@features_hash[:microsoft_graph_authentication] = false
end

@@features_hash
Expand Down
10 changes: 10 additions & 0 deletions app/models/microsoft_graph_credential.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class MicrosoftGraphCredential < Credential
alias_attribute :oauth_id, :external_id

validates :oauth_token, presence: true
validates :oauth_refresh_token, presence: true
validates :oauth_id, presence: true, uniqueness: true
validates :oauth_email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }

normalizes :oauth_email, with: -> email { email.downcase.strip }
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class User < ApplicationRecord
has_one :google_credential, -> { type_is("GoogleCredential") }, class_name: "Credential", inverse_of: :user
has_one :gmail_credential, -> { type_is("GmailCredential") }, class_name: "Credential", inverse_of: :user
has_one :google_tasks_credential, -> { type_is("GoogleTasksCredential") }, class_name: "Credential", inverse_of: :user
has_one :microsoft_graph_credential, -> { type_is("MicrosoftGraphCredential") }, class_name: "Credential", inverse_of: :user
has_one :http_header_credential, -> { type_is("HttpHeaderCredential") }, class_name: "Credential", inverse_of: :user

belongs_to :last_cancelled_message, class_name: "Message", optional: true
Expand Down
3 changes: 2 additions & 1 deletion app/models/user/registerable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def create_initial_assistants_etc
name:,
best: best_models.include?(api_name),
supports_tools: true,
supports_system_message: true,
supports_images:,
input_token_cost_cents:,
output_token_cost_cents:,
Expand All @@ -71,7 +72,7 @@ def create_initial_assistants_etc
input_token_cost_cents = input_token_cost_per_million/million
output_token_cost_cents = output_token_cost_per_million/million

language_models.create!(api_name:, api_service:, name:, supports_tools: false, supports_images:, input_token_cost_cents:, output_token_cost_cents:)
language_models.create!(api_name:, api_service:, name:, supports_tools: false, supports_system_message: false, supports_images:, input_token_cost_cents:, output_token_cost_cents:)
end

assistants.create! name: "GPT-4o", language_model: language_models.best_for_api_service(open_ai_api_service).first
Expand Down
4 changes: 2 additions & 2 deletions app/services/ai_backend/anthropic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ def stream_handler(&chunk_handler)
rescue ::Faraday::UnauthorizedError => e
raise ::Anthropic::ConfigurationError
rescue => e
puts "\nUnhandled error in AIBackend::Anthropic response handler: #{e.message}"
puts e.backtrace
Rails.logger.info "\nUnhandled error in AIBackend::Anthropic response handler: #{e.message}"
Rails.logger.info e.backtrace
end
end

Expand Down
Loading

0 comments on commit 76038a2

Please sign in to comment.