IAUCourseExp commited on
Commit
32f1170
·
verified ·
1 Parent(s): 4f3f8e9

Update my_logic.py

Browse files
Files changed (1) hide show
  1. my_logic.py +245 -245
my_logic.py CHANGED
@@ -1,245 +1,245 @@
1
- import pandas as pd
2
- from collections import defaultdict
3
- from difflib import SequenceMatcher
4
- from sentence_transformers import SentenceTransformer
5
- import faiss
6
- import json
7
- import torch
8
- import numpy as np
9
- from transformers import AutoTokenizer, AutoModel
10
-
11
-
12
- # Load CSV
13
-
14
- # Load FAISS index and metadata
15
- index = faiss.read_index("iau_reviews_index.faiss")
16
- with open("iau_metadata.json", "r", encoding="utf-8") as f:
17
- metadata = json.load(f)
18
-
19
-
20
-
21
- model = SentenceTransformer("HooshvareLab/bert-fa-zwnj-base")
22
- # Load reviews CSV
23
-
24
-
25
- # Load Persian tokenizer and model
26
- tokenizer = AutoTokenizer.from_pretrained("HooshvareLab/bert-fa-zwnj-base")
27
- model = AutoModel.from_pretrained("HooshvareLab/bert-fa-zwnj-base").eval()
28
-
29
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
- model.to(device)
31
-
32
- # Load FAISS index and metadata
33
- index = faiss.read_index("iau_reviews_index.faiss")
34
- with open("iau_metadata.json", "r", encoding="utf-8") as f:
35
- metadata = json.load(f)
36
-
37
- def mean_pooling(model_output, attention_mask):
38
- token_embeddings = model_output[0]
39
- input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size())
40
- return (token_embeddings * input_mask_expanded).sum(1) / input_mask_expanded.sum(1)
41
-
42
- def encode_texts(texts, batch_size=16):
43
- embeddings = []
44
- with torch.no_grad():
45
- for i in range(0, len(texts), batch_size):
46
- batch = texts[i:i+batch_size]
47
- encoded_input = tokenizer(batch, padding=True, truncation=True, return_tensors='pt', max_length=128).to(device)
48
- model_output = model(**encoded_input)
49
- sentence_embeddings = mean_pooling(model_output, encoded_input['attention_mask'])
50
- sentence_embeddings = sentence_embeddings.cpu().numpy()
51
- embeddings.append(sentence_embeddings)
52
- return np.vstack(embeddings)
53
-
54
- def search_reviews(query, top_k=5):
55
-
56
- keywords = query.strip().split()
57
-
58
- candidate_rows = [
59
- r for r in metadata
60
- if any(kw in r["professor"] or kw in r["course"] for kw in keywords)
61
- ]
62
-
63
- if not candidate_rows:
64
- return []
65
-
66
- texts = [r["course"] + " " + r["professor"] + " " + r["comment"] for r in candidate_rows]
67
- vectors = encode_texts(texts)
68
- vectors = vectors / np.linalg.norm(vectors, axis=1, keepdims=True)
69
-
70
- query_vec = encode_texts([query])
71
- query_vec = query_vec / np.linalg.norm(query_vec, axis=1, keepdims=True)
72
-
73
-
74
- local_index = faiss.IndexFlatIP(vectors.shape[1])
75
- local_index.add(vectors)
76
-
77
- D, I = local_index.search(query_vec, min(top_k, len(candidate_rows)))
78
-
79
- return [candidate_rows[i] for i in I[0]]
80
-
81
-
82
- def filter_relevant(results, query):
83
- query = query.replace("؟", "").strip()
84
- query_tokens = set(query.split())
85
-
86
- def is_strict_match(row):
87
- # Normalize and tokenize professor and course
88
- prof_tokens = set(str(row["professor"]).strip().split())
89
- course_tokens = set(str(row["course"]).strip().split())
90
-
91
- # Match only if full token overlap exists (not substrings)
92
- match_prof = prof_tokens & query_tokens
93
- match_course = course_tokens & query_tokens
94
-
95
- return bool(match_prof or match_course)
96
-
97
- # Return all matching results
98
- return [r for r in results if is_strict_match(r)]
99
-
100
-
101
-
102
-
103
-
104
- # ---- Fuzzy similarity score ----
105
- def similar(a, b):
106
- return SequenceMatcher(None, a, b).ratio()
107
-
108
- # ---- Enhanced keyword fallback ----
109
- def keyword_match_reviews(query, metadata):
110
- query = query.strip().replace("؟", "")
111
- keywords = set(query.split())
112
-
113
- results = []
114
- for row in metadata:
115
- prof = str(row["professor"])
116
- course = str(row["course"])
117
- for k in keywords:
118
- if k in prof or k in course or similar(k, prof) > 0.7 or similar(k, course) > 0.7:
119
- results.append(row)
120
- break
121
- return results
122
-
123
- # ---- Sort by relevance ----
124
- def relevance_score(row, query):
125
- score = 0
126
- if row["professor"] in query:
127
- score += 2
128
- if row["course"] in query:
129
- score += 2
130
- if row["professor"].split()[0] in query:
131
- score += 1
132
- if row["course"].split()[0] in query:
133
- score += 1
134
- return score
135
-
136
- # ---- Strict context builder (best prof+course only) ----
137
- def build_strict_context(reviews, user_question):
138
- prof_match_scores = defaultdict(int)
139
- course_match_scores = defaultdict(int)
140
-
141
- for r in reviews:
142
- prof_sim = similar(user_question, r["professor"])
143
- course_sim = similar(user_question, r["course"])
144
- if prof_sim > 0.6:
145
- prof_match_scores[r["professor"]] += prof_sim
146
- if course_sim > 0.6:
147
- course_match_scores[r["course"]] += course_sim
148
-
149
- best_prof = max(prof_match_scores, key=prof_match_scores.get, default="")
150
- best_course = max(course_match_scores, key=course_match_scores.get, default="")
151
-
152
- if best_prof and best_course:
153
- filtered = [
154
- r for r in reviews
155
- if similar(best_prof, r["professor"]) > 0.85 and similar(best_course, r["course"]) > 0.85
156
- ]
157
- elif best_course:
158
- filtered = [r for r in reviews if similar(best_course, r["course"]) > 0.85]
159
- elif best_prof:
160
- filtered = [r for r in reviews if similar(best_prof, r["professor"]) > 0.85]
161
- else:
162
- filtered = reviews
163
-
164
-
165
- result = f"👨‍🏫 استاد: {best_prof or '[نامشخص]'} — 📚 درس: {best_course or '[نامشخص]'}\n💬 نظرات:\n"
166
- for i, r in enumerate(filtered, 1):
167
- result += f"{i}. {r['comment'].strip()}\n🔗 لینک: {r['link']}\n\n"
168
- return result
169
-
170
- # ---- Truncation helper ----
171
- def truncate_reviews_to_fit(reviews, max_chars=127000):
172
- total = 0
173
- final = []
174
- for r in reviews:
175
- size = len(r["comment"])
176
- if total + size > max_chars:
177
- break
178
- final.append(r)
179
- total += size
180
- return final
181
-
182
- # ---- Main answer function ----
183
- def answer_question(user_question, gemini_model):
184
-
185
- print(f"\n🧠 Starting debug for question: {user_question}")
186
-
187
- retrieved = search_reviews(user_question, top_k=100)
188
- print(f"🔍 FAISS returned {len(retrieved)} raw rows")
189
-
190
- retrieved = filter_relevant(retrieved, user_question)
191
- print(f"✅ After filter_relevant(): {len(retrieved)} rows")
192
-
193
- keyword_hits = keyword_match_reviews(user_question, metadata)
194
- print(f"🔠 Keyword hits found: {len(keyword_hits)}")
195
-
196
- existing_links = set(r["link"] for r in retrieved)
197
- added = 0
198
- for r in keyword_hits:
199
- if r["link"] not in existing_links:
200
- retrieved.append(r)
201
- added += 1
202
- print(f"➕ Added {added} unique fallback keyword rows")
203
- print(f"📊 Total before truncation: {len(retrieved)}")
204
-
205
- if not retrieved:
206
- return "❌ هیچ تجربه‌ای در مورد سوال شما در داده‌های کانال یافت نشد."
207
-
208
- retrieved.sort(key=lambda r: relevance_score(r, user_question), reverse=True)
209
- retrieved = truncate_reviews_to_fit(retrieved)
210
- print(f"✂️ After truncation: {len(retrieved)} rows")
211
-
212
- context = build_strict_context(retrieved, user_question)
213
- print("📝 Sample context sent to GPT:\n", context[:100000], "\n...")
214
-
215
- prompt = f"""شما یک دستیار هوشمند انتخاب واحد هستید که فقط و فقط بر اساس نظرات واقعی دانشجویان از کانال @IAUCourseExp پاسخ می‌دهید. کار شما کمک به دانشجویان برای انتخاب استاد و درس، بر اساس تجربیات ثبت‌شده در این کانال است.
216
-
217
- ❗ قوانین مهم:
218
- - فقط از داده‌های همین نظرات استفاده کن. هیچ اطلاعات اضافی، حدسی یا اینترنتی استفاده نکن.
219
- - اگر هیچ نظری درباره سؤال وجود ندارد، فقط بگو: «هیچ تجربه‌ای دربارهٔ این مورد در کانال ثبت نشده است.»
220
- - سوالات دانشجو می‌توانند از انواع مختلف باشند:
221
- • بررسی یک استاد خاص
222
- • مقایسه چند استاد برای یک درس
223
- • معرفی بهترین یا بدترین استادهای یک درس
224
- • تحلیل نظر کلی دانشجویان درمورد یک درس خاص
225
- بنابراین آماده باش که با توجه به داده‌ها به هر نوع سوال، دقیق و قابل اعتماد پاسخ بدهی.
226
- - همه‌ی نظرات مربوط به سوال را بررسی کن (نه فقط یکی یا دو تا) و به‌صورت فهرست‌وار یا خلاصه‌شده تحلیلشان کن.
227
- - برای هر نظر، لینک تلگرام مربوطه را نیز حتماً ذکر کن.
228
- - در پایان پاسخ، نتیجه‌گیری نهایی خود را بنویس: آیا این استاد برای این درس توصیه می‌شود یا نه — فقط بر اساس همین نظرات.
229
- - در انتها حتماً بنویس:
230
- 📊 این پاسخ بر اساس بررسی {len(retrieved)} نظر دانشجویی نوشته شده است.
231
-
232
- 🔎 سوال دانشجو:
233
- {user_question}
234
-
235
- 📄 نظرات دانشجویان (برگرفته از کانال تجربیات انتخاب واحد):
236
- {context}
237
-
238
- 📘 پاسخ نهایی:
239
- """
240
-
241
-
242
- # NEW (Gemini)
243
-
244
- response = gemini_model.generate_content(prompt)
245
- return response.text
 
1
+ import pandas as pd
2
+ from collections import defaultdict
3
+ from difflib import SequenceMatcher
4
+ from sentence_transformers import SentenceTransformer
5
+ import faiss
6
+ import json
7
+ import torch
8
+ import numpy as np
9
+ from transformers import AutoTokenizer, AutoModel
10
+
11
+
12
+ # Load CSV
13
+
14
+ # Load FAISS index and metadata
15
+ index = faiss.read_index("iau_reviews_index.faiss")
16
+ with open("iau_metadata.json", "r", encoding="utf-8") as f:
17
+ metadata = json.load(f)
18
+
19
+
20
+
21
+ model = SentenceTransformer("HooshvareLab/bert-fa-zwnj-base")
22
+ # Load reviews CSV
23
+
24
+
25
+ # Load Persian tokenizer and model
26
+ tokenizer = AutoTokenizer.from_pretrained("HooshvareLab/bert-fa-zwnj-base")
27
+ model = AutoModel.from_pretrained("HooshvareLab/bert-fa-zwnj-base").eval()
28
+
29
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
+ model.to(device)
31
+
32
+ # Load FAISS index and metadata
33
+ index = faiss.read_index("iau_reviews_index.faiss")
34
+ with open("iau_metadata.json", "r", encoding="utf-8") as f:
35
+ metadata = json.load(f)
36
+
37
+ def mean_pooling(model_output, attention_mask):
38
+ token_embeddings = model_output[0]
39
+ input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size())
40
+ return (token_embeddings * input_mask_expanded).sum(1) / input_mask_expanded.sum(1)
41
+
42
+ def encode_texts(texts, batch_size=16):
43
+ embeddings = []
44
+ with torch.no_grad():
45
+ for i in range(0, len(texts), batch_size):
46
+ batch = texts[i:i+batch_size]
47
+ encoded_input = tokenizer(batch, padding=True, truncation=True, return_tensors='pt', max_length=128).to(device)
48
+ model_output = model(**encoded_input)
49
+ sentence_embeddings = mean_pooling(model_output, encoded_input['attention_mask'])
50
+ sentence_embeddings = sentence_embeddings.cpu().numpy()
51
+ embeddings.append(sentence_embeddings)
52
+ return np.vstack(embeddings)
53
+
54
+ def search_reviews(query, top_k=5):
55
+
56
+ keywords = query.strip().split()
57
+
58
+ candidate_rows = [
59
+ r for r in metadata
60
+ if any(kw in r["professor"] or kw in r["course"] for kw in keywords)
61
+ ]
62
+
63
+ if not candidate_rows:
64
+ return []
65
+
66
+ texts = [r["course"] + " " + r["professor"] + " " + r["comment"] for r in candidate_rows]
67
+ vectors = encode_texts(texts)
68
+ vectors = vectors / np.linalg.norm(vectors, axis=1, keepdims=True)
69
+
70
+ query_vec = encode_texts([query])
71
+ query_vec = query_vec / np.linalg.norm(query_vec, axis=1, keepdims=True)
72
+
73
+
74
+ local_index = faiss.IndexFlatIP(vectors.shape[1])
75
+ local_index.add(vectors)
76
+
77
+ D, I = local_index.search(query_vec, min(top_k, len(candidate_rows)))
78
+
79
+ return [candidate_rows[i] for i in I[0]]
80
+
81
+
82
+ def filter_relevant(results, query):
83
+ query = query.replace("؟", "").strip()
84
+ query_tokens = set(query.split())
85
+
86
+ def is_strict_match(row):
87
+ # Normalize and tokenize professor and course
88
+ prof_tokens = set(str(row["professor"]).strip().split())
89
+ course_tokens = set(str(row["course"]).strip().split())
90
+
91
+ # Match only if full token overlap exists (not substrings)
92
+ match_prof = prof_tokens & query_tokens
93
+ match_course = course_tokens & query_tokens
94
+
95
+ return bool(match_prof or match_course)
96
+
97
+ # Return all matching results
98
+ return [r for r in results if is_strict_match(r)]
99
+
100
+
101
+
102
+
103
+
104
+ # ---- Fuzzy similarity score ----
105
+ def similar(a, b):
106
+ return SequenceMatcher(None, a, b).ratio()
107
+
108
+ # ---- Enhanced keyword fallback ----
109
+ def keyword_match_reviews(query, metadata):
110
+ query = query.strip().replace("؟", "")
111
+ keywords = set(query.split())
112
+
113
+ results = []
114
+ for row in metadata:
115
+ prof = str(row["professor"])
116
+ course = str(row["course"])
117
+ for k in keywords:
118
+ if k in prof or k in course or similar(k, prof) > 0.7 or similar(k, course) > 0.7:
119
+ results.append(row)
120
+ break
121
+ return results
122
+
123
+ # ---- Sort by relevance ----
124
+ def relevance_score(row, query):
125
+ score = 0
126
+ if row["professor"] in query:
127
+ score += 2
128
+ if row["course"] in query:
129
+ score += 2
130
+ if row["professor"].split()[0] in query:
131
+ score += 1
132
+ if row["course"].split()[0] in query:
133
+ score += 1
134
+ return score
135
+
136
+ # ---- Strict context builder (best prof+course only) ----
137
+ def build_strict_context(reviews, user_question):
138
+ prof_match_scores = defaultdict(int)
139
+ course_match_scores = defaultdict(int)
140
+
141
+ for r in reviews:
142
+ prof_sim = similar(user_question, r["professor"])
143
+ course_sim = similar(user_question, r["course"])
144
+ if prof_sim > 0.6:
145
+ prof_match_scores[r["professor"]] += prof_sim
146
+ if course_sim > 0.6:
147
+ course_match_scores[r["course"]] += course_sim
148
+
149
+ best_prof = max(prof_match_scores, key=prof_match_scores.get, default="")
150
+ best_course = max(course_match_scores, key=course_match_scores.get, default="")
151
+
152
+ if best_prof and best_course:
153
+ filtered = [
154
+ r for r in reviews
155
+ if similar(best_prof, r["professor"]) > 0.85 and similar(best_course, r["course"]) > 0.85
156
+ ]
157
+ elif best_course:
158
+ filtered = [r for r in reviews if similar(best_course, r["course"]) > 0.85]
159
+ elif best_prof:
160
+ filtered = [r for r in reviews if similar(best_prof, r["professor"]) > 0.85]
161
+ else:
162
+ filtered = reviews
163
+
164
+
165
+ result = f"👨‍🏫 استاد: {best_prof or '[نامشخص]'} — 📚 درس: {best_course or '[نامشخص]'}\n💬 نظرات:\n"
166
+ for i, r in enumerate(filtered, 1):
167
+ result += f"{i}. {r['comment'].strip()}\n🔗 لینک: {r['link']}\n\n"
168
+ return result
169
+
170
+ # ---- Truncation helper ----
171
+ def truncate_reviews_to_fit(reviews, max_chars=127000):
172
+ total = 0
173
+ final = []
174
+ for r in reviews:
175
+ size = len(r["comment"])
176
+ if total + size > max_chars:
177
+ break
178
+ final.append(r)
179
+ total += size
180
+ return final
181
+
182
+ # ---- Main answer function ----
183
+ def answer_question(user_question, gemini_model):
184
+
185
+ print(f"\n🧠 Starting debug for question: {user_question}")
186
+
187
+ retrieved = search_reviews(user_question, top_k=100)
188
+ print(f"🔍 FAISS returned {len(retrieved)} raw rows")
189
+
190
+ retrieved = filter_relevant(retrieved, user_question)
191
+ print(f"✅ After filter_relevant(): {len(retrieved)} rows")
192
+
193
+ keyword_hits = keyword_match_reviews(user_question, metadata)
194
+ print(f"🔠 Keyword hits found: {len(keyword_hits)}")
195
+
196
+ existing_links = set(r["link"] for r in retrieved)
197
+ added = 0
198
+ for r in keyword_hits:
199
+ if r["link"] not in existing_links:
200
+ retrieved.append(r)
201
+ added += 1
202
+ print(f"➕ Added {added} unique fallback keyword rows")
203
+ print(f"📊 Total before truncation: {len(retrieved)}")
204
+
205
+ if not retrieved:
206
+ return "❌ هیچ تجربه‌ای در مورد سوال شما در داده‌های کانال یافت نشد."
207
+
208
+ retrieved.sort(key=lambda r: relevance_score(r, user_question), reverse=True)
209
+ retrieved = truncate_reviews_to_fit(retrieved)
210
+ print(f"✂️ After truncation: {len(retrieved)} rows")
211
+
212
+ context = build_strict_context(retrieved, user_question)
213
+ print("📝 Sample context sent to GPT:\n", context[:100000], "\n...")
214
+
215
+ prompt = f"""شما یک دستیار هوشمند انتخاب واحد هستید که فقط و فقط بر اساس نظرات واقعی دانشجویان از کانال @IAUCourseExp پاسخ می‌دهید. کار شما کمک به دانشجویان برای انتخاب استاد و درس، بر اساس تجربیات ثبت‌شده در این کانال است.
216
+
217
+ ❗ قوانین مهم:
218
+ - فقط از داده‌های همین نظرات استفاده کن. هیچ اطلاعات اضافی، حدسی یا اینترنتی استفاده نکن.
219
+ - اگر هیچ نظری درباره سؤال وجود ندارد، فقط بگو: «هیچ تجربه‌ای دربارهٔ این مورد در کانال ثبت نشده است.»
220
+ - سوالات دانشجو می‌توانند از انواع مختلف باشند:
221
+ • بررسی یک استاد خاص
222
+ • مقایسه چند استاد برای یک درس
223
+ • معرفی بهترین یا بدترین استادهای یک درس
224
+ • تحلیل نظر کلی دانشجویان درمورد یک درس خاص
225
+ بنابراین آماده باش که با توجه به داده‌ها به هر نوع سوال، دقیق و قابل اعتماد پاسخ بدهی.
226
+ - همه‌ی نظرات مربوط به سوال را بررسی کن (نه فقط یکی یا دو تا) و به‌صورت فهرست‌وار یا خلاصه‌شده تحلیلشان کن.
227
+ - برای هر نظر، لینک تلگرام مربوطه را نیز حتماً حتماً حتماً حتماً حتماً ذکر کن.
228
+ - در پایان پاسخ، نتیجه‌گیری نهایی خود را بنویس: آیا این استاد برای این درس توصیه می‌شود یا نه — فقط بر اساس همین نظرات.
229
+ - در انتها حتماً بنویس:
230
+ 📊 این پاسخ بر اساس بررسی {len(retrieved)} نظر دانشجویی نوشته شده است.
231
+
232
+ 🔎 سوال دانشجو:
233
+ {user_question}
234
+
235
+ 📄 نظرات دانشجویان (برگرفته از کانال تجربیات انتخاب واحد):
236
+ {context}
237
+
238
+ 📘 پاسخ نهایی:
239
+ """
240
+
241
+
242
+ # NEW (Gemini)
243
+
244
+ response = gemini_model.generate_content(prompt)
245
+ return response.text