srilakshu012456 commited on
Commit
5e31c75
·
verified ·
1 Parent(s): d018ff6

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +250 -69
main.py CHANGED
@@ -21,7 +21,10 @@ from services.login import router as login_router
21
  from services.generate_ticket import get_valid_token, create_incident
22
 
23
  VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
24
- GEMINI_SSL_VERIFY = os.getenv("GEMINI_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
 
 
 
25
 
26
  def safe_str(e: Any) -> str:
27
  try:
@@ -53,7 +56,9 @@ app.add_middleware(
53
  allow_headers=["*"],
54
  )
55
 
56
- # --- models ---
 
 
57
  class ChatInput(BaseModel):
58
  user_message: str
59
  prev_status: Optional[str] = None
@@ -71,7 +76,9 @@ class TicketStatusInput(BaseModel):
71
  sys_id: Optional[str] = None
72
  number: Optional[str] = None
73
 
74
- # --- filters & extractors (no LLM) ---
 
 
75
  STRICT_OVERLAP = 3
76
  MAX_SENTENCES_CONCISE = 6
77
 
@@ -177,7 +184,7 @@ def _extract_errors_only(text: str, max_lines: int = 12) -> str:
177
  break
178
  return "\n".join(kept).strip() if kept else (text or "").strip()
179
 
180
- # ---- Escalation extractor (optional) ----
181
  ESCALATION_REGEX = re.compile(r"^\s*Escalation Path|\s+→\s+", re.IGNORECASE)
182
 
183
  def _extract_escalation_only(text: str, max_lines: int = 3) -> str:
@@ -216,7 +223,9 @@ def _merge_number_only_lines(lines: List[str]) -> List[str]:
216
  def _strip_leading_mark(s: str) -> str:
217
  return re.sub(r"^\s*(?:\d+[\.)]\s+|[•\-]\s+)", "", (s or "")).strip()
218
 
219
- # --- conversation intents (RESTORED to fix NameError) ---
 
 
220
  def _is_incident_intent(msg_norm: str) -> bool:
221
  intents = [
222
  "create ticket","create a ticket","raise ticket","raise a ticket","open ticket","open a ticket",
@@ -232,79 +241,122 @@ def _is_generic_issue(msg_norm: str) -> bool:
232
  "need support","please help","need assistance","assist me","facing issue","facing a problem","got a problem"
233
  ]
234
  return any(p == msg_norm or p in msg_norm for p in generic) or len(msg_norm.split()) <= 2
235
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  def _compose_errors_response(error_lines: List[str], escalation_lines: List[str]) -> str:
237
- """
238
- Turn SOP 'Common Errors & Resolution' + 'Escalation Path' into a friendly,
239
- user-understandable reply.
240
- """
241
  err = [ln.strip() for ln in (error_lines or []) if ln and ln.strip()]
242
  esc = [ln.strip() for ln in (escalation_lines or []) if ln and ln.strip()]
243
 
244
- # 1) A one-line summary when permission/auth appears in the error lines
245
  summary = None
246
  joined = " ".join(err).lower()
247
- if any(k in joined for k in ["permission", "authorized", "authorization", "role access", "not allowed", "insufficient"]):
248
  summary = (
249
  "It looks like your role doesn’t allow Inventory Adjustment right now. "
250
  "Please verify you have the required authorization/role enabled. "
251
  "If you still cannot proceed, follow the escalation path below."
252
  )
253
 
254
- # 2) Action bullets (convert SOP bullets into simpler points)
255
- actions = []
256
  for ln in err:
257
  s = re.sub(r"^\s*(?:\d+[\.)]\s+|[•\-]\s+)", "", ln).strip()
258
- # Light rewrite for common phrases
259
  s = s.replace("Verify role access", "Verify your role/access in WMS")
260
  s = s.replace("Check if cycle count or task is active", "Check if any cycle count/task is locking the location")
261
  s = s.replace("Confirm unit of measure setup", "Confirm the item’s UOM setup in master data")
262
- s = s.replace("Adjustment not allowed:", "Adjustment not allowed:")
263
  if s:
264
  actions.append(f"• {s}")
265
 
266
- # 3) Escalation line (if present)
267
  esc_text = ""
268
  if esc:
269
- # pick the first escalation line and strip decorative arrows
270
  raw = esc[0]
271
- clean = re.sub(r"^\s*Escalation Path\s*:\s*", "", raw, flags=re.IGNORECASE)
272
- clean = clean.replace("→", "→").strip()
273
  esc_text = f"\n\n**Escalation Path:** {clean}"
274
 
275
- # Compose
276
  top = summary or "Here’s how to resolve this:"
277
  bullet_block = ("\n" + "\n".join(actions)) if actions else ""
278
  return f"{top}{bullet_block}{esc_text}".strip()
279
 
280
-
281
- def _extract_errors_and_escalation(context: str) -> Tuple[List[str], List[str]]:
282
- """
283
- Return (error_lines, escalation_lines) from the already-filtered SOP context.
284
- """
285
- lines = [ln.strip() for ln in (context or "").splitlines() if ln.strip()]
286
- error_lines, escalation_lines = [], []
287
- for ln in lines:
288
- # Collect typical error/resolution bullets
289
- if STEP_LINE_REGEX.match(ln) or ln.lower().startswith(("error", "resolution", "fix", "verify", "check", "not allowed")):
290
- error_lines.append(ln)
291
- # Collect escalation
292
- if re.search(r"Escalation Path", ln, flags=re.IGNORECASE) or ("→" in ln):
293
- escalation_lines.append(ln)
294
- return error_lines, escalation_lines
295
-
296
-
297
  @app.get("/")
298
  async def health_check():
299
  return {"status": "ok"}
300
 
 
 
 
301
  @app.post("/chat")
302
  async def chat_with_ai(input_data: ChatInput):
303
  try:
304
  msg_norm = (input_data.user_message or "").lower().strip()
305
 
306
- # incident & generic handlers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  if _is_incident_intent(msg_norm):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  return {
309
  "bot_response": (
310
  "Okay, let’s create a ServiceNow incident.\n\n"
@@ -312,10 +364,12 @@ async def chat_with_ai(input_data: ChatInput):
312
  "• Detailed Description (steps, error text, IDs, site, environment)"
313
  ),
314
  "status": (input_data.prev_status or "PARTIAL"),
315
- "context_found": False, "ask_resolved": False, "suggest_incident": False,
316
  "show_incident_form": True, "followup": None, "top_hits": [], "sources": [],
317
  "debug": {"intent": "create_ticket"},
318
  }
 
 
319
  if _is_generic_issue(msg_norm):
320
  return {
321
  "bot_response": (
@@ -332,7 +386,58 @@ async def chat_with_ai(input_data: ChatInput):
332
  "debug": {"intent": "generic_issue"},
333
  }
334
 
335
- # Hybrid KB search
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  kb_results = hybrid_search_knowledge_base(input_data.user_message, top_k=10, alpha=0.6, beta=0.4)
337
 
338
  # Build a small context window
@@ -395,7 +500,7 @@ async def chat_with_ai(input_data: ChatInput):
395
  high_conf = (best_combined is not None and best_combined >= gate_combined_ok)
396
  exact = bool(exact_by_filter or high_conf)
397
 
398
- # --- STEPS intent: full SOP steps (numbered) ---
399
  if detected_intent == "steps" and best_doc:
400
  full_steps = get_best_steps_section_text(best_doc)
401
  if not full_steps:
@@ -433,43 +538,39 @@ async def chat_with_ai(input_data: ChatInput):
433
  },
434
  }
435
 
436
- # --- Non-steps intents ---
437
  context = filtered_text
438
  if detected_intent == "errors":
439
- # Keep concise extraction, then compose friendly response
440
- errs = _extract_errors_only(context, max_lines=MAX_SENTENCES_CONCISE)
441
- esc = _extract_escalation_only(context, max_lines=3)
442
- # Build friendly paragraph + bullets from SOP lines
443
- err_lines, esc_lines = _extract_errors_and_escalation(errs + ("\n" + esc if esc else ""))
444
- bot_text = _compose_errors_response(err_lines, esc_lines)
445
-
446
- status = "OK" if exact else "PARTIAL"
447
- return {
448
  "bot_response": bot_text,
449
  "status": status,
450
  "context_found": True,
451
  "ask_resolved": (status == "OK"),
452
  "suggest_incident": False,
453
  "followup": ("Does this help? I can refine." if status == "PARTIAL" else None),
454
- "top_hits": [],
455
- "sources": [],
456
  "debug": {
457
- "used_chunks": len(context.split("\n\n---\n\n")) if context else 0,
458
- "best_distance": best_distance,
459
- "best_combined": best_combined,
460
- "filter_mode": filt_info.get("mode"),
461
- "matched_count": filt_info.get("matched_count"),
462
- "user_intent": detected_intent,
463
- "actions": actions,
464
- "best_doc": best_doc,
465
- "exact": exact,
466
- "high_conf": high_conf,
467
- },
468
- }
469
  elif "navigate" in msg_norm or "menu" in msg_norm or "screen" in msg_norm:
470
  context = _extract_steps_only(context, max_lines=MAX_SENTENCES_CONCISE)
471
- # fall through to default response below
472
- # No-KB gate
473
  if (not context.strip()) or (
474
  (best_combined is None or best_combined < gate_combined_no_kb) and (best_distance is None or best_distance >= gate_distance_no_kb)
475
  ):
@@ -493,7 +594,7 @@ async def chat_with_ai(input_data: ChatInput):
493
  "debug": {"used_chunks": 0, "second_try": second_try, "best_distance": best_distance, "best_combined": best_combined},
494
  }
495
 
496
- # Default response for non-steps
497
  bot_text = context.strip()
498
  status = "OK" if exact else "PARTIAL"
499
  return {
@@ -517,3 +618,83 @@ async def chat_with_ai(input_data: ChatInput):
517
  raise
518
  except Exception as e:
519
  raise HTTPException(status_code=500, detail=safe_str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  from services.generate_ticket import get_valid_token, create_incident
22
 
23
  VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
24
+
25
+ # --------------------------------------------------------
26
+ # Utilities
27
+ # --------------------------------------------------------
28
 
29
  def safe_str(e: Any) -> str:
30
  try:
 
56
  allow_headers=["*"],
57
  )
58
 
59
+ # --------------------------------------------------------
60
+ # Models
61
+ # --------------------------------------------------------
62
  class ChatInput(BaseModel):
63
  user_message: str
64
  prev_status: Optional[str] = None
 
76
  sys_id: Optional[str] = None
77
  number: Optional[str] = None
78
 
79
+ # --------------------------------------------------------
80
+ # KB filters & extractors (no LLM)
81
+ # --------------------------------------------------------
82
  STRICT_OVERLAP = 3
83
  MAX_SENTENCES_CONCISE = 6
84
 
 
184
  break
185
  return "\n".join(kept).strip() if kept else (text or "").strip()
186
 
187
+ # ---- Escalation extractor ----
188
  ESCALATION_REGEX = re.compile(r"^\s*Escalation Path|\s+→\s+", re.IGNORECASE)
189
 
190
  def _extract_escalation_only(text: str, max_lines: int = 3) -> str:
 
223
  def _strip_leading_mark(s: str) -> str:
224
  return re.sub(r"^\s*(?:\d+[\.)]\s+|[•\-]\s+)", "", (s or "")).strip()
225
 
226
+ # --------------------------------------------------------
227
+ # Conversation intents & helpers
228
+ # --------------------------------------------------------
229
  def _is_incident_intent(msg_norm: str) -> bool:
230
  intents = [
231
  "create ticket","create a ticket","raise ticket","raise a ticket","open ticket","open a ticket",
 
241
  "need support","please help","need assistance","assist me","facing issue","facing a problem","got a problem"
242
  ]
243
  return any(p == msg_norm or p in msg_norm for p in generic) or len(msg_norm.split()) <= 2
244
+
245
+ def _is_negative_feedback(msg_norm: str) -> bool:
246
+ phrases = [
247
+ "not resolved","issue not resolved","still not working","not working",
248
+ "didn't work","doesn't work","no change","not fixed","still failing",
249
+ "failed again","broken","fail","not solved","same issue"
250
+ ]
251
+ return any(p in msg_norm for p in phrases)
252
+
253
+ # Parse status intent inside chat (e.g., "status INC0012345")
254
+ def _parse_ticket_status_intent(msg_norm: str) -> Dict[str, Optional[str]]:
255
+ keywords = ["status", "ticket status", "incident status", "check status", "check ticket status", "check incident status"]
256
+ if not any(k in msg_norm for k in keywords):
257
+ return {}
258
+ # Try to capture INC number
259
+ m = re.search(r"(inc\d+)", msg_norm, flags=re.IGNORECASE)
260
+ if m:
261
+ val = m.group(1).strip().upper()
262
+ return {"number": val}
263
+ # Ask the user for number if not present
264
+ return {"ask_number": True}
265
+
266
+ # --------------------------------------------------------
267
+ # Error/escalation friendly composition
268
+ # --------------------------------------------------------
269
  def _compose_errors_response(error_lines: List[str], escalation_lines: List[str]) -> str:
 
 
 
 
270
  err = [ln.strip() for ln in (error_lines or []) if ln and ln.strip()]
271
  esc = [ln.strip() for ln in (escalation_lines or []) if ln and ln.strip()]
272
 
 
273
  summary = None
274
  joined = " ".join(err).lower()
275
+ if any(k in joined for k in ["permission","authorized","authorization","role access","not allowed","insufficient"]):
276
  summary = (
277
  "It looks like your role doesn’t allow Inventory Adjustment right now. "
278
  "Please verify you have the required authorization/role enabled. "
279
  "If you still cannot proceed, follow the escalation path below."
280
  )
281
 
282
+ actions: List[str] = []
 
283
  for ln in err:
284
  s = re.sub(r"^\s*(?:\d+[\.)]\s+|[•\-]\s+)", "", ln).strip()
 
285
  s = s.replace("Verify role access", "Verify your role/access in WMS")
286
  s = s.replace("Check if cycle count or task is active", "Check if any cycle count/task is locking the location")
287
  s = s.replace("Confirm unit of measure setup", "Confirm the item’s UOM setup in master data")
 
288
  if s:
289
  actions.append(f"• {s}")
290
 
 
291
  esc_text = ""
292
  if esc:
 
293
  raw = esc[0]
294
+ clean = re.sub(r"^\s*Escalation Path\s*:?\s*", "", raw, flags=re.IGNORECASE).strip()
 
295
  esc_text = f"\n\n**Escalation Path:** {clean}"
296
 
 
297
  top = summary or "Here’s how to resolve this:"
298
  bullet_block = ("\n" + "\n".join(actions)) if actions else ""
299
  return f"{top}{bullet_block}{esc_text}".strip()
300
 
301
+ # --------------------------------------------------------
302
+ # Health
303
+ # --------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  @app.get("/")
305
  async def health_check():
306
  return {"status": "ok"}
307
 
308
+ # --------------------------------------------------------
309
+ # Chat endpoint
310
+ # --------------------------------------------------------
311
  @app.post("/chat")
312
  async def chat_with_ai(input_data: ChatInput):
313
  try:
314
  msg_norm = (input_data.user_message or "").lower().strip()
315
 
316
+ # --- Negative feedback: proactively offer incident creation
317
+ if _is_negative_feedback(msg_norm):
318
+ return {
319
+ "bot_response": (
320
+ "Sorry it’s still not resolved. I can open a ServiceNow incident to track and escalate.\n\n"
321
+ "Please provide:\n• Short Description (one line)\n"
322
+ "• Detailed Description (what you tried, exact error text, IDs, site, environment)"
323
+ ),
324
+ "status": "PARTIAL",
325
+ "context_found": False,
326
+ "ask_resolved": False,
327
+ "suggest_incident": True,
328
+ "show_incident_form": True,
329
+ "followup": None,
330
+ "top_hits": [], "sources": [],
331
+ "debug": {"intent": "negative_feedback"},
332
+ }
333
+
334
+ # --- Explicit incident intent: try direct creation if enough details; else show form
335
  if _is_incident_intent(msg_norm):
336
+ # If user message has more than the keywords, attempt immediate creation
337
+ minimal_phrases = ["create ticket", "create incident", "open ticket", "open incident", "raise ticket", "raise incident"]
338
+ has_only_minimal = any(msg_norm.strip() == p for p in minimal_phrases)
339
+ if not has_only_minimal and len(msg_norm.split()) > 3:
340
+ short_desc = input_data.user_message.strip()[:100]
341
+ long_desc = input_data.user_message.strip()
342
+ try:
343
+ result = create_incident(short_desc, long_desc)
344
+ if isinstance(result, dict) and not result.get("error"):
345
+ inc_number = result.get("number", "<unknown>")
346
+ return {
347
+ "bot_response": f"✅ Incident created: {inc_number}",
348
+ "status": "OK",
349
+ "context_found": False,
350
+ "ask_resolved": False,
351
+ "suggest_incident": False,
352
+ "followup": "Is there anything else I can assist you with?",
353
+ "top_hits": [], "sources": [],
354
+ "debug": {"intent": "create_ticket_direct"},
355
+ }
356
+ except Exception as e:
357
+ # Fall back to form if direct creation fails
358
+ pass
359
+ # Prompt for details & show form
360
  return {
361
  "bot_response": (
362
  "Okay, let’s create a ServiceNow incident.\n\n"
 
364
  "• Detailed Description (steps, error text, IDs, site, environment)"
365
  ),
366
  "status": (input_data.prev_status or "PARTIAL"),
367
+ "context_found": False, "ask_resolved": False, "suggest_incident": True,
368
  "show_incident_form": True, "followup": None, "top_hits": [], "sources": [],
369
  "debug": {"intent": "create_ticket"},
370
  }
371
+
372
+ # --- Generic openers
373
  if _is_generic_issue(msg_norm):
374
  return {
375
  "bot_response": (
 
386
  "debug": {"intent": "generic_issue"},
387
  }
388
 
389
+ # --- Status intent inside chat
390
+ status_intent = _parse_ticket_status_intent(msg_norm)
391
+ if status_intent:
392
+ if status_intent.get("ask_number"):
393
+ return {
394
+ "bot_response": "Please share the Incident ID (e.g., INC0012345) to check the status.",
395
+ "status": "PARTIAL",
396
+ "context_found": False,
397
+ "ask_resolved": False,
398
+ "suggest_incident": False,
399
+ "show_status_form": True,
400
+ "followup": "Provide the Incident ID and I’ll fetch the status.",
401
+ "top_hits": [], "sources": [],
402
+ "debug": {"intent": "status_request_missing_id"},
403
+ }
404
+ # fetch
405
+ try:
406
+ token = get_valid_token()
407
+ instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
408
+ if not instance_url:
409
+ raise HTTPException(status_code=500, detail="SERVICENOW_INSTANCE_URL missing")
410
+ headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
411
+ number = status_intent.get("number")
412
+ url = f"{instance_url}/api/now/table/incident?number={number}"
413
+ response = requests.get(url, headers=headers, verify=VERIFY_SSL, timeout=25)
414
+ data = response.json()
415
+ lst = data.get("result", [])
416
+ result = (lst or [{}])[0] if response.status_code == 200 else {}
417
+ state_code = builtins.str(result.get("state", "unknown"))
418
+ state_map = {"1":"New","2":"In Progress","3":"On Hold","6":"Resolved","7":"Closed","8":"Canceled"}
419
+ state_label = state_map.get(state_code, state_code)
420
+ short = result.get("short_description", "")
421
+ num = result.get("number", number or "unknown")
422
+ return {
423
+ "bot_response": (
424
+ f"**Ticket:** {num}\n"
425
+ f"**Status:** {state_label}\n"
426
+ f"**Issue description:** {short}"
427
+ ),
428
+ "status": "OK",
429
+ "show_assist_card": True,
430
+ "context_found": False,
431
+ "ask_resolved": False,
432
+ "suggest_incident": False,
433
+ "followup": "Is there anything else I can assist you with?",
434
+ "top_hits": [], "sources": [],
435
+ "debug": {"intent": "status", "http_status": response.status_code},
436
+ }
437
+ except Exception as e:
438
+ raise HTTPException(status_code=500, detail=safe_str(e))
439
+
440
+ # --- Hybrid KB search
441
  kb_results = hybrid_search_knowledge_base(input_data.user_message, top_k=10, alpha=0.6, beta=0.4)
442
 
443
  # Build a small context window
 
500
  high_conf = (best_combined is not None and best_combined >= gate_combined_ok)
501
  exact = bool(exact_by_filter or high_conf)
502
 
503
+ # --- STEPS intent: full SOP steps (numbered)
504
  if detected_intent == "steps" and best_doc:
505
  full_steps = get_best_steps_section_text(best_doc)
506
  if not full_steps:
 
538
  },
539
  }
540
 
541
+ # --- Non-steps intents
542
  context = filtered_text
543
  if detected_intent == "errors":
544
+ errs = _extract_errors_only(context, max_lines=MAX_SENTENCES_CONCISE)
545
+ esc = _extract_escalation_only(context, max_lines=3)
546
+ err_lines, esc_lines = _extract_errors_and_escalation(errs + ("\n" + esc if esc else ""))
547
+ bot_text = _compose_errors_response(err_lines, esc_lines)
548
+ status = "OK" if exact else "PARTIAL"
549
+ return {
 
 
 
550
  "bot_response": bot_text,
551
  "status": status,
552
  "context_found": True,
553
  "ask_resolved": (status == "OK"),
554
  "suggest_incident": False,
555
  "followup": ("Does this help? I can refine." if status == "PARTIAL" else None),
556
+ "top_hits": [], "sources": [],
 
557
  "debug": {
558
+ "used_chunks": len(context.split("\n\n---\n\n")) if context else 0,
559
+ "best_distance": best_distance,
560
+ "best_combined": best_combined,
561
+ "filter_mode": filt_info.get("mode"),
562
+ "matched_count": filt_info.get("matched_count"),
563
+ "user_intent": detected_intent,
564
+ "actions": actions,
565
+ "best_doc": best_doc,
566
+ "exact": exact,
567
+ "high_conf": high_conf,
568
+ },
569
+ }
570
  elif "navigate" in msg_norm or "menu" in msg_norm or "screen" in msg_norm:
571
  context = _extract_steps_only(context, max_lines=MAX_SENTENCES_CONCISE)
572
+
573
+ # --- No-KB gate
574
  if (not context.strip()) or (
575
  (best_combined is None or best_combined < gate_combined_no_kb) and (best_distance is None or best_distance >= gate_distance_no_kb)
576
  ):
 
594
  "debug": {"used_chunks": 0, "second_try": second_try, "best_distance": best_distance, "best_combined": best_combined},
595
  }
596
 
597
+ # --- Default response for non-steps
598
  bot_text = context.strip()
599
  status = "OK" if exact else "PARTIAL"
600
  return {
 
618
  raise
619
  except Exception as e:
620
  raise HTTPException(status_code=500, detail=safe_str(e))
621
+
622
+ # --------------------------------------------------------
623
+ # Incident endpoints (direct create + status + description)
624
+ # --------------------------------------------------------
625
+ @app.post("/incident")
626
+ async def raise_incident(input_data: IncidentInput):
627
+ try:
628
+ result = create_incident(input_data.short_description, input_data.description)
629
+ if isinstance(result, dict) and not result.get("error"):
630
+ inc_number = result.get("number", "<unknown>")
631
+ return {
632
+ "bot_response": f"✅ Incident created: {inc_number}",
633
+ "debug": "Incident created via ServiceNow",
634
+ "persist": True,
635
+ "show_assist_card": True,
636
+ "followup": "Is there anything else I can assist you with?",
637
+ }
638
+ else:
639
+ err = (result or {}).get("error", "Unknown error")
640
+ return {
641
+ "bot_response": f"⚠️ Could not create incident ({err}).",
642
+ "status": "PARTIAL",
643
+ "suggest_incident": True,
644
+ }
645
+ except Exception as e:
646
+ raise HTTPException(status_code=500, detail=safe_str(e))
647
+
648
+ @app.post("/incident_status")
649
+ async def incident_status(input_data: TicketStatusInput):
650
+ try:
651
+ token = get_valid_token()
652
+ instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
653
+ if not instance_url:
654
+ raise HTTPException(status_code=500, detail="SERVICENOW_INSTANCE_URL missing")
655
+ headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
656
+ if input_data.sys_id:
657
+ url = f"{instance_url}/api/now/table/incident/{input_data.sys_id}"
658
+ response = requests.get(url, headers=headers, verify=VERIFY_SSL, timeout=25)
659
+ data = response.json()
660
+ result = data.get("result", {}) if response.status_code == 200 else {}
661
+ elif input_data.number:
662
+ url = f"{instance_url}/api/now/table/incident?number={input_data.number}"
663
+ response = requests.get(url, headers=headers, verify=VERIFY_SSL, timeout=25)
664
+ data = response.json()
665
+ lst = data.get("result", [])
666
+ result = (lst or [{}])[0] if response.status_code == 200 else {}
667
+ else:
668
+ raise HTTPException(status_code=400, detail="Provide IncidentID (number) or sys_id")
669
+ state_code = builtins.str(result.get("state", "unknown"))
670
+ state_map = {"1":"New","2":"In Progress","3":"On Hold","6":"Resolved","7":"Closed","8":"Canceled"}
671
+ state_label = state_map.get(state_code, state_code)
672
+ short = result.get("short_description", "")
673
+ number = result.get("number", input_data.number or "unknown")
674
+ return {
675
+ "bot_response": (
676
+ f"**Ticket:** {number}\n"
677
+ f"**Status:** {state_label}\n"
678
+ f"**Issue description:** {short}"
679
+ ).replace("\n", " \n"),
680
+ "followup": "Is there anything else I can assist you with?",
681
+ "show_assist_card": True,
682
+ "persist": True,
683
+ "debug": "Incident status fetched",
684
+ }
685
+ except Exception as e:
686
+ raise HTTPException(status_code=500, detail=safe_str(e))
687
+
688
+ @app.post("/generate_ticket_desc")
689
+ async def generate_ticket_desc_ep(input_data: TicketDescInput):
690
+ try:
691
+ # Minimal: echo structured JSON (no external LLM)
692
+ issue = (input_data.issue or "").strip()
693
+ short = issue[:100]
694
+ detailed = issue
695
+ return {
696
+ "ShortDescription": short,
697
+ "DetailedDescription": detailed,
698
+ }
699
+ except Exception as e:
700
+ raise HTTPException(status_code=500, detail=safe_str(e))