dakheel commited on
Commit
a549e25
·
verified ·
1 Parent(s): a17e6d3

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +222 -56
app.py CHANGED
@@ -3788,13 +3788,15 @@ UI_CONFIG = {
3788
  "ONLINE_RERANK_MAX_CPU": 12,
3789
  # Smarter selection for the same 12 expensive BGE slots: accuracy up, latency flat.
3790
  "CHEAP_CANDIDATES_PER_BOOK": 3,
3791
- # Set-valued questions often need several sibling records from the same book.
3792
- # Keep the normal per-book recall small, but deepen only the strongest books.
3793
- # This expands the internal answer pool without increasing the visible UI limit
3794
- # or the expensive CPU BGE rerank budget.
3795
- "SET_COVERAGE_CANDIDATES_PER_BOOK": 10,
3796
- "SET_COVERAGE_DEEP_BOOKS": 6,
3797
- "SET_COVERAGE_MAX_CANDIDATES": 144,
 
 
3798
  "MAX_RERANKED_PER_BOOK": 2,
3799
  "DIVERSE_CANDIDATE_SELECTION": True,
3800
  "MIN_RETRIEVER_AGREEMENT": 2,
@@ -6278,74 +6280,170 @@ class ProfessionalEvidenceEngine:
6278
  per_book_limit=max(1,min(4,int(filters.get("evidence_count",1) or 1))); allowed_df=self.df.iloc[answer_allowed]
6279
 
6280
  # The visible evidence_count is a presentation limit, not an answer-completeness
6281
- # limit. Set-valued requests may distribute one complete answer across several
6282
- # sibling records in the same book, so deepen recall only for the strongest
6283
- # books while leaving normal questions at the original small per-book budget.
6284
  query_structure = analyze_direct_answer_intent_ui(query, lang)
6285
  generic_request_type = clean_ui(query_structure.get("generic_request_type", "")).casefold()
 
 
 
 
 
6286
  set_valued_request = generic_request_type in {"list", "components", "conditions", "pillars", "duties"}
6287
 
6288
  base_cheap_per_book=max(
6289
  per_book_limit,
6290
  int(UI_CONFIG.get("CHEAP_CANDIDATES_PER_BOOK",3)),
6291
  )
6292
- deep_cheap_per_book=max(
6293
- base_cheap_per_book,
6294
- int(UI_CONFIG.get("SET_COVERAGE_CANDIDATES_PER_BOOK",10)),
6295
- )
6296
- deep_book_limit=max(
6297
- 1,
6298
- int(UI_CONFIG.get("SET_COVERAGE_DEEP_BOOKS",6)),
6299
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6300
 
6301
  grouped_books=[]
6302
  for bid, group in allowed_df.groupby("book_id", sort=False):
6303
  local=np.asarray(group.index.tolist(), dtype=int)
6304
  if not len(local):
6305
  continue
6306
- best_score=float(np.max(pre[local]))
6307
- grouped_books.append((str(bid), local, best_score))
6308
-
6309
- deep_books=set()
6310
- if set_valued_request:
6311
- deep_books={
6312
- bid
6313
- for bid, _, _ in sorted(
6314
- grouped_books,
6315
- key=lambda row: row[2],
6316
- reverse=True,
6317
- )[:deep_book_limit]
6318
- }
6319
 
6320
  candidate_pool=set(exact_indices)
6321
  preliminary_book_best=[]
6322
  answer_take_by_book={}
6323
  for bid, local, _ in grouped_books:
6324
- take_limit=(
6325
- deep_cheap_per_book
6326
- if set_valued_request and bid in deep_books
6327
- else base_cheap_per_book
6328
- )
6329
- take=min(len(local),take_limit)
6330
- answer_take_by_book[bid]=take_limit
6331
  if take:
6332
  best=local[np.argsort(-pre[local])[:take]]
6333
  candidate_pool.update(map(int,best))
6334
  preliminary_book_best.append(int(best[0]))
6335
-
6336
  candidate_pool.update(global_set)
6337
- max_pool=max(
6338
- int(UI_CONFIG.get("MAX_CANDIDATES",96)),
6339
- len(preliminary_book_best),
6340
- )
6341
  if set_valued_request:
6342
- max_pool=max(
6343
- max_pool,
6344
- min(
6345
- int(UI_CONFIG.get("SET_COVERAGE_MAX_CANDIDATES",144)),
6346
- len(candidate_pool),
6347
- ),
6348
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6349
  ranked_pool=sorted(candidate_pool,key=lambda i:float(pre[i]),reverse=True)
6350
  graph_seeds=ranked_pool[:min(12, len(ranked_pool))]
6351
  graph_candidates, graph_reasons = self.kb_graph.expand(
@@ -6354,7 +6452,19 @@ class ProfessionalEvidenceEngine:
6354
  )
6355
  candidate_pool.update(graph_candidates)
6356
  ranked_pool=sorted(candidate_pool,key=lambda i:float(pre[i]),reverse=True)
6357
- candidates=np.asarray(sorted(set(preliminary_book_best)|set(exact_indices)|set(ranked_pool[:max_pool])|set(graph_candidates),key=lambda i:float(pre[i]),reverse=True),dtype=int)
 
 
 
 
 
 
 
 
 
 
 
 
6358
  rerank_cap=int(UI_CONFIG.get("ONLINE_RERANK_MAX_CPU",12)) if self._model_device=="cpu" else len(candidates)
6359
  arrays_by_name={"word":sw,"char":sc,"bm25":bm,"dense":dn,"fusion":pre}
6360
  promotion_order,promotion_reasons=self._diverse_promotion_order(candidates,exact_indices,arrays_by_name,rerank_cap)
@@ -6384,6 +6494,40 @@ class ProfessionalEvidenceEngine:
6384
  for idx, reasons in graph_reasons.items():
6385
  promotion_reasons.setdefault(int(idx), [])
6386
  promotion_reasons[int(idx)] = sorted(set(promotion_reasons[int(idx)]) | set(reasons))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6387
  rerank_indices=np.asarray(promotion_order,dtype=int); reranked_set=set(map(int,rerank_indices))
6388
  top_sets={name:set(map(int,self._top_indices(arr,allowed,min(per_retriever,24)))) for name,arr in arrays_by_name.items()}
6389
 
@@ -6487,14 +6631,33 @@ class ProfessionalEvidenceEngine:
6487
  if not items:
6488
  missing_books.append(bid); continue
6489
 
6490
- # Keep an internal pool for answer completeness. For ordinary questions
6491
- # this remains the original small budget. For set-valued questions only
6492
- # the strongest books receive deeper sibling-record coverage.
6493
  answer_take=int(answer_take_by_book.get(str(bid),base_cheap_per_book))
6494
- for answer_rank, item in enumerate(items[:answer_take], start=1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6495
  answer_item = dict(item)
6496
  answer_item["book_rank"] = answer_rank
6497
  answer_item["book_records_searched"] = int(self.book_record_counts.get(bid,0))
 
 
 
 
 
 
6498
  answer_pool.append(answer_item)
6499
 
6500
  chosen = items[:per_book_limit]
@@ -6529,7 +6692,10 @@ class ProfessionalEvidenceEngine:
6529
  "index_only_candidates_skipped":int(index_only_rows_skipped),"reranked_candidates":int(len(rerank_indices)),
6530
  "cheap_candidates_per_book":int(base_cheap_per_book),"answer_pool_count":len(answer_pool),"diverse_candidate_selection":True,"kb_native":self.kb_native,
6531
  "generic_request_type":generic_request_type,"set_valued_retrieval_expansion":bool(set_valued_request),
6532
- "set_coverage_deep_books":len(deep_books),"set_coverage_candidates_per_deep_book":int(deep_cheap_per_book if set_valued_request else base_cheap_per_book),
 
 
 
6533
  "top_score":float(max((x["score"] for x in all_items),default=0.0)),
6534
  "latency":time.perf_counter()-started,"array_latency":arrays_sec,"rerank_latency":rerank_sec,
6535
  "cache_hit":False,"minimum_score":requested,"retrieval_mode":mode,
 
3788
  "ONLINE_RERANK_MAX_CPU": 12,
3789
  # Smarter selection for the same 12 expensive BGE slots: accuracy up, latency flat.
3790
  "CHEAP_CANDIDATES_PER_BOOK": 3,
3791
+ # Set-valued questions may distribute one answer across nearby source records.
3792
+ # Do not deepen whole books blindly. Instead, reserve a few reranker slots for
3793
+ # semantically qualified sibling records near a source-local set seed.
3794
+ "SET_RECALL_MAX_SEEDS": 8,
3795
+ "SET_RECALL_SEED_PER_BOOK": 2,
3796
+ "SET_RECALL_PAGE_RADIUS": 4,
3797
+ "SET_RECALL_MAX_SIBLINGS": 12,
3798
+ "SET_RECALL_SIBLINGS_PER_BOOK": 8,
3799
+ "SET_RECALL_RERANK_RESERVE": 6,
3800
  "MAX_RERANKED_PER_BOOK": 2,
3801
  "DIVERSE_CANDIDATE_SELECTION": True,
3802
  "MIN_RETRIEVER_AGREEMENT": 2,
 
6280
  per_book_limit=max(1,min(4,int(filters.get("evidence_count",1) or 1))); allowed_df=self.df.iloc[answer_allowed]
6281
 
6282
  # The visible evidence_count is a presentation limit, not an answer-completeness
6283
+ # limit. Ordinary questions keep the original top-k-per-book behavior.
 
 
6284
  query_structure = analyze_direct_answer_intent_ui(query, lang)
6285
  generic_request_type = clean_ui(query_structure.get("generic_request_type", "")).casefold()
6286
+ generic_subject_terms = [
6287
+ clean_ui(value)
6288
+ for value in (query_structure.get("generic_subject_terms", []) or [])
6289
+ if clean_ui(value)
6290
+ ]
6291
  set_valued_request = generic_request_type in {"list", "components", "conditions", "pillars", "duties"}
6292
 
6293
  base_cheap_per_book=max(
6294
  per_book_limit,
6295
  int(UI_CONFIG.get("CHEAP_CANDIDATES_PER_BOOK",3)),
6296
  )
6297
+
6298
+ def _set_recall_page_number(row) -> int | None:
6299
+ match = re.search(r"\d+", str(getattr(row, "page_number", "") or ""))
6300
+ return int(match.group()) if match else None
6301
+
6302
+ def _set_recall_operator_cue(row) -> bool:
6303
+ if not set_valued_request:
6304
+ return False
6305
+ structural = " ".join(
6306
+ str(value or "")
6307
+ for value in (
6308
+ getattr(row, "ruling", "") if ar else getattr(row, "ruling_en", ""),
6309
+ getattr(row, "title", "") if ar else getattr(row, "title_en", ""),
6310
+ getattr(row, "question", "") if ar else getattr(row, "question_en", ""),
6311
+ getattr(row, "chapter", "") if ar else getattr(row, "chapter_en", ""),
6312
+ )
6313
+ )
6314
+ normalized = norm_ar_ui(structural) if ar else norm_en_ui(structural)
6315
+ if generic_request_type == "conditions":
6316
+ patterns = (
6317
+ (r"(?:^|\s)(?:شرط|شروط|اشتراط|يشترط|الاستطاعة|القدرة)(?:\s|$)",)
6318
+ if ar
6319
+ else (r"\b(?:condition|conditions|requirement|requirements|capacity|ability)\b",)
6320
+ )
6321
+ elif generic_request_type == "pillars":
6322
+ patterns = (
6323
+ (r"(?:^|\s)(?:ركن|اركان|أركان)(?:\s|$)",)
6324
+ if ar
6325
+ else (r"\b(?:pillar|pillars)\b",)
6326
+ )
6327
+ elif generic_request_type == "duties":
6328
+ patterns = (
6329
+ (r"(?:^|\s)(?:واجب|واجبات)(?:\s|$)",)
6330
+ if ar
6331
+ else (r"\b(?:duty|duties|required|obligatory)\b",)
6332
+ )
6333
+ else:
6334
+ patterns = (
6335
+ (r"(?:^|\s)(?:مكونات|اجزاء|أجزاء|عناصر|قائمة)(?:\s|$)",)
6336
+ if ar
6337
+ else (r"\b(?:components?|parts?|items?|list)\b",)
6338
+ )
6339
+ return any(re.search(pattern, normalized, re.I) for pattern in patterns)
6340
+
6341
+ def _set_recall_subject_bound(row) -> bool:
6342
+ if not generic_subject_terms:
6343
+ return True
6344
+ scope = " ".join(
6345
+ str(value or "")
6346
+ for value in (
6347
+ getattr(row, "question", "") if ar else getattr(row, "question_en", ""),
6348
+ getattr(row, "title", "") if ar else getattr(row, "title_en", ""),
6349
+ getattr(row, "chapter", "") if ar else getattr(row, "chapter_en", ""),
6350
+ getattr(row, "answer", "") if ar else getattr(row, "answer_en", ""),
6351
+ getattr(row, "answer_short", "") if ar else getattr(row, "answer_short_en", ""),
6352
+ getattr(row, "answer_detailed", "") if ar else getattr(row, "answer_detailed_en", ""),
6353
+ getattr(row, "answer_evidence", "") if ar else getattr(row, "answer_evidence_en", ""),
6354
+ )
6355
+ )
6356
+ probe = " ".join(generic_subject_terms)
6357
+ return topic_overlap(probe, scope, lang) >= 0.46
6358
 
6359
  grouped_books=[]
6360
  for bid, group in allowed_df.groupby("book_id", sort=False):
6361
  local=np.asarray(group.index.tolist(), dtype=int)
6362
  if not len(local):
6363
  continue
6364
+ grouped_books.append((str(bid), local, float(np.max(pre[local]))))
 
 
 
 
 
 
 
 
 
 
 
 
6365
 
6366
  candidate_pool=set(exact_indices)
6367
  preliminary_book_best=[]
6368
  answer_take_by_book={}
6369
  for bid, local, _ in grouped_books:
6370
+ take=min(len(local),base_cheap_per_book)
6371
+ answer_take_by_book[bid]=base_cheap_per_book
 
 
 
 
 
6372
  if take:
6373
  best=local[np.argsort(-pre[local])[:take]]
6374
  candidate_pool.update(map(int,best))
6375
  preliminary_book_best.append(int(best[0]))
 
6376
  candidate_pool.update(global_set)
6377
+
6378
+ set_seed_indices=[]
 
 
6379
  if set_valued_request:
6380
+ seed_cap=max(1,int(UI_CONFIG.get("SET_RECALL_MAX_SEEDS",8)))
6381
+ seed_per_book=max(1,int(UI_CONFIG.get("SET_RECALL_SEED_PER_BOOK",2)))
6382
+ seed_counts=Counter()
6383
+ qualified=[]
6384
+ for idx in answer_allowed:
6385
+ idx=int(idx)
6386
+ row=self.df.iloc[idx]
6387
+ if not _set_recall_operator_cue(row):
6388
+ continue
6389
+ if not _set_recall_subject_bound(row):
6390
+ continue
6391
+ qualified.append(idx)
6392
+ for idx in sorted(qualified,key=lambda i:float(pre[int(i)]),reverse=True):
6393
+ bid=str(self.df.iloc[int(idx)].book_id)
6394
+ if seed_counts[bid]>=seed_per_book:
6395
+ continue
6396
+ set_seed_indices.append(int(idx))
6397
+ seed_counts[bid]+=1
6398
+ if len(set_seed_indices)>=seed_cap:
6399
+ break
6400
+ candidate_pool.update(set_seed_indices)
6401
+
6402
+ set_sibling_indices=[]
6403
+ set_sibling_distance={}
6404
+ if set_valued_request and set_seed_indices:
6405
+ page_radius=max(0,int(UI_CONFIG.get("SET_RECALL_PAGE_RADIUS",4)))
6406
+ sibling_cap=max(1,int(UI_CONFIG.get("SET_RECALL_MAX_SIBLINGS",12)))
6407
+ sibling_per_book=max(1,int(UI_CONFIG.get("SET_RECALL_SIBLINGS_PER_BOOK",8)))
6408
+ seed_pages_by_book=defaultdict(list)
6409
+ for idx in set_seed_indices:
6410
+ row=self.df.iloc[int(idx)]
6411
+ page=_set_recall_page_number(row)
6412
+ if page is not None:
6413
+ seed_pages_by_book[str(row.book_id)].append(page)
6414
+
6415
+ sibling_candidates=[]
6416
+ for bid, local, _ in grouped_books:
6417
+ seed_pages=seed_pages_by_book.get(str(bid), [])
6418
+ if not seed_pages:
6419
+ continue
6420
+ for idx in local:
6421
+ idx=int(idx)
6422
+ if idx in set_seed_indices:
6423
+ continue
6424
+ row=self.df.iloc[idx]
6425
+ if not _set_recall_operator_cue(row):
6426
+ continue
6427
+ page=_set_recall_page_number(row)
6428
+ if page is None:
6429
+ continue
6430
+ distance=min(abs(page-seed_page) for seed_page in seed_pages)
6431
+ if distance>page_radius:
6432
+ continue
6433
+ sibling_candidates.append((distance,-float(pre[idx]),idx,bid))
6434
+
6435
+ per_book_siblings=Counter()
6436
+ for distance,_,idx,bid in sorted(sibling_candidates):
6437
+ if per_book_siblings[str(bid)]>=sibling_per_book:
6438
+ continue
6439
+ set_sibling_indices.append(int(idx))
6440
+ set_sibling_distance[int(idx)]=int(distance)
6441
+ per_book_siblings[str(bid)]+=1
6442
+ if len(set_sibling_indices)>=sibling_cap:
6443
+ break
6444
+ candidate_pool.update(set_sibling_indices)
6445
+
6446
+ max_pool=max(int(UI_CONFIG.get("MAX_CANDIDATES",96)),len(preliminary_book_best))
6447
  ranked_pool=sorted(candidate_pool,key=lambda i:float(pre[i]),reverse=True)
6448
  graph_seeds=ranked_pool[:min(12, len(ranked_pool))]
6449
  graph_candidates, graph_reasons = self.kb_graph.expand(
 
6452
  )
6453
  candidate_pool.update(graph_candidates)
6454
  ranked_pool=sorted(candidate_pool,key=lambda i:float(pre[i]),reverse=True)
6455
+ candidates=np.asarray(
6456
+ sorted(
6457
+ set(preliminary_book_best)
6458
+ | set(exact_indices)
6459
+ | set(ranked_pool[:max_pool])
6460
+ | set(graph_candidates)
6461
+ | set(set_seed_indices)
6462
+ | set(set_sibling_indices),
6463
+ key=lambda i:float(pre[i]),
6464
+ reverse=True,
6465
+ ),
6466
+ dtype=int,
6467
+ )
6468
  rerank_cap=int(UI_CONFIG.get("ONLINE_RERANK_MAX_CPU",12)) if self._model_device=="cpu" else len(candidates)
6469
  arrays_by_name={"word":sw,"char":sc,"bm25":bm,"dense":dn,"fusion":pre}
6470
  promotion_order,promotion_reasons=self._diverse_promotion_order(candidates,exact_indices,arrays_by_name,rerank_cap)
 
6494
  for idx, reasons in graph_reasons.items():
6495
  promotion_reasons.setdefault(int(idx), [])
6496
  promotion_reasons[int(idx)] = sorted(set(promotion_reasons[int(idx)]) | set(reasons))
6497
+
6498
+ set_recall_ranked=[]
6499
+ if set_valued_request and set_sibling_indices:
6500
+ reserve=min(
6501
+ max(0,int(UI_CONFIG.get("SET_RECALL_RERANK_RESERVE",6))),
6502
+ max(0,rerank_cap//2),
6503
+ )
6504
+ set_recall_ranked=sorted(
6505
+ set_sibling_indices,
6506
+ key=lambda i:(
6507
+ -int(set_sibling_distance.get(int(i),999)),
6508
+ float(pre[int(i)]),
6509
+ ),
6510
+ reverse=True,
6511
+ )[:reserve]
6512
+ protected=set(map(int,exact_indices))|set(map(int,graph_ranked))|set(map(int,set_recall_ranked))
6513
+ for idx in set_recall_ranked:
6514
+ idx=int(idx)
6515
+ if idx in promotion_order:
6516
+ promotion_reasons.setdefault(idx,[])
6517
+ promotion_reasons[idx]=sorted(set(promotion_reasons[idx])|{"set_local_sibling"})
6518
+ continue
6519
+ if len(promotion_order)>=rerank_cap:
6520
+ removable=next(
6521
+ (j for j in reversed(promotion_order) if int(j) not in protected),
6522
+ None,
6523
+ )
6524
+ if removable is None:
6525
+ continue
6526
+ promotion_order.remove(removable)
6527
+ promotion_order.append(idx)
6528
+ promotion_reasons.setdefault(idx,[])
6529
+ promotion_reasons[idx]=sorted(set(promotion_reasons[idx])|{"set_local_sibling"})
6530
+
6531
  rerank_indices=np.asarray(promotion_order,dtype=int); reranked_set=set(map(int,rerank_indices))
6532
  top_sets={name:set(map(int,self._top_indices(arr,allowed,min(per_retriever,24)))) for name,arr in arrays_by_name.items()}
6533
 
 
6631
  if not items:
6632
  missing_books.append(bid); continue
6633
 
6634
+ # Keep the ordinary top-k internal pool, then append only source-local
6635
+ # semantic siblings that were explicitly selected above.
 
6636
  answer_take=int(answer_take_by_book.get(str(bid),base_cheap_per_book))
6637
+ answer_items=list(items[:answer_take])
6638
+ if set_valued_request and set_sibling_indices:
6639
+ sibling_ids={
6640
+ str(self.df.iloc[int(i)].record_id)
6641
+ for i in set_sibling_indices
6642
+ if str(self.df.iloc[int(i)].book_id)==str(bid)
6643
+ }
6644
+ existing_ids={clean_ui(item.get("record_id","")) for item in answer_items}
6645
+ for item in items:
6646
+ rid=clean_ui(item.get("record_id",""))
6647
+ if rid in sibling_ids and rid not in existing_ids:
6648
+ answer_items.append(item)
6649
+ existing_ids.add(rid)
6650
+
6651
+ for answer_rank, item in enumerate(answer_items, start=1):
6652
  answer_item = dict(item)
6653
  answer_item["book_rank"] = answer_rank
6654
  answer_item["book_records_searched"] = int(self.book_record_counts.get(bid,0))
6655
+ answer_item["set_local_sibling"] = bool(
6656
+ clean_ui(item.get("record_id","")) in {
6657
+ str(self.df.iloc[int(i)].record_id)
6658
+ for i in set_sibling_indices
6659
+ }
6660
+ )
6661
  answer_pool.append(answer_item)
6662
 
6663
  chosen = items[:per_book_limit]
 
6692
  "index_only_candidates_skipped":int(index_only_rows_skipped),"reranked_candidates":int(len(rerank_indices)),
6693
  "cheap_candidates_per_book":int(base_cheap_per_book),"answer_pool_count":len(answer_pool),"diverse_candidate_selection":True,"kb_native":self.kb_native,
6694
  "generic_request_type":generic_request_type,"set_valued_retrieval_expansion":bool(set_valued_request),
6695
+ "set_recall_semantic_seeds":len(set_seed_indices),
6696
+ "set_recall_local_siblings":len(set_sibling_indices),
6697
+ "set_recall_reranked_siblings":len(set_recall_ranked),
6698
+ "set_recall_page_radius":int(UI_CONFIG.get("SET_RECALL_PAGE_RADIUS",4)),
6699
  "top_score":float(max((x["score"] for x in all_items),default=0.0)),
6700
  "latency":time.perf_counter()-started,"array_latency":arrays_sec,"rerank_latency":rerank_sec,
6701
  "cache_hit":False,"minimum_score":requested,"retrieval_mode":mode,