srilakshu012456 commited on
Commit
8bc4493
·
verified ·
1 Parent(s): bbd9f1f

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +57 -32
main.py CHANGED
@@ -10,7 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
10
  from pydantic import BaseModel
11
  from dotenv import load_dotenv
12
  from datetime import datetime
13
-
14
  # KB services (Chroma + sentence-transformers + BM25 hybrid)
15
  from services.kb_creation import (
16
  collection,
@@ -290,6 +290,20 @@ def _classify_resolution_llm(user_message: str) -> bool:
290
  except Exception:
291
  return False
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  # ---------------------- Health ----------------------
294
  @app.get("/")
295
  async def health_check():
@@ -303,10 +317,10 @@ async def chat_with_ai(input_data: ChatInput):
303
  A) If 'resolved/working' detected → auto-create tracking incident, auto-mark Resolved, and ask if further help needed.
304
  B) If user intent is 'create incident/ticket' → SHOW INCIDENT FORM (ask Short & Long description). No Yes/No confirmation here.
305
  C) Ticket status intent → if number missing, ask for it; else return status from ServiceNow.
306
- D) Feedback-only → first ask clarifying details; after user shares details, try KB again.
 
307
  If still no context after clarification → suggest incident ONCE (Yes/No).
308
  E) Otherwise, use HYBRID search; rewrite answer from KB only; ask resolved if 'OK', or refine if 'PARTIAL'.
309
-
310
  """
311
  try:
312
  msg_norm = (input_data.user_message or "").lower().strip()
@@ -392,7 +406,7 @@ async def chat_with_ai(input_data: ChatInput):
392
  # --- (B) Incident intent
393
  if _is_incident_intent(msg_norm):
394
  return {
395
- "bot_response":(
396
  "Okay, let’s create a ServiceNow incident.\n\n"
397
  "Please provide:\n• Short Description (one line)\n• Detailed Description (steps, error text, IDs, site, environment)"
398
  ),
@@ -400,13 +414,34 @@ async def chat_with_ai(input_data: ChatInput):
400
  "context_found": False,
401
  "ask_resolved": False,
402
  "suggest_incident": False,
403
- "show_incident_form": True,
404
  "followup": None,
405
  "top_hits": [],
406
  "sources": [],
407
  "debug": {"intent": "create_ticket"},
408
  }
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  # --- (C) Ticket status intent
411
  status_intent = _parse_ticket_status_intent(msg_norm)
412
  if status_intent:
@@ -418,6 +453,7 @@ async def chat_with_ai(input_data: ChatInput):
418
  "ask_resolved": False,
419
  "suggest_incident": False,
420
  "followup": "Provide the Incident ID and I’ll fetch the status.",
 
421
  "top_hits": [],
422
  "sources": [],
423
  "debug": {"intent": "status_request_missing_id"},
@@ -441,7 +477,6 @@ async def chat_with_ai(input_data: ChatInput):
441
  short = result.get("short_description", "")
442
  num = result.get("number", number or "unknown")
443
 
444
- # ✅ Use bold labels + hard line breaks so lines appear separately in Markdown
445
  return {
446
  "bot_response": (
447
  f"**Ticket:** {num} \n"
@@ -449,7 +484,7 @@ async def chat_with_ai(input_data: ChatInput):
449
  f"**Issue description:** {short}"
450
  ),
451
  "status": "OK",
452
- "show_assist_card": True, # <— triggers Yes/No card
453
  "context_found": False,
454
  "ask_resolved": False,
455
  "suggest_incident": False,
@@ -477,7 +512,7 @@ async def chat_with_ai(input_data: ChatInput):
477
  "status": "NO_KB_MATCH",
478
  "context_found": False,
479
  "ask_resolved": False,
480
- "suggest_incident": bool(second_try),
481
  "followup": ("Please reply with the above details." if not second_try else "Shall I create a ticket now?"),
482
  "top_hits": [],
483
  "sources": [],
@@ -492,11 +527,11 @@ async def chat_with_ai(input_data: ChatInput):
492
  best_distance = kb_ctx.get("best_score") # lower = better
493
  best_combined = kb_ctx.get("best_combined") # higher = better
494
 
495
- # Dynamic gating based on combined score primarily, distance secondarily
496
  short_query = len((input_data.user_message or "").split()) <= 4
497
  gate_combined_no_kb = 0.22 if short_query else 0.28 # below this → NO_KB
498
  gate_combined_ok = 0.60 if short_query else 0.55 # above this → OK
499
- gate_distance_no_kb = 2.0 # if semantic distance is very high, treat carefully
500
 
501
  if (not context_found or not context.strip()) or (
502
  (best_combined is None or best_combined < gate_combined_no_kb)
@@ -521,7 +556,7 @@ async def chat_with_ai(input_data: ChatInput):
521
  }
522
 
523
  # We have KB context → LLM rewrite (KB‑only, no Source lines)
524
- threshold_ok = gate_combined_ok # use combined score as the primary signal
525
  enhanced_prompt = (
526
  "Rewrite the following knowledge base context into clear, actionable steps for the user's question. "
527
  "Use ONLY the provided context; do NOT add information that is not present in it. "
@@ -601,9 +636,7 @@ def _set_incident_resolved(sys_id: str) -> bool:
601
  Logs a short diagnostic line on failure so we can see the exact reason.
602
  """
603
  try:
604
- # 1) Get an OAuth token the same way you already do for other ServiceNow calls
605
  token = get_valid_token()
606
- # 2) Read your instance URL from env
607
  instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
608
  if not instance_url:
609
  print("[SN PATCH resolve] missing SERVICENOW_INSTANCE_URL")
@@ -623,36 +656,33 @@ def _set_incident_resolved(sys_id: str) -> bool:
623
  assign_group = os.getenv("SERVICENOW_ASSIGNMENT_GROUP_SYSID") # optional
624
  require_progress = os.getenv("SERVICENOW_REQUIRE_IN_PROGRESS_FIRST", "false").lower() in ("1", "true", "yes")
625
 
626
- # Optional pre-step: set state=2 (In Progress) if your org enforces transitions
627
  if require_progress:
628
  try:
629
- resp1 = requests.patch(url, headers=headers, json={"state": "2"}, verify=VERIFY_SSL, timeout=25) # <-- use VERIFY_SSL
630
  print(f"[SN PATCH progress] status={resp1.status_code} body={resp1.text[:500]}")
631
  except Exception as e:
632
  print(f"[SN PATCH progress] exception={str(e)}")
633
 
634
- # Helper to clean payload
635
  def clean(d: dict) -> dict:
636
  return {k: v for k, v in d.items() if v is not None}
637
 
638
  payload_A = clean({
639
- "state": "6", # numeric Resolved
640
- "close_code": close_code_val, # Resolution code (default field name)
641
- "close_notes": close_notes_val, # Resolution notes (default field name)
642
- "caller_id": caller_sysid, # mandatory in your org
643
  "resolved_at": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
644
  "work_notes": "Auto-resolve set by NOVA.",
645
- "resolved_by": resolved_by_sysid, # optional if BR requires who resolved
646
- "assignment_group": assign_group, # optional if BR requires a group
647
  })
648
  respA = requests.patch(url, headers=headers, json=payload_A, verify=VERIFY_SSL, timeout=25)
649
  if respA.status_code in (200, 204):
650
  return True
651
  print(f"[SN PATCH resolve A] status={respA.status_code} body={respA.text[:500]}")
652
 
653
- # ---- B) PATCH with state label (some orgs map differently)
654
  payload_B = clean({
655
- "state": "Resolved", # label instead of numeric
656
  "close_code": close_code_val,
657
  "close_notes": close_notes_val,
658
  "caller_id": caller_sysid,
@@ -666,8 +696,6 @@ def _set_incident_resolved(sys_id: str) -> bool:
666
  return True
667
  print(f"[SN PATCH resolve B] status={respB.status_code} body={respB.text[:500]}")
668
 
669
- # ---- C) PATCH using custom field names from env
670
- # If your dictionary uses, for example, u_resolution_code/u_resolution_notes
671
  code_field = os.getenv("SERVICENOW_RESOLUTION_CODE_FIELD", "close_code")
672
  notes_field = os.getenv("SERVICENOW_RESOLUTION_NOTES_FIELD", "close_notes")
673
  payload_C = clean({
@@ -685,7 +713,6 @@ def _set_incident_resolved(sys_id: str) -> bool:
685
  return True
686
  print(f"[SN PATCH resolve C] status={respC.status_code} body={respC.text[:500]}")
687
 
688
- # If all attempts failed, return False so the chat shows the soft message
689
  return False
690
  except Exception as e:
691
  print(f"[SN PATCH resolve] exception={str(e)}")
@@ -707,8 +734,7 @@ async def raise_incident(input_data: IncidentInput):
707
  else:
708
  ticket_text = "Incident created."
709
 
710
- # ✅ Do NOT include follow-up question inside bot_response to avoid duplication in UI.
711
- # The front-end will show a Yes/No assist card after this.
712
  return {
713
  "bot_response": f"✅ {ticket_text}",
714
  "debug": "Incident created via ServiceNow",
@@ -793,15 +819,14 @@ async def incident_status(input_data: TicketStatusInput):
793
  short = result.get("short_description", "")
794
  number = result.get("number", input_data.number or "unknown")
795
 
796
- # ✅ Bold labels + hard line breaks so ReactMarkdown renders on separate lines
797
  return {
798
  "bot_response": (
799
  f"**Ticket:** {number} \n"
800
  f"**Status:** {state_label} \n"
801
  f"**Issue description:** {short}"
802
- ).replace("\n", " \n"), # ensure hard-breaks
803
  "followup": "Is there anything else I can assist you with?",
804
- "show_assist_card": True, # <— show Yes/No card from UI after status
805
  "persist": True,
806
  "debug": "Incident status fetched",
807
  }
 
10
  from pydantic import BaseModel
11
  from dotenv import load_dotenv
12
  from datetime import datetime
13
+ #updated
14
  # KB services (Chroma + sentence-transformers + BM25 hybrid)
15
  from services.kb_creation import (
16
  collection,
 
290
  except Exception:
291
  return False
292
 
293
+
294
+ def _is_generic_issue(msg_norm: str) -> bool:
295
+ """
296
+ Returns True for very generic/open-ended issue statements that
297
+ shouldn’t trigger a KB search yet.
298
+ """
299
+ generic_phrases = [
300
+ "issue", "have an issue", "having an issue", "got an issue",
301
+ "problem", "have a problem", "help", "need help", "support",
302
+ "need support", "please help", "need assistance", "assist me",
303
+ "facing issue", "facing a problem", "got a problem"
304
+ ]
305
+ return any(p == msg_norm or p in msg_norm for p in generic_phrases) or len(msg_norm.split()) <= 2
306
+
307
  # ---------------------- Health ----------------------
308
  @app.get("/")
309
  async def health_check():
 
317
  A) If 'resolved/working' detected → auto-create tracking incident, auto-mark Resolved, and ask if further help needed.
318
  B) If user intent is 'create incident/ticket' → SHOW INCIDENT FORM (ask Short & Long description). No Yes/No confirmation here.
319
  C) Ticket status intent → if number missing, ask for it; else return status from ServiceNow.
320
+ D) Generic issue/openers → ask clarifying details first (no KB yet).
321
+ Feedback-only → first ask clarifying details; after user shares details, try KB again.
322
  If still no context after clarification → suggest incident ONCE (Yes/No).
323
  E) Otherwise, use HYBRID search; rewrite answer from KB only; ask resolved if 'OK', or refine if 'PARTIAL'.
 
324
  """
325
  try:
326
  msg_norm = (input_data.user_message or "").lower().strip()
 
406
  # --- (B) Incident intent
407
  if _is_incident_intent(msg_norm):
408
  return {
409
+ "bot_response": (
410
  "Okay, let’s create a ServiceNow incident.\n\n"
411
  "Please provide:\n• Short Description (one line)\n• Detailed Description (steps, error text, IDs, site, environment)"
412
  ),
 
414
  "context_found": False,
415
  "ask_resolved": False,
416
  "suggest_incident": False,
417
+ "show_incident_form": True,
418
  "followup": None,
419
  "top_hits": [],
420
  "sources": [],
421
  "debug": {"intent": "create_ticket"},
422
  }
423
 
424
+ # --- (B.1) Generic issue/open-ended messages → ask for details first
425
+ if _is_generic_issue(msg_norm):
426
+ return {
427
+ "bot_response": (
428
+ "Sure, I can help. Please describe your issue:\n"
429
+ "• Module/area (e.g., Picking, Receiving, Trailer Close)\n"
430
+ "• Exact error message text/code (copy-paste)\n"
431
+ "• IDs involved (Order#, Load ID, Shipment#)\n"
432
+ "• Warehouse/site & environment (prod/test)\n"
433
+ "• When it started and how many users are impacted"
434
+ ),
435
+ "status": "NO_KB_MATCH",
436
+ "context_found": False,
437
+ "ask_resolved": False,
438
+ "suggest_incident": False,
439
+ "followup": "Please reply with the above details.",
440
+ "top_hits": [],
441
+ "sources": [],
442
+ "debug": {"intent": "generic_issue"},
443
+ }
444
+
445
  # --- (C) Ticket status intent
446
  status_intent = _parse_ticket_status_intent(msg_norm)
447
  if status_intent:
 
453
  "ask_resolved": False,
454
  "suggest_incident": False,
455
  "followup": "Provide the Incident ID and I’ll fetch the status.",
456
+ "show_status_form": True, # helps front-end show the status input card
457
  "top_hits": [],
458
  "sources": [],
459
  "debug": {"intent": "status_request_missing_id"},
 
477
  short = result.get("short_description", "")
478
  num = result.get("number", number or "unknown")
479
 
 
480
  return {
481
  "bot_response": (
482
  f"**Ticket:** {num} \n"
 
484
  f"**Issue description:** {short}"
485
  ),
486
  "status": "OK",
487
+ "show_assist_card": True, # show Yes/No card so user can continue
488
  "context_found": False,
489
  "ask_resolved": False,
490
  "suggest_incident": False,
 
512
  "status": "NO_KB_MATCH",
513
  "context_found": False,
514
  "ask_resolved": False,
515
+ "suggest_incident": bool(second_try), # on second try → show Yes/No card
516
  "followup": ("Please reply with the above details." if not second_try else "Shall I create a ticket now?"),
517
  "top_hits": [],
518
  "sources": [],
 
527
  best_distance = kb_ctx.get("best_score") # lower = better
528
  best_combined = kb_ctx.get("best_combined") # higher = better
529
 
530
+ # Dynamic gating
531
  short_query = len((input_data.user_message or "").split()) <= 4
532
  gate_combined_no_kb = 0.22 if short_query else 0.28 # below this → NO_KB
533
  gate_combined_ok = 0.60 if short_query else 0.55 # above this → OK
534
+ gate_distance_no_kb = 2.0
535
 
536
  if (not context_found or not context.strip()) or (
537
  (best_combined is None or best_combined < gate_combined_no_kb)
 
556
  }
557
 
558
  # We have KB context → LLM rewrite (KB‑only, no Source lines)
559
+ threshold_ok = gate_combined_ok
560
  enhanced_prompt = (
561
  "Rewrite the following knowledge base context into clear, actionable steps for the user's question. "
562
  "Use ONLY the provided context; do NOT add information that is not present in it. "
 
636
  Logs a short diagnostic line on failure so we can see the exact reason.
637
  """
638
  try:
 
639
  token = get_valid_token()
 
640
  instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
641
  if not instance_url:
642
  print("[SN PATCH resolve] missing SERVICENOW_INSTANCE_URL")
 
656
  assign_group = os.getenv("SERVICENOW_ASSIGNMENT_GROUP_SYSID") # optional
657
  require_progress = os.getenv("SERVICENOW_REQUIRE_IN_PROGRESS_FIRST", "false").lower() in ("1", "true", "yes")
658
 
 
659
  if require_progress:
660
  try:
661
+ resp1 = requests.patch(url, headers=headers, json={"state": "2"}, verify=VERIFY_SSL, timeout=25)
662
  print(f"[SN PATCH progress] status={resp1.status_code} body={resp1.text[:500]}")
663
  except Exception as e:
664
  print(f"[SN PATCH progress] exception={str(e)}")
665
 
 
666
  def clean(d: dict) -> dict:
667
  return {k: v for k, v in d.items() if v is not None}
668
 
669
  payload_A = clean({
670
+ "state": "6",
671
+ "close_code": close_code_val,
672
+ "close_notes": close_notes_val,
673
+ "caller_id": caller_sysid,
674
  "resolved_at": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
675
  "work_notes": "Auto-resolve set by NOVA.",
676
+ "resolved_by": resolved_by_sysid,
677
+ "assignment_group": assign_group,
678
  })
679
  respA = requests.patch(url, headers=headers, json=payload_A, verify=VERIFY_SSL, timeout=25)
680
  if respA.status_code in (200, 204):
681
  return True
682
  print(f"[SN PATCH resolve A] status={respA.status_code} body={respA.text[:500]}")
683
 
 
684
  payload_B = clean({
685
+ "state": "Resolved",
686
  "close_code": close_code_val,
687
  "close_notes": close_notes_val,
688
  "caller_id": caller_sysid,
 
696
  return True
697
  print(f"[SN PATCH resolve B] status={respB.status_code} body={respB.text[:500]}")
698
 
 
 
699
  code_field = os.getenv("SERVICENOW_RESOLUTION_CODE_FIELD", "close_code")
700
  notes_field = os.getenv("SERVICENOW_RESOLUTION_NOTES_FIELD", "close_notes")
701
  payload_C = clean({
 
713
  return True
714
  print(f"[SN PATCH resolve C] status={respC.status_code} body={respC.text[:500]}")
715
 
 
716
  return False
717
  except Exception as e:
718
  print(f"[SN PATCH resolve] exception={str(e)}")
 
734
  else:
735
  ticket_text = "Incident created."
736
 
737
+ # Do NOT include follow-up question inside bot_response to avoid duplication in UI.
 
738
  return {
739
  "bot_response": f"✅ {ticket_text}",
740
  "debug": "Incident created via ServiceNow",
 
819
  short = result.get("short_description", "")
820
  number = result.get("number", input_data.number or "unknown")
821
 
 
822
  return {
823
  "bot_response": (
824
  f"**Ticket:** {number} \n"
825
  f"**Status:** {state_label} \n"
826
  f"**Issue description:** {short}"
827
+ ).replace("\n", " \n"), # hard breaks for ReactMarkdown
828
  "followup": "Is there anything else I can assist you with?",
829
+ "show_assist_card": True, # Yes/No card so user can continue
830
  "persist": True,
831
  "debug": "Incident status fetched",
832
  }