j-js commited on
Commit
87f1ecd
·
verified ·
1 Parent(s): 18361f1

Update conversation_logic.py

Browse files
Files changed (1) hide show
  1. conversation_logic.py +246 -90
conversation_logic.py CHANGED
@@ -48,6 +48,29 @@ CONTROL_PREFIX_PATTERNS = [
48
  r"^\s*next step\s*$",
49
  ]
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  def _clean_text(text: Optional[str]) -> str:
53
  return (text or "").strip()
@@ -81,25 +104,7 @@ def _extract_question_candidates_from_history_item(item: Dict[str, Any]) -> List
81
 
82
  def _is_followup_hint_only(text: str) -> bool:
83
  low = (text or "").strip().lower()
84
- return low in {
85
- "hint",
86
- "another hint",
87
- "next hint",
88
- "next step",
89
- "first step",
90
- "continue",
91
- "go on",
92
- "walk me through it",
93
- "step by step",
94
- "walkthrough",
95
- "i'm confused",
96
- "im confused",
97
- "confused",
98
- "explain more",
99
- "more explanation",
100
- "can you explain that",
101
- "help me understand",
102
- }
103
 
104
 
105
  def _strip_control_prefix(text: str) -> str:
@@ -137,11 +142,32 @@ def _looks_like_question_text(text: str) -> bool:
137
  "%" in t,
138
  bool(re.search(r"\b\d+\s*:\s*\d+\b", t)),
139
  bool(re.search(r"[a-zA-Z]\s*[\+\-\*/=]", t)),
140
- any(k in low for k in [
141
- "what is", "find", "if ", "how many", "probability", "ratio",
142
- "percent", "equation", "integer", "triangle", "circle",
143
- "mean", "median", "average", "remainder", "prime", "factor",
144
- ]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  ]
146
  )
147
 
@@ -154,7 +180,20 @@ def _classify_input_type(raw_user_text: str) -> str:
154
  return "hint"
155
  if text in {"next hint", "another hint", "more hint", "more hints", "next step", "continue", "go on"} or text.startswith("next hint:"):
156
  return "next_hint"
157
- if any(x in text for x in ["walkthrough", "step by step", "i'm confused", "im confused", "confused", "explain more", "help me understand"]):
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  return "confusion"
159
  if text.startswith("solve:") or text.startswith("solve "):
160
  return "solve"
@@ -191,7 +230,12 @@ def _history_hint_stage(chat_history: Optional[List[Dict[str, Any]]]) -> int:
191
  return min(best, 3)
192
 
193
 
194
- def _recover_question_text(raw_user_text: str, question_text: Optional[str], chat_history: Optional[List[Dict[str, Any]]], input_type: str) -> str:
 
 
 
 
 
195
  explicit = _sanitize_question_text(question_text or "")
196
  if explicit:
197
  return explicit
@@ -208,7 +252,13 @@ def _recover_question_text(raw_user_text: str, question_text: Optional[str], cha
208
  return ""
209
 
210
 
211
- def _choose_effective_question_text(raw_user_text: str, question_text: Optional[str], input_type: str, state: Dict[str, Any], chat_history: Optional[List[Dict[str, Any]]]) -> Tuple[str, bool]:
 
 
 
 
 
 
212
  explicit_question = _sanitize_question_text(question_text or "")
213
  stored_question = _sanitize_question_text(state.get("question_text", ""))
214
  if _is_followup_input(input_type):
@@ -273,12 +323,10 @@ def _normalize_classified_topic(topic: Optional[str], category: Optional[str], q
273
  has_ratio_form = bool(re.search(r"\b\d+\s*:\s*\d+\b", q))
274
  has_algebra_form = (
275
  "=" in q
276
- or bool(re.search(r"\b[xyz]\b", q))
277
  or bool(re.search(r"\d+[a-z]\b", q))
278
  or bool(re.search(r"\b[a-z]\s*[\+\-\*/=]", q))
279
  )
280
- if has_algebra_form:
281
- return "algebra"
282
  if t not in {"general_quant", "general", "unknown", ""}:
283
  return t
284
  if "%" in q or "percent" in q:
@@ -289,10 +337,12 @@ def _normalize_classified_topic(topic: Optional[str], category: Optional[str], q
289
  return "probability"
290
  if any(k in q for k in ["divisible", "remainder", "prime", "factor"]):
291
  return "number_theory"
292
- if any(k in q for k in ["circle", "triangle", "perimeter", "area", "circumference"]):
293
  return "geometry"
294
- if any(k in q for k in ["mean", "median", "average"]):
295
  return "statistics" if c == "Quantitative" else "data"
 
 
296
  if c == "DataInsight":
297
  return "data"
298
  if c == "Verbal":
@@ -303,7 +353,7 @@ def _normalize_classified_topic(topic: Optional[str], category: Optional[str], q
303
 
304
 
305
  def _strip_bullet_prefix(text: str) -> str:
306
- return re.sub(r"^\s*-\s*", "", (text or "").strip())
307
 
308
 
309
  def _safe_steps(steps: List[str]) -> List[str]:
@@ -314,10 +364,6 @@ def _safe_steps(steps: List[str]) -> List[str]:
314
  r"\bthis gives\b",
315
  r"\btherefore\b",
316
  r"\bthus\b",
317
- r"\bso x\s*=",
318
- r"\bso y\s*=",
319
- r"\bx\s*=",
320
- r"\by\s*=",
321
  r"\bresult is\b",
322
  r"\bfinal answer\b",
323
  ]
@@ -382,7 +428,9 @@ def _extract_explainer_scaffold(explainer_result: Any) -> Dict[str, Any]:
382
  }
383
 
384
 
385
- def _get_result_steps(result: SolverResult) -> List[str]:
 
 
386
  display_steps = getattr(result, "display_steps", None)
387
  if isinstance(display_steps, list) and display_steps:
388
  return _safe_steps(display_steps)
@@ -399,7 +447,9 @@ def _get_result_steps(result: SolverResult) -> List[str]:
399
  return []
400
 
401
 
402
- def _apply_safe_step_sanitization(result: SolverResult) -> None:
 
 
403
  safe_steps = _get_result_steps(result)
404
  result.steps = list(safe_steps)
405
  setattr(result, "display_steps", list(safe_steps))
@@ -456,8 +506,42 @@ def _is_direct_solve_request(text: str, intent: str) -> bool:
456
  return False
457
 
458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  class ConversationEngine:
460
- def __init__(self, retriever: Optional[RetrievalEngine] = None, generator: Optional[GeneratorEngine] = None, **kwargs) -> None:
 
 
 
 
 
461
  self.retriever = retriever
462
  self.generator = generator
463
 
@@ -512,12 +596,16 @@ class ConversationEngine:
512
  resolved_help_mode = "hint"
513
  elif input_type == "confusion":
514
  resolved_help_mode = "walkthrough"
 
 
515
 
516
  prior_hint_stage = int(state.get("hint_stage", 0) or 0)
517
  history_hint_stage = _history_hint_stage(chat_history)
518
  hint_stage = _compute_hint_stage(input_type, prior_hint_stage, history_hint_stage)
519
 
520
- is_quant = bool(solver_input) and (inferred_category == "Quantitative" or is_quant_question(solver_input))
 
 
521
 
522
  result = SolverResult(
523
  domain="quant" if is_quant else "general",
@@ -532,20 +620,12 @@ class ConversationEngine:
532
  )
533
 
534
  solver_result: Optional[SolverResult] = None
535
- if is_quant and resolved_help_mode in {"answer", "walkthrough", "instruction", "hint"}:
536
  try:
537
  solver_result = route_solver(solver_input)
538
  except Exception:
539
  solver_result = None
540
- if solver_result is not None:
541
- result = solver_result
542
- result.domain = "quant"
543
- result.meta = result.meta or {}
544
- if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
545
- result.topic = question_topic
546
-
547
- _apply_safe_step_sanitization(result)
548
- solver_steps = _get_result_steps(result)
549
 
550
  explainer_result = None
551
  explainer_understood = False
@@ -559,14 +639,6 @@ class ConversationEngine:
559
  explainer_understood = True
560
  explainer_scaffold = _extract_explainer_scaffold(explainer_result)
561
 
562
- result.help_mode = resolved_help_mode
563
- result.meta = result.meta or {}
564
- result.meta["hint_stage"] = hint_stage
565
- result.meta["max_stage"] = 3
566
- result.meta["recovered_question_text"] = solver_input
567
- result.meta["question_id"] = question_id
568
-
569
- use_solver_steps_for_reply = bool(solver_steps)
570
  fallback_reply_core = ""
571
  fallback_pack: Dict[str, Any] = {}
572
  if solver_input:
@@ -574,24 +646,61 @@ class ConversationEngine:
574
  question_id=question_id,
575
  question_text=solver_input,
576
  options_text=options_text,
577
- topic=result.topic or question_topic,
578
  category=inferred_category,
579
  help_mode=resolved_help_mode,
580
  hint_stage=hint_stage,
581
  verbosity=verbosity,
582
  )
583
 
584
- if resolved_help_mode == "explain" and explainer_understood:
585
- reply = format_explainer_response(
586
- result=explainer_result,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  tone=tone,
588
  verbosity=verbosity,
589
  transparency=transparency,
590
  help_mode=resolved_help_mode,
591
  hint_stage=hint_stage,
 
592
  )
593
- result.meta["explainer_used"] = True
594
- elif resolved_help_mode == "hint" and explainer_understood and not use_solver_steps_for_reply and not fallback_pack.get("support_source") == "question_bank":
595
  reply = format_explainer_response(
596
  result=explainer_result,
597
  tone=tone,
@@ -600,26 +709,71 @@ class ConversationEngine:
600
  help_mode=resolved_help_mode,
601
  hint_stage=hint_stage,
602
  )
 
603
  result.meta["explainer_used"] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  else:
605
- if use_solver_steps_for_reply:
606
- reply_core = _answer_path_from_steps(solver_steps, verbosity=verbosity)
607
- result.meta["question_support_used"] = False
608
- elif fallback_reply_core:
609
- reply_core = fallback_reply_core
610
- result.meta["question_support_used"] = True
611
- result.meta["question_support_source"] = fallback_pack.get("support_source")
612
- result.meta["question_support_topic"] = fallback_pack.get("topic")
613
- elif inferred_category == "Verbal":
614
- reply_core = "I can help analyse the wording or logic, but I need the full question text to guide you properly."
615
- result.meta["question_support_used"] = False
616
- elif inferred_category == "DataInsight":
617
- reply_core = "I can help reason through the data, but I need the full question or chart details to guide you properly."
618
- result.meta["question_support_used"] = False
619
- else:
620
- reply_core = "- Start by identifying the main relationship in the problem."
621
- result.meta["question_support_used"] = False
622
-
623
  reply = format_reply(
624
  reply_core,
625
  tone=tone,
@@ -630,17 +784,18 @@ class ConversationEngine:
630
  topic=result.topic,
631
  )
632
 
633
- if resolved_help_mode in {"hint", "walkthrough", "explain", "instruction"}:
 
634
  result.solved = False
635
  result.answer_letter = None
636
  result.answer_value = None
637
  result.internal_answer = None
638
  result.meta["internal_answer"] = None
639
 
640
- result.meta["can_reveal_answer"] = bool(
641
- result.solved and _is_direct_solve_request(user_text or solver_input, resolved_intent) and hint_stage >= 3
642
- )
643
- if not result.meta.get("can_reveal_answer", False):
644
  result.answer_letter = None
645
  result.answer_value = None
646
  result.internal_answer = None
@@ -666,8 +821,9 @@ class ConversationEngine:
666
  result.meta["question_text"] = solver_input or ""
667
  result.meta["options_count"] = len(options_text or [])
668
  result.meta["category"] = inferred_category if inferred_category else "General"
669
- result.meta["classified_topic"] = question_topic if question_topic else "general"
670
  result.meta["user_last_input_type"] = input_type
671
  result.meta["built_on_previous_turn"] = built_on_previous_turn
672
  result.meta["session_state"] = state
673
- return result
 
 
 
48
  r"^\s*next step\s*$",
49
  ]
50
 
51
+ FOLLOWUP_ONLY_INPUTS = {
52
+ "hint",
53
+ "a hint",
54
+ "give me a hint",
55
+ "can i have a hint",
56
+ "next hint",
57
+ "another hint",
58
+ "next step",
59
+ "continue",
60
+ "go on",
61
+ "walk me through it",
62
+ "step by step",
63
+ "walkthrough",
64
+ "i'm confused",
65
+ "im confused",
66
+ "confused",
67
+ "explain more",
68
+ "more explanation",
69
+ "can you explain that",
70
+ "help me understand",
71
+ "help",
72
+ }
73
+
74
 
75
  def _clean_text(text: Optional[str]) -> str:
76
  return (text or "").strip()
 
104
 
105
  def _is_followup_hint_only(text: str) -> bool:
106
  low = (text or "").strip().lower()
107
+ return low in FOLLOWUP_ONLY_INPUTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
 
110
  def _strip_control_prefix(text: str) -> str:
 
142
  "%" in t,
143
  bool(re.search(r"\b\d+\s*:\s*\d+\b", t)),
144
  bool(re.search(r"[a-zA-Z]\s*[\+\-\*/=]", t)),
145
+ any(
146
+ k in low
147
+ for k in [
148
+ "what is",
149
+ "find",
150
+ "if ",
151
+ "how many",
152
+ "probability",
153
+ "ratio",
154
+ "percent",
155
+ "equation",
156
+ "integer",
157
+ "triangle",
158
+ "circle",
159
+ "mean",
160
+ "median",
161
+ "average",
162
+ "remainder",
163
+ "prime",
164
+ "factor",
165
+ "divisible",
166
+ "area",
167
+ "perimeter",
168
+ "circumference",
169
+ ]
170
+ ),
171
  ]
172
  )
173
 
 
180
  return "hint"
181
  if text in {"next hint", "another hint", "more hint", "more hints", "next step", "continue", "go on"} or text.startswith("next hint:"):
182
  return "next_hint"
183
+ if any(
184
+ x in text
185
+ for x in [
186
+ "walkthrough",
187
+ "step by step",
188
+ "i'm confused",
189
+ "im confused",
190
+ "confused",
191
+ "explain more",
192
+ "help me understand",
193
+ "method",
194
+ "explain",
195
+ ]
196
+ ):
197
  return "confusion"
198
  if text.startswith("solve:") or text.startswith("solve "):
199
  return "solve"
 
230
  return min(best, 3)
231
 
232
 
233
+ def _recover_question_text(
234
+ raw_user_text: str,
235
+ question_text: Optional[str],
236
+ chat_history: Optional[List[Dict[str, Any]]],
237
+ input_type: str,
238
+ ) -> str:
239
  explicit = _sanitize_question_text(question_text or "")
240
  if explicit:
241
  return explicit
 
252
  return ""
253
 
254
 
255
+ def _choose_effective_question_text(
256
+ raw_user_text: str,
257
+ question_text: Optional[str],
258
+ input_type: str,
259
+ state: Dict[str, Any],
260
+ chat_history: Optional[List[Dict[str, Any]]],
261
+ ) -> Tuple[str, bool]:
262
  explicit_question = _sanitize_question_text(question_text or "")
263
  stored_question = _sanitize_question_text(state.get("question_text", ""))
264
  if _is_followup_input(input_type):
 
323
  has_ratio_form = bool(re.search(r"\b\d+\s*:\s*\d+\b", q))
324
  has_algebra_form = (
325
  "=" in q
326
+ or bool(re.search(r"\b[xyzabn]\b", q))
327
  or bool(re.search(r"\d+[a-z]\b", q))
328
  or bool(re.search(r"\b[a-z]\s*[\+\-\*/=]", q))
329
  )
 
 
330
  if t not in {"general_quant", "general", "unknown", ""}:
331
  return t
332
  if "%" in q or "percent" in q:
 
337
  return "probability"
338
  if any(k in q for k in ["divisible", "remainder", "prime", "factor"]):
339
  return "number_theory"
340
+ if any(k in q for k in ["circle", "triangle", "perimeter", "area", "circumference", "rectangle"]):
341
  return "geometry"
342
+ if any(k in q for k in ["mean", "median", "average", "variability", "standard deviation"]):
343
  return "statistics" if c == "Quantitative" else "data"
344
+ if has_algebra_form:
345
+ return "algebra"
346
  if c == "DataInsight":
347
  return "data"
348
  if c == "Verbal":
 
353
 
354
 
355
  def _strip_bullet_prefix(text: str) -> str:
356
+ return re.sub(r"^\s*[-•]\s*", "", (text or "").strip())
357
 
358
 
359
  def _safe_steps(steps: List[str]) -> List[str]:
 
364
  r"\bthis gives\b",
365
  r"\btherefore\b",
366
  r"\bthus\b",
 
 
 
 
367
  r"\bresult is\b",
368
  r"\bfinal answer\b",
369
  ]
 
428
  }
429
 
430
 
431
+ def _get_result_steps(result: Optional[SolverResult]) -> List[str]:
432
+ if result is None:
433
+ return []
434
  display_steps = getattr(result, "display_steps", None)
435
  if isinstance(display_steps, list) and display_steps:
436
  return _safe_steps(display_steps)
 
447
  return []
448
 
449
 
450
+ def _apply_safe_step_sanitization(result: Optional[SolverResult]) -> None:
451
+ if result is None:
452
+ return
453
  safe_steps = _get_result_steps(result)
454
  result.steps = list(safe_steps)
455
  setattr(result, "display_steps", list(safe_steps))
 
506
  return False
507
 
508
 
509
+ def _is_help_first_mode(help_mode: str) -> bool:
510
+ return help_mode in {"hint", "walkthrough", "explain", "instruction", "step_by_step"}
511
+
512
+
513
+ def _should_try_solver(is_quant: bool, help_mode: str, solver_input: str) -> bool:
514
+ if not is_quant or not solver_input:
515
+ return False
516
+ return help_mode in {"answer", "walkthrough", "instruction", "hint", "step_by_step"}
517
+
518
+
519
+ def _should_prefer_question_support(help_mode: str, fallback_pack: Dict[str, Any]) -> bool:
520
+ if not fallback_pack:
521
+ return False
522
+ support_source = str(fallback_pack.get("support_source", "")).strip().lower()
523
+ has_specific_content = support_source in {"question_bank", "question_id", "question_text"}
524
+ if help_mode in {"hint", "walkthrough", "instruction", "step_by_step", "explain"}:
525
+ return has_specific_content or bool(fallback_pack)
526
+ return False
527
+
528
+
529
+ def _minimal_generic_reply(category: Optional[str]) -> str:
530
+ c = normalize_category(category)
531
+ if c == "Verbal":
532
+ return "I can help analyse the wording or logic, but I need the full question text to guide you properly."
533
+ if c == "DataInsight":
534
+ return "I can help reason through the data, but I need the full question or chart details to guide you properly."
535
+ return "Start by identifying the main relationship in the problem."
536
+
537
+
538
  class ConversationEngine:
539
+ def __init__(
540
+ self,
541
+ retriever: Optional[RetrievalEngine] = None,
542
+ generator: Optional[GeneratorEngine] = None,
543
+ **kwargs,
544
+ ) -> None:
545
  self.retriever = retriever
546
  self.generator = generator
547
 
 
596
  resolved_help_mode = "hint"
597
  elif input_type == "confusion":
598
  resolved_help_mode = "walkthrough"
599
+ elif resolved_help_mode == "step_by_step":
600
+ resolved_help_mode = "walkthrough"
601
 
602
  prior_hint_stage = int(state.get("hint_stage", 0) or 0)
603
  history_hint_stage = _history_hint_stage(chat_history)
604
  hint_stage = _compute_hint_stage(input_type, prior_hint_stage, history_hint_stage)
605
 
606
+ is_quant = bool(solver_input) and (
607
+ inferred_category == "Quantitative" or is_quant_question(solver_input)
608
+ )
609
 
610
  result = SolverResult(
611
  domain="quant" if is_quant else "general",
 
620
  )
621
 
622
  solver_result: Optional[SolverResult] = None
623
+ if _should_try_solver(is_quant, resolved_help_mode, solver_input):
624
  try:
625
  solver_result = route_solver(solver_input)
626
  except Exception:
627
  solver_result = None
628
+ _apply_safe_step_sanitization(solver_result)
 
 
 
 
 
 
 
 
629
 
630
  explainer_result = None
631
  explainer_understood = False
 
639
  explainer_understood = True
640
  explainer_scaffold = _extract_explainer_scaffold(explainer_result)
641
 
 
 
 
 
 
 
 
 
642
  fallback_reply_core = ""
643
  fallback_pack: Dict[str, Any] = {}
644
  if solver_input:
 
646
  question_id=question_id,
647
  question_text=solver_input,
648
  options_text=options_text,
649
+ topic=question_topic,
650
  category=inferred_category,
651
  help_mode=resolved_help_mode,
652
  hint_stage=hint_stage,
653
  verbosity=verbosity,
654
  )
655
 
656
+ # Merge solver result into base result, but do not let it automatically decide the reply.
657
+ if solver_result is not None:
658
+ result = solver_result
659
+ result.domain = "quant"
660
+ result.meta = result.meta or {}
661
+ if not result.topic or result.topic in {"general_quant", "general", "unknown"}:
662
+ result.topic = question_topic
663
+ else:
664
+ result.meta = result.meta or {}
665
+ result.topic = question_topic if is_quant else result.topic
666
+
667
+ _apply_safe_step_sanitization(result)
668
+ solver_steps = _get_result_steps(result)
669
+ solver_has_steps = bool(solver_steps)
670
+ prefer_question_support = _should_prefer_question_support(resolved_help_mode, fallback_pack)
671
+ direct_solve_request = _is_direct_solve_request(user_text or solver_input, resolved_intent)
672
+
673
+ result.help_mode = resolved_help_mode
674
+ result.meta = result.meta or {}
675
+ result.meta["hint_stage"] = hint_stage
676
+ result.meta["max_stage"] = 3
677
+ result.meta["recovered_question_text"] = solver_input
678
+ result.meta["question_id"] = question_id
679
+ result.meta["classified_topic"] = question_topic if question_topic else "general"
680
+ result.meta["explainer_understood"] = explainer_understood
681
+ result.meta["explainer_scaffold"] = explainer_scaffold
682
+
683
+ # Source selection priority:
684
+ # 1) question-specific fallback for help modes
685
+ # 2) explainer for explain when specific support is unavailable
686
+ # 3) solver steps for direct answer / walkthrough fallback
687
+ # 4) generic fallback
688
+ if resolved_help_mode == "explain" and prefer_question_support and fallback_reply_core:
689
+ reply_core = fallback_reply_core
690
+ result.meta["response_source"] = "question_support"
691
+ result.meta["question_support_used"] = True
692
+ result.meta["question_support_source"] = fallback_pack.get("support_source")
693
+ result.meta["question_support_topic"] = fallback_pack.get("topic")
694
+ reply = format_reply(
695
+ reply_core,
696
  tone=tone,
697
  verbosity=verbosity,
698
  transparency=transparency,
699
  help_mode=resolved_help_mode,
700
  hint_stage=hint_stage,
701
+ topic=result.topic,
702
  )
703
+ elif resolved_help_mode == "explain" and explainer_understood:
 
704
  reply = format_explainer_response(
705
  result=explainer_result,
706
  tone=tone,
 
709
  help_mode=resolved_help_mode,
710
  hint_stage=hint_stage,
711
  )
712
+ result.meta["response_source"] = "explainer"
713
  result.meta["explainer_used"] = True
714
+ result.meta["question_support_used"] = False
715
+ elif _is_help_first_mode(resolved_help_mode) and prefer_question_support and fallback_reply_core:
716
+ reply_core = fallback_reply_core
717
+ result.meta["response_source"] = "question_support"
718
+ result.meta["question_support_used"] = True
719
+ result.meta["question_support_source"] = fallback_pack.get("support_source")
720
+ result.meta["question_support_topic"] = fallback_pack.get("topic")
721
+ reply = format_reply(
722
+ reply_core,
723
+ tone=tone,
724
+ verbosity=verbosity,
725
+ transparency=transparency,
726
+ help_mode=resolved_help_mode,
727
+ hint_stage=hint_stage,
728
+ topic=result.topic,
729
+ )
730
+ elif resolved_help_mode == "answer" and solver_has_steps:
731
+ reply_core = _answer_path_from_steps(solver_steps, verbosity=verbosity)
732
+ result.meta["response_source"] = "solver_steps"
733
+ result.meta["question_support_used"] = False
734
+ reply = format_reply(
735
+ reply_core,
736
+ tone=tone,
737
+ verbosity=verbosity,
738
+ transparency=transparency,
739
+ help_mode=resolved_help_mode,
740
+ hint_stage=hint_stage,
741
+ topic=result.topic,
742
+ )
743
+ elif resolved_help_mode == "walkthrough" and solver_has_steps and not prefer_question_support:
744
+ reply_core = _answer_path_from_steps(solver_steps, verbosity=verbosity)
745
+ result.meta["response_source"] = "solver_steps"
746
+ result.meta["question_support_used"] = False
747
+ reply = format_reply(
748
+ reply_core,
749
+ tone=tone,
750
+ verbosity=verbosity,
751
+ transparency=transparency,
752
+ help_mode=resolved_help_mode,
753
+ hint_stage=hint_stage,
754
+ topic=result.topic,
755
+ )
756
+ elif fallback_reply_core:
757
+ reply_core = fallback_reply_core
758
+ result.meta["response_source"] = "fallback"
759
+ result.meta["question_support_used"] = bool(fallback_pack)
760
+ result.meta["question_support_source"] = fallback_pack.get("support_source")
761
+ result.meta["question_support_topic"] = fallback_pack.get("topic")
762
+ reply = format_reply(
763
+ reply_core,
764
+ tone=tone,
765
+ verbosity=verbosity,
766
+ transparency=transparency,
767
+ help_mode=resolved_help_mode,
768
+ hint_stage=hint_stage,
769
+ topic=result.topic,
770
+ )
771
  else:
772
+ reply_core = _minimal_generic_reply(inferred_category)
773
+ if not reply_core.startswith("- "):
774
+ reply_core = f"- {reply_core}"
775
+ result.meta["response_source"] = "generic"
776
+ result.meta["question_support_used"] = False
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  reply = format_reply(
778
  reply_core,
779
  tone=tone,
 
784
  topic=result.topic,
785
  )
786
 
787
+ # Never reveal final answers during tutoring/help modes.
788
+ if resolved_help_mode in {"hint", "walkthrough", "explain", "instruction", "step_by_step"}:
789
  result.solved = False
790
  result.answer_letter = None
791
  result.answer_value = None
792
  result.internal_answer = None
793
  result.meta["internal_answer"] = None
794
 
795
+ # Only allow answer metadata to survive for true direct solve requests.
796
+ can_reveal_answer = bool(result.solved and direct_solve_request and not _is_help_first_mode(resolved_help_mode))
797
+ result.meta["can_reveal_answer"] = can_reveal_answer
798
+ if not can_reveal_answer:
799
  result.answer_letter = None
800
  result.answer_value = None
801
  result.internal_answer = None
 
821
  result.meta["question_text"] = solver_input or ""
822
  result.meta["options_count"] = len(options_text or [])
823
  result.meta["category"] = inferred_category if inferred_category else "General"
 
824
  result.meta["user_last_input_type"] = input_type
825
  result.meta["built_on_previous_turn"] = built_on_previous_turn
826
  result.meta["session_state"] = state
827
+ result.meta["used_retrieval"] = False
828
+ result.meta["used_generator"] = False
829
+ return result