mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-27 01:52:18 +00:00
FEATURE: allow limiting results in related topics section (#30)
Also: - Normalizes behavior between logged in and anon, we only show related topics in the related topic section - Renames "suggested" to "related" given this only exists in related section - Adds a spec section to ensure anon does not regress - Adds `ai_embeddings_semantic_related_topics` to limit related topics Renamed settings: ai_embeddings_semantic_suggested_model -> ai_embeddings_semantic_related_model ai_embeddings_semantic_suggested_topics_enabled -> ai_embeddings_semantic_related_topics_enabled Plugins is still in an experimental phase and not much is overidden hence avoiding adding site setting migrations. Co-authored-by: Krzysztof Kotlarek <kotlarek.krzysztof@gmail.com>
This commit is contained in:
parent
1d097b9d82
commit
0d80d9ec49
@ -42,9 +42,10 @@ en:
|
|||||||
ai_embeddings_discourse_service_api_endpoint: "URL where the API is running for the embeddings module"
|
ai_embeddings_discourse_service_api_endpoint: "URL where the API is running for the embeddings module"
|
||||||
ai_embeddings_discourse_service_api_key: "API key for the embeddings API"
|
ai_embeddings_discourse_service_api_key: "API key for the embeddings API"
|
||||||
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_related_model: "Model to use for related 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_enabled: "Use Semantic Search for related topics."
|
ai_embeddings_semantic_related_topics_enabled: "Use Semantic Search for related topics."
|
||||||
|
ai_embeddings_semantic_related_topics: "Maximum number of topics to show in related topic section."
|
||||||
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:
|
||||||
|
@ -124,7 +124,7 @@ plugins:
|
|||||||
- msmarco-distilbert-base-v4
|
- msmarco-distilbert-base-v4
|
||||||
- msmarco-distilbert-base-tas-b
|
- msmarco-distilbert-base-tas-b
|
||||||
- text-embedding-ada-002
|
- text-embedding-ada-002
|
||||||
ai_embeddings_semantic_suggested_model:
|
ai_embeddings_semantic_related_model:
|
||||||
type: enum
|
type: enum
|
||||||
default: all-mpnet-base-v2
|
default: all-mpnet-base-v2
|
||||||
choices:
|
choices:
|
||||||
@ -134,5 +134,6 @@ 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_enabled: false
|
ai_embeddings_semantic_related_topics_enabled: false
|
||||||
|
ai_embeddings_semantic_related_topics: 5
|
||||||
ai_embeddings_pg_connection_string: ""
|
ai_embeddings_pg_connection_string: ""
|
||||||
|
@ -7,13 +7,12 @@ module DiscourseAi
|
|||||||
require_relative "models"
|
require_relative "models"
|
||||||
require_relative "topic"
|
require_relative "topic"
|
||||||
require_relative "jobs/regular/generate_embeddings"
|
require_relative "jobs/regular/generate_embeddings"
|
||||||
require_relative "semantic_suggested"
|
require_relative "semantic_related"
|
||||||
end
|
end
|
||||||
|
|
||||||
def inject_into(plugin)
|
def inject_into(plugin)
|
||||||
plugin.add_to_class(:topic_view, :related_topics) do
|
plugin.add_to_class(:topic_view, :related_topics) do
|
||||||
if !@guardian&.user || topic.private_message? ||
|
if topic.private_message? || !SiteSetting.ai_embeddings_semantic_related_topics_enabled
|
||||||
!SiteSetting.ai_embeddings_semantic_suggested_topics_enabled
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ module DiscourseAi
|
|||||||
TopicList.new(
|
TopicList.new(
|
||||||
:suggested,
|
:suggested,
|
||||||
nil,
|
nil,
|
||||||
DiscourseAi::Embeddings::SemanticSuggested.candidates_for(topic),
|
DiscourseAi::Embeddings::SemanticRelated.candidates_for(topic),
|
||||||
).topics
|
).topics
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ module DiscourseAi
|
|||||||
|
|
||||||
%i[topic_view TopicViewPosts].each do |serializer|
|
%i[topic_view TopicViewPosts].each do |serializer|
|
||||||
plugin.add_to_serializer(serializer, :related_topics) do
|
plugin.add_to_serializer(serializer, :related_topics) do
|
||||||
if object.next_page.nil? && !object.topic.private_message? && scope.authenticated?
|
if object.next_page.nil? && !object.topic.private_message?
|
||||||
object.related_topics.map do |t|
|
object.related_topics.map do |t|
|
||||||
SuggestedTopicSerializer.new(t, scope: scope, root: false)
|
SuggestedTopicSerializer.new(t, scope: scope, root: false)
|
||||||
end
|
end
|
||||||
@ -44,7 +43,7 @@ module DiscourseAi
|
|||||||
|
|
||||||
# custom include method so we also check on semantic search
|
# custom include method so we also check on semantic search
|
||||||
plugin.add_to_serializer(serializer, :include_related_topics?) do
|
plugin.add_to_serializer(serializer, :include_related_topics?) do
|
||||||
plugin.enabled? && SiteSetting.ai_embeddings_semantic_suggested_topics_enabled
|
plugin.enabled? && SiteSetting.ai_embeddings_semantic_related_topics_enabled
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -57,11 +56,6 @@ module DiscourseAi
|
|||||||
|
|
||||||
plugin.on(:topic_created, &callback)
|
plugin.on(:topic_created, &callback)
|
||||||
plugin.on(:topic_edited, &callback)
|
plugin.on(:topic_edited, &callback)
|
||||||
|
|
||||||
DiscoursePluginRegistry.register_list_suggested_for_provider(
|
|
||||||
SemanticSuggested.method(:build_suggested_topics),
|
|
||||||
plugin,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,16 +2,10 @@
|
|||||||
|
|
||||||
module DiscourseAi
|
module DiscourseAi
|
||||||
module Embeddings
|
module Embeddings
|
||||||
class SemanticSuggested
|
class SemanticRelated
|
||||||
def self.build_suggested_topics(topic, pm_params, topic_query)
|
|
||||||
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)
|
def self.candidates_for(topic)
|
||||||
|
return ::Topic.none if SiteSetting.ai_embeddings_semantic_related_topics < 1
|
||||||
|
|
||||||
cache_for =
|
cache_for =
|
||||||
case topic.created_at
|
case topic.created_at
|
||||||
when 6.hour.ago..Time.now
|
when 6.hour.ago..Time.now
|
||||||
@ -30,7 +24,7 @@ module DiscourseAi
|
|||||||
search_suggestions(topic)
|
search_suggestions(topic)
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error("SemanticSuggested: #{e}")
|
Rails.logger.error("SemanticRelated: #{e}")
|
||||||
Jobs.enqueue(:generate_embeddings, topic_id: topic.id)
|
Jobs.enqueue(:generate_embeddings, topic_id: topic.id)
|
||||||
return ::Topic.none
|
return ::Topic.none
|
||||||
end
|
end
|
||||||
@ -42,10 +36,11 @@ module DiscourseAi
|
|||||||
.secured
|
.secured
|
||||||
.where(id: candidate_ids)
|
.where(id: candidate_ids)
|
||||||
.order("array_position(ARRAY#{candidate_ids}, id)")
|
.order("array_position(ARRAY#{candidate_ids}, id)")
|
||||||
|
.limit(SiteSetting.ai_embeddings_semantic_related_topics)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search_suggestions(topic)
|
def self.search_suggestions(topic)
|
||||||
model_name = SiteSetting.ai_embeddings_semantic_suggested_model
|
model_name = SiteSetting.ai_embeddings_semantic_related_model
|
||||||
model = DiscourseAi::Embeddings::Models.list.find { |m| m.name == model_name }
|
model = DiscourseAi::Embeddings::Models.list.find { |m| m.name == model_name }
|
||||||
function =
|
function =
|
||||||
DiscourseAi::Embeddings::Models::SEARCH_FUNCTION_TO_PG_FUNCTION[model.functions.first]
|
DiscourseAi::Embeddings::Models::SEARCH_FUNCTION_TO_PG_FUNCTION[model.functions.first]
|
37
spec/lib/modules/embeddings/semantic_related_spec.rb
Normal file
37
spec/lib/modules/embeddings/semantic_related_spec.rb
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
describe DiscourseAi::Embeddings::SemanticRelated do
|
||||||
|
fab!(:target) { Fabricate(:topic) }
|
||||||
|
fab!(:normal_topic_1) { Fabricate(:topic) }
|
||||||
|
fab!(:normal_topic_2) { Fabricate(:topic) }
|
||||||
|
fab!(:normal_topic_3) { Fabricate(:topic) }
|
||||||
|
fab!(:unlisted_topic) { Fabricate(:topic, visible: false) }
|
||||||
|
fab!(:private_topic) { Fabricate(:private_message_topic) }
|
||||||
|
fab!(:secured_category) { Fabricate(:category, read_restricted: true) }
|
||||||
|
fab!(:secured_category_topic) { Fabricate(:topic, category: secured_category) }
|
||||||
|
|
||||||
|
before { SiteSetting.ai_embeddings_semantic_related_topics_enabled = true }
|
||||||
|
|
||||||
|
describe "#candidates_for" do
|
||||||
|
before do
|
||||||
|
Discourse.cache.clear
|
||||||
|
described_class.stubs(:search_suggestions).returns(
|
||||||
|
Topic.unscoped.order(id: :desc).limit(10).pluck(:id),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
after { Discourse.cache.clear }
|
||||||
|
|
||||||
|
it "returns the related topics without non public topics" do
|
||||||
|
results = described_class.candidates_for(target).to_a
|
||||||
|
expect(results).to include(normal_topic_1)
|
||||||
|
expect(results).to include(normal_topic_2)
|
||||||
|
expect(results).to include(normal_topic_3)
|
||||||
|
expect(results).to_not include(unlisted_topic)
|
||||||
|
expect(results).to_not include(private_topic)
|
||||||
|
expect(results).to_not include(secured_category_topic)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,38 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "rails_helper"
|
|
||||||
|
|
||||||
describe DiscourseAi::Embeddings::SemanticSuggested do
|
|
||||||
fab!(:target) { Fabricate(:topic) }
|
|
||||||
fab!(:normal_topic_1) { Fabricate(:topic) }
|
|
||||||
fab!(:normal_topic_2) { Fabricate(:topic) }
|
|
||||||
fab!(:normal_topic_3) { Fabricate(:topic) }
|
|
||||||
fab!(:unlisted_topic) { Fabricate(:topic, visible: false) }
|
|
||||||
fab!(:private_topic) { Fabricate(:private_message_topic) }
|
|
||||||
fab!(:secured_category) { Fabricate(:category, read_restricted: true) }
|
|
||||||
fab!(:secured_category_topic) { Fabricate(:topic, category: secured_category) }
|
|
||||||
|
|
||||||
before { SiteSetting.ai_embeddings_semantic_suggested_topics_enabled = true }
|
|
||||||
|
|
||||||
describe "#build_suggested_topics" do
|
|
||||||
before do
|
|
||||||
Discourse.cache.clear
|
|
||||||
described_class.stubs(:search_suggestions).returns(
|
|
||||||
Topic.unscoped.order(id: :desc).limit(10).pluck(:id),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
after { Discourse.cache.clear }
|
|
||||||
|
|
||||||
it "returns the suggested topics without non public topics" do
|
|
||||||
suggested = described_class.build_suggested_topics(target, {}, TopicQuery.new(nil))
|
|
||||||
suggested_results = suggested[:result]
|
|
||||||
expect(suggested_results).to include(normal_topic_1)
|
|
||||||
expect(suggested_results).to include(normal_topic_2)
|
|
||||||
expect(suggested_results).to include(normal_topic_3)
|
|
||||||
expect(suggested_results).to_not include(unlisted_topic)
|
|
||||||
expect(suggested_results).to_not include(private_topic)
|
|
||||||
expect(suggested_results).to_not include(secured_category_topic)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -6,25 +6,36 @@ describe ::TopicsController do
|
|||||||
fab!(:topic) { Fabricate(:topic) }
|
fab!(:topic) { Fabricate(:topic) }
|
||||||
fab!(:topic1) { Fabricate(:topic) }
|
fab!(:topic1) { Fabricate(:topic) }
|
||||||
fab!(:topic2) { Fabricate(:topic) }
|
fab!(:topic2) { Fabricate(:topic) }
|
||||||
|
fab!(:topic3) { Fabricate(:topic) }
|
||||||
fab!(:user) { Fabricate(:admin) }
|
fab!(:user) { Fabricate(:admin) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Discourse.cache.clear
|
Discourse.cache.clear
|
||||||
SiteSetting.ai_embeddings_semantic_suggested_topics_enabled = true
|
SiteSetting.ai_embeddings_semantic_related_topics_enabled = true
|
||||||
|
SiteSetting.ai_embeddings_semantic_related_topics = 2
|
||||||
end
|
end
|
||||||
|
|
||||||
after { Discourse.cache.clear }
|
after { Discourse.cache.clear }
|
||||||
|
|
||||||
context "when a user is logged on" do
|
context "when a user is logged on" do
|
||||||
it "includes related topics in payload when configured" do
|
it "includes related topics in payload when configured" do
|
||||||
DiscourseAi::Embeddings::SemanticSuggested.stubs(:search_suggestions).returns([topic2.id])
|
DiscourseAi::Embeddings::SemanticRelated.stubs(:search_suggestions).returns(
|
||||||
|
[topic1.id, topic2.id, topic3.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
get("#{topic.relative_url}.json")
|
||||||
|
json = response.parsed_body
|
||||||
|
|
||||||
|
expect(json["suggested_topics"].length).to eq(0)
|
||||||
|
expect(json["related_topics"].length).to eq(2)
|
||||||
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
||||||
get("#{topic.relative_url}.json")
|
get("#{topic.relative_url}.json")
|
||||||
json = response.parsed_body
|
json = response.parsed_body
|
||||||
|
|
||||||
expect(json["suggested_topics"].length).to eq(0)
|
expect(json["suggested_topics"].length).to eq(0)
|
||||||
expect(json["related_topics"].length).to be > 0
|
expect(json["related_topics"].length).to eq(2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user