j-js commited on
Commit
3b0338e
·
verified ·
1 Parent(s): 2660a67

Update formatting.py

Browse files
Files changed (1) hide show
  1. formatting.py +165 -484
formatting.py CHANGED
@@ -14,15 +14,6 @@ def style_prefix(tone: float) -> str:
14
  return "You’ve got this — let’s solve it cleanly."
15
 
16
 
17
- def _clean_lines(core: str) -> list[str]:
18
- lines = []
19
- for line in (core or "").splitlines():
20
- cleaned = line.strip()
21
- if cleaned:
22
- lines.append(cleaned)
23
- return lines
24
-
25
-
26
  def _normalize_key(text: str) -> str:
27
  text = (text or "").strip().lower()
28
  text = text.replace("’", "'")
@@ -30,56 +21,13 @@ def _normalize_key(text: str) -> str:
30
  return text
31
 
32
 
33
- def _is_wrapper_line(line: str) -> bool:
34
- key = _normalize_key(line).rstrip(":")
35
- wrapper_lines = {
36
- "let's work through it.",
37
- "lets work through it.",
38
- "let's solve it efficiently.",
39
- "lets solve it efficiently.",
40
- "you've got this — let's solve it cleanly.",
41
- "youve got this — lets solve it cleanly.",
42
- "you’ve got this — let’s solve it cleanly.",
43
- "walkthrough",
44
- "hint",
45
- "steps",
46
- "method",
47
- "explanation",
48
- "key idea",
49
- "question breakdown",
50
- "here’s what the question is really asking",
51
- "here's what the question is really asking",
52
- "let’s break down what the question is asking",
53
- "lets break down what the question is asking",
54
- "you’ve got this — here’s what the question is really asking",
55
- "you've got this — here's what the question is really asking",
56
- "what to identify first",
57
- "what to identify",
58
- "set-up path",
59
- "setup path",
60
- "first move",
61
- "next hint",
62
- "next steps",
63
- "watch out for",
64
- "variables to define",
65
- "equations to form",
66
- "key teaching points",
67
- "how to build it",
68
- "what the question gives you",
69
- "constraints or conditions",
70
- "key relationship",
71
- "concepts you will probably need",
72
- "best starting move",
73
- "final answer",
74
- }
75
- return key in wrapper_lines
76
-
77
-
78
- def _strip_leading_wrapper_lines(lines: list[str]) -> list[str]:
79
- cleaned = list(lines)
80
- while cleaned and _is_wrapper_line(cleaned[0]):
81
- cleaned.pop(0)
82
- return cleaned
83
 
84
 
85
  def _dedupe_lines(lines: list[str]) -> list[str]:
@@ -93,95 +41,6 @@ def _dedupe_lines(lines: list[str]) -> list[str]:
93
  return output
94
 
95
 
96
- def _limit_lines_for_verbosity(lines: list[str], verbosity: float, help_mode: str) -> list[str]:
97
- if not lines:
98
- return lines
99
-
100
- if help_mode == "hint":
101
- return lines[:1]
102
-
103
- if help_mode == "answer":
104
- if verbosity < 0.2:
105
- return lines[:2]
106
- if verbosity < 0.45:
107
- return lines[:3]
108
- if verbosity < 0.75:
109
- return lines[:4]
110
- return lines[:5]
111
-
112
- if verbosity < 0.2:
113
- return lines[:1]
114
- if verbosity < 0.45:
115
- return lines[:2]
116
- if verbosity < 0.75:
117
- return lines[:3]
118
- return lines
119
-
120
-
121
- def _extract_topic_from_lines(lines: list[str]) -> str:
122
- joined = " ".join(lines).lower()
123
-
124
- if any(word in joined for word in ["equation", "variable", "isolate", "algebra"]):
125
- return "algebra"
126
- if any(word in joined for word in ["percent", "percentage"]):
127
- return "percent"
128
- if any(word in joined for word in ["ratio", "proportion"]):
129
- return "ratio"
130
- if any(word in joined for word in ["probability", "outcome"]):
131
- return "probability"
132
- if any(word in joined for word in ["mean", "median", "average"]):
133
- return "statistics"
134
- if any(word in joined for word in ["triangle", "circle", "angle", "area", "perimeter"]):
135
- return "geometry"
136
- if any(word in joined for word in ["integer", "factor", "multiple", "prime", "remainder"]):
137
- return "number_theory"
138
-
139
- return "general"
140
-
141
-
142
- def _why_line(topic: str, lines: list[str]) -> str:
143
- joined = " ".join(lines).lower()
144
-
145
- if topic == "algebra":
146
- if "equation" in joined or "isolate" in joined or "variable" in joined:
147
- return "Why: this works because inverse operations undo what is attached to the variable while keeping the equation balanced."
148
- return "Why: the goal is to isolate the variable without changing the balance of the relationship."
149
-
150
- if topic == "percent":
151
- return "Why: percent relationships depend on identifying the correct base value before calculating the change or part."
152
-
153
- if topic == "ratio":
154
- return "Why: ratios compare parts consistently, so the relationship must stay proportional."
155
-
156
- if topic == "probability":
157
- return "Why: probability compares favorable outcomes to the total number of possible outcomes."
158
-
159
- if topic == "statistics":
160
- return "Why: statistical measures describe the distribution, so the method depends on what feature the question asks for."
161
-
162
- if topic == "geometry":
163
- return "Why: geometry problems depend on the properties of the figure and the relationships between its parts."
164
-
165
- if topic == "number_theory":
166
- return "Why: number properties such as divisibility, factors, and remainders follow fixed rules that guide the method."
167
-
168
- return "Why: focus on the structure of the problem before doing any calculations."
169
-
170
-
171
- def _tone_adjust_line(line: str, tone: float) -> str:
172
- line = (line or "").strip()
173
- if not line:
174
- return line
175
-
176
- if tone < 0.25:
177
- line = re.sub(r"^let’s\s+", "", line, flags=re.IGNORECASE)
178
- line = re.sub(r"^let's\s+", "", line, flags=re.IGNORECASE)
179
- line = re.sub(r"^here is the idea in context:\s*", "", line, flags=re.IGNORECASE)
180
- return line.strip()
181
-
182
- return line
183
-
184
-
185
  def _coerce_string(value: Any) -> str:
186
  return (value or "").strip() if isinstance(value, str) else ""
187
 
@@ -199,85 +58,59 @@ def _coerce_list(value: Any) -> List[str]:
199
  return []
200
 
201
 
202
- def _clean_section_items(items: List[str]) -> List[str]:
203
- cleaned = []
204
- seen = set()
205
-
206
- for item in items or []:
207
- text = (item or "").strip()
208
- if not text:
209
- continue
210
- if _is_wrapper_line(text):
211
- continue
212
- key = _normalize_key(text)
213
- if key in seen:
214
- continue
215
- seen.add(key)
216
- cleaned.append(text)
217
-
218
- return cleaned
219
-
220
-
221
- def _append_section(lines: List[str], title: str, items: List[str], limit: int) -> None:
222
- cleaned = _clean_section_items(items)
223
- if not cleaned:
224
- return
225
-
226
- lines.append("")
227
- lines.append(title)
228
- for item in cleaned[:limit]:
229
- lines.append(f"- {item}")
230
-
231
-
232
- def _get_scaffold(result: Any):
233
- return getattr(result, "scaffold", None)
234
 
235
 
236
- def _explainer_topic(result: Any) -> str:
237
- topic = _coerce_string(getattr(result, "topic", ""))
238
- if topic:
239
- return topic
240
 
241
- joined = " ".join(
242
- _coerce_list(getattr(result, "teaching_points", []))
243
- + _coerce_list(getattr(result, "givens", []))
244
- + _coerce_list(getattr(result, "relationships", []))
245
- )
246
- return _extract_topic_from_lines([joined]) if joined else "general"
 
 
 
 
 
 
 
 
 
247
 
248
 
249
- def _safe_solver_steps(steps: Optional[List[str]]) -> List[str]:
250
- if not steps:
251
- return []
252
- banned = [
253
- r"\bthe answer is\b",
254
- r"\banswer:\b",
255
- r"\bfinal answer\b",
256
- r"\bx\s*=",
257
- r"\by\s*=",
258
- r"\bresult is\b",
259
- r"\btherefore\b",
260
- ]
261
- cleaned: List[str] = []
262
- for step in steps:
263
- s = str(step or "").strip()
264
- if not s:
265
- continue
266
- low = s.lower()
267
- if any(re.search(p, low) for p in banned):
268
- continue
269
- cleaned.append(s)
270
- return _dedupe_lines(cleaned)
271
-
272
-
273
- def _explainer_header(tone: float) -> str:
274
- if tone < 0.2:
275
- return "Question breakdown:"
276
- if tone < 0.45:
277
- return "Here’s what the question is really asking:"
278
- if tone < 0.75:
279
- return "Let’s break down what the question is asking:"
280
- return "You’ve got this — here’s what the question is really asking:"
281
 
282
 
283
  def _format_answer_mode(
@@ -288,18 +121,14 @@ def _format_answer_mode(
288
  transparency: float,
289
  ) -> str:
290
  output: List[str] = []
291
-
292
  prefix = style_prefix(tone)
293
  if prefix:
294
  output.append(prefix)
295
  output.append("")
296
 
297
- output.append("Let’s work through it.")
298
- output.append("")
299
-
300
- limited = _limit_lines_for_verbosity(lines, verbosity, "answer")
301
-
302
  if limited:
 
303
  if len(limited) >= 1:
304
  output.append(f"- What to identify: {limited[0]}")
305
  if len(limited) >= 2:
@@ -311,37 +140,37 @@ def _format_answer_mode(
311
 
312
  if transparency >= 0.8:
313
  output.append("")
314
- output.append(_why_line(topic, limited or lines))
315
 
316
  return "\n".join(output).strip()
317
 
318
 
319
- def format_reply(core: str, tone: float, verbosity: float, transparency: float, help_mode: str) -> str:
 
 
 
 
 
 
 
 
320
  prefix = style_prefix(tone)
321
  core = (core or "").strip()
322
 
323
  if not core:
324
  return prefix or "Start with the structure of the problem."
325
 
326
- lines = _clean_lines(core)
327
- lines = _strip_leading_wrapper_lines(lines)
328
- lines = [_tone_adjust_line(line, tone) for line in lines]
329
- lines = [line for line in lines if line]
330
- lines = _dedupe_lines(lines)
331
-
332
  if not lines:
333
  return prefix or "Start with the structure of the problem."
334
 
335
- topic = _extract_topic_from_lines(lines)
336
 
337
  if help_mode == "answer":
338
- return _format_answer_mode(lines, topic, tone, verbosity, transparency)
339
 
340
- lines = _limit_lines_for_verbosity(lines, verbosity, help_mode)
341
- if not lines:
342
- return prefix or "Start with the structure of the problem."
343
-
344
- output: list[str] = []
345
 
346
  if prefix:
347
  output.append(prefix)
@@ -349,228 +178,171 @@ def format_reply(core: str, tone: float, verbosity: float, transparency: float,
349
 
350
  if help_mode == "hint":
351
  output.append("Hint:")
352
- output.extend(lines[:1])
353
-
354
  if transparency >= 0.8:
355
  output.append("")
356
- output.append(_why_line(topic, lines[:1]))
357
-
358
  return "\n".join(output).strip()
359
 
360
- if help_mode == "walkthrough":
361
- output.append("Walkthrough:")
362
- output.extend(lines)
363
-
 
364
  if transparency >= 0.8:
365
  output.append("")
366
- output.append(_why_line(topic, lines))
367
-
368
  return "\n".join(output).strip()
369
 
370
- if help_mode in {"step_by_step", "method", "explain", "concept", "definition"}:
371
  label = {
372
- "step_by_step": "Steps:",
373
  "method": "Method:",
374
  "explain": "Explanation:",
375
  "concept": "Key idea:",
376
  "definition": "Key idea:",
377
- }.get(help_mode, "Explanation:")
378
  output.append(label)
379
- output.extend(lines)
380
-
381
  if transparency >= 0.75:
382
  output.append("")
383
- output.append(_why_line(topic, lines))
384
-
385
  return "\n".join(output).strip()
386
 
387
- output.extend(lines)
 
388
 
389
- if transparency >= 0.85 and help_mode != "answer":
390
  output.append("")
391
- output.append(_why_line(topic, lines))
392
 
393
  return "\n".join(output).strip()
394
 
395
 
396
- def _build_progression_sections(
 
 
 
 
397
  result: Any,
398
- help_mode: str,
399
  hint_stage: int,
400
  verbosity: float,
401
  transparency: float,
402
- solver_steps: Optional[List[str]] = None,
403
- can_reveal_answer: bool = False,
404
- final_answer: Optional[str] = None,
405
  ) -> List[str]:
406
  output: List[str] = []
407
  scaffold = _get_scaffold(result)
408
  if scaffold is None:
409
  return output
410
 
411
- ask = _coerce_string(getattr(scaffold, "ask", ""))
 
412
  concept = _coerce_string(getattr(scaffold, "concept", ""))
413
- target = _coerce_string(getattr(scaffold, "target", ""))
414
- setup_actions = _coerce_list(getattr(scaffold, "setup_actions", []))
415
- intermediate_steps = _coerce_list(getattr(scaffold, "intermediate_steps", []))
416
  first_move = _coerce_string(getattr(scaffold, "first_move", ""))
417
  next_hint = _coerce_string(getattr(scaffold, "next_hint", ""))
 
 
418
  variables_to_define = _coerce_list(getattr(scaffold, "variables_to_define", []))
419
  equations_to_form = _coerce_list(getattr(scaffold, "equations_to_form", []))
420
  common_traps = _coerce_list(getattr(scaffold, "common_traps", []))
421
  hint_ladder = _coerce_list(getattr(scaffold, "hint_ladder", []))
422
  key_operations = _coerce_list(getattr(scaffold, "key_operations", []))
423
 
424
- safe_steps = _safe_solver_steps(solver_steps)
 
 
 
425
 
426
- if help_mode == "definition":
427
- stage = 0
428
- elif help_mode in {"walkthrough", "step_by_step"}:
429
- stage = 3
430
- elif help_mode == "answer" and can_reveal_answer:
431
- stage = 3
432
- elif help_mode == "answer":
433
- stage = max(2, hint_stage)
434
- else:
435
- stage = min(max(hint_stage, 0), 3)
436
 
437
  if stage == 0:
438
- if concept:
439
- output.append(concept)
440
- if ask:
441
- output.append("")
442
- output.append("What to identify:")
443
- output.append(f"- {ask}")
444
  if first_move:
445
  output.append("")
446
  output.append("First move:")
447
  output.append(f"- {first_move}")
448
- elif safe_steps:
449
  output.append("")
450
  output.append("First move:")
451
- output.append(f"- {safe_steps[0]}")
452
  return output
453
 
 
 
 
 
 
 
 
 
 
 
 
454
  if stage == 1:
455
- if ask:
456
- output.append("What to identify:")
457
- output.append(f"- {ask}")
458
- if setup_actions:
459
- output.append("")
460
- output.append("Set-up path:")
461
- for item in setup_actions[:2]:
462
- output.append(f"- {item}")
463
- if first_move:
464
- output.append("")
465
- output.append("First move:")
466
- output.append(f"- {first_move}")
467
  if next_hint:
468
  output.append("")
469
  output.append("Next hint:")
470
  output.append(f"- {next_hint}")
471
- elif safe_steps:
472
  output.append("")
473
  output.append("Next hint:")
474
- output.append(f"- {safe_steps[0]}")
475
  return output
476
 
 
 
 
 
 
 
 
 
 
 
 
477
  if stage == 2:
478
- if ask:
479
- output.append("What to identify:")
480
- output.append(f"- {ask}")
481
- if target:
482
- output.append("")
483
- output.append(f"Target: {target}")
484
- if setup_actions:
485
- output.append("")
486
- output.append("Set-up path:")
487
- for item in setup_actions[: min(3, len(setup_actions))]:
488
- output.append(f"- {item}")
489
- progression_steps = intermediate_steps[:2] or safe_steps[:2]
490
- if progression_steps:
491
- output.append("")
492
- output.append("Next steps:")
493
- for item in progression_steps:
494
- output.append(f"- {item}")
495
- if next_hint:
496
- output.append("")
497
- output.append("Next hint:")
498
- output.append(f"- {next_hint}")
499
- if variables_to_define and transparency >= 0.45:
500
  output.append("")
501
  output.append("Variables to define:")
502
  for item in variables_to_define[:2]:
503
  output.append(f"- {item}")
504
- if equations_to_form and transparency >= 0.5:
505
  output.append("")
506
  output.append("Equations to form:")
507
  for item in equations_to_form[:2]:
508
  output.append(f"- {item}")
 
 
 
 
 
509
  return output
510
 
511
- if ask:
512
- output.append("What to identify first:")
513
- output.append(f"- {ask}")
514
-
515
- if setup_actions:
516
- output.append("")
517
- output.append("Set-up path:")
518
- limit = 2 if verbosity < 0.4 else 3 if verbosity < 0.75 else 5
519
- for item in setup_actions[:limit]:
520
- output.append(f"- {item}")
521
-
522
- deep_steps = intermediate_steps or safe_steps
523
- if deep_steps:
524
- output.append("")
525
- output.append("How to build it:")
526
- limit = 2 if verbosity < 0.75 else 4
527
- for item in deep_steps[:limit]:
528
- output.append(f"- {item}")
529
-
530
- if first_move:
531
- output.append("")
532
- output.append("First move:")
533
- output.append(f"- {first_move}")
534
-
535
- if next_hint and (transparency >= 0.35 or verbosity >= 0.45):
536
- output.append("")
537
- output.append("Next hint:")
538
- output.append(f"- {next_hint}")
539
-
540
- if hint_ladder and verbosity >= 0.65:
541
- output.append("")
542
- output.append("Hint ladder:")
543
- for item in hint_ladder[:3]:
544
- output.append(f"- {item}")
545
-
546
- if variables_to_define and verbosity >= 0.65:
547
  output.append("")
548
  output.append("Variables to define:")
549
  for item in variables_to_define[:3]:
550
  output.append(f"- {item}")
551
 
552
- if equations_to_form and transparency >= 0.55:
553
  output.append("")
554
  output.append("Equations to form:")
555
  for item in equations_to_form[:3]:
556
  output.append(f"- {item}")
557
 
558
- if key_operations and transparency >= 0.55:
559
  output.append("")
560
  output.append("Key operations:")
561
  for item in key_operations[:3]:
562
  output.append(f"- {item}")
563
 
564
- if (transparency >= 0.6 or verbosity >= 0.75) and common_traps:
565
  output.append("")
566
  output.append("Watch out for:")
567
  for item in common_traps[:4]:
568
  output.append(f"- {item}")
569
 
570
- if help_mode == "answer" and can_reveal_answer and final_answer:
571
- output.append("")
572
- output.append(f"Final answer: {final_answer}")
573
-
574
  return output
575
 
576
 
@@ -579,140 +351,49 @@ def format_explainer_response(
579
  tone: float,
580
  verbosity: float,
581
  transparency: float,
582
- help_mode: str = "explain",
583
  hint_stage: int = 0,
584
- solver_steps: Optional[List[str]] = None,
585
- can_reveal_answer: bool = False,
586
- final_answer: Optional[str] = None,
587
  ) -> str:
588
  if not result or not getattr(result, "understood", False):
589
  return "I can help explain what the question is asking, but I need the full wording of the question."
590
 
591
  output: List[str] = []
592
-
593
- header = _explainer_header(tone)
594
- if header:
595
- output.append(header)
596
  output.append("")
597
 
598
- summary = _coerce_string(getattr(result, "summary", ""))
599
- teaching_points = _coerce_list(getattr(result, "teaching_points", []))
600
 
601
- if summary and not _is_wrapper_line(summary):
 
602
  output.append(summary)
603
 
604
- scaffold_sections = _build_progression_sections(
605
  result=result,
606
- help_mode=help_mode,
607
  hint_stage=hint_stage,
608
  verbosity=verbosity,
609
  transparency=transparency,
610
- solver_steps=solver_steps,
611
- can_reveal_answer=can_reveal_answer,
612
- final_answer=final_answer,
613
  )
614
- if scaffold_sections:
615
- output.extend(scaffold_sections)
 
 
616
 
617
- if teaching_points and verbosity >= 0.55 and hint_stage >= 2:
 
618
  output.append("")
619
  output.append("Key teaching points:")
620
- limit = 2 if verbosity < 0.75 else 4
621
- for item in teaching_points[:limit]:
622
  output.append(f"- {item}")
623
 
624
- plain = _coerce_string(getattr(result, "plain_english", ""))
625
- if plain and not summary and not _is_wrapper_line(plain):
626
- output.append(plain)
627
-
628
- asks_for = _coerce_string(getattr(result, "asks_for", ""))
629
- if asks_for and transparency >= 0.35 and hint_stage >= 2:
630
- output.append("")
631
- output.append(f"The question is asking for: {asks_for}")
632
-
633
- if verbosity >= 0.45 and hint_stage >= 2:
634
- _append_section(
635
- output,
636
- "What the question gives you:",
637
- getattr(result, "givens", []) or [],
638
- 5 if verbosity >= 0.7 else 3,
639
- )
640
-
641
- if verbosity >= 0.7 and hint_stage >= 3:
642
- _append_section(
643
- output,
644
- "Constraints or conditions:",
645
- getattr(result, "constraints", []) or [],
646
- 5,
647
- )
648
-
649
- if transparency >= 0.5 and hint_stage >= 2:
650
- _append_section(
651
- output,
652
- "Key relationship:",
653
- getattr(result, "relationships", []) or [],
654
- 4,
655
- )
656
-
657
- if transparency >= 0.55 and hint_stage >= 2:
658
- _append_section(
659
- output,
660
- "Concepts you will probably need:",
661
- getattr(result, "needed_concepts", []) or [],
662
- 4,
663
- )
664
-
665
- if (transparency >= 0.6 or verbosity >= 0.75) and hint_stage >= 3:
666
- _append_section(
667
- output,
668
- "Watch out for:",
669
- getattr(result, "trap_notes", []) or [],
670
- 4,
671
- )
672
-
673
- strategy_hint = _coerce_string(getattr(result, "strategy_hint", ""))
674
- if strategy_hint and verbosity >= 0.35 and not _is_wrapper_line(strategy_hint) and hint_stage >= 1:
675
- output.append("")
676
- output.append(f"Best starting move: {strategy_hint}")
677
 
678
  if transparency >= 0.8:
679
- topic = _explainer_topic(result)
680
- why_seed_lines: List[str] = []
681
-
682
- if summary:
683
- why_seed_lines.append(summary)
684
-
685
- scaffold = _get_scaffold(result)
686
- if scaffold is not None:
687
- ask = _coerce_string(getattr(scaffold, "ask", ""))
688
- first_move = _coerce_string(getattr(scaffold, "first_move", ""))
689
- if ask:
690
- why_seed_lines.append(ask)
691
- if first_move:
692
- why_seed_lines.append(first_move)
693
-
694
- if not why_seed_lines:
695
- why_seed_lines.extend(teaching_points[:2])
696
-
697
- if why_seed_lines:
698
- output.append("")
699
- output.append(_why_line(topic, why_seed_lines))
700
-
701
- final_lines = []
702
- previous_key = None
703
- for line in output:
704
- if line is None:
705
- continue
706
- text = str(line).rstrip()
707
- key = _normalize_key(text)
708
- if key == previous_key and key:
709
- continue
710
- final_lines.append(text)
711
- previous_key = key
712
-
713
- text = "\n".join(final_lines).strip()
714
-
715
- if not text:
716
- return "I can help explain what the question is asking, but I need the full wording of the question."
717
 
718
- return text
 
14
  return "You’ve got this — let’s solve it cleanly."
15
 
16
 
 
 
 
 
 
 
 
 
 
17
  def _normalize_key(text: str) -> str:
18
  text = (text or "").strip().lower()
19
  text = text.replace("’", "'")
 
21
  return text
22
 
23
 
24
+ def _clean_lines(core: str) -> list[str]:
25
+ lines = []
26
+ for line in (core or "").splitlines():
27
+ cleaned = line.strip()
28
+ if cleaned:
29
+ lines.append(cleaned)
30
+ return lines
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
 
33
  def _dedupe_lines(lines: list[str]) -> list[str]:
 
41
  return output
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def _coerce_string(value: Any) -> str:
45
  return (value or "").strip() if isinstance(value, str) else ""
46
 
 
58
  return []
59
 
60
 
61
+ def _limit_steps(steps: List[str], verbosity: float, minimum: int = 1) -> List[str]:
62
+ if not steps:
63
+ return []
64
+ if verbosity < 0.25:
65
+ limit = minimum
66
+ elif verbosity < 0.5:
67
+ limit = max(minimum, 2)
68
+ elif verbosity < 0.75:
69
+ limit = max(minimum, 3)
70
+ else:
71
+ limit = max(minimum, 5)
72
+ return steps[:limit]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
 
75
+ def _why_line(topic: str) -> str:
76
+ topic = (topic or "").lower()
 
 
77
 
78
+ if topic == "algebra":
79
+ return "Why: algebra works by keeping the relationship balanced while undoing the operations attached to the variable."
80
+ if topic == "percent":
81
+ return "Why: percent questions depend on choosing the correct base before doing any calculation."
82
+ if topic == "ratio":
83
+ return "Why: ratio questions depend on preserving the comparison and using one shared scale factor."
84
+ if topic == "probability":
85
+ return "Why: probability compares successful outcomes to all possible outcomes."
86
+ if topic == "statistics":
87
+ return "Why: the right method depends on which summary measure the question actually asks for."
88
+ if topic == "geometry":
89
+ return "Why: geometry depends on the relationships between the parts of the figure."
90
+ if topic == "number_theory":
91
+ return "Why: number properties follow fixed rules about divisibility, factors, and remainders."
92
+ return "Why: start with the structure of the problem before calculating."
93
 
94
 
95
+ def _extract_topic_from_text(text: str, fallback: Optional[str] = None) -> str:
96
+ low = (text or "").lower()
97
+ if fallback:
98
+ return fallback
99
+ if any(word in low for word in ["equation", "variable", "isolate", "algebra"]):
100
+ return "algebra"
101
+ if any(word in low for word in ["percent", "percentage", "%"]):
102
+ return "percent"
103
+ if any(word in low for word in ["ratio", "proportion"]):
104
+ return "ratio"
105
+ if any(word in low for word in ["probability", "outcome", "chance", "odds"]):
106
+ return "probability"
107
+ if any(word in low for word in ["mean", "median", "average"]):
108
+ return "statistics"
109
+ if any(word in low for word in ["triangle", "circle", "angle", "area", "perimeter"]):
110
+ return "geometry"
111
+ if any(word in low for word in ["integer", "factor", "multiple", "prime", "remainder"]):
112
+ return "number_theory"
113
+ return "general"
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
 
116
  def _format_answer_mode(
 
121
  transparency: float,
122
  ) -> str:
123
  output: List[str] = []
 
124
  prefix = style_prefix(tone)
125
  if prefix:
126
  output.append(prefix)
127
  output.append("")
128
 
129
+ limited = _limit_steps(lines, verbosity, minimum=2)
 
 
 
 
130
  if limited:
131
+ output.append("Answer path:")
132
  if len(limited) >= 1:
133
  output.append(f"- What to identify: {limited[0]}")
134
  if len(limited) >= 2:
 
140
 
141
  if transparency >= 0.8:
142
  output.append("")
143
+ output.append(_why_line(topic))
144
 
145
  return "\n".join(output).strip()
146
 
147
 
148
+ def format_reply(
149
+ core: str,
150
+ tone: float,
151
+ verbosity: float,
152
+ transparency: float,
153
+ help_mode: str,
154
+ hint_stage: int = 0,
155
+ topic: Optional[str] = None,
156
+ ) -> str:
157
  prefix = style_prefix(tone)
158
  core = (core or "").strip()
159
 
160
  if not core:
161
  return prefix or "Start with the structure of the problem."
162
 
163
+ lines = _dedupe_lines(_clean_lines(core))
 
 
 
 
 
164
  if not lines:
165
  return prefix or "Start with the structure of the problem."
166
 
167
+ resolved_topic = _extract_topic_from_text(core, topic)
168
 
169
  if help_mode == "answer":
170
+ return _format_answer_mode(lines, resolved_topic, tone, verbosity, transparency)
171
 
172
+ shown = _limit_steps(lines, verbosity, minimum=1)
173
+ output: List[str] = []
 
 
 
174
 
175
  if prefix:
176
  output.append(prefix)
 
178
 
179
  if help_mode == "hint":
180
  output.append("Hint:")
181
+ output.append(f"- {shown[0]}")
 
182
  if transparency >= 0.8:
183
  output.append("")
184
+ output.append(_why_line(resolved_topic))
 
185
  return "\n".join(output).strip()
186
 
187
+ if help_mode in {"instruction", "step_by_step", "walkthrough"}:
188
+ label = "First step:" if help_mode == "instruction" else "Walkthrough:"
189
+ output.append(label)
190
+ for line in shown:
191
+ output.append(f"- {line}")
192
  if transparency >= 0.8:
193
  output.append("")
194
+ output.append(_why_line(resolved_topic))
 
195
  return "\n".join(output).strip()
196
 
197
+ if help_mode in {"method", "explain", "concept", "definition"}:
198
  label = {
 
199
  "method": "Method:",
200
  "explain": "Explanation:",
201
  "concept": "Key idea:",
202
  "definition": "Key idea:",
203
+ }[help_mode]
204
  output.append(label)
205
+ for line in shown:
206
+ output.append(f"- {line}")
207
  if transparency >= 0.75:
208
  output.append("")
209
+ output.append(_why_line(resolved_topic))
 
210
  return "\n".join(output).strip()
211
 
212
+ for line in shown:
213
+ output.append(f"- {line}")
214
 
215
+ if transparency >= 0.85:
216
  output.append("")
217
+ output.append(_why_line(resolved_topic))
218
 
219
  return "\n".join(output).strip()
220
 
221
 
222
+ def _get_scaffold(result: Any):
223
+ return getattr(result, "scaffold", None)
224
+
225
+
226
+ def _staged_scaffold_lines(
227
  result: Any,
 
228
  hint_stage: int,
229
  verbosity: float,
230
  transparency: float,
 
 
 
231
  ) -> List[str]:
232
  output: List[str] = []
233
  scaffold = _get_scaffold(result)
234
  if scaffold is None:
235
  return output
236
 
237
+ stage = max(0, min(int(hint_stage), 3))
238
+
239
  concept = _coerce_string(getattr(scaffold, "concept", ""))
240
+ ask = _coerce_string(getattr(scaffold, "ask", ""))
 
 
241
  first_move = _coerce_string(getattr(scaffold, "first_move", ""))
242
  next_hint = _coerce_string(getattr(scaffold, "next_hint", ""))
243
+ setup_actions = _coerce_list(getattr(scaffold, "setup_actions", []))
244
+ intermediate_steps = _coerce_list(getattr(scaffold, "intermediate_steps", []))
245
  variables_to_define = _coerce_list(getattr(scaffold, "variables_to_define", []))
246
  equations_to_form = _coerce_list(getattr(scaffold, "equations_to_form", []))
247
  common_traps = _coerce_list(getattr(scaffold, "common_traps", []))
248
  hint_ladder = _coerce_list(getattr(scaffold, "hint_ladder", []))
249
  key_operations = _coerce_list(getattr(scaffold, "key_operations", []))
250
 
251
+ if concept and stage == 0 and transparency >= 0.75:
252
+ output.append("Core idea:")
253
+ output.append(f"- {concept}")
254
+ output.append("")
255
 
256
+ if ask:
257
+ output.append("What to identify first:")
258
+ output.append(f"- {ask}")
 
 
 
 
 
 
 
259
 
260
  if stage == 0:
 
 
 
 
 
 
261
  if first_move:
262
  output.append("")
263
  output.append("First move:")
264
  output.append(f"- {first_move}")
265
+ elif hint_ladder:
266
  output.append("")
267
  output.append("First move:")
268
+ output.append(f"- {hint_ladder[0]}")
269
  return output
270
 
271
+ if setup_actions:
272
+ output.append("")
273
+ output.append("Set-up path:")
274
+ for item in _limit_steps(setup_actions, verbosity, minimum=2 if stage >= 1 else 1):
275
+ output.append(f"- {item}")
276
+
277
+ if first_move:
278
+ output.append("")
279
+ output.append("First move:")
280
+ output.append(f"- {first_move}")
281
+
282
  if stage == 1:
 
 
 
 
 
 
 
 
 
 
 
 
283
  if next_hint:
284
  output.append("")
285
  output.append("Next hint:")
286
  output.append(f"- {next_hint}")
287
+ elif len(hint_ladder) >= 2:
288
  output.append("")
289
  output.append("Next hint:")
290
+ output.append(f"- {hint_ladder[1]}")
291
  return output
292
 
293
+ if intermediate_steps:
294
+ output.append("")
295
+ output.append("How to build it:")
296
+ for item in _limit_steps(intermediate_steps, verbosity, minimum=2):
297
+ output.append(f"- {item}")
298
+
299
+ if next_hint:
300
+ output.append("")
301
+ output.append("Next hint:")
302
+ output.append(f"- {next_hint}")
303
+
304
  if stage == 2:
305
+ if variables_to_define:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  output.append("")
307
  output.append("Variables to define:")
308
  for item in variables_to_define[:2]:
309
  output.append(f"- {item}")
310
+ if equations_to_form:
311
  output.append("")
312
  output.append("Equations to form:")
313
  for item in equations_to_form[:2]:
314
  output.append(f"- {item}")
315
+ if key_operations:
316
+ output.append("")
317
+ output.append("Key operations:")
318
+ for item in key_operations[:3]:
319
+ output.append(f"- {item}")
320
  return output
321
 
322
+ if variables_to_define:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  output.append("")
324
  output.append("Variables to define:")
325
  for item in variables_to_define[:3]:
326
  output.append(f"- {item}")
327
 
328
+ if equations_to_form:
329
  output.append("")
330
  output.append("Equations to form:")
331
  for item in equations_to_form[:3]:
332
  output.append(f"- {item}")
333
 
334
+ if key_operations:
335
  output.append("")
336
  output.append("Key operations:")
337
  for item in key_operations[:3]:
338
  output.append(f"- {item}")
339
 
340
+ if common_traps:
341
  output.append("")
342
  output.append("Watch out for:")
343
  for item in common_traps[:4]:
344
  output.append(f"- {item}")
345
 
 
 
 
 
346
  return output
347
 
348
 
 
351
  tone: float,
352
  verbosity: float,
353
  transparency: float,
 
354
  hint_stage: int = 0,
 
 
 
355
  ) -> str:
356
  if not result or not getattr(result, "understood", False):
357
  return "I can help explain what the question is asking, but I need the full wording of the question."
358
 
359
  output: List[str] = []
360
+ prefix = style_prefix(tone)
361
+ if prefix:
362
+ output.append(prefix)
 
363
  output.append("")
364
 
365
+ output.append("Question breakdown:")
366
+ output.append("")
367
 
368
+ summary = _coerce_string(getattr(result, "summary", ""))
369
+ if summary:
370
  output.append(summary)
371
 
372
+ scaffold_lines = _staged_scaffold_lines(
373
  result=result,
 
374
  hint_stage=hint_stage,
375
  verbosity=verbosity,
376
  transparency=transparency,
 
 
 
377
  )
378
+ if scaffold_lines:
379
+ if summary:
380
+ output.append("")
381
+ output.extend(scaffold_lines)
382
 
383
+ teaching_points = _coerce_list(getattr(result, "teaching_points", []))
384
+ if teaching_points and (verbosity >= 0.55 or hint_stage >= 2):
385
  output.append("")
386
  output.append("Key teaching points:")
387
+ for item in _limit_steps(teaching_points, verbosity, minimum=2):
 
388
  output.append(f"- {item}")
389
 
390
+ topic = _extract_topic_from_text(
391
+ f"{summary} {' '.join(teaching_points)}",
392
+ getattr(result, "topic", None),
393
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
395
  if transparency >= 0.8:
396
+ output.append("")
397
+ output.append(_why_line(topic))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
 
399
+ return "\n".join(_dedupe_lines(output)).strip()