j-js commited on
Commit
16b615c
·
verified ·
1 Parent(s): 97c2dde

Update conversation_logic.py

Browse files
Files changed (1) hide show
  1. conversation_logic.py +301 -49
conversation_logic.py CHANGED
@@ -71,6 +71,7 @@ FOLLOWUP_ONLY_INPUTS = {
71
  "help",
72
  }
73
 
 
74
  def _clean_text(text: Optional[str]) -> str:
75
  return (text or "").strip()
76
 
@@ -255,7 +256,7 @@ def _build_topic_query_reply(question_text: str, fallback_topic: str, classified
255
  if specific == "algebra":
256
  return (
257
  "- This is an algebra question.\n"
258
- "- The key skill is rewriting the relationship cleanly, then simplifying the expression the question actually asks for."
259
  )
260
  if specific == "ratio":
261
  return (
@@ -270,7 +271,7 @@ def _build_topic_query_reply(question_text: str, fallback_topic: str, classified
270
  if specific == "probability":
271
  return (
272
  "- This is a probability question.\n"
273
- "- The key skill is deciding what counts as a successful outcome and then comparing favorable outcomes with the total possible outcomes."
274
  )
275
 
276
  label = specific if specific != "general" else (cat.lower() if cat else "quantitative reasoning")
@@ -283,6 +284,19 @@ def _classify_input_type(raw_user_text: str) -> str:
283
  return "empty"
284
  if _is_topic_query(raw_user_text):
285
  return "topic_query"
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  if text in {"hint", "a hint", "give me a hint", "can i have a hint"} or text.startswith("hint:"):
287
  return "hint"
288
  if text in {"next hint", "another hint", "more hint", "more hints", "next step", "continue", "go on"} or text.startswith("next hint:"):
@@ -299,6 +313,8 @@ def _classify_input_type(raw_user_text: str) -> str:
299
  "help me understand",
300
  "method",
301
  "explain",
 
 
302
  ]
303
  ):
304
  return "confusion"
@@ -392,7 +408,7 @@ def _compute_hint_stage(input_type: str, prior_hint_stage: int, fallback_history
392
  if input_type == "next_hint":
393
  return min((base if base > 0 else 1) + 1, 3)
394
  if input_type == "confusion":
395
- return 3
396
  return min(base, 3)
397
 
398
 
@@ -580,6 +596,15 @@ def _parse_numeric_option_set(option: str) -> Optional[List[float]]:
580
  return parts if len(parts) >= 2 else None
581
 
582
 
 
 
 
 
 
 
 
 
 
583
  def _question_specific_ratio_reply(question_text: str) -> str:
584
  q = _clean_text(question_text)
585
  low = q.lower()
@@ -611,12 +636,21 @@ def _question_specific_variability_reply(options_text: Optional[List[str]]) -> s
611
  )
612
 
613
 
614
- def _question_specific_percent_reply(question_text: str) -> str:
615
  clean = _clean_text(question_text)
616
  low = clean.lower()
 
617
  nums = re.findall(r"-?\d+(?:\.\d+)?", clean)
618
 
 
 
 
619
  if "increased by" in low and "decreased by" in low:
 
 
 
 
 
620
  return (
621
  "- For back-to-back percent changes, turn the changes into multipliers instead of trying to combine the percentages directly.\n"
622
  "- Apply the increase multiplier first, then the decrease multiplier to that new amount.\n"
@@ -625,6 +659,17 @@ def _question_specific_percent_reply(question_text: str) -> str:
625
 
626
  if "out of" in low and len(nums) >= 2:
627
  part, whole = nums[0], nums[1]
 
 
 
 
 
 
 
 
 
 
 
628
  return (
629
  f"- This is a part-over-whole percent question: start by writing the fraction as {part}/{whole}.\n"
630
  f"- Use {whole} as the base because it is the total, and {part} as the part that chose the option.\n"
@@ -632,6 +677,11 @@ def _question_specific_percent_reply(question_text: str) -> str:
632
  )
633
 
634
  if any(k in low for k in ["of", "what percent", "%"]):
 
 
 
 
 
635
  return (
636
  "- Ask 'percent of what?' first so you identify the correct base quantity.\n"
637
  "- Put the part over the whole before doing any percent conversion.\n"
@@ -644,12 +694,34 @@ def _question_specific_percent_reply(question_text: str) -> str:
644
  )
645
 
646
 
647
-
648
- def _question_specific_probability_reply(question_text: str, options_text: Optional[List[str]] = None) -> str:
649
  q = _clean_text(question_text)
650
  low = q.lower()
 
651
  option_count = len(options_text or [])
652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  single_draw_markers = [
654
  "chosen at random",
655
  "select one",
@@ -660,6 +732,8 @@ def _question_specific_probability_reply(question_text: str, options_text: Optio
660
  "one object",
661
  "selected at random",
662
  "picked at random",
 
 
663
  ]
664
  container_markers = [
665
  "box contains",
@@ -673,9 +747,21 @@ def _question_specific_probability_reply(question_text: str, options_text: Optio
673
  "coin",
674
  ]
675
 
676
- if any(m in low for m in single_draw_markers) or (
677
- "probability" in low and any(m in low for m in container_markers)
678
- ):
 
 
 
 
 
 
 
 
 
 
 
 
679
  lines = [
680
  "- Start by deciding what counts as a successful outcome in this question.",
681
  "- Then count the total number of possible outcomes in the container or sample space.",
@@ -686,6 +772,11 @@ def _question_specific_probability_reply(question_text: str, options_text: Optio
686
  return "\n".join(lines)
687
 
688
  if "at least" in low:
 
 
 
 
 
689
  return (
690
  "- Start by deciding whether the complement is easier than counting the requested cases directly.\n"
691
  "- For an 'at least' question, it is often simpler to find the probability of the opposite event first.\n"
@@ -693,17 +784,133 @@ def _question_specific_probability_reply(question_text: str, options_text: Optio
693
  )
694
 
695
  if any(k in low for k in ["and", "both", "then", "after"]) and any(k in low for k in ["probability", "chosen", "random"]):
 
 
 
 
 
696
  return (
697
  "- First identify whether the events happen together or separately.\n"
698
  "- Then decide whether you should multiply probabilities, add them, or use the complement.\n"
699
  "- Keep track of whether the total outcomes change after each step."
700
  )
701
 
 
 
 
 
 
 
702
  return (
703
  "- Start by identifying the favorable outcomes and the total possible outcomes.\n"
704
  "- Then build the probability as favorable over total before simplifying or matching an answer choice."
705
  )
706
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
  def _build_question_specific_reply(
708
  *,
709
  question_text: str,
@@ -743,20 +950,13 @@ def _build_question_specific_reply(
743
  if topic == "probability" or any(
744
  k in low for k in ["probability", "chance", "odds", "at random", "chosen at random"]
745
  ):
746
- return _question_specific_probability_reply(q, options_text)
747
 
748
  if topic in {"ratio", "algebra"}:
749
- if re.search(r"\b[a-z]\s*/\s*[a-z]\s*=\s*\d+\s*/\s*\d+", low):
750
- return _question_specific_ratio_reply(q)
751
- if "what is" in low and "(" in low and ")" in low and any(sym in low for sym in ["a+b", "x+y", "a-b", "x-y"]):
752
- return (
753
- "- Start by rewriting one variable in terms of the other using the relationship you were given.\n"
754
- "- Then substitute into the exact expression in parentheses, rather than trying to solve for actual numbers.\n"
755
- "- Simplify only after the whole target expression has been rewritten in one variable or in matching parts."
756
- )
757
 
758
  if topic == "percent" or "%" in low or "percent" in low:
759
- return _question_specific_percent_reply(q)
760
 
761
  if topic == "statistics" and any(k in low for k in ["dataset", "table", "chart", "graph"]):
762
  return (
@@ -764,17 +964,7 @@ def _build_question_specific_reply(
764
  "- Use the structure of the choices to compare them efficiently instead of computing unnecessary extra values."
765
  )
766
 
767
- if topic == "algebra":
768
- return (
769
- "- Turn the wording into one clean relationship first.\n"
770
- "- Then focus on the exact expression the question asks for, rather than solving more than you need to."
771
- )
772
-
773
  if explicit_help_ask:
774
- if topic == "percent":
775
- return _question_specific_percent_reply(q)
776
- if topic == "probability":
777
- return _question_specific_probability_reply(q, options_text)
778
  return "- Start by identifying the main relationship in the question, then use that relationship to set up the first step."
779
 
780
  return ""
@@ -905,7 +1095,7 @@ class ConversationEngine:
905
  if input_type in {"hint", "next_hint"}:
906
  resolved_intent = "hint"
907
  elif input_type == "confusion":
908
- resolved_intent = "walkthrough"
909
  elif input_type in {"solve", "question"} and resolved_intent in {"hint", "walkthrough", "step_by_step"}:
910
  resolved_intent = "answer"
911
 
@@ -913,7 +1103,7 @@ class ConversationEngine:
913
  if input_type in {"hint", "next_hint"}:
914
  resolved_help_mode = "hint"
915
  elif input_type == "confusion":
916
- resolved_help_mode = "walkthrough"
917
  elif resolved_help_mode == "step_by_step":
918
  resolved_help_mode = "walkthrough"
919
 
@@ -979,25 +1169,24 @@ class ConversationEngine:
979
  user_text=user_text,
980
  )
981
 
982
- # Merge solver result into base result only if it agrees with the classified topic.
983
  if solver_result is not None:
984
  result.meta = result.meta or {}
985
  solver_topic = getattr(solver_result, "topic", None) or "unknown"
986
-
987
  compatible_topics = {
988
  question_topic,
989
  "general_quant",
990
  "general",
991
  "unknown",
992
  }
993
- # allow a few sensible cross-matches
994
  if question_topic == "algebra":
995
  compatible_topics.update({"ratio"})
996
  elif question_topic == "ratio":
997
  compatible_topics.update({"algebra"})
998
  elif question_topic == "percent":
999
  compatible_topics.update({"ratio", "algebra"})
1000
-
1001
  if solver_topic in compatible_topics:
1002
  result = solver_result
1003
  result.domain = "quant"
@@ -1096,16 +1285,78 @@ class ConversationEngine:
1096
  "- Compare how far the smallest and largest values sit from the middle value in each dataset.\n"
1097
  "- The set with the widest spread has the greatest variability."
1098
  )
1099
- # Source selection priority:
1100
- # 1) topic query (returned earlier)
1101
- # 2) question-specific guidance for help-first asks when available
1102
- # 3) question-support / explainer fallback
1103
- # 4) solver steps for direct solve / walkthrough
1104
- # 5) generic fallback
1105
- if question_specific_reply_core and (
1106
- _is_help_first_mode(resolved_help_mode)
1107
- or input_type in {"other", "confusion", "hint", "next_hint"}
1108
- or any(phrase in _clean_text(user_text).lower() for phrase in ["how do i solve", "what do i do first", "what should i do first", "how should i start"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1109
  ):
1110
  reply_core = question_specific_reply_core
1111
  result.meta["response_source"] = "question_specific"
@@ -1166,7 +1417,7 @@ class ConversationEngine:
1166
  elif (
1167
  resolved_help_mode == "answer"
1168
  and solver_has_steps
1169
- and result.meta.get("solver_topic_rejected") is None
1170
  and direct_solve_request
1171
  ):
1172
  reply_core = _answer_path_from_steps(solver_steps, verbosity=verbosity)
@@ -1185,7 +1436,7 @@ class ConversationEngine:
1185
  resolved_help_mode == "walkthrough"
1186
  and solver_has_steps
1187
  and not prefer_question_support
1188
- and result.meta.get("solver_topic_rejected") is None
1189
  ):
1190
  reply_core = _answer_path_from_steps(solver_steps, verbosity=verbosity)
1191
  result.meta["response_source"] = "solver_steps"
@@ -1229,14 +1480,14 @@ class ConversationEngine:
1229
  hint_stage=hint_stage,
1230
  topic=result.topic,
1231
  )
1232
- # Never reveal final answers during tutoring/help modes.
1233
  if resolved_help_mode in {"hint", "walkthrough", "explain", "instruction", "step_by_step"}:
1234
  result.solved = False
1235
  result.answer_letter = None
1236
  result.answer_value = None
1237
  result.internal_answer = None
1238
  result.meta["internal_answer"] = None
1239
- # Only allow answer metadata to survive for true direct solve requests.
1240
  can_reveal_answer = bool(result.solved and direct_solve_request and not _is_help_first_mode(resolved_help_mode))
1241
  result.meta["can_reveal_answer"] = can_reveal_answer
1242
  if not can_reveal_answer:
@@ -1257,6 +1508,7 @@ class ConversationEngine:
1257
  topic=result.topic,
1258
  category=inferred_category,
1259
  )
 
1260
  result.reply = reply
1261
  result.help_mode = resolved_help_mode
1262
  result.meta["help_mode"] = resolved_help_mode
 
71
  "help",
72
  }
73
 
74
+
75
  def _clean_text(text: Optional[str]) -> str:
76
  return (text or "").strip()
77
 
 
256
  if specific == "algebra":
257
  return (
258
  "- This is an algebra question.\n"
259
+ "- The key skill is undoing the operations around the variable in a logical order."
260
  )
261
  if specific == "ratio":
262
  return (
 
271
  if specific == "probability":
272
  return (
273
  "- This is a probability question.\n"
274
+ "- The key skill is deciding what counts as a successful outcome and then comparing favorable outcomes with total outcomes."
275
  )
276
 
277
  label = specific if specific != "general" else (cat.lower() if cat else "quantitative reasoning")
 
284
  return "empty"
285
  if _is_topic_query(raw_user_text):
286
  return "topic_query"
287
+
288
+ if any(
289
+ p in text
290
+ for p in [
291
+ "what do i do first",
292
+ "what should i do first",
293
+ "first step",
294
+ "where do i start",
295
+ "how should i start",
296
+ ]
297
+ ):
298
+ return "hint"
299
+
300
  if text in {"hint", "a hint", "give me a hint", "can i have a hint"} or text.startswith("hint:"):
301
  return "hint"
302
  if text in {"next hint", "another hint", "more hint", "more hints", "next step", "continue", "go on"} or text.startswith("next hint:"):
 
313
  "help me understand",
314
  "method",
315
  "explain",
316
+ "how do i solve",
317
+ "how do i do this",
318
  ]
319
  ):
320
  return "confusion"
 
408
  if input_type == "next_hint":
409
  return min((base if base > 0 else 1) + 1, 3)
410
  if input_type == "confusion":
411
+ return 1
412
  return min(base, 3)
413
 
414
 
 
596
  return parts if len(parts) >= 2 else None
597
 
598
 
599
+ def _looks_like_simple_linear_equation(question_text: str) -> bool:
600
+ q = _clean_text(question_text).lower()
601
+ return bool(
602
+ "=" in q
603
+ and re.search(r"\bwhat is\s+[a-z]\b", q)
604
+ and re.search(r"\b\d+[a-z]\b|\b[a-z]\b", q)
605
+ )
606
+
607
+
608
  def _question_specific_ratio_reply(question_text: str) -> str:
609
  q = _clean_text(question_text)
610
  low = q.lower()
 
636
  )
637
 
638
 
639
+ def _question_specific_percent_reply(question_text: str, user_text: str = "") -> str:
640
  clean = _clean_text(question_text)
641
  low = clean.lower()
642
+ user_low = _clean_text(user_text).lower()
643
  nums = re.findall(r"-?\d+(?:\.\d+)?", clean)
644
 
645
+ wants_first = any(p in user_low for p in ["what should i do first", "what do i do first", "first step", "where do i start", "how should i start"])
646
+ wants_method = any(p in user_low for p in ["how do i solve", "how do i do this", "method", "walkthrough", "step by step", "explain"])
647
+
648
  if "increased by" in low and "decreased by" in low:
649
+ if wants_first:
650
+ return (
651
+ "- First turn each percent change into a multiplier instead of combining the percentages directly.\n"
652
+ "- Apply the increase multiplier to the original amount, then apply the decrease multiplier to the updated amount."
653
+ )
654
  return (
655
  "- For back-to-back percent changes, turn the changes into multipliers instead of trying to combine the percentages directly.\n"
656
  "- Apply the increase multiplier first, then the decrease multiplier to that new amount.\n"
 
659
 
660
  if "out of" in low and len(nums) >= 2:
661
  part, whole = nums[0], nums[1]
662
+ if wants_first:
663
+ return (
664
+ f"- First write the relationship as the fraction {part}/{whole}.\n"
665
+ f"- Use {whole} as the total and {part} as the part before doing any percent conversion."
666
+ )
667
+ if wants_method:
668
+ return (
669
+ f"- This is a part-over-whole percent question, so start by writing {part}/{whole}.\n"
670
+ f"- Use {whole} as the base because it is the total, and {part} as the part that matches the condition.\n"
671
+ "- Then convert that fraction to a percent by simplifying or turning it into a decimal and multiplying by 100."
672
+ )
673
  return (
674
  f"- This is a part-over-whole percent question: start by writing the fraction as {part}/{whole}.\n"
675
  f"- Use {whole} as the base because it is the total, and {part} as the part that chose the option.\n"
 
677
  )
678
 
679
  if any(k in low for k in ["of", "what percent", "%"]):
680
+ if wants_first:
681
+ return (
682
+ "- First ask 'percent of what?' so you identify the correct base quantity.\n"
683
+ "- Then put the part over the whole before converting anything to a percent."
684
+ )
685
  return (
686
  "- Ask 'percent of what?' first so you identify the correct base quantity.\n"
687
  "- Put the part over the whole before doing any percent conversion.\n"
 
694
  )
695
 
696
 
697
+ def _question_specific_probability_reply(question_text: str, user_text: str = "", options_text: Optional[List[str]] = None) -> str:
 
698
  q = _clean_text(question_text)
699
  low = q.lower()
700
+ user_low = _clean_text(user_text).lower()
701
  option_count = len(options_text or [])
702
 
703
+ wants_first = any(
704
+ phrase in user_low
705
+ for phrase in [
706
+ "what should i do first",
707
+ "what do i do first",
708
+ "first step",
709
+ "where do i start",
710
+ "how should i start",
711
+ ]
712
+ )
713
+ wants_method = any(
714
+ phrase in user_low
715
+ for phrase in [
716
+ "how do i solve",
717
+ "how do i do this",
718
+ "method",
719
+ "walkthrough",
720
+ "step by step",
721
+ "explain",
722
+ ]
723
+ )
724
+
725
  single_draw_markers = [
726
  "chosen at random",
727
  "select one",
 
732
  "one object",
733
  "selected at random",
734
  "picked at random",
735
+ "one ball is chosen",
736
+ "one card is drawn",
737
  ]
738
  container_markers = [
739
  "box contains",
 
747
  "coin",
748
  ]
749
 
750
+ if any(m in low for m in single_draw_markers) or ("probability" in low and any(m in low for m in container_markers)):
751
+ if wants_first:
752
+ return (
753
+ "- First decide what counts as a successful outcome.\n"
754
+ "- Then count the total number of possible outcomes in the box, bag, or sample space."
755
+ )
756
+ if wants_method:
757
+ lines = [
758
+ "- For a one-draw probability question, use favorable outcomes over total outcomes.",
759
+ "- Count how many outcomes match the condition, then count the total number of possible outcomes.",
760
+ "- Build the fraction favorable/total before matching it to an answer choice.",
761
+ ]
762
+ if option_count:
763
+ lines.append("- Once the fraction is set up, compare it directly with the options.")
764
+ return "\n".join(lines)
765
  lines = [
766
  "- Start by deciding what counts as a successful outcome in this question.",
767
  "- Then count the total number of possible outcomes in the container or sample space.",
 
772
  return "\n".join(lines)
773
 
774
  if "at least" in low:
775
+ if wants_first:
776
+ return (
777
+ "- First check whether the complement is easier than counting the requested cases directly.\n"
778
+ "- For 'at least' problems, the opposite event is often simpler to compute first."
779
+ )
780
  return (
781
  "- Start by deciding whether the complement is easier than counting the requested cases directly.\n"
782
  "- For an 'at least' question, it is often simpler to find the probability of the opposite event first.\n"
 
784
  )
785
 
786
  if any(k in low for k in ["and", "both", "then", "after"]) and any(k in low for k in ["probability", "chosen", "random"]):
787
+ if wants_first:
788
+ return (
789
+ "- First decide whether the events happen together or separately.\n"
790
+ "- Then work out whether you need multiplication, addition, or the complement rule."
791
+ )
792
  return (
793
  "- First identify whether the events happen together or separately.\n"
794
  "- Then decide whether you should multiply probabilities, add them, or use the complement.\n"
795
  "- Keep track of whether the total outcomes change after each step."
796
  )
797
 
798
+ if wants_first:
799
+ return (
800
+ "- First identify the favorable outcomes.\n"
801
+ "- Then identify the total possible outcomes before simplifying anything."
802
+ )
803
+
804
  return (
805
  "- Start by identifying the favorable outcomes and the total possible outcomes.\n"
806
  "- Then build the probability as favorable over total before simplifying or matching an answer choice."
807
  )
808
 
809
+
810
+ def _question_specific_algebra_reply(question_text: str, user_text: str = "") -> str:
811
+ q = _clean_text(question_text)
812
+ low = q.lower()
813
+ user_low = _clean_text(user_text).lower()
814
+
815
+ wants_first = any(
816
+ phrase in user_low
817
+ for phrase in [
818
+ "what should i do first",
819
+ "what do i do first",
820
+ "first step",
821
+ "where do i start",
822
+ "how should i start",
823
+ ]
824
+ )
825
+
826
+ if _looks_like_simple_linear_equation(q):
827
+ if wants_first:
828
+ return (
829
+ "- First look at the variable side and ask which operation is furthest away from the variable.\n"
830
+ "- Undo that outside addition or subtraction on both sides before touching the coefficient."
831
+ )
832
+ return (
833
+ "- Treat this as a linear equation and undo the operations around the variable in reverse order.\n"
834
+ "- First remove the constant attached to the variable side by doing the opposite operation on both sides.\n"
835
+ "- Then undo the multiplication or division on the variable to isolate it."
836
+ )
837
+
838
+ if re.search(r"\b[a-z]\s*/\s*[a-z]\s*=\s*\d+\s*/\s*\d+", low):
839
+ return _question_specific_ratio_reply(q)
840
+
841
+ if "what is" in low and "(" in low and ")" in low and any(sym in low for sym in ["a+b", "x+y", "a-b", "x-y"]):
842
+ return (
843
+ "- Start by rewriting one variable in terms of the other using the relationship you were given.\n"
844
+ "- Then substitute into the exact expression in parentheses, rather than trying to solve for actual numbers.\n"
845
+ "- Simplify only after the whole target expression has been rewritten in one variable or in matching parts."
846
+ )
847
+
848
+ if wants_first:
849
+ return (
850
+ "- First turn the wording into one clean equation.\n"
851
+ "- Then decide which operation around the variable should be undone first."
852
+ )
853
+
854
+ return (
855
+ "- Turn the wording into one clean equation first.\n"
856
+ "- Then undo the operations around the variable in reverse order until the variable stands alone."
857
+ )
858
+
859
+
860
+ def _question_specific_hint_ladder(
861
+ *,
862
+ question_text: str,
863
+ options_text: Optional[List[str]],
864
+ classified_topic: str,
865
+ ) -> List[str]:
866
+ q = _clean_text(question_text)
867
+ low = q.lower()
868
+ topic = (classified_topic or "general").lower()
869
+
870
+ if _looks_like_simple_linear_equation(q) or topic == "algebra":
871
+ return [
872
+ "Look at the variable side and ask which operation is furthest away from the variable.",
873
+ "Undo the addition or subtraction first by doing the opposite on both sides.",
874
+ "Once the variable term is alone, undo the multiplication or division on the variable.",
875
+ ]
876
+
877
+ if topic == "probability" or any(k in low for k in ["probability", "chance", "odds", "at random", "chosen at random"]):
878
+ return [
879
+ "What counts as a successful outcome here?",
880
+ "How many total possible outcomes are there?",
881
+ "Set up the probability as favorable over total before comparing answer choices.",
882
+ ]
883
+
884
+ if topic == "percent" or "%" in low or "percent" in low:
885
+ if "out of" in low:
886
+ return [
887
+ "Which number is the part and which number is the total?",
888
+ "Write the relationship as part over whole before converting anything.",
889
+ "Once the fraction is correct, convert it to a percent.",
890
+ ]
891
+ return [
892
+ "Ask 'percent of what?' first.",
893
+ "Put the part over the base quantity.",
894
+ "Only multiply by 100 after the fraction or equation is set up correctly.",
895
+ ]
896
+
897
+ if any(k in low for k in ["variability", "spread", "standard deviation"]):
898
+ return [
899
+ "This is about spread, not average.",
900
+ "Compare how far the outer values sit from the middle value in each set.",
901
+ "The set with the widest spread has the greatest variability.",
902
+ ]
903
+
904
+ if re.search(r"\b[a-z]\s*/\s*[a-z]\s*=\s*\d+\s*/\s*\d+", low):
905
+ return [
906
+ "Rewrite the ratio using matching parts such as 3k and 4k.",
907
+ "Substitute those matching parts into the expression the question asks for.",
908
+ "Simplify after substitution by cancelling the common factor.",
909
+ ]
910
+
911
+ return []
912
+
913
+
914
  def _build_question_specific_reply(
915
  *,
916
  question_text: str,
 
950
  if topic == "probability" or any(
951
  k in low for k in ["probability", "chance", "odds", "at random", "chosen at random"]
952
  ):
953
+ return _question_specific_probability_reply(q, user_low, options_text)
954
 
955
  if topic in {"ratio", "algebra"}:
956
+ return _question_specific_algebra_reply(q, user_low)
 
 
 
 
 
 
 
957
 
958
  if topic == "percent" or "%" in low or "percent" in low:
959
+ return _question_specific_percent_reply(q, user_low)
960
 
961
  if topic == "statistics" and any(k in low for k in ["dataset", "table", "chart", "graph"]):
962
  return (
 
964
  "- Use the structure of the choices to compare them efficiently instead of computing unnecessary extra values."
965
  )
966
 
 
 
 
 
 
 
967
  if explicit_help_ask:
 
 
 
 
968
  return "- Start by identifying the main relationship in the question, then use that relationship to set up the first step."
969
 
970
  return ""
 
1095
  if input_type in {"hint", "next_hint"}:
1096
  resolved_intent = "hint"
1097
  elif input_type == "confusion":
1098
+ resolved_intent = "method"
1099
  elif input_type in {"solve", "question"} and resolved_intent in {"hint", "walkthrough", "step_by_step"}:
1100
  resolved_intent = "answer"
1101
 
 
1103
  if input_type in {"hint", "next_hint"}:
1104
  resolved_help_mode = "hint"
1105
  elif input_type == "confusion":
1106
+ resolved_help_mode = "explain"
1107
  elif resolved_help_mode == "step_by_step":
1108
  resolved_help_mode = "walkthrough"
1109
 
 
1169
  user_text=user_text,
1170
  )
1171
 
 
1172
  if solver_result is not None:
1173
  result.meta = result.meta or {}
1174
  solver_topic = getattr(solver_result, "topic", None) or "unknown"
1175
+
1176
  compatible_topics = {
1177
  question_topic,
1178
  "general_quant",
1179
  "general",
1180
  "unknown",
1181
  }
1182
+
1183
  if question_topic == "algebra":
1184
  compatible_topics.update({"ratio"})
1185
  elif question_topic == "ratio":
1186
  compatible_topics.update({"algebra"})
1187
  elif question_topic == "percent":
1188
  compatible_topics.update({"ratio", "algebra"})
1189
+
1190
  if solver_topic in compatible_topics:
1191
  result = solver_result
1192
  result.domain = "quant"
 
1285
  "- Compare how far the smallest and largest values sit from the middle value in each dataset.\n"
1286
  "- The set with the widest spread has the greatest variability."
1287
  )
1288
+
1289
+ if input_type in {"hint", "next_hint"}:
1290
+ hint_lines: List[str] = []
1291
+
1292
+ custom_ladder = _question_specific_hint_ladder(
1293
+ question_text=solver_input,
1294
+ options_text=options_text,
1295
+ classified_topic=question_topic,
1296
+ )
1297
+ if custom_ladder:
1298
+ idx = min(max(hint_stage - 1, 0), len(custom_ladder) - 1)
1299
+ hint_lines = [custom_ladder[idx]]
1300
+
1301
+ if not hint_lines and explainer_scaffold:
1302
+ ladder = _safe_meta_list(explainer_scaffold.get("hint_ladder", []))
1303
+ first_move = _safe_meta_text(explainer_scaffold.get("first_move"))
1304
+ next_hint_text = _safe_meta_text(explainer_scaffold.get("next_hint"))
1305
+
1306
+ if hint_stage <= 1 and first_move:
1307
+ hint_lines = [first_move]
1308
+ elif ladder:
1309
+ idx = min(max(hint_stage - 1, 0), len(ladder) - 1)
1310
+ hint_lines = [ladder[idx]]
1311
+ elif next_hint_text:
1312
+ hint_lines = [next_hint_text]
1313
+
1314
+ if not hint_lines and fallback_pack:
1315
+ fallback_hints = _safe_meta_list(fallback_pack.get("hint_ladder", []))
1316
+ if fallback_hints:
1317
+ idx = min(max(hint_stage - 1, 0), len(fallback_hints) - 1)
1318
+ hint_lines = [fallback_hints[idx]]
1319
+
1320
+ if not hint_lines and fallback_reply_core:
1321
+ split_lines = [line.strip("- ").strip() for line in fallback_reply_core.splitlines() if line.strip()]
1322
+ if split_lines:
1323
+ idx = min(max(hint_stage - 1, 0), len(split_lines) - 1)
1324
+ hint_lines = [split_lines[idx]]
1325
+
1326
+ if not hint_lines:
1327
+ hint_lines = [_minimal_generic_reply(inferred_category)]
1328
+
1329
+ reply_core = "\n".join(f"- {line}" for line in hint_lines if str(line).strip())
1330
+ result.meta["response_source"] = "hint_ladder"
1331
+ result.meta["question_support_used"] = bool(fallback_pack)
1332
+ result.meta["question_support_source"] = fallback_pack.get("support_source") if fallback_pack else None
1333
+ result.meta["question_support_topic"] = fallback_pack.get("topic") if fallback_pack else None
1334
+
1335
+ reply = format_reply(
1336
+ reply_core,
1337
+ tone=tone,
1338
+ verbosity=verbosity,
1339
+ transparency=transparency,
1340
+ help_mode="hint",
1341
+ hint_stage=hint_stage,
1342
+ topic=result.topic,
1343
+ )
1344
+
1345
+ elif question_specific_reply_core and (
1346
+ input_type not in {"hint", "next_hint"}
1347
+ and (
1348
+ _is_help_first_mode(resolved_help_mode)
1349
+ or input_type in {"other", "confusion"}
1350
+ or any(
1351
+ phrase in _clean_text(user_text).lower()
1352
+ for phrase in [
1353
+ "how do i solve",
1354
+ "what do i do first",
1355
+ "what should i do first",
1356
+ "how should i start",
1357
+ ]
1358
+ )
1359
+ )
1360
  ):
1361
  reply_core = question_specific_reply_core
1362
  result.meta["response_source"] = "question_specific"
 
1417
  elif (
1418
  resolved_help_mode == "answer"
1419
  and solver_has_steps
1420
+ and solver_topic_ok
1421
  and direct_solve_request
1422
  ):
1423
  reply_core = _answer_path_from_steps(solver_steps, verbosity=verbosity)
 
1436
  resolved_help_mode == "walkthrough"
1437
  and solver_has_steps
1438
  and not prefer_question_support
1439
+ and solver_topic_ok
1440
  ):
1441
  reply_core = _answer_path_from_steps(solver_steps, verbosity=verbosity)
1442
  result.meta["response_source"] = "solver_steps"
 
1480
  hint_stage=hint_stage,
1481
  topic=result.topic,
1482
  )
1483
+
1484
  if resolved_help_mode in {"hint", "walkthrough", "explain", "instruction", "step_by_step"}:
1485
  result.solved = False
1486
  result.answer_letter = None
1487
  result.answer_value = None
1488
  result.internal_answer = None
1489
  result.meta["internal_answer"] = None
1490
+
1491
  can_reveal_answer = bool(result.solved and direct_solve_request and not _is_help_first_mode(resolved_help_mode))
1492
  result.meta["can_reveal_answer"] = can_reveal_answer
1493
  if not can_reveal_answer:
 
1508
  topic=result.topic,
1509
  category=inferred_category,
1510
  )
1511
+
1512
  result.reply = reply
1513
  result.help_mode = resolved_help_mode
1514
  result.meta["help_mode"] = resolved_help_mode