Coverage for src / local_deep_research / news / core / relevance_service.py: 100%
52 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:55 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:55 +0000
1"""
2Relevance and trending calculation service.
3Handles business logic for calculating relevance scores and trending metrics.
4"""
6from typing import Optional, Dict, List
7from .base_card import BaseCard
10class RelevanceService:
11 """Service for calculating relevance and trending scores."""
13 def calculate_relevance(
14 self, card: BaseCard, user_prefs: Optional[Dict]
15 ) -> float:
16 """
17 Calculate relevance score for a card based on user preferences.
19 Args:
20 card: The card to score
21 user_prefs: User preferences dictionary
23 Returns:
24 Relevance score between 0 and 1
25 """
26 if not user_prefs:
27 # Default relevance based on impact
28 return float(getattr(card, "impact_score", 5)) / 10.0
30 score = 0.5 # Base score
32 # Category preference
33 if hasattr(card, "category"):
34 if card.category in user_prefs.get("liked_categories", []):
35 score += 0.2
36 elif card.category in user_prefs.get("disliked_categories", []):
37 score -= 0.2
39 # Impact threshold
40 if hasattr(card, "impact_score"):
41 threshold = user_prefs.get("impact_threshold", 5)
42 if card.impact_score >= threshold:
43 score += 0.1
44 else:
45 score -= 0.1
47 # Topic matching (simplified without embeddings)
48 if hasattr(card, "topics"):
49 liked_topics = user_prefs.get("liked_topics", [])
50 for topic in card.topics:
51 if any(liked in topic.lower() for liked in liked_topics):
52 score += 0.1
53 break
55 # Ensure score is in valid range
56 return max(0.0, min(1.0, score))
58 def calculate_trending_score(self, card: BaseCard) -> float:
59 """
60 Calculate trending score based on impact and engagement.
62 Args:
63 card: The card to score
65 Returns:
66 Trending score
67 """
68 if not hasattr(card, "impact_score"):
69 return 0.0
71 # Calculate engagement
72 engagement = (
73 card.interaction.get("views", 0)
74 + card.interaction.get("votes_up", 0) * 2
75 - card.interaction.get("votes_down", 0)
76 )
78 # Combine impact and engagement
79 trending_score: float = float(getattr(card, "impact_score", 0)) + (
80 engagement / 10
81 )
83 return trending_score
85 def filter_trending(
86 self, cards: List[BaseCard], min_impact: int = 7, limit: int = 10
87 ) -> List[BaseCard]:
88 """
89 Filter and sort cards by trending score.
91 Args:
92 cards: List of cards to filter
93 min_impact: Minimum impact score required
94 limit: Maximum number of cards to return
96 Returns:
97 List of trending cards sorted by score
98 """
99 trending = []
101 for card in cards:
102 if (
103 hasattr(card, "impact_score")
104 and card.impact_score >= min_impact
105 ):
106 setattr(
107 card, "trending_score", self.calculate_trending_score(card)
108 )
109 trending.append(card)
111 # Sort by trending score
112 trending.sort(
113 key=lambda c: getattr(c, "trending_score", 0.0), reverse=True
114 )
116 return trending[:limit]
118 def personalize_feed(
119 self,
120 cards: List[BaseCard],
121 user_prefs: Optional[Dict],
122 include_seen: bool = True,
123 ) -> List[BaseCard]:
124 """
125 Personalize a feed of cards based on user preferences.
127 Args:
128 cards: List of cards to personalize
129 user_prefs: User preferences
130 include_seen: Whether to include already viewed cards
132 Returns:
133 Personalized list of cards
134 """
135 personalized = []
137 for card in cards:
138 # Calculate relevance
139 setattr(
140 card,
141 "relevance_score",
142 self.calculate_relevance(card, user_prefs),
143 )
145 # Filter seen if requested
146 if not include_seen and card.interaction.get("viewed"):
147 continue
149 personalized.append(card)
151 # Sort by relevance
152 personalized.sort(
153 key=lambda c: getattr(c, "relevance_score", 0.0), reverse=True
154 )
156 return personalized
159# Singleton instance
160_relevance_service = None
163def get_relevance_service() -> RelevanceService:
164 """Get or create the global RelevanceService instance."""
165 global _relevance_service
166 if _relevance_service is None:
167 _relevance_service = RelevanceService()
168 return _relevance_service