Skip to content

Commit

Permalink
Add Admin::InsightsController
Browse files Browse the repository at this point in the history
Allow admins to show, create, destroy insights for info requests from
within the Admin UI.
  • Loading branch information
gbp committed Nov 20, 2024
1 parent b19190e commit 0842651
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 0 deletions.
48 changes: 48 additions & 0 deletions app/controllers/admin/insights_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
##
# Controller for running AI insights tasks from the admin UI
#
class Admin::InsightsController < AdminController
before_action :find_info_request
before_action :find_insight, only: [:show, :destroy]

def show
end

def new
last = Insight.last
@insight = @info_request.insights.new(
model: last&.model, temperature: last&.temperature || 0.5,
template: last&.template
)
end

def create
@insight = @info_request.insights.new(insight_params)
if @insight.save
redirect_to admin_info_request_insight_path(@info_request, @insight),
notice: 'Insight was successfully created.'
else
render :new
end
end

def destroy
@insight.destroy
redirect_to admin_request_path(@info_request),
notice: 'Insight was successfully deleted.'
end

private

def find_info_request
@info_request = InfoRequest.find(params[:info_request_id])
end

def find_insight
@insight = @info_request.insights.find(params[:id])
end

def insight_params
params.require(:insight).permit(:model, :temperature, :template)
end
end
14 changes: 14 additions & 0 deletions app/models/insight.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
# updated_at :datetime not null
#
class Insight < ApplicationRecord
admin_columns exclude: [:template, :output],
include: [:duration, :prompt, :response]

after_commit :queue, on: :create

belongs_to :info_request, optional: false
Expand All @@ -35,12 +38,23 @@ def self.models
client.tags.first['models'].map { _1['model'] }.sort
end

def duration
return unless output['total_duration']

seconds = output['total_duration'].to_f / 1_000_000_000
ActiveSupport::Duration.build(seconds.to_i).inspect
end

def prompt
template.gsub('[initial_request]') do
outgoing_messages.first.body[0...500]
end
end

def response
output['response']
end

private

def queue
Expand Down
34 changes: 34 additions & 0 deletions app/views/admin/insights/_list.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<div class="row">
<% if insights.any? %>
<table class="table table-condensed table-hover span12">
<tr>
<th>ID</th>
<th>Model</th>
<th>Template</th>
<th>Created at</th>
<th>Updated at</th>
<th>Actions</th>
</tr>

<% insights.each do |insight| %>
<tr class="<%= cycle('odd', 'even') %>">
<td class="id"><%= insight.to_param %></td>
<td class="model"><%= insight.model %></td>
<td class="temperature"><%= insight.temperature %></td>
<td class="template"><%= truncate(insight.template, length: 150) %></td>
<td class="created_at"><%= admin_date(insight.created_at) %></td>
<td class="updated_at"><%= admin_date(insight.updated_at) %></td>
<td><%= link_to "Show", admin_info_request_insight_path(info_request, insight), class: 'btn' %></td>
</tr>
<% end %>
</table>
<% else %>
<p class="span12">None yet.</p>
<% end %>
</div>

<div class="row">
<p class="span12">
<%= link_to "New insight", new_admin_info_request_insight_path(info_request), :class => "btn btn-info" %>
</p>
</div>
45 changes: 45 additions & 0 deletions app/views/admin/insights/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<% @title = 'New insight' %>

<div class="row">
<div class="span12">
<div class="page-header">
<h1><%= @title %></h1>
</div>
</div>
</div>

<%= form_for [:admin, @info_request, @insight], html: { class: 'form form-horizontal' } do |f| %>
<%= foi_error_messages_for :insight %>

<div class="control-group">
<%= f.label :model, class: 'control-label' %>
<div class="controls">
<%= f.select :model, Insight.models, {}, class: 'span6' %>
</div>
</div>

<div class="control-group">
<%= f.label :temperature, class: 'control-label' %>
<div class="controls">
<%= f.range_field :temperature, min: 0, max: 1, step: 0.1, class: 'span6', autocomplete: 'off', oninput: 'insight_temperature_display.value = insight_temperature.value' %>
<%= content_tag(:output, @insight.temperature, id: 'insight_temperature_display') %>
</div>
</div>

<div class="control-group">
<%= f.label :template, class: 'control-label' %>
<div class="controls">
<%= f.text_area :template, class: 'span6', rows: 10 %>

<div class="help-block">
Add <strong>[initial_request]</strong> to substitute in the first 500
characters from the body of the initial outgoing message into the prompt
sent to the model.
</div>
</div>
</div>

<div class="form-actions">
<%= submit_tag 'Create', class: 'btn btn-success' %>
</div>
<% end %>
28 changes: 28 additions & 0 deletions app/views/admin/insights/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<table class="table table-striped table-condensed">
<tbody>
<tr>
<td>
<b>ID</b>
</td>
<td>
<%= @insight.id %>
</td>
</tr>
<% @insight.for_admin_column do |name, value| %>
<tr>
<td>
<b><%= name.humanize %></b>
</td>
<td>
<% if name == 'prompt' || name == 'response' %>
<pre><%= value&.strip %></pre>
<% else %>
<%= h admin_value(value) %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>

<%= link_to "Destroy", admin_info_request_insight_path(@info_request, @insight), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger' %>
8 changes: 8 additions & 0 deletions app/views/admin_request/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,11 @@
<%= render partial: 'admin/notes/show',
locals: { notes: @info_request.all_notes,
notable: @info_request } %>

<hr>

<h2>Insights</h2>

<%= render partial: 'admin/insights/list',
locals: { info_request: @info_request,
insights: @info_request.insights } %>
8 changes: 8 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,14 @@ def matches?(request)
end
####

#### Admin::Insights controller
namespace :admin do
resources :info_requests, only: [], path: 'requests' do
resources :insights, only: [:show, :new, :create, :destroy]
end
end
####

#### Api controller
match '/api/v2/request.json' => 'api#create_request',
:as => :api_create_request,
Expand Down
97 changes: 97 additions & 0 deletions spec/controllers/admin/insights_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
require 'spec_helper'

RSpec.describe Admin::InsightsController, type: :controller do
let(:info_request) { FactoryBot.create(:info_request) }
let(:insight) { FactoryBot.create(:insight, info_request: info_request) }

describe 'GET #show' do
it 'renders the show template' do
get :show, params: { info_request_id: info_request, id: insight }
expect(response).to render_template(:show)
end
end

describe 'GET #new' do
it 'assigns a new insight' do
get :new, params: { info_request_id: info_request }
expect(assigns(:insight)).to be_a(Insight)
expect(assigns(:insight)).to be_new_record
end

context 'when previous insights exist' do
let!(:last_insight) do
FactoryBot.create(
:insight, model: 'Model', temperature: '0.7', template: 'Template'
)
end

it 'copies model, temperature and template from last insight' do
get :new, params: { info_request_id: info_request }
expect(assigns(:insight).model).to eq(last_insight.model)
expect(assigns(:insight).temperature).to eq(last_insight.temperature)
expect(assigns(:insight).template).to eq(last_insight.template)
end
end
end

describe 'POST #create' do
let(:valid_params) do
{
info_request_id: info_request,
insight: {
model: 'TestModel', temperature: '0.3', template: 'TestTemplate'
}
}
end

context 'with valid params' do
it 'creates a new insight' do
expect {
post :create, params: valid_params
}.to change(Insight, :count).by(1)
end

it 'redirects to the created insight' do
post :create, params: valid_params
expect(response).to redirect_to(
admin_info_request_insight_path(info_request, Insight.last)
)
end
end

context 'with invalid params' do
let(:invalid_params) do
{
info_request_id: info_request,
insight: { model: nil, temperature: nil, template: nil }
}
end

it 'renders the new template' do
post :create, params: invalid_params
expect(response).to render_template(:new)
end
end
end

describe 'DELETE #destroy' do
let!(:insight_to_delete) do
FactoryBot.create(:insight, info_request: info_request)
end

it 'destroys the insight' do
expect {
delete :destroy, params: {
info_request_id: info_request, id: insight_to_delete
}
}.to change(Insight, :count).by(-1)
end

it 'redirects to the info request page' do
delete :destroy, params: {
info_request_id: info_request, id: insight_to_delete
}
expect(response).to redirect_to(admin_request_path(info_request))
end
end
end
21 changes: 21 additions & 0 deletions spec/models/insight_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@
end
end

describe '#duration' do
it 'returns nil when total_duration is not present' do
insight = FactoryBot.build(:insight)
expect(insight.duration).to be_nil
end

it 'returns formatted duration when total_duration exists' do
insight = FactoryBot.build(:insight)
insight.output = { 'total_duration' => 3_000_000_000 }
expect(insight.duration).to eq('3 seconds')
end
end

describe '#prompt' do
it 'replaces [initial_request] with first outgoing message body' do
outgoing_message = instance_double(
Expand All @@ -97,4 +110,12 @@
expect(insight.prompt).to eq('Template with message content')
end
end

describe '#response' do
it 'returns response from output' do
insight = FactoryBot.build(:insight)
insight.output = { 'response' => 'test response' }
expect(insight.response).to eq('test response')
end
end
end

0 comments on commit 0842651

Please sign in to comment.