| from __future__ import annotations |
|
|
| import re |
| from typing import Any, List, Optional |
|
|
|
|
| def style_prefix(tone: float) -> str: |
| if tone < 0.2: |
| return "" |
| if tone < 0.45: |
| return "Ok —" |
| if tone < 0.75: |
| return "Let’s work through it." |
| return "You’ve got this — let’s work through it step by step." |
|
|
|
|
| def _clean_lines(core: str) -> List[str]: |
| lines: List[str] = [] |
| for line in (core or "").splitlines(): |
| cleaned = line.strip() |
| if cleaned: |
| lines.append(cleaned.lstrip("- ").strip()) |
| return lines |
|
|
|
|
| def _normalize_key(text: str) -> str: |
| return re.sub(r"\s+", " ", (text or "").strip().lower()) |
|
|
|
|
| def _dedupe_lines(lines: List[str]) -> List[str]: |
| seen = set() |
| out: List[str] = [] |
| for line in lines: |
| key = _normalize_key(line) |
| if key and key not in seen: |
| seen.add(key) |
| out.append(line.strip()) |
| return out |
|
|
|
|
| def _extract_topic_from_text(core: str, topic: Optional[str]) -> str: |
| if topic: |
| return str(topic).strip().lower() |
| text = (core or "").lower() |
| if "probability" in text or "favorable" in text or "sample space" in text: |
| return "probability" |
| if "percent" in text or "%" in text: |
| return "percent" |
| if "ratio" in text or "multiplier" in text: |
| return "ratio" |
| if "variable" in text or "equation" in text: |
| return "algebra" |
| if "variability" in text or "standard deviation" in text or "spread" in text: |
| return "statistics" |
| if "rectangle" in text or "perimeter" in text or "area" in text: |
| return "geometry" |
| return "general" |
|
|
|
|
| def _normalize_display_lines(lines: List[str]) -> List[str]: |
| return [re.sub(r"\s+", " ", (line or "").strip()) for line in lines if str(line).strip()] |
|
|
|
|
| def _limit_steps(lines: List[str], verbosity: float, minimum: int = 1) -> List[str]: |
| if not lines: |
| return [] |
| if verbosity < 0.22: |
| limit = minimum |
| elif verbosity < 0.55: |
| limit = max(minimum, min(2, len(lines))) |
| elif verbosity < 0.82: |
| limit = max(minimum, min(4, len(lines))) |
| else: |
| limit = len(lines) |
| return lines[:limit] |
|
|
|
|
| def _why_line(topic: str) -> str: |
| if topic == "algebra": |
| return "Why this helps: reversing operations in the right order keeps the equation equivalent while you isolate the variable." |
| if topic == "percent": |
| return "Why this helps: percent questions usually break when the base quantity is chosen incorrectly." |
| if topic == "ratio": |
| return "Why this helps: ratio numbers are usually parts, not the final quantities themselves." |
| if topic == "probability": |
| return "Why this helps: the numerator and denominator must be counted under the same rules." |
| if topic == "statistics": |
| return "Why this helps: statistics questions depend on choosing the right measure before calculating." |
| if topic == "geometry": |
| return "Why this helps: matching the right formula to the shape simplifies the rest of the work." |
| return "Why this helps: getting the structure right first makes the next step clearer." |
|
|
|
|
| def _tone_rewrite(line: str, tone: float, position: int = 0) -> str: |
| text = (line or "").strip() |
| if not text: |
| return text |
| if tone < 0.25: |
| return text |
| if tone < 0.55: |
| return f"Start here: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text |
| if tone < 0.8: |
| return f"A good place to start is this: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text |
| return f"You can start with this: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text |
|
|
|
|
| def _transparency_expansion(line: str, topic: str, transparency: float, position: int = 0) -> str: |
| text = (line or "").strip() |
| if not text or transparency < 0.35: |
| return text |
| if transparency < 0.7: |
| if position == 0: |
| if topic == "algebra": |
| return f"{text} This keeps the equation balanced while you isolate the variable." |
| if topic == "percent": |
| return f"{text} This keeps the percent relationship tied to the correct base quantity." |
| if topic == "ratio": |
| return f"{text} This turns the ratio into usable quantities instead of labels." |
| if topic == "probability": |
| return f"{text} This separates successful outcomes from total outcomes." |
| return text |
| if position == 0: |
| if topic == "algebra": |
| return f"{text} In algebra, each step should preserve an equivalent equation so the solution does not change while the variable is isolated." |
| if topic == "percent": |
| return f"{text} Percent problems become clearer once the base quantity is fixed, because every percentage must refer back to some amount." |
| if topic == "ratio": |
| return f"{text} Ratio numbers usually describe relative parts, so turning them into multiples of one common quantity is what makes the setup usable." |
| if topic == "probability": |
| return f"{text} Probability depends on a consistent sample space, so the numerator and denominator must be counted under the same rules." |
| if topic == "statistics": |
| return f"{text} Statistics questions often hinge on choosing the right measure first, because different measures capture different features of the data." |
| if topic == "geometry": |
| return f"{text} Geometry problems often become routine once the correct formula is chosen, because the rest is usually substitution and algebra." |
| return f"{text} This makes the underlying structure explicit before you calculate." |
| return text |
|
|
|
|
| def _styled_lines(lines: List[str], tone: float, transparency: float, topic: str) -> List[str]: |
| output: List[str] = [] |
| for i, line in enumerate(lines): |
| rewritten = _tone_rewrite(line, tone, i) |
| rewritten = _transparency_expansion(rewritten, topic, transparency, i) |
| output.append(rewritten) |
| return output |
|
|
|
|
| def format_reply( |
| core: str, |
| tone: float, |
| verbosity: float, |
| transparency: float, |
| help_mode: str, |
| hint_stage: int = 0, |
| topic: Optional[str] = None, |
| ) -> str: |
| prefix = style_prefix(tone) |
| core = (core or "").strip() |
| if not core: |
| return prefix or "Start with the structure of the problem." |
|
|
| lines = _dedupe_lines(_clean_lines(core)) |
| if not lines: |
| return prefix or "Start with the structure of the problem." |
|
|
| resolved_topic = _extract_topic_from_text(core, topic) |
| normalized_lines = _normalize_display_lines(lines) |
| output: List[str] = [] |
| if prefix: |
| output.extend([prefix, ""]) |
|
|
| if help_mode == "hint": |
| idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1)) |
| if verbosity < 0.25: |
| shown = [normalized_lines[idx]] |
| elif verbosity < 0.62: |
| shown = normalized_lines[idx: idx + 2] or [normalized_lines[idx]] |
| else: |
| shown = normalized_lines[: min(4, len(normalized_lines))] |
| shown = _styled_lines(shown, tone, transparency, resolved_topic) |
| output.append("Hint:") |
| output.extend(f"- {line}" for line in shown) |
| if transparency >= 0.75: |
| output.extend(["", _why_line(resolved_topic)]) |
| return "\n".join(output).strip() |
|
|
| if help_mode in {"walkthrough", "instruction", "step_by_step"}: |
| shown = _limit_steps(normalized_lines, verbosity, minimum=2 if help_mode == "walkthrough" else 1) |
| shown = _styled_lines(shown, tone, transparency, resolved_topic) |
| output.append("Walkthrough:" if help_mode == "walkthrough" else "Step-by-step path:") |
| output.extend(f"- {line}" for line in shown) |
| if transparency >= 0.7: |
| output.extend(["", _why_line(resolved_topic)]) |
| return "\n".join(output).strip() |
|
|
| if help_mode in {"method", "explain", "concept", "definition"}: |
| shown = _limit_steps(normalized_lines, verbosity, minimum=1) |
| shown = _styled_lines(shown, tone, transparency, resolved_topic) |
| output.append("Explanation:") |
| output.extend(f"- {line}" for line in shown) |
| if transparency >= 0.6: |
| output.extend(["", _why_line(resolved_topic)]) |
| return "\n".join(output).strip() |
|
|
| if help_mode == "answer": |
| shown = _limit_steps(normalized_lines, verbosity, minimum=2) |
| answer_transparency = transparency if verbosity >= 0.45 else min(transparency, 0.45) |
| shown = _styled_lines(shown, tone, answer_transparency, resolved_topic) |
| output.append("Answer path:") |
| output.extend(f"- {line}" for line in shown) |
| if transparency >= 0.75: |
| output.extend(["", _why_line(resolved_topic)]) |
| return "\n".join(output).strip() |
|
|
| shown = _limit_steps(normalized_lines, verbosity, minimum=1) |
| shown = _styled_lines(shown, tone, transparency, resolved_topic) |
| output.extend(f"- {line}" for line in shown) |
| if transparency >= 0.8: |
| output.extend(["", _why_line(resolved_topic)]) |
| return "\n".join(output).strip() |
|
|
|
|
| def format_explainer_response( |
| result: Any, |
| tone: float, |
| verbosity: float, |
| transparency: float, |
| help_mode: str = "explain", |
| hint_stage: int = 0, |
| ) -> str: |
| if not result: |
| return "I can help explain what the question is asking, but I need the full wording of the question." |
| summary = getattr(result, "summary", "") or "" |
| teaching_points = getattr(result, "teaching_points", []) or [] |
| core_lines: List[str] = [] |
| if isinstance(summary, str) and summary.strip(): |
| core_lines.append(summary.strip()) |
| if isinstance(teaching_points, list): |
| for item in teaching_points: |
| text = str(item).strip() |
| if text: |
| core_lines.append(text) |
| if not core_lines: |
| core_lines = ["Start by identifying what the question is asking."] |
| return format_reply( |
| core="\n".join(core_lines), |
| tone=tone, |
| verbosity=verbosity, |
| transparency=transparency, |
| help_mode=help_mode, |
| hint_stage=hint_stage, |
| topic=getattr(result, "topic", None), |
| ) |
|
|