j-js commited on
Commit
7a67ad5
·
verified ·
1 Parent(s): a064a65

Update conversation_logic.py

Browse files
Files changed (1) hide show
  1. conversation_logic.py +174 -66
conversation_logic.py CHANGED
@@ -1,7 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import re
4
- from typing import Any, Dict, List, Optional, Set, Tuple
5
 
6
  from context_parser import detect_intent, intent_to_help_mode
7
  from formatting import format_reply, format_explainer_response
@@ -25,6 +25,21 @@ DIRECT_SOLVE_PATTERNS = [
25
  ]
26
 
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  def _clean_text(text: Optional[str]) -> str:
29
  return (text or "").strip()
30
 
@@ -68,6 +83,20 @@ def _is_followup_hint_only(text: str) -> bool:
68
  }
69
 
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def _sanitize_question_text(text: str) -> str:
72
  raw = (text or "").strip()
73
  if not raw:
@@ -77,45 +106,33 @@ def _sanitize_question_text(text: str) -> str:
77
 
78
  # Unity payload safety: take the actual question line
79
  if len(lines) >= 9 and re.fullmatch(r"-?\d+", lines[2]) and lines[4].lower() in {"true", "false"}:
80
- return lines[1]
81
 
82
  # If the first line is a command and second line looks like the question, use the second line
83
  if len(lines) >= 2 and lines[0].lower() in {
84
  "hint", "next hint", "another hint", "continue", "go on", "walkthrough", "step by step", "solve"
85
  }:
86
- return lines[1]
87
 
88
  # If clearly corrupted multiline payload leaked in, use first substantive line that looks like a question/math line
89
  for line in lines:
 
90
  if (
91
- "=" in line
92
- or "%" in line
93
- or ":" in line
94
- or re.search(r"\b(probability|ratio|percent|integer|triangle|circle|mean|median)\b", line.lower())
95
- or re.search(r"[a-zA-Z]\s*[\+\-\*/=]", line)
96
- or re.search(r"[\+\-\*/=]\s*[a-zA-Z0-9]", line)
97
  ):
98
- return line
99
 
100
- return lines[0] if lines else raw
101
 
102
 
103
  def _extract_question_text(raw_user_text: str) -> str:
104
  text = _clean_text(raw_user_text)
105
- lowered = text.lower()
106
-
107
- prefixes = [
108
- "solve:",
109
- "solve ",
110
- "question:",
111
- "q:",
112
- ]
113
-
114
- for p in prefixes:
115
- if lowered.startswith(p):
116
- return text[len(p):].strip()
117
-
118
- return text
119
 
120
 
121
  def _classify_input_type(raw_user_text: str) -> str:
@@ -140,6 +157,15 @@ def _classify_input_type(raw_user_text: str) -> str:
140
  if first in structured_map:
141
  return structured_map[first]
142
 
 
 
 
 
 
 
 
 
 
143
  if t in {"hint", "a hint", "give me a hint", "can i have a hint"}:
144
  return "hint"
145
 
@@ -163,13 +189,12 @@ def _classify_input_type(raw_user_text: str) -> str:
163
  ]):
164
  return "confusion"
165
 
166
- if t.startswith("solve:") or t.startswith("solve "):
167
- return "solve"
168
 
169
- if "=" in text and re.search(r"[A-Za-z]", text):
170
  return "question"
171
 
172
- if any(k in t for k in [
173
  "what is",
174
  "find",
175
  "if ",
@@ -264,7 +289,8 @@ def _choose_effective_question_text(
264
  question_text=question_text,
265
  chat_history=chat_history,
266
  )
267
- return _sanitize_question_text(recovered), bool(_sanitize_question_text(recovered))
 
268
 
269
  if explicit_question:
270
  return explicit_question, False
@@ -366,12 +392,17 @@ def _normalize_classified_topic(topic: Optional[str], category: Optional[str], q
366
  return "general"
367
 
368
 
 
 
 
 
369
  def _safe_steps(steps: List[str]) -> List[str]:
370
  cleaned: List[str] = []
371
  banned_patterns = [
372
  r"\bthe answer is\b",
373
  r"\banswer:\b",
374
  r"\bthat gives\b",
 
375
  r"\btherefore\b",
376
  r"\bso x\s*=",
377
  r"\bso y\s*=",
@@ -382,11 +413,12 @@ def _safe_steps(steps: List[str]) -> List[str]:
382
  ]
383
 
384
  for step in steps:
385
- s = (step or "").strip()
386
  lowered = s.lower()
387
  if any(re.search(p, lowered) for p in banned_patterns):
388
  continue
389
- cleaned.append(s)
 
390
 
391
  return cleaned
392
 
@@ -436,41 +468,99 @@ def _extract_explainer_scaffold(explainer_result: Any) -> Dict[str, Any]:
436
  }
437
 
438
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  def _compose_reply(
440
  result: SolverResult,
441
  intent: str,
442
  verbosity: float,
443
  category: Optional[str] = None,
 
 
444
  ) -> str:
445
- steps = _safe_steps(result.steps or [])
446
  topic = (result.topic or "").lower()
 
 
447
 
448
  if intent == "hint":
449
- return steps[0] if steps else "Start by identifying the main relationship in the problem."
450
 
451
  if intent == "instruction":
452
- return (
453
- f"First step: {steps[0]}"
454
- if steps else
455
- "First, identify the key relationship or comparison in the question."
456
- )
457
 
458
  if intent == "definition":
459
- return (
460
- f"Here is the idea in context:\n- {steps[0]}"
461
- if steps else
462
- "This is asking for the meaning of the term or idea in the question."
463
- )
464
 
465
  if intent in {"walkthrough", "step_by_step"}:
466
- if steps:
467
- shown_steps = (
468
- steps[:2] if verbosity < 0.25 else
469
- steps[:3] if verbosity < 0.6 else
470
- steps[:4] if verbosity < 0.85 else
471
- steps
472
- )
473
- return "\n".join(f"- {s}" for s in shown_steps)
474
 
475
  if topic == "algebra":
476
  return "\n".join([
@@ -659,6 +749,19 @@ class ConversationEngine:
659
  meta={},
660
  )
661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  explainer_result = route_explainer(solver_input)
663
  explainer_understood = bool(
664
  explainer_result is not None and getattr(explainer_result, "understood", False)
@@ -671,18 +774,6 @@ class ConversationEngine:
671
  if explainer_understood else []
672
  )
673
 
674
- if is_quant and resolved_help_mode in {"answer", "walkthrough", "instruction"}:
675
- solver_result = route_solver(solver_input)
676
- if solver_result is not None:
677
- original_meta = result.meta or {}
678
- result = solver_result
679
- result.domain = "quant"
680
- result.meta = result.meta or {}
681
- result.meta.update(original_meta)
682
-
683
- if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
684
- result.topic = question_topic
685
-
686
  # force the externally resolved mode after solver routing
687
  result.help_mode = resolved_help_mode
688
 
@@ -692,14 +783,16 @@ class ConversationEngine:
692
  result.meta["recovered_question_text"] = solver_input
693
 
694
  if explainer_understood:
695
- result.meta["explainer_used"] = resolved_help_mode in {"explain", "hint"}
696
  result.meta["bridge_ready"] = bool(explainer_meta.get("bridge_ready", False))
697
  result.meta["hint_style"] = explainer_meta.get("hint_style")
698
  result.meta["explainer_summary"] = explainer_summary
699
  result.meta["explainer_teaching_points"] = explainer_teaching_points
700
  result.meta["scaffold"] = explainer_scaffold
701
 
702
- if resolved_help_mode in {"explain", "hint"} and explainer_understood:
 
 
703
  reply = format_explainer_response(
704
  result=explainer_result,
705
  tone=tone,
@@ -709,12 +802,26 @@ class ConversationEngine:
709
  hint_stage=hint_stage,
710
  )
711
  result.solved = False
 
 
 
 
 
 
 
 
 
 
 
 
712
  else:
713
  reply_core = _compose_reply(
714
  result=result,
715
  intent=resolved_intent,
716
  verbosity=verbosity,
717
  category=inferred_category,
 
 
718
  )
719
  reply = format_reply(
720
  reply_core,
@@ -739,6 +846,7 @@ class ConversationEngine:
739
  result.answer_letter = None
740
  result.answer_value = None
741
  result.internal_answer = None
 
742
 
743
  state = _update_session_state(
744
  state,
 
1
  from __future__ import annotations
2
 
3
  import re
4
+ from typing import Any, Dict, List, Optional, Tuple
5
 
6
  from context_parser import detect_intent, intent_to_help_mode
7
  from formatting import format_reply, format_explainer_response
 
25
  ]
26
 
27
 
28
+ CONTROL_PREFIX_PATTERNS = [
29
+ r"^\s*solve\s*:\s*",
30
+ r"^\s*solve\s+",
31
+ r"^\s*question\s*:\s*",
32
+ r"^\s*q\s*:\s*",
33
+ r"^\s*hint\s*:\s*",
34
+ r"^\s*next hint\s*:\s*",
35
+ r"^\s*another hint\s*:\s*",
36
+ r"^\s*walkthrough\s*:\s*",
37
+ r"^\s*step by step\s*:\s*",
38
+ r"^\s*explain\s*:\s*",
39
+ r"^\s*method\s*:\s*",
40
+ ]
41
+
42
+
43
  def _clean_text(text: Optional[str]) -> str:
44
  return (text or "").strip()
45
 
 
83
  }
84
 
85
 
86
+ def _strip_control_prefix(text: str) -> str:
87
+ cleaned = (text or "").strip()
88
+ if not cleaned:
89
+ return ""
90
+
91
+ previous = None
92
+ while previous != cleaned:
93
+ previous = cleaned
94
+ for pattern in CONTROL_PREFIX_PATTERNS:
95
+ cleaned = re.sub(pattern, "", cleaned, flags=re.I).strip()
96
+
97
+ return cleaned
98
+
99
+
100
  def _sanitize_question_text(text: str) -> str:
101
  raw = (text or "").strip()
102
  if not raw:
 
106
 
107
  # Unity payload safety: take the actual question line
108
  if len(lines) >= 9 and re.fullmatch(r"-?\d+", lines[2]) and lines[4].lower() in {"true", "false"}:
109
+ return _strip_control_prefix(lines[1])
110
 
111
  # If the first line is a command and second line looks like the question, use the second line
112
  if len(lines) >= 2 and lines[0].lower() in {
113
  "hint", "next hint", "another hint", "continue", "go on", "walkthrough", "step by step", "solve"
114
  }:
115
+ return _strip_control_prefix(lines[1])
116
 
117
  # If clearly corrupted multiline payload leaked in, use first substantive line that looks like a question/math line
118
  for line in lines:
119
+ candidate = _strip_control_prefix(line)
120
  if (
121
+ "=" in candidate
122
+ or "%" in candidate
123
+ or ":" in candidate
124
+ or re.search(r"\b(probability|ratio|percent|integer|triangle|circle|mean|median)\b", candidate.lower())
125
+ or re.search(r"[a-zA-Z]\s*[\+\-\*/=]", candidate)
126
+ or re.search(r"[\+\-\*/=]\s*[a-zA-Z0-9]", candidate)
127
  ):
128
+ return candidate
129
 
130
+ return _strip_control_prefix(lines[0] if lines else raw)
131
 
132
 
133
  def _extract_question_text(raw_user_text: str) -> str:
134
  text = _clean_text(raw_user_text)
135
+ return _strip_control_prefix(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
 
138
  def _classify_input_type(raw_user_text: str) -> str:
 
157
  if first in structured_map:
158
  return structured_map[first]
159
 
160
+ if t.startswith("hint:"):
161
+ return "hint"
162
+ if t.startswith("next hint:") or t.startswith("another hint:"):
163
+ return "next_hint"
164
+ if t.startswith("walkthrough:") or t.startswith("step by step:"):
165
+ return "confusion"
166
+ if t.startswith("solve:") or t.startswith("solve "):
167
+ return "solve"
168
+
169
  if t in {"hint", "a hint", "give me a hint", "can i have a hint"}:
170
  return "hint"
171
 
 
189
  ]):
190
  return "confusion"
191
 
192
+ stripped = _strip_control_prefix(text)
 
193
 
194
+ if "=" in stripped and re.search(r"[A-Za-z]", stripped):
195
  return "question"
196
 
197
+ if any(k in stripped.lower() for k in [
198
  "what is",
199
  "find",
200
  "if ",
 
289
  question_text=question_text,
290
  chat_history=chat_history,
291
  )
292
+ cleaned = _sanitize_question_text(recovered)
293
+ return cleaned, bool(cleaned)
294
 
295
  if explicit_question:
296
  return explicit_question, False
 
392
  return "general"
393
 
394
 
395
+ def _strip_bullet_prefix(text: str) -> str:
396
+ return re.sub(r"^\s*-\s*", "", (text or "").strip())
397
+
398
+
399
  def _safe_steps(steps: List[str]) -> List[str]:
400
  cleaned: List[str] = []
401
  banned_patterns = [
402
  r"\bthe answer is\b",
403
  r"\banswer:\b",
404
  r"\bthat gives\b",
405
+ r"\bthis gives\b",
406
  r"\btherefore\b",
407
  r"\bso x\s*=",
408
  r"\bso y\s*=",
 
413
  ]
414
 
415
  for step in steps:
416
+ s = _strip_bullet_prefix(step)
417
  lowered = s.lower()
418
  if any(re.search(p, lowered) for p in banned_patterns):
419
  continue
420
+ if s:
421
+ cleaned.append(s)
422
 
423
  return cleaned
424
 
 
468
  }
469
 
470
 
471
+ def _get_result_steps(result: SolverResult) -> List[str]:
472
+ display_steps = getattr(result, "display_steps", None)
473
+ if isinstance(display_steps, list) and display_steps:
474
+ return _safe_steps(display_steps)
475
+
476
+ result_steps = getattr(result, "steps", None)
477
+ if isinstance(result_steps, list) and result_steps:
478
+ return _safe_steps(result_steps)
479
+
480
+ meta = getattr(result, "meta", {}) or {}
481
+ meta_display_steps = meta.get("display_steps")
482
+ if isinstance(meta_display_steps, list) and meta_display_steps:
483
+ return _safe_steps(meta_display_steps)
484
+
485
+ meta_steps = meta.get("steps")
486
+ if isinstance(meta_steps, list) and meta_steps:
487
+ return _safe_steps(meta_steps)
488
+
489
+ return []
490
+
491
+
492
+ def _solver_has_useful_steps(result: Optional[SolverResult]) -> bool:
493
+ if result is None:
494
+ return False
495
+ return len(_get_result_steps(result)) > 0
496
+
497
+
498
+ def _build_hint_from_steps(
499
+ steps: List[str],
500
+ hint_stage: int,
501
+ fallback: Optional[List[str]] = None,
502
+ ) -> str:
503
+ safe_steps = _safe_steps(steps)
504
+ if safe_steps:
505
+ stage = max(1, min(int(hint_stage or 1), 3))
506
+ if stage == 1:
507
+ return f"- {safe_steps[0]}"
508
+ if stage == 2:
509
+ return "\n".join(f"- {s}" for s in safe_steps[:2])
510
+ return "\n".join(f"- {s}" for s in safe_steps[:3])
511
+
512
+ fallback = _safe_steps(fallback or [])
513
+ if fallback:
514
+ stage = max(1, min(int(hint_stage or 1), 3))
515
+ return "\n".join(f"- {s}" for s in fallback[:stage])
516
+
517
+ return "- Start by identifying the main relationship in the problem."
518
+
519
+
520
+ def _walkthrough_from_steps(steps: List[str], verbosity: float) -> str:
521
+ safe_steps = _safe_steps(steps)
522
+ if not safe_steps:
523
+ return ""
524
+
525
+ shown_steps = (
526
+ safe_steps[:2] if verbosity < 0.25 else
527
+ safe_steps[:3] if verbosity < 0.6 else
528
+ safe_steps[:4] if verbosity < 0.85 else
529
+ safe_steps
530
+ )
531
+ return "\n".join(f"- {s}" for s in shown_steps)
532
+
533
+
534
  def _compose_reply(
535
  result: SolverResult,
536
  intent: str,
537
  verbosity: float,
538
  category: Optional[str] = None,
539
+ hint_stage: int = 0,
540
+ explainer_scaffold: Optional[Dict[str, Any]] = None,
541
  ) -> str:
542
+ steps = _get_result_steps(result)
543
  topic = (result.topic or "").lower()
544
+ scaffold = explainer_scaffold or {}
545
+ scaffold_hint_ladder = _safe_meta_list(scaffold.get("hint_ladder", []))
546
 
547
  if intent == "hint":
548
+ return _build_hint_from_steps(steps, hint_stage=hint_stage, fallback=scaffold_hint_ladder)
549
 
550
  if intent == "instruction":
551
+ if steps:
552
+ return f"First step: {steps[0]}"
553
+ return "First, identify the key relationship or comparison in the question."
 
 
554
 
555
  if intent == "definition":
556
+ if steps:
557
+ return f"Here is the idea in context:\n- {steps[0]}"
558
+ return "This is asking for the meaning of the term or idea in the question."
 
 
559
 
560
  if intent in {"walkthrough", "step_by_step"}:
561
+ step_reply = _walkthrough_from_steps(steps, verbosity=verbosity)
562
+ if step_reply:
563
+ return step_reply
 
 
 
 
 
564
 
565
  if topic == "algebra":
566
  return "\n".join([
 
749
  meta={},
750
  )
751
 
752
+ solver_result: Optional[SolverResult] = None
753
+ if is_quant and resolved_help_mode in {"answer", "walkthrough", "instruction", "hint"}:
754
+ solver_result = route_solver(solver_input)
755
+ if solver_result is not None:
756
+ original_meta = result.meta or {}
757
+ result = solver_result
758
+ result.domain = "quant"
759
+ result.meta = result.meta or {}
760
+ result.meta.update(original_meta)
761
+
762
+ if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
763
+ result.topic = question_topic
764
+
765
  explainer_result = route_explainer(solver_input)
766
  explainer_understood = bool(
767
  explainer_result is not None and getattr(explainer_result, "understood", False)
 
774
  if explainer_understood else []
775
  )
776
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  # force the externally resolved mode after solver routing
778
  result.help_mode = resolved_help_mode
779
 
 
783
  result.meta["recovered_question_text"] = solver_input
784
 
785
  if explainer_understood:
786
+ result.meta["explainer_used"] = False
787
  result.meta["bridge_ready"] = bool(explainer_meta.get("bridge_ready", False))
788
  result.meta["hint_style"] = explainer_meta.get("hint_style")
789
  result.meta["explainer_summary"] = explainer_summary
790
  result.meta["explainer_teaching_points"] = explainer_teaching_points
791
  result.meta["scaffold"] = explainer_scaffold
792
 
793
+ use_solver_steps_for_hint = resolved_help_mode == "hint" and _solver_has_useful_steps(result)
794
+
795
+ if resolved_help_mode == "explain" and explainer_understood:
796
  reply = format_explainer_response(
797
  result=explainer_result,
798
  tone=tone,
 
802
  hint_stage=hint_stage,
803
  )
804
  result.solved = False
805
+ result.meta["explainer_used"] = True
806
+ elif resolved_help_mode == "hint" and explainer_understood and not use_solver_steps_for_hint:
807
+ reply = format_explainer_response(
808
+ result=explainer_result,
809
+ tone=tone,
810
+ verbosity=verbosity,
811
+ transparency=transparency,
812
+ help_mode=resolved_help_mode,
813
+ hint_stage=hint_stage,
814
+ )
815
+ result.solved = False
816
+ result.meta["explainer_used"] = True
817
  else:
818
  reply_core = _compose_reply(
819
  result=result,
820
  intent=resolved_intent,
821
  verbosity=verbosity,
822
  category=inferred_category,
823
+ hint_stage=hint_stage,
824
+ explainer_scaffold=explainer_scaffold,
825
  )
826
  reply = format_reply(
827
  reply_core,
 
846
  result.answer_letter = None
847
  result.answer_value = None
848
  result.internal_answer = None
849
+ result.meta["internal_answer"] = None
850
 
851
  state = _update_session_state(
852
  state,