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 { AUTO_GROUPS } from "discourse/lib/constants";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRoute { export default class AdminPluginsShowDiscourseAiAgentsNew extends DiscourseRoute {
async model() { 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("allowed_group_ids", [AUTO_GROUPS.trust_level_0.id]);
record.set("rag_uploads", []); record.set("rag_uploads", []);
// these match the defaults on the table // these match the defaults on the table
record.set("rag_chunk_tokens", 374); record.set("rag_chunk_tokens", 374);
record.set("rag_chunk_overlap_tokens", 10); record.set("rag_chunk_overlap_tokens", 10);
record.set("rag_conversation_chunks", 10); record.set("rag_conversation_chunks", 10);
record.set("allow_personal_messages", true); record.set("allow_agentl_messages", true);
record.set("tool_details", false); record.set("tool_details", false);
return record; return record;
} }
@ -18,8 +18,8 @@ export default class AdminPluginsShowDiscourseAiPersonasNew extends DiscourseRou
setupController(controller, model) { setupController(controller, model) {
super.setupController(controller, model); super.setupController(controller, model);
controller.set( controller.set(
"allPersonas", "allAgents",
this.modelFor("adminPlugins.show.discourse-ai-personas") 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"; const prefix = "discourse_ai.features.list.header";
return [ return [
i18n(`${prefix}.name`), i18n(`${prefix}.name`),
i18n(`${prefix}.persona`), i18n(`${prefix}.agent`),
i18n(`${prefix}.groups`), i18n(`${prefix}.groups`),
"", "",
]; ];
@ -75,21 +75,21 @@ export default RouteTemplate(
</span> </span>
</td> </td>
<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 <DButton
class="btn-flat btn-small ai-feature-list__row-item-persona" class="btn-flat btn-small ai-feature-list__row-item-agent"
@translatedLabel={{feature.persona.name}} @translatedLabel={{feature.agent.name}}
@route="adminPlugins.show.discourse-ai-personas.edit" @route="adminPlugins.show.discourse-ai-agents.edit"
@routeModels={{feature.persona.id}} @routeModels={{feature.agent.id}}
/> />
</td> </td>
<td <td
class="d-admin-row__detail ai-feature-list__row-item ai-feature-list__groups" 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"> <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> <li>{{group.name}}</li>
{{/each}} {{/each}}
</ul> </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 <AiToolEditor
@tools={{this.allTools}} @tools={{this.allTools}}
@model={{this.model}} @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 <AiToolEditor
@tools={{this.allTools}} @tools={{this.allTools}}
@model={{this.model}} @model={{this.model}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,18 +7,18 @@ module ::Jobs
def execute(args) def execute(args)
return unless bot_user = User.find_by(id: args[:bot_user_id]) return unless bot_user = User.find_by(id: args[:bot_user_id])
return unless post = Post.includes(:topic).find_by(id: args[:post_id]) return unless post = Post.includes(:topic).find_by(id: args[:post_id])
persona_id = args[:persona_id] agent_id = args[:agent_id]
begin begin
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id) agent = DiscourseAi::Agents::Agent.find_by(user: post.user, id: agent_id)
raise DiscourseAi::Personas::Bot::BOT_NOT_FOUND if persona.nil? 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) 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( 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
end end

View File

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

View File

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

View File

@ -1,16 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
class AiPersona < ActiveRecord::Base class AiAgent < ActiveRecord::Base
# TODO remove this line 01-10-2025 # TODO remove this line 01-10-2025
self.ignored_columns = %i[default_llm question_consolidator_llm] self.ignored_columns = %i[default_llm question_consolidator_llm]
# places a hard limit, so per site we cache a maximum of 500 classes # 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 :name, presence: true, uniqueness: true, length: { maximum: 100 }
validates :description, presence: true, length: { maximum: 2000 } validates :description, presence: true, length: { maximum: 2000 }
validates :system_prompt, presence: true, length: { maximum: 10_000_000 } 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 :chat_preconditions
validate :allowed_seeded_model, if: :default_llm_id validate :allowed_seeded_model, if: :default_llm_id
validate :well_formated_examples validate :well_formated_examples
@ -41,50 +41,50 @@ class AiPersona < ActiveRecord::Base
before_destroy :ensure_not_system before_destroy :ensure_not_system
before_update :regenerate_rag_fragments before_update :regenerate_rag_fragments
def self.persona_cache def self.agent_cache
@persona_cache ||= ::DiscourseAi::MultisiteHash.new("persona_cache") @agent_cache ||= ::DiscourseAi::MultisiteHash.new("agent_cache")
end end
scope :ordered, -> { order("priority DESC, lower(name) ASC") } scope :ordered, -> { order("priority DESC, lower(name) ASC") }
def self.all_personas(enabled_only: true) def self.all_agents(enabled_only: true)
persona_cache[:value] ||= AiPersona agent_cache[:value] ||= AiAgent
.ordered .ordered
.all .all
.limit(MAX_PERSONAS_PER_SITE) .limit(MAX_AGENTS_PER_SITE)
.map(&:class_instance) .map(&:class_instance)
if enabled_only if enabled_only
persona_cache[:value].select { |p| p.enabled } agent_cache[:value].select { |p| p.enabled }
else else
persona_cache[:value] agent_cache[:value]
end end
end end
def self.persona_users(user: nil) def self.agent_users(user: nil)
persona_users = agent_users =
persona_cache[:persona_users] ||= AiPersona agent_cache[:agent_users] ||= AiAgent
.where(enabled: true) .where(enabled: true)
.joins(:user) .joins(:user)
.map do |persona| .map do |agent|
{ {
id: persona.id, id: agent.id,
user_id: persona.user_id, user_id: agent.user_id,
username: persona.user.username_lower, username: agent.user.username_lower,
allowed_group_ids: persona.allowed_group_ids, allowed_group_ids: agent.allowed_group_ids,
default_llm_id: persona.default_llm_id, default_llm_id: agent.default_llm_id,
force_default_llm: persona.force_default_llm, force_default_llm: agent.force_default_llm,
allow_chat_channel_mentions: persona.allow_chat_channel_mentions, allow_chat_channel_mentions: agent.allow_chat_channel_mentions,
allow_chat_direct_messages: persona.allow_chat_direct_messages, allow_chat_direct_messages: agent.allow_chat_direct_messages,
allow_topic_mentions: persona.allow_topic_mentions, allow_topic_mentions: agent.allow_topic_mentions,
allow_personal_messages: persona.allow_personal_messages, allow_agentl_messages: agent.allow_agentl_messages,
} }
end end
if user 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 else
persona_users agent_users
end end
end end
@ -93,31 +93,31 @@ class AiPersona < ActiveRecord::Base
allow_chat_channel_mentions: false, allow_chat_channel_mentions: false,
allow_chat_direct_messages: false, allow_chat_direct_messages: false,
allow_topic_mentions: false, allow_topic_mentions: false,
allow_personal_messages: false allow_agentl_messages: false
) )
index = 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 = agents =
persona_cache[index.to_sym] ||= persona_users.select do |persona| agent_cache[index.to_sym] ||= agent_users.select do |agent|
next true if allow_chat_channel_mentions && persona[:allow_chat_channel_mentions] next true if allow_chat_channel_mentions && agent[:allow_chat_channel_mentions]
next true if allow_chat_direct_messages && persona[:allow_chat_direct_messages] next true if allow_chat_direct_messages && agent[:allow_chat_direct_messages]
next true if allow_topic_mentions && persona[:allow_topic_mentions] next true if allow_topic_mentions && agent[:allow_topic_mentions]
next true if allow_personal_messages && persona[:allow_personal_messages] next true if allow_agentl_messages && agent[:allow_agentl_messages]
false false
end end
if user 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 else
personas agents
end end
end end
after_commit :bump_cache after_commit :bump_cache
def bump_cache def bump_cache
self.class.persona_cache.flush! self.class.agent_cache.flush!
end end
def tools_can_not_be_duplicated def tools_can_not_be_duplicated
@ -138,7 +138,7 @@ class AiPersona < ActiveRecord::Base
end end
if seen_tools.include?(inner_name) 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 break
else else
seen_tools.add(inner_name) seen_tools.add(inner_name)
@ -154,7 +154,7 @@ class AiPersona < ActiveRecord::Base
.pluck(:tool_name) .pluck(:tool_name)
.each do |tool_name| .each do |tool_name|
if builtin_tool_names.include?(tool_name.downcase) 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 break
end end
end end
@ -176,7 +176,7 @@ class AiPersona < ActiveRecord::Base
allow_chat_channel_mentions allow_chat_channel_mentions
allow_chat_direct_messages allow_chat_direct_messages
allow_topic_mentions allow_topic_mentions
allow_personal_messages allow_agentl_messages
force_default_llm force_default_llm
name name
description description
@ -208,14 +208,14 @@ class AiPersona < ActiveRecord::Base
if inner_name.start_with?("custom-") if inner_name.start_with?("custom-")
custom_tool_id = inner_name.split("-", 2).last.to_i custom_tool_id = inner_name.split("-", 2).last.to_i
if AiTool.exists?(id: custom_tool_id, enabled: true) if AiTool.exists?(id: custom_tool_id, enabled: true)
klass = DiscourseAi::Personas::Tools::Custom.class_instance(custom_tool_id) klass = DiscourseAi::Agents::Tools::Custom.class_instance(custom_tool_id)
end end
else else
inner_name = inner_name.gsub("Tool", "") inner_name = inner_name.gsub("Tool", "")
inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name) inner_name = "List#{inner_name}" if %w[Categories Tags].include?(inner_name)
begin begin
klass = "DiscourseAi::Personas::Tools::#{inner_name}".constantize klass = "DiscourseAi::Agents::Tools::#{inner_name}".constantize
options[klass] = current_options if current_options options[klass] = current_options if current_options
rescue StandardError rescue StandardError
end end
@ -225,14 +225,14 @@ class AiPersona < ActiveRecord::Base
klass klass
end end
persona_class = DiscourseAi::Personas::Persona.system_personas_by_id[self.id] agent_class = DiscourseAi::Agents::Agent.system_agents_by_id[self.id]
if persona_class if agent_class
return( return(
# we need a new copy so we don't leak information # we need a new copy so we don't leak information
# across sites # across sites
Class.new(persona_class) do Class.new(agent_class) do
# required for localization # 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| instance_attributes.each do |key, value|
# description/name are localized # description/name are localized
define_singleton_method(key) { value } if key != :description && key != :name define_singleton_method(key) { value } if key != :description && key != :name
@ -242,9 +242,9 @@ class AiPersona < ActiveRecord::Base
) )
end 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 } } instance_attributes.each { |key, value| define_singleton_method(key) { value } }
define_singleton_method(:to_s) do define_singleton_method(:to_s) do
@ -254,24 +254,24 @@ class AiPersona < ActiveRecord::Base
define_singleton_method(:inspect) { to_s } define_singleton_method(:inspect) { to_s }
define_method(:initialize) do |*args, **kwargs| 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) super(*args, **kwargs)
end end
define_method(:tools) { tools } define_method(:tools) { tools }
define_method(:force_tool_use) { force_tool_use } 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(:options) { options }
define_method(:temperature) { @ai_persona&.temperature } define_method(:temperature) { @ai_agent&.temperature }
define_method(:top_p) { @ai_persona&.top_p } define_method(:top_p) { @ai_agent&.top_p }
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." } define_method(:system_prompt) { @ai_agent&.system_prompt || "You are a helpful bot." }
define_method(:uploads) { @ai_persona&.uploads } define_method(:uploads) { @ai_agent&.uploads }
define_method(:response_format) { @ai_persona&.response_format } define_method(:response_format) { @ai_agent&.response_format }
define_method(:examples) { @ai_persona&.examples } define_method(:examples) { @ai_agent&.examples }
end end
end end
FIRST_PERSONA_USER_ID = -1200 FIRST_AGENT_USER_ID = -1200
def create_user! def create_user!
raise "User already exists" if user_id && User.exists?(user_id) 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 # find the first id smaller than FIRST_USER_ID that is not taken
id = nil 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 ( WITH seq AS (
SELECT generate_series(?, ?, -1) AS id 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 || allow_chat_channel_mentions || allow_chat_direct_messages || allow_topic_mentions ||
force_default_llm force_default_llm
) && !default_llm_id ) && !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
end end
def system_persona_unchangeable def system_agent_unchangeable
error_msg = I18n.t("discourse_ai.ai_bot.personas.cannot_edit_system_persona") error_msg = I18n.t("discourse_ai.ai_bot.agents.cannot_edit_system_agent")
if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? || if top_p_changed? || temperature_changed? || system_prompt_changed? || name_changed? ||
description_changed? description_changed?
@ -356,7 +356,7 @@ class AiPersona < ActiveRecord::Base
def ensure_not_system def ensure_not_system
if 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 throw :abort
end end
end end
@ -380,13 +380,13 @@ class AiPersona < ActiveRecord::Base
return return
end end
errors.add(:examples, I18n.t("discourse_ai.personas.malformed_examples")) errors.add(:examples, I18n.t("discourse_ai.agents.malformed_examples"))
end end
end end
# == Schema Information # == Schema Information
# #
# Table name: ai_personas # Table name: ai_agents
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# name :string(100) not null # name :string(100) not null
@ -414,7 +414,7 @@ end
# allow_chat_channel_mentions :boolean default(FALSE), not null # allow_chat_channel_mentions :boolean default(FALSE), not null
# allow_chat_direct_messages :boolean default(FALSE), not null # allow_chat_direct_messages :boolean default(FALSE), not null
# allow_topic_mentions :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 # force_default_llm :boolean default(FALSE), not null
# rag_llm_model_id :bigint # rag_llm_model_id :bigint
# default_llm_id :bigint # default_llm_id :bigint
@ -424,5 +424,5 @@ end
# #
# Indexes # 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 end
def runner(parameters, llm:, bot_user:, context: nil) def runner(parameters, llm:, bot_user:, context: nil)
DiscourseAi::Personas::ToolRunner.new( DiscourseAi::Agents::ToolRunner.new(
parameters: parameters, parameters: parameters,
llm: llm, llm: llm,
bot_user: bot_user, bot_user: bot_user,
@ -45,10 +45,10 @@ class AiTool < ActiveRecord::Base
) )
end end
after_commit :bump_persona_cache after_commit :bump_agent_cache
def bump_persona_cache def bump_agent_cache
AiPersona.persona_cache.flush! AiAgent.agent_cache.flush!
end end
def regenerate_rag_fragments 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. * 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. * 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: * Parameters:
* name (string): The name of the target persona. * name (string): The name of the target agent.
* Returns: Object { respondTo: function(params) } or null if persona not found. * Returns: Object { respondTo: function(params) } or null if agent 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). * 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: * Parameters:
* params (Object, optional): { instructions: string, whisper: boolean } * params (Object, optional): { instructions: string, whisper: boolean }
* Returns: { success: boolean, post_id?: number, post_number?: number, message_id?: number } or { error: string } * 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). * 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). * 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). * 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). * participants (string): Comma-separated list of usernames in a PM (if applicable).
* // ... other potential context-specific properties added by the calling environment. * // ... other potential context-specific properties added by the calling environment.
* *

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
class LlmModelSerializer < ApplicationSerializer class LlmModelSerializer < ApplicationSerializer
# TODO: we probably should rename the table LlmModel to AiLlm # 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 # LLM model is a bit confusing given that large langauge model model is a confusing
# name # name
root "ai_llm" root "ai_llm"

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ const CREATE_ATTRIBUTES = [
"allow_chat", "allow_chat",
"tool_details", "tool_details",
"forced_tool_count", "forced_tool_count",
"allow_personal_messages", "allow_agentl_messages",
"allow_topic_mentions", "allow_topic_mentions",
"allow_chat_channel_mentions", "allow_chat_channel_mentions",
"allow_chat_direct_messages", "allow_chat_direct_messages",
@ -58,16 +58,16 @@ const SYSTEM_ATTRIBUTES = [
"rag_llm_model_id", "rag_llm_model_id",
"question_consolidator_llm_id", "question_consolidator_llm_id",
"tool_details", "tool_details",
"allow_personal_messages", "allow_agentl_messages",
"allow_topic_mentions", "allow_topic_mentions",
"allow_chat_channel_mentions", "allow_chat_channel_mentions",
"allow_chat_direct_messages", "allow_chat_direct_messages",
]; ];
export default class AiPersona extends RestModel { export default class AiAgent extends RestModel {
async createUser() { async createUser() {
const result = await ajax( 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", type: "POST",
} }
@ -143,10 +143,10 @@ export default class AiPersona extends RestModel {
fromPOJO(data) { fromPOJO(data) {
const dataClone = JSON.parse(JSON.stringify(data)); const dataClone = JSON.parse(JSON.stringify(data));
const persona = AiPersona.create(dataClone); const agent = AiAgent.create(dataClone);
persona.tools = this.flattenedToolStructure(dataClone); agent.tools = this.flattenedToolStructure(dataClone);
return persona; return agent;
} }
toPOJO() { toPOJO() {

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { eq } from "truth-helpers";
import icon from "discourse/helpers/d-icon"; import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
export default class AiPersonaCollapsableExample extends Component { export default class AiAgentCollapsableExample extends Component {
@tracked collapsed = true; @tracked collapsed = true;
get caretIcon() { get caretIcon() {
@ -26,7 +26,7 @@ export default class AiPersonaCollapsableExample extends Component {
} }
get exampleTitle() { 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, number: this.args.exampleNumber + 1,
}); });
} }
@ -41,7 +41,7 @@ export default class AiPersonaCollapsableExample extends Component {
<exPair.Field <exPair.Field
@title={{i18n @title={{i18n
(concat (concat
"discourse_ai.ai_persona.examples." "discourse_ai.ai_agent.examples."
(if (eq pairIdx 0) "user" "model") (if (eq pairIdx 0) "user" "model")
) )
}} }}
@ -57,8 +57,8 @@ export default class AiPersonaCollapsableExample extends Component {
<@form.Container> <@form.Container>
<@form.Button <@form.Button
@action={{this.deletePair}} @action={{this.deletePair}}
@label="discourse_ai.ai_persona.examples.remove" @label="discourse_ai.ai_agent.examples.remove"
class="ai-persona-editor__delete_example btn-danger" class="ai-agent-editor__delete_example btn-danger"
/> />
</@form.Container> </@form.Container>
{{/unless}} {{/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 { i18n } from "discourse-i18n";
import DropdownSelectBox from "select-kit/components/dropdown-select-box"; 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"; const LLM_SELECTOR_KEY = "ai_llm_selector_id";
export default class AiPersonaLlmSelector extends Component { export default class AiAgentLlmSelector extends Component {
@service currentUser; @service currentUser;
@service keyValueStore; @service keyValueStore;
@ -20,7 +20,7 @@ export default class AiPersonaLlmSelector extends Component {
super(...arguments); super(...arguments);
if (this.botOptions?.length) { if (this.botOptions?.length) {
this.#loadStoredPersona(); this.#loadStoredAgent();
this.#loadStoredLlm(); this.#loadStoredLlm();
next(() => { next(() => {
@ -34,25 +34,25 @@ export default class AiPersonaLlmSelector extends Component {
} }
get hasLlmSelector() { 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() { get botOptions() {
if (!this.currentUser.ai_enabled_personas) { if (!this.currentUser.ai_enabled_agents) {
return; return;
} }
let enabledPersonas = this.currentUser.ai_enabled_personas; let enabledAgents = this.currentUser.ai_enabled_agents;
if (!this.hasLlmSelector) { if (!this.hasLlmSelector) {
enabledPersonas = enabledPersonas.filter((persona) => persona.username); enabledAgents = enabledAgents.filter((agent) => agent.username);
} }
return enabledPersonas.map((persona) => { return enabledAgents.map((agent) => {
return { return {
id: persona.id, id: agent.id,
name: persona.name, name: agent.name,
description: persona.description, description: agent.description,
}; };
}); });
} }
@ -67,8 +67,8 @@ export default class AiPersonaLlmSelector extends Component {
set value(newValue) { set value(newValue) {
this._value = newValue; this._value = newValue;
this.keyValueStore.setItem(PERSONA_SELECTOR_KEY, newValue); this.keyValueStore.setItem(AGENT_SELECTOR_KEY, newValue);
this.args.setPersonaId(newValue); this.args.setAgentId(newValue);
this.setAllowLLMSelector(); this.setAllowLLMSelector();
this.resetTargetRecipients(); this.resetTargetRecipients();
} }
@ -79,11 +79,11 @@ export default class AiPersonaLlmSelector extends Component {
return; return;
} }
const persona = this.currentUser.ai_enabled_personas.find( const agent = this.currentUser.ai_enabled_agents.find(
(innerPersona) => innerPersona.id === this._value (innerAgent) => innerAgent.id === this._value
); );
this.allowLLMSelector = !persona?.force_default_llm; this.allowLLMSelector = !agent?.force_default_llm;
} }
get currentLlm() { get currentLlm() {
@ -104,16 +104,16 @@ export default class AiPersonaLlmSelector extends Component {
).username; ).username;
this.args.setTargetRecipient(botUsername); this.args.setTargetRecipient(botUsername);
} else { } else {
const persona = this.currentUser.ai_enabled_personas.find( const agent = this.currentUser.ai_enabled_agents.find(
(innerPersona) => innerPersona.id === this._value (innerAgent) => innerAgent.id === this._value
); );
this.args.setTargetRecipient(persona.username || ""); this.args.setTargetRecipient(agent.username || "");
} }
} }
get llmOptions() { get llmOptions() {
const availableBots = this.currentUser.ai_enabled_chat_bots const availableBots = this.currentUser.ai_enabled_chat_bots
.filter((bot) => !bot.is_persona) .filter((bot) => !bot.is_agent)
.filter(Boolean); .filter(Boolean);
return availableBots return availableBots
@ -130,18 +130,18 @@ export default class AiPersonaLlmSelector extends Component {
return this.allowLLMSelector && this.llmOptions.length > 1; return this.allowLLMSelector && this.llmOptions.length > 1;
} }
#loadStoredPersona() { #loadStoredAgent() {
let personaId = this.keyValueStore.getItem(PERSONA_SELECTOR_KEY); let agentId = this.keyValueStore.getItem(AGENT_SELECTOR_KEY);
this._value = this.botOptions[0].id; this._value = this.botOptions[0].id;
if (personaId) { if (agentId) {
personaId = parseInt(personaId, 10); agentId = parseInt(agentId, 10);
if (this.botOptions.any((bot) => bot.id === personaId)) { if (this.botOptions.any((bot) => bot.id === agentId)) {
this._value = personaId; this._value = agentId;
} }
} }
this.args.setPersonaId(this._value); this.args.setAgentId(this._value);
} }
#loadStoredLlm() { #loadStoredLlm() {
@ -172,13 +172,13 @@ export default class AiPersonaLlmSelector extends Component {
} }
<template> <template>
<div class="persona-llm-selector"> <div class="agent-llm-selector">
<div class="persona-llm-selector__selection-wrapper gpt-persona"> <div class="agent-llm-selector__selection-wrapper gpt-agent">
{{#if @showLabels}} {{#if @showLabels}}
<label>{{i18n "discourse_ai.ai_bot.persona"}}</label> <label>{{i18n "discourse_ai.ai_bot.agent"}}</label>
{{/if}} {{/if}}
<DropdownSelectBox <DropdownSelectBox
class="persona-llm-selector__persona-dropdown" class="agent-llm-selector__agent-dropdown"
@value={{this.value}} @value={{this.value}}
@content={{this.botOptions}} @content={{this.botOptions}}
@options={{hash @options={{hash
@ -188,12 +188,12 @@ export default class AiPersonaLlmSelector extends Component {
/> />
</div> </div>
{{#if this.showLLMSelector}} {{#if this.showLLMSelector}}
<div class="persona-llm-selector__selection-wrapper llm-selector"> <div class="agent-llm-selector__selection-wrapper llm-selector">
{{#if @showLabels}} {{#if @showLabels}}
<label>{{i18n "discourse_ai.ai_bot.llm"}}</label> <label>{{i18n "discourse_ai.ai_bot.llm"}}</label>
{{/if}} {{/if}}
<DropdownSelectBox <DropdownSelectBox
class="persona-llm-selector__llm-dropdown" class="agent-llm-selector__llm-dropdown"
@value={{this.currentLlm}} @value={{this.currentLlm}}
@content={{this.llmOptions}} @content={{this.llmOptions}}
@options={{hash icon=(if @showLabels "angle-down" "globe")}} @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 { i18n } from "discourse-i18n";
import AiLlmSelector from "./ai-llm-selector"; import AiLlmSelector from "./ai-llm-selector";
export default class AiPersonaToolOptions extends Component { export default class AiAgentToolOptions extends Component {
get showToolOptions() { get showToolOptions() {
const allTools = this.args.allTools; const allTools = this.args.allTools;
if (!allTools || !this.args.data.tools) { if (!allTools || !this.args.data.tools) {
@ -36,19 +36,19 @@ export default class AiPersonaToolOptions extends Component {
<template> <template>
{{#if this.showToolOptions}} {{#if this.showToolOptions}}
<@form.Container <@form.Container
@title={{i18n "discourse_ai.ai_persona.tool_options"}} @title={{i18n "discourse_ai.ai_agent.tool_options"}}
@direction="column" @direction="column"
@format="full" @format="full"
> >
<@form.Object <@form.Object
@name="toolOptions" @name="toolOptions"
@title={{i18n "discourse_ai.ai_persona.tool_options"}} @title={{i18n "discourse_ai.ai_agent.tool_options"}}
as |toolObj optsPerTool| as |toolObj optsPerTool|
> >
{{#each (this.formObjectKeys optsPerTool) as |toolId|}} {{#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|}} {{#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}} {{toolMeta.name}}
</div> </div>
<toolObj.Object @name={{toolId}} as |optionsObj optionData|> <toolObj.Object @name={{toolId}} as |optionsObj optionData|>
@ -73,7 +73,7 @@ export default class AiPersonaToolOptions extends Component {
@value={{field.value}} @value={{field.value}}
@llms={{@llms}} @llms={{@llms}}
@onChange={{field.set}} @onChange={{field.set}}
@class="ai-persona-tool-option-editor__llms" @class="ai-agent-tool-option-editor__llms"
/> />
</field.Custom> </field.Custom>
{{else if (eq optionMeta.type "boolean")}} {{else if (eq optionMeta.type "boolean")}}

View File

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

View File

@ -116,7 +116,7 @@ export default class AiLlmEditorForm extends Component {
const localized = usedBy.map((m) => { const localized = usedBy.map((m) => {
return i18n(`discourse_ai.llms.usage.${m.type}`, { 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}} @onChange={{@onChange}}
@options={{hash @options={{hash
filterable=true filterable=true
none="discourse_ai.ai_persona.no_llm_selected" none="discourse_ai.ai_agent.no_llm_selected"
}} }}
class={{@class}} class={{@class}}
/> />

View File

@ -112,9 +112,9 @@ export default class AiLlmsListEditor extends Component {
} }
localizeUsage(usage) { localizeUsage(usage) {
if (usage.type === "ai_persona") { if (usage.type === "ai_agent") {
return i18n("discourse_ai.llms.usage.ai_persona", { return i18n("discourse_ai.llms.usage.ai_agent", {
persona: usage.name, agent: usage.name,
}); });
} else if (usage.type === "automation") { } else if (usage.type === "automation") {
return i18n("discourse_ai.llms.usage.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() { get canContinueConversation() {
const personas = this.currentUser?.ai_enabled_personas; const agents = this.currentUser?.ai_enabled_agents;
if (!personas) { if (!agents) {
return false; return false;
} }
@ -163,16 +163,16 @@ export default class AiSearchDiscoveries extends Component {
return false; return false;
} }
const discoverPersona = personas.find( const discoverAgent = agents.find(
(persona) => (agent) =>
persona.id === parseInt(this.siteSettings?.ai_bot_discover_persona, 10) agent.id === parseInt(this.siteSettings?.ai_bot_discover_agent, 10)
); );
const discoverPersonaHasBot = discoverPersona?.username; const discoverAgentHasBot = discoverAgent?.username;
return ( return (
this.discobotDiscoveries.discovery?.length > 0 && this.discobotDiscoveries.discovery?.length > 0 &&
!this.smoothStreamer.isStreaming && !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 { prettyJSON } from "discourse/lib/formatter";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
export default class AiPersonaResponseFormatEditor extends Component { export default class AiAgentResponseFormatEditor extends Component {
@tracked showJsonEditorModal = false; @tracked showJsonEditorModal = false;
jsonSchema = { jsonSchema = {
type: "array", type: "array",
uniqueItems: true, 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: { items: {
type: "object", 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: { properties: {
key: { key: {
type: "string", type: "string",
@ -30,7 +30,7 @@ export default class AiPersonaResponseFormatEditor extends Component {
}; };
get editorTitle() { get editorTitle() {
return i18n("discourse_ai.ai_persona.response_format.title"); return i18n("discourse_ai.ai_agent.response_format.title");
} }
get responseFormatAsJSON() { get responseFormatAsJSON() {
@ -64,21 +64,21 @@ export default class AiPersonaResponseFormatEditor extends Component {
<template> <template>
<@form.Container @title={{this.editorTitle}} @format="large"> <@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)}} {{#if (gt @data.response_format.length 0)}}
<pre class="ai-persona-editor__response-format-pre"> <pre class="ai-agent-editor__response-format-pre">
<code <code
>{{this.displayJSON}}</code> >{{this.displayJSON}}</code>
</pre> </pre>
{{else}} {{else}}
<div class="ai-persona-editor__response-format-none"> <div class="ai-agent-editor__response-format-none">
{{i18n "discourse_ai.ai_persona.response_format.no_format"}} {{i18n "discourse_ai.ai_agent.response_format.no_format"}}
</div> </div>
{{/if}} {{/if}}
<@form.Button <@form.Button
@action={{this.openModal}} @action={{this.openModal}}
@label="discourse_ai.ai_persona.response_format.open_modal" @label="discourse_ai.ai_agent.response_format.open_modal"
@disabled={{@data.system}} @disabled={{@data.system}}
/> />
</div> </div>

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@ import AiSearchDiscoveriesTooltip from "../../components/ai-search-discoveries-t
export default class AiFullPageDiscobotDiscoveries extends Component { export default class AiFullPageDiscobotDiscoveries extends Component {
static shouldRender(_args, { siteSettings, currentUser }) { static shouldRender(_args, { siteSettings, currentUser }) {
return ( return (
siteSettings.ai_bot_discover_persona && siteSettings.ai_bot_discover_agent &&
currentUser?.can_use_ai_bot_discover_persona && currentUser?.can_use_ai_bot_discover_agent &&
currentUser?.user_option?.ai_search_discoveries 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 { export default class AiDiscobotDiscoveries extends Component {
static shouldRender(args, { siteSettings, currentUser }) { static shouldRender(args, { siteSettings, currentUser }) {
return ( return (
siteSettings.ai_bot_discover_persona && siteSettings.ai_bot_discover_agent &&
currentUser?.can_use_ai_bot_discover_persona && currentUser?.can_use_ai_bot_discover_agent &&
currentUser?.user_option?.ai_search_discoveries 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, checked: this.model.user_option.ai_search_discoveries,
isIncluded: (() => { isIncluded: (() => {
return ( return (
this.siteSettings.ai_bot_discover_persona && this.siteSettings.ai_bot_discover_agent &&
this.model?.can_use_ai_bot_discover_persona && this.model?.can_use_ai_bot_discover_agent &&
this.siteSettings.ai_bot_enabled this.siteSettings.ai_bot_enabled
); );
})(), })(),

View File

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

View File

@ -16,7 +16,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
@tracked loading = false; @tracked loading = false;
personaId; agentId;
targetUsername; targetUsername;
uploads = []; uploads = [];
@ -35,12 +35,12 @@ export default class AiBotConversationsHiddenSubmit extends Service {
async submitToBot() { async submitToBot() {
if ( if (
this.inputValue.length < this.inputValue.length <
this.siteSettings.min_personal_message_post_length this.siteSettings.min_agentl_message_post_length
) { ) {
return this.dialog.alert({ return this.dialog.alert({
message: i18n( message: i18n(
"discourse_ai.ai_bot.conversations.min_input_length_message", "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(), didConfirm: () => this.focusInput(),
didCancel: () => this.focusInput(), didCancel: () => this.focusInput(),
@ -78,7 +78,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
title, title,
archetype: "private_message", archetype: "private_message",
target_recipients: this.targetUsername, 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", description: "discourse_ai.llms.preconfigured.description",
}, },
{ {
label: "discourse_ai.ai_persona.short_title", label: "discourse_ai.ai_agent.short_title",
route: "adminPlugins.show.discourse-ai-personas", route: "adminPlugins.show.discourse-ai-agents",
description: "discourse_ai.ai_persona.persona_description", description: "discourse_ai.ai_agent.agent_description",
}, },
{ {
label: "discourse_ai.embeddings.short_title", 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 { withPluginApi } from "discourse/lib/plugin-api";
import { registerWidgetShim } from "discourse/widgets/render-glimmer"; import { registerWidgetShim } from "discourse/widgets/render-glimmer";
import AiBotHeaderIcon from "../discourse/components/ai-bot-header-icon"; 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 AiCancelStreamingButton from "../discourse/components/post-menu/ai-cancel-streaming-button";
import AiDebugButton from "../discourse/components/post-menu/ai-debug-button"; import AiDebugButton from "../discourse/components/post-menu/ai-debug-button";
import AiShareButton from "../discourse/components/post-menu/ai-share-button"; import AiShareButton from "../discourse/components/post-menu/ai-share-button";
@ -53,35 +53,35 @@ function initializeAIBotReplies(api) {
}); });
} }
function initializePersonaDecorator(api) { function initializeAgentDecorator(api) {
api.renderAfterWrapperOutlet("post-meta-data-poster-name", AiPersonaFlair); api.renderAfterWrapperOutlet("post-meta-data-poster-name", AiAgentFlair);
withSilencedDeprecations("discourse.post-stream-widget-overrides", () => withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
initializeWidgetPersonaDecorator(api) initializeWidgetAgentDecorator(api)
); );
} }
function initializeWidgetPersonaDecorator(api) { function initializeWidgetAgentDecorator(api) {
api.decorateWidget(`poster-name:after`, (dec) => { api.decorateWidget(`poster-name:after`, (dec) => {
const botType = getBotType(dec.attrs.user); const botType = getBotType(dec.attrs.user);
// we have 2 ways of decorating // we have 2 ways of decorating
// 1. if a bot is a LLM we decorate with persona name // 1. if a bot is a LLM we decorate with agent name
// 2. if bot is a persona we decorate with LLM name // 2. if bot is a agent we decorate with LLM name
if (botType === "llm") { if (botType === "llm") {
return dec.widget.attach("persona-flair", { return dec.widget.attach("agent-flair", {
personaName: dec.model?.topic?.ai_persona_name, agentName: dec.model?.topic?.ai_agent_name,
}); });
} else if (botType === "persona") { } else if (botType === "agent") {
return dec.widget.attach("persona-flair", { return dec.widget.attach("agent-flair", {
personaName: dec.model?.llm_name, agentName: dec.model?.llm_name,
}); });
} }
}); });
registerWidgetShim( registerWidgetShim(
"persona-flair", "agent-flair",
"span.persona-flair", "span.agent-flair",
hbs`{{@data.personaName}}` hbs`{{@data.agentName}}`
); );
} }
@ -149,11 +149,11 @@ function initializeShareTopicButton(api) {
showShareConversationModal(modal, this.topic.id); showShareConversationModal(modal, this.topic.id);
}, },
classNames: ["share-ai-conversation-button"], classNames: ["share-ai-conversation-button"],
dependentKeys: ["topic.ai_persona_name"], dependentKeys: ["topic.ai_agent_name"],
displayed() { displayed() {
return ( return (
currentUser?.can_share_ai_bot_conversations && currentUser?.can_share_ai_bot_conversations &&
this.topic.ai_persona_name this.topic.ai_agent_name
); );
}, },
}); });
@ -171,7 +171,7 @@ export default {
withPluginApi((api) => { withPluginApi((api) => {
attachHeaderIcon(api); attachHeaderIcon(api);
initializeAIBotReplies(api); initializeAIBotReplies(api);
initializePersonaDecorator(api); initializeAgentDecorator(api);
initializeDebugButton(api, container); initializeDebugButton(api, container);
initializeShareButton(api, container); initializeShareButton(api, container);
initializeShareTopicButton(api, container); initializeShareTopicButton(api, container);

View File

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

View File

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

View File

@ -157,7 +157,7 @@ body.has-ai-conversations-sidebar {
flex-direction: column; flex-direction: column;
height: calc(100dvh - var(--header-offset) - 5em); height: calc(100dvh - var(--header-offset) - 5em);
.persona-llm-selector { .agent-llm-selector {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
justify-content: flex-start; 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; margin-top: 0;
} }
.ai-persona-list-editor { .ai-agent-list-editor {
&__header { &__header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -23,7 +23,7 @@
} }
} }
.ai-persona-tool-option-editor { .ai-agent-tool-option-editor {
&__instructions { &__instructions {
color: var(--primary-medium); color: var(--primary-medium);
font-size: var(--font-down-1); font-size: var(--font-down-1);
@ -31,7 +31,7 @@
} }
} }
.ai-personas__container { .ai-agents__container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -39,7 +39,7 @@
width: 100%; width: 100%;
} }
.ai-persona-editor { .ai-agent-editor {
padding-left: 0.5em; padding-left: 0.5em;
&__tool-options { &__tool-options {

View File

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

View File

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

View File

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

View File

@ -6,8 +6,8 @@ en:
descriptions: descriptions:
discourse_ai: discourse_ai:
search: "Allows AI search" search: "Allows AI search"
stream_completion: "Allows streaming AI persona completions" stream_completion: "Allows streaming AI agent completions"
update_personas: "Allows updating AI personas" update_agents: "Allows updating AI agents"
site_settings: site_settings:
categories: categories:
@ -100,17 +100,17 @@ en:
label: "Tool" label: "Tool"
description: "Tool to use for triage (tool must have no parameters defined)" description: "Tool to use for triage (tool must have no parameters defined)"
llm_persona_triage: llm_agent_triage:
fields: fields:
persona: agent:
label: "Persona" label: "Agent"
description: "AI Persona to use for triage (must have default LLM and User set)" description: "AI Agent to use for triage (must have default LLM and User set)"
whisper: whisper:
label: "Reply as 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: silent_mode:
label: "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: llm_triage:
fields: fields:
system_prompt: system_prompt:
@ -146,15 +146,15 @@ en:
flag_post: flag_post:
label: "Flag post" label: "Flag post"
description: "Flags post (either as spam or for review)" description: "Flags post (either as spam or for review)"
include_personal_messages: include_agentl_messages:
label: "Include personal messages" label: "Include agentl messages"
description: "Also scan and triage personal messages" description: "Also scan and triage agentl messages"
whisper: whisper:
label: "Reply as Whisper" label: "Reply as Whisper"
description: "Whether the AI's response should be a whisper" description: "Whether the AI's response should be a whisper"
reply_persona: reply_agent:
label: "Reply Persona" label: "Reply Agent"
description: "AI Persona to use for replies (must have default LLM), will be prioritized over canned reply" description: "AI Agent to use for replies (must have default LLM), will be prioritized over canned reply"
model: model:
label: "Model" label: "Model"
description: "Language model used for triage" description: "Language model used for triage"
@ -167,12 +167,12 @@ en:
features: features:
short_title: "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" back: "Back"
list: list:
header: header:
name: "Name" name: "Name"
persona: "Persona" agent: "Agent"
groups: "Groups" groups: "Groups"
edit: "Edit" edit: "Edit"
set_up: "Set up" set_up: "Set up"
@ -257,7 +257,7 @@ en:
last_month: "Last month" last_month: "Last month"
custom: "Custom..." custom: "Custom..."
ai_persona: ai_agent:
ai_tools: "Tools" ai_tools: "Tools"
tool_strategies: tool_strategies:
all: "Apply to all replies" all: "Apply to all replies"
@ -269,7 +269,7 @@ en:
edit: "Edit" edit: "Edit"
description: "Description" description: "Description"
no_llm_selected: "No language model selected" 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: "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)" 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 vision_enabled: Vision enabled
@ -282,47 +282,47 @@ en:
tool_details: Show tool details tool_details: Show tool details
tool_details_help: Will show end users details on which tools the language model has triggered. tool_details_help: Will show end users details on which tools the language model has triggered.
mentionable: Allow mentions 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 user: User
create_user: Create 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: 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: 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. 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 system_prompt: System prompt
forced_tool_strategy: Forced tool strategy forced_tool_strategy: Forced tool strategy
allow_chat_direct_messages: "Allow chat direct messages" 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: "Allow chat channel mentions"
allow_chat_channel_mentions_help: "If enabled, users in allowed groups can mention this persona in chat channels." allow_chat_channel_mentions_help: "If enabled, users in allowed groups can mention this agent in chat channels."
allow_personal_messages: "Allow personal messages" allow_agentl_messages: "Allow agentl messages"
allow_personal_messages_help: "If enabled, users in allowed groups can send personal messages to this persona." 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: "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" force_default_llm: "Always use default language model"
save: "Save" save: "Save"
saved: "Persona saved" saved: "Agent saved"
enabled: "Enabled?" enabled: "Enabled?"
tools: "Enabled tools" tools: "Enabled tools"
forced_tools: "Forced tools" forced_tools: "Forced tools"
allowed_groups: "Allowed groups" allowed_groups: "Allowed groups"
confirm_delete: "Are you sure you want to delete this persona?" confirm_delete: "Are you sure you want to delete this agent?"
new: "New persona" new: "New agent"
no_personas: "You have not created any personas yet" no_agents: "You have not created any agents yet"
title: "Personas" title: "Agents"
short_title: "Personas" short_title: "Agents"
delete: "Delete" delete: "Delete"
temperature: "Temperature" 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)" 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: "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)" 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: "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" tool_options: "Tool options"
rag_conversation_chunks: "Search conversation chunks" 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." 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: response_format:
title: "JSON response format" title: "JSON response format"
no_format: "No JSON format specified" no_format: "No JSON format specified"
@ -344,7 +344,7 @@ en:
ai_bot: ai_bot:
title: "AI bot options" 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: rag:
title: "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)" 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" new: "New tool"
tool_name: "Tool Name" 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: "Description"
description_help: "A clear description of the tool's purpose for the language model" 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." subheader_description: "Tools extend the capabilities of AI bots with user-defined JavaScript functions."
@ -455,7 +455,7 @@ en:
ai_bot: "AI bot" ai_bot: "AI bot"
ai_helper: "Helper" ai_helper: "Helper"
ai_helper_image_caption: "Image caption" ai_helper_image_caption: "Image caption"
ai_persona: "Persona (%{persona})" ai_agent: "Agent (%{agent})"
ai_summarization: "Summarize" ai_summarization: "Summarize"
ai_embeddings_semantic_search: "AI search" ai_embeddings_semantic_search: "AI search"
ai_spam: "Spam" ai_spam: "Spam"
@ -681,7 +681,7 @@ en:
click_to_run_label: "Run Artifact" click_to_run_label: "Run Artifact"
ai_bot: ai_bot:
persona: "Persona" agent: "Agent"
llm: "Model" llm: "Model"
pm_warning: "AI chatbot messages are monitored regularly by moderators." pm_warning: "AI chatbot messages are monitored regularly by moderators."
cancel_streaming: "Stop reply" cancel_streaming: "Stop reply"

View File

@ -10,9 +10,9 @@ en:
llm_tool_triage: llm_tool_triage:
title: Triage posts using AI Tool title: Triage posts using AI Tool
description: "Triage posts using custom logic in an AI tool" description: "Triage posts using custom logic in an AI tool"
llm_persona_triage: llm_agent_triage:
title: Triage posts using AI Persona title: Triage posts using AI Agent
description: "Respond to posts using a specific AI persona" description: "Respond to posts using a specific AI agent"
llm_triage: llm_triage:
title: Triage posts using AI title: Triage posts using AI
description: "Triage posts using a large language model" 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_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_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_enabled: "Use Semantic Search for related topics."
ai_embeddings_semantic_related_topics: "Maximum number of topics to show in related topic section." 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." 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_enabled: "Enable the summarize feature"
ai_summarization_model: "Model to use for summarization" 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_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_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" 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_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_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_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_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)" 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_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_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_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_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" 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." description: "This report provides sentiment analysis for posts, grouped by category, with positive, negative, and neutral scores for each post and category."
overall_sentiment: overall_sentiment:
title: "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(%)" xaxis: "Positive(%)"
yaxis: "Date" yaxis: "Date"
emotion_admiration: emotion_admiration:
@ -267,8 +267,8 @@ en:
title: "%{title} - AI Conversation - %{site_name}" title: "%{title} - AI Conversation - %{site_name}"
errors: errors:
not_allowed: "You are not allowed to share this topic" not_allowed: "You are not allowed to share this topic"
other_people_in_pm: "Personal messages with other humans cannot be shared publicly" other_people_in_pm: "Agentl messages with other humans cannot be shared publicly"
other_content_in_pm: "Personal messages containing posts from other people 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" failed_to_share: "Failed to share the conversation"
conversation_deleted: "Conversation share deleted successfully" conversation_deleted: "Conversation share deleted successfully"
spam_detection: 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]" 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]" default_pm_prefix: "[Untitled AI bot PM]"
thinking: "Thinking..." thinking: "Thinking..."
personas: agents:
default_llm_required: "Default LLM model is required prior to enabling Chat" 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_delete_system_agent: "System agents 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_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" cannot_have_duplicate_tools: "Can not have duplicate tools"
github_helper: github_helper:
name: "GitHub Helper" name: "GitHub Helper"
@ -326,10 +326,10 @@ en:
description: "AI Bot specialized in creating interactive web artifacts" description: "AI Bot specialized in creating interactive web artifacts"
summarizer: summarizer:
name: "Summarizer" name: "Summarizer"
description: "Default persona used to power AI summaries" description: "Default agent used to power AI summaries"
short_summarizer: short_summarizer:
name: "Summarizer (short form)" 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!" topic_not_found: "Summary unavailable, topic not found!"
summarizing: "Summarizing topic" summarizing: "Summarizing topic"
searching: "Searching for: '%{query}'" 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." 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." cannot_edit_builtin: "You can't edit a built-in model."
personas: agents:
malformed_examples: "The given examples have the wrong format." malformed_examples: "The given examples have the wrong format."
embeddings: embeddings:
@ -554,12 +554,12 @@ en:
quota_exceeded: "You have exceeded the quota for this model. Please try again in %{relative_time}." 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" quota_required: "You must specify maximum tokens or usages for this model"
no_query_specified: The query parameter is required, please specify it. 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. no_user_for_agent: The agent 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. 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. 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. 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. agent_disabled: The agent specified is disabled. Check the agent_name or agent_id params.
no_default_llm: The persona must have a default_llm defined. no_default_llm: The agent must have a default_llm defined.
user_not_allowed: The user is not allowed to participate in the topic. 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. prompt_message_length: The message %{idx} is over the 1000 character limit.
dashboard: dashboard:

View File

@ -63,12 +63,12 @@ Discourse::Application.routes.draw do
:constraints => StaffConstraint.new :constraints => StaffConstraint.new
scope "/admin/plugins/discourse-ai", constraints: AdminConstraint.new do scope "/admin/plugins/discourse-ai", constraints: AdminConstraint.new do
resources :ai_personas, resources :ai_agents,
only: %i[index new create edit update destroy], only: %i[index new create edit update destroy],
path: "ai-personas", path: "ai-agents",
controller: "discourse_ai/admin/ai_personas" 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( resources(
:ai_tools, :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-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" put "/ai-agents/:id/files/remove", to: "discourse_ai/admin/ai_agents#remove_file"
get "/ai-personas/:id/files/status", to: "discourse_ai/admin/ai_personas#indexing_status_check" get "/ai-agents/:id/files/status", to: "discourse_ai/admin/ai_agents#indexing_status_check"
post "/rag-document-fragments/files/upload", post "/rag-document-fragments/files/upload",
to: "discourse_ai/admin/rag_document_fragments#upload_file" 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 # frozen_string_literal: true
if defined?(DiscourseAutomation) if defined?(DiscourseAutomation)
DiscourseAutomation::Scriptable.add("llm_persona_triage") do DiscourseAutomation::Scriptable.add("llm_agent_triage") do
version 1 version 1
run_in_background run_in_background
triggerables %i[post_created_edited] triggerables %i[post_created_edited]
field :persona, field :agent,
component: :choices, component: :choices,
required: true, required: true,
extra: { extra: {
content: DiscourseAi::Automation.available_persona_choices, content: DiscourseAi::Automation.available_agent_choices,
} }
field :whisper, component: :boolean field :whisper, component: :boolean
field :silent_mode, component: :boolean field :silent_mode, component: :boolean
@ -20,28 +20,28 @@ if defined?(DiscourseAutomation)
post = context["post"] post = context["post"]
next if post&.user&.bot? next if post&.user&.bot?
persona_id = fields.dig("persona", "value") agent_id = fields.dig("agent", "value")
whisper = !!fields.dig("whisper", "value") whisper = !!fields.dig("whisper", "value")
silent_mode = !!fields.dig("silent_mode", "value") silent_mode = !!fields.dig("silent_mode", "value")
begin begin
RateLimiter.new( RateLimiter.new(
Discourse.system_user, Discourse.system_user,
"llm_persona_triage_#{post.id}", "llm_agent_triage_#{post.id}",
SiteSetting.ai_automation_max_triage_per_post_per_minute, SiteSetting.ai_automation_max_triage_per_post_per_minute,
1.minute, 1.minute,
).performed! ).performed!
RateLimiter.new( RateLimiter.new(
Discourse.system_user, Discourse.system_user,
"llm_persona_triage", "llm_agent_triage",
SiteSetting.ai_automation_max_triage_per_minute, SiteSetting.ai_automation_max_triage_per_minute,
1.minute, 1.minute,
).performed! ).performed!
DiscourseAi::Automation::LlmPersonaTriage.handle( DiscourseAi::Automation::LlmAgentTriage.handle(
post: post, post: post,
persona_id: persona_id, agent_id: agent_id,
whisper: whisper, whisper: whisper,
automation: self.automation, automation: self.automation,
silent_mode: silent_mode, silent_mode: silent_mode,
@ -49,7 +49,7 @@ if defined?(DiscourseAutomation)
rescue => e rescue => e
Discourse.warn_exception( Discourse.warn_exception(
e, 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? raise e if Rails.env.tests?
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module DiscourseAi module DiscourseAi
module Personas module Agents
class Bot class Bot
attr_reader :model attr_reader :model
@ -13,19 +13,19 @@ module DiscourseAi
# limit is arbitrary, but 5 which was used in the past was too low # limit is arbitrary, but 5 which was used in the past was too low
MAX_TOOLS = 20 MAX_TOOLS = 20
def self.as(bot_user, persona: DiscourseAi::Personas::General.new, model: nil) def self.as(bot_user, agent: DiscourseAi::Agents::General.new, model: nil)
new(bot_user, persona, model) new(bot_user, agent, model)
end end
def initialize(bot_user, persona, model = nil) def initialize(bot_user, agent, model = nil)
@bot_user = bot_user @bot_user = bot_user
@persona = persona @agent = agent
@model = @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 end
attr_reader :bot_user attr_reader :bot_user
attr_accessor :persona attr_accessor :agent
def llm def llm
DiscourseAi::Completions::Llm.proxy(model) DiscourseAi::Completions::Llm.proxy(model)
@ -35,12 +35,12 @@ module DiscourseAi
return if prompt.tool_choice == :none return if prompt.tool_choice == :none
context.chosen_tools ||= [] 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) } 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 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 end
if force_tool if force_tool
@ -57,7 +57,7 @@ module DiscourseAi
end end
context.cancel_manager ||= DiscourseAi::Completions::CancelManager.new context.cancel_manager ||= DiscourseAi::Completions::CancelManager.new
current_llm = llm current_llm = llm
prompt = persona.craft_prompt(context, llm: current_llm) prompt = agent.craft_prompt(context, llm: current_llm)
total_completions = 0 total_completions = 0
ongoing_chain = true ongoing_chain = true
@ -67,11 +67,11 @@ module DiscourseAi
llm_kwargs = llm_args.dup llm_kwargs = llm_args.dup
llm_kwargs[:user] = user llm_kwargs[:user] = user
llm_kwargs[:temperature] = persona.temperature if persona.temperature llm_kwargs[:temperature] = agent.temperature if agent.temperature
llm_kwargs[:top_p] = persona.top_p if persona.top_p llm_kwargs[:top_p] = agent.top_p if agent.top_p
llm_kwargs[:response_format] = build_json_schema( llm_kwargs[:response_format] = build_json_schema(
persona.response_format, agent.response_format,
) if persona.response_format.present? ) if agent.response_format.present?
needs_newlines = false needs_newlines = false
tools_ran = 0 tools_ran = 0
@ -82,7 +82,7 @@ module DiscourseAi
tool_halted = false 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 existing_tools = Set.new
current_thinking = [] current_thinking = []
@ -96,7 +96,7 @@ module DiscourseAi
**llm_kwargs, **llm_kwargs,
) do |partial| ) do |partial|
tool = tool =
persona.find_tool( agent.find_tool(
partial, partial,
bot_user: user, bot_user: user,
llm: current_llm, llm: current_llm,
@ -183,7 +183,7 @@ module DiscourseAi
end end
def returns_json? def returns_json?
persona.response_format.present? agent.response_format.present?
end end
private private
@ -285,7 +285,7 @@ module DiscourseAi
def self.guess_model(bot_user) def self.guess_model(bot_user)
associated_llm = LlmModel.find_by(user_id: bot_user.id) 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 associated_llm
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module Personas module Agents
class Persona class Agent
class << self class << self
def default_enabled def default_enabled
true true
@ -36,8 +36,8 @@ module DiscourseAi
false false
end end
def system_personas def system_agents
@system_personas ||= { @system_agents ||= {
General => -1, General => -1,
SqlHelper => -2, SqlHelper => -2,
Artist => -3, Artist => -3,
@ -55,17 +55,17 @@ module DiscourseAi
} }
end end
def system_personas_by_id def system_agents_by_id
@system_personas_by_id ||= system_personas.invert @system_agents_by_id ||= system_agents.invert
end end
def all(user:) def all(user:)
# listing tools has to be dynamic cause site settings may change # listing tools has to be dynamic cause site settings may change
AiPersona.all_personas.filter do |persona| AiAgent.all_agents.filter do |agent|
next false if !user.in_any_groups?(persona.allowed_group_ids) next false if !user.in_any_groups?(agent.allowed_group_ids)
if persona.system if agent.system
instance = persona.new instance = agent.new
( (
instance.required_tools == [] || instance.required_tools == [] ||
(instance.required_tools - all_available_tools).empty? (instance.required_tools - all_available_tools).empty?
@ -77,15 +77,15 @@ module DiscourseAi
end end
def find_by(id: nil, name: nil, user:) 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 end
def name 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 end
def description 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 end
def all_available_tools def all_available_tools
@ -134,8 +134,8 @@ module DiscourseAi
end end
def id def id
@ai_persona&.id || self.class.system_personas[self.class.superclass] || @ai_agent&.id || self.class.system_agents[self.class.superclass] ||
self.class.system_personas[self.class] self.class.system_agents[self.class]
end end
def tools def tools
@ -234,7 +234,7 @@ module DiscourseAi
prompt.max_pixels = self.class.vision_max_pixels if self.class.vision_enabled prompt.max_pixels = self.class.vision_max_pixels if self.class.vision_enabled
prompt.tools = available_tools.map(&:signature) if available_tools prompt.tools = available_tools.map(&:signature) if available_tools
available_tools.each do |tool| available_tools.each do |tool|
tool.inject_prompt(prompt: prompt, context: context, persona: self) tool.inject_prompt(prompt: prompt, context: context, agent: self)
end end
prompt prompt
end end
@ -307,7 +307,7 @@ module DiscourseAi
tool_klass.new( tool_klass.new(
arguments, arguments,
tool_call_id: function_id || function_name, 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, bot_user: bot_user,
llm: llm, llm: llm,
context: context, context: context,
@ -331,7 +331,7 @@ module DiscourseAi
def rag_fragments_prompt(conversation_context, llm:, user:) def rag_fragments_prompt(conversation_context, llm:, user:)
upload_refs = 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 !DiscourseAi::Embeddings.enabled?
return nil if conversation_context.blank? || upload_refs.blank? return nil if conversation_context.blank? || upload_refs.blank?
@ -346,7 +346,7 @@ module DiscourseAi
consolidated_question = latest_interactions[0][:content] consolidated_question = latest_interactions[0][:content]
else else
consolidated_question = consolidated_question =
DiscourseAi::Personas::QuestionConsolidator.consolidate_question( DiscourseAi::Agents::QuestionConsolidator.consolidate_question(
llm, llm,
latest_interactions, latest_interactions,
user, user,
@ -376,7 +376,7 @@ module DiscourseAi
interactions_vector, interactions_vector,
limit: search_limit, limit: search_limit,
offset: 0, 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 ON
rag_document_fragments.id = rag_document_fragment_id AND rag_document_fragments.id = rag_document_fragment_id AND
rag_document_fragments.target_id = :target_id AND rag_document_fragments.target_id = :target_id AND

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
#frozen_string_literal: true #frozen_string_literal: true
module DiscourseAi module DiscourseAi
module Personas module Agents
class SqlHelper < Persona class SqlHelper < Agent
def self.schema def self.schema
return @schema if defined?(@schema) return @schema if defined?(@schema)
@ -74,7 +74,7 @@ module DiscourseAi
``` ```
The user_actions tables stores likes (action_type 1). 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}. 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 bookmarkable_type can be: Post,Topic,ChatMessage and more

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module DiscourseAi module DiscourseAi
module Personas module Agents
module Tools module Tools
class Custom < Tool class Custom < Tool
def self.class_instance(tool_id) def self.class_instance(tool_id)
@ -33,7 +33,7 @@ module DiscourseAi
end end
def self.has_custom_context? 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 # which will expire this class
return @has_custom_context if defined?(@has_custom_context) return @has_custom_context if defined?(@has_custom_context)
@ -47,7 +47,7 @@ module DiscourseAi
@has_custom_context @has_custom_context
end end
def self.inject_prompt(prompt:, context:, persona:) def self.inject_prompt(prompt:, context:, agent:)
if has_custom_context? if has_custom_context?
ai_tool = AiTool.find_by(id: tool_id) ai_tool = AiTool.find_by(id: tool_id)
if ai_tool if ai_tool

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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