Skip to content

Commit

Permalink
Merge branch 'EN-7354-openai-matching-clean' into staging
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-entourage committed Dec 17, 2024
2 parents d4f60cd + 62509c1 commit c9210f7
Showing 8 changed files with 129 additions and 74 deletions.
6 changes: 5 additions & 1 deletion app/controllers/admin/openai_requests_controller.rb
Original file line number Diff line number Diff line change
@@ -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)
35 changes: 3 additions & 32 deletions app/jobs/openai_request_job.rb
Original file line number Diff line number Diff line change
@@ -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
36 changes: 30 additions & 6 deletions app/models/openai_request.rb
Original file line number Diff line number Diff line change
@@ -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,22 +34,36 @@ 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

def run
OpenaiRequestJob.perform_later(id)
end

def safe_json_parse json_string
JSON.parse(json_string)
rescue JSON::ParserError
{}
end
end
34 changes: 30 additions & 4 deletions app/services/openai_services/basic_performer.rb
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions app/services/openai_services/basic_response.rb
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions app/services/openai_services/matching_performer.rb
Original file line number Diff line number Diff line change
@@ -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
33 changes: 2 additions & 31 deletions app/services/openai_services/matching_response.rb
Original file line number Diff line number Diff line change
@@ -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 {
8 changes: 8 additions & 0 deletions app/views/admin/openai_requests/index.html.erb
Original file line number Diff line number Diff line change
@@ -2,6 +2,14 @@
<div style="margin: auto">
<h1>openai_requests</h1>

<ul class="nav nav-tabs">
<% OpenaiAssistant.pluck(:module_type).each do |module_type| %>
<li role="presentation" class="<%= :active if @module_type == module_type.to_s %>">
<%= link_to module_type, @params.merge(module_type: module_type) %>
</li>
<% end %>
</ul>

<div class="row">
<% unless @openai_requests.none? %>
<table class="table custom-table">

0 comments on commit c9210f7

Please sign in to comment.