# frozen_string_literal: true describe DiscourseAi::TopicSummarization do fab!(:user) { Fabricate(:admin) } fab!(:topic) { Fabricate(:topic, highest_post_number: 2) } fab!(:post_1) { Fabricate(:post, topic: topic, post_number: 1) } fab!(:post_2) { Fabricate(:post, topic: topic, post_number: 2) } before do assign_fake_provider_to(:ai_summarization_model) SiteSetting.ai_summarization_enabled = true end let(:strategy) { DiscourseAi::Summarization.topic_summary(topic) } describe "#summarize" do subject(:summarization) { described_class.new(strategy, user) } def assert_summary_is_cached(topic, summary_response) cached_summary = AiSummary.find_by(target: topic, summary_type: AiSummary.summary_types[:complete]) expect(cached_summary.content_range).to cover(*topic.posts.map(&:post_number)) expect(cached_summary.summarized_text).to eq(summary) expect(cached_summary.original_content_sha).to be_present expect(cached_summary.algorithm).to eq("fake") end context "when the content was summarized in a single chunk" do let(:summary) { "This is the final summary" } it "caches the summary" do DiscourseAi::Completions::Llm.with_prepared_responses([summary]) do section = summarization.summarize expect(section.summarized_text).to eq(summary) assert_summary_is_cached(topic, summary) end end it "returns the cached version in subsequent calls" do summarization.summarize cached_summary_text = "This is a cached summary" AiSummary.find_by(target: topic, summary_type: AiSummary.summary_types[:complete]).update!( summarized_text: cached_summary_text, updated_at: 24.hours.ago, ) summarization = described_class.new(strategy, user) section = summarization.summarize expect(section.summarized_text).to eq(cached_summary_text) end end describe "invalidating cached summaries" do let(:cached_text) { "This is a cached summary" } let(:updated_summary) { "This is the final summary" } def cached_summary AiSummary.find_by(target: topic, summary_type: AiSummary.summary_types[:complete]) end before do # a bit tricky, but fold_content now caches an instance of LLM # once it is cached with_prepared_responses will not work as expected # since it is glued to the old llm instance # so we create the cached summary totally independantly DiscourseAi::Completions::Llm.with_prepared_responses([cached_text]) do strategy = DiscourseAi::Summarization.topic_summary(topic) described_class.new(strategy, user).summarize end cached_summary.update!(summarized_text: cached_text, created_at: 24.hours.ago) end context "when the user can requests new summaries" do context "when there are no new posts" do it "returns the cached summary" do section = summarization.summarize expect(section.summarized_text).to eq(cached_text) end end context "when there are new posts" do before { cached_summary.update!(original_content_sha: "outdated_sha") } it "returns a new summary" do DiscourseAi::Completions::Llm.with_prepared_responses([updated_summary]) do section = summarization.summarize expect(section.summarized_text).to eq(updated_summary) end end context "when the cached summary is less than one hour old" do before { cached_summary.update!(created_at: 30.minutes.ago) } it "returns the cached summary" do cached_summary.update!(created_at: 30.minutes.ago) section = summarization.summarize expect(section.summarized_text).to eq(cached_text) expect(section.outdated).to eq(true) end it "returns a new summary if the skip_age_check flag is passed" do DiscourseAi::Completions::Llm.with_prepared_responses([updated_summary]) do section = summarization.summarize(skip_age_check: true) expect(section.summarized_text).to eq(updated_summary) end end end end end end describe "stream partial updates" do let(:summary) { "This is the final summary" } it "receives a blk that is passed to the underlying strategy and called with partial summaries" do partial_result = +"" DiscourseAi::Completions::Llm.with_prepared_responses([summary]) do summarization.summarize { |partial_summary| partial_result << partial_summary } end expect(partial_result).to eq(summary) end end end end