mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-24 08:32:14 +00:00
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:
parent
5b6d39a206
commit
30242a27e6
@ -15,7 +15,7 @@ module DiscourseAi
|
||||
LocalizedAiPersonaSerializer.new(persona, root: false)
|
||||
end
|
||||
tools =
|
||||
DiscourseAi::AiBot::Personas::Persona.all_available_tools.map do |tool|
|
||||
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
|
||||
AiToolSerializer.new(tool, root: false)
|
||||
end
|
||||
AiTool
|
||||
|
@ -12,11 +12,11 @@ module ::Jobs
|
||||
return if message.blank?
|
||||
|
||||
personaClass =
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
|
||||
DiscourseAi::Personas::Persona.find_by(id: args[:persona_id], user: message.user)
|
||||
return if personaClass.blank?
|
||||
|
||||
user = User.find_by(id: personaClass.user_id)
|
||||
bot = DiscourseAi::AiBot::Bot.as(user, persona: personaClass.new)
|
||||
bot = DiscourseAi::Personas::Bot.as(user, persona: personaClass.new)
|
||||
|
||||
DiscourseAi::AiBot::Playground.new(bot).reply_to_chat_message(
|
||||
message,
|
||||
|
@ -10,13 +10,13 @@ module ::Jobs
|
||||
persona_id = args[:persona_id]
|
||||
|
||||
begin
|
||||
persona = DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id)
|
||||
raise DiscourseAi::AiBot::Bot::BOT_NOT_FOUND if persona.nil?
|
||||
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
|
||||
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil?
|
||||
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
|
||||
|
||||
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
|
||||
rescue DiscourseAi::AiBot::Bot::BOT_NOT_FOUND
|
||||
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
|
||||
Rails.logger.warn(
|
||||
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
|
||||
)
|
||||
|
@ -19,7 +19,7 @@ module Jobs
|
||||
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
|
||||
|
||||
bot =
|
||||
DiscourseAi::AiBot::Bot.as(
|
||||
DiscourseAi::Personas::Bot.as(
|
||||
Discourse.system_user,
|
||||
persona: ai_persona_klass.new,
|
||||
model: llm_model,
|
||||
@ -31,7 +31,7 @@ module Jobs
|
||||
base = { query: query, model_used: llm_model.display_name }
|
||||
|
||||
context =
|
||||
DiscourseAi::AiBot::BotContext.new(
|
||||
DiscourseAi::Personas::BotContext.new(
|
||||
messages: [{ type: :user, content: query }],
|
||||
skip_tool_details: true,
|
||||
)
|
||||
|
@ -201,14 +201,14 @@ class AiPersona < ActiveRecord::Base
|
||||
if inner_name.start_with?("custom-")
|
||||
custom_tool_id = inner_name.split("-", 2).last.to_i
|
||||
if AiTool.exists?(id: custom_tool_id, enabled: true)
|
||||
klass = DiscourseAi::AiBot::Tools::Custom.class_instance(custom_tool_id)
|
||||
klass = DiscourseAi::Personas::Tools::Custom.class_instance(custom_tool_id)
|
||||
end
|
||||
else
|
||||
inner_name = inner_name.gsub("Tool", "")
|
||||
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
|
||||
|
||||
begin
|
||||
klass = "DiscourseAi::AiBot::Tools::#{inner_name}".constantize
|
||||
klass = "DiscourseAi::Personas::Tools::#{inner_name}".constantize
|
||||
options[klass] = current_options if current_options
|
||||
rescue StandardError
|
||||
end
|
||||
@ -218,7 +218,7 @@ class AiPersona < ActiveRecord::Base
|
||||
klass
|
||||
end
|
||||
|
||||
persona_class = DiscourseAi::AiBot::Personas::Persona.system_personas_by_id[self.id]
|
||||
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id]
|
||||
if persona_class
|
||||
instance_attributes.each do |key, value|
|
||||
# description/name are localized
|
||||
@ -230,7 +230,7 @@ class AiPersona < ActiveRecord::Base
|
||||
|
||||
ai_persona_id = self.id
|
||||
|
||||
Class.new(DiscourseAi::AiBot::Personas::Persona) do
|
||||
Class.new(DiscourseAi::Personas::Persona) do
|
||||
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
|
||||
|
||||
define_singleton_method(:to_s) do
|
||||
|
@ -36,7 +36,7 @@ class AiTool < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def runner(parameters, llm:, bot_user:, context: nil)
|
||||
DiscourseAi::AiBot::ToolRunner.new(
|
||||
DiscourseAi::Personas::ToolRunner.new(
|
||||
parameters: parameters,
|
||||
llm: llm,
|
||||
bot_user: bot_user,
|
||||
|
@ -1,18 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
DiscourseAi::AiBot::Personas::Persona.system_personas.each do |persona_class, id|
|
||||
DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
|
||||
persona = AiPersona.find_by(id: id)
|
||||
if !persona
|
||||
persona = AiPersona.new
|
||||
persona.id = id
|
||||
if persona_class == DiscourseAi::AiBot::Personas::WebArtifactCreator
|
||||
if persona_class == DiscourseAi::Personas::WebArtifactCreator
|
||||
# this is somewhat sensitive, so we default it to staff
|
||||
persona.allowed_group_ids = [Group::AUTO_GROUPS[:staff]]
|
||||
else
|
||||
persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]]
|
||||
end
|
||||
persona.enabled = true
|
||||
persona.priority = true if persona_class == DiscourseAi::AiBot::Personas::General
|
||||
persona.priority = true if persona_class == DiscourseAi::Personas::General
|
||||
end
|
||||
|
||||
names = [
|
@ -110,7 +110,7 @@ module DiscourseAi
|
||||
scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map)
|
||||
end,
|
||||
) do
|
||||
DiscourseAi::AiBot::Personas::Persona
|
||||
DiscourseAi::Personas::Persona
|
||||
.all(user: scope.user)
|
||||
.map do |persona|
|
||||
{
|
||||
@ -205,8 +205,7 @@ module DiscourseAi
|
||||
include_condition: -> { SiteSetting.ai_bot_enabled && object.topic.private_message? },
|
||||
) do
|
||||
id = topic.custom_fields["ai_persona_id"]
|
||||
name =
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
|
||||
name = DiscourseAi::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
|
||||
name || topic.custom_fields["ai_persona"]
|
||||
end
|
||||
|
||||
|
@ -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
|
@ -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
|
@ -150,13 +150,11 @@ module DiscourseAi
|
||||
persona = nil
|
||||
|
||||
if persona_id
|
||||
persona =
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
|
||||
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
|
||||
end
|
||||
|
||||
if !persona && persona_name = post.topic.custom_fields["ai_persona"]
|
||||
persona =
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, name: persona_name)
|
||||
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, name: persona_name)
|
||||
end
|
||||
|
||||
# edge case, llm was mentioned in an ai persona conversation
|
||||
@ -172,11 +170,11 @@ module DiscourseAi
|
||||
end
|
||||
end
|
||||
|
||||
persona ||= DiscourseAi::AiBot::Personas::General
|
||||
persona ||= DiscourseAi::Personas::General
|
||||
|
||||
bot_user = User.find(persona.user_id) if persona && persona.force_default_llm
|
||||
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
|
||||
new(bot).update_playground_with(post)
|
||||
end
|
||||
end
|
||||
@ -198,8 +196,8 @@ module DiscourseAi
|
||||
|
||||
bot_user = user || ai_persona.user
|
||||
raise Discourse::InvalidParameters.new(:user) if bot_user.nil?
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
|
||||
playground = DiscourseAi::AiBot::Playground.new(bot)
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
|
||||
playground = new(bot)
|
||||
|
||||
playground.reply_to(
|
||||
post,
|
||||
@ -236,14 +234,54 @@ module DiscourseAi
|
||||
include_uploads: bot.persona.class.vision_enabled,
|
||||
)
|
||||
|
||||
bot
|
||||
.get_updated_title(messages, post, user)
|
||||
.tap do |new_title|
|
||||
PostRevisor.new(post.topic.first_post, post.topic).revise!(
|
||||
bot.bot_user,
|
||||
title: new_title.sub(/\A"/, "").sub(/"\Z/, ""),
|
||||
)
|
||||
# conversation context may contain tool calls, and confusing user names
|
||||
# clean it up
|
||||
conversation = +""
|
||||
messages.each do |context|
|
||||
if context[:type] == :user
|
||||
conversation << "User said:\n#{context[:content]}\n\n"
|
||||
elsif context[:type] == :model
|
||||
conversation << "Model said:\n#{context[:content]}\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
system_insts = <<~TEXT.strip
|
||||
You are titlebot. Given a conversation, you will suggest a title.
|
||||
|
||||
- You will never respond with anything but the suggested title.
|
||||
- You will always match the conversation language in your title suggestion.
|
||||
- Title will capture the essence of the conversation.
|
||||
TEXT
|
||||
|
||||
instruction = <<~TEXT.strip
|
||||
Given the following conversation:
|
||||
|
||||
{{{
|
||||
#{conversation}
|
||||
}}}
|
||||
|
||||
Reply only with a title that is 7 words or less.
|
||||
TEXT
|
||||
|
||||
title_prompt =
|
||||
DiscourseAi::Completions::Prompt.new(
|
||||
system_insts,
|
||||
messages: [type: :user, content: instruction],
|
||||
topic_id: post.topic_id,
|
||||
)
|
||||
|
||||
new_title =
|
||||
bot
|
||||
.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)
|
||||
MessageBus.publish(
|
||||
@ -271,7 +309,7 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
context =
|
||||
BotContext.new(
|
||||
DiscourseAi::Personas::BotContext.new(
|
||||
participants: participants,
|
||||
message_id: message.id,
|
||||
channel_id: channel.id,
|
||||
@ -372,7 +410,7 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
context =
|
||||
BotContext.new(
|
||||
DiscourseAi::Personas::BotContext.new(
|
||||
post: post,
|
||||
custom_instructions: custom_instructions,
|
||||
messages:
|
||||
@ -575,8 +613,7 @@ module DiscourseAi
|
||||
|
||||
def schedule_bot_reply(post)
|
||||
persona_id =
|
||||
DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] ||
|
||||
bot.persona.class.id
|
||||
DiscourseAi::Personas::Persona.system_personas[bot.persona.class] || bot.persona.class.id
|
||||
::Jobs.enqueue(
|
||||
:create_ai_reply,
|
||||
post_id: post.id,
|
||||
|
@ -77,8 +77,8 @@ module DiscourseAi
|
||||
io.flush
|
||||
|
||||
persona_class =
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(id: persona.id, user: current_user)
|
||||
bot = DiscourseAi::AiBot::Bot.as(persona.user, persona: persona_class.new)
|
||||
DiscourseAi::Personas::Persona.find_by(id: persona.id, user: current_user)
|
||||
bot = DiscourseAi::Personas::Bot.as(persona.user, persona: persona_class.new)
|
||||
|
||||
data =
|
||||
{
|
||||
|
@ -7,7 +7,7 @@ module DiscourseAi
|
||||
return if !tool
|
||||
return if !tool.parameters.blank?
|
||||
|
||||
context = DiscourseAi::AiBot::BotContext.new(post: post)
|
||||
context = DiscourseAi::Personas::BotContext.new(post: post)
|
||||
|
||||
runner = tool.runner({}, llm: nil, bot_user: Discourse.system_user, context: context)
|
||||
runner.invoke
|
||||
|
@ -10,7 +10,7 @@ module DiscourseAi
|
||||
.find { |persona| persona.id == SiteSetting.ai_discord_search_persona.to_i }
|
||||
.new
|
||||
@bot =
|
||||
DiscourseAi::AiBot::Bot.as(
|
||||
DiscourseAi::Personas::Bot.as(
|
||||
Discourse.system_user,
|
||||
persona: @persona,
|
||||
model: LlmModel.find(@persona.class.default_llm_id),
|
||||
|
@ -4,7 +4,7 @@ module DiscourseAi
|
||||
module Discord::Bot
|
||||
class Search < Base
|
||||
def initialize(body)
|
||||
@search = DiscourseAi::AiBot::Tools::Search
|
||||
@search = DiscourseAi::Personas::Tools::Search
|
||||
super(body)
|
||||
end
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module ArtifactUpdateStrategies
|
||||
class InvalidFormatError < StandardError
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module ArtifactUpdateStrategies
|
||||
class Diff < Base
|
||||
attr_reader :failed_searches
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module ArtifactUpdateStrategies
|
||||
class Full < Base
|
||||
private
|
@ -1,19 +1,18 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class Artist < Persona
|
||||
def tools
|
||||
[Tools::Image]
|
||||
end
|
||||
module Personas
|
||||
class Artist < Persona
|
||||
def tools
|
||||
[Tools::Image]
|
||||
end
|
||||
|
||||
def required_tools
|
||||
[Tools::Image]
|
||||
end
|
||||
def required_tools
|
||||
[Tools::Image]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
You are artistbot and you are here to help people generate images.
|
||||
|
||||
You generate images using stable diffusion.
|
||||
@ -31,7 +30,6 @@ module DiscourseAi
|
||||
- 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
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class Bot
|
||||
attr_reader :model
|
||||
|
||||
@ -13,7 +13,7 @@ module DiscourseAi
|
||||
# limit is arbitrary, but 5 which was used in the past was too low
|
||||
MAX_TOOLS = 20
|
||||
|
||||
def self.as(bot_user, persona: DiscourseAi::AiBot::Personas::General.new, model: nil)
|
||||
def self.as(bot_user, persona: DiscourseAi::Personas::General.new, model: nil)
|
||||
new(bot_user, persona, model)
|
||||
end
|
||||
|
||||
@ -27,49 +27,8 @@ module DiscourseAi
|
||||
attr_reader :bot_user
|
||||
attr_accessor :persona
|
||||
|
||||
def get_updated_title(conversation_context, post, user)
|
||||
system_insts = <<~TEXT.strip
|
||||
You are titlebot. Given a conversation, you will suggest a title.
|
||||
|
||||
- You will never respond with anything but the suggested title.
|
||||
- You will always match the conversation language in your title suggestion.
|
||||
- Title will capture the essence of the conversation.
|
||||
TEXT
|
||||
|
||||
# conversation context may contain tool calls, and confusing user names
|
||||
# clean it up
|
||||
conversation = +""
|
||||
conversation_context.each do |context|
|
||||
if context[:type] == :user
|
||||
conversation << "User said:\n#{context[:content]}\n\n"
|
||||
elsif context[:type] == :model
|
||||
conversation << "Model said:\n#{context[:content]}\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
instruction = <<~TEXT.strip
|
||||
Given the following conversation:
|
||||
|
||||
{{{
|
||||
#{conversation}
|
||||
}}}
|
||||
|
||||
Reply only with a title that is 7 words or less.
|
||||
TEXT
|
||||
|
||||
title_prompt =
|
||||
DiscourseAi::Completions::Prompt.new(
|
||||
system_insts,
|
||||
messages: [type: :user, content: instruction],
|
||||
topic_id: post.topic_id,
|
||||
)
|
||||
|
||||
DiscourseAi::Completions::Llm
|
||||
.proxy(model)
|
||||
.generate(title_prompt, user: user, feature_name: "bot_title")
|
||||
.strip
|
||||
.split("\n")
|
||||
.last
|
||||
def llm
|
||||
@llm ||= DiscourseAi::Completions::Llm.proxy(model)
|
||||
end
|
||||
|
||||
def force_tool_if_needed(prompt, context)
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class BotContext
|
||||
attr_accessor :messages,
|
||||
:topic_id,
|
17
lib/personas/creative.rb
Normal file
17
lib/personas/creative.rb
Normal 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
|
@ -1,19 +1,18 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class DallE3 < Persona
|
||||
def tools
|
||||
[Tools::DallE]
|
||||
end
|
||||
module Personas
|
||||
class DallE3 < Persona
|
||||
def tools
|
||||
[Tools::DallE]
|
||||
end
|
||||
|
||||
def required_tools
|
||||
[Tools::DallE]
|
||||
end
|
||||
def required_tools
|
||||
[Tools::DallE]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
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).
|
||||
@ -32,7 +31,6 @@ module DiscourseAi
|
||||
Just generate the images
|
||||
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,15 +1,14 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class DiscourseHelper < Persona
|
||||
def tools
|
||||
[Tools::DiscourseMetaSearch]
|
||||
end
|
||||
module Personas
|
||||
class DiscourseHelper < Persona
|
||||
def tools
|
||||
[Tools::DiscourseMetaSearch]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
You are Discourse Helper Bot
|
||||
|
||||
- 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.
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,22 +1,21 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class General < Persona
|
||||
def tools
|
||||
[
|
||||
Tools::Search,
|
||||
Tools::Google,
|
||||
Tools::Image,
|
||||
Tools::Read,
|
||||
Tools::ListCategories,
|
||||
Tools::ListTags,
|
||||
]
|
||||
end
|
||||
module Personas
|
||||
class General < Persona
|
||||
def tools
|
||||
[
|
||||
Tools::Search,
|
||||
Tools::Google,
|
||||
Tools::Image,
|
||||
Tools::Read,
|
||||
Tools::ListCategories,
|
||||
Tools::ListTags,
|
||||
]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
You are a helpful Discourse assistant.
|
||||
You _understand_ and **generate** Discourse Markdown.
|
||||
You live in a Discourse Forum Message.
|
||||
@ -27,7 +26,6 @@ module DiscourseAi
|
||||
The participants in this conversation are: {participants}
|
||||
The date now is: {time}, much has changed since you were trained.
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,20 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class GithubHelper < Persona
|
||||
def tools
|
||||
[
|
||||
Tools::GithubFileContent,
|
||||
Tools::GithubPullRequestDiff,
|
||||
Tools::GithubSearchCode,
|
||||
Tools::GithubSearchFiles,
|
||||
]
|
||||
end
|
||||
module Personas
|
||||
class GithubHelper < Persona
|
||||
def tools
|
||||
[
|
||||
Tools::GithubFileContent,
|
||||
Tools::GithubPullRequestDiff,
|
||||
Tools::GithubSearchCode,
|
||||
Tools::GithubSearchFiles,
|
||||
]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
You are a helpful GitHub assistant.
|
||||
You _understand_ and **generate** Discourse Flavored Markdown.
|
||||
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.
|
||||
When asked about a specific repository, pull request, or file, try to use the available tools to provide accurate and helpful information.
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
395
lib/personas/persona.rb
Normal file
395
lib/personas/persona.rb
Normal 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
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class QuestionConsolidator
|
||||
attr_reader :llm, :messages, :user, :max_tokens
|
||||
|
@ -1,19 +1,18 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class Researcher < Persona
|
||||
def tools
|
||||
[Tools::Google, Tools::WebBrowser]
|
||||
end
|
||||
module Personas
|
||||
class Researcher < Persona
|
||||
def tools
|
||||
[Tools::Google, Tools::WebBrowser]
|
||||
end
|
||||
|
||||
def required_tools
|
||||
[Tools::Google]
|
||||
end
|
||||
def required_tools
|
||||
[Tools::Google]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
You are a research assistant with access to two powerful tools:
|
||||
|
||||
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.
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,15 +1,14 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class SettingsExplorer < Persona
|
||||
def tools
|
||||
[Tools::SettingContext, Tools::SearchSettings]
|
||||
end
|
||||
module Personas
|
||||
class SettingsExplorer < Persona
|
||||
def tools
|
||||
[Tools::SettingContext, Tools::SearchSettings]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
You are Discourse Site settings bot.
|
||||
|
||||
- You are able to find information about all the site settings.
|
||||
@ -19,7 +18,6 @@ module DiscourseAi
|
||||
|
||||
Current time is: {time}
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,52 +1,50 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class SqlHelper < Persona
|
||||
def self.schema
|
||||
return @schema if defined?(@schema)
|
||||
module Personas
|
||||
class SqlHelper < Persona
|
||||
def self.schema
|
||||
return @schema if defined?(@schema)
|
||||
|
||||
tables = Hash.new
|
||||
priority_tables = %w[
|
||||
posts
|
||||
topics
|
||||
notifications
|
||||
users
|
||||
user_actions
|
||||
user_emails
|
||||
categories
|
||||
groups
|
||||
]
|
||||
tables = Hash.new
|
||||
priority_tables = %w[
|
||||
posts
|
||||
topics
|
||||
notifications
|
||||
users
|
||||
user_actions
|
||||
user_emails
|
||||
categories
|
||||
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
|
||||
where table_schema = 'public'
|
||||
order by table_name
|
||||
SQL
|
||||
|
||||
priority =
|
||||
+(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
|
||||
priority = +(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
|
||||
|
||||
other_tables = +""
|
||||
tables.each do |table_name, _|
|
||||
next if priority_tables.include?(table_name)
|
||||
other_tables << "#{table_name} "
|
||||
end
|
||||
|
||||
@schema = { priority_tables: priority, other_tables: other_tables }
|
||||
other_tables = +""
|
||||
tables.each do |table_name, _|
|
||||
next if priority_tables.include?(table_name)
|
||||
other_tables << "#{table_name} "
|
||||
end
|
||||
|
||||
def tools
|
||||
[Tools::DbSchema]
|
||||
end
|
||||
@schema = { priority_tables: priority, other_tables: other_tables }
|
||||
end
|
||||
|
||||
def temperature
|
||||
0.2
|
||||
end
|
||||
def tools
|
||||
[Tools::DbSchema]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def temperature
|
||||
0.2
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
You are a PostgreSQL expert.
|
||||
- Avoid returning any text to the user prior to a tool call.
|
||||
- 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.
|
||||
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class ToolRunner
|
||||
attr_reader :tool, :parameters, :llm
|
||||
attr_accessor :running_attached_function, :timeout, :custom_raw
|
||||
@ -14,11 +14,11 @@ module DiscourseAi
|
||||
MAX_HTTP_REQUESTS = 20
|
||||
|
||||
def initialize(parameters:, llm:, bot_user:, context: nil, tool:, timeout: nil)
|
||||
if context && !context.is_a?(DiscourseAi::AiBot::BotContext)
|
||||
if context && !context.is_a?(DiscourseAi::Personas::BotContext)
|
||||
raise ArgumentError, "context must be a BotContext object"
|
||||
end
|
||||
|
||||
context ||= DiscourseAi::AiBot::BotContext.new
|
||||
context ||= DiscourseAi::Personas::BotContext.new
|
||||
|
||||
@parameters = parameters
|
||||
@llm = llm
|
||||
@ -339,7 +339,7 @@ module DiscourseAi
|
||||
return { error: "Persona not found" } if persona_class.nil?
|
||||
|
||||
persona = persona_class.new
|
||||
bot = DiscourseAi::AiBot::Bot.as(@bot_user || persona.user, persona: persona)
|
||||
bot = DiscourseAi::Personas::Bot.as(@bot_user || persona.user, persona: persona)
|
||||
playground = DiscourseAi::AiBot::Playground.new(bot)
|
||||
|
||||
if @context.post_id
|
||||
@ -488,7 +488,7 @@ module DiscourseAi
|
||||
headers = (options && options["headers"]) || {}
|
||||
|
||||
result = {}
|
||||
DiscourseAi::AiBot::Tools::Tool.send_http_request(
|
||||
DiscourseAi::Personas::Tools::Tool.send_http_request(
|
||||
url,
|
||||
headers: headers,
|
||||
) do |response|
|
||||
@ -517,7 +517,7 @@ module DiscourseAi
|
||||
body = options && options["body"]
|
||||
|
||||
result = {}
|
||||
DiscourseAi::AiBot::Tools::Tool.send_http_request(
|
||||
DiscourseAi::Personas::Tools::Tool.send_http_request(
|
||||
url,
|
||||
method: method,
|
||||
headers: headers,
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class CreateArtifact < Tool
|
||||
def self.name
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Custom < Tool
|
||||
def self.class_instance(tool_id)
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class DallE < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class DbSchema < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class DiscourseMetaSearch < Tool
|
||||
class << self
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class GithubFileContent < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class GithubPullRequestDiff < Tool
|
||||
LARGE_OBJECT_THRESHOLD = 30_000
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class GithubSearchCode < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class GithubSearchFiles < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Google < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Image < Tool
|
||||
def self.signature
|
@ -4,7 +4,7 @@ require "mini_racer"
|
||||
require "json"
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class JavascriptEvaluator < Tool
|
||||
TIMEOUT = 500
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class ListCategories < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class ListTags < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Option
|
||||
attr_reader :tool, :name, :type, :values, :default
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class RandomPicker < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
MAX_POSTS = 100
|
||||
|
||||
module Tools
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class ReadArtifact < Tool
|
||||
MAX_HTML_SIZE = 30.kilobytes
|
@ -1,7 +1,7 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Search < Tool
|
||||
attr_reader :last_query
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class SearchSettings < Tool
|
||||
INCLUDE_DESCRIPTIONS_MAX_LENGTH = 10
|
@ -1,7 +1,7 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class SettingContext < Tool
|
||||
MAX_CONTEXT_TOKENS = 2000
|
@ -1,7 +1,7 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Summarize < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Time < Tool
|
||||
def self.signature
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class Tool
|
||||
# Why 30 mega bytes?
|
||||
@ -63,9 +63,9 @@ module DiscourseAi
|
||||
@persona_options = persona_options
|
||||
@bot_user = bot_user
|
||||
@llm = llm
|
||||
@context = context.nil? ? DiscourseAi::AiBot::BotContext.new(messages: []) : context
|
||||
if !@context.is_a?(DiscourseAi::AiBot::BotContext)
|
||||
raise ArgumentError, "context must be a DiscourseAi::AiBot::Context"
|
||||
@context = context.nil? ? DiscourseAi::Personas::BotContext.new(messages: []) : context
|
||||
if !@context.is_a?(DiscourseAi::Personas::BotContext)
|
||||
raise ArgumentError, "context must be a DiscourseAi::Personas::Context"
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class UpdateArtifact < Tool
|
||||
def self.name
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
module Tools
|
||||
class WebBrowser < Tool
|
||||
def self.signature
|
@ -1,19 +1,18 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Personas
|
||||
class WebArtifactCreator < Persona
|
||||
def tools
|
||||
[Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact]
|
||||
end
|
||||
module Personas
|
||||
class WebArtifactCreator < Persona
|
||||
def tools
|
||||
[Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact]
|
||||
end
|
||||
|
||||
def required_tools
|
||||
[Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact]
|
||||
end
|
||||
def required_tools
|
||||
[Tools::CreateArtifact, Tools::UpdateArtifact, Tools::ReadArtifact]
|
||||
end
|
||||
|
||||
def system_prompt
|
||||
<<~PROMPT
|
||||
def system_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.
|
||||
|
||||
Core Principles:
|
||||
@ -49,7 +48,6 @@ module DiscourseAi
|
||||
|
||||
Remember: Great components combine structure (HTML), presentation (CSS), and behavior (JavaScript) to create memorable user experiences.
|
||||
PROMPT
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -83,6 +83,8 @@ after_initialize do
|
||||
|
||||
add_admin_route("discourse_ai.title", "discourse-ai", { use_new_show_route: true })
|
||||
|
||||
register_seedfu_fixtures(Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "personas"))
|
||||
|
||||
[
|
||||
DiscourseAi::Embeddings::EntryPoint.new,
|
||||
DiscourseAi::Sentiment::EntryPoint.new,
|
||||
|
@ -13,7 +13,7 @@ RSpec.describe DiscourseAi::Discord::Bot::PersonaReplier do
|
||||
|
||||
before do
|
||||
SiteSetting.ai_discord_search_persona = persona.id.to_s
|
||||
allow_any_instance_of(DiscourseAi::AiBot::Bot).to receive(:reply).and_return(
|
||||
allow_any_instance_of(DiscourseAi::Personas::Bot).to receive(:reply).and_return(
|
||||
"This is a reply from bot!",
|
||||
)
|
||||
allow(persona_replier).to receive(:create_reply)
|
||||
|
@ -20,7 +20,7 @@ RSpec.describe DiscourseAi::Discord::Bot::Search do
|
||||
|
||||
describe "#handle_interaction!" do
|
||||
it "creates a reply with search results" do
|
||||
allow_any_instance_of(DiscourseAi::AiBot::Tools::Search).to receive(:invoke).and_return(
|
||||
allow_any_instance_of(DiscourseAi::Personas::Tools::Search).to receive(:invoke).and_return(
|
||||
{ rows: [%w[Title /link]] },
|
||||
)
|
||||
search.handle_interaction!
|
||||
|
@ -22,14 +22,10 @@ RSpec.describe DiscourseAi::AiBot::Playground do
|
||||
fab!(:bot) do
|
||||
persona =
|
||||
AiPersona
|
||||
.find(
|
||||
DiscourseAi::AiBot::Personas::Persona.system_personas[
|
||||
DiscourseAi::AiBot::Personas::General
|
||||
],
|
||||
)
|
||||
.find(DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General])
|
||||
.class_instance
|
||||
.new
|
||||
DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
|
||||
DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
|
||||
end
|
||||
|
||||
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
|
||||
@ -103,7 +99,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
|
||||
)
|
||||
end
|
||||
|
||||
let(:bot) { DiscourseAi::AiBot::Bot.as(bot_user, persona: ai_persona.class_instance.new) }
|
||||
let(:bot) { DiscourseAi::Personas::Bot.as(bot_user, persona: ai_persona.class_instance.new) }
|
||||
|
||||
let(:playground) { DiscourseAi::AiBot::Playground.new(bot) }
|
||||
|
||||
@ -173,8 +169,8 @@ RSpec.describe DiscourseAi::AiBot::Playground do
|
||||
|
||||
it "uses custom tool in conversation" do
|
||||
persona_klass = AiPersona.all_personas.find { |p| p.name == ai_persona.name }
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona_klass.new)
|
||||
playground = DiscourseAi::AiBot::Playground.new(bot)
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona_klass.new)
|
||||
playground = described_class.new(bot)
|
||||
|
||||
responses = [tool_call, "custom tool did stuff (maybe)"]
|
||||
|
||||
@ -213,7 +209,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
|
||||
custom_tool.update!(enabled: false)
|
||||
# so we pick up new cache
|
||||
persona_klass = AiPersona.all_personas.find { |p| p.name == ai_persona.name }
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona_klass.new)
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona_klass.new)
|
||||
playground = DiscourseAi::AiBot::Playground.new(bot)
|
||||
|
||||
responses = ["custom tool did stuff (maybe)", tool_call]
|
||||
@ -968,7 +964,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
|
||||
|
||||
it "supports disabling tool details" do
|
||||
persona = Fabricate(:ai_persona, tool_details: false, tools: ["Search"])
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.class_instance.new)
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.class_instance.new)
|
||||
playground = described_class.new(bot)
|
||||
|
||||
response1 =
|
||||
@ -1021,13 +1017,11 @@ RSpec.describe DiscourseAi::AiBot::Playground do
|
||||
|
||||
let(:persona) do
|
||||
AiPersona.find(
|
||||
DiscourseAi::AiBot::Personas::Persona.system_personas[
|
||||
DiscourseAi::AiBot::Personas::DallE3
|
||||
],
|
||||
DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::DallE3],
|
||||
)
|
||||
end
|
||||
|
||||
let(:bot) { DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.class_instance.new) }
|
||||
let(:bot) { DiscourseAi::Personas::Bot.as(bot_user, persona: persona.class_instance.new) }
|
||||
let(:data) do
|
||||
image =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::ArtifactUpdateStrategies::Diff do
|
||||
RSpec.describe DiscourseAi::Personas::ArtifactUpdateStrategies::Diff do
|
||||
fab!(:user)
|
||||
fab!(:post)
|
||||
fab!(:artifact) { Fabricate(:ai_artifact) }
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Bot do
|
||||
RSpec.describe DiscourseAi::Personas::Bot do
|
||||
subject(:bot) { described_class.as(bot_user) }
|
||||
|
||||
fab!(:admin)
|
||||
@ -48,11 +48,11 @@ RSpec.describe DiscourseAi::AiBot::Bot do
|
||||
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
|
||||
)
|
||||
|
||||
personaClass = DiscourseAi::AiBot::Personas::Persona.find_by(user: admin, name: "TestPersona")
|
||||
personaClass = DiscourseAi::Personas::Persona.find_by(user: admin, name: "TestPersona")
|
||||
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: personaClass.new)
|
||||
bot = described_class.as(bot_user, persona: personaClass.new)
|
||||
bot.reply(
|
||||
DiscourseAi::AiBot::BotContext.new(messages: [{ type: :user, content: "test" }]),
|
||||
DiscourseAi::Personas::BotContext.new(messages: [{ type: :user, content: "test" }]),
|
||||
) do |_partial, _cancel, _placeholder|
|
||||
# we just need the block so bot has something to call with results
|
||||
end
|
||||
@ -64,7 +64,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
|
||||
|
||||
context "when using function chaining" do
|
||||
it "yields a loading placeholder while proceeds to invoke the command" do
|
||||
tool = DiscourseAi::AiBot::Tools::ListCategories.new({}, bot_user: nil, llm: nil)
|
||||
tool = DiscourseAi::Personas::Tools::ListCategories.new({}, bot_user: nil, llm: nil)
|
||||
partial_placeholder = +(<<~HTML)
|
||||
<details>
|
||||
<summary>#{tool.summary}</summary>
|
||||
@ -75,7 +75,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
|
||||
HTML
|
||||
|
||||
context =
|
||||
DiscourseAi::AiBot::BotContext.new(
|
||||
DiscourseAi::Personas::BotContext.new(
|
||||
messages: [{ type: :user, content: "Does my site has tags?" }],
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
class TestPersona < DiscourseAi::AiBot::Personas::Persona
|
||||
class TestPersona < DiscourseAi::Personas::Persona
|
||||
def tools
|
||||
[
|
||||
DiscourseAi::AiBot::Tools::ListTags,
|
||||
DiscourseAi::AiBot::Tools::Search,
|
||||
DiscourseAi::AiBot::Tools::Image,
|
||||
DiscourseAi::Personas::Tools::ListTags,
|
||||
DiscourseAi::Personas::Tools::Search,
|
||||
DiscourseAi::Personas::Tools::Image,
|
||||
]
|
||||
end
|
||||
def system_prompt
|
||||
@ -19,7 +19,7 @@ class TestPersona < DiscourseAi::AiBot::Personas::Persona
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
RSpec.describe DiscourseAi::Personas::Persona do
|
||||
let :persona do
|
||||
TestPersona.new
|
||||
end
|
||||
@ -36,7 +36,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
end
|
||||
|
||||
let(:context) do
|
||||
DiscourseAi::AiBot::BotContext.new(
|
||||
DiscourseAi::Personas::BotContext.new(
|
||||
site_url: Discourse.base_url,
|
||||
site_title: "test site title",
|
||||
site_description: "test site description",
|
||||
@ -84,12 +84,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
)
|
||||
|
||||
tool_instance =
|
||||
DiscourseAi::AiBot::Personas::Artist.new.find_tool(
|
||||
tool_call,
|
||||
bot_user: nil,
|
||||
llm: nil,
|
||||
context: nil,
|
||||
)
|
||||
DiscourseAi::Personas::Artist.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
|
||||
|
||||
expect(tool_instance.parameters[:prompts]).to eq(["cat oil painting", "big car"])
|
||||
expect(tool_instance.parameters[:aspect_ratio]).to eq("16:9")
|
||||
@ -108,12 +103,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
)
|
||||
|
||||
tool_instance =
|
||||
DiscourseAi::AiBot::Personas::General.new.find_tool(
|
||||
tool_call,
|
||||
bot_user: nil,
|
||||
llm: nil,
|
||||
context: nil,
|
||||
)
|
||||
DiscourseAi::Personas::General.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
|
||||
|
||||
expect(tool_instance.parameters.key?(:status)).to eq(false)
|
||||
|
||||
@ -129,12 +119,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
)
|
||||
|
||||
tool_instance =
|
||||
DiscourseAi::AiBot::Personas::General.new.find_tool(
|
||||
tool_call,
|
||||
bot_user: nil,
|
||||
llm: nil,
|
||||
context: nil,
|
||||
)
|
||||
DiscourseAi::Personas::General.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
|
||||
|
||||
expect(tool_instance.parameters[:status]).to eq("open")
|
||||
end
|
||||
@ -152,12 +137,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
)
|
||||
|
||||
search =
|
||||
DiscourseAi::AiBot::Personas::General.new.find_tool(
|
||||
tool_call,
|
||||
bot_user: nil,
|
||||
llm: nil,
|
||||
context: nil,
|
||||
)
|
||||
DiscourseAi::Personas::General.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
|
||||
|
||||
expect(search.parameters[:max_posts]).to eq(3)
|
||||
expect(search.parameters[:search_query]).to eq("hello world")
|
||||
@ -177,12 +157,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
)
|
||||
|
||||
tool_instance =
|
||||
DiscourseAi::AiBot::Personas::DallE3.new.find_tool(
|
||||
tool_call,
|
||||
bot_user: nil,
|
||||
llm: nil,
|
||||
context: nil,
|
||||
)
|
||||
DiscourseAi::Personas::DallE3.new.find_tool(tool_call, bot_user: nil, llm: nil, context: nil)
|
||||
expect(tool_instance.parameters[:prompts]).to eq(["cat oil painting", "big car"])
|
||||
end
|
||||
|
||||
@ -200,29 +175,29 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
|
||||
)
|
||||
|
||||
custom_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
|
||||
custom_persona = DiscourseAi::Personas::Persona.all(user: user).last
|
||||
expect(custom_persona.name).to eq("zzzpun_bot")
|
||||
expect(custom_persona.description).to eq("you write puns")
|
||||
|
||||
instance = custom_persona.new
|
||||
expect(instance.tools).to eq([DiscourseAi::AiBot::Tools::Image])
|
||||
expect(instance.tools).to eq([DiscourseAi::Personas::Tools::Image])
|
||||
expect(instance.craft_prompt(context).messages.first[:content]).to eq("you are pun bot")
|
||||
|
||||
# should update
|
||||
persona.update!(name: "zzzpun_bot2")
|
||||
custom_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
|
||||
custom_persona = DiscourseAi::Personas::Persona.all(user: user).last
|
||||
expect(custom_persona.name).to eq("zzzpun_bot2")
|
||||
|
||||
# can be disabled
|
||||
persona.update!(enabled: false)
|
||||
last_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
|
||||
last_persona = DiscourseAi::Personas::Persona.all(user: user).last
|
||||
expect(last_persona.name).not_to eq("zzzpun_bot2")
|
||||
|
||||
persona.update!(enabled: true)
|
||||
# no groups have access
|
||||
persona.update!(allowed_group_ids: [])
|
||||
|
||||
last_persona = DiscourseAi::AiBot::Personas::Persona.all(user: user).last
|
||||
last_persona = DiscourseAi::Personas::Persona.all(user: user).last
|
||||
expect(last_persona.name).not_to eq("zzzpun_bot2")
|
||||
end
|
||||
end
|
||||
@ -237,31 +212,31 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
SiteSetting.ai_google_custom_search_cx = "abc123"
|
||||
|
||||
# should be ordered by priority and then alpha
|
||||
expect(DiscourseAi::AiBot::Personas::Persona.all(user: user)).to eq(
|
||||
expect(DiscourseAi::Personas::Persona.all(user: user)).to eq(
|
||||
[
|
||||
DiscourseAi::AiBot::Personas::General,
|
||||
DiscourseAi::AiBot::Personas::Artist,
|
||||
DiscourseAi::AiBot::Personas::Creative,
|
||||
DiscourseAi::AiBot::Personas::DiscourseHelper,
|
||||
DiscourseAi::AiBot::Personas::GithubHelper,
|
||||
DiscourseAi::AiBot::Personas::Researcher,
|
||||
DiscourseAi::AiBot::Personas::SettingsExplorer,
|
||||
DiscourseAi::AiBot::Personas::SqlHelper,
|
||||
DiscourseAi::Personas::General,
|
||||
DiscourseAi::Personas::Artist,
|
||||
DiscourseAi::Personas::Creative,
|
||||
DiscourseAi::Personas::DiscourseHelper,
|
||||
DiscourseAi::Personas::GithubHelper,
|
||||
DiscourseAi::Personas::Researcher,
|
||||
DiscourseAi::Personas::SettingsExplorer,
|
||||
DiscourseAi::Personas::SqlHelper,
|
||||
],
|
||||
)
|
||||
|
||||
# it should allow staff access to WebArtifactCreator
|
||||
expect(DiscourseAi::AiBot::Personas::Persona.all(user: admin)).to eq(
|
||||
expect(DiscourseAi::Personas::Persona.all(user: admin)).to eq(
|
||||
[
|
||||
DiscourseAi::AiBot::Personas::General,
|
||||
DiscourseAi::AiBot::Personas::Artist,
|
||||
DiscourseAi::AiBot::Personas::Creative,
|
||||
DiscourseAi::AiBot::Personas::DiscourseHelper,
|
||||
DiscourseAi::AiBot::Personas::GithubHelper,
|
||||
DiscourseAi::AiBot::Personas::Researcher,
|
||||
DiscourseAi::AiBot::Personas::SettingsExplorer,
|
||||
DiscourseAi::AiBot::Personas::SqlHelper,
|
||||
DiscourseAi::AiBot::Personas::WebArtifactCreator,
|
||||
DiscourseAi::Personas::General,
|
||||
DiscourseAi::Personas::Artist,
|
||||
DiscourseAi::Personas::Creative,
|
||||
DiscourseAi::Personas::DiscourseHelper,
|
||||
DiscourseAi::Personas::GithubHelper,
|
||||
DiscourseAi::Personas::Researcher,
|
||||
DiscourseAi::Personas::SettingsExplorer,
|
||||
DiscourseAi::Personas::SqlHelper,
|
||||
DiscourseAi::Personas::WebArtifactCreator,
|
||||
],
|
||||
)
|
||||
|
||||
@ -270,27 +245,25 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
SiteSetting.ai_google_custom_search_api_key = ""
|
||||
SiteSetting.ai_artifact_security = "disabled"
|
||||
|
||||
expect(DiscourseAi::AiBot::Personas::Persona.all(user: admin)).to contain_exactly(
|
||||
DiscourseAi::AiBot::Personas::General,
|
||||
DiscourseAi::AiBot::Personas::SqlHelper,
|
||||
DiscourseAi::AiBot::Personas::SettingsExplorer,
|
||||
DiscourseAi::AiBot::Personas::Creative,
|
||||
DiscourseAi::AiBot::Personas::DiscourseHelper,
|
||||
DiscourseAi::AiBot::Personas::GithubHelper,
|
||||
expect(DiscourseAi::Personas::Persona.all(user: admin)).to contain_exactly(
|
||||
DiscourseAi::Personas::General,
|
||||
DiscourseAi::Personas::SqlHelper,
|
||||
DiscourseAi::Personas::SettingsExplorer,
|
||||
DiscourseAi::Personas::Creative,
|
||||
DiscourseAi::Personas::DiscourseHelper,
|
||||
DiscourseAi::Personas::GithubHelper,
|
||||
)
|
||||
|
||||
AiPersona.find(
|
||||
DiscourseAi::AiBot::Personas::Persona.system_personas[
|
||||
DiscourseAi::AiBot::Personas::General
|
||||
],
|
||||
DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General],
|
||||
).update!(enabled: false)
|
||||
|
||||
expect(DiscourseAi::AiBot::Personas::Persona.all(user: user)).to contain_exactly(
|
||||
DiscourseAi::AiBot::Personas::SqlHelper,
|
||||
DiscourseAi::AiBot::Personas::SettingsExplorer,
|
||||
DiscourseAi::AiBot::Personas::Creative,
|
||||
DiscourseAi::AiBot::Personas::DiscourseHelper,
|
||||
DiscourseAi::AiBot::Personas::GithubHelper,
|
||||
expect(DiscourseAi::Personas::Persona.all(user: user)).to contain_exactly(
|
||||
DiscourseAi::Personas::SqlHelper,
|
||||
DiscourseAi::Personas::SettingsExplorer,
|
||||
DiscourseAi::Personas::Creative,
|
||||
DiscourseAi::Personas::DiscourseHelper,
|
||||
DiscourseAi::Personas::GithubHelper,
|
||||
)
|
||||
end
|
||||
end
|
||||
@ -304,7 +277,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
SiteSetting.ai_embeddings_enabled = true
|
||||
end
|
||||
|
||||
let(:ai_persona) { DiscourseAi::AiBot::Personas::Persona.all(user: user).first.new }
|
||||
let(:ai_persona) { DiscourseAi::Personas::Persona.all(user: user).first.new }
|
||||
|
||||
let(:with_cc) do
|
||||
context.messages = [{ content: "Tell me the time", type: :user }]
|
||||
@ -343,7 +316,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
UploadReference.ensure_exist!(target: custom_ai_persona, upload_ids: [upload.id])
|
||||
|
||||
custom_persona =
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
|
||||
DiscourseAi::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
|
||||
|
||||
# this means that we will consolidate
|
||||
context.messages = [
|
||||
@ -415,7 +388,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::Persona do
|
||||
UploadReference.ensure_exist!(target: custom_ai_persona, upload_ids: [upload.id])
|
||||
|
||||
custom_persona =
|
||||
DiscourseAi::AiBot::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
|
||||
DiscourseAi::Personas::Persona.find_by(id: custom_ai_persona.id, user: user).new
|
||||
|
||||
expect(custom_persona.class.rag_conversation_chunks).to eq(3)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::QuestionConsolidator do
|
||||
RSpec.describe DiscourseAi::Personas::QuestionConsolidator do
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{Fabricate(:fake_model).id}") }
|
||||
let(:fake_endpoint) { DiscourseAi::Completions::Endpoints::Fake }
|
||||
|
@ -1,13 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Personas::Researcher do
|
||||
RSpec.describe DiscourseAi::Personas::Researcher do
|
||||
let :researcher do
|
||||
subject
|
||||
end
|
||||
|
||||
it "renders schema" do
|
||||
expect(researcher.tools).to eq(
|
||||
[DiscourseAi::AiBot::Tools::Google, DiscourseAi::AiBot::Tools::WebBrowser],
|
||||
[DiscourseAi::Personas::Tools::Google, DiscourseAi::Personas::Tools::WebBrowser],
|
||||
)
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Personas::SettingsExplorer do
|
||||
RSpec.describe DiscourseAi::Personas::SettingsExplorer do
|
||||
let :settings_explorer do
|
||||
subject
|
||||
end
|
||||
@ -14,7 +14,7 @@ RSpec.describe DiscourseAi::AiBot::Personas::SettingsExplorer do
|
||||
expect(prompt).to include("site_description")
|
||||
|
||||
expect(settings_explorer.tools).to eq(
|
||||
[DiscourseAi::AiBot::Tools::SettingContext, DiscourseAi::AiBot::Tools::SearchSettings],
|
||||
[DiscourseAi::Personas::Tools::SettingContext, DiscourseAi::Personas::Tools::SearchSettings],
|
||||
)
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Personas::SqlHelper do
|
||||
RSpec.describe DiscourseAi::Personas::SqlHelper do
|
||||
let :sql_helper do
|
||||
subject
|
||||
end
|
||||
@ -12,6 +12,6 @@ RSpec.describe DiscourseAi::AiBot::Personas::SqlHelper do
|
||||
expect(prompt).not_to include("translation_key") # not a priority table
|
||||
expect(prompt).to include("user_api_keys") # not a priority table
|
||||
|
||||
expect(sql_helper.tools).to eq([DiscourseAi::AiBot::Tools::DbSchema])
|
||||
expect(sql_helper.tools).to eq([DiscourseAi::Personas::Tools::DbSchema])
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::CreateArtifact do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::CreateArtifact do
|
||||
fab!(:llm_model)
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
||||
fab!(:post)
|
||||
@ -34,7 +34,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::CreateArtifact do
|
||||
{ html_body: "hello" },
|
||||
bot_user: Fabricate(:user),
|
||||
llm: llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(post: post),
|
||||
)
|
||||
|
||||
tool.parameters = { name: "hello", specification: "hello spec" }
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::DallE do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::DallE do
|
||||
let(:prompts) { ["a pink cow", "a red cow"] }
|
||||
|
||||
fab!(:gpt_35_turbo) { Fabricate(:llm_model, name: "gpt-3.5-turbo") }
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::DbSchema do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::DbSchema do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::DiscourseMetaSearch do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::DiscourseMetaSearch do
|
||||
before { SiteSetting.ai_bot_enabled = true }
|
||||
|
||||
fab!(:llm_model) { Fabricate(:llm_model, max_prompt_tokens: 8192) }
|
@ -2,7 +2,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::GithubFileContent do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::GithubFileContent do
|
||||
fab!(:llm_model)
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::GithubPullRequestDiff do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::GithubPullRequestDiff do
|
||||
let(:bot_user) { Fabricate(:user) }
|
||||
fab!(:llm_model)
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -2,7 +2,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::GithubSearchCode do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::GithubSearchCode do
|
||||
let(:bot_user) { Fabricate(:user) }
|
||||
fab!(:llm_model)
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -2,7 +2,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::GithubSearchFiles do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::GithubSearchFiles do
|
||||
fab!(:llm_model)
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
||||
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::Google do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::Google do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::Image do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::Image do
|
||||
let(:progress_blk) { Proc.new {} }
|
||||
let(:prompts) { ["a pink cow", "a red cow"] }
|
||||
|
||||
@ -9,7 +9,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Image do
|
||||
{ prompts: prompts, seeds: [99, 32] },
|
||||
bot_user: bot_user,
|
||||
llm: llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new,
|
||||
context: DiscourseAi::Personas::BotContext.new,
|
||||
)
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::JavascriptEvaluator do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::JavascriptEvaluator do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::ListCategories do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::ListCategories do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::ListTags do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::ListTags do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -2,7 +2,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::RandomPicker do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::RandomPicker do
|
||||
describe "#invoke" do
|
||||
subject { described_class.new({ options: options }, bot_user: nil, llm: nil).invoke }
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::ReadArtifact do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
fab!(:post)
|
||||
@ -25,7 +25,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
|
||||
{ url: "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/#{artifact.id}" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -44,7 +44,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
|
||||
{ url: "invalid-url" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -58,7 +58,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
|
||||
{ url: "#{Discourse.base_url}/discourse-ai/ai-bot/artifacts/99999" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -91,7 +91,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
|
||||
{ url: "https://example.com" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -120,7 +120,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::ReadArtifact do
|
||||
{ url: "https://example.com" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::Read do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::Read do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
||||
@ -56,7 +56,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Read do
|
||||
persona_options: {
|
||||
"read_private" => true,
|
||||
},
|
||||
context: DiscourseAi::AiBot::BotContext.new(user: admin),
|
||||
context: DiscourseAi::Personas::BotContext.new(user: admin),
|
||||
)
|
||||
results = tool.invoke
|
||||
expect(results[:content]).to include("hello there")
|
||||
@ -66,7 +66,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Read do
|
||||
{ topic_id: topic_with_tags.id, post_numbers: [post1.post_number] },
|
||||
bot_user: bot_user,
|
||||
llm: llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(user: admin),
|
||||
context: DiscourseAi::Personas::BotContext.new(user: admin),
|
||||
)
|
||||
|
||||
results = tool.invoke
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::SearchSettings do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::SearchSettings do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::Search do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::Search do
|
||||
before { SearchIndexer.enable }
|
||||
after { SearchIndexer.disable }
|
||||
|
||||
@ -60,7 +60,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::Search do
|
||||
persona_options: persona_options,
|
||||
bot_user: bot_user,
|
||||
llm: llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(user: user),
|
||||
context: DiscourseAi::Personas::BotContext.new(user: user),
|
||||
)
|
||||
|
||||
expect(search.options[:base_query]).to eq("#funny")
|
@ -8,7 +8,7 @@ def has_rg?
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::SettingContext, if: has_rg? do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::SettingContext, if: has_rg? do
|
||||
fab!(:llm_model)
|
||||
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::Summarize do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::Summarize do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -1,6 +1,6 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::Time do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::Time do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::Tool do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::Tool do
|
||||
let :tool_class do
|
||||
described_class
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::UpdateArtifact do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
fab!(:post)
|
||||
@ -47,7 +47,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
persona_options: {
|
||||
"update_algorithm" => "full",
|
||||
},
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -91,7 +91,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
persona_options: {
|
||||
"update_algorithm" => "full",
|
||||
},
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -115,7 +115,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
{ artifact_id: artifact.id, instructions: "Invalid update" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -129,7 +129,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
{ artifact_id: -1, instructions: "Update something" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -155,7 +155,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
persona_options: {
|
||||
"update_algorithm" => "full",
|
||||
},
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
)
|
||||
|
||||
tool.invoke {}
|
||||
@ -186,7 +186,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
persona_options: {
|
||||
"update_algorithm" => "full",
|
||||
},
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
)
|
||||
.invoke {}
|
||||
end
|
||||
@ -212,7 +212,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
persona_options: {
|
||||
"update_algorithm" => "full",
|
||||
},
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
)
|
||||
|
||||
result = tool.invoke {}
|
||||
@ -262,7 +262,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
{ artifact_id: artifact.id, instructions: "Change the text to Updated and color to red" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
persona_options: {
|
||||
"update_algorithm" => "diff",
|
||||
},
|
||||
@ -330,7 +330,7 @@ RSpec.describe DiscourseAi::AiBot::Tools::UpdateArtifact do
|
||||
{ artifact_id: artifact.id, instructions: "Change the text to Updated and color to red" },
|
||||
bot_user: bot_user,
|
||||
llm: llm_model.to_llm,
|
||||
context: DiscourseAi::AiBot::BotContext.new(messages: [], post: post),
|
||||
context: DiscourseAi::Personas::BotContext.new(messages: [], post: post),
|
||||
persona_options: {
|
||||
"update_algorithm" => "diff",
|
||||
},
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::AiBot::Tools::WebBrowser do
|
||||
RSpec.describe DiscourseAi::Personas::Tools::WebBrowser do
|
||||
fab!(:llm_model)
|
||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
@ -121,7 +121,7 @@ RSpec.describe AiTool do
|
||||
},
|
||||
)
|
||||
|
||||
expect { runner.invoke }.to raise_error(DiscourseAi::AiBot::ToolRunner::TooManyRequestsError)
|
||||
expect { runner.invoke }.to raise_error(DiscourseAi::Personas::ToolRunner::TooManyRequestsError)
|
||||
end
|
||||
|
||||
it "can perform GET HTTP requests" do
|
||||
|
@ -19,7 +19,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
|
||||
expect(response.parsed_body["ai_personas"].length).to eq(AiPersona.count)
|
||||
expect(response.parsed_body["meta"]["tools"].length).to eq(
|
||||
DiscourseAi::AiBot::Personas::Persona.all_available_tools.length,
|
||||
DiscourseAi::Personas::Persona.all_available_tools.length,
|
||||
)
|
||||
end
|
||||
|
||||
@ -136,10 +136,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
it "returns localized persona names and descriptions" do
|
||||
get "/admin/plugins/discourse-ai/ai-personas.json"
|
||||
|
||||
id =
|
||||
DiscourseAi::AiBot::Personas::Persona.system_personas[
|
||||
DiscourseAi::AiBot::Personas::General
|
||||
]
|
||||
id = DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::General]
|
||||
persona = response.parsed_body["ai_personas"].find { |p| p["id"] == id }
|
||||
|
||||
expect(persona["name"]).to eq("Général")
|
||||
@ -301,7 +298,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
end
|
||||
|
||||
it "does not allow temperature and top p changes on stock personas" do
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
|
||||
params: {
|
||||
ai_persona: {
|
||||
top_p: 0.5,
|
||||
@ -335,7 +332,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
|
||||
context "with system personas" do
|
||||
it "does not allow editing of system prompts" do
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
|
||||
params: {
|
||||
ai_persona: {
|
||||
system_prompt: "you are not a helpful bot",
|
||||
@ -348,7 +345,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
end
|
||||
|
||||
it "does not allow editing of tools" do
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
|
||||
params: {
|
||||
ai_persona: {
|
||||
tools: %w[SearchCommand ImageCommand],
|
||||
@ -361,7 +358,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
end
|
||||
|
||||
it "does not allow editing of name and description cause it is localized" do
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
|
||||
params: {
|
||||
ai_persona: {
|
||||
name: "bob",
|
||||
@ -375,7 +372,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
end
|
||||
|
||||
it "does allow some actions" do
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json",
|
||||
put "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json",
|
||||
params: {
|
||||
ai_persona: {
|
||||
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_1]],
|
||||
@ -413,7 +410,7 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
||||
|
||||
it "is not allowed to delete system personas" do
|
||||
expect {
|
||||
delete "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}.json"
|
||||
delete "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}.json"
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body["errors"].join).not_to be_blank
|
||||
# let's make sure this is translated
|
||||
|
@ -55,7 +55,7 @@ RSpec.describe "Admin AI persona configuration", type: :system, js: true do
|
||||
end
|
||||
|
||||
it "will not allow deletion or editing of system personas" do
|
||||
visit "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}/edit"
|
||||
visit "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::Personas::Persona.system_personas.values.first}/edit"
|
||||
expect(page).not_to have_selector(".ai-persona-editor__delete")
|
||||
expect(form.field("system_prompt")).to be_disabled
|
||||
end
|
||||
|
@ -16,7 +16,7 @@ RSpec.describe "AI personas", type: :system, js: true do
|
||||
persona_selector =
|
||||
PageObjects::Components::SelectKit.new(".persona-llm-selector__persona-dropdown")
|
||||
|
||||
id = DiscourseAi::AiBot::Personas::Persona.all(user: admin).first.id
|
||||
id = DiscourseAi::Personas::Persona.all(user: admin).first.id
|
||||
|
||||
expect(persona_selector).to have_selected_value(id)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user