ecceembusra commited on
Commit
e8711e2
·
verified ·
1 Parent(s): 154a74f

Update rag_pipeline.py

Browse files
Files changed (1) hide show
  1. rag_pipeline.py +59 -93
rag_pipeline.py CHANGED
@@ -8,34 +8,31 @@ import numpy as np
8
 
9
  from providers import embed, generate, rerank, qa_extract
10
 
11
-
12
  # =========================
13
- # Depo yolları
14
  # =========================
15
  VSTORE_DIR = "vectorstore"
16
  FAISS_FILE = "index.faiss"
17
  META_JSONL = "meta.jsonl"
18
 
19
-
20
  # =========================
21
  # Hız / kalite ayarları
22
  # =========================
23
- TOP_K_DEFAULT = 4 # Kaç pasaj döndürelim?
24
- FETCH_K_DEFAULT = 16 # FAISS'ten kaç aday çekelim?
25
- HIGH_SCORE_THRES = 0.78 # erken karar eşiği (cosine)
26
- MARGIN_THRES = 0.06 # top1 - top2 farkı (erken karar)
 
27
 
28
- CTX_CHAR_LIMIT = 1400 # LLM'e verilecek maksimum bağlam karakteri
29
- QA_SCORE_THRES = 0.25 # ekstraktif QA güven eşiği (bilerek düşük)
30
- QA_PER_PASSAGES = 4 # kaç hit üzerinde tek tek QA denensin
31
 
32
- # Basit "title" ve "lexical" boost ağırlıkları
33
  W_TITLE_BOOST = 0.25
34
  W_LEXICAL = 0.15
35
 
36
-
37
  # =========================
38
- # Yardımcı regex'ler
39
  # =========================
40
  DATE_RX = re.compile(
41
  r"\b(\d{1,2}\s+(Ocak|Şubat|Mart|Nisan|Mayıs|Haziran|Temmuz|Ağustos|Eylül|Ekim|Kasım|Aralık)\s+\d{3,4}"
@@ -43,52 +40,17 @@ DATE_RX = re.compile(
43
  r"|\d{4})\b",
44
  flags=re.IGNORECASE
45
  )
46
-
47
  DEATH_KEYS = ["öldü", "vefat", "hayatını kaybet", "ölümü", "ölüm"]
48
  FOUND_KEYS = ["kuruldu", "kuruluş", "kurulmuştur", "kuruluşu", "kuruluş tarihi"]
49
 
50
- CAP_WORD_RX = re.compile(r"\b([A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+(?:\s+[A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+)*)\b")
51
- NAME_RX = re.compile(r"\b([A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+(?:\s+[A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+){0,3})\b")
52
-
53
-
54
- # =========================
55
- # Küçük yardımcılar
56
- # =========================
57
  def _split_sentences(txt: str) -> List[str]:
58
  parts = re.split(r"(?<=[.!?])\s+", (txt or "").strip())
59
  return [p.strip() for p in parts if p.strip()]
60
 
61
-
62
- def _keywords_from_query(q: str) -> List[str]:
63
- q = (q or "").strip()
64
- caps = [m.group(1) for m in CAP_WORD_RX.finditer(q)]
65
- nums = re.findall(r"\b\d{3,4}\b", q)
66
- base = [w.lower() for w in re.findall(r"[A-Za-zÇĞİIÖŞÜçğıiöşü]+", q) if len(w) > 2]
67
- # sıralı benzersiz
68
- return list(dict.fromkeys(caps + nums + base))
69
-
70
-
71
- def _lexical_overlap(q_tokens: List[str], text: str) -> float:
72
- toks = re.findall(r"[A-Za-zÇĞİIÖŞÜçğıiöşü]+", (text or "").lower())
73
- if not toks:
74
- return 0.0
75
- qset = set([t for t in q_tokens if len(t) > 2])
76
- tset = set([t for t in toks if len(t) > 2])
77
- inter = len(qset & tset)
78
- denom = len(qset) or 1
79
- return inter / denom
80
-
81
-
82
  def _extract_fact_sentence(query: str, hits: List[Dict]) -> Tuple[str, str]:
83
- """
84
- 'ne zaman öldü / ne zaman kuruldu' tipindeki sorularda
85
- tarih + anahtar kelime içeren ilk cümleyi yakala.
86
- Döndür: (cümle, kaynak_url) | ("", "")
87
- """
88
  q = (query or "").lower()
89
  if "ne zaman" not in q:
90
  return "", ""
91
-
92
  if any(k in q for k in DEATH_KEYS):
93
  keylist = DEATH_KEYS
94
  elif any(k in q for k in ["kuruldu", "kuruluş"]):
@@ -102,19 +64,17 @@ def _extract_fact_sentence(query: str, hits: List[Dict]) -> Tuple[str, str]:
102
  return s, h.get("source", "")
103
  return "", ""
104
 
 
 
 
 
105
 
106
  def _expand_named_span(answer: str, hits: List[Dict]) -> str:
107
- """
108
- QA'dan gelen 'Kemal' gibi kısa/eksik özel adı,
109
- bağlamdaki en uzun uygun özel adla genişletir.
110
- """
111
  ans = (answer or "").strip()
112
  if not ans or len(ans.split()) > 2:
113
  return ans
114
-
115
  ans_low = ans.lower()
116
 
117
- # Öncelikli alias'lar
118
  preferred_aliases = [
119
  "Mustafa Kemal Atatürk",
120
  "Sabiha Gökçen",
@@ -129,61 +89,79 @@ def _expand_named_span(answer: str, hits: List[Dict]) -> str:
129
  best = ans
130
  for h in hits:
131
  for sent in _split_sentences(h.get("text", "")):
132
- if ans_low not in sent.lower():
133
  continue
134
  for m in NAME_RX.finditer(sent):
135
  cand = m.group(1).strip()
136
- if ans_low in cand.lower():
137
- # Tamamen büyük/kurumsal kısa adları ele (biraz kabaca)
138
- if len(cand) >= len(best) and any(ch.islower() for ch in cand):
139
- if len(cand.split()) >= len(best.split()):
140
- best = cand
141
  return best
142
 
143
-
144
  # =========================
145
- # Vektör deposunu yükle
146
  # =========================
147
- def load_vectorstore() -> Tuple[faiss.Index, List[Dict]]:
148
- index_path = os.path.join(VSTORE_DIR, FAISS_FILE)
149
- meta_path = os.path.join(VSTORE_DIR, META_JSONL)
150
- if not (os.path.exists(index_path) and os.path.exists(meta_path)):
 
 
 
 
151
  raise FileNotFoundError(
152
  "Vektör deposu bulunamadı. Önce `python data_preparation.py` çalıştırın:\n"
153
- f"- {index_path}\n- {meta_path}"
154
  )
155
 
156
- index = faiss.read_index(index_path) # IndexFlatIP veya HNSW olabilir
157
- # HNSW ise efSearch ayarı
158
  try:
159
- index.hnsw.efSearch = 32 # güvenli varsayılan
160
  except Exception:
161
  pass
162
 
163
  records: List[Dict] = []
164
- with open(meta_path, "r", encoding="utf-8") as f:
165
  for line in f:
166
  obj = json.loads(line)
167
  records.append({
168
  "text": obj.get("text", ""),
169
  "metadata": obj.get("metadata", {}),
170
  })
171
-
172
  if not records:
173
  raise RuntimeError("meta.jsonl boş görünüyor.")
174
  return index, records
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
  # =========================
178
  # Retrieval + (koşullu) Rerank + title/lexical boost
179
  # =========================
180
  @lru_cache(maxsize=256)
181
  def _cached_query_vec(e5_query: str) -> np.ndarray:
182
- """E5 sorgu embedding'ini cache'ler."""
183
  v = embed([e5_query]).astype("float32")
184
  return v
185
 
186
-
187
  def search_chunks(
188
  query: str,
189
  index: faiss.Index,
@@ -208,11 +186,10 @@ def search_chunks(
208
  "source": md.get("source", ""),
209
  "score_vec": float(s),
210
  })
211
-
212
  if not pool:
213
  return []
214
 
215
- # --- title & lexical boost ---
216
  q_tokens = _keywords_from_query(q)
217
  q_tokens_lower = [t.lower() for t in q_tokens]
218
  for p in pool:
@@ -224,12 +201,11 @@ def search_chunks(
224
 
225
  pool_by_boost = sorted(pool, key=lambda x: x["score_boosted"], reverse=True)
226
 
227
- # --- erken karar: top1 güçlü ve fark yüksekse rerank yapma ---
228
  if len(pool_by_boost) >= 2:
229
  top1, top2 = pool_by_boost[0]["score_boosted"], pool_by_boost[1]["score_boosted"]
230
  else:
231
  top1, top2 = pool_by_boost[0]["score_boosted"], 0.0
232
-
233
  do_rerank = not (top1 >= HIGH_SCORE_THRES and (top1 - top2) >= MARGIN_THRES)
234
 
235
  if do_rerank:
@@ -240,7 +216,6 @@ def search_chunks(
240
 
241
  return pool_by_boost[:top_k]
242
 
243
-
244
  # =========================
245
  # LLM bağlamı ve kaynak listesi
246
  # =========================
@@ -253,7 +228,6 @@ def _format_sources(hits: List[Dict]) -> str:
253
  seen.add(u)
254
  return "\n".join(f"- {u}" for u in urls) if urls else "- (yok)"
255
 
256
-
257
  def _llm_context(hits: List[Dict], limit: int = CTX_CHAR_LIMIT) -> str:
258
  ctx, total = [], 0
259
  for i, h in enumerate(hits, 1):
@@ -264,9 +238,8 @@ def _llm_context(hits: List[Dict], limit: int = CTX_CHAR_LIMIT) -> str:
264
  total += len(block)
265
  return "\n\n---\n\n".join(ctx)
266
 
267
-
268
  # =========================
269
- # Nihai cevap (kural → QA → LLM → güvenli özet)
270
  # =========================
271
  def generate_answer(
272
  query: str,
@@ -278,12 +251,12 @@ def generate_answer(
278
  if not hits:
279
  return "Bilgi bulunamadı."
280
 
281
- # 0) Kural-tabanlı hızlı çıkarım (tarih/kuruluş soruları)
282
  rule_sent, rule_src = _extract_fact_sentence(query, hits)
283
  if rule_sent:
284
  return f"{rule_sent}\n\nKaynaklar:\n- {rule_src if rule_src else _format_sources(hits)}"
285
 
286
- # 1) Pasaj bazlı ekstraktif QA
287
  best = {"answer": None, "score": 0.0, "src": None}
288
  for h in hits[:QA_PER_PASSAGES]:
289
  try:
@@ -294,11 +267,8 @@ def generate_answer(
294
  score = float(qa.get("score", 0.0))
295
  ans = qa["answer"].strip()
296
 
297
- # Cevap tarih/özel ad içeriyorsa ekstra güven
298
  if re.search(r"\b(19\d{2}|20\d{2}|Atatürk|Gökçen|Kemal|Ankara|Fenerbahçe)\b", ans, flags=re.IGNORECASE):
299
  score += 0.30
300
-
301
- # Çok kısa veya eksik isimse → bağlamdan tam özel ada genişlet
302
  if len(ans.split()) <= 2:
303
  ans = _expand_named_span(ans, hits)
304
 
@@ -307,7 +277,6 @@ def generate_answer(
307
 
308
  if best["answer"] and best["score"] >= QA_SCORE_THRES:
309
  final = best["answer"].strip()
310
- # Soru "kimdir/kim" ise doğal cümleye dök
311
  if any(k in (query or "").lower() for k in ["kimdir", "kim"]):
312
  if not final.endswith("."):
313
  final += "."
@@ -315,7 +284,7 @@ def generate_answer(
315
  src_line = f"Kaynaklar:\n- {best['src']}" if best["src"] else "Kaynaklar:\n" + _format_sources(hits)
316
  return f"{final}\n\n{src_line}"
317
 
318
- # 2) QA düşük güven verdiyse → LLM (varsa)
319
  context = _llm_context(hits)
320
  prompt = (
321
  "Aşağıdaki BAĞLAM Wikipedia parçalarından alınmıştır.\n"
@@ -324,8 +293,6 @@ def generate_answer(
324
  f"Soru:\n{query}\n\nBağlam:\n{context}\n\nYanıtı 1-2 cümlede ver."
325
  )
326
  llm_ans = (generate(prompt) or "").strip()
327
-
328
- # 3) LLM yapılandırılmamışsa → güvenli özet fallback
329
  if (not llm_ans) or ("yapılandırılmadı" in llm_ans.lower()):
330
  text = hits[0].get("text", "")
331
  first = re.split(r"(?<=[.!?])\s+", text.strip())[:2]
@@ -335,12 +302,11 @@ def generate_answer(
335
  llm_ans += "\n\nKaynaklar:\n" + _format_sources(hits)
336
  return llm_ans
337
 
338
-
339
  # =========================
340
  # Hızlı test
341
  # =========================
342
  if __name__ == "__main__":
343
- idx, recs = load_vectorstore()
344
  for q in [
345
  "Atatürk ne zaman öldü?",
346
  "Türkiye'nin ilk cumhurbaşkanı kimdir?",
 
8
 
9
  from providers import embed, generate, rerank, qa_extract
10
 
 
11
  # =========================
12
+ # Varsayılan dizin/isimler (istemci override edebilir)
13
  # =========================
14
  VSTORE_DIR = "vectorstore"
15
  FAISS_FILE = "index.faiss"
16
  META_JSONL = "meta.jsonl"
17
 
 
18
  # =========================
19
  # Hız / kalite ayarları
20
  # =========================
21
+ TOP_K_DEFAULT = 4
22
+ FETCH_K_DEFAULT = 16
23
+ HNSW_EFSEARCH = 32
24
+ HIGH_SCORE_THRES = 0.78
25
+ MARGIN_THRES = 0.06
26
 
27
+ CTX_CHAR_LIMIT = 1400
28
+ QA_SCORE_THRES = 0.25
29
+ QA_PER_PASSAGES = 4
30
 
 
31
  W_TITLE_BOOST = 0.25
32
  W_LEXICAL = 0.15
33
 
 
34
  # =========================
35
+ # Kural-tabanlı çıkarım yardımcıları
36
  # =========================
37
  DATE_RX = re.compile(
38
  r"\b(\d{1,2}\s+(Ocak|Şubat|Mart|Nisan|Mayıs|Haziran|Temmuz|Ağustos|Eylül|Ekim|Kasım|Aralık)\s+\d{3,4}"
 
40
  r"|\d{4})\b",
41
  flags=re.IGNORECASE
42
  )
 
43
  DEATH_KEYS = ["öldü", "vefat", "hayatını kaybet", "ölümü", "ölüm"]
44
  FOUND_KEYS = ["kuruldu", "kuruluş", "kurulmuştur", "kuruluşu", "kuruluş tarihi"]
45
 
 
 
 
 
 
 
 
46
  def _split_sentences(txt: str) -> List[str]:
47
  parts = re.split(r"(?<=[.!?])\s+", (txt or "").strip())
48
  return [p.strip() for p in parts if p.strip()]
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def _extract_fact_sentence(query: str, hits: List[Dict]) -> Tuple[str, str]:
 
 
 
 
 
51
  q = (query or "").lower()
52
  if "ne zaman" not in q:
53
  return "", ""
 
54
  if any(k in q for k in DEATH_KEYS):
55
  keylist = DEATH_KEYS
56
  elif any(k in q for k in ["kuruldu", "kuruluş"]):
 
64
  return s, h.get("source", "")
65
  return "", ""
66
 
67
+ # =========================
68
+ # İsim normalizasyonu (kısa span → tam özel ad)
69
+ # =========================
70
+ NAME_RX = re.compile(r"\b([A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+(?:\s+[A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+){0,3})\b")
71
 
72
  def _expand_named_span(answer: str, hits: List[Dict]) -> str:
 
 
 
 
73
  ans = (answer or "").strip()
74
  if not ans or len(ans.split()) > 2:
75
  return ans
 
76
  ans_low = ans.lower()
77
 
 
78
  preferred_aliases = [
79
  "Mustafa Kemal Atatürk",
80
  "Sabiha Gökçen",
 
89
  best = ans
90
  for h in hits:
91
  for sent in _split_sentences(h.get("text", "")):
92
+ if ans_low not in sent.lower():
93
  continue
94
  for m in NAME_RX.finditer(sent):
95
  cand = m.group(1).strip()
96
+ if ans_low in cand.lower() and any(ch.islower() for ch in cand):
97
+ if len(cand.split()) >= len(best.split()):
98
+ best = cand
 
 
99
  return best
100
 
 
101
  # =========================
102
+ # Vektör deposunu yükle (PARAMETRELİ)
103
  # =========================
104
+ def load_vectorstore(vstore_dir: str = "vectorstore") -> Tuple[faiss.Index, List[Dict]]:
105
+ """Hugging Face Spaces gibi ortamlarda da kullanılabilsin diye
106
+ vektör deposu kök dizini parametre olarak alınır.
107
+ """
108
+ faiss_file = os.path.join(vstore_dir, "index.faiss")
109
+ meta_file = os.path.join(vstore_dir, "meta.jsonl")
110
+
111
+ if not (os.path.exists(faiss_file) and os.path.exists(meta_file)):
112
  raise FileNotFoundError(
113
  "Vektör deposu bulunamadı. Önce `python data_preparation.py` çalıştırın:\n"
114
+ f"- {faiss_file}\n- {meta_file}"
115
  )
116
 
117
+ index = faiss.read_index(faiss_file)
 
118
  try:
119
+ index.hnsw.efSearch = HNSW_EFSEARCH
120
  except Exception:
121
  pass
122
 
123
  records: List[Dict] = []
124
+ with open(meta_file, "r", encoding="utf-8") as f:
125
  for line in f:
126
  obj = json.loads(line)
127
  records.append({
128
  "text": obj.get("text", ""),
129
  "metadata": obj.get("metadata", {}),
130
  })
 
131
  if not records:
132
  raise RuntimeError("meta.jsonl boş görünüyor.")
133
  return index, records
134
 
135
+ # =========================
136
+ # Anahtar kelime çıkarımı + lexical puan
137
+ # =========================
138
+ _CAP_WORD = re.compile(r"\b([A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+(?:\s+[A-ZÇĞİIÖŞÜ][a-zçğıiöşü]+)*)\b")
139
+
140
+ def _keywords_from_query(q: str) -> List[str]:
141
+ q = (q or "").strip()
142
+ caps = [m.group(1) for m in _CAP_WORD.finditer(q)]
143
+ nums = re.findall(r"\b\d{3,4}\b", q)
144
+ base = [w.lower() for w in re.findall(r"[A-Za-zÇĞİIÖŞÜçğıiöşü]+", q) if len(w) > 2]
145
+ return list(dict.fromkeys(caps + nums + base))
146
+
147
+ def _lexical_overlap(q_tokens: List[str], text: str) -> float:
148
+ toks = re.findall(r"[A-Za-zÇĞİIÖŞÜçğıiöşü]+", (text or "").lower())
149
+ if not toks:
150
+ return 0.0
151
+ qset = set([t for t in q_tokens if len(t) > 2])
152
+ tset = set([t for t in toks if len(t) > 2])
153
+ inter = len(qset & tset)
154
+ denom = len(qset) or 1
155
+ return inter / denom
156
 
157
  # =========================
158
  # Retrieval + (koşullu) Rerank + title/lexical boost
159
  # =========================
160
  @lru_cache(maxsize=256)
161
  def _cached_query_vec(e5_query: str) -> np.ndarray:
 
162
  v = embed([e5_query]).astype("float32")
163
  return v
164
 
 
165
  def search_chunks(
166
  query: str,
167
  index: faiss.Index,
 
186
  "source": md.get("source", ""),
187
  "score_vec": float(s),
188
  })
 
189
  if not pool:
190
  return []
191
 
192
+ # title & lexical boost
193
  q_tokens = _keywords_from_query(q)
194
  q_tokens_lower = [t.lower() for t in q_tokens]
195
  for p in pool:
 
201
 
202
  pool_by_boost = sorted(pool, key=lambda x: x["score_boosted"], reverse=True)
203
 
204
+ # erken karar
205
  if len(pool_by_boost) >= 2:
206
  top1, top2 = pool_by_boost[0]["score_boosted"], pool_by_boost[1]["score_boosted"]
207
  else:
208
  top1, top2 = pool_by_boost[0]["score_boosted"], 0.0
 
209
  do_rerank = not (top1 >= HIGH_SCORE_THRES and (top1 - top2) >= MARGIN_THRES)
210
 
211
  if do_rerank:
 
216
 
217
  return pool_by_boost[:top_k]
218
 
 
219
  # =========================
220
  # LLM bağlamı ve kaynak listesi
221
  # =========================
 
228
  seen.add(u)
229
  return "\n".join(f"- {u}" for u in urls) if urls else "- (yok)"
230
 
 
231
  def _llm_context(hits: List[Dict], limit: int = CTX_CHAR_LIMIT) -> str:
232
  ctx, total = [], 0
233
  for i, h in enumerate(hits, 1):
 
238
  total += len(block)
239
  return "\n\n---\n\n".join(ctx)
240
 
 
241
  # =========================
242
+ # Nihai cevap
243
  # =========================
244
  def generate_answer(
245
  query: str,
 
251
  if not hits:
252
  return "Bilgi bulunamadı."
253
 
254
+ # kural-tabanlı ilk hamle
255
  rule_sent, rule_src = _extract_fact_sentence(query, hits)
256
  if rule_sent:
257
  return f"{rule_sent}\n\nKaynaklar:\n- {rule_src if rule_src else _format_sources(hits)}"
258
 
259
+ # ekstraktif QA
260
  best = {"answer": None, "score": 0.0, "src": None}
261
  for h in hits[:QA_PER_PASSAGES]:
262
  try:
 
267
  score = float(qa.get("score", 0.0))
268
  ans = qa["answer"].strip()
269
 
 
270
  if re.search(r"\b(19\d{2}|20\d{2}|Atatürk|Gökçen|Kemal|Ankara|Fenerbahçe)\b", ans, flags=re.IGNORECASE):
271
  score += 0.30
 
 
272
  if len(ans.split()) <= 2:
273
  ans = _expand_named_span(ans, hits)
274
 
 
277
 
278
  if best["answer"] and best["score"] >= QA_SCORE_THRES:
279
  final = best["answer"].strip()
 
280
  if any(k in (query or "").lower() for k in ["kimdir", "kim"]):
281
  if not final.endswith("."):
282
  final += "."
 
284
  src_line = f"Kaynaklar:\n- {best['src']}" if best["src"] else "Kaynaklar:\n" + _format_sources(hits)
285
  return f"{final}\n\n{src_line}"
286
 
287
+ # LLM fallback
288
  context = _llm_context(hits)
289
  prompt = (
290
  "Aşağıdaki BAĞLAM Wikipedia parçalarından alınmıştır.\n"
 
293
  f"Soru:\n{query}\n\nBağlam:\n{context}\n\nYanıtı 1-2 cümlede ver."
294
  )
295
  llm_ans = (generate(prompt) or "").strip()
 
 
296
  if (not llm_ans) or ("yapılandırılmadı" in llm_ans.lower()):
297
  text = hits[0].get("text", "")
298
  first = re.split(r"(?<=[.!?])\s+", text.strip())[:2]
 
302
  llm_ans += "\n\nKaynaklar:\n" + _format_sources(hits)
303
  return llm_ans
304
 
 
305
  # =========================
306
  # Hızlı test
307
  # =========================
308
  if __name__ == "__main__":
309
+ idx, recs = load_vectorstore(VSTORE_DIR)
310
  for q in [
311
  "Atatürk ne zaman öldü?",
312
  "Türkiye'nin ilk cumhurbaşkanı kimdir?",