From 3ee911ae96766169f0ca1b09a92a5a2daab2c35c Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Wed, 13 Nov 2024 19:02:15 +0530 Subject: [PATCH 1/4] Add support for more field types --- app/dashboards/form_field_dashboard.rb | 23 +- app/helpers/openai_helper.rb | 16 +- app/models/form_field.rb | 81 ++++++ app/views/fields/string_array/_form.html.erb | 15 ++ app/views/fields/string_array/_index.html.erb | 1 + app/views/fields/string_array/_show.html.erb | 5 + ...40_add_validation_fields_to_form_fields.rb | 21 ++ db/schema.rb | 7 +- db/seeds.rb | 236 ++++++++++++++---- lib/administrate/field/string_array.rb | 19 ++ 10 files changed, 366 insertions(+), 58 deletions(-) create mode 100644 app/views/fields/string_array/_form.html.erb create mode 100644 app/views/fields/string_array/_index.html.erb create mode 100644 app/views/fields/string_array/_show.html.erb create mode 100644 db/migrate/20241113130440_add_validation_fields_to_form_fields.rb create mode 100644 lib/administrate/field/string_array.rb diff --git a/app/dashboards/form_field_dashboard.rb b/app/dashboards/form_field_dashboard.rb index bf992e8..4457d5e 100644 --- a/app/dashboards/form_field_dashboard.rb +++ b/app/dashboards/form_field_dashboard.rb @@ -10,7 +10,16 @@ class FormFieldDashboard < Administrate::BaseDashboard ATTRIBUTE_TYPES = { id: Field::Number, description: Field::String, + friendly_name: Field::String, + field_type: Field::Select.with_options( + collection: FormField.field_types.keys.map { |t| [ t.humanize, t ] } + ), metadata: Field::String.with_options(searchable: false), + minimum: Field::String, + maximum: Field::String, + enum_options: Field::StringArray.with_options( + hint: "Enter options for single/multi select fields, one per line" + ), page: Field::BelongsTo, title: Field::String, created_at: Field::DateTime, @@ -25,6 +34,8 @@ class FormFieldDashboard < Administrate::BaseDashboard COLLECTION_ATTRIBUTES = %i[ id title + friendly_name + field_type description page ].freeze @@ -34,9 +45,14 @@ class FormFieldDashboard < Administrate::BaseDashboard SHOW_PAGE_ATTRIBUTES = %i[ id title + friendly_name + field_type description page metadata + minimum + maximum + enum_options created_at updated_at ].freeze @@ -45,10 +61,15 @@ class FormFieldDashboard < Administrate::BaseDashboard # an array of attributes that will be displayed # on the model's form (`new` and `edit`) pages. FORM_ATTRIBUTES = %i[ + page + friendly_name title description - page metadata + field_type + minimum + maximum + enum_options ].freeze # COLLECTION_FILTERS diff --git a/app/helpers/openai_helper.rb b/app/helpers/openai_helper.rb index c3ab900..6cb6ce0 100644 --- a/app/helpers/openai_helper.rb +++ b/app/helpers/openai_helper.rb @@ -14,7 +14,7 @@ def ai_transcribe(file) def system_prompt(transcription) return transcription.page.prompt if transcription.page.prompt.present? - "You are an AI assitant filling the form for an user. Make sure that you do not populate the form with any data that the user did not provide. Ensure all data shared by users are correctly split into function arguments." + "You are an AI assistant filling the form for a user. Make sure that you do not populate the form with any data that the user did not provide. Ensure all data shared by users are correctly split into function arguments." end def ai_generate_completion(transcription) @@ -71,11 +71,10 @@ def ai_generate_completion(transcription) end def smart_description(form_field, context) - if context[form_field.title] - "#{form_field.description}; Context: #{context[form_field.title]}" - else - form_field.description - end + base_description = form_field.description.presence || form_field.friendly_name + context_info = context[form_field.title] if context.present? + + [ base_description, context_info ].compact.join("; Context: ") end def usage_tokens(transcription, usage) @@ -89,10 +88,9 @@ def usage_tokens(transcription, usage) def create_types_form_form_fields(form_fields, context) fields = {} form_fields.each do |form_field| - fields[form_field.title] = { - type: :string, + fields[form_field.title] = form_field.to_json_schema_for_ai.merge( description: smart_description(form_field, context) - } + ) end fields end diff --git a/app/models/form_field.rb b/app/models/form_field.rb index dff2002..8a2f07c 100644 --- a/app/models/form_field.rb +++ b/app/models/form_field.rb @@ -1,3 +1,84 @@ class FormField < ApplicationRecord belongs_to :page + + # Using string enum for better readability + enum field_type: { + string: "string", # Text input + number: "number", # Numeric input + boolean: "boolean", # Yes/No values + single_select: "single_select", # Single choice from options + multi_select: "multi_select" # Multiple choices from options + } + + validates :title, presence: true + validates :friendly_name, presence: true + validates :field_type, presence: true + validate :validate_select_options + validate :validate_number_constraints + + # Virtual attribute for enum options + attr_writer :enum_options_raw + + def enum_options_raw + @enum_options_raw || enum_options&.join("\n") + end + + before_validation :process_enum_options + + def to_json_schema_for_ai + schema = { + type: json_schema_type, + description: description + } + + case field_type + when "single_select", "multi_select" + schema[:enum] = enum_options if enum_options.present? + when "number" + schema[:minimum] = minimum if minimum.present? + schema[:maximum] = maximum if maximum.present? + end + + schema.compact + end + + private + + def json_schema_type + case field_type + when "single_select" + "string" + when "multi_select" + "array" + else + field_type + end + end + + def validate_select_options + return unless ["single_select", "multi_select"].include?(field_type) + + if enum_options.blank? || enum_options.any?(&:blank?) + errors.add(:enum_options, "must have at least one non-empty option for select fields") + end + end + + def validate_number_constraints + return unless field_type == "number" + + if minimum.present? && maximum.present? && maximum.to_f < minimum.to_f + errors.add(:maximum, "must be greater than minimum") + end + end + + def process_enum_options + return unless @enum_options_raw.present? + + # Split by newlines, remove empty lines and whitespace + self.enum_options = @enum_options_raw + .split("\n") + .map(&:strip) + .reject(&:blank?) + .uniq + end end diff --git a/app/views/fields/string_array/_form.html.erb b/app/views/fields/string_array/_form.html.erb new file mode 100644 index 0000000..d68bebf --- /dev/null +++ b/app/views/fields/string_array/_form.html.erb @@ -0,0 +1,15 @@ +
+ <%= f.label field.attribute %> +
+
+ <%= f.text_area "#{field.attribute}_raw", + value: field.data&.join("\n"), + class: "string-array-input", + rows: 5, + placeholder: "Enter one option per line" %> + <% if field.options.key?(:hint) %> +

+ <%= field.options[:hint] %> +

+ <% end %> +
diff --git a/app/views/fields/string_array/_index.html.erb b/app/views/fields/string_array/_index.html.erb new file mode 100644 index 0000000..6d9dbc9 --- /dev/null +++ b/app/views/fields/string_array/_index.html.erb @@ -0,0 +1 @@ +<%= field.to_s %> diff --git a/app/views/fields/string_array/_show.html.erb b/app/views/fields/string_array/_show.html.erb new file mode 100644 index 0000000..3d0ac37 --- /dev/null +++ b/app/views/fields/string_array/_show.html.erb @@ -0,0 +1,5 @@ +<% field.data.map do |item| %> +
+ - <%= item %> +
+<% end %> diff --git a/db/migrate/20241113130440_add_validation_fields_to_form_fields.rb b/db/migrate/20241113130440_add_validation_fields_to_form_fields.rb new file mode 100644 index 0000000..acab43e --- /dev/null +++ b/db/migrate/20241113130440_add_validation_fields_to_form_fields.rb @@ -0,0 +1,21 @@ +class AddValidationFieldsToFormFields < ActiveRecord::Migration[8.0] + def change + add_column :form_fields, :friendly_name, :string + add_column :form_fields, :field_type, :string, null: false, default: 'string' + + # For number fields + add_column :form_fields, :minimum, :string + add_column :form_fields, :maximum, :string + + # For select fields + add_column :form_fields, :enum_options, :string, array: true, default: [] + + # Copy existing title values to friendly_name + FormField.find_each do |field| + field.update_column(:friendly_name, field.title) + end + + # Make friendly_name required after copying data + change_column_null :form_fields, :friendly_name, false + end +end diff --git a/db/schema.rb b/db/schema.rb index ccffd20..42a8fa4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_10_02_081501) do +ActiveRecord::Schema[8.0].define(version: 2024_11_13_130440) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -86,6 +86,11 @@ t.jsonb "metadata", default: {}, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "friendly_name", null: false + t.string "field_type", default: "string", null: false + t.string "minimum" + t.string "maximum" + t.string "enum_options", default: [], array: true t.index ["page_id"], name: "index_form_fields_on_page_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 8da2279..bf9bbdf 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -58,63 +58,205 @@ # Create form fields for the Consultation Form page (Care template) consultation_form_fields = [ - { title: "history_of_present_illness", description: "Details about the current illness, including when" }, - { title: "examination_details", description: "Details about the examination performed and clinic" }, - { title: "weight", description: "The patient's weight, usually measured in kilogram" }, - { title: "height", description: "The patient's height, usually measured in centimet" }, - { title: "consultation_notes", description: "General Advice given to the patient" }, - { title: "special_instruction", description: "Any special instructions for the patient or other" }, - { title: "treatment_plan", description: "Treatment plan for the patient, including prescrib" }, - { title: "patient_no", description: "The inpatient or out patient number assigned to th" } + { + title: "history_of_present_illness", + friendly_name: "History of Present Illness", + description: "Details about the current illness, including when", + field_type: "string" + }, + { + title: "examination_details", + friendly_name: "Examination Details", + description: "Details about the examination performed and clinic", + field_type: "string" + }, + { + title: "weight", + friendly_name: "Weight", + description: "The patient's weight, usually measured in kilogram", + field_type: "number", + minimum: 0, + maximum: 500 + }, + { + title: "height", + friendly_name: "Height", + description: "The patient's height, usually measured in centimeters", + field_type: "number", + minimum: 0, + maximum: 300 + }, + { + title: "consultation_notes", + friendly_name: "Consultation Notes", + description: "General Advice given to the patient", + field_type: "string" + }, + { + title: "special_instruction", + friendly_name: "Special Instructions", + description: "Any special instructions for the patient or other", + field_type: "string" + }, + { + title: "treatment_plan", + friendly_name: "Treatment Plan", + description: "Treatment plan for the patient, including prescriptions", + field_type: "string" + }, + { + title: "patient_no", + friendly_name: "Patient Number", + description: "The inpatient or out patient number assigned to the patient", + field_type: "string" + } ] -consultation_form_fields.each do |field_data| - FormField.create!( - page: consultation_form, - title: field_data[:title], - description: field_data[:description] - ) -end - # Create form fields for the Log Update page (Care template) log_update_fields = [ - { title: "physical_examination_info", description: "Physical Examination Details, including any complaints" }, - { title: "other_details", description: "Other details, including Treatement Plans, Advice," }, - { title: "diastolic", description: "Blood Pressure Diastolic as integer" }, - { title: "systolic", description: "Blood Pressure Systolic as integer" }, - { title: "temperature", description: "Temperature in Fahrenheit" }, - { title: "resp", description: "Respiratory Rate as integer" }, - { title: "spo2", description: "SPO2 Value as integer" }, - { title: "pulse", description: "Pulse Rate as integer" } + { + title: "physical_examination_info", + friendly_name: "Physical Examination Info", + description: "Physical Examination Details, including any complaints", + field_type: "string" + }, + { + title: "other_details", + friendly_name: "Other Details", + description: "Other details, including Treatment Plans, Advice", + field_type: "string" + }, + { + title: "diastolic", + friendly_name: "Diastolic BP", + description: "Blood Pressure Diastolic as integer", + field_type: "number", + minimum: 0, + maximum: 200 + }, + { + title: "systolic", + friendly_name: "Systolic BP", + description: "Blood Pressure Systolic as integer", + field_type: "number", + minimum: 0, + maximum: 300 + }, + { + title: "temperature", + friendly_name: "Temperature", + description: "Temperature in Fahrenheit", + field_type: "number", + minimum: 90, + maximum: 110 + }, + { + title: "resp", + friendly_name: "Respiratory Rate", + description: "Respiratory Rate as integer", + field_type: "number", + minimum: 0, + maximum: 100 + }, + { + title: "spo2", + friendly_name: "SPO2", + description: "SPO2 Value as integer", + field_type: "number", + minimum: 0, + maximum: 100 + }, + { + title: "pulse", + friendly_name: "Pulse Rate", + description: "Pulse Rate as integer", + field_type: "number", + minimum: 0, + maximum: 300 + } ] -log_update_fields.each do |field_data| - FormField.create!( - page: log_update, - title: field_data[:title], - description: field_data[:description] - ) -end - # Create form fields for the Demo Page 1 (Medispeak template) medispeak_demo_fields = [ - { title: "gender", description: "Gender from options Male, Female, Other" }, - { title: "symptoms", description: "Applicable Symptoms from options Headache, Fever," }, - { title: "preferred_time", description: "The Preferred Time from the options Morning, After" }, - { title: "full_name", description: "Full name" }, - { title: "age", description: "Age of the user" }, - { title: "email", description: "Email of the user" }, - { title: "phone_number", description: "Phone Number of the user" }, - { title: "additional_notes", description: "Full text of the conversation" }, - { title: "date_of_birth", description: "Date of Birth as a Javascript Date String" } + { + title: "gender", + friendly_name: "Gender", + description: "Gender from options Male, Female, Other", + field_type: "single_select", + enum_options: ["Male", "Female", "Other"] + }, + { + title: "symptoms", + friendly_name: "Symptoms", + description: "Applicable Symptoms from options", + field_type: "multi_select", + enum_options: ["Headache", "Fever", "Cough", "Cold", "Body Pain"] + }, + { + title: "preferred_time", + friendly_name: "Preferred Time", + description: "The Preferred Time from the options", + field_type: "single_select", + enum_options: ["Morning", "Afternoon", "Evening"] + }, + { + title: "full_name", + friendly_name: "Full Name", + description: "Full name", + field_type: "string" + }, + { + title: "age", + friendly_name: "Age", + description: "Age of the user", + field_type: "number", + minimum: 0, + maximum: 150 + }, + { + title: "email", + friendly_name: "Email", + description: "Email of the user", + field_type: "string" + }, + { + title: "phone_number", + friendly_name: "Phone Number", + description: "Phone Number of the user", + field_type: "string" + }, + { + title: "additional_notes", + friendly_name: "Additional Notes", + description: "Full text of the conversation", + field_type: "string" + }, + { + title: "date_of_birth", + friendly_name: "Date of Birth", + description: "Date of Birth as a Javascript Date String", + field_type: "string" + } ] -medispeak_demo_fields.each do |field_data| - FormField.create!( - page: medispeak_demo_page, - title: field_data[:title], - description: field_data[:description] - ) +# Update the creation loops to include all attributes +[ + [consultation_form, consultation_form_fields], + [log_update, log_update_fields], + [medispeak_demo_page, medispeak_demo_fields] +].each do |page, fields| + fields.each do |field_data| + FormField.create!( + page: page, + title: field_data[:title], + friendly_name: field_data[:friendly_name], + description: field_data[:description], + field_type: field_data[:field_type], + minimum: field_data[:minimum], + maximum: field_data[:maximum], + enum_options: field_data[:enum_options] + ) + end end puts "Seeds created successfully!" diff --git a/lib/administrate/field/string_array.rb b/lib/administrate/field/string_array.rb new file mode 100644 index 0000000..01afc64 --- /dev/null +++ b/lib/administrate/field/string_array.rb @@ -0,0 +1,19 @@ +require "administrate/field/base" + +module Administrate + module Field + class StringArray < Administrate::Field::Base + def to_s + data&.join("\n") + end + + def self.permitted_attribute(attr, _options = nil) + ["#{attr}_raw", attr => []] + end + + def self.html_class + "string_array" + end + end + end +end From 35bdaf1e9b1e5d033e9525f84f7306bc686e37fc Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Thu, 14 Nov 2024 00:19:23 +0530 Subject: [PATCH 2/4] Update string array listing --- app/views/fields/string_array/_show.html.erb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/fields/string_array/_show.html.erb b/app/views/fields/string_array/_show.html.erb index 3d0ac37..3819d11 100644 --- a/app/views/fields/string_array/_show.html.erb +++ b/app/views/fields/string_array/_show.html.erb @@ -1,5 +1,5 @@ -<% field.data.map do |item| %> -
- - <%= item %> -
-<% end %> + From c750373925c8a39addc8a823c355d75ac2e6e11a Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Thu, 14 Nov 2024 00:20:44 +0530 Subject: [PATCH 3/4] Upease rubocop --- Gemfile | 2 +- app/models/form_field.rb | 2 +- config/initializers/cors.rb | 4 ++-- db/seeds.rb | 12 ++++++------ lib/administrate/field/string_array.rb | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index b3d6ff5..565bf3b 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem "devise", "~> 4.9" # Tailwind CSS gem "tailwindcss-rails", "~> 2.7" # CORS for API -gem 'rack-cors', "~> 2.0.1" +gem "rack-cors", "~> 2.0.1" diff --git a/app/models/form_field.rb b/app/models/form_field.rb index 8a2f07c..7f1f7cc 100644 --- a/app/models/form_field.rb +++ b/app/models/form_field.rb @@ -56,7 +56,7 @@ def json_schema_type end def validate_select_options - return unless ["single_select", "multi_select"].include?(field_type) + return unless [ "single_select", "multi_select" ].include?(field_type) if enum_options.blank? || enum_options.any?(&:blank?) errors.add(:enum_options, "must have at least one non-empty option for select fields") diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 8750ca2..aa4c4f5 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -1,6 +1,6 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do - origins '*' - resource '*', headers: :any, methods: [:get, :post, :patch, :put] + origins "*" + resource "*", headers: :any, methods: [ :get, :post, :patch, :put ] end end diff --git a/db/seeds.rb b/db/seeds.rb index bf9bbdf..cff81ba 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -183,21 +183,21 @@ friendly_name: "Gender", description: "Gender from options Male, Female, Other", field_type: "single_select", - enum_options: ["Male", "Female", "Other"] + enum_options: [ "Male", "Female", "Other" ] }, { title: "symptoms", friendly_name: "Symptoms", description: "Applicable Symptoms from options", field_type: "multi_select", - enum_options: ["Headache", "Fever", "Cough", "Cold", "Body Pain"] + enum_options: [ "Headache", "Fever", "Cough", "Cold", "Body Pain" ] }, { title: "preferred_time", friendly_name: "Preferred Time", description: "The Preferred Time from the options", field_type: "single_select", - enum_options: ["Morning", "Afternoon", "Evening"] + enum_options: [ "Morning", "Afternoon", "Evening" ] }, { title: "full_name", @@ -241,9 +241,9 @@ # Update the creation loops to include all attributes [ - [consultation_form, consultation_form_fields], - [log_update, log_update_fields], - [medispeak_demo_page, medispeak_demo_fields] + [ consultation_form, consultation_form_fields ], + [ log_update, log_update_fields ], + [ medispeak_demo_page, medispeak_demo_fields ] ].each do |page, fields| fields.each do |field_data| FormField.create!( diff --git a/lib/administrate/field/string_array.rb b/lib/administrate/field/string_array.rb index 01afc64..19f2c81 100644 --- a/lib/administrate/field/string_array.rb +++ b/lib/administrate/field/string_array.rb @@ -8,7 +8,7 @@ def to_s end def self.permitted_attribute(attr, _options = nil) - ["#{attr}_raw", attr => []] + [ "#{attr}_raw", attr => [] ] end def self.html_class From 52026425d327a0100ff07e41fa14fb43661b8e51 Mon Sep 17 00:00:00 2001 From: Bodhish Thomas Date: Thu, 14 Nov 2024 00:25:47 +0530 Subject: [PATCH 4/4] Try adding config for specs --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 038861c..ba370fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: push: - branches: [ main ] + branches: [main] jobs: scan_ruby: @@ -89,6 +89,13 @@ jobs: env: RAILS_ENV: test DATABASE_URL: postgres://postgres:postgres@localhost:5432 + DB_NAME: medispeak + DB_NAME_TEST: medispeak_test + DB_HOST: localhost + DB_USERNAME: postgres + DB_PASSWORD: postgres + DB_PORT: 5432 + # REDIS_URL: redis://localhost:6379/0 run: bin/rails db:test:prepare test test:system