β€œbotayla” commited on
Commit
808cc4e
Β·
1 Parent(s): 682426d

add keybert model in requirments.txt and pyproject.toml

Browse files
pyproject.toml CHANGED
@@ -14,8 +14,10 @@ dependencies = [
14
  "groq>=0.9.0",
15
  "greenlet==3.3.1",
16
  "httpx==0.26.0",
 
17
  "openai-whisper==20250625",
18
  "passlib[bcrypt]==1.7.4",
 
19
  "pydantic-core==2.41.5",
20
  "pydantic-settings==2.1.0",
21
  "pydantic[email]==2.12.5",
 
14
  "groq>=0.9.0",
15
  "greenlet==3.3.1",
16
  "httpx==0.26.0",
17
+ "keybert>=0.8.0",
18
  "openai-whisper==20250625",
19
  "passlib[bcrypt]==1.7.4",
20
+ "sentence-transformers>=3.0.0",
21
  "pydantic-core==2.41.5",
22
  "pydantic-settings==2.1.0",
23
  "pydantic[email]==2.12.5",
requirements.txt CHANGED
@@ -39,8 +39,8 @@ firebase-admin==6.5.0
39
  dnspython
40
 
41
  # --- ML & Recommendations ---
42
- # keybert
43
- # sentence-transformers
44
 
45
  # --- Qwen Local Inference ---
46
  transformers>=4.37.0
 
39
  dnspython
40
 
41
  # --- ML & Recommendations ---
42
+ keybert>=0.8.0
43
+ sentence-transformers>=3.0.0
44
 
45
  # --- Qwen Local Inference ---
46
  transformers>=4.37.0
src/recommendation/recommender.py CHANGED
@@ -24,7 +24,7 @@ class RecommendationService:
24
  def __init__(self, api_key: Optional[str] = None):
25
  self.api_key = "AIzaSyA3erB-Lxd5SOoBOXaumOCVaEr3TcgYG60"
26
  self.youtube = build("youtube", "v3", developerKey=self.api_key)
27
- self.groq_client = Groq(api_key="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") # Ω‡Ω†Ψ§
28
 
29
  # ──────────────────────────────────────────────
30
  # Step 1: top 3 categories
@@ -36,17 +36,17 @@ class RecommendationService:
36
  cat = note.get("category")
37
  if not cat:
38
  continue
39
- cats = cat if isinstance(cat, list) else [cat]
40
- for c in cats:
41
  if c and c != "Uncategorized":
42
  counter[c] += 1
43
 
44
  top = [cat for cat, _ in counter.most_common(top_n)]
45
- logger.info(f"πŸ† Top categories: {top}")
46
  return top
47
 
48
  # ──────────────────────────────────────────────
49
- # Step 2: keywords from latest note per category
50
  # ──────────────────────────────────────────────
51
  def _latest_notes_per_category(
52
  self, notes: List[Dict], categories: List[str], top_n: int = 2
@@ -54,7 +54,7 @@ class RecommendationService:
54
  """
55
  return a dict mapping each category to its latest N notes, sorted by createdAt.
56
  """
57
- buckets: Dict[str, List[Dict]] = {cat: [] for cat in categories}
58
 
59
  for note in notes:
60
  cat = note.get("category")
@@ -65,12 +65,17 @@ class RecommendationService:
65
 
66
  # sort each category's notes by createdAt and keep top N
67
  return {
68
- cat: sorted(notes_list, key=lambda n: n.get("createdAt", 0), reverse=True)[:top_n]
 
 
69
  for cat, notes_list in buckets.items()
70
  }
71
-
 
 
 
72
  async def _extract_keywords_with_claude(
73
- self, notes: List[Dict], category: str # ← List Ψ¨Ψ―Ω„ Dict
74
  ) -> List[str]:
75
 
76
  # combine all relevant text fields from the notes into one string for context
@@ -88,7 +93,7 @@ class RecommendationService:
88
  f"extract 3 to 5 concise English keywords or short phrases that best "
89
  f"represent the core topic for a YouTube educational search. "
90
  f"Reply with ONLY a JSON array of strings, no explanation.\n\n"
91
- f"Notes:\n{combined_content[:2000]}" # ← زودي Ψ§Ω„Ψ­Ψ― شوية
92
  )
93
 
94
  try:
@@ -108,15 +113,15 @@ class RecommendationService:
108
  raw = re.sub(r"```json|```", "", raw).strip()
109
  keywords = json.loads(raw)
110
  if isinstance(keywords, list):
111
- logger.info(f"πŸ”‘ Keywords for '{category}': {keywords}")
112
  return [str(k) for k in keywords[:5]]
113
  except Exception as e:
114
- logger.warning(f"⚠️ Claude keyword extraction failed for '{category}': {e}")
115
 
116
  return [category] # fallback
117
 
118
  # ──────────────────────────────────────────────
119
- # Step 3: build query & search YouTube
120
  # ──────────────────────────────────────────────
121
  async def _build_search_query(
122
  self, category_keywords: Dict[str, List[str]]
@@ -129,7 +134,7 @@ class RecommendationService:
129
  for keywords in category_keywords.values():
130
  parts.extend(keywords[:2])
131
  query = " OR ".join(parts[:6]) # YouTube search works best under ~60 chars
132
- logger.info(f"πŸ” Final search query: {query}")
133
  return query
134
 
135
  async def get_youtube_recommendations(
@@ -140,7 +145,7 @@ class RecommendationService:
140
  return []
141
 
142
  enhanced_query = f"{query} tutorial "
143
- logger.info(f"🎬 Searching YouTube: {enhanced_query}")
144
 
145
  try:
146
  loop = asyncio.get_event_loop()
@@ -176,11 +181,11 @@ class RecommendationService:
176
 
177
  random.shuffle(videos)
178
  result = videos[:limit]
179
- logger.info(f"βœ… Returning {len(result)} recommendations")
180
  return result
181
 
182
  except Exception as e:
183
- logger.error(f"❌ YouTube search failed: {e}")
184
  return []
185
 
186
  # ──────────────────────────────────────────────
@@ -189,7 +194,7 @@ class RecommendationService:
189
  async def get_recommendations_for_user(
190
  self, db, user_id: str, limit: int = 5
191
  ) -> List[Dict]:
192
- logger.info(f"πŸ“š Fetching notes for user: {user_id}")
193
 
194
  # ── Fetch notes ──────────────────────────
195
  try:
@@ -199,20 +204,20 @@ class RecommendationService:
199
  .stream()
200
  )
201
  notes = [doc.to_dict() for doc in notes_docs]
202
- logger.info(f"πŸ“ Found {len(notes)} notes")
203
  except Exception as e:
204
- logger.error(f"❌ Firebase fetch failed: {e}")
205
  notes = []
206
 
207
  if not notes:
208
- logger.info("⚠️ No notes β€” falling back to general recommendations")
209
  return await self.get_youtube_recommendations("educational tutorials", limit)
210
 
211
  # ── Step 1: top 3 categories ─────────────
212
  top_categories = self._get_top_categories(notes, top_n=3)
213
 
214
  if not top_categories:
215
- logger.info("⚠️ No valid categories β€” falling back")
216
  return await self.get_youtube_recommendations("educational tutorials", limit)
217
 
218
  # ── Step 2: keywords via Claude ──────────
@@ -232,7 +237,7 @@ class RecommendationService:
232
 
233
  category_keywords: Dict[str, List[str]] = {
234
  cat: kws
235
- for cat, kws in zip(valid_categories, keyword_results) # βœ… zip ΨΉΩ„Ω‰ نفس Ψ§Ω„Ω„ΩŠΨ³Ψͺ
236
  }
237
  # ── Step 3: build query & recommend ──────
238
  all_videos = []
@@ -240,7 +245,7 @@ class RecommendationService:
240
  for category, keywords in category_keywords.items():
241
  query = " ".join(keywords[:3])
242
 
243
- logger.info(f"🎯 Searching category: {category} | Query: {query}")
244
 
245
  videos = await self.get_youtube_recommendations(query, limit=2)
246
 
 
24
  def __init__(self, api_key: Optional[str] = None):
25
  self.api_key = "AIzaSyA3erB-Lxd5SOoBOXaumOCVaEr3TcgYG60"
26
  self.youtube = build("youtube", "v3", developerKey=self.api_key)
27
+ self.groq_client = Groq(api_key="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")
28
 
29
  # ──────────────────────────────────────────────
30
  # Step 1: top 3 categories
 
36
  cat = note.get("category")
37
  if not cat:
38
  continue
39
+ cats = cat if isinstance(cat, list) else [cat] # sometimes category is a list(["AI","Technology"]), sometimes a string("AI")
40
+ for c in cats: # loop through each category in the list
41
  if c and c != "Uncategorized":
42
  counter[c] += 1
43
 
44
  top = [cat for cat, _ in counter.most_common(top_n)]
45
+ logger.info(f" Top categories: {top}")
46
  return top
47
 
48
  # ──────────────────────────────────────────────
49
+ # Step 2: keywords from latest 2notes per category
50
  # ──────────────────────────────────────────────
51
  def _latest_notes_per_category(
52
  self, notes: List[Dict], categories: List[str], top_n: int = 2
 
54
  """
55
  return a dict mapping each category to its latest N notes, sorted by createdAt.
56
  """
57
+ buckets: Dict[str, List[Dict]] = {cat: [] for cat in categories} # buckets ={"AI": [note1,note2,..], "Technology": [], "Science": []}
58
 
59
  for note in notes:
60
  cat = note.get("category")
 
65
 
66
  # sort each category's notes by createdAt and keep top N
67
  return {
68
+ cat: sorted(notes_list,
69
+ key=lambda n: n.get("createdAt", 0), # sort by createdAt timestamp, default to 0 if missing
70
+ reverse=True)[:top_n] # sort in descending order and take the top 2
71
  for cat, notes_list in buckets.items()
72
  }
73
+
74
+ # ──────────────────────────────────────────────
75
+ # Step 3: extract keywords via grok(llama-3.3-70b-versatile)
76
+ # ──────────────────────────────────────────────
77
  async def _extract_keywords_with_claude(
78
+ self, notes: List[Dict], category: str
79
  ) -> List[str]:
80
 
81
  # combine all relevant text fields from the notes into one string for context
 
93
  f"extract 3 to 5 concise English keywords or short phrases that best "
94
  f"represent the core topic for a YouTube educational search. "
95
  f"Reply with ONLY a JSON array of strings, no explanation.\n\n"
96
+ f"Notes:\n{combined_content[:3000]}"
97
  )
98
 
99
  try:
 
113
  raw = re.sub(r"```json|```", "", raw).strip()
114
  keywords = json.loads(raw)
115
  if isinstance(keywords, list):
116
+ logger.info(f" Keywords for '{category}': {keywords}")
117
  return [str(k) for k in keywords[:5]]
118
  except Exception as e:
119
+ logger.warning(f" Grok keyword extraction failed for '{category}': {e}")
120
 
121
  return [category] # fallback
122
 
123
  # ──────────────────────────────────────────────
124
+ # Step 4: build query & search YouTube
125
  # ──────────────────────────────────────────────
126
  async def _build_search_query(
127
  self, category_keywords: Dict[str, List[str]]
 
134
  for keywords in category_keywords.values():
135
  parts.extend(keywords[:2])
136
  query = " OR ".join(parts[:6]) # YouTube search works best under ~60 chars
137
+ logger.info(f" Final search query: {query}")
138
  return query
139
 
140
  async def get_youtube_recommendations(
 
145
  return []
146
 
147
  enhanced_query = f"{query} tutorial "
148
+ logger.info(f" Searching YouTube: {enhanced_query}")
149
 
150
  try:
151
  loop = asyncio.get_event_loop()
 
181
 
182
  random.shuffle(videos)
183
  result = videos[:limit]
184
+ logger.info(f" Returning {len(result)} recommendations")
185
  return result
186
 
187
  except Exception as e:
188
+ logger.error(f" YouTube search failed: {e}")
189
  return []
190
 
191
  # ──────────────────────────────────────────────
 
194
  async def get_recommendations_for_user(
195
  self, db, user_id: str, limit: int = 5
196
  ) -> List[Dict]:
197
+ logger.info(f" Fetching notes for user: {user_id}")
198
 
199
  # ── Fetch notes ──────────────────────────
200
  try:
 
204
  .stream()
205
  )
206
  notes = [doc.to_dict() for doc in notes_docs]
207
+ logger.info(f"Found {len(notes)} notes")
208
  except Exception as e:
209
+ logger.error(f" Firebase fetch failed: {e}")
210
  notes = []
211
 
212
  if not notes:
213
+ logger.info(" No notes β€” falling back to general recommendations")
214
  return await self.get_youtube_recommendations("educational tutorials", limit)
215
 
216
  # ── Step 1: top 3 categories ─────────────
217
  top_categories = self._get_top_categories(notes, top_n=3)
218
 
219
  if not top_categories:
220
+ logger.info(" No valid categories β€” falling back")
221
  return await self.get_youtube_recommendations("educational tutorials", limit)
222
 
223
  # ── Step 2: keywords via Claude ──────────
 
237
 
238
  category_keywords: Dict[str, List[str]] = {
239
  cat: kws
240
+ for cat, kws in zip(valid_categories, keyword_results)
241
  }
242
  # ── Step 3: build query & recommend ──────
243
  all_videos = []
 
245
  for category, keywords in category_keywords.items():
246
  query = " ".join(keywords[:3])
247
 
248
+ logger.info(f" Searching category: {category} | Query: {query}")
249
 
250
  videos = await self.get_youtube_recommendations(query, limit=2)
251