REFACTOR: Move personas into it's own module.

This commit is contained in:
Roman Rizzi
2025-03-27 15:10:53 -03:00
parent a6b08270c0
commit 5bfe231486
91 changed files with 746 additions and 819 deletions
+2 -3
View File
@@ -110,7 +110,7 @@ module DiscourseAi
scope.user.in_any_groups?(SiteSetting.ai_bot_allowed_groups_map)
end,
) do
DiscourseAi::AiBot::Personas::Persona
DiscourseAi::Personas::Persona
.all(user: scope.user)
.map do |persona|
{
@@ -205,8 +205,7 @@ module DiscourseAi
include_condition: -> { SiteSetting.ai_bot_enabled && object.topic.private_message? },
) do
id = topic.custom_fields["ai_persona_id"]
name =
DiscourseAi::AiBot::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
name = DiscourseAi::Personas::Persona.find_by(user: scope.user, id: id.to_i)&.name if id
name || topic.custom_fields["ai_persona"]
end
-19
View File
@@ -1,19 +0,0 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Creative < Persona
def tools
[]
end
def system_prompt
<<~PROMPT
You are a helpful bot
PROMPT
end
end
end
end
end
File diff suppressed because it is too large Load Diff
+50 -17
View File
@@ -150,13 +150,11 @@ module DiscourseAi
persona = nil
if persona_id
persona =
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, id: persona_id.to_i)
end
if !persona && persona_name = post.topic.custom_fields["ai_persona"]
persona =
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, name: persona_name)
persona = DiscourseAi::Personas::Persona.find_by(user: post.user, name: persona_name)
end
# edge case, llm was mentioned in an ai persona conversation
@@ -172,11 +170,11 @@ module DiscourseAi
end
end
persona ||= DiscourseAi::AiBot::Personas::General
persona ||= DiscourseAi::Personas::General
bot_user = User.find(persona.user_id) if persona && persona.force_default_llm
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
new(bot).update_playground_with(post)
end
end
@@ -198,7 +196,7 @@ module DiscourseAi
bot_user = user || ai_persona.user
raise Discourse::InvalidParameters.new(:user) if bot_user.nil?
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona)
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona)
playground = DiscourseAi::AiBot::Playground.new(bot)
playground.reply_to(
@@ -307,17 +305,53 @@ module DiscourseAi
end
def title_playground(post, user)
context = conversation_context(post)
conversation =
conversation_context(post).reduce(+"") do |memo, c|
if c[:type] == :user || c[:type] == :model
memo << "#{c[:type].to_s.humanize} said:\n#{c[:content]}\n\n"
end
bot
.get_updated_title(context, post, user)
.tap do |new_title|
PostRevisor.new(post.topic.first_post, post.topic).revise!(
bot.bot_user,
title: new_title.sub(/\A"/, "").sub(/"\Z/, ""),
)
memo
end
system_insts = <<~TEXT.strip
You are titlebot. Given a conversation, you will suggest a title.
- You will never respond with anything but the suggested title.
- You will always match the conversation language in your title suggestion.
- Title will capture the essence of the conversation.
TEXT
instruction = <<~TEXT.strip
Given the following conversation:
{{{
#{conversation}
}}}
Reply only with a title that is 7 words or less.
TEXT
title_prompt =
DiscourseAi::Completions::Prompt.new(
system_insts,
messages: [type: :user, content: instruction],
topic_id: post.topic_id,
)
title =
bot
.llm
.generate(title_prompt, user: user, feature_name: "bot_title")
.strip
.split("\n")
.last
PostRevisor.new(post.topic.first_post, post.topic).revise!(
bot.bot_user,
title: title.sub(/\A"/, "").sub(/"\Z/, ""),
)
allowed_users = post.topic.topic_allowed_users.pluck(:user_id)
MessageBus.publish(
"/discourse-ai/ai-bot/topic/#{post.topic.id}",
@@ -710,8 +744,7 @@ module DiscourseAi
def schedule_bot_reply(post)
persona_id =
DiscourseAi::AiBot::Personas::Persona.system_personas[bot.persona.class] ||
bot.persona.class.id
DiscourseAi::Personas::Persona.system_personas[bot.persona.class] || bot.persona.class.id
::Jobs.enqueue(
:create_ai_reply,
post_id: post.id,
+2 -2
View File
@@ -77,8 +77,8 @@ module DiscourseAi
io.flush
persona_class =
DiscourseAi::AiBot::Personas::Persona.find_by(id: persona.id, user: current_user)
bot = DiscourseAi::AiBot::Bot.as(persona.user, persona: persona_class.new)
DiscourseAi::Personas::Persona.find_by(id: persona.id, user: current_user)
bot = DiscourseAi::Personas::Bot.as(persona.user, persona: persona_class.new)
data =
{
+3 -3
View File
@@ -333,7 +333,7 @@ module DiscourseAi
return { error: "Persona not found" } if persona_class.nil?
persona = persona_class.new
bot = DiscourseAi::AiBot::Bot.as(@bot_user || persona.user, persona: persona)
bot = DiscourseAi::Personas::Bot.as(@bot_user || persona.user, persona: persona)
playground = DiscourseAi::AiBot::Playground.new(bot)
if @context[:post_id]
@@ -482,7 +482,7 @@ module DiscourseAi
headers = (options && options["headers"]) || {}
result = {}
DiscourseAi::AiBot::Tools::Tool.send_http_request(
DiscourseAi::Personas::Tools::Tool.send_http_request(
url,
headers: headers,
) do |response|
@@ -511,7 +511,7 @@ module DiscourseAi
body = options && options["body"]
result = {}
DiscourseAi::AiBot::Tools::Tool.send_http_request(
DiscourseAi::Personas::Tools::Tool.send_http_request(
url,
method: method,
headers: headers,
+1 -6
View File
@@ -9,12 +9,7 @@ module DiscourseAi
.all_personas
.find { |persona| persona.id == SiteSetting.ai_discord_search_persona.to_i }
.new
@bot =
DiscourseAi::AiBot::Bot.as(
Discourse.system_user,
persona: @persona,
model: LlmModel.find(@persona.class.default_llm_id),
)
@bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: @persona)
super(body)
end
+1 -1
View File
@@ -4,7 +4,7 @@ module DiscourseAi
module Discord::Bot
class Search < Base
def initialize(body)
@search = DiscourseAi::AiBot::Tools::Search
@search = DiscourseAi::Personas::Tools::Search
super(body)
end
@@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module ArtifactUpdateStrategies
class InvalidFormatError < StandardError
end
@@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module ArtifactUpdateStrategies
class Diff < Base
attr_reader :failed_searches
@@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module ArtifactUpdateStrategies
class Full < Base
private
@@ -1,19 +1,18 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Artist < Persona
def tools
[Tools::Image]
end
module Personas
class Artist < Persona
def tools
[Tools::Image]
end
def required_tools
[Tools::Image]
end
def required_tools
[Tools::Image]
end
def system_prompt
<<~PROMPT
def system_prompt
<<~PROMPT
You are artistbot and you are here to help people generate images.
You generate images using stable diffusion.
@@ -31,7 +30,6 @@ module DiscourseAi
- Be creative with your prompts, offer diverse options
- You can use the seeds to regenerate the same image and amend the prompt keeping general style
PROMPT
end
end
end
end
+12 -53
View File
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Bot
attr_reader :model
@@ -13,7 +13,7 @@ module DiscourseAi
# limit is arbitrary, but 5 which was used in the past was too low
MAX_TOOLS = 20
def self.as(bot_user, persona: DiscourseAi::AiBot::Personas::General.new, model: nil)
def self.as(bot_user, persona:, model: nil)
new(bot_user, persona, model)
end
@@ -27,49 +27,8 @@ module DiscourseAi
attr_reader :bot_user
attr_accessor :persona
def get_updated_title(conversation_context, post, user)
system_insts = <<~TEXT.strip
You are titlebot. Given a conversation, you will suggest a title.
- You will never respond with anything but the suggested title.
- You will always match the conversation language in your title suggestion.
- Title will capture the essence of the conversation.
TEXT
# conversation context may contain tool calls, and confusing user names
# clean it up
conversation = +""
conversation_context.each do |context|
if context[:type] == :user
conversation << "User said:\n#{context[:content]}\n\n"
elsif context[:type] == :model
conversation << "Model said:\n#{context[:content]}\n\n"
end
end
instruction = <<~TEXT.strip
Given the following conversation:
{{{
#{conversation}
}}}
Reply only with a title that is 7 words or less.
TEXT
title_prompt =
DiscourseAi::Completions::Prompt.new(
system_insts,
messages: [type: :user, content: instruction],
topic_id: post.topic_id,
)
DiscourseAi::Completions::Llm
.proxy(model)
.generate(title_prompt, user: user, feature_name: "bot_title")
.strip
.split("\n")
.last
def llm
DiscourseAi::Completions::Llm.proxy(model)
end
def force_tool_if_needed(prompt, context)
@@ -93,8 +52,8 @@ module DiscourseAi
end
def reply(context, &update_blk)
llm = DiscourseAi::Completions::Llm.proxy(model)
prompt = persona.craft_prompt(context, llm: llm)
current_llm = llm
prompt = persona.craft_prompt(context, llm: current_llm)
total_completions = 0
ongoing_chain = true
@@ -120,7 +79,7 @@ module DiscourseAi
current_thinking = []
result =
llm.generate(
current_llm.generate(
prompt,
feature_name: "bot",
partial_tool_calls: allow_partial_tool_calls,
@@ -131,7 +90,7 @@ module DiscourseAi
persona.find_tool(
partial,
bot_user: user,
llm: llm,
llm: current_llm,
context: context,
existing_tools: existing_tools,
)
@@ -158,7 +117,7 @@ module DiscourseAi
process_tool(
tool: tool,
raw_context: raw_context,
llm: llm,
current_llm: current_llm,
cancel: cancel,
update_blk: update_blk,
prompt: prompt,
@@ -242,7 +201,7 @@ module DiscourseAi
def process_tool(
tool:,
raw_context:,
llm:,
current_llm:,
cancel:,
update_blk:,
prompt:,
@@ -250,7 +209,7 @@ module DiscourseAi
current_thinking:
)
tool_call_id = tool.tool_call_id
invocation_result_json = invoke_tool(tool, llm, cancel, context, &update_blk).to_json
invocation_result_json = invoke_tool(tool, cancel, context, &update_blk).to_json
tool_call_message = {
type: :tool_call,
@@ -296,7 +255,7 @@ module DiscourseAi
raw_context << [invocation_result_json, tool_call_id, "tool", tool.name]
end
def invoke_tool(tool, llm, cancel, context, &update_blk)
def invoke_tool(tool, cancel, context, &update_blk)
show_placeholder = !context[:skip_tool_details] && !tool.class.allow_partial_tool_calls?
update_blk.call("", cancel, build_placeholder(tool.summary, "")) if show_placeholder
+17
View File
@@ -0,0 +1,17 @@
#frozen_string_literal: true
module DiscourseAi
module Personas
class Creative < Persona
def tools
[]
end
def system_prompt
<<~PROMPT
You are a helpful bot
PROMPT
end
end
end
end
@@ -1,19 +1,18 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class DallE3 < Persona
def tools
[Tools::DallE]
end
module Personas
class DallE3 < Persona
def tools
[Tools::DallE]
end
def required_tools
[Tools::DallE]
end
def required_tools
[Tools::DallE]
end
def system_prompt
<<~PROMPT
def system_prompt
<<~PROMPT
As a DALL-E-3 bot, you're tasked with generating images based on user prompts.
- Be specific and detailed in your prompts. Include elements like subject, medium (e.g., oil on canvas), artist style, lighting, time of day, and website style (e.g., ArtStation, DeviantArt).
@@ -32,7 +31,6 @@ module DiscourseAi
Just generate the images
PROMPT
end
end
end
end
@@ -1,15 +1,14 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class DiscourseHelper < Persona
def tools
[Tools::DiscourseMetaSearch]
end
module Personas
class DiscourseHelper < Persona
def tools
[Tools::DiscourseMetaSearch]
end
def system_prompt
<<~PROMPT
def system_prompt
<<~PROMPT
You are Discourse Helper Bot
- Discourse Helper Bot understands *markdown* and responds in Discourse **markdown**.
@@ -41,7 +40,6 @@ module DiscourseAi
The date now is: {time}, much has changed since you were trained.
PROMPT
end
end
end
end
@@ -1,22 +1,21 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class General < Persona
def tools
[
Tools::Search,
Tools::Google,
Tools::Image,
Tools::Read,
Tools::ListCategories,
Tools::ListTags,
]
end
module Personas
class General < Persona
def tools
[
Tools::Search,
Tools::Google,
Tools::Image,
Tools::Read,
Tools::ListCategories,
Tools::ListTags,
]
end
def system_prompt
<<~PROMPT
def system_prompt
<<~PROMPT
You are a helpful Discourse assistant.
You _understand_ and **generate** Discourse Markdown.
You live in a Discourse Forum Message.
@@ -27,7 +26,6 @@ module DiscourseAi
The participants in this conversation are: {participants}
The date now is: {time}, much has changed since you were trained.
PROMPT
end
end
end
end
@@ -1,20 +1,19 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class GithubHelper < Persona
def tools
[
Tools::GithubFileContent,
Tools::GithubPullRequestDiff,
Tools::GithubSearchCode,
Tools::GithubSearchFiles,
]
end
module Personas
class GithubHelper < Persona
def tools
[
Tools::GithubFileContent,
Tools::GithubPullRequestDiff,
Tools::GithubSearchCode,
Tools::GithubSearchFiles,
]
end
def system_prompt
<<~PROMPT
def system_prompt
<<~PROMPT
You are a helpful GitHub assistant.
You _understand_ and **generate** Discourse Flavored Markdown.
You live in a Discourse Forum Message.
@@ -22,7 +21,6 @@ module DiscourseAi
Your purpose is to assist users with GitHub-related tasks and questions.
When asked about a specific repository, pull request, or file, try to use the available tools to provide accurate and helpful information.
PROMPT
end
end
end
end
File diff suppressed because it is too large Load Diff
@@ -1,19 +1,18 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class Researcher < Persona
def tools
[Tools::Google, Tools::WebBrowser]
end
module Personas
class Researcher < Persona
def tools
[Tools::Google, Tools::WebBrowser]
end
def required_tools
[Tools::Google]
end
def required_tools
[Tools::Google]
end
def system_prompt
<<~PROMPT
def system_prompt
<<~PROMPT
You are a research assistant with access to two powerful tools:
1. Google search - for finding relevant information across the internet.
@@ -45,7 +44,6 @@ module DiscourseAi
Remember, efficient use of your tools not only saves time but also ensures the high quality and relevance of the information provided.
PROMPT
end
end
end
end
@@ -1,15 +1,14 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class SettingsExplorer < Persona
def tools
[Tools::SettingContext, Tools::SearchSettings]
end
module Personas
class SettingsExplorer < Persona
def tools
[Tools::SettingContext, Tools::SearchSettings]
end
def system_prompt
<<~PROMPT
def system_prompt
<<~PROMPT
You are Discourse Site settings bot.
- You are able to find information about all the site settings.
@@ -19,7 +18,6 @@ module DiscourseAi
Current time is: {time}
PROMPT
end
end
end
end
@@ -1,52 +1,50 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
class SqlHelper < Persona
def self.schema
return @schema if defined?(@schema)
module Personas
class SqlHelper < Persona
def self.schema
return @schema if defined?(@schema)
tables = Hash.new
priority_tables = %w[
posts
topics
notifications
users
user_actions
user_emails
categories
groups
]
tables = Hash.new
priority_tables = %w[
posts
topics
notifications
users
user_actions
user_emails
categories
groups
]
DB.query(<<~SQL).each { |row| (tables[row.table_name] ||= []) << row.column_name }
DB.query(<<~SQL).each { |row| (tables[row.table_name] ||= []) << row.column_name }
select table_name, column_name from information_schema.columns
where table_schema = 'public'
order by table_name
SQL
priority =
+(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
priority = +(priority_tables.map { |name| "#{name}(#{tables[name].join(",")})" }.join("\n"))
other_tables = +""
tables.each do |table_name, _|
next if priority_tables.include?(table_name)
other_tables << "#{table_name} "
end
@schema = { priority_tables: priority, other_tables: other_tables }
other_tables = +""
tables.each do |table_name, _|
next if priority_tables.include?(table_name)
other_tables << "#{table_name} "
end
def tools
[Tools::DbSchema]
end
@schema = { priority_tables: priority, other_tables: other_tables }
end
def temperature
0.2
end
def tools
[Tools::DbSchema]
end
def system_prompt
<<~PROMPT
def temperature
0.2
end
def system_prompt
<<~PROMPT
You are a PostgreSQL expert.
- Avoid returning any text to the user prior to a tool call.
- You understand and generate Discourse Markdown but specialize in creating queries.
@@ -100,7 +98,6 @@ module DiscourseAi
NEVER look up schema for the tables listed above, as their full schema is already provided.
PROMPT
end
end
end
end
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class CreateArtifact < Tool
def self.name
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class Custom < Tool
def self.class_instance(tool_id)
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class DallE < Tool
def self.signature
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class DbSchema < Tool
def self.signature
@@ -1,7 +1,7 @@
#frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class DiscourseMetaSearch < Tool
class << self
@@ -1,6 +1,6 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class GithubFileContent < Tool
def self.signature
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class GithubPullRequestDiff < Tool
LARGE_OBJECT_THRESHOLD = 30_000
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module DiscourseAi
module AiBot
module Personas
module Tools
class GithubSearchCode < Tool
def self.signature

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