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) LocalizedAiPersonaSerializer.new(persona, root: false)
end end
tools = 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) AiToolSerializer.new(tool, root: false)
end end
AiTool AiTool

View File

@ -12,11 +12,11 @@ module ::Jobs
return if message.blank? return if message.blank?
personaClass = 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? return if personaClass.blank?
user = User.find_by(id: personaClass.user_id) 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( DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
message, message,

View File

@ -10,13 +10,13 @@ module ::Jobs
persona_id = args[:persona_id] persona_id = args[:persona_id]
begin begin
persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id) persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil? 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) 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( Rails.logger.warn(
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled", "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? return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
bot = bot =
DiscourseAi::AiBot::Bot.as( DiscourseAi::Personas::Bot.as(
Discourse.system_user, Discourse.system_user,
persona: ai_persona_klass.new, persona: ai_persona_klass.new,
model: llm_model, model: llm_model,
@ -31,7 +31,7 @@ module Jobs
base = { query: query, model_used: llm_model.display_name } base = { query: query, model_used: llm_model.display_name }
context = context =
DiscourseAi::AiBot::BotContext.new( DiscourseAi::Personas::BotContext.new(
messages: [{ type: :user, content: query }], messages: [{ type: :user, content: query }],
skip_tool_details: true, skip_tool_details: true,
) )

View File

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

View File

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

View File

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

View File

@ -110,7 +110,7 @@ module DiscourseAi
scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map) scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map)
end, end,
) do ) do
DiscourseAi::AiBot::Personas::Persona DiscourseAi::Personas::Persona
.all(user: scope.user) .all(user: scope.user)
.map do |persona| .map do |persona|
{ {
@ -205,8 +205,7 @@ module DiscourseAi
include_condition: -> { SiteSetting.ai_bot_enabled && object.topic.private_message? }, include_condition: -> { SiteSetting.ai_bot_enabled && object.topic.private_message? },
) do ) do
id = topic.custom_fields["ai_persona_id"] id = topic.custom_fields["ai_persona_id"]
name = name = DiscourseAi::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
DiscourseAi::AiBot::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
name || topic.custom_fields["ai_persona"] name || topic.custom_fields["ai_persona"]
end 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 persona = nil
if persona_id if persona_id
persona = persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
end end
if !persona && persona_name = post.topic.custom_fields["ai_persona"] if !persona && persona_name = post.topic.custom_fields["ai_persona"]
persona = persona = DiscourseAi::Personas::Persona.find_by(user: post.user, name: persona_name)
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, name: persona_name)
end end
# edge case, llm was mentioned in an ai persona conversation # edge case, llm was mentioned in an ai persona conversation
@ -172,11 +170,11 @@ module DiscourseAi
end end
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_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) new(bot).update_playground_with(post)
end end
end end
@ -198,8 +196,8 @@ module DiscourseAi
bot_user = user || ai_persona.user bot_user = user || ai_persona.user
raise Discourse::InvalidParameters.new(:user) if bot_user.nil? raise Discourse::InvalidParameters.new(:user) if bot_user.nil?
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona) bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
playground = DiscourseAi::AiBot::Playground.new(bot) playground = new(bot)
playground.reply_to( playground.reply_to(
post, post,
@ -236,14 +234,54 @@ module DiscourseAi
include_uploads: bot.persona.class.vision_enabled, include_uploads: bot.persona.class.vision_enabled,
) )
bot # conversation context may contain tool calls, and confusing user names
.get_updated_title(messages, post, user) # clean it up
.tap do |new_title| conversation = +""
PostRevisor.new(post.topic.first_post, post.topic).revise!( messages.each do |context|
bot.bot_user, if context[:type] == :user
title: new_title.sub(/\A"/, "").sub(/"\Z/, ""), conversation << "User said:\n#{context[:content]}\n\n"
) elsif context[:type] == :model
conversation << "Model said:\n#{context[:content]}\n\n"
end 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
.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/, ""),
)
allowed_users = post.topic.topic_allowed_users.pluck(:user_id) allowed_users = post.topic.topic_allowed_users.pluck(:user_id)
MessageBus.publish( MessageBus.publish(
@ -271,7 +309,7 @@ module DiscourseAi
end end
context = context =
BotContext.new( DiscourseAi::Personas::BotContext.new(
participants: participants, participants: participants,
message_id: message.id, message_id: message.id,
channel_id: channel.id, channel_id: channel.id,
@ -372,7 +410,7 @@ module DiscourseAi
end end
context = context =
BotContext.new( DiscourseAi::Personas::BotContext.new(
post: post, post: post,
custom_instructions: custom_instructions, custom_instructions: custom_instructions,
messages: messages:
@ -575,8 +613,7 @@ module DiscourseAi
def schedule_bot_reply(post) def schedule_bot_reply(post)
persona_id = persona_id =
DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] || DiscourseAi::Personas::Persona.system_personas[bot.persona.class] || bot.persona.class.id
bot.persona.class.id
::Jobs.enqueue( ::Jobs.enqueue(
:create_ai_reply, :create_ai_reply,
post_id: post.id, post_id: post.id,

View File

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

View File

@ -7,7 +7,7 @@ module DiscourseAi
return if !tool return if !tool
return if !tool.parameters.blank? 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 = tool.runner({}, llm: nil, bot_user: Discourse.system_user, context: context)
runner.invoke runner.invoke

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,18 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class Artist < Persona
class Artist < Persona def tools
def tools [Tools::Image]
[Tools::Image] end
end
def required_tools def required_tools
[Tools::Image] [Tools::Image]
end end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
You are artistbot and you are here to help people generate images. You are artistbot and you are here to help people generate images.
You generate images using stable diffusion. You generate images using stable diffusion.
@ -31,7 +30,6 @@ module DiscourseAi
- Be creative with your prompts, offer diverse options - Be creative with your prompts, offer diverse options
- You can use the seeds to regenerate the same image and amend the prompt keeping general style - You can use the seeds to regenerate the same image and amend the prompt keeping general style
PROMPT PROMPT
end
end end
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
class Bot class Bot
attr_reader :model attr_reader :model
@ -13,7 +13,7 @@ module DiscourseAi
# limit is arbitrary, but 5 which was used in the past was too low # limit is arbitrary, but 5 which was used in the past was too low
MAX_TOOLS = 20 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) new(bot_user, persona, model)
end end
@ -27,49 +27,8 @@ module DiscourseAi
attr_reader :bot_user attr_reader :bot_user
attr_accessor :persona attr_accessor :persona
def get_updated_title(conversation_context, post, user) def llm
system_insts = <<~TEXT.strip @llm ||= DiscourseAi::Completions::Llm.proxy(model)
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
end end
def force_tool_if_needed(prompt, context) def force_tool_if_needed(prompt, context)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
class BotContext class BotContext
attr_accessor :messages, attr_accessor :messages,
:topic_id, :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,19 +1,18 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class DallE3 < Persona
class DallE3 < Persona def tools
def tools [Tools::DallE]
[Tools::DallE] end
end
def required_tools def required_tools
[Tools::DallE] [Tools::DallE]
end end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
As a DALL-E-3 bot, you're tasked with generating images based on user prompts. As a DALL-E-3 bot, you're tasked with generating images based on user prompts.
- Be specific and detailed in your prompts. Include elements like subject, medium (e.g., oil on canvas), artist style, lighting, time of day, and website style (e.g., ArtStation, DeviantArt). - Be specific and detailed in your prompts. Include elements like subject, medium (e.g., oil on canvas), artist style, lighting, time of day, and website style (e.g., ArtStation, DeviantArt).
@ -32,7 +31,6 @@ module DiscourseAi
Just generate the images Just generate the images
PROMPT PROMPT
end
end end
end end
end end

View File

@ -1,15 +1,14 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class DiscourseHelper < Persona
class DiscourseHelper < Persona def tools
def tools [Tools::DiscourseMetaSearch]
[Tools::DiscourseMetaSearch] end
end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
You are Discourse Helper Bot You are Discourse Helper Bot
- Discourse Helper Bot understands *markdown* and responds in Discourse **markdown**. - Discourse Helper Bot understands *markdown* and responds in Discourse **markdown**.
@ -41,7 +40,6 @@ module DiscourseAi
The date now is: {time}, much has changed since you were trained. The date now is: {time}, much has changed since you were trained.
PROMPT PROMPT
end
end end
end end
end end

View File

@ -1,22 +1,21 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class General < Persona
class General < Persona def tools
def tools [
[ Tools::Search,
Tools::Search, Tools::Google,
Tools::Google, Tools::Image,
Tools::Image, Tools::Read,
Tools::Read, Tools::ListCategories,
Tools::ListCategories, Tools::ListTags,
Tools::ListTags, ]
] end
end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
You are a helpful Discourse assistant. You are a helpful Discourse assistant.
You _understand_ and **generate** Discourse Markdown. You _understand_ and **generate** Discourse Markdown.
You live in a Discourse Forum Message. You live in a Discourse Forum Message.
@ -27,7 +26,6 @@ module DiscourseAi
The participants in this conversation are: {participants} The participants in this conversation are: {participants}
The date now is: {time}, much has changed since you were trained. The date now is: {time}, much has changed since you were trained.
PROMPT PROMPT
end
end end
end end
end end

View File

@ -1,20 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class GithubHelper < Persona
class GithubHelper < Persona def tools
def tools [
[ Tools::GithubFileContent,
Tools::GithubFileContent, Tools::GithubPullRequestDiff,
Tools::GithubPullRequestDiff, Tools::GithubSearchCode,
Tools::GithubSearchCode, Tools::GithubSearchFiles,
Tools::GithubSearchFiles, ]
] end
end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
You are a helpful GitHub assistant. You are a helpful GitHub assistant.
You _understand_ and **generate** Discourse Flavored Markdown. You _understand_ and **generate** Discourse Flavored Markdown.
You live in a Discourse Forum Message. You live in a Discourse Forum Message.
@ -22,7 +21,6 @@ module DiscourseAi
Your purpose is to assist users with GitHub-related tasks and questions. Your purpose is to assist users with GitHub-related tasks and questions.
When asked about a specific repository, pull request, or file, try to use the available tools to provide accurate and helpful information. When asked about a specific repository, pull request, or file, try to use the available tools to provide accurate and helpful information.
PROMPT PROMPT
end
end 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 # frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
class QuestionConsolidator class QuestionConsolidator
attr_reader :llm, :messages, :user, :max_tokens attr_reader :llm, :messages, :user, :max_tokens

View File

@ -1,19 +1,18 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class Researcher < Persona
class Researcher < Persona def tools
def tools [Tools::Google, Tools::WebBrowser]
[Tools::Google, Tools::WebBrowser] end
end
def required_tools def required_tools
[Tools::Google] [Tools::Google]
end end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
You are a research assistant with access to two powerful tools: You are a research assistant with access to two powerful tools:
1. Google search - for finding relevant information across the internet. 1. Google search - for finding relevant information across the internet.
@ -45,7 +44,6 @@ module DiscourseAi
Remember, efficient use of your tools not only saves time but also ensures the high quality and relevance of the information provided. Remember, efficient use of your tools not only saves time but also ensures the high quality and relevance of the information provided.
PROMPT PROMPT
end
end end
end end
end end

View File

@ -1,15 +1,14 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class SettingsExplorer < Persona
class SettingsExplorer < Persona def tools
def tools [Tools::SettingContext, Tools::SearchSettings]
[Tools::SettingContext, Tools::SearchSettings] end
end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
You are Discourse Site settings bot. You are Discourse Site settings bot.
- You are able to find information about all the site settings. - You are able to find information about all the site settings.
@ -19,7 +18,6 @@ module DiscourseAi
Current time is: {time} Current time is: {time}
PROMPT PROMPT
end
end end
end end
end end

View File

@ -1,52 +1,50 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class SqlHelper < Persona
class SqlHelper < Persona def self.schema
def self.schema return @schema if defined?(@schema)
return @schema if defined?(@schema)
tables = Hash.new tables = Hash.new
priority_tables = %w[ priority_tables = %w[
posts posts
topics topics
notifications notifications
users users
user_actions user_actions
user_emails user_emails
categories categories
groups groups
] ]
DB.query(<<~SQL).each { |row| (tables[row.table_name] ||= []) << row.column_name } DB.query(<<~SQL).each { |row| (tables[row.table_name] ||= []) << row.column_name }
select table_name, column_name from information_schema.columns select table_name, column_name from information_schema.columns
where table_schema = 'public' where table_schema = 'public'
order by table_name order by table_name
SQL SQL
priority = priority = +(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
+(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
other_tables = +"" other_tables = +""
tables.each do |table_name, _| tables.each do |table_name, _|
next if priority_tables.include?(table_name) next if priority_tables.include?(table_name)
other_tables << "#{table_name} " other_tables << "#{table_name} "
end
@schema = { priority_tables: priority, other_tables: other_tables }
end end
def tools @schema = { priority_tables: priority, other_tables: other_tables }
[Tools::DbSchema] end
end
def temperature def tools
0.2 [Tools::DbSchema]
end end
def system_prompt def temperature
<<~PROMPT 0.2
end
def system_prompt
<<~PROMPT
You are a PostgreSQL expert. You are a PostgreSQL expert.
- Avoid returning any text to the user prior to a tool call. - Avoid returning any text to the user prior to a tool call.
- You understand and generate Discourse Markdown but specialize in creating queries. - You understand and generate Discourse Markdown but specialize in creating queries.
@ -100,7 +98,6 @@ module DiscourseAi
NEVER look up schema for the tables listed above, as their full schema is already provided. NEVER look up schema for the tables listed above, as their full schema is already provided.
PROMPT PROMPT
end
end end
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,18 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module AiBot module Personas
module Personas class WebArtifactCreator < Persona
class WebArtifactCreator < Persona def tools
def tools [Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact]
[Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact] end
end
def required_tools def required_tools
[Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact] [Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact]
end end
def system_prompt def system_prompt
<<~PROMPT <<~PROMPT
You are the Web Creator, an AI assistant specializing in building interactive web components. You create engaging and functional web experiences using HTML, CSS, and JavaScript. You live in a Discourse PM and communicate using Markdown. You are the Web Creator, an AI assistant specializing in building interactive web components. You create engaging and functional web experiences using HTML, CSS, and JavaScript. You live in a Discourse PM and communicate using Markdown.
Core Principles: Core Principles:
@ -49,7 +48,6 @@ module DiscourseAi
Remember: Great components combine structure (HTML), presentation (CSS), and behavior (JavaScript) to create memorable user experiences. Remember: Great components combine structure (HTML), presentation (CSS), and behavior (JavaScript) to create memorable user experiences.
PROMPT PROMPT
end
end 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 }) 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::Embeddings::EntryPoint.new,
DiscourseAi::Sentiment::EntryPoint.new, DiscourseAi::Sentiment::EntryPoint.new,

View File

@ -13,7 +13,7 @@ RSpec.describe DiscourseAi::Discord::Bot::PersonaReplier do
before do before do
SiteSetting.ai_discord_search_persona = persona.id.to_s 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!", "This is a reply from bot!",
) )
allow(persona_replier).to receive(:create_reply) allow(persona_replier).to receive(:create_reply)

View File

@ -20,7 +20,7 @@ RSpec.describe DiscourseAi::Discord::Bot::Search do
describe "#handle_interaction!" do describe "#handle_interaction!" do
it "creates a reply with search results" 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]] }, { rows: [%w[Title /link]] },
) )
search.handle_interaction! search.handle_interaction!

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # 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(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{Fabricate(:fake_model).id}") }
let(:fake_endpoint) { DiscourseAi::Completions::Endpoints::Fake } let(:fake_endpoint) { DiscourseAi::Completions::Endpoints::Fake }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Personas::SqlHelper do RSpec.describe DiscourseAi::Personas::SqlHelper do
let :sql_helper do let :sql_helper do
subject subject
end 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).not_to include("translation_key") # not a priority table
expect(prompt).to include("user_api_keys") # 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
end end

View File

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

View File

@ -1,6 +1,6 @@
#frozen_string_literal: true #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"] } let(:prompts) { ["a pink cow", "a red cow"] }
fab!(:gpt_35_turbo) { Fabricate(:llm_model, name: "gpt-3.5-turbo") } fab!(:gpt_35_turbo) { Fabricate(:llm_model, name: "gpt-3.5-turbo") }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ def has_rg?
end end
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) fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) } let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Tools::WebBrowser do RSpec.describe DiscourseAi::Personas::Tools::WebBrowser do
fab!(:llm_model) fab!(:llm_model)
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) } let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") } 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 end
it "can perform GET HTTP requests" do 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["ai_personas"].length).to eq(AiPersona.count)
expect(response.parsed_body["meta"]["tools"].length).to eq( 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 end
@ -136,10 +136,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
it "returns localized persona names and descriptions" do it "returns localized persona names and descriptions" do
get "/admin/plugins/discourse-ai/ai-personas.json" get "/admin/plugins/discourse-ai/ai-personas.json"
id = id = DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General]
DiscourseAi::AiBot::Personas::Persona.system_personas[
DiscourseAi::AiBot::Personas::General
]
persona = response.parsed_body["ai_personas"].find { |p| p["id"] == id } persona = response.parsed_body["ai_personas"].find { |p| p["id"] == id }
expect(persona["name"]).to eq("Général") expect(persona["name"]).to eq("Général")
@ -301,7 +298,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end end
it "does not allow temperature and top p changes on stock personas" do 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: { params: {
ai_persona: { ai_persona: {
top_p: 0.5, top_p: 0.5,
@ -335,7 +332,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
context "with system personas" do context "with system personas" do
it "does not allow editing of system prompts" 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: { params: {
ai_persona: { ai_persona: {
system_prompt: "you are not a helpful bot", system_prompt: "you are not a helpful bot",
@ -348,7 +345,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end end
it "does not allow editing of tools" do 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: { params: {
ai_persona: { ai_persona: {
tools: %w[SearchCommand ImageCommand], tools: %w[SearchCommand ImageCommand],
@ -361,7 +358,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end end
it "does not allow editing of name and description cause it is localized" do 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: { params: {
ai_persona: { ai_persona: {
name: "bob", name: "bob",
@ -375,7 +372,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
end end
it "does allow some actions" do 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: { params: {
ai_persona: { ai_persona: {
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_1]], 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 it "is not allowed to delete system personas" do
expect { 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).to have_http_status(:unprocessable_entity)
expect(response.parsed_body["errors"].join).not_to be_blank expect(response.parsed_body["errors"].join).not_to be_blank
# let's make sure this is translated # 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 end
it "will not allow deletion or editing of system personas" do 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(page).not_to have_selector(".ai-persona-editor__delete")
expect(form.field("system_prompt")).to be_disabled expect(form.field("system_prompt")).to be_disabled
end end

View File

@ -16,7 +16,7 @@ RSpec.describe "AI personas", type: :system, js: true do
persona_selector = persona_selector =
PageObjects::Components::SelectKit.new(".persona-llm-selector__persona-dropdown") 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) expect(persona_selector).to have_selected_value(id)