Update conversation_logic.py
Browse files- conversation_logic.py +96 -53
conversation_logic.py
CHANGED
|
@@ -24,19 +24,28 @@ DIRECT_SOLVE_PATTERNS = [
|
|
| 24 |
r"\bcalculate\b",
|
| 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 |
|
|
@@ -80,6 +89,7 @@ def _is_followup_hint_only(text: str) -> bool:
|
|
| 80 |
"go on",
|
| 81 |
"walk me through it",
|
| 82 |
"step by step",
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
|
|
@@ -104,23 +114,22 @@ def _sanitize_question_text(text: str) -> str:
|
|
| 104 |
|
| 105 |
lines = [line.strip() for line in raw.splitlines() if line.strip()]
|
| 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",
|
|
|
|
| 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)
|
|
@@ -195,15 +204,8 @@ def _classify_input_type(raw_user_text: str) -> str:
|
|
| 195 |
return "question"
|
| 196 |
|
| 197 |
if any(k in stripped.lower() for k in [
|
| 198 |
-
"what is",
|
| 199 |
-
"
|
| 200 |
-
"if ",
|
| 201 |
-
"how many",
|
| 202 |
-
"probability",
|
| 203 |
-
"ratio",
|
| 204 |
-
"percent",
|
| 205 |
-
"equation",
|
| 206 |
-
"integer",
|
| 207 |
]):
|
| 208 |
return "question"
|
| 209 |
|
|
@@ -252,7 +254,7 @@ def _recover_question_text_from_history(
|
|
| 252 |
return _sanitize_question_text(_extract_question_text(raw_user_text))
|
| 253 |
|
| 254 |
if not chat_history:
|
| 255 |
-
return
|
| 256 |
|
| 257 |
for item in reversed(chat_history):
|
| 258 |
role = str(item.get("role", "")).lower()
|
|
@@ -265,9 +267,11 @@ def _recover_question_text_from_history(
|
|
| 265 |
if _is_followup_hint_only(low):
|
| 266 |
continue
|
| 267 |
|
| 268 |
-
|
|
|
|
|
|
|
| 269 |
|
| 270 |
-
return
|
| 271 |
|
| 272 |
|
| 273 |
def _choose_effective_question_text(
|
|
@@ -289,8 +293,10 @@ def _choose_effective_question_text(
|
|
| 289 |
question_text=question_text,
|
| 290 |
chat_history=chat_history,
|
| 291 |
)
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
| 294 |
|
| 295 |
if explicit_question:
|
| 296 |
return explicit_question, False
|
|
@@ -334,7 +340,8 @@ def _update_session_state(
|
|
| 334 |
topic: Optional[str],
|
| 335 |
category: Optional[str],
|
| 336 |
) -> Dict[str, Any]:
|
| 337 |
-
|
|
|
|
| 338 |
state["hint_stage"] = int(hint_stage or 0)
|
| 339 |
state["user_last_input_type"] = user_last_input_type
|
| 340 |
state["built_on_previous_turn"] = bool(built_on_previous_turn)
|
|
@@ -501,18 +508,16 @@ def _build_hint_from_steps(
|
|
| 501 |
fallback: Optional[List[str]] = None,
|
| 502 |
) -> str:
|
| 503 |
safe_steps = _safe_steps(steps)
|
|
|
|
|
|
|
| 504 |
if safe_steps:
|
| 505 |
-
|
| 506 |
-
|
| 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 |
-
|
| 515 |
-
return
|
| 516 |
|
| 517 |
return "- Start by identifying the main relationship in the problem."
|
| 518 |
|
|
@@ -531,6 +536,19 @@ def _walkthrough_from_steps(steps: List[str], verbosity: float) -> str:
|
|
| 531 |
return "\n".join(f"- {s}" for s in shown_steps)
|
| 532 |
|
| 533 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
def _compose_reply(
|
| 535 |
result: SolverResult,
|
| 536 |
intent: str,
|
|
@@ -602,14 +620,9 @@ def _compose_reply(
|
|
| 602 |
])
|
| 603 |
|
| 604 |
if intent in {"explain", "method", "concept", "answer"}:
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
steps[:2] if verbosity < 0.6 else
|
| 609 |
-
steps[:3] if verbosity < 0.85 else
|
| 610 |
-
steps
|
| 611 |
-
)
|
| 612 |
-
return "\n".join(f"- {s}" for s in shown_steps)
|
| 613 |
|
| 614 |
if topic == "algebra":
|
| 615 |
return "\n".join([
|
|
@@ -655,6 +668,29 @@ def _is_direct_solve_request(text: str, intent: str) -> bool:
|
|
| 655 |
return False
|
| 656 |
|
| 657 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
class ConversationEngine:
|
| 659 |
def __init__(
|
| 660 |
self,
|
|
@@ -732,7 +768,9 @@ class ConversationEngine:
|
|
| 732 |
fallback_history_stage=history_hint_stage,
|
| 733 |
)
|
| 734 |
|
| 735 |
-
is_quant =
|
|
|
|
|
|
|
| 736 |
|
| 737 |
result = SolverResult(
|
| 738 |
domain="quant" if is_quant else "general",
|
|
@@ -740,7 +778,7 @@ class ConversationEngine:
|
|
| 740 |
help_mode=resolved_help_mode,
|
| 741 |
answer_letter=None,
|
| 742 |
answer_value=None,
|
| 743 |
-
topic=question_topic,
|
| 744 |
used_retrieval=False,
|
| 745 |
used_generator=False,
|
| 746 |
internal_answer=None,
|
|
@@ -762,7 +800,7 @@ class ConversationEngine:
|
|
| 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)
|
| 768 |
)
|
|
@@ -774,7 +812,6 @@ class ConversationEngine:
|
|
| 774 |
if explainer_understood else []
|
| 775 |
)
|
| 776 |
|
| 777 |
-
# force the externally resolved mode after solver routing
|
| 778 |
result.help_mode = resolved_help_mode
|
| 779 |
|
| 780 |
result.meta = result.meta or {}
|
|
@@ -790,7 +827,9 @@ class ConversationEngine:
|
|
| 790 |
result.meta["explainer_teaching_points"] = explainer_teaching_points
|
| 791 |
result.meta["scaffold"] = explainer_scaffold
|
| 792 |
|
| 793 |
-
|
|
|
|
|
|
|
| 794 |
|
| 795 |
if resolved_help_mode == "explain" and explainer_understood:
|
| 796 |
reply = format_explainer_response(
|
|
@@ -823,15 +862,19 @@ class ConversationEngine:
|
|
| 823 |
hint_stage=hint_stage,
|
| 824 |
explainer_scaffold=explainer_scaffold,
|
| 825 |
)
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
|
| 836 |
if resolved_help_mode != "answer":
|
| 837 |
result.solved = False
|
|
@@ -866,8 +909,8 @@ class ConversationEngine:
|
|
| 866 |
result.meta["intent"] = resolved_intent
|
| 867 |
result.meta["question_text"] = solver_input or ""
|
| 868 |
result.meta["options_count"] = len(options_text or [])
|
| 869 |
-
result.meta["category"] = inferred_category
|
| 870 |
-
result.meta["classified_topic"] = question_topic
|
| 871 |
result.meta["user_last_input_type"] = input_type
|
| 872 |
result.meta["built_on_previous_turn"] = built_on_previous_turn
|
| 873 |
result.meta["session_state"] = state
|
|
|
|
| 24 |
r"\bcalculate\b",
|
| 25 |
]
|
| 26 |
|
|
|
|
| 27 |
CONTROL_PREFIX_PATTERNS = [
|
| 28 |
r"^\s*solve\s*:\s*",
|
| 29 |
r"^\s*solve\s+",
|
| 30 |
r"^\s*question\s*:\s*",
|
| 31 |
r"^\s*q\s*:\s*",
|
| 32 |
r"^\s*hint\s*:\s*",
|
| 33 |
+
r"^\s*hint\s*$",
|
| 34 |
r"^\s*next hint\s*:\s*",
|
| 35 |
+
r"^\s*next hint\s*$",
|
| 36 |
r"^\s*another hint\s*:\s*",
|
| 37 |
+
r"^\s*another hint\s*$",
|
| 38 |
r"^\s*walkthrough\s*:\s*",
|
| 39 |
+
r"^\s*walkthrough\s*$",
|
| 40 |
r"^\s*step by step\s*:\s*",
|
| 41 |
+
r"^\s*step by step\s*$",
|
| 42 |
r"^\s*explain\s*:\s*",
|
| 43 |
+
r"^\s*explain\s*$",
|
| 44 |
r"^\s*method\s*:\s*",
|
| 45 |
+
r"^\s*method\s*$",
|
| 46 |
+
r"^\s*continue\s*$",
|
| 47 |
+
r"^\s*go on\s*$",
|
| 48 |
+
r"^\s*next step\s*$",
|
| 49 |
]
|
| 50 |
|
| 51 |
|
|
|
|
| 89 |
"go on",
|
| 90 |
"walk me through it",
|
| 91 |
"step by step",
|
| 92 |
+
"walkthrough",
|
| 93 |
}
|
| 94 |
|
| 95 |
|
|
|
|
| 114 |
|
| 115 |
lines = [line.strip() for line in raw.splitlines() if line.strip()]
|
| 116 |
|
|
|
|
| 117 |
if len(lines) >= 9 and re.fullmatch(r"-?\d+", lines[2]) and lines[4].lower() in {"true", "false"}:
|
| 118 |
return _strip_control_prefix(lines[1])
|
| 119 |
|
|
|
|
| 120 |
if len(lines) >= 2 and lines[0].lower() in {
|
| 121 |
+
"hint", "next hint", "another hint", "continue", "go on",
|
| 122 |
+
"walkthrough", "step by step", "solve", "explain", "method"
|
| 123 |
}:
|
| 124 |
return _strip_control_prefix(lines[1])
|
| 125 |
|
|
|
|
| 126 |
for line in lines:
|
| 127 |
candidate = _strip_control_prefix(line)
|
| 128 |
+
if not candidate:
|
| 129 |
+
continue
|
| 130 |
if (
|
| 131 |
"=" in candidate
|
| 132 |
or "%" in candidate
|
|
|
|
| 133 |
or re.search(r"\b(probability|ratio|percent|integer|triangle|circle|mean|median)\b", candidate.lower())
|
| 134 |
or re.search(r"[a-zA-Z]\s*[\+\-\*/=]", candidate)
|
| 135 |
or re.search(r"[\+\-\*/=]\s*[a-zA-Z0-9]", candidate)
|
|
|
|
| 204 |
return "question"
|
| 205 |
|
| 206 |
if any(k in stripped.lower() for k in [
|
| 207 |
+
"what is", "find", "if ", "how many", "probability", "ratio",
|
| 208 |
+
"percent", "equation", "integer"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
]):
|
| 210 |
return "question"
|
| 211 |
|
|
|
|
| 254 |
return _sanitize_question_text(_extract_question_text(raw_user_text))
|
| 255 |
|
| 256 |
if not chat_history:
|
| 257 |
+
return ""
|
| 258 |
|
| 259 |
for item in reversed(chat_history):
|
| 260 |
role = str(item.get("role", "")).lower()
|
|
|
|
| 267 |
if _is_followup_hint_only(low):
|
| 268 |
continue
|
| 269 |
|
| 270 |
+
recovered = _sanitize_question_text(_extract_question_text(text))
|
| 271 |
+
if recovered:
|
| 272 |
+
return recovered
|
| 273 |
|
| 274 |
+
return ""
|
| 275 |
|
| 276 |
|
| 277 |
def _choose_effective_question_text(
|
|
|
|
| 293 |
question_text=question_text,
|
| 294 |
chat_history=chat_history,
|
| 295 |
)
|
| 296 |
+
if recovered:
|
| 297 |
+
return recovered, True
|
| 298 |
+
|
| 299 |
+
return "", True
|
| 300 |
|
| 301 |
if explicit_question:
|
| 302 |
return explicit_question, False
|
|
|
|
| 340 |
topic: Optional[str],
|
| 341 |
category: Optional[str],
|
| 342 |
) -> Dict[str, Any]:
|
| 343 |
+
if question_text:
|
| 344 |
+
state["question_text"] = question_text
|
| 345 |
state["hint_stage"] = int(hint_stage or 0)
|
| 346 |
state["user_last_input_type"] = user_last_input_type
|
| 347 |
state["built_on_previous_turn"] = bool(built_on_previous_turn)
|
|
|
|
| 508 |
fallback: Optional[List[str]] = None,
|
| 509 |
) -> str:
|
| 510 |
safe_steps = _safe_steps(steps)
|
| 511 |
+
stage = max(1, min(int(hint_stage or 1), 3))
|
| 512 |
+
|
| 513 |
if safe_steps:
|
| 514 |
+
index = min(stage - 1, len(safe_steps) - 1)
|
| 515 |
+
return f"- {safe_steps[index]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
|
| 517 |
fallback = _safe_steps(fallback or [])
|
| 518 |
if fallback:
|
| 519 |
+
index = min(stage - 1, len(fallback) - 1)
|
| 520 |
+
return f"- {fallback[index]}"
|
| 521 |
|
| 522 |
return "- Start by identifying the main relationship in the problem."
|
| 523 |
|
|
|
|
| 536 |
return "\n".join(f"- {s}" for s in shown_steps)
|
| 537 |
|
| 538 |
|
| 539 |
+
def _answer_path_from_steps(steps: List[str], verbosity: float) -> str:
|
| 540 |
+
safe_steps = _safe_steps(steps)
|
| 541 |
+
if not safe_steps:
|
| 542 |
+
return ""
|
| 543 |
+
|
| 544 |
+
shown_steps = (
|
| 545 |
+
safe_steps[:2] if verbosity < 0.35 else
|
| 546 |
+
safe_steps[:3] if verbosity < 0.8 else
|
| 547 |
+
safe_steps
|
| 548 |
+
)
|
| 549 |
+
return "\n".join(f"- {s}" for s in shown_steps)
|
| 550 |
+
|
| 551 |
+
|
| 552 |
def _compose_reply(
|
| 553 |
result: SolverResult,
|
| 554 |
intent: str,
|
|
|
|
| 620 |
])
|
| 621 |
|
| 622 |
if intent in {"explain", "method", "concept", "answer"}:
|
| 623 |
+
step_reply = _answer_path_from_steps(steps, verbosity=verbosity)
|
| 624 |
+
if step_reply:
|
| 625 |
+
return step_reply
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
|
| 627 |
if topic == "algebra":
|
| 628 |
return "\n".join([
|
|
|
|
| 668 |
return False
|
| 669 |
|
| 670 |
|
| 671 |
+
def _render_local_response(help_mode: str, body: str) -> str:
|
| 672 |
+
body = (body or "").strip()
|
| 673 |
+
if not body:
|
| 674 |
+
return "Let’s work through it."
|
| 675 |
+
|
| 676 |
+
if help_mode == "hint":
|
| 677 |
+
return f"Let’s work through it.\n\nHint:\n{body}"
|
| 678 |
+
|
| 679 |
+
if help_mode == "walkthrough":
|
| 680 |
+
return f"Let’s work through it.\n\nWalkthrough:\n{body}"
|
| 681 |
+
|
| 682 |
+
if help_mode == "answer":
|
| 683 |
+
return f"Let’s work through it.\n\nAnswer path:\n{body}"
|
| 684 |
+
|
| 685 |
+
if help_mode == "method":
|
| 686 |
+
return f"Let’s work through it.\n\nMethod:\n{body}"
|
| 687 |
+
|
| 688 |
+
if help_mode == "explain":
|
| 689 |
+
return f"Let’s work through it.\n\nExplanation:\n{body}"
|
| 690 |
+
|
| 691 |
+
return f"Let’s work through it.\n\n{body}"
|
| 692 |
+
|
| 693 |
+
|
| 694 |
class ConversationEngine:
|
| 695 |
def __init__(
|
| 696 |
self,
|
|
|
|
| 768 |
fallback_history_stage=history_hint_stage,
|
| 769 |
)
|
| 770 |
|
| 771 |
+
is_quant = bool(solver_input) and (
|
| 772 |
+
inferred_category == "Quantitative" or is_quant_question(solver_input)
|
| 773 |
+
)
|
| 774 |
|
| 775 |
result = SolverResult(
|
| 776 |
domain="quant" if is_quant else "general",
|
|
|
|
| 778 |
help_mode=resolved_help_mode,
|
| 779 |
answer_letter=None,
|
| 780 |
answer_value=None,
|
| 781 |
+
topic=question_topic if is_quant else "general",
|
| 782 |
used_retrieval=False,
|
| 783 |
used_generator=False,
|
| 784 |
internal_answer=None,
|
|
|
|
| 800 |
if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
|
| 801 |
result.topic = question_topic
|
| 802 |
|
| 803 |
+
explainer_result = route_explainer(solver_input) if solver_input else None
|
| 804 |
explainer_understood = bool(
|
| 805 |
explainer_result is not None and getattr(explainer_result, "understood", False)
|
| 806 |
)
|
|
|
|
| 812 |
if explainer_understood else []
|
| 813 |
)
|
| 814 |
|
|
|
|
| 815 |
result.help_mode = resolved_help_mode
|
| 816 |
|
| 817 |
result.meta = result.meta or {}
|
|
|
|
| 827 |
result.meta["explainer_teaching_points"] = explainer_teaching_points
|
| 828 |
result.meta["scaffold"] = explainer_scaffold
|
| 829 |
|
| 830 |
+
use_solver_steps = _solver_has_useful_steps(result)
|
| 831 |
+
use_solver_steps_for_hint = resolved_help_mode == "hint" and use_solver_steps
|
| 832 |
+
use_local_render = use_solver_steps and resolved_help_mode in {"answer", "walkthrough", "hint", "method"}
|
| 833 |
|
| 834 |
if resolved_help_mode == "explain" and explainer_understood:
|
| 835 |
reply = format_explainer_response(
|
|
|
|
| 862 |
hint_stage=hint_stage,
|
| 863 |
explainer_scaffold=explainer_scaffold,
|
| 864 |
)
|
| 865 |
+
|
| 866 |
+
if use_local_render:
|
| 867 |
+
reply = _render_local_response(resolved_help_mode, reply_core)
|
| 868 |
+
else:
|
| 869 |
+
reply = format_reply(
|
| 870 |
+
reply_core,
|
| 871 |
+
tone=tone,
|
| 872 |
+
verbosity=verbosity,
|
| 873 |
+
transparency=transparency,
|
| 874 |
+
help_mode=resolved_help_mode,
|
| 875 |
+
hint_stage=hint_stage,
|
| 876 |
+
topic=result.topic,
|
| 877 |
+
)
|
| 878 |
|
| 879 |
if resolved_help_mode != "answer":
|
| 880 |
result.solved = False
|
|
|
|
| 909 |
result.meta["intent"] = resolved_intent
|
| 910 |
result.meta["question_text"] = solver_input or ""
|
| 911 |
result.meta["options_count"] = len(options_text or [])
|
| 912 |
+
result.meta["category"] = inferred_category if inferred_category else "General"
|
| 913 |
+
result.meta["classified_topic"] = question_topic if question_topic else "general"
|
| 914 |
result.meta["user_last_input_type"] = input_type
|
| 915 |
result.meta["built_on_previous_turn"] = built_on_previous_turn
|
| 916 |
result.meta["session_state"] = state
|