Skip to content

Commit 86266a4

Browse files
committed
Expand multi-signal coverage to RSS and Reddit plugins
1 parent 06bcaf8 commit 86266a4

12 files changed

Lines changed: 215 additions & 1 deletion

File tree

‎core/tasks.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
__all__ = [
4242
"recompute_source_quality",
4343
"recompute_authority_scores",
44+
"run_all_source_quality_recomputations",
4445
"run_all_authority_recomputations",
4546
"run_relevance_scoring_skill",
4647
"run_summarization_skill",
@@ -94,6 +95,19 @@ def run_all_authority_recomputations():
9495
return len(project_ids)
9596

9697

98+
@shared_task(name="core.tasks.run_all_source_quality_recomputations")
99+
def run_all_source_quality_recomputations() -> int:
100+
"""Queue source-quality recomputation for every project."""
101+
102+
project_ids = list(Project.objects.values_list("id", flat=True))
103+
for project_id in project_ids:
104+
if settings.CELERY_TASK_ALWAYS_EAGER:
105+
recompute_source_quality(project_id)
106+
else:
107+
_enqueue_task(recompute_source_quality, project_id)
108+
return len(project_ids)
109+
110+
97111
@shared_task(name="core.tasks.process_content")
98112
def process_content(content_id: int):
99113
"""Run the main AI pipeline for a stored content item."""
@@ -616,6 +630,8 @@ def _engagement_total_for_content(
616630
+ _metadata_number(metadata.get("reply_count"))
617631
+ _metadata_number(metadata.get("repost_count"))
618632
)
633+
if content.source_plugin == "reddit":
634+
return _metadata_number(metadata.get("score"))
619635
if content.source_plugin == "mastodon":
620636
return (
621637
_metadata_number(metadata.get("favorite_count"))

‎core/tests/test_entrypoints.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,16 @@ def test_wsgi_module_sets_default_settings_and_builds_application(mocker):
4242

4343
def test_celery_app_redirects_worker_stdout_at_info_level():
4444
assert app.conf.worker_redirect_stdouts_level == "INFO"
45+
46+
47+
def test_celery_app_schedules_source_quality_before_authority_recompute():
48+
beat_schedule = app.conf.beat_schedule
49+
50+
assert (
51+
beat_schedule["run-all-source-quality-recomputations-nightly"]["task"]
52+
== "core.tasks.run_all_source_quality_recomputations"
53+
)
54+
assert (
55+
beat_schedule["run-all-authority-recomputations-nightly"]["task"]
56+
== "core.tasks.run_all_authority_recomputations"
57+
)

‎core/tests/test_tasks.py‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
queue_content_skill,
1010
recompute_authority_scores,
1111
run_all_authority_recomputations,
12+
run_all_source_quality_recomputations,
1213
run_relevance_scoring_skill,
1314
run_summarization_skill,
1415
)
@@ -86,6 +87,43 @@ def test_run_all_authority_recomputations_executes_inline_when_eager(
8687
delay_mock.assert_not_called()
8788

8889

90+
def test_run_all_source_quality_recomputations_enqueues_all_projects(
91+
source_plugin_context, mocker
92+
):
93+
delay_mock = mocker.patch("core.tasks.recompute_source_quality.delay")
94+
other_project = Project.objects.create(
95+
name="Source Quality Project",
96+
topic_description="Signals",
97+
)
98+
99+
enqueued_count = run_all_source_quality_recomputations()
100+
101+
assert enqueued_count == 2
102+
delay_mock.assert_any_call(source_plugin_context.project.id)
103+
delay_mock.assert_any_call(_require_pk(other_project))
104+
assert delay_mock.call_count == 2
105+
106+
107+
def test_run_all_source_quality_recomputations_executes_inline_when_eager(
108+
source_plugin_context, settings, mocker
109+
):
110+
settings.CELERY_TASK_ALWAYS_EAGER = True
111+
recompute_mock = mocker.patch("core.tasks.recompute_source_quality")
112+
delay_mock = mocker.patch("core.tasks.recompute_source_quality.delay")
113+
other_project = Project.objects.create(
114+
name="Inline Source Quality Project",
115+
topic_description="Signals",
116+
)
117+
118+
enqueued_count = run_all_source_quality_recomputations()
119+
120+
assert enqueued_count == 2
121+
recompute_mock.assert_any_call(source_plugin_context.project.id)
122+
recompute_mock.assert_any_call(_require_pk(other_project))
123+
assert recompute_mock.call_count == 2
124+
delay_mock.assert_not_called()
125+
126+
89127
def test_recompute_authority_scores_updates_entities_and_creates_snapshots(
90128
source_plugin_context, mocker
91129
):
@@ -198,6 +236,62 @@ def test_recompute_authority_scores_updates_entities_and_creates_snapshots(
198236
assert primary_snapshot.weights_at_compute["engagement"] == pytest.approx(0.15)
199237

200238

239+
def test_recompute_authority_scores_uses_bluesky_and_reddit_engagement_metadata(
240+
source_plugin_context, mocker
241+
):
242+
mocker.patch("content.signals.queue_topic_centroid_recompute")
243+
project = source_plugin_context.project
244+
ProjectConfig.objects.create(project=project)
245+
entity = source_plugin_context.entity
246+
bluesky_content = Content.objects.create(
247+
project=project,
248+
entity=entity,
249+
url="https://bsky.app/profile/example/post/abc",
250+
canonical_url="https://bsky.app/profile/example/post/abc",
251+
title="Bluesky authority content",
252+
author="example.bsky.social",
253+
source_plugin=SourcePluginName.BLUESKY,
254+
published_date="2026-04-28T12:00:00Z",
255+
content_text="Bluesky authority content.",
256+
source_metadata={"like_count": 4, "reply_count": 1, "repost_count": 2},
257+
)
258+
reddit_content = Content.objects.create(
259+
project=project,
260+
entity=entity,
261+
url="https://www.reddit.com/r/python/comments/abc123/test",
262+
canonical_url="https://www.reddit.com/r/python/comments/abc123/test",
263+
title="Reddit authority content",
264+
author="redditor",
265+
source_plugin=SourcePluginName.REDDIT,
266+
published_date="2026-04-28T13:00:00Z",
267+
content_text="Reddit authority content.",
268+
source_metadata={"score": 10, "comment_count": 3},
269+
)
270+
EntityMention.objects.create(
271+
project=project,
272+
content=bluesky_content,
273+
entity=entity,
274+
role=EntityMentionRole.AUTHOR,
275+
sentiment="positive",
276+
span="Example",
277+
confidence=0.9,
278+
)
279+
EntityMention.objects.create(
280+
project=project,
281+
content=reddit_content,
282+
entity=entity,
283+
role=EntityMentionRole.AUTHOR,
284+
sentiment="neutral",
285+
span="Example",
286+
confidence=0.9,
287+
)
288+
289+
recompute_authority_scores(project.id)
290+
291+
snapshot = EntityAuthoritySnapshot.objects.get(entity=entity)
292+
assert snapshot.engagement_component == pytest.approx(1.0)
293+
294+
201295
def test_queue_content_skill_enqueues_relevance_task(source_plugin_context, mocker):
202296
content = Content.objects.create(
203297
project=source_plugin_context.project,

‎frontend/tsconfig.tsbuildinfo‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎ingestion/plugins/bluesky.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def _build_content_item(self, post, published_date: datetime) -> ContentItem:
152152
"author_did": self._nested_value(post, "author", "did") or "",
153153
"author_handle": author_handle,
154154
"embedded_url": external_url or "",
155+
"like_count": getattr(post, "like_count", 0) or 0,
155156
"post_uri": getattr(post, "uri", ""),
156157
"reply_count": getattr(post, "reply_count", 0) or 0,
157158
"repost_count": getattr(post, "repost_count", 0) or 0,

‎ingestion/plugins/reddit.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ def fetch_new_content(self, since: datetime | None) -> list[ContentItem]:
5252
published_date=published_date,
5353
content_text=(submission.selftext or submission.title).strip(),
5454
source_plugin=SourcePluginName.REDDIT,
55+
source_metadata={
56+
"comment_count": getattr(submission, "num_comments", 0) or 0,
57+
"permalink": getattr(submission, "permalink", "") or "",
58+
"score": getattr(submission, "score", 0) or 0,
59+
"submission_id": submission.id,
60+
"subreddit": self.source_config.config["subreddit"],
61+
"upvote_ratio": getattr(submission, "upvote_ratio", 0.0) or 0.0,
62+
},
5563
)
5664
)
5765
return items

‎ingestion/tests/test_bluesky.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def test_bluesky_fetch_new_content_prefers_embedded_links_and_filters_replies(
9090
indexed_at=old_iso,
9191
author=SimpleNamespace(did="did:plc:alice", handle="Alice.BSky.social"),
9292
record=SimpleNamespace(text="Old post", created_at=old_iso),
93+
like_count=0,
9394
reply_count=0,
9495
repost_count=0,
9596
embed=None,
@@ -99,6 +100,7 @@ def test_bluesky_fetch_new_content_prefers_embedded_links_and_filters_replies(
99100
indexed_at=iso_now,
100101
author=SimpleNamespace(did="did:plc:alice", handle="Alice.BSky.social"),
101102
record=SimpleNamespace(text="A reply", created_at=iso_now),
103+
like_count=1,
102104
reply_count=1,
103105
repost_count=0,
104106
embed=None,
@@ -114,6 +116,7 @@ def test_bluesky_fetch_new_content_prefers_embedded_links_and_filters_replies(
114116
title=" Linked article ",
115117
)
116118
),
119+
like_count=5,
117120
reply_count=2,
118121
repost_count=3,
119122
)
@@ -122,6 +125,7 @@ def test_bluesky_fetch_new_content_prefers_embedded_links_and_filters_replies(
122125
indexed_at=iso_now,
123126
author=SimpleNamespace(did="did:plc:alice", handle="Alice.BSky.social"),
124127
record=SimpleNamespace(text="Duplicate", created_at=iso_now),
128+
like_count=0,
125129
reply_count=0,
126130
repost_count=0,
127131
embed=None,
@@ -151,6 +155,7 @@ def test_bluesky_fetch_new_content_prefers_embedded_links_and_filters_replies(
151155
"author_did": "did:plc:alice",
152156
"author_handle": "alice.bsky.social",
153157
"embedded_url": "https://example.com/article",
158+
"like_count": 5,
154159
"post_uri": "at://did:plc:alice/app.bsky.feed.post/fresh",
155160
"reply_count": 2,
156161
"repost_count": 3,

‎ingestion/tests/test_reddit.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def test_reddit_fetch_new_content_deduplicates_and_filters_by_since(
6363
title=" Duplicate title ",
6464
selftext="",
6565
author="redditor",
66+
num_comments=7,
67+
score=12,
68+
upvote_ratio=0.91,
6669
created_utc=now.timestamp(),
6770
)
6871
duplicate_submission = SimpleNamespace(
@@ -72,6 +75,9 @@ def test_reddit_fetch_new_content_deduplicates_and_filters_by_since(
7275
title="Duplicate title",
7376
selftext="Ignored duplicate",
7477
author="redditor",
78+
num_comments=7,
79+
score=12,
80+
upvote_ratio=0.91,
7581
created_utc=now.timestamp(),
7682
)
7783
subreddit = SimpleNamespace(
@@ -89,6 +95,14 @@ def test_reddit_fetch_new_content_deduplicates_and_filters_by_since(
8995
assert items[0].author == "redditor"
9096
assert items[0].content_text == "Duplicate title"
9197
assert items[0].source_plugin == SourcePluginName.REDDIT
98+
assert items[0].source_metadata == {
99+
"comment_count": 7,
100+
"permalink": "/r/python/comments/dup-1/test",
101+
"score": 12,
102+
"submission_id": duplicate_id,
103+
"subreddit": "python",
104+
"upvote_ratio": 0.91,
105+
}
92106

93107

94108
def test_reddit_health_check_returns_true(reddit_context, mocker):

‎ingestion/tests/test_tasks.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ def test_run_ingestion_creates_content_from_reddit_posts(source_plugin_context,
189189
title="Reddit Post",
190190
selftext="Post body",
191191
author="redditor",
192+
num_comments=6,
193+
score=14,
194+
upvote_ratio=0.88,
192195
created_utc=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc).timestamp(),
193196
)
194197
subreddit = SimpleNamespace(
@@ -205,6 +208,8 @@ def test_run_ingestion_creates_content_from_reddit_posts(source_plugin_context,
205208
process_content_delay_mock.assert_called_once_with(_require_pk(content))
206209
assert content.source_plugin == SourcePluginName.REDDIT
207210
assert content.entity is None
211+
assert content.source_metadata["score"] == 14
212+
assert content.source_metadata["comment_count"] == 6
208213

209214

210215
def test_ingest_source_config_deduplicates_bluesky_posts_by_post_uri(

‎newsletter_maker/settings/celery.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"task": "core.tasks.refresh_linkedin_tokens",
2424
"schedule": 60 * 60,
2525
},
26+
"run-all-source-quality-recomputations-nightly": {
27+
"task": "core.tasks.run_all_source_quality_recomputations",
28+
"schedule": crontab(hour=1, minute=45),
29+
},
2630
"run-all-authority-recomputations-nightly": {
2731
"task": "core.tasks.run_all_authority_recomputations",
2832
"schedule": crontab(hour=2, minute=0),

0 commit comments

Comments
 (0)