Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -12,20 +12,67 @@ from pydantic import BaseModel
|
|
| 12 |
from dotenv import load_dotenv
|
| 13 |
from datetime import datetime
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
|
| 30 |
GEMINI_SSL_VERIFY = os.getenv("GEMINI_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
|
| 31 |
|
|
@@ -56,7 +103,10 @@ async def lifespan(app: FastAPI):
|
|
| 56 |
|
| 57 |
|
| 58 |
app = FastAPI(lifespan=lifespan)
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
origins = ["https://chatbotnova-chatbot-frontend.hf.space"]
|
| 62 |
app.add_middleware(
|
|
@@ -171,9 +221,47 @@ def _detect_error_families(msg: str) -> list:
|
|
| 171 |
return fams
|
| 172 |
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
def _get_steps_for_action(best_doc: str, actions: list) -> Optional[str]:
|
| 178 |
"""
|
| 179 |
Return the full text of the steps section whose action_tag matches the user's intent.
|
|
@@ -549,8 +637,8 @@ def _build_tracking_descriptions(issue_text: str, resolved_text: str) -> Tuple[s
|
|
| 549 |
resolved = (resolved_text or "").strip()
|
| 550 |
short_desc = issue[:100] if issue else (resolved[:100] or "Issue resolved (user confirmation)")
|
| 551 |
long_desc = (
|
| 552 |
-
f
|
| 553 |
-
f
|
| 554 |
f"Tracking record created automatically by NOVA."
|
| 555 |
).strip()
|
| 556 |
return short_desc, long_desc
|
|
@@ -1246,50 +1334,71 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 1246 |
next_step_applied = False
|
| 1247 |
next_step_info: Dict[str, Any] = {}
|
| 1248 |
|
|
|
|
| 1249 |
if best_doc and detected_intent == "steps":
|
| 1250 |
context_preformatted = False
|
| 1251 |
full_steps = None
|
| 1252 |
|
| 1253 |
-
# 1) Try by KB action tags
|
| 1254 |
action_steps = _get_steps_for_action(best_doc, kb_results.get("actions", []))
|
| 1255 |
if action_steps:
|
| 1256 |
full_steps = action_steps
|
| 1257 |
else:
|
|
|
|
| 1258 |
if kb_results.get("actions"):
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
|
|
|
| 1264 |
asked_action = _detect_action_from_query(input_data.user_message)
|
| 1265 |
-
if not
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
|
|
|
|
| 1271 |
if not full_steps:
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1275 |
if not full_steps:
|
| 1276 |
-
|
| 1277 |
if not full_steps:
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
if full_steps:
|
| 1283 |
-
# Always add Save lines if present anywhere in the doc
|
| 1284 |
save_lines = _find_save_lines_in_doc(best_doc, max_lines=2)
|
| 1285 |
if save_lines:
|
| 1286 |
low_steps = (full_steps or "").lower()
|
| 1287 |
if not any(s in low_steps for s in SAVE_SYNS):
|
| 1288 |
full_steps = (full_steps or "").rstrip() + "\n" + save_lines
|
| 1289 |
|
|
|
|
| 1290 |
asked_action = _detect_action_from_query(input_data.user_message)
|
| 1291 |
full_steps = _filter_steps_by_action(full_steps, asked_action)
|
| 1292 |
|
|
|
|
| 1293 |
numbered_full = _ensure_numbering(full_steps)
|
| 1294 |
next_only = _resolve_next_steps(input_data.user_message, numbered_full, max_next=6, min_score=0.35)
|
| 1295 |
|
|
@@ -1468,7 +1577,6 @@ Return ONLY the rewritten guidance."""
|
|
| 1468 |
except Exception as e:
|
| 1469 |
raise HTTPException(status_code=500, detail=safe_str(e))
|
| 1470 |
|
| 1471 |
-
|
| 1472 |
# ------------------------------ Ticket description generation ------------------------------
|
| 1473 |
@app.post("/generate_ticket_desc")
|
| 1474 |
async def generate_ticket_desc_ep(input_data: TicketDescInput):
|
|
|
|
| 12 |
from dotenv import load_dotenv
|
| 13 |
from datetime import datetime
|
| 14 |
|
| 15 |
+
# ------------------------------ Safe imports for Pylance ------------------------------
|
| 16 |
+
# If your editor/Pylance can't resolve the local package, these stubs prevent "unresolved import"
|
| 17 |
+
# while you fix your workspace settings (see tips below).
|
| 18 |
+
try:
|
| 19 |
+
# Import shared vocab from KB services
|
| 20 |
+
from services.kb_creation import ACTION_SYNONYMS, MODULE_VOCAB
|
| 21 |
+
from services.kb_creation import (
|
| 22 |
+
collection,
|
| 23 |
+
ingest_documents,
|
| 24 |
+
hybrid_search_knowledge_base,
|
| 25 |
+
get_section_text,
|
| 26 |
+
get_best_steps_section_text,
|
| 27 |
+
get_best_errors_section_text,
|
| 28 |
+
get_escalation_text, # for escalation heading
|
| 29 |
+
)
|
| 30 |
+
from services.kb_creation import bm25_docs
|
| 31 |
+
from services.login import router as login_router
|
| 32 |
+
from services.generate_ticket import get_valid_token, create_incident
|
| 33 |
+
except Exception:
|
| 34 |
+
# Lightweight fallbacks (only to make Pylance happy; you should use real implementations at runtime)
|
| 35 |
+
ACTION_SYNONYMS: Dict[str, List[str]] = {"create": ["create"], "update": ["update"], "delete": ["delete"]}
|
| 36 |
+
MODULE_VOCAB: Dict[str, List[str]] = {"appointments": ["appointment", "schedule"]}
|
| 37 |
+
bm25_docs: List[Dict[str, Any]] = []
|
| 38 |
+
|
| 39 |
+
class _DummyColl:
|
| 40 |
+
def count(self) -> int:
|
| 41 |
+
return 0
|
| 42 |
+
|
| 43 |
+
def collection() -> _DummyColl:
|
| 44 |
+
return _DummyColl()
|
| 45 |
+
|
| 46 |
+
def ingest_documents(folder_path: str) -> None:
|
| 47 |
+
raise NotImplementedError("ingest_documents stub in editor mode")
|
| 48 |
+
|
| 49 |
+
def hybrid_search_knowledge_base(q: str, top_k: int = 10, alpha: float = 0.6, beta: float = 0.4) -> Dict[str, Any]:
|
| 50 |
+
return {"documents": [], "metadatas": [], "distances": [], "combined_scores": [], "user_intent": "neutral", "best_doc": None, "actions": []}
|
| 51 |
+
|
| 52 |
+
def get_section_text(doc: Optional[str], section: Optional[str]) -> str:
|
| 53 |
+
return ""
|
| 54 |
+
|
| 55 |
+
def get_best_steps_section_text(doc: Optional[str]) -> str:
|
| 56 |
+
return ""
|
| 57 |
+
|
| 58 |
+
def get_best_errors_section_text(doc: Optional[str]) -> str:
|
| 59 |
+
return ""
|
| 60 |
|
| 61 |
+
def get_escalation_text(doc: Optional[str]) -> str:
|
| 62 |
+
return ""
|
| 63 |
+
|
| 64 |
+
class _DummyRouter:
|
| 65 |
+
pass
|
| 66 |
+
|
| 67 |
+
login_router = _DummyRouter()
|
| 68 |
+
|
| 69 |
+
def get_valid_token() -> str:
|
| 70 |
+
return ""
|
| 71 |
+
|
| 72 |
+
def create_incident(short: str, desc: str) -> Dict[str, Any]:
|
| 73 |
+
return {"error": "ServiceNow not configured"}
|
| 74 |
+
|
| 75 |
+
# ------------------------------ Config ------------------------------
|
| 76 |
VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
|
| 77 |
GEMINI_SSL_VERIFY = os.getenv("GEMINI_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
|
| 78 |
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
app = FastAPI(lifespan=lifespan)
|
| 106 |
+
try:
|
| 107 |
+
app.include_router(login_router)
|
| 108 |
+
except Exception:
|
| 109 |
+
pass # stubbed router in editor-only mode
|
| 110 |
|
| 111 |
origins = ["https://chatbotnova-chatbot-frontend.hf.space"]
|
| 112 |
app.add_middleware(
|
|
|
|
| 221 |
return fams
|
| 222 |
|
| 223 |
|
| 224 |
+
def _get_steps_by_title_keywords_global(keywords: List[str], prefer_doc: Optional[str] = None) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
| 225 |
+
"""
|
| 226 |
+
Search ALL SOP docs for a 'steps' section whose *section title* contains any of `keywords`.
|
| 227 |
+
Returns (doc_name, section_title, steps_text). Prefers `prefer_doc` and 'appointments' module.
|
| 228 |
+
"""
|
| 229 |
+
if not keywords:
|
| 230 |
+
return None, None, None
|
| 231 |
+
keys_low = [k.strip().lower() for k in keywords if k and k.strip()]
|
| 232 |
|
| 233 |
+
candidates: List[Tuple[float, str, str, str]] = [] # (score, doc, section_title, text)
|
| 234 |
+
seen = set()
|
| 235 |
+
for d in bm25_docs:
|
| 236 |
+
m = d.get("meta", {}) or {}
|
| 237 |
+
if m.get("intent_tag") != "steps":
|
| 238 |
+
continue
|
| 239 |
+
doc = m.get("filename")
|
| 240 |
+
sec = (m.get("section") or "").strip()
|
| 241 |
+
if not doc or not sec or (doc, sec) in seen:
|
| 242 |
+
continue
|
| 243 |
+
sec_low = sec.lower()
|
| 244 |
+
if any(k in sec_low for k in keys_low):
|
| 245 |
+
txt = get_section_text(doc, sec)
|
| 246 |
+
if not txt or not txt.strip():
|
| 247 |
+
continue
|
| 248 |
+
score = 1.0
|
| 249 |
+
if prefer_doc and doc == prefer_doc:
|
| 250 |
+
score += 0.4
|
| 251 |
+
mtags = (m.get("module_tags") or "").lower()
|
| 252 |
+
if "appointments" in mtags:
|
| 253 |
+
score += 0.3
|
| 254 |
+
candidates.append((score, doc, sec, txt.strip()))
|
| 255 |
+
seen.add((doc, sec))
|
| 256 |
+
|
| 257 |
+
if not candidates:
|
| 258 |
+
return None, None, None
|
| 259 |
+
|
| 260 |
+
candidates.sort(key=lambda x: x[0], reverse=True)
|
| 261 |
+
_, best_doc_g, best_sec_g, best_text_g = candidates[0]
|
| 262 |
+
return best_doc_g, best_sec_g, best_text_g
|
| 263 |
+
|
| 264 |
+
# --- Action-targeted steps selector (uses existing KB metadata) ---
|
| 265 |
def _get_steps_for_action(best_doc: str, actions: list) -> Optional[str]:
|
| 266 |
"""
|
| 267 |
Return the full text of the steps section whose action_tag matches the user's intent.
|
|
|
|
| 637 |
resolved = (resolved_text or "").strip()
|
| 638 |
short_desc = issue[:100] if issue else (resolved[:100] or "Issue resolved (user confirmation)")
|
| 639 |
long_desc = (
|
| 640 |
+
f'User reported: "{issue}". '
|
| 641 |
+
f'User confirmation: "{resolved}". '
|
| 642 |
f"Tracking record created automatically by NOVA."
|
| 643 |
).strip()
|
| 644 |
return short_desc, long_desc
|
|
|
|
| 1334 |
next_step_applied = False
|
| 1335 |
next_step_info: Dict[str, Any] = {}
|
| 1336 |
|
| 1337 |
+
# ---------------- Steps branch (fixed indentation) ----------------
|
| 1338 |
if best_doc and detected_intent == "steps":
|
| 1339 |
context_preformatted = False
|
| 1340 |
full_steps = None
|
| 1341 |
|
| 1342 |
+
# 1) Try by KB action tags in current doc
|
| 1343 |
action_steps = _get_steps_for_action(best_doc, kb_results.get("actions", []))
|
| 1344 |
if action_steps:
|
| 1345 |
full_steps = action_steps
|
| 1346 |
else:
|
| 1347 |
+
# 1a) Global by KB actions
|
| 1348 |
if kb_results.get("actions"):
|
| 1349 |
+
alt_doc, alt_steps = _get_steps_for_action_global(kb_results["actions"], prefer_doc=best_doc)
|
| 1350 |
+
if alt_steps:
|
| 1351 |
+
best_doc = alt_doc
|
| 1352 |
+
full_steps = alt_steps
|
| 1353 |
+
|
| 1354 |
+
# 1b) Global using asked_action (when kb_results["actions"] is empty)
|
| 1355 |
asked_action = _detect_action_from_query(input_data.user_message)
|
| 1356 |
+
if not full_steps and asked_action:
|
| 1357 |
+
alt_doc2, alt_steps2 = _get_steps_for_action_global([asked_action], prefer_doc=best_doc)
|
| 1358 |
+
if alt_steps2:
|
| 1359 |
+
best_doc = alt_doc2
|
| 1360 |
+
full_steps = alt_steps2
|
| 1361 |
|
| 1362 |
+
# 1c) Title-keywords global fallback (robust when action_tag is missing)
|
| 1363 |
if not full_steps:
|
| 1364 |
+
if asked_action == "update":
|
| 1365 |
+
kd = ["updation", "update", "reschedule"]
|
| 1366 |
+
elif asked_action == "delete":
|
| 1367 |
+
kd = ["deletion", "delete", "cancel"]
|
| 1368 |
+
elif asked_action == "create":
|
| 1369 |
+
kd = ["creation", "create", "new"]
|
| 1370 |
+
else:
|
| 1371 |
+
kd = ["updation", "update", "deletion", "delete", "cancel", "creation"]
|
| 1372 |
+
alt_doc3, alt_sec3, alt_steps3 = _get_steps_by_title_keywords_global(kd, prefer_doc=best_doc)
|
| 1373 |
+
if alt_steps3:
|
| 1374 |
+
best_doc = alt_doc3
|
| 1375 |
+
full_steps = alt_steps3
|
| 1376 |
+
|
| 1377 |
+
# Existing local fallbacks
|
| 1378 |
+
if not full_steps:
|
| 1379 |
+
default_sec = _pick_default_action_section(best_doc)
|
| 1380 |
+
if default_sec:
|
| 1381 |
+
full_steps = get_section_text(best_doc, default_sec)
|
| 1382 |
if not full_steps:
|
| 1383 |
+
full_steps = get_best_steps_section_text(best_doc)
|
| 1384 |
if not full_steps:
|
| 1385 |
+
sec = (top_meta or {}).get("section")
|
| 1386 |
+
if sec:
|
| 1387 |
+
full_steps = get_section_text(best_doc, sec)
|
| 1388 |
+
|
| 1389 |
if full_steps:
|
| 1390 |
+
# Always add Save lines if present anywhere in the doc
|
| 1391 |
save_lines = _find_save_lines_in_doc(best_doc, max_lines=2)
|
| 1392 |
if save_lines:
|
| 1393 |
low_steps = (full_steps or "").lower()
|
| 1394 |
if not any(s in low_steps for s in SAVE_SYNS):
|
| 1395 |
full_steps = (full_steps or "").rstrip() + "\n" + save_lines
|
| 1396 |
|
| 1397 |
+
# Remove aggressive trimming; keep full section
|
| 1398 |
asked_action = _detect_action_from_query(input_data.user_message)
|
| 1399 |
full_steps = _filter_steps_by_action(full_steps, asked_action)
|
| 1400 |
|
| 1401 |
+
# Number steps and apply next-step logic
|
| 1402 |
numbered_full = _ensure_numbering(full_steps)
|
| 1403 |
next_only = _resolve_next_steps(input_data.user_message, numbered_full, max_next=6, min_score=0.35)
|
| 1404 |
|
|
|
|
| 1577 |
except Exception as e:
|
| 1578 |
raise HTTPException(status_code=500, detail=safe_str(e))
|
| 1579 |
|
|
|
|
| 1580 |
# ------------------------------ Ticket description generation ------------------------------
|
| 1581 |
@app.post("/generate_ticket_desc")
|
| 1582 |
async def generate_ticket_desc_ep(input_data: TicketDescInput):
|