From d56b57ee40ed59b54beb32eaa61145c53a696596 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 8 Apr 2026 22:51:13 -0400 Subject: [PATCH] =?UTF-8?q?feat(briefing):=20cluster-level=20preference=20?= =?UTF-8?q?filtering=20=E2=80=94=20rank=20themes=20by=20user=20interest,?= =?UTF-8?q?=20suppress=20disliked=20topics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/briefing_pipeline.py | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 02e05f4..6bf64b6 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -652,8 +652,9 @@ def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, te lines.extend(f" - {p}" for p in internal_data["stale_projects"]) lines.append("") - # News — clustered by topic for thematic synthesis + # News — clustered by topic, ranked by preference score rss = external_data.get("rss_items") or [] + topic_scores = external_data.get("topic_scores") or {} if rss: # Group articles by their primary topic clusters: dict[str, list[dict]] = {} @@ -666,19 +667,34 @@ def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, te else: uncategorized.append(item) - # Sort clusters by size (most articles = most active theme) - sorted_clusters = sorted(clusters.items(), key=lambda x: len(x[1]), reverse=True) + # Score each cluster by aggregate preference (positive = user likes this topic) + def cluster_score(topic: str, items: list[dict]) -> float: + pref = topic_scores.get(topic, 0.0) + return pref * 2.0 + len(items) # preference-weighted, size as tiebreak + + # Filter out clusters where the user has a strongly negative preference + filtered_clusters = [ + (topic, items) for topic, items in clusters.items() + if topic_scores.get(topic, 0.0) >= -2.0 + ] + + # Sort by preference-weighted score + sorted_clusters = sorted(filtered_clusters, key=lambda x: cluster_score(x[0], x[1]), reverse=True) lines.append("NEWS THEMES (synthesize 1-2 themes into your briefing; mention article count per theme):") - for topic, items in sorted_clusters[:4]: + for i, (topic, items) in enumerate(sorted_clusters[:4]): count = len(items) - titles = [i.get("title", "") for i in items[:3]] - sources = list({i.get("feed_title") or i.get("source") or "News" for i in items}) - lines.append(f" [{topic.title()}] ({count} article{'s' if count != 1 else ''} from {', '.join(sources[:2])})") + titles = [it.get("title", "") for it in items[:3]] + sources = list({it.get("feed_title") or it.get("source") or "News" for it in items}) + pref_indicator = "" + pref = topic_scores.get(topic, 0.0) + if pref >= 2.0: + pref_indicator = " ★" # user's preferred topic + lines.append(f" [{topic.title()}]{pref_indicator} ({count} article{'s' if count != 1 else ''} from {', '.join(sources[:2])})") for t in titles: lines.append(f" • {t}") - # Include one excerpt for the top cluster - if items == sorted_clusters[0][1]: + # Include excerpt for preferred clusters or the top cluster + if pref >= 1.0 or i == 0: excerpt = (items[0].get("content") or items[0].get("snippet") or "")[:400].strip() if excerpt: lines.append(f" > {excerpt}") @@ -793,6 +809,7 @@ async def run_compilation( external_data_filtered = { "rss_items": filtered_rss, "weather": [], + "topic_scores": topic_scores, } briefing_text = await _llm_synthesise(