Skip to content

Commit

Permalink
Add versions to prompts (#38)
Browse files Browse the repository at this point in the history
* Add versions to prompts

* Add rerun with latest prompt version button for test run

* Add showing diff for prompt versions

* Fix creating new prompt version
  • Loading branch information
Vadser authored Jul 26, 2024
1 parent 8fb2923 commit 4e5ae4c
Show file tree
Hide file tree
Showing 25 changed files with 228 additions and 20 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,7 @@ gem 'pg', '~> 1.5'
gem 'draper', '~> 4.0'

gem 'data_migrate', '~> 9.4'

gem 'ancestry', '~> 4.3'

gem 'diffy', '~> 3.4'
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ancestry (4.3.3)
activerecord (>= 5.2.6)
ast (2.4.2)
base64 (0.2.0)
bcrypt (3.1.20)
Expand Down Expand Up @@ -128,6 +130,7 @@ GEM
devise (~> 4.0)
warden-jwt_auth (~> 0.10)
diff-lcs (1.5.1)
diffy (3.4.2)
dotenv (3.1.2)
draper (4.0.2)
actionpack (>= 5.0)
Expand Down Expand Up @@ -427,13 +430,15 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
ancestry (~> 4.3)
bootsnap
capybara
data_migrate (~> 9.4)
database_cleaner
debug
devise (~> 4.9)
devise-jwt (~> 0.12.0)
diffy (~> 3.4)
dotenv (~> 3.1)
draper (~> 4.0)
factory_bot_rails
Expand Down
13 changes: 13 additions & 0 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,16 @@
.underline-link {
text-decoration: underline !important;
}

.diff {overflow:auto;}
.diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;}
.diff del, .diff ins{display:block;text-decoration:none;}
.diff li{padding:0; display:table-row;margin: 0;height:1em;}
.diff li.ins{background:#dfd; color:#080}
.diff li.del{background:#fee; color:#b00}
.diff li:hover{background:#ffc}
.diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;}
.diff del strong{font-weight:normal;background:#fcc;}
.diff ins strong{font-weight:normal;background:#9f9;}
.diff li.diff-comment { display: none; }
.diff li.diff-block-info { background: none repeat scroll 0 0 gray; }
3 changes: 2 additions & 1 deletion app/controllers/compare_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

class CompareController < ApplicationController
def index
@prompts = current_user.prompts
@prompts = current_user.prompts.latest_versions
@ancestor_prompts = Prompt.where(id: @prompts.map(&:ancestor_ids).flatten)
@model_versions = current_user.model_versions.includes(:model)
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/models_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def destroy
@model.destroy!

respond_to do |format|
format.html { redirect_to models_url, notice: 'Model successfully destroyed.' }
format.html { redirect_to root_path, notice: 'Model successfully destroyed.' }
format.json { head :no_content }
end
end
Expand Down
32 changes: 30 additions & 2 deletions app/controllers/prompts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
# frozen_string_literal: true

class PromptsController < ApplicationController
before_action :set_prompt, only: %i[show destroy]
before_action :set_prompt, only: %i[show destroy diff]

# GET /prompts or /prompts.json
def index
@prompts = current_user.prompts
@prompts = current_user.prompts.latest_versions
end

# GET /prompts/1 or /prompts/1.json
def show
@previous_versions = @prompt.ancestors.sort_by { |p| -p.id }
@latest_version = @prompt.descendants.order(:id).first unless @prompt.childless?
@test_runs = @prompt.test_runs.includes(model_versions: :model).order(created_at: :desc)
end

def edit
@parent_prompt = current_user.prompts.latest_versions.find(params[:id])
@prompt = current_user.prompts.new
end

def diff
@previous_prompt_version = current_user.prompts.find(params[:prompt_version_id])
end

def update
parent_prompt = current_user.prompts.find(params[:id])

@prompt = current_user.prompts.new(prompt_params.merge(parent: parent_prompt))

respond_to do |format|
if @prompt.save
format.html { redirect_to prompt_url(@prompt), notice: 'Prompt was successfully updated.' }
format.json { render :show, status: :created, location: @prompt }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @prompt.errors, status: :unprocessable_entity }
end
end
end

# GET /prompts/new
def new
@prompt = current_user.prompts.new
Expand All @@ -35,6 +62,7 @@ def create

# DELETE /prompts/1 or /prompts/1.json
def destroy
@prompt.ancestors.destroy_all
@prompt.destroy!

respond_to do |format|
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/test_runs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ def index

# GET /test_runs/1 or /test_runs/1.json
def show
@latest_prompt_version = @test_run.prompt.descendants.order(:id).first
@test_model_version_runs = @test_run.test_model_version_runs.includes(:test_results, model_version: :model).with_passed_test_results_count.decorate
end

# GET /test_runs/new
def new
@prompt = current_user.prompts.find_by(id: params[:prompt_id])
@prompts = current_user.prompts
@prompts = current_user.prompts.latest_versions
@ancestor_prompts = Prompt.where(id: @prompts.map(&:ancestor_ids).flatten)
@model_versions = current_user.model_versions.includes(:model)
@assertions = current_user.assertions
@test_run = current_user.test_runs.new(prompt: @prompt)
Expand Down
7 changes: 7 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ def rating_tag(model)
content_tag(:span, '&#9733;'.html_safe, class: "star #{'selected' if model.rating.to_i >= rating_value}", data: { value: rating_value })
end.join('').html_safe
end

def options_with_versions(latest_versions, previous_versions)
latest_versions.map do |version|
ancestors = previous_versions.select { |ancestor| ancestor.id.in?(version.ancestor_ids) }
{ ancestors.last&.name || version.name => [[version.name, version.id]] + ancestors.map { |v| [v.name, v.id] } }
end.reduce(:merge)
end
end
3 changes: 3 additions & 0 deletions app/models/prompt.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# frozen_string_literal: true

class Prompt < ApplicationRecord
has_ancestry
belongs_to :user
has_many :test_runs, dependent: :destroy
has_many :test_results, through: :test_runs

scope :latest_versions, -> { joins("LEFT JOIN prompts AS c ON c.ancestry LIKE CONCAT('%/', prompts.id, '/%')").group('prompts.id').having('COUNT(c.id) = 0') }

default_scope { order(id: :desc) }
end
2 changes: 1 addition & 1 deletion app/models/test_model_version_run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class TestModelVersionRun < ApplicationRecord
belongs_to :test_run
belongs_to :model_version
has_many :test_results
has_many :test_results, dependent: :destroy

default_scope { order(id: :desc) }

Expand Down
2 changes: 1 addition & 1 deletion app/models/test_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class TestResult < ApplicationRecord
belongs_to :test_model_version_run
has_many :assertion_results
has_many :assertion_results, dependent: :destroy

enum :status, %i[pending completed failed], scopes: true, default: :pending

Expand Down
2 changes: 1 addition & 1 deletion app/views/compare/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<% else %>
<div>
<label for="prompt-id-select" class="block text-sm font-medium text-gray-700">Select Prompt:</label>
<%= select_tag 'prompt-id-select', options_from_collection_for_select(@prompts, 'id', 'name', @prompts.first&.id),
<%= select_tag 'prompt-id-select', grouped_options_for_select(options_with_versions(@prompts, @ancestor_prompts)),
class: 'form-control w-full px-3 py-2 border border-gray-300 rounded-md mb-4', id: 'prompt-id-select' %>
</div>
<div>
Expand Down
12 changes: 12 additions & 0 deletions app/views/prompts/diff.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="container px-6 mx-auto mt-10 grid">
<div id="<%= dom_id @prompt %>">
<p class="my-5">
<strong class="block font-medium mb-1">Value:</strong>
<%= Diffy::Diff.new(@previous_prompt_version.value, @prompt.value).to_s(:html).html_safe %>
</p>
</div>

<div class="inline mt-6">
<%= link_to "Back to prompt", prompt_path(@prompt), class: "rounded-lg py-3 px-5 secondary-button inline-block font-medium" %>
</div>
</div>
32 changes: 32 additions & 0 deletions app/views/prompts/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div class="container px-6 mx-auto mt-10 grid">
<h1 class="font-bold text-4xl">Edit prompt</h1>

<%= form_with(url: prompt_path(@parent_prompt), method: :patch, class: "contents") do |form| %>
<% if @prompt.errors.any? %>
<div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
<h2><%= pluralize(@prompt.errors.count, "error") %> prohibited this prompt from being saved:</h2>

<ul>
<% @prompt.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="my-5">
<%= form.label :name %>
<%= form.text_field 'prompt[name]', value: "#{@parent_prompt.name.gsub(/ Version \d+/, '')} Version #{@parent_prompt.ancestor_ids.size + 2}", class: "block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full" %>
</div>

<div class="my-5">
<%= form.label :value %>
<%= form.text_area 'prompt[value]', value: @parent_prompt.value, rows: 4, class: "block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full" %>
</div>

<div class="inline">
<%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
<%= link_to "Back to prompt", prompt_path(@parent_prompt), class: "ml-2 rounded-lg py-3 px-5 secondary-button inline-block font-medium" %>
</div>
<% end %>
</div>
1 change: 1 addition & 0 deletions app/views/prompts/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@
</tbody>
</table>
</div>
</div>
</div>
51 changes: 47 additions & 4 deletions app/views/prompts/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
<%= render @prompt %>

<div class="inline">
<%= link_to "Back to prompts", prompts_path, class: "rounded-lg py-3 px-5 secondary-button inline-block font-medium" %>
<% if @prompt.childless? %>
<%= link_to "Back to prompts", prompts_path, class: "rounded-lg py-3 px-5 secondary-button inline-block font-medium" %>
<% else %>
<%= link_to "Back to latest version", prompt_path(@latest_version), class: "rounded-lg py-3 px-5 secondary-button inline-block font-medium" %>
<% end %>
<%= link_to "Create test runs", new_test_run_path(prompt_id: @prompt), class: "ml-2 rounded-lg py-3 px-5 secondary-button inline-block font-medium" %>
<div class="inline-block ml-2">
<%= button_to "Destroy this prompt", @prompt, method: :delete, class: "mt-2 rounded-lg py-3 px-5 delete-button font-medium" %>
</div>
<% if @prompt.childless? %>
<%= link_to "Edit this prompt", edit_prompt_path(@prompt), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<div class="inline-block ml-2">
<%= button_to "Destroy this prompt", @prompt, method: :delete, class: "mt-2 rounded-lg py-3 px-5 delete-button font-medium" %>
</div>
<% end %>
</div>

<div class="w-full w-full mx-auto mt-10 overflow-hidden rounded-lg shadow-xs">
Expand Down Expand Up @@ -58,4 +65,40 @@
</table>
</div>
</div>

<% if @previous_versions.present? %>
<div class="w-full overflow-hidden rounded-lg shadow-xs mx-auto mt-10 w-full">
<h1 class="font-bold text-4xl">Previous Versions</h1>
<div class="w-full overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<thead>
<tr
class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800"
>
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Created At</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody
class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800"
>
<% @previous_versions.each do |prompt| %>
<tr class="text-gray-700 dark:text-gray-400">
<td class="px-4 py-3 text-sm">
<p class="font-semibold"><%= link_to prompt.name, prompt, class: "text-blue-500 underline-link" %></p>
</td>
<td class="px-4 py-3 text-sm">
<%= prompt.created_at.to_date %>
</td>
<td class="px-4 py-3 text-sm">
<%= link_to 'Show diff', diff_prompt_path(@prompt, prompt), class: "text-blue-500 underline-link" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
</div>
2 changes: 1 addition & 1 deletion app/views/test_runs/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<div class="my-5">
<%= form.label :prompt_id %>
<%= form.collection_select :prompt_id, @prompts, :id, :name, {}, { class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" } %>
<%= form.select :prompt_id, grouped_options_for_select(options_with_versions(@prompts, @ancestor_prompts), @prompt&.id), {}, { class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" } %>
</div>
<div class="my-5">
<%= form.label :calls %>
Expand Down
6 changes: 6 additions & 0 deletions app/views/test_runs/_test_run.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@
<p class="my-5">
<strong class="font-medium mr-2">Prompt:</strong> <%= link_to test_run.prompt.name, test_run.prompt, class: "text-blue-500 underline-link" %>
</p>

<% if @latest_prompt_version.present? %>
<div class="inline-block ml-2">
<%= button_to "Rerun with latest prompt version", test_runs_path, params: { test_run: { name: "#{test_run.name} ", calls: test_run.calls, assertion_ids: test_run.assertion_ids, passing_threshold: test_run.passing_threshold, model_version_ids: test_run.model_version_ids, prompt_id: @latest_prompt_version } }, class: "mt-2 rounded-lg py-3 px-5 secondary-button font-medium" %>
</div>
<% end %>
</div>
3 changes: 3 additions & 0 deletions config/initializers/ancestry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# frozen_string_literal: true

Ancestry.default_ancestry_format = :materialized_path2
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
Rails.application.routes.draw do
resources :assertions
resources :test_runs
resources :prompts, except: %i[edit update]
resources :prompts do
member do
get 'diff/:prompt_version_id', action: :diff, as: :diff
end
end

resources :test_runs, except: %i[edit update destroy] do
resources :test_model_version_runs, only: %i[show]
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20240724124609_add_ancestry_to_prompts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class AddAncestryToPrompts < ActiveRecord::Migration[7.1]
def change
change_table(:prompts) do |t|
t.string 'ancestry', collation: 'C', null: false, default: '/'
t.index 'ancestry'
end
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions spec/controllers/compare_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

RSpec.describe CompareController, type: :controller do
let(:user) { create(:user) }
let(:prompt1) { create(:prompt, user:) }
let(:prompt2) { create(:prompt, user:) }
let!(:prompt1) { create(:prompt, user:) }
let!(:prompt2) { create(:prompt, user:) }
let(:model) { create(:model, user:) }
let(:model_version_1) { create(:model_version, model:) }
let(:model_version_2) { create(:model_version, model:) }
let!(:model_version_1) { create(:model_version, model:) }
let!(:model_version_2) { create(:model_version, model:) }
let(:test_run) { create(:test_run) }
let(:test_model_version_run) { create(:test_model_version_run, test_run:, model_version: model_version_1) }
let!(:test_result) { create(:test_result, test_model_version_run:, status: 'completed') }
Expand Down
Loading

0 comments on commit 4e5ae4c

Please sign in to comment.