diff --git a/app/controllers/admin/insights_controller.rb b/app/controllers/admin/insights_controller.rb
new file mode 100644
index 0000000000..a9e16e5b27
--- /dev/null
+++ b/app/controllers/admin/insights_controller.rb
@@ -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
diff --git a/app/models/insight.rb b/app/models/insight.rb
index 889fe638c8..aa9491ad0a 100644
--- a/app/models/insight.rb
+++ b/app/models/insight.rb
@@ -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
@@ -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
diff --git a/app/views/admin/insights/_list.html.erb b/app/views/admin/insights/_list.html.erb
new file mode 100644
index 0000000000..01b0058216
--- /dev/null
+++ b/app/views/admin/insights/_list.html.erb
@@ -0,0 +1,34 @@
+
+ <% if insights.any? %>
+
+
+ ID |
+ Model |
+ Template |
+ Created at |
+ Updated at |
+ Actions |
+
+
+ <% insights.each do |insight| %>
+
+ <%= insight.to_param %> |
+ <%= insight.model %> |
+ <%= insight.temperature %> |
+ <%= truncate(insight.template, length: 150) %> |
+ <%= admin_date(insight.created_at) %> |
+ <%= admin_date(insight.updated_at) %> |
+ <%= link_to "Show", admin_info_request_insight_path(info_request, insight), class: 'btn' %> |
+
+ <% end %>
+
+ <% else %>
+
None yet.
+ <% end %>
+
+
+
+
+ <%= link_to "New insight", new_admin_info_request_insight_path(info_request), :class => "btn btn-info" %>
+
+
diff --git a/app/views/admin/insights/new.html.erb b/app/views/admin/insights/new.html.erb
new file mode 100644
index 0000000000..098cb70673
--- /dev/null
+++ b/app/views/admin/insights/new.html.erb
@@ -0,0 +1,45 @@
+<% @title = 'New insight' %>
+
+
+
+<%= form_for [:admin, @info_request, @insight], html: { class: 'form form-horizontal' } do |f| %>
+ <%= foi_error_messages_for :insight %>
+
+
+ <%= f.label :model, class: 'control-label' %>
+
+ <%= f.select :model, Insight.models, {}, class: 'span6' %>
+
+
+
+
+ <%= f.label :temperature, class: 'control-label' %>
+
+ <%= 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') %>
+
+
+
+
+ <%= f.label :template, class: 'control-label' %>
+
+ <%= f.text_area :template, class: 'span6', rows: 10 %>
+
+
+ Add [initial_request] to substitute in the first 500
+ characters from the body of the initial outgoing message into the prompt
+ sent to the model.
+
+
+
+
+
+ <%= submit_tag 'Create', class: 'btn btn-success' %>
+
+<% end %>
diff --git a/app/views/admin/insights/show.html.erb b/app/views/admin/insights/show.html.erb
new file mode 100644
index 0000000000..29ea3e9050
--- /dev/null
+++ b/app/views/admin/insights/show.html.erb
@@ -0,0 +1,28 @@
+
+
+
+
+ ID
+ |
+
+ <%= @insight.id %>
+ |
+
+ <% @insight.for_admin_column do |name, value| %>
+
+
+ <%= name.humanize %>
+ |
+
+ <% if name == 'prompt' || name == 'response' %>
+ <%= value&.strip %>
+ <% else %>
+ <%= h admin_value(value) %>
+ <% end %>
+ |
+
+ <% end %>
+
+
+
+<%= link_to "Destroy", admin_info_request_insight_path(@info_request, @insight), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger' %>
diff --git a/app/views/admin_request/show.html.erb b/app/views/admin_request/show.html.erb
index 191544d993..f5207c3dc8 100644
--- a/app/views/admin_request/show.html.erb
+++ b/app/views/admin_request/show.html.erb
@@ -413,3 +413,11 @@
<%= render partial: 'admin/notes/show',
locals: { notes: @info_request.all_notes,
notable: @info_request } %>
+
+
+
+Insights
+
+<%= render partial: 'admin/insights/list',
+ locals: { info_request: @info_request,
+ insights: @info_request.insights } %>
diff --git a/config/routes.rb b/config/routes.rb
index 14a77c88c6..6a133e64a7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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,
diff --git a/spec/controllers/admin/insights_controller_spec.rb b/spec/controllers/admin/insights_controller_spec.rb
new file mode 100644
index 0000000000..98a2bb44c3
--- /dev/null
+++ b/spec/controllers/admin/insights_controller_spec.rb
@@ -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
diff --git a/spec/models/insight_spec.rb b/spec/models/insight_spec.rb
index a150db81d7..f2b3a3f17b 100644
--- a/spec/models/insight_spec.rb
+++ b/spec/models/insight_spec.rb
@@ -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(
@@ -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