discourse-ai/spec/jobs/scheduled/generate_concepts_from_popular_items_spec.rb
Rafael dos Santos Silva 478f31de47
FEATURE: add inferred concepts system (#1330)
* FEATURE: add inferred concepts system

This commit adds a new inferred concepts system that:
- Creates a model for storing concept labels that can be applied to topics
- Provides AI personas for finding new concepts and matching existing ones
- Adds jobs for generating concepts from popular topics
- Includes a scheduled job that automatically processes engaging topics

* FEATURE: Extend inferred concepts to include posts

* Adds support for concepts to be inferred from and applied to posts
* Replaces daily task with one that handles both topics and posts
* Adds database migration for posts_inferred_concepts join table
* Updates PersonaContext to include inferred concepts



Co-authored-by: Roman Rizzi <rizziromanalejandro@gmail.com>
Co-authored-by: Keegan George <kgeorge13@gmail.com>
2025-06-02 14:29:20 -03:00

260 lines
7.8 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Jobs::GenerateConceptsFromPopularItems do
fab!(:topic) { Fabricate(:topic, posts_count: 6, views: 150, like_count: 12) }
fab!(:post) { Fabricate(:post, like_count: 8, post_number: 2) }
before do
SiteSetting.inferred_concepts_enabled = true
SiteSetting.inferred_concepts_daily_topics_limit = 20
SiteSetting.inferred_concepts_daily_posts_limit = 30
SiteSetting.inferred_concepts_min_posts = 5
SiteSetting.inferred_concepts_min_likes = 10
SiteSetting.inferred_concepts_min_views = 100
SiteSetting.inferred_concepts_post_min_likes = 5
SiteSetting.inferred_concepts_lookback_days = 30
SiteSetting.inferred_concepts_background_match = false
end
describe "#execute" do
it "does nothing when inferred_concepts_enabled is false" do
SiteSetting.inferred_concepts_enabled = false
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).not_to receive(
:find_candidate_topics,
)
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).not_to receive(
:find_candidate_posts,
)
allow(Jobs).to receive(:enqueue)
subject.execute({})
end
it "processes popular topics when enabled" do
candidate_topics = [topic]
freeze_time do
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).with(
limit: 20,
min_posts: 5,
min_likes: 10,
min_views: 100,
created_after: 30.days.ago,
).and_return(candidate_topics)
allow(Jobs).to receive(:enqueue).with(
:generate_inferred_concepts,
item_type: "topics",
item_ids: [topic.id],
batch_size: 10,
)
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).and_return([])
subject.execute({})
end
end
it "processes popular posts when enabled" do
candidate_posts = [post]
freeze_time do
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).and_return([])
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).with(
limit: 30,
min_likes: 5,
exclude_first_posts: true,
created_after: 30.days.ago,
).and_return(candidate_posts)
allow(Jobs).to receive(:enqueue).with(
:generate_inferred_concepts,
item_type: "posts",
item_ids: [post.id],
batch_size: 10,
)
subject.execute({})
end
end
it "schedules background matching jobs when enabled" do
SiteSetting.inferred_concepts_background_match = true
candidate_topics = [topic]
candidate_posts = [post]
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).and_return(candidate_topics)
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).and_return(candidate_posts)
# Expect generation jobs
allow(Jobs).to receive(:enqueue).with(
:generate_inferred_concepts,
item_type: "topics",
item_ids: [topic.id],
batch_size: 10,
)
allow(Jobs).to receive(:enqueue).with(
:generate_inferred_concepts,
item_type: "posts",
item_ids: [post.id],
batch_size: 10,
)
# Expect background matching jobs
allow(Jobs).to receive(:enqueue_in).with(
1.hour,
:generate_inferred_concepts,
item_type: "topics",
item_ids: [topic.id],
batch_size: 10,
match_only: true,
)
allow(Jobs).to receive(:enqueue_in).with(
1.hour,
:generate_inferred_concepts,
item_type: "posts",
item_ids: [post.id],
batch_size: 10,
match_only: true,
)
subject.execute({})
end
it "does not schedule jobs when no candidates found" do
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).and_return([])
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).and_return([])
allow(Jobs).to receive(:enqueue)
allow(Jobs).to receive(:enqueue_in)
subject.execute({})
end
it "uses site setting values for topic filtering" do
SiteSetting.inferred_concepts_daily_topics_limit = 50
SiteSetting.inferred_concepts_min_posts = 8
SiteSetting.inferred_concepts_min_likes = 15
SiteSetting.inferred_concepts_min_views = 200
SiteSetting.inferred_concepts_lookback_days = 45
freeze_time do
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).with(
limit: 50,
min_posts: 8,
min_likes: 15,
min_views: 200,
created_after: 45.days.ago,
).and_return([])
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).and_return([])
subject.execute({})
end
end
it "uses site setting values for post filtering" do
SiteSetting.inferred_concepts_daily_posts_limit = 40
SiteSetting.inferred_concepts_post_min_likes = 8
SiteSetting.inferred_concepts_lookback_days = 45
freeze_time do
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).and_return([])
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).with(
limit: 40,
min_likes: 8,
exclude_first_posts: true,
created_after: 45.days.ago,
).and_return([])
subject.execute({})
end
end
it "handles nil site setting values gracefully" do
SiteSetting.inferred_concepts_daily_topics_limit = nil
SiteSetting.inferred_concepts_daily_posts_limit = nil
SiteSetting.inferred_concepts_min_posts = nil
SiteSetting.inferred_concepts_min_likes = nil
SiteSetting.inferred_concepts_min_views = nil
SiteSetting.inferred_concepts_post_min_likes = nil
# Keep lookback_days at default so .days.ago doesn't fail
freeze_time do
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).with(
limit: 0, # nil becomes 0
min_posts: 0, # nil becomes 0
min_likes: 0, # nil becomes 0
min_views: 0, # nil becomes 0
created_after: 30.days.ago, # default from before block
).and_return([])
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).with(
limit: 0, # nil becomes 0
min_likes: 0, # nil becomes 0
exclude_first_posts: true,
created_after: 30.days.ago, # default from before block
).and_return([])
subject.execute({})
end
end
it "processes both topics and posts in the same run" do
candidate_topics = [topic]
candidate_posts = [post]
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_topics,
).and_return(candidate_topics)
expect_any_instance_of(DiscourseAi::InferredConcepts::Manager).to receive(
:find_candidate_posts,
).and_return(candidate_posts)
allow(Jobs).to receive(:enqueue).twice
subject.execute({})
end
end
context "when scheduling the job" do
it "is scheduled to run daily" do
expect(described_class.every).to eq(1.day)
end
end
end