mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-02-07 12:08:13 +00:00
* DEV: AI bot migration to the Llm pattern. We added tool and conversation context support to the Llm service in discourse-ai#366, meaning we met all the conditions to migrate this module. This PR migrates to the new pattern, meaning adding a new bot now requires minimal effort as long as the service supports it. On top of this, we introduce the concept of a "Playground" to separate the PM-specific bits from the completion, allowing us to use the bot in other contexts like chat in the future. Commands are called tools, and we simplified all the placeholder logic to perform updates in a single place, making the flow more one-wayish. * Followup fixes based on testing * Cleanup unused inference code * FIX: text-based tools could be in the middle of a sentence * GPT-4-turbo support * Use new LLM API
203 lines
5.1 KiB
Ruby
203 lines
5.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class AiPersona < ActiveRecord::Base
|
|
# places a hard limit, so per site we cache a maximum of 500 classes
|
|
MAX_PERSONAS_PER_SITE = 500
|
|
|
|
validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
|
|
validates :description, presence: true, length: { maximum: 2000 }
|
|
validates :system_prompt, presence: true, length: { maximum: 10_000_000 }
|
|
validate :system_persona_unchangeable, on: :update, if: :system
|
|
|
|
before_destroy :ensure_not_system
|
|
|
|
class MultisiteHash
|
|
def initialize(id)
|
|
@hash = Hash.new { |h, k| h[k] = {} }
|
|
@id = id
|
|
|
|
MessageBus.subscribe(channel_name) { |message| @hash[message.data] = {} }
|
|
end
|
|
|
|
def channel_name
|
|
"/multisite-hash-#{@id}"
|
|
end
|
|
|
|
def current_db
|
|
RailsMultisite::ConnectionManagement.current_db
|
|
end
|
|
|
|
def [](key)
|
|
@hash.dig(current_db, key)
|
|
end
|
|
|
|
def []=(key, val)
|
|
@hash[current_db][key] = val
|
|
end
|
|
|
|
def flush!
|
|
@hash[current_db] = {}
|
|
MessageBus.publish(channel_name, current_db)
|
|
end
|
|
end
|
|
|
|
def self.persona_cache
|
|
@persona_cache ||= MultisiteHash.new("persona_cache")
|
|
end
|
|
|
|
scope :ordered, -> { order("priority DESC, lower(name) ASC") }
|
|
|
|
def self.all_personas
|
|
persona_cache[:value] ||= AiPersona
|
|
.ordered
|
|
.where(enabled: true)
|
|
.all
|
|
.limit(MAX_PERSONAS_PER_SITE)
|
|
.map(&:class_instance)
|
|
end
|
|
|
|
after_commit :bump_cache
|
|
|
|
def bump_cache
|
|
self.class.persona_cache.flush!
|
|
end
|
|
|
|
def class_instance
|
|
allowed_group_ids = self.allowed_group_ids
|
|
id = self.id
|
|
system = self.system
|
|
|
|
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
|
|
if persona_class
|
|
persona_class.define_singleton_method :allowed_group_ids do
|
|
allowed_group_ids
|
|
end
|
|
|
|
persona_class.define_singleton_method :id do
|
|
id
|
|
end
|
|
|
|
persona_class.define_singleton_method :system do
|
|
system
|
|
end
|
|
|
|
return persona_class
|
|
end
|
|
|
|
name = self.name
|
|
description = self.description
|
|
ai_persona_id = self.id
|
|
|
|
options = {}
|
|
|
|
tools = self.respond_to?(:commands) ? self.commands : self.tools
|
|
|
|
tools =
|
|
tools.filter_map do |element|
|
|
inner_name = element
|
|
current_options = nil
|
|
|
|
if element.is_a?(Array)
|
|
inner_name = element[0]
|
|
current_options = element[1]
|
|
end
|
|
|
|
# Won't migrate data yet. Let's rewrite to the tool name.
|
|
inner_name = inner_name.gsub("Command", "")
|
|
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
|
|
|
|
begin
|
|
klass = ("DiscourseAi::AiBot::Tools::#{inner_name}").constantize
|
|
options[klass] = current_options if current_options
|
|
klass
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
end
|
|
|
|
Class.new(DiscourseAi::AiBot::Personas::Persona) do
|
|
define_singleton_method :id do
|
|
id
|
|
end
|
|
|
|
define_singleton_method :name do
|
|
name
|
|
end
|
|
|
|
define_singleton_method :description do
|
|
description
|
|
end
|
|
|
|
define_singleton_method :system do
|
|
system
|
|
end
|
|
|
|
define_singleton_method :allowed_group_ids do
|
|
allowed_group_ids
|
|
end
|
|
|
|
define_singleton_method :to_s do
|
|
"#<DiscourseAi::AiBot::Personas::Persona::Custom @name=#{self.name} @allowed_group_ids=#{self.allowed_group_ids.join(",")}>"
|
|
end
|
|
|
|
define_singleton_method :inspect do
|
|
"#<DiscourseAi::AiBot::Personas::Persona::Custom @name=#{self.name} @allowed_group_ids=#{self.allowed_group_ids.join(",")}>"
|
|
end
|
|
|
|
define_method :initialize do |*args, **kwargs|
|
|
@ai_persona = AiPersona.find_by(id: ai_persona_id)
|
|
super(*args, **kwargs)
|
|
end
|
|
|
|
define_method :tools do
|
|
tools
|
|
end
|
|
|
|
define_method :options do
|
|
options
|
|
end
|
|
|
|
define_method :system_prompt do
|
|
@ai_persona&.system_prompt || "You are a helpful bot."
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def system_persona_unchangeable
|
|
if system_prompt_changed? || commands_changed? || name_changed? || description_changed?
|
|
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona"))
|
|
end
|
|
end
|
|
|
|
def ensure_not_system
|
|
if system
|
|
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_delete_system_persona"))
|
|
throw :abort
|
|
end
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: ai_personas
|
|
#
|
|
# id :bigint not null, primary key
|
|
# name :string(100) not null
|
|
# description :string(2000) not null
|
|
# commands :string default([]), not null, is an Array
|
|
# system_prompt :string(10000000) not null
|
|
# allowed_group_ids :integer default([]), not null, is an Array
|
|
# created_by_id :integer
|
|
# enabled :boolean default(TRUE), not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# system :boolean default(FALSE), not null
|
|
# priority :integer default(0), not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_ai_personas_on_name (name) UNIQUE
|
|
#
|