srilakshu012456 commited on
Commit
a2a1c90
·
verified ·
1 Parent(s): 125282c

Update services/kb_creation.py

Browse files
Files changed (1) hide show
  1. services/kb_creation.py +50 -32
services/kb_creation.py CHANGED
@@ -71,26 +71,24 @@ def _split_by_sections(doc: Document) -> List[Tuple[str, List[str]]]:
71
  def _paragraphs_to_lines(paragraphs: List[str]) -> List[str]:
72
  """
73
  Split paragraphs into bullet-aware lines:
74
- - Preserve bullets and numbered list lines as separate atomic lines.
75
- - Split long paragraphs by '. ' into manageable lines.
76
  """
77
  lines: List[str] = []
78
  for p in (paragraphs or []):
79
  p = (p or "").strip()
80
  if not p:
81
  continue
82
- # If looks like a bullet/numbered item, keep as is
83
  if BULLET_RE.match(p):
84
  lines.append(p)
85
  continue
86
- # Otherwise split by sentence boundaries
87
  parts = [s.strip() for s in re.split(r"(?<=[.!?])\s+", p) if s.strip()]
88
  lines.extend(parts)
89
  return lines
90
 
91
- def _chunk_text_with_context(doc_title: str, section_title: str, paragraphs: List[str], max_words: int = 300) -> List[str]:
92
  """
93
- Smaller chunks for better recall; bullet-aware.
94
  """
95
  lines = _paragraphs_to_lines(paragraphs)
96
  chunks: List[str] = []
@@ -100,7 +98,6 @@ def _chunk_text_with_context(doc_title: str, section_title: str, paragraphs: Lis
100
  for ln in lines:
101
  w = ln.split()
102
  if current_len + len(w) > max_words or (BULLET_RE.match(ln) and current):
103
- # close current chunk
104
  chunk = " ".join(current).strip()
105
  if chunk:
106
  chunks.append(chunk)
@@ -220,7 +217,7 @@ def ingest_documents(folder_path: str) -> None:
220
  total_chunks = 0
221
 
222
  for s_idx, (section_title, paras) in enumerate(sections):
223
- chunks = _chunk_text_with_context(doc_title, section_title, paras, max_words=300)
224
  total_chunks += len(chunks)
225
 
226
  base_intent = _infer_intent_tag(section_title)
@@ -255,7 +252,6 @@ def ingest_documents(folder_path: str) -> None:
255
  except Exception as e2:
256
  print(f"[KB] ERROR: Upsert failed for {doc_id}: {e2}")
257
 
258
- # Build BM25 index entries
259
  tokens = _tokenize(chunk)
260
  tf: Dict[str, int] = {}
261
  for tkn in tokens:
@@ -395,7 +391,7 @@ def search_knowledge_base(query: str, top_k: int = 10) -> dict:
395
  "ids": ids,
396
  }
397
 
398
- # ---------------------------- Hybrid search (improved) ----------------------------
399
  ACTION_SYNONYMS = {
400
  "create": ["create", "creation", "add", "new", "generate"],
401
  "update": ["update", "modify", "change", "edit"],
@@ -481,8 +477,12 @@ def _intent_weight(meta: dict, user_intent: str) -> float:
481
  st = ((meta or {}).get("section", "") or "").lower()
482
  topics = (meta or {}).get("topic_tags", "") or ""
483
  topic_list = [t.strip() for t in topics.split(",") if t.strip()]
484
- if user_intent == "errors" and (any(k in st for k in ["escalation", "permissions", "access", "known issues", "common issues"]) or ("permissions" in topic_list)):
485
- return 0.95
 
 
 
 
486
  if user_intent == "steps" and any(k in st for k in ["inbound receiving", "receiving", "goods receipt", "grn"]):
487
  return 0.75
488
  return -0.2
@@ -512,30 +512,26 @@ def _phrase_boost_score(text: str, q_terms: List[str]) -> float:
512
  score = 0.0
513
  for bg in bigrams:
514
  if bg and bg in low:
515
- score += 0.35
516
  for tg in trigrams:
517
  if tg and tg in low:
518
- score += 0.60
519
- return min(score, 1.5)
520
 
521
  def _literal_query_match_boost(text: str, query_norm: str) -> float:
522
- """
523
- Extra boost if the exact normalized query substring (or key tokens) appear in the chunk.
524
- Helps errors like 'item mismatch' pick the right KB line.
525
- """
526
  t = (text or "").lower()
527
  q = (query_norm or "").lower()
528
  boost = 0.0
529
  if q and q in t:
530
- boost += 0.6
531
- # Also check key 2-word error tokens present in query (e.g., 'item mismatch')
532
  toks = [tok for tok in q.split() if len(tok) > 2]
533
  bigrams = _make_ngrams(toks, 2)
534
  for bg in bigrams:
535
  if bg in t:
536
- boost += 0.6
537
  break
538
- return min(boost, 1.2)
539
 
540
  def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6, beta: float = 0.4) -> dict:
541
  norm_query = _normalize_query(query)
@@ -544,7 +540,7 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
544
  actions = _extract_actions(query)
545
  user_modules = _extract_modules_from_query(query)
546
 
547
- sem_res = search_knowledge_base(norm_query, top_k=max(top_k, 30))
548
  sem_docs = sem_res.get("documents", [])
549
  sem_metas = sem_res.get("metadatas", [])
550
  sem_dists = sem_res.get("distances", [])
@@ -560,7 +556,7 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
560
 
561
  sem_sims = [dist_to_sim(d) for d in sem_dists]
562
 
563
- bm25_hits = bm25_search(norm_query, top_k=max(50, top_k * 5))
564
  bm25_max = max([s for _, s in bm25_hits], default=1.0)
565
  bm25_norm_pairs = [(idx, (score / bm25_max) if bm25_max > 0 else 0.0) for idx, score in bm25_hits]
566
  bm25_id_to_norm, bm25_id_to_text, bm25_id_to_meta = {}, {}, {}
@@ -573,12 +569,12 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
573
  union_ids = set(sem_ids) | set(bm25_id_to_norm.keys())
574
 
575
  gamma = 0.30 # meta overlap
576
- delta = 0.50 # intent boost
577
  epsilon = 0.30 # action weight
578
  zeta = 0.65 # module weight
579
- eta = 0.45 # phrase-level boost
580
  theta = 0.40 # heading alignment bonus
581
- iota = 0.40 # literal query match boost
582
 
583
  combined_records_ext: List[Tuple[str, float, float, str, Dict[str, Any], float, float, float, float, float, float, float]] = []
584
  for cid in union_ids:
@@ -631,6 +627,24 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
631
  (cid, final_score, (sem_dist if sem_dist is not None else 999.0), text, meta, m_overlap, intent_boost, act_wt, mod_wt, phrase_wt, heading_bonus, literal_wt)
632
  )
633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  from collections import defaultdict
635
  doc_groups: Dict[str, List[Tuple[str, float, float, str, Dict[str, Any], float, float, float, float, float, float, float]]] = defaultdict(list)
636
  for rec in combined_records_ext:
@@ -648,8 +662,11 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
648
  total_heading = sum(r[10] for r in recs)
649
  total_literal = sum(r[11] for r in recs)
650
  total_penalty = sum(min(0.0, r[6]) for r in recs) + sum(min(0.0, r[7]) for r in recs)
651
- esc_weight = 0.3 if any("escalation" in ((r[4] or {}).get("section", "")).lower() for r in recs) else 0.0
652
- perm_weight = 0.3 if any("permissions" in (((r[4] or {}).get("topic_tags") or [])) for r in recs) else 0.0
 
 
 
653
  return (
654
  total_score
655
  + 0.4 * total_overlap
@@ -658,9 +675,9 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
658
  + 0.8 * total_module
659
  + 0.6 * total_phrase
660
  + 0.6 * total_heading
661
- + 0.6 * total_literal
 
662
  + 0.3 * total_penalty
663
- + esc_weight + perm_weight
664
  )
665
 
666
  best_doc, best_doc_prior = None, -1.0
@@ -734,6 +751,7 @@ def get_best_errors_section_text(filename: str) -> str:
734
  or "access" in sec
735
  or "known issues" in sec
736
  or "common issues" in sec
 
737
  or ("permissions" in topic_list)
738
  ):
739
  t = (d.get("text") or "").strip()
 
71
  def _paragraphs_to_lines(paragraphs: List[str]) -> List[str]:
72
  """
73
  Split paragraphs into bullet-aware lines:
74
+ - Preserve bullets/numbered list lines as separate atomic lines.
75
+ - Split long paragraphs by sentence boundaries.
76
  """
77
  lines: List[str] = []
78
  for p in (paragraphs or []):
79
  p = (p or "").strip()
80
  if not p:
81
  continue
 
82
  if BULLET_RE.match(p):
83
  lines.append(p)
84
  continue
 
85
  parts = [s.strip() for s in re.split(r"(?<=[.!?])\s+", p) if s.strip()]
86
  lines.extend(parts)
87
  return lines
88
 
89
+ def _chunk_text_with_context(doc_title: str, section_title: str, paragraphs: List[str], max_words: int = 160) -> List[str]:
90
  """
91
+ Smaller chunks (≈160 words), bullet-aware for better recall of error bullets.
92
  """
93
  lines = _paragraphs_to_lines(paragraphs)
94
  chunks: List[str] = []
 
98
  for ln in lines:
99
  w = ln.split()
100
  if current_len + len(w) > max_words or (BULLET_RE.match(ln) and current):
 
101
  chunk = " ".join(current).strip()
102
  if chunk:
103
  chunks.append(chunk)
 
217
  total_chunks = 0
218
 
219
  for s_idx, (section_title, paras) in enumerate(sections):
220
+ chunks = _chunk_text_with_context(doc_title, section_title, paras, max_words=160)
221
  total_chunks += len(chunks)
222
 
223
  base_intent = _infer_intent_tag(section_title)
 
252
  except Exception as e2:
253
  print(f"[KB] ERROR: Upsert failed for {doc_id}: {e2}")
254
 
 
255
  tokens = _tokenize(chunk)
256
  tf: Dict[str, int] = {}
257
  for tkn in tokens:
 
391
  "ids": ids,
392
  }
393
 
394
+ # ---------------------------- Hybrid search (improved + exact-match rerank) ----------------------------
395
  ACTION_SYNONYMS = {
396
  "create": ["create", "creation", "add", "new", "generate"],
397
  "update": ["update", "modify", "change", "edit"],
 
477
  st = ((meta or {}).get("section", "") or "").lower()
478
  topics = (meta or {}).get("topic_tags", "") or ""
479
  topic_list = [t.strip() for t in topics.split(",") if t.strip()]
480
+ # Prefer errors sections strongly
481
+ if user_intent == "errors" and (
482
+ any(k in st for k in ["common errors", "known issues", "common issues", "errors", "escalation", "permissions", "access"])
483
+ or ("permissions" in topic_list)
484
+ ):
485
+ return 1.10 # stronger than before
486
  if user_intent == "steps" and any(k in st for k in ["inbound receiving", "receiving", "goods receipt", "grn"]):
487
  return 0.75
488
  return -0.2
 
512
  score = 0.0
513
  for bg in bigrams:
514
  if bg and bg in low:
515
+ score += 0.40
516
  for tg in trigrams:
517
  if tg and tg in low:
518
+ score += 0.70
519
+ return min(score, 2.0)
520
 
521
  def _literal_query_match_boost(text: str, query_norm: str) -> float:
522
+ """Extra boost if exact normalized query substring or bigrams appear."""
 
 
 
523
  t = (text or "").lower()
524
  q = (query_norm or "").lower()
525
  boost = 0.0
526
  if q and q in t:
527
+ boost += 0.8
 
528
  toks = [tok for tok in q.split() if len(tok) > 2]
529
  bigrams = _make_ngrams(toks, 2)
530
  for bg in bigrams:
531
  if bg in t:
532
+ boost += 0.8
533
  break
534
+ return min(boost, 1.6)
535
 
536
  def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6, beta: float = 0.4) -> dict:
537
  norm_query = _normalize_query(query)
 
540
  actions = _extract_actions(query)
541
  user_modules = _extract_modules_from_query(query)
542
 
543
+ sem_res = search_knowledge_base(norm_query, top_k=max(top_k, 40))
544
  sem_docs = sem_res.get("documents", [])
545
  sem_metas = sem_res.get("metadatas", [])
546
  sem_dists = sem_res.get("distances", [])
 
556
 
557
  sem_sims = [dist_to_sim(d) for d in sem_dists]
558
 
559
+ bm25_hits = bm25_search(norm_query, top_k=max(80, top_k * 6))
560
  bm25_max = max([s for _, s in bm25_hits], default=1.0)
561
  bm25_norm_pairs = [(idx, (score / bm25_max) if bm25_max > 0 else 0.0) for idx, score in bm25_hits]
562
  bm25_id_to_norm, bm25_id_to_text, bm25_id_to_meta = {}, {}, {}
 
569
  union_ids = set(sem_ids) | set(bm25_id_to_norm.keys())
570
 
571
  gamma = 0.30 # meta overlap
572
+ delta = 0.55 # intent boost (stronger)
573
  epsilon = 0.30 # action weight
574
  zeta = 0.65 # module weight
575
+ eta = 0.50 # phrase-level boost (stronger)
576
  theta = 0.40 # heading alignment bonus
577
+ iota = 0.60 # literal query match boost (stronger)
578
 
579
  combined_records_ext: List[Tuple[str, float, float, str, Dict[str, Any], float, float, float, float, float, float, float]] = []
580
  for cid in union_ids:
 
627
  (cid, final_score, (sem_dist if sem_dist is not None else 999.0), text, meta, m_overlap, intent_boost, act_wt, mod_wt, phrase_wt, heading_bonus, literal_wt)
628
  )
629
 
630
+ # ---- Exact-match rerank for errors ----
631
+ if user_intent == "errors":
632
+ exact_hits = []
633
+ for rec in combined_records_ext:
634
+ text_lower = (rec[3] or "").lower()
635
+ if any(phrase in text_lower for phrase in [
636
+ norm_query, # whole normalized query
637
+ # common 2-gram patterns extracted from the query
638
+ *(_make_ngrams([tok for tok in norm_query.split() if len(tok) > 2], 2))
639
+ ]):
640
+ exact_hits.append(rec)
641
+ if exact_hits:
642
+ # Move exact hits to front and keep order by current final_score
643
+ rest = [r for r in combined_records_ext if r not in exact_hits]
644
+ exact_hits.sort(key=lambda x: x[1], reverse=True)
645
+ rest.sort(key=lambda x: x[1], reverse=True)
646
+ combined_records_ext = exact_hits + rest
647
+
648
  from collections import defaultdict
649
  doc_groups: Dict[str, List[Tuple[str, float, float, str, Dict[str, Any], float, float, float, float, float, float, float]]] = defaultdict(list)
650
  for rec in combined_records_ext:
 
662
  total_heading = sum(r[10] for r in recs)
663
  total_literal = sum(r[11] for r in recs)
664
  total_penalty = sum(min(0.0, r[6]) for r in recs) + sum(min(0.0, r[7]) for r in recs)
665
+ # Errors doc prior: if many chunks are from an errors/known issues section, add a bonus
666
+ errors_section_bonus = 0.0
667
+ if any("error" in ((r[4] or {}).get("section", "")).lower() or "known issues" in ((r[4] or {}).get("section", "")).lower()
668
+ or "common issues" in ((r[4] or {}).get("section", "")).lower() for r in recs):
669
+ errors_section_bonus = 0.5
670
  return (
671
  total_score
672
  + 0.4 * total_overlap
 
675
  + 0.8 * total_module
676
  + 0.6 * total_phrase
677
  + 0.6 * total_heading
678
+ + 0.7 * total_literal
679
+ + errors_section_bonus
680
  + 0.3 * total_penalty
 
681
  )
682
 
683
  best_doc, best_doc_prior = None, -1.0
 
751
  or "access" in sec
752
  or "known issues" in sec
753
  or "common issues" in sec
754
+ or "errors" in sec
755
  or ("permissions" in topic_list)
756
  ):
757
  t = (d.get("text") or "").strip()