REFACTOR: Move personas into its own module. (#1233)

This change moves all the personas code into its own module. We want to treat them as a building block features can built on top of, same as `Completions::Llm`.

The code to title a message was moved from `Bot` to `Playground`.
This commit is contained in:
Roman Rizzi 2025-03-31 14:42:33 -03:00 committed by GitHub
parent 5b6d39a206
commit 30242a27e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 781 additions and 843 deletions

View File

@ -15,7 +15,7 @@ module DiscourseAi
LocalizedAiPersonaSerializer.new(persona, root: false)
end
tools =
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
AiToolSerializer.new(tool, root: false)
end
AiTool

View File

@ -12,11 +12,11 @@ module ::Jobs
return if message.blank?
personaClass =
DiscourseAi::AiBot::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
DiscourseAi::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
return if personaClass.blank?
user = User.find_by(id: personaClass.user_id)
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
bot = DiscourseAi::Personas::Bot.as(user, persona: personaClass.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
message,

View File

@ -10,13 +10,13 @@ module ::Jobs
persona_id = args[:persona_id]
begin
persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil?
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil?
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
rescue DiscourseAi::AiBot::Bot::BOT_NOT_FOUND
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
Rails.logger.warn(
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
)

View File

@ -19,7 +19,7 @@ module Jobs
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
bot =
DiscourseAi::AiBot::Bot.as(
DiscourseAi::Personas::Bot.as(
Discourse.system_user,
persona: ai_persona_klass.new,
model: llm_model,
@ -31,7 +31,7 @@ module Jobs
base = { query: query, model_used: llm_model.display_name }
context =
DiscourseAi::AiBot::BotContext.new(
DiscourseAi::Personas::BotContext.new(
messages: [{ type: :user, content: query }],
skip_tool_details: true,
)

View File

@ -201,14 +201,14 @@ class AiPersona < ActiveRecord::Base
if inner_name.start_with?("custom-")
custom_tool_id = inner_name.split("-", 2).last.to_i
if AiTool.exists?(id: custom_tool_id, enabled: true)
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
klass = DiscourseAi::Personas::Tools::Custom.class_instance(custom_tool_id)
end
else
inner_name = inner_name.gsub("Tool", "")
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
begin
klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize
klass = "DiscourseAi::Personas::Tools::#{inner_name}".constantize
options[klass] = current_options if current_options
rescue StandardError
end
@ -218,7 +218,7 @@ class AiPersona < ActiveRecord::Base
klass
end
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id]
if persona_class
instance_attributes.each do |key, value|
# description/name are localized
@ -230,7 +230,7 @@ class AiPersona < ActiveRecord::Base
ai_persona_id = self.id
Class.new(DiscourseAi::AiBot::Personas::Persona) do
Class.new(DiscourseAi::Personas::Persona) do
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
define_singleton_method(:to_s) do

View File

@ -36,7 +36,7 @@ class AiTool < ActiveRecord::Base
end
def runner(parameters, llm:, bot_user:, context: nil)
DiscourseAi::AiBot::ToolRunner.new(
DiscourseAi::Personas::ToolRunner.new(
parameters: parameters,
llm: llm,
bot_user: bot_user,

View File

@ -1,18 +1,18 @@
# frozen_string_literal: true
DiscourseAi::AiBot::Personas::Persona.system_personas.each do |persona_class, id|
DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
persona = AiPersona.find_by(id: id)
if !persona
persona = AiPersona.new
persona.id = id
if persona_class == DiscourseAi::AiBot::Personas::WebArtifactCreator
if persona_class == DiscourseAi::Personas::WebArtifactCreator
# this is somewhat sensitive, so we default it to staff
persona.allowed_group_ids = [Group::AUTO_GROUPS[:staff]]
else
persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]]
end
persona.enabled = true
persona.priority = true if persona_class == DiscourseAi::AiBot::Personas::General
persona.priority = true if persona_class == DiscourseAi::Personas::General
end
names = [

View File

@ -110,7 +110,7 @@ module DiscourseAi
scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map)
end,
) do
DiscourseAi::AiBot::Personas::Persona
DiscourseAi::Personas::Persona
.all(user: scope.user)
.map do |persona|
{
@ -205,8 +205,7 @@ module DiscourseAi
include_condition: -> { SiteSetting.ai_bot_enabled && object.topic.private_message? },
) do
id = topic.custom_fields["ai_persona_id"]
name =
DiscourseAi::AiBot::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
name = DiscourseAi::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
name || topic.custom_fields["ai_persona"]
end

View File

@ -1,19 +0,0 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Creative < Persona
def tools
[]
end
def system_prompt
<<~PROMPT
You are a helpful bot
PROMPT
end
end
end
end
end

View File

@ -1,397 +0,0 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Persona
class << self
def rag_conversation_chunks
10
end
def vision_enabled
false
end
def vision_max_pixels
1_048_576
end
def question_consolidator_llm_id
nil
end
def force_default_llm
false
end
def allow_chat_channel_mentions
false
end
def allow_chat_direct_messages
false
end
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,
Personas::DiscourseHelper => -8,
Personas::GithubHelper => -9,
Personas::WebArtifactCreator => -10,
}
end
def system_personas_by_id
@system_personas_by_id ||= system_personas.invert
end
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
def find_by(id: nil, name: nil, user:)
all(user: user).find { |persona| persona.id == id || persona.name == name }
end
def name
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.name")
end
def description
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.description")
end
def all_available_tools
tools = [
Tools::ListCategories,
Tools::Time,
Tools::Search,
Tools::Read,
Tools::DbSchema,
Tools::SearchSettings,
Tools::SettingContext,
Tools::RandomPicker,
Tools::DiscourseMetaSearch,
Tools::GithubFileContent,
Tools::GithubPullRequestDiff,
Tools::GithubSearchFiles,
Tools::WebBrowser,
Tools::JavascriptEvaluator,
]
if SiteSetting.ai_artifact_security.in?(%w[lax strict])
tools << Tools::CreateArtifact
tools << Tools::UpdateArtifact
tools << Tools::ReadArtifact
end
tools << Tools::GithubSearchCode if SiteSetting.ai_bot_github_access_token.present?
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
end
tools
end
end
def id
@ai_persona&.id || self.class.system_personas[self.class]
end
def tools
[]
end
def force_tool_use
[]
end
def forced_tool_count
-1
end
def required_tools
[]
end
def temperature
nil
end
def top_p
nil
end
def options
{}
end
def available_tools
self
.class
.all_available_tools
.filter { |tool| tools.include?(tool) }
.concat(tools.filter(&:custom?))
end
def craft_prompt(context, llm: nil)
system_insts =
system_prompt.gsub(/\{(\w+)\}/) do |match|
found = context.lookup_template_param(match[1..-2])
found.nil? ? match : found.to_s
end
prompt_insts = <<~TEXT.strip
#{system_insts}
#{available_tools.map(&:custom_system_message).compact_blank.join("\n")}
TEXT
question_consolidator_llm = llm
if self.class.question_consolidator_llm_id.present?
question_consolidator_llm ||=
DiscourseAi::Completions::Llm.proxy(
LlmModel.find_by(id: self.class.question_consolidator_llm_id),
)
end
if context.custom_instructions.present?
prompt_insts << "\n"
prompt_insts << context.custom_instructions
end
fragments_guidance =
rag_fragments_prompt(
context.messages,
llm: question_consolidator_llm,
user: context.user,
)&.strip
prompt_insts << fragments_guidance if fragments_guidance.present?
prompt =
DiscourseAi::Completions::Prompt.new(
prompt_insts,
messages: context.messages,
topic_id: context.topic_id,
post_id: context.post_id,
)
prompt.max_pixels = self.class.vision_max_pixels if self.class.vision_enabled
prompt.tools = available_tools.map(&:signature) if available_tools
available_tools.each do |tool|
tool.inject_prompt(prompt: prompt, context: context, persona: self)
end
prompt
end
def find_tool(partial, bot_user:, llm:, context:, existing_tools: [])
return nil if !partial.is_a?(DiscourseAi::Completions::ToolCall)
tool_instance(
partial,
bot_user: bot_user,
llm: llm,
context: context,
existing_tools: existing_tools,
)
end
def allow_partial_tool_calls?
available_tools.any? { |tool| tool.allow_partial_tool_calls? }
end
protected
def tool_instance(tool_call, bot_user:, llm:, context:, existing_tools:)
function_id = tool_call.id
function_name = tool_call.name
return nil if function_name.nil?
tool_klass = available_tools.find { |c| c.signature.dig(:name) == function_name }
return nil if tool_klass.nil?
arguments = {}
tool_klass.signature[:parameters].to_a.each do |param|
name = param[:name]
value = tool_call.parameters[name.to_sym]
if param[:type] == "array" && value
value =
begin
JSON.parse(value)
rescue JSON::ParserError
[value.to_s]
end
elsif param[:type] == "string" && value
value = strip_quotes(value).to_s
elsif param[:type] == "integer" && value
value = strip_quotes(value).to_i
end
if param[:enum] && value && !param[:enum].include?(value)
# invalid enum value
value = nil
end
arguments[name.to_sym] = value if value
end
tool_instance =
existing_tools.find { |t| t.name == function_name && t.tool_call_id == function_id }
if tool_instance
tool_instance.parameters = arguments
tool_instance
else
tool_klass.new(
arguments,
tool_call_id: function_id || function_name,
persona_options: options[tool_klass].to_h,
bot_user: bot_user,
llm: llm,
context: context,
)
end
end
def strip_quotes(value)
if value.is_a?(String)
if value.start_with?('"') && value.end_with?('"')
value = value[1..-2]
elsif value.start_with?("'") && value.end_with?("'")
value = value[1..-2]
else
value
end
else
value
end
end
def rag_fragments_prompt(conversation_context, llm:, user:)
upload_refs =
UploadReference.where(target_id: id, target_type: "AiPersona").pluck(:upload_id)
return nil if !DiscourseAi::Embeddings.enabled?
return nil if conversation_context.blank? || upload_refs.blank?
latest_interactions =
conversation_context.select { |ctx| %i[model user].include?(ctx[:type]) }.last(10)
return nil if latest_interactions.empty?
# first response
if latest_interactions.length == 1
consolidated_question = latest_interactions[0][:content]
else
consolidated_question =
DiscourseAi::AiBot::QuestionConsolidator.consolidate_question(
llm,
latest_interactions,
user,
)
end
return nil if !consolidated_question
vector = DiscourseAi::Embeddings::Vector.instance
reranker = DiscourseAi::Inference::HuggingFaceTextEmbeddings
interactions_vector = vector.vector_from(consolidated_question)
rag_conversation_chunks = self.class.rag_conversation_chunks
search_limit =
if reranker.reranker_configured?
rag_conversation_chunks * 5
else
rag_conversation_chunks
end
schema = DiscourseAi::Embeddings::Schema.for(RagDocumentFragment)
candidate_fragment_ids =
schema
.asymmetric_similarity_search(
interactions_vector,
limit: search_limit,
offset: 0,
) { |builder| builder.join(<<~SQL, target_id: id, target_type: "AiPersona") }
rag_document_fragments ON
rag_document_fragments.id = rag_document_fragment_id AND
rag_document_fragments.target_id = :target_id AND
rag_document_fragments.target_type = :target_type
SQL
.map(&:rag_document_fragment_id)
fragments =
RagDocumentFragment.where(upload_id: upload_refs, id: candidate_fragment_ids).pluck(
:fragment,
:metadata,
)
if reranker.reranker_configured?
guidance = fragments.map { |fragment, _metadata| fragment }
ranks =
DiscourseAi::Inference::HuggingFaceTextEmbeddings
.rerank(conversation_context.last[:content], guidance)
.to_a
.take(rag_conversation_chunks)
.map { _1[:index] }
if ranks.empty?
fragments = fragments.take(rag_conversation_chunks)
else
fragments = ranks.map { |idx| fragments[idx] }
end
end
<<~TEXT
<guidance>
The following texts will give you additional guidance for your response.
We included them because we believe they are relevant to this conversation topic.
Texts:
#{
fragments
.map do |fragment, metadata|
if metadata.present?
["# #{metadata}", fragment].join("\n")
else
fragment
end
end
.join("\n")
}
</guidance>
TEXT
end
end
end
end
end

View File

@ -150,13 +150,11 @@ module DiscourseAi
persona = nil
if persona_id
persona =
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
end
if !persona && persona_name = post.topic.custom_fields["ai_persona"]
persona =
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, name: persona_name)
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, name: persona_name)
end
# edge case, llm was mentioned in an ai persona conversation
@ -172,11 +170,11 @@ module DiscourseAi
end
end
persona ||= DiscourseAi::AiBot::Personas::General
persona ||= DiscourseAi::Personas::General
bot_user = User.find(persona.user_id) if persona && persona.force_default_llm
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
new(bot).update_playground_with(post)
end
end
@ -198,8 +196,8 @@ module DiscourseAi
bot_user = user || ai_persona.user
raise Discourse::InvalidParameters.new(:user) if bot_user.nil?
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
playground = DiscourseAi::AiBot::Playground.new(bot)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
playground = new(bot)
playground.reply_to(
post,
@ -236,14 +234,54 @@ module DiscourseAi
include_uploads: bot.persona.class.vision_enabled,
)
# conversation context may contain tool calls, and confusing user names
# clean it up
conversation = +""
messages.each do |context|
if context[:type] == :user
conversation << "User said:\n#{context[:content]}\n\n"
elsif context[:type] == :model
conversation << "Model said:\n#{context[:content]}\n\n"
end
end
system_insts = <<~TEXT.strip
You are titlebot. Given a conversation, you will suggest a title.
- You will never respond with anything but the suggested title.
- You will always match the conversation language in your title suggestion.
- Title will capture the essence of the conversation.
TEXT
instruction = <<~TEXT.strip
Given the following conversation:
{{{
#{conversation}
}}}
Reply only with a title that is 7 words or less.
TEXT
title_prompt =
DiscourseAi::Completions::Prompt.new(
system_insts,
messages: [type: :user, content: instruction],
topic_id: post.topic_id,
)
new_title =
bot
.get_updated_title(messages, post, user)
.tap do |new_title|
.llm
.generate(title_prompt, user: user, feature_name: "bot_title")
.strip
.split("\n")
.last
PostRevisor.new(post.topic.first_post, post.topic).revise!(
bot.bot_user,
title: new_title.sub(/\A"/, "").sub(/"\Z/, ""),
)
end
allowed_users = post.topic.topic_allowed_users.pluck(:user_id)
MessageBus.publish(
@ -271,7 +309,7 @@ module DiscourseAi
end
context =
BotContext.new(
DiscourseAi::Personas::BotContext.new(
participants: participants,
message_id: message.id,
channel_id: channel.id,
@ -372,7 +410,7 @@ module DiscourseAi
end
context =
BotContext.new(
DiscourseAi::Personas::BotContext.new(
post: post,
custom_instructions: custom_instructions,
messages:
@ -575,8 +613,7 @@ module DiscourseAi
def schedule_bot_reply(post)
persona_id =
DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] ||
bot.persona.class.id
DiscourseAi::Personas::Persona.system_personas[bot.persona.class] || bot.persona.class.id
::Jobs.enqueue(
:create_ai_reply,
post_id: post.id,

View File

@ -77,8 +77,8 @@ module DiscourseAi
io.flush
persona_class =
DiscourseAi::AiBot::Personas::Persona.find_by(id: persona.id, user: current_user)
bot = DiscourseAi::AiBot::Bot.as(persona.user, persona: persona_class.new)
DiscourseAi::Personas::Persona.find_by(id: persona.id, user: current_user)
bot = DiscourseAi::Personas::Bot.as(persona.user, persona: persona_class.new)
data =
{

View File

@ -7,7 +7,7 @@ module DiscourseAi
return if !tool
return if !tool.parameters.blank?
context = DiscourseAi::AiBot::BotContext.new(post: post)
context = DiscourseAi::Personas::BotContext.new(post: post)
runner = tool.runner({}, llm: nil, bot_user: Discourse.system_user, context: context)
runner.invoke

View File

@ -10,7 +10,7 @@ module DiscourseAi
.find { |persona| persona.id == SiteSetting.ai_discord_search_persona.to_i }
.new
@bot =
DiscourseAi::AiBot::Bot.as(
DiscourseAi::Personas::Bot.as(
Discourse.system_user,
persona: @persona,
model: LlmModel.find(@persona.class.default_llm_id),

View File

@ -4,7 +4,7 @@ module DiscourseAi
module Discord::Bot
class Search < Base
def initialize(body)
@search = DiscourseAi::AiBot::Tools::Search
@search = DiscourseAi::Personas::Tools::Search
super(body)
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module ArtifactUpdateStrategies
class InvalidFormatError < StandardError
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module ArtifactUpdateStrategies
class Diff < Base
attr_reader :failed_searches

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module ArtifactUpdateStrategies
class Full < Base
private

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Artist < Persona
def tools
@ -34,5 +33,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Bot
attr_reader :model
@ -13,7 +13,7 @@ module DiscourseAi
# limit is arbitrary, but 5 which was used in the past was too low
MAX_TOOLS = 20
def self.as(bot_user, persona: DiscourseAi::AiBot::Personas::General.new, model: nil)
def self.as(bot_user, persona: DiscourseAi::Personas::General.new, model: nil)
new(bot_user, persona, model)
end
@ -27,49 +27,8 @@ module DiscourseAi
attr_reader :bot_user
attr_accessor :persona
def get_updated_title(conversation_context, post, user)
system_insts = <<~TEXT.strip
You are titlebot. Given a conversation, you will suggest a title.
- You will never respond with anything but the suggested title.
- You will always match the conversation language in your title suggestion.
- Title will capture the essence of the conversation.
TEXT
# conversation context may contain tool calls, and confusing user names
# clean it up
conversation = +""
conversation_context.each do |context|
if context[:type] == :user
conversation << "User said:\n#{context[:content]}\n\n"
elsif context[:type] == :model
conversation << "Model said:\n#{context[:content]}\n\n"
end
end
instruction = <<~TEXT.strip
Given the following conversation:
{{{
#{conversation}
}}}
Reply only with a title that is 7 words or less.
TEXT
title_prompt =
DiscourseAi::Completions::Prompt.new(
system_insts,
messages: [type: :user, content: instruction],
topic_id: post.topic_id,
)
DiscourseAi::Completions::Llm
.proxy(model)
.generate(title_prompt, user: user, feature_name: "bot_title")
.strip
.split("\n")
.last
def llm
@llm ||= DiscourseAi::Completions::Llm.proxy(model)
end
def force_tool_if_needed(prompt, context)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class BotContext
attr_accessor :messages,
:topic_id,

17
lib/personas/creative.rb Normal file
View File

@ -0,0 +1,17 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Creative < Persona
def tools
[]
end
def system_prompt
<<~PROMPT
You are a helpful bot
PROMPT
end
end
end
end

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class DallE3 < Persona
def tools
@ -35,5 +34,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class DiscourseHelper < Persona
def tools
@ -44,5 +43,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class General < Persona
def tools
@ -30,5 +29,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class GithubHelper < Persona
def tools
@ -25,5 +24,4 @@ module DiscourseAi
end
end
end
end
end

395
lib/personas/persona.rb Normal file
View File

@ -0,0 +1,395 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Persona
class << self
def rag_conversation_chunks
10
end
def vision_enabled
false
end
def vision_max_pixels
1_048_576
end
def question_consolidator_llm_id
nil
end
def force_default_llm
false
end
def allow_chat_channel_mentions
false
end
def allow_chat_direct_messages
false
end
def system_personas
@system_personas ||= {
General => -1,
SqlHelper => -2,
Artist => -3,
SettingsExplorer => -4,
Researcher => -5,
Creative => -6,
DallE3 => -7,
DiscourseHelper => -8,
GithubHelper => -9,
WebArtifactCreator => -10,
}
end
def system_personas_by_id
@system_personas_by_id ||= system_personas.invert
end
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
def find_by(id: nil, name: nil, user:)
all(user: user).find { |persona| persona.id == id || persona.name == name }
end
def name
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.name")
end
def description
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.description")
end
def all_available_tools
tools = [
Tools::ListCategories,
Tools::Time,
Tools::Search,
Tools::Read,
Tools::DbSchema,
Tools::SearchSettings,
Tools::SettingContext,
Tools::RandomPicker,
Tools::DiscourseMetaSearch,
Tools::GithubFileContent,
Tools::GithubPullRequestDiff,
Tools::GithubSearchFiles,
Tools::WebBrowser,
Tools::JavascriptEvaluator,
]
if SiteSetting.ai_artifact_security.in?(%w[lax strict])
tools << Tools::CreateArtifact
tools << Tools::UpdateArtifact
tools << Tools::ReadArtifact
end
tools << Tools::GithubSearchCode if SiteSetting.ai_bot_github_access_token.present?
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
end
tools
end
end
def id
@ai_persona&.id || self.class.system_personas[self.class]
end
def tools
[]
end
def force_tool_use
[]
end
def forced_tool_count
-1
end
def required_tools
[]
end
def temperature
nil
end
def top_p
nil
end
def options
{}
end
def available_tools
self
.class
.all_available_tools
.filter { |tool| tools.include?(tool) }
.concat(tools.filter(&:custom?))
end
def craft_prompt(context, llm: nil)
system_insts =
system_prompt.gsub(/\{(\w+)\}/) do |match|
found = context.lookup_template_param(match[1..-2])
found.nil? ? match : found.to_s
end
prompt_insts = <<~TEXT.strip
#{system_insts}
#{available_tools.map(&:custom_system_message).compact_blank.join("\n")}
TEXT
question_consolidator_llm = llm
if self.class.question_consolidator_llm_id.present?
question_consolidator_llm ||=
DiscourseAi::Completions::Llm.proxy(
LlmModel.find_by(id: self.class.question_consolidator_llm_id),
)
end
if context.custom_instructions.present?
prompt_insts << "\n"
prompt_insts << context.custom_instructions
end
fragments_guidance =
rag_fragments_prompt(
context.messages,
llm: question_consolidator_llm,
user: context.user,
)&.strip
prompt_insts << fragments_guidance if fragments_guidance.present?
prompt =
DiscourseAi::Completions::Prompt.new(
prompt_insts,
messages: context.messages,
topic_id: context.topic_id,
post_id: context.post_id,
)
prompt.max_pixels = self.class.vision_max_pixels if self.class.vision_enabled
prompt.tools = available_tools.map(&:signature) if available_tools
available_tools.each do |tool|
tool.inject_prompt(prompt: prompt, context: context, persona: self)
end
prompt
end
def find_tool(partial, bot_user:, llm:, context:, existing_tools: [])
return nil if !partial.is_a?(DiscourseAi::Completions::ToolCall)
tool_instance(
partial,
bot_user: bot_user,
llm: llm,
context: context,
existing_tools: existing_tools,
)
end
def allow_partial_tool_calls?
available_tools.any? { |tool| tool.allow_partial_tool_calls? }
end
protected
def tool_instance(tool_call, bot_user:, llm:, context:, existing_tools:)
function_id = tool_call.id
function_name = tool_call.name
return nil if function_name.nil?
tool_klass = available_tools.find { |c| c.signature.dig(:name) == function_name }
return nil if tool_klass.nil?
arguments = {}
tool_klass.signature[:parameters].to_a.each do |param|
name = param[:name]
value = tool_call.parameters[name.to_sym]
if param[:type] == "array" && value
value =
begin
JSON.parse(value)
rescue JSON::ParserError
[value.to_s]
end
elsif param[:type] == "string" && value
value = strip_quotes(value).to_s
elsif param[:type] == "integer" && value
value = strip_quotes(value).to_i
end
if param[:enum] && value && !param[:enum].include?(value)
# invalid enum value
value = nil
end
arguments[name.to_sym] = value if value
end
tool_instance =
existing_tools.find { |t| t.name == function_name && t.tool_call_id == function_id }
if tool_instance
tool_instance.parameters = arguments
tool_instance
else
tool_klass.new(
arguments,
tool_call_id: function_id || function_name,
persona_options: options[tool_klass].to_h,
bot_user: bot_user,
llm: llm,
context: context,
)
end
end
def strip_quotes(value)
if value.is_a?(String)
if value.start_with?('"') && value.end_with?('"')
value = value[1..-2]
elsif value.start_with?("'") && value.end_with?("'")
value = value[1..-2]
else
value
end
else
value
end
end
def rag_fragments_prompt(conversation_context, llm:, user:)
upload_refs =
UploadReference.where(target_id: id, target_type: "AiPersona").pluck(:upload_id)
return nil if !DiscourseAi::Embeddings.enabled?
return nil if conversation_context.blank? || upload_refs.blank?
latest_interactions =
conversation_context.select { |ctx| %i[model user].include?(ctx[:type]) }.last(10)
return nil if latest_interactions.empty?
# first response
if latest_interactions.length == 1
consolidated_question = latest_interactions[0][:content]
else
consolidated_question =
DiscourseAi::Personas::QuestionConsolidator.consolidate_question(
llm,
latest_interactions,
user,
)
end
return nil if !consolidated_question
vector = DiscourseAi::Embeddings::Vector.instance
reranker = DiscourseAi::Inference::HuggingFaceTextEmbeddings
interactions_vector = vector.vector_from(consolidated_question)
rag_conversation_chunks = self.class.rag_conversation_chunks
search_limit =
if reranker.reranker_configured?
rag_conversation_chunks * 5
else
rag_conversation_chunks
end
schema = DiscourseAi::Embeddings::Schema.for(RagDocumentFragment)
candidate_fragment_ids =
schema
.asymmetric_similarity_search(
interactions_vector,
limit: search_limit,
offset: 0,
) { |builder| builder.join(<<~SQL, target_id: id, target_type: "AiPersona") }
rag_document_fragments ON
rag_document_fragments.id = rag_document_fragment_id AND
rag_document_fragments.target_id = :target_id AND
rag_document_fragments.target_type = :target_type
SQL
.map(&:rag_document_fragment_id)
fragments =
RagDocumentFragment.where(upload_id: upload_refs, id: candidate_fragment_ids).pluck(
:fragment,
:metadata,
)
if reranker.reranker_configured?
guidance = fragments.map { |fragment, _metadata| fragment }
ranks =
DiscourseAi::Inference::HuggingFaceTextEmbeddings
.rerank(conversation_context.last[:content], guidance)
.to_a
.take(rag_conversation_chunks)
.map { _1[:index] }
if ranks.empty?
fragments = fragments.take(rag_conversation_chunks)
else
fragments = ranks.map { |idx| fragments[idx] }
end
end
<<~TEXT
<guidance>
The following texts will give you additional guidance for your response.
We included them because we believe they are relevant to this conversation topic.
Texts:
#{
fragments
.map do |fragment, metadata|
if metadata.present?
["# #{metadata}", fragment].join("\n")
else
fragment
end
end
.join("\n")
}
</guidance>
TEXT
end
end
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class QuestionConsolidator
attr_reader :llm, :messages, :user, :max_tokens

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Researcher < Persona
def tools
@ -48,5 +47,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class SettingsExplorer < Persona
def tools
@ -22,5 +21,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class SqlHelper < Persona
def self.schema
@ -25,8 +24,7 @@ module DiscourseAi
order by table_name
SQL
priority =
+(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
priority = +(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
other_tables = +""
tables.each do |table_name, _|
@ -103,5 +101,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class ToolRunner
attr_reader :tool, :parameters, :llm
attr_accessor :running_attached_function, :timeout, :custom_raw
@ -14,11 +14,11 @@ module DiscourseAi
MAX_HTTP_REQUESTS = 20
def initialize(parameters:, llm:, bot_user:, context: nil, tool:, timeout: nil)
if context && !context.is_a?(DiscourseAi::AiBot::BotContext)
if context && !context.is_a?(DiscourseAi::Personas::BotContext)
raise ArgumentError, "context must be a BotContext object"
end
context ||= DiscourseAi::AiBot::BotContext.new
context ||= DiscourseAi::Personas::BotContext.new
@parameters = parameters
@llm = llm
@ -339,7 +339,7 @@ module DiscourseAi
return { error: "Persona not found" } if persona_class.nil?
persona = persona_class.new
bot = DiscourseAi::AiBot::Bot.as(@bot_user || persona.user, persona: persona)
bot = DiscourseAi::Personas::Bot.as(@bot_user || persona.user, persona: persona)
playground = DiscourseAi::AiBot::Playground.new(bot)
if @context.post_id
@ -488,7 +488,7 @@ module DiscourseAi
headers = (options && options["headers"]) || {}
result = {}
DiscourseAi::AiBot::Tools::Tool.send_http_request(
DiscourseAi::Personas::Tools::Tool.send_http_request(
url,
headers: headers,
) do |response|
@ -517,7 +517,7 @@ module DiscourseAi
body = options && options["body"]
result = {}
DiscourseAi::AiBot::Tools::Tool.send_http_request(
DiscourseAi::Personas::Tools::Tool.send_http_request(
url,
method: method,
headers: headers,

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class CreateArtifact < Tool
def self.name

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Custom < Tool
def self.class_instance(tool_id)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class DallE < Tool
def self.signature

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class DbSchema < Tool
def self.signature

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class DiscourseMetaSearch < Tool
class << self

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class GithubFileContent < Tool
def self.signature

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class GithubPullRequestDiff < Tool
LARGE_OBJECT_THRESHOLD = 30_000

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class GithubSearchCode < Tool
def self.signature

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class GithubSearchFiles < Tool
def self.signature

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Google < Tool
def self.signature

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Image < Tool
def self.signature

View File

@ -4,7 +4,7 @@ require "mini_racer"
require "json"
module DiscourseAi
module AiBot
module Personas
module Tools
class JavascriptEvaluator < Tool
TIMEOUT = 500

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class ListCategories < Tool
def self.signature

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class ListTags < Tool
def self.signature

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Option
attr_reader :tool, :name, :type, :values, :default

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class RandomPicker < Tool
def self.signature

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
MAX_POSTS = 100
module Tools

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class ReadArtifact < Tool
MAX_HTML_SIZE = 30.kilobytes

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Search < Tool
attr_reader :last_query

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class SearchSettings < Tool
INCLUDE_DESCRIPTIONS_MAX_LENGTH = 10

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class SettingContext < Tool
MAX_CONTEXT_TOKENS = 2000

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Summarize < Tool
def self.signature

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Time < Tool
def self.signature

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Tool
# Why 30 mega bytes?
@ -63,9 +63,9 @@ module DiscourseAi
@persona_options = persona_options
@bot_user = bot_user
@llm = llm
@context = context.nil? ? DiscourseAi::AiBot::BotContext.new(messages: []) : context
if !@context.is_a?(DiscourseAi::AiBot::BotContext)
raise ArgumentError, "context must be a DiscourseAi::AiBot::Context"
@context = context.nil? ? DiscourseAi::Personas::BotContext.new(messages: []) : context
if !@context.is_a?(DiscourseAi::Personas::BotContext)
raise ArgumentError, "context must be a DiscourseAi::Personas::Context"
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class UpdateArtifact < Tool
def self.name

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class WebBrowser < Tool
def self.signature

View File

@ -1,7 +1,6 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class WebArtifactCreator < Persona
def tools
@ -52,5 +51,4 @@ module DiscourseAi
end
end
end
end
end

View File

@ -83,6 +83,8 @@ after_initialize do
add_admin_route("discourse_ai.title", "discourse-ai", { use_new_show_route: true })
register_seedfu_fixtures(Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "personas"))
[
DiscourseAi::Embeddings::EntryPoint.new,
DiscourseAi::Sentiment::EntryPoint.new,

View File

@ -13,7 +13,7 @@ RSpec.describe DiscourseAi::Discord::Bot::PersonaReplier do
before do
SiteSetting.ai_discord_search_persona = persona.id.to_s
allow_any_instance_of(DiscourseAi::AiBot::Bot).to receive(:reply).and_return(
allow_any_instance_of(DiscourseAi::Personas::Bot).to receive(:reply).and_return(
"This is a reply from bot!",
)
allow(persona_replier).to receive(:create_reply)

View File

@ -20,7 +20,7 @@ RSpec.describe DiscourseAi::Discord::Bot::Search do
describe "#handle_interaction!" do
it "creates a reply with search results" do
allow_any_instance_of(DiscourseAi::AiBot::Tools::Search).to receive(:invoke).and_return(
allow_any_instance_of(DiscourseAi::Personas::Tools::Search).to receive(:invoke).and_return(
{ rows: [%w[Title /link]] },
)
search.handle_interaction!

View File

@ -22,14 +22,10 @@ RSpec.describe DiscourseAi::AiBot::Playground do
fab!(:bot) do
persona =
AiPersona
.find(
DiscourseAi::AiBot::Personas::Persona.system_personas[
DiscourseAi::AiBot::Personas::General
],
)
.find(DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General])
.class_instance
.new
DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
end
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
@ -103,7 +99,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
)
end
let(:bot) { DiscourseAi::AiBot::Bot.as(bot_user, persona: ai_persona.class_instance.new) }
let(:bot) { DiscourseAi::Personas::Bot.as(bot_user, persona: ai_persona.class_instance.new) }
let(:playground) { DiscourseAi::AiBot::Playground.new(bot) }
@ -173,8 +169,8 @@ RSpec.describe DiscourseAi::AiBot::Playground do
it "uses custom tool in conversation" do
persona_klass = AiPersona.all_personas.find { |p| p.name == ai_persona.name }
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona_klass.new)
playground = DiscourseAi::AiBot::Playground.new(bot)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona_klass.new)
playground = described_class.new(bot)
responses = [tool_call, "custom tool did stuff (maybe)"]
@ -213,7 +209,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
custom_tool.update!(enabled: false)
# so we pick up new cache
persona_klass = AiPersona.all_personas.find { |p| p.name == ai_persona.name }
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona_klass.new)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona_klass.new)
playground = DiscourseAi::AiBot::Playground.new(bot)
responses = ["custom tool did stuff (maybe)", tool_call]
@ -968,7 +964,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
it "supports disabling tool details" do
persona = Fabricate(:ai_persona, tool_details: false, tools: ["Search"])
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.class_instance.new)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.class_instance.new)
playground = described_class.new(bot)
response1 =
@ -1021,13 +1017,11 @@ RSpec.describe DiscourseAi::AiBot::Playground do
let(:persona) do
AiPersona.find(
DiscourseAi::AiBot::Personas::Persona.system_personas[
DiscourseAi::AiBot::Personas::DallE3
],
DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::DallE3],
)
end
let(:bot) { DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.class_instance.new) }
let(:bot) { DiscourseAi::Personas::Bot.as(bot_user, persona: persona.class_instance.new) }
let(:data) do
image =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::ArtifactUpdateStrategies::Diff do
RSpec.describe DiscourseAi::Personas::ArtifactUpdateStrategies::Diff do
fab!(:user)
fab!(:post)
fab!(:artifact) { Fabricate(:ai_artifact) }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Bot do
RSpec.describe DiscourseAi::Personas::Bot do
subject(:bot) { described_class.as(bot_user) }
fab!(:admin)
@ -48,11 +48,11 @@ RSpec.describe DiscourseAi::AiBot::Bot do
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
)
personaClass = DiscourseAi::AiBot::Personas::Persona.find_by(user: admin, name: "TestPersona")
personaClass = DiscourseAi::Personas::Persona.find_by(user: admin, name: "TestPersona")
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: personaClass.new)
bot = described_class.as(bot_user, persona: personaClass.new)
bot.reply(
DiscourseAi::AiBot::BotContext.new(messages: [{ type: :user, content: "test" }]),
DiscourseAi::Personas::BotContext.new(messages: [{ type: :user, content: "test" }]),
) do |_partial, _cancel, _placeholder|
# we just need the block so bot has something to call with results
end
@ -64,7 +64,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
context "when using function chaining" do
it "yields a loading placeholder while proceeds to invoke the command" do
tool = DiscourseAi::AiBot::Tools::ListCategories.new({}, bot_user: nil, llm: nil)
tool = DiscourseAi::Personas::Tools::ListCategories.new({}, bot_user: nil, llm: nil)
partial_placeholder = +(<<~HTML)
<details>
<summary>#{tool.summary}</summary>
@ -75,7 +75,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
HTML
context =
DiscourseAi::AiBot::BotContext.new(
DiscourseAi::Personas::BotContext.new(
messages: [{ type: :user, content: "Does my site has tags?" }],
)

View File

@ -1,11 +1,11 @@
#frozen_string_literal: true
class TestPersona < DiscourseAi::AiBot::Personas::Persona
class TestPersona < DiscourseAi::Personas::Persona
def tools
[
DiscourseAi::AiBot::Tools::ListTags,
DiscourseAi::AiBot::Tools::Search,
DiscourseAi::AiBot::Tools::Image,
DiscourseAi::Personas::Tools::ListTags,
DiscourseAi::Personas::Tools::Search,
DiscourseAi::Personas::Tools::Image,
]
end
def system_prompt
@ -19,7 +19,7 @@ class TestPersona < DiscourseAi::AiBot::Personas::Persona
end
end
RSpec.describe DiscourseAi::AiBot::Personas::Persona do
RSpec.describe DiscourseAi::Personas::Persona do
let :persona do
TestPersona.new
end
@ -36,7 +36,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
end
let(:context) do
DiscourseAi::AiBot::BotContext.new(
DiscourseAi::Personas::BotContext.new(
site_url: Discourse.base_url,
site_title: "test site title",
site_description: "test site description",
@ -84,12 +84,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
)
tool_instance =
DiscourseAi::AiBot::Personas::Artist.new.find_tool(
tool_call,
bot_user: nil,
llm: nil,
context: nil,
)
DiscourseAi::Personas::Artist.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
expect(tool_instance.parameters[:prompts]).to eq(["cat oil painting", "big car"])
expect(tool_instance.parameters[:aspect_ratio]).to eq("16:9")
@ -108,12 +103,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
)
tool_instance =
DiscourseAi::AiBot::Personas::General.new.find_tool(
tool_call,
bot_user: nil,
llm: nil,
context: nil,
)
DiscourseAi::Personas::General.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
expect(tool_instance.parameters.key?(:status)).to eq(false)
@ -129,12 +119,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
)
tool_instance =
DiscourseAi::AiBot::Personas::General.new.find_tool(
tool_call,
bot_user: nil,
llm: nil,
context: nil,
)
DiscourseAi::Personas::General.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
expect(tool_instance.parameters[:status]).to eq("open")
end
@ -152,12 +137,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
)
search =
DiscourseAi::AiBot::Personas::General.new.find_tool(
tool_call,
bot_user: nil,
llm: nil,
context: nil,
)
DiscourseAi::Personas::General.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
expect(search.parameters[:max_posts]).to eq(3)
expect(search.parameters[:search_query]).to eq("hello world")
@ -177,12 +157,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
)
tool_instance =
DiscourseAi::AiBot::Personas::DallE3.new.find_tool(
tool_call,
bot_user: nil,
llm: nil,
context: nil,
)
DiscourseAi::Personas::DallE3.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
expect(tool_instance.parameters[:prompts]).to eq(["cat oil painting", "big car"])
end
@ -200,29 +175,29 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
)
custom_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
custom_persona = DiscourseAi::Personas::Persona.all(user: user).last
expect(custom_persona.name).to eq("zzzpun_bot")
expect(custom_persona.description).to eq("you write puns")
instance = custom_persona.new
expect(instance.tools).to eq([DiscourseAi::AiBot::Tools::Image])
expect(instance.tools).to eq([DiscourseAi::Personas::Tools::Image])
expect(instance.craft_prompt(context).messages.first[:content]).to eq("you are pun bot")
# should update
persona.update!(name: "zzzpun_bot2")
custom_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
custom_persona = DiscourseAi::Personas::Persona.all(user: user).last
expect(custom_persona.name).to eq("zzzpun_bot2")
# can be disabled
persona.update!(enabled: false)
last_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
last_persona = DiscourseAi::Personas::Persona.all(user: user).last
expect(last_persona.name).not_to eq("zzzpun_bot2")
persona.update!(enabled: true)
# no groups have access
persona.update!(allowed_group_ids: [])
last_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
last_persona = DiscourseAi::Personas::Persona.all(user: user).last
expect(last_persona.name).not_to eq("zzzpun_bot2")
end
end
@ -237,31 +212,31 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
SiteSetting.ai_google_custom_search_cx = "abc123"
# should be ordered by priority and then alpha
expect(DiscourseAi::AiBot::Personas::Persona.all(user: user)).to eq(
expect(DiscourseAi::Personas::Persona.all(user: user)).to eq(
[
DiscourseAi::AiBot::Personas::General,
DiscourseAi::AiBot::Personas::Artist,
DiscourseAi::AiBot::Personas::Creative,
DiscourseAi::AiBot::Personas::DiscourseHelper,
DiscourseAi::AiBot::Personas::GithubHelper,
DiscourseAi::AiBot::Personas::Researcher,
DiscourseAi::AiBot::Personas::SettingsExplorer,
DiscourseAi::AiBot::Personas::SqlHelper,
DiscourseAi::Personas::General,
DiscourseAi::Personas::Artist,
DiscourseAi::Personas::Creative,
DiscourseAi::Personas::DiscourseHelper,
DiscourseAi::Personas::GithubHelper,
DiscourseAi::Personas::Researcher,
DiscourseAi::Personas::SettingsExplorer,
DiscourseAi::Personas::SqlHelper,
],
)
# it should allow staff access to WebArtifactCreator
expect(DiscourseAi::AiBot::Personas::Persona.all(user: admin)).to eq(
expect(DiscourseAi::Personas::Persona.all(user: admin)).to eq(
[
DiscourseAi::AiBot::Personas::General,
DiscourseAi::AiBot::Personas::Artist,
DiscourseAi::AiBot::Personas::Creative,
DiscourseAi::AiBot::Personas::DiscourseHelper,
DiscourseAi::AiBot::Personas::GithubHelper,
DiscourseAi::AiBot::Personas::Researcher,
DiscourseAi::AiBot::Personas::SettingsExplorer,
DiscourseAi::AiBot::Personas::SqlHelper,
DiscourseAi::AiBot::Personas::WebArtifactCreator,
DiscourseAi::Personas::General,
DiscourseAi::Personas::Artist,
DiscourseAi::Personas::Creative,
DiscourseAi::Personas::DiscourseHelper,
DiscourseAi::Personas::GithubHelper,
DiscourseAi::Personas::Researcher,
DiscourseAi::Personas::SettingsExplorer,
DiscourseAi::Personas::SqlHelper,
DiscourseAi::Personas::WebArtifactCreator,
],
)
@ -270,27 +245,25 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
SiteSetting.ai_google_custom_search_api_key = ""
SiteSetting.ai_artifact_security = "disabled"
expect(DiscourseAi::AiBot::Personas::Persona.all(user: admin)).to contain_exactly(
DiscourseAi::AiBot::Personas::General,
DiscourseAi::AiBot::Personas::SqlHelper,
DiscourseAi::AiBot::Personas::SettingsExplorer,
DiscourseAi::AiBot::Personas::Creative,
DiscourseAi::AiBot::Personas::DiscourseHelper,
DiscourseAi::AiBot::Personas::GithubHelper,
expect(DiscourseAi::Personas::Persona.all(user: admin)).to contain_exactly(
DiscourseAi::Personas::General,
DiscourseAi::Personas::SqlHelper,
DiscourseAi::Personas::SettingsExplorer,
DiscourseAi::Personas::Creative,
DiscourseAi::Personas::DiscourseHelper,
DiscourseAi::Personas::GithubHelper,
)
AiPersona.find(
DiscourseAi::AiBot::Personas::Persona.system_personas[
DiscourseAi::AiBot::Personas::General
],
DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General],
).update!(enabled: false)
expect(DiscourseAi::AiBot::Personas::Persona.all(user: user)).to contain_exactly(
DiscourseAi::AiBot::Personas::SqlHelper,
DiscourseAi::AiBot::Personas::SettingsExplorer,
DiscourseAi::AiBot::Personas::Creative,
DiscourseAi::AiBot::Personas::DiscourseHelper,
DiscourseAi::AiBot::Personas::GithubHelper,
expect(DiscourseAi::Personas::Persona.all(user: user)).to contain_exactly(
DiscourseAi::Personas::SqlHelper,
DiscourseAi::Personas::SettingsExplorer,
DiscourseAi::Personas::Creative,
DiscourseAi::Personas::DiscourseHelper,
DiscourseAi::Personas::GithubHelper,
)
end
end
@ -304,7 +277,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
SiteSetting.ai_embeddings_enabled = true
end
let(:ai_persona) { DiscourseAi::AiBot::Personas::Persona.all(user: user).first.new }
let(:ai_persona) { DiscourseAi::Personas::Persona.all(user: user).first.new }
let(:with_cc) do
context.messages = [{ content: "Tell me the time", type: :user }]
@ -343,7 +316,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
UploadReference.ensure_exist!(target: custom_ai_persona, upload_ids: [upload.id])
custom_persona =
DiscourseAi::AiBot::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
DiscourseAi::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
# this means that we will consolidate
context.messages = [
@ -415,7 +388,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
UploadReference.ensure_exist!(target: custom_ai_persona, upload_ids: [upload.id])
custom_persona =
DiscourseAi::AiBot::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
DiscourseAi::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
expect(custom_persona.class.rag_conversation_chunks).to eq(3)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::QuestionConsolidator do
RSpec.describe DiscourseAi::Personas::QuestionConsolidator do
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{Fabricate(:fake_model).id}") }
let(:fake_endpoint) { DiscourseAi::Completions::Endpoints::Fake }

View File

@ -1,13 +1,13 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Personas::Researcher do
RSpec.describe DiscourseAi::Personas::Researcher do
let :researcher do
subject
end
it "renders schema" do
expect(researcher.tools).to eq(
[DiscourseAi::AiBot::Tools::Google, DiscourseAi::AiBot::Tools::WebBrowser],
[DiscourseAi::Personas::Tools::Google, DiscourseAi::Personas::Tools::WebBrowser],
)
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Personas::SettingsExplorer do
RSpec.describe DiscourseAi::Personas::SettingsExplorer do
let :settings_explorer do
subject
end
@ -14,7 +14,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::SettingsExplorer do
expect(prompt).to include("site_description")
expect(settings_explorer.tools).to eq(
[DiscourseAi::AiBot::Tools::SettingContext, DiscourseAi::AiBot::Tools::SearchSettings],
[DiscourseAi::Personas::Tools::SettingContext, DiscourseAi::Personas::Tools::SearchSettings],
)
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Personas::SqlHelper do
RSpec.describe DiscourseAi::Personas::SqlHelper do
let :sql_helper do
subject
end
@ -12,6 +12,6 @@ RSpec.describe DiscourseAi::AiBot::Personas::SqlHelper do
expect(prompt).not_to include("translation_key") # not a priority table
expect(prompt).to include("user_api_keys") # not a priority table
expect(sql_helper.tools).to eq([DiscourseAi::AiBot::Tools::DbSchema])
expect(sql_helper.tools).to eq([DiscourseAi::Personas::Tools::DbSchema])
end
end

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::CreateArtifact do
RSpec.describe DiscourseAi::Personas::Tools::CreateArtifact do
fab!(:llm_model)
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
fab!(:post)
@ -34,7 +34,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::CreateArtifact do
{ html_body: "hello" },
bot_user: Fabricate(:user),
llm: llm,
context: DiscourseAi::AiBot::BotContext.new(post: post),
context: DiscourseAi::Personas::BotContext.new(post: post),
)
tool.parameters = { name: "hello", specification: "hello spec" }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::DallE do
RSpec.describe DiscourseAi::Personas::Tools::DallE do
let(:prompts) { ["a pink cow", "a red cow"] }
fab!(:gpt_35_turbo) { Fabricate(:llm_model, name: "gpt-3.5-turbo") }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::DbSchema do
RSpec.describe DiscourseAi::Personas::Tools::DbSchema do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::DiscourseMetaSearch do
RSpec.describe DiscourseAi::Personas::Tools::DiscourseMetaSearch do
before { SiteSetting.ai_bot_enabled = true }
fab!(:llm_model) { Fabricate(:llm_model, max_prompt_tokens: 8192) }

View File

@ -2,7 +2,7 @@
require "rails_helper"
RSpec.describe DiscourseAi::AiBot::Tools::GithubFileContent do
RSpec.describe DiscourseAi::Personas::Tools::GithubFileContent do
fab!(:llm_model)
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -2,7 +2,7 @@
require "rails_helper"
RSpec.describe DiscourseAi::AiBot::Tools::GithubPullRequestDiff do
RSpec.describe DiscourseAi::Personas::Tools::GithubPullRequestDiff do
let(:bot_user) { Fabricate(:user) }
fab!(:llm_model)
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -2,7 +2,7 @@
require "rails_helper"
RSpec.describe DiscourseAi::AiBot::Tools::GithubSearchCode do
RSpec.describe DiscourseAi::Personas::Tools::GithubSearchCode do
let(:bot_user) { Fabricate(:user) }
fab!(:llm_model)
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -2,7 +2,7 @@
require "rails_helper"
RSpec.describe DiscourseAi::AiBot::Tools::GithubSearchFiles do
RSpec.describe DiscourseAi::Personas::Tools::GithubSearchFiles do
fab!(:llm_model)
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::Google do
RSpec.describe DiscourseAi::Personas::Tools::Google do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::Image do
RSpec.describe DiscourseAi::Personas::Tools::Image do
let(:progress_blk) { Proc.new {} }
let(:prompts) { ["a pink cow", "a red cow"] }
@ -9,7 +9,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Image do
{ prompts: prompts, seeds: [99, 32] },
bot_user: bot_user,
llm: llm,
context: DiscourseAi::AiBot::BotContext.new,
context: DiscourseAi::Personas::BotContext.new,
)
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::JavascriptEvaluator do
RSpec.describe DiscourseAi::Personas::Tools::JavascriptEvaluator do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::ListCategories do
RSpec.describe DiscourseAi::Personas::Tools::ListCategories do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::ListTags do
RSpec.describe DiscourseAi::Personas::Tools::ListTags do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -2,7 +2,7 @@
require "rails_helper"
RSpec.describe DiscourseAi::AiBot::Tools::RandomPicker do
RSpec.describe DiscourseAi::Personas::Tools::RandomPicker do
describe "#invoke" do
subject { described_class.new({ options: options }, bot_user: nil, llm: nil).invoke }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
RSpec.describe DiscourseAi::Personas::Tools::ReadArtifact do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
fab!(:post)
@ -25,7 +25,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
{ url: "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/#{artifact.id}" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(post: post),
context: DiscourseAi::Personas::BotContext.new(post: post),
)
result = tool.invoke {}
@ -44,7 +44,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
{ url: "invalid-url" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(post: post),
context: DiscourseAi::Personas::BotContext.new(post: post),
)
result = tool.invoke {}
@ -58,7 +58,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
{ url: "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/99999" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(post: post),
context: DiscourseAi::Personas::BotContext.new(post: post),
)
result = tool.invoke {}
@ -91,7 +91,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
{ url: "https://example.com" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(post: post),
context: DiscourseAi::Personas::BotContext.new(post: post),
)
result = tool.invoke {}
@ -120,7 +120,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
{ url: "https://example.com" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(post: post),
context: DiscourseAi::Personas::BotContext.new(post: post),
)
result = tool.invoke {}

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::Read do
RSpec.describe DiscourseAi::Personas::Tools::Read do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
@ -56,7 +56,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Read do
persona_options: {
"read_private" => true,
},
context: DiscourseAi::AiBot::BotContext.new(user: admin),
context: DiscourseAi::Personas::BotContext.new(user: admin),
)
results = tool.invoke
expect(results[:content]).to include("hello there")
@ -66,7 +66,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Read do
{ topic_id: topic_with_tags.id, post_numbers: [post1.post_number] },
bot_user: bot_user,
llm: llm,
context: DiscourseAi::AiBot::BotContext.new(user: admin),
context: DiscourseAi::Personas::BotContext.new(user: admin),
)
results = tool.invoke

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::SearchSettings do
RSpec.describe DiscourseAi::Personas::Tools::SearchSettings do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::Search do
RSpec.describe DiscourseAi::Personas::Tools::Search do
before { SearchIndexer.enable }
after { SearchIndexer.disable }
@ -60,7 +60,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Search do
persona_options: persona_options,
bot_user: bot_user,
llm: llm,
context: DiscourseAi::AiBot::BotContext.new(user: user),
context: DiscourseAi::Personas::BotContext.new(user: user),
)
expect(search.options[:base_query]).to eq("#funny")

View File

@ -8,7 +8,7 @@ def has_rg?
end
end
RSpec.describe DiscourseAi::AiBot::Tools::SettingContext, if: has_rg? do
RSpec.describe DiscourseAi::Personas::Tools::SettingContext, if: has_rg? do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::Summarize do
RSpec.describe DiscourseAi::Personas::Tools::Summarize do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::Time do
RSpec.describe DiscourseAi::Personas::Tools::Time do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::Tool do
RSpec.describe DiscourseAi::Personas::Tools::Tool do
let :tool_class do
described_class
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
RSpec.describe DiscourseAi::Personas::Tools::UpdateArtifact do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
fab!(:post)
@ -47,7 +47,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
persona_options: {
"update_algorithm" => "full",
},
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
)
result = tool.invoke {}
@ -91,7 +91,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
persona_options: {
"update_algorithm" => "full",
},
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
)
result = tool.invoke {}
@ -115,7 +115,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
{ artifact_id: artifact.id, instructions: "Invalid update" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
)
result = tool.invoke {}
@ -129,7 +129,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
{ artifact_id: -1, instructions: "Update something" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
)
result = tool.invoke {}
@ -155,7 +155,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
persona_options: {
"update_algorithm" => "full",
},
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
)
tool.invoke {}
@ -186,7 +186,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
persona_options: {
"update_algorithm" => "full",
},
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
)
.invoke {}
end
@ -212,7 +212,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
persona_options: {
"update_algorithm" => "full",
},
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
)
result = tool.invoke {}
@ -262,7 +262,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
{ artifact_id: artifact.id, instructions: "Change the text to Updated and color to red" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
persona_options: {
"update_algorithm" => "diff",
},
@ -330,7 +330,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
{ artifact_id: artifact.id, instructions: "Change the text to Updated and color to red" },
bot_user: bot_user,
llm: llm_model.to_llm,
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
persona_options: {
"update_algorithm" => "diff",
},

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::WebBrowser do
RSpec.describe DiscourseAi::Personas::Tools::WebBrowser do
fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }

View File

@ -121,7 +121,7 @@ RSpec.describe AiTool do
},
)
expect { runner.invoke }.to raise_error(DiscourseAi::AiBot::ToolRunner::TooManyRequestsError)
expect { runner.invoke }.to raise_error(DiscourseAi::Personas::ToolRunner::TooManyRequestsError)
end
it "can perform GET HTTP requests" do

View File

@ -19,7 +19,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
expect(response.parsed_body["ai_personas"].length).to eq(AiPersona.count)
expect(response.parsed_body["meta"]["tools"].length).to eq(
DiscourseAi::AiBot::Personas::Persona.all_available_tools.length,
DiscourseAi::Personas::Persona.all_available_tools.length,
)
end
@ -136,10 +136,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
it "returns localized persona names and descriptions" do
get "/admin/plugins/discourse-ai/ai-personas.json"
id =
DiscourseAi::AiBot::Personas::Persona.system_personas[
DiscourseAi::AiBot::Personas::General
]
id = DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General]
persona = response.parsed_body["ai_personas"].find { |p| p["id"] == id }
expect(persona["name"]).to eq("Général")
@ -301,7 +298,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end
it "does not allow temperature and top p changes on stock personas" do
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
params: {
ai_persona: {
top_p: 0.5,
@ -335,7 +332,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
context "with system personas" do
it "does not allow editing of system prompts" do
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
params: {
ai_persona: {
system_prompt: "you are not a helpful bot",
@ -348,7 +345,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end
it "does not allow editing of tools" do
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
params: {
ai_persona: {
tools: %w[SearchCommand ImageCommand],
@ -361,7 +358,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end
it "does not allow editing of name and description cause it is localized" do
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
params: {
ai_persona: {
name: "bob",
@ -375,7 +372,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end
it "does allow some actions" do
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
params: {
ai_persona: {
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_1]],
@ -413,7 +410,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
it "is not allowed to delete system personas" do
expect {
delete "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json"
delete "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json"
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body["errors"].join).not_to be_blank
# let's make sure this is translated

View File

@ -55,7 +55,7 @@ RSpec.describe "Admin AI persona configuration", type: :system, js: true do
end
it "will not allow deletion or editing of system personas" do
visit "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}/edit"
visit "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}/edit"
expect(page).not_to have_selector(".ai-persona-editor__delete")
expect(form.field("system_prompt")).to be_disabled
end

View File

@ -16,7 +16,7 @@ RSpec.describe "AI personas", type: :system, js: true do
persona_selector =
PageObjects::Components::SelectKit.new(".persona-llm-selector__persona-dropdown")
id = DiscourseAi::AiBot::Personas::Persona.all(user: admin).first.id
id = DiscourseAi::Personas::Persona.all(user: admin).first.id
expect(persona_selector).to have_selected_value(id)