j-js commited on
Commit
94976bf
·
verified ·
1 Parent(s): 04b7754

Update conversation_logic.py

Browse files
Files changed (1) hide show
  1. conversation_logic.py +170 -64
conversation_logic.py CHANGED
@@ -63,20 +63,76 @@ def _extract_text_from_history_item(item: Dict[str, Any]) -> str:
63
  if not isinstance(item, dict):
64
  return ""
65
 
66
- for key in ("content", "text", "message", "question_text", "raw_user_text"):
 
67
  value = item.get(key)
68
  if isinstance(value, str) and value.strip():
69
  return value.strip()
70
 
71
  meta = item.get("meta")
72
  if isinstance(meta, dict):
73
- value = meta.get("question_text")
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  if isinstance(value, str) and value.strip():
75
  return value.strip()
76
 
77
  return ""
78
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  def _is_followup_hint_only(text: str) -> bool:
81
  low = (text or "").strip().lower()
82
  return low in {
@@ -90,6 +146,13 @@ def _is_followup_hint_only(text: str) -> bool:
90
  "walk me through it",
91
  "step by step",
92
  "walkthrough",
 
 
 
 
 
 
 
93
  }
94
 
95
 
@@ -144,6 +207,33 @@ def _extract_question_text(raw_user_text: str) -> str:
144
  return _strip_control_prefix(text)
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def _classify_input_type(raw_user_text: str) -> str:
148
  text = _clean_text(raw_user_text)
149
  if not text:
@@ -238,38 +328,51 @@ def _history_hint_stage(chat_history: Optional[List[Dict[str, Any]]]) -> int:
238
  except Exception:
239
  pass
240
 
 
 
 
 
 
 
 
 
241
  return min(best, 3)
242
 
243
 
244
- def _recover_question_text_from_history(
245
  raw_user_text: str,
246
  question_text: Optional[str],
247
  chat_history: Optional[List[Dict[str, Any]]],
 
248
  ) -> str:
249
  explicit = _sanitize_question_text(question_text or "")
250
  if explicit:
251
  return explicit
252
 
253
- if not _is_followup_hint_only(raw_user_text):
254
- return _sanitize_question_text(_extract_question_text(raw_user_text))
 
 
 
 
255
 
256
  if not chat_history:
257
  return ""
258
 
259
  for item in reversed(chat_history):
260
- role = str(item.get("role", "")).lower()
261
- text = _extract_text_from_history_item(item)
262
-
263
- if not text or role == "assistant":
264
- continue
265
-
266
- low = text.lower().strip()
267
- if _is_followup_hint_only(low):
268
- continue
269
-
270
- recovered = _sanitize_question_text(_extract_question_text(text))
271
- if recovered:
272
- return recovered
273
 
274
  return ""
275
 
@@ -285,13 +388,21 @@ def _choose_effective_question_text(
285
  stored_question = _sanitize_question_text(state.get("question_text", ""))
286
 
287
  if _is_followup_input(input_type):
288
- if stored_question:
 
 
 
 
 
 
 
289
  return stored_question, True
290
 
291
- recovered = _recover_question_text_from_history(
292
  raw_user_text=raw_user_text,
293
  question_text=question_text,
294
  chat_history=chat_history,
 
295
  )
296
  if recovered:
297
  return recovered, True
@@ -411,6 +522,7 @@ def _safe_steps(steps: List[str]) -> List[str]:
411
  r"\bthat gives\b",
412
  r"\bthis gives\b",
413
  r"\btherefore\b",
 
414
  r"\bso x\s*=",
415
  r"\bso y\s*=",
416
  r"\bx\s*=",
@@ -422,12 +534,20 @@ def _safe_steps(steps: List[str]) -> List[str]:
422
  for step in steps:
423
  s = _strip_bullet_prefix(step)
424
  lowered = s.lower()
425
- if any(re.search(p, lowered) for p in banned_patterns):
426
  continue
427
  if s:
428
  cleaned.append(s)
429
 
430
- return cleaned
 
 
 
 
 
 
 
 
431
 
432
 
433
  def _safe_meta_list(items: Any) -> List[str]:
@@ -496,6 +616,17 @@ def _get_result_steps(result: SolverResult) -> List[str]:
496
  return []
497
 
498
 
 
 
 
 
 
 
 
 
 
 
 
499
  def _solver_has_useful_steps(result: Optional[SolverResult]) -> bool:
500
  if result is None:
501
  return False
@@ -533,7 +664,7 @@ def _walkthrough_from_steps(steps: List[str], verbosity: float) -> str:
533
  safe_steps[:4] if verbosity < 0.85 else
534
  safe_steps
535
  )
536
- return "\n".join(f"- {s}" for s in shown_steps)
537
 
538
 
539
  def _answer_path_from_steps(steps: List[str], verbosity: float) -> str:
@@ -546,7 +677,7 @@ def _answer_path_from_steps(steps: List[str], verbosity: float) -> str:
546
  safe_steps[:3] if verbosity < 0.8 else
547
  safe_steps
548
  )
549
- return "\n".join(f"- {s}" for s in shown_steps)
550
 
551
 
552
  def _compose_reply(
@@ -567,13 +698,13 @@ def _compose_reply(
567
 
568
  if intent == "instruction":
569
  if steps:
570
- return f"First step: {steps[0]}"
571
- return "First, identify the key relationship or comparison in the question."
572
 
573
  if intent == "definition":
574
  if steps:
575
- return f"Here is the idea in context:\n- {steps[0]}"
576
- return "This is asking for the meaning of the term or idea in the question."
577
 
578
  if intent in {"walkthrough", "step_by_step"}:
579
  step_reply = _walkthrough_from_steps(steps, verbosity=verbosity)
@@ -668,29 +799,6 @@ def _is_direct_solve_request(text: str, intent: str) -> bool:
668
  return False
669
 
670
 
671
- def _render_local_response(help_mode: str, body: str) -> str:
672
- body = (body or "").strip()
673
- if not body:
674
- return "Let’s work through it."
675
-
676
- if help_mode == "hint":
677
- return f"Let’s work through it.\n\nHint:\n{body}"
678
-
679
- if help_mode == "walkthrough":
680
- return f"Let’s work through it.\n\nWalkthrough:\n{body}"
681
-
682
- if help_mode == "answer":
683
- return f"Let’s work through it.\n\nAnswer path:\n{body}"
684
-
685
- if help_mode == "method":
686
- return f"Let’s work through it.\n\nMethod:\n{body}"
687
-
688
- if help_mode == "explain":
689
- return f"Let’s work through it.\n\nExplanation:\n{body}"
690
-
691
- return f"Let’s work through it.\n\n{body}"
692
-
693
-
694
  class ConversationEngine:
695
  def __init__(
696
  self,
@@ -800,6 +908,8 @@ class ConversationEngine:
800
  if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
801
  result.topic = question_topic
802
 
 
 
803
  explainer_result = route_explainer(solver_input) if solver_input else None
804
  explainer_understood = bool(
805
  explainer_result is not None and getattr(explainer_result, "understood", False)
@@ -829,7 +939,6 @@ class ConversationEngine:
829
 
830
  use_solver_steps = _solver_has_useful_steps(result)
831
  use_solver_steps_for_hint = resolved_help_mode == "hint" and use_solver_steps
832
- use_local_render = use_solver_steps and resolved_help_mode in {"answer", "walkthrough", "hint", "method"}
833
 
834
  if resolved_help_mode == "explain" and explainer_understood:
835
  reply = format_explainer_response(
@@ -863,18 +972,15 @@ class ConversationEngine:
863
  explainer_scaffold=explainer_scaffold,
864
  )
865
 
866
- if use_local_render:
867
- reply = _render_local_response(resolved_help_mode, reply_core)
868
- else:
869
- reply = format_reply(
870
- reply_core,
871
- tone=tone,
872
- verbosity=verbosity,
873
- transparency=transparency,
874
- help_mode=resolved_help_mode,
875
- hint_stage=hint_stage,
876
- topic=result.topic,
877
- )
878
 
879
  if resolved_help_mode != "answer":
880
  result.solved = False
 
63
  if not isinstance(item, dict):
64
  return ""
65
 
66
+ direct_keys = ("content", "text", "message", "question_text", "raw_user_text")
67
+ for key in direct_keys:
68
  value = item.get(key)
69
  if isinstance(value, str) and value.strip():
70
  return value.strip()
71
 
72
  meta = item.get("meta")
73
  if isinstance(meta, dict):
74
+ for key in ("question_text", "recovered_question_text"):
75
+ value = meta.get(key)
76
+ if isinstance(value, str) and value.strip():
77
+ return value.strip()
78
+
79
+ nested_state = meta.get("session_state")
80
+ if isinstance(nested_state, dict):
81
+ value = nested_state.get("question_text")
82
+ if isinstance(value, str) and value.strip():
83
+ return value.strip()
84
+
85
+ nested_state = item.get("session_state")
86
+ if isinstance(nested_state, dict):
87
+ value = nested_state.get("question_text")
88
  if isinstance(value, str) and value.strip():
89
  return value.strip()
90
 
91
  return ""
92
 
93
 
94
+ def _extract_question_candidates_from_history_item(item: Dict[str, Any]) -> List[str]:
95
+ if not isinstance(item, dict):
96
+ return []
97
+
98
+ candidates: List[str] = []
99
+
100
+ direct_keys = ("question_text", "raw_user_text", "content", "text", "message")
101
+ for key in direct_keys:
102
+ value = item.get(key)
103
+ if isinstance(value, str) and value.strip():
104
+ candidates.append(value.strip())
105
+
106
+ meta = item.get("meta")
107
+ if isinstance(meta, dict):
108
+ for key in ("question_text", "recovered_question_text"):
109
+ value = meta.get(key)
110
+ if isinstance(value, str) and value.strip():
111
+ candidates.append(value.strip())
112
+
113
+ nested_state = meta.get("session_state")
114
+ if isinstance(nested_state, dict):
115
+ value = nested_state.get("question_text")
116
+ if isinstance(value, str) and value.strip():
117
+ candidates.append(value.strip())
118
+
119
+ nested_state = item.get("session_state")
120
+ if isinstance(nested_state, dict):
121
+ value = nested_state.get("question_text")
122
+ if isinstance(value, str) and value.strip():
123
+ candidates.append(value.strip())
124
+
125
+ deduped: List[str] = []
126
+ seen = set()
127
+ for candidate in candidates:
128
+ key = candidate.strip().lower()
129
+ if key and key not in seen:
130
+ seen.add(key)
131
+ deduped.append(candidate.strip())
132
+
133
+ return deduped
134
+
135
+
136
  def _is_followup_hint_only(text: str) -> bool:
137
  low = (text or "").strip().lower()
138
  return low in {
 
146
  "walk me through it",
147
  "step by step",
148
  "walkthrough",
149
+ "i'm confused",
150
+ "im confused",
151
+ "confused",
152
+ "explain more",
153
+ "more explanation",
154
+ "can you explain that",
155
+ "help me understand",
156
  }
157
 
158
 
 
207
  return _strip_control_prefix(text)
208
 
209
 
210
+ def _looks_like_question_text(text: str) -> bool:
211
+ t = (text or "").strip()
212
+ if not t:
213
+ return False
214
+
215
+ low = t.lower()
216
+
217
+ if "=" in t:
218
+ return True
219
+ if "%" in t:
220
+ return True
221
+ if re.search(r"\b\d+\s*:\s*\d+\b", t):
222
+ return True
223
+ if re.search(r"[a-zA-Z]\s*[\+\-\*/=]", t):
224
+ return True
225
+ if re.search(r"[\+\-\*/=]\s*[a-zA-Z0-9]", t):
226
+ return True
227
+ if any(k in low for k in [
228
+ "what is", "find", "if ", "how many", "probability", "ratio",
229
+ "percent", "equation", "integer", "triangle", "circle",
230
+ "mean", "median", "average", "remainder", "prime", "factor",
231
+ ]):
232
+ return True
233
+
234
+ return False
235
+
236
+
237
  def _classify_input_type(raw_user_text: str) -> str:
238
  text = _clean_text(raw_user_text)
239
  if not text:
 
328
  except Exception:
329
  pass
330
 
331
+ if isinstance(meta, dict):
332
+ nested_state = meta.get("session_state")
333
+ if isinstance(nested_state, dict) and "hint_stage" in nested_state:
334
+ try:
335
+ best = max(best, int(nested_state["hint_stage"]))
336
+ except Exception:
337
+ pass
338
+
339
  return min(best, 3)
340
 
341
 
342
+ def _recover_question_text(
343
  raw_user_text: str,
344
  question_text: Optional[str],
345
  chat_history: Optional[List[Dict[str, Any]]],
346
+ input_type: str,
347
  ) -> str:
348
  explicit = _sanitize_question_text(question_text or "")
349
  if explicit:
350
  return explicit
351
 
352
+ direct_candidate = _sanitize_question_text(_extract_question_text(raw_user_text))
353
+ if direct_candidate and _looks_like_question_text(direct_candidate):
354
+ return direct_candidate
355
+
356
+ if not _is_followup_input(input_type):
357
+ return direct_candidate
358
 
359
  if not chat_history:
360
  return ""
361
 
362
  for item in reversed(chat_history):
363
+ candidates = _extract_question_candidates_from_history_item(item)
364
+ for candidate in candidates:
365
+ recovered = _sanitize_question_text(candidate)
366
+ if not recovered:
367
+ continue
368
+ if _is_followup_hint_only(recovered):
369
+ continue
370
+ if _looks_like_question_text(recovered):
371
+ return recovered
372
+
373
+ extracted = _sanitize_question_text(_extract_question_text(recovered))
374
+ if extracted and not _is_followup_hint_only(extracted) and _looks_like_question_text(extracted):
375
+ return extracted
376
 
377
  return ""
378
 
 
388
  stored_question = _sanitize_question_text(state.get("question_text", ""))
389
 
390
  if _is_followup_input(input_type):
391
+ if explicit_question and _looks_like_question_text(explicit_question):
392
+ return explicit_question, False
393
+
394
+ direct_candidate = _sanitize_question_text(_extract_question_text(raw_user_text))
395
+ if direct_candidate and _looks_like_question_text(direct_candidate):
396
+ return direct_candidate, False
397
+
398
+ if stored_question and _looks_like_question_text(stored_question):
399
  return stored_question, True
400
 
401
+ recovered = _recover_question_text(
402
  raw_user_text=raw_user_text,
403
  question_text=question_text,
404
  chat_history=chat_history,
405
+ input_type=input_type,
406
  )
407
  if recovered:
408
  return recovered, True
 
522
  r"\bthat gives\b",
523
  r"\bthis gives\b",
524
  r"\btherefore\b",
525
+ r"\bthus\b",
526
  r"\bso x\s*=",
527
  r"\bso y\s*=",
528
  r"\bx\s*=",
 
534
  for step in steps:
535
  s = _strip_bullet_prefix(step)
536
  lowered = s.lower()
537
+ if any(re.search(pattern, lowered) for pattern in banned_patterns):
538
  continue
539
  if s:
540
  cleaned.append(s)
541
 
542
+ deduped: List[str] = []
543
+ seen = set()
544
+ for step in cleaned:
545
+ key = step.strip().lower()
546
+ if key and key not in seen:
547
+ seen.add(key)
548
+ deduped.append(step.strip())
549
+
550
+ return deduped
551
 
552
 
553
  def _safe_meta_list(items: Any) -> List[str]:
 
616
  return []
617
 
618
 
619
+ def _apply_safe_step_sanitization(result: SolverResult) -> None:
620
+ safe_steps = _get_result_steps(result)
621
+
622
+ result.steps = list(safe_steps)
623
+ setattr(result, "display_steps", list(safe_steps))
624
+
625
+ result.meta = result.meta or {}
626
+ result.meta["steps"] = list(safe_steps)
627
+ result.meta["display_steps"] = list(safe_steps)
628
+
629
+
630
  def _solver_has_useful_steps(result: Optional[SolverResult]) -> bool:
631
  if result is None:
632
  return False
 
664
  safe_steps[:4] if verbosity < 0.85 else
665
  safe_steps
666
  )
667
+ return "\n".join(f"- {step}" for step in shown_steps)
668
 
669
 
670
  def _answer_path_from_steps(steps: List[str], verbosity: float) -> str:
 
677
  safe_steps[:3] if verbosity < 0.8 else
678
  safe_steps
679
  )
680
+ return "\n".join(f"- {step}" for step in shown_steps)
681
 
682
 
683
  def _compose_reply(
 
698
 
699
  if intent == "instruction":
700
  if steps:
701
+ return f"- {steps[0]}"
702
+ return "- First, identify the key relationship or comparison in the question."
703
 
704
  if intent == "definition":
705
  if steps:
706
+ return f"- {steps[0]}"
707
+ return "- This is asking for the meaning of the term or idea in the question."
708
 
709
  if intent in {"walkthrough", "step_by_step"}:
710
  step_reply = _walkthrough_from_steps(steps, verbosity=verbosity)
 
799
  return False
800
 
801
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  class ConversationEngine:
803
  def __init__(
804
  self,
 
908
  if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
909
  result.topic = question_topic
910
 
911
+ _apply_safe_step_sanitization(result)
912
+
913
  explainer_result = route_explainer(solver_input) if solver_input else None
914
  explainer_understood = bool(
915
  explainer_result is not None and getattr(explainer_result, "understood", False)
 
939
 
940
  use_solver_steps = _solver_has_useful_steps(result)
941
  use_solver_steps_for_hint = resolved_help_mode == "hint" and use_solver_steps
 
942
 
943
  if resolved_help_mode == "explain" and explainer_understood:
944
  reply = format_explainer_response(
 
972
  explainer_scaffold=explainer_scaffold,
973
  )
974
 
975
+ reply = format_reply(
976
+ reply_core,
977
+ tone=tone,
978
+ verbosity=verbosity,
979
+ transparency=transparency,
980
+ help_mode=resolved_help_mode,
981
+ hint_stage=hint_stage,
982
+ topic=result.topic,
983
+ )
 
 
 
984
 
985
  if resolved_help_mode != "answer":
986
  result.solved = False