diff --git a/SKILL.md b/SKILL.md index bad4d0f..fb79cac 100644 --- a/SKILL.md +++ b/SKILL.md @@ -950,7 +950,7 @@ Read the research output carefully. Pay attention to: **ANTI-PATTERN TO AVOID**: If user asks about "clawdbot skills" and research returns ClawdBot content (self-hosted AI agent), do NOT synthesize this as "Claude Code skills" just because both involve "skills". Read what the research actually says. -**FUN CONTENT: If the research output includes a "## Best Takes" section or items tagged with `fun:` scores, weave at least 2-3 of the funniest/cleverest quotes into your synthesis.** Reddit comments and X posts with high fun scores are the voice of the people. A 1,338-upvote comment that says "Where's the limewire link" tells you more about the cultural moment than a news article. Quote the actual text. Don't put fun content in a separate section - mix it into the narrative where it fits naturally. This is what makes the report feel alive rather than like a news summary. +**FUN CONTENT: If the research output includes a "## Best Takes" section at the top of the compact output, quote at least TWO of those items verbatim in your synthesis.** These are pre-ranked comedy and wit lines, pulled from both candidate titles and individual top_comments across every source. A comment entry looks like `"WHAT?! I reached my monthly limit just reading this post" -- r/Anthropic in "Claude Opus 4.7 is a regression" (2,304 upvotes) (fun:88)`. Use the full body, attribute to the container (r/subreddit, @handle on platform), and keep the upvote number so readers can gauge community agreement. Reddit comments and X posts with high fun scores are the voice of the people. Don't put fun content in a separate section - mix it into the narrative where it fits naturally. If no Best Takes section appears, scan items tagged with `fun:` scores in the cluster listings and weave 2-3 of those instead. **ELI5 MODE: If ELI5_MODE is true for this run, apply these writing guidelines to your ENTIRE synthesis. If ELI5_MODE is false, skip this block completely and write normally.** diff --git a/scripts/lib/render.py b/scripts/lib/render.py index ce5618b..0db41e0 100644 --- a/scripts/lib/render.py +++ b/scripts/lib/render.py @@ -18,9 +18,9 @@ SOURCE_LABELS = { _FUN_LEVELS = { - "low": {"threshold": 80.0, "limit": 2}, - "medium": {"threshold": 70.0, "limit": 5}, - "high": {"threshold": 55.0, "limit": 8}, + "low": {"threshold": 75.0, "limit": 2}, + "medium": {"threshold": 55.0, "limit": 5}, + "high": {"threshold": 40.0, "limit": 8}, } _AI_SAFETY_NOTE = ( @@ -60,6 +60,11 @@ def render_compact(report: schema.Report, cluster_limit: int = 8, fun_level: str lines.extend(f"- {warning}" for warning in report.warnings) lines.append("") + fun_params = _FUN_LEVELS.get(fun_level, _FUN_LEVELS["medium"]) + best_takes = _render_best_takes(report.ranked_candidates, limit=fun_params["limit"], threshold=fun_params["threshold"]) + if best_takes: + lines.extend(best_takes + [""]) + lines.append("## Ranked Evidence Clusters") lines.append("") candidate_by_id = {candidate.candidate_id: candidate for candidate in report.ranked_candidates} @@ -80,11 +85,6 @@ def render_compact(report: schema.Report, cluster_limit: int = 8, fun_level: str lines.extend(_render_stats(report)) - fun_params = _FUN_LEVELS.get(fun_level, _FUN_LEVELS["medium"]) - best_takes = _render_best_takes(report.ranked_candidates, limit=fun_params["limit"], threshold=fun_params["threshold"]) - if best_takes: - lines.extend([""] + best_takes) - lines.extend(_render_source_coverage(report)) return "\n".join(lines).strip() + "\n" @@ -654,33 +654,78 @@ def _source_label(source: str) -> str: def _render_best_takes(candidates, limit=5, threshold=70.0): - gems = sorted( - (c for c in candidates if c.fun_score is not None and c.fun_score >= threshold), - key=lambda c: -(c.fun_score or 0), - ) - if len(gems) < 2: - return [] - lines = ["## Best Takes", ""] - for candidate in gems[:limit]: - text = candidate.title.strip() + # Build a unified list of gems: candidate-level entries and comment-level entries, + # each tagged by type so they can share the sort but render with different attribution. + gems: list[tuple[str, float, object]] = [] + for candidate in candidates: + if candidate.fun_score is not None and candidate.fun_score >= threshold: + gems.append(("candidate", candidate.fun_score, candidate)) for item in candidate.source_items: - for comment in item.metadata.get("top_comments", [])[:3]: - body = (comment.get("body") or comment.get("text") or "") if isinstance(comment, dict) else str(comment) - body = body.strip() - if body and len(body) < len(text) and len(body) > 10: - text = body - source_label = _source_label(candidate.source) - author = candidate.source_items[0].author if candidate.source_items else None - attribution = f"@{author} on {source_label}" if author and candidate.source in ("x", "tiktok", "instagram", "threads") else f"{source_label}" - if author and candidate.source == "reddit": - container = candidate.source_items[0].container if candidate.source_items else None - attribution = f"r/{container} comment" if container else "Reddit" - score_tag = f"(fun:{candidate.fun_score:.0f})" - reason = f" -- {candidate.fun_explanation}" if candidate.fun_explanation and candidate.fun_explanation != "heuristic-fallback" else "" - lines.append(f'- "{_truncate(text, 280)}" -- {attribution} {score_tag}{reason}') + for comment in item.metadata.get("top_comments", []) or []: + if not isinstance(comment, dict): + continue + comment_score = comment.get("fun_score") + if comment_score is None or comment_score < threshold: + continue + gems.append(("comment", float(comment_score), (candidate, item, comment))) + if not gems: + return [] + gems.sort(key=lambda g: -g[1]) + lines = ["## Best Takes", ""] + for kind, score, payload in gems[:limit]: + if kind == "candidate": + lines.append(_render_candidate_gem(payload)) + else: + candidate, item, comment = payload + lines.append(_render_comment_gem(candidate, item, comment)) return lines +def _render_candidate_gem(candidate) -> str: + text = candidate.title.strip() + for item in candidate.source_items: + for comment in item.metadata.get("top_comments", [])[:3]: + body = (comment.get("body") or comment.get("excerpt") or comment.get("text") or "") if isinstance(comment, dict) else str(comment) + body = body.strip() + if body and len(body) < len(text) and len(body) > 10: + text = body + source_label = _source_label(candidate.source) + author = candidate.source_items[0].author if candidate.source_items else None + attribution = f"@{author} on {source_label}" if author and candidate.source in ("x", "tiktok", "instagram", "threads") else f"{source_label}" + if author and candidate.source == "reddit": + container = candidate.source_items[0].container if candidate.source_items else None + attribution = f"r/{container} comment" if container else "Reddit" + score_tag = f"(fun:{candidate.fun_score:.0f})" + reason = f" -- {candidate.fun_explanation}" if candidate.fun_explanation and candidate.fun_explanation != "heuristic-fallback" else "" + return f'- "{_truncate(text, 280)}" -- {attribution} {score_tag}{reason}' + + +def _render_comment_gem(candidate, item, comment) -> str: + body = (comment.get("body") or comment.get("excerpt") or comment.get("text") or "").strip() + upvotes = comment.get("score") or comment.get("ups") or comment.get("upvotes") or comment.get("likes") or 0 + source_label = _source_label(candidate.source) + parent_title = (candidate.title or "").strip() + comment_author = comment.get("author") + vote_label = _vote_label_for(candidate.source) + fun_tag = f"(fun:{comment.get('fun_score', 0):.0f})" + + if candidate.source == "reddit": + container = item.container if item else None + source_attribution = f"r/{container}" if container else source_label + elif comment_author: + source_attribution = f"@{comment_author} on {source_label}" + else: + source_attribution = source_label + + vote_suffix = "" + if isinstance(upvotes, int) and upvotes: + vote_suffix = f" ({upvotes:,} {vote_label})" + + in_clause = f' in "{_truncate(parent_title, 100)}"' if parent_title else "" + + return f'- "{_truncate(body, 280)}" -- {source_attribution}{in_clause}{vote_suffix} {fun_tag}' + + def _truncate(text: str, limit: int) -> str: text = text.strip() if len(text) <= limit: diff --git a/scripts/lib/rerank.py b/scripts/lib/rerank.py index 680f4ff..da7fc05 100644 --- a/scripts/lib/rerank.py +++ b/scripts/lib/rerank.py @@ -285,11 +285,13 @@ def _apply_fun_scores(candidates: list[schema.Candidate], payload: dict) -> None c.fun_score, c.fun_explanation = scores[c.candidate_id] else: _apply_single_fun_fallback(c) + _score_comments_per_candidate(c) def _apply_fun_fallback(candidates: list[schema.Candidate]) -> None: for c in candidates: _apply_single_fun_fallback(c) + _score_comments_per_candidate(c) def _apply_single_fun_fallback(candidate: schema.Candidate) -> None: @@ -304,6 +306,72 @@ def _apply_single_fun_fallback(candidate: schema.Candidate) -> None: candidate.fun_explanation = "heuristic-fallback" +_FUN_MARKERS = ("lol", "lmao", "dead", "hilarious", "funny", "bruh", "ratio", + "nah", "bro", "ain't no way", "i'm crying", "rent free") + + +def _comment_body(comment: dict) -> str: + for key in ("body", "excerpt", "text"): + value = comment.get(key) if isinstance(comment, dict) else None + if value: + return str(value).strip() + return "" + + +def _comment_upvotes(comment: dict) -> int: + for key in ("score", "ups", "upvotes", "likes"): + value = comment.get(key) if isinstance(comment, dict) else None + if value is not None: + try: + return int(value) + except (TypeError, ValueError): + continue + return 0 + + +def _parent_raw_upvotes(candidate: schema.Candidate) -> int: + for item in candidate.source_items: + eng = item.engagement + if isinstance(eng, dict): + for key in ("score", "ups", "upvotes", "likes"): + value = eng.get(key) + if value is not None: + try: + return int(value) + except (TypeError, ValueError): + continue + elif isinstance(eng, (int, float)) and eng: + return int(eng) + return 0 + + +def _score_comments_per_candidate(candidate: schema.Candidate) -> None: + """Annotate each of the top 3 comments on this candidate with its own fun_score. + + Scoring: (ratio-to-parent bonus, capped 50) + shortness bonus (0-30) + marker bonus (0-20). + A comment with high upvotes relative to its parent thread dominates an absolute-high + comment on a dominant parent thread, which is the viral-wit signal. + """ + parent_upvotes = _parent_raw_upvotes(candidate) + for item in candidate.source_items: + comments = item.metadata.get("top_comments") or [] + if not isinstance(comments, list): + continue + for comment in comments[:3]: + if not isinstance(comment, dict): + continue + body = _comment_body(comment) + if not body: + continue + upvotes = _comment_upvotes(comment) + ratio = upvotes / max(parent_upvotes, 1) if parent_upvotes else min(upvotes / 100.0, 2.5) + ratio_bonus = min(ratio * 20.0, 50.0) + body_len = len(body) + shortness_bonus = max(0.0, (200 - body_len) / 200.0) * 30.0 + marker_bonus = 20.0 if any(m in body.lower() for m in _FUN_MARKERS) else 0.0 + comment["fun_score"] = max(0.0, min(100.0, ratio_bonus + shortness_bonus + marker_bonus)) + + def _normalized_rrf(rrf_score: float) -> float: # Empirical ceiling for normalized RRF scores at the pool sizes we use. # Max single-stream RRF at rank 1 is 1/(K+1) ~ 0.016; multi-stream diff --git a/tests/test_render_v3.py b/tests/test_render_v3.py index 1c1308b..64b071b 100644 --- a/tests/test_render_v3.py +++ b/tests/test_render_v3.py @@ -387,15 +387,124 @@ class RenderBestTakesCompactTests(unittest.TestCase): text = render.render_compact(report) self.assertNotIn("## Best Takes", text) - def test_no_best_takes_with_1_high_fun_candidate(self): - """No Best Takes section when only 1 candidate above threshold.""" + def test_best_takes_with_1_high_fun_candidate(self): + """Best Takes section appears even with only 1 candidate above threshold. + + The single-gem floor means a single viral quote is enough to show the block, + which is essential for the default medium level on most research topics. + """ candidates = [ self._make_candidate("c1", fun_score=80), - self._make_candidate("c2", fun_score=50), + self._make_candidate("c2", fun_score=40), ] report = self._make_report_with_candidates(candidates) text = render.render_compact(report) - self.assertNotIn("## Best Takes", text) + self.assertIn("## Best Takes", text) + self.assertIn("(fun:80)", text) + + def test_best_takes_renders_above_clusters(self): + """Best Takes section appears before the Ranked Evidence Clusters header.""" + candidates = [ + self._make_candidate("c1", fun_score=85), + self._make_candidate("c2", fun_score=75), + ] + report = self._make_report_with_candidates(candidates) + text = render.render_compact(report) + self.assertLess(text.index("## Best Takes"), text.index("## Ranked Evidence Clusters")) + + def test_comment_level_gem_appears_in_best_takes(self): + """A high-fun top_comment on a low-fun parent candidate still qualifies.""" + item = schema.SourceItem( + item_id="item-c1", + source="reddit", + title="Post c1", + body="Body.", + url="https://reddit.com/r/test/comments/c1/", + container="test", + published_at="2026-03-15", + date_confidence="high", + engagement={"score": 300, "num_comments": 30}, + metadata={ + "top_comments": [{ + "body": "WHAT?! I reached my monthly limit just reading this post", + "excerpt": "WHAT?! I reached my monthly limit just reading this post", + "score": 2304, + "fun_score": 88.0, + }], + }, + ) + candidate = schema.Candidate( + candidate_id="c1", + item_id="item-c1", + source="reddit", + title="Post c1", + url="https://reddit.com/r/test/comments/c1/", + snippet="", + subquery_labels=["primary"], + native_ranks={"primary:reddit": 1}, + local_relevance=0.9, + freshness=90, + engagement=88, + source_quality=1.0, + rrf_score=0.02, + rerank_score=92, + final_score=90, + sources=["reddit"], + source_items=[item], + fun_score=35.0, # parent below threshold + ) + report = self._make_report_with_candidates([candidate]) + text = render.render_compact(report) + self.assertIn("## Best Takes", text) + self.assertIn("WHAT?! I reached my monthly limit", text) + self.assertIn("2,304", text) + self.assertIn("r/test", text) + self.assertIn('in "Post c1"', text) + + def test_comment_and_candidate_gems_sorted_by_fun_score(self): + """Higher fun score comes first, regardless of whether it's a candidate or a comment.""" + item = schema.SourceItem( + item_id="item-c1", + source="reddit", + title="Post c1", + body="Body.", + url="https://reddit.com/r/test/comments/c1/", + container="test", + engagement={"score": 100}, + metadata={ + "top_comments": [{ + "body": "higher-ranked comment", + "score": 800, + "fun_score": 90.0, + }], + }, + ) + comment_candidate = schema.Candidate( + candidate_id="cc", + item_id="item-c1", + source="reddit", + title="Parent thread title", + url="https://reddit.com/r/test/comments/c1/", + snippet="", + subquery_labels=["primary"], + native_ranks={"primary:reddit": 1}, + local_relevance=0.9, + freshness=90, + engagement=10, + source_quality=1.0, + rrf_score=0.02, + sources=["reddit"], + source_items=[item], + fun_score=30.0, + ) + lower_candidate = self._make_candidate("cl", fun_score=70) + report = self._make_report_with_candidates([comment_candidate, lower_candidate]) + text = render.render_compact(report) + comment_pos = text.find("higher-ranked comment") + lower_pos = text.find("(fun:70)") + self.assertNotEqual(comment_pos, -1) + self.assertNotEqual(lower_pos, -1) + self.assertLess(comment_pos, lower_pos) if __name__ == "__main__": diff --git a/tests/test_rerank_fun.py b/tests/test_rerank_fun.py index b43a917..f929607 100644 --- a/tests/test_rerank_fun.py +++ b/tests/test_rerank_fun.py @@ -9,7 +9,12 @@ SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" sys.path.insert(0, str(SCRIPTS_DIR)) from lib import schema -from lib.rerank import _apply_single_fun_fallback, _extract_comment_text +from lib.rerank import ( + _apply_fun_fallback, + _apply_single_fun_fallback, + _extract_comment_text, + _score_comments_per_candidate, +) def _make_candidate( @@ -17,10 +22,12 @@ def _make_candidate( snippet: str = "", engagement: float | None = 0.0, top_comments: list[dict] | None = None, + parent_raw_engagement: int | None = None, ) -> schema.Candidate: """Build a minimal Candidate with optional source_items carrying top_comments.""" source_items = [] if top_comments is not None: + item_engagement = {"score": parent_raw_engagement} if parent_raw_engagement is not None else {} source_items.append( schema.SourceItem( item_id="si-1", @@ -28,6 +35,7 @@ def _make_candidate( title=title, body="", url="https://reddit.com/r/test/1", + engagement=item_engagement, metadata={"top_comments": top_comments}, ) ) @@ -112,6 +120,89 @@ class TestFunFallbackCommentText: assert candidate.fun_explanation == "heuristic-fallback" +class TestScoreCommentsPerCandidate: + """Per-comment fun scoring: high-ratio viral comments outrank absolute-high comments on dominant threads.""" + + def test_high_ratio_comment_outranks_low_ratio(self): + """A 2304-upvote comment on a 300-upvote parent should score higher than + a 400-upvote comment on a 3400-upvote parent (high ratio = viral wit).""" + viral = _make_candidate( + parent_raw_engagement=300, + top_comments=[{"body": "WHAT?! I reached my monthly limit just reading this post", "score": 2304}], + ) + average = _make_candidate( + parent_raw_engagement=3400, + top_comments=[{"body": "we can't trust benchmarks anymore and need to re-run them", "score": 400}], + ) + _score_comments_per_candidate(viral) + _score_comments_per_candidate(average) + viral_score = viral.source_items[0].metadata["top_comments"][0]["fun_score"] + average_score = average.source_items[0].metadata["top_comments"][0]["fun_score"] + assert viral_score > average_score + + def test_2304_upvote_comment_on_small_parent_crosses_medium_threshold(self): + """The exact Opus 4.7 case: the comment should score >= 55 (medium threshold).""" + candidate = _make_candidate( + parent_raw_engagement=300, + top_comments=[{ + "body": "WHAT?! I reached my monthly limit just reading this post", + "score": 2304, + }], + ) + _score_comments_per_candidate(candidate) + comment = candidate.source_items[0].metadata["top_comments"][0] + assert comment["fun_score"] >= 55.0 + + def test_reddit_excerpt_field_also_works(self): + """Reddit uses 'excerpt' not 'body'. The scorer must handle that.""" + candidate = _make_candidate( + parent_raw_engagement=100, + top_comments=[{"excerpt": "bruh this is gold", "score": 500}], + ) + _score_comments_per_candidate(candidate) + comment = candidate.source_items[0].metadata["top_comments"][0] + assert "fun_score" in comment + assert comment["fun_score"] > 0 + + def test_no_parent_upvotes_does_not_crash(self): + """A candidate without parent engagement still scores comments via the absolute-upvote fallback.""" + candidate = _make_candidate( + parent_raw_engagement=None, + top_comments=[{"body": "lmao", "score": 200}], + ) + _score_comments_per_candidate(candidate) + comment = candidate.source_items[0].metadata["top_comments"][0] + assert "fun_score" in comment + + def test_malformed_comments_skipped(self): + """Non-dict entries and missing-body entries are skipped without raising.""" + candidate = _make_candidate( + parent_raw_engagement=500, + top_comments=[ + "not a dict", # malformed, still counts toward the top-3 window + {"body": "", "score": 10}, # empty body within window + {"body": "valid", "score": 50}, # valid within window + ], + ) + _score_comments_per_candidate(candidate) + comments = candidate.source_items[0].metadata["top_comments"] + # Only the valid one gets a fun_score + valid = [c for c in comments if isinstance(c, dict) and c.get("fun_score") is not None] + assert len(valid) == 1 + assert valid[0]["body"] == "valid" + + def test_score_comments_runs_after_fallback(self): + """_apply_fun_fallback wires in comment scoring automatically.""" + candidate = _make_candidate( + parent_raw_engagement=300, + top_comments=[{"body": "bro what 😭", "score": 1500}], + ) + _apply_fun_fallback([candidate]) + comment = candidate.source_items[0].metadata["top_comments"][0] + assert "fun_score" in comment + assert candidate.fun_score is not None + + class TestExtractCommentText: """Verify _extract_comment_text handles edge cases."""