Update conversation_logic.py
Browse files- conversation_logic.py +151 -27
conversation_logic.py
CHANGED
|
@@ -222,32 +222,12 @@ def _specific_topic_from_question(question_text: str, fallback_topic: str, class
|
|
| 222 |
return "median"
|
| 223 |
if "range" in q:
|
| 224 |
return "range"
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
"proportion",
|
| 231 |
-
"proportional",
|
| 232 |
-
"part to whole",
|
| 233 |
-
"out of",
|
| 234 |
-
"for every",
|
| 235 |
-
"in the same ratio",
|
| 236 |
-
"in proportion",
|
| 237 |
-
"directly proportional",
|
| 238 |
-
"inversely proportional",
|
| 239 |
-
]
|
| 240 |
-
)
|
| 241 |
-
ratio_like = ratio_like or bool(re.search(r"\b[a-z]\s*/\s*[a-z]\b", q))
|
| 242 |
-
ratio_like = ratio_like or bool(re.search(r"\b\d+\s*/\s*\d+\b", q))
|
| 243 |
-
ratio_like = ratio_like or bool(re.search(r"\b[a-z]\s*:\s*[a-z]\b", q))
|
| 244 |
-
ratio_like = ratio_like or bool(re.search(r"\b\d+\s*:\s*\d+\b", q))
|
| 245 |
-
ratio_like = ratio_like or bool(re.search(r"\b[a-z]+\s+to\s+[a-z]+\b", q))
|
| 246 |
-
ratio_like = ratio_like or bool(re.search(r"\b\d+\s+to\s+\d+\b", q))
|
| 247 |
-
|
| 248 |
-
if ratio_like:
|
| 249 |
return "ratio"
|
| 250 |
-
|
| 251 |
if topic == "data" and any(k in q for k in ["dataset", "table", "chart", "graph"]):
|
| 252 |
return "statistics"
|
| 253 |
return topic
|
|
@@ -580,6 +560,115 @@ def _solver_has_useful_steps(result: Optional[SolverResult]) -> bool:
|
|
| 580 |
return bool(result is not None and _get_result_steps(result))
|
| 581 |
|
| 582 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
def _answer_path_from_steps(steps: List[str], verbosity: float) -> str:
|
| 584 |
safe_steps = _safe_steps(steps)
|
| 585 |
if not safe_steps:
|
|
@@ -770,6 +859,14 @@ class ConversationEngine:
|
|
| 770 |
hint_stage=hint_stage,
|
| 771 |
verbosity=verbosity,
|
| 772 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
|
| 774 |
# Merge solver result into base result only if it agrees with the classified topic.
|
| 775 |
if solver_result is not None:
|
|
@@ -877,8 +974,15 @@ class ConversationEngine:
|
|
| 877 |
|
| 878 |
if fallback_pack and fallback_pack.get("topic") == "statistics":
|
| 879 |
qlow = (solver_input or "").lower()
|
|
|
|
| 880 |
if any(k in qlow for k in ["variability", "spread", "standard deviation"]):
|
| 881 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 882 |
fallback_reply_core = (
|
| 883 |
"- Notice this is asking about variability, which means spread, not the mean.\n"
|
| 884 |
"- Compare how far the smallest and largest values sit from the middle value in each dataset.\n"
|
|
@@ -968,6 +1072,26 @@ class ConversationEngine:
|
|
| 968 |
hint_stage=hint_stage,
|
| 969 |
topic=result.topic,
|
| 970 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 971 |
elif fallback_reply_core:
|
| 972 |
reply_core = fallback_reply_core
|
| 973 |
result.meta["response_source"] = "fallback"
|
|
@@ -1041,4 +1165,4 @@ class ConversationEngine:
|
|
| 1041 |
result.meta["session_state"] = state
|
| 1042 |
result.meta["used_retrieval"] = False
|
| 1043 |
result.meta["used_generator"] = False
|
| 1044 |
-
return result
|
|
|
|
| 222 |
return "median"
|
| 223 |
if "range" in q:
|
| 224 |
return "range"
|
| 225 |
+
if (
|
| 226 |
+
"ratio" in q
|
| 227 |
+
or re.search(r"\b[a-z]\s*/\s*[a-z]\b", q)
|
| 228 |
+
or re.search(r"\b\d+\s*/\s*\d+\b", q)
|
| 229 |
+
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
return "ratio"
|
|
|
|
| 231 |
if topic == "data" and any(k in q for k in ["dataset", "table", "chart", "graph"]):
|
| 232 |
return "statistics"
|
| 233 |
return topic
|
|
|
|
| 560 |
return bool(result is not None and _get_result_steps(result))
|
| 561 |
|
| 562 |
|
| 563 |
+
def _parse_numeric_option_set(option: str) -> Optional[List[float]]:
|
| 564 |
+
raw = _clean_text(option)
|
| 565 |
+
if not raw:
|
| 566 |
+
return None
|
| 567 |
+
try:
|
| 568 |
+
parts = [float(x.strip()) for x in raw.split(",") if x.strip()]
|
| 569 |
+
except Exception:
|
| 570 |
+
return None
|
| 571 |
+
return parts if len(parts) >= 2 else None
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def _question_specific_ratio_reply(question_text: str) -> str:
|
| 575 |
+
q = _clean_text(question_text)
|
| 576 |
+
low = q.lower()
|
| 577 |
+
if re.search(r"\b[a-z]\s*/\s*[a-z]\s*=\s*\d+\s*/\s*\d+", low) and re.search(r"what is\s*\(", low):
|
| 578 |
+
return (
|
| 579 |
+
"- Treat the ratio as matching parts: if a/b = 3/4, you can set a = 3k and b = 4k.\n"
|
| 580 |
+
"- Substitute those part-values into the expression the question asks for instead of solving for specific numbers.\n"
|
| 581 |
+
"- After substitution, simplify the expression by cancelling the common factor k."
|
| 582 |
+
)
|
| 583 |
+
return (
|
| 584 |
+
"- Rewrite the ratio using matching parts, such as 3k and 4k, before touching the target expression.\n"
|
| 585 |
+
"- Build the requested expression from those parts, then simplify only at the end."
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
def _question_specific_variability_reply(options_text: Optional[List[str]]) -> str:
|
| 590 |
+
parsed = [_parse_numeric_option_set(opt) for opt in (options_text or [])]
|
| 591 |
+
valid = [p for p in parsed if p]
|
| 592 |
+
if valid and all(len(p) == 3 for p in valid):
|
| 593 |
+
return (
|
| 594 |
+
"- This is asking about variability, so compare spread rather than average.\n"
|
| 595 |
+
"- For each three-number set, use the middle value as the centre and compare how far the outer numbers sit from it.\n"
|
| 596 |
+
"- The dataset whose values stretch furthest away from the centre is the one with the greatest variability."
|
| 597 |
+
)
|
| 598 |
+
return (
|
| 599 |
+
"- This is asking about variability, so focus on spread rather than the average.\n"
|
| 600 |
+
"- Compare how tightly clustered or widely spaced the values are in each answer choice.\n"
|
| 601 |
+
"- The choice with the widest spread is the strongest candidate."
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def _question_specific_percent_reply(question_text: str) -> str:
|
| 606 |
+
low = _clean_text(question_text).lower()
|
| 607 |
+
if "increased by" in low and "decreased by" in low:
|
| 608 |
+
return (
|
| 609 |
+
"- For back-to-back percent changes, turn the changes into multipliers instead of trying to combine the percentages directly.\n"
|
| 610 |
+
"- Apply the increase multiplier first, then the decrease multiplier to that new amount.\n"
|
| 611 |
+
"- Compare the final multiplier with 1 to decide whether the result is above or below the original."
|
| 612 |
+
)
|
| 613 |
+
return (
|
| 614 |
+
"- Identify the base quantity first, because percent relationships only make sense relative to a base.\n"
|
| 615 |
+
"- Translate the wording into either a multiplier or a percent equation before simplifying."
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
def _build_question_specific_reply(
|
| 620 |
+
*,
|
| 621 |
+
question_text: str,
|
| 622 |
+
options_text: Optional[List[str]],
|
| 623 |
+
classified_topic: str,
|
| 624 |
+
help_mode: str,
|
| 625 |
+
input_type: str,
|
| 626 |
+
user_text: str,
|
| 627 |
+
) -> str:
|
| 628 |
+
q = _clean_text(question_text)
|
| 629 |
+
low = q.lower()
|
| 630 |
+
topic = (classified_topic or "general").lower()
|
| 631 |
+
user_low = _clean_text(user_text).lower()
|
| 632 |
+
|
| 633 |
+
if not q:
|
| 634 |
+
return ""
|
| 635 |
+
|
| 636 |
+
if any(k in low for k in ["variability", "spread", "standard deviation"]):
|
| 637 |
+
return _question_specific_variability_reply(options_text)
|
| 638 |
+
|
| 639 |
+
if topic in {"ratio", "algebra"}:
|
| 640 |
+
if re.search(r"\b[a-z]\s*/\s*[a-z]\s*=\s*\d+\s*/\s*\d+", low):
|
| 641 |
+
return _question_specific_ratio_reply(q)
|
| 642 |
+
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"]):
|
| 643 |
+
return (
|
| 644 |
+
"- Start by rewriting one variable in terms of the other using the relationship you were given.\n"
|
| 645 |
+
"- Then substitute into the exact expression in parentheses, rather than trying to solve for actual numbers.\n"
|
| 646 |
+
"- Simplify only after the whole target expression has been rewritten in one variable or in matching parts."
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
if topic == "percent" or "%" in low or "percent" in low:
|
| 650 |
+
return _question_specific_percent_reply(q)
|
| 651 |
+
|
| 652 |
+
if topic == "statistics" and any(k in low for k in ["dataset", "table", "chart", "graph"]):
|
| 653 |
+
return (
|
| 654 |
+
"- Read the question stem first, then decide which statistic matters before comparing answer choices.\n"
|
| 655 |
+
"- Use the structure of the choices to compare them efficiently instead of computing unnecessary extra values."
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
if topic == "algebra":
|
| 659 |
+
return (
|
| 660 |
+
"- Turn the wording into one clean relationship first.\n"
|
| 661 |
+
"- Then focus on the exact expression the question asks for, rather than solving more than you need to."
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
if input_type in {"hint", "next_hint", "confusion"} or any(
|
| 665 |
+
phrase in user_low for phrase in ["how do i solve", "what do i do first", "what should i do first", "how should i start"]
|
| 666 |
+
):
|
| 667 |
+
return "- Start by identifying the main relationship in the question, then rewrite the target expression using that relationship before simplifying."
|
| 668 |
+
|
| 669 |
+
return ""
|
| 670 |
+
|
| 671 |
+
|
| 672 |
def _answer_path_from_steps(steps: List[str], verbosity: float) -> str:
|
| 673 |
safe_steps = _safe_steps(steps)
|
| 674 |
if not safe_steps:
|
|
|
|
| 859 |
hint_stage=hint_stage,
|
| 860 |
verbosity=verbosity,
|
| 861 |
)
|
| 862 |
+
question_specific_reply_core = _build_question_specific_reply(
|
| 863 |
+
question_text=solver_input,
|
| 864 |
+
options_text=options_text,
|
| 865 |
+
classified_topic=question_topic,
|
| 866 |
+
help_mode=resolved_help_mode,
|
| 867 |
+
input_type=input_type,
|
| 868 |
+
user_text=user_text,
|
| 869 |
+
)
|
| 870 |
|
| 871 |
# Merge solver result into base result only if it agrees with the classified topic.
|
| 872 |
if solver_result is not None:
|
|
|
|
| 974 |
|
| 975 |
if fallback_pack and fallback_pack.get("topic") == "statistics":
|
| 976 |
qlow = (solver_input or "").lower()
|
| 977 |
+
wants_topic = input_type == "topic_query"
|
| 978 |
if any(k in qlow for k in ["variability", "spread", "standard deviation"]):
|
| 979 |
+
if wants_topic:
|
| 980 |
+
fallback_reply_core = (
|
| 981 |
+
"- This is a statistics / data insight question about variability (spread).\n"
|
| 982 |
+
"- Focus on how spread out each dataset is rather than the average.\n"
|
| 983 |
+
"- Compare how far the outer values sit from the middle value in each set."
|
| 984 |
+
)
|
| 985 |
+
elif resolved_help_mode == "answer":
|
| 986 |
fallback_reply_core = (
|
| 987 |
"- Notice this is asking about variability, which means spread, not the mean.\n"
|
| 988 |
"- Compare how far the smallest and largest values sit from the middle value in each dataset.\n"
|
|
|
|
| 1072 |
hint_stage=hint_stage,
|
| 1073 |
topic=result.topic,
|
| 1074 |
)
|
| 1075 |
+
elif question_specific_reply_core and (
|
| 1076 |
+
_is_help_first_mode(resolved_help_mode)
|
| 1077 |
+
or input_type in {"other", "confusion", "hint", "next_hint"}
|
| 1078 |
+
or not direct_solve_request
|
| 1079 |
+
or not fallback_reply_core
|
| 1080 |
+
):
|
| 1081 |
+
reply_core = question_specific_reply_core
|
| 1082 |
+
result.meta["response_source"] = "question_specific"
|
| 1083 |
+
result.meta["question_support_used"] = bool(fallback_pack)
|
| 1084 |
+
result.meta["question_support_source"] = fallback_pack.get("support_source") if fallback_pack else None
|
| 1085 |
+
result.meta["question_support_topic"] = fallback_pack.get("topic") if fallback_pack else None
|
| 1086 |
+
reply = format_reply(
|
| 1087 |
+
reply_core,
|
| 1088 |
+
tone=tone,
|
| 1089 |
+
verbosity=verbosity,
|
| 1090 |
+
transparency=transparency,
|
| 1091 |
+
help_mode=resolved_help_mode,
|
| 1092 |
+
hint_stage=hint_stage,
|
| 1093 |
+
topic=result.topic,
|
| 1094 |
+
)
|
| 1095 |
elif fallback_reply_core:
|
| 1096 |
reply_core = fallback_reply_core
|
| 1097 |
result.meta["response_source"] = "fallback"
|
|
|
|
| 1165 |
result.meta["session_state"] = state
|
| 1166 |
result.meta["used_retrieval"] = False
|
| 1167 |
result.meta["used_generator"] = False
|
| 1168 |
+
return result
|