Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions app/controllers/discourse_ai/ai_helper/assistant_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ export default class AiTitleSuggestion extends Component {
}

<template>
<AiTitleSuggester @composer={{@outletArgs.composer}} />
<AiTitleSuggester @composer={{@outletArgs.composer}} @topicState="new" />
</template>
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ export default class AiTitleSuggestion extends Component {
}

<template>
<AiTitleSuggester @buffered={{@outletArgs.buffered}} />
<AiTitleSuggester @buffered={{@outletArgs.buffered}} @topicState="edit" />
</template>
}
16 changes: 16 additions & 0 deletions lib/ai_helper/assistant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
64 changes: 64 additions & 0 deletions spec/requests/ai_helper/assistant_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
"<item>What are your favourite fruits?</item><item>Love for fruits</item><item>Fruits are amazing</item><item>Favourite fruit list</item><item>Fruit share topic</item>"
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
"<item>Apples - the best fruit</item><item>Why apples are great</item><item>Apples are the best fruit</item><item>My love for apples</item><item>I love apples</item>"
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) }
Expand Down