diff --git a/assets/javascripts/discourse/connectors/topic-above-suggested/related-topics.js b/assets/javascripts/discourse/connectors/topic-above-suggested/related-topics.js
new file mode 100644
index 00000000..41f954dd
--- /dev/null
+++ b/assets/javascripts/discourse/connectors/topic-above-suggested/related-topics.js
@@ -0,0 +1,15 @@
+export default {
+ shouldRender(args) {
+ return (args.model.related_topics?.length || 0) > 0;
+ },
+ setupComponent(args, component) {
+ if (component.model.related_topics) {
+ component.set(
+ "relatedTopics",
+ component.model.related_topics.map((topic) =>
+ this.store.createRecord("topic", topic)
+ )
+ );
+ }
+ },
+};
diff --git a/assets/javascripts/discourse/templates/connectors/topic-above-suggested/related-topics.hbs b/assets/javascripts/discourse/templates/connectors/topic-above-suggested/related-topics.hbs
new file mode 100644
index 00000000..282be2d7
--- /dev/null
+++ b/assets/javascripts/discourse/templates/connectors/topic-above-suggested/related-topics.hbs
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
index bc251d66..463143c0 100644
--- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
+++ b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
@@ -25,3 +25,7 @@
margin-bottom: 20px;
}
}
+
+.topic-above-suggested-outlet.related-topics {
+ margin: 4.5em 0 1em;
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 818df0dc..8bbd29cd 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1,6 +1,8 @@
en:
js:
discourse_ai:
+ related_topics:
+ title: "Related Topics"
ai_helper:
title: "Suggest changes using AI"
description: "Choose one of the options below, and the AI will suggest you a new version of the text."
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index c46492c5..07e0ed07 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -44,7 +44,7 @@ en:
ai_embeddings_models: "Discourse will generate embeddings for each of the models enabled here"
ai_embeddings_semantic_suggested_model: "Model to use for suggested topics."
ai_embeddings_generate_for_pms: "Generate embeddings for personal messages."
- ai_embeddings_semantic_suggested_topics_anons_enabled: "Use Semantic Search for suggested topics for anonymous users."
+ ai_embeddings_semantic_suggested_topics_enabled: "Use Semantic Search for related topics."
ai_embeddings_pg_connection_string: "PostgreSQL connection string for the embeddings module. Needs pgvector extension enabled and a series of tables created. See docs for more info."
reviewables:
diff --git a/config/settings.yml b/config/settings.yml
index 4d7c2136..9e23f304 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -134,5 +134,5 @@ plugins:
- multi-qa-mpnet-base-dot-v1
- paraphrase-multilingual-mpnet-base-v2
ai_embeddings_generate_for_pms: false
- ai_embeddings_semantic_suggested_topics_anons_enabled: false
+ ai_embeddings_semantic_suggested_topics_enabled: false
ai_embeddings_pg_connection_string: ""
diff --git a/lib/modules/embeddings/entry_point.rb b/lib/modules/embeddings/entry_point.rb
index 93c4c499..73c8547a 100644
--- a/lib/modules/embeddings/entry_point.rb
+++ b/lib/modules/embeddings/entry_point.rb
@@ -11,6 +11,43 @@ module DiscourseAi
end
def inject_into(plugin)
+ plugin.add_to_class(:topic_view, :related_topics) do
+ if !@guardian&.user || topic.private_message? ||
+ !SiteSetting.ai_embeddings_semantic_suggested_topics_enabled
+ return nil
+ end
+
+ @related_topics ||=
+ TopicList.new(
+ :suggested,
+ nil,
+ DiscourseAi::Embeddings::SemanticSuggested.candidates_for(topic),
+ ).topics
+ end
+
+ plugin.register_modifier(
+ :topic_view_suggested_topics_options,
+ ) do |suggested_options, topic_view|
+ related_topics = topic_view.related_topics
+ include_random = related_topics.nil? || related_topics.length == 0
+ suggested_options.merge(include_random: include_random)
+ end
+
+ %i[topic_view TopicViewPosts].each do |serializer|
+ plugin.add_to_serializer(serializer, :related_topics) do
+ if object.next_page.nil? && !object.topic.private_message? && scope.authenticated?
+ object.related_topics.map do |t|
+ SuggestedTopicSerializer.new(t, scope: scope, root: false)
+ end
+ end
+ end
+
+ # custom include method so we also check on semantic search
+ plugin.add_to_serializer(serializer, :include_related_topics?) do
+ plugin.enabled? && SiteSetting.ai_embeddings_semantic_suggested_topics_enabled
+ end
+ end
+
callback =
Proc.new do |topic|
if SiteSetting.ai_embeddings_enabled
diff --git a/lib/modules/embeddings/semantic_suggested.rb b/lib/modules/embeddings/semantic_suggested.rb
index a170418f..fd01e9ba 100644
--- a/lib/modules/embeddings/semantic_suggested.rb
+++ b/lib/modules/embeddings/semantic_suggested.rb
@@ -4,10 +4,14 @@ module DiscourseAi
module Embeddings
class SemanticSuggested
def self.build_suggested_topics(topic, pm_params, topic_query)
- return unless SiteSetting.ai_embeddings_semantic_suggested_topics_anons_enabled
+ return unless SiteSetting.ai_embeddings_semantic_suggested_topics_enabled
return if topic_query.user
return if topic.private_message?
+ { result: candidates_for(topic), params: {} }
+ end
+
+ def self.candidates_for(topic)
cache_for =
case topic.created_at
when 6.hour.ago..Time.now
@@ -28,19 +32,16 @@ module DiscourseAi
rescue StandardError => e
Rails.logger.error("SemanticSuggested: #{e}")
Jobs.enqueue(:generate_embeddings, topic_id: topic.id)
- return { result: [], params: {} }
+ return ::Topic.none
end
# array_position forces the order of the topics to be preserved
- candidates =
- ::Topic
- .visible
- .listable_topics
- .secured
- .where(id: candidate_ids)
- .order("array_position(ARRAY#{candidate_ids}, id)")
-
- { result: candidates, params: {} }
+ ::Topic
+ .visible
+ .listable_topics
+ .secured
+ .where(id: candidate_ids)
+ .order("array_position(ARRAY#{candidate_ids}, id)")
end
def self.search_suggestions(topic)
diff --git a/spec/lib/modules/embeddings/semantic_suggested_spec.rb b/spec/lib/modules/embeddings/semantic_suggested_spec.rb
index d6f2affd..5e3a525e 100644
--- a/spec/lib/modules/embeddings/semantic_suggested_spec.rb
+++ b/spec/lib/modules/embeddings/semantic_suggested_spec.rb
@@ -12,7 +12,7 @@ describe DiscourseAi::Embeddings::SemanticSuggested do
fab!(:secured_category) { Fabricate(:category, read_restricted: true) }
fab!(:secured_category_topic) { Fabricate(:topic, category: secured_category) }
- before { SiteSetting.ai_embeddings_semantic_suggested_topics_anons_enabled = true }
+ before { SiteSetting.ai_embeddings_semantic_suggested_topics_enabled = true }
describe "#build_suggested_topics" do
before do
diff --git a/spec/requests/topic_spec.rb b/spec/requests/topic_spec.rb
new file mode 100644
index 00000000..c13d4020
--- /dev/null
+++ b/spec/requests/topic_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe ::TopicsController do
+ fab!(:topic) { Fabricate(:topic) }
+ fab!(:topic1) { Fabricate(:topic) }
+ fab!(:topic2) { Fabricate(:topic) }
+ fab!(:user) { Fabricate(:admin) }
+
+ before do
+ Discourse.cache.clear
+ SiteSetting.ai_embeddings_semantic_suggested_topics_enabled = true
+ end
+
+ after { Discourse.cache.clear }
+
+ context "when a user is logged on" do
+ it "includes related topics in payload when configured" do
+ DiscourseAi::Embeddings::SemanticSuggested.stubs(:search_suggestions).returns([topic2.id])
+ sign_in(user)
+
+ get("#{topic.relative_url}.json")
+ json = response.parsed_body
+
+ expect(json["suggested_topics"].length).to eq(0)
+ expect(json["related_topics"].length).to be > 0
+ end
+ end
+end