From 62509c19837bc22712ffe0cc523fc2449543edeb Mon Sep 17 00:00:00 2001 From: nicolas-entourage <75681929+nicolas-entourage@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:01:37 +0100 Subject: [PATCH] EN-7354 add openai_request.module_type to allow to work with different types of assistants --- .../admin/openai_requests_controller.rb | 6 ++- app/jobs/openai_request_job.rb | 35 ++----------- app/models/concerns/matchable.rb | 2 +- app/models/openai_request.rb | 36 +++++++++++--- .../openai_services/basic_performer.rb | 49 +++++++++++++------ .../openai_services/basic_response.rb | 36 ++++++++++++++ .../openai_services/matching_performer.rb | 29 +++++++---- .../openai_services/matching_response.rb | 33 +------------ .../admin/openai_requests/index.html.erb | 8 +++ ...2000_add_module_type_to_openai_requests.rb | 5 ++ db/schema.rb | 4 ++ 11 files changed, 148 insertions(+), 95 deletions(-) create mode 100644 app/services/openai_services/basic_response.rb create mode 100644 db/migrate/20241217102000_add_module_type_to_openai_requests.rb 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 3c5f5bbc1..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(instance: instance).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/concerns/matchable.rb b/app/models/concerns/matchable.rb index 05459dd0b..0731fe05e 100644 --- a/app/models/concerns/matchable.rb +++ b/app/models/concerns/matchable.rb @@ -41,7 +41,7 @@ def on_save def ensure_openai_request_exists! return if @instance.openai_request - @instance.build_openai_request.save! + @instance.build_openai_request(module_type: :matching).save! end end end 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 b4beb339d..9d9d66a31 100644 --- a/app/services/openai_services/basic_performer.rb +++ b/app/services/openai_services/basic_performer.rb @@ -1,19 +1,19 @@ module OpenaiServices class BasicPerformer - attr_reader :configuration, :client, :callback, :assistant_id, :instance + attr_reader :configuration, :client, :callback, :assistant_id, :openai_request, :instance class BasicPerformerCallback < Callback end - def initialize instance: + def initialize openai_request: + @openai_request = openai_request + @instance = @openai_request.instance @callback = BasicPerformerCallback.new - @configuration = get_configuration + @configuration = OpenaiAssistant.find_by_module_type(@openai_request.module_type) @client = OpenAI::Client.new(access_token: @configuration.api_key) @assistant_id = @configuration.assistant_id - - @instance = instance end def perform @@ -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 @@ -71,11 +71,6 @@ def find_run_message thread_id, run_id private - # OpenaiAssistant.find_by_version(?) - def get_configuration - raise NotImplementedError, "this method get_configuration has to be defined in your class" - end - # format: { role: string, content: { type: "text", text: string }} def user_message raise NotImplementedError, "this method user_message has to be defined in your class" @@ -85,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 a8dc2fca7..c9a5e5899 100644 --- a/app/services/openai_services/matching_performer.rb +++ b/app/services/openai_services/matching_performer.rb @@ -5,16 +5,6 @@ class MatchingPerformer < BasicPerformer class MatcherCallback < Callback end - def initialize instance: - super(instance: instance) - - @user = instance.user - end - - def get_configuration - OpenaiAssistant.find_by_module_type(:matching) - end - def user_message { role: "user", @@ -31,6 +21,25 @@ 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 + def get_formatted_prompt action_type = opposite_action_type = instance.class.name.camelize.downcase 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 @@