このコミットが含まれているのは:
2026-06-09 23:05:37 +09:00
コミット ae1deaac8c
14個のファイルの変更505行の追加38行の削除
+27 -1
ファイルの表示
@@ -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
+57
ファイルの表示
@@ -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
+21
ファイルの表示
@@ -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
+19
ファイルの表示
@@ -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
+3 -1
ファイルの表示
@@ -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 }
+22
ファイルの表示
@@ -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
+18
ファイルの表示
@@ -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