srilakshu012456 commited on
Commit
f57f7d9
·
verified ·
1 Parent(s): 755763e

Update services/kb_creation.py

Browse files
Files changed (1) hide show
  1. services/kb_creation.py +36 -39
services/kb_creation.py CHANGED
@@ -8,15 +8,15 @@ from docx import Document
8
  from sentence_transformers import SentenceTransformer
9
  import chromadb
10
 
11
- # ---------------------------- ChromaDB setup ----------------------------
12
  CHROMA_PATH = os.path.join(os.getcwd(), "chroma_db")
13
  client = chromadb.PersistentClient(path=CHROMA_PATH)
14
  collection = client.get_or_create_collection(name="knowledge_base")
15
 
16
- # ---------------------------- Embedding model ----------------------------
17
  model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
18
 
19
- # ---------------------------- BM25 (lightweight) ----------------------------
20
  BM25_INDEX_FILE = os.path.join(CHROMA_PATH, "bm25_index.pkl")
21
  bm25_docs: List[Dict[str, Any]] = []
22
  bm25_inverted: Dict[str, List[int]] = {}
@@ -26,7 +26,7 @@ bm25_ready: bool = False
26
  BM25_K1 = 1.5
27
  BM25_B = 0.75
28
 
29
- # ---------------------------- Utilities ----------------------------
30
  def _tokenize(text: str) -> List[str]:
31
  if not text:
32
  return []
@@ -42,8 +42,8 @@ def _normalize_query(q: str) -> str:
42
  def _tokenize_meta_value(val: Optional[str]) -> List[str]:
43
  return _tokenize(val or "")
44
 
45
- # ---------------------------- DOCX parsing & chunking ----------------------------
46
- BULLET_RE = re.compile(r"^\s*(?:[\-\*\u2022]|\d+[.)])\s+", re.IGNORECASE)
47
 
48
  def _split_by_sections(doc: Document) -> List[Tuple[str, List[str]]]:
49
  sections: List[Tuple[str, List[str]]] = []
@@ -87,14 +87,11 @@ def _paragraphs_to_lines(paragraphs: List[str]) -> List[str]:
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] = []
95
  current: List[str] = []
96
  current_len = 0
97
-
98
  for ln in lines:
99
  w = ln.split()
100
  if current_len + len(w) > max_words or (BULLET_RE.match(ln) and current):
@@ -106,22 +103,19 @@ def _chunk_text_with_context(doc_title: str, section_title: str, paragraphs: Lis
106
  else:
107
  current.append(ln)
108
  current_len += len(w)
109
-
110
  if current:
111
  chunk = " ".join(current).strip()
112
  if chunk:
113
  chunks.append(chunk)
114
-
115
  if not chunks:
116
  body = " ".join(lines).strip()
117
  if body:
118
  chunks = [body]
119
  return chunks
120
 
121
- # ---------------------------- Intent & Module tagging ----------------------------
122
  SECTION_STEPS_HINTS = ["process steps", "procedure", "how to", "workflow", "instructions", "steps"]
123
  SECTION_ERRORS_HINTS = ["common errors", "resolution", "troubleshooting", "known issues", "common issues", "escalation", "escalation path", "permissions", "access"]
124
-
125
  PERMISSION_TERMS = [
126
  "permission", "permissions", "access", "access right", "authorization", "authorisation",
127
  "role", "role access", "role mapping", "security", "security profile", "privilege", "insufficient",
@@ -129,7 +123,6 @@ PERMISSION_TERMS = [
129
  ]
130
  ERROR_TERMS = ["error", "issue", "fail", "failure", "not working", "cannot", "can't", "mismatch", "locked", "wrong", "denied"]
131
  STEP_VERBS = ["navigate", "select", "scan", "verify", "confirm", "print", "move", "complete", "click", "open", "choose", "enter", "update", "save", "delete", "create", "attach", "assign"]
132
-
133
  MODULE_VOCAB = {
134
  "receiving": [
135
  "receive", "receiving", "inbound receiving", "inbound", "goods receipt", "grn",
@@ -196,7 +189,7 @@ def _derive_module_tags(text: str, filename: str, section_title: str) -> List[st
196
  found = ["appointments"]
197
  return list(sorted(set(found)))
198
 
199
- # ---------------------------- Ingestion ----------------------------
200
  def ingest_documents(folder_path: str) -> None:
201
  print(f"[KB] Checking folder: {folder_path}")
202
  files = [f for f in os.listdir(folder_path) if f.lower().endswith('.docx')]
@@ -219,7 +212,6 @@ def ingest_documents(folder_path: str) -> None:
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)
224
 
225
  for c_idx, chunk in enumerate(chunks):
@@ -243,6 +235,7 @@ def ingest_documents(folder_path: str) -> None:
243
  "topic_tags": ", ".join(topic_tags) if topic_tags else "",
244
  "module_tags": ", ".join(module_tags) if module_tags else "",
245
  }
 
246
  try:
247
  collection.add(ids=[doc_id], embeddings=[embedding], documents=[chunk], metadatas=[meta])
248
  except Exception:
@@ -266,6 +259,7 @@ def ingest_documents(folder_path: str) -> None:
266
  "length": len(tokens),
267
  "meta": meta,
268
  })
 
269
  seen = set()
270
  for term in tf.keys():
271
  bm25_inverted.setdefault(term, []).append(idx)
@@ -279,7 +273,6 @@ def ingest_documents(folder_path: str) -> None:
279
  if N > 0:
280
  bm25_avgdl = sum(d["length"] for d in bm25_docs) / float(N)
281
  bm25_ready = True
282
-
283
  payload = {
284
  "bm25_docs": bm25_docs,
285
  "bm25_inverted": bm25_inverted,
@@ -294,7 +287,7 @@ def ingest_documents(folder_path: str) -> None:
294
  print(f"[KB] BM25 index saved: {BM25_INDEX_FILE}")
295
  print(f"[KB] Documents ingested. Total entries in Chroma: {collection.count()}")
296
 
297
- # ---------------------------- BM25 load ----------------------------
298
  def _load_bm25_index() -> None:
299
  global bm25_docs, bm25_inverted, bm25_df, bm25_avgdl, bm25_ready
300
  if not os.path.exists(BM25_INDEX_FILE):
@@ -314,7 +307,7 @@ def _load_bm25_index() -> None:
314
 
315
  _load_bm25_index()
316
 
317
- # ---------------------------- BM25 search ----------------------------
318
  def _bm25_score_for_doc(query_terms: List[str], doc_idx: int) -> float:
319
  if not bm25_ready or doc_idx < 0 or doc_idx >= len(bm25_docs):
320
  return 0.0
@@ -346,12 +339,14 @@ def bm25_search(query: str, top_k: int = 50) -> List[Tuple[int, float]]:
346
  q_terms = _tokenize(norm)
347
  if not q_terms:
348
  return []
 
349
  candidates = set()
350
  for t in q_terms:
351
  for idx in bm25_inverted.get(t, []):
352
  candidates.add(idx)
353
  if not candidates:
354
  candidates = set(range(len(bm25_docs)))
 
355
  scored = []
356
  for idx in candidates:
357
  s = _bm25_score_for_doc(q_terms, idx)
@@ -360,7 +355,7 @@ def bm25_search(query: str, top_k: int = 50) -> List[Tuple[int, float]]:
360
  scored.sort(key=lambda x: x[1], reverse=True)
361
  return scored[:top_k]
362
 
363
- # ---------------------------- Semantic-only ----------------------------
364
  def search_knowledge_base(query: str, top_k: int = 10) -> dict:
365
  query_embedding = model.encode(query).tolist()
366
  res = collection.query(
@@ -391,13 +386,14 @@ def search_knowledge_base(query: str, top_k: int = 10) -> dict:
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"],
398
  "delete": ["delete", "remove"],
399
  "navigate": ["navigate", "go to", "open"],
400
  }
 
401
  ERROR_INTENT_TERMS = [
402
  "error", "issue", "fail", "not working", "resolution", "fix",
403
  "permission", "permissions", "access", "no access", "authorization", "authorisation",
@@ -448,7 +444,8 @@ def _action_weight(text: str, actions: List[str]) -> float:
448
  for syn in ACTION_SYNONYMS.get(act, [act]):
449
  if syn in t:
450
  score += 1.0
451
- conflicts = {"create": ["delete"], "delete": ["create"], "update": ["delete"], "navigate": []}
 
452
  for act in actions:
453
  for bad in conflicts.get(act, []):
454
  for syn in ACTION_SYNONYMS.get(bad, [bad]):
@@ -482,7 +479,7 @@ def _intent_weight(meta: dict, user_intent: str) -> float:
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
@@ -568,13 +565,13 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
568
 
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:
@@ -633,8 +630,7 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
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)
@@ -662,11 +658,13 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
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
@@ -696,7 +694,6 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
696
 
697
  reordered = best_recs + other_recs
698
  top = reordered[:top_k]
699
-
700
  documents = [t[3] for t in top]
701
  metadatas = [t[4] for t in top]
702
  distances = [t[2] for t in top]
@@ -715,7 +712,7 @@ def hybrid_search_knowledge_base(query: str, top_k: int = 10, alpha: float = 0.6
715
  "actions": actions,
716
  }
717
 
718
- # ---------------------------- Section fetch helpers ----------------------------
719
  def get_section_text(filename: str, section: str) -> str:
720
  texts: List[str] = []
721
  for d in bm25_docs:
@@ -759,7 +756,7 @@ def get_best_errors_section_text(filename: str) -> str:
759
  texts.append(t)
760
  return "\n\n".join(texts).strip()
761
 
762
- # ---------------------------- Admin helpers ----------------------------
763
  def get_kb_runtime_info() -> Dict[str, Any]:
764
  return {
765
  "chroma_path": CHROMA_PATH,
 
8
  from sentence_transformers import SentenceTransformer
9
  import chromadb
10
 
11
+ # --------------------------- ChromaDB setup ---------------------------
12
  CHROMA_PATH = os.path.join(os.getcwd(), "chroma_db")
13
  client = chromadb.PersistentClient(path=CHROMA_PATH)
14
  collection = client.get_or_create_collection(name="knowledge_base")
15
 
16
+ # --------------------------- Embedding model ---------------------------
17
  model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
18
 
19
+ # --------------------------- BM25 (lightweight) ---------------------------
20
  BM25_INDEX_FILE = os.path.join(CHROMA_PATH, "bm25_index.pkl")
21
  bm25_docs: List[Dict[str, Any]] = []
22
  bm25_inverted: Dict[str, List[int]] = {}
 
26
  BM25_K1 = 1.5
27
  BM25_B = 0.75
28
 
29
+ # --------------------------- Utilities ---------------------------
30
  def _tokenize(text: str) -> List[str]:
31
  if not text:
32
  return []
 
42
  def _tokenize_meta_value(val: Optional[str]) -> List[str]:
43
  return _tokenize(val or "")
44
 
45
+ # --------------------------- DOCX parsing & chunking ---------------------------
46
+ BULLET_RE = re.compile(r"^\s*(?:[\-\*\u2022]|\d+[.\)]\s+)", re.IGNORECASE)
47
 
48
  def _split_by_sections(doc: Document) -> List[Tuple[str, List[str]]]:
49
  sections: List[Tuple[str, List[str]]] = []
 
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
+ """Smaller chunks (~160 words), bullet-aware for better recall."""
 
 
91
  lines = _paragraphs_to_lines(paragraphs)
92
  chunks: List[str] = []
93
  current: List[str] = []
94
  current_len = 0
 
95
  for ln in lines:
96
  w = ln.split()
97
  if current_len + len(w) > max_words or (BULLET_RE.match(ln) and current):
 
103
  else:
104
  current.append(ln)
105
  current_len += len(w)
 
106
  if current:
107
  chunk = " ".join(current).strip()
108
  if chunk:
109
  chunks.append(chunk)
 
110
  if not chunks:
111
  body = " ".join(lines).strip()
112
  if body:
113
  chunks = [body]
114
  return chunks
115
 
116
+ # --------------------------- Intent & Module tagging ---------------------------
117
  SECTION_STEPS_HINTS = ["process steps", "procedure", "how to", "workflow", "instructions", "steps"]
118
  SECTION_ERRORS_HINTS = ["common errors", "resolution", "troubleshooting", "known issues", "common issues", "escalation", "escalation path", "permissions", "access"]
 
119
  PERMISSION_TERMS = [
120
  "permission", "permissions", "access", "access right", "authorization", "authorisation",
121
  "role", "role access", "role mapping", "security", "security profile", "privilege", "insufficient",
 
123
  ]
124
  ERROR_TERMS = ["error", "issue", "fail", "failure", "not working", "cannot", "can't", "mismatch", "locked", "wrong", "denied"]
125
  STEP_VERBS = ["navigate", "select", "scan", "verify", "confirm", "print", "move", "complete", "click", "open", "choose", "enter", "update", "save", "delete", "create", "attach", "assign"]
 
126
  MODULE_VOCAB = {
127
  "receiving": [
128
  "receive", "receiving", "inbound receiving", "inbound", "goods receipt", "grn",
 
189
  found = ["appointments"]
190
  return list(sorted(set(found)))
191
 
192
+ # --------------------------- Ingestion ---------------------------
193
  def ingest_documents(folder_path: str) -> None:
194
  print(f"[KB] Checking folder: {folder_path}")
195
  files = [f for f in os.listdir(folder_path) if f.lower().endswith('.docx')]
 
212
  for s_idx, (section_title, paras) in enumerate(sections):
213
  chunks = _chunk_text_with_context(doc_title, section_title, paras, max_words=160)
214
  total_chunks += len(chunks)
 
215
  base_intent = _infer_intent_tag(section_title)
216
 
217
  for c_idx, chunk in enumerate(chunks):
 
235
  "topic_tags": ", ".join(topic_tags) if topic_tags else "",
236
  "module_tags": ", ".join(module_tags) if module_tags else "",
237
  }
238
+
239
  try:
240
  collection.add(ids=[doc_id], embeddings=[embedding], documents=[chunk], metadatas=[meta])
241
  except Exception:
 
259
  "length": len(tokens),
260
  "meta": meta,
261
  })
262
+
263
  seen = set()
264
  for term in tf.keys():
265
  bm25_inverted.setdefault(term, []).append(idx)
 
273
  if N > 0:
274
  bm25_avgdl = sum(d["length"] for d in bm25_docs) / float(N)
275
  bm25_ready = True
 
276
  payload = {
277
  "bm25_docs": bm25_docs,
278
  "bm25_inverted": bm25_inverted,
 
287
  print(f"[KB] BM25 index saved: {BM25_INDEX_FILE}")
288
  print(f"[KB] Documents ingested. Total entries in Chroma: {collection.count()}")
289
 
290
+ # --------------------------- BM25 load ---------------------------
291
  def _load_bm25_index() -> None:
292
  global bm25_docs, bm25_inverted, bm25_df, bm25_avgdl, bm25_ready
293
  if not os.path.exists(BM25_INDEX_FILE):
 
307
 
308
  _load_bm25_index()
309
 
310
+ # --------------------------- BM25 search ---------------------------
311
  def _bm25_score_for_doc(query_terms: List[str], doc_idx: int) -> float:
312
  if not bm25_ready or doc_idx < 0 or doc_idx >= len(bm25_docs):
313
  return 0.0
 
339
  q_terms = _tokenize(norm)
340
  if not q_terms:
341
  return []
342
+
343
  candidates = set()
344
  for t in q_terms:
345
  for idx in bm25_inverted.get(t, []):
346
  candidates.add(idx)
347
  if not candidates:
348
  candidates = set(range(len(bm25_docs)))
349
+
350
  scored = []
351
  for idx in candidates:
352
  s = _bm25_score_for_doc(q_terms, idx)
 
355
  scored.sort(key=lambda x: x[1], reverse=True)
356
  return scored[:top_k]
357
 
358
+ # --------------------------- Semantic-only ---------------------------
359
  def search_knowledge_base(query: str, top_k: int = 10) -> dict:
360
  query_embedding = model.encode(query).tolist()
361
  res = collection.query(
 
386
  "ids": ids,
387
  }
388
 
389
+ # --------------------------- Hybrid search (improved + exact-match rerank) ---------------------------
390
  ACTION_SYNONYMS = {
391
+ "create": ["create", "creation", "add", "new", "generate", "book", "schedule", "set up"],
392
+ "update": ["update", "modify", "change", "edit", "reschedule", "adjust", "move"],
393
  "delete": ["delete", "remove"],
394
  "navigate": ["navigate", "go to", "open"],
395
  }
396
+
397
  ERROR_INTENT_TERMS = [
398
  "error", "issue", "fail", "not working", "resolution", "fix",
399
  "permission", "permissions", "access", "no access", "authorization", "authorisation",
 
444
  for syn in ACTION_SYNONYMS.get(act, [act]):
445
  if syn in t:
446
  score += 1.0
447
+ # conflict matrix: penalize mismatched operations (e.g., user wants update but chunk talks about create)
448
+ conflicts = {"create": ["delete"], "delete": ["create"], "update": ["create", "delete"], "navigate": []}
449
  for act in actions:
450
  for bad in conflicts.get(act, []):
451
  for syn in ACTION_SYNONYMS.get(bad, [bad]):
 
479
  any(k in st for k in ["common errors", "known issues", "common issues", "errors", "escalation", "permissions", "access"])
480
  or ("permissions" in topic_list)
481
  ):
482
+ return 1.10
483
  if user_intent == "steps" and any(k in st for k in ["inbound receiving", "receiving", "goods receipt", "grn"]):
484
  return 0.75
485
  return -0.2
 
565
 
566
  union_ids = set(sem_ids) | set(bm25_id_to_norm.keys())
567
 
568
+ gamma = 0.30 # meta overlap
569
+ delta = 0.55 # intent boost (stronger)
570
+ epsilon = 0.30 # action weight
571
+ zeta = 0.65 # module weight
572
+ eta = 0.50 # phrase-level boost (stronger)
573
+ theta = 0.40 # heading alignment bonus
574
+ iota = 0.60 # literal query match boost (stronger)
575
 
576
  combined_records_ext: List[Tuple[str, float, float, str, Dict[str, Any], float, float, float, float, float, float, float]] = []
577
  for cid in union_ids:
 
630
  for rec in combined_records_ext:
631
  text_lower = (rec[3] or "").lower()
632
  if any(phrase in text_lower for phrase in [
633
+ norm_query,
 
634
  *(_make_ngrams([tok for tok in norm_query.split() if len(tok) > 2], 2))
635
  ]):
636
  exact_hits.append(rec)
 
658
  total_heading = sum(r[10] for r in recs)
659
  total_literal = sum(r[11] for r in recs)
660
  total_penalty = sum(min(0.0, r[6]) for r in recs) + sum(min(0.0, r[7]) for r in recs)
661
+
662
+ # Errors doc prior: bonus for errors/known issues sections
663
  errors_section_bonus = 0.0
664
  if any("error" in ((r[4] or {}).get("section", "")).lower() or "known issues" in ((r[4] or {}).get("section", "")).lower()
665
  or "common issues" in ((r[4] or {}).get("section", "")).lower() for r in recs):
666
  errors_section_bonus = 0.5
667
+
668
  return (
669
  total_score
670
  + 0.4 * total_overlap
 
694
 
695
  reordered = best_recs + other_recs
696
  top = reordered[:top_k]
 
697
  documents = [t[3] for t in top]
698
  metadatas = [t[4] for t in top]
699
  distances = [t[2] for t in top]
 
712
  "actions": actions,
713
  }
714
 
715
+ # --------------------------- Section fetch helpers ---------------------------
716
  def get_section_text(filename: str, section: str) -> str:
717
  texts: List[str] = []
718
  for d in bm25_docs:
 
756
  texts.append(t)
757
  return "\n\n".join(texts).strip()
758
 
759
+ # --------------------------- Admin helpers ---------------------------
760
  def get_kb_runtime_info() -> Dict[str, Any]:
761
  return {
762
  "chroma_path": CHROMA_PATH,