Coverage for src / local_deep_research / news / recommender / topic_based.py: 12%

94 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-01-11 00:51 +0000

1""" 

2Topic-based recommender that generates recommendations from news topics. 

3This is the primary recommender for v1. 

4""" 

5 

6from typing import List, Dict, Any, Optional 

7from loguru import logger 

8 

9from .base_recommender import BaseRecommender 

10from ..core.base_card import NewsCard 

11from ..core.card_factory import CardFactory 

12from ...search_system import AdvancedSearchSystem 

13 

14 

15class TopicBasedRecommender(BaseRecommender): 

16 """ 

17 Recommends news based on topics extracted from recent news analysis. 

18 

19 This recommender: 

20 1. Gets recent news topics from the topic registry 

21 2. Filters based on user preferences 

22 3. Generates search queries for interesting topics 

23 4. Creates NewsCards from the results 

24 """ 

25 

26 def __init__(self, **kwargs): 

27 """Initialize the topic-based recommender.""" 

28 super().__init__(**kwargs) 

29 self.max_recommendations = 5 # Default limit 

30 

31 def generate_recommendations( 

32 self, user_id: str, context: Optional[Dict[str, Any]] = None 

33 ) -> List[NewsCard]: 

34 """ 

35 Generate recommendations based on trending topics. 

36 

37 Args: 

38 user_id: User to generate recommendations for 

39 context: Optional context like current news being viewed 

40 

41 Returns: 

42 List of NewsCard recommendations 

43 """ 

44 logger.info( 

45 f"Generating topic-based recommendations for user {user_id}" 

46 ) 

47 

48 recommendations = [] 

49 

50 try: 

51 # Update progress 

52 self._update_progress("Getting trending topics", 10) 

53 

54 # Get trending topics 

55 trending_topics = self._get_trending_topics(context) 

56 

57 # Filter by user preferences 

58 self._update_progress("Applying user preferences", 30) 

59 preferences = self._get_user_preferences(user_id) 

60 filtered_topics = self._filter_topics_by_preferences( 

61 trending_topics, preferences 

62 ) 

63 

64 # Generate recommendations for top topics 

65 self._update_progress("Generating news searches", 50) 

66 

67 for i, topic in enumerate( 

68 filtered_topics[: self.max_recommendations] 

69 ): 

70 progress = 50 + ( 

71 40 * i / len(filtered_topics[: self.max_recommendations]) 

72 ) 

73 self._update_progress(f"Searching for: {topic}", int(progress)) 

74 

75 # Create search query 

76 query = self._generate_topic_query(topic) 

77 

78 # Register with priority manager 

79 try: 

80 # Execute search 

81 card = self._create_recommendation_card( 

82 topic, query, user_id 

83 ) 

84 if card: 

85 recommendations.append(card) 

86 

87 except Exception as e: 

88 logger.exception( 

89 f"Error creating recommendation for topic '{topic}': {e}" 

90 ) 

91 continue 

92 

93 self._update_progress("Recommendations complete", 100) 

94 

95 # Sort by relevance 

96 recommendations = self._sort_by_relevance(recommendations, user_id) 

97 

98 logger.info( 

99 f"Generated {len(recommendations)} recommendations for user {user_id}" 

100 ) 

101 

102 except Exception as e: 

103 logger.exception("Error generating recommendations") 

104 self._update_progress(f"Error: {str(e)}", 100) 

105 

106 return recommendations 

107 

108 def _get_trending_topics( 

109 self, context: Optional[Dict[str, Any]] 

110 ) -> List[str]: 

111 """ 

112 Get trending topics to recommend. 

113 

114 Args: 

115 context: Optional context 

116 

117 Returns: 

118 List of trending topic strings 

119 """ 

120 topics = [] 

121 

122 # Get from topic registry if available 

123 if self.topic_registry: 

124 topics.extend( 

125 self.topic_registry.get_trending_topics(hours=24, limit=20) 

126 ) 

127 

128 # Add context-based topics if provided 

129 if context: 

130 if "current_news_topics" in context: 

131 topics.extend(context["current_news_topics"]) 

132 if "current_category" in context: 

133 # Could fetch related topics based on category 

134 pass 

135 

136 # Fallback topics if none found 

137 if not topics: 

138 logger.warning("No trending topics found, using defaults") 

139 topics = [ 

140 "artificial intelligence developments", 

141 "cybersecurity threats", 

142 "climate change", 

143 "economic policy", 

144 "technology innovation", 

145 ] 

146 

147 return topics 

148 

149 def _filter_topics_by_preferences( 

150 self, topics: List[str], preferences: Dict[str, Any] 

151 ) -> List[str]: 

152 """ 

153 Filter topics based on user preferences. 

154 

155 Args: 

156 topics: List of topics to filter 

157 preferences: User preferences 

158 

159 Returns: 

160 Filtered list of topics 

161 """ 

162 filtered = [] 

163 

164 # Get preference lists 

165 disliked_topics = [ 

166 t.lower() for t in preferences.get("disliked_topics", []) 

167 ] 

168 interests = preferences.get("interests", {}) 

169 

170 for topic in topics: 

171 topic_lower = topic.lower() 

172 

173 # Skip disliked topics 

174 if any(disliked in topic_lower for disliked in disliked_topics): 

175 continue 

176 

177 # Boost topics matching interests 

178 boost = 1.0 

179 for interest, weight in interests.items(): 

180 if interest.lower() in topic_lower: 

181 boost = weight 

182 break 

183 

184 # Add with boost information 

185 filtered.append((topic, boost)) 

186 

187 # Sort by boost (highest first) 

188 filtered.sort(key=lambda x: x[1], reverse=True) 

189 

190 # Return just the topics 

191 return [topic for topic, _ in filtered] 

192 

193 def _generate_topic_query(self, topic: str) -> str: 

194 """ 

195 Generate a search query for a topic. 

196 

197 Args: 

198 topic: The topic to search for 

199 

200 Returns: 

201 Search query string 

202 """ 

203 # Add news-specific context 

204 return f"{topic} latest news today breaking developments" 

205 

206 def _create_recommendation_card( 

207 self, topic: str, query: str, user_id: str 

208 ) -> Optional[NewsCard]: 

209 """ 

210 Create a news card from a topic recommendation. 

211 

212 Args: 

213 topic: The topic 

214 query: The search query used 

215 user_id: The user ID 

216 

217 Returns: 

218 NewsCard or None if search fails 

219 """ 

220 try: 

221 # Use news search strategy 

222 search_system = AdvancedSearchSystem(strategy_name="news") 

223 

224 # Mark as news search to use priority system 

225 results = search_system.analyze_topic(query, is_news_search=True) 

226 

227 if "error" in results: 

228 logger.error( 

229 f"Search failed for topic '{topic}': {results['error']}" 

230 ) 

231 return None 

232 

233 # Check if we have news items directly from the search 

234 news_items = results.get("news_items", []) 

235 

236 # Use the news items from search results 

237 news_data = { 

238 "items": news_items, 

239 "item_count": len(news_items), 

240 "big_picture": results.get("formatted_findings", ""), 

241 "topics": [], 

242 } 

243 

244 if not news_items: 

245 logger.warning(f"No news items found for topic '{topic}'") 

246 return None 

247 

248 # Create card using factory 

249 # Use the most impactful news item as the main content 

250 main_item = max(news_items, key=lambda x: x.get("impact_score", 0)) 

251 

252 card = CardFactory.create_news_card_from_analysis( 

253 news_item=main_item, 

254 source_search_id=results.get("search_id"), 

255 user_id=user_id, 

256 additional_metadata={ 

257 "recommender": self.strategy_name, 

258 "original_topic": topic, 

259 "query_used": query, 

260 "total_items_found": len(news_items), 

261 "big_picture": news_data.get("big_picture", ""), 

262 "topics_extracted": news_data.get("topics", []), 

263 }, 

264 ) 

265 

266 # Add the full analysis as the first version 

267 if card: 

268 card.add_version( 

269 research_results={ 

270 "search_results": results, 

271 "news_analysis": news_data, 

272 "query": query, 

273 "strategy": "news_aggregation", 

274 }, 

275 query=query, 

276 strategy="news_aggregation", 

277 ) 

278 

279 return card 

280 

281 except Exception as e: 

282 logger.exception( 

283 f"Error creating recommendation card for topic '{topic}': {e}" 

284 ) 

285 return None 

286 

287 

288class SearchBasedRecommender(BaseRecommender): 

289 """ 

290 Recommends news based on user's recent searches. 

291 Only works if search tracking is enabled. 

292 """ 

293 

294 def generate_recommendations( 

295 self, user_id: str, context: Optional[Dict[str, Any]] = None 

296 ) -> List[NewsCard]: 

297 """ 

298 Generate recommendations from user's search history. 

299 

300 Args: 

301 user_id: User to generate recommendations for 

302 context: Optional context 

303 

304 Returns: 

305 List of NewsCard recommendations 

306 """ 

307 logger.info( 

308 f"Generating search-based recommendations for user {user_id}" 

309 ) 

310 

311 # This would need access to search history 

312 # For now, return empty since search tracking is OFF by default 

313 logger.warning( 

314 "Search-based recommendations not available - search tracking is disabled" 

315 ) 

316 return [] 

317 

318 # Future implementation would: 

319 # 1. Get user's recent searches 

320 # 2. Transform them to news queries 

321 # 3. Create recommendation cards