| from __future__ import annotations |
|
|
| import re |
| from typing import Any, List |
|
|
|
|
| def style_prefix(tone: float) -> str: |
| if tone < 0.2: |
| return "" |
| if tone < 0.45: |
| return "Let’s solve it efficiently." |
| if tone < 0.75: |
| return "Let’s work through it." |
| return "You’ve got this — let’s solve it cleanly." |
|
|
|
|
| def _clean_lines(core: str) -> list[str]: |
| lines = [] |
| 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 _is_wrapper_line(line: str) -> bool: |
| key = _normalize_key(line).rstrip(":") |
| wrapper_lines = { |
| "let's work through it.", |
| "lets work through it.", |
| "let's solve it efficiently.", |
| "lets solve it efficiently.", |
| "you've got this — let's solve it cleanly.", |
| "youve got this — lets solve it cleanly.", |
| "you’ve got this — let’s solve it cleanly.", |
| "walkthrough", |
| "hint", |
| "steps", |
| "method", |
| "explanation", |
| "key idea", |
| "question breakdown", |
| "here’s what the question is really asking", |
| "here's what the question is really asking", |
| "let’s break down what the question is asking", |
| "lets break down what the question is asking", |
| "you’ve got this — here’s what the question is really asking", |
| "you've got this — here's what the question is really asking", |
| "what to identify first", |
| "set-up path", |
| "setup path", |
| "first move", |
| "next hint", |
| "watch out for", |
| "variables to define", |
| "equations to form", |
| "key teaching points", |
| "how to build it", |
| } |
| return key in wrapper_lines |
|
|
|
|
| def _strip_leading_wrapper_lines(lines: list[str]) -> list[str]: |
| cleaned = list(lines) |
| while cleaned and _is_wrapper_line(cleaned[0]): |
| cleaned.pop(0) |
| return cleaned |
|
|
|
|
| def _dedupe_lines(lines: list[str]) -> list[str]: |
| seen = set() |
| output = [] |
| 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 _limit_lines_for_verbosity(lines: list[str], verbosity: float, help_mode: str) -> list[str]: |
| if not lines: |
| return lines |
|
|
| if help_mode == "hint": |
| return lines[:1] |
|
|
| if verbosity < 0.2: |
| return lines[:1] |
|
|
| if verbosity < 0.45: |
| return lines[:2] |
|
|
| if verbosity < 0.75: |
| return lines[:3] |
|
|
| return lines |
|
|
|
|
| def _extract_topic_from_lines(lines: list[str]) -> str: |
| joined = " ".join(lines).lower() |
|
|
| if any(word in joined for word in ["equation", "variable", "isolate", "algebra"]): |
| return "algebra" |
| if any(word in joined for word in ["percent", "percentage"]): |
| return "percent" |
| if any(word in joined for word in ["ratio", "proportion"]): |
| return "ratio" |
| if any(word in joined for word in ["probability", "outcome"]): |
| return "probability" |
| if any(word in joined for word in ["mean", "median", "average"]): |
| return "statistics" |
| if any(word in joined for word in ["triangle", "circle", "angle", "area", "perimeter"]): |
| return "geometry" |
| if any(word in joined for word in ["integer", "factor", "multiple", "prime", "remainder"]): |
| return "number_theory" |
|
|
| return "general" |
|
|
|
|
| def _why_line(topic: str, lines: list[str]) -> str: |
| joined = " ".join(lines).lower() |
|
|
| if topic == "algebra": |
| if "equation" in joined or "isolate" in joined or "variable" in joined: |
| return "Why: this works because inverse operations undo what is attached to the variable while keeping the equation balanced." |
| return "Why: the goal is to isolate the variable without changing the balance of the relationship." |
|
|
| if topic == "percent": |
| return "Why: percent relationships depend on identifying the correct base value before calculating the change or part." |
|
|
| if topic == "ratio": |
| return "Why: ratios compare parts consistently, so the relationship must stay proportional." |
|
|
| if topic == "probability": |
| return "Why: probability compares favorable outcomes to the total number of possible outcomes." |
|
|
| if topic == "statistics": |
| return "Why: statistical measures describe the distribution, so the method depends on what feature the question asks for." |
|
|
| if topic == "geometry": |
| return "Why: geometry problems depend on the properties of the figure and the relationships between its parts." |
|
|
| if topic == "number_theory": |
| return "Why: number properties such as divisibility, factors, and remainders follow fixed rules that guide the method." |
|
|
| return "Why: focus on the structure of the problem before doing any calculations." |
|
|
|
|
| def _tone_adjust_line(line: str, tone: float) -> str: |
| line = (line or "").strip() |
| if not line: |
| return line |
|
|
| if tone < 0.25: |
| line = re.sub(r"^let’s\s+", "", line, flags=re.IGNORECASE) |
| line = re.sub(r"^let's\s+", "", line, flags=re.IGNORECASE) |
| line = re.sub(r"^here is the idea in context:\s*", "", line, flags=re.IGNORECASE) |
| return line.strip() |
|
|
| return line |
|
|
|
|
| def format_reply(core: str, tone: float, verbosity: float, transparency: float, help_mode: str) -> str: |
| prefix = style_prefix(tone) |
| core = (core or "").strip() |
|
|
| if not core: |
| return prefix or "Start with the structure of the problem." |
|
|
| lines = _clean_lines(core) |
| lines = _strip_leading_wrapper_lines(lines) |
| lines = [_tone_adjust_line(line, tone) for line in lines] |
| lines = [line for line in lines if line] |
| lines = _dedupe_lines(lines) |
| lines = _limit_lines_for_verbosity(lines, verbosity, help_mode) |
|
|
| if not lines: |
| return prefix or "Start with the structure of the problem." |
|
|
| topic = _extract_topic_from_lines(lines) |
|
|
| output: list[str] = [] |
|
|
| if prefix: |
| output.append(prefix) |
| output.append("") |
|
|
| if help_mode == "hint": |
| output.append("Hint:") |
| output.extend(lines[:1]) |
|
|
| if transparency >= 0.8: |
| output.append("") |
| output.append(_why_line(topic, lines[:1])) |
|
|
| return "\n".join(output).strip() |
|
|
| if help_mode == "walkthrough": |
| output.append("Walkthrough:") |
| output.extend(lines) |
|
|
| if transparency >= 0.8: |
| output.append("") |
| output.append(_why_line(topic, lines)) |
|
|
| return "\n".join(output).strip() |
|
|
| if help_mode in {"step_by_step", "method", "explain", "concept"}: |
| label = { |
| "step_by_step": "Steps:", |
| "method": "Method:", |
| "explain": "Explanation:", |
| "concept": "Key idea:", |
| }.get(help_mode, "Explanation:") |
| output.append(label) |
| output.extend(lines) |
|
|
| if transparency >= 0.75: |
| output.append("") |
| output.append(_why_line(topic, lines)) |
|
|
| return "\n".join(output).strip() |
|
|
| output.extend(lines) |
|
|
| if transparency >= 0.85 and help_mode != "answer": |
| output.append("") |
| output.append(_why_line(topic, lines)) |
|
|
| return "\n".join(output).strip() |
|
|
|
|
| def _explainer_header(tone: float) -> str: |
| if tone < 0.2: |
| return "Question breakdown:" |
| if tone < 0.45: |
| return "Here’s what the question is really asking:" |
| if tone < 0.75: |
| return "Let’s break down what the question is asking:" |
| return "You’ve got this — here’s what the question is really asking:" |
|
|
|
|
| def _clean_section_items(items: List[str]) -> List[str]: |
| cleaned = [] |
| seen = set() |
|
|
| for item in items or []: |
| text = (item or "").strip() |
| if not text: |
| continue |
| if _is_wrapper_line(text): |
| continue |
| key = _normalize_key(text) |
| if key in seen: |
| continue |
| seen.add(key) |
| cleaned.append(text) |
|
|
| return cleaned |
|
|
|
|
| def _append_section(lines: List[str], title: str, items: List[str], limit: int) -> None: |
| cleaned = _clean_section_items(items) |
| if not cleaned: |
| return |
|
|
| lines.append("") |
| lines.append(title) |
| for item in cleaned[:limit]: |
| lines.append(f"- {item}") |
|
|
|
|
| def _coerce_string(value: Any) -> str: |
| return (value or "").strip() if isinstance(value, str) else "" |
|
|
|
|
| def _coerce_list(value: Any) -> List[str]: |
| if not value: |
| return [] |
| if isinstance(value, list): |
| return [str(v).strip() for v in value if str(v).strip()] |
| if isinstance(value, tuple): |
| return [str(v).strip() for v in value if str(v).strip()] |
| if isinstance(value, str): |
| text = value.strip() |
| return [text] if text else [] |
| return [] |
|
|
|
|
| def _get_scaffold(result: Any): |
| return getattr(result, "scaffold", None) |
|
|
|
|
| def _explainer_topic(result: Any) -> str: |
| topic = _coerce_string(getattr(result, "topic", "")) |
| if topic: |
| return topic |
|
|
| joined = " ".join( |
| _coerce_list(getattr(result, "teaching_points", [])) |
| + _coerce_list(getattr(result, "givens", [])) |
| + _coerce_list(getattr(result, "relationships", [])) |
| ) |
| return _extract_topic_from_lines([joined]) if joined else "general" |
|
|
|
|
| def _build_scaffold_sections( |
| result: Any, |
| verbosity: float, |
| transparency: float, |
| ) -> List[str]: |
| output: List[str] = [] |
| scaffold = _get_scaffold(result) |
|
|
| if scaffold is None: |
| return output |
|
|
| ask = _coerce_string(getattr(scaffold, "ask", "")) |
| setup_actions = _coerce_list(getattr(scaffold, "setup_actions", [])) |
| intermediate_steps = _coerce_list(getattr(scaffold, "intermediate_steps", [])) |
| first_move = _coerce_string(getattr(scaffold, "first_move", "")) |
| next_hint = _coerce_string(getattr(scaffold, "next_hint", "")) |
| variables_to_define = _coerce_list(getattr(scaffold, "variables_to_define", [])) |
| equations_to_form = _coerce_list(getattr(scaffold, "equations_to_form", [])) |
| common_traps = _coerce_list(getattr(scaffold, "common_traps", [])) |
|
|
| if ask: |
| output.append("") |
| output.append("What to identify first:") |
| output.append(f"- {ask}") |
|
|
| if setup_actions: |
| output.append("") |
| output.append("Set-up path:") |
| limit = 2 if verbosity < 0.4 else 3 if verbosity < 0.75 else 5 |
| for item in setup_actions[:limit]: |
| output.append(f"- {item}") |
|
|
| if verbosity >= 0.55 and intermediate_steps: |
| output.append("") |
| output.append("How to build it:") |
| limit = 2 if verbosity < 0.75 else 4 |
| for item in intermediate_steps[:limit]: |
| output.append(f"- {item}") |
|
|
| if first_move: |
| output.append("") |
| output.append("First move:") |
| output.append(f"- {first_move}") |
|
|
| if next_hint and (transparency >= 0.35 or verbosity >= 0.45): |
| output.append("") |
| output.append("Next hint:") |
| output.append(f"- {next_hint}") |
|
|
| if verbosity >= 0.65 and variables_to_define: |
| output.append("") |
| output.append("Variables to define:") |
| for item in variables_to_define[:3]: |
| output.append(f"- {item}") |
|
|
| if transparency >= 0.55 and equations_to_form: |
| output.append("") |
| output.append("Equations to form:") |
| for item in equations_to_form[:3]: |
| output.append(f"- {item}") |
|
|
| if transparency >= 0.6 or verbosity >= 0.75: |
| if common_traps: |
| output.append("") |
| output.append("Watch out for:") |
| for item in common_traps[:4]: |
| output.append(f"- {item}") |
|
|
| return output |
|
|
|
|
| def format_explainer_response( |
| result: Any, |
| tone: float, |
| verbosity: float, |
| transparency: float, |
| ) -> str: |
| if not result or not getattr(result, "understood", False): |
| return "I can help explain what the question is asking, but I need the full wording of the question." |
|
|
| output: List[str] = [] |
|
|
| header = _explainer_header(tone) |
| if header: |
| output.append(header) |
| output.append("") |
|
|
| summary = _coerce_string(getattr(result, "summary", "")) |
| teaching_points = _coerce_list(getattr(result, "teaching_points", [])) |
|
|
| if summary and not _is_wrapper_line(summary): |
| output.append(summary) |
|
|
| scaffold_sections = _build_scaffold_sections(result, verbosity, transparency) |
| if scaffold_sections: |
| output.extend(scaffold_sections) |
|
|
| if teaching_points and verbosity >= 0.5: |
| output.append("") |
| output.append("Key teaching points:") |
| limit = 2 if verbosity < 0.75 else 4 |
| for item in teaching_points[:limit]: |
| output.append(f"- {item}") |
|
|
| plain = _coerce_string(getattr(result, "plain_english", "")) |
| if plain and not summary and not _is_wrapper_line(plain): |
| output.append(plain) |
|
|
| asks_for = _coerce_string(getattr(result, "asks_for", "")) |
| if asks_for and transparency >= 0.3: |
| output.append("") |
| output.append(f"The question is asking for: {asks_for}") |
|
|
| if verbosity >= 0.4: |
| _append_section( |
| output, |
| "What the question gives you:", |
| getattr(result, "givens", []) or [], |
| 5 if verbosity >= 0.7 else 3, |
| ) |
|
|
| if verbosity >= 0.65: |
| _append_section( |
| output, |
| "Constraints or conditions:", |
| getattr(result, "constraints", []) or [], |
| 5, |
| ) |
|
|
| if transparency >= 0.45: |
| _append_section( |
| output, |
| "Key relationship:", |
| getattr(result, "relationships", []) or [], |
| 4, |
| ) |
|
|
| if transparency >= 0.5: |
| _append_section( |
| output, |
| "Concepts you will probably need:", |
| getattr(result, "needed_concepts", []) or [], |
| 4, |
| ) |
|
|
| if transparency >= 0.55 or verbosity >= 0.7: |
| _append_section( |
| output, |
| "Watch out for:", |
| getattr(result, "trap_notes", []) or [], |
| 4, |
| ) |
|
|
| strategy_hint = _coerce_string(getattr(result, "strategy_hint", "")) |
| if strategy_hint and verbosity >= 0.35 and not _is_wrapper_line(strategy_hint): |
| output.append("") |
| output.append(f"Best starting move: {strategy_hint}") |
|
|
| if transparency >= 0.8: |
| topic = _explainer_topic(result) |
| why_seed_lines: List[str] = [] |
|
|
| if summary: |
| why_seed_lines.append(summary) |
|
|
| scaffold = _get_scaffold(result) |
| if scaffold is not None: |
| ask = _coerce_string(getattr(scaffold, "ask", "")) |
| first_move = _coerce_string(getattr(scaffold, "first_move", "")) |
| if ask: |
| why_seed_lines.append(ask) |
| if first_move: |
| why_seed_lines.append(first_move) |
|
|
| if not why_seed_lines: |
| why_seed_lines.extend(teaching_points[:2]) |
|
|
| if why_seed_lines: |
| output.append("") |
| output.append(_why_line(topic, why_seed_lines)) |
|
|
| final_lines = [] |
| previous_key = None |
| for line in output: |
| if line is None: |
| continue |
| text = str(line).rstrip() |
| key = _normalize_key(text) |
| if key == previous_key and key: |
| continue |
| final_lines.append(text) |
| previous_key = key |
|
|
| text = "\n".join(final_lines).strip() |
|
|
| if not text: |
| return "I can help explain what the question is asking, but I need the full wording of the question." |
|
|
| return text |