WIP: migrate persona to agent

This commit is contained in:
Sam Saffron 2025-05-29 15:40:46 +10:00
parent ad5c48d9ae
commit 399feafc4f
No known key found for this signature in database
GPG Key ID: B9606168D2FFD9F5
223 changed files with 2456 additions and 1957 deletions

View File

@ -0,0 +1,19 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsShowDiscourseAiAgentsEdit extends DiscourseRoute {
async model(params) {
const allAgents = this.modelFor(
"adminPlugins.show.discourse-ai-agents"
);
const id = parseInt(params.id, 10);
return allAgents.findBy("id", id);
}
setupController(controller, model) {
super.setupController(controller, model);
controller.set(
"allAgents",
this.modelFor("adminPlugins.show.discourse-ai-agents")
);
}
}

View File

@ -1,16 +1,16 @@
import { AUTO_GROUPS } from "discourse/lib/constants";
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRoute {
export default class AdminPluginsShowDiscourseAiAgentsNew extends DiscourseRoute {
async model() {
const record = this.store.createRecord("ai-persona");
const record = this.store.createRecord("ai-agent");
record.set("allowed_group_ids", [AUTO_GROUPS.trust_level_0.id]);
record.set("rag_uploads", []);
// these match the defaults on the table
record.set("rag_chunk_tokens", 374);
record.set("rag_chunk_overlap_tokens", 10);
record.set("rag_conversation_chunks", 10);
record.set("allow_personal_messages", true);
record.set("allow_agentl_messages", true);
record.set("tool_details", false);
return record;
}
@ -18,8 +18,8 @@ export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRou
setupController(controller, model) {
super.setupController(controller, model);
controller.set(
"allPersonas",
this.modelFor("adminPlugins.show.discourse-ai-personas")
"allAgents",
this.modelFor("adminPlugins.show.discourse-ai-agents")
);
}
}

View File

@ -0,0 +1,7 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiAiAgentsRoute extends DiscourseRoute {
model() {
return this.store.findAll("ai-agent");
}
}

View File

@ -1,19 +0,0 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsShowDiscourseAiPersonasEdit extends DiscourseRoute {
async model(params) {
const allPersonas = this.modelFor(
"adminPlugins.show.discourse-ai-personas"
);
const id = parseInt(params.id, 10);
return allPersonas.findBy("id", id);
}
setupController(controller, model) {
super.setupController(controller, model);
controller.set(
"allPersonas",
this.modelFor("adminPlugins.show.discourse-ai-personas")
);
}
}

View File

@ -1,7 +0,0 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiAiPersonasRoute extends DiscourseRoute {
model() {
return this.store.findAll("ai-persona");
}
}

View File

@ -0,0 +1,4 @@
<AiAgentListEditor
@agents={{this.allAgents}}
@currentAgent={{this.model}}
/>

View File

@ -0,0 +1 @@
<AiAgentListEditor @agents={{this.model}} />

View File

@ -0,0 +1,4 @@
<AiAgentListEditor
@agents={{this.allAgents}}
@currentAgent={{this.model}}
/>

View File

@ -15,7 +15,7 @@ export default RouteTemplate(
const prefix = "discourse_ai.features.list.header";
return [
i18n(`${prefix}.name`),
i18n(`${prefix}.persona`),
i18n(`${prefix}.agent`),
i18n(`${prefix}.groups`),
"",
];
@ -75,21 +75,21 @@ export default RouteTemplate(
</span>
</td>
<td
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__persona"
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__agent"
>
<DButton
class="btn-flat btn-small ai-feature-list__row-item-persona"
@translatedLabel={{feature.persona.name}}
@route="adminPlugins.show.discourse-ai-personas.edit"
@routeModels={{feature.persona.id}}
class="btn-flat btn-small ai-feature-list__row-item-agent"
@translatedLabel={{feature.agent.name}}
@route="adminPlugins.show.discourse-ai-agents.edit"
@routeModels={{feature.agent.id}}
/>
</td>
<td
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__groups"
>
{{#if (gt feature.persona.allowed_groups.length 0)}}
{{#if (gt feature.agent.allowed_groups.length 0)}}
<ul class="ai-feature-list__row-item-groups">
{{#each feature.persona.allowed_groups as |group|}}
{{#each feature.agent.allowed_groups as |group|}}
<li>{{group.name}}</li>
{{/each}}
</ul>

View File

@ -1,4 +0,0 @@
<AiPersonaListEditor
@personas={{this.allPersonas}}
@currentPersona={{this.model}}
/>

View File

@ -1 +0,0 @@
<AiPersonaListEditor @personas={{this.model}} />

View File

@ -1,4 +0,0 @@
<AiPersonaListEditor
@personas={{this.allPersonas}}
@currentPersona={{this.model}}
/>

View File

@ -1,4 +1,4 @@
<section class="ai-persona-tool-editor__current admin-detail pull-left">
<section class="ai-agent-tool-editor__current admin-detail pull-left">
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}

View File

@ -1,4 +1,4 @@
<section class="ai-persona-tool-editor__current admin-detail pull-left">
<section class="ai-agent-tool-editor__current admin-detail pull-left">
<AiToolEditor
@tools={{this.allTools}}
@model={{this.model}}

View File

@ -2,20 +2,20 @@
module DiscourseAi
module Admin
class AiPersonasController < ::Admin::AdminController
class AiAgentsController < ::Admin::AdminController
requires_plugin ::DiscourseAi::PLUGIN_NAME
before_action :find_ai_persona, only: %i[edit update destroy create_user]
before_action :find_ai_agent, only: %i[edit update destroy create_user]
def index
ai_personas =
AiPersona.ordered.map do |persona|
ai_agents =
AiAgent.ordered.map do |agent|
# we use a special serializer here cause names and descriptions are
# localized for system personas
LocalizedAiPersonaSerializer.new(persona, root: false)
# localized for system agents
LocalizedAiAgentSerializer.new(agent, root: false)
end
tools =
DiscourseAi::Personas::Persona.all_available_tools.map do |tool|
DiscourseAi::Agents::Agent.all_available_tools.map do |tool|
AiToolSerializer.new(tool, root: false)
end
AiTool
@ -36,7 +36,7 @@ module DiscourseAi
allowed_seeded_llm_ids: SiteSetting.ai_bot_allowed_seeded_models_map,
)
render json: {
ai_personas: ai_personas,
ai_agents: ai_agents,
meta: {
tools: tools,
llms: llms,
@ -51,55 +51,55 @@ module DiscourseAi
end
def edit
render json: LocalizedAiPersonaSerializer.new(@ai_persona)
render json: LocalizedAiAgentSerializer.new(@ai_agent)
end
def create
ai_persona = AiPersona.new(ai_persona_params.except(:rag_uploads))
if ai_persona.save
RagDocumentFragment.link_target_and_uploads(ai_persona, attached_upload_ids)
ai_agent = AiAgent.new(ai_agent_params.except(:rag_uploads))
if ai_agent.save
RagDocumentFragment.link_target_and_uploads(ai_agent, attached_upload_ids)
render json: {
ai_persona: LocalizedAiPersonaSerializer.new(ai_persona, root: false),
ai_agent: LocalizedAiAgentSerializer.new(ai_agent, root: false),
},
status: :created
else
render_json_error ai_persona
render_json_error ai_agent
end
end
def create_user
user = @ai_persona.create_user!
user = @ai_agent.create_user!
render json: BasicUserSerializer.new(user, root: "user")
end
def update
if @ai_persona.update(ai_persona_params.except(:rag_uploads))
RagDocumentFragment.update_target_uploads(@ai_persona, attached_upload_ids)
if @ai_agent.update(ai_agent_params.except(:rag_uploads))
RagDocumentFragment.update_target_uploads(@ai_agent, attached_upload_ids)
render json: LocalizedAiPersonaSerializer.new(@ai_persona, root: false)
render json: LocalizedAiAgentSerializer.new(@ai_agent, root: false)
else
render_json_error @ai_persona
render_json_error @ai_agent
end
end
def destroy
if @ai_persona.destroy
if @ai_agent.destroy
head :no_content
else
render_json_error @ai_persona
render_json_error @ai_agent
end
end
def stream_reply
persona =
AiPersona.find_by(name: params[:persona_name]) ||
AiPersona.find_by(id: params[:persona_id])
return render_json_error(I18n.t("discourse_ai.errors.persona_not_found")) if persona.nil?
agent =
AiAgent.find_by(name: params[:agent_name]) ||
AiAgent.find_by(id: params[:agent_id])
return render_json_error(I18n.t("discourse_ai.errors.agent_not_found")) if agent.nil?
return render_json_error(I18n.t("discourse_ai.errors.persona_disabled")) if !persona.enabled
return render_json_error(I18n.t("discourse_ai.errors.agent_disabled")) if !agent.enabled
if persona.default_llm.blank?
if agent.default_llm.blank?
return render_json_error(I18n.t("discourse_ai.errors.no_default_llm"))
end
@ -107,8 +107,8 @@ module DiscourseAi
return render_json_error(I18n.t("discourse_ai.errors.no_query_specified"))
end
if !persona.user_id
return render_json_error(I18n.t("discourse_ai.errors.no_user_for_persona"))
if !agent.user_id
return render_json_error(I18n.t("discourse_ai.errors.no_user_for_agent"))
end
if !params[:username] && !params[:user_unique_id]
@ -142,7 +142,7 @@ module DiscourseAi
DiscourseAi::AiBot::ResponseHttpStreamer.queue_streamed_reply(
io: io,
persona: persona,
agent: agent,
user: user,
topic: topic,
query: params[:query].to_s,
@ -178,17 +178,17 @@ module DiscourseAi
end
end
def find_ai_persona
@ai_persona = AiPersona.find(params[:id])
def find_ai_agent
@ai_agent = AiAgent.find(params[:id])
end
def attached_upload_ids
ai_persona_params[:rag_uploads].to_a.map { |h| h[:id] }
ai_agent_params[:rag_uploads].to_a.map { |h| h[:id] }
end
def ai_persona_params
def ai_agent_params
permitted =
params.require(:ai_persona).permit(
params.require(:ai_agent).permit(
:name,
:description,
:enabled,
@ -209,7 +209,7 @@ module DiscourseAi
:allow_chat_channel_mentions,
:allow_chat_direct_messages,
:allow_topic_mentions,
:allow_personal_messages,
:allow_agentl_messages,
:tool_details,
:forced_tool_count,
:force_default_llm,
@ -217,15 +217,15 @@ module DiscourseAi
rag_uploads: [:id],
)
if tools = params.dig(:ai_persona, :tools)
if tools = params.dig(:ai_agent, :tools)
permitted[:tools] = permit_tools(tools)
end
if response_format = params.dig(:ai_persona, :response_format)
if response_format = params.dig(:ai_agent, :response_format)
permitted[:response_format] = permit_response_format(response_format)
end
if examples = params.dig(:ai_persona, :examples)
if examples = params.dig(:ai_agent, :examples)
permitted[:examples] = permit_examples(examples)
end

View File

@ -17,19 +17,19 @@ module DiscourseAi
private
def serialize_features(features)
features.map { |feature| feature.merge(persona: serialize_persona(feature[:persona])) }
features.map { |feature| feature.merge(agent: serialize_agent(feature[:agent])) }
end
def serialize_feature(feature)
return nil if feature.blank?
feature.merge(persona: serialize_persona(feature[:persona]))
feature.merge(agent: serialize_agent(feature[:agent]))
end
def serialize_persona(persona)
return nil if persona.blank?
def serialize_agent(agent)
return nil if agent.blank?
serialize_data(persona, AiFeaturesPersonaSerializer, root: false)
serialize_data(agent, AiFeaturesAgentSerializer, root: false)
end
end
end

View File

@ -6,8 +6,8 @@ module DiscourseAi
requires_plugin ::DiscourseAi::PLUGIN_NAME
def indexing_status_check
if params[:target_type] == "AiPersona"
@target = AiPersona.find(params[:target_id])
if params[:target_type] == "AiAgent"
@target = AiAgent.find(params[:target_id])
elsif params[:target_type] == "AiTool"
@target = AiTool.find(params[:target_id])
else

View File

@ -46,17 +46,17 @@ module DiscourseAi
end
def discover
ai_persona =
AiPersona
.all_personas(enabled_only: false)
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
ai_agent =
AiAgent
.all_agents(enabled_only: false)
.find { |agent| agent.id == SiteSetting.ai_bot_discover_agent.to_i }
if ai_persona.nil? || !current_user.in_any_groups?(ai_persona.allowed_group_ids.to_a)
if ai_agent.nil? || !current_user.in_any_groups?(ai_agent.allowed_group_ids.to_a)
raise Discourse::InvalidAccess.new
end
if ai_persona.default_llm_id.blank?
render_json_error "Discover persona is missing a default LLM model.", status: 503
if ai_agent.default_llm_id.blank?
render_json_error "Discover agent is missing a default LLM model.", status: 503
return
end
@ -77,7 +77,7 @@ module DiscourseAi
user = User.find(params[:user_id])
bot_user_id = AiPersona.find_by(id: SiteSetting.ai_bot_discover_persona).user_id
bot_user_id = AiAgent.find_by(id: SiteSetting.ai_bot_discover_agent).user_id
bot_username = User.find_by(id: bot_user_id).username
query = params[:query]

View File

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

View File

@ -7,18 +7,18 @@ module ::Jobs
def execute(args)
return unless bot_user = User.find_by(id: args[:bot_user_id])
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
persona_id = args[:persona_id]
agent_id = args[:agent_id]
begin
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id)
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil?
agent = DiscourseAi::Agents::Agent.find_by(user: post.user, id: agent_id)
raise DiscourseAi::Agents::Bot::BOT_NOT_FOUND if agent.nil?
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
bot = DiscourseAi::Agents::Bot.as(bot_user, agent: agent.new)
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
rescue DiscourseAi::Agents::Bot::BOT_NOT_FOUND
Rails.logger.warn(
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
"Bot not found for post #{post.id} - perhaps agent was deleted or bot was disabled",
)
end
end

View File

@ -9,8 +9,8 @@ module Jobs
return unless SiteSetting.ai_discord_search_enabled
if SiteSetting.ai_discord_search_mode == "persona"
DiscourseAi::Discord::Bot::PersonaReplier.new(interaction).handle_interaction!
if SiteSetting.ai_discord_search_mode == "agent"
DiscourseAi::Discord::Bot::AgentReplier.new(interaction).handle_interaction!
else
DiscourseAi::Discord::Bot::Search.new(interaction).handle_interaction!
end

View File

@ -8,20 +8,20 @@ module Jobs
return if (user = User.find_by(id: args[:user_id])).nil?
return if (query = args[:query]).blank?
ai_persona_klass =
AiPersona
.all_personas(enabled_only: false)
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
ai_agent_klass =
AiAgent
.all_agents(enabled_only: false)
.find { |agent| agent.id == SiteSetting.ai_bot_discover_agent.to_i }
if ai_persona_klass.nil? || !user.in_any_groups?(ai_persona_klass.allowed_group_ids.to_a)
if ai_agent_klass.nil? || !user.in_any_groups?(ai_agent_klass.allowed_group_ids.to_a)
return
end
return if (llm_model = LlmModel.find_by(id: ai_persona_klass.default_llm_id)).nil?
return if (llm_model = LlmModel.find_by(id: ai_agent_klass.default_llm_id)).nil?
bot =
DiscourseAi::Personas::Bot.as(
DiscourseAi::Agents::Bot.as(
Discourse.system_user,
persona: ai_persona_klass.new,
agent: ai_agent_klass.new,
model: llm_model,
)
@ -31,7 +31,7 @@ module Jobs
base = { query: query, model_used: llm_model.display_name }
context =
DiscourseAi::Personas::BotContext.new(
DiscourseAi::Agents::BotContext.new(
messages: [{ type: :user, content: query }],
skip_tool_details: true,
)

View File

@ -1,16 +1,16 @@
# frozen_string_literal: true
class AiPersona < ActiveRecord::Base
class AiAgent < ActiveRecord::Base
# TODO remove this line 01-10-2025
self.ignored_columns = %i[default_llm question_consolidator_llm]
# places a hard limit, so per site we cache a maximum of 500 classes
MAX_PERSONAS_PER_SITE = 500
MAX_AGENTS_PER_SITE = 500
validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
validates :description, presence: true, length: { maximum: 2000 }
validates :system_prompt, presence: true, length: { maximum: 10_000_000 }
validate :system_persona_unchangeable, on: :update, if: :system
validate :system_agent_unchangeable, on: :update, if: :system
validate :chat_preconditions
validate :allowed_seeded_model, if: :default_llm_id
validate :well_formated_examples
@ -41,50 +41,50 @@ class AiPersona < ActiveRecord::Base
before_destroy :ensure_not_system
before_update :regenerate_rag_fragments
def self.persona_cache
@persona_cache ||= ::DiscourseAi::MultisiteHash.new("persona_cache")
def self.agent_cache
@agent_cache ||= ::DiscourseAi::MultisiteHash.new("agent_cache")
end
scope :ordered, -> { order("priority DESC, lower(name) ASC") }
def self.all_personas(enabled_only: true)
persona_cache[:value] ||= AiPersona
def self.all_agents(enabled_only: true)
agent_cache[:value] ||= AiAgent
.ordered
.all
.limit(MAX_PERSONAS_PER_SITE)
.limit(MAX_AGENTS_PER_SITE)
.map(&:class_instance)
if enabled_only
persona_cache[:value].select { |p| p.enabled }
agent_cache[:value].select { |p| p.enabled }
else
persona_cache[:value]
agent_cache[:value]
end
end
def self.persona_users(user: nil)
persona_users =
persona_cache[:persona_users] ||= AiPersona
def self.agent_users(user: nil)
agent_users =
agent_cache[:agent_users] ||= AiAgent
.where(enabled: true)
.joins(:user)
.map do |persona|
.map do |agent|
{
id: persona.id,
user_id: persona.user_id,
username: persona.user.username_lower,
allowed_group_ids: persona.allowed_group_ids,
default_llm_id: persona.default_llm_id,
force_default_llm: persona.force_default_llm,
allow_chat_channel_mentions: persona.allow_chat_channel_mentions,
allow_chat_direct_messages: persona.allow_chat_direct_messages,
allow_topic_mentions: persona.allow_topic_mentions,
allow_personal_messages: persona.allow_personal_messages,
id: agent.id,
user_id: agent.user_id,
username: agent.user.username_lower,
allowed_group_ids: agent.allowed_group_ids,
default_llm_id: agent.default_llm_id,
force_default_llm: agent.force_default_llm,
allow_chat_channel_mentions: agent.allow_chat_channel_mentions,
allow_chat_direct_messages: agent.allow_chat_direct_messages,
allow_topic_mentions: agent.allow_topic_mentions,
allow_agentl_messages: agent.allow_agentl_messages,
}
end
if user
persona_users.select { |persona_user| user.in_any_groups?(persona_user[:allowed_group_ids]) }
agent_users.select { |agent_user| user.in_any_groups?(agent_user[:allowed_group_ids]) }
else
persona_users
agent_users
end
end
@ -93,31 +93,31 @@ class AiPersona < ActiveRecord::Base
allow_chat_channel_mentions: false,
allow_chat_direct_messages: false,
allow_topic_mentions: false,
allow_personal_messages: false
allow_agentl_messages: false
)
index =
"modality-#{allow_chat_channel_mentions}-#{allow_chat_direct_messages}-#{allow_topic_mentions}-#{allow_personal_messages}"
"modality-#{allow_chat_channel_mentions}-#{allow_chat_direct_messages}-#{allow_topic_mentions}-#{allow_agentl_messages}"
personas =
persona_cache[index.to_sym] ||= persona_users.select do |persona|
next true if allow_chat_channel_mentions && persona[:allow_chat_channel_mentions]
next true if allow_chat_direct_messages && persona[:allow_chat_direct_messages]
next true if allow_topic_mentions && persona[:allow_topic_mentions]
next true if allow_personal_messages && persona[:allow_personal_messages]
agents =
agent_cache[index.to_sym] ||= agent_users.select do |agent|
next true if allow_chat_channel_mentions && agent[:allow_chat_channel_mentions]
next true if allow_chat_direct_messages && agent[:allow_chat_direct_messages]
next true if allow_topic_mentions && agent[:allow_topic_mentions]
next true if allow_agentl_messages && agent[:allow_agentl_messages]
false
end
if user
personas.select { |u| user.in_any_groups?(u[:allowed_group_ids]) }
agents.select { |u| user.in_any_groups?(u[:allowed_group_ids]) }
else
personas
agents
end
end
after_commit :bump_cache
def bump_cache
self.class.persona_cache.flush!
self.class.agent_cache.flush!
end
def tools_can_not_be_duplicated
@ -138,7 +138,7 @@ class AiPersona < ActiveRecord::Base
end
if seen_tools.include?(inner_name)
errors.add(:tools, I18n.t("discourse_ai.ai_bot.personas.cannot_have_duplicate_tools"))
errors.add(:tools, I18n.t("discourse_ai.ai_bot.agents.cannot_have_duplicate_tools"))
break
else
seen_tools.add(inner_name)
@ -154,7 +154,7 @@ class AiPersona < ActiveRecord::Base
.pluck(:tool_name)
.each do |tool_name|
if builtin_tool_names.include?(tool_name.downcase)
errors.add(:tools, I18n.t("discourse_ai.ai_bot.personas.cannot_have_duplicate_tools"))
errors.add(:tools, I18n.t("discourse_ai.ai_bot.agents.cannot_have_duplicate_tools"))
break
end
end
@ -176,7 +176,7 @@ class AiPersona < ActiveRecord::Base
allow_chat_channel_mentions
allow_chat_direct_messages
allow_topic_mentions
allow_personal_messages
allow_agentl_messages
force_default_llm
name
description
@ -208,14 +208,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::Personas::Tools::Custom.class_instance(custom_tool_id)
klass = DiscourseAi::Agents::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::Personas::Tools::#{inner_name}".constantize
klass = "DiscourseAi::Agents::Tools::#{inner_name}".constantize
options[klass] = current_options if current_options
rescue StandardError
end
@ -225,14 +225,14 @@ class AiPersona < ActiveRecord::Base
klass
end
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id]
if persona_class
agent_class = DiscourseAi::Agents::Agent.system_agents_by_id[self.id]
if agent_class
return(
# we need a new copy so we don't leak information
# across sites
Class.new(persona_class) do
Class.new(agent_class) do
# required for localization
define_singleton_method(:to_s) { persona_class.to_s }
define_singleton_method(:to_s) { agent_class.to_s }
instance_attributes.each do |key, value|
# description/name are localized
define_singleton_method(key) { value } if key != :description && key != :name
@ -242,9 +242,9 @@ class AiPersona < ActiveRecord::Base
)
end
ai_persona_id = self.id
ai_agent_id = self.id
Class.new(DiscourseAi::Personas::Persona) do
Class.new(DiscourseAi::Agents::Agent) do
instance_attributes.each { |key, value| define_singleton_method(key) { value } }
define_singleton_method(:to_s) do
@ -254,24 +254,24 @@ class AiPersona < ActiveRecord::Base
define_singleton_method(:inspect) { to_s }
define_method(:initialize) do |*args, **kwargs|
@ai_persona = AiPersona.find_by(id: ai_persona_id)
@ai_agent = AiAgent.find_by(id: ai_agent_id)
super(*args, **kwargs)
end
define_method(:tools) { tools }
define_method(:force_tool_use) { force_tool_use }
define_method(:forced_tool_count) { @ai_persona&.forced_tool_count }
define_method(:forced_tool_count) { @ai_agent&.forced_tool_count }
define_method(:options) { options }
define_method(:temperature) { @ai_persona&.temperature }
define_method(:top_p) { @ai_persona&.top_p }
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
define_method(:uploads) { @ai_persona&.uploads }
define_method(:response_format) { @ai_persona&.response_format }
define_method(:examples) { @ai_persona&.examples }
define_method(:temperature) { @ai_agent&.temperature }
define_method(:top_p) { @ai_agent&.top_p }
define_method(:system_prompt) { @ai_agent&.system_prompt || "You are a helpful bot." }
define_method(:uploads) { @ai_agent&.uploads }
define_method(:response_format) { @ai_agent&.response_format }
define_method(:examples) { @ai_agent&.examples }
end
end
FIRST_PERSONA_USER_ID = -1200
FIRST_AGENT_USER_ID = -1200
def create_user!
raise "User already exists" if user_id && User.exists?(user_id)
@ -279,7 +279,7 @@ class AiPersona < ActiveRecord::Base
# find the first id smaller than FIRST_USER_ID that is not taken
id = nil
id = DB.query_single(<<~SQL, FIRST_PERSONA_USER_ID, FIRST_PERSONA_USER_ID - 200).first
id = DB.query_single(<<~SQL, FIRST_AGENT_USER_ID, FIRST_AGENT_USER_ID - 200).first
WITH seq AS (
SELECT generate_series(?, ?, -1) AS id
)
@ -323,12 +323,12 @@ class AiPersona < ActiveRecord::Base
allow_chat_channel_mentions || allow_chat_direct_messages || allow_topic_mentions ||
force_default_llm
) && !default_llm_id
errors.add(:default_llm, I18n.t("discourse_ai.ai_bot.personas.default_llm_required"))
errors.add(:default_llm, I18n.t("discourse_ai.ai_bot.agents.default_llm_required"))
end
end
def system_persona_unchangeable
error_msg = I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona")
def system_agent_unchangeable
error_msg = I18n.t("discourse_ai.ai_bot.agents.cannot_edit_system_agent")
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
description_changed?
@ -356,7 +356,7 @@ class AiPersona < ActiveRecord::Base
def ensure_not_system
if system
errors.add(:base, I18n.t("discourse_ai.ai_bot.personas.cannot_delete_system_persona"))
errors.add(:base, I18n.t("discourse_ai.ai_bot.agents.cannot_delete_system_agent"))
throw :abort
end
end
@ -380,13 +380,13 @@ class AiPersona < ActiveRecord::Base
return
end
errors.add(:examples, I18n.t("discourse_ai.personas.malformed_examples"))
errors.add(:examples, I18n.t("discourse_ai.agents.malformed_examples"))
end
end
# == Schema Information
#
# Table name: ai_personas
# Table name: ai_agents
#
# id :bigint not null, primary key
# name :string(100) not null
@ -414,7 +414,7 @@ end
# allow_chat_channel_mentions :boolean default(FALSE), not null
# allow_chat_direct_messages :boolean default(FALSE), not null
# allow_topic_mentions :boolean default(FALSE), not null
# allow_personal_messages :boolean default(TRUE), not null
# allow_agentl_messages :boolean default(TRUE), not null
# force_default_llm :boolean default(FALSE), not null
# rag_llm_model_id :bigint
# default_llm_id :bigint
@ -424,5 +424,5 @@ end
#
# Indexes
#
# index_ai_personas_on_name (name) UNIQUE
# index_ai_agents_on_name (name) UNIQUE
#

View File

@ -36,7 +36,7 @@ class AiTool < ActiveRecord::Base
end
def runner(parameters, llm:, bot_user:, context: nil)
DiscourseAi::Personas::ToolRunner.new(
DiscourseAi::Agents::ToolRunner.new(
parameters: parameters,
llm: llm,
bot_user: bot_user,
@ -45,10 +45,10 @@ class AiTool < ActiveRecord::Base
)
end
after_commit :bump_persona_cache
after_commit :bump_agent_cache
def bump_persona_cache
AiPersona.persona_cache.flush!
def bump_agent_cache
AiAgent.agent_cache.flush!
end
def regenerate_rag_fragments
@ -176,11 +176,11 @@ class AiTool < ActiveRecord::Base
* user_id_or_username (number | string): The ID or username of the user.
* Returns: Object (User details using UserSerializer structure) or null if not found.
*
* discourse.getPersona(name): Gets an object representing another AI Persona configured on the site.
* discourse.getAgent(name): Gets an object representing another AI Agent configured on the site.
* Parameters:
* name (string): The name of the target persona.
* Returns: Object { respondTo: function(params) } or null if persona not found.
* respondTo(params): Instructs the target persona to generate a response within the current context (e.g., replying to the same post or chat message).
* name (string): The name of the target agent.
* Returns: Object { respondTo: function(params) } or null if agent not found.
* respondTo(params): Instructs the target agent to generate a response within the current context (e.g., replying to the same post or chat message).
* Parameters:
* params (Object, optional): { instructions: string, whisper: boolean }
* Returns: { success: boolean, post_id?: number, post_number?: number, message_id?: number } or { error: string }
@ -201,7 +201,7 @@ class AiTool < ActiveRecord::Base
* private_message (boolean): Whether the context is a private message (in Post context).
* message_id (number): ID of the chat message triggering the tool (if in Chat context).
* channel_id (number): ID of the chat channel (if in Chat context).
* user (Object): Details of the user invoking the tool/persona (structure may vary, often null or SystemUser details unless explicitly passed).
* user (Object): Details of the user invoking the tool/agent (structure may vary, often null or SystemUser details unless explicitly passed).
* participants (string): Comma-separated list of usernames in a PM (if applicable).
* // ... other potential context-specific properties added by the calling environment.
*

View File

@ -2,7 +2,7 @@
class RagDocumentFragment < ActiveRecord::Base
# TODO Jan 2025 - remove
self.ignored_columns = %i[ai_persona_id]
self.ignored_columns = %i[ai_agent_id]
belongs_to :upload
belongs_to :target, polymorphic: true
@ -38,7 +38,7 @@ class RagDocumentFragment < ActiveRecord::Base
end
end
def indexing_status(persona, uploads)
def indexing_status(agent, uploads)
embeddings_table = DiscourseAi::Embeddings::Schema.for(self).table
results =
@ -56,8 +56,8 @@ class RagDocumentFragment < ActiveRecord::Base
WHERE uploads.id IN (:upload_ids)
GROUP BY uploads.id
SQL
target_id: persona.id,
target_type: persona.class.to_s,
target_id: agent.id,
target_type: agent.class.to_s,
upload_ids: uploads.map(&:id),
)

View File

@ -51,13 +51,13 @@ class SharedAiConversation < ActiveRecord::Base
# but this name works
class SharedPost
attr_accessor :user
attr_reader :id, :user_id, :created_at, :cooked, :persona
attr_reader :id, :user_id, :created_at, :cooked, :agent
def initialize(post)
@id = post[:id]
@user_id = post[:user_id]
@created_at = DateTime.parse(post[:created_at])
@cooked = post[:cooked]
@persona = post[:persona]
@agent = post[:agent]
end
end
@ -140,9 +140,9 @@ class SharedAiConversation < ActiveRecord::Base
llm_name = ActiveSupport::Inflector.humanize(llm_name) if llm_name
llm_name ||= I18n.t("discourse_ai.unknown_model")
persona = nil
if persona_id = topic.custom_fields["ai_persona_id"]
persona = AiPersona.find_by(id: persona_id.to_i)&.name
agent = nil
if agent_id = topic.custom_fields["ai_agent_id"]
agent = AiAgent.find_by(id: agent_id.to_i)&.name
end
posts =
@ -167,7 +167,7 @@ class SharedAiConversation < ActiveRecord::Base
cooked: cook_artifacts(post),
}
mapped[:persona] = persona if ai_bot_participant&.id == post.user_id
mapped[:agent] = agent if ai_bot_participant&.id == post.user_id
mapped[:username] = post.user&.username if include_usernames
mapped
end,

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class AiFeaturesPersonaSerializer < ApplicationSerializer
class AiFeaturesAgentSerializer < ApplicationSerializer
attributes :id, :name, :system_prompt, :allowed_groups, :enabled
def allowed_groups

View File

@ -2,7 +2,7 @@
class LlmModelSerializer < ApplicationSerializer
# TODO: we probably should rename the table LlmModel to AiLlm
# it is consistent with AiPersona and AiTool
# it is consistent with AiAgent and AiTool
# LLM model is a bit confusing given that large langauge model model is a confusing
# name
root "ai_llm"

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class LocalizedAiPersonaSerializer < ApplicationSerializer
root "ai_persona"
class LocalizedAiAgentSerializer < ApplicationSerializer
root "ai_agent"
attributes :id,
:name,
@ -29,7 +29,7 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer
:allow_chat_channel_mentions,
:allow_chat_direct_messages,
:allow_topic_mentions,
:allow_personal_messages,
:allow_agentl_messages,
:force_default_llm,
:response_format,
:examples

View File

@ -43,8 +43,8 @@
<article class="post">
<header class="post__header">
<span class="post__user"><%= post.user.username %></span>
<%if post.persona.present? %>
<span class="post__persona"><%= post.persona %></span>
<%if post.agent.present? %>
<span class="post__agent"><%= post.agent %></span>
<% end %>
<time class="post__date" datetime="<%= post.created_at.iso8601 %>"><%= post.created_at.strftime('%Y-%m-%d') %></time>
</header>

View File

@ -4,7 +4,7 @@ export default {
path: "/plugins",
map() {
this.route("discourse-ai-personas", { path: "ai-personas" }, function () {
this.route("discourse-ai-agents", { path: "ai-agents" }, function () {
this.route("new");
this.route("edit", { path: "/:id/edit" });
});

View File

@ -16,6 +16,6 @@ export default class Adapter extends RestAdapter {
}
apiNameFor() {
return "ai-persona";
return "ai-agent";
}
}

View File

@ -29,7 +29,7 @@ const CREATE_ATTRIBUTES = [
"allow_chat",
"tool_details",
"forced_tool_count",
"allow_personal_messages",
"allow_agentl_messages",
"allow_topic_mentions",
"allow_chat_channel_mentions",
"allow_chat_direct_messages",
@ -58,16 +58,16 @@ const SYSTEM_ATTRIBUTES = [
"rag_llm_model_id",
"question_consolidator_llm_id",
"tool_details",
"allow_personal_messages",
"allow_agentl_messages",
"allow_topic_mentions",
"allow_chat_channel_mentions",
"allow_chat_direct_messages",
];
export default class AiPersona extends RestModel {
export default class AiAgent extends RestModel {
async createUser() {
const result = await ajax(
`/admin/plugins/discourse-ai/ai-personas/${this.id}/create-user.json`,
`/admin/plugins/discourse-ai/ai-agents/${this.id}/create-user.json`,
{
type: "POST",
}
@ -143,10 +143,10 @@ export default class AiPersona extends RestModel {
fromPOJO(data) {
const dataClone = JSON.parse(JSON.stringify(data));
const persona = AiPersona.create(dataClone);
persona.tools = this.flattenedToolStructure(dataClone);
const agent = AiAgent.create(dataClone);
agent.tools = this.flattenedToolStructure(dataClone);
return persona;
return agent;
}
toPOJO() {

View File

@ -8,8 +8,8 @@ export default class AiFeature extends RestModel {
"ref",
"description",
"enable_setting",
"persona",
"persona_setting"
"agent",
"agent_setting"
);
}
}

View File

@ -15,15 +15,15 @@ import Group from "discourse/models/group";
import { i18n } from "discourse-i18n";
import AdminUser from "admin/models/admin-user";
import GroupChooser from "select-kit/components/group-chooser";
import AiPersonaResponseFormatEditor from "../components/modal/ai-persona-response-format-editor";
import AiAgentResponseFormatEditor from "../components/modal/ai-agent-response-format-editor";
import AiLlmSelector from "./ai-llm-selector";
import AiPersonaCollapsableExample from "./ai-persona-example";
import AiPersonaToolOptions from "./ai-persona-tool-options";
import AiAgentCollapsableExample from "./ai-agent-example";
import AiAgentToolOptions from "./ai-agent-tool-options";
import AiToolSelector from "./ai-tool-selector";
import RagOptionsFk from "./rag-options-fk";
import RagUploader from "./rag-uploader";
export default class PersonaEditor extends Component {
export default class AgentEditor extends Component {
@service router;
@service store;
@service dialog;
@ -59,12 +59,12 @@ export default class PersonaEditor extends Component {
}
get allTools() {
return this.args.personas.resultSetMeta.tools;
return this.args.agents.resultSetMeta.tools;
}
get maxPixelValues() {
const l = (key) =>
i18n(`discourse_ai.ai_persona.vision_max_pixel_sizes.${key}`);
i18n(`discourse_ai.ai_agent.vision_max_pixel_sizes.${key}`);
return [
{ name: l("low"), id: 65536 },
{ name: l("medium"), id: 262144 },
@ -76,14 +76,14 @@ export default class PersonaEditor extends Component {
const content = [
{
id: -1,
name: i18n("discourse_ai.ai_persona.tool_strategies.all"),
name: i18n("discourse_ai.ai_agent.tool_strategies.all"),
},
];
[1, 2, 5].forEach((i) => {
content.push({
id: i,
name: i18n("discourse_ai.ai_persona.tool_strategies.replies", {
name: i18n("discourse_ai.ai_agent.tool_strategies.replies", {
count: i,
}),
});
@ -112,23 +112,23 @@ export default class PersonaEditor extends Component {
this.isSaving = true;
try {
const personaToSave = Object.assign(
const agentToSave = Object.assign(
this.args.model,
this.args.model.fromPOJO(data)
);
await personaToSave.save();
this.#sortPersonas();
await agentToSave.save();
this.#sortAgents();
if (isNew && this.args.model.rag_uploads.length === 0) {
this.args.personas.addObject(personaToSave);
this.args.agents.addObject(agentToSave);
this.router.transitionTo(
"adminPlugins.show.discourse-ai-personas.edit",
personaToSave
"adminPlugins.show.discourse-ai-agents.edit",
agentToSave
);
} else {
this.toasts.success({
data: { message: i18n("discourse_ai.ai_persona.saved") },
data: { message: i18n("discourse_ai.ai_agent.saved") },
duration: 2000,
});
}
@ -151,12 +151,12 @@ export default class PersonaEditor extends Component {
@action
delete() {
return this.dialog.confirm({
message: i18n("discourse_ai.ai_persona.confirm_delete"),
message: i18n("discourse_ai.ai_agent.confirm_delete"),
didConfirm: () => {
return this.args.model.destroyRecord().then(() => {
this.args.personas.removeObject(this.args.model);
this.args.agents.removeObject(this.args.model);
this.router.transitionTo(
"adminPlugins.show.discourse-ai-personas.index"
"adminPlugins.show.discourse-ai-agents.index"
);
});
},
@ -259,7 +259,7 @@ export default class PersonaEditor extends Component {
return updatedOptions;
}
async persistField(dirtyData, field, newValue, sortPersonas) {
async persistField(dirtyData, field, newValue, sortAgents) {
if (!this.args.model.isNew) {
const updatedDirtyData = Object.assign({}, dirtyData);
updatedDirtyData[field] = newValue;
@ -270,8 +270,8 @@ export default class PersonaEditor extends Component {
this.dirtyFormData = updatedDirtyData;
await this.args.model.update(args);
if (sortPersonas) {
this.#sortPersonas();
if (sortAgents) {
this.#sortAgents();
}
} catch (e) {
popupAjaxError(e);
@ -279,8 +279,8 @@ export default class PersonaEditor extends Component {
}
}
#sortPersonas() {
const sorted = this.args.personas.toArray().sort((a, b) => {
#sortAgents() {
const sorted = this.args.agents.toArray().sort((a, b) => {
if (a.priority && !b.priority) {
return -1;
} else if (!a.priority && b.priority) {
@ -289,20 +289,20 @@ export default class PersonaEditor extends Component {
return a.name.localeCompare(b.name);
}
});
this.args.personas.clear();
this.args.personas.setObjects(sorted);
this.args.agents.clear();
this.args.agents.setObjects(sorted);
}
<template>
<BackButton
@route="adminPlugins.show.discourse-ai-personas"
@label="discourse_ai.ai_persona.back"
@route="adminPlugins.show.discourse-ai-agents"
@label="discourse_ai.ai_agent.back"
/>
<div class="ai-persona-editor" {{didInsert this.updateAllGroups @model.id}}>
<div class="ai-agent-editor" {{didInsert this.updateAllGroups @model.id}}>
<Form @onSubmit={{this.save}} @data={{this.formData}} as |form data|>
<form.Field
@name="name"
@title={{i18n "discourse_ai.ai_persona.name"}}
@title={{i18n "discourse_ai.ai_agent.name"}}
@validation="required|length:1,100"
@disabled={{data.system}}
@format="large"
@ -313,7 +313,7 @@ export default class PersonaEditor extends Component {
<form.Field
@name="description"
@title={{i18n "discourse_ai.ai_persona.description"}}
@title={{i18n "discourse_ai.ai_agent.description"}}
@validation="required|length:1,100"
@disabled={{data.system}}
@format="large"
@ -324,7 +324,7 @@ export default class PersonaEditor extends Component {
<form.Field
@name="system_prompt"
@title={{i18n "discourse_ai.ai_persona.system_prompt"}}
@title={{i18n "discourse_ai.ai_agent.system_prompt"}}
@validation="required|length:1,100000"
@disabled={{data.system}}
@format="large"
@ -333,28 +333,28 @@ export default class PersonaEditor extends Component {
<field.Textarea />
</form.Field>
<AiPersonaResponseFormatEditor @form={{form}} @data={{data}} />
<AiAgentResponseFormatEditor @form={{form}} @data={{data}} />
<form.Field
@name="default_llm_id"
@title={{i18n "discourse_ai.ai_persona.default_llm"}}
@tooltip={{i18n "discourse_ai.ai_persona.default_llm_help"}}
@title={{i18n "discourse_ai.ai_agent.default_llm"}}
@tooltip={{i18n "discourse_ai.ai_agent.default_llm_help"}}
@format="large"
as |field|
>
<field.Custom>
<AiLlmSelector
@value={{field.value}}
@llms={{@personas.resultSetMeta.llms}}
@llms={{@agents.resultSetMeta.llms}}
@onChange={{field.set}}
@class="ai-persona-editor__llms"
@class="ai-agent-editor__llms"
/>
</field.Custom>
</form.Field>
<form.Field
@name="allowed_group_ids"
@title={{i18n "discourse_ai.ai_persona.allowed_groups"}}
@title={{i18n "discourse_ai.ai_agent.allowed_groups"}}
@format="large"
as |field|
>
@ -369,8 +369,8 @@ export default class PersonaEditor extends Component {
<form.Field
@name="vision_enabled"
@title={{i18n "discourse_ai.ai_persona.vision_enabled"}}
@tooltip={{i18n "discourse_ai.ai_persona.vision_enabled_help"}}
@title={{i18n "discourse_ai.ai_agent.vision_enabled"}}
@tooltip={{i18n "discourse_ai.ai_agent.vision_enabled_help"}}
@format="large"
as |field|
>
@ -380,7 +380,7 @@ export default class PersonaEditor extends Component {
{{#if data.vision_enabled}}
<form.Field
@name="vision_max_pixels"
@title={{i18n "discourse_ai.ai_persona.vision_max_pixels"}}
@title={{i18n "discourse_ai.ai_agent.vision_max_pixels"}}
@onSet={{this.onChangeMaxPixels}}
@format="large"
as |field|
@ -397,8 +397,8 @@ export default class PersonaEditor extends Component {
<form.Field
@name="max_context_posts"
@title={{i18n "discourse_ai.ai_persona.max_context_posts"}}
@tooltip={{i18n "discourse_ai.ai_persona.max_context_posts_help"}}
@title={{i18n "discourse_ai.ai_agent.max_context_posts"}}
@tooltip={{i18n "discourse_ai.ai_agent.max_context_posts_help"}}
@format="large"
as |field|
>
@ -408,8 +408,8 @@ export default class PersonaEditor extends Component {
{{#unless data.system}}
<form.Field
@name="temperature"
@title={{i18n "discourse_ai.ai_persona.temperature"}}
@tooltip={{i18n "discourse_ai.ai_persona.temperature_help"}}
@title={{i18n "discourse_ai.ai_agent.temperature"}}
@tooltip={{i18n "discourse_ai.ai_agent.temperature_help"}}
@disabled={{data.system}}
@format="large"
as |field|
@ -419,8 +419,8 @@ export default class PersonaEditor extends Component {
<form.Field
@name="top_p"
@title={{i18n "discourse_ai.ai_persona.top_p"}}
@tooltip={{i18n "discourse_ai.ai_persona.top_p_help"}}
@title={{i18n "discourse_ai.ai_agent.top_p"}}
@tooltip={{i18n "discourse_ai.ai_agent.top_p_help"}}
@disabled={{data.system}}
@format="large"
as |field|
@ -430,22 +430,22 @@ export default class PersonaEditor extends Component {
{{/unless}}
<form.Section
@title={{i18n "discourse_ai.ai_persona.examples.title"}}
@subtitle={{i18n "discourse_ai.ai_persona.examples.examples_help"}}
@title={{i18n "discourse_ai.ai_agent.examples.title"}}
@subtitle={{i18n "discourse_ai.ai_agent.examples.examples_help"}}
>
{{#unless data.system}}
<form.Container>
<form.Button
@action={{fn this.addExamplesPair form data}}
@label="discourse_ai.ai_persona.examples.new"
class="ai-persona-editor__new_example"
@label="discourse_ai.ai_agent.examples.new"
class="ai-agent-editor__new_example"
/>
</form.Container>
{{/unless}}
{{#if (gt data.examples.length 0)}}
<form.Collection @name="examples" as |exCollection exCollectionIdx|>
<AiPersonaCollapsableExample
<AiAgentCollapsableExample
@examplesCollection={{exCollection}}
@exampleNumber={{exCollectionIdx}}
@system={{data.system}}
@ -455,10 +455,10 @@ export default class PersonaEditor extends Component {
{{/if}}
</form.Section>
<form.Section @title={{i18n "discourse_ai.ai_persona.ai_tools"}}>
<form.Section @title={{i18n "discourse_ai.ai_agent.ai_tools"}}>
<form.Field
@name="tools"
@title={{i18n "discourse_ai.ai_persona.tools"}}
@title={{i18n "discourse_ai.ai_agent.tools"}}
@format="large"
as |field|
>
@ -467,7 +467,7 @@ export default class PersonaEditor extends Component {
@value={{field.value}}
@disabled={{data.system}}
@onChange={{fn this.updateToolNames form data}}
@content={{@personas.resultSetMeta.tools}}
@content={{@agents.resultSetMeta.tools}}
/>
</field.Custom>
</form.Field>
@ -475,7 +475,7 @@ export default class PersonaEditor extends Component {
{{#if (gt data.tools.length 0)}}
<form.Field
@name="forcedTools"
@title={{i18n "discourse_ai.ai_persona.forced_tools"}}
@title={{i18n "discourse_ai.ai_agent.forced_tools"}}
@format="large"
as |field|
>
@ -493,7 +493,7 @@ export default class PersonaEditor extends Component {
{{#if (gt data.forcedTools.length 0)}}
<form.Field
@name="forced_tool_count"
@title={{i18n "discourse_ai.ai_persona.forced_tool_strategy"}}
@title={{i18n "discourse_ai.ai_agent.forced_tool_strategy"}}
@format="large"
as |field|
>
@ -508,19 +508,19 @@ export default class PersonaEditor extends Component {
{{#if (gt data.tools.length 0)}}
<form.Field
@name="tool_details"
@title={{i18n "discourse_ai.ai_persona.tool_details"}}
@tooltip={{i18n "discourse_ai.ai_persona.tool_details_help"}}
@title={{i18n "discourse_ai.ai_agent.tool_details"}}
@tooltip={{i18n "discourse_ai.ai_agent.tool_details_help"}}
@format="large"
as |field|
>
<field.Checkbox />
</form.Field>
<AiPersonaToolOptions
<AiAgentToolOptions
@form={{form}}
@data={{data}}
@llms={{@personas.resultSetMeta.llms}}
@allTools={{@personas.resultSetMeta.tools}}
@llms={{@agents.resultSetMeta.llms}}
@allTools={{@agents.resultSetMeta.tools}}
/>
{{/if}}
</form.Section>
@ -535,10 +535,10 @@ export default class PersonaEditor extends Component {
<field.Custom>
<RagUploader
@target={{data}}
@targetName="AiPersona"
@targetName="AiAgent"
@updateUploads={{fn this.updateUploads form}}
@onRemove={{fn this.removeUpload form data field.value}}
@allowImages={{@personas.resultSetMeta.settings.rag_images_enabled}}
@allowImages={{@agents.resultSetMeta.settings.rag_images_enabled}}
/>
</field.Custom>
</form.Field>
@ -546,16 +546,16 @@ export default class PersonaEditor extends Component {
<RagOptionsFk
@form={{form}}
@data={{data}}
@llms={{@personas.resultSetMeta.llms}}
@allowImages={{@personas.resultSetMeta.settings.rag_images_enabled}}
@llms={{@agents.resultSetMeta.llms}}
@allowImages={{@agents.resultSetMeta.settings.rag_images_enabled}}
>
<form.Field
@name="rag_conversation_chunks"
@title={{i18n
"discourse_ai.ai_persona.rag_conversation_chunks"
"discourse_ai.ai_agent.rag_conversation_chunks"
}}
@tooltip={{i18n
"discourse_ai.ai_persona.rag_conversation_chunks_help"
"discourse_ai.ai_agent.rag_conversation_chunks_help"
}}
@format="large"
as |field|
@ -566,10 +566,10 @@ export default class PersonaEditor extends Component {
<form.Field
@name="question_consolidator_llm_id"
@title={{i18n
"discourse_ai.ai_persona.question_consolidator_llm"
"discourse_ai.ai_agent.question_consolidator_llm"
}}
@tooltip={{i18n
"discourse_ai.ai_persona.question_consolidator_llm_help"
"discourse_ai.ai_agent.question_consolidator_llm_help"
}}
@format="large"
as |field|
@ -577,9 +577,9 @@ export default class PersonaEditor extends Component {
<field.Custom>
<AiLlmSelector
@value={{field.value}}
@llms={{@personas.resultSetMeta.llms}}
@llms={{@agents.resultSetMeta.llms}}
@onChange={{field.set}}
@class="ai-persona-editor__llms"
@class="ai-agent-editor__llms"
/>
</field.Custom>
</form.Field>
@ -587,10 +587,10 @@ export default class PersonaEditor extends Component {
</form.Section>
{{/if}}
<form.Section @title={{i18n "discourse_ai.ai_persona.ai_bot.title"}}>
<form.Section @title={{i18n "discourse_ai.ai_agent.ai_bot.title"}}>
<form.Field
@name="enabled"
@title={{i18n "discourse_ai.ai_persona.enabled"}}
@title={{i18n "discourse_ai.ai_agent.enabled"}}
@onSet={{fn this.toggleEnabled data}}
as |field|
>
@ -599,21 +599,21 @@ export default class PersonaEditor extends Component {
<form.Field
@name="priority"
@title={{i18n "discourse_ai.ai_persona.priority"}}
@title={{i18n "discourse_ai.ai_agent.priority"}}
@onSet={{fn this.togglePriority data}}
@tooltip={{i18n "discourse_ai.ai_persona.priority_help"}}
@tooltip={{i18n "discourse_ai.ai_agent.priority_help"}}
as |field|
>
<field.Toggle />
</form.Field>
{{#if @model.isNew}}
<div>{{i18n "discourse_ai.ai_persona.ai_bot.save_first"}}</div>
<div>{{i18n "discourse_ai.ai_agent.ai_bot.save_first"}}</div>
{{else}}
{{#if data.default_llm_id}}
<form.Field
@name="force_default_llm"
@title={{i18n "discourse_ai.ai_persona.force_default_llm"}}
@title={{i18n "discourse_ai.ai_agent.force_default_llm"}}
@format="large"
as |field|
>
@ -622,12 +622,12 @@ export default class PersonaEditor extends Component {
{{/if}}
<form.Container
@title={{i18n "discourse_ai.ai_persona.user"}}
@title={{i18n "discourse_ai.ai_agent.user"}}
@tooltip={{unless
data.user
(i18n "discourse_ai.ai_persona.create_user_help")
(i18n "discourse_ai.ai_agent.create_user_help")
}}
class="ai-persona-editor__ai_bot_user"
class="ai-agent-editor__ai_bot_user"
>
{{#if data.user}}
<a
@ -643,20 +643,20 @@ export default class PersonaEditor extends Component {
{{else}}
<form.Button
@action={{fn this.createUser form}}
@label="discourse_ai.ai_persona.create_user"
class="ai-persona-editor__create-user"
@label="discourse_ai.ai_agent.create_user"
class="ai-agent-editor__create-user"
/>
{{/if}}
</form.Container>
{{#if data.user}}
<form.Field
@name="allow_personal_messages"
@name="allow_agentl_messages"
@title={{i18n
"discourse_ai.ai_persona.allow_personal_messages"
"discourse_ai.ai_agent.allow_agentl_messages"
}}
@tooltip={{i18n
"discourse_ai.ai_persona.allow_personal_messages_help"
"discourse_ai.ai_agent.allow_agentl_messages_help"
}}
@format="large"
as |field|
@ -666,9 +666,9 @@ export default class PersonaEditor extends Component {
<form.Field
@name="allow_topic_mentions"
@title={{i18n "discourse_ai.ai_persona.allow_topic_mentions"}}
@title={{i18n "discourse_ai.ai_agent.allow_topic_mentions"}}
@tooltip={{i18n
"discourse_ai.ai_persona.allow_topic_mentions_help"
"discourse_ai.ai_agent.allow_topic_mentions_help"
}}
@format="large"
as |field|
@ -680,10 +680,10 @@ export default class PersonaEditor extends Component {
<form.Field
@name="allow_chat_direct_messages"
@title={{i18n
"discourse_ai.ai_persona.allow_chat_direct_messages"
"discourse_ai.ai_agent.allow_chat_direct_messages"
}}
@tooltip={{i18n
"discourse_ai.ai_persona.allow_chat_direct_messages_help"
"discourse_ai.ai_agent.allow_chat_direct_messages_help"
}}
@format="large"
as |field|
@ -694,10 +694,10 @@ export default class PersonaEditor extends Component {
<form.Field
@name="allow_chat_channel_mentions"
@title={{i18n
"discourse_ai.ai_persona.allow_chat_channel_mentions"
"discourse_ai.ai_agent.allow_chat_channel_mentions"
}}
@tooltip={{i18n
"discourse_ai.ai_persona.allow_chat_channel_mentions_help"
"discourse_ai.ai_agent.allow_chat_channel_mentions_help"
}}
@format="large"
as |field|
@ -715,7 +715,7 @@ export default class PersonaEditor extends Component {
{{#unless (or @model.isNew @model.system)}}
<form.Button
@action={{this.delete}}
@label="discourse_ai.ai_persona.delete"
@label="discourse_ai.ai_agent.delete"
class="btn-danger"
/>
{{/unless}}

View File

@ -7,7 +7,7 @@ import { eq } from "truth-helpers";
import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n";
export default class AiPersonaCollapsableExample extends Component {
export default class AiAgentCollapsableExample extends Component {
@tracked collapsed = true;
get caretIcon() {
@ -26,7 +26,7 @@ export default class AiPersonaCollapsableExample extends Component {
}
get exampleTitle() {
return i18n("discourse_ai.ai_persona.examples.collapsable_title", {
return i18n("discourse_ai.ai_agent.examples.collapsable_title", {
number: this.args.exampleNumber + 1,
});
}
@ -41,7 +41,7 @@ export default class AiPersonaCollapsableExample extends Component {
<exPair.Field
@title={{i18n
(concat
"discourse_ai.ai_persona.examples."
"discourse_ai.ai_agent.examples."
(if (eq pairIdx 0) "user" "model")
)
}}
@ -57,8 +57,8 @@ export default class AiPersonaCollapsableExample extends Component {
<@form.Container>
<@form.Button
@action={{this.deletePair}}
@label="discourse_ai.ai_persona.examples.remove"
class="ai-persona-editor__delete_example btn-danger"
@label="discourse_ai.ai_agent.examples.remove"
class="ai-agent-editor__delete_example btn-danger"
/>
</@form.Container>
{{/unless}}

View File

@ -0,0 +1,117 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DPageSubheader from "discourse/components/d-page-subheader";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import concatClass from "discourse/helpers/concat-class";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
import AiAgentEditor from "./ai-agent-editor";
export default class AiAgentListEditor extends Component {
@service adminPluginNavManager;
@action
async toggleEnabled(agent) {
const oldValue = agent.enabled;
const newValue = !oldValue;
try {
agent.set("enabled", newValue);
await agent.save();
} catch (err) {
agent.set("enabled", oldValue);
popupAjaxError(err);
}
}
<template>
<DBreadcrumbsItem
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-agents"
@label={{i18n "discourse_ai.ai_agent.short_title"}}
/>
<section class="ai-agent-list-editor__current admin-detail pull-left">
{{#if @currentAgent}}
<AiAgentEditor @model={{@currentAgent}} @agents={{@agents}} />
{{else}}
<DPageSubheader
@titleLabel={{i18n "discourse_ai.ai_agent.short_title"}}
@descriptionLabel={{i18n
"discourse_ai.ai_agent.agent_description"
}}
@learnMoreUrl="https://meta.discourse.org/t/ai-bot-agents/306099"
>
<:actions as |actions|>
<actions.Primary
@label="discourse_ai.ai_agent.new"
@route="adminPlugins.show.discourse-ai-agents.new"
@icon="plus"
class="ai-agent-list-editor__new-button"
/>
</:actions>
</DPageSubheader>
{{#if @agents}}
<table class="content-list ai-agent-list-editor d-admin-table">
<thead>
<tr>
<th>{{i18n "discourse_ai.ai_agent.name"}}</th>
<th>{{i18n "discourse_ai.ai_agent.list.enabled"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each @agents as |agent|}}
<tr
data-agent-id={{agent.id}}
class={{concatClass
"ai-agent-list__row d-admin-row__content"
(if agent.priority "priority")
}}
>
<td class="d-admin-row__overview">
<div class="ai-agent-list__name-with-description">
<div class="ai-agent-list__name">
<strong>
{{agent.name}}
</strong>
</div>
<div class="ai-agent-list__description">
{{agent.description}}
</div>
</div>
</td>
<td class="d-admin-row__detail">
<DToggleSwitch
@state={{agent.enabled}}
{{on "click" (fn this.toggleEnabled agent)}}
/>
</td>
<td class="d-admin-row__controls">
<LinkTo
@route="adminPlugins.show.discourse-ai-agents.edit"
@model={{agent}}
class="btn btn-text btn-small"
>{{i18n "discourse_ai.ai_agent.edit"}} </LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<AdminConfigAreaEmptyList
@ctaLabel="discourse_ai.ai_agent.new"
@ctaRoute="adminPlugins.show.discourse-ai-agents.new"
@ctaClass="ai-agent-list-editor__empty-new-button"
@emptyLabel="discourse_ai.ai_agent.no_agents"
/>
{{/if}}
{{/if}}
</section>
</template>
}

View File

@ -6,10 +6,10 @@ import { service } from "@ember/service";
import { i18n } from "discourse-i18n";
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
const PERSONA_SELECTOR_KEY = "ai_persona_selector_id";
const AGENT_SELECTOR_KEY = "ai_agent_selector_id";
const LLM_SELECTOR_KEY = "ai_llm_selector_id";
export default class AiPersonaLlmSelector extends Component {
export default class AiAgentLlmSelector extends Component {
@service currentUser;
@service keyValueStore;
@ -20,7 +20,7 @@ export default class AiPersonaLlmSelector extends Component {
super(...arguments);
if (this.botOptions?.length) {
this.#loadStoredPersona();
this.#loadStoredAgent();
this.#loadStoredLlm();
next(() => {
@ -34,25 +34,25 @@ export default class AiPersonaLlmSelector extends Component {
}
get hasLlmSelector() {
return this.currentUser.ai_enabled_chat_bots.any((bot) => !bot.is_persona);
return this.currentUser.ai_enabled_chat_bots.any((bot) => !bot.is_agent);
}
get botOptions() {
if (!this.currentUser.ai_enabled_personas) {
if (!this.currentUser.ai_enabled_agents) {
return;
}
let enabledPersonas = this.currentUser.ai_enabled_personas;
let enabledAgents = this.currentUser.ai_enabled_agents;
if (!this.hasLlmSelector) {
enabledPersonas = enabledPersonas.filter((persona) => persona.username);
enabledAgents = enabledAgents.filter((agent) => agent.username);
}
return enabledPersonas.map((persona) => {
return enabledAgents.map((agent) => {
return {
id: persona.id,
name: persona.name,
description: persona.description,
id: agent.id,
name: agent.name,
description: agent.description,
};
});
}
@ -67,8 +67,8 @@ export default class AiPersonaLlmSelector extends Component {
set value(newValue) {
this._value = newValue;
this.keyValueStore.setItem(PERSONA_SELECTOR_KEY, newValue);
this.args.setPersonaId(newValue);
this.keyValueStore.setItem(AGENT_SELECTOR_KEY, newValue);
this.args.setAgentId(newValue);
this.setAllowLLMSelector();
this.resetTargetRecipients();
}
@ -79,11 +79,11 @@ export default class AiPersonaLlmSelector extends Component {
return;
}
const persona = this.currentUser.ai_enabled_personas.find(
(innerPersona) => innerPersona.id === this._value
const agent = this.currentUser.ai_enabled_agents.find(
(innerAgent) => innerAgent.id === this._value
);
this.allowLLMSelector = !persona?.force_default_llm;
this.allowLLMSelector = !agent?.force_default_llm;
}
get currentLlm() {
@ -104,16 +104,16 @@ export default class AiPersonaLlmSelector extends Component {
).username;
this.args.setTargetRecipient(botUsername);
} else {
const persona = this.currentUser.ai_enabled_personas.find(
(innerPersona) => innerPersona.id === this._value
const agent = this.currentUser.ai_enabled_agents.find(
(innerAgent) => innerAgent.id === this._value
);
this.args.setTargetRecipient(persona.username || "");
this.args.setTargetRecipient(agent.username || "");
}
}
get llmOptions() {
const availableBots = this.currentUser.ai_enabled_chat_bots
.filter((bot) => !bot.is_persona)
.filter((bot) => !bot.is_agent)
.filter(Boolean);
return availableBots
@ -130,18 +130,18 @@ export default class AiPersonaLlmSelector extends Component {
return this.allowLLMSelector && this.llmOptions.length > 1;
}
#loadStoredPersona() {
let personaId = this.keyValueStore.getItem(PERSONA_SELECTOR_KEY);
#loadStoredAgent() {
let agentId = this.keyValueStore.getItem(AGENT_SELECTOR_KEY);
this._value = this.botOptions[0].id;
if (personaId) {
personaId = parseInt(personaId, 10);
if (this.botOptions.any((bot) => bot.id === personaId)) {
this._value = personaId;
if (agentId) {
agentId = parseInt(agentId, 10);
if (this.botOptions.any((bot) => bot.id === agentId)) {
this._value = agentId;
}
}
this.args.setPersonaId(this._value);
this.args.setAgentId(this._value);
}
#loadStoredLlm() {
@ -172,13 +172,13 @@ export default class AiPersonaLlmSelector extends Component {
}
<template>
<div class="persona-llm-selector">
<div class="persona-llm-selector__selection-wrapper gpt-persona">
<div class="agent-llm-selector">
<div class="agent-llm-selector__selection-wrapper gpt-agent">
{{#if @showLabels}}
<label>{{i18n "discourse_ai.ai_bot.persona"}}</label>
<label>{{i18n "discourse_ai.ai_bot.agent"}}</label>
{{/if}}
<DropdownSelectBox
class="persona-llm-selector__persona-dropdown"
class="agent-llm-selector__agent-dropdown"
@value={{this.value}}
@content={{this.botOptions}}
@options={{hash
@ -188,12 +188,12 @@ export default class AiPersonaLlmSelector extends Component {
/>
</div>
{{#if this.showLLMSelector}}
<div class="persona-llm-selector__selection-wrapper llm-selector">
<div class="agent-llm-selector__selection-wrapper llm-selector">
{{#if @showLabels}}
<label>{{i18n "discourse_ai.ai_bot.llm"}}</label>
{{/if}}
<DropdownSelectBox
class="persona-llm-selector__llm-dropdown"
class="agent-llm-selector__llm-dropdown"
@value={{this.currentLlm}}
@content={{this.llmOptions}}
@options={{hash icon=(if @showLabels "angle-down" "globe")}}

View File

@ -4,7 +4,7 @@ import { eq } from "truth-helpers";
import { i18n } from "discourse-i18n";
import AiLlmSelector from "./ai-llm-selector";
export default class AiPersonaToolOptions extends Component {
export default class AiAgentToolOptions extends Component {
get showToolOptions() {
const allTools = this.args.allTools;
if (!allTools || !this.args.data.tools) {
@ -36,19 +36,19 @@ export default class AiPersonaToolOptions extends Component {
<template>
{{#if this.showToolOptions}}
<@form.Container
@title={{i18n "discourse_ai.ai_persona.tool_options"}}
@title={{i18n "discourse_ai.ai_agent.tool_options"}}
@direction="column"
@format="full"
>
<@form.Object
@name="toolOptions"
@title={{i18n "discourse_ai.ai_persona.tool_options"}}
@title={{i18n "discourse_ai.ai_agent.tool_options"}}
as |toolObj optsPerTool|
>
{{#each (this.formObjectKeys optsPerTool) as |toolId|}}
<div class="ai-persona-editor__tool-options">
<div class="ai-agent-editor__tool-options">
{{#let (get this.toolsMetadata toolId) as |toolMeta|}}
<div class="ai-persona-editor__tool-options-name">
<div class="ai-agent-editor__tool-options-name">
{{toolMeta.name}}
</div>
<toolObj.Object @name={{toolId}} as |optionsObj optionData|>
@ -73,7 +73,7 @@ export default class AiPersonaToolOptions extends Component {
@value={{field.value}}
@llms={{@llms}}
@onChange={{field.set}}
@class="ai-persona-tool-option-editor__llms"
@class="ai-agent-tool-option-editor__llms"
/>
</field.Custom>
{{else if (eq optionMeta.type "boolean")}}

View File

@ -23,7 +23,7 @@ import {
} from "discourse/lib/user-status-on-autocomplete";
import { clipboardHelpers } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";
import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-persona-llm-selector";
import AiAgentLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-agent-llm-selector";
export default class AiBotConversations extends Component {
@service aiBotConversationsHiddenSubmit;
@ -133,8 +133,8 @@ export default class AiBotConversations extends Component {
}
@action
setPersonaId(id) {
this.aiBotConversationsHiddenSubmit.personaId = id;
setAgentId(id) {
this.aiBotConversationsHiddenSubmit.agentId = id;
}
@action
@ -279,9 +279,9 @@ export default class AiBotConversations extends Component {
<template>
<div class="ai-bot-conversations">
<AiPersonaLlmSelector
<AiAgentLlmSelector
@showLabels={{true}}
@setPersonaId={{this.setPersonaId}}
@setAgentId={{this.setAgentId}}
@setTargetRecipient={{this.setTargetRecipient}}
/>

View File

@ -116,7 +116,7 @@ export default class AiLlmEditorForm extends Component {
const localized = usedBy.map((m) => {
return i18n(`discourse_ai.llms.usage.${m.type}`, {
persona: m.name,
agent: m.name,
});
});

View File

@ -8,7 +8,7 @@ const AiLlmSelector = <template>
@onChange={{@onChange}}
@options={{hash
filterable=true
none="discourse_ai.ai_persona.no_llm_selected"
none="discourse_ai.ai_agent.no_llm_selected"
}}
class={{@class}}
/>

View File

@ -112,9 +112,9 @@ export default class AiLlmsListEditor extends Component {
}
localizeUsage(usage) {
if (usage.type === "ai_persona") {
return i18n("discourse_ai.llms.usage.ai_persona", {
persona: usage.name,
if (usage.type === "ai_agent") {
return i18n("discourse_ai.llms.usage.ai_agent", {
agent: usage.name,
});
} else if (usage.type === "automation") {
return i18n("discourse_ai.llms.usage.automation", {

View File

@ -1,117 +0,0 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DPageSubheader from "discourse/components/d-page-subheader";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import concatClass from "discourse/helpers/concat-class";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
import AiPersonaEditor from "./ai-persona-editor";
export default class AiPersonaListEditor extends Component {
@service adminPluginNavManager;
@action
async toggleEnabled(persona) {
const oldValue = persona.enabled;
const newValue = !oldValue;
try {
persona.set("enabled", newValue);
await persona.save();
} catch (err) {
persona.set("enabled", oldValue);
popupAjaxError(err);
}
}
<template>
<DBreadcrumbsItem
@path="/admin/plugins/{{this.adminPluginNavManager.currentPlugin.name}}/ai-personas"
@label={{i18n "discourse_ai.ai_persona.short_title"}}
/>
<section class="ai-persona-list-editor__current admin-detail pull-left">
{{#if @currentPersona}}
<AiPersonaEditor @model={{@currentPersona}} @personas={{@personas}} />
{{else}}
<DPageSubheader
@titleLabel={{i18n "discourse_ai.ai_persona.short_title"}}
@descriptionLabel={{i18n
"discourse_ai.ai_persona.persona_description"
}}
@learnMoreUrl="https://meta.discourse.org/t/ai-bot-personas/306099"
>
<:actions as |actions|>
<actions.Primary
@label="discourse_ai.ai_persona.new"
@route="adminPlugins.show.discourse-ai-personas.new"
@icon="plus"
class="ai-persona-list-editor__new-button"
/>
</:actions>
</DPageSubheader>
{{#if @personas}}
<table class="content-list ai-persona-list-editor d-admin-table">
<thead>
<tr>
<th>{{i18n "discourse_ai.ai_persona.name"}}</th>
<th>{{i18n "discourse_ai.ai_persona.list.enabled"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each @personas as |persona|}}
<tr
data-persona-id={{persona.id}}
class={{concatClass
"ai-persona-list__row d-admin-row__content"
(if persona.priority "priority")
}}
>
<td class="d-admin-row__overview">
<div class="ai-persona-list__name-with-description">
<div class="ai-persona-list__name">
<strong>
{{persona.name}}
</strong>
</div>
<div class="ai-persona-list__description">
{{persona.description}}
</div>
</div>
</td>
<td class="d-admin-row__detail">
<DToggleSwitch
@state={{persona.enabled}}
{{on "click" (fn this.toggleEnabled persona)}}
/>
</td>
<td class="d-admin-row__controls">
<LinkTo
@route="adminPlugins.show.discourse-ai-personas.edit"
@model={{persona}}
class="btn btn-text btn-small"
>{{i18n "discourse_ai.ai_persona.edit"}} </LinkTo>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<AdminConfigAreaEmptyList
@ctaLabel="discourse_ai.ai_persona.new"
@ctaRoute="adminPlugins.show.discourse-ai-personas.new"
@ctaClass="ai-persona-list-editor__empty-new-button"
@emptyLabel="discourse_ai.ai_persona.no_personas"
/>
{{/if}}
{{/if}}
</section>
</template>
}

View File

@ -154,8 +154,8 @@ export default class AiSearchDiscoveries extends Component {
}
get canContinueConversation() {
const personas = this.currentUser?.ai_enabled_personas;
if (!personas) {
const agents = this.currentUser?.ai_enabled_agents;
if (!agents) {
return false;
}
@ -163,16 +163,16 @@ export default class AiSearchDiscoveries extends Component {
return false;
}
const discoverPersona = personas.find(
(persona) =>
persona.id === parseInt(this.siteSettings?.ai_bot_discover_persona, 10)
const discoverAgent = agents.find(
(agent) =>
agent.id === parseInt(this.siteSettings?.ai_bot_discover_agent, 10)
);
const discoverPersonaHasBot = discoverPersona?.username;
const discoverAgentHasBot = discoverAgent?.username;
return (
this.discobotDiscoveries.discovery?.length > 0 &&
!this.smoothStreamer.isStreaming &&
discoverPersonaHasBot
discoverAgentHasBot
);
}

View File

@ -7,16 +7,16 @@ import ModalJsonSchemaEditor from "discourse/components/modal/json-schema-editor
import { prettyJSON } from "discourse/lib/formatter";
import { i18n } from "discourse-i18n";
export default class AiPersonaResponseFormatEditor extends Component {
export default class AiAgentResponseFormatEditor extends Component {
@tracked showJsonEditorModal = false;
jsonSchema = {
type: "array",
uniqueItems: true,
title: i18n("discourse_ai.ai_persona.response_format.modal.root_title"),
title: i18n("discourse_ai.ai_agent.response_format.modal.root_title"),
items: {
type: "object",
title: i18n("discourse_ai.ai_persona.response_format.modal.key_title"),
title: i18n("discourse_ai.ai_agent.response_format.modal.key_title"),
properties: {
key: {
type: "string",
@ -30,7 +30,7 @@ export default class AiPersonaResponseFormatEditor extends Component {
};
get editorTitle() {
return i18n("discourse_ai.ai_persona.response_format.title");
return i18n("discourse_ai.ai_agent.response_format.title");
}
get responseFormatAsJSON() {
@ -64,21 +64,21 @@ export default class AiPersonaResponseFormatEditor extends Component {
<template>
<@form.Container @title={{this.editorTitle}} @format="large">
<div class="ai-persona-editor__response-format">
<div class="ai-agent-editor__response-format">
{{#if (gt @data.response_format.length 0)}}
<pre class="ai-persona-editor__response-format-pre">
<pre class="ai-agent-editor__response-format-pre">
<code
>{{this.displayJSON}}</code>
</pre>
{{else}}
<div class="ai-persona-editor__response-format-none">
{{i18n "discourse_ai.ai_persona.response_format.no_format"}}
<div class="ai-agent-editor__response-format-none">
{{i18n "discourse_ai.ai_agent.response_format.no_format"}}
</div>
{{/if}}
<@form.Button
@action={{this.openModal}}
@label="discourse_ai.ai_persona.response_format.open_modal"
@label="discourse_ai.ai_agent.response_format.open_modal"
@disabled={{@data.system}}
/>
</div>

View File

@ -1,14 +1,14 @@
import Component from "@glimmer/component";
import { isGPTBot } from "../../lib/ai-bot-helper";
export default class AiPersonaFlair extends Component {
export default class AiAgentFlair extends Component {
static shouldRender(args) {
return isGPTBot(args.post.user);
}
<template>
<span class="persona-flair">
{{@outletArgs.post.topic.ai_persona_name}}
<span class="agent-flair">
{{@outletArgs.post.topic.ai_agent_name}}
</span>
</template>
}

View File

@ -70,7 +70,7 @@ export default class RagOptionsFk extends Component {
@value={{field.value}}
@llms={{this.visionLlms}}
@onChange={{field.set}}
@class="ai-persona-editor__llms"
@class="ai-agent-editor__llms"
/>
</field.Custom>
</@form.Field>

View File

@ -1,7 +1,7 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-persona-llm-selector";
import AiAgentLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-agent-llm-selector";
function isBotMessage(composer, currentUser) {
if (
@ -21,7 +21,7 @@ function isBotMessage(composer, currentUser) {
export default class BotSelector extends Component {
static shouldRender(args, container) {
return (
container?.currentUser?.ai_enabled_personas &&
container?.currentUser?.ai_enabled_agents &&
isBotMessage(args.model, container.currentUser)
);
}
@ -29,8 +29,8 @@ export default class BotSelector extends Component {
@service currentUser;
@action
setPersonaIdOnComposer(id) {
this.args.outletArgs.model.metaData = { ai_persona_id: id };
setAgentIdOnComposer(id) {
this.args.outletArgs.model.metaData = { ai_agent_id: id };
}
@action
@ -39,8 +39,8 @@ export default class BotSelector extends Component {
}
<template>
<AiPersonaLlmSelector
@setPersonaId={{this.setPersonaIdOnComposer}}
<AiAgentLlmSelector
@setAgentId={{this.setAgentIdOnComposer}}
@setTargetRecipient={{this.setTargetRecipientsOnComposer}}
/>
</template>

View File

@ -9,8 +9,8 @@ import AiSearchDiscoveriesTooltip from "../../components/ai-search-discoveries-t
export default class AiFullPageDiscobotDiscoveries extends Component {
static shouldRender(_args, { siteSettings, currentUser }) {
return (
siteSettings.ai_bot_discover_persona &&
currentUser?.can_use_ai_bot_discover_persona &&
siteSettings.ai_bot_discover_agent &&
currentUser?.can_use_ai_bot_discover_agent &&
currentUser?.user_option?.ai_search_discoveries
);
}

View File

@ -8,8 +8,8 @@ import AiSearchDiscoveriesTooltip from "../../components/ai-search-discoveries-t
export default class AiDiscobotDiscoveries extends Component {
static shouldRender(args, { siteSettings, currentUser }) {
return (
siteSettings.ai_bot_discover_persona &&
currentUser?.can_use_ai_bot_discover_persona &&
siteSettings.ai_bot_discover_agent &&
currentUser?.can_use_ai_bot_discover_agent &&
currentUser?.user_option?.ai_search_discoveries
);
}

View File

@ -35,8 +35,8 @@ export default class PreferencesAiController extends Controller {
checked: this.model.user_option.ai_search_discoveries,
isIncluded: (() => {
return (
this.siteSettings.ai_bot_discover_persona &&
this.model?.can_use_ai_bot_discover_persona &&
this.siteSettings.ai_bot_discover_agent &&
this.model?.can_use_ai_bot_discover_agent &&
this.siteSettings.ai_bot_enabled
);
})(),

View File

@ -5,7 +5,7 @@ import Composer from "discourse/models/composer";
import { i18n } from "discourse-i18n";
import ShareFullTopicModal from "../components/modal/share-full-topic-modal";
const MAX_PERSONA_USER_ID = -1200;
const MAX_AGENT_USER_ID = -1200;
let enabledChatBotMap = null;
@ -40,12 +40,12 @@ export function getBotType(user) {
if (!bot) {
return;
}
return bot.is_persona ? "persona" : "llm";
return bot.is_agent ? "agent" : "llm";
}
export function isPostFromAiBot(post, currentUser) {
return (
post.user_id <= MAX_PERSONA_USER_ID ||
post.user_id <= MAX_AGENT_USER_ID ||
!!currentUser?.ai_enabled_chat_bots?.any(
(bot) => post.username === bot.username
)
@ -66,7 +66,7 @@ export async function composeAiBotMessage(
options = {
skipFocus: false,
topicBody: "",
personaUsername: null,
agentUsername: null,
}
) {
const currentUser = composer.currentUser;
@ -77,8 +77,8 @@ export async function composeAiBotMessage(
botUsername = currentUser.ai_enabled_chat_bots.find(
(bot) => bot.model_name === targetBot
)?.username;
} else if (options.personaUsername) {
botUsername = options.personaUsername;
} else if (options.agentUsername) {
botUsername = options.agentUsername;
} else {
botUsername = currentUser.ai_enabled_chat_bots[0].username;
}

View File

@ -16,7 +16,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
@tracked loading = false;
personaId;
agentId;
targetUsername;
uploads = [];
@ -35,12 +35,12 @@ export default class AiBotConversationsHiddenSubmit extends Service {
async submitToBot() {
if (
this.inputValue.length <
this.siteSettings.min_personal_message_post_length
this.siteSettings.min_agentl_message_post_length
) {
return this.dialog.alert({
message: i18n(
"discourse_ai.ai_bot.conversations.min_input_length_message",
{ count: this.siteSettings.min_personal_message_post_length }
{ count: this.siteSettings.min_agentl_message_post_length }
),
didConfirm: () => this.focusInput(),
didCancel: () => this.focusInput(),
@ -78,7 +78,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
title,
archetype: "private_message",
target_recipients: this.targetUsername,
meta_data: { ai_persona_id: this.personaId },
meta_data: { ai_agent_id: this.agentId },
},
});

View File

@ -22,9 +22,9 @@ export default {
description: "discourse_ai.llms.preconfigured.description",
},
{
label: "discourse_ai.ai_persona.short_title",
route: "adminPlugins.show.discourse-ai-personas",
description: "discourse_ai.ai_persona.persona_description",
label: "discourse_ai.ai_agent.short_title",
route: "adminPlugins.show.discourse-ai-agents",
description: "discourse_ai.ai_agent.agent_description",
},
{
label: "discourse_ai.embeddings.short_title",

View File

@ -3,7 +3,7 @@ import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { withPluginApi } from "discourse/lib/plugin-api";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon";
import AiPersonaFlair from "../discourse/components/post/ai-persona-flair";
import AiAgentFlair from "../discourse/components/post/ai-agent-flair";
import AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button";
import AiDebugButton from "../discourse/components/post-menu/ai-debug-button";
import AiShareButton from "../discourse/components/post-menu/ai-share-button";
@ -53,35 +53,35 @@ function initializeAIBotReplies(api) {
});
}
function initializePersonaDecorator(api) {
api.renderAfterWrapperOutlet("post-meta-data-poster-name", AiPersonaFlair);
function initializeAgentDecorator(api) {
api.renderAfterWrapperOutlet("post-meta-data-poster-name", AiAgentFlair);
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
initializeWidgetPersonaDecorator(api)
initializeWidgetAgentDecorator(api)
);
}
function initializeWidgetPersonaDecorator(api) {
function initializeWidgetAgentDecorator(api) {
api.decorateWidget(`poster-name:after`, (dec) => {
const botType = getBotType(dec.attrs.user);
// we have 2 ways of decorating
// 1. if a bot is a LLM we decorate with persona name
// 2. if bot is a persona we decorate with LLM name
// 1. if a bot is a LLM we decorate with agent name
// 2. if bot is a agent we decorate with LLM name
if (botType === "llm") {
return dec.widget.attach("persona-flair", {
personaName: dec.model?.topic?.ai_persona_name,
return dec.widget.attach("agent-flair", {
agentName: dec.model?.topic?.ai_agent_name,
});
} else if (botType === "persona") {
return dec.widget.attach("persona-flair", {
personaName: dec.model?.llm_name,
} else if (botType === "agent") {
return dec.widget.attach("agent-flair", {
agentName: dec.model?.llm_name,
});
}
});
registerWidgetShim(
"persona-flair",
"span.persona-flair",
hbs`{{@data.personaName}}`
"agent-flair",
"span.agent-flair",
hbs`{{@data.agentName}}`
);
}
@ -149,11 +149,11 @@ function initializeShareTopicButton(api) {
showShareConversationModal(modal, this.topic.id);
},
classNames: ["share-ai-conversation-button"],
dependentKeys: ["topic.ai_persona_name"],
dependentKeys: ["topic.ai_agent_name"],
displayed() {
return (
currentUser?.can_share_ai_bot_conversations &&
this.topic.ai_persona_name
this.topic.ai_agent_name
);
},
});
@ -171,7 +171,7 @@ export default {
withPluginApi((api) => {
attachHeaderIcon(api);
initializeAIBotReplies(api);
initializePersonaDecorator(api);
initializeAgentDecorator(api);
initializeDebugButton(api, container);
initializeShareButton(api, container);
initializeShareTopicButton(api, container);

View File

@ -6,7 +6,7 @@ export default apiInitializer((api) => {
if (
!settings.ai_bot_enabled ||
!currentUser?.can_use_ai_bot_discover_persona
!currentUser?.can_use_ai_bot_discover_agent
) {
return;
}

View File

@ -8,7 +8,7 @@
display: block;
}
&__row-item-persona {
&__row-item-agent {
padding: 0;
text-align: left;

View File

@ -157,7 +157,7 @@ body.has-ai-conversations-sidebar {
flex-direction: column;
height: calc(100dvh - var(--header-offset) - 5em);
.persona-llm-selector {
.agent-llm-selector {
display: flex;
gap: 0.5em;
justify-content: flex-start;

View File

@ -1,8 +1,8 @@
.admin-contents .ai-persona-list-editor {
.admin-contents .ai-agent-list-editor {
margin-top: 0;
}
.ai-persona-list-editor {
.ai-agent-list-editor {
&__header {
display: flex;
justify-content: space-between;
@ -23,7 +23,7 @@
}
}
.ai-persona-tool-option-editor {
.ai-agent-tool-option-editor {
&__instructions {
color: var(--primary-medium);
font-size: var(--font-down-1);
@ -31,7 +31,7 @@
}
}
.ai-personas__container {
.ai-agents__container {
display: flex;
flex-direction: row;
align-items: center;
@ -39,7 +39,7 @@
width: 100%;
}
.ai-persona-editor {
.ai-agent-editor {
padding-left: 0.5em;
&__tool-options {

View File

@ -11,7 +11,7 @@ nav.post-controls .actions button.cancel-streaming {
}
}
.persona-llm-selector {
.agent-llm-selector {
display: flex;
justify-content: space-between;
align-items: center;
@ -24,7 +24,7 @@ nav.post-controls .actions button.cancel-streaming {
}
.ai-bot-pm {
.gpt-persona {
.gpt-agent {
margin-bottom: 5px;
}
@ -75,7 +75,7 @@ article.streaming nav.post-controls .actions button.cancel-streaming {
}
}
.topic-body .persona-flair {
.topic-body .agent-flair {
order: 2;
font-size: var(--font-down-1);
}

View File

@ -1,4 +1,4 @@
.ai-persona-editor {
.ai-agent-editor {
&__system_prompt,
&__description,
.select-kit.multi-select {

View File

@ -45,7 +45,7 @@
}
.ai-tool-list-editor__current,
.ai-persona-list-editor__current,
.ai-agent-list-editor__current,
.ai-llms-list-editor__configured {
.d-admin-table {
tr:hover {

View File

@ -6,8 +6,8 @@ en:
descriptions:
discourse_ai:
search: "Allows AI search"
stream_completion: "Allows streaming AI persona completions"
update_personas: "Allows updating AI personas"
stream_completion: "Allows streaming AI agent completions"
update_agents: "Allows updating AI agents"
site_settings:
categories:
@ -100,17 +100,17 @@ en:
label: "Tool"
description: "Tool to use for triage (tool must have no parameters defined)"
llm_persona_triage:
llm_agent_triage:
fields:
persona:
label: "Persona"
description: "AI Persona to use for triage (must have default LLM and User set)"
agent:
label: "Agent"
description: "AI Agent to use for triage (must have default LLM and User set)"
whisper:
label: "Reply as Whisper"
description: "Whether the persona's response should be a whisper"
description: "Whether the agent's response should be a whisper"
silent_mode:
label: "Silent Mode"
description: "In silent mode persona will receive the content but will not post anything on the forum - useful when performing triage using tools"
description: "In silent mode agent will receive the content but will not post anything on the forum - useful when performing triage using tools"
llm_triage:
fields:
system_prompt:
@ -146,15 +146,15 @@ en:
flag_post:
label: "Flag post"
description: "Flags post (either as spam or for review)"
include_personal_messages:
label: "Include personal messages"
description: "Also scan and triage personal messages"
include_agentl_messages:
label: "Include agentl messages"
description: "Also scan and triage agentl messages"
whisper:
label: "Reply as Whisper"
description: "Whether the AI's response should be a whisper"
reply_persona:
label: "Reply Persona"
description: "AI Persona to use for replies (must have default LLM), will be prioritized over canned reply"
reply_agent:
label: "Reply Agent"
description: "AI Agent to use for replies (must have default LLM), will be prioritized over canned reply"
model:
label: "Model"
description: "Language model used for triage"
@ -167,12 +167,12 @@ en:
features:
short_title: "Features"
description: "These are the AI features available to visitors on your site. These can be configured to use specific personas and LLMs, and can be access controlled by groups."
description: "These are the AI features available to visitors on your site. These can be configured to use specific agents and LLMs, and can be access controlled by groups."
back: "Back"
list:
header:
name: "Name"
persona: "Persona"
agent: "Agent"
groups: "Groups"
edit: "Edit"
set_up: "Set up"
@ -257,7 +257,7 @@ en:
last_month: "Last month"
custom: "Custom..."
ai_persona:
ai_agent:
ai_tools: "Tools"
tool_strategies:
all: "Apply to all replies"
@ -269,7 +269,7 @@ en:
edit: "Edit"
description: "Description"
no_llm_selected: "No language model selected"
use_parent_llm: "Use personas language model"
use_parent_llm: "Use agents language model"
max_context_posts: "Max context posts"
max_context_posts_help: "The maximum number of posts to use as context for the AI when responding to a user. (empty for default)"
vision_enabled: Vision enabled
@ -282,47 +282,47 @@ en:
tool_details: Show tool details
tool_details_help: Will show end users details on which tools the language model has triggered.
mentionable: Allow mentions
mentionable_help: If enabled, users in allowed groups can mention this user in posts, the AI will respond as this persona.
mentionable_help: If enabled, users in allowed groups can mention this user in posts, the AI will respond as this agent.
user: User
create_user: Create user
create_user_help: You can optionally attach a user to this persona. If you do, the AI will use this user to respond to requests.
create_user_help: You can optionally attach a user to this agent. If you do, the AI will use this user to respond to requests.
default_llm: Default language model
default_llm_help: The default language model to use for this persona. Required if you wish to mention persona on public posts.
default_llm_help: The default language model to use for this agent. Required if you wish to mention agent on public posts.
question_consolidator_llm: Language Model for Question Consolidator
question_consolidator_llm_help: The language model to use for the question consolidator, you may choose a less powerful model to save costs.
system_prompt: System prompt
forced_tool_strategy: Forced tool strategy
allow_chat_direct_messages: "Allow chat direct messages"
allow_chat_direct_messages_help: "If enabled, users in allowed groups can send direct messages to this persona."
allow_chat_direct_messages_help: "If enabled, users in allowed groups can send direct messages to this agent."
allow_chat_channel_mentions: "Allow chat channel mentions"
allow_chat_channel_mentions_help: "If enabled, users in allowed groups can mention this persona in chat channels."
allow_personal_messages: "Allow personal messages"
allow_personal_messages_help: "If enabled, users in allowed groups can send personal messages to this persona."
allow_chat_channel_mentions_help: "If enabled, users in allowed groups can mention this agent in chat channels."
allow_agentl_messages: "Allow agentl messages"
allow_agentl_messages_help: "If enabled, users in allowed groups can send agentl messages to this agent."
allow_topic_mentions: "Allow topic mentions"
allow_topic_mentions_help: "If enabled, users in allowed groups can mention this persona in topics."
allow_topic_mentions_help: "If enabled, users in allowed groups can mention this agent in topics."
force_default_llm: "Always use default language model"
save: "Save"
saved: "Persona saved"
saved: "Agent saved"
enabled: "Enabled?"
tools: "Enabled tools"
forced_tools: "Forced tools"
allowed_groups: "Allowed groups"
confirm_delete: "Are you sure you want to delete this persona?"
new: "New persona"
no_personas: "You have not created any personas yet"
title: "Personas"
short_title: "Personas"
confirm_delete: "Are you sure you want to delete this agent?"
new: "New agent"
no_agents: "You have not created any agents yet"
title: "Agents"
short_title: "Agents"
delete: "Delete"
temperature: "Temperature"
temperature_help: "Temperature to use for the LLM. Increase to increase creativity (leave empty to use model default, generally a value from 0.0 to 2.0)"
top_p: "Top P"
top_p_help: "Top P to use for the LLM, increase to increase randomness (leave empty to use model default, generally a value from 0.0 to 1.0)"
priority: "Priority"
priority_help: "Priority personas are displayed to users at the top of the persona list. If multiple personas have priority, they will be sorted alphabetically."
priority_help: "Priority agents are displayed to users at the top of the agent list. If multiple agents have priority, they will be sorted alphabetically."
tool_options: "Tool options"
rag_conversation_chunks: "Search conversation chunks"
rag_conversation_chunks_help: "The number of chunks to use for the RAG model searches. Increase to increase the amount of context the AI can use."
persona_description: "Personas are a powerful feature that allows you to customize the behavior of the AI engine in your Discourse forum. They act as a 'system message' that guides the AI's responses and interactions, helping to create a more personalized and engaging user experience."
agent_description: "Agents are a powerful feature that allows you to customize the behavior of the AI engine in your Discourse forum. They act as a 'system message' that guides the AI's responses and interactions, helping to create a more agentlized and engaging user experience."
response_format:
title: "JSON response format"
no_format: "No JSON format specified"
@ -344,7 +344,7 @@ en:
ai_bot:
title: "AI bot options"
save_first: "More AI bot options will become available once you save the persona."
save_first: "More AI bot options will become available once you save the agent."
rag:
title: "RAG"
@ -377,7 +377,7 @@ en:
name_help: "Name will show up in the Discourse UI and is the short identifier you will use to find the tool in various settings, it should be distinct (it is required)"
new: "New tool"
tool_name: "Tool Name"
tool_name_help: "Tool Name is presented to the large language model. It is not distinct, but it is distinct per persona. (persona validates on save)"
tool_name_help: "Tool Name is presented to the large language model. It is not distinct, but it is distinct per agent. (agent validates on save)"
description: "Description"
description_help: "A clear description of the tool's purpose for the language model"
subheader_description: "Tools extend the capabilities of AI bots with user-defined JavaScript functions."
@ -455,7 +455,7 @@ en:
ai_bot: "AI bot"
ai_helper: "Helper"
ai_helper_image_caption: "Image caption"
ai_persona: "Persona (%{persona})"
ai_agent: "Agent (%{agent})"
ai_summarization: "Summarize"
ai_embeddings_semantic_search: "AI search"
ai_spam: "Spam"
@ -681,7 +681,7 @@ en:
click_to_run_label: "Run Artifact"
ai_bot:
persona: "Persona"
agent: "Agent"
llm: "Model"
pm_warning: "AI chatbot messages are monitored regularly by moderators."
cancel_streaming: "Stop reply"

View File

@ -10,9 +10,9 @@ en:
llm_tool_triage:
title: Triage posts using AI Tool
description: "Triage posts using custom logic in an AI tool"
llm_persona_triage:
title: Triage posts using AI Persona
description: "Respond to posts using a specific AI persona"
llm_agent_triage:
title: Triage posts using AI Agent
description: "Respond to posts using a specific AI agent"
llm_triage:
title: Triage posts using AI
description: "Triage posts using a large language model"
@ -76,7 +76,7 @@ en:
ai_auto_image_caption_allowed_groups: "Users on these groups can toggle automatic image captioning."
ai_embeddings_selected_model: "Use the selected model for generating embeddings."
ai_embeddings_generate_for_pms: "Generate embeddings for personal messages."
ai_embeddings_generate_for_pms: "Generate embeddings for agentl messages."
ai_embeddings_semantic_related_topics_enabled: "Use Semantic Search for related topics."
ai_embeddings_semantic_related_topics: "Maximum number of topics to show in related topic section."
ai_embeddings_backfill_batch_size: "Number of embeddings to backfill every 15 minutes."
@ -88,7 +88,7 @@ en:
ai_summarization_enabled: "Enable the summarize feature"
ai_summarization_model: "Model to use for summarization"
ai_summarization_persona: "Persona to use for summarize feature"
ai_summarization_agent: "Agent to use for summarize feature"
ai_custom_summarization_allowed_groups: "Groups allowed to use create new summaries."
ai_pm_summarization_allowed_groups: "Groups allowed to create and view summaries in PMs."
ai_summary_gists_enabled: "Generate brief summaries of latest replies in topics automatically"
@ -99,7 +99,7 @@ en:
ai_bot_enable_chat_warning: "Display a warning when PM chat is initiated. Can be overriden by editing the translation string: discourse_ai.ai_bot.pm_warning"
ai_bot_allowed_groups: "When the GPT Bot has access to the PM, it will reply to members of these groups."
ai_bot_debugging_allowed_groups: "Allow these groups to see a debug button on posts which displays the raw AI request and response"
ai_bot_public_sharing_allowed_groups: "Allow these groups to share AI personal messages with the public via a unique publicly available link. Note: if your site requires login, shares will also require login."
ai_bot_public_sharing_allowed_groups: "Allow these groups to share AI agentl messages with the public via a unique publicly available link. Note: if your site requires login, shares will also require login."
ai_bot_add_to_header: "Display a button in the header to start a PM with a AI Bot"
ai_bot_github_access_token: "GitHub access token for use with GitHub AI tools (required for search support)"
@ -114,7 +114,7 @@ en:
ai_discord_app_id: "The ID of the Discord application you would like to connect Discord search to"
ai_discord_app_public_key: "The public key of the Discord application you would like to connect Discord search to"
ai_discord_search_mode: "Select the search mode to use for Discord search"
ai_discord_search_persona: "The persona to use for Discord search."
ai_discord_search_agent: "The agent to use for Discord search."
ai_discord_allowed_guilds: "Discord guilds (servers) where the bot is allowed to search"
ai_bot_enable_dedicated_ux: "Allow for full screen bot interface, instead of a PM"
@ -129,7 +129,7 @@ en:
description: "This report provides sentiment analysis for posts, grouped by category, with positive, negative, and neutral scores for each post and category."
overall_sentiment:
title: "Overall sentiment"
description: 'The chart compares the number of posts classified as either positive or negative. These are calculated when positive or negative scores > the set threshold score. This means neutral posts are not shown. Personal messages (PMs) are also excluded. Classified with "cardiffnlp/twitter-roberta-base-sentiment-latest"'
description: 'The chart compares the number of posts classified as either positive or negative. These are calculated when positive or negative scores > the set threshold score. This means neutral posts are not shown. Agentl messages (PMs) are also excluded. Classified with "cardiffnlp/twitter-roberta-base-sentiment-latest"'
xaxis: "Positive(%)"
yaxis: "Date"
emotion_admiration:
@ -267,8 +267,8 @@ en:
title: "%{title} - AI Conversation - %{site_name}"
errors:
not_allowed: "You are not allowed to share this topic"
other_people_in_pm: "Personal messages with other humans cannot be shared publicly"
other_content_in_pm: "Personal messages containing posts from other people cannot be shared publicly"
other_people_in_pm: "Agentl messages with other humans cannot be shared publicly"
other_content_in_pm: "Agentl messages containing posts from other people cannot be shared publicly"
failed_to_share: "Failed to share the conversation"
conversation_deleted: "Conversation share deleted successfully"
spam_detection:
@ -283,10 +283,10 @@ en:
reply_error: "Sorry, it looks like our system encountered an unexpected issue while trying to reply.\n\n[details='Error details']\n%{details}\n[/details]"
default_pm_prefix: "[Untitled AI bot PM]"
thinking: "Thinking..."
personas:
agents:
default_llm_required: "Default LLM model is required prior to enabling Chat"
cannot_delete_system_persona: "System personas cannot be deleted, please disable it instead"
cannot_edit_system_persona: "System personas can only be renamed, you may not edit tools or system prompt, instead disable and make a copy"
cannot_delete_system_agent: "System agents cannot be deleted, please disable it instead"
cannot_edit_system_agent: "System agents can only be renamed, you may not edit tools or system prompt, instead disable and make a copy"
cannot_have_duplicate_tools: "Can not have duplicate tools"
github_helper:
name: "GitHub Helper"
@ -326,10 +326,10 @@ en:
description: "AI Bot specialized in creating interactive web artifacts"
summarizer:
name: "Summarizer"
description: "Default persona used to power AI summaries"
description: "Default agent used to power AI summaries"
short_summarizer:
name: "Summarizer (short form)"
description: "Default persona used to power AI short summaries for topic lists' items"
description: "Default agent used to power AI short summaries for topic lists' items"
topic_not_found: "Summary unavailable, topic not found!"
summarizing: "Summarizing topic"
searching: "Searching for: '%{query}'"
@ -521,7 +521,7 @@ en:
other: "We couldn't delete this model because %{settings} are using it. Update the settings and try again."
cannot_edit_builtin: "You can't edit a built-in model."
personas:
agents:
malformed_examples: "The given examples have the wrong format."
embeddings:
@ -554,12 +554,12 @@ en:
quota_exceeded: "You have exceeded the quota for this model. Please try again in %{relative_time}."
quota_required: "You must specify maximum tokens or usages for this model"
no_query_specified: The query parameter is required, please specify it.
no_user_for_persona: The persona specified does not have a user associated with it.
persona_not_found: The persona specified does not exist. Check the persona_name or persona_id params.
no_user_for_agent: The agent specified does not have a user associated with it.
agent_not_found: The agent specified does not exist. Check the agent_name or agent_id params.
no_user_specified: The username or the user_unique_id parameter is required, please specify it.
user_not_found: The user specified does not exist. Check the username param.
persona_disabled: The persona specified is disabled. Check the persona_name or persona_id params.
no_default_llm: The persona must have a default_llm defined.
agent_disabled: The agent specified is disabled. Check the agent_name or agent_id params.
no_default_llm: The agent must have a default_llm defined.
user_not_allowed: The user is not allowed to participate in the topic.
prompt_message_length: The message %{idx} is over the 1000 character limit.
dashboard:

View File

@ -63,12 +63,12 @@ Discourse::Application.routes.draw do
:constraints => StaffConstraint.new
scope "/admin/plugins/discourse-ai", constraints: AdminConstraint.new do
resources :ai_personas,
resources :ai_agents,
only: %i[index new create edit update destroy],
path: "ai-personas",
controller: "discourse_ai/admin/ai_personas"
path: "ai-agents",
controller: "discourse_ai/admin/ai_agents"
post "/ai-personas/stream-reply" => "discourse_ai/admin/ai_personas#stream_reply"
post "/ai-agents/stream-reply" => "discourse_ai/admin/ai_agents#stream_reply"
resources(
:ai_tools,
@ -79,10 +79,10 @@ Discourse::Application.routes.draw do
post "/ai-tools/:id/test", to: "discourse_ai/admin/ai_tools#test"
post "/ai-personas/:id/create-user", to: "discourse_ai/admin/ai_personas#create_user"
post "/ai-agents/:id/create-user", to: "discourse_ai/admin/ai_agents#create_user"
put "/ai-personas/:id/files/remove", to: "discourse_ai/admin/ai_personas#remove_file"
get "/ai-personas/:id/files/status", to: "discourse_ai/admin/ai_personas#indexing_status_check"
put "/ai-agents/:id/files/remove", to: "discourse_ai/admin/ai_agents#remove_file"
get "/ai-agents/:id/files/status", to: "discourse_ai/admin/ai_agents#indexing_status_check"
post "/rag-document-fragments/files/upload",
to: "discourse_ai/admin/rag_document_fragments#upload_file"

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
class CopyPersonaTablesToAgent < ActiveRecord::Migration[7.0]
def up
# Copy the main table structure and data
if table_exists?(:ai_personas) && !table_exists?(:ai_agents)
execute <<~SQL
CREATE TABLE ai_agents AS
SELECT * FROM ai_personas
SQL
# Copy indexes from ai_personas to ai_agents
execute <<~SQL
CREATE UNIQUE INDEX index_ai_agents_on_id
ON ai_agents USING btree (id)
SQL
# Copy any other indexes that exist on ai_personas
indexes = execute(<<~SQL).to_a
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'ai_personas'
AND indexname != 'ai_personas_pkey'
SQL
indexes.each do |index|
new_index_def = index['indexdef'].gsub('ai_personas', 'ai_agents')
new_index_name = index['indexname'].gsub('ai_personas', 'ai_agents')
new_index_def = new_index_def.gsub(index['indexname'], new_index_name)
execute(new_index_def)
end
end
# Update polymorphic associations to point to new table
execute <<~SQL
UPDATE rag_document_fragments
SET target_type = 'AiAgent'
WHERE target_type = 'AiPersona'
SQL
execute <<~SQL
UPDATE upload_references
SET target_type = 'AiAgent'
WHERE target_type = 'AiPersona'
SQL
# Migrate persona-related site settings to agent equivalents
migrate_site_setting('ai_summarization_persona', 'ai_summarization_agent')
migrate_site_setting('ai_summary_gists_persona', 'ai_summary_gists_agent')
migrate_site_setting('ai_bot_discover_persona', 'ai_bot_discover_agent')
migrate_site_setting('ai_discord_search_persona', 'ai_discord_search_agent')
end
def down
drop_table :ai_agents if table_exists?(:ai_agents)
# Revert polymorphic associations
execute <<~SQL
UPDATE rag_document_fragments
SET target_type = 'AiPersona'
WHERE target_type = 'AiAgent'
SQL
execute <<~SQL
UPDATE upload_references
SET target_type = 'AiPersona'
WHERE target_type = 'AiAgent'
SQL
# Remove the new agent settings (keep the old persona ones)
['ai_summarization_agent', 'ai_summary_gists_agent', 'ai_bot_discover_agent', 'ai_discord_search_agent'].each do |setting|
execute "DELETE FROM site_settings WHERE name = '#{setting}'"
end
end
private
def migrate_site_setting(old_name, new_name)
execute <<~SQL
INSERT INTO site_settings (name, value, data_type, created_at, updated_at)
SELECT '#{new_name}', value, data_type, NOW(), NOW()
FROM site_settings
WHERE name = '#{old_name}'
AND NOT EXISTS (SELECT 1 FROM site_settings WHERE name = '#{new_name}')
SQL
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
class DropPersonaTables < ActiveRecord::Migration[7.0]
def up
# Drop the old table after copying to new one
drop_table :ai_personas if table_exists?(:ai_personas)
# Remove old persona settings after copying to agent settings
old_persona_settings = [
'ai_summarization_persona',
'ai_summary_gists_persona',
'ai_bot_discover_persona',
'ai_discord_search_persona'
]
old_persona_settings.each do |setting|
execute "DELETE FROM site_settings WHERE name = '#{setting}'"
end
end
def down
raise ActiveRecord::IrreversibleMigration, "Cannot recreate dropped persona tables and settings"
end
end

View File

@ -1,17 +1,17 @@
# frozen_string_literal: true
if defined?(DiscourseAutomation)
DiscourseAutomation::Scriptable.add("llm_persona_triage") do
DiscourseAutomation::Scriptable.add("llm_agent_triage") do
version 1
run_in_background
triggerables %i[post_created_edited]
field :persona,
field :agent,
component: :choices,
required: true,
extra: {
content: DiscourseAi::Automation.available_persona_choices,
content: DiscourseAi::Automation.available_agent_choices,
}
field :whisper, component: :boolean
field :silent_mode, component: :boolean
@ -20,28 +20,28 @@ if defined?(DiscourseAutomation)
post = context["post"]
next if post&.user&.bot?
persona_id = fields.dig("persona", "value")
agent_id = fields.dig("agent", "value")
whisper = !!fields.dig("whisper", "value")
silent_mode = !!fields.dig("silent_mode", "value")
begin
RateLimiter.new(
Discourse.system_user,
"llm_persona_triage_#{post.id}",
"llm_agent_triage_#{post.id}",
SiteSetting.ai_automation_max_triage_per_post_per_minute,
1.minute,
).performed!
RateLimiter.new(
Discourse.system_user,
"llm_persona_triage",
"llm_agent_triage",
SiteSetting.ai_automation_max_triage_per_minute,
1.minute,
).performed!
DiscourseAi::Automation::LlmPersonaTriage.handle(
DiscourseAi::Automation::LlmAgentTriage.handle(
post: post,
persona_id: persona_id,
agent_id: agent_id,
whisper: whisper,
automation: self.automation,
silent_mode: silent_mode,
@ -49,7 +49,7 @@ if defined?(DiscourseAutomation)
rescue => e
Discourse.warn_exception(
e,
message: "llm_persona_triage: skipped triage on post #{post.id}",
message: "llm_agent_triage: skipped triage on post #{post.id}",
)
raise e if Rails.env.tests?
end

View File

@ -10,7 +10,7 @@ if defined?(DiscourseAutomation)
triggerables %i[post_created_edited]
# TODO move to triggerables
field :include_personal_messages, component: :boolean
field :include_agentl_messages, component: :boolean
# Inputs
field :model,
@ -39,11 +39,11 @@ if defined?(DiscourseAutomation)
default: "review"
field :canned_reply_user, component: :user
field :canned_reply, component: :message
field :reply_persona,
field :reply_agent,
component: :choices,
extra: {
content:
DiscourseAi::Automation.available_persona_choices(
DiscourseAi::Automation.available_agent_choices(
require_user: false,
require_default_llm: true,
),
@ -55,13 +55,13 @@ if defined?(DiscourseAutomation)
next if post&.user&.bot?
if post.topic.private_message?
include_personal_messages = fields.dig("include_personal_messages", "value")
next if !include_personal_messages
include_agentl_messages = fields.dig("include_agentl_messages", "value")
next if !include_agentl_messages
end
canned_reply = fields.dig("canned_reply", "value")
canned_reply_user = fields.dig("canned_reply_user", "value")
reply_persona_id = fields.dig("reply_persona", "value")
reply_agent_id = fields.dig("reply_agent", "value")
whisper = fields.dig("whisper", "value")
# nothing to do if we already replied
@ -113,7 +113,7 @@ if defined?(DiscourseAutomation)
tags: tags,
canned_reply: canned_reply,
canned_reply_user: canned_reply_user,
reply_persona_id: reply_persona_id,
reply_agent_id: reply_agent_id,
whisper: whisper,
hide_topic: hide_topic,
flag_post: flag_post,

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Artist < Persona
module Agents
class Artist < Agent
def tools
[Tools::Image]
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module Personas
module Agents
class Bot
attr_reader :model
@ -13,19 +13,19 @@ 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::Personas::General.new, model: nil)
new(bot_user, persona, model)
def self.as(bot_user, agent: DiscourseAi::Agents::General.new, model: nil)
new(bot_user, agent, model)
end
def initialize(bot_user, persona, model = nil)
def initialize(bot_user, agent, model = nil)
@bot_user = bot_user
@persona = persona
@agent = agent
@model =
model || self.class.guess_model(bot_user) || LlmModel.find(@persona.class.default_llm_id)
model || self.class.guess_model(bot_user) || LlmModel.find(@agent.class.default_llm_id)
end
attr_reader :bot_user
attr_accessor :persona
attr_accessor :agent
def llm
DiscourseAi::Completions::Llm.proxy(model)
@ -35,12 +35,12 @@ module DiscourseAi
return if prompt.tool_choice == :none
context.chosen_tools ||= []
forced_tools = persona.force_tool_use.map { |tool| tool.name }
forced_tools = agent.force_tool_use.map { |tool| tool.name }
force_tool = forced_tools.find { |name| !context.chosen_tools.include?(name) }
if force_tool && persona.forced_tool_count > 0
if force_tool && agent.forced_tool_count > 0
user_turns = prompt.messages.select { |m| m[:type] == :user }.length
force_tool = false if user_turns > persona.forced_tool_count
force_tool = false if user_turns > agent.forced_tool_count
end
if force_tool
@ -57,7 +57,7 @@ module DiscourseAi
end
context.cancel_manager ||= DiscourseAi::Completions::CancelManager.new
current_llm = llm
prompt = persona.craft_prompt(context, llm: current_llm)
prompt = agent.craft_prompt(context, llm: current_llm)
total_completions = 0
ongoing_chain = true
@ -67,11 +67,11 @@ module DiscourseAi
llm_kwargs = llm_args.dup
llm_kwargs[:user] = user
llm_kwargs[:temperature] = persona.temperature if persona.temperature
llm_kwargs[:top_p] = persona.top_p if persona.top_p
llm_kwargs[:temperature] = agent.temperature if agent.temperature
llm_kwargs[:top_p] = agent.top_p if agent.top_p
llm_kwargs[:response_format] = build_json_schema(
persona.response_format,
) if persona.response_format.present?
agent.response_format,
) if agent.response_format.present?
needs_newlines = false
tools_ran = 0
@ -82,7 +82,7 @@ module DiscourseAi
tool_halted = false
allow_partial_tool_calls = persona.allow_partial_tool_calls?
allow_partial_tool_calls = agent.allow_partial_tool_calls?
existing_tools = Set.new
current_thinking = []
@ -96,7 +96,7 @@ module DiscourseAi
**llm_kwargs,
) do |partial|
tool =
persona.find_tool(
agent.find_tool(
partial,
bot_user: user,
llm: current_llm,
@ -183,7 +183,7 @@ module DiscourseAi
end
def returns_json?
persona.response_format.present?
agent.response_format.present?
end
private
@ -285,7 +285,7 @@ module DiscourseAi
def self.guess_model(bot_user)
associated_llm = LlmModel.find_by(user_id: bot_user.id)
return if associated_llm.nil? # Might be a persona user. Handled by constructor.
return if associated_llm.nil? # Might be a agent user. Handled by constructor.
associated_llm
end

View File

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

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Creative < Persona
module Agents
class Creative < Agent
def tools
[]
end

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class DallE3 < Persona
module Agents
class DallE3 < Agent
def tools
[Tools::DallE]
end

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Designer < Persona
module Agents
class Designer < Agent
def tools
[Tools::CreateImage, Tools::EditImage]
end

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class DiscourseHelper < Persona
module Agents
class DiscourseHelper < Agent
def tools
[Tools::DiscourseMetaSearch]
end

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class ForumResearcher < Persona
module Agents
class ForumResearcher < Agent
def self.default_enabled
false
end

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class General < Persona
module Agents
class General < Agent
def tools
[
Tools::Search,

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true
module DiscourseAi
module Personas
class GithubHelper < Persona
module Agents
class GithubHelper < Agent
def tools
[
Tools::GithubFileContent,

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Persona
module Agents
class Agent
class << self
def default_enabled
true
@ -36,8 +36,8 @@ module DiscourseAi
false
end
def system_personas
@system_personas ||= {
def system_agents
@system_agents ||= {
General => -1,
SqlHelper => -2,
Artist => -3,
@ -55,17 +55,17 @@ module DiscourseAi
}
end
def system_personas_by_id
@system_personas_by_id ||= system_personas.invert
def system_agents_by_id
@system_agents_by_id ||= system_agents.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)
AiAgent.all_agents.filter do |agent|
next false if !user.in_any_groups?(agent.allowed_group_ids)
if persona.system
instance = persona.new
if agent.system
instance = agent.new
(
instance.required_tools == [] ||
(instance.required_tools - all_available_tools).empty?
@ -77,15 +77,15 @@ module DiscourseAi
end
def find_by(id: nil, name: nil, user:)
all(user: user).find { |persona| persona.id == id || persona.name == name }
all(user: user).find { |agent| agent.id == id || agent.name == name }
end
def name
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.name")
I18n.t("discourse_ai.ai_bot.agents.#{to_s.demodulize.underscore}.name")
end
def description
I18n.t("discourse_ai.ai_bot.personas.#{to_s.demodulize.underscore}.description")
I18n.t("discourse_ai.ai_bot.agents.#{to_s.demodulize.underscore}.description")
end
def all_available_tools
@ -134,8 +134,8 @@ module DiscourseAi
end
def id
@ai_persona&.id || self.class.system_personas[self.class.superclass] ||
self.class.system_personas[self.class]
@ai_agent&.id || self.class.system_agents[self.class.superclass] ||
self.class.system_agents[self.class]
end
def tools
@ -234,7 +234,7 @@ module DiscourseAi
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)
tool.inject_prompt(prompt: prompt, context: context, agent: self)
end
prompt
end
@ -307,7 +307,7 @@ module DiscourseAi
tool_klass.new(
arguments,
tool_call_id: function_id || function_name,
persona_options: options[tool_klass].to_h,
agent_options: options[tool_klass].to_h,
bot_user: bot_user,
llm: llm,
context: context,
@ -331,7 +331,7 @@ module DiscourseAi
def rag_fragments_prompt(conversation_context, llm:, user:)
upload_refs =
UploadReference.where(target_id: id, target_type: "AiPersona").pluck(:upload_id)
UploadReference.where(target_id: id, target_type: "AiAgent").pluck(:upload_id)
return nil if !DiscourseAi::Embeddings.enabled?
return nil if conversation_context.blank? || upload_refs.blank?
@ -346,7 +346,7 @@ module DiscourseAi
consolidated_question = latest_interactions[0][:content]
else
consolidated_question =
DiscourseAi::Personas::QuestionConsolidator.consolidate_question(
DiscourseAi::Agents::QuestionConsolidator.consolidate_question(
llm,
latest_interactions,
user,
@ -376,7 +376,7 @@ module DiscourseAi
interactions_vector,
limit: search_limit,
offset: 0,
) { |builder| builder.join(<<~SQL, target_id: id, target_type: "AiPersona") }
) { |builder| builder.join(<<~SQL, target_id: id, target_type: "AiAgent") }
rag_document_fragments ON
rag_document_fragments.id = rag_document_fragment_id AND
rag_document_fragments.target_id = :target_id AND

View File

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

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Researcher < Persona
module Agents
class Researcher < Agent
def tools
[Tools::Google, Tools::WebBrowser]
end

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class SettingsExplorer < Persona
module Agents
class SettingsExplorer < Agent
def tools
[Tools::SettingContext, Tools::SearchSettings]
end

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true
module DiscourseAi
module Personas
class ShortSummarizer < Persona
module Agents
class ShortSummarizer < Agent
def self.default_enabled
false
end

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class SqlHelper < Persona
module Agents
class SqlHelper < Agent
def self.schema
return @schema if defined?(@schema)
@ -74,7 +74,7 @@ module DiscourseAi
```
The user_actions tables stores likes (action_type 1).
The topics table stores private/personal messages it uses archetype private_message for them.
The topics table stores private/agentl messages it uses archetype private_message for them.
notification_level can be: {muted: 0, regular: 1, tracking: 2, watching: 3, watching_first_post: 4}.
bookmarkable_type can be: Post,Topic,ChatMessage and more

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true
module DiscourseAi
module Personas
class Summarizer < Persona
module Agents
class Summarizer < Agent
def self.default_enabled
false
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module Personas
module Agents
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::Personas::BotContext)
if context && !context.is_a?(DiscourseAi::Agents::BotContext)
raise ArgumentError, "context must be a BotContext object"
end
context ||= DiscourseAi::Personas::BotContext.new
context ||= DiscourseAi::Agents::BotContext.new
@parameters = parameters
@llm = llm
@ -82,8 +82,8 @@ module DiscourseAi
search: function(params) {
return _discourse_search(params);
},
updatePersona: function(persona_id_or_name, updates) {
const result = _discourse_update_persona(persona_id_or_name, updates);
updateAgent: function(agent_id_or_name, updates) {
const result = _discourse_update_agent(agent_id_or_name, updates);
if (result.error) {
throw new Error(result.error);
}
@ -92,29 +92,29 @@ module DiscourseAi
getPost: _discourse_get_post,
getTopic: _discourse_get_topic,
getUser: _discourse_get_user,
getPersona: function(name) {
const personaDetails = _discourse_get_persona(name);
if (personaDetails.error) {
throw new Error(personaDetails.error);
getAgent: function(name) {
const agentDetails = _discourse_get_agent(name);
if (agentDetails.error) {
throw new Error(agentDetails.error);
}
// merge result.persona with {}..
// merge result.agent with {}..
return Object.assign({
update: function(updates) {
const result = _discourse_update_persona(name, updates);
const result = _discourse_update_agent(name, updates);
if (result.error) {
throw new Error(result.error);
}
return result;
},
respondTo: function(params) {
const result = _discourse_respond_to_persona(name, params);
const result = _discourse_respond_to_agent(name, params);
if (result.error) {
throw new Error(result.error);
}
return result;
}
}, personaDetails.persona);
}, agentDetails.agent);
},
createChatMessage: function(params) {
const result = _discourse_create_chat_message(params);
@ -365,15 +365,15 @@ module DiscourseAi
)
mini_racer_context.attach(
"_discourse_respond_to_persona",
->(persona_name, params) do
"_discourse_respond_to_agent",
->(agent_name, params) do
in_attached_function do
# if we have 1000s of personas this can be slow ... we may need to optimize
persona_class = AiPersona.all_personas.find { |persona| persona.name == persona_name }
return { error: "Persona not found" } if persona_class.nil?
# if we have 1000s of agents this can be slow ... we may need to optimize
agent_class = AiAgent.all_agents.find { |agent| agent.name == agent_name }
return { error: "Agent not found" } if agent_class.nil?
persona = persona_class.new
bot = DiscourseAi::Personas::Bot.as(@bot_user || persona.user, persona: persona)
agent = agent_class.new
bot = DiscourseAi::Agents::Bot.as(@bot_user || agent.user, agent: agent)
playground = DiscourseAi::AiBot::Playground.new(bot)
if @context.post_id
@ -479,17 +479,17 @@ module DiscourseAi
)
mini_racer_context.attach(
"_discourse_get_persona",
->(persona_name) do
"_discourse_get_agent",
->(agent_name) do
in_attached_function do
persona = AiPersona.find_by(name: persona_name)
agent = AiAgent.find_by(name: agent_name)
return { error: "Persona not found" } if persona.nil?
return { error: "Agent not found" } if agent.nil?
# Return a subset of relevant persona attributes
# Return a subset of relevant agent attributes
{
persona:
persona.attributes.slice(
agent:
agent.attributes.slice(
"id",
"name",
"description",
@ -503,7 +503,7 @@ module DiscourseAi
"allow_chat_channel_mentions",
"allow_chat_direct_messages",
"allow_topic_mentions",
"allow_personal_messages",
"allow_agentl_messages",
),
}
end
@ -511,19 +511,19 @@ module DiscourseAi
)
mini_racer_context.attach(
"_discourse_update_persona",
->(persona_id_or_name, updates) do
"_discourse_update_agent",
->(agent_id_or_name, updates) do
in_attached_function do
# Find persona by ID or name
persona = nil
if persona_id_or_name.is_a?(Integer) ||
persona_id_or_name.to_i.to_s == persona_id_or_name
persona = AiPersona.find_by(id: persona_id_or_name.to_i)
# Find agent by ID or name
agent = nil
if agent_id_or_name.is_a?(Integer) ||
agent_id_or_name.to_i.to_s == agent_id_or_name
agent = AiAgent.find_by(id: agent_id_or_name.to_i)
else
persona = AiPersona.find_by(name: persona_id_or_name)
agent = AiAgent.find_by(name: agent_id_or_name)
end
return { error: "Persona not found" } if persona.nil?
return { error: "Agent not found" } if agent.nil?
allowed_updates = {}
@ -545,12 +545,12 @@ module DiscourseAi
TrueClass,
) || updates["enabled"].is_a?(FalseClass)
if persona.update(allowed_updates)
if agent.update(allowed_updates)
return(
{
success: true,
persona:
persona.attributes.slice(
agent:
agent.attributes.slice(
"id",
"name",
"description",
@ -562,7 +562,7 @@ module DiscourseAi
}
)
else
return { error: persona.errors.full_messages.join(", ") }
return { error: agent.errors.full_messages.join(", ") }
end
end
end,
@ -612,7 +612,7 @@ module DiscourseAi
headers = (options && options["headers"]) || {}
result = {}
DiscourseAi::Personas::Tools::Tool.send_http_request(
DiscourseAi::Agents::Tools::Tool.send_http_request(
url,
headers: headers,
) do |response|
@ -641,7 +641,7 @@ module DiscourseAi
body = options && options["body"]
result = {}
DiscourseAi::Personas::Tools::Tool.send_http_request(
DiscourseAi::Agents::Tools::Tool.send_http_request(
url,
method: method,
headers: headers,

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module Personas
module Agents
module Tools
class Custom < Tool
def self.class_instance(tool_id)
@ -33,7 +33,7 @@ module DiscourseAi
end
def self.has_custom_context?
# note on safety, this can be cached safely, we bump the whole persona cache when an ai tool is saved
# note on safety, this can be cached safely, we bump the whole agent cache when an ai tool is saved
# which will expire this class
return @has_custom_context if defined?(@has_custom_context)
@ -47,7 +47,7 @@ module DiscourseAi
@has_custom_context
end
def self.inject_prompt(prompt:, context:, persona:)
def self.inject_prompt(prompt:, context:, agent:)
if has_custom_context?
ai_tool = AiTool.find_by(id: tool_id)
if ai_tool

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More