FEATURE: attempt to include related topics above suggested (#28)

Allows related topics to show up for logged on users

- Introduces a new "Related Topics" block above suggested when related topics exist
- Renames `ai_embeddings_semantic_suggested_topics_anons_enabled` -> `ai_embeddings_semantic_suggested_topics_enabled` (given it is only deployed on 1 site not bothering with a migration)
- Adds an integration test to ensure data arrives correctly on the client
This commit is contained in:
Sam 2023-03-31 09:07:22 +11:00 committed by GitHub
parent b942a18298
commit 1d097b9d82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 109 additions and 14 deletions

View File

@ -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)
)
);
}
},
};

View File

@ -0,0 +1,6 @@
<div>
<h3 id="related-topics-title" class="related-topics-title">
{{i18n "discourse_ai.related_topics.title"}}
</h3>
<BasicTopicList @topics={{this.relatedTopics}} />
</div>

View File

@ -25,3 +25,7 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
} }
.topic-above-suggested-outlet.related-topics {
margin: 4.5em 0 1em;
}

View File

@ -1,6 +1,8 @@
en: en:
js: js:
discourse_ai: discourse_ai:
related_topics:
title: "Related Topics"
ai_helper: ai_helper:
title: "Suggest changes using AI" title: "Suggest changes using AI"
description: "Choose one of the options below, and the AI will suggest you a new version of the text." description: "Choose one of the options below, and the AI will suggest you a new version of the text."

View File

@ -44,7 +44,7 @@ en:
ai_embeddings_models: "Discourse will generate embeddings for each of the models enabled here" 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_semantic_suggested_model: "Model to use for suggested topics."
ai_embeddings_generate_for_pms: "Generate embeddings for personal messages." 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." 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: reviewables:

View File

@ -134,5 +134,5 @@ plugins:
- multi-qa-mpnet-base-dot-v1 - multi-qa-mpnet-base-dot-v1
- paraphrase-multilingual-mpnet-base-v2 - paraphrase-multilingual-mpnet-base-v2
ai_embeddings_generate_for_pms: false 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: "" ai_embeddings_pg_connection_string: ""

View File

@ -11,6 +11,43 @@ module DiscourseAi
end end
def inject_into(plugin) 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 = callback =
Proc.new do |topic| Proc.new do |topic|
if SiteSetting.ai_embeddings_enabled if SiteSetting.ai_embeddings_enabled

View File

@ -4,10 +4,14 @@ module DiscourseAi
module Embeddings module Embeddings
class SemanticSuggested class SemanticSuggested
def self.build_suggested_topics(topic, pm_params, topic_query) 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_query.user
return if topic.private_message? return if topic.private_message?
{ result: candidates_for(topic), params: {} }
end
def self.candidates_for(topic)
cache_for = cache_for =
case topic.created_at case topic.created_at
when 6.hour.ago..Time.now when 6.hour.ago..Time.now
@ -28,19 +32,16 @@ module DiscourseAi
rescue StandardError => e rescue StandardError => e
Rails.logger.error("SemanticSuggested: #{e}") Rails.logger.error("SemanticSuggested: #{e}")
Jobs.enqueue(:generate_embeddings, topic_id: topic.id) Jobs.enqueue(:generate_embeddings, topic_id: topic.id)
return { result: [], params: {} } return ::Topic.none
end end
# array_position forces the order of the topics to be preserved # array_position forces the order of the topics to be preserved
candidates = ::Topic
::Topic .visible
.visible .listable_topics
.listable_topics .secured
.secured .where(id: candidate_ids)
.where(id: candidate_ids) .order("array_position(ARRAY#{candidate_ids}, id)")
.order("array_position(ARRAY#{candidate_ids}, id)")
{ result: candidates, params: {} }
end end
def self.search_suggestions(topic) def self.search_suggestions(topic)

View File

@ -12,7 +12,7 @@ describe DiscourseAi::Embeddings::SemanticSuggested do
fab!(:secured_category) { Fabricate(:category, read_restricted: true) } fab!(:secured_category) { Fabricate(:category, read_restricted: true) }
fab!(:secured_category_topic) { Fabricate(:topic, category: secured_category) } 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 describe "#build_suggested_topics" do
before do before do

View File

@ -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