Skip to content

Commit

Permalink
Adding an API Key per user
Browse files Browse the repository at this point in the history
Having an API key per user will allow users to utilize the AI capabilities within CodeHarbor (i.e. Unittest generator)

- The generate unittest button is now always visible
- If key is invalid or missing, an alert is shown to the user
- If key is available and valid, a unittest will be created for the task
  • Loading branch information
Melhaya committed Jul 3, 2024
1 parent 04fc65c commit f6874a5
Show file tree
Hide file tree
Showing 22 changed files with 123 additions and 71 deletions.
4 changes: 3 additions & 1 deletion app/controllers/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,10 @@ def export_external_confirm
# rubocop:enable Metrics/AbcSize

def generate_test
TaskService::GptGenerateTests.call(task: @task)
TaskService::GptGenerateTests.call(task: @task, user: current_user)
flash[:notice] = I18n.t('tasks.task_service.gpt_generate_tests.successful_generation')
rescue Gpt::InvalidApiKeyError
flash[:alert] = I18n.t('tasks.task_service.gpt_generate_tests.invalid_api_key')
rescue Gpt::MissingLanguageError
flash[:alert] = I18n.t('tasks.task_service.gpt_generate_tests.no_language')
rescue Gpt::InvalidTaskDescription
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 personal_openai_api_key])
end

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

module Gpt
class InvalidApiKeyError < StandardError; end
end
1 change: 1 addition & 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]
validates :personal_openai_api_key, allow_blank: true, length: {maximum: 255}

has_many :tasks, dependent: :nullify

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.present? and update?
end

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

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

@task = task
@user = user
@access_token = @user.personal_openai_api_key.presence
validate_api_key!
end

def execute
Expand All @@ -21,7 +24,7 @@ def execute
private

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

def gpt_response
Expand Down Expand Up @@ -64,5 +67,18 @@ def training_prompts
PROMPT
]
end

def validate_api_key!
if @access_token.blank?
raise Gpt::InvalidApiKeyError
else
begin
response = client.models.list
raise Gpt::InvalidApiKeyError unless response['data']
rescue Faraday::UnauthorizedError, OpenAI::Error
raise Gpt::InvalidApiKeyError
end
end
end
end
end
7 changes: 3 additions & 4 deletions app/views/tasks/show.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -573,10 +573,9 @@
i.fa-regular.fa-clone
=< t('common.button.duplicate')

- if policy(@task).generate_test?
= 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')
= 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')

- if current_user.present?
= link_to t('common.button.back'), tasks_path, class: 'btn btn-important'
Expand Down
8 changes: 8 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,14 @@
autocomplete: 'new-password',
class: 'form-control'

.form-group.field-element
= f.label :personal_openai_api_key, class: 'form-label'
= f.text_field :personal_openai_api_key,
required: false,
placeholder: t('users.show.personal_openai_api_key'),
autocomplete: 'off',
class: 'form-control'

= render 'avatar_form', f:

- if resource.password_set?
Expand Down
6 changes: 6 additions & 0 deletions app/views/users/show.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
| :
.col.row-value
= @user.email
.row
.col-auto.row-label
= t('.personal_openai_api_key')
| :
.col.row-value
= @user.personal_openai_api_key
.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.

1 change: 1 addition & 0 deletions config/locales/de/controllers/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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_api_key: Der API-Schlüssel fehlt in Ihrem Profil oder ist ungültig. Bitte fügen Sie den entsprechenden API-Schlüssel in Ihrem Profil hinzu, um diese Funktion zu nutzen.
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.
1 change: 1 addition & 0 deletions config/locales/de/views/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ de:
delete_modal:
title: Warnung
full_name: Vollständiger Name
personal_openai_api_key: Persönlicher OpenAI API-Schlüssel
private_information: Private Informationen
public_information: Öffentliche Informationen
send_message: Nachricht senden
1 change: 1 addition & 0 deletions config/locales/en/controllers/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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_api_key: The API key is missing from your profile or is invalid. Please add the appropriate API key in your profile to use this feature.
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.
1 change: 1 addition & 0 deletions config/locales/en/views/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ en:
delete_modal:
title: Warning
full_name: Full name
personal_openai_api_key: Personal OpenAI API Key
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 @@ -11,5 +11,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-3.5-turbo
1 change: 0 additions & 1 deletion config/settings/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,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-3.5-turbo
1 change: 0 additions & 1 deletion config/settings/test.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-3.5-turbo
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddPersonalOpenaiApiKeyToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :personal_openai_api_key, :string
end
end
5 changes: 3 additions & 2 deletions 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_05_31_160738) do
ActiveRecord::Schema[7.1].define(version: 2024_07_02_224053) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
Expand Down Expand Up @@ -326,7 +326,8 @@
t.datetime "locked_at"
t.string "preferred_locale"
t.boolean "password_set", default: true, null: false
t.integer "status_group", limit: 1, default: 0, null: false, comment: "Used as enum in Rails"
t.integer "status_group", limit: 2, default: 0, null: false, comment: "Used as enum in Rails"
t.string "personal_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
30 changes: 22 additions & 8 deletions spec/controllers/tasks_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'rails_helper'
require 'webmock/rspec'

RSpec.describe TasksController do
render_views
Expand All @@ -9,7 +10,6 @@
let(:collection) { create(:collection, users: [user], tasks: []) }
let(:valid_attributes) { {user:, access_level:} }
let(:access_level) { :private }

let(:invalid_attributes) { {title: ''} }

describe 'GET #index' do
Expand Down Expand Up @@ -1043,17 +1043,16 @@
end

describe 'POST #generate_test' do
let(:task_user) { create(:user) }
let(:task_user) { create(:user, personal_openai_api_key: 'valid_api_key') }
let(:access_level) { :public }
let(:task) { create(:task, user: task_user, access_level:) }
let(:mock_client) { instance_double(OpenAI::Client) }
let(:mock_models) { instance_double(OpenAI::Models, list: {'data' => [{'id' => 'text-davinci-002'}]}) }

before do
sign_in task_user
Settings.open_ai.access_token = 'access_token'
end

after do
Settings.open_ai.access_token = nil
allow(OpenAI::Client).to receive(:new).and_return(mock_client)
allow(mock_client).to receive(:models).and_return(mock_models)
end

context 'when GptGenerateTests is successful' do
Expand All @@ -1063,7 +1062,7 @@
end

it 'calls the GptGenerateTests service with the correct task' do
expect(TaskService::GptGenerateTests).to have_received(:call).with(task:)
expect(TaskService::GptGenerateTests).to have_received(:call).with(task:, user: task_user)
end

it 'redirects to the task show page' do
Expand Down Expand Up @@ -1104,5 +1103,20 @@
expect(flash[:alert]).to eq(I18n.t('tasks.task_service.gpt_generate_tests.invalid_description'))
end
end

context 'when GptGenerateTests raises InvalidApiKeyError' do
before do
allow(TaskService::GptGenerateTests).to receive(:call).and_raise(Gpt::InvalidApiKeyError)
post :generate_test, params: {id: task.id}
end

it 'redirects to the task show page' do
expect(response).to redirect_to(task_path(task))
end

it 'sets flash to the appropriate message' do
expect(flash[:alert]).to eq(I18n.t('tasks.task_service.gpt_generate_tests.invalid_api_key'))
end
end
end
end
40 changes: 2 additions & 38 deletions spec/policies/task_policy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,40 +32,12 @@

it { is_expected.to permit_only_actions(generic_user_permissions) }

context 'when user is admin' do
let(:user) { create(:admin) }

context 'without gpt access token' do
it { is_expected.to forbid_only_actions %i[generate_test] }
end

context 'with gpt access token' do
before do
Settings.open_ai.access_token = 'access_token'
end

after do
Settings.open_ai.access_token = nil
end

it { is_expected.to permit_all_actions }
end
end

context 'when task is from user' do
let(:task_user) { user }

context 'without gpt access token' do
it { is_expected.to forbid_only_actions %i[generate_test] }
end

context 'with gpt access token' do
before do
Settings.open_ai.access_token = 'access_token'
end

after do
Settings.open_ai.access_token = nil
allow(user).to receive(:personal_openai_api_key).and_return('access_token')
end

it { is_expected.to permit_all_actions }
Expand All @@ -92,19 +64,11 @@
context 'when user is group-admin' do
let(:role) { :admin }

context 'without gpt access token' do
it { is_expected.to permit_only_actions(group_member_permissions) }
end

context 'with gpt access token' do
let(:group_member_permissions) { generic_user_permissions + %i[edit update duplicate show export_external_start export_external_check export_external_confirm download add_to_collection duplicate generate_test] }

before do
Settings.open_ai.access_token = 'access_token'
end

after do
Settings.open_ai.access_token = nil
allow(user).to receive(:personal_openai_api_key).and_return('access_token')
end

it { is_expected.to permit_only_actions(group_member_permissions) }
Expand Down
Loading

0 comments on commit f6874a5

Please sign in to comment.