このコミットが含まれているのは:
@@ -12,9 +12,35 @@ class GekanatorQuestionSuggestionsController < ApplicationController
|
||||
answer: params.require(:answer))
|
||||
|
||||
if suggestion.save
|
||||
render json: { id: suggestion.id }, status: :created
|
||||
render json: {
|
||||
id: suggestion.id,
|
||||
count: game.question_suggestions.count
|
||||
}, status: :created
|
||||
else
|
||||
render_validation_error suggestion
|
||||
end
|
||||
end
|
||||
|
||||
def ai_convert
|
||||
return head :not_found unless current_user&.admin?
|
||||
|
||||
suggestion = GekanatorQuestionSuggestion.find_by(id: params[:id])
|
||||
return head :not_found unless suggestion
|
||||
if Gekanator::AiRunBudget.exceeded_after_next_run?
|
||||
suggestion.gekanator_ai_runs.create!(
|
||||
model: 'budget_guard',
|
||||
status: 'blocked_budget',
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
estimated_cost_jpy: 0)
|
||||
return head :payment_required
|
||||
end
|
||||
|
||||
Gekanator::QuestionSuggestionAiConverter.call(
|
||||
suggestion: suggestion,
|
||||
user: current_user)
|
||||
head :no_content
|
||||
rescue NotImplementedError
|
||||
head :not_implemented
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
class GekanatorQuestionsController < ApplicationController
|
||||
def index
|
||||
return head :not_found unless current_user&.admin?
|
||||
|
||||
questions = GekanatorQuestion.accepted.order(priority_weight: :desc, id: :asc)
|
||||
|
||||
render json: {
|
||||
questions: questions.map { |question| question_json(question) }
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def question_json question
|
||||
{
|
||||
id: question_id_for(question),
|
||||
text: question.text,
|
||||
kind: question.kind,
|
||||
condition: condition_json(question.condition),
|
||||
source: question.source,
|
||||
priority_weight: question.priority_weight
|
||||
}
|
||||
end
|
||||
|
||||
def question_id_for question
|
||||
condition = condition_json(question.condition).deep_symbolize_keys
|
||||
|
||||
case condition[:type]
|
||||
when 'tag'
|
||||
"tag:#{ condition[:key] }"
|
||||
when 'source'
|
||||
"source:#{ condition[:host] }"
|
||||
when 'original-year'
|
||||
"original-year:#{ condition[:year] }"
|
||||
when 'original-month'
|
||||
"original-month:#{ condition[:month] }"
|
||||
when 'original-month-day'
|
||||
"original-month-day:#{ condition[:monthDay] || condition[:month_day] }"
|
||||
when 'title-length-greater-than'
|
||||
"title:length-greater-than:#{ condition[:length] }"
|
||||
when 'title-has-ascii'
|
||||
'title:ascii'
|
||||
else
|
||||
"catalog:#{ question.id }"
|
||||
end
|
||||
end
|
||||
|
||||
def condition_json condition
|
||||
json = condition.deep_dup.as_json
|
||||
|
||||
if json['type'] == 'original-month-day' && json['monthDay'].blank?
|
||||
json['monthDay'] = json.delete('month_day')
|
||||
end
|
||||
|
||||
json
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class GekanatorAiRun < ApplicationRecord
|
||||
STATUSES = ['pending', 'running', 'succeeded', 'failed', 'blocked_budget'].freeze
|
||||
|
||||
belongs_to :gekanator_question_suggestion
|
||||
|
||||
validates :model, presence: true, length: { maximum: 255 }
|
||||
validates :status, presence: true, inclusion: { in: STATUSES }
|
||||
validates :input_tokens,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :output_tokens,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :estimated_cost_jpy,
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
scope :this_month, lambda {
|
||||
where(created_at: Time.current.beginning_of_month..Time.current.end_of_month)
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
class GekanatorQuestion < ApplicationRecord
|
||||
KINDS = ['tag', 'source', 'title', 'original_date'].freeze
|
||||
SOURCES = ['user_suggested', 'ai_generated', 'admin_curated'].freeze
|
||||
STATUSES = ['pending', 'accepted', 'rejected'].freeze
|
||||
|
||||
belongs_to :gekanator_question_suggestion, optional: true
|
||||
belongs_to :created_by, class_name: 'User', optional: true
|
||||
|
||||
validates :kind, presence: true, inclusion: { in: KINDS }
|
||||
validates :source, presence: true, inclusion: { in: SOURCES }
|
||||
validates :status, presence: true, inclusion: { in: STATUSES }
|
||||
validates :text, presence: true, length: { maximum: 1000 }
|
||||
validates :condition, presence: true
|
||||
validates :priority_weight,
|
||||
presence: true,
|
||||
numericality: { greater_than: 0 }
|
||||
|
||||
scope :accepted, -> { where(status: 'accepted') }
|
||||
end
|
||||
@@ -1,9 +1,11 @@
|
||||
class GekanatorQuestionSuggestion < ApplicationRecord
|
||||
MAX_QUESTIONS_PER_GAME = 1
|
||||
MAX_QUESTIONS_PER_GAME = 3
|
||||
ANSWERS = ['yes', 'no', 'partial', 'probably_no', 'unknown'].freeze
|
||||
|
||||
belongs_to :gekanator_game
|
||||
belongs_to :user
|
||||
has_many :gekanator_questions, dependent: :nullify
|
||||
has_many :gekanator_ai_runs, dependent: :destroy
|
||||
|
||||
validates :question_text, presence: true, length: { maximum: 1000 }
|
||||
validates :answer, presence: true, inclusion: { in: ANSWERS }
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
module Gekanator
|
||||
class AiRunBudget
|
||||
MONTHLY_LIMIT_JPY = BigDecimal('450').freeze
|
||||
MAX_RUN_ESTIMATED_COST_JPY = BigDecimal('5').freeze
|
||||
|
||||
def self.remaining_monthly_budget_jpy
|
||||
MONTHLY_LIMIT_JPY - monthly_cost_jpy
|
||||
end
|
||||
|
||||
def self.monthly_cost_jpy
|
||||
GekanatorAiRun.this_month.sum(:estimated_cost_jpy)
|
||||
end
|
||||
|
||||
def self.exceeded?
|
||||
monthly_cost_jpy >= MONTHLY_LIMIT_JPY
|
||||
end
|
||||
|
||||
def self.exceeded_after_next_run?
|
||||
monthly_cost_jpy + MAX_RUN_ESTIMATED_COST_JPY >= MONTHLY_LIMIT_JPY
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
module Gekanator
|
||||
class QuestionSuggestionAiConverter
|
||||
def self.call(...) = new(...).call
|
||||
|
||||
def initialize suggestion:, user:
|
||||
@suggestion = suggestion
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
raise NotImplementedError, 'AI question conversion is not implemented yet.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :suggestion, :user
|
||||
end
|
||||
end
|
||||
新しい課題から参照
ユーザをブロックする