Spaces:
Sleeping
Sleeping
Update main.py
Browse files
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+|[
|
| 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 (
|
| 126 |
def _build_clarifying_message() -> str:
|
| 127 |
return (
|
| 128 |
-
"I couldn
|
| 129 |
-
|
| 130 |
-
"
|
| 131 |
-
"
|
| 132 |
-
"
|
| 133 |
-
"
|
| 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
|
| 152 |
-
f
|
| 153 |
-
f
|
| 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 |
-
|
| 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 |
-
|
| 233 |
-
+|
|
| 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"^[
|
| 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
|
| 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 you
|
| 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 couldn
|
| 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, let
|
| 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,
|
| 411 |
-
"followup": "Reply with the above details or say
|
| 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 I
|
| 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 don
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 570 |
-
|
| 571 |
-
|
| 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
|
| 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 |
-
"
|
| 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
|
| 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,
|