| 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) |
| return lines |
|
|
|
|
| def _normalize_key(text: str) -> str: |
| text = (text or "").strip().lower() |
| text = text.replace("’", "'") |
| text = re.sub(r"\s+", " ", text) |
| return text |
|
|
|
|
| def _dedupe_lines(lines: List[str]) -> List[str]: |
| seen = set() |
| output: List[str] = [] |
| for line in lines: |
| key = _normalize_key(line) |
| if key and key not in seen: |
| seen.add(key) |
| output.append(line.strip()) |
| return output |
|
|
|
|
| def _strip_bullet_prefix(text: str) -> str: |
| return re.sub(r"^\s*[-•]\s*", "", (text or "").strip()) |
|
|
|
|
| def _normalize_display_lines(lines: List[str]) -> List[str]: |
| cleaned: List[str] = [] |
| for line in lines: |
| item = _strip_bullet_prefix(line) |
| if item: |
| cleaned.append(item) |
| return cleaned |
|
|
|
|
| def _limit_steps(steps: List[str], verbosity: float, minimum: int = 1) -> List[str]: |
| if not steps: |
| return [] |
| if verbosity < 0.2: |
| limit = minimum |
| elif verbosity < 0.45: |
| limit = max(minimum, 2) |
| elif verbosity < 0.7: |
| limit = max(minimum, 3) |
| else: |
| limit = max(minimum, 5) |
| return steps[:limit] |
|
|
|
|
| def _extract_topic_from_text(text: str, fallback: Optional[str] = None) -> str: |
| low = (text or "").lower() |
| if fallback: |
| return fallback |
| if any(word in low for word in ["equation", "variable", "isolate", "algebra"]): |
| return "algebra" |
| if any(word in low for word in ["percent", "percentage", "%"]): |
| return "percent" |
| if any(word in low for word in ["ratio", "proportion"]): |
| return "ratio" |
| if any(word in low for word in ["probability", "outcome", "chance", "odds"]): |
| return "probability" |
| if any(word in low for word in ["mean", "median", "average", "data", "variance", "standard deviation"]): |
| return "statistics" |
| if any(word in low for word in ["triangle", "circle", "angle", "area", "perimeter", "circumference", "rectangle"]): |
| return "geometry" |
| if any(word in low for word in ["integer", "factor", "multiple", "prime", "remainder", "divisible"]): |
| return "number_theory" |
| return "general" |
|
|
|
|
| def _why_line(topic: Optional[str]) -> str: |
| topic = (topic or "general").strip().lower() |
|
|
| if topic == "algebra": |
| return "Why this helps: algebra becomes easier when you reverse operations in order and keep the variable isolated." |
| if topic == "percent": |
| return "Why this helps: percent problems work best when you identify the base amount and apply each change to the correct value." |
| if topic == "ratio": |
| return "Why this helps: ratios represent parts of a whole, so turning them into total parts keeps the set-up consistent." |
| if topic == "probability": |
| return "Why this helps: probability is favorable outcomes over total outcomes, so clear counting matters first." |
| if topic == "statistics": |
| return "Why this helps: statistics questions depend on choosing the right measure before you calculate anything." |
| if topic == "geometry": |
| return "Why this helps: geometry usually becomes manageable once you identify the correct formula and substitute carefully." |
| if topic == "number_theory": |
| return "Why this helps: number theory questions usually depend on patterns, divisibility, or factor structure rather than brute force." |
| return "Why this helps: identifying the structure first makes the next step clearer and reduces avoidable mistakes." |
|
|
|
|
| def _tone_rewrite(line: str, tone: float, position: int = 0) -> str: |
| text = (line or "").strip() |
| if not text: |
| return text |
|
|
| if tone < 0.2: |
| return text |
| if tone < 0.45: |
| return text[:1].upper() + text[1:] if text else text |
| if tone < 0.75: |
| return f"Start here: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text |
| 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 |
|
|
|
|
| def _transparency_expansion(line: str, topic: str, transparency: float, position: int = 0) -> str: |
| text = (line or "").strip() |
| if not text: |
| return text |
|
|
| if transparency < 0.35: |
| return text |
| if transparency < 0.7: |
| if position == 0: |
| if topic == "algebra": |
| return f"{text} This helps isolate the variable." |
| if topic == "percent": |
| return f"{text} This keeps track of the percent relationship correctly." |
| if topic == "ratio": |
| return f"{text} This helps turn the ratio into usable parts." |
| if topic == "probability": |
| return f"{text} This sets up favorable versus total outcomes." |
| return text |
|
|
| if position == 0: |
| if topic == "algebra": |
| return f"{text} In algebra, the goal is to isolate the variable by reversing operations in the opposite order." |
| if topic == "percent": |
| return f"{text} Percent questions are easiest when you identify the base amount and apply the change to the right quantity." |
| if topic == "ratio": |
| return f"{text} Ratio questions become easier once you treat the ratio numbers as parts of one total." |
| if topic == "probability": |
| return f"{text} Probability depends on counting the favorable cases and the total possible cases accurately." |
| if topic == "statistics": |
| return f"{text} Statistics questions usually depend on choosing the correct measure before calculating." |
| if topic == "geometry": |
| return f"{text} Geometry problems are usually solved by matching the shape to the correct formula first." |
| return f"{text} This works because it clarifies the structure 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.append(prefix) |
| output.append("") |
|
|
| if help_mode == "hint": |
| if verbosity < 0.25: |
| idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1)) |
| shown = [normalized_lines[idx]] |
| elif verbosity < 0.6: |
| idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1)) |
| shown = normalized_lines[idx:idx + 2] or [normalized_lines[idx]] |
| else: |
| shown = normalized_lines[: min(3, len(normalized_lines))] |
|
|
| shown = _styled_lines(shown, tone, transparency, resolved_topic) |
| output.append("Hint:") |
| for line in shown: |
| output.append(f"- {line}") |
|
|
| if transparency >= 0.75: |
| output.append("") |
| output.append(_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:") |
| for line in shown: |
| output.append(f"- {line}") |
|
|
| if transparency >= 0.7: |
| output.append("") |
| output.append(_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:") |
| for line in shown: |
| output.append(f"- {line}") |
|
|
| if transparency >= 0.6: |
| output.append("") |
| output.append(_why_line(resolved_topic)) |
| return "\n".join(output).strip() |
|
|
| if help_mode == "answer": |
| shown = _limit_steps(normalized_lines, verbosity, minimum=2) |
| shown = _styled_lines(shown, tone, transparency if verbosity >= 0.45 else min(transparency, 0.4), resolved_topic) |
| output.append("Answer path:") |
| for line in shown: |
| output.append(f"- {line}") |
|
|
| if transparency >= 0.75: |
| output.append("") |
| output.append(_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) |
| for line in shown: |
| output.append(f"- {line}") |
|
|
| if transparency >= 0.8: |
| output.append("") |
| output.append(_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), |
| ) |