Update conversation_logic.py
Browse files- 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
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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 = "
|
| 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 = "
|
| 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 |
-
|
| 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 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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
|