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

105 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:55 +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: 

88 logger.exception( 

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

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 llm = None 

221 search = None 

222 search_system = None 

223 try: 

224 # Use news search strategy 

225 from ...config.llm_config import get_llm 

226 from ...config.search_config import get_search 

227 

228 llm = get_llm() 

229 search = get_search(llm_instance=llm) 

230 search_system = AdvancedSearchSystem( 

231 llm=llm, search=search, strategy_name="news" 

232 ) 

233 

234 # Mark as news search to use priority system 

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

236 

237 if "error" in results: 

238 logger.error( 

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

240 ) 

241 return None 

242 

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

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

245 

246 # Use the news items from search results 

247 news_data = { 

248 "items": news_items, 

249 "item_count": len(news_items), 

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

251 "topics": [], 

252 } 

253 

254 if not news_items: 

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

256 return None 

257 

258 # Create card using factory 

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

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

261 

262 card = CardFactory.create_news_card_from_analysis( 

263 news_item=main_item, 

264 source_search_id=str(results.get("search_id") or ""), 

265 user_id=user_id, 

266 additional_metadata={ 

267 "recommender": self.strategy_name, 

268 "original_topic": topic, 

269 "query_used": query, 

270 "total_items_found": len(news_items), 

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

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

273 }, 

274 ) 

275 

276 # Add the full analysis as the first version 

277 if card: 277 ↛ 289line 277 didn't jump to line 289 because the condition on line 277 was always true

278 card.add_version( 

279 research_results={ 

280 "search_results": results, 

281 "news_analysis": news_data, 

282 "query": query, 

283 "strategy": "news_aggregation", 

284 }, 

285 query=query, 

286 strategy="news_aggregation", 

287 ) 

288 

289 return card 

290 

291 except Exception: 

292 logger.exception( 

293 f"Error creating recommendation card for topic '{topic}'" 

294 ) 

295 return None 

296 finally: 

297 from ...utilities.resource_utils import safe_close 

298 

299 safe_close(search_system, "news search system", allow_none=True) 

300 safe_close(search, "news search engine", allow_none=True) 

301 safe_close(llm, "news LLM", allow_none=True) 

302 

303 

304class SearchBasedRecommender(BaseRecommender): 

305 """ 

306 Recommends news based on user's recent searches. 

307 Only works if search tracking is enabled. 

308 """ 

309 

310 def generate_recommendations( 

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

312 ) -> List[NewsCard]: 

313 """ 

314 Generate recommendations from user's search history. 

315 

316 Args: 

317 user_id: User to generate recommendations for 

318 context: Optional context 

319 

320 Returns: 

321 List of NewsCard recommendations 

322 """ 

323 logger.info( 

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

325 ) 

326 

327 # This would need access to search history 

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

329 logger.warning( 

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

331 ) 

332 return [] 

333 

334 # Future implementation would: 

335 # 1. Get user's recent searches 

336 # 2. Transform them to news queries 

337 # 3. Create recommendation cards