Compare commits

...
Author SHA1 Message Date
Sam Saffron 4ba091aa8b WIP rename persona -> agent 2025-05-29 15:17:34 +10:00
289 changed files with 2786 additions and 2372 deletions
@@ -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")
);
}
}
@@ -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")
); );
} }
} }
@@ -0,0 +1,7 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiAiAgentsRoute extends DiscourseRoute {
model() {
return this.store.findAll("ai-agent");
}
}
@@ -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")
);
}
}
@@ -1,7 +0,0 @@
import DiscourseRoute from "discourse/routes/discourse";
export default class DiscourseAiAiPersonasRoute extends DiscourseRoute {
model() {
return this.store.findAll("ai-persona");
}
}
@@ -0,0 +1,4 @@
<AiAgentListEditor
@agents={{this.allAgents}}
@currentAgent={{this.model}}
/>
@@ -0,0 +1 @@
<AiAgentListEditor @agents={{this.model}} />
@@ -0,0 +1,4 @@
<AiAgentListEditor
@agents={{this.allAgents}}
@currentAgent={{this.model}}
/>
@@ -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>
@@ -1,4 +0,0 @@
<AiPersonaListEditor
@personas={{this.allPersonas}}
@currentPersona={{this.model}}
/>
@@ -1 +0,0 @@
<AiPersonaListEditor @personas={{this.model}} />
@@ -1,4 +0,0 @@
<AiPersonaListEditor
@personas={{this.allPersonas}}
@currentPersona={{this.model}}
/>
@@ -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}}
@@ -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}}
@@ -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
@@ -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
@@ -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
@@ -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]
+5 -5
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,
+6 -6
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
+2 -2
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
+9 -9
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,
) )
@@ -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
# #
+9 -9
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.
* *
+4 -4
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),
) )
+6 -6
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,
@@ -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
+1 -1
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"
@@ -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
@@ -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>

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