srilakshu012456 commited on
Commit
ea5f821
·
verified ·
1 Parent(s): 91d8722

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +59 -220
main.py CHANGED
@@ -227,14 +227,13 @@ def _get_steps_for_action_global(actions: list, prefer_doc: Optional[str] = None
227
  candidates.sort(key=lambda x: x[0], reverse=True)
228
  _, best_doc_global, best_text = candidates[0]
229
  return best_doc_global, best_text
230
-
231
  def _pick_default_action_section(best_doc: str) -> Optional[str]:
232
  """
233
  Backward-compatible wrapper: prefer-action version with no preference.
234
  """
235
  return _pick_default_action_section_with_preference(best_doc, None)
236
 
237
-
238
  def _pick_default_action_section_with_preference(best_doc: str, prefer_action: Optional[str]) -> Optional[str]:
239
  """
240
  Generic: if there are sections tagged with the requested action (via action_tag),
@@ -265,8 +264,7 @@ def _pick_default_action_section_with_preference(best_doc: str, prefer_action: O
265
  return t
266
  return sections[0] if sections else None
267
 
268
- # ------------------------------ Action -> section selector (exact match by section title) ------------------------------
269
- # Maps user action to likely section name tokens (lowercase)
270
  ACTION_SECTION_KEYS = {
271
  "create": ("create", "creation", "appointment creation", "new appointment", "book", "schedule"),
272
  "update": ("update", "updation", "reschedule", "change", "modify", "edit"),
@@ -274,17 +272,11 @@ ACTION_SECTION_KEYS = {
274
  }
275
 
276
  def _pick_action_section(best_doc: str, action: Optional[str]) -> Optional[str]:
277
- """
278
- Return the exact section title (original casing) inside `best_doc` whose name
279
- contains any token for the given action. Falls back to None.
280
- """
281
  if not best_doc or not action:
282
  return None
283
  keys = ACTION_SECTION_KEYS.get(action.lower(), ())
284
  if not keys:
285
  return None
286
-
287
- # scan available sections for this doc
288
  candidates = []
289
  for d in bm25_docs:
290
  m = d.get("meta", {}) or {}
@@ -294,32 +286,24 @@ def _pick_action_section(best_doc: str, action: Optional[str]) -> Optional[str]:
294
  continue
295
  low = sec.lower()
296
  if any(k in low for k in keys):
297
- # prefer exact action match over generic words
298
  score = 1.0
299
  if action.lower() in low:
300
  score += 0.5
301
- # appointments module slight boost
302
  mtags = (m.get("module_tags") or "").lower()
303
  if "appointments" in mtags:
304
  score += 0.2
305
  candidates.append((score, sec))
306
-
307
  if not candidates:
308
  return None
309
-
310
- # pick best scored section title
311
  candidates.sort(key=lambda x: x[0], reverse=True)
312
- return candidates[0][1] # exact section title
313
 
314
  def _get_steps_text_for_action(best_doc: str, action: Optional[str]) -> Optional[str]:
315
- """
316
- Use the section title returned by `_pick_action_section` to fetch its text.
317
- """
318
  sec = _pick_action_section(best_doc, action)
319
  if not sec:
320
  return None
321
  txt = get_section_text(best_doc, sec)
322
- return (txt or "").strip() or None
323
 
324
  # ------------------------------ Save lines helpers ------------------------------
325
  SAVE_SYNS = ("save", "saving", "save changes", "click save", "press save", "select save")
@@ -334,30 +318,52 @@ def _find_save_lines_in_section(section_text: str, max_lines: int = 2) -> str:
334
  break
335
  return "\n".join(lines)
336
 
337
- # ------------------------------ Boundary cutter to avoid bleed ------------------------------
338
- BOUNDARY_HEADINGS = (
339
- "appointment schedule updation","schedule updation","updation","update",
340
- "appointment deletion","deletion","delete","cancel",
341
- "escalation","common errors","known issues","errors & resolution",
342
- "pre-requisites","prerequisites"
343
- )
 
 
 
 
 
 
 
 
344
 
345
- def _cut_at_next_boundary(section_text: str, current_action: Optional[str]) -> str:
 
 
 
 
 
346
  if not (section_text or "").strip():
347
  return section_text
 
 
 
348
  lines = [ln for ln in (section_text or "").splitlines() if ln.strip()]
349
  out: List[str] = []
 
350
  for ln in lines:
351
  low = ln.lower().strip()
352
- is_heading_like = (":" in ln and len(ln.split(":")[0]) <= 80) or any(k in low for k in BOUNDARY_HEADINGS)
353
- if is_heading_like:
354
- if current_action == "create" and any(k in low for k in ("update","updation","deletion","delete","cancel")):
355
- break
356
- if current_action == "update" and any(k in low for k in ("deletion","delete","cancel")):
357
  break
358
- if current_action is None and any(k in low for k in BOUNDARY_HEADINGS):
 
 
 
359
  break
 
360
  out.append(ln)
 
361
  return "\n".join(out).strip()
362
 
363
  # ------------------------------ Text normalization / numbering ------------------------------
@@ -602,58 +608,6 @@ def _build_tracking_descriptions(issue_text: str, resolved_text: str) -> Tuple[s
602
  f"Tracking record created automatically by NOVA."
603
  ).strip()
604
  return short_desc, long_desc
605
-
606
- def _build_doc_section_index(best_doc: str) -> Dict[str, Optional[str]]:
607
- """
608
- Build a dictionary for the given doc:
609
- { lower(section_title): lower(action_tag or None) }
610
- """
611
- index: Dict[str, Optional[str]] = {}
612
- for d in bm25_docs:
613
- m = d.get("meta", {}) or {}
614
- if m.get("filename") == best_doc and m.get("intent_tag") == "steps":
615
- sec = (m.get("section") or "").strip()
616
- tag = (m.get("action_tag") or "").strip().lower() or None
617
- if sec:
618
- index[sec.lower()] = tag
619
- return index
620
-
621
- def _cut_at_next_boundary_generic(section_text: str, best_doc: str, current_action: Optional[str]) -> str:
622
- """
623
- Stop when we hit any known section heading in the same doc.
624
- If current_action is set, stop only when the next heading belongs to a different action_tag.
625
- Fully generic: no hard-coded SOP words.
626
- """
627
- if not (section_text or "").strip():
628
- return section_text
629
-
630
- index = _build_doc_section_index(best_doc)
631
- known_headings = set(index.keys())
632
- lines = [ln for ln in (section_text or "").splitlines() if ln.strip()]
633
- out: List[str] = []
634
-
635
- for ln in lines:
636
- low = ln.lower().strip()
637
-
638
- # If any known heading appears in the line, consider it a boundary.
639
- # We use substring match so headings like "Update: ..." or "Delete – ..." work.
640
- matched_heading = None
641
- for h in known_headings:
642
- if h in low:
643
- matched_heading = h
644
- break
645
-
646
- if matched_heading:
647
- next_action = index.get(matched_heading)
648
- # If we know the current action, break only when the upcoming heading is a different action section.
649
- # If current_action is None, break at the first heading we encounter.
650
- if (current_action and next_action and next_action != current_action) or (current_action is None):
651
- break
652
-
653
- out.append(ln)
654
-
655
- return "\n".join(out).strip()
656
-
657
 
658
  # ------------------------------ Incident helpers ------------------------------
659
  def _is_incident_intent(msg_norm: str) -> bool:
@@ -701,108 +655,6 @@ def _has_negation_resolved(msg_norm: str) -> bool:
701
  ]
702
  return any(p in msg_norm for p in neg_phrases)
703
 
704
- def _filter_context_for_query(context: str, query: str) -> Tuple[str, Dict[str, Any]]:
705
- STRICT_OVERLAP = 3
706
- MAX_SENTENCES_STRICT = 4
707
- MAX_SENTENCES_CONCISE = 3
708
-
709
- def _norm(text: str) -> str:
710
- t = (text or "").lower()
711
- t = re.sub(r"[^\w\s]", " ", t)
712
- t = re.sub(r"\s+", " ", t).strip()
713
- return t
714
-
715
- def _split_sentences(ctx: str) -> List[str]:
716
- raw_sents = re.split(r"(?<=[.!?])\s+|\n+|-\s*|\*\s*", ctx or "")
717
- return [s.strip() for s in raw_sents if s and len(s.strip()) > 2]
718
-
719
- ctx = (context or "").strip()
720
- if not ctx or not query:
721
- return ctx, {'mode': 'concise', 'matched_count': 0, 'all_sentences': 0}
722
- q_norm = _norm(query)
723
- q_terms = [t for t in q_norm.split() if len(t) > 2]
724
- if not q_terms:
725
- return ctx, {'mode': 'concise', 'matched_count': 0, 'all_sentences': 0}
726
- sentences = _split_sentences(ctx)
727
- matched_exact, matched_any = [], []
728
- for s in sentences:
729
- s_norm = _norm(s)
730
- is_bullet = bool(re.match(r"^[\-\*]\s*", s))
731
- overlap = sum(1 for t in q_terms if t in s_norm) + (1 if is_bullet else 0)
732
- if overlap >= STRICT_OVERLAP:
733
- matched_exact.append(s)
734
- elif overlap > 0:
735
- matched_any.append(s)
736
- if matched_exact:
737
- kept = matched_exact[:MAX_SENTENCES_STRICT]
738
- return "\n".join(kept).strip(), {'mode': 'exact', 'matched_count': len(kept), 'all_sentences': len(sentences)}
739
- if matched_any:
740
- kept = matched_any[:MAX_SENTENCES_CONCISE]
741
- return "\n".join(kept).strip(), {'mode': 'concise', 'matched_count': len(kept), 'all_sentences': len(sentences)}
742
- kept = sentences[:MAX_SENTENCES_CONCISE]
743
- return "\n".join(kept).strip(), {'mode': 'concise', 'matched_count': 0, 'all_sentences': len(sentences)}
744
-
745
- def _extract_errors_only(text: str, max_lines: int = 12) -> str:
746
- kept: List[str] = []
747
- for ln in _normalize_lines(text):
748
- if re.match(r"^\s*[-*\u2022]\s*", ln) or (":" in ln):
749
- kept.append(ln)
750
- if len(kept) >= max_lines:
751
- break
752
- return "\n".join(kept).strip() if kept else (text or "").strip()
753
-
754
- def _filter_permission_lines(text: str, max_lines: int = 6) -> str:
755
- PERM_SYNONYMS = (
756
- "permission", "permissions", "access", "authorization", "authorisation",
757
- "role", "role mapping", "security profile", "not allowed", "not authorized", "denied", "insufficient"
758
- )
759
- kept: List[str] = []
760
- for ln in _normalize_lines(text):
761
- low = ln.lower()
762
- if any(k in low for k in PERM_SYNONYMS):
763
- kept.append(ln)
764
- if len(kept) >= max_lines:
765
- break
766
- return "\n".join(kept).strip() if kept else (text or "").strip()
767
-
768
- def _extract_escalation_line(text: str) -> Optional[str]:
769
- if not text:
770
- return None
771
- lines = _normalize_lines(text)
772
- if not lines:
773
- return None
774
- start_idx = None
775
- for i, ln in enumerate(lines):
776
- low = ln.lower()
777
- if "escalation" in low or "escalation path" in low or "escalate" in low:
778
- start_idx = i
779
- break
780
- block = []
781
- if start_idx is not None:
782
- for j in range(start_idx, min(len(lines), start_idx + 6)):
783
- if not lines[j].strip():
784
- break
785
- block.append(lines[j].strip())
786
- else:
787
- block = [ln.strip() for ln in lines if ("->" in ln or "→" in ln)]
788
- if not block:
789
- return None
790
- text_block = " ".join(block)
791
- m = re.search(r"escalation[^:]*:\s*(.+)", text_block, flags=re.IGNORECASE)
792
- path = m.group(1).strip() if m else None
793
- if not path:
794
- arrow_lines = [ln for ln in block if ("->" in ln or "→" in ln)]
795
- if arrow_lines:
796
- path = arrow_lines[0]
797
- if not path:
798
- m2 = re.search(r"(operator.*?administrator|operator.*)", text_block, flags=re.IGNORECASE)
799
- path = m2.group(1).strip() if m2 else None
800
- if not path:
801
- return None
802
- path = path.replace("->", "→").strip()
803
- path = re.sub(r"^(?i:escalation\s*path)\s*:\s*", "", path).strip()
804
- return f"If you want to escalate the issue, follow: {path}"
805
-
806
  # ------------------------------ Health ------------------------------
807
  @app.get("/")
808
  async def health_check():
@@ -853,12 +705,6 @@ async def chat_with_ai(input_data: ChatInput):
853
  except Exception:
854
  is_llm_resolved = False
855
 
856
- def _has_negation_resolved(msg_norm: str) -> bool:
857
- return any(p in msg_norm for p in [
858
- "not resolved", "issue not resolved", "still not working", "not working",
859
- "didn't work", "doesn't work", "no change", "not fixed", "still failing", "failed again", "broken", "fail",
860
- ])
861
-
862
  if _has_negation_resolved(msg_norm):
863
  is_llm_resolved = False
864
  if (not _has_negation_resolved(msg_norm)) and (any(p in msg_norm for p in [
@@ -1162,42 +1008,34 @@ async def chat_with_ai(input_data: ChatInput):
1162
  context_preformatted = False
1163
  full_steps = None
1164
 
1165
- # Detect asked action generically using ACTION_SYNONYMS_EXT (already defined)
1166
- asked_action = _detect_action_from_query(input_data.user_message) # returns 'create'/'update'/'delete'/None
1167
 
 
1168
  action_steps = _get_steps_for_action(best_doc, kb_results.get("actions", []))
1169
  if action_steps:
1170
  full_steps = action_steps
1171
  else:
 
1172
  if kb_results.get("actions"):
1173
  alt_doc, alt_steps = _get_steps_for_action_global(kb_results["actions"], prefer_doc=best_doc)
1174
  if alt_steps:
1175
  best_doc = alt_doc
1176
  full_steps = alt_steps
1177
-
1178
- if not full_steps and asked_action:
1179
- via_tag_steps = _get_steps_by_action_tag(best_doc, asked_action)
1180
- if via_tag_steps:
1181
- full_steps = via_tag_steps
1182
- if not full_steps:
1183
- default_sec = _pick_default_action_section_with_preference(best_doc, asked_action)
1184
- if default_sec:
1185
- full_steps = get_section_text(best_doc, default_sec)
1186
 
 
1187
  if not full_steps and asked_action:
1188
- exact_action_steps = _get_steps_text_for_action(best_doc, asked_action)
1189
- if exact_action_steps:
1190
- full_steps = exact_action_steps
1191
 
1192
- if not full_steps and asked_action:
1193
- alt_doc2, alt_steps2 = _get_steps_for_action_global([asked_action], prefer_doc=best_doc)
1194
- if alt_steps2:
1195
- best_doc = alt_doc2
1196
- full_steps = alt_steps2
1197
  if not full_steps:
1198
- default_sec = _pick_default_action_section(best_doc)
1199
  if default_sec:
1200
  full_steps = get_section_text(best_doc, default_sec)
 
 
1201
  if not full_steps:
1202
  full_steps = get_best_steps_section_text(best_doc)
1203
  if not full_steps:
@@ -1206,10 +1044,10 @@ async def chat_with_ai(input_data: ChatInput):
1206
  full_steps = get_section_text(best_doc, sec)
1207
 
1208
  if full_steps:
1209
- # NEW: cut at boundary so 'Creation' doesn't bleed into 'Updation/Delete'
1210
- asked_action = _detect_action_from_query(input_data.user_message)
1211
- full_steps = _cut_at_next_boundary_generic(full_steps, best_doc, asked_action)
1212
- # NEW: scope 'Save' to this section
1213
  save_local = _find_save_lines_in_section(full_steps, max_lines=2)
1214
  if save_local:
1215
  low_steps = (full_steps or "").lower()
@@ -1218,7 +1056,7 @@ async def chat_with_ai(input_data: ChatInput):
1218
 
1219
  # Number + Next steps
1220
  numbered_full = _ensure_numbering(full_steps)
1221
- next_only = _resolve_next_steps(input_data.user_message, numbered_full, max_next=6, min_score=0.35)
1222
  if next_only is not None:
1223
  if len(next_only) == 0:
1224
  context = "You are at the final step of this SOP. No further steps."
@@ -1233,9 +1071,10 @@ async def chat_with_ai(input_data: ChatInput):
1233
  else:
1234
  context = full_steps
1235
  context_preformatted = False
 
1236
  context_found = True
1237
  filt_info = {'mode': None, 'matched_count': None, 'all_sentences': None}
1238
-
1239
  elif best_doc and detected_intent == "errors":
1240
  full_errors = get_best_errors_section_text(best_doc)
1241
  if full_errors:
 
227
  candidates.sort(key=lambda x: x[0], reverse=True)
228
  _, best_doc_global, best_text = candidates[0]
229
  return best_doc_global, best_text
230
+
231
  def _pick_default_action_section(best_doc: str) -> Optional[str]:
232
  """
233
  Backward-compatible wrapper: prefer-action version with no preference.
234
  """
235
  return _pick_default_action_section_with_preference(best_doc, None)
236
 
 
237
  def _pick_default_action_section_with_preference(best_doc: str, prefer_action: Optional[str]) -> Optional[str]:
238
  """
239
  Generic: if there are sections tagged with the requested action (via action_tag),
 
264
  return t
265
  return sections[0] if sections else None
266
 
267
+ # ------------------------------ Action -> section selector (optional fallback by title) ------------------------------
 
268
  ACTION_SECTION_KEYS = {
269
  "create": ("create", "creation", "appointment creation", "new appointment", "book", "schedule"),
270
  "update": ("update", "updation", "reschedule", "change", "modify", "edit"),
 
272
  }
273
 
274
  def _pick_action_section(best_doc: str, action: Optional[str]) -> Optional[str]:
 
 
 
 
275
  if not best_doc or not action:
276
  return None
277
  keys = ACTION_SECTION_KEYS.get(action.lower(), ())
278
  if not keys:
279
  return None
 
 
280
  candidates = []
281
  for d in bm25_docs:
282
  m = d.get("meta", {}) or {}
 
286
  continue
287
  low = sec.lower()
288
  if any(k in low for k in keys):
 
289
  score = 1.0
290
  if action.lower() in low:
291
  score += 0.5
 
292
  mtags = (m.get("module_tags") or "").lower()
293
  if "appointments" in mtags:
294
  score += 0.2
295
  candidates.append((score, sec))
 
296
  if not candidates:
297
  return None
 
 
298
  candidates.sort(key=lambda x: x[0], reverse=True)
299
+ return candidates[0][1]
300
 
301
  def _get_steps_text_for_action(best_doc: str, action: Optional[str]) -> Optional[str]:
 
 
 
302
  sec = _pick_action_section(best_doc, action)
303
  if not sec:
304
  return None
305
  txt = get_section_text(best_doc, sec)
306
+ return (txt or "").strip() or None
307
 
308
  # ------------------------------ Save lines helpers ------------------------------
309
  SAVE_SYNS = ("save", "saving", "save changes", "click save", "press save", "select save")
 
318
  break
319
  return "\n".join(lines)
320
 
321
+ # ------------------------------ Generic boundary cutter (metadata-driven) ------------------------------
322
+ def _build_doc_section_index(best_doc: str) -> Dict[str, Optional[str]]:
323
+ """
324
+ Build a dictionary for the given doc:
325
+ { lower(section_title): lower(action_tag or None) }
326
+ """
327
+ index: Dict[str, Optional[str]] = {}
328
+ for d in bm25_docs:
329
+ m = d.get("meta", {}) or {}
330
+ if m.get("filename") == best_doc and m.get("intent_tag") == "steps":
331
+ sec = (m.get("section") or "").strip()
332
+ tag = (m.get("action_tag") or "").strip().lower() or None
333
+ if sec:
334
+ index[sec.lower()] = tag
335
+ return index
336
 
337
+ def _cut_at_next_boundary_generic(section_text: str, best_doc: str, current_action: Optional[str]) -> str:
338
+ """
339
+ Stop when we hit any known section heading in the same doc.
340
+ If current_action is set, stop only when the next heading belongs to a different action_tag.
341
+ Fully generic: no hard-coded SOP words.
342
+ """
343
  if not (section_text or "").strip():
344
  return section_text
345
+
346
+ index = _build_doc_section_index(best_doc)
347
+ known_headings = set(index.keys())
348
  lines = [ln for ln in (section_text or "").splitlines() if ln.strip()]
349
  out: List[str] = []
350
+
351
  for ln in lines:
352
  low = ln.lower().strip()
353
+
354
+ matched_heading = None
355
+ for h in known_headings:
356
+ if h in low:
357
+ matched_heading = h
358
  break
359
+
360
+ if matched_heading:
361
+ next_action = index.get(matched_heading)
362
+ if (current_action and next_action and next_action != current_action) or (current_action is None):
363
  break
364
+
365
  out.append(ln)
366
+
367
  return "\n".join(out).strip()
368
 
369
  # ------------------------------ Text normalization / numbering ------------------------------
 
608
  f"Tracking record created automatically by NOVA."
609
  ).strip()
610
  return short_desc, long_desc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
  # ------------------------------ Incident helpers ------------------------------
613
  def _is_incident_intent(msg_norm: str) -> bool:
 
655
  ]
656
  return any(p in msg_norm for p in neg_phrases)
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  # ------------------------------ Health ------------------------------
659
  @app.get("/")
660
  async def health_check():
 
705
  except Exception:
706
  is_llm_resolved = False
707
 
 
 
 
 
 
 
708
  if _has_negation_resolved(msg_norm):
709
  is_llm_resolved = False
710
  if (not _has_negation_resolved(msg_norm)) and (any(p in msg_norm for p in [
 
1008
  context_preformatted = False
1009
  full_steps = None
1010
 
1011
+ # Detect asked action generically using ACTION_SYNONYMS_EXT (already defined)
1012
+ asked_action = _detect_action_from_query(input_data.user_message) # 'create'/'update'/'delete'/None
1013
 
1014
+ # 1) Try KB-tagged action section (existing)
1015
  action_steps = _get_steps_for_action(best_doc, kb_results.get("actions", []))
1016
  if action_steps:
1017
  full_steps = action_steps
1018
  else:
1019
+ # 2) Global action lookup (existing)
1020
  if kb_results.get("actions"):
1021
  alt_doc, alt_steps = _get_steps_for_action_global(kb_results["actions"], prefer_doc=best_doc)
1022
  if alt_steps:
1023
  best_doc = alt_doc
1024
  full_steps = alt_steps
 
 
 
 
 
 
 
 
 
1025
 
1026
+ # 3) Generic: exact section by meta.action_tag in SAME best_doc
1027
  if not full_steps and asked_action:
1028
+ via_tag_steps = _get_steps_by_action_tag(best_doc, asked_action)
1029
+ if via_tag_steps:
1030
+ full_steps = via_tag_steps
1031
 
1032
+ # 4) Prefer requested action when defaulting
 
 
 
 
1033
  if not full_steps:
1034
+ default_sec = _pick_default_action_section_with_preference(best_doc, asked_action)
1035
  if default_sec:
1036
  full_steps = get_section_text(best_doc, default_sec)
1037
+
1038
+ # 5) Existing fallbacks
1039
  if not full_steps:
1040
  full_steps = get_best_steps_section_text(best_doc)
1041
  if not full_steps:
 
1044
  full_steps = get_section_text(best_doc, sec)
1045
 
1046
  if full_steps:
1047
+ # Generic boundary cutter (prevents create/update/delete bleed)
1048
+ full_steps = _cut_at_next_boundary_generic(full_steps, best_doc, asked_action)
1049
+
1050
+ # Scope 'Save' lines to current section
1051
  save_local = _find_save_lines_in_section(full_steps, max_lines=2)
1052
  if save_local:
1053
  low_steps = (full_steps or "").lower()
 
1056
 
1057
  # Number + Next steps
1058
  numbered_full = _ensure_numbering(full_steps)
1059
+ next_only = _resolve_next_steps(input_data.user_message, numbered_full, max_next=6, min_score=0.30)
1060
  if next_only is not None:
1061
  if len(next_only) == 0:
1062
  context = "You are at the final step of this SOP. No further steps."
 
1071
  else:
1072
  context = full_steps
1073
  context_preformatted = False
1074
+
1075
  context_found = True
1076
  filt_info = {'mode': None, 'matched_count': None, 'all_sentences': None}
1077
+
1078
  elif best_doc and detected_intent == "errors":
1079
  full_errors = get_best_errors_section_text(best_doc)
1080
  if full_errors: