diff --git a/app/controllers/discourse_chatbot/chatbot_controller.rb b/app/controllers/discourse_chatbot/chatbot_controller.rb
index 6789a1e..30789cd 100644
--- a/app/controllers/discourse_chatbot/chatbot_controller.rb
+++ b/app/controllers/discourse_chatbot/chatbot_controller.rb
@@ -18,6 +18,42 @@ def start_bot_convo
evaluation = ::DiscourseChatbot::EventEvaluation.new
over_quota = evaluation.over_quota(current_user.id)
+ kick_off_statement = I18n.t("chatbot.quick_access_kick_off.announcement")
+
+ if SiteSetting.chatbot_user_fields_collection
+
+ trust_level = ::DiscourseChatbot::EventEvaluation.new.trust_level(current_user.id)
+ opts = { trust_level: trust_level, user_id: current_user.id }
+
+ start_bot = ::DiscourseChatbot::OpenAiBotRag.new(opts, false)
+
+ system_message = { "role": "system", "content": I18n.t("chatbot.prompt.system.rag.private", current_date_time: DateTime.current) }
+ assistant_message = { "role": "assistant", "content": I18n.t("chatbot.prmopt.quick_access_kick_off.announcement") }
+
+ system_message_suffix = start_bot.get_system_message_suffix(opts)
+ system_message[:content] += " " + system_message_suffix
+
+ messages = [system_message, assistant_message]
+
+ model = start_bot.model_name
+
+ parameters = {
+ model: model,
+ messages: messages,
+ max_completion_tokens: SiteSetting.chatbot_max_response_tokens,
+ temperature: SiteSetting.chatbot_request_temperature / 100.0,
+ top_p: SiteSetting.chatbot_request_top_p / 100.0,
+ frequency_penalty: SiteSetting.chatbot_request_frequency_penalty / 100.0,
+ presence_penalty: SiteSetting.chatbot_request_presence_penalty / 100.0
+ }
+
+ res = start_bot.client.chat(
+ parameters: parameters
+ )
+
+ kick_off_statement = res.dig("choices", 0, "message", "content")
+ end
+
if channel_type == "chat"
bot_author = ::User.find_by(username: SiteSetting.chatbot_bot_user)
@@ -44,7 +80,7 @@ def start_bot_convo
Chat::CreateMessage.call(
chat_channel_id: chat_channel_id,
guardian: guardian,
- message: over_quota ? I18n.t('chatbot.errors.overquota') : I18n.t("chatbot.quick_access_kick_off.announcement"),
+ message: over_quota ? I18n.t('chatbot.errors.overquota') : kick_off_statement,
)
end
@@ -53,7 +89,7 @@ def start_bot_convo
elsif channel_type == "personal message"
default_opts = {
post_alert_options: { skip_send_email: true },
- raw: over_quota ? I18n.t('chatbot.errors.overquota') : I18n.t("chatbot.quick_access_kick_off.announcement"),
+ raw: over_quota ? I18n.t('chatbot.errors.overquota') : kick_off_statement,
skip_validations: true,
title: I18n.t("chatbot.pm_prefix"),
archetype: Archetype.private_message,
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 75db617..374a14d 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -72,11 +72,12 @@ en:
chatbot_forum_search_function_results_topic_max_posts_count: "The maximum number of Posts to be returned in the search results if content_type is 'topics'"
chatbot_tool_choice_first_iteration: "Choose if a tool must be used for the first iteration of the bots response thinking cycle. If you choose 'force_local_forum_search' the bot will search the forum for information, if you choose 'force_a_function' the bot will use a function of its choice, if you choose 'not_forced' (default) the bot will not be forced to use a tool for its first cycle."
chatbot_forum_search_function_hybrid_search: "(EXPERIMENTAL): Enable hybrid search mode. This will cause the bot to search using native keyword search in addition to embedding based semantic search and it will attempt to blend the results"
- chatbot_url_integrity_check: "(EXPERIMENTAL): Enable URL integrity check. If the LLM has returned an invalid forum URL, this will be highlighted back to the LLM"
- chatbot_locations_plugin_support: "(EXPERIMENTAL currently user locations only) Natural language querying capability for Locations Plugin (when using RAG mode, requires Locations Plugin to be installed)"
- chatbot_escalate_to_staff_function: "(EXPERIMENTAL, Chat only) if user requests human assistance or gets irritated, escalate to staff via PM (requires staff group to be populated)"
- chatbot_escalate_to_staff_groups: "(EXPERIMENTAL, Chat only) groups added to escalation PM, e.g. support team"
- chatbot_escalate_to_staff_max_history: "(EXPERIMENTAL, Chat only) number of chat messages included in transcript added to escalation PM"
+ chatbot_url_integrity_check: "Enable URL integrity check. If the LLM has returned an invalid forum URL, this will be highlighted back to the LLM"
+ chatbot_locations_plugin_support: "(currently user locations only) Natural language querying capability for Locations Plugin (when using RAG mode, requires Locations Plugin to be installed)"
+ chatbot_escalate_to_staff_function: "(Chat only) if user requests human assistance or gets irritated, escalate to staff via PM (requires staff group to be populated)"
+ chatbot_escalate_to_staff_groups: "(Chat only) groups added to escalation PM, e.g. support team"
+ chatbot_escalate_to_staff_max_history: "(Chat only) number of chat messages included in transcript added to escalation PM"
+ chatbot_user_fields_collection: "(EXPERIMENTAL) Collect empty user fields from the user as part of the conversation"
chatbot_news_api_token: "News API token for news (if left blank, news will never be searched)Get one at NewsAPI.org"
chatbot_firecrawl_api_token: "Firecrawl API token for crawling remote websites. If left blank, crawling will not be available. Get one at https://www.firecrawl.dev/"
chatbot_jina_api_token: "Jina API token for web crawl and search. Get one at https://jina.ai. Alternative to Firecrawl and Serp. If Firecrawl API token is populated, it will be used in preference to Jina for crawling. Ditto Serp API for Searching."
@@ -109,7 +110,26 @@ en:
post: "%{username} said %{raw}"
private_message:
title_creation: "Create a short Topic title from summarising the prior messages"
+ quick_access_kick_off:
+ announcement: "I must greet the user warmly and kick off any questions I want answered"
function:
+ user_information:
+ name: "user_information_for_%{user_field}"
+ system_message:
+ general: "You must be proactive and ask the user to tell you what their %{name} is. Do not wait to be prompted!"
+ confirmation: "You must be proactive and ask the user to tell you what response is to the declaration: %{name}. Do not wait to be prompted!"
+ dropdown: "You must be proactive and ask the user to tell you what their selection of: %{name} is. The choices are %{options}. Do not wait to be prompted!"
+ closing_statement: "However, ONLY ask ONE question at a time. Do not ask multiple questions in one go. Ask last question first."
+ description:
+ general: get information about %{user_field} for the user
+ confirmation: confirm if the user accepts the following %{user_field}
+ parameters:
+ answer:
+ text: the answer to the question about what user's %{user_field} is
+ confirmation: "user's confirmation or otherwise (true or false) of: %{user_field}. If user does not confirm it is false. If user does confirm, it is true."
+ dropdown: "the user's selection of: %{user_field} which can be one of: %{options}"
+ answer: "Confirm with the user the infomration they gave about %{user_field} has been saved."
+ error: "I couldn't save any information about %{user_field} for answer '%{answer}'"
calculator:
description: |
Useful for getting the result of a math expression. It is a general purpose calculator. It works with Ruby expressions.
diff --git a/config/settings.yml b/config/settings.yml
index 7c53e32..3b5bff6 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -367,6 +367,9 @@ plugins:
chatbot_escalate_to_staff_max_history:
client: false
default: 10
+ chatbot_user_fields_collection:
+ client: false
+ default: false
chatbot_news_api_token:
client: false
default: ''
diff --git a/lib/discourse_chatbot/bots/open_ai_bot_base.rb b/lib/discourse_chatbot/bots/open_ai_bot_base.rb
index 04ad4d5..49a05ff 100644
--- a/lib/discourse_chatbot/bots/open_ai_bot_base.rb
+++ b/lib/discourse_chatbot/bots/open_ai_bot_base.rb
@@ -4,6 +4,7 @@
module ::DiscourseChatbot
class OpenAIBotBase < Bot
+ attr_reader :client, :model_name
def initialize(opts)
::OpenAI.configure do |config|
@@ -39,21 +40,24 @@ def initialize(opts)
end
end
- @model_name =
- SiteSetting.chatbot_support_vision == "directly" ? SiteSetting.chatbot_open_ai_vision_model :
- case opts[:trust_level]
- when TRUST_LEVELS[0], TRUST_LEVELS[1], TRUST_LEVELS[2]
- SiteSetting.send("chatbot_open_ai_model_custom_" + opts[:trust_level] + "_trust") ?
- SiteSetting.send("chatbot_open_ai_model_custom_name_" + opts[:trust_level] + "_trust") :
- SiteSetting.send("chatbot_open_ai_model_" + opts[:trust_level] + "_trust")
- else
- SiteSetting.chatbot_open_ai_model_custom_low_trust ? SiteSetting.chatbot_open_ai_model_custom_name_low_trust : SiteSetting.chatbot_open_ai_model_low_trust
- end
+ @model_name = get_model(opts)
end
def get_response(prompt, opts)
raise "Overwrite me!"
end
+ def get_model(opts)
+ SiteSetting.chatbot_support_vision == "directly" ? SiteSetting.chatbot_open_ai_vision_model :
+ case opts[:trust_level]
+ when TRUST_LEVELS[0], TRUST_LEVELS[1], TRUST_LEVELS[2]
+ SiteSetting.send("chatbot_open_ai_model_custom_" + opts[:trust_level] + "_trust") ?
+ SiteSetting.send("chatbot_open_ai_model_custom_name_" + opts[:trust_level] + "_trust") :
+ SiteSetting.send("chatbot_open_ai_model_" + opts[:trust_level] + "_trust")
+ else
+ SiteSetting.chatbot_open_ai_model_custom_low_trust ? SiteSetting.chatbot_open_ai_model_custom_name_low_trust : SiteSetting.chatbot_open_ai_model_low_trust
+ end
+ end
+
end
end
diff --git a/lib/discourse_chatbot/bots/open_ai_bot_rag.rb b/lib/discourse_chatbot/bots/open_ai_bot_rag.rb
index ab087ef..c30847c 100644
--- a/lib/discourse_chatbot/bots/open_ai_bot_rag.rb
+++ b/lib/discourse_chatbot/bots/open_ai_bot_rag.rb
@@ -19,6 +19,7 @@
"DiscourseChatbot::WebSearchFunction",
"DiscourseChatbot::WebCrawlerFunction",
"DiscourseChatbot::NewsFunction",
+"DiscourseChatbot::UserFieldFunction",
"DiscourseChatbot::EscalateToStaffFunction",
"DiscourseChatbot::CalculatorFunction"]
@@ -30,9 +31,9 @@ class OpenAiBotRag < OpenAIBotBase
FORCE_A_FUNCTION = "force_a_function"
FORCE_LOCAL_SEARCH_FUNCTION = "force_local_forum_search"
- def initialize(opts)
- super
- merge_functions(opts)
+ def initialize(opts, tools = true)
+ super(opts)
+ merge_functions(opts) if tools
end
def get_response(prompt, opts)
@@ -40,11 +41,19 @@ def get_response(prompt, opts)
if private_discussion
system_message = { "role": "system", "content": I18n.t("chatbot.prompt.system.rag.private", current_date_time: DateTime.current) }
+
+ if SiteSetting.chatbot_user_fields_collection
+ system_message[:content] += " " + get_system_message_suffix(opts)
+ end
else
system_message = { "role": "system", "content": I18n.t("chatbot.prompt.system.rag.open", current_date_time: DateTime.current) }
end
- prompt.unshift(system_message)
+ if SiteSetting.chatbot_user_fields_collection
+ prompt << system_message
+ else
+ prompt.unshift(system_message)
+ end
@inner_thoughts = []
@posts_ids_found = []
@@ -61,6 +70,35 @@ def get_response(prompt, opts)
}
end
+ def get_system_message_suffix(opts)
+ system_message_suffix = ""
+ system_message_suffix_array = []
+ UserField.where(editable: true).order(:id).each do |user_field|
+ user_field_options = []
+ user_field_id = user_field.id
+ user_field_type = user_field.field_type_enum
+ if user_field_type == "dropdown"
+ UserFieldOption.where(user_field_id: user_field_id).each do |option|
+ user_field_options << option.value
+ end
+ end
+ if !::UserCustomField.where(user_id: opts[:user_id], name: "user_field_#{UserField.find_by(name: user_field.name).id}" ).exists? ||
+ ::UserCustomField.where(user_id: opts[:user_id], name: "user_field_#{UserField.find_by(name: user_field.name).id}" ).first.value.blank?
+ system_message_suffix_array << case user_field_type
+ when "confirm"
+ I18n.t("chatbot.prompt.function.user_information.system_message.confirmation", name: user_field.name, description: user_field.description)
+ when "dropdown"
+ I18n.t("chatbot.prompt.function.user_information.system_message.dropdown", name: user_field.name, options: user_field_options.to_sentence)
+ else
+ I18n.t("chatbot.prompt.function.user_information.system_message.general", name: user_field.name, description: user_field.description)
+ end
+ end
+ break if system_message_suffix_array.length > 1
+ end
+ system_message_suffix = system_message_suffix_array.reverse.join(" ")
+ system_message_suffix += " " + I18n.t("chatbot.prompt.function.user_information.system_message.closing_statement")
+ end
+
def merge_functions(opts)
calculator_function = ::DiscourseChatbot::CalculatorFunction.new
wikipedia_function = ::DiscourseChatbot::WikipediaFunction.new
@@ -98,6 +136,17 @@ def merge_functions(opts)
functions = [calculator_function, wikipedia_function]
+ if opts[:private] && SiteSetting.chatbot_user_fields_collection
+ start_length = functions.length
+ UserField.where(editable: true).order(:id).each do |user_field|
+ if !::UserCustomField.where(user_id: opts[:user_id], name: "user_field_#{UserField.find_by(name: user_field.name).id}" ).exists? ||
+ ::UserCustomField.where(user_id: opts[:user_id], name: "user_field_#{UserField.find_by(name: user_field.name).id}" ).first.value.blank?
+ functions << ::DiscourseChatbot::UserFieldFunction.new(user_field.name, opts[:user_id])
+ end
+ break if functions.length > start_length + 1
+ end
+ end
+
functions << forum_search_function if forum_search_function
functions << vision_function if vision_function
functions << paint_function if SiteSetting.chatbot_support_picture_creation
diff --git a/lib/discourse_chatbot/functions/user_field_function.rb b/lib/discourse_chatbot/functions/user_field_function.rb
new file mode 100644
index 0000000..c3f33b3
--- /dev/null
+++ b/lib/discourse_chatbot/functions/user_field_function.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require_relative '../function'
+
+module DiscourseChatbot
+ class UserFieldFunction < Function
+ def initialize(user_field, user_id)
+ @user_field_options = []
+ @user_field = user_field
+ @user_field_object = UserField.find_by(name: user_field)
+ @user_field_id = @user_field_object.id
+ @user_field_type = @user_field_object.field_type_enum
+ if @user_field_type == "dropdown"
+ UserFieldOption.where(user_field_id: @user_field_id).each do |option|
+ @user_field_options << option.value
+ end
+ end
+ @function_name = user_field.downcase.gsub(" ", "_")
+ @user_custom_field_name = "user_field_#{@user_field_id}"
+ @user_id = user_id
+ super()
+ end
+
+ def name
+ I18n.t("chatbot.prompt.function.user_information.name", user_field: @function_name)
+ end
+
+ def description
+ case @user_field_type
+ when "confirm"
+ I18n.t("chatbot.prompt.function.user_information.description.confirmation", user_field: @user_field)
+ else
+ I18n.t("chatbot.prompt.function.user_information.description.general", user_field: @user_field)
+ end
+ end
+
+ def parameters
+ case @user_field_type
+ when "text"
+ [
+ { name: "answer", type: String, description: I18n.t("chatbot.prompt.function.user_information.parameters.answer.text", user_field: @user_field) } ,
+ ]
+ when "confirm"
+ [
+ { name: "answer", type: String, enum: ["true", "false"], description: I18n.t("chatbot.prompt.function.user_information.parameters.answer.confirmation", user_field: @user_field) } ,
+ ]
+ when "dropdown"
+ [
+ { name: "answer", type: String, enum: @user_field_options, description: I18n.t("chatbot.prompt.function.user_information.parameters.answer.dropdown", user_field: @user_field, options: @user_field_options) } ,
+ ]
+ end
+ end
+
+ def required
+ ['answer']
+ end
+
+ def process(args)
+ begin
+ super(args)
+ ucf = ::UserCustomField.where(user_id: @user_id, name: @user_custom_field_name).first
+
+ if ucf
+ ucf.value = args[parameters[0][:name]]
+ ucf.save!
+ else
+ ::UserCustomField.create!(user_id: @user_id, name: @user_custom_field_name, value: args[parameters[0][:name]])
+ end
+
+ I18n.t("chatbot.prompt.function.user_information.answer", user_field: @user_field)
+ rescue StandardError => e
+ Rails.logger.error("Chatbot: Error occurred while attempting to store answer in a User Custom Field: #{e.message}")
+ I18n.t("chatbot.prompt.function.user_information.error", user_field: @user_field, answer: args[parameters[0][:name]])
+ end
+ end
+ end
+end
diff --git a/plugin.rb b/plugin.rb
index 74037b7..d89f331 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# name: discourse-chatbot
# about: a plugin that allows you to have a conversation with a configurable chatbot in Discourse Chat, Topics and Private Messages
-# version: 1.1.2
+# version: 1.2.0
# authors: merefield
# url: https://github.com/merefield/discourse-chatbot
@@ -95,6 +95,7 @@ def progress_debug_message(message)
../lib/discourse_chatbot/bots/open_ai_bot_rag.rb
../lib/discourse_chatbot/safe_ruby/lib/safe_ruby.rb
../lib/discourse_chatbot/function.rb
+ ../lib/discourse_chatbot/functions/user_field_function.rb
../lib/discourse_chatbot/functions/calculator_function.rb
../lib/discourse_chatbot/functions/escalate_to_staff_function.rb
../lib/discourse_chatbot/functions/news_function.rb