Coverage for src / local_deep_research / news / preference_manager / base_preference.py: 26%

70 statements  

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

1""" 

2Base class for preference management. 

3Following LDR's pattern from BaseSearchStrategy. 

4""" 

5 

6from abc import ABC, abstractmethod 

7from typing import Dict, Any, List, Optional 

8from datetime import timedelta 

9from loguru import logger 

10 

11from ..core.utils import utc_now 

12 

13 

14class BasePreferenceManager(ABC): 

15 """Abstract base class for preference management.""" 

16 

17 def __init__(self, storage_backend: Optional[Any] = None): 

18 """ 

19 Initialize the base preference manager. 

20 

21 Args: 

22 storage_backend: Optional storage backend for preferences 

23 """ 

24 self.storage_backend = storage_backend 

25 

26 @abstractmethod 

27 def get_preferences(self, user_id: str) -> Dict[str, Any]: 

28 """ 

29 Get user preferences. 

30 

31 Args: 

32 user_id: ID of the user 

33 

34 Returns: 

35 Dictionary of user preferences 

36 """ 

37 pass 

38 

39 @abstractmethod 

40 def update_preferences( 

41 self, user_id: str, preferences: Dict[str, Any] 

42 ) -> Dict[str, Any]: 

43 """ 

44 Update user preferences. 

45 

46 Args: 

47 user_id: ID of the user 

48 preferences: Dictionary of preferences to update 

49 

50 Returns: 

51 Updated preferences 

52 """ 

53 pass 

54 

55 def add_interest( 

56 self, user_id: str, interest: str, weight: float = 1.0 

57 ) -> None: 

58 """ 

59 Add an interest to user preferences. 

60 

61 Args: 

62 user_id: ID of the user 

63 interest: The interest to add 

64 weight: Weight/importance of this interest 

65 """ 

66 prefs = self.get_preferences(user_id) 

67 

68 if "interests" not in prefs: 

69 prefs["interests"] = {} 

70 

71 prefs["interests"][interest] = weight 

72 prefs["interests_updated_at"] = utc_now().isoformat() 

73 

74 self.update_preferences(user_id, prefs) 

75 logger.info(f"Added interest '{interest}' for user {user_id}") 

76 

77 def remove_interest(self, user_id: str, interest: str) -> None: 

78 """ 

79 Remove an interest from user preferences. 

80 

81 Args: 

82 user_id: ID of the user 

83 interest: The interest to remove 

84 """ 

85 prefs = self.get_preferences(user_id) 

86 

87 if "interests" in prefs and interest in prefs["interests"]: 

88 del prefs["interests"][interest] 

89 prefs["interests_updated_at"] = utc_now().isoformat() 

90 

91 self.update_preferences(user_id, prefs) 

92 logger.info(f"Removed interest '{interest}' for user {user_id}") 

93 

94 def ignore_topic(self, user_id: str, topic: str) -> None: 

95 """ 

96 Add a topic to the ignore list. 

97 

98 Args: 

99 user_id: ID of the user 

100 topic: Topic to ignore 

101 """ 

102 prefs = self.get_preferences(user_id) 

103 

104 if "disliked_topics" not in prefs: 

105 prefs["disliked_topics"] = [] 

106 

107 if topic not in prefs["disliked_topics"]: 

108 prefs["disliked_topics"].append(topic) 

109 prefs["preferences_updated_at"] = utc_now().isoformat() 

110 

111 self.update_preferences(user_id, prefs) 

112 logger.info(f"Added '{topic}' to ignore list for user {user_id}") 

113 

114 def boost_source( 

115 self, user_id: str, source: str, weight: float = 1.5 

116 ) -> None: 

117 """ 

118 Boost a particular news source. 

119 

120 Args: 

121 user_id: ID of the user 

122 source: Source domain to boost 

123 weight: Boost weight 

124 """ 

125 prefs = self.get_preferences(user_id) 

126 

127 if "source_weights" not in prefs: 

128 prefs["source_weights"] = {} 

129 

130 prefs["source_weights"][source] = weight 

131 prefs["preferences_updated_at"] = utc_now().isoformat() 

132 

133 self.update_preferences(user_id, prefs) 

134 logger.info( 

135 f"Set source weight for '{source}' to {weight} for user {user_id}" 

136 ) 

137 

138 def get_default_preferences(self) -> Dict[str, Any]: 

139 """ 

140 Get default preferences for new users. 

141 

142 Returns: 

143 Dictionary of default preferences 

144 """ 

145 return { 

146 "liked_categories": [], 

147 "disliked_categories": [], 

148 "liked_topics": [], 

149 "disliked_topics": [], 

150 "interests": {}, 

151 "source_weights": {}, 

152 "impact_threshold": 5, # Default threshold 

153 "focus_preferences": { 

154 "surprising": False, 

155 "breaking": True, 

156 "positive": False, 

157 "local": False, 

158 }, 

159 "custom_search_terms": "", 

160 "search_strategy": "news_aggregation", 

161 "created_at": utc_now().isoformat(), 

162 "preferences_updated_at": utc_now().isoformat(), 

163 } 

164 

165 

166class TopicRegistry: 

167 """ 

168 Registry for dynamically discovered topics. 

169 Not user-specific - tracks global topic trends. 

170 """ 

171 

172 def __init__(self, llm_client: Optional[Any] = None): 

173 """ 

174 Initialize topic registry. 

175 

176 Args: 

177 llm_client: LLM client for topic extraction 

178 """ 

179 self.llm_client = llm_client 

180 self.topics: Dict[str, Dict[str, Any]] = {} 

181 

182 def extract_topics(self, content: str, max_topics: int = 5) -> List[str]: 

183 """ 

184 Extract topics from content using topic generator. 

185 

186 Args: 

187 content: Text content to analyze 

188 max_topics: Maximum number of topics to extract 

189 

190 Returns: 

191 List of extracted topics 

192 """ 

193 from ..utils.topic_generator import generate_topics 

194 

195 # Use topic generator to extract topics 

196 topics = generate_topics( 

197 query="", # No specific query, just analyzing content 

198 findings=content, 

199 category="", 

200 max_topics=max_topics, 

201 ) 

202 

203 # Register discovered topics 

204 for topic in topics: 

205 self.register_topic(topic) 

206 

207 return topics 

208 

209 def register_topic(self, topic: str) -> None: 

210 """ 

211 Register a discovered topic. 

212 

213 Args: 

214 topic: Topic to register 

215 """ 

216 if topic not in self.topics: 

217 self.topics[topic] = { 

218 "first_seen": utc_now(), 

219 "count": 0, 

220 "last_seen": utc_now(), 

221 } 

222 

223 self.topics[topic]["count"] += 1 

224 self.topics[topic]["last_seen"] = utc_now() 

225 

226 def get_trending_topics( 

227 self, hours: int = 24, limit: int = 10 

228 ) -> List[str]: 

229 """ 

230 Get trending topics from the last N hours. 

231 

232 Args: 

233 hours: Look back period in hours 

234 limit: Maximum number of topics to return 

235 

236 Returns: 

237 List of trending topic names 

238 """ 

239 cutoff_time = utc_now() - timedelta(hours=hours) 

240 

241 # Filter topics seen recently 

242 recent_topics = [ 

243 (topic, data) 

244 for topic, data in self.topics.items() 

245 if data["last_seen"] >= cutoff_time 

246 ] 

247 

248 # Sort by count (most frequent first) 

249 recent_topics.sort(key=lambda x: x[1]["count"], reverse=True) 

250 

251 # Return topic names only 

252 return [topic for topic, _ in recent_topics[:limit]] 

253 

254 def get_topic_info(self, topic: str) -> Optional[Dict[str, Any]]: 

255 """ 

256 Get information about a specific topic. 

257 

258 Args: 

259 topic: Topic to look up 

260 

261 Returns: 

262 Topic information or None if not found 

263 """ 

264 return self.topics.get(topic)