Skip to content

Commit

Permalink
implement openAI (#516)
Browse files Browse the repository at this point in the history
  • Loading branch information
usernaimandrey committed Apr 19, 2023
1 parent 9e1e3bb commit f4facf6
Show file tree
Hide file tree
Showing 22 changed files with 246 additions and 6 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
EMAIL_FROM=local@local.local
GITHUB_APP_SECRET=1cbaa8d7eaa9897204615ae07383791bf3c6113d
GITHUB_APP_ID=4bf7b244da515541298a
OPENAI_ACCESS_TOKEN=1cbdfyggfhhrtfvddjfj024lfh37fgen65fjgb

RECAPTCHA_SITE_KEY=1111key2222
RECAPTCHA_SECRET_KEY='$eCr3T'

HOST=localhost

OPENAI_ACCESS_TOKEN = 1235fff12345
PASSWORD_SPECIAL_USER=
EMAIL_SPECIAL_USER=
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
HOST=www.example.com
EMAIL_SPECIAL_USER=info@hexlet.io
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ gem 'devise'
gem 'devise-bootstrap-views'
gem 'devise-i18n'
gem 'dotenv-rails'
gem 'dry-container'
gem 'enumerize'
gem 'flash_rails_messages'
gem 'geocoder'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ GEM
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
dry-container (0.11.0)
concurrent-ruby (~> 1.0)
e2mmap (0.1.0)
enumerize (2.6.1)
activesupport (>= 3.2)
Expand Down Expand Up @@ -491,6 +493,7 @@ DEPENDENCIES
devise-bootstrap-views
devise-i18n
dotenv-rails
dry-container
enumerize
factory_bot_rails
faker
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/web/account/resumes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def create

if @resume.save
change_visibility(@resume)
OpenAiJob.perform_later(@resume.id) if @resume.published? && !@resume.evaluated_ai?
f(:success)
redirect_to account_resumes_path
else
Expand All @@ -35,6 +36,7 @@ def update
resume = @resume.becomes(Web::Account::ResumeForm)
if resume.update(params[:resume])
change_visibility(@resume)
OpenAiJob.perform_later(@resume.id) if @resume.published? && !@resume.evaluated_ai?
f(:success)
redirect_to account_resumes_path
else
Expand Down
12 changes: 12 additions & 0 deletions app/jobs/open_ai_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class OpenAiJob < ApplicationJob
queue_as :default

retry_on StandardError, wait: 5.seconds, attempts: 3

def perform(resume_id)
resume = Resume.find(resume_id)
ResumeAutoAnswerService.evaluate_resume(resume)
end
end
11 changes: 11 additions & 0 deletions app/lib/application_container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class ApplicationContainer
extend Dry::Container::Mixin

if Rails.env.test?
register(:open_ai_helper, memoize: true) { OpenAiHelperStub.new }
else
register(:open_ai_helper, memoize: true) { OpenAiHelper.new }
end
end
32 changes: 32 additions & 0 deletions app/lib/open_ai_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

class OpenAiHelper
attr_reader :client

MODEL = 'gpt-3.5-turbo'
TEMPERATURE = 0.7

def initialize
@client = OpenAI::Client.new
end

def send_content(prompt, resume_content)
response = chat([{ role: 'user', content: prompt }, { role: 'user', content: resume_content }])

raise StandardError, response['error']['message'] if response['error']

response
end

private

def chat(messages = [])
client.chat(
parameters: {
model: MODEL,
messages:,
temperature: TEMPERATURE
}
)
end
end
21 changes: 21 additions & 0 deletions app/lib/open_ai_helper_stub.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

class OpenAiHelperStub
def initialize(*); end

def send_content(*)
chat
end

private

def chat(*)
{ 'choices' =>
[{ 'message' =>
{ 'role' => 'assistant',
'content' =>
'Резюме выглядит хорошо структурированным и содержит подробное описание проектов и навыков, которые соответствуют требованиям junior backend-разработчика (Ruby on Rails). Важно, что автор указывает желание профессионального развития, ревью кода и наска, что свидетельствует о его готовности к обучению и улучшению своих навыков. Единственное, что можно улучшить - добавить информацию о своих личных качествах и достижениях в области программирования.' },
'finish_reason' => 'stop',
'index' => 0 }] }
end
end
2 changes: 1 addition & 1 deletion app/models/resume/answer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Resume::Answer < ApplicationRecord
include Resume::AnswerRepository
# FIXME: add unique index
validates :resume, uniqueness: { scope: :user }
validates :content, presence: true, length: { minimum: 20 }
validates :content, presence: true, length: { minimum: 10 }

belongs_to :resume, counter_cache: true
belongs_to :user
Expand Down
37 changes: 37 additions & 0 deletions app/services/resume_auto_answer_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

class ResumeAutoAnswerService
class << self
def evaluate_resume(resume)
client = ApplicationContainer[:open_ai_helper]
resume_summary = prepare_resume(resume)
response_recommendation = client.send_content(I18n.t('open_ai_command.evaluate_resume'), resume_summary)
response_covering_letter = client.send_content(I18n.t('open_ai_command.write_cover_letter'), resume_summary)
response_edited_text = client.send_content(I18n.t('open_ai_command.edit_text'), resume_summary)

content_recommendation = response_recommendation.dig('choices', 0, 'message', 'content')
content_covering_letter = response_covering_letter.dig('choices', 0, 'message', 'content')
content_edited_text = response_edited_text.dig('choices', 0, 'message', 'content')

content = I18n.t('recommendation_open_ai', recommendation: content_recommendation, letter: content_covering_letter, edit_text: content_edited_text, scope: 'web.answers')

ActiveRecord::Base.transaction do
user = User.find_by(email: ENV.fetch('EMAIL_SPECIAL_USER'))
attrs = { content: }
Resume::AnswerMutator.create(resume, attrs, user)
resume.evaluated_ai = true
resume.save!
end
end

def prepare_resume(resume)
resume
.serializable_hash
.deep_symbolize_keys
.slice(:name, :summary, :skills_description, :awards_description, :contact_phone, :contact_email)
.values
.join('\n')
end
end
private_class_method :prepare_resume
end
13 changes: 13 additions & 0 deletions config/locales/en.views.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,16 @@ en:
users:
edit:
header: Edit user
answers:
recommendation_open_ai: |
### Improvement recommendation:
%{recommendation}
### Cover letter:
%{letter}
### Editing:
%{edit_text}"
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
en:
recommendation_open_ai: "### Improvement recommendation:\n %{recommendation}\n### Cover letter:\n %{letter}\n### Editing:\n %{edit_text}"
application_name: Hexlet CV
in_the_city: "in the city %{city_name}"
months_nominative_case:
Expand Down Expand Up @@ -61,3 +62,7 @@ en:
previous: " Prev"
next: "Next "
truncate: "&hellip;"
open_ai_command:
evaluate_resume: resume evaluation
write_cover_letter: write a cover letter
edit_text: edit the text
13 changes: 12 additions & 1 deletion config/locales/ru.views.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ ru:
remove_work: Удалить опыт работы
education_fields:
remove_education: Удалить образование
answer:
add_a_comment: Добавить комментарий
apply: Принять
title_apply: Принять рекомендации
Expand Down Expand Up @@ -236,6 +235,18 @@ ru:
few: Просмотра
many: Просмотров
answers:
recommendation_open_ai: |
### Рекомендация по улучшению:
%{recommendation}
### Сопроводительное письмо:
%{letter}
### Редактура:
%{edit_text}"
comments:
new:
header: Создание комментария
Expand Down
4 changes: 4 additions & 0 deletions config/locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ ru:
next: "Следующая "
previous: " Предыдущая"
truncate: "&hellip;"
open_ai_command:
evaluate_resume: оцени резюме
write_cover_letter: напиши сопроводительное письмо
edit_text: проведи редактуру текста
5 changes: 5 additions & 0 deletions db/migrate/20230416145907_add_evaluated_open_ai_to_resumes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddEvaluatedOpenAiToResumes < ActiveRecord::Migration[7.0]
def change
add_column :resumes, :evaluated_ai, :boolean
end
end
44 changes: 43 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,34 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_04_07_133244) do
ActiveRecord::Schema[7.0].define(version: 2023_04_16_145907) do
create_table "career_items", force: :cascade do |t|
t.integer "order"
t.string "career_id"
t.string "step_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

create_table "career_members", force: :cascade do |t|
t.integer "user_id"
t.integer "career_id"
t.datetime "finished_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

create_table "careers", force: :cascade do |t|
t.string "name"
t.text "description"
t.string "direction"
t.string "locale"
t.integer "creator_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["creator_id"], name: "index_careers_on_creator_id"
end

create_table "countries", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
Expand Down Expand Up @@ -152,9 +179,22 @@
t.string "contact_phone"
t.string "contact_email"
t.string "contact_telegram"
t.boolean "evaluated_ai"
t.index ["user_id"], name: "index_resumes_on_user_id"
end

create_table "steps", force: :cascade do |t|
t.string "name"
t.text "description"
t.text "tasks"
t.boolean "review_needed"
t.integer "creator_id", null: false
t.string "locale"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["creator_id"], name: "index_steps_on_creator_id"
end

create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.string "taggable_type"
Expand Down Expand Up @@ -269,6 +309,7 @@
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
end

add_foreign_key "careers", "users", column: "creator_id"
add_foreign_key "notifications", "users"
add_foreign_key "resume_answer_comments", "resume_answers", column: "answer_id"
add_foreign_key "resume_answer_comments", "resumes"
Expand All @@ -282,6 +323,7 @@
add_foreign_key "resume_educations", "resumes"
add_foreign_key "resume_works", "resumes"
add_foreign_key "resumes", "users"
add_foreign_key "steps", "users", column: "creator_id"
add_foreign_key "taggings", "tags"
add_foreign_key "vacancies", "countries"
add_foreign_key "vacancies", "users", column: "creator_id"
Expand Down
3 changes: 3 additions & 0 deletions make-compose.mk
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ compose-test:

compose-ci-check:
docker-compose run --rm app make ci-setup-check

compose-rails:
docker-compose run --rm app bin/rails $(T)
17 changes: 15 additions & 2 deletions test/controllers/web/account/resumes_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,27 @@ class Web::Account::ResumesControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end

test '#create' do
test '#create and published' do
user_special = users(:special)
attrs = FactoryBot.attributes_for :resume
education_attrs = attrs[:educations_attributes].first
works_attrs = attrs[:works_attributes].first
params = {
publish: true,
resume: attrs
}

post account_resumes_path, params: { resume: attrs }
post(account_resumes_path, params:)
assert_response :redirect

resume = Resume.find_by(name: attrs[:name])

last_answer = resume.answers.last
assert { resume }
assert { resume.educations.exists?(description: education_attrs[:description]) }
assert { resume.works.exists?(company: works_attrs[:company]) }
assert { resume.evaluated_ai? }
assert { last_answer.user_id == user_special.id }
end

test '#edit' do
Expand All @@ -39,6 +48,7 @@ class Web::Account::ResumesControllerTest < ActionDispatch::IntegrationTest
end

test '#update' do
user_special = users(:special)
resume = resumes(:one)
work = resume_works(:one)

Expand All @@ -50,8 +60,11 @@ class Web::Account::ResumesControllerTest < ActionDispatch::IntegrationTest

resume.reload
work.reload
last_answer = resume.answers.last
assert { resume.name == attrs[:name] }
assert { work.company == work_attrs[:company] }
assert { resume.evaluated_ai? }
assert { last_answer.user_id == user_special.id }
end

test 'should publish published resume' do
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ banned:
email: $LABEL@email.com
about: Banned
state: banned

special:
<<: *DEFAULTS
email: info@hexlet.io
about: bot
Loading

0 comments on commit f4facf6

Please sign in to comment.