Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin insights #8460

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ gem 'google-cloud-storage', '~> 1.52', require: false
# Storage content analyzers
gem 'excel_analyzer', path: 'gems/excel_analyzer', require: false

# AI insights
gem "ollama-ai", "~> 1.3.0"

group :test do
gem 'fivemat', '~> 1.3.7'
gem 'webmock', '~> 3.24.0'
Expand Down
17 changes: 17 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ GEM
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
erubi (1.13.0)
ethon (0.16.0)
ffi (>= 1.15.0)
exception_notification (4.5.0)
actionmailer (>= 5.2, < 8)
activesupport (>= 5.2, < 8)
Expand All @@ -199,8 +201,16 @@ GEM
logger
faraday-net_http (3.3.0)
net-http
faraday-typhoeus (1.1.0)
faraday (~> 2.0)
typhoeus (~> 1.4)
fast_gettext (3.1.0)
prime
ffi (1.17.0)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-arm64-darwin)
ffi (1.17.0-x86_64-darwin)
ffi (1.17.0-x86_64-linux-gnu)
fivemat (1.3.7)
flipper (0.28.3)
concurrent-ruby (< 2)
Expand Down Expand Up @@ -358,6 +368,10 @@ GEM
oink (0.10.1)
activerecord
hodel_3000_compliant_logger
ollama-ai (1.3.0)
faraday (~> 2.10)
faraday-typhoeus (~> 1.1)
typhoeus (~> 1.4, >= 1.4.1)
open4 (1.3.4)
os (1.1.4)
ostruct (0.6.0)
Expand Down Expand Up @@ -534,6 +548,8 @@ GEM
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
Expand Down Expand Up @@ -628,6 +644,7 @@ DEPENDENCIES
net-ssh-gateway (>= 1.1.0, < 3.0.0)
nokogiri (~> 1.16.7)
oink (~> 0.10.1)
ollama-ai (~> 1.3.0)
open4 (~> 1.3.0)
pg (~> 1.5.9)
pry (~> 0.15.0)
Expand Down
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
30 changes: 30 additions & 0 deletions app/jobs/insight_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
##
# InsightJob is responsible for generating InfoRequest insights using an AI
# model run via Ollama.
#
class InsightJob < ApplicationJob
queue_as :insights

delegate :model, :temperature, :prompt, to: :@insight

def perform(insight)
@insight = insight

insight.update(output: results.first)
end

private

def results
client.generate(
{ model: model, prompt: prompt, temperature: temperature, stream: false }
)
end

def client
Ollama.new(
credentials: { address: ENV['OLLAMA_URL'] },
options: { server_sent_events: true }
)
end
end
2 changes: 2 additions & 0 deletions app/models/info_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ def self.admin_title
-> { extraction },
class_name: 'Project::Submission'

has_many :insights, dependent: :destroy

attr_reader :followup_bad_reason

scope :internal, -> { where.not(user_id: nil) }
Expand Down
52 changes: 52 additions & 0 deletions app/models/insight.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# == Schema Information
# Schema version: 20241024140606
#
# Table name: insights
#
# id :bigint not null, primary key
# info_request_id :bigint
# model :string
# temperature :decimal(8, 2)
# template :text
# output :jsonb
# created_at :datetime not null
# 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
has_many :outgoing_messages, through: :info_request

serialize :output, type: Hash, coder: JSON, default: {}

validates :model, presence: true
validates :temperature, presence: true
validates :template, presence: true

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
InsightJob.perform_later(self)
end
end
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.text_field :model, 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
13 changes: 13 additions & 0 deletions db/migrate/20241024140606_create_insights.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateInsights < ActiveRecord::Migration[7.0]
def change
create_table :insights do |t|
t.references :info_request, foreign_key: true
t.string :model
t.decimal :temperature, precision: 8, scale: 2
t.text :template
t.jsonb :output

t.timestamps
end
end
end
Loading
Loading