Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -202,6 +202,92 @@ def _ensure_numbering(text: str) -> str:
|
|
| 202 |
out.append(f"{marker} {seg}")
|
| 203 |
return "\n".join(out)
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
def _filter_error_lines_by_query(text: str, query: str, max_lines: int = 1) -> str:
|
| 206 |
"""
|
| 207 |
Pick the most relevant 'Common Errors & Resolution' bullet(s) for the user's message.
|
|
@@ -906,8 +992,18 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 906 |
sec = (top_meta or {}).get("section")
|
| 907 |
if sec: full_steps = get_section_text(best_doc, sec)
|
| 908 |
if full_steps:
|
| 909 |
-
|
| 910 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
elif detected_intent == "errors":
|
| 912 |
full_errors = get_best_errors_section_text(best_doc)
|
| 913 |
#assist_followup = None # collect a helpful follow-up for generic cases
|
|
|
|
| 202 |
out.append(f"{marker} {seg}")
|
| 203 |
return "\n".join(out)
|
| 204 |
|
| 205 |
+
# --- Next-step helpers (generic; SOP-agnostic) ---
|
| 206 |
+
|
| 207 |
+
def _norm_text(s: str) -> str:
|
| 208 |
+
import re
|
| 209 |
+
s = (s or "").lower()
|
| 210 |
+
s = re.sub(r"[^\w\s]", " ", s)
|
| 211 |
+
s = re.sub(r"\s+", " ", s).strip()
|
| 212 |
+
return s
|
| 213 |
+
|
| 214 |
+
def _split_sop_into_steps(numbered_text: str) -> list:
|
| 215 |
+
"""
|
| 216 |
+
Split a numbered/bulleted SOP block (already passed through _ensure_numbering)
|
| 217 |
+
into atomic steps. Returns a list of raw step strings (order preserved).
|
| 218 |
+
Safe for circled digits, '1.' styles, and bullets.
|
| 219 |
+
"""
|
| 220 |
+
lines = [ln.strip() for ln in (numbered_text or "").splitlines() if ln.strip()]
|
| 221 |
+
steps = []
|
| 222 |
+
for ln in lines:
|
| 223 |
+
# Strip circled/number/bullet marker
|
| 224 |
+
cleaned = ln
|
| 225 |
+
cleaned = re.sub(r"^\s*(?:[\u2460-\u2473]|\d+[.)]|[-*•])\s*", "", cleaned)
|
| 226 |
+
if cleaned:
|
| 227 |
+
steps.append(cleaned)
|
| 228 |
+
return steps
|
| 229 |
+
|
| 230 |
+
def _soft_match_score(a: str, b: str) -> float:
|
| 231 |
+
# Simple Jaccard-like score on tokens for fuzzy matching
|
| 232 |
+
ta = set(_norm_text(a).split())
|
| 233 |
+
tb = set(_norm_text(b).split())
|
| 234 |
+
if not ta or not tb:
|
| 235 |
+
return 0.0
|
| 236 |
+
inter = len(ta & tb)
|
| 237 |
+
union = len(ta | tb)
|
| 238 |
+
return inter / union if union else 0.0
|
| 239 |
+
|
| 240 |
+
def _detect_next_intent(user_query: str) -> bool:
|
| 241 |
+
q = _norm_text(user_query)
|
| 242 |
+
# Conservative rules to avoid false triggers
|
| 243 |
+
keys = [
|
| 244 |
+
"after", "after this", "what next", "whats next", "next step",
|
| 245 |
+
"then what", "following step", "continue", "subsequent", "proceed"
|
| 246 |
+
]
|
| 247 |
+
return any(k in q for k in keys)
|
| 248 |
+
|
| 249 |
+
def _resolve_next_steps(user_query: str, numbered_text: str, max_next: int = 6, min_score: float = 0.35):
|
| 250 |
+
"""
|
| 251 |
+
If 'what's next' intent is detected and we can reliably match the user's
|
| 252 |
+
referenced line to a SOP step, return ONLY the subsequent steps.
|
| 253 |
+
Else return None to preserve current behavior.
|
| 254 |
+
"""
|
| 255 |
+
if not _detect_next_intent(user_query):
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
steps = _split_sop_into_steps(numbered_text)
|
| 259 |
+
if not steps:
|
| 260 |
+
return None
|
| 261 |
+
|
| 262 |
+
q = user_query or ""
|
| 263 |
+
best_idx, best_score = -1, -1.0
|
| 264 |
+
for idx, step in enumerate(steps):
|
| 265 |
+
# Exact substring match gets max score; else use soft match
|
| 266 |
+
score = 1.0 if _norm_text(step) in _norm_text(q) else _soft_match_score(q, step)
|
| 267 |
+
if score > best_score:
|
| 268 |
+
best_score, best_idx = score, idx
|
| 269 |
+
|
| 270 |
+
if best_idx < 0 or best_score < min_score:
|
| 271 |
+
return None # fallback to full SOP
|
| 272 |
+
|
| 273 |
+
start = best_idx + 1
|
| 274 |
+
if start >= len(steps):
|
| 275 |
+
return [] # user is at final step
|
| 276 |
+
|
| 277 |
+
end = min(start + max_next, len(steps))
|
| 278 |
+
return steps[start:end]
|
| 279 |
+
|
| 280 |
+
def _format_steps_as_numbered(steps: list) -> str:
|
| 281 |
+
"""
|
| 282 |
+
Render a small list of steps with circled numbers for visual continuity.
|
| 283 |
+
"""
|
| 284 |
+
circled = {1:"\u2460",2:"\u2461",3:"\u2462",4:"\u2463",5:"\u2464",6:"\u2465",7:"\u2466",8:"\u2467",9:"\u2468",10:"\u2469",
|
| 285 |
+
11:"\u246a",12:"\u246b",13:"\u246c",14:"\u246d",15:"\u246e",16:"\u246f",17:"\u2470",18:"\u2471",19:"\u2472",20:"\u2473"}
|
| 286 |
+
out = []
|
| 287 |
+
for i, s in enumerate(steps, start=1):
|
| 288 |
+
out.append(f"{circled.get(i, str(i))} {s}")
|
| 289 |
+
return "\n".join(out)
|
| 290 |
+
|
| 291 |
def _filter_error_lines_by_query(text: str, query: str, max_lines: int = 1) -> str:
|
| 292 |
"""
|
| 293 |
Pick the most relevant 'Common Errors & Resolution' bullet(s) for the user's message.
|
|
|
|
| 992 |
sec = (top_meta or {}).get("section")
|
| 993 |
if sec: full_steps = get_section_text(best_doc, sec)
|
| 994 |
if full_steps:
|
| 995 |
+
|
| 996 |
+
numbered = _ensure_numbering(full_steps) # keep your existing formatting
|
| 997 |
+
# NEW: return only subsequent steps when user asks "what's next"
|
| 998 |
+
next_only = _resolve_next_steps(input_data.user_message, numbered, max_next=6, min_score=0.35)
|
| 999 |
+
if next_only is not None:
|
| 1000 |
+
if len(next_only) == 0:
|
| 1001 |
+
context = "You are at the final step of this SOP. No further steps."
|
| 1002 |
+
else:
|
| 1003 |
+
context = _format_steps_as_numbered(next_only)
|
| 1004 |
+
else:
|
| 1005 |
+
context=numbered
|
| 1006 |
+
|
| 1007 |
elif detected_intent == "errors":
|
| 1008 |
full_errors = get_best_errors_section_text(best_doc)
|
| 1009 |
#assist_followup = None # collect a helpful follow-up for generic cases
|