srilakshu012456 commited on
Commit
760864d
·
verified ·
1 Parent(s): 7a1faf2

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +58 -30
main.py CHANGED
@@ -3,7 +3,8 @@ import os
3
  import json
4
  import re
5
  import requests
6
- from typing import Optional, Any, Dict
 
7
  from contextlib import asynccontextmanager
8
  from fastapi import FastAPI, HTTPException
9
  from fastapi.middleware.cors import CORSMiddleware
@@ -23,6 +24,13 @@ from services.generate_ticket import get_valid_token, create_incident
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
  load_dotenv()
27
  os.environ["POSTHOG_DISABLED"] = "true"
28
 
@@ -36,7 +44,7 @@ async def lifespan(app: FastAPI):
36
  else:
37
  print(f"✅ KB already populated with {collection.count()} entries. Skipping ingestion.")
38
  except Exception as e:
39
- print(f"⚠️ KB ingestion failed: {e}")
40
  yield
41
 
42
  app = FastAPI(lifespan=lifespan)
@@ -152,7 +160,7 @@ def _build_clarifying_message() -> str:
152
  "Reply with these details and I’ll search again."
153
  )
154
 
155
- def _build_tracking_descriptions(issue_text: str, resolved_text: str) -> tuple[str, str]:
156
  issue = (issue_text or "").strip()
157
  resolved = (resolved_text or "").strip()
158
  short_desc = issue[:100] if issue else (resolved[:100] or "Issue resolved (user confirmation)")
@@ -222,9 +230,12 @@ def _classify_resolution_llm(user_message: str) -> bool:
222
  resp = requests.post(GEMINI_URL, headers=headers, json=payload, timeout=12, verify=GEMINI_SSL_VERIFY)
223
  data = resp.json()
224
  text = (
225
- data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "")
226
- ).strip().lower()
227
- return "true" in text
 
 
 
228
  except Exception:
229
  return False
230
 
@@ -248,11 +259,11 @@ def _normalize_for_match(text: str) -> str:
248
  t = re.sub(r"\s+", " ", t).strip()
249
  return t
250
 
251
- def _split_sentences(ctx: str) -> list[str]:
252
  raw_sents = re.split(r"(?<=[.!?])\s+|\n+|•\s*|-\s*", ctx or "")
253
  return [s.strip() for s in raw_sents if s and len(s.strip()) > 2]
254
 
255
- def _filter_context_for_query(context: str, query: str) -> tuple[str, dict]:
256
  ctx = (context or "").strip()
257
  if not ctx or not query:
258
  return ctx, {'mode': 'concise', 'matched_count': 0, 'all_sentences': 0}
@@ -303,7 +314,7 @@ ACTION_SYNS_FLAT = {
303
  "navigate": ["navigate", "go to", "open"],
304
  }
305
 
306
- def _action_in_line(ln: str, target_actions: list[str]) -> bool:
307
  s = (ln or "").lower()
308
  for act in target_actions:
309
  for syn in ACTION_SYNS_FLAT.get(act, [act]):
@@ -327,13 +338,12 @@ def _is_procedural_line(ln: str) -> bool:
327
  return True
328
  return False
329
 
330
- def _extract_steps_only(text: str, max_lines: int = 12, target_actions: list[str] | None = None) -> str:
331
  lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
332
  kept = []
333
  for ln in lines:
334
  if _is_procedural_line(ln):
335
- # If specific action requested (e.g., create), keep only lines containing that action
336
- if target_actions and len(target_actions) > 0:
337
  if not _action_in_line(ln, target_actions):
338
  continue
339
  kept.append(ln)
@@ -429,7 +439,7 @@ async def chat_with_ai(input_data: ChatInput):
429
  }
430
  except Exception as e:
431
  return {
432
- "bot_response": f"⚠️ Something went wrong while creating the tracking incident: {str(e)}",
433
  "status": "PARTIAL",
434
  "context_found": False,
435
  "ask_resolved": False,
@@ -505,12 +515,16 @@ async def chat_with_ai(input_data: ChatInput):
505
  data = response.json()
506
  lst = data.get("result", [])
507
  result = (lst or [{}])[0] if response.status_code == 200 else {}
508
- state_code = str(result.get("state", "unknown"))
509
  state_label = STATE_MAP.get(state_code, state_code)
510
  short = result.get("short_description", "")
511
  num = result.get("number", number or "unknown")
512
  return {
513
- "bot_response": (f"**Ticket:** {num} \n" f"**Status:** {state_label} \n" f"**Issue description:** {short}"),
 
 
 
 
514
  "status": "OK",
515
  "show_assist_card": True,
516
  "context_found": False,
@@ -522,7 +536,7 @@ async def chat_with_ai(input_data: ChatInput):
522
  "debug": {"intent": "status", "http_status": response.status_code},
523
  }
524
  except Exception as e:
525
- raise HTTPException(status_code=500, detail=str(e))
526
 
527
  # ---- Hybrid KB search ----
528
  kb_results = hybrid_search_knowledge_base(input_data.user_message, top_k=10, alpha=0.6, beta=0.4)
@@ -537,7 +551,6 @@ async def chat_with_ai(input_data: ChatInput):
537
  detected_intent = kb_results.get("user_intent", "neutral")
538
  actions = kb_results.get("actions", [])
539
 
540
- # Shape context by intent + action
541
  q = (input_data.user_message or "").lower()
542
  if detected_intent == "steps" or any(k in q for k in ["steps", "procedure", "perform", "do", "process"]):
543
  context = _extract_steps_only(context, max_lines=12, target_actions=actions)
@@ -546,7 +559,6 @@ async def chat_with_ai(input_data: ChatInput):
546
  elif any(k in q for k in ["navigate", "navigation", "menu", "screen"]):
547
  context = _extract_navigation_only(context, max_lines=6)
548
 
549
- # Gating
550
  short_query = len((input_data.user_message or "").split()) <= 4
551
  gate_combined_no_kb = 0.22 if short_query else 0.28
552
  gate_combined_ok = 0.60 if short_query else 0.55
@@ -574,12 +586,11 @@ async def chat_with_ai(input_data: ChatInput):
574
  "debug": {"used_chunks": 0, "second_try": second_try, "best_distance": best_distance, "best_combined": best_combined},
575
  }
576
 
577
- # LLM rewrite (optional, may rate-limit)
578
  enhanced_prompt = (
579
  "From the provided context, output only the actionable steps/procedure relevant to the user's question. "
580
  "Use ONLY the provided context; do NOT add information that is not present. "
581
- ("Return ONLY lines containing the requested action verbs." if actions else "")
582
- + " Do NOT include document names, section titles, or 'Source:' lines.\n\n"
583
  f"### Context\n{context}\n\n"
584
  f"### Question\n{input_data.user_message}\n\n"
585
  "### Output\n"
@@ -604,7 +615,7 @@ async def chat_with_ai(input_data: ChatInput):
604
  bot_text = ""
605
 
606
  if not bot_text.strip():
607
- bot_text = context # strict steps-only fallback
608
  bot_text = _strip_any_source_lines(bot_text).strip()
609
 
610
  status = "OK" if (
@@ -640,7 +651,8 @@ async def chat_with_ai(input_data: ChatInput):
640
  except HTTPException:
641
  raise
642
  except Exception as e:
643
- raise HTTPException(status_code=500, detail=str(e))
 
644
 
645
  def _set_incident_resolved(sys_id: str) -> bool:
646
  try:
@@ -668,7 +680,7 @@ def _set_incident_resolved(sys_id: str) -> bool:
668
  resp1 = requests.patch(url, headers=headers, json={"state": "2"}, verify=VERIFY_SSL, timeout=25)
669
  print(f"[SN PATCH progress] status={resp1.status_code} body={resp1.text[:500]}")
670
  except Exception as e:
671
- print(f"[SN PATCH progress] exception={str(e)}")
672
 
673
  def clean(d: dict) -> dict:
674
  return {k: v for k, v in d.items() if v is not None}
@@ -721,7 +733,7 @@ def _set_incident_resolved(sys_id: str) -> bool:
721
  print(f"[SN PATCH resolve C] status={respC.status_code} body={respC.text[:500]}")
722
  return False
723
  except Exception as e:
724
- print(f"[SN PATCH resolve] exception={str(e)}")
725
  return False
726
 
727
  @app.post("/incident")
@@ -746,7 +758,7 @@ async def raise_incident(input_data: IncidentInput):
746
  "followup": "Is there anything else I can assist you with?",
747
  }
748
  except Exception as e:
749
- raise HTTPException(status_code=500, detail=str(e))
750
 
751
  @app.post("/generate_ticket_desc")
752
  async def generate_ticket_desc_ep(input_data: TicketDescInput):
@@ -783,7 +795,7 @@ async def generate_ticket_desc_ep(input_data: TicketDescInput):
783
  except Exception:
784
  return {"ShortDescription": "", "DetailedDescription": "", "error": "Invalid JSON returned"}
785
  except Exception as e:
786
- raise HTTPException(status_code=500, detail=str(e))
787
 
788
  @app.post("/incident_status")
789
  async def incident_status(input_data: TicketStatusInput):
@@ -806,16 +818,32 @@ async def incident_status(input_data: TicketStatusInput):
806
  result = (lst or [{}])[0] if response.status_code == 200 else {}
807
  else:
808
  raise HTTPException(status_code=400, detail="Provide IncidentID (number) or sys_id")
809
- state_code = str(result.get("state", "unknown"))
810
  state_label = STATE_MAP.get(state_code, state_code)
811
  short = result.get("short_description", "")
812
  number = result.get("number", input_data.number or "unknown")
813
  return {
814
- "bot_response": (f"**Ticket:** {number} \n" f"**Status:** {state_label} \n" f"**Issue description:** {short}").replace("\n", " \n"),
 
 
 
 
815
  "followup": "Is there anything else I can assist you with?",
816
  "show_assist_card": True,
817
  "persist": True,
818
  "debug": "Incident status fetched",
819
  }
820
  except Exception as e:
821
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import json
4
  import re
5
  import requests
6
+ import builtins
7
+ from typing import Optional, Any, Dict, List
8
  from contextlib import asynccontextmanager
9
  from fastapi import FastAPI, HTTPException
10
  from fastapi.middleware.cors import CORSMiddleware
 
24
  VERIFY_SSL = os.getenv("SERVICENOW_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
25
  GEMINI_SSL_VERIFY = os.getenv("GEMINI_SSL_VERIFY", "true").lower() in ("1", "true", "yes")
26
 
27
+ # --- Safe stringify to avoid shadowing 'str' ---
28
+ def safe_str(e: Any) -> str:
29
+ try:
30
+ return builtins.str(e)
31
+ except Exception:
32
+ return "<error stringify failed>"
33
+
34
  load_dotenv()
35
  os.environ["POSTHOG_DISABLED"] = "true"
36
 
 
44
  else:
45
  print(f"✅ KB already populated with {collection.count()} entries. Skipping ingestion.")
46
  except Exception as e:
47
+ print(f"⚠️ KB ingestion failed: {safe_str(e)}")
48
  yield
49
 
50
  app = FastAPI(lifespan=lifespan)
 
160
  "Reply with these details and I’ll search again."
161
  )
162
 
163
+ def _build_tracking_descriptions(issue_text: str, resolved_text: str) -> Tuple[str, str]:
164
  issue = (issue_text or "").strip()
165
  resolved = (resolved_text or "").strip()
166
  short_desc = issue[:100] if issue else (resolved[:100] or "Issue resolved (user confirmation)")
 
230
  resp = requests.post(GEMINI_URL, headers=headers, json=payload, timeout=12, verify=GEMINI_SSL_VERIFY)
231
  data = resp.json()
232
  text = (
233
+ data.get("candidates", [{}])[0]
234
+ .get("content", {})
235
+ .get("parts", [{}])[0]
236
+ .get("text", "")
237
+ )
238
+ return "true" in (text or "").strip().lower()
239
  except Exception:
240
  return False
241
 
 
259
  t = re.sub(r"\s+", " ", t).strip()
260
  return t
261
 
262
+ def _split_sentences(ctx: str) -> List[str]:
263
  raw_sents = re.split(r"(?<=[.!?])\s+|\n+|•\s*|-\s*", ctx or "")
264
  return [s.strip() for s in raw_sents if s and len(s.strip()) > 2]
265
 
266
+ def _filter_context_for_query(context: str, query: str) -> Tuple[str, Dict[str, Any]]:
267
  ctx = (context or "").strip()
268
  if not ctx or not query:
269
  return ctx, {'mode': 'concise', 'matched_count': 0, 'all_sentences': 0}
 
314
  "navigate": ["navigate", "go to", "open"],
315
  }
316
 
317
+ def _action_in_line(ln: str, target_actions: List[str]) -> bool:
318
  s = (ln or "").lower()
319
  for act in target_actions:
320
  for syn in ACTION_SYNS_FLAT.get(act, [act]):
 
338
  return True
339
  return False
340
 
341
+ def _extract_steps_only(text: str, max_lines: int = 12, target_actions: Optional[List[str]] = None) -> str:
342
  lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
343
  kept = []
344
  for ln in lines:
345
  if _is_procedural_line(ln):
346
+ if target_actions:
 
347
  if not _action_in_line(ln, target_actions):
348
  continue
349
  kept.append(ln)
 
439
  }
440
  except Exception as e:
441
  return {
442
+ "bot_response": f"⚠️ Something went wrong while creating the tracking incident: {safe_str(e)}",
443
  "status": "PARTIAL",
444
  "context_found": False,
445
  "ask_resolved": False,
 
515
  data = response.json()
516
  lst = data.get("result", [])
517
  result = (lst or [{}])[0] if response.status_code == 200 else {}
518
+ state_code = builtins.str(result.get("state", "unknown"))
519
  state_label = STATE_MAP.get(state_code, state_code)
520
  short = result.get("short_description", "")
521
  num = result.get("number", number or "unknown")
522
  return {
523
+ "bot_response": (
524
+ f"**Ticket:** {num} \n"
525
+ f"**Status:** {state_label} \n"
526
+ f"**Issue description:** {short}"
527
+ ),
528
  "status": "OK",
529
  "show_assist_card": True,
530
  "context_found": False,
 
536
  "debug": {"intent": "status", "http_status": response.status_code},
537
  }
538
  except Exception as e:
539
+ raise HTTPException(status_code=500, detail=safe_str(e))
540
 
541
  # ---- Hybrid KB search ----
542
  kb_results = hybrid_search_knowledge_base(input_data.user_message, top_k=10, alpha=0.6, beta=0.4)
 
551
  detected_intent = kb_results.get("user_intent", "neutral")
552
  actions = kb_results.get("actions", [])
553
 
 
554
  q = (input_data.user_message or "").lower()
555
  if detected_intent == "steps" or any(k in q for k in ["steps", "procedure", "perform", "do", "process"]):
556
  context = _extract_steps_only(context, max_lines=12, target_actions=actions)
 
559
  elif any(k in q for k in ["navigate", "navigation", "menu", "screen"]):
560
  context = _extract_navigation_only(context, max_lines=6)
561
 
 
562
  short_query = len((input_data.user_message or "").split()) <= 4
563
  gate_combined_no_kb = 0.22 if short_query else 0.28
564
  gate_combined_ok = 0.60 if short_query else 0.55
 
586
  "debug": {"used_chunks": 0, "second_try": second_try, "best_distance": best_distance, "best_combined": best_combined},
587
  }
588
 
 
589
  enhanced_prompt = (
590
  "From the provided context, output only the actionable steps/procedure relevant to the user's question. "
591
  "Use ONLY the provided context; do NOT add information that is not present. "
592
+ + ("Return ONLY lines containing the requested action verbs. " if actions else "")
593
+ + "Do NOT include document names, section titles, or 'Source:' lines.\n\n"
594
  f"### Context\n{context}\n\n"
595
  f"### Question\n{input_data.user_message}\n\n"
596
  "### Output\n"
 
615
  bot_text = ""
616
 
617
  if not bot_text.strip():
618
+ bot_text = context
619
  bot_text = _strip_any_source_lines(bot_text).strip()
620
 
621
  status = "OK" if (
 
651
  except HTTPException:
652
  raise
653
  except Exception as e:
654
+ # Ensure we never crash due to shadowed str()
655
+ raise HTTPException(status_code=500, detail=safe_str(e))
656
 
657
  def _set_incident_resolved(sys_id: str) -> bool:
658
  try:
 
680
  resp1 = requests.patch(url, headers=headers, json={"state": "2"}, verify=VERIFY_SSL, timeout=25)
681
  print(f"[SN PATCH progress] status={resp1.status_code} body={resp1.text[:500]}")
682
  except Exception as e:
683
+ print(f"[SN PATCH progress] exception={safe_str(e)}")
684
 
685
  def clean(d: dict) -> dict:
686
  return {k: v for k, v in d.items() if v is not None}
 
733
  print(f"[SN PATCH resolve C] status={respC.status_code} body={respC.text[:500]}")
734
  return False
735
  except Exception as e:
736
+ print(f"[SN PATCH resolve] exception={safe_str(e)}")
737
  return False
738
 
739
  @app.post("/incident")
 
758
  "followup": "Is there anything else I can assist you with?",
759
  }
760
  except Exception as e:
761
+ raise HTTPException(status_code=500, detail=safe_str(e))
762
 
763
  @app.post("/generate_ticket_desc")
764
  async def generate_ticket_desc_ep(input_data: TicketDescInput):
 
795
  except Exception:
796
  return {"ShortDescription": "", "DetailedDescription": "", "error": "Invalid JSON returned"}
797
  except Exception as e:
798
+ raise HTTPException(status_code=500, detail=safe_str(e))
799
 
800
  @app.post("/incident_status")
801
  async def incident_status(input_data: TicketStatusInput):
 
818
  result = (lst or [{}])[0] if response.status_code == 200 else {}
819
  else:
820
  raise HTTPException(status_code=400, detail="Provide IncidentID (number) or sys_id")
821
+ state_code = builtins.str(result.get("state", "unknown"))
822
  state_label = STATE_MAP.get(state_code, state_code)
823
  short = result.get("short_description", "")
824
  number = result.get("number", input_data.number or "unknown")
825
  return {
826
+ "bot_response": (
827
+ f"**Ticket:** {number} \n"
828
+ f"**Status:** {state_label} \n"
829
+ f"**Issue description:** {short}"
830
+ ).replace("\n", " \n"),
831
  "followup": "Is there anything else I can assist you with?",
832
  "show_assist_card": True,
833
  "persist": True,
834
  "debug": "Incident status fetched",
835
  }
836
  except Exception as e:
837
+ raise HTTPException(status_code=500, detail=safe_str(e))
838
+
839
+ # ---- Admin (optional) ----
840
+ @app.get("/kb/info")
841
+ async def kb_info():
842
+ from services.kb_creation import get_kb_runtime_info
843
+ return get_kb_runtime_info()
844
+
845
+ @app.post("/kb/reset")
846
+ async def kb_reset():
847
+ from services.kb_creation import reset_kb
848
+ folder_path = os.path.join(os.getcwd(), "documents")
849
+ return reset_kb(folder_path)