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

Allow users to configure a personal OpenAI API key #1496

Merged
merged 13 commits into from
Aug 1, 2024
Merged
8 changes: 3 additions & 5 deletions app/controllers/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,10 @@ def export_external_confirm
# rubocop:enable Metrics/AbcSize

def generate_test
TaskService::GptGenerateTests.call(task: @task)
TaskService::GptGenerateTests.call(task: @task, openai_api_key: current_user.openai_api_key)
flash[:notice] = I18n.t('tasks.task_service.gpt_generate_tests.successful_generation')
rescue Gpt::MissingLanguageError
flash[:alert] = I18n.t('tasks.task_service.gpt_generate_tests.no_language')
rescue Gpt::InvalidTaskDescription
flash[:alert] = I18n.t('tasks.task_service.gpt_generate_tests.invalid_description')
rescue Gpt::Error => e
flash[:alert] = e.localized_message
ensure
redirect_to @task
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/users/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def configure_sign_up_params

# If you have extra params to permit, append them to the sanitizer.
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: %i[first_name last_name avatar])
devise_parameter_sanitizer.permit(:account_update, keys: %i[first_name last_name avatar openai_api_key])
end

def after_update_path_for(resource)
Expand Down
19 changes: 19 additions & 0 deletions app/errors/gpt/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Gpt
class Error < ApplicationError
class InternalServerError < Error; end

class InvalidApiKey < Error; end

class InvalidTaskDescription < Error; end

class MissingLanguage < Error; end

class UnauthorizedError < Error; end

def localized_message
I18n.t("errors.gpt.#{self.class.name&.demodulize&.underscore}")
end
end
end
5 changes: 0 additions & 5 deletions app/errors/gpt/invalid_task_description.rb

This file was deleted.

5 changes: 0 additions & 5 deletions app/errors/gpt/missing_language_error.rb

This file was deleted.

9 changes: 9 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class User < ApplicationRecord
validates :email, presence: true, uniqueness: {case_sensitive: false}
validates :first_name, :last_name, :status_group, presence: true
validates :password_set, inclusion: [true, false]
validate :validate_openai_api_key, if: -> { openai_api_key.present? }

has_many :tasks, dependent: :nullify

Expand Down Expand Up @@ -148,6 +149,14 @@ def to_s

private

def validate_openai_api_key
return unless openai_api_key_changed?

TaskService::GptGenerateTests.new_client! openai_api_key
rescue Gpt::Error::InvalidApiKey
errors.add(:base, :invalid_api_key)
end

def avatar_format
avatar_blob = avatar.blob
if avatar_blob.content_type.start_with? 'image/'
Expand Down
2 changes: 1 addition & 1 deletion app/policies/task_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def manage?
end

def generate_test?
Settings.open_ai.access_token.present? and update?
user&.openai_api_key.present? and update?
end

private
Expand Down
39 changes: 31 additions & 8 deletions app/services/task_service/gpt_generate_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

module TaskService
class GptGenerateTests < ServiceBase
def initialize(task:)
def initialize(task:, openai_api_key:)
super()
raise Gpt::MissingLanguageError if task.programming_language&.language.blank?
raise Gpt::Error::MissingLanguage if task.programming_language&.language.blank?

@task = task
@client = self.class.new_client! openai_api_key
end

def execute
Expand All @@ -18,13 +19,17 @@
@task.tests << test
end

private
def self.new_client!(access_token)
raise Gpt::Error::InvalidApiKey if access_token.blank?

def client
@client ||= OpenAI::Client.new
client = OpenAI::Client.new(access_token:)
validate! client
client
end

def gpt_response
private

def gpt_response # rubocop:disable Metrics/AbcSize
# train client with some prompts
messages = training_prompts.map do |prompt|
{role: 'system', content: prompt}
Expand All @@ -34,7 +39,7 @@
messages << {role: 'user', content: @task.description}

# create gpt client
response = client.chat(
response = @client.chat(
parameters: {
model: Settings.open_ai.model,
messages:,
Expand All @@ -46,9 +51,15 @@
raw_response = response.dig('choices', 0, 'message', 'content')

# check for ``` in the response and extract the text between the first set
raise Gpt::InvalidTaskDescription unless raw_response.include?('```')
raise Gpt::Error::InvalidTaskDescription unless raw_response.include?('```')

raw_response[/```(.*?)```/m, 1].lines[1..]&.join&.strip
rescue Faraday::UnauthorizedError => e
raise Gpt::Error::UnauthorizedError.new("Unauthorized access to OpenAI: #{e.message}")

Check warning on line 58 in app/services/task_service/gpt_generate_tests.rb

View check run for this annotation

Codecov / codecov/patch

app/services/task_service/gpt_generate_tests.rb#L58

Added line #L58 was not covered by tests
rescue Faraday::Error => e
raise Gpt::Error::InternalServerError.new("Could not communicate with OpenAI due to #{e.inspect}")
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError, EOFError => e
raise Gpt::Error.new(e)
end

def training_prompts
Expand All @@ -64,5 +75,17 @@
PROMPT
]
end

def self.validate!(client)
response = client.models.list
raise Gpt::Error::InvalidApiKey unless response['data']
rescue Faraday::UnauthorizedError, OpenAI::Error
raise Gpt::Error::InvalidApiKey
rescue Faraday::Error => e
raise Gpt::Error::InternalServerError.new("Could not communicate with OpenAI due to #{e.inspect}")
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError, EOFError
raise Gpt::Error
end
private_class_method :validate!
end
end
13 changes: 11 additions & 2 deletions app/views/tasks/show.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -574,9 +574,18 @@
=< t('common.button.duplicate')

- if policy(@task).generate_test?
Melhaya marked this conversation as resolved.
Show resolved Hide resolved
MrSerth marked this conversation as resolved.
Show resolved Hide resolved
= link_to generate_test_task_path(@task), method: :post, class: 'btn btn-important' do
= link_to generate_test_task_path(@task),
method: :post,
class: 'btn btn-important' do
i.fa-solid.fa-wand-magic-sparkles
=< t('.button.generate_test')
= t('.button.generate_test')
- elsif policy(@task).update?
div data-bs-toggle='tooltip' title=t('.button.api_key_required') data-bs-delay=150
= link_to generate_test_task_path(@task),
method: :post,
class: 'btn btn-important disabled' do
i.fa-solid.fa-wand-magic-sparkles
= t('.button.generate_test')

- if current_user.present?
= link_to t('common.button.back'), tasks_path, class: 'btn btn-important'
Expand Down
10 changes: 10 additions & 0 deletions app/views/users/registrations/edit.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@
autocomplete: 'new-password',
class: 'form-control'

.form-group.field-element
MrSerth marked this conversation as resolved.
Show resolved Hide resolved
= f.label :openai_api_key, t('users.show.openai_api_key'), class: 'form-label'
= f.text_field :openai_api_key,
required: false,
placeholder: t('users.show.openai_api_key'),
autocomplete: 'off',
class: 'form-control'
small.form-text.text-body-secondary
= t('.openai_api_key_usage_html', openai_api_link: 'https://platform.openai.com/api-keys')

= render 'avatar_form', f:

- if resource.password_set?
Expand Down
9 changes: 9 additions & 0 deletions app/views/users/show.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@
| :
.col.row-value
= @user.email
.row
.col-auto.row-label
= t('.openai_api_key')
| :
.col.row-value
- if @user.openai_api_key.present?
= t('.openai_api_key_provided')
- else
= t('.openai_api_key_not_provided')
.row.vertical
.col.row-label
= t('.account_links.created')
Expand Down
7 changes: 0 additions & 7 deletions config/initializers/open_ai.rb

This file was deleted.

2 changes: 0 additions & 2 deletions config/locales/de/controllers/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,4 @@ de:
task_found: 'In der externen App wurde eine entsprechende Aufgabe gefunden. Sie können: <ul><li><b>Überschreiben</b> Sie die Aufgabe in der externen App. Dadurch werden alle Änderungen, die in CodeHarbor vorgenommen wurden, auf die externe App übertragen.<br>Vorsicht: Dadurch werden alle potenziellen Änderungen in der externen App überschrieben. Dadurch wird die Aufgabe geändert (und möglicherweise zerstört), auch wenn sie derzeit in einem Kurs verwendet wird.</li><li><b>Erstellen Sie eine neue</b> Aufgabe, die den aktuellen Zustand dieser Aufgabe kopiert. Dadurch wird eine Kopie dieser Aufgabe in CodeHarbor erstellt, die dann als völlig neue Aufgabe in der externen App exportiert wird.</li></ul>'
task_found_no_right: 'In der externen App wurde eine entsprechende Aufgabe gefunden, aber Sie haben keine Rechte, sie zu bearbeiten. Sie können: <ul><li><b>Eine neue Aufgabe erstellen</b>, die den aktuellen Zustand dieser Aufgabe kopiert. Dadurch wird eine Kopie dieser Aufgabe in CodeHarbor erstellt, die dann als völlig neue Aufgabe in der externen App exportiert wird.</li></ul>'
gpt_generate_tests:
invalid_description: Die angegebene Aufgabenbeschreibung stellt keine gültige Programmieraufgabe dar und kann daher nicht zum Generieren eines Unit-Tests genutzt werden. Bitte stellen Sie sicher, dass die Aufgabenbeschreibung eine klar formulierte Problemstellung enthält, die durch ein Programm gelöst werden kann.
no_language: Für diese Aufgabe ist keine Programmiersprache angegeben. Bitte geben Sie die Sprache an, bevor Sie fortfahren.
successful_generation: Unit-Test erfolgreich generiert. Bitte überprüfen Sie den generierten Test und vergeben Sie einen passenden Dateinamen.
9 changes: 9 additions & 0 deletions config/locales/de/errors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
de:
errors:
gpt:
error: Beim Generieren des Unit-Tests ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.
internal_server_error: Der OpenAI-Server ist derzeit nicht verfügbar. Bitte versuchen Sie es später erneut, wenn das Problem behoben wurde.
invalid_api_key: Der API-Schlüssel in Ihrem Profil ist ungültig. Bitte fügen Sie den entsprechenden API-Schlüssel in Ihrem Profil hinzu, um die KI-Funktionen zu nutzen.
invalid_task_description: Die angegebene Aufgabenbeschreibung stellt keine gültige Programmieraufgabe dar und kann daher nicht zum Generieren eines Unit-Tests genutzt werden. Bitte stellen Sie sicher, dass die Aufgabenbeschreibung eine klar formulierte Problemstellung enthält, die durch ein Programm gelöst werden kann.
missing_language: Für diese Aufgabe ist keine Programmiersprache angegeben. Bitte geben Sie die Sprache an, bevor Sie fortfahren.
MrSerth marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions config/locales/de/models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ de:
avatar:
not_an_image: muss ein Bild sein
size_over_10_mb: Größe muss kleiner als 10MB sein
invalid_api_key: Der API-Schlüssel in Ihrem Profil ist ungültig. Bitte fügen Sie den entsprechenden API-Schlüssel in Ihrem Profil hinzu, um die KI-Funktionen zu nutzen.
models:
account_link:
one: Account-Link
Expand Down
1 change: 1 addition & 0 deletions config/locales/de/views/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ de:
add_to_collection_hint: Speichern Sie Aufgaben für später, indem Sie sie zu einer Sammlung hinzufügen.
button:
add_to_collection: Zu Sammlung hinzufügen
api_key_required: OpenAI API-Schlüssel ist erforderlich, um einen Test zu erstellen
create_collection: Neue Sammlung anlegen
download_as_zip: Diese Aufgabe als ZIP-Datei herunterladen.
export: Exportieren
Expand Down
4 changes: 4 additions & 0 deletions config/locales/de/views/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ de:
add_identity: Account mit %{kind} verknüpfen
cannot_remove_last_identity: Account-Verknüpfung zu %{kind} kann nicht entfernt werden, da weder ein Passwort gesetzt noch eine andere Identität verknüpft ist
manage_omniauth: Verknüpfte Accounts verwalten
openai_api_key_usage_html: Geben Sie hier Ihren OpenAI-API-Schlüssel ein, um verschiedene KI-Funktionen innerhalb der Plattform zu nutzen. Sie können einen API-Schlüssel auf der <a href='%{openai_api_link}' target='_blank' rel='noopener noreferrer'>OpenAI-Website</a> erstellen.
remove_identity: Account-Verknüpfung zu %{kind} entfernen
shared:
notification_modal:
Expand All @@ -51,6 +52,9 @@ de:
delete_modal:
title: Warnung
full_name: Vollständiger Name
openai_api_key: OpenAI API-Schlüssel
openai_api_key_not_provided: Nicht eingegeben
openai_api_key_provided: Eingegeben
private_information: Private Informationen
public_information: Öffentliche Informationen
send_message: Nachricht senden
2 changes: 0 additions & 2 deletions config/locales/en/controllers/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,4 @@ en:
task_found: 'A corresponding task has been found on the external app. You can: <ul><li><b>Overwrite</b> the task on the external app. This will transfer all changes made on CodeHarbor to the external app.<br>Careful: This will overwrite all potential changes made on the external app. This will change (and might break) the task, even if it is currently in use by a course.</li><li><b>Create a new</b> task which copies the current state of this task. This will create a copy of this task on CodeHarbor, which will then be exported as a completely new exercise to the external app.</li></ul>'
task_found_no_right: 'A corresponding task has been found on external app, but you don''t have the rights to edit it. You can: <ul><li><b>Create a new</b> task which copies the current state of this task. This will create a copy of this task on CodeHarbor, which will then be exported as a completely new task to the external app.</li></ul>'
gpt_generate_tests:
invalid_description: The task description provided does not represent a valid programming task and therefore cannot be used to generate a unit test. Please make sure that the task description contains a clearly formulated problem that can be solved by a program.
no_language: Programming language is not specified for this task. Please specify the language before proceeding.
successful_generation: Unit test generated successfully. Please check the generated test and assign an appropriate filename.
9 changes: 9 additions & 0 deletions config/locales/en/errors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
en:
errors:
gpt:
error: An error occurred while generating the unit test. Please try again later.
internal_server_error: The OpenAI server is currently experiencing an outage. Please try again later when the issue is fixed.
invalid_api_key: The API key in your profile is invalid. Please add the appropriate API key in your profile to use the AI features.
invalid_task_description: The task description provided does not represent a valid programming task and therefore cannot be used to generate a unit test. Please make sure that the task description contains a clearly formulated problem that can be solved by a program.
missing_language: Programming language is not specified for this task. Please specify the language before proceeding.
1 change: 1 addition & 0 deletions config/locales/en/models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ en:
avatar:
not_an_image: needs to be an image
size_over_10_mb: size needs to be less than 10MB
invalid_api_key: The API key in your profile is invalid. Please add the appropriate API key in your profile to use the AI features.
models:
account_link:
one: Account Link
Expand Down
1 change: 1 addition & 0 deletions config/locales/en/views/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ en:
add_to_collection_hint: Save Tasks for later by adding them to a collection.
button:
add_to_collection: Add to Collection
api_key_required: OpenAI API key is required to generate a test
create_collection: Create new Collection
download_as_zip: Download this Task as a ZIP file.
export: Export
Expand Down
4 changes: 4 additions & 0 deletions config/locales/en/views/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ en:
add_identity: Link %{kind} account
cannot_remove_last_identity: Cannot remove account link to %{kind} because neither a password is set nor another identity is linked
manage_omniauth: Manage linked accounts
openai_api_key_usage_html: Enter your OpenAI API key here to use various AI features within the platform. You can create an API key on the <a href='%{openai_api_link}' target='_blank' rel='noopener noreferrer'>OpenAI website</a>.
remove_identity: Remove account link to %{kind}
shared:
notification_modal:
Expand All @@ -51,6 +52,9 @@ en:
delete_modal:
title: Warning
full_name: Full name
openai_api_key: OpenAI API Key
openai_api_key_not_provided: Not entered
openai_api_key_provided: Entered
private_information: Private Information
public_information: Public Information
send_message: Send Message
1 change: 0 additions & 1 deletion config/settings/development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ omniauth:
oai_pmh:
admin_mail: [email protected]
open_ai:
access_token: ~ # Add a valid API key from https://platform.openai.com/api-keys
model: gpt-4o-mini
1 change: 0 additions & 1 deletion config/settings/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ omniauth:
oai_pmh:
admin_mail: [email protected]
open_ai:
access_token: ~ # Add a valid API key from https://platform.openai.com/api-keys
model: gpt-4o-mini
1 change: 0 additions & 1 deletion config/settings/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,4 @@ nbp:
name: CodeHarbor
slug: CoHaP2
open_ai:
access_token: ~ # Add a valid API key from https://platform.openai.com/api-keys
model: gpt-4o-mini
7 changes: 7 additions & 0 deletions db/migrate/20240703221801_add_openai_api_key_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddOpenaiApiKeyToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :openai_api_key, :string
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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_06_09_104041) do
ActiveRecord::Schema[7.1].define(version: 2024_07_03_221801) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
Expand Down Expand Up @@ -430,6 +430,7 @@
t.string "preferred_locale"
t.boolean "password_set", default: true, null: false
t.integer "status_group", limit: 2, default: 0, null: false, comment: "Used as enum in Rails"
kkoehn marked this conversation as resolved.
Show resolved Hide resolved
t.string "openai_api_key"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
Expand Down
Loading
Loading