diff --git a/app/controllers/admin/openai_requests_controller.rb b/app/controllers/admin/openai_requests_controller.rb index 822032393..d8ed877e3 100644 --- a/app/controllers/admin/openai_requests_controller.rb +++ b/app/controllers/admin/openai_requests_controller.rb @@ -5,7 +5,11 @@ class OpenaiRequestsController < Admin::BaseController before_action :set_openai_request, only: [:show] def index - @openai_requests = OpenaiRequest + @params = params.permit(:module_type) + @module_type = params[:module_type] || :matching + + @openai_requests = OpenaiRequest.includes(:instance) + .where(module_type: @module_type) .order(updated_at: :desc) .page(page) .per(per) diff --git a/app/jobs/openai_request_job.rb b/app/jobs/openai_request_job.rb index 18a9521ae..3529bcaf1 100644 --- a/app/jobs/openai_request_job.rb +++ b/app/jobs/openai_request_job.rb @@ -8,39 +8,10 @@ class OpenaiRequestJob def perform openai_request_id openai_request = OpenaiRequest.find(openai_request_id) - return unless instance = openai_request.instance + # cancel performer whenever instance is null + return unless openai_request.instance - OpenaiServices::MatchingPerformer.new(openai_request: openai_request).perform do |on| - on.success do |response| - openai_request.update_columns( - error: nil, - response: response.to_json, - openai_assistant_id: response.metadata[:assistant_id], - openai_thread_id: response.metadata[:thread_id], - openai_run_id: response.metadata[:run_id], - openai_message_id: response.metadata[:message_id], - status: :success, - run_ends_at: Time.current, - updated_at: Time.current - ) - - response.each_recommandation do |matching, score, explanation, index| - instance.matchings.build(match: matching, score: score, explanation: explanation, position: index) - end - - instance.save(validate: false) - end - - on.failure do |error, response| - openai_request.update_columns( - error: error, - response: response.to_json, - status: :error, - run_ends_at: Time.current, - updated_at: Time.current - ) - end - end + openai_request.performer_instance.perform end def self.perform_later openai_request_id diff --git a/app/models/openai_request.rb b/app/models/openai_request.rb index a75f0c433..ab827e5d0 100644 --- a/app/models/openai_request.rb +++ b/app/models/openai_request.rb @@ -1,6 +1,16 @@ class OpenaiRequest < ApplicationRecord belongs_to :instance, polymorphic: true + # when adding a new module_type, it would be required to: + # 1. create a new openai_assistant instance + # 2. create a class that inherits from BasicPerformer. Check MatchingPerformer for example + # 3. add this class to performer_instance method + # 4. create a response class that inherits from BasicResponse. Check MatchingResponse for evample + # 5. add this class to performer_response method + enum module_type: { + matching: 'matching' + } + after_commit :run, on: :create def instance @@ -24,17 +34,25 @@ def thread_link end def response_valid? - matching_response.valid? + response_instance.valid? end def formatted_response - matching_response.parsed_response + response_instance.parsed_response + end + + # add module_type case if needed + def response_instance + @response_instance ||= begin + if matching? + OpenaiServices::MatchingResponse.new(response: safe_json_parse(response)) + end + end end - def matching_response - @matching_response ||= OpenaiServices::MatchingResponse.new(response: JSON.parse(response)) - rescue - @matching_response ||= OpenaiServices::MatchingResponse.new(response: Hash.new) + # add module_type case if needed + def performer_instance + return OpenaiServices::MatchingPerformer.new(openai_request: self) if matching? end attr_accessor :forced_matching @@ -42,4 +60,10 @@ def matching_response def run OpenaiRequestJob.perform_later(id) end + + def safe_json_parse json_string + JSON.parse(json_string) + rescue JSON::ParserError + {} + end end diff --git a/app/services/openai_services/basic_performer.rb b/app/services/openai_services/basic_performer.rb index 8fd6f6753..9d9d66a31 100644 --- a/app/services/openai_services/basic_performer.rb +++ b/app/services/openai_services/basic_performer.rb @@ -35,15 +35,15 @@ def perform # wait for completion status = status_loop(thread['id'], run['id']) - return callback.on_failure.try(:call, "Failure status #{status}") unless ['completed', 'requires_action'].include?(status) + return handle_failure("Failure status #{status}") unless ['completed', 'requires_action'].include?(status) response = get_response_class.new(response: find_run_message(thread['id'], run['id'])) - return callback.on_failure.try(:call, "Response not valid", response) unless response.valid? + return handle_failure("Response not valid", response) unless response.valid? - callback.on_success.try(:call, response) + handle_success(response) rescue => e - callback.on_failure.try(:call, e.message, nil) + handle_failure(e.message) end def status_loop thread_id, run_id @@ -80,5 +80,31 @@ def user_message def get_response_class raise NotImplementedError, "this method get_response_class has to be defined in your class" end + + def handle_success response + openai_request.update_columns( + error: nil, + response: response.to_json, + openai_assistant_id: response.metadata[:assistant_id], + openai_thread_id: response.metadata[:thread_id], + openai_run_id: response.metadata[:run_id], + openai_message_id: response.metadata[:message_id], + status: :success, + run_ends_at: Time.current, + updated_at: Time.current + ) + callback.on_success.try(:call, response) + end + + def handle_failure error, response = nil + openai_request.update_columns( + error: error, + response: response&.to_json, + status: :error, + run_ends_at: Time.current, + updated_at: Time.current + ) + callback.on_failure.try(:call, error, response) + end end end diff --git a/app/services/openai_services/basic_response.rb b/app/services/openai_services/basic_response.rb new file mode 100644 index 000000000..a5ac4f02b --- /dev/null +++ b/app/services/openai_services/basic_response.rb @@ -0,0 +1,36 @@ +module OpenaiServices + class BasicResponse + def initialize response: nil + @response = response + @parsed_response = parsed_response + end + + def valid? + raise NotImplementedError, "this method valid? has to be defined in your class" + end + + def parsed_response + return unless @response + return unless content = @response["content"] + return unless content.any? && first_content = content[0] + return unless first_content["type"] == "text" + return unless value = first_content["text"]["value"]&.gsub("\n", "") + return unless json = value[/\{.*\}/m] + + JSON.parse(json) + end + + def to_json + @response.to_json + end + + def metadata + { + message_id: @response["id"], + assistant_id: @response["assistant_id"], + thread_id: @response["thread_id"], + run_id: @response["run_id"] + } + end + end +end diff --git a/app/services/openai_services/matching_performer.rb b/app/services/openai_services/matching_performer.rb index e0a009413..c9a5e5899 100644 --- a/app/services/openai_services/matching_performer.rb +++ b/app/services/openai_services/matching_performer.rb @@ -21,6 +21,21 @@ def get_response_class private + def handle_success(response) + super(response) + + response.each_recommandation do |matching, score, explanation, index| + openai_request.instance.matchings.build( + match: matching, + score: score, + explanation: explanation, + position: index + ) + end + + openai_request.instance.save(validate: false) + end + def user @user ||= instance.user end diff --git a/app/services/openai_services/matching_response.rb b/app/services/openai_services/matching_response.rb index 052b512a9..0916cdc76 100644 --- a/app/services/openai_services/matching_response.rb +++ b/app/services/openai_services/matching_response.rb @@ -6,52 +6,23 @@ module OpenaiServices # "id"=>"e8bWJqPHAcxY", # "name"=>"Sophie : les portraits des bénévoles", # "score"=>"0.96", - # "explanation"=>"Ce ressource présente des histoires de bénévoles et peut vous inspirer pour obtenir de l'aide." + # "explanation"=>"Cette ressource présente des histoires de bénévoles et peut vous inspirer pour obtenir de l'aide." # }] # } - MatchingResponse = Struct.new(:response) do + class MatchingResponse < BasicResponse TYPES = %w{contribution solicitation outing resource poi} - def initialize(response: nil) - @response = response - @parsed_response = parsed_response - end - def valid? recommandations.any? end - def parsed_response - return unless @response - return unless content = @response["content"] - return unless content.any? && first_content = content[0] - return unless first_content["type"] == "text" - return unless value = first_content["text"]["value"]&.gsub("\n", "") - return unless json = value[/\{.*\}/m] - - JSON.parse(json) - end - - def to_json - @response.to_json - end - def recommandations return [] unless @parsed_response @parsed_response["recommandations"] end - def metadata - { - message_id: @response["id"], - assistant_id: @response["assistant_id"], - thread_id: @response["thread_id"], - run_id: @response["run_id"] - } - end - def best_recommandation each_recommandation do |instance, score, explanation, index| return { diff --git a/app/views/admin/openai_requests/index.html.erb b/app/views/admin/openai_requests/index.html.erb index 4e49a122c..f4bfd42ee 100644 --- a/app/views/admin/openai_requests/index.html.erb +++ b/app/views/admin/openai_requests/index.html.erb @@ -2,6 +2,14 @@