2023-08-30 02:15:03 -04:00
|
|
|
#frozen_string_literal: true
|
|
|
|
|
|
|
|
module DiscourseAi
|
|
|
|
module AiBot
|
|
|
|
module Personas
|
|
|
|
class Persona
|
2024-01-04 08:44:07 -05:00
|
|
|
class << self
|
|
|
|
def system_personas
|
|
|
|
@system_personas ||= {
|
|
|
|
Personas::General => -1,
|
|
|
|
Personas::SqlHelper => -2,
|
|
|
|
Personas::Artist => -3,
|
|
|
|
Personas::SettingsExplorer => -4,
|
|
|
|
Personas::Researcher => -5,
|
|
|
|
Personas::Creative => -6,
|
|
|
|
Personas::DallE3 => -7,
|
|
|
|
}
|
|
|
|
end
|
2023-08-30 02:15:03 -04:00
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def system_personas_by_id
|
|
|
|
@system_personas_by_id ||= system_personas.invert
|
|
|
|
end
|
2023-09-14 02:46:56 -04:00
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def all(user:)
|
|
|
|
# listing tools has to be dynamic cause site settings may change
|
|
|
|
AiPersona.all_personas.filter do |persona|
|
|
|
|
next false if !user.in_any_groups?(persona.allowed_group_ids)
|
|
|
|
|
|
|
|
if persona.system
|
|
|
|
instance = persona.new
|
|
|
|
(
|
|
|
|
instance.required_tools == [] ||
|
|
|
|
(instance.required_tools - all_available_tools).empty?
|
|
|
|
)
|
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2023-08-30 02:15:03 -04:00
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def find_by(id: nil, name: nil, user:)
|
|
|
|
all(user: user).find { |persona| persona.id == id || persona.name == name }
|
|
|
|
end
|
2023-12-07 16:42:56 -05:00
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def name
|
|
|
|
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.name")
|
|
|
|
end
|
2023-09-14 02:46:56 -04:00
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def description
|
|
|
|
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.description")
|
2023-08-30 02:15:03 -04:00
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def all_available_tools
|
|
|
|
tools = [
|
|
|
|
Tools::ListCategories,
|
|
|
|
Tools::Time,
|
|
|
|
Tools::Search,
|
|
|
|
Tools::Summarize,
|
|
|
|
Tools::Read,
|
|
|
|
Tools::DbSchema,
|
|
|
|
Tools::SearchSettings,
|
|
|
|
Tools::Summarize,
|
|
|
|
Tools::SettingContext,
|
|
|
|
]
|
|
|
|
|
|
|
|
tools << Tools::ListTags if SiteSetting.tagging_enabled
|
|
|
|
tools << Tools::Image if SiteSetting.ai_stability_api_key.present?
|
|
|
|
|
|
|
|
tools << Tools::DallE if SiteSetting.ai_openai_api_key.present?
|
|
|
|
if SiteSetting.ai_google_custom_search_api_key.present? &&
|
|
|
|
SiteSetting.ai_google_custom_search_cx.present?
|
|
|
|
tools << Tools::Google
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
tools
|
2023-08-30 02:15:03 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def tools
|
|
|
|
[]
|
2023-08-30 02:15:03 -04:00
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def required_tools
|
|
|
|
[]
|
|
|
|
end
|
2023-08-30 02:15:03 -04:00
|
|
|
|
2024-02-02 15:09:34 -05:00
|
|
|
def temperature
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def top_p
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def options
|
|
|
|
{}
|
|
|
|
end
|
2023-08-30 02:15:03 -04:00
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def available_tools
|
|
|
|
self.class.all_available_tools.filter { |tool| tools.include?(tool) }
|
2023-08-30 02:15:03 -04:00
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def craft_prompt(context)
|
|
|
|
system_insts =
|
|
|
|
system_prompt.gsub(/\{(\w+)\}/) do |match|
|
|
|
|
found = context[match[1..-2].to_sym]
|
|
|
|
found.nil? ? match : found.to_s
|
|
|
|
end
|
2023-08-30 02:15:03 -04:00
|
|
|
|
2024-01-12 12:36:44 -05:00
|
|
|
prompt =
|
|
|
|
DiscourseAi::Completions::Prompt.new(
|
|
|
|
<<~TEXT.strip,
|
|
|
|
#{system_insts}
|
|
|
|
#{available_tools.map(&:custom_system_message).compact_blank.join("\n")}
|
2024-01-04 08:44:07 -05:00
|
|
|
TEXT
|
2024-01-12 12:36:44 -05:00
|
|
|
messages: context[:conversation_context].to_a,
|
|
|
|
)
|
2023-08-30 02:15:03 -04:00
|
|
|
|
2024-01-12 12:36:44 -05:00
|
|
|
prompt.tools = available_tools.map(&:signature) if available_tools
|
|
|
|
|
|
|
|
prompt
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
end
|
|
|
|
|
2024-01-04 08:44:07 -05:00
|
|
|
def find_tool(partial)
|
|
|
|
parsed_function = Nokogiri::HTML5.fragment(partial)
|
|
|
|
function_id = parsed_function.at("tool_id")&.text
|
|
|
|
function_name = parsed_function.at("tool_name")&.text
|
|
|
|
return false if function_name.nil?
|
|
|
|
|
|
|
|
tool_klass = available_tools.find { |c| c.signature.dig(:name) == function_name }
|
|
|
|
return false if tool_klass.nil?
|
|
|
|
|
2024-01-04 22:39:32 -05:00
|
|
|
arguments = {}
|
|
|
|
tool_klass.signature[:parameters].to_a.each do |param|
|
|
|
|
name = param[:name]
|
|
|
|
value = parsed_function.at(name)&.text
|
|
|
|
|
|
|
|
if param[:type] == "array" && value
|
|
|
|
value =
|
|
|
|
begin
|
|
|
|
JSON.parse(value)
|
|
|
|
rescue JSON::ParserError
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
arguments[name.to_sym] = value if value
|
|
|
|
end
|
2024-01-04 08:44:07 -05:00
|
|
|
|
|
|
|
tool_klass.new(
|
|
|
|
arguments,
|
|
|
|
tool_call_id: function_id,
|
|
|
|
persona_options: options[tool_klass].to_h,
|
|
|
|
)
|
2023-08-30 02:15:03 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|