Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 6aaf8a0

Browse files
authored
DEV: Use existing topic embeddings when suggesting tags/categories on edit (#1189)
When editing a topic (instead of creating one) and using the tag/category suggestion buttons. We want to use existing topic embeddings instead of creating new ones.
1 parent ac29d30 commit 6aaf8a0

File tree

9 files changed

+72
-58
lines changed

9 files changed

+72
-58
lines changed

app/controllers/discourse_ai/ai_helper/assistant_controller.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,26 @@ def suggest_title
7878
end
7979

8080
def suggest_category
81-
input = get_text_param!
82-
input_hash = { text: input }
81+
if params[:topic_id]
82+
opts = { topic_id: params[:topic_id] }
83+
else
84+
input = get_text_param!
85+
opts = { text: input }
86+
end
8387

84-
render json:
85-
DiscourseAi::AiHelper::SemanticCategorizer.new(
86-
input_hash,
87-
current_user,
88-
).categories,
88+
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(current_user, opts).categories,
8989
status: 200
9090
end
9191

9292
def suggest_tags
93-
input = get_text_param!
94-
input_hash = { text: input }
93+
if params[:topic_id]
94+
opts = { topic_id: params[:topic_id] }
95+
else
96+
input = get_text_param!
97+
opts = { text: input }
98+
end
9599

96-
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(input_hash, current_user).tags,
100+
render json: DiscourseAi::AiHelper::SemanticCategorizer.new(current_user, opts).tags,
97101
status: 200
98102
end
99103

assets/javascripts/discourse/components/suggestion-menus/ai-category-suggester.gjs

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,13 @@ export default class AiCategorySuggester extends Component {
2020
@tracked untriggers = [];
2121
@tracked triggerIcon = "discourse-sparkles";
2222
@tracked content = null;
23-
@tracked topicContent = null;
24-
25-
constructor() {
26-
super(...arguments);
27-
if (!this.topicContent && this.args.composer?.reply === undefined) {
28-
this.fetchTopicContent();
29-
}
30-
}
31-
32-
async fetchTopicContent() {
33-
await ajax(`/t/${this.args.buffered.content.id}.json`).then(
34-
({ post_stream }) => {
35-
this.topicContent = post_stream.posts[0].cooked;
36-
}
37-
);
38-
}
3923

4024
get showSuggestionButton() {
4125
const composerFields = document.querySelector(".composer-fields");
42-
this.content = this.args.composer?.reply || this.topicContent;
43-
const showTrigger = this.content?.length > MIN_CHARACTER_COUNT;
26+
this.content = this.args.composer?.reply;
27+
const showTrigger =
28+
this.content?.length > MIN_CHARACTER_COUNT ||
29+
this.args.topicState === "edit";
4430

4531
if (composerFields) {
4632
if (showTrigger) {
@@ -62,12 +48,20 @@ export default class AiCategorySuggester extends Component {
6248
this.loading = true;
6349
this.triggerIcon = "spinner";
6450

51+
const data = {};
52+
53+
if (this.content) {
54+
data.text = this.content;
55+
} else {
56+
data.topic_id = this.args.buffered.content.id;
57+
}
58+
6559
try {
6660
const { assistant } = await ajax(
6761
"/discourse-ai/ai-helper/suggest_category",
6862
{
6963
method: "POST",
70-
data: { text: this.content },
64+
data,
7165
}
7266
);
7367
this.suggestions = assistant;

assets/javascripts/discourse/components/suggestion-menus/ai-tag-suggester.gjs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,13 @@ export default class AiTagSuggester extends Component {
2121
@tracked untriggers = [];
2222
@tracked triggerIcon = "discourse-sparkles";
2323
@tracked content = null;
24-
@tracked topicContent = null;
25-
26-
constructor() {
27-
super(...arguments);
28-
if (!this.topicContent && this.args.composer?.reply === undefined) {
29-
this.fetchTopicContent();
30-
}
31-
}
32-
33-
async fetchTopicContent() {
34-
await ajax(`/t/${this.args.buffered.content.id}.json`).then(
35-
({ post_stream }) => {
36-
this.topicContent = post_stream.posts[0].cooked;
37-
}
38-
);
39-
}
4024

4125
get showSuggestionButton() {
4226
const composerFields = document.querySelector(".composer-fields");
43-
this.content = this.args.composer?.reply || this.topicContent;
44-
const showTrigger = this.content?.length > MIN_CHARACTER_COUNT;
27+
this.content = this.args.composer?.reply;
28+
const showTrigger =
29+
this.content?.length > MIN_CHARACTER_COUNT ||
30+
this.args.topicState === "edit";
4531

4632
if (composerFields) {
4733
if (showTrigger) {
@@ -74,15 +60,25 @@ export default class AiTagSuggester extends Component {
7460
this.loading = true;
7561
this.triggerIcon = "spinner";
7662

63+
const data = {};
64+
65+
if (this.content) {
66+
data.text = this.content;
67+
} else {
68+
data.topic_id = this.args.buffered.content.id;
69+
}
70+
7771
try {
7872
const { assistant } = await ajax("/discourse-ai/ai-helper/suggest_tags", {
7973
method: "POST",
80-
data: { text: this.content },
74+
data,
8175
});
8276
this.suggestions = assistant;
77+
8378
const model = this.args.composer
8479
? this.args.composer
8580
: this.args.buffered;
81+
8682
if (this.#tagSelectorHasValues()) {
8783
this.suggestions = this.suggestions.filter(
8884
(s) => !model.get("tags").includes(s.name)

assets/javascripts/discourse/connectors/after-composer-category-input/ai-category-suggestion.gjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export default class AiCategorySuggestion extends Component {
1313
}
1414

1515
<template>
16-
<AiCategorySuggester @composer={{@outletArgs.composer}} />
16+
<AiCategorySuggester @composer={{@outletArgs.composer}} @topicState="new" />
1717
</template>
1818
}

assets/javascripts/discourse/connectors/after-composer-tag-input/ai-tag-suggestion.gjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export default class AiTagSuggestion extends Component {
1313
}
1414

1515
<template>
16-
<AiTagSuggester @composer={{@outletArgs.composer}} />
16+
<AiTagSuggester @composer={{@outletArgs.composer}} @topicState="new" />
1717
</template>
1818
}

assets/javascripts/discourse/connectors/edit-topic-category__after/ai-category-suggestion.gjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export default class AiCategorySuggestion extends Component {
1313
}
1414

1515
<template>
16-
<AiCategorySuggester @buffered={{@outletArgs.buffered}} />
16+
<AiCategorySuggester
17+
@buffered={{@outletArgs.buffered}}
18+
@topicState="edit"
19+
/>
1720
</template>
1821
}

assets/javascripts/discourse/connectors/edit-topic-tags__after/ai-tag-suggestion.gjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export default class AiCategorySuggestion extends Component {
1313
}
1414

1515
<template>
16-
<AiTagSuggester @buffered={{@outletArgs.buffered}} />
16+
<AiTagSuggester @buffered={{@outletArgs.buffered}} @topicState="edit" />
1717
</template>
1818
}

lib/ai_helper/semantic_categorizer.rb

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
module DiscourseAi
33
module AiHelper
44
class SemanticCategorizer
5-
def initialize(input, user)
5+
def initialize(user, opts)
66
@user = user
7-
@text = input[:text]
7+
@text = opts[:text]
88
@vector = DiscourseAi::Embeddings::Vector.instance
99
@schema = DiscourseAi::Embeddings::Schema.for(Topic)
10+
@topic_id = opts[:topic_id]
1011
end
1112

1213
def categories
13-
return [] if @text.blank?
14+
return [] if @text.blank? && @topic_id.nil?
1415
return [] if !DiscourseAi::Embeddings.enabled?
1516

1617
candidates = nearest_neighbors
@@ -55,7 +56,7 @@ def categories
5556
end
5657

5758
def tags
58-
return [] if @text.blank?
59+
return [] if @text.blank? && @topic_id.nil?
5960
return [] if !DiscourseAi::Embeddings.enabled?
6061

6162
candidates = nearest_neighbors(limit: 100)
@@ -100,7 +101,23 @@ def tags
100101
private
101102

102103
def nearest_neighbors(limit: 50)
103-
raw_vector = @vector.vector_from(@text)
104+
if @topic_id
105+
target = Topic.find_by(id: @topic_id)
106+
embeddings = @schema.find_by_target(target)&.embeddings
107+
108+
if embeddings.blank?
109+
@text =
110+
DiscourseAi::Summarization::Strategies::TopicSummary
111+
.new(target)
112+
.targets_data
113+
.pluck(:text)
114+
raw_vector = @vector.vector_from(@text)
115+
else
116+
raw_vector = JSON.parse(embeddings)
117+
end
118+
else
119+
raw_vector = @vector.vector_from(@text)
120+
end
104121

105122
muted_category_ids = nil
106123
if @user.present?

spec/lib/modules/ai_helper/semantic_categorizer_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
fab!(:topic) { Fabricate(:topic, category: category) }
1717

1818
let(:vector) { DiscourseAi::Embeddings::Vector.instance }
19-
let(:categorizer) { DiscourseAi::AiHelper::SemanticCategorizer.new({ text: "hello" }, user) }
19+
let(:categorizer) { DiscourseAi::AiHelper::SemanticCategorizer.new(user, { text: "hello" }) }
2020
let(:expected_embedding) { [0.0038493] * vector.vdef.dimensions }
2121

2222
before do

0 commit comments

Comments
 (0)