Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -16,6 +16,7 @@ from services.kb_creation import (
|
|
| 16 |
hybrid_search_knowledge_base,
|
| 17 |
get_section_text,
|
| 18 |
get_best_steps_section_text,
|
|
|
|
| 19 |
)
|
| 20 |
from services.login import router as login_router
|
| 21 |
from services.generate_ticket import get_valid_token, create_incident
|
|
@@ -123,12 +124,12 @@ def _as_numbered_steps(text: str) -> str:
|
|
| 123 |
# ---------------- Clarifying message (ASCII-only) ----------------
|
| 124 |
def _build_clarifying_message() -> str:
|
| 125 |
return (
|
| 126 |
-
"I couldn't find matching content in the KB yet. To help me narrow it down, please share:\n
|
| 127 |
"- Module/area (e.g., Picking, Receiving, Trailer Close)\n"
|
| 128 |
"- Exact error message text/code (copy-paste)\n"
|
| 129 |
"- IDs involved (Order#, Load ID, Shipment#)\n"
|
| 130 |
"- Warehouse/site & environment (prod/test)\n"
|
| 131 |
-
"- When it started and how many users are impacted\n
|
| 132 |
"You can also say 'create ticket' and I'll raise a ServiceNow incident now."
|
| 133 |
)
|
| 134 |
|
|
@@ -207,6 +208,9 @@ STRICT_OVERLAP = 3
|
|
| 207 |
MAX_SENTENCES_STRICT = 4
|
| 208 |
MAX_SENTENCES_CONCISE = 3
|
| 209 |
|
|
|
|
|
|
|
|
|
|
| 210 |
def _normalize_for_match(text: str) -> str:
|
| 211 |
t = (text or "").lower()
|
| 212 |
t = re.sub(r"[^\w\s]", " ", t)
|
|
@@ -214,7 +218,6 @@ def _normalize_for_match(text: str) -> str:
|
|
| 214 |
return t
|
| 215 |
|
| 216 |
def _split_sentences(ctx: str) -> List[str]:
|
| 217 |
-
# Split on sentence boundaries, newlines, or ASCII bullets (-, *)
|
| 218 |
raw_sents = re.split(r"(?<=[.!?])\s+|\n+|\-\s*|\*\s*", ctx or "")
|
| 219 |
return [s.strip() for s in raw_sents if s and len(s.strip()) > 2]
|
| 220 |
|
|
@@ -262,7 +265,7 @@ def _extract_navigation_only(text: str, max_lines: int = 6) -> str:
|
|
| 262 |
ERROR_STARTS = (
|
| 263 |
"error", "resolution", "fix", "verify", "check",
|
| 264 |
"permission", "access", "authorization", "authorisation",
|
| 265 |
-
"role", "role mapping", "security profile", "escalation"
|
| 266 |
)
|
| 267 |
|
| 268 |
def _extract_errors_only(text: str, max_lines: int = 12) -> str:
|
|
@@ -376,31 +379,21 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 376 |
"debug": {"intent": "create_ticket"},
|
| 377 |
}
|
| 378 |
|
| 379 |
-
# --- Generic opener ---
|
| 380 |
-
if len(msg_norm.split()) <= 2 or any(p in msg_norm for p in ("issue", "problem", "help", "support")):
|
| 381 |
-
return {
|
| 382 |
-
"bot_response": _build_clarifying_message(),
|
| 383 |
-
"status": "NO_KB_MATCH",
|
| 384 |
-
"context_found": False,
|
| 385 |
-
"ask_resolved": False,
|
| 386 |
-
"suggest_incident": True,
|
| 387 |
-
"followup": "Reply with the above details or say 'create ticket'.",
|
| 388 |
-
"top_hits": [],
|
| 389 |
-
"sources": [],
|
| 390 |
-
"debug": {"intent": "generic_issue"},
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
# --- Status intent ---
|
| 394 |
status_intent = _parse_ticket_status_intent(msg_norm)
|
| 395 |
if status_intent:
|
| 396 |
if status_intent.get("ask_number"):
|
|
|
|
| 397 |
return {
|
| 398 |
-
"bot_response":
|
|
|
|
|
|
|
|
|
|
| 399 |
"status": "PARTIAL",
|
| 400 |
"context_found": False,
|
| 401 |
"ask_resolved": False,
|
| 402 |
"suggest_incident": False,
|
| 403 |
-
"followup":
|
| 404 |
"show_status_form": True,
|
| 405 |
"top_hits": [],
|
| 406 |
"sources": [],
|
|
@@ -441,6 +434,21 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 441 |
except Exception as e:
|
| 442 |
raise HTTPException(status_code=500, detail=safe_str(e))
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
# --- Hybrid KB search ---
|
| 445 |
kb_results = hybrid_search_knowledge_base(input_data.user_message, top_k=10, alpha=0.6, beta=0.4)
|
| 446 |
documents = kb_results.get("documents", [])
|
|
@@ -474,20 +482,31 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 474 |
best_doc = kb_results.get("best_doc")
|
| 475 |
top_meta = (metadatas or [{}])[0] if metadatas else {}
|
| 476 |
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
# Intent shaping
|
| 487 |
q = (input_data.user_message or "").lower()
|
| 488 |
if detected_intent == "errors" or any(k in q for k in [
|
| 489 |
"error", "issue", "fail", "not working", "resolution", "fix",
|
| 490 |
-
"permission", "access", "authorization", "escalation", "role", "security profile"
|
| 491 |
]):
|
| 492 |
context = _extract_errors_only(context, max_lines=12)
|
| 493 |
elif any(k in q for k in ["navigate", "navigation", "menu", "screen"]):
|
|
@@ -505,11 +524,7 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 505 |
and (best_distance is None or best_distance >= gate_distance_no_kb)
|
| 506 |
):
|
| 507 |
second_try = (input_data.prev_status or "").upper() == "NO_KB_MATCH"
|
| 508 |
-
clarify_text = (
|
| 509 |
-
_build_clarifying_message()
|
| 510 |
-
if not second_try
|
| 511 |
-
else "I still don't find a relevant KB match for this scenario even after clarification."
|
| 512 |
-
)
|
| 513 |
return {
|
| 514 |
"bot_response": clarify_text,
|
| 515 |
"status": "NO_KB_MATCH",
|
|
|
|
| 16 |
hybrid_search_knowledge_base,
|
| 17 |
get_section_text,
|
| 18 |
get_best_steps_section_text,
|
| 19 |
+
get_best_errors_section_text,
|
| 20 |
)
|
| 21 |
from services.login import router as login_router
|
| 22 |
from services.generate_ticket import get_valid_token, create_incident
|
|
|
|
| 124 |
# ---------------- Clarifying message (ASCII-only) ----------------
|
| 125 |
def _build_clarifying_message() -> str:
|
| 126 |
return (
|
| 127 |
+
"I couldn't find matching content in the KB yet. To help me narrow it down, please share:\n"
|
| 128 |
"- Module/area (e.g., Picking, Receiving, Trailer Close)\n"
|
| 129 |
"- Exact error message text/code (copy-paste)\n"
|
| 130 |
"- IDs involved (Order#, Load ID, Shipment#)\n"
|
| 131 |
"- Warehouse/site & environment (prod/test)\n"
|
| 132 |
+
"- When it started and how many users are impacted\n"
|
| 133 |
"You can also say 'create ticket' and I'll raise a ServiceNow incident now."
|
| 134 |
)
|
| 135 |
|
|
|
|
| 208 |
MAX_SENTENCES_STRICT = 4
|
| 209 |
MAX_SENTENCES_CONCISE = 3
|
| 210 |
|
| 211 |
+
PERM_QUERY_TERMS = ["permission", "permissions", "access", "access right", "authorization", "authorisation", "role", "role access", "security", "security profile", "privilege", "not allowed", "not authorized", "denied"]
|
| 212 |
+
|
| 213 |
+
|
| 214 |
def _normalize_for_match(text: str) -> str:
|
| 215 |
t = (text or "").lower()
|
| 216 |
t = re.sub(r"[^\w\s]", " ", t)
|
|
|
|
| 218 |
return t
|
| 219 |
|
| 220 |
def _split_sentences(ctx: str) -> List[str]:
|
|
|
|
| 221 |
raw_sents = re.split(r"(?<=[.!?])\s+|\n+|\-\s*|\*\s*", ctx or "")
|
| 222 |
return [s.strip() for s in raw_sents if s and len(s.strip()) > 2]
|
| 223 |
|
|
|
|
| 265 |
ERROR_STARTS = (
|
| 266 |
"error", "resolution", "fix", "verify", "check",
|
| 267 |
"permission", "access", "authorization", "authorisation",
|
| 268 |
+
"role", "role mapping", "security profile", "escalation", "not allowed", "not authorized", "denied"
|
| 269 |
)
|
| 270 |
|
| 271 |
def _extract_errors_only(text: str, max_lines: int = 12) -> str:
|
|
|
|
| 379 |
"debug": {"intent": "create_ticket"},
|
| 380 |
}
|
| 381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
# --- Status intent ---
|
| 383 |
status_intent = _parse_ticket_status_intent(msg_norm)
|
| 384 |
if status_intent:
|
| 385 |
if status_intent.get("ask_number"):
|
| 386 |
+
# Show clarifying bullets explicitly in bot_response (not only followup)
|
| 387 |
return {
|
| 388 |
+
"bot_response": (
|
| 389 |
+
"To check a ticket status, please share the Incident ID (e.g., INC0012345).\n\n"
|
| 390 |
+
"You can paste the ID here or say 'cancel'."
|
| 391 |
+
),
|
| 392 |
"status": "PARTIAL",
|
| 393 |
"context_found": False,
|
| 394 |
"ask_resolved": False,
|
| 395 |
"suggest_incident": False,
|
| 396 |
+
"followup": None,
|
| 397 |
"show_status_form": True,
|
| 398 |
"top_hits": [],
|
| 399 |
"sources": [],
|
|
|
|
| 434 |
except Exception as e:
|
| 435 |
raise HTTPException(status_code=500, detail=safe_str(e))
|
| 436 |
|
| 437 |
+
# --- Generic opener ---
|
| 438 |
+
if len(msg_norm.split()) <= 2 or any(p in msg_norm for p in ("issue", "problem", "help", "support")):
|
| 439 |
+
# Return clarifying bullets directly in bot_response; followup is CTA only
|
| 440 |
+
return {
|
| 441 |
+
"bot_response": _build_clarifying_message(),
|
| 442 |
+
"status": "NO_KB_MATCH",
|
| 443 |
+
"context_found": False,
|
| 444 |
+
"ask_resolved": False,
|
| 445 |
+
"suggest_incident": True,
|
| 446 |
+
"followup": "Reply with the above details or say 'create ticket'.",
|
| 447 |
+
"top_hits": [],
|
| 448 |
+
"sources": [],
|
| 449 |
+
"debug": {"intent": "generic_issue"},
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
# --- Hybrid KB search ---
|
| 453 |
kb_results = hybrid_search_knowledge_base(input_data.user_message, top_k=10, alpha=0.6, beta=0.4)
|
| 454 |
documents = kb_results.get("documents", [])
|
|
|
|
| 482 |
best_doc = kb_results.get("best_doc")
|
| 483 |
top_meta = (metadatas or [{}])[0] if metadatas else {}
|
| 484 |
|
| 485 |
+
# If the query is a permissions question, force error handling & prefer errors section from SOP
|
| 486 |
+
is_perm_query = any(t in msg_norm for t in PERM_QUERY_TERMS)
|
| 487 |
+
if is_perm_query:
|
| 488 |
+
detected_intent = "errors"
|
| 489 |
+
|
| 490 |
+
# Prefer full 'Process Steps' or 'Errors & Resolution' section from the best SOP
|
| 491 |
+
if best_doc:
|
| 492 |
+
if detected_intent == "steps":
|
| 493 |
+
full_steps = get_best_steps_section_text(best_doc)
|
| 494 |
+
if not full_steps:
|
| 495 |
+
sec = (top_meta or {}).get("section")
|
| 496 |
+
if sec:
|
| 497 |
+
full_steps = get_section_text(best_doc, sec)
|
| 498 |
+
if full_steps:
|
| 499 |
+
context = _as_numbered_steps(full_steps)
|
| 500 |
+
elif detected_intent == "errors":
|
| 501 |
+
full_errors = get_best_errors_section_text(best_doc)
|
| 502 |
+
if full_errors:
|
| 503 |
+
context = _extract_errors_only(full_errors, max_lines=12)
|
| 504 |
|
| 505 |
# Intent shaping
|
| 506 |
q = (input_data.user_message or "").lower()
|
| 507 |
if detected_intent == "errors" or any(k in q for k in [
|
| 508 |
"error", "issue", "fail", "not working", "resolution", "fix",
|
| 509 |
+
"permission", "access", "authorization", "escalation", "role", "security profile", "not allowed", "not authorized", "denied"
|
| 510 |
]):
|
| 511 |
context = _extract_errors_only(context, max_lines=12)
|
| 512 |
elif any(k in q for k in ["navigate", "navigation", "menu", "screen"]):
|
|
|
|
| 524 |
and (best_distance is None or best_distance >= gate_distance_no_kb)
|
| 525 |
):
|
| 526 |
second_try = (input_data.prev_status or "").upper() == "NO_KB_MATCH"
|
| 527 |
+
clarify_text = _build_clarifying_message()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
return {
|
| 529 |
"bot_response": clarify_text,
|
| 530 |
"status": "NO_KB_MATCH",
|