srilakshu012456 commited on
Commit
4bd1f48
·
verified ·
1 Parent(s): fef56e9

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +60 -115
main.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import json
4
  import re
@@ -98,10 +97,9 @@ def _format_steps_markdown(lines: List[str]) -> str:
98
  s = (ln or "").strip()
99
  if not s:
100
  continue
101
- s = re.sub(r"^\s*(?:\d+[\.\)]\s+|[\-]\s+)", "", s).strip()
102
  items.append(f"{i}. {s}")
103
- return "
104
- ".join(items).strip()
105
 
106
  def _as_numbered_steps(text: str) -> str:
107
  raw_lines: List[str] = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
@@ -122,24 +120,16 @@ def _as_numbered_steps(text: str) -> str:
122
  i += 1
123
  return _format_steps_markdown(merged)
124
 
125
- # ---------------- Clarifying message (with ticket option) ----------------
126
  def _build_clarifying_message() -> str:
127
  return (
128
- "I couldnt find matching content in the KB yet. To help me narrow it down, please share:
129
-
130
- "
131
- " Module/area (e.g., Picking, Receiving, Trailer Close)
132
- "
133
- " Exact error message text/code (copy-paste)
134
- "
135
- "• IDs involved (Order#, Load ID, Shipment#)
136
- "
137
- "• Warehouse/site & environment (prod/test)
138
- "
139
- "• When it started and how many users are impacted
140
-
141
- "
142
- "You can also say ‘create ticket’ and I’ll raise a ServiceNow incident now."
143
  )
144
 
145
  # ---------------- Resolution/Incident helpers ----------------
@@ -148,9 +138,9 @@ def _build_tracking_descriptions(issue_text: str, resolved_text: str) -> Tuple[s
148
  resolved = (resolved_text or "").strip()
149
  short_desc = issue[:100] if issue else (resolved[:100] or "Issue resolved (user confirmation)")
150
  long_desc = (
151
- f'User reported: "{issue}". '
152
- f'User confirmation: "{resolved}". '
153
- f'Tracking record created automatically by NOVA.'
154
  ).strip()
155
  return short_desc, long_desc
156
 
@@ -194,14 +184,9 @@ def _has_negation_resolved(msg_norm: str) -> bool:
194
  def _classify_resolution_llm(user_message: str) -> bool:
195
  if not GEMINI_API_KEY:
196
  return False
197
- prompt = (
198
- "Classify if the following user message indicates that the issue is resolved or working now.
199
- "
200
- "Return only 'true' or 'false'.
201
-
202
- "
203
- f"Message: {user_message}"
204
- )
205
  headers = {"Content-Type": "application/json"}
206
  payload = {"contents": [{"parts": [{"text": prompt}]}]}
207
  try:
@@ -229,8 +214,8 @@ def _normalize_for_match(text: str) -> str:
229
  return t
230
 
231
  def _split_sentences(ctx: str) -> List[str]:
232
- raw_sents = re.split(r"(?<=[.!?])\s+|
233
- +|\s*|\-\s*", ctx or "")
234
  return [s.strip() for s in raw_sents if s and len(s.strip()) > 2]
235
 
236
  def _filter_context_for_query(context: str, query: str) -> Tuple[str, Dict[str, Any]]:
@@ -245,7 +230,7 @@ def _filter_context_for_query(context: str, query: str) -> Tuple[str, Dict[str,
245
  matched_exact, matched_any = [], []
246
  for s in sentences:
247
  s_norm = _normalize_for_match(s)
248
- is_bullet = bool(re.match(r"^[\-\*]\s*", s))
249
  overlap = sum(1 for t in q_terms if t in s_norm) + (1 if is_bullet else 0)
250
  if overlap >= STRICT_OVERLAP:
251
  matched_exact.append(s)
@@ -253,15 +238,12 @@ def _filter_context_for_query(context: str, query: str) -> Tuple[str, Dict[str,
253
  matched_any.append(s)
254
  if matched_exact:
255
  kept = matched_exact[:MAX_SENTENCES_STRICT]
256
- return "
257
- ".join(kept).strip(), {'mode': 'exact', 'matched_count': len(kept), 'all_sentences': len(sentences)}
258
  if matched_any:
259
  kept = matched_any[:MAX_SENTENCES_CONCISE]
260
- return "
261
- ".join(kept).strip(), {'mode': 'concise', 'matched_count': len(kept), 'all_sentences': len(sentences)}
262
  kept = sentences[:MAX_SENTENCES_CONCISE]
263
- return "
264
- ".join(kept).strip(), {'mode': 'concise', 'matched_count': 0, 'all_sentences': len(sentences)}
265
 
266
  # ---------------- Navigation extraction ----------------
267
  NAV_LINE_REGEX = re.compile(r"(navigate\s+to|login|log in|menu|screen)", re.IGNORECASE)
@@ -270,14 +252,13 @@ def _extract_navigation_only(text: str, max_lines: int = 6) -> str:
270
  lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
271
  kept: List[str] = []
272
  for ln in lines:
273
- if NAV_LINE_REGEX.search(ln):
274
  kept.append(ln)
275
  if len(kept) >= max_lines:
276
  break
277
- return "
278
- ".join(kept).strip() if kept else (text or "").strip()
279
 
280
- # ---------------- Errors extraction (tightened for auth/role/access) ----------------
281
  ERROR_STARTS = (
282
  "error", "resolution", "fix", "verify", "check",
283
  "permission", "access", "authorization", "authorisation",
@@ -293,8 +274,7 @@ def _extract_errors_only(text: str, max_lines: int = 12) -> str:
293
  kept.append(ln)
294
  if len(kept) >= max_lines:
295
  break
296
- return "
297
- ".join(kept).strip() if kept else (text or "").strip()
298
 
299
  @app.get("/")
300
  async def health_check():
@@ -308,7 +288,7 @@ async def chat_with_ai(input_data: ChatInput):
308
  # --- Yes/No handlers ---
309
  if msg_norm in ("yes", "y", "sure", "ok", "okay"):
310
  return {
311
- "bot_response": ("Great! Tell me what youd like to do next — check another ticket, create an incident, or describe your issue."),
312
  "status": "OK",
313
  "followup": "You can say: 'create ticket', 'incident status INC0012345', or describe your problem.",
314
  "options": [],
@@ -354,7 +334,7 @@ async def chat_with_ai(input_data: ChatInput):
354
  else:
355
  err = (result or {}).get("error", "Unknown error")
356
  return {
357
- "bot_response": f"⚠️ I couldnt create the tracking incident automatically ({err}).",
358
  "status": "PARTIAL",
359
  "context_found": False,
360
  "ask_resolved": False,
@@ -381,13 +361,9 @@ async def chat_with_ai(input_data: ChatInput):
381
  if _is_incident_intent(msg_norm):
382
  return {
383
  "bot_response": (
384
- "Okay, lets create a ServiceNow incident.
385
-
386
- "
387
- "Please provide:
388
- • Short Description (one line)
389
- "
390
- "• Detailed Description (steps, error text, IDs, site, environment)"
391
  ),
392
  "status": (input_data.prev_status or "PARTIAL"),
393
  "context_found": False,
@@ -407,8 +383,8 @@ async def chat_with_ai(input_data: ChatInput):
407
  "status": "NO_KB_MATCH",
408
  "context_found": False,
409
  "ask_resolved": False,
410
- "suggest_incident": True, # offer ticket immediately
411
- "followup": "Reply with the above details or say create ticket.",
412
  "top_hits": [],
413
  "sources": [],
414
  "debug": {"intent": "generic_issue"},
@@ -424,7 +400,7 @@ async def chat_with_ai(input_data: ChatInput):
424
  "context_found": False,
425
  "ask_resolved": False,
426
  "suggest_incident": False,
427
- "followup": "Provide the Incident ID and Ill fetch the status.",
428
  "show_status_form": True,
429
  "top_hits": [],
430
  "sources": [],
@@ -448,10 +424,8 @@ async def chat_with_ai(input_data: ChatInput):
448
  num = result.get("number", number or "unknown")
449
  return {
450
  "bot_response": (
451
- f"**Ticket:** {num}
452
- "
453
- f"**Status:** {state_label}
454
- "
455
  f"**Issue description:** {short}"
456
  ),
457
  "status": "OK",
@@ -489,11 +463,7 @@ async def chat_with_ai(input_data: ChatInput):
489
  m["combined"] = comb
490
  items.append({"text": text, "meta": m})
491
  selected = items[:max(1, 2)]
492
- context_raw = "
493
-
494
- ---
495
-
496
- ".join([s["text"] for s in selected]) if selected else ""
497
  filtered_text, filt_info = _filter_context_for_query(context_raw, input_data.user_message)
498
  context = filtered_text
499
  context_found = bool(context.strip())
@@ -538,7 +508,7 @@ async def chat_with_ai(input_data: ChatInput):
538
  clarify_text = (
539
  _build_clarifying_message()
540
  if not second_try
541
- else "I still dont find a relevant KB match for this scenario even after clarification."
542
  )
543
  return {
544
  "bot_response": clarify_text,
@@ -546,33 +516,24 @@ async def chat_with_ai(input_data: ChatInput):
546
  "context_found": False,
547
  "ask_resolved": False,
548
  "suggest_incident": True,
549
- "followup": ("Reply with the above details or say create ticket." if not second_try else "Shall I create a ticket now?"),
550
  "top_hits": [],
551
  "sources": [],
552
  "debug": {"used_chunks": 0, "second_try": second_try, "best_distance": best_distance, "best_combined": best_combined},
553
  }
554
 
555
  # LLM rewrite (constrained to provided context)
556
- enhanced_prompt = (
557
- "From the provided context, output only the actionable content relevant to the user's question. "
558
- "Use ONLY the provided context; do NOT add information that is not present.
559
-
560
- "
561
- f"### Context
562
  {context}
563
 
564
- "
565
- f"### Question
566
  {input_data.user_message}
567
 
568
- "
569
- "### Output
570
- "
571
- "- Return numbered/bulleted steps in the same order when appropriate.
572
- "
573
- "- If context is insufficient, add: 'This may be partial based on available KB.'
574
- "
575
- )
576
  headers = {"Content-Type": "application/json"}
577
  payload = {"contents": [{"parts": [{"text": enhanced_prompt}]}]}
578
  try:
@@ -585,13 +546,12 @@ async def chat_with_ai(input_data: ChatInput):
585
  resp = type("RespStub", (), {"status_code": 0})()
586
  result = {}
587
  try:
588
- bot_text = result["candidates"][0]["content"]["parts"][0]["text"] if isinstance(result, dict) else ""
589
  except Exception:
590
  bot_text = ""
591
  if not bot_text.strip():
592
  bot_text = context
593
- bot_text = "
594
- ".join([ln for ln in bot_text.splitlines() if not re.match(r"^\s*source\s*:", ln, flags=re.IGNORECASE)]).strip()
595
 
596
  if detected_intent == "steps":
597
  bot_text = _as_numbered_steps(bot_text)
@@ -615,11 +575,7 @@ async def chat_with_ai(input_data: ChatInput):
615
  "top_hits": [],
616
  "sources": [],
617
  "debug": {
618
- "used_chunks": len(context.split("
619
-
620
- ---
621
-
622
- ")) if context else 0,
623
  "best_distance": best_distance,
624
  "best_combined": best_combined,
625
  "http_status": getattr(resp, "status_code", 0),
@@ -745,18 +701,12 @@ async def raise_incident(input_data: IncidentInput):
745
  async def generate_ticket_desc_ep(input_data: TicketDescInput):
746
  try:
747
  prompt = (
748
- f"You are helping generate ServiceNow ticket descriptions based on the issue: {input_data.issue}.
749
- "
750
- "Please return the output strictly in JSON format with the following keys:
751
- "
752
- "{
753
- "
754
- ' "ShortDescription": "A concise summary of the issue (max 100 characters)",
755
- '
756
- ' "DetailedDescription": "A detailed explanation of the issue"
757
- '
758
- "}
759
- "
760
  "Do not include any extra text, comments, or explanations outside the JSON."
761
  )
762
  headers = {"Content-Type": "application/json"}
@@ -767,13 +717,12 @@ async def generate_ticket_desc_ep(input_data: TicketDescInput):
767
  except Exception:
768
  return {"ShortDescription": "", "DetailedDescription": "", "error": "Gemini returned non-JSON"}
769
  try:
770
- text = data["candidates"][0]["content"]["parts"][0]["text"].strip()
771
  except Exception:
772
  return {"ShortDescription": "", "DetailedDescription": "", "error": "Gemini parsing failed"}
773
  if text.startswith("```"):
774
  lines = [ln for ln in text.splitlines() if not ln.strip().startswith("```")]
775
- text = "
776
- ".join(lines).strip()
777
  try:
778
  ticket_json = json.loads(text)
779
  return {
@@ -812,14 +761,10 @@ async def incident_status(input_data: TicketStatusInput):
812
  number = result.get("number", input_data.number or "unknown")
813
  return {
814
  "bot_response": (
815
- f"**Ticket:** {number}
816
- "
817
- f"**Status:** {state_label}
818
- "
819
  f"**Issue description:** {short}"
820
- ).replace("
821
- ", "
822
- "),
823
  "followup": "Is there anything else I can assist you with?",
824
  "show_assist_card": True,
825
  "persist": True,
 
 
1
  import os
2
  import json
3
  import re
 
97
  s = (ln or "").strip()
98
  if not s:
99
  continue
100
+ s = re.sub(r"^\s*(?:\d+[\.\)]\s+|[\-\*]\s+)", "", s).strip()
101
  items.append(f"{i}. {s}")
102
+ return "\n".join(items).strip()
 
103
 
104
  def _as_numbered_steps(text: str) -> str:
105
  raw_lines: List[str] = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
 
120
  i += 1
121
  return _format_steps_markdown(merged)
122
 
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\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\n"
132
+ "You can also say 'create ticket' and I'll raise a ServiceNow incident now."
 
 
 
 
 
 
 
 
133
  )
134
 
135
  # ---------------- Resolution/Incident helpers ----------------
 
138
  resolved = (resolved_text or "").strip()
139
  short_desc = issue[:100] if issue else (resolved[:100] or "Issue resolved (user confirmation)")
140
  long_desc = (
141
+ f"User reported: \"{issue}\". "
142
+ f"User confirmation: \"{resolved}\". "
143
+ f"Tracking record created automatically by NOVA."
144
  ).strip()
145
  return short_desc, long_desc
146
 
 
184
  def _classify_resolution_llm(user_message: str) -> bool:
185
  if not GEMINI_API_KEY:
186
  return False
187
+ prompt = f"""Classify if the following user message indicates that the issue is resolved or working now.\n
188
+ Return only 'true' or 'false'.\n\n
189
+ Message: {user_message}"""
 
 
 
 
 
190
  headers = {"Content-Type": "application/json"}
191
  payload = {"contents": [{"parts": [{"text": prompt}]}]}
192
  try:
 
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
 
221
  def _filter_context_for_query(context: str, query: str) -> Tuple[str, Dict[str, Any]]:
 
230
  matched_exact, matched_any = [], []
231
  for s in sentences:
232
  s_norm = _normalize_for_match(s)
233
+ is_bullet = bool(re.match(r"^[\-\*]\s*", s))
234
  overlap = sum(1 for t in q_terms if t in s_norm) + (1 if is_bullet else 0)
235
  if overlap >= STRICT_OVERLAP:
236
  matched_exact.append(s)
 
238
  matched_any.append(s)
239
  if matched_exact:
240
  kept = matched_exact[:MAX_SENTENCES_STRICT]
241
+ return "\n".join(kept).strip(), {'mode': 'exact', 'matched_count': len(kept), 'all_sentences': len(sentences)}
 
242
  if matched_any:
243
  kept = matched_any[:MAX_SENTENCES_CONCISE]
244
+ return "\n".join(kept).strip(), {'mode': 'concise', 'matched_count': len(kept), 'all_sentences': len(sentences)}
 
245
  kept = sentences[:MAX_SENTENCES_CONCISE]
246
+ return "\n".join(kept).strip(), {'mode': 'concise', 'matched_count': 0, 'all_sentences': len(sentences)}
 
247
 
248
  # ---------------- Navigation extraction ----------------
249
  NAV_LINE_REGEX = re.compile(r"(navigate\s+to|login|log in|menu|screen)", re.IGNORECASE)
 
252
  lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()]
253
  kept: List[str] = []
254
  for ln in lines:
255
+ if NAV_LINE_REGEX.search(ln) or ln.lower().startswith("log in") or ln.lower().startswith("login"):
256
  kept.append(ln)
257
  if len(kept) >= max_lines:
258
  break
259
+ return "\n".join(kept).strip() if kept else (text or "").strip()
 
260
 
261
+ # ---------------- Errors extraction ----------------
262
  ERROR_STARTS = (
263
  "error", "resolution", "fix", "verify", "check",
264
  "permission", "access", "authorization", "authorisation",
 
274
  kept.append(ln)
275
  if len(kept) >= max_lines:
276
  break
277
+ return "\n".join(kept).strip() if kept else (text or "").strip()
 
278
 
279
  @app.get("/")
280
  async def health_check():
 
288
  # --- Yes/No handlers ---
289
  if msg_norm in ("yes", "y", "sure", "ok", "okay"):
290
  return {
291
+ "bot_response": ("Great! Tell me what you'd like to do next — check another ticket, create an incident, or describe your issue."),
292
  "status": "OK",
293
  "followup": "You can say: 'create ticket', 'incident status INC0012345', or describe your problem.",
294
  "options": [],
 
334
  else:
335
  err = (result or {}).get("error", "Unknown error")
336
  return {
337
+ "bot_response": f"⚠️ I couldn't create the tracking incident automatically ({err}).",
338
  "status": "PARTIAL",
339
  "context_found": False,
340
  "ask_resolved": False,
 
361
  if _is_incident_intent(msg_norm):
362
  return {
363
  "bot_response": (
364
+ "Okay, let's create a ServiceNow incident.\n\n"
365
+ "Please provide:\n- Short Description (one line)\n"
366
+ "- Detailed Description (steps, error text, IDs, site, environment)"
 
 
 
 
367
  ),
368
  "status": (input_data.prev_status or "PARTIAL"),
369
  "context_found": False,
 
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"},
 
400
  "context_found": False,
401
  "ask_resolved": False,
402
  "suggest_incident": False,
403
+ "followup": "Provide the Incident ID and I'll fetch the status.",
404
  "show_status_form": True,
405
  "top_hits": [],
406
  "sources": [],
 
424
  num = result.get("number", number or "unknown")
425
  return {
426
  "bot_response": (
427
+ f"**Ticket:** {num}\n"
428
+ f"**Status:** {state_label}\n"
 
 
429
  f"**Issue description:** {short}"
430
  ),
431
  "status": "OK",
 
463
  m["combined"] = comb
464
  items.append({"text": text, "meta": m})
465
  selected = items[:max(1, 2)]
466
+ context_raw = "\n\n---\n\n".join([s["text"] for s in selected]) if selected else ""
 
 
 
 
467
  filtered_text, filt_info = _filter_context_for_query(context_raw, input_data.user_message)
468
  context = filtered_text
469
  context_found = bool(context.strip())
 
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,
 
516
  "context_found": False,
517
  "ask_resolved": False,
518
  "suggest_incident": True,
519
+ "followup": ("Reply with the above details or say 'create ticket'." if not second_try else "Shall I create a ticket now?"),
520
  "top_hits": [],
521
  "sources": [],
522
  "debug": {"used_chunks": 0, "second_try": second_try, "best_distance": best_distance, "best_combined": best_combined},
523
  }
524
 
525
  # LLM rewrite (constrained to provided context)
526
+ enhanced_prompt = f"""From the provided context, output only the actionable content relevant to the user's question. Use ONLY the provided context; do NOT add information that is not present.\n\n
527
+ ### Context
 
 
 
 
528
  {context}
529
 
530
+ ### Question
 
531
  {input_data.user_message}
532
 
533
+ ### Output
534
+ - Return numbered/bulleted steps in the same order when appropriate.
535
+ - If context is insufficient, add: 'This may be partial based on available KB.'
536
+ """
 
 
 
 
537
  headers = {"Content-Type": "application/json"}
538
  payload = {"contents": [{"parts": [{"text": enhanced_prompt}]}]}
539
  try:
 
546
  resp = type("RespStub", (), {"status_code": 0})()
547
  result = {}
548
  try:
549
+ bot_text = result.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "")
550
  except Exception:
551
  bot_text = ""
552
  if not bot_text.strip():
553
  bot_text = context
554
+ bot_text = "\n".join([ln for ln in bot_text.splitlines() if not re.match(r"^\s*source\s*:\s*", ln, flags=re.IGNORECASE)]).strip()
 
555
 
556
  if detected_intent == "steps":
557
  bot_text = _as_numbered_steps(bot_text)
 
575
  "top_hits": [],
576
  "sources": [],
577
  "debug": {
578
+ "used_chunks": len(context.split("\n\n---\n\n")) if context else 0,
 
 
 
 
579
  "best_distance": best_distance,
580
  "best_combined": best_combined,
581
  "http_status": getattr(resp, "status_code", 0),
 
701
  async def generate_ticket_desc_ep(input_data: TicketDescInput):
702
  try:
703
  prompt = (
704
+ f"You are helping generate ServiceNow ticket descriptions based on the issue: {input_data.issue}.\n"
705
+ "Please return the output strictly in JSON format with the following keys:\n"
706
+ "{\n"
707
+ ' "ShortDescription": "A concise summary of the issue (max 100 characters)",\n'
708
+ ' "DetailedDescription": "A detailed explanation of the issue"\n'
709
+ "}\n"
 
 
 
 
 
 
710
  "Do not include any extra text, comments, or explanations outside the JSON."
711
  )
712
  headers = {"Content-Type": "application/json"}
 
717
  except Exception:
718
  return {"ShortDescription": "", "DetailedDescription": "", "error": "Gemini returned non-JSON"}
719
  try:
720
+ text = data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "").strip()
721
  except Exception:
722
  return {"ShortDescription": "", "DetailedDescription": "", "error": "Gemini parsing failed"}
723
  if text.startswith("```"):
724
  lines = [ln for ln in text.splitlines() if not ln.strip().startswith("```")]
725
+ text = "\n".join(lines).strip()
 
726
  try:
727
  ticket_json = json.loads(text)
728
  return {
 
761
  number = result.get("number", input_data.number or "unknown")
762
  return {
763
  "bot_response": (
764
+ f"**Ticket:** {number} \n"
765
+ f"**Status:** {state_label} \n"
 
 
766
  f"**Issue description:** {short}"
767
+ ).replace("\n", " \n"),
 
 
768
  "followup": "Is there anything else I can assist you with?",
769
  "show_assist_card": True,
770
  "persist": True,