Skip to content

Commit

Permalink
EN-7354 add openai_request.module_type to allow to work with differen…
Browse files Browse the repository at this point in the history
…t types of assistants
  • Loading branch information
nicolas-entourage committed Dec 17, 2024
1 parent 198790e commit 62509c1
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 95 deletions.
6 changes: 5 additions & 1 deletion app/controllers/admin/openai_requests_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 3 additions & 32 deletions app/jobs/openai_request_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/matchable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Expand All @@ -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
49 changes: 35 additions & 14 deletions app/services/openai_services/basic_performer.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
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
29 changes: 19 additions & 10 deletions app/services/openai_services/matching_performer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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

Expand Down
33 changes: 2 additions & 31 deletions app/services/openai_services/matching_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions app/views/admin/openai_requests/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddModuleTypeToOpenaiRequests < ActiveRecord::Migration[6.1]
def change
add_column :openai_requests, :module_type, :string, default: :matching
end
end
4 changes: 4 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,9 @@
t.integer "days_for_outings", default: 30
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "module_type", default: "matching"
t.integer "max_prompt_tokens", default: 1048576
t.integer "max_completion_tokens", default: 1024
end

create_table "openai_requests", force: :cascade do |t|
Expand All @@ -734,6 +737,7 @@
t.string "instance_class", default: "Entourage"
t.string "response"
t.string "error"
t.string "module_type", default: "matching"
t.index ["instance_type", "instance_id"], name: "index_openai_requests_on_instance_type_and_instance_id", unique: true
end

Expand Down

0 comments on commit 62509c1

Please sign in to comment.