Browse Source

Add describe_soft_lettings_validations task

pull/2438/head
Kat 2 years ago
parent
commit
54fcc6d9f4
  1. 209
      lib/tasks/generate_documentation.rake
  2. 72
      spec/lib/tasks/generate_documentation_spec.rb

209
lib/tasks/generate_documentation.rake

@ -1,5 +1,5 @@
namespace :generate_documentation do
desc "Import lettings address data from a csv file"
desc "Generate documentation for hard lettings validations"
task describe_lettings_validations: :environment do
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
en_yml = File.read("./config/locales/en.yml")
@ -21,7 +21,7 @@ namespace :generate_documentation do
Validations::FinancialValidations,
Validations::TenancyValidations,
Validations::DateValidations,
Validations::LocalAuthorityValidations].map(&:instance_methods).flatten
Validations::LocalAuthorityValidations].map { |x| x.instance_methods + x.private_instance_methods }.flatten
all_helper_methods = all_methods - validation_methods
validation_methods.each do |meth|
@ -69,14 +69,6 @@ namespace :generate_documentation do
type: :string,
description: "A human-readbale description of the validation",
},
conditions: {
type: :array,
description: "A list of conditions that must be met for this validation to run, in human-readable text (not code)",
items: {
type: :string,
description: "A single condition (write this in a human-readable way)",
},
},
cases: {
type: :array,
description: "A list of cases that this validation triggers on, each with specific details",
@ -122,14 +114,128 @@ namespace :generate_documentation do
},
other_validated_models: {
type: :string,
description: "Comma separated list of any other models (other than log) that were used in this validation. These are possible models: user, organisation, scheme, location, organisation_relationship. Only leave this blank if no other models were used in this validation.",
description: "Comma separated list of any other models (other than log) that were used in this validation. These are possible models (only add a value to this field if other validation models are one of these models): User, Organisation, Scheme, Location, Organisation_relationship, LaRentRange. Only leave this blank if no other models were used in this validation.",
},
},
required: %w[case_description errors validation_type other_validated_models],
},
},
},
required: %w[validation_name description conditions cases],
required: %w[description cases],
},
},
},
],
tool_choice: { type: "function", function: { name: "create_documentation_for_given_validation" } },
},
)
rescue StandardError => e
Rails.logger.error("Failed to describe #{meth}")
Rails.logger.error("Error #{e.message}")
sleep(5)
next
end
begin
result = JSON.parse(response.dig("choices", 0, "message", "tool_calls", 0, "function", "arguments"))
result["cases"].each do |case_info|
case_info["errors"].each do |error|
Validation.create!(log_type: "lettings",
validation_name: meth.to_s,
description: result["description"],
field: error["field"],
error_message: error["error_message"],
case: case_info["case_description"],
section: form.get_question(error["field"], nil)&.subsection&.id,
from: case_info["from"] || "",
to: case_info["to"] || "",
validation_type: case_info["validation_type"],
hard_soft: "hard",
other_validated_models: case_info["other_validated_models"])
end
end
Rails.logger.info("******** described #{meth} ********")
rescue StandardError => e
Rails.logger.error("Failed to save #{meth}")
Rails.logger.error("Error #{e.message}")
end
end
end
desc "Generate documentation for soft lettings validations"
task describe_soft_lettings_validations: :environment do
include Validations::SoftValidations
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
current_form = FormHandler.instance.forms["current_lettings"]
previous_form = FormHandler.instance.forms["previous_lettings"]
all_helper_methods = Validations::SoftValidations.private_instance_methods
# describe all soft validations
validation_descriptions = {}
Validations::SoftValidations.instance_methods.each do |meth|
validation_source = method(meth).source
helper_methods_source = all_helper_methods.map { |helper_method|
if validation_source.include?(helper_method.to_s)
method(helper_method).source
end
}.compact.join("\n")
begin
response = client.chat(
parameters: {
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "You write amazing documentation, as a senior technical writer. Your audience is non-technical team members. You have been asked to document the validations in a Rails application. The application collects social housing data for different collection years. There are validations on different fields, sometimes the validations depend on several fields.
You are given a method that contains a validation. Describe what given method does, be very explicit about all the different validation cases (be specific about the years for which these validations would run, which values would be invalid or which values are required, look at private helper methods to understand what is being checked in more detail).
You should create `create_documentation_for_given_validation` method. Call it once, and include the documentation for given validation.",
},
{
role: "user",
content: "Describe #{meth} validation in detail. Here is the content of the validation:
#{validation_source}
Look at these helper methods where needed to understand what is being checked in more detail: #{helper_methods_source}",
},
],
tools: [
{
type: "function",
function: {
name: "create_documentation_for_given_validation",
description: "Use this function to save the complete documentation, covering given validation in the provided code.",
parameters: {
type: :object,
properties: {
description: {
type: :string,
description: "A human-readbale description of the validation",
},
from: {
type: :number,
description: "the year from which the validation starts. If this validation runs for logs with a startdate after a certain year, specify that year here, only if it is not specified in the validation method, leave this field blank",
},
to: {
type: :number,
description: "the year in which the validation ends. If this validation runs for logs with a startdate before a certain year, specify that year here, only if it is not specified in the validation method, leave this field blank",
},
validation_type: {
type: :string,
enum: %w[presence format minimum maximum range inclusion length other],
description: "The type of validation that is being performed. This should be one of the following: presence (validates that the question is answered), format (validates that the answer format is valid), minimum (validates that entered value is more than minimum allowed value), maximum (validates that entered value is less than maximum allowed value), range (values must be between two values), inclusion (validates that the values that are not allowed arent selected), length (validates the length of the answer), other",
},
other_validated_models: {
type: :string,
description: "Comma separated list of any other models (other than log) that were used in this validation. These are possible models (only add a value to this field if other validation models are one of these models): User, Organisation, Scheme, Location, Organisation_relationship, LaRentRange. Only leave this blank if no other models were used in this validation.",
},
},
required: %w[description validation_type other_validated_models],
},
},
},
@ -146,24 +252,75 @@ namespace :generate_documentation do
result = JSON.parse(response.dig("choices", 0, "message", "tool_calls", 0, "function", "arguments"))
result["cases"].each do |case_info|
case_info["errors"].each do |error|
validation_descriptions[meth.to_s] = result
end
# add a validation for each interruption screen page for both forms
[current_form, previous_form].each do |form|
interruption_screen_pages = form.pages.select { |page| page.questions.first.type == "interruption_screen" }
interruption_screen_pages_grouped_by_question = interruption_screen_pages.group_by { |page| page.questions.first.id }
interruption_screen_pages_grouped_by_question.each do |_question_id, pages|
pages.map do |page|
subsection_pages = form.subsection_for_page(page).pages
page_index = subsection_pages.index(page)
page_the_validation_applied_to = subsection_pages[page_index - 1]
loop do
break unless page_the_validation_applied_to.questions.first.type == "interruption_screen"
page_index -= 1
page_the_validation_applied_to = subsection_pages[page_index - 1]
end
validation_depends_on_hash = page.depends_on.each_with_object({}) do |depends_on, result|
depends_on.each do |key, value|
if validation_descriptions.include?(key)
result[key] = value
end
end
end
if validation_depends_on_hash.empty?
Rails.logger.info("No validation description found for #{page.questions.first.id}")
next
end
if Validation.where(validation_name: validation_depends_on_hash.keys.first, field: page_the_validation_applied_to.questions.first.id, from: form.start_date).exists?
Rails.logger.info("Validation #{validation_depends_on_hash.keys.first} already exists for #{page_the_validation_applied_to.questions.first.id} for start year #{form.start_date.year}")
next
end
result = validation_descriptions[validation_depends_on_hash.keys.first]
informative_text = page.informative_text
if informative_text.present? && !(informative_text.is_a? String)
informative_text = I18n.t(page.informative_text["translation"])
end
title_text = page.title_text
if title_text.present? && !(title_text.is_a? String)
title_text = I18n.t(page.title_text["translation"])
end
error_message = [title_text, informative_text, page.questions.first.hint_text].compact.join("\n")
case_info = page.depends_on.first.values.first ? "Provided values fulfill the description" : "Provided values do not fulfill the description"
Validation.create!(log_type: "lettings",
validation_name: meth.to_s,
validation_name: validation_depends_on_hash.keys.first.to_s,
description: result["description"],
field: error["field"],
error_message: error["error_message"],
case: case_info["case_description"],
section: form.get_question(error["field"], nil)&.subsection&.id,
from: case_info["from"] || "",
to: case_info["to"] || "",
validation_type: case_info["validation_type"],
hard_soft: "hard",
other_validated_models: case_info["other_validated_models"])
field: page_the_validation_applied_to.questions.first.id,
error_message:,
case: case_info,
section: form.get_question(page_the_validation_applied_to.questions.first.id, nil)&.subsection&.id,
from: form.start_date,
to: form.start_date + 1.year,
validation_type: result["validation_type"],
hard_soft: "soft",
other_validated_models: result["other_validated_models"])
Rails.logger.info("******** described #{validation_depends_on_hash.keys.first} for #{page_the_validation_applied_to.questions.first.id} ********")
end
end
Rails.logger.info("******** described #{meth} ********")
end
end
end

72
spec/lib/tasks/generate_documentation_spec.rb

@ -7,8 +7,8 @@ RSpec.describe "generate_documentation" do
let(:client) { instance_double(OpenAI::Client) }
let(:response) do
{ "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" =>
"{\n \"description\": \"Validates the format.\",\n \"conditions\": [\n \"The validation runs if the previous postcode is known.\"\n ],\n \"cases\": [\n {\n \"case_description\": \"Previous postcode is known and current postcode is blank\",\n \"errors\": [\n {\n \"error_message\": \"Enter a valid postcode\",\n \"field\": \"ppostcode_full\"\n }\n ],\n \"validation_type\": \"format\"\n }]\n}" } }] }, }]}
{ "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" =>
"{\n \"description\": \"Validates the format.\",\n \"cases\": [\n {\n \"case_description\": \"Previous postcode is known and current postcode is blank\",\n \"errors\": [\n {\n \"error_message\": \"Enter a valid postcode\",\n \"field\": \"ppostcode_full\"\n }\n ],\n \"validation_type\": \"format\",\n \"other_validated_models\": \"User\" }]\n}" } }] } }] }
end
before do
@ -21,6 +21,7 @@ RSpec.describe "generate_documentation" do
context "when the rake task is run" do
it "creates new validation documentation records" do
expect(Rails.logger).to receive(:info).with(/described/).at_least(:once)
expect { task.invoke }.to change(Validation, :count)
expect(Validation.where(validation_name: "validate_numeric_min_max").count).to eq(1)
expect(Validation.where(validation_name: "validate_layear").count).to eq(1)
@ -33,6 +34,73 @@ RSpec.describe "generate_documentation" do
expect(any_validation.to).to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("hard")
expect(any_validation.other_validated_models).to eq("User")
end
it "calls openAI client" do
expect(client).to receive(:chat)
task.invoke
end
it "skips if the validation already exists in the database" do
task.invoke
expect { task.invoke }.not_to change(Validation, :count)
end
context "when openAI response is not a JSON" do
let(:response) { "not a JSON" }
it "raises an error" do
expect(Rails.logger).to receive(:error).with(/Failed to save/).at_least(:once)
expect(Rails.logger).to receive(:error).with(/Error/).at_least(:once)
task.invoke
end
end
context "when openAI response does not have expected fields" do
let(:response) { { "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" => "{}" } }] } }] } }
it "raises an error" do
expect(Rails.logger).to receive(:error).with(/Failed to save/).at_least(:once)
expect(Rails.logger).to receive(:error).with(/Error/).at_least(:once)
task.invoke
end
end
end
end
describe ":describe_soft_lettings_validations", type: :task do
subject(:task) { Rake::Task["generate_documentation:describe_soft_lettings_validations"] }
let(:client) { instance_double(OpenAI::Client) }
let(:response) do
{ "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" =>
"{\n \"description\": \"Validates the format.\",\n \"validation_type\": \"format\",\n \"other_validated_models\": \"User\"}" } }] } }] }
end
before do
Rake.application.rake_require("tasks/generate_documentation")
Rake::Task.define_task(:environment)
task.reenable
allow(OpenAI::Client).to receive(:new).and_return(client)
allow(client).to receive(:chat).and_return(response)
end
context "when the rake task is run" do
it "creates new validation documentation records" do
expect { task.invoke }.to change(Validation, :count)
expect(Validation.where(validation_name: "rent_in_soft_min_range?").count).to be_positive
expect(Validation.where(validation_name: "major_repairs_date_in_soft_range?").count).to be_positive
any_validation = Validation.first
expect(any_validation.description).to eq("Validates the format.")
expect(any_validation.field).not_to be_empty
expect(any_validation.error_message).not_to be_empty
expect(any_validation.case).to eq("Provided values fulfill the description")
expect(any_validation.from).not_to be_nil
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("soft")
expect(any_validation.other_validated_models).to eq("User")
end
it "calls openAI client" do

Loading…
Cancel
Save