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:
parent
b942a18298
commit
1d097b9d82
|
@ -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)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -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>
|
|
@ -25,3 +25,7 @@
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topic-above-suggested-outlet.related-topics {
|
||||||
|
margin: 4.5em 0 1em;
|
||||||
|
}
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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: ""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue