7ab46f907f
Reviewed-on: #368 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
169 行
4.7 KiB
Ruby
169 行
4.7 KiB
Ruby
module Gekanator
|
|
class QuestionSuggestionAiConverter
|
|
# Temporary heuristic converter for #361.
|
|
# This creates pending ai_generated questions without external LLM calls;
|
|
# accepted questions are still distributed only after admin approval.
|
|
TITLE_LENGTH_RE = /\Aタイトルは\s*(\d+)\s*文字以上[??]\z/
|
|
ORIGINAL_YEAR_RE = /\Aオリジナルの投稿年は\s*(\d{4})\s*年[??]\z/
|
|
ORIGINAL_MONTH_RE = /\Aオリジナルの投稿月は\s*(\d{1,2})\s*月[??]\z/
|
|
ORIGINAL_MONTH_DAY_RE = /\Aオリジナルの投稿日は\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日[??]\z/
|
|
TITLE_CONTAINS_RE = /\A題名に「(.+?)」が含まれる[??]\z/
|
|
SOURCE_RE = /\A(.+?)\s+の投稿を思[ひい]浮かべて[ゐい]る[??]\z/
|
|
|
|
def self.call(...) = new(...).call
|
|
|
|
def initialize suggestion:, user:
|
|
@suggestion = suggestion
|
|
@user = user
|
|
end
|
|
|
|
def call
|
|
suggestion.with_lock do
|
|
existing = existing_generated_question
|
|
return existing if existing
|
|
|
|
run = suggestion.gekanator_ai_runs.create!(
|
|
model: 'heuristic_converter_v1',
|
|
status: 'running',
|
|
input_tokens: 0,
|
|
output_tokens: 0,
|
|
estimated_cost_jpy: 0)
|
|
|
|
question_attributes = build_question
|
|
question =
|
|
question_attributes &&
|
|
GekanatorQuestion.create!(
|
|
**question_attributes,
|
|
source: 'ai_generated',
|
|
status: 'pending',
|
|
gekanator_question_suggestion: suggestion,
|
|
created_by: user)
|
|
|
|
run.update!(status: question ? 'succeeded' : 'failed')
|
|
question
|
|
end
|
|
rescue => error
|
|
run&.update!(status: 'failed') if run&.persisted? && run.status != 'failed'
|
|
raise error
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :suggestion, :user
|
|
|
|
def existing_generated_question
|
|
suggestion
|
|
.gekanator_questions
|
|
.where(source: 'ai_generated')
|
|
.order(id: :desc)
|
|
.first
|
|
end
|
|
|
|
def build_question
|
|
text = normalized_text
|
|
return nil if text.blank?
|
|
|
|
structured_question_for(text) || post_similarity_question_for(text)
|
|
end
|
|
|
|
def normalized_text
|
|
suggestion.question_text.to_s.gsub(/[[:space:]]+/, ' ').strip
|
|
end
|
|
|
|
def structured_question_for text
|
|
case text
|
|
when TITLE_LENGTH_RE
|
|
length = Regexp.last_match(1).to_i
|
|
return nil if length <= 0
|
|
|
|
{
|
|
text:,
|
|
kind: 'title',
|
|
condition: {
|
|
type: 'title-length-at-least',
|
|
length:
|
|
},
|
|
priority_weight: 0.95
|
|
}
|
|
when /\A題名に英数字が混じって[ゐい]る[??]\z/
|
|
{
|
|
text: '題名に英数字が混じってゐる?',
|
|
kind: 'title',
|
|
condition: { type: 'title-has-ascii' },
|
|
priority_weight: 0.95
|
|
}
|
|
when ORIGINAL_YEAR_RE
|
|
year = Regexp.last_match(1).to_i
|
|
{
|
|
text:,
|
|
kind: 'original_date',
|
|
condition: { type: 'original-year', year: },
|
|
priority_weight: 0.95
|
|
}
|
|
when ORIGINAL_MONTH_RE
|
|
month = Regexp.last_match(1).to_i
|
|
return nil unless month.between?(1, 12)
|
|
|
|
{
|
|
text:,
|
|
kind: 'original_date',
|
|
condition: { type: 'original-month', month: },
|
|
priority_weight: 0.95
|
|
}
|
|
when ORIGINAL_MONTH_DAY_RE
|
|
month = Regexp.last_match(1).to_i
|
|
day = Regexp.last_match(2).to_i
|
|
return nil unless month.between?(1, 12) && day.between?(1, 31)
|
|
|
|
{
|
|
text:,
|
|
kind: 'original_date',
|
|
condition: {
|
|
type: 'original-month-day',
|
|
monthDay: "#{ month }-#{ day }"
|
|
},
|
|
priority_weight: 0.95
|
|
}
|
|
when TITLE_CONTAINS_RE
|
|
title_text = Regexp.last_match(1).to_s.strip
|
|
return nil if title_text.blank?
|
|
|
|
{
|
|
text: "題名に「#{ title_text }」が含まれる?",
|
|
kind: 'title',
|
|
condition: { type: 'title-contains', text: title_text },
|
|
priority_weight: 0.95
|
|
}
|
|
when SOURCE_RE
|
|
host = Regexp.last_match(1).to_s.strip
|
|
return nil if host.blank?
|
|
|
|
{
|
|
text:,
|
|
kind: 'source',
|
|
condition: { type: 'source', host: },
|
|
priority_weight: 0.95
|
|
}
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def post_similarity_question_for text
|
|
return nil if suggestion.answer == 'unknown'
|
|
|
|
{
|
|
text:,
|
|
kind: 'post_similarity',
|
|
condition: {
|
|
type: 'post-similarity',
|
|
postId: suggestion.gekanator_game.correct_post_id,
|
|
answer: suggestion.answer,
|
|
threshold: 0.65
|
|
},
|
|
priority_weight: 1.0
|
|
}
|
|
end
|
|
end
|
|
end
|