Coverage for src / local_deep_research / news / recommender / base_recommender.py: 99%
63 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"""
2Base class for all news recommendation strategies.
3Following LDR's pattern from BaseSearchStrategy.
4"""
6from abc import ABC, abstractmethod
7from typing import List, Dict, Any, Optional, Callable
8from loguru import logger
10from ..core.base_card import NewsCard
11from ..preference_manager.base_preference import BasePreferenceManager
12from ..rating_system.base_rater import BaseRatingSystem
15class BaseRecommender(ABC):
16 """Abstract base class for all recommendation strategies."""
18 def __init__(
19 self,
20 preference_manager: Optional[BasePreferenceManager] = None,
21 rating_system: Optional[BaseRatingSystem] = None,
22 topic_registry: Optional[Any] = None,
23 search_system: Optional[Any] = None,
24 ):
25 """
26 Initialize the base recommender with common dependencies.
28 Args:
29 preference_manager: Manager for user preferences
30 rating_system: System for tracking ratings
31 topic_registry: Registry of discovered topics
32 search_system: LDR search system for executing queries
33 """
34 self.preference_manager = preference_manager
35 self.rating_system = rating_system
36 self.topic_registry = topic_registry
37 self.search_system = search_system
39 # Progress tracking (following LDR pattern)
40 self.progress_callback: Optional[
41 Callable[[str, Optional[int], dict], None]
42 ] = None
44 # Strategy name for identification
45 self.strategy_name = self.__class__.__name__
47 def set_progress_callback(
48 self, callback: Callable[[str, Optional[int], dict], None]
49 ) -> None:
50 """Set a callback function to receive progress updates."""
51 self.progress_callback = callback
53 def _update_progress(
54 self,
55 message: str,
56 progress_percent: Optional[int] = None,
57 metadata: Optional[dict] = None,
58 ) -> None:
59 """Send a progress update via the callback if available."""
60 if self.progress_callback:
61 self.progress_callback(message, progress_percent, metadata or {})
63 @abstractmethod
64 def generate_recommendations(
65 self, user_id: str, context: Optional[Dict[str, Any]] = None
66 ) -> List[NewsCard]:
67 """
68 Generate news recommendations for a user.
70 Args:
71 user_id: The user to generate recommendations for
72 context: Optional context (e.g., current page, recent activity)
74 Returns:
75 List of NewsCard objects representing recommendations
76 """
77 pass
79 def _get_user_preferences(self, user_id: str) -> Dict[str, Any]:
80 """Get user preferences if preference manager is available."""
81 if self.preference_manager:
82 return self.preference_manager.get_preferences(user_id)
83 return {}
85 def _get_user_ratings(
86 self, user_id: str, limit: int = 50
87 ) -> List[Dict[str, Any]]:
88 """Get recent user ratings if rating system is available."""
89 if self.rating_system:
90 return self.rating_system.get_recent_ratings(user_id, limit)
91 return []
93 def _execute_search(
94 self, query: str, strategy: Optional[str] = None
95 ) -> Dict[str, Any]:
96 """
97 Execute a search using the LDR search system.
99 Args:
100 query: The search query
101 strategy: Optional search strategy to use
103 Returns:
104 Search results dictionary
105 """
106 if not self.search_system:
107 logger.warning("No search system available for recommendations")
108 return {"error": "Search system not configured"}
110 try:
111 # Use news strategy by default if available
112 if strategy is None:
113 strategy = "news_aggregation"
115 # Execute search
116 results: Dict[str, Any] = self.search_system.analyze_topic(query)
117 return results
119 except Exception:
120 logger.exception("Error executing search for recommendations")
121 return {"error": "Recommendation search failed"}
123 def _filter_by_preferences(
124 self, cards: List[NewsCard], preferences: Dict[str, Any]
125 ) -> List[NewsCard]:
126 """
127 Filter cards based on user preferences.
129 Args:
130 cards: List of news cards to filter
131 preferences: User preferences dictionary
133 Returns:
134 Filtered list of cards
135 """
136 filtered = cards
138 # Filter by categories if specified
139 if (
140 "liked_categories" in preferences
141 and preferences["liked_categories"]
142 ):
143 # Boost liked categories rather than filtering out others
144 for card in filtered:
145 if card.category in preferences["liked_categories"]:
146 card.metadata["preference_boost"] = 1.2
148 # Filter by impact threshold
149 if "impact_threshold" in preferences:
150 threshold = preferences["impact_threshold"]
151 filtered = [
152 card for card in filtered if card.impact_score >= threshold
153 ]
155 # Apply disliked topics
156 if "disliked_topics" in preferences and preferences["disliked_topics"]:
157 filtered = [
158 card
159 for card in filtered
160 if not any(
161 topic in card.topic.lower()
162 for topic in preferences["disliked_topics"]
163 )
164 ]
166 return filtered
168 def _sort_by_relevance(
169 self, cards: List[NewsCard], user_id: str
170 ) -> List[NewsCard]:
171 """
172 Sort cards by relevance to the user.
173 Default implementation uses impact score and preference boost.
175 Args:
176 cards: List of cards to sort
177 user_id: User ID for personalization
179 Returns:
180 Sorted list of cards
181 """
183 def calculate_score(card: NewsCard) -> float:
184 # Base score from impact
185 score: float = card.impact_score / 10.0
187 # Apply preference boost if exists
188 boost: float = float(card.metadata.get("preference_boost", 1.0))
189 score *= boost
191 # Could add more factors here (recency, etc.)
192 return score
194 # Sort by calculated score (highest first)
195 return sorted(cards, key=calculate_score, reverse=True)
197 def get_strategy_info(self) -> Dict[str, Any]:
198 """Get information about this recommendation strategy."""
199 return {
200 "name": self.strategy_name,
201 "has_preference_manager": self.preference_manager is not None,
202 "has_rating_system": self.rating_system is not None,
203 "has_search_system": self.search_system is not None,
204 "description": self.__doc__ or "No description available",
205 }