From 907d1ca67b40a3431f2b5b75c967e97bb00254cf Mon Sep 17 00:00:00 2001 From: Keegan George Date: Mon, 10 Mar 2025 16:52:51 -0700 Subject: [PATCH 1/4] DEV: Improve title suggester suggestions when editing topic --- .../ai_helper/assistant_controller.rb | 29 ++++++++++++----- .../suggestion-menus/ai-title-suggester.gjs | 32 +++++++------------ .../ai-title-suggestion.gjs | 2 +- .../ai-title-suggestion.gjs | 2 +- lib/ai_helper/assistant.rb | 16 ++++++++++ 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb index 32da7b819..1fc18f4cc 100644 --- a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb +++ b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb @@ -53,19 +53,32 @@ def suggest end def suggest_title - input = get_text_param! + assistant = DiscourseAi::AiHelper::Assistant.new + + if params[:topic_id] + topic = Topic.find_by(id: params[:topic_id]) + + topic_content = + topic + .posts + .joins(:user) + .pluck(:post_number, :raw, :username, :last_version_at) + .map do |pn, raw_text, username, last_version_at| + { poster: username, id: pn, text: raw_text, last_version_at: last_version_at } + end + + truncated_content = topic_content.map { |item| assistant.truncate(item) } + + input = truncated_content + else + input = get_text_param! + end prompt = CompletionPrompt.enabled_by_name("generate_titles") raise Discourse::InvalidParameters.new(:mode) if !prompt hijack do - render json: - DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt( - prompt, - input, - current_user, - ), - status: 200 + render json: assistant.generate_and_send_prompt(prompt, input, current_user), status: 200 end rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"), diff --git a/assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs b/assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs index 5142a8edd..7bee7637e 100644 --- a/assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs +++ b/assets/javascripts/discourse/components/suggestion-menus/ai-title-suggester.gjs @@ -17,30 +17,15 @@ export default class AiTitleSuggester extends Component { @tracked untriggers = []; @tracked triggerIcon = "discourse-sparkles"; @tracked content = null; - @tracked topicContent = null; - - constructor() { - super(...arguments); - - if (!this.topicContent && this.args.composer?.reply === undefined) { - this.fetchTopicContent(); - } - } - - async fetchTopicContent() { - await ajax(`/t/${this.args.buffered.content.id}.json`).then( - ({ post_stream }) => { - this.topicContent = post_stream.posts[0].cooked; - } - ); - } get showSuggestionButton() { const composerFields = document.querySelector(".composer-fields"); const editTopicTitleField = document.querySelector(".edit-topic-title"); - this.content = this.args.composer?.reply || this.topicContent; - const showTrigger = this.content?.length > MIN_CHARACTER_COUNT; + this.content = this.args.composer?.reply; + const showTrigger = + this.content?.length > MIN_CHARACTER_COUNT || + this.args.topicState === "edit"; if (composerFields) { if (showTrigger) { @@ -69,13 +54,20 @@ export default class AiTitleSuggester extends Component { this.loading = true; this.triggerIcon = "spinner"; + const data = {}; + + if (this.content) { + data.text = this.content; + } else { + data.topic_id = this.args.buffered.content.id; + } try { const { suggestions } = await ajax( "/discourse-ai/ai-helper/suggest_title", { method: "POST", - data: { text: this.content }, + data, } ); this.suggestions = suggestions; diff --git a/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs b/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs index 691f210dd..74ce20e2b 100644 --- a/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs +++ b/assets/javascripts/discourse/connectors/after-composer-title-input/ai-title-suggestion.gjs @@ -13,6 +13,6 @@ export default class AiTitleSuggestion extends Component { } } diff --git a/assets/javascripts/discourse/connectors/edit-topic-title__after/ai-title-suggestion.gjs b/assets/javascripts/discourse/connectors/edit-topic-title__after/ai-title-suggestion.gjs index d3449e8fb..c17f5a4d5 100644 --- a/assets/javascripts/discourse/connectors/edit-topic-title__after/ai-title-suggestion.gjs +++ b/assets/javascripts/discourse/connectors/edit-topic-title__after/ai-title-suggestion.gjs @@ -13,6 +13,6 @@ export default class AiTitleSuggestion extends Component { } } diff --git a/lib/ai_helper/assistant.rb b/lib/ai_helper/assistant.rb index 8de1bcf8c..0b7dcbd0d 100644 --- a/lib/ai_helper/assistant.rb +++ b/lib/ai_helper/assistant.rb @@ -206,6 +206,22 @@ def generate_image_caption(upload, user) raw_caption.delete("|").squish.truncate_words(IMAGE_CAPTION_MAX_WORDS) end + def truncate(item) + item_content = item[:text].to_s + split_1, split_2 = + [item_content[0, item_content.size / 2], item_content[(item_content.size / 2)..-1]] + + truncation_length = 500 + tokenizer = helper_llm.llm_model.tokenizer_class + + item[:text] = [ + tokenizer.truncate(split_1, truncation_length), + tokenizer.truncate(split_2.reverse, truncation_length).reverse, + ].join(" ") + + item + end + private SANITIZE_REGEX_STR = From e911370862142d3f3b515e9f54d1145971474595 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Tue, 11 Mar 2025 08:35:47 -0700 Subject: [PATCH 2/4] DEV: add spec --- .../ai_helper/assistant_controller_spec.rb | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/spec/requests/ai_helper/assistant_controller_spec.rb b/spec/requests/ai_helper/assistant_controller_spec.rb index 24a3b73a5..87b295783 100644 --- a/spec/requests/ai_helper/assistant_controller_spec.rb +++ b/spec/requests/ai_helper/assistant_controller_spec.rb @@ -134,6 +134,70 @@ end end + describe "#suggest_title" do + fab!(:topic) + fab!(:post_1) { Fabricate(:post, topic: topic, raw: "I love apples") } + fab!(:post_3) { Fabricate(:post, topic: topic, raw: "I love mangos") } + fab!(:post_2) { Fabricate(:post, topic: topic, raw: "I love bananas") } + + context "when logged in as an allowed user" do + fab!(:user) + + before do + sign_in(user) + user.group_ids = [Group::AUTO_GROUPS[:trust_level_1]] + SiteSetting.composer_ai_helper_allowed_groups = Group::AUTO_GROUPS[:trust_level_1] + end + + context "when suggesting titles with a topic_id" do + let(:title_suggestions) do + "What are your favourite fruits?Love for fruitsFruits are amazingFavourite fruit listFruit share topic" + end + let(:title_suggestions_array) do + [ + "What are your favourite fruits?", + "Love for fruits", + "Fruits are amazing", + "Favourite fruit list", + "Fruit share topic", + ] + end + + it "returns title suggestions based on all topic post context" do + DiscourseAi::Completions::Llm.with_prepared_responses([title_suggestions]) do + post "/discourse-ai/ai-helper/suggest_title", params: { topic_id: topic.id } + + expect(response.status).to eq(200) + expect(response.parsed_body["suggestions"]).to eq(title_suggestions_array) + end + end + end + + context "when suggesting titles with input text" do + let(:title_suggestions) do + "Apples - the best fruitWhy apples are greatApples are the best fruitMy love for applesI love apples" + end + let(:title_suggestions_array) do + [ + "Apples - the best fruit", + "Why apples are great", + "Apples are the best fruit", + "My love for apples", + "I love apples", + ] + end + it "returns title suggestions based on the input text" do + DiscourseAi::Completions::Llm.with_prepared_responses([title_suggestions]) do + post "/discourse-ai/ai-helper/suggest_title", params: { text: post_1.raw } + + expect(response.status).to eq(200) + expect(response.parsed_body["suggestions"]).to eq(title_suggestions_array) + end + end + end + end + end + describe "#caption_image" do let(:image) { plugin_file_from_fixtures("100x100.jpg") } let(:upload) { UploadCreator.new(image, "image.jpg").create_for(Discourse.system_user.id) } From 138d757b741016614caaef61646ee25fd2013754 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Tue, 11 Mar 2025 10:41:37 -0700 Subject: [PATCH 3/4] DEV: Use summarization best topics/truncate logic --- .../ai_helper/assistant_controller.rb | 18 ++---------------- lib/ai_helper/assistant.rb | 16 ---------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb index 1fc18f4cc..07fee3b73 100644 --- a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb +++ b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb @@ -53,23 +53,9 @@ def suggest end def suggest_title - assistant = DiscourseAi::AiHelper::Assistant.new - if params[:topic_id] topic = Topic.find_by(id: params[:topic_id]) - - topic_content = - topic - .posts - .joins(:user) - .pluck(:post_number, :raw, :username, :last_version_at) - .map do |pn, raw_text, username, last_version_at| - { poster: username, id: pn, text: raw_text, last_version_at: last_version_at } - end - - truncated_content = topic_content.map { |item| assistant.truncate(item) } - - input = truncated_content + input = DiscourseAi::Summarization::Strategies::TopicSummary.new(topic).targets_data else input = get_text_param! end @@ -78,7 +64,7 @@ def suggest_title raise Discourse::InvalidParameters.new(:mode) if !prompt hijack do - render json: assistant.generate_and_send_prompt(prompt, input, current_user), status: 200 + render json: DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(prompt, input, current_user), status: 200 end rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"), diff --git a/lib/ai_helper/assistant.rb b/lib/ai_helper/assistant.rb index 0b7dcbd0d..8de1bcf8c 100644 --- a/lib/ai_helper/assistant.rb +++ b/lib/ai_helper/assistant.rb @@ -206,22 +206,6 @@ def generate_image_caption(upload, user) raw_caption.delete("|").squish.truncate_words(IMAGE_CAPTION_MAX_WORDS) end - def truncate(item) - item_content = item[:text].to_s - split_1, split_2 = - [item_content[0, item_content.size / 2], item_content[(item_content.size / 2)..-1]] - - truncation_length = 500 - tokenizer = helper_llm.llm_model.tokenizer_class - - item[:text] = [ - tokenizer.truncate(split_1, truncation_length), - tokenizer.truncate(split_2.reverse, truncation_length).reverse, - ].join(" ") - - item - end - private SANITIZE_REGEX_STR = From d7291eadfe1a99fbfd49a8def1991a6409120499 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Tue, 11 Mar 2025 10:47:27 -0700 Subject: [PATCH 4/4] FIX: Linting --- .../discourse_ai/ai_helper/assistant_controller.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb index 07fee3b73..0d0f85ee6 100644 --- a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb +++ b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb @@ -64,7 +64,13 @@ def suggest_title raise Discourse::InvalidParameters.new(:mode) if !prompt hijack do - render json: DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(prompt, input, current_user), status: 200 + render json: + DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt( + prompt, + input, + current_user, + ), + status: 200 end rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),