Spaces:
Sleeping
Sleeping
Update services/kb_creation.py
Browse files- 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 17 |
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
|
| 18 |
|
| 19 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 46 |
-
BULLET_RE = re.compile(r"^\s*(?:[\-\*\u2022]|\d+[.)]
|
| 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
| 572 |
-
delta = 0.55
|
| 573 |
-
epsilon = 0.30
|
| 574 |
-
zeta = 0.65
|
| 575 |
-
eta = 0.50
|
| 576 |
-
theta = 0.40
|
| 577 |
-
iota = 0.60
|
| 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,
|
| 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 |
-
|
|
|
|
| 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 |
-
# ---------------------------
|
| 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 |
-
# ---------------------------
|
| 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,
|