Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions app/services/ai/email_digest_summary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,42 @@ def cache_key
def prompt
base_url = "https://#{Settings::General.app_domain}"
article_list = @articles.map do |a|
"- Title: #{a.title}\n URL: #{base_url}#{a.path}\n Description: #{a.description}\n Tags: #{a.cached_tag_list}"
content = "- Title: #{a.title}\n URL: #{base_url}#{a.path}\n Description: #{a.description}\n Tags: #{a.cached_tag_list}"

if a.respond_to?(:comments_count) && a.comments_count > 15
top_comments = Comment.where(commentable_id: a.id, commentable_type: "Article")
.where(hidden_by_commentable_user: false, deleted: false)
.order(score: :desc)
.limit(5)
.pluck(:body_markdown)
if top_comments.any?
content += "\n Top Comments (use for extra context if relevant):\n - " + top_comments.map do |c|
c.tr("\n", " ").truncate(150)
end.join("\n - ")
end
end
content
end.join("\n\n")

<<~PROMPT
You are an insightful technical curator for the DEV community.
I will provide you with a list of articles from a developer community digest.
I will provide you with a list of articles from a developer community digest, sometimes including top comments for articles with high engagement.

Your task is to write a brief, engaging "Digest Overview" (about 2 short paragraphs) that:
1. Provides an overview of the key themes covered in these articles.
2. Explains how these themes connect or run together.
3. Offers a brief perspective on why this specific collection of articles is interesting.
1. Identifies and describes the most interesting thematic threads in this collection.
2. Synthesizes how these articles connect or relate to broader trends.

Guidelines for Selection & Synthesis:
- You do NOT need to mention every article. Focus on the ones that make the most sense together to draw out compelling themes.
- Use the provided "Top Comments" as additional context if they are contextually relevant to the theme or provide interesting community perspective alongside the article content.

Guidelines:
Guidelines for Formatting:
- Keep it very concise and not overly wordy.
- Use markdown **bold links** like **[Article Title](URL)** when mentioning the key themes and articles.
- Use markdown **bold links** like **[Keyword/Phrase](URL)** when mentioning themes and articles.
- Prefer using keywords or phrases that help communicate the themes as the anchor text, rather than just the article titles (unless the title is particularly descriptive or useful for the theme).
- Output should be markup with these links embedded naturally in the flow.
- Do not use any headers (like # or ##).
- Do not list each article one by one; focus on the synthesis of the themes.
- Do not list each article one by one; focus on the synthesis.

Articles:
#{article_list}
Expand Down
12 changes: 6 additions & 6 deletions app/services/email_digest_article_collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def initialize(user, force_send: false)

def articles_to_send
# rubocop:disable Metrics/BlockLength
order = Arel.sql("((score * ((feed_success_score * 12) + 0.1)) - (clickbait_score * 2)) DESC")
order = Arel.sql("(((score + comment_score) * ((feed_success_score * 12) + 0.1)) - (clickbait_score * 2)) DESC")
instrument ARTICLES_TO_SEND, tags: { user_id: @user.id } do
return [] unless @force_send || should_receive_email?

Expand All @@ -22,7 +22,7 @@ def articles_to_send
set_subforem_context

articles_query = @user.followed_articles
.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id)
.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id, :comment_score, :comments_count)
.published
.full_posts
.where("published_at > ?", cutoff_date)
Expand All @@ -42,7 +42,7 @@ def articles_to_send
articles_query = if @skip_subforem_filtering
# If skipping subforem filtering, get articles from anywhere
Article.select(
:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id
:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id, :comment_score, :comments_count
)
.published
.full_posts
Expand All @@ -56,7 +56,7 @@ def articles_to_send
else
# Normal logic with subforem filtering and tags
Article.select(
:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id
:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id, :comment_score, :comments_count
)
.published
.full_posts
Expand All @@ -75,7 +75,7 @@ def articles_to_send
if articles.length < 3
if @skip_subforem_filtering
# If we're skipping subforem filtering, get articles from anywhere
articles_query = Article.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id)
articles_query = Article.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id, :comment_score, :comments_count)
.published
.full_posts
.where("published_at > ?", cutoff_date)
Expand All @@ -89,7 +89,7 @@ def articles_to_send
fallback_subforem_ids << default_subforem_id
end

articles_query = Article.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id)
articles_query = Article.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id, :comment_score, :comments_count)
.published
.full_posts
.where("published_at > ?", cutoff_date)
Expand Down
28 changes: 24 additions & 4 deletions spec/services/ai/email_digest_summary_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
RSpec.describe Ai::EmailDigestSummary, type: :service do
let(:articles) do
[
instance_double(Article, id: 1, title: "Title 1", path: "/path1", description: "Desc 1", cached_tag_list: "ruby"),
instance_double(Article, id: 1, title: "Title 1", path: "/path1", description: "Desc 1", cached_tag_list: "ruby",
comments_count: 0),
instance_double(Article, id: 2, title: "Title 2", path: "/path2", description: "Desc 2",
cached_tag_list: "rails"),
cached_tag_list: "rails", comments_count: 0),
]
end
let(:ai_client) { instance_double(Ai::Base) }
Expand Down Expand Up @@ -48,9 +49,9 @@
# Same ID, different path
modified_articles = [
instance_double(Article, id: 1, path: "/different_path", title: "Title 1", description: "Desc 1",
cached_tag_list: "ruby"),
cached_tag_list: "ruby", comments_count: 0),
instance_double(Article, id: 2, path: "/path2", title: "Title 2", description: "Desc 2",
cached_tag_list: "rails"),
cached_tag_list: "rails", comments_count: 0),
]
new_service = described_class.new(modified_articles, ai_client: ai_client)
new_service.generate
Expand All @@ -69,5 +70,24 @@

expect(service.generate).to be_nil
end

context "when an article has many comments" do
let(:article_with_comments) do
instance_double(Article, id: 3, title: "Highly Discussed", path: "/path3", description: "Desc 3",
cached_tag_list: "discuss", comments_count: 20)
end
let(:articles) { [article_with_comments] }

it "includes top comments in the prompt" do
allow(Comment).to receive_message_chain(:where, :where, :order, :limit, :pluck)
.and_return(["Comment 1", "Comment 2"])

service.generate

expect(ai_client).to have_received(:call).with(/Top Comments \(use for extra context if relevant\):/)
expect(ai_client).to have_received(:call).with(/Comment 1/)
expect(ai_client).to have_received(:call).with(/Comment 2/)
end
end
end
end
18 changes: 18 additions & 0 deletions spec/services/email_digest_article_collector_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,24 @@
end
end

context "when articles have comment scores" do
it "factors in comment_score to the ordering" do
# Article 1: score 20, comment_score 0 -> total 20
# Article 2: score 15, comment_score 10 -> total 25
# Article 2 should come first despite lower base score
create(:article, public_reactions_count: 20, score: 20, comment_score: 0, featured: true,
subforem: default_subforem, title: "A1")
create(:article, public_reactions_count: 15, score: 15, comment_score: 10, featured: true,
subforem: default_subforem, title: "A2")
create(:article, public_reactions_count: 10, score: 15, comment_score: 0, featured: true,
subforem: default_subforem, title: "A3")

result = described_class.new(user).articles_to_send
expect(result.first.title).to eq("A2")
expect(result.second.title).to eq("A1")
end
end

context "when the last email included the title of the first article" do
it "bumps the second article to the front" do
articles = create_list(:article, 5, public_reactions_count: 40, featured: true, score: 40,
Expand Down
Loading